Azure Functions with Fluent Validation

2022, Jul 12

Apis can be easily implemented with the power of Azure Functions, and when you need to validate models, Fluent Validation comes in handy, helping us to validate models elegantly. Fluent Validation is a validation library available as a NuGet package, that can be easily implemented by developers. It uses a fluent API that leverages lambda expressions to validate rules.

Check my previous article if you want more details about the Fluent Validation in .NET 6. Otherwise, stick to this article as I'm going straight to coding.

Creating the project

On Visual Studio 2022 create a new Functions project. Make sure you get the settings right.

VStudio

If you are like me and like Dependency Injection, check the PlayGoKids repository for this example.

You are going to notice that I have deleted the out-of-the-box Functions1.cs, added Startup.cs and created other classes for Products. The solution looks like this:

Solution

The NuGet packages FluentValidation and FluentValidation.AspNetCore are assigned to the project.

On Startup.cs the FluentValidation library is hooked up to Service Collection. This is possible because of the NuGet package FluentValidation.AspNetCore.

Startup.cs

[assembly: FunctionsStartup(typeof(Startup))]
namespace FluentValidationFunction
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddFluentValidation(conf =>
            {
                conf.RegisterValidatorsFromAssembly(typeof(Startup).Assembly);
            });
        }
    }
}

Models

The models created are ProductViewModel.cs and its validator ProductViewModelValidator.cs, that contains the Fluent Validation.

ProductViewModel.cs

public class ProductViewModel
{
    public string Name { get; set; }
    public string Sku { get; set; }
    public int Quantity { get; set; }
    public double Price { get; set; }
}

ProductViewModelValidator.cs

public class ProductViewModelValidator : AbstractValidator<ProductViewModel>
{
    public ProductViewModelValidator()
    {
        RuleFor(model => model.Name).NotNull().NotEmpty().WithMessage("Please specify a name");
        RuleFor(model => model.Sku).NotNull().NotEmpty().Length(3, 10);
        RuleFor(model => model.Quantity).GreaterThanOrEqualTo(0);
        RuleFor(model => model.Price).NotEqual(0).When(model => model.Quantity > 0)
            .WithMessage("Please specify a price");
    }
}

The rules are simple to understand, for more details please check my previous article.

The Open Api

The solution has a POST endpoint (with OpenApi attributes) that allows users to submit a Product. The Fluent Validation is executed on the request, and in case it fails, it returns a BadRequest.

ProductApi.cs

public class ProductApi
{
    private readonly ILogger<ProductApi> _logger;
    private readonly IValidator<ProductViewModel> _validator;

    public ProductApi(ILogger<ProductApi> log, IValidator<ProductViewModel> validator)
    {
        _logger = log;
        _validator = validator;
    }

    [FunctionName(nameof(AddProductAsync))]
    [OpenApiOperation(operationId: nameof(AddProductAsync), tags: new[] {"name"})]
    [OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)]
    [OpenApiRequestBody(contentType: "application/json", bodyType: typeof(ProductViewModel), Description = nameof(ProductViewModel), Required = true)]
    [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json ", bodyType: typeof(string), Description = "The OK response")]
    public async Task<IActionResult> AddProductAsync(
        [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]
        HttpRequest req)
    {
        _logger.LogInformation($"{nameof(AddProductAsync)} has been triggered");

        // Deserialize object
        var productJson = await req.ReadAsStringAsync();
        var productViewModel = JsonConvert.DeserializeObject<ProductViewModel>(productJson);

        // Validating
        var productValidationResult = await _validator.ValidateAsync(productViewModel);

        if (!productValidationResult.IsValid)
        {
            return new BadRequestObjectResult(productValidationResult.Errors.Select(e => new
            {
                e.ErrorCode, e.PropertyName, e.ErrorMessage
            }));
        }

        // TODO: Perform add product

        return new OkResult();
    }
}

Running the solution

Run the solution and open Swagger to perform the POST:

http://localhost:7270/api/swagger/ui

That was the port number set for this function.

OpenApi

When submitting a payload that fails the rules, the validation is displayed.

curl -X POST "http://localhost:7270/api/AddProductAsync" -H  "accept: application/json " -H  "Content-Type: application/json" -d "{  \"name\": \"Marmite\",  \"sku\": \"MARMITE\",  \"quantity\": 2,  \"price\": 0}"

Because Price was not provided when Quantity is greater than zero, the error message is displayed:

Validation

The model is validated and the error message is displayed, as expected.