You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I needed a custom validator that performs an "at least one required" validation. My use case was an email sending form: at least one of "To", "Cc" and "Bcc" must be supplied.
I repurposed my other custom validator for this, so it may not be the most elegant (I used the same approach for handling multiple properties) but it works on both server and client sides.
Posting my solution here in case it helps someone else (and for my own future reference! 😆).
AtLeastOneRequiredAttribute.cs
[AttributeUsage(AttributeTargets.Property,AllowMultiple=false)]publicsealedclassAtLeastOneRequiredAttribute:ValidationAttribute,IClientModelValidator{// based on// https://github.com/haacked/aspnet-client-validation/blob/062c8433f6c696bc41cd5d6811c840905c63bc9c/README.MD#adding-custom-validation// https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-7.0#custom-client-side-validation// https://github.com/dotnet/runtime/blob/dff486f2d78d3f932d0f9bfa38043f85e358fb8c/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/CompareAttribute.cs// https://github.com/dotnet/aspnetcore/blob/d0ca5a8d20ac50a33d5451e998a5d411a810c8d7/src/Mvc/Mvc.DataAnnotations/src/CompareAttributeAdapter.csprivateconststring_errorMessage=@"At least one of '{0}', '{1}' and '{2}' must be populated";publicoverrideboolRequiresValidationContext=>true;publicstringPropertyName1{get;}publicstringPropertyName2{get;}publicstringPropertyName3{get;}publicAtLeastOneRequiredAttribute(stringpropertyName1,stringpropertyName2,stringpropertyName3):base(_errorMessage){ArgumentNullException.ThrowIfNullOrWhiteSpace(propertyName1,nameof(propertyName1));ArgumentNullException.ThrowIfNullOrWhiteSpace(propertyName2,nameof(propertyName2));ArgumentNullException.ThrowIfNullOrWhiteSpace(propertyName3,nameof(propertyName3));PropertyName1=propertyName1;PropertyName2=propertyName2;PropertyName3=propertyName3;}publicoverridestringFormatErrorMessage(stringname)=>string.Format(CultureInfo.CurrentCulture,ErrorMessageString,PropertyName1,PropertyName2,PropertyName3);protectedoverrideValidationResult?IsValid(object?value,ValidationContextvalidationContext){ArgumentNullException.ThrowIfNull(validationContext,nameof(validationContext));varproperty1Info=validationContext.ObjectType.GetRuntimeProperty(PropertyName1);varproperty2Info=validationContext.ObjectType.GetRuntimeProperty(PropertyName2);varproperty3Info=validationContext.ObjectType.GetRuntimeProperty(PropertyName3);if(property1Info==null)returnnewValidationResult(string.Format("Could not find a property named {0}.",PropertyName1));if(property2Info==null)returnnewValidationResult(string.Format("Could not find a property named {0}.",PropertyName2));if(property3Info==null)returnnewValidationResult(string.Format("Could not find a property named {0}.",PropertyName3));if(property1Info.GetIndexParameters().Length>0)thrownewArgumentException(string.Format("The property {0}.{1} could not be found.",validationContext.ObjectType.FullName,PropertyName1));if(property2Info.GetIndexParameters().Length>0)thrownewArgumentException(string.Format("The property {0}.{1} could not be found.",validationContext.ObjectType.FullName,PropertyName2));if(property3Info.GetIndexParameters().Length>0)thrownewArgumentException(string.Format("The property {0}.{1} could not be found.",validationContext.ObjectType.FullName,PropertyName3));varproperty1Value=property1Info.GetValue(validationContext.ObjectInstance,null);varproperty2Value=property2Info.GetValue(validationContext.ObjectInstance,null);varproperty3Value=property3Info.GetValue(validationContext.ObjectInstance,null);if(property1Value!=null||property2Value!=null||property3Value!=null){returnValidationResult.Success;}else{varerrorMessage=FormatErrorMessage(validationContext.DisplayName);varmemberNames=new[]{PropertyName1,PropertyName2,PropertyName3};returnnewValidationResult(errorMessage,memberNames);}}publicvoidAddValidation(ClientModelValidationContextcontext){ArgumentNullException.ThrowIfNull(context,nameof(context));MergeAttribute(context.Attributes,"data-val","true");MergeAttribute(context.Attributes,"data-val-atleastonerequired",FormatErrorMessage(context.ModelMetadata.Name!));// null forgiving: false positive as must be non-null for propertyMergeAttribute(context.Attributes,"data-val-atleastonerequired-propertyname1",$"*.{PropertyName1}");MergeAttribute(context.Attributes,"data-val-atleastonerequired-propertyname2",$"*.{PropertyName2}");MergeAttribute(context.Attributes,"data-val-atleastonerequired-propertyname3",$"*.{PropertyName3}");// allows other property to be nested or not, e.g. `FooModel.User.Name` and `FooModel.UserName`, respectively; https://github.com/dotnet/aspnetcore/blob/d0ca5a8d20ac50a33d5451e998a5d411a810c8d7/src/Mvc/Mvc.DataAnnotations/src/CompareAttributeAdapter.cs#L19}privatestaticboolMergeAttribute(IDictionary<string,string>attributes,stringkey,stringvalue){if(attributes.ContainsKey(key))returnfalse;attributes.Add(key,value);returntrue;}}
PS: if you enable the repo's wiki or discussions tab, I'd have posted there. We should have a place where we can collect our custom validators. It would also make the library more appealing to new users.
@haacked, @dahlbyk
I needed a custom validator that performs an "at least one required" validation. My use case was an email sending form: at least one of "To", "Cc" and "Bcc" must be supplied.
I repurposed my other custom validator for this, so it may not be the most elegant (I used the same approach for handling multiple properties) but it works on both server and client sides.
Posting my solution here in case it helps someone else (and for my own future reference! 😆).
AtLeastOneRequiredAttribute.cs
Add to js init scripts:
Example:
Notes:
Please close this issue once you've seen it.
The text was updated successfully, but these errors were encountered: