C# DataAnnotations validation does not work for list - c#

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; }
}

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

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);

How to reuse model validation outside of a controller?

The specific functionality of MVC model validation that I want to leverage is validating data BEFORE it has been assigned to properties of an object instance.
For example if I have the class:
public class Test
{
[Required(ErrorMessage="Id is required")]
public int Id { get; set; }
[Required(ErrorMessage="Name is required")]
[RegularExpression(Constants.SomeRegex, ErrorMessage = "Please enter a valid value for Name")]
public int Name { get; set; }
}
I want to be able to validate that the value assigned to 'Id' can at least be assigned before trying to create an instance. In this case that would mean being assignable to an integer - so the value "ABC" would fail validation.
Of course I can't create an instance of Test with the value "ABC" for Id, it's not assignable to Int32.
MVC controllers implement this functionality - errors will be reported back before an instance of the model class can be created.
To this end, I have so far attempted using System.CompondentModel.DataAnnotations.Validator
public bool IsValid(IDictionary<object, object> data, out OfferConfig offerConfig)
{
offerConfig = new OfferConfig();
var context = new ValidationContext(offerConfig, data);
var results = new List<ValidationResult>();
return Validator.TryValidateObject(offerConfig, context, results, true);
}
And passing in an instance implementing IDictionary
var dict = new Dictionary<object, object>
{
{"Id", dataTable.Rows[i][0].ToString()},
{"Name", dataTable.Rows[i][1].ToString()}
}
Like so:
Test testInstance;
bool isValid = IsValid(dict, out testInstance);
But maybe the Validator doesn't work as I'm expecting. Is the data argument supposed to be string object representations of the model properties? The validation results out appear as if values simply have not been assigned rather than being incorrect.
Hopefully someone can see what I'm trying to achieve here...
Simply create new validation attribute in which you will place our validation logic. Like this:
public class StringIdLengthRangeAttribute : ValidationAttribute
{
public int Minimum { get; set; }
public int Maximum { get; set; }
public StringLengthRangeAttribute()
{
this.Minimum = 0;
this.Maximum = int.MaxValue;
}
public override bool IsValid(object value)
{
string strValue = value as string;
if (!string.IsNullOrEmpty(strValue))
{
int len = strValue.Length;
return len >= this.Minimum && len <= this.Maximum;
}
return true;
}
}
This is example validation of length - replace it with your validation logic.
And your class:
public class Test
{
[Required]
[StringIdLengthRange(Minimum = 10, Maximum = 20)]
public string Id { get; set; }
}
With such attribute you are able to use that logic on any other fields.

ASP.NET MVC: Custom Validation by DataAnnotation

I have a Model with 4 properties which are of type string. I know you can validate the length of a single property by using the StringLength annotation. However I want to validate the length of the 4 properties combined.
What is the MVC way to do this with data annotation?
I'm asking this because I'm new to MVC and want to do it the correct way before making my own solution.
You could write a custom validation attribute:
public class CombinedMinLengthAttribute: ValidationAttribute
{
public CombinedMinLengthAttribute(int minLength, params string[] propertyNames)
{
this.PropertyNames = propertyNames;
this.MinLength = minLength;
}
public string[] PropertyNames { get; private set; }
public int MinLength { get; private set; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var properties = this.PropertyNames.Select(validationContext.ObjectType.GetProperty);
var values = properties.Select(p => p.GetValue(validationContext.ObjectInstance, null)).OfType<string>();
var totalLength = values.Sum(x => x.Length) + Convert.ToString(value).Length;
if (totalLength < this.MinLength)
{
return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
}
return null;
}
}
and then you might have a view model and decorate one of its properties with it:
public class MyViewModel
{
[CombinedMinLength(20, "Bar", "Baz", ErrorMessage = "The combined minimum length of the Foo, Bar and Baz properties should be longer than 20")]
public string Foo { get; set; }
public string Bar { get; set; }
public string Baz { get; set; }
}
Self validated model
Your model should implement an interface IValidatableObject. Put your validation code in Validate method:
public class MyModel : IValidatableObject
{
public string Title { get; set; }
public string Description { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Title == null)
yield return new ValidationResult("*", new [] { nameof(Title) });
if (Description == null)
yield return new ValidationResult("*", new [] { nameof(Description) });
}
}
Please notice: this is a server-side validation. It doesn't work on client-side. You validation will be performed only after form submission.
ExpressiveAnnotations gives you such a possibility:
[Required]
[AssertThat("Length(FieldA) + Length(FieldB) + Length(FieldC) + Length(FieldD) > 50")]
public string FieldA { get; set; }
To improve Darin's answer, it can be bit shorter:
public class UniqueFileName : ValidationAttribute
{
private readonly NewsService _newsService = new NewsService();
public override bool IsValid(object value)
{
if (value == null) { return false; }
var file = (HttpPostedFile) value;
return _newsService.IsFileNameUnique(file.FileName);
}
}
Model:
[UniqueFileName(ErrorMessage = "This file name is not unique.")]
Do note that an error message is required, otherwise the error will be empty.
Background:
Model validations are required for ensuring that the received data we receive is valid and correct so that we can do the further processing with this data. We can validate a model in an action method. The built-in validation attributes are Compare, Range, RegularExpression, Required, StringLength. However we may have scenarios wherein we required validation attributes other than the built-in ones.
Custom Validation Attributes
public class EmployeeModel
{
[Required]
[UniqueEmailAddress]
public string EmailAddress {get;set;}
public string FirstName {get;set;}
public string LastName {get;set;}
public int OrganizationId {get;set;}
}
To create a custom validation attribute, you will have to derive this class from ValidationAttribute.
public class UniqueEmailAddress : ValidationAttribute
{
private IEmployeeRepository _employeeRepository;
[Inject]
public IEmployeeRepository EmployeeRepository
{
get { return _employeeRepository; }
set
{
_employeeRepository = value;
}
}
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
var model = (EmployeeModel)validationContext.ObjectInstance;
if(model.Field1 == null){
return new ValidationResult("Field1 is null");
}
if(model.Field2 == null){
return new ValidationResult("Field2 is null");
}
if(model.Field3 == null){
return new ValidationResult("Field3 is null");
}
return ValidationResult.Success;
}
}
Hope this helps. Cheers !
References
Code Project - Custom Validation Attribute in ASP.NET MVC3
Haacked - ASP.NET MVC 2 Custom Validation
A bit late to answer, but for who is searching.
You can easily do this by using an extra property with the data annotation:
public string foo { get; set; }
public string bar { get; set; }
[MinLength(20, ErrorMessage = "too short")]
public string foobar
{
get
{
return foo + bar;
}
}
That's all that is too it really. If you really want to display in a specific place the validation error as well, you can add this in your view:
#Html.ValidationMessage("foobar", "your combined text is too short")
doing this in the view can come in handy if you want to do localization.
Hope this helps!

Categories

Resources