Skip to content

Allow registering a code-first gRPC service without an attribute on the contract #4396

Description

@ArieGato

Request

A way to register a code-first gRPC service contract from the host, so that
[WolverineGrpcService] does not have to sit on the [ServiceContract] interface itself.

Why

Adopting Wolverine.Grpc code-first means putting [WolverineGrpcService] on the
[ServiceContract] interface. That interface is the one clients bind, so the attribute —
and WolverineFx.Grpc with its transitive dependencies — lands in the contracts/messages
assembly and travels to every consumer of it. A client process ends up carrying:

Google.Api.CommonProtos, Google.Protobuf, Grpc.AspNetCore.Server,
Grpc.AspNetCore.Server.ClientFactory, Grpc.StatusProto,
protobuf-net.Grpc.AspNetCore, Wolverine.Grpc

Grpc.AspNetCore.Server is the notable one: the client ships the server hosting stack.

A contracts assembly is meant to be the one thing both sides can reference cheaply, and
nothing server-side belongs in it.

PrivateAssets="all" does not avoid it

It compiles, deploys, and keeps every server-side test green — then fails on the first real
client call. protobuf-net.Grpc's ProxyEmitter calls Attribute.GetCustomAttributes when
building a client proxy, which throws when the attribute's assembly cannot be resolved at
runtime. Full asset flow is required, which is why it reaches consumers.

There is no way to opt in from the host today

  • IsCodeFirstServiceContract (GrpcGraph.cs:291-298) requires [WolverineGrpcService]
    declared on the interface with inherit: false
  • FindCodeFirstServiceContracts (GrpcGraph.cs:284-289) scans GetExportedTypes()
  • WolverineGrpcOptions exposes no per-type registration
  • Discovery.IncludeAssembly widens which assemblies are scanned but cannot nominate a type

Declaring a server-side interface that inherits the contract and carries the attributes is
not a way around it: operation discovery reads
serviceContractType.GetMethods() (CodeFirstGrpcServiceChain.cs:498), so the generated
implementation forwards only the derived interface's own methods and the host fails at
MapWolverineGrpcServices with CS0535 for each inherited operation.

Proposed solutions

1. Explicit registration

A registration method on WolverineGrpcOptions, matching the existing AddPolicy<T>()
shape:

builder.Services.AddWolverineGrpc(grpc =>
{
    grpc.AddCodeFirstService<IThingService>();
});

The contract keeps [ServiceContract] — protobuf-net needs it and clients already bind it —
and carries nothing server-side. From the source this looks like three edits:

  • a list on WolverineGrpcOptions, populated by AddCodeFirstService<T>()
  • GrpcGraph.DiscoverServices unions it with the scan results — it already receives
    WolverineGrpcOptions (GrpcGraph.cs:77), so the read point exists — deduplicating
    against a type that is both attributed and registered
  • IsCodeFirstServiceContract relaxed for explicitly registered types: still require
    [ServiceContract], drop the [WolverineGrpcService] requirement

TypeLoadMode.Static needs nothing extra: GrpcServiceRegistryCodeFile is projected from
the chains (GrpcGraph.cs:62-66), so anything that becomes a chain during discovery is
captured for cold start automatically.

2. An assembly-level attribute

[assembly: WolverineGrpcService(typeof(IThingService))]

Declarative like today, no bootstrap ordering to reason about, and the decision sits in the
host that serves the contract rather than in the contract itself. There is precedent for
assembly-level markers in Wolverine ([WolverineModule]).

Current workaround

A source generator emits a host-side twin interface that restates the contract's operations
and carries [WolverineGrpcService] plus
[ServiceContract(Name = "<the contract's resolved name>")]. It works — same wire name, same
mapped operations, contracts assembly clean — but it is a generated duplicate of an interface
that already exists, purely to hold one attribute.

Environment

  • WolverineFx.Grpc 6.35.0
  • protobuf-net.Grpc 1.2.5, protobuf-net 3.2.56
  • .NET 10

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions