FluentValidation how to check for Length if string is not null? - c#

I'm testing a PUT with two string:
company.CurrencyCode = request.CurrencyCode ?? company.CurrencyCode;
company.CountryIso2 = request.Country ?? company.CountryIso2;
and I tried with a rule like:
public UpdateCompanyValidator()
{
RuleSet(ApplyTo.Put, () =>
{
RuleFor(r => r.CountryIso2)
.Length(2)
.When(x => !x.Equals(null));
RuleFor(r => r.CurrencyCode)
.Length(3)
.When(x => !x.Equals(null));
});
}
as I don't mind to get a null on those properties, but I would like to test the Length when the property is not a null.
What is the best way to apply rules when a property is nullable and we just want to test if it's not null?

One of the ways would be:
public class ModelValidation : AbstractValidator<Model>
{
public ModelValidation()
{
RuleFor(x => x.Country).Must(x => x == null || x.Length >= 2);
}
}

I prefer the following syntax:
When(m => m.CountryIso2 != null,
() => {
RuleFor(m => m.CountryIso2)
.Length(2);
);

Best syntax for me:
RuleFor(t => t.DocumentName)
.NotEmpty()
.WithMessage("message")
.DependentRules(() =>
{
RuleFor(d2 => d2.DocumentName).MaximumLength(200)
.WithMessage(string.Format(stringLocalizer[""message""], 200));
});

Related

Fluent Validation seems to stop after the first failure

I've been updating my fluent Validations from 10 to 11 but I've suddenly got a lot of unit test errors that push me toward believing that Fluent Validations is not executing all of the rules. This seems against all the documentation and I'm not finding anyone else complaining about it so I'm confused.
I have a test that looks for several null values, and should return a failure for each, however it only returns the first failure, swapping the rule order makes the error return differently.
Any tips are appreciated.
public sealed class Validator : AbstractValidator<Command>
{
public Validator()
{
//ClassLevelCascadeMode = CascadeMode.Continue;
RuleFor(x => x.Id).ValidateId();
RuleFor(x => x.Name).ValidateAccountName();
RuleFor(x => x.Description).ValidateAccountDescription();
RuleFor(x => x.Status).ValidateAccountStatus();
When(
x => !(x.prop1is null) || !(x.prop2 is null) || !(x.prop 3 is null),
() =>
{
RuleFor(x => x.prop1Enum).NotNull().IsInEnum();
RuleFor(x => x.prop2Number).ValidateNumber();
RuleFor(x => x.prop3Number).ValidateNumber();
}
);
}
}
Test
public void Validator_ShouldReturnErrors_WhenAccountInfoIsPartiallyProvided(
bool excludeProp1,
bool excludeProp2,
bool excludeProp3,
UpdateAccount.Validator sut,
IFixture fixture
)
{
// arrange
var command = BuildValidCommand(fixture, excludeProp1, excludeProp2, excludeprop3)
.Create();
// act
var result = sut.Validate(command);
// assert
using var _ = new AssertionScope();
result.Errors.Should().HaveCountLessOrEqualTo(3);
if (excludeprop1)
{
result.Errors.Should()
.Contain(
f => f.PropertyName == nameof(UpdateAccount.Command.prop1) &&
f.ErrorMessage.Contains("must not be empty")
);
}
if (excludeProp2)
{
result.Errors.Should()
.Contain(
f => f.PropertyName == nameof(UpdateAccount.Command.Prop2) &&
f.ErrorMessage.Contains("must not be empty")
);
}
if (excludeProp3)
{
result.Errors.Should()
.Contain(
f => f.PropertyName == nameof(UpdateAccount.Command.Prop3) &&
f.ErrorMessage.Contains("must not be empty")
);
}
}
I figured out that the data generators for my tests was putting the validators in this bad state. I have fixed them all by putting them into the proper state.

How to return an object with a collection instead of just the collection from a Web API?

At the moment, in my controller's service method GetSubAccounts(accountId), I have this:
Account account = await context.Accounts.SingleOrDefaultAsync(x => x.Id == accountId);
IQueryable<Account> subAccounts = context.Accounts.Include(x => x.AccountCodes).AsNoTracking();
return await mapper.ProjectTo<SubAccountViewModel>(subAccounts, null, x => x.SubAccounts)
.Where(x => x.PersonId == account.PersonId && x.AccountId != null).ToListAsync();
My SubAccountViewModel is as follows: (note that it has a collection of itself)
public class SubAccountViewModel : Base.AccountViewModel
{
public virtual ICollection<AccountCodeViewModel> AccountCodes { get; set; }
public virtual ICollection<SubAccountViewModel> SubAccounts { get; set; }
}
My mapping profile is:
internal class SubAccountMappingProfile : Profile
{
public SubAccountMappingProfile()
{
CreateMap<Account, SubAccountViewModel>()
.ForMember(x => x.AccountCodes, options => options.ExplicitExpansion())
.ForMember(x => x.SubAccounts, options => options.ExplicitExpansion())
.ReverseMap();
}
}
And this is the JSON I'm getting as a result:
[
{
"id":"c236718f-9d91-4eea-91ee-66760a716343",
"personId":"06d3857d-6a49-4e1c-b63c-7edc83d30cbd",
"accountId":null,
"username":"test same person",
"email":"testsameperson#gmail.com",
"birthDate":"2021-01-02",
"subaccounts":null
}
]
The problem:
I'm getting a top-level array of subaccounts for the accountId parameter I pass to the method. Fine. (There's just one, but nevermind that.)
What I do want is the main account at top-level, with the array of subaccounts as part of it.
I.e.
{
"id":"f61fedc2-eb60-4ba9-9d17-8d41b9cae9f1",
"personId":"06d3857d-6a49-4e1c-b63c-7edc83d30cbd",
"accountId":"f61fedc2-eb60-4ba9-9d17-8d41b9cae9f1",
"username":"test person",
"email":"testperson#gmail.com",
"birthDate":"2021-01-01",
"subaccounts":[
{
"id":"c236718f-9d91-4eea-91ee-66760a716343",
"personId":"06d3857d-6a49-4e1c-b63c-7edc83d30cbd",
"accountId":"f61fedc2-eb60-4ba9-9d17-8d41b9cae9f1",
"username":"test same person",
"email":"testsameperson#gmail.com",
"birthDate":"2021-01-02",
"subaccounts":null
}
]
}
How do I do it?
The problem was one of logic.
To start with, my service method (and my API controller) was returning Task<IEnumerable<SubAccountViewModel>>, when it should return Task<SubAccountViewModel>.
Then my solution was:
Account account = await context.Accounts.SingleOrDefaultAsync(x => x.Id == accountId);
IQueryable<Account> accounts = context.Accounts.AsNoTracking();
SubAccountViewModel subAccountViewModel = await mapper.ProjectTo<SubAccountViewModel>(accounts, null, x => x.AccountCodes)
.SingleOrDefaultAsync(x => x.Id == accountId);
subAccountViewModel.SubAccounts = await mapper.ProjectTo<SubAccountViewModel>(accounts, null, x => x.AccountCodes, x => x.SubAccounts)
.Where(x => x.PersonId == account.PersonId && x.AccountId != null).ToListAsync();
return subAccountViewModel;
This returns the resultset I wanted.

AutoMapper core Condition not working

I want the null value during mapping DTO to DBO model to be ignored. This is the code:
DTO / DBO models have both property named items:
public virtual ICollection<price_list_item> items { get; set; }
DBO constructor:
public price_list()
{
this.items = new List<price_list_item>();
}
DTO constructor has no propert initialization
public price_list()
{
}
AutoMapper Profile:
this.CreateMap<DTO.price_list, DBO.price_list>()
.ForMember(m => m.id, src => src.Ignore())
.ForMember(m => m.currency_id, src => src.MapFrom(f => f.currency))
.ForMember(dest => dest.items, opt => opt.Condition(src => (src.items != null)))
API Controller:
[HttpPut]
[Route("{id:long}")]
public async Task<DTO.price_list> UpdateOneAsync(long id, [FromBody]DTO.price_list payload)
{
if (payload == null)
{
throw new ArgumentNullException("payload");
}
Console.WriteLine(payload.items == null);
var _entity = await this.IDataRepository.price_lists
.Where(w => w.id == id)
.Include(i => i.items)
.FirstOrDefaultAsync();
if (_entity == null)
{
NotFound();
return null;
}
Console.WriteLine(_entity.items.Count);
// map fields to existing model
this.IMapper.Map<DTO.price_list, DBO.price_list>(payload, _entity);
Console.WriteLine(_entity.items.Count);
When I send to API a JSON without any sign of 'items' property, Console output is:
True
1200 // price list in dbo has 1200 items
0 // here I need to have still 1200 items
What am I doing wrong? Why the condition is not respected and items property is not 'skiped' ?
Thanks
Lucian thanks, PreCondition solved the problem. This is working code:
this.CreateMap<DTO.price_list, DBO.price_list>()
.ForMember(m => m.id, src => src.Ignore())
.ForMember(m => m.currency_id, src => src.MapFrom(f => f.currency))
.ForMember(dest => dest.items, opt => opt.PreCondition(src => (src.items != null)))

Automapper: "Missing type map configuration or unsupported mapping"

AssertConfigurationIsValid Passes, and the object being tried is fully populated, but I get the error on the first Map request called.
I'm trying to map
Survey ToLoad = Mapper.Map<Survey>(U);
I'm initializing automapper with the code below.
//Lots of other Maps
Mapper.Initialize(cfg => cfg.CreateMap<User, SMUser>()
.ForMember(t => t.AccountType, s => s.MapFrom(so => so.AccountType != null ? so.AccountType : String.Empty))
.ForMember(t => t.Username, s => s.MapFrom(so => so.Username != null ? so.Username : String.Empty)));
Mapper.Initialize(cfg => cfg.CreateMap<SurveyMonkey.Containers.Survey, Survey>().ForMember(t => t.AnalyzeUrl, s => s.MapFrom(so => so.AnalyzeUrl != null ? so.AnalyzeUrl : String.Empty))
.ForMember(t => t.Category, s => s.MapFrom(so => so.Category != null ? so.Category : String.Empty))
.ForMember(t => t.CollectUrl, s => s.MapFrom(so => so.CollectUrl != null ? so.CollectUrl : String.Empty))
.ForMember(t => t.EditUrl, s => s.MapFrom(so => so.EditUrl != null ? so.EditUrl : String.Empty))
.ForMember(t => t.Language, s => s.MapFrom(so => so.Language != null ? so.Language : String.Empty))
.ForMember(t => t.Preview, s => s.MapFrom(so => so.Preview != null ? so.Preview : String.Empty))
.ForMember(t => t.SummaryUrl, s => s.MapFrom(so => so.SummaryUrl != null ? so.SummaryUrl : String.Empty))
.ForMember(t => t.Title, s => s.MapFrom(so => so.Title != null ? so.Title : String.Empty))
//Some more members
);
//LISTS
Mapper.Initialize(cfg => cfg.CreateMap<List<SurveyMonkey.Containers.Collector>, List<Collector>>());
//Lots of other List Maps
I'm using the latest Stable version from Nuget (5.2.0).
Only call Mapper.Initialize once with the whole configuration, or you will overwrite it.
You can wrap the configuration in a class that inherits AutoMapper.Profile:
using AutoMapper;
public class MyAutoMapperProfile : Profile {
protected override void Configure() {
CreateMap<User, SMUser>();
CreateMap<SurveyMonkey.Containers.Survey, Survey>();
CreateMap<List<SurveyMonkey.Containers.Collector>, List<Collector>>();
}
}
Then initialize the Mapper using this Profile:
Mapper.Initialize(cfg => {
cfg.AddProfile<MyAutoMapperProfile>();
cfg.AddProfile<OtherAutoMapperProfile>();
});
AutoMapper Configuration
I got same error and in my startup class in ConfigureServices methode I use
services.AddAutoMapper(typeof(startup)); => startup class.
Because of that my automapper profile class (the class that inherit from Automapper.Propfle, in this case
public class MyAutoMapperProfile : Profile) not getting read.
To fix this I have replace startup class with the MyAutoMapperProfile like below
services.AddAutoMapper(typeof(MyAutoMapperProfile));.
you can debug and check whether your automapper mapping class getting hit or not.

NHibernate extension for querying non mapped property

I'm looking for a way to get total price count from the Costs list in my object. I can't get Projections.Sum to work in my QueryOver so I tried another way but I'm having problems with it. I want to use a unmapped property in my QueryOver. I found this example but it's giving an error.
Object:
public class Participant
{
public int Id { get; set; }
public double TotalPersonalCosts { get { return Costs.Where(x => x.Code.Equals("Persoonlijk") && x.CostApprovalStatus == CostApprovalStatus.AdministratorApproved).Sum(x => x.Price.Amount); } }
public IList<Cost> Costs { get; set; }
}
The property TotalPersonalCosts is not mapped and contains the total price count.
Extension Class:
public static class ParticipantExtensions
{
private static string BuildPropertyName(string alias, string property)
{
if (!string.IsNullOrEmpty(alias))
{
return string.Format("{0}.{1}", alias, property);
}
return property;
}
public static IProjection ProcessTotalPersonalCosts(System.Linq.Expressions.Expression expr)
{
Expression<Func<Participant, double>> w = r => r.TotalPersonalCosts;
string aliasName = ExpressionProcessor.FindMemberExpression(expr);
string totalPersonalCostName = ExpressionProcessor.FindMemberExpression(w.Body);
PropertyProjection totalPersonalCostProjection =
Projections.Property(BuildPropertyName(aliasName, totalPersonalCostName));
return totalPersonalCostProjection;
}
}
My QueryOver:
public override PagedList<AccountantViewInfo> Execute()
{
ExpressionProcessor.RegisterCustomProjection(
() => default(Participant).TotalPersonalCosts,
expr => ParticipantExtensions.ProcessTotalPersonalCosts(expr.Expression));
AccountantViewInfo infoLine = null;
Trip tr = null;
Participant pa = null;
Cost c = null;
Price p = null;
var infoLines = Session.QueryOver(() => tr)
.JoinAlias(() => tr.Participants, () => pa);
if (_status == 0)
infoLines.Where(() => pa.TotalCostApprovalStatus == TotalCostApprovalStatus.CostPrinted || pa.TotalCostApprovalStatus == TotalCostApprovalStatus.CostPaid);
else if (_status == 1)
infoLines.Where(() => pa.TotalCostApprovalStatus == TotalCostApprovalStatus.CostPrinted);
else
infoLines.Where(() => pa.TotalCostApprovalStatus == TotalCostApprovalStatus.CostPaid);
infoLines.WhereRestrictionOn(() => pa.Employee.Id).IsIn(_employeeIds)
.Select(
Projections.Property("pa.Id").WithAlias(() => infoLine.Id),
Projections.Property("pa.Employee").WithAlias(() => infoLine.Employee),
Projections.Property("pa.ProjectCode").WithAlias(() => infoLine.ProjectCode),
Projections.Property("tr.Id").WithAlias(() => infoLine.TripId),
Projections.Property("tr.Destination").WithAlias(() => infoLine.Destination),
Projections.Property("tr.Period").WithAlias(() => infoLine.Period),
Projections.Property("pa.TotalPersonalCosts").WithAlias(() => infoLine.Period)
);
infoLines.TransformUsing(Transformers.AliasToBean<AccountantViewInfo>());
var count = infoLines.List<AccountantViewInfo>().Count();
var items = infoLines.List<AccountantViewInfo>().ToList().Skip((_myPage - 1) * _itemsPerPage).Take(_itemsPerPage).Distinct();
return new PagedList<AccountantViewInfo>
{
Items = items.ToList(),
Page = _myPage,
ResultsPerPage = _itemsPerPage,
TotalResults = count,
};
}
Here the .Expression property is not found from expr.
I don't know what I'm doing wrong. Any help or alternatives would be much appreciated!
Solution with Projection.Sum() thx to xanatos
.Select(
Projections.Group(() => pa.Id).WithAlias(() => infoLine.Id),
Projections.Group(() => pa.Employee).WithAlias(() => infoLine.Employee),
Projections.Group(() => pa.ProjectCode).WithAlias(() => infoLine.ProjectCode),
Projections.Group(() => tr.Id).WithAlias(() => infoLine.TripId),
Projections.Group(() => tr.Destination).WithAlias(() => infoLine.Destination),
Projections.Group(() => tr.Period).WithAlias(() => infoLine.Period),
Projections.Sum(() => c.Price.Amount).WithAlias(() => infoLine.TotalPersonalCost)
);
You can't use unmapped columns as projection columns of a NHibernate query.
And the way you are trying to do it is conceptually wrong: the ParticipantExtensions methods will be called BEFORE executing the query to the server, and their purpose is to modify the SQL query that will be executed. An IProjection (the "thing" that is returned by ProcessTotalPersonaCosts) is a something that will be put between the SELECT and the FROM in the query. The TotalCosts can't be returned by the SQL server because the SQL doesn't know about TotalCosts

Categories