在原本的结构里面,由于默认服务引用的都是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.HostingFunShow.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.AspNetCoreFunShow.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.AspNetCoreFunShow.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>
实现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,此包是实现访问内嵌资源文件的根本。
然后在项目文件的
<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