ABP微服務系列學習-搭建自己的微服務結構(一)

2023-02-28 18:02:59

在原本的結構裡面,由於預設服務參照的都是ABP原生的模組,所以結構目錄裡面沒有包含modules目錄,這裡我們新增一個modules目錄,用於存放我們的自定義模組。
在shared裡面,我們再抽一個EventData的模組,用於訊息佇列共用資料實體。修改後結構如下圖所示:

開始搭建

由於我們沒有商業版的程式碼生成器,那就純手工搭建這個結構了。這裡我們使用VS Code作為編輯器配合dotnet cli操作
建立新的空白解決方案,後續通過再VS來編輯解決方案的內容。

dotnet new sln -n FunShow

然後在解決方案目錄下建立目錄

建立Shared專案

使用dotnet cli建立shared目錄下的專案

dotnet new classlib -n FunShow.Shared.Hosting -f net7.0
dotnet new classlib -n FunShow.Shared.Hosting.AspNetCore -f net7.0
dotnet new classlib -n FunShow.Shared.Hosting.Gateways -f net7.0
dotnet new classlib -n FunShow.Shared.Hosting.Microservices -f net7.0
dotnet new classlib -n FunShow.Shared.Localization -f net7.0
dotnet new classlib -n FunShow.Shared.EventData -f net7.0
dotnet new console -n FunShow.DbMigrator -f net7.0

編輯.csproj檔案

FunShow.Shared.Hosting

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <RootNamespace>FunShow.Shared.Hosting</RootNamespace>
  </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Serilog.Extensions.Logging" Version="3.1.0" />
        <PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
        <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
        <PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
    </ItemGroup>

    <ItemGroup>
        <PackageReference Include="Volo.Abp.Autofac" Version="7.0.0" />
        <PackageReference Include="Volo.Abp.Data" Version="7.0.0" />
    </ItemGroup>
</Project>

FunShow.Shared.Hosting.AspNetCore

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <RootNamespace>FunShow.Shared.Hosting.AspNetCore</RootNamespace>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Serilog.AspNetCore" Version="6.1.0" />
    <PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
    <PackageReference Include="Serilog.Sinks.ElasticSearch" Version="8.4.1" />
    <PackageReference Include="prometheus-net.AspNetCore" Version="4.1.1" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Volo.Abp.Swashbuckle" Version="7.0.0" />
    <PackageReference Include="Volo.Abp.AspNetCore.Serilog" Version="7.0.0" />
    <ProjectReference Include="..\FunShow.Shared.Hosting\FunShow.Shared.Hosting.csproj" />
  </ItemGroup>
</Project>

FunShow.Shared.Hosting.Gateways
這裡閘道器我們使用yarp

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Yarp.ReverseProxy" Version="1.1.1" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\FunShow.Shared.Hosting.AspNetCore\FunShow.Shared.Hosting.AspNetCore.csproj" />
  </ItemGroup>
</Project>

FunShow.Shared.Hosting.Microservices

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\FunShow.Shared.Hosting.AspNetCore\FunShow.Shared.Hosting.AspNetCore.csproj" />
  </ItemGroup>

  <ItemGroup>
      <PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="7.0.1" />
      <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.1" />
      <PackageReference Include="DistributedLock.Redis" Version="1.0.2" />
  </ItemGroup>

  <ItemGroup>
      <PackageReference Include="Volo.Abp.AspNetCore.MultiTenancy" Version="7.0.0" />
      <PackageReference Include="Volo.Abp.EventBus.RabbitMQ" Version="7.0.0" />
      <PackageReference Include="Volo.Abp.BackgroundJobs.RabbitMQ" Version="7.0.0" />
      <PackageReference Include="Volo.Abp.Caching.StackExchangeRedis" Version="7.0.0" />
      <PackageReference Include="Volo.Abp.DistributedLocking" Version="7.0.0" />
      <PackageReference Include="Volo.Abp.EntityFrameworkCore" Version="7.0.0" />
  </ItemGroup>

</Project>

FunShow.Shared.EventData

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Volo.Abp.EventBus.Abstractions" Version="7.0.0" />
  </ItemGroup>
</Project>

DbMigrator我們後續到資料遷移時再去完善

實現FunShow.Shared.Hosting

新增類FunShowSharedHostingModule.cs

using Volo.Abp.Autofac;
using Volo.Abp.Data;
using Volo.Abp.Modularity;

namespace FunShow.Shared.Hosting;

[DependsOn(
    typeof(AbpAutofacModule),
    typeof(AbpDataModule)
)]
public class FunShowSharedHostingModule : AbpModule
{
    public override void PreConfigureServices(ServiceConfigurationContext context)
    {
        // https://www.npgsql.org/efcore/release-notes/6.0.html#opting-out-of-the-new-timestamp-mapping-logic
        System.AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
    
    }
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
    	ConfigureDatabaseConnections();
    }
    
    private void ConfigureDatabaseConnections()
    {
        Configure<AbpDbConnectionOptions>(options =>
        {
        
            options.Databases.Configure("AdministrationService", database =>
            {
                database.MappedConnections.Add("AbpAuditLogging");
                database.MappedConnections.Add("AbpPermissionManagement");
                database.MappedConnections.Add("AbpSettingManagement");
                database.MappedConnections.Add("AbpFeatureManagement");
            });
        
            options.Databases.Configure("IdentityService", database =>
            {
                database.MappedConnections.Add("AbpIdentity");
                database.MappedConnections.Add("OpenIddict");
            });
        });
    }
}

這裡我打算用PGSQL,所以需要設定一下

System.AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);

ConfigureDatabaseConnections方法裡面作用是設定資料庫連線字串對映關係,把ABP基礎模組的資料庫對映到微服務對應資料庫。目前設定2個基礎服務相關的連結字串。

實現FunShow.Shared.Hosting.AspNetCore

新增類FunShowSharedHostingAspNetCoreModule.cs

using System;
using Volo.Abp.AspNetCore.Serilog;
using Volo.Abp.Modularity;
using Volo.Abp.Swashbuckle;


namespace FunShow.Shared.Hosting.AspNetCore;


[DependsOn(
    typeof(FunShowSharedHostingModule),
    typeof(AbpAspNetCoreSerilogModule),
    typeof(AbpSwashbuckleModule)
)]
public class FunShowSharedHostingAspNetCoreModule : AbpModule
{


}

新增類SwaggerConfigurationHelper.cs

using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using Volo.Abp.Modularity;


namespace FunShow.Shared.Hosting.AspNetCore
{
    public static class SwaggerConfigurationHelper
    {
        public static void Configure(
            ServiceConfigurationContext context,
            string apiTitle
        )
        {
            context.Services.AddAbpSwaggerGen(options =>
            {
                options.SwaggerDoc("v1", new OpenApiInfo { Title = apiTitle, Version = "v1" });
                options.DocInclusionPredicate((docName, description) => true);
                options.CustomSchemaIds(type => type.FullName);
            });
        }
        
        public static void ConfigureWithAuth(
            ServiceConfigurationContext context,
            string authority,
            Dictionary<string, string> scopes,
            string apiTitle,
            string apiVersion = "v1",
            string apiName = "v1"
        )
        {
            context.Services.AddAbpSwaggerGenWithOAuth(
                authority: authority,
                scopes: scopes,
                options =>
                {
                    options.SwaggerDoc(apiName, new OpenApiInfo { Title = apiTitle, Version = apiVersion });
                    options.DocInclusionPredicate((docName, description) => true);
                    options.CustomSchemaIds(type => type.FullName);
                });
        }
    }
}

新增類SerilogConfigurationHelper.cs

using System;
using System.IO;
using Microsoft.Extensions.Configuration;
using Serilog;
using Serilog.Events;
using Serilog.Sinks.Elasticsearch;


namespace FunShow.Shared.Hosting.AspNetCore
{
    public static class SerilogConfigurationHelper
    {
        public static void Configure(string applicationName)
        {
            var configuration = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json")
                .AddEnvironmentVariables()
                .Build();


            Log.Logger = new LoggerConfiguration()
    #if DEBUG
                    .MinimumLevel.Debug()
    #else
                    .MinimumLevel.Information()
    #endif
                    .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
                .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
                .Enrich.FromLogContext()
                .Enrich.WithProperty("Application", $"{applicationName}")
                .WriteTo.Async(c => c.File("Logs/logs.txt"))
                // .WriteTo.Elasticsearch(
                //     new ElasticsearchSinkOptions(new Uri(configuration["ElasticSearch:Url"]))
                //     {
                //         AutoRegisterTemplate = true,
                //         AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv6,
                //         IndexFormat = "Walk-log-{0:yyyy.MM}"
                //     })
                .WriteTo.Async(c => c.Console())
                .CreateLogger();
        }
    }
}

這裡我們先註釋掉寫入ES的設定。先預留,後續有需要可以放開註釋,或者設定其他紀錄檔記錄方式。

實現FunShow.Shared.Hosting.Gateways

新增類FunShowSharedHostingGatewaysModule.cs

using FunShow.Shared.Hosting.AspNetCore;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Modularity;


namespace FunShow.Shared.Hosting.Gateways;


[DependsOn(
        typeof(FunShowSharedHostingAspNetCoreModule)
    )]
public class FunShowSharedHostingGatewaysModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        var configuration = context.Services.GetConfiguration();
        
        context.Services.AddReverseProxy()
            .LoadFromConfig(configuration.GetSection("ReverseProxy"));
    }
}

新增類GatewayHostBuilderExtensions.cs

using Microsoft.Extensions.Configuration;


namespace Microsoft.Extensions.Hosting
{
    public static class GatewayHostBuilderExtensions
    {
        public const string AppYarpJsonPath = "yarp.json";


        public static IHostBuilder AddYarpJson(
            this IHostBuilder hostBuilder,
            bool optional = true,
            bool reloadOnChange = true,
            string path = AppYarpJsonPath)
        {
            return hostBuilder.ConfigureAppConfiguration((_, builder) =>
            {
                builder.AddJsonFile(
                        path: AppYarpJsonPath,
                        optional: optional,
                        reloadOnChange: reloadOnChange
                    )
                    .AddEnvironmentVariables();
            });
        }
    }
}

這個類用於擴充套件IHostBuilder方法,設定閘道器讀取組態檔,這裡採用yarp作為閘道器元件,原商業版微服務模板採用的是ocelot。
新增類YarpSwaggerUIBuilderExtensions.cs,用於設定swagger

using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Volo.Abp;
using Yarp.ReverseProxy.Configuration;


namespace FunShow.Shared.Hosting.Gateways
{
    public static class YarpSwaggerUIBuilderExtensions
    {
        public static IApplicationBuilder UseSwaggerUIWithYarp(this IApplicationBuilder app,
            ApplicationInitializationContext context)
        {
            app.UseSwagger();
            app.UseSwaggerUI(options =>
            {
                var configuration = context.ServiceProvider.GetRequiredService<IConfiguration>();
                var logger = context.ServiceProvider.GetRequiredService<ILogger<ApplicationInitializationContext>>();
                var proxyConfigProvider = context.ServiceProvider.GetRequiredService<IProxyConfigProvider>();
                var yarpConfig = proxyConfigProvider.GetConfig();


                var routedClusters = yarpConfig.Clusters
                    .SelectMany(t => t.Destinations,
                        (clusterId, destination) => new {clusterId.ClusterId, destination.Value});


                var groupedClusters = routedClusters
                    .GroupBy(q => q.Value.Address)
                    .Select(t => t.First())
                    .Distinct()
                    .ToList();


                foreach (var clusterGroup in groupedClusters)
                {
                    var routeConfig = yarpConfig.Routes.FirstOrDefault(q =>
                        q.ClusterId == clusterGroup.ClusterId);
                    if (routeConfig == null)
                    {
                        logger.LogWarning($"Swagger UI: Couldn't find route configuration for {clusterGroup.ClusterId}...");
                        continue;
                    }


                    options.SwaggerEndpoint($"{clusterGroup.Value.Address}/swagger/v1/swagger.json", $"{routeConfig.RouteId} API");
                    options.OAuthClientId(configuration["AuthServer:SwaggerClientId"]);
                    options.OAuthClientSecret(configuration["AuthServer:SwaggerClientSecret"]);
                }
            });
            
            return app;
        }
    }
}

實現FunShow.Shared.Hosting.Microservices

新增類FunShowSharedHostingMicroservicesModule.cs

using Medallion.Threading;
using Medallion.Threading.Redis;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;
using FunShow.Shared.Hosting.AspNetCore;
using StackExchange.Redis;
using Volo.Abp.AspNetCore.MultiTenancy;
using Volo.Abp.BackgroundJobs.RabbitMQ;
using Volo.Abp.Caching;
using Volo.Abp.Caching.StackExchangeRedis;
using Volo.Abp.DistributedLocking;
using Volo.Abp.EventBus.RabbitMq;
using Volo.Abp.Modularity;
using Volo.Abp.MultiTenancy;
using Volo.Abp.EntityFrameworkCore;


namespace FunShow.Shared.Hosting.Microservices;


[DependsOn(
    typeof(AbpEntityFrameworkCoreModule),
    typeof(FunShowSharedHostingAspNetCoreModule),
    typeof(AbpBackgroundJobsRabbitMqModule),
    typeof(AbpAspNetCoreMultiTenancyModule),
    typeof(AbpDistributedLockingModule),
    typeof(AbpEventBusRabbitMqModule),
    typeof(AbpCachingStackExchangeRedisModule)
)]
public class FunShowSharedHostingMicroservicesModule: AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        var configuration = context.Services.GetConfiguration();
        var hostingEnvironment = context.Services.GetHostingEnvironment();


        Configure<AbpMultiTenancyOptions>(options =>
        {
            options.IsEnabled = true;
        });


        Configure<AbpDistributedCacheOptions>(options =>
        {
            options.KeyPrefix = "FunShow:";
        });


        var redis = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]);


        context.Services
            .AddDataProtection()
            .SetApplicationName("FunShow")
            .PersistKeysToStackExchangeRedis(redis, "FunShow-Protection-Keys");


        context.Services.AddSingleton<IDistributedLockProvider>(_ =>
            new RedisDistributedSynchronizationProvider(redis.GetDatabase()));
    }
}

新增JWT設定類JwtBearerConfigurationHelper.cs

using System;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Modularity;


namespace FunShow.Shared.Hosting.Microservices
{
    public static class JwtBearerConfigurationHelper
    {
        public static void Configure(
            ServiceConfigurationContext context,
            string audience)
        {
            var configuration = context.Services.GetConfiguration();


            context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.Authority = configuration["AuthServer:Authority"];
                    options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]);
                    options.Audience = audience;
                });
        }
    }
}

實現FunShow.Shared.Localization

在專案中新增nuget包Microsoft.Extensions.FileProviders.Embedded,此包是實現存取內嵌資原始檔的根本。
然後在專案檔案的標籤中新增xml設定

<RootNamespace>FunShow</RootNamespace>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>

如果沒有上述設定,系統是無法讀取多語言設定的。
建立Localization目錄,新增類FunShowResource.cs

using Volo.Abp.Localization;


namespace FunShow.Localization
{
    [LocalizationResourceName("FunShow")]
    public class FunShowResource
    {
        
    }
}

在Localization目錄建立FunShow子目錄,新增en.json和zh-Hans.json檔案

{
    "culture": "en",
    "texts": {
      "Menu:Home": "Home",
      "Login": "Login",
      "Menu:Dashboard": "Dashboard"
    }
  }
{
    "culture": "zh-Hans",
    "texts": {
      "Menu:Home": "家",
      "Login": "登入",
      "Menu:Dashboard": "儀表盤"
    }
  }

新增類FunShowSharedLocalizationModule.cs

using FunShow.Localization;
using Volo.Abp.Localization;
using Volo.Abp.Modularity;
using Volo.Abp.Validation;
using Volo.Abp.Validation.Localization;
using Volo.Abp.VirtualFileSystem;


namespace FunShow.Shared.Localization;


[DependsOn(
    typeof(AbpValidationModule)
    )]
public class FunShowSharedLocalizationModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        Configure<AbpVirtualFileSystemOptions>(options =>
        {
            options.FileSets.AddEmbedded<FunShowSharedLocalizationModule>();
        });


        Configure<AbpLocalizationOptions>(options =>
        {
            options.Resources
                .Add<FunShowResource>("en")
                .AddBaseTypes(
                    typeof(AbpValidationResource)
                ).AddVirtualJson("/Localization/FunShow");


            options.DefaultResourceType = typeof(FunShowResource);
        });
    }
}

實現FunShow.Shared.EventData

新增類FunShowSharedEventDataModule.cs

using Volo.Abp.Modularity;


namespace FunShow.Shared.EventData;


public class FunShowSharedEventDataModule: AbpModule
{


}

至此我們完成了Shared專案的初始化,後續有一些共用的修改再返回來修改對應的專案。

下一章我們來實現基礎的AdministrationService和IdentityService