diff --git a/README.md b/README.md index 2e6e5e455..89dc13dce 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ Table of contents - [Project Structure](#project-structure) - [Hosting](#hosting) - [DI](#di) +- [Options](#options) - [Swagger](#swagger) - [Unity Supports](#unity-supports) - [Pack to Docker and deploy](#pack-to-docker-and-deploy) @@ -331,9 +332,60 @@ Here is example of what kind of filter can be stacked. GlobalFilter can attach to MagicOnionOptions. +MagicOnion filters supports [DI](#DI) by [MagicOnion.Hosting](#Hosting). + +```csharp +public class MyStreamingHubFilterAttribute : StreamingHubFilterAttribute +{ + private readonly ILogger _logger; + + // the `logger` parameter will be injected at instantiating. + public MyStreamingHubFilterAttribute(ILogger logger) + { + _logger = logger; + } + + public override async ValueTask Invoke(StreamingHubContext context, Func next) + { + _logger.LogInformation($"MyStreamingHubFilter Begin: {context.Path}"); + await next(context); + _logger.LogInformation($"MyStreamingHubFilter End: {context.Path}"); + } +} +``` + +Register filters using attributes with constructor injection(you can use `[FromTypeFilter]` and `[FromServiceFilter]`). + +``` +[FromTypeFilter(typeof(MyFilterAttribute))] +public class MyService : ServiceBase, IMyService +{ + // The filter will instantiate from type. + [FromTypeFilter(typeof(MySecondFilterAttribute))] + public UnaryResult Foo() + { + return UnaryResult(0); + } + + // The filter will instantiate from type with some arguments. if the arguments are missing, it will be obtained from `IServiceLocator` + [FromTypeFilter(typeof(MyThirdFilterAttribute), Arguments = new object[] { "foo", 987654 })] + public UnaryResult Bar() + { + return UnaryResult(0); + } + + // The filter instance will be provided via `IServiceLocator`. + [FromServiceFilter(typeof(MyFourthFilterAttribute))] + public UnaryResult Baz() + { + return UnaryResult(0); + } +} +``` + ClientFilter --- -MagicOnion client-filter is powerful feature to hook before-after invoke. It is useful than gRPC client interceptor. +MagicOnion client-filter is a powerful feature to hook before-after invoke. It is useful than gRPC client interceptor. > Currently only suppots on Unary. @@ -473,6 +525,17 @@ public class RetryFilter : IClientFilter throw new Exception("Retry failed", lastException); } } + +public class EncryptFilter : IClientFilter +{ + public async ValueTask SendAsync(RequestContext context, Func> next) + { + context.SetRequestMutator(bytes => Encrypt(bytes)); + context.SetResponseMutator(bytes => Decrypt(bytes)); + + return await next(context); + } +} ``` ServiceContext and Lifecycle @@ -766,6 +829,132 @@ public class MyFirstService : ServiceBase, IMyFirstService } ``` +Options +--- +Configure MagicOnion hosting using `Microsoft.Extensions.Options` that align to .NET Core way. In many real use cases, Using setting files (ex. appsettings.json), environment variables, etc ... to configure an application. + +For example, We have Production and Development configurations and have some differences about listening ports, certificates and others. + +This example makes hosting MagicOnion easier and configurations moved to external files, environment variables. appsettings.json looks like below. + +```csharp +{ + "MagicOnion": { + "Service": { + "IsReturnExceptionStackTraceInErrorDetail": false + }, + "ChannelOptions": { + "grpc.primary_user_agent": "MagicOnion/1.0 (Development)", + "grpc.max_receive_message_length": 4194304 + }, + "ServerPorts": [ + { + "Host": "localhost", + "Port": 12345, + "UseInsecureConnection": false, + "ServerCredentials": [ + { + "CertificatePath": "./server.crt", + "KeyPath": "./server.key" + } + ] + } + ] + }} +``` + +An application setting files is not required by default. You can simply call UseMagicOnion() then it starts service on localhost:12345 (Insecure connection). + +```csharp +class Program +{ + static async Task Main(string[] args) + { + await MagicOnionHost.CreateDefaultBuilder() + .UseMagicOnion() + .RunConsoleAsync(); + } +} +``` + +Of course, you can also flexibly configure hosting by code. During configuration, you can access `IHostingEnvironment` / `IConfiguration` instances and configure +`MagicOnionOptions`. + +```csharp +class Program +{ + static async Task Main(string[] args) + { + await MagicOnionHost.CreateDefaultBuilder() + .UseMagicOnion() + .ConfigureServices((hostContext, services) => + { + services.Configure(options => + { + if (hostContext.HostingEnvironment.IsDevelopment()) + { + options.Service.GlobalFilters = new[] { new MyFilterAttribute(null) }; + } + options.ChannelOptions.MaxReceiveMessageLength = 1024 * 1024 * 10; + }); + }) + .RunConsoleAsync(); + } +} +``` + +This configuration method supports multiple MagicOnion hosting scenarios. + +```json +{ + "MagicOnion": { + "ServerPorts": [ + { + "Host": "localhost", + "Port": 12345, + "UseInsecureConnection": true + } + ] + }, + "MagicOnion-Management": { + "ServerPorts": [ + { + "Host": "localhost", + "Port": 23456, + "UseInsecureConnection": true + } + ] + } +} +``` + +```csharp +class Program +{ + static async Task Main(string[] args) + { + await MagicOnionHost.CreateDefaultBuilder() + .UseMagicOnion(types: new[] { typeof(MyService) }) + .UseMagicOnion(configurationName: "MagicOnion-Management", types: new[] { typeof(ManagementService) }) + .ConfigureServices((hostContext, services) => + { + services.Configure(options => + { + options.ChannelOptions.MaxReceiveMessageLength = 1024 * 1024 * 10; + }); + services.Configure("MagicOnion-Management", options => + { + if (hostContext.HostingEnvironment.IsDevelopment()) + { + options.Service.GlobalFilters = new[] { new MyFilterAttribute(null) }; + } + }); + }) + .RunConsoleAsync(); + } +} +``` + Swagger --- MagicOnion has built-in Http1 JSON Gateway and [Swagger](http://swagger.io/) integration for Unary operation. It can execute and debug RPC-API easily. diff --git a/sandbox/Sandbox.Hosting/Program.cs b/sandbox/Sandbox.Hosting/Program.cs index 130368bf4..447799a95 100644 --- a/sandbox/Sandbox.Hosting/Program.cs +++ b/sandbox/Sandbox.Hosting/Program.cs @@ -35,6 +35,9 @@ static async Task Main(string[] args) options.Service.GlobalFilters.Add(); // options.Service.GlobalFilters.Add(new MyFilterAttribute(logger)); + + // options.ServerPorts = new[]{ new MagicOnionHostingServerPortOptions(){ Port = opti + } options.ChannelOptions.MaxReceiveMessageLength = 1024 * 1024 * 10; options.ChannelOptions.Add(new ChannelOption("grpc.keepalive_time_ms", 10000)); diff --git a/src/MagicOnion/Client/ClientFilter.cs b/src/MagicOnion/Client/ClientFilter.cs index 18bd27dbb..76d0bfd5e 100644 --- a/src/MagicOnion/Client/ClientFilter.cs +++ b/src/MagicOnion/Client/ClientFilter.cs @@ -21,6 +21,7 @@ public abstract class RequestContext public Type ResponseType { get; } public abstract Type RequestType { get; } public Func RequestMutator { get; private set; } + public Func ResponseMutator { get; private set; } Dictionary items; public IDictionary Items @@ -49,12 +50,18 @@ internal RequestContext(MagicOnionClientBase client, string methodPath, CallOpti this.Filters = filters; this.RequestMethod = requestMethod; this.RequestMutator = DefaultMutator; + this.ResponseMutator = DefaultMutator; } public void SetRequestMutator(Func mutator) { this.RequestMutator = mutator; } + + public void SetResponseMutator(Func mutator) + { + this.ResponseMutator = mutator; + } } public class RequestContext : RequestContext @@ -100,7 +107,7 @@ public ResponseContext() this.ResponseMutator = DefaultMutator; } - public void SetRequestMutator(Func mutator) + public void SetResponseMutator(Func mutator) { this.ResponseMutator = mutator; } diff --git a/src/MagicOnion/Client/MagicOnionClientBase.cs b/src/MagicOnion/Client/MagicOnionClientBase.cs index 591de64b0..567eb3ffa 100644 --- a/src/MagicOnion/Client/MagicOnionClientBase.cs +++ b/src/MagicOnion/Client/MagicOnionClientBase.cs @@ -18,7 +18,9 @@ static protected ResponseContext CreateResponseContext(RequestContext { var self = context.Client; var callResult = self.callInvoker.AsyncUnaryCall(method, self.host, context.CallOptions, context.RequestMutator(MagicOnionMarshallers.UnsafeNilBytes)); - return new ResponseContext(callResult, self.resolver); + var response = new ResponseContext(callResult, self.resolver); + response.SetResponseMutator(context.ResponseMutator); + return response; } static protected ResponseContext CreateResponseContext(RequestContext context, Method method) @@ -26,7 +28,9 @@ static protected ResponseContext CreateResponseContext(Requ var self = context.Client; var message = LZ4MessagePackSerializer.Serialize(((RequestContext)context).Request, self.resolver); var callResult = self.callInvoker.AsyncUnaryCall(method, self.host, context.CallOptions, context.RequestMutator(message)); - return new ResponseContext(callResult, self.resolver); + var response = new ResponseContext(callResult, self.resolver); + response.SetResponseMutator(context.ResponseMutator); + return response; } }