舊 WCF 專案成功遷移到 asp.net core web api

順風椰子皮發表於2020-08-16

背景

接上一篇,放棄了 asp.net core + gRPC 的方案後,我靈光一閃,為什麼不用 web api 呢?不也是 asp.net core 的嗎?雖然 RESTful 不是強約束,客戶端寫起來也麻煩,但還是可以滿足基本需求,避免大幅修改舊有的業務邏輯程式碼

在網上找到相當多的文章,比較 gRPC 和 RESTful 的優缺點,結論都是 gRPC 推薦用作內部系統間呼叫RESTful 推薦用作對外開放介面
選擇 RESTful 另一個最重要的原因是,gRPC 的底層框架需要HTTP2,而 win7 不支援HTTP2,有相當一部分使用者在 win7 上。上篇有人推薦 grpc web ,由於專案是 WPF 桌面客戶端,這種 web 方式可能就更不適合了。

Entity Framework Core

這部分基本與上一篇的內容一致,為了保證單篇文章的獨立性。把這部分內容完全 copy 過來???。

舊的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"中;

EF remove PrivateAssets

我的命令: 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類庫最好是 Net Standard 方便移植;
  • 新建一個類來繼承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("MyDatabase");
    services.AddDbContext<DataAccess.Context.Entities>(
        options => options.UseLazyLoadingProxies().UseMySQL(_dbString));
    services.AddGrpc();
}
{
    "ConnectionStrings": {
        "MyDatabase": "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": "*"
}

服務端 asp.net core web api

這部分可是水還是有點深了。由於最近幾年主要以 WPF 桌面軟體開發為主,很少了解 asp.net core 。這次算是惡補了一下,下面是個人總結,一切以官方文件為準

啟動類 StartUp

啟動類 StartUp.cs ,在這裡面主要是註冊服務(Swagger、mvc等),註冊中介軟體(身份認證、全域性異常捕獲等),以及不同環境的切換(Development、Production)。下面是我的 StartUp 類,有幾點經驗總結:

  • 初始化讀取全域性配置引數,比如 log4net.config連線字串等;
  • web api 只需要新增 services.AddControllers();,而不是 AddMvc();
  • Swagger 只在開發環境下啟用,而生產環境無效《在 ASP.NET Core 中使用多個環境》 多環境開發測試,真的太好用了,強烈推薦使用
  • 在根路徑下增加返回內容Hello Asp.Net Core WebApi 3.1!,為了方便測試是否執行成功。
public void ConfigureServices(IServiceCollection services)
{
    InitConfig();
    services.AddControllers();
    services.AddSwaggerDocument(SwaggerDocumentConfig); // Register the Swagger services
}

private void InitConfig()
{
    Entities.SetDefaultDBString(Configuration.GetConnectionString("MyDatabase"));
    Common.LogMaker.InitLog4NetConfig(Configuration.GetSection("log4net").Value);
    Common.WebApiLogger.Singleton.LogMaker.LogInfo("Start WebApi!");
}

private void SwaggerDocumentConfig(NSwag.Generation.AspNetCore.AspNetCoreOpenApiDocumentGeneratorSettings config)
{
    config.PostProcess = document =>
    {
        document.Info.Version = typeof(Startup).Assembly.GetName().Version.ToString();
        document.Info.Title = "Test Web Api";
        document.Info.Description = "僅供測試和發開使用。";
        document.Info.Contact = new NSwag.OpenApiContact
        {
            Name = "long",
            Email = "long@test.com"
        };
    };
}


public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();

        // Register the Swagger generator and the Swagger UI middlewares
        app.UseOpenApi();
        app.UseSwaggerUi3();
    }

    //app.UseCustomExceptionMiddleware(); // 全域性異常中介軟體
    app.UseRouting();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello Asp.Net Core WebApi 3.1!");
        });

        endpoints.MapControllers();
    });
}

路由 和 Controller

真心的覺得 asp.net core 的路由設計,真的是太棒了!而我只用到了其中很小的一部分《REST Api 的屬性路由》。其中有個注意點,全域性路由與屬性路由會有衝突,需要特別注意。

為了方便管理路由,靈活使用,以及後期版本的維護,建立一個路由模板Controller基類,所有 Controller 都繼承自 MyControllerBase

public class MyV1ApiRouteAttribute : Attribute, IRouteTemplateProvider
{
    public string Template => "api/v1/[controller]/[action]";
    public int? Order => 0;
    public string Name { get; set; }
}

[ApiController]
[MyV1ApiRoute]
[Produces(MediaTypeNames.Application.Json)]
public class MyControllerBase : ControllerBase
{
}

Nswag

NswagSwashbuckle 是微軟官方推薦的 Swagger 工具(官方 swagger 線上試用?)。我選擇 Nswag 的主要原因是,它提供的工具,根據 API 生成 C# 客戶端程式碼,其實到最後我也沒有使用這個功能。 《NSwag 和 ASP.NET Core 入門》

Nswag 使用起來也非常簡單,參考我的 啟動類 StartUp 中的寫法。如果想要把程式碼中的註釋也體現在 Swagger 文件中,需要執行一些額外的操作。
在 csproj 檔案中增加 <GenerateDocumentationFile>true</GenerateDocumentationFile>,另外,最好在 /Project/PropertyGroup/NoWarn 中增加 1591,否則你會得到一大堆的 warning : # CS1591: Missing XML comment for publicly visible type or member. 原因是專案中存在沒有註釋的方法,屬性或類

vs-swagger

客戶端 WebApiClient

在網上尋找有沒有現成的 RESTful 的 C# 工具,發現了WebApiClient,看了一下樣例,確實非常簡單,非常省事兒,只需要寫個簡單的 Interface 介面類,就可以了,關鍵是它還支援各種奇奇怪怪的 HTTP 介面
PS: 最開始讀 README.md 時候,總是一臉懵逼,一直把它當成 server 端的工具?。直到開始寫客戶端的時候,才真正看懂了他的文件。

WebApiClient.Tool

Swagger 是一個與語言無關的規範,用於描述 REST API。既然 Swagger 是一種規範,那麼極有可能存在,根據 Swagger.json 生成程式碼的工具。想著 WebApiClient 開發者是不是也已經提供了工具,果然不出所料,WebApiClient.Tools

只需執行一行命令,就可以根據 Swagger.json 直接生成客戶端的實體類,介面,甚至包括註釋,簡直爽的不要不要的,完美的避開了手寫程式碼的過程
我的命令: WebApiClient.Tools.Swagger.exe --swagger=http://10.50.40.237:5000/swagger/v1/swagger.json --namespace=MyWebApiProxy

WebApiClient.JIT

由於還有一大部分的 win7 桌面軟體使用者,而他們大概率不會安裝 net core ,所以只能選擇 net framework 的版本 WebApiClient.JIT。使用起來也相當方便,只需要在啟動的時候初始化一下webapi地址,然後在需要的時候呼叫即可。
WebApiClient 提供的是一個非同步的介面,由於舊專案升級,避免大幅改動,就沒有使用非同步的功能。

public static void InitialWebApiConfig(string _baseUrl)
{
    HttpApi.Register<IUserManagementApi>().ConfigureHttpApiConfig(c =>
    {
        c.HttpHost = new Uri(_baseUrl);
        c.FormatOptions.DateTimeFormat = DateTimeFormats.ISO8601_WithoutMillisecond;
    });
}

public void Todo()
{
    using (var client = HttpApi.Resolve<IUserManagementApi>())
    {
        var _req = new LoginRequestV2();
        _response = client.UserLoginExAsync(_req).InvokeAsync().Result;
    }
}

部署 Ubuntu + Nginx

專案的服務端,對作業系統沒有特別要求,所以直接選擇最新的 Ubuntu 20.04.1 LTS 。吸取了 gRPC 部署的一些經驗,這次只部署 http 服務,額外增加了 nginx 反向代理,只是因為在官網上看到了《使用 Nginx 在 Linux 上託管 ASP.NET Core》?。

Kestrel 是 ASP.NET Core 專案模板指定的預設 Web 伺服器,所以一般情況下,ASP.NET Core是不需要額外的容器的。《ASP.NET Core 中的 Web 伺服器實現》。下面是我的具體實現操作:

  1. 根據文件《在 Linux 上安裝 .NET Core》《安裝 Nginx》 指引,安裝 aspnetcore-runtime-3.1nginx
  2. 建立 Linux 的 web api 的服務檔案,並啟動。我的示例,--urls這個是非常實用的引數,可多埠;重點注意 ASPNETCORE_ENVIRONMENT 在配置是生產環境,還是開發環境
> sudo nano /etc/systemd/system/kestrel-mywebapi.service

[Unit]
Description=mywebapi App running on Ubuntu

[Service]
WorkingDirectory=/home/user/publish
ExecStart=/usr/bin/dotnet /home/user/publish/MyWebApi.dll --urls http://localhost:5000
Restart=always
# Restart service after 10 seconds if the dotnet service crashes:
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=dotnet-example
User=user
# Production Development
Environment=ASPNETCORE_ENVIRONMENT=Development
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false

[Install]
WantedBy=multi-user.target

> sudo systemctl enable kestrel-mywebapi.service
> sudo systemctl restart kestrel-mywebapi.service
> sudo systemctl status kestrel-mywebapi.service
  1. 配置 Nginx ,這部分我用的比較簡單,只用到轉發功能,未來可能在這一層增加 SSL ;另外 try_files $uri $uri/ =404 這一句需要註釋掉才行
> sudo nano /etc/nginx/sites-available/default

server {
    listen 80 default_server;
    listen [::]:80 default_server;

    root /var/www/html;
    index index.html index.htm index.nginx-debian.html;
    server_name _;
    location / {
            # First attempt to serve request as file, then
            # as directory, then fall back to displaying a 404.
            # try_files $uri $uri/ =404;
            proxy_pass http://localhost:5000;
    }

}

> sudo nginx -t    # 驗證配置檔案的語法
> sudo nginx -s reload
  1. 開啟防火牆埠,Ubuntu 是預設關閉22埠。安全起見,避免被頻繁掃描,建議把 ssh 預設埠 22 改為其他不常見的埠號。
sudo netstat -aptn
sudo apt-get install ufw
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp

sudo ufw enable
sudo ufw status

瀏覽器測試 http://10.50.40.237,返回預期的Hello Asp.Net Core WebApi 3.1!,完美?。還有一個小坑就是 https 還沒有配置。

自動化指令碼

在開發階段,需要經常得編譯,打包,上傳。雖然 VS2019 具有直接釋出到 FTP 的功能,不過我沒有使用。一方面,該功能從來沒用過,另一方面,還是想自己寫個更加靈活的指令碼。
目前只實現了 編譯,打包,上傳的功能,後續再增加 ssh 登入,解壓,重啟 asp.net 。

echo only win10

cd D:\Projects\lst\01-MyWebApi\MyWebApi
rem 已註釋:dotnet publish --output bin/publish/ --configuration Release --runtime linux-x64 --framework netcoreapp3.1 --self-contained false
dotnet publish -p:PublishProfileFullPath=/Properties/PublishProfiles/FolderProfile.pubxml --output bin/publish/

cd bin
tar.exe -a -c -f publish.zip publish

"C:\Program Files\PuTTY\psftp.exe"

open 10.50.40.237
Welcome123
put "D:\Projects\lst\01-LstWebApi\LenovoSmartToolWebApi\bin\publish.zip"
exit

rem 已註釋:"C:\Program Files\PuTTY\putty.exe" user@10.40.50.237 22 -pw password

pause

另外,用一個 WinForm 的測試小程式,嘗試了 self-contained = true 這種釋出方式,不需要客戶端安裝 net core 就能執行,發現編譯後從 1M+ 大幅增加到 150M+ ,妥妥的嚇壞了。即使使用了 PublishTrimmed=true 《剪裁獨立部署和可執行檔案》,大小也有 100M+,果斷放棄。

其他的改動

log4net 由 1.2.13.0 升級到 2.0.8後,初始化配置檔案方法新增一個引數ILoggerRepository

public static void InitLog4NetConfig(string file)
{
    var _rep = LogManager.GetRepository(System.Reflection.Assembly.GetCallingAssembly());
    log4net.Config.XmlConfigurator.Configure(_rep, new System.IO.FileInfo(file));
}

部署到 Ubuntu 後,發現 log4net 報錯。需要把 log4net.Appender.ColoredConsoleAppender 替換為 log4net.Appender.ManagedColoredConsoleAppender。是由於不同的 Appender 支援的 Framework 不同, ColoredConsoleAppender 支援 NET Framework 1.0~4.0 , ManagedColoredConsoleAppender 支援 NET Framework 2.0+ 。詳見:《Apache log4net™ Supported Frameworks》

MD5 摘要也出現問題,需要更改。原因是 HashPasswordForStoringInConfigFile 在 net core 中已經不可用了,該方法在 framework 4.5 中也提示為廢棄的方法。修改為下面新的 MD5 方法即可。

public static string GetOldMD5(string str)
{
    string result = System.Web.Security.FormsAuthentication.HashPasswordForStoringInConfigFile(str, "MD5").ToLower();
    return result;
}

public static string GetNewMD5(string str)
{
    MD5CryptoServiceProvider _MD5Provider = new MD5CryptoServiceProvider();
    byte[] bytes = Encoding.Default.GetBytes(str);
    byte[] encoded = _MD5Provider.ComputeHash(bytes);

    result = Encoding.ASCII.GetString(encoded).ToLower();
    return result;
}

總結

目前,只遷移了一部分的 WCF 介面過來,等待部署到生產環境,可以穩定執行後,再將剩餘部分全部遷移過來。這次的嘗試比較成功:

  1. 一是滿足了基本需求,較少改動老舊程式碼
  2. 二是大部分程式碼由工具生成,比如 API 文件,介面的實體類;
  3. 三是很多常用功能,都有現成的外掛來完成。

我只需要修改編輯器的 ERROR 的提示就可以了。感覺沒有寫什麼程式碼?。。。頂多只寫了幾行粘合程式碼?,一種搭積木的感覺?。其中 asp.net web api 還有很多的功能沒有使用,還需要更加細化到專案中。路漫漫~~

相關文章