Server and Client
Override the generated base class, map it with MapGrpcService, then call it from the generated client with a deadline.
public override Task<TReply> MethodName(TRequest request, ServerCallContext context)The generated code hands each side half of the call. On the server, a service named Greeter in the contract produces an abstract class Greeter.GreeterBase with one virtual method per rpc. Every generated method returns the status Unimplemented until you override it, so a service answers exactly the methods you wrote and no others.
Serving it
builder.Services.AddGrpc() adds the server pieces. app.MapGrpcService<GreeterService>() routes calls to your class. The route comes from the contract rather than from an attribute: package greet.v1, service Greeter, method SayHello is served at /greet.v1.Greeter/SayHello. Rename any of those three and existing callers get Unimplemented. Called with grpcurl, the example below answers {"message": "Hello Ada"}, and an empty name comes back as status InvalidArgument with the message name is required.
Calling it
A channel is the client's long-lived connection to one address, built with GrpcChannel.ForAddress. Create it once and share it, because one channel carries many calls at the same time. From a channel you construct the generated Greeter.GreeterClient. Each unary rpc generates two methods: SayHello blocks the calling thread, and SayHelloAsync returns a call object you await to get the reply.
Deadlines
A deadline is the absolute UTC time after which the caller stops waiting. Nothing sets one for you. client.SayHelloAsync(request, deadline: DateTime.UtcNow.AddSeconds(5)) is shorthand for the overload that takes a CallOptions value, new CallOptions(deadline: DateTime.UtcNow.AddSeconds(5)); they are the same call. The deadline travels with the request, and when it passes the server sees ServerCallContext.CancellationToken cancelled.
Example
// Server project from: dotnet new grpc
// Protos/greet.proto: package greet.v1; option csharp_namespace = "Greet.V1";
// service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); }
// message HelloRequest { string name = 1; }
// message HelloReply { string message = 1; }
using Greet.V1;
using Grpc.Core;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc();
var app = builder.Build();
// Serves /greet.v1.Greeter/SayHello from GreeterService.
app.MapGrpcService<GreeterService>();
app.Run();
public class GreeterService : Greeter.GreeterBase
{
// Without this override the generated method answers Unimplemented.
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
if (string.IsNullOrEmpty(request.Name))
{
// The caller receives this status code and message, not an exception.
throw new RpcException(new Status(StatusCode.InvalidArgument, "name is required"));
}
// The method is not async, so the reply is wrapped in a completed Task.
return Task.FromResult(new HelloReply { Message = $"Hello {request.Name}" });
}
}When to use it
- Implement the first method of a service generated from a .proto file
- Call another team's gRPC service from an ASP.NET Core app
- Put a time limit on an internal call so a slow dependency cannot pin your threads
- Return a meaningful failure for a missing or invalid request instead of a generic error
More examples
A client with a deadline, both ways of setting it
Every failure arrives as an RpcException carrying a status code, including failures that never reached the server, such as Unavailable when nothing is listening. Filtering with when lets you treat a timeout differently from a server-side rejection. Note what is being awaited: SayHelloAsync returns a call object, and awaiting it produces the reply. Hold the call object instead when you need the response headers, trailers or final status.
using Greet.V1;
using Grpc.Core;
using Grpc.Net.Client;
// One channel, created once and shared. It is not a per-call object.
using var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Greeter.GreeterClient(channel);
try
{
// Shorthand: deadline is a named argument on the generated method.
HelloReply reply = await client.SayHelloAsync(
new HelloRequest { Name = "Ada" },
deadline: DateTime.UtcNow.AddSeconds(5));
Console.WriteLine(reply.Message);
// The same call, longhand, through the CallOptions overload.
var options = new CallOptions(deadline: DateTime.UtcNow.AddSeconds(5));
HelloReply again = await client.SayHelloAsync(new HelloRequest { Name = "Ada" }, options);
Console.WriteLine(again.Message);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
{
Console.WriteLine("the greeter did not answer within five seconds");
}
catch (RpcException ex)
{
Console.WriteLine($"{ex.StatusCode}: {ex.Status.Detail}");
}Registering the client once, injecting it everywhere
AddGrpcClient registers the generated client as a transient service and hands the underlying HTTP handler to HttpClientFactory, which is the same lifetime machinery HttpClient uses. The client type is then injected like any other dependency, and the address lives in startup code where configuration can supply it. This is the registration to reach for in a web app or worker; a short console program can keep a single GrpcChannel instead.
// Needs the Grpc.Net.ClientFactory package.
using Greet.V1;
var builder = WebApplication.CreateBuilder(args);
// One place that knows the address. The factory manages the connection.
builder.Services.AddGrpcClient<Greeter.GreeterClient>(options =>
{
options.Address = new Uri("https://greeter.internal:5001");
});
builder.Services.AddScoped<WelcomeService>();
var app = builder.Build();
app.MapGet("/welcome/{name}", (string name, WelcomeService welcome) => welcome.GreetAsync(name));
app.Run();
public class WelcomeService
{
private readonly Greeter.GreeterClient _greeter;
// The generated client is injected. Nothing here builds a channel.
public WelcomeService(Greeter.GreeterClient greeter)
{
_greeter = greeter;
}
public async Task<string> GreetAsync(string name)
{
HelloReply reply = await _greeter.SayHelloAsync(
new HelloRequest { Name = name },
deadline: DateTime.UtcNow.AddSeconds(2));
return reply.Message;
}
}Passing the deadline on to the next service
A deadline is only useful if the whole chain respects it. ServerCallContext carries both the caller's deadline and a cancellation token, and forwarding them means one caller giving up releases work in every service behind it. Doing this by hand is easy to forget on one call out of ten, so a client registered with AddGrpcClient can add EnableCallContextPropagation() from the Grpc.AspNetCore.Server.ClientFactory package to attach them automatically.
using Grpc.Core;
using Orders.V1;
using Pricing.V1;
public class OrdersService : Orders.OrdersBase
{
private readonly Pricing.PricingClient _pricing;
private readonly OrderStore _store;
public OrdersService(Pricing.PricingClient pricing, OrderStore store)
{
_pricing = pricing;
_store = store;
}
public override async Task<Order> GetOrder(GetOrderRequest request, ServerCallContext context)
{
// Pass the call's token to anything that waits, so a timed-out
// call stops working instead of finishing into a dead connection.
Order? order = await _store.FindAsync(request.OrderId, context.CancellationToken);
if (order is null)
{
throw new RpcException(new Status(StatusCode.NotFound, $"no order {request.OrderId}"));
}
// context.Deadline is the caller's deadline. Give the child call the same one.
PriceReply price = await _pricing.GetPriceAsync(
new PriceRequest { OrderId = order.OrderId },
deadline: context.Deadline,
cancellationToken: context.CancellationToken);
order.Total = price.Total;
return order;
}
}
Discussion