加载中...
.netcore 定制化项目开发的思考和实现
发表于:2020-05-06 |
字数统计: 4.3k | 阅读时长: 22分钟 |

今年年初进了一家新公司,进入之后一边维护老项目一边了解项目流程,为了接下来的项目重做积累点经验。

先说下老项目吧,.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();

        </span><span style="color: #0000ff;">var</span> configBuilder = <span style="color: #0000ff;">new</span><span style="color: #000000;"> ConfigurationBuilder();

        </span><span style="color: #0000ff;">if</span> (!File.Exists(Path.Combine(Directory.GetCurrentDirectory(), <span style="color: #800000;">"</span><span style="color: #800000;">appsettings.json</span><span style="color: #800000;">"</span><span style="color: #000000;">)))
        {
            </span><span style="color: #0000ff;">var</span> binLocation = Assembly.GetEntryAssembly()?<span style="color: #000000;">.Location;
            </span><span style="color: #0000ff;">if</span> (!<span style="color: #0000ff;">string</span><span style="color: #000000;">.IsNullOrEmpty(binLocation))
            {
                </span><span style="color: #0000ff;">var</span> binPath = <span style="color: #0000ff;">new</span> FileInfo(binLocation).Directory?<span style="color: #000000;">.FullName;
                </span><span style="color: #0000ff;">if</span> (File.Exists(Path.Combine(binPath, <span style="color: #800000;">"</span><span style="color: #800000;">appsettings.json</span><span style="color: #800000;">"</span><span style="color: #000000;">)))
                {
                    Directory.SetCurrentDirectory(binPath);
                    configBuilder.SetBasePath(binPath)
                        .AddJsonFile(</span><span style="color: #800000;">"</span><span style="color: #800000;">appsettings.json</span><span style="color: #800000;">"</span>, optional: <span style="color: #0000ff;">true</span>, reloadOnChange: <span style="color: #0000ff;">true</span><span style="color: #000000;">)
                        .AddEnvironmentVariables();
                }
            }
        }
        </span><span style="color: #0000ff;">else</span><span style="color: #000000;">
        {
            configBuilder.SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile(</span><span style="color: #800000;">"</span><span style="color: #800000;">appsettings.json</span><span style="color: #800000;">"</span>, optional: <span style="color: #0000ff;">true</span>, reloadOnChange: <span style="color: #0000ff;">true</span><span style="color: #000000;">)
                .AddEnvironmentVariables();
        }

        </span><span style="color: #0000ff;">if</span> (webHostBuilderContext != <span style="color: #0000ff;">null</span><span style="color: #000000;">)
        {
            </span><span style="color: #0000ff;">var</span> env =<span style="color: #000000;"> webHostBuilderContext.HostingEnvironment;
            configBuilder
                .AddJsonFile($</span><span style="color: #800000;">"</span><span style="color: #800000;">appsettings.{env.EnvironmentName}.json</span><span style="color: #800000;">"</span>, optional: <span style="color: #0000ff;">true</span>, reloadOnChange: <span style="color: #0000ff;">true</span><span style="color: #000000;">);
        }

        </span><span style="color: #0000ff;">var</span> config =<span style="color: #000000;"> configBuilder.Build();

        </span><span style="color: #0000ff;">new</span> AppSettingProvider().Initial(config);<span style="color: #008000;">//</span><span style="color: #008000;">添加静态的配置全局配置文件</span>

        <span style="color: #0000ff;">var</span> gd =<span style="color: #000000;"> AssemblyHelper.GetGlobalData();

        </span><span style="color: #0000ff;">var</span> currentNamespace =<span style="color: #000000;"> MethodBase.GetCurrentMethod().DeclaringType.Namespace;
        </span><span style="color: #008000;">//</span><span style="color: #008000;">获取标准web类库的Assembly</span>
        <span style="color: #0000ff;">var</span> currentAssembly = gd.AllAssembly.Where(x =&gt; x.ManifestModule.Name == $<span style="color: #800000;">"</span><span style="color: #800000;">{currentNamespace}.dll</span><span style="color: #800000;">"</span><span style="color: #000000;">).FirstOrDefault();

        StackTrace ss </span>= <span style="color: #0000ff;">new</span> StackTrace(<span style="color: #0000ff;">true</span><span style="color: #000000;">);
        MethodBase mb </span>= ss.GetFrame(ss.FrameCount - <span style="color: #800080;">1</span><span style="color: #000000;">).GetMethod();

        </span><span style="color: #0000ff;">var</span> userNamespace = mb.DeclaringType.Namespace;<span style="color: #008000;">//</span><span style="color: #008000;">调用标准web类库的定制版项目命名空间</span>
services.AddMvc(options => { options.EnableEndpointRouting = false; });
        services.AddRazorPages()</span><span style="color: #008000;">//</span><span style="color: #008000;">添加RazorPages</span>

.AddRazorRuntimeCompilation()
.ConfigureApplicationPartManager(m
=>
{
//将标准web类库的Controllers添加到定制版,即我们要运行的网站中
var feature = new ControllerFeature();

            </span><span style="color: #0000ff;">if</span> (currentAssembly != <span style="color: #0000ff;">null</span><span style="color: #000000;">)
            {
                m.ApplicationParts.Add(</span><span style="color: #0000ff;">new</span><span style="color: #000000;"> AssemblyPart(currentAssembly));
            }
            m.PopulateFeature(feature);
            services.AddSingleton(feature.Controllers.Select(t </span>=&gt;<span style="color: #000000;"> t.AsType()).ToArray());
        })
        .AddControllersAsServices()
        .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);</span><span style="color: #008000;">//</span><span style="color: #008000;">添加多语言支持

        </span><span style="color: #008000;">//</span><span style="color: #008000;">services.Configure&lt;MvcRazorRuntimeCompilationOptions&gt;(options =&gt;
        </span><span style="color: #008000;">//</span><span style="color: #008000;">{
        </span><span style="color: #008000;">//</span><span style="color: #008000;">    if (currentAssembly != null)
        </span><span style="color: #008000;">//</span><span style="color: #008000;">    {
        </span><span style="color: #008000;">//</span><span style="color: #008000;">        options.FileProviders.Add(
        </span><span style="color: #008000;">//</span><span style="color: #008000;">        new EmbeddedFileProvider(
        </span><span style="color: #008000;">//</span><span style="color: #008000;">            currentAssembly,
        </span><span style="color: #008000;">//</span><span style="color: #008000;">            currentNamespace </span><span style="color: #008000;">//</span><span style="color: #008000;"> your external assembly's base namespace
        </span><span style="color: #008000;">//</span><span style="color: #008000;">        )
        </span><span style="color: #008000;">//</span><span style="color: #008000;">    );
        </span><span style="color: #008000;">//</span><span style="color: #008000;">    }
        </span><span style="color: #008000;">//</span><span style="color: #008000;">});</span>
        services.AddSingleton&lt;ILoginUserService, LoginUserService&gt;();<span style="color: #008000;">//</span><span style="color: #008000;">添加需要引用的其他服务</span>
services.AddMvc(options => { options.Conventions.Add(new ApiControllerVersionConvention());//添加版本控制时忽略添加的某些重要属性 });
        services.AddApiVersioning(o </span>=&gt;<span style="color: #000000;"> {
            o.ReportApiVersions </span>= <span style="color: #0000ff;">true</span>;<span style="color: #008000;">//</span><span style="color: #008000;">返回版本可使用的版本
            </span><span style="color: #008000;">//</span><span style="color: #008000;">o.ApiVersionReader = new UrlSegmentApiVersionReader();
            </span><span style="color: #008000;">//</span><span style="color: #008000;">o.ApiVersionReader = ApiVersionReader.Combine(new HeaderApiVersionReader("api-version"), new QueryStringApiVersionReader("api-version"));
            </span><span style="color: #008000;">//</span><span style="color: #008000;">o.ApiVersionReader = ApiVersionReader.Combine(new QueryStringApiVersionReader("api-version"));</span>
            o.ApiVersionReader = ApiVersionReader.Combine(<span style="color: #0000ff;">new</span> HeaderApiVersionReader(<span style="color: #800000;">"</span><span style="color: #800000;">api-version</span><span style="color: #800000;">"</span>));<span style="color: #008000;">//</span><span style="color: #008000;">版本号以什么形式,什么字段传递</span>
            o.AssumeDefaultVersionWhenUnspecified = <span style="color: #0000ff;">true</span><span style="color: #000000;">;
            o.DefaultApiVersion </span>= <span style="color: #0000ff;">new</span> ApiVersion(<span style="color: #800080;">1</span>, <span style="color: #800080;">0</span>);<span style="color: #008000;">//</span><span style="color: #008000;">默认版本号</span>
            o.ApiVersionSelector = <span style="color: #0000ff;">new</span> CurrentImplementationApiVersionSelector(o);<span style="color: #008000;">//</span><span style="color: #008000;">默认以当前最高版本进行访问</span>

});

        </span><span style="color: #0000ff;">return</span><span style="color: #000000;"> services;
    }</span></pre>
        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(</span><span style="color: #0000ff;">async</span> (context, next) =&gt;<span style="color: #000000;">
        {
            </span><span style="color: #0000ff;">try</span><span style="color: #000000;">
            {
                </span><span style="color: #0000ff;">await</span><span style="color: #000000;"> next.Invoke();
            }
            </span><span style="color: #0000ff;">catch</span><span style="color: #000000;"> (ConnectionResetException) { }
            </span><span style="color: #0000ff;">if</span> (context.Response.StatusCode == <span style="color: #800080;">404</span><span style="color: #000000;">)
            {
                </span><span style="color: #0000ff;">await</span> context.Response.WriteAsync(<span style="color: #0000ff;">string</span><span style="color: #000000;">.Empty);
            }
        });

        app.UseMiddleware</span>&lt;CustomRewriteMiddleware&gt;<span style="color: #000000;">();

        </span><span style="color: #0000ff;">if</span> (customRoutes != <span style="color: #0000ff;">null</span><span style="color: #000000;">)
        {
            app.UseMvc(customRoutes);
        }
        </span><span style="color: #0000ff;">else</span><span style="color: #000000;">
        {
            app.UseMvc(routes </span>=&gt;<span style="color: #000000;">
            {
                routes.MapRoute(
                    name: </span><span style="color: #800000;">"</span><span style="color: #800000;">areaRoute</span><span style="color: #800000;">"</span><span style="color: #000000;">,
                    template: </span><span style="color: #800000;">"</span><span style="color: #800000;">{area:exists}/{controller=Home}/{action=Index}/{id?}</span><span style="color: #800000;">"</span><span style="color: #000000;">);
                routes.MapRoute(
                    name: </span><span style="color: #800000;">"</span><span style="color: #800000;">default</span><span style="color: #800000;">"</span><span style="color: #000000;">,
                    template: </span><span style="color: #800000;">"</span><span style="color: #800000;">{controller=Home}/{action=Index}/{id?}</span><span style="color: #800000;">"</span><span style="color: #000000;">);
            });

        }

        </span><span style="color: #0000ff;">return</span><span style="color: #000000;"> app;
    }</span></pre>

 

我们在标准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;

    </span><span style="color: #0000ff;">public</span> HomeController(ILogger&lt;HomeController&gt; logger, ILoginUserService userService) : <span style="color: #0000ff;">base</span><span style="color: #000000;">(logger, userService)
    {
        _logger </span>=<span style="color: #000000;"> logger;
        _userService </span>=<span style="color: #000000;"> 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文件即可

 

      

上一篇:
api.versioning 版本控制 自动识别最高版本和多Area但同名Contoller问题解决办法
下一篇:
netcore 非注入全局获取配置文件
本文目录
本文目录