Using DataAnnotation in ASP.NET Core to add angular 4 validation attributes

What is the problem? The default validation attributes, added by ASP.NET Core, are not useful for angular.

For Example the validation attribute for an min-length value in ASP.NET Core is data-val-minlength-min. In angular 4 the value is minlength.

For a more precise distinction, here is an example:

Default ASP.NET Core attributes for an minlength of 5.

<input type="password" data-val-minlength="The field Description must be a string with a minimum length of '5'."
   data-val-minlength-min="5"
   id="Password" name="Password" />

angular attribute for an minlength of 5.

<input type="password" minlength="5" id="Password" name="Password" />
<div *ngIf="Password.invalid && (Password.dirty || Password.touched)" 
   class="alert alert-danger">
  <div *ngIf="Password.errors.minlength">
    The field Description must be a string with a minimum length of '5'.
  </div>
</div>

But don’t worry. Because ASP.NET Core is Open Source and uses Dependency Injection. We can try to change the behavior. In the source code we can see that all DataAnnotaion attributes are handeld in the Class ValidationAttributeAdapterProvider .This class implements the Interface IValidationAttributeAdapterProvider. With this Information we try to implement our own DataAnnotaion handling. First of all we create our version of the ValidationAttributeAdapterProvider. We will call it AngularValidationAttributeAdapterProvider.

public class AngularValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
{
   /// <summary>
   /// Creates an <see cref="IAttributeAdapter"/> for the given attribute.
   /// <summary>
   /// <param name="attribute">The attribute to create an adapter for.</param>
   /// <param name="stringLocalizer">The localizer to provide to the adapter.</param>
   /// <returns>An <see cref="IAttributeAdapter"> for the given attribute.</returns>
   public IAttributeAdapter GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer stringLocalizer)
   {
   }
}

Now we have our structure to add our AttributAdapter for the angular validation. For our own MinLengthAngularAttributeAdapter we are using the MinLengthAttributeAdapter as an template.

The only thing we need to do, is to change the following lines of code from

MergeAttribute(context.Attributes, "data-val", "true");
MergeAttribute(context.Attributes, "data-val-minlength", GetErrorMessage(context));
MergeAttribute(context.Attributes, "data-val-minlength-min", _min);

to

MergeAttribute(context.Attributes, "name", context.ModelMetadata.PropertyName);
MergeAttribute(context.Attributes, $"#{context.ModelMetadata.PropertyName}", string.Empty);
MergeAttribute(context.Attributes, "minlength", this.min);

The final Class looks like

    public class MinLengthAngularAttributeAdapter : AttributeAdapterBase<MinLengthAttribute>
    {
        private readonly string min;

        /// <summary>
        /// Initializes a new instance of the <see cref="MinLengthAngularAttributeAdapter"/> class.
        /// </summary>
        /// <param name="attribute"></param>
        /// <param name="stringLocalizer"></param>
        public MinLengthAngularAttributeAdapter(MinLengthAttribute attribute, IStringLocalizer stringLocalizer)
            : base(attribute, stringLocalizer)
        {
            this.min = this.Attribute.Length.ToString(CultureInfo.InvariantCulture);
        }

        public override void AddValidation(ClientModelValidationContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            MergeAttribute(context.Attributes, "name", context.ModelMetadata.PropertyName);
            MergeAttribute(context.Attributes, $"#{context.ModelMetadata.PropertyName}", string.Empty);
            MergeAttribute(context.Attributes, "minlength", this.min);
        }

        /// <inheritdoc />
        public override string GetErrorMessage(ModelValidationContextBase validationContext)
        {
            if (validationContext == null)
            {
                throw new ArgumentNullException(nameof(validationContext));
            }

            return this.GetErrorMessage(
                validationContext.ModelMetadata,
                validationContext.ModelMetadata.GetDisplayName(),
                this.Attribute.Length);
        }
    }

The next step is to add our MinLengthAngularAttributeAdapter to the AngularValidationAttributeAdapterProvider

public class AngularValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
{
   /// <summary>
   /// Creates an <see cref="IAttributeAdapter"/> for the given attribute.
   /// <summary>
   /// <param name="attribute">The attribute to create an adapter for.</param>
   /// <param name="stringLocalizer">The localizer to provide to the adapter.</param>
   /// <returns>An <see cref="IAttributeAdapter"> for the given attribute.</returns>
   public IAttributeAdapter GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer stringLocalizer)
   {
       if (attribute == null)
       {
           throw new ArgumentNullException(nameof(attribute));
       }

       IAttributeAdapter adapter;

       var type = attribute.GetType();

       if (type == typeof(MinLengthAttribute))
       {
            adapter = new MinLengthAngularAttributeAdapter((MinLengthAttribute)attribute, stringLocalizer);
       }

       return adapter;
   }
}

The last step is to register our own class in the IoC from ASP.NET Core. To do this, we have to add the following line of Code, before we call the AddMvc method.

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    // Add your own Provider bevore AddMvc is called. Other the Mvc Provider is used.
    services.AddSingleton<IValidationAttributeAdapterProvider, AngularValidationAttributeAdapterProvider>();
    services.AddMvc();
}

That is it. Now we have our own angluar validation. We only need to add the missing AttributeAdapter. For Example: requierd, maxlength, etc.