Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save danwize/300d66ef0d76a49c17dd1c745bc40e4b to your computer and use it in GitHub Desktop.
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.
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);
}
}
}
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);
}
}
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());
}
}
//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");
});
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