April 25, 2026

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.