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
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.Grpcwith its transitive dependencies — lands in the contracts/messagesassembly and travels to every consumer of it. A client process ends up carrying:
Grpc.AspNetCore.Serveris 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 itIt compiles, deploys, and keeps every server-side test green — then fails on the first real
client call. protobuf-net.Grpc's
ProxyEmittercallsAttribute.GetCustomAttributeswhenbuilding 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: falseFindCodeFirstServiceContracts(GrpcGraph.cs:284-289) scansGetExportedTypes()WolverineGrpcOptionsexposes no per-type registrationDiscovery.IncludeAssemblywidens which assemblies are scanned but cannot nominate a typeDeclaring 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 generatedimplementation forwards only the derived interface's own methods and the host fails at
MapWolverineGrpcServiceswithCS0535for each inherited operation.Proposed solutions
1. Explicit registration
A registration method on
WolverineGrpcOptions, matching the existingAddPolicy<T>()shape:
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:
WolverineGrpcOptions, populated byAddCodeFirstService<T>()GrpcGraph.DiscoverServicesunions it with the scan results — it already receivesWolverineGrpcOptions(GrpcGraph.cs:77), so the read point exists — deduplicatingagainst a type that is both attributed and registered
IsCodeFirstServiceContractrelaxed for explicitly registered types: still require[ServiceContract], drop the[WolverineGrpcService]requirementTypeLoadMode.Staticneeds nothing extra:GrpcServiceRegistryCodeFileis projected fromthe chains (
GrpcGraph.cs:62-66), so anything that becomes a chain during discovery iscaptured for cold start automatically.
2. An assembly-level attribute
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, samemapped operations, contracts assembly clean — but it is a generated duplicate of an interface
that already exists, purely to hold one attribute.
Environment