.netcore 定制化項目開發的思考和實現


今年年初進了一家新公司,進入之后一邊維護老項目一邊了解項目流程,為了接下來的項目重做積累點經驗。

先說下老項目吧,.net fx 3.5+oracle......

在實際維護中逐漸發現,老項目有標准版、定制版兩種,標准版就是一套代碼,粗略計算了下,全部版本加起來有20+個版本,如果項目重做后還是依照這個模式去開發維護,估計距離猝死也不遠了,並且不同版本代碼的復用率極低(好吧,根本沒有)。打個比方,我在標准版中發現了一個bug,需要去其他的20+版本里面都修改一遍,刪庫跑路了解一下。。。。

為了提升工資(偷懶),進公司沒多久就在想辦法,如何去提高不同項目的代碼復用率,然后想起來了wtm、abp、simplcommerce這三種項目,似乎有不同項目中代碼服用的地方。

wtm、abp類似,是將底層的部分controller、view封裝在底層類庫,然后項目最外層去使用;

simplcommerce是將所有的模塊放在各個類庫中,然后在主項目中集成;

(或許是我看的不夠深入,歡迎指正)

這三種項目,對於我的不同項目提交代碼復用率來說,不能直接起到作用,但是卻提供了一種思路,我們可以將原始的標准版作為一個類庫,然后在不同的項目中引用這個類庫,做到絕大部分的代碼復用,少部分修改

 

我們如果想在定制項目中對標准版某個controller的某個action進行修改該怎么辦?

1.我首先想到的是在個性化項目中寫一個同名的controller,然后這個controller繼承自默認版本的對應controller,來達到重寫的目的,但是這個慣性思維陷入誤區了,mvc對於controller的控制不和普通的type繼承一樣,如果同名controller存在,則會報錯。。。在運行時我們可以判斷出是哪個action不同,但是無法通過emit來進行修改,所以這種辦法不可以。

2.第一種辦法不行,那么我們是否可以對於同名controller進行名稱上的修改,比如homecontroller在Tailor.Custom1中修改未TailorCustom1homecontroller,然后利用路由進行重定向?結果發現路由重定向,要么自定義一個路由中間件(求大佬給解決辦法,我不會。。),要么在請求進入的時候對請求進行重定向(這種重定向就是對HttpContext.Request.Path進行特殊判斷和處理,符合條件的進行重定向,但是可能會有很大的問題)

3.使用版本控制的思路,這個似乎可以,我們將標准版default中所有的都作為版本1.0,然后定制化作為2.0,在請求進入的時候,將請求頭添加一個version,如果mvc找不到這個version的controller或者action,會自動轉到默認的1.0版本中

 

那我們開始新建一個簡化版的項目,大概的分組可以做這樣

native/default作為標准版web類庫;

Tailor.Custom* 是定制化網站;

entity是實體、service是服務,實體和服務我們暫且不說,先說明下default這個標准web類庫,這個類庫就是上面所說的標准類庫,讓其他的Tailor.Custom1、Tailor.Custom1.Https、Tailor.Custom2.Https、Tailor.Custom3.Https(以下稱定制項目)去引用,然后再各自的項目中可以個性化修改

標准web類庫的csproj文件做適當的修改以更改成web類庫

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

  <PropertyGroup Label="Globals">
    <SccProjectName>SAK</SccProjectName>
    <SccProvider>SAK</SccProvider>
    <SccAuxPath>SAK</SccAuxPath>
    <SccLocalPath>SAK</SccLocalPath>
  </PropertyGroup>

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <OutputType>Library</OutputType>
  </PropertyGroup>
  ...
</Project>

然后借鑒wtm中使用項目對web類庫的引用,在標准web類庫中添加FrameworkServiceExtension.cs文件

        public static IServiceCollection AddFrameworkService(this IServiceCollection services,
            WebHostBuilderContext webHostBuilderContext = null
        )//在定制版本的Startup.ConfigureServices中添加services.AddFrameworkService();即可
        {
            CurrentDirectoryHelpers.SetCurrentDirectory();

            var configBuilder = new ConfigurationBuilder();

            if (!File.Exists(Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json")))
            {
                var binLocation = Assembly.GetEntryAssembly()?.Location;
                if (!string.IsNullOrEmpty(binLocation))
                {
                    var binPath = new FileInfo(binLocation).Directory?.FullName;
                    if (File.Exists(Path.Combine(binPath, "appsettings.json")))
                    {
                        Directory.SetCurrentDirectory(binPath);
                        configBuilder.SetBasePath(binPath)
                            .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                            .AddEnvironmentVariables();
                    }
                }
            }
            else
            {
                configBuilder.SetBasePath(Directory.GetCurrentDirectory())
                    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                    .AddEnvironmentVariables();
            }

            if (webHostBuilderContext != null)
            {
                var env = webHostBuilderContext.HostingEnvironment;
                configBuilder
                    .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
            }

            var config = configBuilder.Build();

            new AppSettingProvider().Initial(config);//添加靜態的配置全局配置文件

            var gd = AssemblyHelper.GetGlobalData();

            var currentNamespace = MethodBase.GetCurrentMethod().DeclaringType.Namespace;
            //獲取標准web類庫的Assembly
            var currentAssembly = gd.AllAssembly.Where(x => x.ManifestModule.Name == $"{currentNamespace}.dll").FirstOrDefault();

            StackTrace ss = new StackTrace(true);
            MethodBase mb = ss.GetFrame(ss.FrameCount - 1).GetMethod();

            var userNamespace = mb.DeclaringType.Namespace;//調用標准web類庫的定制版項目命名空間

            services.AddMvc(options =>
            {
                options.EnableEndpointRouting = false;
            });

            services.AddRazorPages()//添加RazorPages
                .AddRazorRuntimeCompilation()
            .ConfigureApplicationPartManager(m =>
            {
                //將標准web類庫的Controllers添加到定制版,即我們要運行的網站中
                var feature = new ControllerFeature();

                if (currentAssembly != null)
                {
                    m.ApplicationParts.Add(new AssemblyPart(currentAssembly));
                }
                m.PopulateFeature(feature);
                services.AddSingleton(feature.Controllers.Select(t => t.AsType()).ToArray());
            })
            .AddControllersAsServices()
            .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);//添加多語言支持

            //services.Configure<MvcRazorRuntimeCompilationOptions>(options =>
            //{
            //    if (currentAssembly != null)
            //    {
            //        options.FileProviders.Add(
            //        new EmbeddedFileProvider(
            //            currentAssembly,
            //            currentNamespace // your external assembly's base namespace
            //        )
            //    );
            //    }
            //});
            services.AddSingleton<ILoginUserService, LoginUserService>();//添加需要引用的其他服務

            services.AddMvc(options =>
            {
                options.Conventions.Add(new ApiControllerVersionConvention());//添加版本控制時忽略添加的某些重要屬性
            });

            services.AddApiVersioning(o => {
                o.ReportApiVersions = true;//返回版本可使用的版本
                //o.ApiVersionReader = new UrlSegmentApiVersionReader();
                //o.ApiVersionReader = ApiVersionReader.Combine(new HeaderApiVersionReader("api-version"), new QueryStringApiVersionReader("api-version"));
                //o.ApiVersionReader = ApiVersionReader.Combine(new QueryStringApiVersionReader("api-version"));
                o.ApiVersionReader = ApiVersionReader.Combine(new HeaderApiVersionReader("api-version"));//版本號以什么形式,什么字段傳遞
                o.AssumeDefaultVersionWhenUnspecified = true;
                o.DefaultApiVersion = new ApiVersion(1, 0);//默認版本號
                o.ApiVersionSelector = new CurrentImplementationApiVersionSelector(o);//默認以當前最高版本進行訪問
            });

            return services;
        }
        public static IApplicationBuilder UseFrameworkService(this IApplicationBuilder app, Action<IRouteBuilder> customRoutes = null)//在定制版本的Startup.ConfigureServices中添加services.UseFrameworkService();即可
{ app.UseExceptionHandler(
"/Home/Error"); app.UseStaticFiles(); app.UseAuthentication(); app.Use(async (context, next) => { try { await next.Invoke(); } catch (ConnectionResetException) { } if (context.Response.StatusCode == 404) { await context.Response.WriteAsync(string.Empty); } }); app.UseMiddleware<CustomRewriteMiddleware>(); if (customRoutes != null) { app.UseMvc(customRoutes); } else { app.UseMvc(routes => { routes.MapRoute( name: "areaRoute", template: "{area:exists}/{controller=Home}/{action=Index}/{id?}"); routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); } return app; }

 

我們在標准web類庫中,將所有的Controller都添加上默認的版本號1.0

    [ApiVersion("1.0")]
    [Route("[controller]/[action]")]
    [ApiController]
或者Areas中的添加
    [Area("User")]//User時Area的name
    [ApiVersion("1.0")]
    [Route("[area]/[controller]/[action]")]
    [ApiController]

我們的定制版本中,需要重寫的Controller添加上對應標准web類庫里面對應名字的Controller,對應的ApiVersion修改成大於1.0的版本號,新添加的Controller繼承自對應的標准web類庫的對應Controller

namespace Tailor.Custom3.Https.Controllers
{
    [ApiVersion("2.0")]
    [Route("[controller]/[action]")]
    [ApiController]
    public class HomeController : Default.Controllers.HomeController
    {
        private readonly ILogger<HomeController> _logger;
        private readonly ILoginUserService _userService;

        public HomeController(ILogger<HomeController> logger, ILoginUserService userService) : base(logger, userService)
        {
            _logger = logger;
            _userService = userService;
        }
     }
}

此時,我們如果需要對某些Action進行重寫,則override對應Action,然后進行重寫;//Tailor.Custom1.Https和Tailor.Custom3.Https

我們如果需要對某些cshtml進行重寫,則在對應目錄添加相同名字的cshtml,然后進行重寫;//Tailor.Custom2.Https中只對cshtml進行重寫,Tailor.Custom3.Https中對Controller和cshtml都進行重寫

此時我們就可以寫一個標准版web類庫,定制項目進行局部更改,如發現標准版web類庫出現bug,可以只修改一處,處處生成上傳即可;再進一步,我們可以將生成的標准版web類庫的dll文件上傳到指定的服務器特定目錄,其他服務器對此目錄進行定時的加載或者判斷版本再去加載,這樣就可以省去很大的精力

 

但是在實際的項目使用中發現,可能由於Microsoft.AspNetCore.Mvc.Versioning這個包本身的問題,當我們的標准web類庫中Controller有重名,但是不是同一個Views或者Areas目錄下時,我們的版本控制將會出現所有的同名Controller的可使用版本信息將會變成所有的控制版本。。。這個暫時可以利用不同Controller名字進行規避,詳見:https://github.com/microsoft/aspnet-api-versioning/issues/630 【已修復】

具體實現代碼地址:https://github.com/wangpengzong/Tailor

Native/Default是標准版網站類庫

Tailor.Custom* 是定制化網站,可以在此路徑下繼承Native/Default的對應Controller,利用overvide對需要重寫的action進行重寫,不需要重寫的不進行overvide即可,或者對cshtml進行重寫,不需要重寫的不在對應路徑下增加cshtml文件即可

 

      


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM