Skip to content

Commit e7d5887

Browse files
authored
Add support for defining a custom metadata lookup in the servicebinder (#121) (#138)
* Move `GetMetadata` to the `ServiceBinder` to allow override (#121) * Add singleton with `Authorize` attribute example (#121)
1 parent 786f359 commit e7d5887

File tree

10 files changed

+229
-71
lines changed

10 files changed

+229
-71
lines changed

examples/pb-net-grpc/Client_CS/Program.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
using Grpc.Core;
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using Grpc.Core;
25
using Grpc.Net.Client;
36
using MegaCorp;
47
using ProtoBuf.Grpc;
58
using ProtoBuf.Grpc.Client;
69
using Shared_CS;
7-
using System;
8-
using System.Threading;
9-
using System.Threading.Tasks;
1010

1111
namespace Client_CS
1212
{
@@ -21,6 +21,7 @@ static async Task Main()
2121
Console.WriteLine(result.Result); // 48
2222

2323
var clock = http.CreateGrpcService<ITimeService>();
24+
var counter = http.CreateGrpcService<ICounter>();
2425
using var cancel = new CancellationTokenSource(TimeSpan.FromMinutes(1));
2526
var options = new CallOptions(cancellationToken: cancel.Token);
2627

@@ -29,10 +30,14 @@ static async Task Main()
2930
await foreach (var time in clock.SubscribeAsync(new CallContext(options)))
3031
{
3132
Console.WriteLine($"The time is now: {time.Time}");
33+
var currentInc = await counter.IncrementAsync(new IncrementRequest { Inc = 1 });
34+
Console.WriteLine($"Time received {currentInc.Result} times");
3235
}
3336
}
34-
catch (RpcException) { }
37+
catch (RpcException ex) { Console.WriteLine(ex); }
3538
catch (OperationCanceledException) { }
39+
Console.WriteLine("Press [Enter] to exit");
40+
Console.ReadLine();
3641
}
3742
}
3843
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System.Security.Claims;
2+
using System.Text.Encodings.Web;
3+
using System.Threading.Tasks;
4+
using Microsoft.AspNetCore.Authentication;
5+
using Microsoft.Extensions.Logging;
6+
using Microsoft.Extensions.Options;
7+
8+
namespace Server_CS
9+
{
10+
class FakeAuthHandler : AuthenticationHandler<FakeAuthOptions>
11+
{
12+
public const string SchemeName = "Fake";
13+
14+
public FakeAuthHandler(
15+
IOptionsMonitor<FakeAuthOptions> options,
16+
ILoggerFactory logger,
17+
UrlEncoder encoder,
18+
ISystemClock clock)
19+
: base(options, logger, encoder, clock)
20+
{
21+
}
22+
23+
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
24+
{
25+
if (!Options.AlwaysAuthenticate)
26+
return Task.FromResult(AuthenticateResult.NoResult());
27+
28+
var claimsIdentity = new ClaimsIdentity(SchemeName);
29+
var ticket = new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), Scheme.Name);
30+
return Task.FromResult(AuthenticateResult.Success(ticket));
31+
}
32+
}
33+
34+
class FakeAuthOptions : AuthenticationSchemeOptions
35+
{
36+
public bool AlwaysAuthenticate { get; set; } = false;
37+
}
38+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System.Threading.Tasks;
2+
using Microsoft.AspNetCore.Authorization;
3+
using Shared_CS;
4+
5+
namespace Server_CS
6+
{
7+
[Authorize]
8+
public class MyCounter : ICounter
9+
{
10+
private int counter = 0;
11+
private readonly object counterLock = new object();
12+
13+
ValueTask<IncrementResult> ICounter.IncrementAsync(IncrementRequest request)
14+
{
15+
lock (counterLock)
16+
{
17+
counter += request.Inc;
18+
var result = new IncrementResult { Result = counter };
19+
return new ValueTask<IncrementResult>(result);
20+
}
21+
}
22+
}
23+
}

examples/pb-net-grpc/Server_CS/Properties/launchSettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
},
1818
"Server_CS": {
1919
"commandName": "Project",
20-
"launchBrowser": true,
20+
"launchBrowser": false,
2121
"applicationUrl": "https://localhost:5001;http://localhost:5000",
2222
"environmentVariables": {
2323
"ASPNETCORE_ENVIRONMENT": "Development"
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Reflection;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using ProtoBuf.Grpc.Configuration;
7+
8+
namespace Server_CS
9+
{
10+
internal class ServiceBinderWithServiceResolutionFromServiceCollection : ServiceBinder
11+
{
12+
private readonly IServiceCollection services;
13+
14+
public ServiceBinderWithServiceResolutionFromServiceCollection(IServiceCollection services)
15+
{
16+
this.services = services;
17+
}
18+
19+
public override IList<object> GetMetadata(MethodInfo method, Type contractType, Type serviceType)
20+
{
21+
var resolvedServiceType = serviceType;
22+
if (serviceType.IsInterface)
23+
resolvedServiceType = services.SingleOrDefault(x => x.ServiceType == serviceType)?.ImplementationType ?? serviceType;
24+
25+
return base.GetMetadata(method, contractType, resolvedServiceType);
26+
}
27+
}
28+
}

examples/pb-net-grpc/Server_CS/Startup.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
using Microsoft.AspNetCore.Builder;
22
using Microsoft.AspNetCore.Hosting;
33
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.DependencyInjection.Extensions;
5+
using ProtoBuf.Grpc.Configuration;
46
using ProtoBuf.Grpc.Server;
7+
using Shared_CS;
58

69
namespace Server_CS
710
{
@@ -15,16 +18,26 @@ public void ConfigureServices(IServiceCollection services)
1518
{
1619
config.ResponseCompressionLevel = System.IO.Compression.CompressionLevel.Optimal;
1720
});
21+
services.TryAddSingleton(BinderConfiguration.Create(binder: new ServiceBinderWithServiceResolutionFromServiceCollection(services)));
1822
services.AddCodeFirstGrpcReflection();
23+
24+
services.AddAuthentication(FakeAuthHandler.SchemeName)
25+
.AddScheme<FakeAuthOptions, FakeAuthHandler>(FakeAuthHandler.SchemeName, options => options.AlwaysAuthenticate = true);
26+
services.AddAuthorization();
27+
services.AddSingleton<ICounter, MyCounter>();
1928
}
2029

2130
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
2231
public void Configure(IApplicationBuilder app, IWebHostEnvironment _)
2332
{
2433
app.UseRouting();
2534

35+
app.UseAuthentication();
36+
app.UseAuthorization();
37+
2638
app.UseEndpoints(endpoints =>
2739
{
40+
endpoints.MapGrpcService<ICounter>();
2841
endpoints.MapGrpcService<MyCalculator>();
2942
endpoints.MapGrpcService<MyTimeService>();
3043
endpoints.MapCodeFirstGrpcReflectionService();
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System.Runtime.Serialization;
2+
using System.ServiceModel;
3+
using System.Threading.Tasks;
4+
namespace Shared_CS
5+
{
6+
[ServiceContract]
7+
public interface ICounter
8+
{
9+
ValueTask<IncrementResult> IncrementAsync(IncrementRequest request);
10+
}
11+
12+
[DataContract]
13+
public class IncrementRequest
14+
{
15+
[DataMember(Order = 1)]
16+
public int Inc { get; set; }
17+
}
18+
19+
[DataContract]
20+
public class IncrementResult
21+
{
22+
[DataMember(Order = 1)]
23+
public int Result { get; set; }
24+
}
25+
}

src/protobuf-net.Grpc/Configuration/ServerBinder.cs

Lines changed: 18 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
using Grpc.Core;
2-
using ProtoBuf.Grpc.Internal;
3-
using System;
1+
using System;
42
using System.Collections.Generic;
53
using System.Linq;
64
using System.Linq.Expressions;
75
using System.Reflection;
86
using System.Threading.Tasks;
7+
using Grpc.Core;
8+
using ProtoBuf.Grpc.Internal;
99

1010
namespace ProtoBuf.Grpc.Configuration
1111
{
@@ -49,7 +49,7 @@ public int Bind(object state, Type serviceType, BinderConfiguration? binderConfi
4949

5050
var serviceContractSimplifiedExceptions = serviceImplSimplifiedExceptions || serviceContract.IsDefined(typeof(SimpleRpcExceptionsAttribute));
5151
int svcOpCount = 0;
52-
var bindCtx = new ServiceBindContext(serviceContract, serviceType, state);
52+
var bindCtx = new ServiceBindContext(serviceContract, serviceType, state, binderConfiguration.Binder);
5353
foreach (var op in ContractOperation.FindOperations(binderConfiguration, serviceContract, this))
5454
{
5555
if (ServerInvokerLookup.TryGetValue(op.MethodType, op.Context, op.Result, op.Void, out var invoker)
@@ -271,77 +271,37 @@ protected internal sealed class ServiceBindContext
271271
/// The caller-provided state for this operation
272272
/// </summary>
273273
public object State { get; }
274+
275+
/// <summary>
276+
/// The service binder to use.
277+
/// </summary>
278+
public ServiceBinder ServiceBinder { get; }
279+
274280
/// <summary>
275281
/// The service contract interface type
276282
/// </summary>
277283
public Type ContractType { get; }
284+
278285
/// <summary>
279286
/// The concrete service type
280287
/// </summary>
281288
public Type ServiceType { get; }
282289

283-
private InterfaceMapping? _map;
284-
private InterfaceMapping GetMap() // lazily memoized
285-
=> _map ??= ServiceType.GetInterfaceMap(ContractType);
286-
internal ServiceBindContext(Type contractType, Type serviceType, object state)
290+
internal ServiceBindContext(Type contractType, Type serviceType, object state, ServiceBinder serviceBinder)
287291
{
288292
State = state;
293+
ServiceBinder = serviceBinder;
289294
ContractType = contractType;
290295
ServiceType = serviceType;
291296
}
292297

293298
/// <summary>
294-
/// Gets the implementing method from a method definition
295-
/// </summary>
296-
public MethodInfo? GetImplementation(MethodInfo serviceMethod)
297-
{
298-
if (ContractType != ServiceType & serviceMethod is object)
299-
{
300-
var map = GetMap();
301-
var from = map.InterfaceMethods;
302-
var to = map.TargetMethods;
303-
int end = Math.Min(from.Length, to.Length);
304-
for (int i = 0; i < end; i++)
305-
{
306-
if (from[i] == serviceMethod) return to[i];
307-
}
308-
}
309-
return null;
310-
}
311-
312-
/// <summary>
313-
/// Gets the metadata associated with a specific contract method
299+
/// <para>Gets the metadata associated with a specific contract method.</para>
300+
/// <para>Note: Later is higher priority in the code that consumes this.</para>
314301
/// </summary>
315-
public List<object> GetMetadata(MethodInfo method)
316-
{
317-
// consider the various possible sources of distinct metadata
318-
object[]
319-
contractType = ContractType.GetCustomAttributes(inherit: true),
320-
contractMethod = method.GetCustomAttributes(inherit: true),
321-
serviceType = Array.Empty<object>(),
322-
serviceMethod = Array.Empty<object>();
323-
if (ContractType != ServiceType & ContractType.IsInterface & ServiceType.IsClass)
324-
{
325-
serviceType = ServiceType.GetCustomAttributes(inherit: true);
326-
serviceMethod = GetImplementation(method)?.GetCustomAttributes(inherit: true)
327-
?? Array.Empty<object>();
328-
}
329-
330-
// note: later is higher priority in the code that consumes this, but
331-
// GetAttributes() is "most derived to least derived", so: add everything
332-
// backwards, then reverse
333-
var metadata = new List<object>(
334-
contractType.Length + contractMethod.Length +
335-
serviceType.Length + serviceMethod.Length);
336-
337-
metadata.AddRange(serviceMethod);
338-
metadata.AddRange(serviceType);
339-
metadata.AddRange(contractMethod);
340-
metadata.AddRange(contractType);
341-
metadata.Reverse();
342-
return metadata;
343-
}
302+
/// <returns>Prioritised list of metadata.</returns>
303+
public IList<object> GetMetadata(MethodInfo method)
304+
=> ServiceBinder.GetMetadata(method, ContractType, ServiceType);
344305
}
345306
}
346-
347307
}

0 commit comments

Comments
 (0)