.netcore MVC模块化开发框架搭建基础
环境:
.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 插件化框架基本上就搭建完成了。是不是很简单