7

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);
})
2

2 Answers 2

13

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<T>(); (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>(); 
Sign up to request clarification or add additional context in comments.

1 Comment

will not work with AoT due to the refelction, but nice option if not needed with Aot
-1

Thanks WillC for the answer. It's really helpful extension to validate incoming model in MinimalApi
I’m just expanding their answer to work with nested models



public static class ValidationFilter
{
    public static RouteHandlerBuilder Validate<T>(this RouteHandlerBuilder builder)
    {
        builder.AddEndpointFilter(
            async (invocationContext, next) =>
            {
                T argument = invocationContext.Arguments.OfType<T>().FirstOrDefault();
                (List<ValidationResult> Results, bool IsValid) response = argument.DataAnnotationsValidate();

                if (!response.IsValid)
                {
                    Dictionary<string, string[]> errors = response
                        .Results.GroupBy(r => r.MemberNames.FirstOrDefault(string.Empty))
                        .ToDictionary(g => g.Key, g => g.Select(r => r.ErrorMessage ?? string.Empty).ToArray());

                    return Results.ValidationProblem(errors, statusCode: 400);
                }

                return await next(invocationContext);
            }
        );

        return builder;
    }

    private static (List<ValidationResult> Results, bool IsValid) DataAnnotationsValidate(this object model)
    {
        List<ValidationResult> results = [];
        ValidationContext context = new(model);

        bool isValid = Validator.TryValidateObject(model, context, results, true);

        foreach (PropertyInfo property in model.GetType().GetProperties())
        {
            object value = property.GetValue(model);
            if (value != null && property.PropertyType.IsClass && property.PropertyType != typeof(string))
            {
                List<ValidationResult> nestedResults = [];
                ValidationContext nestedContext = new(value);
                bool nestedValid = Validator.TryValidateObject(value, nestedContext, nestedResults, true);

                if (!nestedValid)
                {
                    results.AddRange(nestedResults);
                    isValid = false;
                }
            }
        }

        return (results, isValid);
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.