I am trying to use FluentValidation to validate the property 'Username' if another property 'Found' is true.
Object Contains:
string Username
bool Found
RuleFor(x => x.Username)
.NotEmpty().DependentRules(() => {
RuleFor(y => y.Found).Equals(true); //Not valid syntax
})
.WithMessage("Not Found");
Unfortunately, there seems to be no easy way to do this?
Use the When clause.
RuleFor(x => x.Username).NotEmpty().When(x => x.Found);
Working example
The dependent rules is a bit different; basically the rules specified in the dependent rules block will only be tested if the rule they're attached to passes.
As per the doco
RuleFor(x => x.Surname).NotNull().DependentRules(() => {
RuleFor(x => x.Forename).NotNull();
});
Here the rule against Forename will only be run if the Surname rule passes.
Related
I'm trying to validate a class that has three required properties.
If one or more of them is null it should trigger a single validation message.
Is there a idiomatic way to describe this in fluent validator?
I'm looking at dependant rules but the bottom of the documentation's page advises against using them.
Furthermore I still want to validate all three properties. I just don't want duplicate messages.
I noticed RuleSets, but these seem to serve a different purpose.
Alternatively I could create a validator specifically for these three options but without message and then chain the new validator in the original one. Then I think I can give that one a single message.
But that's a lot of ceremony for a system that is built around being readable.
So looking for a readable way to express validation for three fields with a single message as result.
There are 3 main ways you can do this with FluentValidation: Conditions, dependent rules or a custom rule.
Conditions
You can use 3 separate rule declarations with When conditions to ensure that you only ever get a single validation message.
RuleFor(x => x.Property1).NotNull()
.WithMessage("At least one is required");
RuleFor(x => x.Property2).NotNull()
.When(x => x.Property2 != null)
.WithMessage("At least one is required");
RuleFor(x => x.Property3).NotNull()
.When(x => x.Property1 != null && x.Property2 != null)
.WithMessage("At least one is required");
Dependent Rules
RuleFor(x => x.Property1).NotNull()
.WithMessage("At least one is required")
.DependentRules(() => {
RuleFor(x => x.Property2).NotNull()
.WithMessage("At least one is required")
.DependentRules(() => {
RuleFor(x => x.Property3).NotNull().WithMessage("At least one is required");
});
});
I don't particularly like this approach - I think it's hard to read (hence the warning in the documentation), but if you like this approach it'll work just fine.
Custom logic
RuleFor(x => x)
.Must(x => x.Property1 != null && x.Property2 != null && x.Property3 != null)
.WithMessage("At least one is required");
This approach is slightly different as it creates a model-level rule, so the error message will be associated with the entire model, rather than with a specific property.
Stop the validator when the first rule fails by setting the CascadeMode property:
public class MyClassValidator : AbstractValidator<MyClass>
{
public DestinationDeviceValidator()
{
this.CascadeMode = CascadeMode.Stop;
this.RuleFor(x => x.Property1)
.NotNull();
this.RuleFor(x => x.Property2)
.NotNull();
this.RuleFor(x => x.Property3)
.NotNull();
}
}
I'm trying to manually scaffold part of an existing database schema in EF Core 2.2. The database in question is part of a 3rd party ERP software, and I have zero control over the design of the database itself, so I'm trying to work with what I am given. So far it's going okay, however I am hitting a snag due to a questionable database design choice by the ERP vendor.
Consider the following one-to-many relationship I'm trying to build in my OnModelCreating() method within my DbContext (via Fluent API):
modelBuilder.Entity<SalesOrderHeader>(s => {
s.HasMany<WorkOrderHeader>(e => e.WorkOrders)
.WithOne()
.HasPrincipalKey(e => e.SalesOrderNumber)
.HasForeignKey(e => e.RelatedPO_SONumber);
});
This is almost what I need, however, in the WorkOrders table "RelatedPO_SONumber" is of type string, where "SalesOrderNumber" in SalesOrderHeaders table is of type int. Further, what's stored in "RelatedPO_SONumber" is a 0-padded string (always 8 characters long) of "SalesOrderNumber" (eg SalesOrderHeader.SalesOrderNumber = 1234567, WorkOrder.RelatedPO_SONumber = "01234567").
Here's what I tried and might help illustrate what I'm trying to do via format specifier (throws ArgumentException "The expression should represent a simple property access: 't => t.MyProperty'"):
modelBuilder.Entity<SalesOrderHeader>(s => {
s.HasMany<WorkOrderHeader>(e => e.WorkOrders)
.WithOne()
.HasPrincipalKey(e => e.SalesOrderNumber.ToString("D8"))
.HasForeignKey(e => e.RelatedPO_SONumber);
});
I tried an alternative approach which was to make another property in my SalesOrderHeader entity definition, and using that as the argument for HasPrincipalKey():
// within SalesOrderHeader
[NotMapped]
public string ZeroPaddedSONumber => SalesOrderNumber.ToString("D8");
// within DbContext OnModelCreating()
modelBuilder.Entity<SalesOrderHeader>(s => {
s.HasMany<WorkOrderHeader>(e => e.WorkOrders)
.WithOne()
.HasPrincipalKey(e => e.ZeroPaddedSONumber)
.HasForeignKey(e => e.RelatedPO_SONumber);
});
This resulted in:
"InvalidOperationException: No backing field could be found for property 'ZeroPaddedSONumber' of entity type 'SalesOrderHeader' and the property does not have a setter."
Is there a way to pass anything other than a strict property reference, but rather a transformed "version" of that property?"
Thanks in advance.
EDIT: Following a suggestion in a comment, I tried this:
modelBuilder.Entity<SalesOrderHeader>(s => {
s.Property(e => e.SalesOrderNumber)
.HasConversion(n => n.ToString("D8"), s => int.Parse(s));
s.HasMany<WorkOrderHeader>(e => e.WorkOrders)
.WithOne()
.HasPrincipalKey(e => e.SalesOrderNumber)
.HasForeignKey(e => e.RelatedPO_SONumber);
});
This doesn't seem to work either, I get the following exception (I get the same exception if I omit the conversion):
The types of the properties specified for the foreign key {'RelatedPO_SONumber'} on entity type 'WorkOrderHeader' do not match the types of the properties in the principal key {'SalesOrderNumber'} on entity type 'SalesOrderHeader'.
public class Validator : AbstractValidator<Query>
{
public Validator()
{
CascadeMode = CascadeMode.StopOnFirstFailure;
RuleFor(x => x.A).NotEmpty();
RuleFor(x => x.B).NotEmpty();
RuleFor(x => x).MustAsync(...);
}
}
I'like to construct validator which dones't call MustAsync when above rules are not met. Unfortunately settings CascadeMode to StopOnFirstFailure in validator doesn't do the work.
As stated by the author
That's the correct behaviour - CascadeMode only affects validators
within the same rule chain. Independent calls to RuleFor are separate,
and not dependent on the success or failure of other rules.
See this.
So it would apply for this case
Rulefor(x => x.A)
.NotEmpty()
.Length(10);
=> the Length validation would only be applied if A is not empty.
So you'll have to use a When extension in your MustAsync rule, checking if A and B are not empty (or an if around this rule).
I am new at FluentValidation in general. I am writing a validator, and I can't seem to figure out how to do a .WithMessage with a WarningMessage instead of an ErrorMessage and use params.
I can do this:
RuleFor(x => x.Endorsement)
.Must((coverage, endorsement) => HaveCoveragePerAcreOverMinimum(_coverage, coverage))
.When(x => (!HaveSpecialRequest(_coverage) && !HavePermissionsToOverrideLimits()))
.WithMessage("Some error message {0}", x => x.MyError);
But that sets it as an ErrorMessage and I need a Warning Message. I tried this but no dice:
RuleFor(x => x.Endorsement)
.Must((coverage, endorsement) => HaveCoveragePerAcreOverMinimum(_coverage, coverage))
.When(x => (!HaveSpecialRequest(_coverage) && !HavePermissionsToOverrideLimits()))
.WithMessage(new WarningMessage("Some warning message {0}", x => x.MyError));
There's no direct implementation of Warning message in FluentValidation (or Mvc's ModelState).
In FluentValidation, you've got a
WithState() extension method that you can use for this purpose.
First, you can create an enum
public enum ValidationErrorLevel
{
Error,
Warning
}
Then, you can write a few extension methods, in a static class, to use warnings and errors.
One to use in your Validator classes
public static IRuleBuilderOptions<T, TProperty> AsWarning<T, TProperty>(this IRuleBuilderOptions<T, TProperty> rule)
{
return rule.WithState<T, TProperty>(x => ValidationErrorLevel.Warning);
}
You can use it this way
RuleFor(x => x.Endorsement)
.Must((coverage, endorsement) => HaveCoveragePerAcreOverMinimum(_coverage, coverage))
.When(x => (!HaveSpecialRequest(_coverage) && !HavePermissionsToOverrideLimits()))
.WithMessage("Some error message {0}", x => x.MyError)
.AsWarning();
Few other to use to manage your validation results
public static IList<ValidationFailure> GetWarnings(this ValidationResult result)
{
return result.Errors.Where(m => m.CustomState != null && Convert.ToInt32(m.CustomState) == (int)ValidationErrorLevel.Warning).ToList();
}
public static IList<ValidationFailure> GetErrors(this ValidationResult result)
{
return result.Errors.Except(result.GetWarnings()).ToList();
}
Then, you should use
validator.Validate(<someclass>)
instead of
validator.ValidateAndThrow(<someclass>)
var results = validator.Validate(<someclass>);
You can then put errors in ModelState, for example
foreach (var error in result.GetErrors()) {
ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
}
and do something else for Warnings, for example put it in TempData
TempData["warnings"] = new List<string>();
((List<string>)TempData[WarningMessageKey]).AddRange(result.GetWarnings().Select(warning => warning.ErrorMessage));
Then you can display them like any other TempData.
Validation (in general) attaches errors to the ModelState - which in itself is an object so if you put a break point on the if(ModelState.IsValid) line you can look at the other properties of the ModelState.
The thing with errors (and error messages) they are either there or not. If there is no issue there is no error message and if there is an issue an error message will be added.
If I were you, if the model state is not valid in your controller I would get all error messages using
var allErrors = ModelState.Values.SelectMany(v => v.Errors);
and then iterate over each one and decide what you want to do with it.
I hope this answers or at least helps as I am not totally sure what you are asking / trying to get to.
I have two models: User and UserProfile
Inside the User model, I have defined the following relations:
public function relations()
{
// NOTE: you may need to adjust the relation name and the related
// class name for the relations automatically generated below.
return array(
'userProfile' => array(self::HAS_ONE, 'UserProfile', 'user_id'),
);
}
In UserProfile, I have this relation defined:
public function relations()
{
// NOTE: you may need to adjust the relation name and the related
// class name for the relations automatically generated below.
return array(
'user' => array(self::BELONGS_TO, 'User', 'user_id'),
);
}
Now when I run the following code in my controller:
$user = User::model()->with('userProfile')->findByPK($userId);
$userProfile = $user->userProfile;
print_r($userProfile);
The $userProfile variable is null. I've checked and double-checked the database and code, I've re-read the Yii documentation as well, and seems everything is the way it should be. But it just refuses to work!
Any idea what am I doing wrong?
Generally, you can't have this:
'userProfile' => array(self::HAS_ONE, 'UserProfile', 'user_id'),
and this:
'user' => array(self::BELONGS_TO, 'User', 'user_id'),
both use the user_id key unless both your tables have a user_id key as their primary key. More likely, what you're after is this, like you have:
'userProfile' => array(self::HAS_ONE, 'UserProfile', 'user_id'),
But what that equates to is the SQL statement:
user.id = userProfile.user_id
If that isn't what you want, then you'll need to adjust accordingly. One of the most helpful things for figuring this out is either turning on basic logging of your SQL statements or using the Yii debug toolbar Makes it much easier to see what SQL is being run vs. what you thought would be run.