SHARE:

gRPC & ASP.NET Core 3.1: Message validation

Introduction

Validation is a very important feature in applications but in ASP.NET Core 3.1 and gRPC it’s a bit complicated…. Proto3 doesn’t provide any validation with its syntax and Microsoft gaves us the habit to use DataAnnotation in ASP.NET and ASP.NET Core, unfortunaltey this feature is not available neither in gRPC. So a solution is required to fix that limitation, in this article I will show you how to use packages I have designed : Calzolari.Grpc.AspNetCore.Validation and Calzolari.Grpc.Net.Client.Validation. The full documentation is available here: https://github.com/AnthonyGiretti/grpc-aspnetcore-validator (forked and improved with more features from https://github.com/enif-lee/grpc-dotnet-validator).

Validation server side

First we’ll need to download the server side package with the following command:

Install-Package Calzolari.Grpc.AspNetCore.Validation -Version 3.1.2

The package is based on FluentValidation, if you want to learn more about it here is the link of their great documentation: https://docs.fluentvalidation.net/en/latest/

Now let’s consider the following service:

using AutoMapper;
using DemoGrpc.Domain.Entities;
using DemoGrpc.Protobufs;
using DempGrpc.Services.Interfaces;
using Grpc.Core;
using System.Threading.Tasks;
namespace DemoGrpc.Web.Services
{
public class CountryGrpcService : CountryService.CountryServiceBase
{
private readonly ICountryService _countryService;
private readonly IMapper _mapper;
public CountryGrpcService(ICountryService countryService, IMapper mapper)
{
_countryService = countryService;
_mapper = mapper;
}
public override async Task<CountryReply> Create(CountryCreateRequest request, ServerCallContext context)
{
var createCountry = _mapper.Map<Country>(request);
var country = await _countryService.AddAsync(createCountry);
return _mapper.Map<CountryReply>(country);
}
}
}

It’s a creation operation so we need to implement a validation on CountryCreateRequest parameter which is the message defined in the Proto3 file:

syntax = "proto3";
option csharp_namespace = "DemoGrpc.Protobufs";
service CountryService {
rpc Create (CountryCreateRequest) returns (CountryReply) {}
}
message CountryCreateRequest {
string Name = 1;
string Description = 2;
}
message CountryReply {
int32 Id = 1;
string Name = 2;
string Description = 3;
}

We need to make mandatory the “Name” and “Description” properties, and based on FluentValidation documentation the validator to write should look like this:

using DemoGrpc.Protobufs;
using FluentValidation;
namespace DemoGrpc.Web.Validator
{
public class CountryCreateRequestValidator : AbstractValidator<CountryCreateRequest>
{
public CountryCreateRequestValidator()
{
RuleFor(request => request.Name).NotEmpty().WithMessage("Name is mandatory.");
RuleFor(request => request.Description).NotEmpty().WithMessage("Description is mandatory.");
}
}
}

Once done, we will add three intructions in our Startup.cs:

  • Enable message validation with EnableMessageValidation() extension on gRPC options.
  • Configure validation with AddGrpcValidation() extension on services.
  • And finally add our custom validator with FluentValidation extension AddValidator() on services.

Our Startup.cs should look like this:

using AutoMapper;
using Calzolari.Grpc.AspNetCore.Validation;
using DemoGrpc.Repository;
using DemoGrpc.Repository.Interfaces;
using DemoGrpc.Web.Services;
using DemoGrpc.Web.Validator;
using DempGrpc.Services;
using DempGrpc.Services.Interfaces;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Reflection;
namespace DemoAspNetCore3
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc(options =>
{
options.EnableMessageValidation();
});
services.AddGrpcValidation();
services.AddValidator<CountryCreateRequestValidator>();
services.AddAutoMapper(Assembly.Load("DemoGrpc.Web"));
services.AddScoped<ICountryService, CountryService>();
services.AddScoped<ICountryRepository, EFCountryRepository>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<CountryGrpcService>();
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
});
});
}
}
}

The gRPC service is now well configured, now we can write the client and consume our service.

If you are using Blazor WebAssembly client-side, don’t forget to setup CORS rules with the following headers server-side:

.WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding", "Grpc-Accept-Encoding", "validation-errors-text");

Catching validation errors client side

To be able to read error validation messages we need to install the following package:

Install-Package Calzolari.Grpc.Net.Client.Validation -Version 3.1.2

Now let’s write our client, miss the “Description” property and add an extension on RpcException named GetValidationErrors() in our catch block.

The client should look like this:

using Calzolari.Grpc.Net.Client.Validation;
using DemoGrpc.Protobufs;
using Grpc.Core;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading.Tasks;
using static DemoGrpc.Protobufs.CountryService;
namespace ConsoleAppGRPC
{
class Program
{
static async Task Main(string[] args)
{
// DI
var services = new ServiceCollection();
// https://grpcwebdemo.azurewebsites.net
// gRPC
services.AddGrpcClient<CountryServiceClient>(o =>
{
o.Address = new Uri("https://localhost:5001");
});
var provider = services.BuildServiceProvider();
var client = provider.GetRequiredService<CountryServiceClient>();
try
{
var createdCountry = await client.CreateAsync(new CountryCreateRequest{Name = "Japan", Description = ""});
}
catch (RpcException e)
{
var errors = e.GetValidationErrors(); // Gets validation errors list
Console.WriteLine(e.Message);
}
}
}
}

Now let’s try!

Demo

Let’s execute and see how errors look like:

Actually the library return 3 metadata

  • The value passed to the service
  • The error message
  • The involved property name

So how do you like it? 🙂

If you have any suggestion to make the library more powerful just let me know 😉

Written by

anthonygiretti

Anthony is a specialist in Web technologies (14 years of experience), in particular Microsoft .NET and learns the Cloud Azure platform. He has received twice the Microsoft MVP award and he is also certified Microsoft MCSD and Azure Fundamentals.