
gRPC code-first data annotation validation
If you’ve worked with ASP.NET MVC or Minimal APIs, you’re probably used to decorating your models with [Required], [StringLength], and [Range] attributes and having validation just work out of the box. It’s one of those things you take for granted. So when you move to
code-first gRPC, you’d naturally expect the same System.ComponentModel.DataAnnotations attributes to be validated automatically on incoming requests. Oddly, they aren’t. gRPC in ASP.NET Core simply ignores data annotations entirely — there’s no built-in middleware or interceptor to validate them. You’re left to roll your own validation logic or go without.
That gap is what led me to create Codify.GrpcCodeFirstDataAnnotations, a NuGet package that brings familiar DataAnnotations validation to gRPC code-first services via a server-side interceptor.
Packages
The solution is split into two packages:
| Package | Purpose |
|---|---|
| Codify.GrpcCodeFirstDataAnnotations | Server-side interceptor that validates incoming gRPC requests |
| Codify.GrpcCodeFirstDataAnnotations.Exceptions | Client-side helper to read validation errors from gRPC RpcException trailers |
Server Setup
Install the NuGet package and register the validation interceptor:
dotnet add package Codify.GrpcCodeFirstDataAnnotations
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpcDataAnnotationValidation();
builder.Services.AddCodeFirstGrpc(options =>
{
options.EnableDataAnnotationValidation();
});
You can optionally configure the validation behaviour:
builder.Services.Configure<GrpcDataAnnotationValidationOptions>(options =>
{
// Also validate `required` nullable reference type properties
options.ValidateRequiredNullableProperties = true;
// Log validation failures at Warning level
options.ValidationFailureLogLevel = LogLevel.Warning;
});
Annotating Your Models
Once the interceptor is registered, just decorate your request models with the same DataAnnotations attributes you’d use in MVC:
[DataContract]
public class CreatePersonRequest
{
[DataMember(Order = 1)]
[Required]
[StringLength(50)]
public string FirstName { get; set; }
[DataMember(Order = 2)]
[Required]
[EmailAddress]
public string Email { get; set; }
[DataMember(Order = 3)]
[Range(1, 120)]
public int Age { get; set; }
[DataMember(Order = 4)]
[Range(typeof(TimeSpan), "00:30:00", "08:00:00")]
public TimeSpan SessionDuration { get; set; }
[DataMember(Order = 5)]
public required string Job { get; set; }
}
When a request fails validation, the interceptor throws an RpcException with StatusCode.InvalidArgument and includes the validation errors as Base64-encoded JSON in the gRPC trailers.
Reading Validation Errors on the Client
The companion exceptions package makes it easy to extract structured validation errors from the RpcException:
dotnet add package Codify.GrpcCodeFirstDataAnnotations.Exceptions
try
{
var response = await client.CreatePersonAsync(request);
}
catch (RpcException ex)
{
var errors = ex.GetValidationErrors();
foreach (var error in errors)
{
Console.WriteLine($"{string.Join(", ", error.PropertyNames)}: {error.ErrorMessage}");
}
}
Custom Validation Handling
If you need to customise how validation failures are processed, implement IDataAnnotationsResultHandler:
public class MyCustomHandler : IDataAnnotationsResultHandler
{
public Task<DataAnnotationValidationResult> HandleAsync(IList<ValidationResult> failures)
{
// Custom logic here
}
}
Register it before calling AddGrpcDataAnnotationValidation:
builder.Services.AddSingleton<IDataAnnotationsResultHandler, MyCustomHandler>();
Supported gRPC Method Types
All four gRPC method types are supported:
- Unary
- Server streaming
- Client streaming
- Duplex streaming
For streaming methods, each incoming message is validated as it is read.
The source is on GitHub — contributions and feedback are welcome.