Learnixo
Back to blog
AI Systemsintermediate

Unary RPC — Request-Response gRPC in ASP.NET Core

Implement unary gRPC endpoints in ASP.NET Core: service implementation, error handling with StatusCode, authentication, dependency injection, and calling gRPC services from .NET clients.

Asma Hafeez KhanMay 16, 20265 min read
gRPCUnary RPCASP.NET Core.NETAPI
Share:𝕏

Unary RPC — The Simplest Pattern

Unary RPC: one request, one response. Equivalent to a standard HTTP call but over gRPC.

Client: gRPC stub call → sends CreatePatientRequest
Server: executes service method → returns CreatePatientResponse
One round trip, one result

Service Implementation

C#
// Generated from .proto — you implement the abstract methods
// Generated class: SystemForge.Clinical.Grpc.V1.ClinicalPatientService.ClinicalPatientServiceBase

// Infrastructure/Grpc/ClinicalPatientGrpcService.cs
public sealed class ClinicalPatientGrpcService
    : ClinicalPatientService.ClinicalPatientServiceBase
{
    private readonly CreatePatientHandler _createHandler;
    private readonly GetPatientHandler    _getHandler;
    private readonly ILogger<ClinicalPatientGrpcService> _logger;

    public ClinicalPatientGrpcService(
        CreatePatientHandler createHandler,
        GetPatientHandler    getHandler,
        ILogger<ClinicalPatientGrpcService> logger)
    {
        _createHandler = createHandler;
        _getHandler    = getHandler;
        _logger        = logger;
    }

    public override async Task<CreatePatientResponse> CreatePatient(
        CreatePatientRequest request,
        ServerCallContext context)
    {
        _logger.LogInformation(
            "gRPC CreatePatient: MRN={MRN}", request.Mrn);

        var cmd = new CreatePatientCommand(
            request.Name,
            DateOnly.Parse(request.DateOfBirth),
            request.Mrn);

        var result = await _createHandler.Handle(cmd, context.CancellationToken);

        if (result.IsFailure)
        {
            throw result.Error.Code switch
            {
                "Patient.MRNAlreadyExists" =>
                    new RpcException(new Status(StatusCode.AlreadyExists, result.Error.Description)),
                "Patient.InvalidDateOfBirth" =>
                    new RpcException(new Status(StatusCode.InvalidArgument, result.Error.Description)),
                _ =>
                    new RpcException(new Status(StatusCode.Internal, "An error occurred."))
            };
        }

        return new CreatePatientResponse { PatientId = result.Value.ToString() };
    }

    public override async Task<GetPatientResponse> GetPatient(
        GetPatientRequest request,
        ServerCallContext context)
    {
        if (!Guid.TryParse(request.PatientId, out var id))
            throw new RpcException(new Status(StatusCode.InvalidArgument,
                "PatientId must be a valid UUID."));

        var result = await _getHandler.Handle(
            new GetPatientQuery(id), context.CancellationToken);

        if (result.IsFailure)
            throw new RpcException(new Status(StatusCode.NotFound,
                $"Patient {request.PatientId} not found."));

        var p = result.Value;
        return new GetPatientResponse
        {
            Id         = p.Id.ToString(),
            Name       = p.Name,
            Mrn        = p.MRN,
            Department = p.Department,
            IsActive   = p.IsActive,
        };
    }
}

Server Registration

C#
// Program.cs
builder.Services.AddGrpc(options =>
{
    options.EnableDetailedErrors = builder.Environment.IsDevelopment();
    options.MaxReceiveMessageSize = 4 * 1024 * 1024;  // 4MB
    options.MaxSendMessageSize    = 4 * 1024 * 1024;  // 4MB
});

builder.Services.AddGrpcReflection();  // allows grpcurl and Postman discovery

// Register service handlers (same as HTTP API handlers)
builder.Services.AddScoped<CreatePatientHandler>();
builder.Services.AddScoped<GetPatientHandler>();

var app = builder.Build();

app.MapGrpcService<ClinicalPatientGrpcService>();

if (app.Environment.IsDevelopment())
    app.MapGrpcReflectionService();  // enables discovery tools

gRPC Status Codes

gRPC status codes — use instead of HTTP status codes:
  OK              = 0   ← success
  CANCELLED       = 1   ← client cancelled the request
  INVALID_ARGUMENT = 3  ← bad input (HTTP 400)
  NOT_FOUND       = 5   ← resource not found (HTTP 404)
  ALREADY_EXISTS  = 6   ← duplicate resource (HTTP 409)
  PERMISSION_DENIED = 7 ← auth failure (HTTP 403)
  UNAUTHENTICATED = 16  ← not authenticated (HTTP 401)
  RESOURCE_EXHAUSTED = 8 ← rate limited (HTTP 429)
  INTERNAL        = 13  ← unexpected error (HTTP 500)
  UNAVAILABLE     = 14  ← service unavailable (HTTP 503)
  DEADLINE_EXCEEDED = 4 ← timeout (HTTP 408/504)

Authentication in gRPC

C#
// Program.cs — add JWT bearer auth for gRPC
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        // Same JWT configuration as REST API
        options.TokenValidationParameters = /* ... */;
    });

builder.Services.AddAuthorization();

// Service: read the user from the context
public override async Task<CreatePatientResponse> CreatePatient(
    CreatePatientRequest request,
    ServerCallContext context)
{
    // Access the authenticated user via context
    var user   = context.GetHttpContext().User;
    var userId = user.FindFirstValue(JwtRegisteredClaimNames.Sub);

    if (!user.IsInRole("Doctor"))
        throw new RpcException(new Status(StatusCode.PermissionDenied,
            "Only doctors can create patients."));

    // ...
}

// Or use policy-based auth on the service class
[Authorize(Policy = "DoctorsOnly")]
public sealed class ClinicalPatientGrpcService
    : ClinicalPatientService.ClinicalPatientServiceBase { }

gRPC Client in .NET

C#
// NuGet: Grpc.Net.Client, Google.Protobuf, Grpc.Tools (client-side)
builder.Services.AddGrpcClient<ClinicalPatientService.ClinicalPatientServiceClient>(options =>
{
    options.Address = new Uri("https://clinical-api.systemforge.internal");
});

// Or manual client creation
using var channel = GrpcChannel.ForAddress("https://clinical-api.internal");
var client  = new ClinicalPatientService.ClinicalPatientServiceClient(channel);

// Call the service
var request  = new CreatePatientRequest
{
    Name        = "John Smith",
    Mrn         = "MRN-001",
    DateOfBirth = "1985-03-15"
};

try
{
    var response = await client.CreatePatientAsync(request,
        deadline: DateTime.UtcNow.AddSeconds(10));
    Console.WriteLine($"Created: {response.PatientId}");
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.AlreadyExists)
{
    Console.WriteLine($"MRN already exists: {ex.Status.Detail}");
}

Deadlines

C#
// Always set a deadline — prevents indefinite hanging
var response = await client.GetPatientAsync(
    new GetPatientRequest { PatientId = id },
    deadline: DateTime.UtcNow.AddSeconds(5),       // absolute deadline
    cancellationToken: cancellationToken);           // or CancellationToken

// Client sees: RpcException with StatusCode.DeadlineExceeded if exceeded
// Server sees: context.CancellationToken cancelled — clean up early

Production issue I've seen: A gRPC client calling a downstream pharmacy service had no deadline set. The pharmacy service had a bug that caused some requests to hang indefinitely. The clinical API accumulated thousands of in-flight gRPC calls, each holding a thread. Within minutes, the thread pool was exhausted. Adding a 5-second deadline meant the call failed fast with DeadlineExceeded rather than hanging forever.


Red Flag / Green Answer

Red Flag: "We throw Exception in our gRPC service methods — the client sees StatusCode.Internal with no details."

Exception (not RpcException) causes the framework to return INTERNAL with a generic message. The client cannot distinguish "patient not found" from "database down" — both look like server errors. Use RpcException with the appropriate StatusCode.

Green Answer:

Map Result failures to RpcException with the right StatusCode. PatientErrors.NotFoundStatusCode.NotFound. PatientErrors.MRNAlreadyExistsStatusCode.AlreadyExists. Clients can handle these discriminately.


Key Takeaway

Unary gRPC in ASP.NET Core: inherit from the generated service base, inject application handlers, delegate to them. Map Result failures to RpcException with the correct StatusCode — gRPC status codes replace HTTP status codes. Always set deadlines on gRPC calls — missing deadlines cause thread pool exhaustion under failure scenarios. Use gRPC reflection in development to enable discovery tools like grpcurl and Postman.

gRPC Knowledge Check

5 questions · Test what you just learned · Instant explanations

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.