Skip to content

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, …) via FaultContractTransformerServerFilter. Clients receive typed faults through the same C# interface contract.
  • RichErrorGrpcRichErrorInterceptor produces Google Status + google.rpc.ErrorInfo / BadRequest details. 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.HealthCheck surfaces 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.Grpc contains zero .proto files and zero <Protobuf Include> items.
  • Every I*Service interface in *.ServiceModel carries [ServiceContract]; every public method carries [OperationContract].
  • Grpc.Tools is never referenced.
  • REST and gRPC share the same C# interfaces.

Cross-references

  • Canonical one-line statement: ConnectSoft.BaseTemplate/docs/Technology Stack.md — "ServiceModel.Grpc is code-first (no .proto files)".
  • 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.