关于c#:将WCF迁移到gRPC

2次阅读

共计 10640 个字符,预计需要花费 27 分钟才能阅读完成。

应用 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 格局重写现有的数据契约要快得多。

 欢送关注我的公众号,如果你有喜爱的外文技术文章,能够通过公众号留言举荐给我。

正文完
 0