FluentValidation set validator on collection with Ruleset - asp.net

I'm creating a POST Rest API(ASP.NET Web API) to perform write operations,so have to validate data before inserting them into Database.I'm pretty new to using FluentValidation to validate the data.
Suppose below are classes that I have and need to validate.
public class Listing
{
public Type Type { get; set; }
public Source Source { get; set; }
public List<ListingDetails> ListingDetails { get;set; }
}
public class ListingDetails
{
public int Id{ get; set; }
public ListingStatus Status { get; set; }
}
public enum ListingStatus
{
Active = 1,
Converted = 2,
LostToCompetitor = 3
}
Below code is responsible for validating the status based on the provided ruleset.
public class ListingStatusValidator : AbstractValidator<ListingDetails>
{
public ListingStatusValidator()
{
RuleSet("A", () =>
{
RuleFor(x=>x.InquiryId).GreaterThan(0);
});
RuleSet("B", () =>
{
RuleFor(x => x.Status).IsInEnum().NotEqual(ListingStatus.Active);
});
RuleSet("C", () =>
{
RuleFor(x => x.Status).IsInEnum();
});
}
}
Below is the piece of code used to validatelisting.
public class ListingValidator : AbstractValidator<Listing>
{
public ListingValidator()
{
RuleSet("common", () =>
{
When(x => x.ListingDetails != null && x.ListingDetails.Count <= 1000, () =>
RuleForEach(x => x.ListingDetails).SetValidator(new ListingStatusValidator()));
});
}
}
Now to validate we will call validate method of the validator like below.
var validation = new ListingValidator().Validate(listing,ruleSet:"common");
Is it possible to pass ruleset when validating using setvalidator on collection of objects.Please see below snippet to understand what I'm trying to achieve.
public class ListingValidator : AbstractValidator<Listing>
{
public ListingValidator()
{
When(x => x.ListingDetails != null && x.ListingDetails.Count <= 1000, () =>
RuleForEach(x => x.ListingDetails).SetValidator(new ListingStatusValidator(),ruleset:"A,B,C"));
}
}

You can execute more then one RuleSet using RulesetValidatorSelector
var validation = new ListingValidator()
.Validate(listing, new RulesetValidatorSelector("common", "A", "B", "C"));
In this case you don't need to specify RuleSets for ListingStatusValidator, RuleSets from ListingValidator will be passed to nested validator.

Related

Syncfusion TreeView doesnt show data

I have a list that matches the requirements that I get through the request from js.
Data from the request comes filled in, but the list is not displayed
< ejs-treeview id="treedata" created="created">
< e-treeview-fields dataSource="#Model.Items" id="LevelCode" parentId="ParentLevelCode" text="Name" hasChildren="HasChild"></e-treeview-fields>
< /ejs-treeview>
function created()
{
getCategories();
}
function getCategories() {
let treedata = document.getElementById('treedata').ej2_instances[0];
let request = new ej.base.Ajax(`/Category/GetAll`, 'GET');
request.send();
request.onSuccess = data => {
if (treedata.element !== undefined) {
let final = JSON.parse(data);
treedata.fields.dataSource = final.Categories;
treedata.dataBind();
treedata.refresh();
}
};
}
public class GetAllCategoriesHandlerResponseItem
{
public string Id { get; set; }
public string Name { get; set; }
public bool HasChild { get; set; }
public string LevelCode { get; set; }
public string ParentLevelCode { get; set; }
}
In TreeView component, the fields property has been provided to set or get the data source and other data-related information. You can use this property to dynamically change the TreeView component data source. But you need to specify the properties in its predefined structure to update the TreeView data source.
Check the below code snippet.
function getCategories() {
let treedata = document.getElementById('treedata').ej2_instances[0];
let request = new ej.base.Ajax(`/Category/GetAll`, 'GET');
request.send();
request.onSuccess = data => {
if (treedata.element !== undefined) {
let final = JSON.parse(data);
treedata.fields = {datasource: final.Categories, id:"LevelCode", parentId:"ParentLevelCode", text:"Name", hasChildren:"HasChild" };
treedata.dataBind();
treedata.refresh();
}
};
}
You can refer to the below link to know about the details.
https://www.syncfusion.com/kb/10135/how-to-refresh-the-data-in-ej2-treeview

Mudblazor Select with multiselect and Fluentvalidation For-Expression

I am binding to a select field in multiselect mode and I ran into a problem with the "For" property of the select field".
Here is a code snippet
When using a select field an options type must be set and in this example it will be string. To make validation work the "For"-Property needs to be set and pointing to a valid property of the same type as the select fields option (and thats string).
But I am expecting a multiselect, so I am binding to an IEnumerable<string> in my model and the validation code is also set for this property.
I don`t have the necessary property to bind to and even if I did, the validation would not work as expected.
How do I make this work? I tried building a custom expression which would point to the first element of the array, but I am bad with expressions and couldn`t make it work.
#using FluentValidation
<MudCard>
<MudForm Model="#model" #ref="#form" Validation="#(testValidator.ValidateValue)" ValidationDelay="0">
<MudCardContent>
<MudSelect T="string" Label="Name"
HelperText="Pick your favorite name" MultiSelection="false" #bind-Value="model.Name" For="() => model.Name">
#foreach (var name in _names)
{
<MudSelectItem T="string" Value="#name">#name</MudSelectItem>
}
</MudSelect>
<MudSelect T="string" Label="Names"
HelperText="Pick your favorite names" MultiSelection="true" #bind-SelectedValues="model.Names"
#* For="() => model.Names" This needs to be set to make validation work *#
>
#foreach (var name in _names)
{
<MudSelectItem T="string" Value="#name">#name</MudSelectItem>
}
</MudSelect>
</MudCardContent>
</MudForm>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto" OnClick="#(async () => await Submit())">Order</MudButton>
</MudCardActions>
</MudCard>
#code {
[Inject] ISnackbar Snackbar { get; set; }
private string[] _names = new string[] {
"Toni", "Matthew", "David"
};
MudForm form;
TestModelFluentValidator testValidator = new TestModelFluentValidator();
TestModel model = new TestModel();
public class TestModel
{
public string Name { get; set; }
public IEnumerable<string> Names { get; set; }
}
private async Task Submit()
{
await form.Validate();
if (form.IsValid)
{
Snackbar.Add("Submited!");
}
}
/// <summary>
/// A standard AbstractValidator which contains multiple rules and can be shared with the back end API
/// </summary>
/// <typeparam name="OrderModel"></typeparam>
public class TestModelFluentValidator : AbstractValidator<TestModel>
{
public TestModelFluentValidator()
{
RuleFor(x => x.Name)
.NotEmpty();
RuleFor(x => x.Names).Must((parent, property) => property.Contains("Toni"))
.WithMessage("Toni not found in those names!");
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
var result = await ValidateAsync(ValidationContext<TestModel>.CreateWithOptions((TestModel)model, x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}
}
Edit: Added code sample and trimmed unecessary code.
Mudblazor snippet.
Ok, so you can trick the component by introducing a dummy property and binding the multi-select component to it then testing its name during validation.
When the form component passes the dummy property name to the validation method, you change the passed dummy name to the name of your collection so it's matched when fluent validation kicks in.
Something like this:
#using FluentValidation
#using System.Reflection
<MudCard>
<MudForm Model="#model" #ref="#form" Validation="#(testValidator.ValidateValue)" ValidationDelay="0">
<MudCardContent>
<MudSelect T="string" Label="Name"
HelperText="Pick your favorite name" MultiSelection="false" #bind-Value="model.Name" For="() => model.Name">
#foreach (var name in _names)
{
<MudSelectItem T="string" Value="#name">#name</MudSelectItem>
}
</MudSelect>
<MudSelect T="string" Label="Names"
HelperText="Pick your favorite names" MultiSelection="true" #bind-Value="model.NameCollection" #bind-SelectedValues="model.Names"
For="#(() => model.NameCollection)"
>
#foreach (var name in _names)
{
<MudSelectItem T="string" Value="#name">#name</MudSelectItem>
}
</MudSelect>
</MudCardContent>
</MudForm>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto" OnClick="#(async () => await Submit())">Order</MudButton>
</MudCardActions>
</MudCard>
#code {
[Inject] ISnackbar Snackbar { get; set; }
private string[] _names = new string[] {
"Toni", "Matthew", "David"
};
MudForm form;
TestModelFluentValidator testValidator = new TestModelFluentValidator();
TestModel model = new TestModel();
public class TestModel
{
public string Name { get; set; }
public string NameCollection { get; set; }
public IEnumerable<string> Names { get; set; }
}
private async Task Submit()
{
await form.Validate();
if (form.IsValid)
{
Snackbar.Add("Submited!");
}
}
/// <summary>
/// A standard AbstractValidator which contains multiple rules and can be shared with the back end API
/// </summary>
/// <typeparam name="OrderModel"></typeparam>
public class TestModelFluentValidator : AbstractValidator<TestModel>
{
public TestModelFluentValidator()
{
RuleFor(x => x.Name)
.NotEmpty();
RuleFor(x => x.Names).Must((parent, property) => property.Contains("Toni"))
.WithMessage("Toni not found in those names!");
}
private async Task<bool> IsUniqueAsync(string email)
{
// Simulates a long running http call
await Task.Delay(2000);
return email.ToLower() != "test#test.com";
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
propertyName = propertyName == nameof(TestModel.NameCollection) ? nameof(TestModel.Names) : propertyName;
var result = await ValidateAsync(ValidationContext<TestModel>.CreateWithOptions((TestModel)model, x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}
}

Automapper can't map nested collection

I'm trying to figure out how to do this mapping in automapper 9.0.0. Can anybody help me out?
I have these classes
class User
{
public string Name { get; set; }
public Address[] Addresses { get; set; }
}
class Address
{
public string StreetName { get; set; }
}
class UserDto
{
public string Name { get; set; }
public PropertiesDto Properties { get; set; }
}
class PropertiesDto
{
public AddressDto[] Addresses { get; set; }
}
class AddressDto
{
public string StreetName { get; set; }
}
My goal is to place the array of addresses inside a 'PropertiesDto' object, where there eventually will be a lot of other arrays.
var user = new User { Name = "Foo", Addresses = new[] { new Address { StreetName = "Main St." } } };
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Address[], AddressDto[]>();
cfg.CreateMap<User, UserDto>()
.ForMember(d => d.Properties.Addresses, opt => opt.MapFrom(s => s.Addresses));
});
IMapper mapper = new Mapper(config);
var dtoUser = mapper.Map<UserDto>(user);
Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(dtoUser));
Console.WriteLine("Hit enter...");
Console.ReadLine();
The code fails with this error message.
Unhandled exception. System.ArgumentException: Expression 'd => d.Properties.Addresses' must resolve to top-level member and not any child object's properties. You can use ForPath, a custom resolver on the child type or the AfterMap option instead. (Parameter 'lambdaExpression')
There is a special command for just this situation - if you're mapping to a property below the top level, then just use .ForPath (as the error message suggests) in place of .ForMember. Like this:
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Address[], AddressDto[]>();
cfg.CreateMap<User, UserDto>()
.ForPath(d => d.Properties.Addresses, opt => opt.MapFrom(s => s.Addresses));
});
What worked for me was using reverse mapping.
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Address, AddressDto>();
cfg.CreateMap<UserDto, User>()
.ForMember(d => d.Addresses, opt => opt.MapFrom(s => s.Properties.Addresses))
.ReverseMap();
});

How can I access FluentValidation errors from within an api post method?

In the Fluent Validation docs
There is an example
public class PeopleController : Controller {
public ActionResult Create() {
return View();
}
[HttpPost]
public ActionResult Create(Person person) {
if(! ModelState.IsValid) { // re-render the view when validation failed.
// How do I get the Validator error messages here?
return View("Create", person);
}
TempData["notice"] = "Person successfully created";
return RedirectToAction("Index");
}
}
public class Person {
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public int Age { get; set; }
}
Where the validator has been set up as
public class PersonValidator : AbstractValidator<Person> {
public PersonValidator() {
RuleFor(x => x.Id).NotNull();
RuleFor(x => x.Name).Length(0, 10);
RuleFor(x => x.Email).EmailAddress();
RuleFor(x => x.Age).InclusiveBetween(18, 60);
}
}
Suppose a validation fails. How can I access the Validation Error from within the Create method?
I ask because I am using FluentValidation with an API and need a way for the API to communicate validation errors.
Check ModelState for errors ( True / False )
<% YourModel.ModelState.IsValid %>
Check for a specific Property error
<% YourModel.ModelState["Property"].Errors %>
Check for all errors
<% YourModel.ModelState.Values.Any(x => x.Errors.Count >= 1) %>
There are a ton of good answers to this question on here.
Here is a link to one and here is another

Mapper Dto to Entity in EF using self many to many relationship

I have a project with 4 classes:Direction, Area, Section and Local. Direction have many areas, Area have many sections and section have many locals. Local have positives locals and negatives locals, therefore Local entity will have a self many to many relationship. I'm using Automapper for convert LocalDto to Local, but when i try to update this entity with positives locals and/or negatives locals inserted, the system generate this exception:
The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.
So, they are my mapper classes for my entities:
*******DirectionMapper*******
public static class DirectionMappers
{
public static void SettingMappingDirectionToDirectionDto()
{
Mapper.CreateMap<Direction, DirectionDto>()
.ForMember(directionDto => directionDto.AreasDtosList,
mc => mc.MapFrom(direction => direction.AreasCollection));
}
public static void SettingMappingDirectionDtoToDirection()
{
Mapper.CreateMap<DirectionDto, Direction>()
.ForMember(direction => direction.AreasCollection,
mc => mc.MapFrom(directionDto => directionDto.AreasDtosList));
}
public static void SettingMappingDirectionToString()
{
Mapper.CreateMap<Direction, string>().ConvertUsing(direction => direction.Name ?? string.Empty);
}
}
********AreaMapper**********
public class AreaMappers
{
public static void SettingMappingAreaToAreaDto()
{
Mapper.CreateMap<Area, AreaDto>()
.ForMember(areaDto => areaDto.SectionsDtosList, mc => mc.MapFrom(area => area.SectionsCollection))
.ForMember(areaDto => areaDto.DirectionDto, mc => mc.MapFrom(area => area.Direction));
}
public static void SettingMappingAreaDtoToArea()
{
Mapper.CreateMap<AreaDto, Area>()
.ForMember(area => area.SectionsCollection, mc => mc.MapFrom(areaDto => areaDto.SectionsDtosList))
.ForMember(area => area.Direction,mc=> mc.MapFrom(areaDto=> areaDto.DirectionDto));
}
public static void SettingMappingAreaToString()
{
Mapper.CreateMap<Area, string>().ConvertUsing(area => area.Name ?? string.Empty);
}
}
******SectionMapper*******************
public class SectionMappers
{
public static void SettingMappingSectionToSectionDto()
{
Mapper.CreateMap<Section, SectionDto>()
.ForMember(sectionDto => sectionDto.LocalsDtosList, mc => mc.MapFrom(section => section.LocalsCollection))
.ForMember(sectionDto => sectionDto.AreaDto, mc => mc.MapFrom(section => section.Area));
}
public static void SettingMappingSectionDtoToSection()
{
Mapper.CreateMap<SectionDto, Section>()
.ForMember(section => section.LocalsCollection,
mc => mc.MapFrom(sectionDto => sectionDto.LocalsDtosList))
.ForMember(section => section.Area, mc => mc.MapFrom(sectionDto => sectionDto.AreaDto));
}
public static void SettingMappingSectionToString()
{
Mapper.CreateMap<Section, string>().ConvertUsing(section => section.Name ?? string.Empty);
}
}
******LocalMapper (the main course)******
public static class LocalMappers
{
public static void SettingMappingLocalToLocalDto()
{
Mapper.CreateMap<Local, LocalDto>()
.ForMember(localDto => localDto.PositivesLocalsDtos,
mc => mc.MapFrom(local => local.PositivesLocals)
)
.ForMember(localDto => localDto.NegativesLocalsDtos,
mc => mc.MapFrom(local => local.NegativesLocals)
)
.ForMember(localDto => localDto.SectionDto, mc => mc.MapFrom(local => local.Section));
}
public static void SettingMappingLocalDtoToLocal()
{
Mapper.CreateMap<LocalDto, Local>()
.ForMember(local => local.PositivesLocals,
mc => mc.MapFrom(localDto => localDto.PositivesLocalsDtos)
)
.ForMember(local => local.NegativesLocals,
mc => mc.MapFrom(localDto => localDto.NegativesLocalsDtos)
)
.ForMember(local => local.Section, mc => mc.MapFrom(localDto => localDto.SectionDto));
}
public static void SettingMappingLocalToString()
{
Mapper.CreateMap<Local, string>().ConvertUsing(local => local.Number ?? string.Empty);
}
}
Well, this's a service method for Local update:
public AppOperationResult Update(int id, LocalDto localDto)
{
var appOperationResult = CommunValidations.IsDtoNull(localDto);
if (appOperationResult != null) return appOperationResult;
var tupleValidation = localDto.IsModelDtoValidateForUpdate(id);
var isValidate = tupleValidation.Item1;
if (isValidate)
{
if (TryUpdateLocalFromLocalDto(id, localDto)) return AppOperationResult.Successful();
}
string messageError = tupleValidation.Item2;
return AppOperationResult.WithError(messageError);
}
And these are the methods I did to add positive and negative locals (i call them AdjacentLocals):
public AppOperationResult AddAdjacentLocalsToLocal(AdjacentLocalsToLocalDto adjacentLocalsToLocal)
{
var localDto = adjacentLocalsToLocal.LocalToModify;
var appOperationResult = CommunValidations.IsDtoNull(localDto);
if (appOperationResult != null) return appOperationResult;
var tupleValidation = localDto.IsModelDtoValidate();
var isValidate = tupleValidation.Item1;
if (isValidate)
{
if (TryToAddAdjacentLocalsToLocal(adjacentLocalsToLocal, localDto))
return AppOperationResult.Successful();
}
string messageError = tupleValidation.Item2;
return AppOperationResult.WithError(messageError);
}
private bool TryToAddAdjacentLocalsToLocal(AdjacentLocalsToLocalDto adjacentLocalsToLocal, LocalDto localDto)
{
var positiveLocals = adjacentLocalsToLocal.PositiveLocals;
var negativeLocals = adjacentLocalsToLocal.NegativeLocals;
var positiveslocalsRepeated = positiveLocals.Intersect(localDto.PositivesLocalsDtos);
positiveLocals.RemoveAll(x => positiveslocalsRepeated.Contains(x));
var negativeslocalsRepeated = negativeLocals.Intersect(localDto.NegativesLocalsDtos);
negativeLocals.RemoveAll(x => negativeslocalsRepeated.Contains(x));
localDto.PositivesLocalsDtos = new List<LocalDto>(positiveLocals);
localDto.NegativesLocalsDtos = new List<LocalDto>(negativeLocals);
return TryUpdateLocalFromLocalDto(localDto.Id, localDto);
}
private bool TryUpdateLocalFromLocalDto(int idLocal, LocalDto localDto)
{
var local = _localServices.GetById(idLocal);
local.PositivesLocals.Clear();
local.NegativesLocals.Clear();
_localServices.Update(local);
if (local != null)
{
localDto.Id = idLocal;
var localUpdated = _mappingServices.Map(localDto, local);
_localServices.Update(localUpdated);
return true;
}
return false;
}
********LocalDto*************
public class LocalDto
{
public int Id { get; set; }
public string Number { get; set; }
public float Volumen { get; set; }
public int NumberMaxPeople { get; set; }
public SectionDto SectionDto { get; set; }
public List<LocalDto> PositivesLocalsDtos { get; set; }
public List<LocalDto> NegativesLocalsDtos { get; set; }
}
I'm working using ASP.NET WEB API philosophy,that's why I pass the list of adjacent places with a JSON (correctly), because I think the relationship between the objects in the lists with the database record is lost, but I do not understand why, since these local DTOs they are mapped correctly and return the corresponding local object. However, when I update a local with out a any list of positives or negatives locals, no problem.. so i think that problem is with the self many to many relationship.
I have traced the code several times, I check if all the entities have their relationships and everything seems to be fine, but when I try to update the Local entity inserting adjacents locals(positive and negative local) gives me the error that I mentioned above. So, i . I await your answers.Regards
I think what is happening is the following some entity entity framework that is not linking the existing sections in your database when you use the service of automapper, so I suggest that in your Dto not use the relationships for the other dto, for example:
public class LocalDto
{
public int Id { get; set; }
public string Number { get; set; }
public float Volumen { get; set; }
public int NumberMaxPeople { get; set; }
public SectionDto SectionDto { get; set; }
public List<LocalDto> PositivesLocalsDtos { get; set; }
public List<LocalDto> NegativesLocalsDtos { get; set; }
}
change it to :
public class LocalDto
{
public int Id { get; set; }
public string Number { get; set; }
public float Volumen { get; set; }
public int NumberMaxPeople { get; set; }
public int SectionId { get; set; }
public List<LocalDto> PositivesLocalsDtos { get; set; }
public List<LocalDto> NegativesLocalsDtos { get; set; }
}
and you must also change the mapper associated with these entities,this must be removed from the class LocalMappers,
.ForMember(localDto => localDto.SectionDto, mc => mc.MapFrom(local => local.Section));
This solution is for all your Dtos that have relations, link them to the id of the entity with which it is related not with the Dtos
I hope I've helped

Resources