Validating form when model is a collection of sub models - c#

I've got a view which needs several models to work correctly. So, I created a model which is a collection of multiple (sub) models. This is the model.
public class PolicyDetail
{
public Policy Policy { get; set; }
public IEnumerable<Insured> Insureds { get; set; }
public IEnumerable<Risk> Risks { get; set; }
public IEnumerable<Construction> Constructions { get; set; }
}
And here's an example of what one of the sub models look like, which is an actual entity from the database:
public class Policy
{
[Key]
public int PolicyID { get; set; }
[DisplayName("Policy Number")]
public Guid PolicyNumber { get; set; }
[Required(ErrorMessage = "Please enter a valid Effective Date.")]
[DataType(DataType.DateTime)]
[DisplayName("Effective Date")]
[DisplayFormat(DataFormatString = "{0:MM/dd/yyyy}", ApplyFormatInEditMode = true)]
public DateTime EffDate { get; set; }
[Required(ErrorMessage = "Please enter a valid Expiration Date.")]
[DataType(DataType.DateTime)]
[DisplayName("Expiration Date")]
[DisplayFormat(DataFormatString = "{0:MM/dd/yyyy}", ApplyFormatInEditMode = true)]
public DateTime ExpDate { get; set; }
public Boolean IsActive { get; set; }
}
This was all working well, right up until I tried to submit a form with errors in it to test the validation. I should have seen this coming (maybe?) but because the actual model doesn't have any validation tags on it, it always passes the if (ModelState.IsValid) check. Is there some way to enforce, or inherit, all of the Data Annotations from the sub classes?
Or, am I going about this all wrong, using a model which is a collection of other models? The thing is, I want to be able to edit/add multiple db entities from the same view.
EDIT:
This article by Josh Carroll looks to be EXACTLY what I need. But when I implement it, I get a Null Object error. Here's what I'm doing:
public class PolicyDetail
{
[Required, ValidateObject]
public Policy Policy { get; set; }
public IEnumerable<Insured> Insureds { get; set; }
public IEnumerable<Risk> Risks { get; set; }
public IEnumerable<Construction> Constructions { get; set; }
}
Then in the override method he provides:
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var results = new List<ValidationResult>();
var context = new ValidationContext(value, null, null);
Validator.TryValidateObject(value, context, results, true);
if (results.Count != 0)
{
var compositeResults = new CompositeValidationResult(String.Format("Validation for {0} failed!", validationContext.DisplayName));
results.ForEach(compositeResults.AddResult);
return compositeResults;
}
return ValidationResult.Success;
}
}
the parameter "value" comes in null, so it errors on this line:
Validator.TryValidateObject(value, context, results, true);
Am I missing something? Doing something wrong?

You can manually call the validations on the sub-models using this: https://msdn.microsoft.com/en-us/library/dd411772.aspx
var context = new ValidationContext(model.Policy, serviceProvider: null, items: null);
var validationResults = new List<ValidationResult>();
bool isValid = Validator.TryValidateObject(model.Policy, context, validationResults, true);
You can then use the ModelState.AddModelError to build the response from that.
Definitely not the most elegant possible solution, but might be easier than rewriting what you have.

Related

Blazor trigger custom validation message

I have the following class which is being used as an input model for an EditForm in a Blazor server side application.
public class KundeInput
{
[ValidateComplexType]
public List<AnsprechpartnerInput> Ansprechpartner { get; } = new List<AnsprechpartnerInput>();
public string? Kundennummer { get; }
[Required]
[MaxLength(60)]
public string Firma { get; set; } = String.Empty;
[MaxLength(60)]
public string? Name2 { get; set; }
[MaxLength(60)]
public string? Name3 { get; set; }
}
As you can see, my model contains a list of another model called AnsprechpartnerInput. Here is this model:
public class AnsprechpartnerInput
{
public string? Kundennummer { get; set; }
public int Nummer { get; } = -1;
[MaxLength(60)]
[Required]
public string Vorname { get; set; } = String.Empty;
[MaxLength(60)]
[Required]
public string Nachname { get; set; } = String.Empty;
[MaxLength(40)]
[Required]
public string? Bereich { get; set; }
/ * More properties */
}
The validation works fine. However, once I have multiple invalid AnsprechpartnerInput models in my list, the ValidationSummary becomes a mess. Because it displays e.g. 5 times field xyz is invalid.
I know I can set a custom message with the ErrorMessage property but I am not able to use other attributes from my model in this message.
What I want to achive is this:
[Required(ErrorMessage = $"Vorname of {Kundennummer} is required")]
public string Vorname { get; set; } = String.Empty;
I already tried to change the message with reflection but accoridng to Microsoft this way is not recommend or supported
https://github.com/dotnet/aspnetcore/issues/25611
Is there any way to get it to work? I thought of string replacement but I am not sure how I can figure out the right model for my ValidationMessage.
Also is there any way to validate the items of the list by one and get a boolean result? Let's say I want to achive this:
#foreach (var ansprechpartner in Input.Ansprechpartner)
{
if (Input.SelectedAnsprechpartner is null)
Input.SelectedAnsprechpartner = ansprechpartner;
<a #onclick="() => Input.SelectedAnsprechpartner = ansprechpartner"
class="#GetNavListClass(Input.SelectedAnsprechpartner == ansprechpartner)"
id="list-ansprechpartner-tab-#(ansprechpartner.Nummer)"
data-toggle="list"
href="#list-ansprechpartner-#(ansprechpartner.Nummer)"
role="tab"
aria-controls="#(ansprechpartner.Nummer)">
#((MarkupString)(ansprechpartner.Nummer < 0 ? "<span class=\"font-weight-bold\">NEU</span>" : $"({ansprechpartner.Nummer})")) #ansprechpartner.Vorname #ansprechpartner.Nachname
</a>
// When the model ansprechpartner is invalid, I want to display an icon
}
Thanks for any help!
PS: Blazor rocks!
You should use a custom validation attribute where you can explicitly add any error message you want
public class KundennummerValidationAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var model = (AnsprechpartnerInput)validationContext.ObjectInstance;
if(string.IsNullOrEmpty((string)value))
{
return new ValidationResult($"Vorname of {model.Kundennummer} is required", new[] { "Kundennummer" });
}
return ValidationResult.Success;
}
}
then use
[KundennummerValidation]
public string Vorname { get; set; } = String.Empty;
result :
Validation summary:

How to check validation of each object separately?

This my class Obj and Action. But ModelState.isValid checks the whole array obj, but I need a separate object in each cycle to check whether it pass validation.
public class Obj
{
[HiddenInput(DisplayValue = false)]
public string Id { get; set; }
[Required(ErrorMessage = "The field is required")]
public string Name { get; set; }
[Required(ErrorMessage = "The field is required.")]
[Range(1000, 2019, ErrorMessage = "Year of publication must be between 1000 and 2017.")]
[Display(Name = "Year of publication")]
public int Year { get; set; }
[DataType(DataType.MultilineText)]
public string Desc { get; set; }
}
public ActionResult Create(Obj[] obj)
{
foreach (var b in obj)
{
if (ModalState.isValid)
{
//...
}
}
return View();
}
To validate manually with the data annotations you can create your own ValidationContext for the object in question and then run TryValidateObject for each of the objects.
var validationResults = new List<ValidationResult>();
foreach (var b in obj)
{
var context = new ValidationContext(b);
var isValid = Validator.TryValidateObject(b,context,validationResults);
'do whatever.....
}
Try to use these methods: TryValidateModel or ValidateModel

C# DataAnnotations validation does not work for list

I have following object
public class TestClass
{
[Required]
public int TestId { get; set; }
}
I validate using:
List<ValidationResult> results = new List<ValidationResult>();
var vc = new ValidationContext(data);
if (Validator.TryValidateObject(data, vc, results, true))
return;
This validates perfectly fine if data is of type TestClass but not when I pass list of TestClass items (List<TestClass>)
How can I validate the items withing a list without iterating?
TryValidateObject expects an object and not a list of. You have to write a helper class. Furthermore it even doesn't recursively check the Validation. See this SO question for more...
Usually I add the Validate method in the class that I need to validate, and I foreach the property that has a list of T.
For example:
public class ClassToValidate : IValidatableObject
{
// Property with a non-custom type
[Required(AllowEmptyStrings = false, ErrorMessage = "\"Language\" is required. It can not be empty or whitespace")]
public string Language { get; set; }
// Property with a custom type
public Source Source { get; set; }
// Property with a custom type list
public List<Streaming> StreamingList { get; set; } = new List<Streaming>();
// Validate method that you need to implement because of "IValidatableObject"
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// The validation result that will be returned
List<ValidationResult> result = new List<ValidationResult>();
// Language doesn't need to be added in the Validate method because it isn't a custom object
// Custom single T object to be validated
Validator.TryValidateObject(Source, new ValidationContext(Source), result, true);
// Custom list<T> object to be validated
foreach (var streaming in StreamingList)
{
Validator.TryValidateObject(streaming, new ValidationContext(streaming), result, true);
}
return result;
}
}
public class Source
{
[Range(16, 192, ErrorMessage = "\"Bitrate\" must be between 16 and 192")]
public int? Bitrate { get; set; }
public string Codec { get; set; }
}
public class Streaming
{
[Required(AllowEmptyStrings = false, ErrorMessage = "\"Url\" can not be empty")]
public string Url { get; set; }
[Range(0, 1000, ErrorMessage = "\"Offset\" must be between 0 and 1000")]
public int Offset { get; set; }
}

TryValidateObject not returning results on Range

I'm trying to leverage the DataAnnotations.Validator outside of MVC. I have two services that validate their respective models. Both models inherit from a Base class I wrote that had a ValidateModel() method.
public class BaseValidatableDomainModel : IValidatableDomainModel
{
public BaseValidatableDomainModel()
{
ModelState = new ModelStateDictionary();
}
public ModelStateDictionary ModelState { get; set; }
public virtual void ValidateModel()
{
var validationContext = new ValidationContext(this, serviceProvider: null, items: null);
var results = new List<ValidationResult>();
Validator.TryValidateObject(this, validationContext, results);
foreach (var thisInvalidResult in results)
{
ModelState.AddModelError(thisInvalidResult.MemberNames.FirstOrDefault(),thisInvalidResult.ErrorMessage);
}
}
}
I have a test for each service that verifies the service behaves correctly when the data is invalid. The one model correctly errors for PhoneNumber == null:
[Required]
public string PhoneNumber { get; set; }
However, the other model does not error when CompanyId is 0. CompanyId is defined as this:
public class CompanyAddressDomainModel : BaseValidatableDomainModel
{
// Other fields
[Required]
[Range(1, int.MaxValue, ErrorMessage = "Company is required")]
public int CompanyId { get; set; }
[Required]
public AddressInputDomainModel Address { get; set; }
}
The code calls validation like this:
CompanyAddressDomainModel companyAddress = // set values
companyAddress.ValidateModel();
if (!companyAddress.ModelState.IsValid)
{
return companyAddress;
}
Why would it catch some validation, and not others? As far as I can tell, the two services and models are defined the same. If more information is needed, please let me know.
If I test with CompanyId == 0 and Address == null I do see the address error, but not the CompanyId error.
Looks like I have to tell TryValidateObject to validate all.
public static bool TryValidateObject(
object instance,
ValidationContext validationContext,
ICollection<ValidationResult> validationResults,
bool validateAllProperties
)
Like this:
Validator.TryValidateObject(this, validationContext, results, true);

Fluent Validation Inconsistent with ASP.NET MVC 5

I'm using Fluent Validation v5.5 with ASP.NET v5.2.2 and I'm getting some inconsistent results with the validation.
My view model is:
public class QuoteViewModel
{
[Display(Name = #"Day")]
public int DateOfBirthDay { get; set; }
[Display(Name = #"Month")]
public int DateOfBirthMonth { get; set; }
[Display(Name = #"Year")]
public int DateOfBirthYear { get; set; }
[Display(Name = #"Gender")]
public Gender? Gender { get; set; }
[Display(Name = #"State")]
public int StateId { get; set; }
}
My controller method is:
public ActionResult Quote(QuoteViewModel viewModel)
{
var _validator = new QuoteValidator();
var results = _validator.Validate(viewModel);
if (!ModelState.IsValid)
{
return Json(false);
}
return Json(true);
}
My validator is:
public class QuoteValidator : AbstractValidator<QuoteViewModel>
{
public QuoteValidator()
{
RuleFor(x => x.Gender).NotEmpty();
RuleFor(x => x.StateId).NotEmpty();
RuleFor(x => x.DateOfBirthDay).NotEmpty().InclusiveBetween(1, 31);
RuleFor(x => x.DateOfBirthMonth).NotEmpty().InclusiveBetween(1, 12);
RuleFor(x => x.DateOfBirthYear).NotEmpty().LessThanOrEqualTo(DateTime.UtcNow.Year);
}
}
I'm running a test that posts all blank value form fields. Thus the view model fields retain default values after the view model object is created.
For comparison, in the controller I'm running the validation explicitly and the results aren't consistent with the validation result in ModelState.
ModelState is showing 4 errors, all triggered by NotEmpty rules. NotEmpty on the nullable enum Gender doesn't seem to trigger.
The explicit validation is returning 7 out of 8 errors, the LessThanOrEqualTo rule won't fire since the DateOfBirthYear defaults to zero.
My pain point is I can't figure out why ModelState is missing the NotEmpty error on the nullable enum Gender.
The only way I've been able to trigger that error is to post just the Gender value.
Please help.
EDIT:
After stepping through some code, it appears that the issue is related to the Fluent Validation RequiredFluentValidationPropertyValidator. The Gender field is a nullable value type which is bound to null. The following snippet from RequiredFluentValidationPropertyValidator prevents validation:
ShouldValidate = isNonNullableValueType && nullWasSpecified;
!ModelState.IsValid doesn't use your validation result it uses defaulf MVC validation (that can be added through DataAnnotations). You have to check !results.IsValid instead which contains the validation result of your QuoteValidator.
If you want to use default ModelState.IsValid you have to mark your model with validator attribute:
[Validator(typeof(QuoteValidator))]
public class QuoteViewModel
{
[Display(Name = #"Day")]
public int DateOfBirthDay { get; set; }
[Display(Name = #"Month")]
public int DateOfBirthMonth { get; set; }
[Display(Name = #"Year")]
public int DateOfBirthYear { get; set; }
[Display(Name = #"Gender")]
public Gender? Gender { get; set; }
[Display(Name = #"State")]
public int StateId { get; set; }
}
And add the following line to your Application_Start method:
protected void Application_Start() {
FluentValidationModelValidatorProvider.Configure();
}

Categories

Resources