Out-Of-Process Azure Functions with Fluent Validation
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.
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
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.
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:
The model is validated and the error message is displayed, as expected.
Check the PlayGoKids repository for this example.