Out-Of-Process Azure Functions with Fluent Validation

2023, Aug 02

Creating Azure Functions has never been easier, thanks to the Out-Of-Process (Isolated) model, which allows us to effortlessly hook services to the middleware. And when it comes to model validation, Fluent Validation comes to the rescue, providing an elegant and efficient way to validate our models.

In this post I'm providing a new implementation of Fluent Validation in Azure Functions with .NET 7, using the Out-Of-Process model. This is a follow-up of my previous article, where I explained how to use Fluent Validation in Azure Functions In-Process with .NET 6.

Creating the project

On Visual Studio 2022 create a new Functions project. Make sure to select the .NET 7 runtime and the Isolated model.

VStudio

This is the solution was structured with:

  • Apis: Contains the Azure Functions
  • Extensions: Contains the Fluent Validation extension
  • Models: Contains the models and their validators
  • Requests: Contains the http requests

Solution

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

On Program.cs the Service Collection extension for FluentValidation is hooked up to Service Collection. This is possible because of the NuGet package FluentValidation.AspNetCore.

Program.cs

using FluentValidationFunction.Extensions;
using Microsoft.Extensions.Hosting;

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureServices((ctx, svc) =>
    {
        svc.AddFluentValidation();
    })
    .Build();

host.Run();

ServiceCollectionExtensions.cs

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddFluentValidation(this IServiceCollection services)
    {
        services.AddFluentValidationAutoValidation()
            .AddValidatorsFromAssembly(typeof(ServiceCollectionExtensions).Assembly);

        return services;
    }
}

Models

The models created are ProductViewModel.cs and its validator ProductViewModelValidator.cs, which 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;
    }

    [Function(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<HttpResponseData> AddProductAsync([HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
    {
        _logger.LogInformation($"{nameof(AddProductAsync)} has been triggered");

        // Deserialize object
        var productViewModel = await req.ReadFromJsonAsync<ProductViewModel>();

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

        if (!productValidationResult.IsValid)
        {
            var responseWithErrors = req.CreateResponse();
            await responseWithErrors.WriteAsJsonAsync(productValidationResult.Errors.Select(e => new
            {
                e.ErrorCode,
                e.PropertyName,
                e.ErrorMessage
            }));
            responseWithErrors.StatusCode = HttpStatusCode.BadRequest;
            return responseWithErrors;
        }

        // TODO: Perform add product

        var response = req.CreateResponse(HttpStatusCode.OK);
        return response;
    }
}

NOTE: On the isolated model, there were some changes on how you guarantee the return of the correct HTTP status code. Please check my previous article if you want to know more about that.

Running the solution

Run the solution and open Swagger to perform the POST:

http://localhost:7275/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:7275/api/AddProductAsync" -H  "accept: application/json " -H  "Content-Type: application/json" -d "{  \"name\": \"Marmite\",  \"sku\": \"MARMITE\",  \"quantity\": 2,  \"price\": 0}"

Alternatively, you can run the Requests\add-product.http file from Visual Studio 2022.

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.

Check the PlayGoKids repository for this example.