Skip to content

Commit 8e12f90

Browse files
committed
Add documentation for [SubService]
1 parent 4df7b01 commit 8e12f90

File tree

2 files changed

+103
-17
lines changed

2 files changed

+103
-17
lines changed

docs/gettingstarted.md

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,63 @@ public class MultiplyResult
7474
Object models can be arbitrarily deep and complex (including lists, arrays, etc), but should be trees (not graphs).
7575

7676

77-
Service contracts are interfaces marked with `[ServiceContract]`. You can optionally specify the gRPC service name, otherwise it'll use
78-
reasonable convention-based defaults. Individual RPC calls are methods, which can optionally be marked with `[OperationContract]` to control
79-
the name.
77+
Service contracts are interfaces marked with `[ServiceContract]` (from `System.ServiceModel`) or `[Service]` (protobuf-net.Grpc inbuilt). You can optionally specify the gRPC service name, otherwise it'll use
78+
reasonable convention-based defaults. Individual RPC calls are methods, which can optionally be marked with `[OperationContract]` (from `System.ServiceModel`) or `[Operation]` (protobuf-net.Grpc inbuilt) to control
79+
the name. There is also `[SubService]`, which is used as below.
80+
81+
### Interface inheritance works, with 2 possible scenarios:
82+
83+
1. Inherited interfaces as distinct routable services
84+
85+
Consider:
86+
87+
``` c#
88+
[Service("foo")]
89+
interface IFoo : IBar
90+
{
91+
Task A();
92+
}
93+
[Service("bar")]
94+
interface IBar
95+
{
96+
Task B();
97+
}
98+
```
99+
100+
Here, the bindings are `/foo/A` and `/bar/B`. A client can be constructed for `IBar` by itself, since `IBar` is routable (via `/bar/`). If there was an additional service that *also* inherited `IBar`, they
101+
could not be used independently, since both uses would want to route via `/bar/.
102+
103+
2. Inherited interfaces as composition
104+
105+
Consider:
106+
107+
``` c#
108+
[Service("foo")]
109+
interface IFoo : IBar
110+
{
111+
Task A();
112+
}
113+
[SubService]
114+
interface IBar
115+
{
116+
Task B();
117+
}
118+
```
119+
120+
Here, the bindings are `/foo/A`, `/foo/B`. The methods from `IBar` are lifted upwards and form part of the `IFoo` service. As a consequence, a client **cannot** be constructed for `IBar` in isolation, as
121+
`IBar` is not routable without knowing the top-level service to use. The upside of this, however, is that we can add additional services with the same common API, and route them independently. For example, we can add:
122+
123+
``` c#
124+
[Service("blap")]
125+
interface IBlap : IBar
126+
{
127+
Task C();
128+
}
129+
```
130+
131+
This adds the bindings `/blap/C` and `/blap/B`, so now we have two completely independent routable implementations of `B()`. This is especially useful for generic scenarios, common repositories, etc.
132+
133+
### Call types
80134

81135
In gRPC, there are 4 types of call available:
82136

@@ -88,7 +142,7 @@ In gRPC, there are 4 types of call available:
88142
Let's start with unary; a simple example there might be:
89143

90144
``` c#
91-
[ServiceContract(Name = "Hyper.Calculator")]
145+
[ServiceContract(Name = "Hyper.Calculator")] // or [Service("Hyper.Calculator")]
92146
public interface ICalculator
93147
{
94148
ValueTask<MultiplyResult> MultiplyAsync(MultiplyRequest request);

tests/protobuf-net.Grpc.Test/ContractOperationTests.cs

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,15 @@ public void ServerSignatureCount()
6464
[Fact]
6565
public void CheckAllMethodsCovered()
6666
{
67-
var expected = new HashSet<string>(typeof(IAllOptions).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Select(x => x.Name));
67+
var expected = new HashSet<string>(typeof(IAllOptions).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Select(x => x.Name));
6868
Assert.Equal(ContractOperation.SignatureCount, expected.Count);
6969

7070
var attribs = GetType().GetMethod(nameof(CheckMethodIdentification))!.GetCustomAttributesData();
71-
foreach(var attrib in attribs)
71+
foreach (var attrib in attribs)
7272
{
7373
if (attrib.AttributeType != typeof(InlineDataAttribute)) continue;
7474

75-
foreach(var arg in attrib.ConstructorArguments)
75+
foreach (var arg in attrib.ConstructorArguments)
7676
{
7777
var vals = (ReadOnlyCollection<CustomAttributeTypedArgument>)arg.Value!;
7878
var name = (string)vals[0].Value!;
@@ -82,45 +82,77 @@ public void CheckAllMethodsCovered()
8282

8383
Assert.Empty(expected);
8484
}
85-
86-
85+
86+
8787
[Service]
8888
[SubService]
8989
public interface IIServiceWithBothServiceAndSubServiceAttributes
9090
{
9191
void SomeMethod();
9292
}
93-
93+
9494
[Fact]
9595
public void WhenInterfaceHasAttributesServiceAndSubService_Throw()
9696
{
9797
var config = BinderConfiguration.Default;
9898
Action activation = () => ContractOperation.ExpandWithInterfacesMarkedAsSubService(
99-
config.Binder,
99+
config.Binder,
100100
typeof(IIServiceWithBothServiceAndSubServiceAttributes));
101101
Assert.Throws<ArgumentException>(activation.Invoke);
102102
}
103-
103+
104104
public class ServiceContractClassInheritsServiceAndSubServiceAttributes
105105
: IIServiceWithBothServiceAndSubServiceAttributes, IGrpcService
106106
{
107107
public void SomeMethod()
108108
{
109109
}
110110
}
111-
111+
112112
[Fact]
113113
public void WhenServiceContractClassImplementsInterfaceHavingAttributesServiceAndServiceInheritable_Throw()
114114
{
115115
var config = BinderConfiguration.Default;
116116
Action activation = () => ContractOperation.ExpandWithInterfacesMarkedAsSubService(
117-
config.Binder,
117+
config.Binder,
118118
typeof(ServiceContractClassInheritsServiceAndSubServiceAttributes));
119119
Assert.Throws<ArgumentException>(activation.Invoke);
120120
}
121121

122-
123-
122+
[Fact]
123+
public void MultiLevelSubServiceBindings()
124+
{
125+
// server
126+
var serverBinder = new TestServerBinder();
127+
Assert.Equal(5, serverBinder.Bind<IOuter>(null!));
128+
var methods = string.Join(",", serverBinder.Methods.OrderBy(_ => _));
129+
130+
Assert.Equal("/other/C,/other/D,/outer/A,/outer/B,/outer/C", methods);
131+
}
132+
133+
[Service("outer")]
134+
public interface IOuter : IMiddle, IOtherMiddle
135+
{
136+
void A();
137+
}
138+
139+
[SubService]
140+
public interface IMiddle : IInner
141+
{
142+
void B();
143+
}
144+
[SubService]
145+
public interface IInner
146+
{
147+
void C();
148+
}
149+
150+
[Service("other")]
151+
public interface IOtherMiddle : IInner
152+
{
153+
void D();
154+
}
155+
124156
#pragma warning disable CS0618 // Empty
125157
[Theory]
126158
[InlineData(nameof(IAllOptions.Client_AsyncUnary), typeof(HelloRequest), typeof(HelloReply), MethodType.Unary, (int)ContextKind.CallOptions, (int)ResultKind.Grpc, (int)VoidKind.None, (int)ResultKind.Sync)]
@@ -271,7 +303,7 @@ public void WriteAllMethodSignatures()
271303
{
272304
_output.WriteLine(grp.Key.ToString());
273305
_output.WriteLine("");
274-
foreach(var (kind, signature) in grp.OrderBy(x => x.Signature))
306+
foreach (var (kind, signature) in grp.OrderBy(x => x.Signature))
275307
_output.WriteLine(signature);
276308
_output.WriteLine("");
277309
}

0 commit comments

Comments
 (0)