From 029358a843b70504b5d0d5b69a6593f4c4b4bca2 Mon Sep 17 00:00:00 2001 From: Jose Date: Fri, 1 Dec 2023 11:04:05 +0100 Subject: [PATCH] Add IProtobufClient abstraction --- .../protobuf/GenericHost/Client/Client.csproj | 34 +++++++++ .../GenericHost/Client/ClientHostedService.cs | 48 +++++++++++++ .../protobuf/GenericHost/Client/Program.cs | 72 +++++++++++++++++++ .../GenericHost/Client/appsettings.json | 15 ++++ examples/protobuf/GenericHost/GenericHost.sln | 36 ++++++++++ examples/protobuf/GenericHost/README.md | 23 ++++++ .../protobuf/GenericHost/Server/Chatbot.cs | 21 ++++++ .../protobuf/GenericHost/Server/Program.cs | 63 ++++++++++++++++ .../protobuf/GenericHost/Server/Server.csproj | 34 +++++++++ .../GenericHost/Server/ServerHostedService.cs | 26 +++++++ .../GenericHost/Server/appsettings.json | 19 +++++ .../protobuf/GenericHost/proto/greeter.proto | 22 ++++++ src/IceRpc.Protobuf/IProtobufClient.cs | 17 +++++ .../ProtobufServiceProviderExtensions.cs | 54 ++++++++++++++ .../IceRpc-Protobuf-DI-Client/Program.cs | 4 +- tools/IceRpc.ProtocGen/ClientGenerator.cs | 4 +- 16 files changed, 489 insertions(+), 3 deletions(-) create mode 100644 examples/protobuf/GenericHost/Client/Client.csproj create mode 100644 examples/protobuf/GenericHost/Client/ClientHostedService.cs create mode 100644 examples/protobuf/GenericHost/Client/Program.cs create mode 100644 examples/protobuf/GenericHost/Client/appsettings.json create mode 100644 examples/protobuf/GenericHost/GenericHost.sln create mode 100644 examples/protobuf/GenericHost/README.md create mode 100644 examples/protobuf/GenericHost/Server/Chatbot.cs create mode 100644 examples/protobuf/GenericHost/Server/Program.cs create mode 100644 examples/protobuf/GenericHost/Server/Server.csproj create mode 100644 examples/protobuf/GenericHost/Server/ServerHostedService.cs create mode 100644 examples/protobuf/GenericHost/Server/appsettings.json create mode 100644 examples/protobuf/GenericHost/proto/greeter.proto create mode 100644 src/IceRpc.Protobuf/IProtobufClient.cs create mode 100644 src/IceRpc.Protobuf/ProtobufServiceProviderExtensions.cs diff --git a/examples/protobuf/GenericHost/Client/Client.csproj b/examples/protobuf/GenericHost/Client/Client.csproj new file mode 100644 index 0000000000..f2dfd31d84 --- /dev/null +++ b/examples/protobuf/GenericHost/Client/Client.csproj @@ -0,0 +1,34 @@ + + + + Exe + net8.0 + enable + enable + + true + + + + + + + PreserveNewest + + + + + PreserveNewest + + + + + + + + + + + + + diff --git a/examples/protobuf/GenericHost/Client/ClientHostedService.cs b/examples/protobuf/GenericHost/Client/ClientHostedService.cs new file mode 100644 index 0000000000..5cad659d07 --- /dev/null +++ b/examples/protobuf/GenericHost/Client/ClientHostedService.cs @@ -0,0 +1,48 @@ +// Copyright (c) ZeroC, Inc. + +using IceRpc; +using Microsoft.Extensions.Hosting; +using VisitorCenter; + +namespace GenericHostClient; + +/// The hosted client service is ran and managed by the .NET Generic Host. +public class ClientHostedService : BackgroundService +{ + // The host application lifetime is used to stop the .NET Generic Host. + private readonly IHostApplicationLifetime _applicationLifetime; + + private readonly ClientConnection _connection; + + // The IGreeter managed by the DI container. + private readonly IGreeter _greeter; + + // All the parameters are injected by the DI container. + public ClientHostedService( + IGreeter greeter, + ClientConnection connection, + IHostApplicationLifetime applicationLifetime) + { + _applicationLifetime = applicationLifetime; + _connection = connection; + _greeter = greeter; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + try + { + var request = new GreetRequest { Name = Environment.UserName }; + GreetResponse response = await _greeter.GreetAsync(request, cancellationToken: stoppingToken); + Console.WriteLine(response.Greeting); + await _connection.ShutdownAsync(stoppingToken); + } + catch (Exception exception) + { + Console.WriteLine($"Failed to connect to the server:\n{exception}"); + } + + // Stop the generic host once the invocation is done. + _applicationLifetime.StopApplication(); + } +} diff --git a/examples/protobuf/GenericHost/Client/Program.cs b/examples/protobuf/GenericHost/Client/Program.cs new file mode 100644 index 0000000000..88e8e072ce --- /dev/null +++ b/examples/protobuf/GenericHost/Client/Program.cs @@ -0,0 +1,72 @@ +// Copyright (c) ZeroC, Inc. + +using GenericHostClient; +using IceRpc; +using IceRpc.Extensions.DependencyInjection; +using IceRpc.Protobuf; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System.Diagnostics; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using VisitorCenter; + +// Configure the host. +IHostBuilder hostBuilder = Host.CreateDefaultBuilder(args) + // Set the content root path to the build directory of the client (e.g.: Client/bin/Debug/net8.0) + .UseContentRoot(AppContext.BaseDirectory) + + // Configures the .NET Generic Host services. + .ConfigureServices((hostContext, services) => + { + // Add the ClientHostedService to the hosted services of the .NET Generic Host. + services.AddHostedService(); + + // Bind the client connection options to the "appsettings.json" configuration "Client" section, + // and add a Configure callback to configure its authentication options. + services + .AddOptions() + .Bind(hostContext.Configuration.GetSection("Client")) + .Configure(options => + { + // Configure the authentication options + var rootCA = new X509Certificate2( + Path.Combine( + hostContext.HostingEnvironment.ContentRootPath, + hostContext.Configuration.GetValue("CertificateAuthoritiesFile")!)); + + options.ClientAuthenticationOptions = new SslClientAuthenticationOptions + { + // A certificate validation callback that uses the configured certificate authorities file to + // validate the peer certificates. + RemoteCertificateValidationCallback = (sender, certificate, chain, errors) => + { + using var customChain = new X509Chain(); + customChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + customChain.ChainPolicy.DisableCertificateDownloads = true; + customChain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + customChain.ChainPolicy.CustomTrustStore.Add(rootCA); + return customChain.Build((X509Certificate2)certificate!); + } + }; + }); + + services + // The activity source used by the telemetry interceptor. + .AddSingleton(_ => new ActivitySource("IceRpc")) + // Add a ClientConnection singleton. This ClientConnections uses the ClientConnectionOptions provided by the + // the IOptions configured/bound above. + .AddIceRpcClientConnection() + // Add an invoker singleton; this invoker corresponds to the invocation pipeline. This invocation pipeline + // flows into the ClientConnection singleton. + .AddIceRpcInvoker(builder => builder.UseTelemetry().UseLogger().Into()) + // Add an IGreeter singleton that uses the invoker singleton registered above. + .AddSingleton(provider => provider.CreateProtobufClient()); + }); + +// Build the host. +using IHost host = hostBuilder.Build(); + +// Run hosted program. +host.Run(); diff --git a/examples/protobuf/GenericHost/Client/appsettings.json b/examples/protobuf/GenericHost/Client/appsettings.json new file mode 100644 index 0000000000..4af96b2366 --- /dev/null +++ b/examples/protobuf/GenericHost/Client/appsettings.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "Console": { + "IncludeScopes": true, + "LogLevel": { + "Default": "Trace" + } + } + }, + "Client": { + "ServerAddress": "icerpc://localhost:10000", + "ConnectTimeout": "00:00:05" + }, + "CertificateAuthoritiesFile": "cacert.der" +} diff --git a/examples/protobuf/GenericHost/GenericHost.sln b/examples/protobuf/GenericHost/GenericHost.sln new file mode 100644 index 0000000000..92351a5ee0 --- /dev/null +++ b/examples/protobuf/GenericHost/GenericHost.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33122.133 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Client", "Client\Client.csproj", "{BBF1199A-46A4-4AE9-AFFE-4D8DD59EB874}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Server", "Server\Server.csproj", "{527EEA4D-77B9-4252-A2CD-C641A25CAD53}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F3B101B0-217D-4B27-9CA7-DA9CFDB2C47A}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BBF1199A-46A4-4AE9-AFFE-4D8DD59EB874}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BBF1199A-46A4-4AE9-AFFE-4D8DD59EB874}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BBF1199A-46A4-4AE9-AFFE-4D8DD59EB874}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BBF1199A-46A4-4AE9-AFFE-4D8DD59EB874}.Release|Any CPU.Build.0 = Release|Any CPU + {527EEA4D-77B9-4252-A2CD-C641A25CAD53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {527EEA4D-77B9-4252-A2CD-C641A25CAD53}.Debug|Any CPU.Build.0 = Debug|Any CPU + {527EEA4D-77B9-4252-A2CD-C641A25CAD53}.Release|Any CPU.ActiveCfg = Release|Any CPU + {527EEA4D-77B9-4252-A2CD-C641A25CAD53}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0B36F1E1-0592-4A15-9981-67BC4A653EC4} + EndGlobalSection +EndGlobal diff --git a/examples/protobuf/GenericHost/README.md b/examples/protobuf/GenericHost/README.md new file mode 100644 index 0000000000..e43ef77c7f --- /dev/null +++ b/examples/protobuf/GenericHost/README.md @@ -0,0 +1,23 @@ +# GenericHost + +This example shows how to use dependency injection and the .NET Generic Host with IceRPC client and server applications. + +You can build the client and server applications with: + +``` shell +dotnet build +``` + +First start the Server program: + +```shell +cd Server +dotnet run +``` + +In a separate window, start the Client program: + +```shell +cd Client +dotnet run +``` diff --git a/examples/protobuf/GenericHost/Server/Chatbot.cs b/examples/protobuf/GenericHost/Server/Chatbot.cs new file mode 100644 index 0000000000..a9a27c7d37 --- /dev/null +++ b/examples/protobuf/GenericHost/Server/Chatbot.cs @@ -0,0 +1,21 @@ +// Copyright (c) ZeroC, Inc. + +using IceRpc.Features; +using IceRpc.Protobuf; +using VisitorCenter; + +namespace GenericHostServer; + +/// A Chatbot is an IceRPC service that implements Protobuf service 'Greeter'. +[ProtobufService] +public partial class Chatbot : IGreeterService +{ + public ValueTask GreetAsync( + GreetRequest message, + IFeatureCollection features, + CancellationToken cancellationToken) + { + Console.WriteLine($"Dispatching Greet request {{ name = '{message.Name}' }}"); + return new(new GreetResponse { Greeting = $"Hello, {message.Name}!" }); + } +} diff --git a/examples/protobuf/GenericHost/Server/Program.cs b/examples/protobuf/GenericHost/Server/Program.cs new file mode 100644 index 0000000000..58c49d67e2 --- /dev/null +++ b/examples/protobuf/GenericHost/Server/Program.cs @@ -0,0 +1,63 @@ +// Copyright (c) ZeroC, Inc. + +using GenericHostServer; +using IceRpc; +using IceRpc.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System.Diagnostics; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using VisitorCenter; + +// Configure the host. +IHostBuilder hostBuilder = Host.CreateDefaultBuilder(args) + // Set the content root path to the build directory of the server (e.g.: Server/bin/Debug/net8.0) + .UseContentRoot(AppContext.BaseDirectory) + + // Configure the .NET Generic Host services. + .ConfigureServices((hostContext, services) => + { + // Add the ServerHostedService to the hosted services of the .NET Generic Host. + services.AddHostedService(); + + // The activity source used by the telemetry interceptor. + services.AddSingleton(sp => new ActivitySource("IceRpc")); + + string workingDirectory = Directory.GetCurrentDirectory(); + Console.WriteLine(workingDirectory); + + // Bind the server options to the "appsettings.json" configuration "Server" section, and add a Configure + // callback to configure its authentication options. + services + .AddOptions() + .Bind(hostContext.Configuration.GetSection("Server")) + .Configure(options => + { + options.ServerAuthenticationOptions = new SslServerAuthenticationOptions + { + ServerCertificate = new X509Certificate2( + Path.Combine( + hostContext.HostingEnvironment.ContentRootPath, + hostContext.Configuration.GetValue("Certificate:File")!)) + }; + }); + + // Add the Slice service that implements Slice interface `Greeter`, as a singleton. + services.AddSingleton(); + + // Add a server and configure the dispatcher using a dispatcher builder. The server uses the ServerOptions + // provided by the IOptions singleton configured/bound above. + services.AddIceRpcServer( + builder => builder + .UseTelemetry() + .UseLogger() + .Map()); + }); + +// Build the host. +using IHost host = hostBuilder.Build(); + +// Run hosted program. +host.Run(); diff --git a/examples/protobuf/GenericHost/Server/Server.csproj b/examples/protobuf/GenericHost/Server/Server.csproj new file mode 100644 index 0000000000..961360e1c6 --- /dev/null +++ b/examples/protobuf/GenericHost/Server/Server.csproj @@ -0,0 +1,34 @@ + + + + Exe + net8.0 + enable + enable + + true + + + + + + + PreserveNewest + + + + + PreserveNewest + + + + + + + + + + + + + diff --git a/examples/protobuf/GenericHost/Server/ServerHostedService.cs b/examples/protobuf/GenericHost/Server/ServerHostedService.cs new file mode 100644 index 0000000000..d9a2791bd2 --- /dev/null +++ b/examples/protobuf/GenericHost/Server/ServerHostedService.cs @@ -0,0 +1,26 @@ +// Copyright (c) ZeroC, Inc. + +using IceRpc; +using Microsoft.Extensions.Hosting; + +namespace GenericHostServer; + +/// The server hosted service is ran and managed by the .NET Generic Host +public class ServerHostedService : IHostedService +{ + // The IceRPC server accepts connections from IceRPC clients. + private readonly Server _server; + + public ServerHostedService(Server server) => _server = server; + + public Task StartAsync(CancellationToken cancellationToken) + { + // Start listening for client connections. + _server.Listen(); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) => + // Shuts down the IceRPC server when the hosted service is stopped. + _server.ShutdownAsync(cancellationToken); +} diff --git a/examples/protobuf/GenericHost/Server/appsettings.json b/examples/protobuf/GenericHost/Server/appsettings.json new file mode 100644 index 0000000000..795c9af11a --- /dev/null +++ b/examples/protobuf/GenericHost/Server/appsettings.json @@ -0,0 +1,19 @@ +{ + "Logging": { + "Console": { + "IncludeScopes": true, + "LogLevel": { + "Default": "Trace" + } + } + }, + "Server": { + "ServerAddress": "icerpc://[::0]:10000", + "ConnectionOptions": { + "CloseTimeout": "00:00:05" + } + }, + "Certificate": { + "File": "server.p12" + } +} diff --git a/examples/protobuf/GenericHost/proto/greeter.proto b/examples/protobuf/GenericHost/proto/greeter.proto new file mode 100644 index 0000000000..4997cfbe63 --- /dev/null +++ b/examples/protobuf/GenericHost/proto/greeter.proto @@ -0,0 +1,22 @@ +// Copyright (c) ZeroC, Inc. + +syntax = "proto3"; + +package visitor_center; +option csharp_namespace = "VisitorCenter"; + +// Represents a simple greeter. +service Greeter { + // Creates a personalized greeting. + rpc Greet (GreetRequest) returns (GreetResponse); +} + +// The request contains the name of the person to greet. +message GreetRequest { + string name = 1; +} + +// The response contains the greeting. +message GreetResponse { + string greeting = 1; +} diff --git a/src/IceRpc.Protobuf/IProtobufClient.cs b/src/IceRpc.Protobuf/IProtobufClient.cs new file mode 100644 index 0000000000..f4d738a42c --- /dev/null +++ b/src/IceRpc.Protobuf/IProtobufClient.cs @@ -0,0 +1,17 @@ +// Copyright (c) ZeroC, Inc. + +namespace IceRpc.Protobuf; + +/// Represents a local ambassador for a remote service. +public interface IProtobufClient +{ + /// Gets or initializes the encode options, used to customize the encoding of payloads created from this + /// client. + ProtobufEncodeOptions? EncodeOptions { get; init; } + + /// Gets or initializes the invocation pipeline of this client. + IInvoker Invoker { get; init; } + + /// Gets or initializes the address of the remote service. + ServiceAddress ServiceAddress { get; init; } +} diff --git a/src/IceRpc.Protobuf/ProtobufServiceProviderExtensions.cs b/src/IceRpc.Protobuf/ProtobufServiceProviderExtensions.cs new file mode 100644 index 0000000000..371ed47303 --- /dev/null +++ b/src/IceRpc.Protobuf/ProtobufServiceProviderExtensions.cs @@ -0,0 +1,54 @@ +// Copyright (c) ZeroC, Inc. + +namespace IceRpc.Protobuf; + +/// Provides extension methods for to create Protobuf clients. +public static class ProtobufServiceProviderExtensions +{ + /// Creates a Protobuf client with this service provider. + /// The Protobuf client struct. + /// The service provider. + /// The service address of the new client; null is equivalent to the default service + /// address for the client type. + /// A new instance of . + /// The new client uses the retrieved from as its + /// invocation pipeline, and the retrieved from as + /// its encode options. + public static TClient CreateProtobufClient( + this IServiceProvider provider, + ServiceAddress? serviceAddress = null) + where TClient : struct, IProtobufClient + { + var invoker = (IInvoker?)provider.GetService(typeof(IInvoker)); + if (invoker is null) + { + throw new InvalidOperationException("Could not find service of type 'IInvoker' in the service container."); + } + + return serviceAddress is null ? + new TClient + { + EncodeOptions = (ProtobufEncodeOptions?)provider.GetService(typeof(ProtobufEncodeOptions)), + Invoker = invoker + } + : + new TClient + { + EncodeOptions = (ProtobufEncodeOptions?)provider.GetService(typeof(ProtobufEncodeOptions)), + Invoker = invoker, + ServiceAddress = serviceAddress + }; + } + + /// Creates a Protobuf client with this service provider. + /// The Protobuf client struct. + /// The service provider. + /// The service address of the client as a URI. + /// A new instance of . + /// The new client uses the retrieved from as its + /// invocation pipeline, and the retrieved from as + /// its encode options. + public static TClient CreateProtobufClient(this IServiceProvider provider, Uri serviceAddressUri) + where TClient : struct, IProtobufClient => + provider.CreateProtobufClient(new ServiceAddress(serviceAddressUri)); +} diff --git a/src/IceRpc.Templates/Templates/IceRpc-Protobuf-DI-Client/Program.cs b/src/IceRpc.Templates/Templates/IceRpc-Protobuf-DI-Client/Program.cs index cf81aee4ff..1daf9c2ae6 100644 --- a/src/IceRpc.Templates/Templates/IceRpc-Protobuf-DI-Client/Program.cs +++ b/src/IceRpc.Templates/Templates/IceRpc-Protobuf-DI-Client/Program.cs @@ -33,7 +33,9 @@ builder => builder .UseDeadline(hostContext.Configuration.GetValue("Deadline:DefaultTimeout")) .UseLogger() - .Into()); + .Into()) + // Add an IGreeter singleton that uses the invoker singleton registered above. + .AddSingleton(provider => provider.CreateProtobufClient()); }); // Build the host. diff --git a/tools/IceRpc.ProtocGen/ClientGenerator.cs b/tools/IceRpc.ProtocGen/ClientGenerator.cs index 23d2ad4421..18c2483299 100644 --- a/tools/IceRpc.ProtocGen/ClientGenerator.cs +++ b/tools/IceRpc.ProtocGen/ClientGenerator.cs @@ -107,7 +107,7 @@ internal static string GenerateImplementation(ServiceDescriptor service) [global::System.Obsolete]"; } clientImplementation += @$" -public readonly partial record struct {clientImplementationName} : I{service.Name.ToPascalCase()} +public readonly partial record struct {clientImplementationName} : I{service.Name.ToPascalCase()}, IProtobufClient {{ /// Represents the default path for IceRPC services that implement Protobuf service /// {service.FullName}. @@ -115,7 +115,7 @@ internal static string GenerateImplementation(ServiceDescriptor service) /// Gets or initializes the encode options, used to customize the encoding of payloads created from this /// client. - ProtobufEncodeOptions? EncodeOptions {{ get; init; }} + public ProtobufEncodeOptions? EncodeOptions {{ get; init; }} = null; /// Gets or initializes the invoker of this client. public IceRpc.IInvoker Invoker {{ get; init; }}