Search code examples
c#.netdata-annotationsminimal-apisasp.net-minimal-apis

Using DataAnnotation Model Validation in Minimal Api


I am porting an application from Asp.net Controllers to Asp.net Minimal-Apis. The current project is using model-based DataAnnotations. Controllers do model validation out of the box but MinApi does not.

For an example such as below, what is the best way to do DataAnnotation model validation in MinimalApi?

Example Data Annotation Model:

using System.ComponentModel.DataAnnotations;

namespace minApi.Models;

public class Account
{
  [Required]
  public int AccountId { get; set; }

  [Required, MaxLength(50)]
  public string AccountName { get; set; };

  [Required, EmailAddress]
  public string AccountEmail { get; set; };

  [Required, Phone]
  public string AccountPhone { get; set; };

  [Required, MaxLength(50)]
  public string StreetAddress { get; set; };

  [Required, MaxLength(50)]
  public string City { get; set; };

  [Required, MaxLength(2)]
  public string StateProvince { get; set; };

  [Required, MaxLength(10)]
  public string PostalCode { get; set; };

  public bool IsActive { get; set; } = true;

  public override string ToString() => $"{AccountName} AccountId: {AccountId}";
}

Example Minimal-Api With Model:

accounts.MapPost("/saveAccount", (IAccountManager _accountManager, [FromBody] Account account) =>
{
    var acct = _accountManager.SaveAccount(account);

    return Results.Ok(acct);
})

Solution

  • Here is what I came up with to validate DataAnnotation models in Minimal-Apis. Please post any other suggestions to improve or better alternatives.

    Add the Helpers and Helpers.Extensions code somewhere in the project.

    Now you can add .Validate(); ,with T being the object to validate, to any Min-Api methods in your project that you need to validate.

    Helper Extension Method:

    using System.ComponentModel.DataAnnotations;
    
    namespace minApi.Helpers.Extensions;
    
    public static class DataValidator
    {
        public static (List<ValidationResult> Results, bool IsValid) DataAnnotationsValidate(this object model)
        {
           var results = new List<ValidationResult>();
           var context = new ValidationContext(model);
    
           var isValid = Validator.TryValidateObject(model, context, results, true);
    
           return (results, isValid);
       }
    }
    

    Validator Helper:

    using Microsoft.AspNetCore.Mvc;
    using minApi.Helpers.Extensions;
    using System.Reflection;
    
    namespace minApi.Helpers;
    
    public static class CustomRouteHandlerBuilder
    {
    
      public static RouteHandlerBuilder Validate<T>(this RouteHandlerBuilder builder, bool firstErrorOnly = true)
      { 
         builder.AddEndpointFilter(async (invocationContext, next) =>
         {
            var argument = invocationContext.Arguments.OfType<T>().FirstOrDefault();
            var response = argument.DataAnnotationsValidate();
    
            if (!response.IsValid)
            {
                string errorMessage =   firstErrorOnly ? 
                                        response.Results.FirstOrDefault().ErrorMessage : 
                                        string.Join("|", response.Results.Select(x => x.ErrorMessage));
    
                return Results.Problem(errorMessage, statusCode: 400);
            }
    
            return await next(invocationContext);
         });
    
         return builder;
      }
    
    }
    

    Validation Code Added to Minimal-Api Method (the last line):

    accounts.MapPost("/saveAccount", (IAccountManager _accountManager, [FromBody] Account account) =>
    {
        var acct = _accountManager.SaveAccount(account);
    
        return Results.Ok(acct);
    })      
    .Validate<Account>();