環境:
.net core 3.1
MSSSQL , MYSQL
MVC
EFCore
AutoFac
前言:
不同的框架主要解決開發中出現的不同的問題,本框架主要解決多個項目在開發過程中多個模塊的重復使用造成冗余和不便於管理。
項目適用背景:
1.不同項目之間業務邏輯有所關聯並不是完全獨立的項目
比如 水果商店 和 衣服商店 都是商店的東西有業務上的關聯。但是 水果商店 和 在線教育 就不同了,屬於兩種不同的業務邏輯
2.兩個項目主模塊不能同時存在,只能對模塊進行依賴
一、項目概覽
1.項目目錄結構(其中紅框部分為主項目1,主項目2)
總體結構:
模塊結構(使用Area來進行模塊化):
2.模塊引用(主項目與主項目之間不能互相引用),下圖在 FtCap.mvc.web 入口項目 處引用了 主項目2
3.調試與發布
我這里使用的動態編譯,也就是 .cshtml 不會編譯成 dll。具體怎么設置可以自行百度
調試:直接啟動項目即可調試,不過這里的調試有個小問題,被引用項目的 .cshtml,無法做到動態編譯,也就是調試狀態中改了.cshtml頁面之后在瀏覽器刷新后無法更新。入口項目是沒有問題的,知道怎么解決的朋友歡迎在下面留言
發布:直接在入口項目右鍵發布即可,發布后沒有上述的問題
二、框架大致結構圖:
項目初始化:
每個模塊必須創建 ModuleInitializer
類,並繼承 IModuleInitializer
接口。入口利用接口編譯模塊進行初始化,大致步驟如下:
IModuleInitializer 提供兩個方法用來進行初始化
public interface IModuleInitializer { void ConfigureServices(IServiceCollection serviceCollection, IConfiguration configuration); void Configure(IApplicationBuilder app, IWebHostEnvironment env); }
模塊內部初始化示例:
/// <summary>
/// 功能描述 :初始化類
/// 創 建 者 :Bear.Tirisfal
/// 創建日期 :2020/8/23 10:04:51
/// QQ :571115139
/// </summary>
public class ModuleInitializer : IModuleInitializer { public const string AreaName = "SiteShare"; private static readonly string ModuleName = $"FtCap.Module.{AreaName}"; public void ConfigureServices(IServiceCollection services, IConfiguration configuration) { #region 主要模塊基礎配置 //加載配置文件 AppSettings.InitStaticConfig<Configs>(AreaName); //注入動態路由轉換類 //services.AddScoped<SlugRouteValueTransformer>(); services.ConfigureOptions(typeof(CommonConfigureOptions)); //注入數據庫 context if (Configs.DbConnType?.ToUpper() == "MSSQL") { services.AddDbContextPool<ProjectDbContext>(option => { option.UseSqlServer(Configs.DbConnStr); }); } else { services.AddDbContextPool<ProjectDbContext>(option => { option.UseMySql(Configs.DbConnStr); }); } //注入數據庫服務 services.AddTransient(typeof(IRepository<>), typeof(Repository<>)); services.AddTransient(typeof(IRepositoryWithTypedId<,>), typeof(RepositoryWithTypedId<,>)); //加載mongodb鏈接一個主項目只加載一次 MongoConfig.InitMongoDb(AppSettings.CoreSetting.MonogDbConn); #endregion ConstVar.SrcPath = $"_content/{ModuleName}"; } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseMiddleware<ExceptionMiddleware>(); app.UseEndpoints(endpoints => { //endpoints.MapDynamicControllerRoute<SlugRouteValueTransformer>("/{**slug}"); endpoints.MapAreaControllerRoute( name: "default", areaName: ModuleInitializer.AreaName, pattern: "/{controller=Home}/{action=Index}/{id?}" ); }); }
三、項目初始化流程:
上面說明了項目的大概架構,下面講講如何通過入口項目 FtCap.mvc.web 來初始化模塊的
初始化的工作都在 基礎層(FtCap.Infrastructure) 進行的
1.在入口項目創建 modules.json 文件記錄所有模塊使用情況,並方便后面讀取程序集做准備。文件內容大致如下:
id:程序集完整名稱
isBundledWithHost:是否通過入口項目引用(這里可以直接將外部dll放進bin目錄引用模塊,而不需要通過入口項目添加引用)
version:版本
2.讀取 modules.json 並加載每個模塊
首先,得有一個 模塊類 來接收這個 JSON 數據,以及對應的程序集
直接上代碼:
public class ModuleInfo { public string Id { get; set; } public string Name { get; set; } /// <summary> /// 是否已經在程序集中引用 /// </summary> public bool IsBundledWithHost { get; set; } /// <summary> /// 版本 /// </summary> public Version Version { get; set; } /// <summary> /// 對應程序集 /// </summary> public Assembly Assembly { get; set; } }
然后,獲取各個程序集到集合中(這里添加了一個 IServiceCollection 的擴展,方便在 setup.cs 中調用):
public static IServiceCollection AddModules(this IServiceCollection services) { foreach (var module in _modulesConfig.GetModules())//GetModules 是讀取JSON文件,並返回對象 { if(!module.IsBundledWithHost) { TryLoadModuleAssembly(module.Id, module); if (module.Assembly == null) { throw new Exception($"Cannot find main assembly for module {module.Id}"); } } else { module.Assembly = Assembly.Load(new AssemblyName(module.Id)); } GlobalConfiguration.Modules.Add(module); } return services; }
最后、我們需要用這里的模塊信息處理3個地方(1.初始化,也就是調用各個模塊的ModuleInitializer,2.加載各個模塊MVC的控制器和視圖,3. 注冊EF要用到的實體對象 )
1.初始化,很簡單,遍歷集合調用 接口方法就行了:
foreach (var module in GlobalConfiguration.Modules) { var moduleInitializerType = module.Assembly.GetTypes() .FirstOrDefault(t => typeof(IModuleInitializer).IsAssignableFrom(t)); if ((moduleInitializerType != null) && (moduleInitializerType != typeof(IModuleInitializer))) { var moduleInitializer = (IModuleInitializer)Activator.CreateInstance(moduleInitializerType); services.AddSingleton(typeof(IModuleInitializer), moduleInitializer); moduleInitializer.ConfigureServices(services, _configuration); } }
2.加載各個模塊MVC的控制器和視圖,這段代碼比較固定,在網上也有很多參考:
foreach (var module in modules.Where(x => !x.IsBundledWithHost)) { AddApplicationPart(mvcBuilder, module.Assembly); } --------------------- private static void AddApplicationPart(IMvcBuilder mvcBuilder, Assembly assembly) { var partFactory = ApplicationPartFactory.GetApplicationPartFactory(assembly); foreach (var part in partFactory.GetApplicationParts(assembly)) { mvcBuilder.PartManager.ApplicationParts.Add(part); } var relatedAssemblies = RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: false); foreach (var relatedAssembly in relatedAssemblies) { partFactory = ApplicationPartFactory.GetApplicationPartFactory(relatedAssembly); foreach (var part in partFactory.GetApplicationParts(relatedAssembly)) { mvcBuilder.PartManager.ApplicationParts.Add(part); } } }
3.加載EF實體對象:
這里說明一下EF model 在設計的時候增加了一個父類 EntityBase,和 IModuleInitializer 作用一樣 並用於區分哪些類需要注冊到 EF 中,下面代碼主要
在 OnModelCreating 中加載的模塊數據
/// <summary>
/// 功能描述 :通用數據庫訪問上下文
/// 創 建 者 :Bear.Tirisfal
/// 創建日期 :2020/8/23 10:04:51
/// QQ :571115139
/// </summary>
public class ProjectDbContext : IdentityDbContext { public ProjectDbContext(DbContextOptions options) : base(options) { } public override int SaveChanges(bool acceptAllChangesOnSuccess) { ValidateEntities(); return base.SaveChanges(acceptAllChangesOnSuccess); } public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken)) { ValidateEntities(); return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); } protected override void OnModelCreating(ModelBuilder modelBuilder) { List<Type> typeToRegisters = new List<Type>(); foreach (var module in GlobalConfiguration.Modules) { typeToRegisters.AddRange(module.Assembly.DefinedTypes.Select(t => t.AsType())); } RegisterEntities(modelBuilder, typeToRegisters); RegisterConvention(modelBuilder); base.OnModelCreating(modelBuilder); RegisterCustomMappings(modelBuilder, typeToRegisters); if (Database.ProviderName == "Microsoft.EntityFrameworkCore.Sqlite") { foreach (var entityType in modelBuilder.Model.GetEntityTypes()) { var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset) || p.PropertyType == typeof(DateTimeOffset?)); foreach (var property in properties) { modelBuilder .Entity(entityType.Name) .Property(property.Name) .HasConversion(new DateTimeOffsetToBinaryConverter()); } var decimalProperties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == typeof(decimal) || p.PropertyType == typeof(decimal?)); foreach (var property in decimalProperties) { modelBuilder .Entity(entityType.Name) .Property(property.Name) .HasConversion<double>(); } } } } private void ValidateEntities() { var modifiedEntries = ChangeTracker.Entries() .Where(x => (x.State == EntityState.Added || x.State == EntityState.Modified)); foreach (var entity in modifiedEntries) { if (entity.Entity is ValidatableObject validatableObject) { var validationResults = validatableObject.Validate(); if (validationResults.Any()) { throw new ValidationException(entity.Entity.GetType(), validationResults); } } } } private static void RegisterConvention(ModelBuilder modelBuilder) { foreach (var entity in modelBuilder.Model.GetEntityTypes()) { if (entity.ClrType.Namespace != null) { var nameParts = entity.ClrType.Namespace.Split('.'); var tableName = entity.ClrType.Name; //string.Concat(nameParts[2], "_", entity.ClrType.Name); modelBuilder.Entity(entity.Name).ToTable(tableName); } } foreach (var relationship in modelBuilder.Model.GetEntityTypes().SelectMany(e => e.GetForeignKeys())) { relationship.DeleteBehavior = DeleteBehavior.Restrict; } } private static void RegisterEntities(ModelBuilder modelBuilder, IEnumerable<Type> typeToRegisters) { var entityTypes = typeToRegisters.Where(x => x.GetTypeInfo().IsSubclassOf(typeof(EntityBase)) && !x.GetTypeInfo().IsAbstract); foreach (var type in entityTypes) { modelBuilder.Entity(type); } } private static void RegisterCustomMappings(ModelBuilder modelBuilder, IEnumerable<Type> typeToRegisters) { var customModelBuilderTypes = typeToRegisters.Where(x => typeof(ICustomModelBuilder).IsAssignableFrom(x)); foreach (var builderType in customModelBuilderTypes) { if (builderType != null && builderType != typeof(ICustomModelBuilder)) { var builder = (ICustomModelBuilder)Activator.CreateInstance(builderType); builder.Build(modelBuilder); } } } }
四、配置文件,靜態資源文件管理:
1.配置文件管理:
每個模塊可以使用不同的配置文件,通過將 json 數據 添加到 靜態model中來保存各個模塊的配置
規定 配置文件格式 {areaName}.Config.json 上面 ModuleInitializer 已經列出來了使用方式
module.core.cs
public static void InitStaticConfig<T>(string areaName) { string path = $"{areaName}.Config.json"; var builder = new ConfigurationBuilder(); builder.AddJsonFile(path); builder.Build().Get<T>(); }
module.模塊.cs
//加載配置文件 AppSettings.InitStaticConfig<Configs>(AreaName);
2.靜態資源文件管理:
上篇文章 已經介紹過,關於靜態資源的管理和壓縮
搭建完成后可以進行一系列的優化,例如我這里使用了 Directory.Build.props 來統一 模塊的引用和一些基礎配置,還可以添加一些自定義 TagHelpers來管理不同的模塊資源
當然你還可以使用.tagets 來添加一些編譯事件。后續會將源碼放到 github 上
通過 props 和 tagets來實現 nuget包版本,.net 版本的管理 方便后面升級
創建 Dependencies.AspNetCore.props(.net 各個包的版本)
Dependencies.props(nuget 包各個版本)
FtCap.Commons.props(程序集版本等其他信息)
FtCap.Commons.targets(構建文件)
上面兩個dependencies 依賴文件,創建PackageManagement變量
然后再target文件中 將 關聯的版本號加載到程序集中。程序集工程文件的引用就無需版本號了
四個文件的源碼如下:
Dependencies.AspNetCore.props
<Project> <PropertyGroup> <AspNetCoreVersion>5.0.1</AspNetCoreVersion> <AspNetCoreTargetFramework>netcoreapp5.0</AspNetCoreTargetFramework> </PropertyGroup> <ItemGroup> <PackageManagement Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="$(AspNetCoreVersion)" /> <PackageManagement Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="$(AspNetCoreVersion)" /> <PackageManagement Include="Microsoft.EntityFrameworkCore.Design" Version="$(AspNetCoreVersion)" /> <PackageManagement Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="$(AspNetCoreVersion)" /> <PackageManagement Include="Microsoft.EntityFrameworkCore.SqlServer" Version="$(AspNetCoreVersion)" /> <PackageManagement Include="Microsoft.EntityFrameworkCore.Tools" Version="$(AspNetCoreVersion)" /> <PackageManagement Include="Microsoft.Extensions.FileProviders.Embedded" Version="$(AspNetCoreVersion)" /> <PackageManagement Include="Microsoft.EntityFrameworkCore.Relational" Version="$(AspNetCoreVersion)" /> <!--其他需要跟着版本升級的包--> <PackageManagement Include="Pomelo.EntityFrameworkCore.MySql" Version="5.0.0-alpha.2"/> <PackageManagement Include="Z.EntityFramework.Plus.EFCore" Version="5.1.12"/> <PackageManagement Include="System.Drawing.Common" Version="5.0.0"/> </ItemGroup> </Project>
Dependencies.props
<Project> <Import Project="Dependencies.AspNetCore.props" /> <ItemGroup> <PackageManagement Include="Lucene.Net" Version="4.8.0-beta00013" /> <PackageManagement Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00013" /> <PackageManagement Include="Lucene.Net.QueryParser" Version="4.8.0-beta00013" /> <PackageManagement Include="Newtonsoft.Json" Version="12.0.3" /> <PackageManagement Include="NLog" Version="4.7.4"/> <PackageManagement Include="NLog.Web.AspNetCore" Version="4.9.3" /> <PackageManagement Include="Autofac.Extensions.DependencyInjection" Version="7.1.0" /> <PackageManagement Include="BuildBundlerMinifier" Version="3.2.449" /> <PackageManagement Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.3.1"/> <PackageManagement Include="MongoDB.Driver" Version="2.11.5"/> </ItemGroup> </Project>
FtCap.Commons.props
<Project> <Import Project="Dependencies.props" /> <PropertyGroup> <VersionPrefix>1.0.0</VersionPrefix> <VersionSuffix>rc1</VersionSuffix> <VersionSuffix Condition="'$(VersionSuffix)'!='' AND '$(BuildNumber)' != ''">$(VersionSuffix)-$(BuildNumber)</VersionSuffix> <LangVersion>latest</LangVersion> <!--<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <WarningsNotAsErrors>612,618,114</WarningsNotAsErrors>--> <DebugType>portable</DebugType> <!--<NetStandardImplicitPackageVersion>2.0.0-*</NetStandardImplicitPackageVersion> <GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> <GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> <GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>--> <LangVersion>8.0</LangVersion> <!-- Common Nuget properties--> </PropertyGroup> </Project>
Comoms.tagets
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Target Name="VerifyIncludeBuildOutputProperty" AfterTargets="BeforeCompile" BeforeTargets="CoreCompile"> <PropertyGroup> <_IsEmptyAssembly Condition=" '@(Compile)' == '' and '@(EmbeddedResource)' == '' ">true</_IsEmptyAssembly> </PropertyGroup> <Warning Condition=" '$(_IsEmptyAssembly)' == 'true' and '$(IncludeBuildOutput)' != 'false' " Code="OC2001" Text="Project contains no 'Compile' or 'EmbeddedResource' items. Set <IncludeBuildOutput>false</IncludeBuildOutput> in $(MSBuildProjectFile)." File="$(MSBuildProjectFullPath)" /> </Target> <Target Name="ApplyPackageManagement" BeforeTargets="CollectPackageReferences" DependsOnTargets="ApplyPackageManagementItems" /> <Target Name="ApplyPackageManagementItems" Inputs="@(PackageManagement)" Outputs="%(PackageManagement.Identity)"> <PropertyGroup> <_PackageManagementIdentity>%(PackageManagement.Identity)</_PackageManagementIdentity> <_PackageManagementVersion>%(PackageManagement.Version)</_PackageManagementVersion> </PropertyGroup> <Warning Condition=" '%(PackageReference.Identity)' == '$(_PackageManagementIdentity)' and '%(PackageReference.Version)' == '$(_PackageManagementVersion)' " Code="OC2002" Text="PackageReference %(PackageReference.Identity)@%(PackageReference.Version) Version attribute is not needed" File="$(MSBuildProjectFullPath)" /> <ItemGroup> <PackageReference Condition=" '%(Identity)' == '$(_PackageManagementIdentity)' " Version="$(_PackageManagementVersion)" ManagedVersion="true" /> </ItemGroup> </Target> <Target Name="BeforePackageManagement" BeforeTargets="ApplyPackageManagement"> <Message Text="BeforePackageManagement: %(PackageReference.Identity)@%(PackageReference.Version)" Importance="low" /> </Target> <Target Name="AfterPackageManagement" AfterTargets="ApplyPackageManagement"> <Message Text="AfterPackageManagement: %(PackageReference.Identity)@%(PackageReference.Version)" Importance="low" /> <ItemGroup> <UnmanagedPackageReference Include="@(PackageReference)" /> <UnmanagedPackageReference Remove="@(UnmanagedPackageReference)" Condition=" '%(UnmanagedPackageReference.ManagedVersion)' == 'true' " /> <UnmanagedPackageReference Remove="@(UnmanagedPackageReference)" Condition=" '%(UnmanagedPackageReference.IsImplicitlyDefined)' == 'true' " /> <UnmanagedPackageReference Remove="@(UnmanagedPackageReference)" Condition=" $([System.String]::Copy('%(Identity)').StartsWith('System.')) " /> </ItemGroup> <Warning Condition=" '@(UnmanagedPackageReference)' != '' and '%(Identity)' != 'Microsoft.AspNetCore.App' " Code="OC2003" Text="%(UnmanagedPackageReference.Identity)@%(UnmanagedPackageReference.Version) is an unmanaged PackageReference" File="$(MSBuildProjectFullPath)" /> </Target> </Project>
在各個目錄下的 Directory.Build.props,Directory.Build.targets 導入comoms就能動態加載版本號了,一定注意這兩個文件的位置,只會對同級目錄的文件夾生效。也就是當前項目的 工程文件 會找上級目錄的Directory.Build文件並加載執行。所以文件的擺放大概是這樣:
在需要引用第三方包的情況下 直接在 csproj的項目文件下 添加 <PackageReference Include="***" />就能引用對應的包了。這種感覺像極了 java pom.xml。
比如我們可以在nuget官網找到對應的包,復制他的路徑,粘貼到工程文件里面。去掉版本號這樣就完成了引用
關於.net core 插件化框架基本上就搭建完成了。是不是很簡單