gRPC Communication¶
Purpose & Overview¶
gRPC is the preferred high-throughput, server-to-server communication protocol across ConnectSoft microservices and SaaS templates. In ConnectSoft templates gRPC is code-first via ServiceModel.Grpc (max-ieremenko) — contracts are C# interfaces decorated with [ServiceContract]/[OperationContract]/[FaultContract], request messages use [MessageContract], DTOs use [DataContract]. There are no .proto files, no Grpc.Tools codegen, and no Protos/ folder. Endpoints are generated at runtime by ServiceModel.Grpc; clients use ServiceModel.Grpc.Client.ClientFactory with JsonMarshallerFactory to call the same C# interfaces the server implements — no generated stubs.
ConnectSoft gRPC canonical wording
The one-line statement that ships in every template and doc: ConnectSoft templates use code-first gRPC via ServiceModel.Grpc (no .proto files). Contracts are C# interfaces in *.ServiceModel with [ServiceContract]/[OperationContract]/[FaultContract]; request messages use [MessageContract]; DTOs use [DataContract]. The *.ServiceModel.Grpc assembly carries only server-side Grpc<Name>Service adapter classes implementing those interfaces. Endpoints are generated at runtime by AddServiceModelGrpc + MapGrpcService<T>; clients use ServiceModel.Grpc.Client.ClientFactory with JsonMarshallerFactory. Grpc.Tools / Protos/ folders are not used.
Architecture layering¶
ConsumerService / Client App
↓ (gRPC over HTTP/2; JSON marshaller by default)
*.ServiceModel.Grpc (server adapters)
├── Grpc<Name>Service : IXxxService
├── Interceptors (logging, validation, rich error / fault contracts)
└── AutoMapper (request DTO → domain input; domain output → response DTO)
↓
*.DomainModel.Impl
├── Processors (commands)
└── Retrievers (queries)
↓
*.PersistenceModel.NHibernate (or Dapper / EFCore)
Layer responsibilities¶
| Layer | Purpose |
|---|---|
*.ServiceModel |
Authoring surface — C# I*Service interfaces, [MessageContract] requests, [DataContract] DTOs, [FaultContract] faults. REST controllers and gRPC adapters both implement these interfaces. |
*.ServiceModel.Grpc |
Server-side adapter classes (Grpc<Name>Service) only. Delegates to domain services via constructor injection. No .proto, no Grpc.Tools, no Protos/. |
*.ApplicationModel / GrpcExtensions |
Calls AddGrpc(), AddServiceModelGrpc(options => options.DefaultMarshallerFactory = new JsonMarshallerFactory()), AddGrpcReflection(); registers interceptors + fault handlers. |
*.DomainModel / *.DomainModel.Impl |
Business logic — processors and retrievers. |
*.PersistenceModel.NHibernate |
Data access. |
Authoring the contract¶
Contracts live in *.ServiceModel:
namespace ConnectSoft.Xxx.ServiceModel
{
using System.ServiceModel;
[ServiceContract(Namespace = ServiceModelConstants.XxxNamespace, Name = "XxxService")]
public interface IXxxService
{
[OperationContract(Name = "GetById")]
[FaultContract(typeof(ValidationFault))]
[FaultContract(typeof(ResourceNotFoundFault))]
Task<XxxResponse> GetByIdAsync(GetByIdRequest request, CancellationToken token);
}
}
Request/response messages are [MessageContract] classes with [MessageBodyMember] members; DTOs are [DataContract] with ordered [DataMember]s. See ConnectSoft.IdentityTemplate.ServiceModel/LoginRequest.cs and UserDto.cs for canonical examples.
Server adapter¶
*.ServiceModel.Grpc only implements the interface:
public class GrpcXxxService : IXxxService
{
private readonly IXxxRetriever retriever;
private readonly IMapper mapper;
public GrpcXxxService(IXxxRetriever retriever, IMapper mapper)
{
this.retriever = retriever;
this.mapper = mapper;
}
public async Task<XxxResponse> GetByIdAsync(GetByIdRequest request, CancellationToken token)
{
Xxx domain = await this.retriever.GetByIdAsync(this.mapper.Map<XxxInput>(request), token).ConfigureAwait(false);
return this.mapper.Map<XxxResponse>(domain);
}
}
The .csproj references ServiceModel.Grpc.AspNetCore (v1.22.x) and ConnectSoft.Extensions.ServiceModel.Grpc. It does not reference Grpc.Tools, Google.Protobuf, and has no <Protobuf Include> items. ArchitectureTests assert these facts repo-by-repo.
Runtime wiring¶
// ApplicationModel/GrpcExtensions.cs
services.AddGrpc(options => ConfigureGrpcOptions(options));
services.AddServiceModelGrpc(options =>
{
options.DefaultMarshallerFactory = new JsonMarshallerFactory();
});
services.AddGrpcReflection();
// Program.cs endpoint map
endpoints.MapGrpcService<GrpcXxxService>();
endpoints.MapGrpcReflectionService();
Client usage¶
Consumers reference the *.ServiceModel NuGet (no generated stubs) and use ServiceModel.Grpc.Client.ClientFactory:
ClientFactory clientFactory = new ClientFactory(new ServiceModelGrpcClientOptions
{
MarshallerFactory = new JsonMarshallerFactory(),
});
IXxxService client = clientFactory.CreateClient<IXxxService>(GrpcChannel.ForAddress("https://xxx.local"));
XxxResponse response = await client.GetByIdAsync(request, CancellationToken.None);
JsonMarshallerFactory is the default — switch to ProtobufMarshallerFactory only when wire size dominates. The C# contract is unchanged either way.
Error handling strategies¶
OptionsExtensions.GrpcOptions.GrpcErrorHandlingStrategy:
FaultContract— domain exceptions map to[FaultContract]types (ValidationFault,ResourceNotFoundFault,ConflictFault,ForbiddenFault, …) viaFaultContractTransformerServerFilter. Clients receive typed faults through the same C# interface contract.RichError—GrpcRichErrorInterceptorproduces GoogleStatus+google.rpc.ErrorInfo/BadRequestdetails. Works with any gRPC-native client.
GrpcServerLoggingInterceptor is always registered.
Streaming¶
ServiceModel.Grpc supports all four call types (unary, server streaming, client streaming, bidirectional) from C# method signatures (IAsyncEnumerable<T>, CallOptions, IServerStreamWriter<T>). No .proto change is required; the runtime introspects the C# signature.
Health + reflection¶
AddGrpcReflection()+MapGrpcReflectionService()— server contracts become discoverable by tooling (grpcurl, BloomRPC) through the same C# interfaces.- ASP.NET Core health checks expose
/health;Grpc.HealthChecksurfaces them as gRPC calls.
REST parity¶
REST controllers in *.ServiceModel.RestApi implement the same I*Service interfaces. That is how ConnectSoft templates avoid contract drift between REST and gRPC — there is one contract, two transports.
Discipline (enforced by ArchitectureTests in every template)¶
*.ServiceModel.Grpccontains zero.protofiles and zero<Protobuf Include>items.- Every
I*Serviceinterface in*.ServiceModelcarries[ServiceContract]; every public method carries[OperationContract]. Grpc.Toolsis never referenced.- REST and gRPC share the same C# interfaces.
Cross-references¶
- Canonical one-line statement:
ConnectSoft.BaseTemplate/docs/Technology Stack.md— "ServiceModel.Grpcis code-first (no.protofiles)". - Per-template foundation:
ConnectSoft.IdentityTemplate/Docs/public/foundations/grpc.md. - Sample contract + adapter:
ConnectSoft.IdentityTemplate.ServiceModel/IAuthenticationService.cs+ConnectSoft.IdentityTemplate.ServiceModel.Grpc/GrpcAuthenticationService.cs. - Common client helpers:
ConnectSoft.Extensions.ServiceModel.Grpc+ConnectSoft.Extensions.ServiceModel.Grpc.Client.
Non-ConnectSoft / foreign-stack gRPC¶
When integrating with a third-party service that authors its gRPC surface via .proto + Grpc.Tools codegen, wrap that integration in an anti-corruption layer under *.FlowModel.MassTransit/Adapters/* (or the equivalent infrastructure seam). Keep the generated proto surface out of *.ServiceModel / *.ServiceModel.Grpc so the one-contract-two-transports invariant is preserved for ConnectSoft-owned surfaces.