应用protobuf-net.Grpc将WCF服务迁徙到gRPC非常简单。在这篇博文中,咱们将看看它到底有多简略。微软对于将WCF服务迁徙到gRPC的官网指南只提到了Gooogle.Protobuf形式,如果你有很多数据契约须要迁徙到.proto格局,这可能会很耗时。然而,通过应用protobuf-net.Grpc咱们可能重用旧的WCF数据契约和服务契约,而只须要做最小的代码更改。
迁徙数据契约和服务契约
在本节中,咱们将应用一个简略的申请响应的组合服务,它能够让你下载给定交易者的单个投资组合或所有投资组合。服务和数据契约的定义如下:
[ServiceContract]public interface IPortfolioService{ [OperationContract] Task<Portfolio> Get(Guid traderId, int portfolioId); [OperationContract] Task<List<Portfolio>> GetAll(Guid traderId);}[DataContract]public class Portfolio{ [DataMember] public int Id { get; set; } [DataMember] public Guid TraderId { get; set; } [DataMember] public List<PortfolioItem> Items { get; set; }}[DataContract]public class PortfolioItem{ [DataMember] public int Id { get; set; } [DataMember] public int ShareId { get; set; } [DataMember] public int Holding { get; set; } [DataMember] public decimal Cost { get; set; }}
在将数据契约和服务契约迁徙到gRPC之前,我倡议为契约创立一个新的类库。这些契约能够通过我的项目援用或包援用在服务器和客户端之间很容易地共享,这取决于你的WCF解决方案的构造。一旦咱们创立了类库,咱们将源文件进行复制并开始迁徙到gRPC。
不像应用Google.Protobuf迁徙到gRPC那样,数据契约只须要最小的更改。咱们须要做的惟一一件事是在DataMember属性中定义Order属性。这相当于在创立.proto格局的音讯时定义字段号。这些字段号用于标识音讯二进制格局中的字段,并且在应用音讯后不应更改。
[DataContract]public class Portfolio{ [DataMember(Order = 1)] public int Id { get; set; } [DataMember(Order = 2)] public Guid TraderId { get; set; } [DataMember(Order = 3)] public List<PortfolioItem> Items { get; set; }}[DataContract]public class PortfolioItem{ [DataMember(Order = 1)] public int Id { get; set; } [DataMember(Order = 2)] public int ShareId { get; set; } [DataMember(Order = 3)] public int Holding { get; set; } [DataMember(Order = 4)] public decimal Cost { get; set; }}
因为gRPC和WCF之间的差别,服务契约将须要更多的批改。gRPC服务中的RPC办法必须只定义一种音讯类型作为申请参数,并且只返回一条音讯。咱们不能承受标量类型(即根本类型)作为申请参数,也不能返回标量类型。咱们须要将所有原始参数合并到一个音讯中(即DataContract)。这也解释了Guid参数类型,因为它可能被序列化为字符串,这取决于你如何配置protobuf-net。咱们也不能承受音讯列表(或标量)或返回音讯列表(或标量)。记住这些规定后,咱们须要批改咱们的服务契约,使其看起来像上面这样:
[ServiceContract]public interface IPortfolioService{ [OperationContract] Task<Portfolio> Get(GetPortfolioRequest request); [OperationContract] Task<PortfolioCollection> GetAll(GetAllPortfoliosRequest request);}
服务契约中的上述更改迫使咱们创立一些额定的数据契约。因而,咱们创立如下:
[DataContract]public class GetPortfolioRequest{ [DataMember(Order = 1)] public Guid TraderId { get; set; } [DataMember(Order = 2)] public int PortfolioId { get; set; }}[DataContract]public class GetAllPortfoliosRequest{ [DataMember(Order = 1)] public Guid TraderId { get; set; }}[DataContract]public class PortfolioCollection{ [DataMember(Order = 1)] public List<Portfolio> Items { get; set; }}
基本上是这样。当初咱们曾经将咱们的WCF服务契约和数据契约迁徙到gRPC。下一步是将数据层迁徙到.net Core。
将PortfolioData库迁徙到.net Core
接下来,咱们将把PortfolioData库迁徙到.net Core,就像微软指南中形容的那样。然而,咱们不须要复制模型(Portfolio.cs和PortfolioItem.cs),因为它们曾经在咱们在上一节中创立的类库中定义了。相同,咱们将向该共享库增加一个我的项目援用。下一步是将WCF服务迁徙到ASP.Net Core应用程序。
将WCF服务迁徙到ASP.Net Core应用程序
咱们须要做的第一件事是创立一个ASP.Net Core应用程序。因而,要么启动你最喜爱的IDE,创立一个根本的ASP.NET Core application或从命令行运行dotnet new web。接下来,咱们须要增加一个对protobuf-net.Grpc的包。应用你最喜爱的包管理器装置它,或者简略地运行dotnet add package protobuf-net.Grpc.AspNetCore。咱们还须要向上一节中创立的PortfolioData库增加一个我的项目援用。
当初咱们曾经筹备好了我的项目,并且增加了所有的依赖项,咱们能够持续并创立portfolio服务。创立一个具备以下内容的新类。
public class PortfolioService : IPortfolioService{ private readonly IPortfolioRepository _repository; public PortfolioService(IPortfolioRepository repository) { _repository = repository; } public async Task<Portfolio> Get(GetPortfolioRequest request) { var portfolio = await _repository.GetAsync(request.TraderId, request.PortfolioId); return portfolio; } public async Task<PortfolioCollection> GetAll(GetAllPortfoliosRequest request) { var portfolios = await _repository.GetAllAsync(request.TraderId); var response = new PortfolioCollection { Items = portfolios }; return response; }}
下面的服务看起来与WCF服务实现十分类似,除了输出参数类型和返回参数类型之外。
最初但并非最不重要的,咱们须要将protobuf-net.Grpc接入ASP.Net Core管道,并在DI容器中注册。在启Startup.cs,咱们将做以下补充:
public class Startup{ public void ConfigureServices(IServiceCollection services) { services.AddScoped<IPortfolioRepository, PortfolioRepository>(); services.AddCodeFirstGrpc(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapGrpcService<PortfolioService>(); }); }}
当初咱们曾经有了gRPC服务。咱们列表上的下一件事是创立客户端应用程序。
创立gRPC客户端应用程序
对于咱们的客户端应用程序,咱们将持续创立一个控制台应用程序。要么应用你最喜爱的IDE创立一个控制台,要么间接从命令行运行dotnet new console。接下来,咱们须要增加对protobuf-net.Grpc和Grpc.Net.Client的NuGet包。应用你最喜爱的包管理器装置它们,或者简略地运行dotnet add package protobuf-net.Grpc和dotnet add package Grpc.Net.Client。咱们还须要向咱们在第一节中创立的共享库增加一个我的项目援用。
在咱们的Program.cs中,咱们将增加以下代码来创立gRPC客户端并与gRPC服务通信。
class Program{ private const string ServerAddress = "https://localhost:5001"; static async Task Main() { var channel = GrpcChannel.ForAddress(ServerAddress); var portfolios = channel.CreateGrpcService<IPortfolioService>(); try { var request = new GetPortfolioRequest { TraderId = Guid.Parse("68CB16F7-42BD-4330-A191-FA5904D2E5A0"), PortfolioId = 42 }; var response = await portfolios.Get(request); Console.WriteLine($"Portfolio contains {response.Items.Count} items."); } catch (RpcException e) { Console.WriteLine(e.ToString()); } }}
当初咱们能够测试咱们的实现,首先启动ASP.NET Core应用程序,而后启动控制台应用程序。
将WCF双工服务迁徙到gRPC
当初咱们曾经介绍了应用protobuf-net.Grpc 将WCF服务迁徙到gRPC的基本知识,咱们能够看看一些更简单的例子。
在本节中,咱们将查看SimpleStockPriceTicker,这是一个双工服务,客户端启动连贯,服务器应用回调接口在更新可用时发送更新。WCF服务有一个没有返回类型的办法,因为它应用回调接口ISimpleStockTickerCallback实时向客户端发送数据。
[ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(ISimpleStockTickerCallback))]public interface ISimpleStockTickerService{ [OperationContract(IsOneWay = true)] void Subscribe(string[] symbols);}[ServiceContract]public interface ISimpleStockTickerCallback{ [OperationContract(IsOneWay = true)] void Update(string symbol, decimal price);}
当将这个服务迁徙到gRPC时,咱们能够应用gRPC流。gRPC服务器流的工作形式与下面的WCF服务相似。例如,客户端发送一个申请,而服务器以一个音讯流响应。在protobuf-net.Grpc中实现服务器流的习用办法是从RPC办法返回IAsyncEnumerable<T>。通过这种形式,咱们能够在客户端和服务器端为服务契约应用雷同的接口。请留神protobuf-net.Grpc也反对Google.Protobuf模式(在服务器端应用IServerStreamWriter<T>,在客户端应用AsyncServerStreamingCall<T>),须要咱们为客户端和服务端应用独自的接口办法。应用IAsyncEnumerable<T>作为流媒体将使咱们的服务契约看起来像上面的代码。
[ServiceContract]public interface IStockTickerService{ [OperationContract] IAsyncEnumerable<StockTickerUpdate> Subscribe(SubscribeRequest request, CallContext context = default);}
请留神CallContext参数,它是客户端和服务器端的gRPC调用上下文。这容许咱们在客户端和服务器端拜访调用上下文,而不须要独自的接口。Gogogle.Protobuf生成的代码将在客户端应用调用,而在服务器端应用ServerCallContext。
因为WCF服务只应用根本类型作为参数,所以咱们须要创立一组能够用作参数的数据契约。下面的服务附带的数据契约看起来像这样。留神,咱们曾经向响应音讯增加了一个工夫戳字段,这个字段在原始WCF服务中不存在。
[DataContract]public class SubscribeRequest{ [DataMember(Order = 1)] public List<string> Symbols { get; set; } = new List<string>();}[DataContract]public class StockTickerUpdate{ [DataMember(Order = 1)] public string Symbol { get; set; } [DataMember(Order = 2)] public decimal Price { get; set; } [DataMember(Order = 3)] public DateTime Time { get; set; }}
通过重用微软迁徙指南中的IStockPriceSubscriberFactory,咱们能够实现上面的服务。通过应用System.Threading.Channels,能够很容易地将事件流到一个异步可枚举对象。
public class StockTickerService : IStockTickerService, IDisposable{ private readonly IStockPriceSubscriberFactory _subscriberFactory; private readonly ILogger<StockTickerService> _logger; private IStockPriceSubscriber _subscriber; public StockTickerService(IStockPriceSubscriberFactory subscriberFactory, ILogger<StockTickerService> logger) { _subscriberFactory = subscriberFactory; _logger = logger; } public IAsyncEnumerable<StockTickerUpdate> Subscribe(SubscribeRequest request, CallContext context = default) { var buffer = Channel.CreateUnbounded<StockTickerUpdate>(); _subscriber = _subscriberFactory.GetSubscriber(request.Symbols.ToArray()); _subscriber.Update += async (sender, args) => { try { await buffer.Writer.WriteAsync(new StockTickerUpdate { Symbol = args.Symbol, Price = args.Price, Time = DateTime.UtcNow }); } catch (Exception e) { _logger.LogError($"Failed to write message: {e.Message}"); } }; return buffer.AsAsyncEnumerable(context.CancellationToken); } public void Dispose() { _subscriber?.Dispose(); }}
WCF全双工服务容许双向异步、实时消息传递。在后面的示例中,客户机启动了一个申请并接管到一个更新流。在这个版本中,客户端流化申请音讯,以便对订阅列表增加和删除,而不用创立新的订阅。WCF服务契约的定义如下。客户端应用Subscribe办法启动订阅,并应用AddSymbol和RemoveSymbol办法增加或删除。更新通过回调接口接管,这与后面的服务器流示例雷同。
[ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IFullStockTickerCallback))]public interface IFullStockTickerService{ [OperationContract(IsOneWay = true)] void Subscribe(); [OperationContract(IsOneWay = true)] void AddSymbol(string symbol); [OperationContract(IsOneWay = true)] void RemoveSymbol(string symbol);}[ServiceContract]public interface IFullStockTickerCallback{ [OperationContract(IsOneWay = true)] void Update(string symbol, decimal price);}
应用protobuf-net.Gprc实现的等价服务契约的状况如下。该服务承受申请音讯流并返回响应音讯流。
[ServiceContract]public interface IFullStockTicker{ [OperationContract] IAsyncEnumerable<StockTickerUpdate> Subscribe(IAsyncEnumerable<SymbolRequest> request, CallContext context = default);}
上面定义了附带的数据契约。该申请包含一个action属性,该属性指定该符号是应该从订阅中增加还是删除。响应音讯与后面的示例雷同。
public enum SymbolRequestAction{ Add = 0, Remove = 1}[DataContract]public class SymbolRequest{ [DataMember(Order = 1)] public SymbolRequestAction Action { get; set; } [DataMember(Order = 2)] public string Symbol { get; set; }}[DataContract]public class StockTickerUpdate{ [DataMember(Order = 1)] public string Symbol { get; set; } [DataMember(Order = 2)] public decimal Price { get; set; } [DataMember(Order = 3)] public DateTime Time { get; set; }}
服务的实现如下所示。咱们应用与后面示例雷同的技术,通过IAsyncEnumerable<T>来流动事件,另外创立一个后台任务,它枚举申请流,并对单个申请进行响应。
public class FullStockTickerService : IFullStockTicker, IDisposable{ private readonly IFullStockPriceSubscriberFactory _subscriberFactory; private readonly ILogger<FullStockTickerService> _logger; private IFullStockPriceSubscriber _subscriber; private Task _processRequestTask; private CancellationTokenSource _cts; public FullStockTickerService(IFullStockPriceSubscriberFactory subscriberFactory, ILogger<FullStockTickerService> logger) { _subscriberFactory = subscriberFactory; _logger = logger; _cts = new CancellationTokenSource(); } public IAsyncEnumerable<StockTickerUpdate> Subscribe(IAsyncEnumerable<SymbolRequest> request, CallContext context) { var cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token, context.CancellationToken).Token; var buffer = Channel.CreateUnbounded<StockTickerUpdate>(); _subscriber = _subscriberFactory.GetSubscriber(); _subscriber.Update += async (sender, args) => { try { await buffer.Writer.WriteAsync(new StockTickerUpdate { Symbol = args.Symbol, Price = args.Price, Time = DateTime.UtcNow }); } catch (Exception e) { _logger.LogError($"Failed to write message: {e.Message}"); } }; _processRequestTask = ProcessRequests(request, buffer.Writer, cancellationToken); return buffer.AsAsyncEnumerable(cancellationToken); } private async Task ProcessRequests(IAsyncEnumerable<SymbolRequest> requests, ChannelWriter<StockTickerUpdate> writer, CancellationToken cancellationToken) { await foreach (var request in requests.WithCancellation(cancellationToken)) { switch (request.Action) { case SymbolRequestAction.Add: _subscriber.Add(request.Symbol); break; case SymbolRequestAction.Remove: _subscriber.Remove(request.Symbol); break; default: _logger.LogWarning($"Unknown Action '{request.Action}'."); break; } } writer.Complete(); } public void Dispose() { _cts.Cancel(); _subscriber?.Dispose(); }}
总结
祝贺你!你曾经走到这一步了。当初你晓得了将WCF服务迁徙到gRPC的另一种办法。心愿这种技术比用.proto格局重写现有的数据契约要快得多。
欢送关注我的公众号,如果你有喜爱的外文技术文章,能够通过公众号留言举荐给我。