1. 程式人生 > 實用技巧 >舊 WCF 專案遷移到 asp.net core + gRPC 的嘗試

舊 WCF 專案遷移到 asp.net core + gRPC 的嘗試

一個月前,公司的執行WCF的windows伺服器down掉了,由於 AWS 沒有通知,沒有能第一時間發現問題。
所以,客戶提出將WCF服務由C#改為JAVA,在Linux上面執行;一方面,AWS對Linux有較多的監控措施,另一方面,假如出現問題,可以設定自動重啟等服務。

老舊的WCF服務

目前WCF服務,主要提供windows桌面軟體的資料介面,應該有五六年的歷史了。我進入公司後,WCF服務的程式碼,一直由我一個人來維護。存在很多歷史遺留問題,也有不同版本的共存。

如果java重寫的話,其中的業務邏輯程式碼,難免會出現各種各樣的bug,增加開發和測試的工作量。聽說,要移植到linux服務上後,第一時間想到的就是跨平臺

.net core
.net core 經過了四年的發展,到目前的 3.1 LST版本,已經是非常成熟的跨平臺解決方案了。

之後,我就在網上查詢,有沒有WCF的.net core 版本,查詢到的資訊總結如下:

  1. Core WCF不打算做WCF到.NET Core的100%相容的移植;
  2. 對於新應用程式,WCF這種SOAP技術不建議使用;
  3. 對於老的應用程式,建議將這些保留在.NET Framework上;
  4. 如果您真的想將一箇舊的應用程式遷移到.NET Core並且想繼續使用WCF和WF, 社群的開源專案也是可以的,但是上生產的時間表就要到了2020年.NET 5;
  5. 開源社群,也強烈建議目前不要用於生產環境。

很遺憾,想不改動程式碼就遷移到 Linux 上面,基本是不可能的了。
我的最理想情況,儘量少的手寫程式碼,最好可以像WCF一樣,自動生成代理類,像訪問原生代碼一樣,來呼叫介面。之後,就發現了asp.net core + gRPC這種形式。

瞭解gRPC

gRPC 的好處非常多:高效能傳輸資料小,支援多語言生成工具使用HTTP2協議,這些好處網上都有大量詳細的介紹,本文不做贅述。
其實我最看重的部分還是:客戶端和服務端程式碼,都可以通過一個 proto 協議檔案來自動生成

而微軟官方,也建議用 ASP.NET Core gRPC。 《適用於 WCF 開發人員的 ASP.NET Core gRPC》

gRPC 的 proto 檔案

為了瞭解 proto 檔案的寫法,硬著頭皮看谷歌英文文件, proto3 勉強了解大概。《Language Guide (proto3)》,下面列出一些,我在使用過程中的經驗總結:

  1. 一個RPC服務必須有且僅有一個入參一個出參;假如不需要的話,可以設定為空的物件google.protobuf.Empty
  2. 基本型別( string, int32 等)不能作為PRC服務的引數,可使用谷歌提供的封裝物件,如:google.protobuf.StringValuegoogle.protobuf.Int32Value 詳見 google/protobuf/wrappers.proto檔案;
  3. proto3 不允許null值,這是由於 Protobuf 二進位制序列化,空和null不能區分,利用google.protobuf.StringValue 則可以實現null值;同第2點;
  4. string name=1;這個數字必須寫,用作 Protobuf 二進位制序列化,並且常用的屬性最好放在前12;PS: 太不習慣了,總以為是在賦值操作;
  5. 列舉型別必須從0開始,即:enum Weekday {Sunday=0;Monday=2;}
  6. 時間型別google.protobuf.Timestamp,必須是 UTC 時間;
  7. 訊息體 message 不能繼承,可多層巢狀,可以匯入 import;
// 我的例子
syntax = "proto3";

option csharp_namespace = "GrpcServiceTest.Protos";

import "Protos/ClientInfoModel.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";

package UserManagement;
service UserManagement {
    rpc UserReset(google.protobuf.Empty) returns (google.protobuf.Empty);
    rpc UserLogin(LoginRequestV2) returns(LoginResponseV2);
}

message LoginRequestV2 {
    string UserName = 1;
    string Password = 2;
}

message LoginResponseV2 {
    int32 TAG = 1;
    string Message = 2;
    UserModelV2 UserInfo = 3;

    message UserModelV2 {
        int64 UserID = 1;
        string UserName = 2;
        google.protobuf.StringValue Address = 3;
        google.protobuf.Timestamp LastLoginTime = 4;
        repeated PrivGroupPluginModelV2 PrivGroupPlugins = 5;
        bool IsDeleted = 6;

        message PrivGroupPluginModelV2{
        int64 Id=1;
        google.protobuf.Timestamp CreateDateTime=2;
        google.protobuf.Timestamp ModifyDateTime=3;
        int64 PluginId=4;
        int64 PrivGroupPluginID=5;
        }
    }
}

根據 proto 生成程式碼

用vs2019,選擇gRPC Service專案模板,建立專案。它會自動加上nuget包Grpc.AspNetCore。如果沒有的話,則需要自己安裝nuget包:Grpc.coreGoogle.ProtobufGrpc.Tools
由 proto 檔案生成程式碼有兩種方式:

  1. 通過vs右鍵 proto檔案,選擇 屬性Property,選擇Build Action中的Protobuf complier,會看到 gRPC Stub Classes,有三個選項 Server Only , Clent Only 和 Both 按需選擇;
  2. 編輯專案檔案 csproj,編輯 Protobuf 屬性,這種方法還可以使用路徑巨集萬用字元等,相當方便,強烈推薦
<ItemGroup>
    <Protobuf Include="Protos/*.proto" OutputDir="%(ProjectDir)ServerGrpc" GrpcServices="Server" />
</ItemGroup>

asp.net core 3.1

現在,恰好趕上了net core 3.1的這個 LST版本 ( long-term-support )的釋出,而 NET Core 3.0 生命週期終結於 2020年3月3日,下個大一統版本 NET 5 ,正式版本還要等到明年。至於為什麼沒有 NET 4.0版本,官方解釋,為了避免於 .NET Framework 4.X 產生歧義。

一步步的按照官方文件的指引,跟著做就可以了。《使用 ASP.NET Core 的 gRPC 服務》《教程:在 ASP.NET Core 中建立 gRPC 客戶端和伺服器》

仔細回想了一下,這部分確實沒有什麼值得說的,官方文件已經非常的詳細了。唯一不同的感受就是,net core 需要什麼功能的話,需要通過nuget來安裝;這點與 net framework 大有不同,framework 更像是,一次幫你全部裝好。

Entity Framework Core

舊的WCF專案,資料庫訪問使用的是 Entity Framework + Linq + MySql。需要安裝的 Nuget 包:

  • MySql.Data.EntityFrameworkCore Mysql的EF核心庫;
  • Microsoft.EntityFrameworkCore.Proxies 《Lazy loading》 懶載入的外掛;
  • Microsoft.EntityFrameworkCore.DesignMicrosoft.EntityFrameworkCore.Tools 這兩個外掛,用於生成程式碼;

另外,還需要下載安裝 mysql-connector-net-8.0.21.msi 來訪問資料庫。其中有一個 Scaffold-DbContextbug 99419 TINYINT(1) 轉化為 byte,而不是預期的 bool。這個問題將會在 8.0.22 版本中修復,目前只能手動修改。
EF當然是 Database First 了,生成EF程式碼需要在Package Manager Console用到 Scaffold-DbContext 命令,有三點需要注意:

  • Start up 啟始專案一定要是引用它的專案,並且編譯成功的;
  • Default project 生成後,程式碼存放的專案;
  • 如果生成失敗,提示:“Your startup project 'XXXX' doesn't reference Microsoft.EntityFrameworkCore.Design. This package is required for the Entity Framework Core Tools to work. Ensure your startup project is correct, install the package, and try again.”。編輯專案檔案 csproj 移除 <PrivateAssets>All</PrivateAssets> 從 "Microsoft.EntityFrameworkCore.Design"和"Microsoft.EntityFrameworkCore.Tools"中;

我的命令: Scaffold-DbContext -Connection "server=10.50.40.50;port=3306;user=myuser;password=123456;database=dbname" -Provider MySql.Data.EntityFrameworkCore -OutputDir "EFModel" -ContextDir "Context" -Project "DataAccess" -Context "BaseEntities" -UseDatabaseNames -Force

其他建議:

  • Library類庫最好是 netstandard 方便移植;
  • 新建一個類來繼承BaseEntities,覆蓋 OnConfiguring 方法,可配置的資料庫連線字串;
public class Entities : BaseEntities
{
    private static string _lstDBString;

    public static void SetDefaultDBString(string _dbString)
    {
        if (string.IsNullOrEmpty(_lstDBString))
        {
            _lstDBString = _dbString;
        }
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (!optionsBuilder.IsConfigured)
        {
            optionsBuilder.UseLazyLoadingProxies().UseMySQL(_lstDBString);
        }
    }
}
  • 最好採用 asp.net core 的框架注入;鑑於專案的原因,假如強行採用的話,改動比較大,只好放棄;
public void ConfigureServices(IServiceCollection services)
{
    string _dbString = Configuration.GetConnectionString("LstDatabase");
    services.AddDbContext<DataAccess.Context.Entities>(
        options => options.UseLazyLoadingProxies().UseMySQL(_dbString));
    services.AddGrpc();
}
  • 資料庫連結字串有多種存放的方式,有更加安全的方式;而我採用簡單方式存放在 appsettings.json
{
    "ConnectionStrings": {
        "LstDatabase": "server=127.0.0.1;port=3306;user=myuser;password=123456;database=dbname"
    },
    "log4net": "log4net.config",
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Information"
        }
    },
    "AllowedHosts": "*"
}

部署到 Ubuntu

生產環境執行的伺服器是 Ubuntu 14.04.6 LTS,在《ubuntu Releases wiki》上描述,14版本在去年已經停止了標準支援,而 .net core 的 runtime 最低支援也是 Ubuntu 16.04.6 LTS,只好選擇最新的版本Ubuntu 20.04.1 LTS

安裝Ubuntu Server系統小插曲:IT支援部門的同事,幫忙重灌了兩遍系統,一次14.04桌面版,一次20.04伺服器版;安裝20版本後,發現網絡卡沒有啟用,主機後面網線的燈都沒有亮起來。
由於我和他都不熟悉Ubuntu系統,網上查詢辦法,然後用手機拍照,再來伺服器上嘗試,搞了好一會兒,才連上網路,SSH也居然沒有啟用