Forked from nickalbrecht/CustomValidationAttributeAdapterProvider.cs
Created
June 25, 2020 17:29
-
-
Save danwize/300d66ef0d76a49c17dd1c745bc40e4b to your computer and use it in GitHub Desktop.
Attribute to mark properties backed by primitive types or structs (int, DateTime, Guid, etc) as requiring a value other than their default value. `RequireNonDefaultAttribute` alone is enough for Server side validation. If you want to use this for client side as well, you'll need the other files too.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System.ComponentModel.DataAnnotations; | |
| using Microsoft.AspNetCore.Mvc.DataAnnotations; | |
| using Microsoft.Extensions.Localization; | |
| public class CustomValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider | |
| { | |
| readonly IValidationAttributeAdapterProvider baseProvider = new ValidationAttributeAdapterProvider(); | |
| public IAttributeAdapter GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer stringLocalizer) | |
| { | |
| if (attribute is RequireNonDefaultAttribute) | |
| return new RequireNonDefaultAttributeAdapter((RequireNonDefaultAttribute) attribute, stringLocalizer); | |
| else | |
| { | |
| return baseProvider.GetAttributeAdapter(attribute, stringLocalizer); | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using System.Collections.Concurrent; | |
| using System.ComponentModel.DataAnnotations; | |
| /// <summary> | |
| /// Override of <see cref="ValidationAttribute.IsValid(object)"/> | |
| /// </summary> | |
| /// <remarks>Is meant for use with primitive types, structs (like DateTime, Guid), or enums. Specifically ignores null values (considers them valid) so that this can be combined with RequiredAttribute.</remarks> | |
| /// <example> | |
| /// //Allows you to effectively mark the field as required with out having to resort to Guid? and then having to deal with SomeId.GetValueOrDefault() everywhere (and then test for Guid.Empty) | |
| /// [RequireNonDefault] | |
| /// public Guid SomeId { get; set;} | |
| /// | |
| /// //Enforces validation that requires the field cannot be 0 | |
| /// [RequireNonDefault] | |
| /// public int SomeId { get; set; } | |
| /// | |
| /// //The nullable int lets the field be optional, but if it IS provided, it can't be 0 | |
| /// [RequireNonDefault] | |
| /// public int? Age { get; set;} | |
| /// | |
| /// //Forces a value other than the default Enum, so `Unspecified` is not allowd | |
| /// [RequireNonDefault] | |
| /// public Fruit Favourite { get; set; } | |
| /// public enum Fruit { Unspecified, Apple, Banana } | |
| /// </example> | |
| [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] | |
| public sealed class RequireNonDefaultAttribute : ValidationAttribute | |
| { | |
| private static ConcurrentDictionary<string, object> defaultInstancesCache = new ConcurrentDictionary<string, object>(); | |
| public RequireNonDefaultAttribute() | |
| : base("The {0} field requires a non-default value.") | |
| { | |
| } | |
| /// <param name="value">The value to test</param> | |
| /// <returns><c>false</c> if the <paramref name="value"/> is equal the default value of an instance of its own type.</returns> | |
| public override bool IsValid(object value) | |
| { | |
| if (value is null) | |
| return true; //Only meant to test default values. Use `System.ComponentModel.DataAnnotations.RequiredAttribute` to consider NULL invalid | |
| var type = value.GetType(); | |
| if (!defaultInstancesCache.TryGetValue(type.FullName, out var defaultInstance)) | |
| { | |
| //Helps to avoid repeat overhead of reflection for any given type (FullName includes full namespace, so something like System.Int32, System.Decimal, System.Guid, etc) | |
| defaultInstance = Activator.CreateInstance(Nullable.GetUnderlyingType(type) ?? type); | |
| defaultInstancesCache[type.FullName] = defaultInstance; | |
| } | |
| return !Equals(value, defaultInstance); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System.ComponentModel.DataAnnotations; | |
| using Microsoft.AspNetCore.Mvc.DataAnnotations; | |
| using Microsoft.Extensions.Localization; | |
| public class RequireNonDefaultAttributeAdapter : AttributeAdapterBase<RequireNonDefaultAttribute> | |
| { | |
| public RequireNonDefaultAttributeAdapter(RequireNonDefaultAttribute attribute, IStringLocalizer stringLocalizer) | |
| : base(attribute, stringLocalizer) | |
| { | |
| } | |
| public override string GetErrorMessage(ModelValidationContextBase validationContext) | |
| { | |
| if (validationContext == null) | |
| { | |
| throw new ArgumentNullException(nameof(validationContext)); | |
| } | |
| return GetErrorMessage(validationContext.ModelMetadata, validationContext.ModelMetadata.GetDisplayName()); | |
| } | |
| public override void AddValidation(ClientModelValidationContext context) | |
| { | |
| if (context == null) | |
| { | |
| throw new ArgumentNullException(nameof(context)); | |
| } | |
| MergeAttribute(context.Attributes, "data-val", "true"); | |
| MergeAttribute(context.Attributes, "data-val-notequals", GetErrorMessage(context)); | |
| MergeAttribute(context.Attributes, "data-val-notequals-val", Activator.CreateInstance(Nullable.GetUnderlyingType(context.ModelMetadata.ModelType) ?? context.ModelMetadata.ModelType).ToString()); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //Code for wiring up unobtrusive client side validation. | |
| (function ($) { | |
| jQuery.validator.addMethod("notequals", function (value, element, param) { | |
| return value != param; | |
| }); | |
| jQuery.validator.unobtrusive.adapters.addSingleVal("notequals", "val"); | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public class Startup | |
| { | |
| //Rest of the file excluded for brevity. This is your Startup class when dealing with an ASP.NET Core application | |
| public void ConfigureServices(IServiceCollection services) | |
| { | |
| services.AddSingleton<Microsoft.AspNetCore.Mvc.DataAnnotations.IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>(); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment