ASP.NET Core 沉思录 - 环境的思考

640?wx_fmt=jpeg

我的博客换新家啦,新的地址为:https://clrdaily.com :-D

今天我们来一起思考一下如何在不同的环境应用不同的配置。这里的配置不仅仅指 IConfiguration 还包含 IWebHostBuilder 的创建过程和 Startup 的初始化过程。

0 太长不读

  • 环境造成的差异在架构中基本体现在 Infrastructure 中的各个 Adapter 中。而不应当入侵应用程序内部

  • 在 ASP.NET Core 中我们需要考虑如何将这些 Adapter(一)放在 service collection 中 (二)(可选)添加到 pipeline 中。

  • ASP.NET Core 默认提供了一系列手段来判断当前的环境,只不过这些手段的设计奇怪且不完整。

  • IWebHostBuilder 的配制方法大多和环境相关,但 UseSetting 和环境无关。

  • 我们应当应用开闭原则,将相同环境的配置聚合起来,不同环境的配置进行统一抽象。方便维护和扩展。

  • 当我们进行设计的时候,需要注意不要将思路局限在 Framework 的设计上,而应当切实考虑我们真正希望解决的问题。

1 架构层面的思考

Web Service 的开发和部署过程会涉及若干环境。总的来说可以分为开发环境和部署环境。而部署环境往往又分为 QA、Stage 和 Production 等。对于不同的环境,应用程序可能需要应用不同的配置或实现。还是回到架构的层面上,如下图:

640?wx_fmt=png

那么这种不同应该体现在架构的哪一个层面上呢?应当让这些不同体现在 Infrastructure 的那些 Adapters 上。因为 Adapter 是其中直接和环境相关的部分。

用一个典型的例子来表示。假定一个注册用户 Account 的业务。在 Application Service 层面,我们提供了如下的接口:

public class AccountRegistrationService {
    public AccountRegistrationResult Register(AccountRegistrationRequest request{
        Account account = this.repository.CreateDetached();
        // initialize account from request
        account.Save();
        return AccountRegistrationResult.Create(account);
    }
}

在 Domain 层面我们有代表领域对象 Account 的类型 AccountAccount 类型的 Save() 方法可以保存账户信息,其中的实现类似:

public class Account {
    ...

    public void Save() {
        this.repository.Save(this);
    }
}

而其中的 repository 则依赖 UnitOfWork 而 UnitOfWork 则可能依赖于具体的持久化实现或者依赖于其他远程服务:

public class AccountRepository {
    readonly IUnitOfWork session;

    public Account CreateDetached() {
        return new Account(this);
    }

    public void Save(Account account{
        this.session.RegisterNew(account);
    }
}

在这个例子中,AccountService 属于 Application Service 层面,AccountAccountRepository 则属于 Domain 层面。这两层的依赖关系是 Application Service 依赖于 Domain。而 Domain 中的 UnitOfWork 则是一个接口。假设我们需要将数据写入数据库。则这个接口的实现需要持久化的支持例如它需要使用特定的 IDbConnection (Adapter)。即 IUnitOfWork 的实现位于 Infrastructure 层,并在 Infrastructure 层调用 Adapter 向 DB 中写入信息。

而对于不同的环境则可以使用不同的实现,例如,对于运行单元测试的环境,我们不妨叫她 Test 环境。这个 DB 很有可能是一个 in memory 的 SQLite 数据库。而在生产环境则是 MySQL 的集群。

应用程序的内部逻辑最终全部依赖与特定的抽象或接口。它们全部严密的包裹在 Infrastructure 之中,并和外部环境完全隔离。而 Infrastructure 中的 Adapter 则负责联系外部环境。综上所述,环境相关的变化应当全部封闭在 Infrastructure 中。

2 ASP.NET Core 中的对应关系

ASP.NET Core 应用程序中的组件的初始化由两个部分构成,第一个部分就是将组件中的类型添加到依赖注入的 IServiceCollection 实例中,以便进行创建;第二个部分(可选)即将组件通过 IApplicationBuilder 添加到应用程序的处理流水线中。我们一个一个来思考。

2.1 依赖注入

ASP.NET Core Web Application 中用依赖注入来决定某种抽象的实现类型。但需要指出的是 ASP.NET 应用程序的依赖注入是分两个阶段进行的。(我们将在另外一篇中介绍),简单来说 ServiceCollection 的构建分为两个部分:

  • 为了构建宿主环境而添加的类型;(Infrastructure 层)

  • 为了应用程序本身而添加的 Framework(例如 MvC)和各种业务类型。(Infrastructure 层,Application + Domain 层)。

而和环境相关的部分主要位于 “为了构建宿主环境而添加的类型” 中。这一部分的代码属于在 IStartup 初始化之前的 WebHostBuilder 构建代码中。一般来说,我们习惯于将 UseStartup 调用放在 IWebHostBuilder 实例创建的最后,那么也就是 UseStartup 之前的代码:

public static IWebHostBuilder CreateWebHostBuilder(string[] args)
{
    return new WebHostBuilder()
        .UseKestrel()
        .ConfigureLogging(...)
        //
        // The configurations before UseStartup are environment specific
        //
        .UseStartup<Startup>();
}

2.2 流水线

在流水线配置中主要考虑的是 Web 输入输出上的的变化。例如 Production 环境需要配置 SSL,消除敏感 Header,消除详细的 Error Information 等等。

将组件配置到应用程序的流水线的操作是在 IStartup 接口的实现中进行的。定义 IStartup 接口实现的方式大体有两种,第一种是调用 WebHostBuilderExtensions.Configure 方法,另一种是使用 WebHostBuilderExtensions.UseStartup 方法。不论使用何种方式最终都会归结到对 IApplicationBuilder 的操作:

public void Configure(IApplicationBuilder app{
    // building pipeline
}

在这个时候,宿主初始化相关的类型已经全部可以使用了。因此取用环境相关的信息(环境类型,配置等)就更方便了。

3 落地

ASP.NET Core 对这个环节的设计很奇怪。一方面,它提供了非常底层的基于 IHostingEnvironment.EnvironmentName 的值来进行环境区分的方法。例如,官方范例中往往会使用如下的代码:

new WebHostBuilder()
    .UseKestrel()
    .ConfigureLogging((context, logBuilder) => {
        if (context.HostingEnvironment.IsDevelopment()) {
            ...
        }
        else if (context.HostingEnvironment.IsProduction()) {
            ...
        }
        else {
            ...
        }
    })
    ...

而另一方面却又在 Startup 上设计了命名的 Convension。例如:

class DevelopmentStartup {}     // for Development
class ProductionStartup {}      // for Production
class Startup {}                // fallback

...

webHostBuilder.UseStartup(assemblyName);

又例如:

class Startup  {
    public void ConfigureServices(IServiceCollection services{ }
    public void ConfigureStagingServices(IServiceCollection services{ }
    public void Configure(IApplicationBuilder app, IHostingEnvironment env{ }
    public void ConfigureStaging(IApplicationBuilder app, IHostingEnvironment env{ }
}

这些设计差异很大且每一个都不彻底。而在实际项目中环境属于一个扩展点;而每一套环境的各项配置应当是内聚的。因此上述几种方式或多或少会增加维护上的成本。而较好的设计应当针对如下三个问题:

  • 能够立刻说出,我的系统支持几种环境;

  • 每一种环境的各种类型的配置(例如,配置源、日志记录、HTTP Client、数据库)是什么样子的,有什么差异;

  • 能不能用两步添加一个新的环境:第一,一次性创建一个新环境的所有配置,第二,将这个环境纳入到系统初始化过程中。

为了达到这个要求,需要考虑统一的实现手段。

3.1 在 WebHost 开始构建之前我们并不能确定环境信息

一个最简单的想法就是根据不同的环境采取两种完全不同的 WebHostBuilder 配置流程。例如:

WebHostBuilder builder = new WebHostBuilderFactory().Create(env.EnvironmentName);

遗憾的是这种设计本身是有问题的。首先,若干环节都可以影响环境的最终确定,包括:

  • 当前 Session 的 ASPNETCORE_ENVIRONMENT 的值;(请参见 https://github.com/aspnet/AspNetCore/blob/master/src/Hosting/Hosting/src/WebHostBuilder.cs#L44)

  • Properties/launchSettings.json 中选定 Profile 中 ASPNETCORE_ENVIRONMENT 的值(如果用 dotnet run 命令执行的话)

  • WebHostBuilder.UseEnvironment(name) 的参数值;

  • WebHostBuilder.UseSetting(key, value)keyWebHostDefaults.EnvironmentKey 时的值。

  • 若 Host 在 IIS 中,则 web.config 中关于 environmentVariable 的设置。

因此只有在 WebHostBuilder 开始 Build 时,我们才可以最终确定环境名称。

3.2 `UseSetting` 并不是环境相关的

另一种方案是包装 IWebHostBuilder 使其能够依据环境做出相应的 Dispatch。例如:

abstract class EnvironmentAwareWebHostBuilder : IWebHostBuilder {
    IWebHostBuilder UnderlyingBuilder { get; }
    protected abstract bool IsSupported(IHostingEnvironment hostingEnvironment);

    protected EnvironmentAwareWebHostBuilder(IWebHostBuilder underlyingBuilder)
    
{
        // Validation omitted
        UnderlyingBuilder = underlyingBuilder;
    }

    // ...
}

从而我们可以分别为不同的环境进行相应的配置。以 ConfigureService 方法为例:

public IWebHostBuilder ConfigureServices(Action<IServiceCollection> configureServices)
{
    UnderlyingBuilder.ConfigureServices(
        (context, services) =>
        {
            if (!IsSupported(context.HostingEnvironment)) { return; }

            configureServices(services);
        });
    return this;
}

按照上述方式包装 ConfigureAppConfiguration,这样就可以构造以下的扩展方法:

public static IWebHostBuilder UseEnvironment(
    this IWebHostBuilder builder,
    string environmentName, 
    Action<IWebHostBuilder> configureBuilder
)
{
    bool IsEnvironmentSupported(IHostingEnvironment h=> 
        h.IsEnvironment(config.environmentName);

    EnvironmentAwareWebHostBuilder environmentAwareBuilder =
        new DelegatedWebHostBuilder(builder, IsEnvironmentSupported);
    config.configureBuilder(environmentAwareBuilder);

    return builder;
}

这种方案下的 WebHostBuilder 初始化逻辑就变成了:

webHostBuilder
    .UseEnvironment("Development", wb => {
        wb
            .ConfigureService((ctx, cb) => { ... })
            .ConfigureLogging((lb) => { ... })
            ...
    })
    .UseEnvironment("Production", wb => {
        // configure for production
    });

这样我们至少就可以用若干扩展方法类将不同环境完全分开了。但是这个实现方案是有问题的:UseSetting 方法。IWebHostBuilder 所公开的方法中除了 BuildConfigureServicesConfigureAppConfiguration 之外还有第四个方法:UseSetting。和上述 ConfigureXxx 方法不同,UseSetting 方法执行完毕之后其影响马上生效,而且该方法无法根据不同的环境作出变化。即,如果我们使用了:

webHostBuilder
    .UseEnvironment("Development", wb => wb.UseSetting("Foo""Bar"))
    .UseEnvironment("Production", wb => wb.UseSetting("Foo""O_o"));

且当前环境为 DevelopmentIConfiguration 实例的 "Foo" 对应的值为 "O_o"。这就会造成混淆。

3.3 还是从扩展点来思考

从第 2 节的论述中我们已经知道和环境相关的配置可能存在于宿主环境初始化过程中,也可能存在 Startup 初始化过程中(即 WebHost.Run 方法执行过程中)。因此我们必须综合考虑这两个部分,但是这个两个部分天生是不同的。那么强行进行统一也是不合适的。

根据开闭原则,我们还是应该从扩展点上来考虑。首先我们能够确定我们的 Adapter 有哪些。又有哪一些 Adapter 是和环境相关的。例如我们和环境相关的 Adapter 有 DB,配置文件加载,日志记录,HttpClient(在非 Development 环境中我们可能需要进行客户端证书验证),在流水线创建过程中需要根据环境配置是否需要 HTTPS 强制跳转,需要配置错误信息的详细程度等等。在梳理好这些内容后我们就能有针对性的创建方法对各个部分进行配置了,我们可以使用工厂模式:

class WebHostConfigureFactory {
    ...

    public IWebHostConfigurator Create(string environmentName{
        return cachedConfigurators[environmentName];
    }
}

而每一个 IWebHostConfigurator 中都包含了所有的环境相关配置:

interface IWebHostConfigurator {
    void AddDatabase(IHostingEnvironment environment, IServiceCollection services);
    void LoadConfiguration(IHostingEnvironment environment, IConfigurationBuilder configBuilder);
    void ConfigureLogging(IHostingEnvironment environment, ILoggingBuilder loggingBuilder);
    void AddHttpClient(IHostingEnvironment environment, IServiceCollection services);
    void ConfigureHttpsRedirection(IHostingEnvironment environment, IConfiguration configuration, IApplicationBuilder builder);
    void ConfigureErrorHandler(IHostingEnvironment environment,  IConfiguration configuration, IApplicationBuilder builder);
}

而这样我们为各个环境的扩展点建立了抽象,从而统一配置过程:

static IWebHostBuilder CreateWebHostBuilder() {
    return new WebHostBuilder()
        .UseKestrel()
        //
        // Common configurations
        //
        .ConfigureServices((context, services) => {
            IWebHostConfigurator configurator = factory.Create(context.HostingEnvironment.EnvironmentName);

            configurator.AddDatabase(context.HostingEnvironment, services);
            configurator.AddHttpClient(context.HostingEnvironment, services);
        })
        .ConfigureLogging((context, logBuilder) => {
            factory
                .Create(context.HostingEnvironment.EnvironmentName)
                .ConfigureLogging(context.HostingEnvironment, logBuilder);
        })
        .UseStartup<Startup>();
}

...

class Startup {
    ...

    public void Configure(IApplicationBuilder app{
        IWebHostConfigurator configurator = factory.Create(hostingEnvironment.EnvironmentName);
        configurator.ConfigureHttpsRedirection(hostingEnvironment, configuration, app);
        configurator.ConfigureErrorHandler(hostingEnvironment, configuration, app);

        // Common configurations
    }
}

4 总结

请跳到文章开头 :-D

参考资料

  • R. C. Martin and R. C. Martin, Clean architecture: a craftsman’s guide to software structure and design. London, England: Prentice Hall, 2018.

  • Unit-of-work: https://martinfowler.com/eaaCatalog/unitOfWork.html

  • Dependency Injection in ASP.NET Core: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.2

  • App startup in ASP.NET Core: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/startup?view=aspnetcore-2.2

  • Use multiple environments in ASP.NET Core: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/environments?view=aspnetcore-2.2

如果您觉得本文对您有帮助,也欢迎分享给其他的人。我们一起进步。欢迎关注我的微信公众号:

640?wx_fmt=jpeg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/316778.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深度:从 Office 365 新图标来看微软背后的设计新理念

开始表演请关注我的公众号“寒树Office”来获取一些新鲜而有趣的新闻与知识&#xff0c;最近又有两家俱乐部上线了&#xff08;东莞与长沙&#xff09;&#xff0c;俱乐部的活动告一段落&#xff0c;接下来的日子里我将持续与大家分享 Office 365 的精彩内容&#xff0c;这次很…

NET Core微服务之路:实战SkyWalking+Exceptionless体验生产下追踪系统

前言当一个APM或一个日志中心实际部署在生产环境中时&#xff0c;是有点力不从心的。比如如下场景分析的问题&#xff1a;从APM上说&#xff0c;知道某个节点出现异常&#xff0c;或延迟过过高&#xff0c;却不能及时知道日志反馈情况&#xff0c;总不可能去相应的节点上一个一…

.NET 中创建支持集合初始化器的类型

对象初始化器和集合初始化器只是语法糖&#xff0c;但是能让你的代码看起来更加清晰。至少能让对象初始化的代码和其他业务执行的代码分开&#xff0c;可读性会好一些。本文将编写一个类型&#xff0c;可以使用集合初始化器构造这个类型。不只是添加元素的集合初始化器&#xf…

【无码专区8】三角形二维数点——计数有多少个给定点落在三角形区域内

因为只有std&#xff0c;没有自我实现&#xff0c;所以是无码专区 主要是为了训练思维能力 solution才是dls正解&#xff0c;但是因为只有潦草几句&#xff0c;所以大部分会有我自己基于正解上面的算法实现过程&#xff0c;可能选择的算法跟std中dls的实现不太一样。 std可能…

为什么我的会话状态在ASP.NET Core中不工作了?

原文&#xff1a;Why isnt my session state working in ASP.NET Core? Session state, GDPR, and non-essential cookies作者&#xff1a;Andrew Lock译文&#xff1a;https://www.cnblogs.com/lwqlun/p/10526380.html译者&#xff1a;Lamond Lu在本篇博客中&#xff0c;我将…

现身说法:实际业务出发分析百亿数据量下的多表查询优化

今天给大家带来的讨论主题是通过实战经验来对百亿数据量下的多表数据查询进行优化&#xff0c;俗话说的好&#xff0c;一切脱离业务的架构都是耍流氓&#xff0c;接下来我就整理一下今天早上微信群里石头哥给大家分享的百亿数据量多表查询架构以及优化思路。由于本文内容整理自…

Help Jimmy POJ - 1661

Help Jimmy POJ - 1661 题意&#xff1a; 场景中包括多个长度和高度各不相同的平台。地面是最低的平台&#xff0c;高度为零&#xff0c;长度无限。 Jimmy老鼠在时刻0从高于所有平台的某处开始下落&#xff0c;它的下落速度始终为1米/秒。当Jimmy落到某个平台上时&#xff0c…

ASP.NET Core 沉思录 - ServiceProvider 的二度出生

ASP.NET Core 终于将几乎所有的对象创建工作都和依赖注入框架集成了起来。并对大部分的日常工作进行了抽象。使得整个框架扩展更加方便。各个部分的集成也更加容易。今天我们要思考的部分仍然是从一段每一个工程中都大同小异的代码开始的。IWebHostBuilder CreateWebHostBuilde…

Acwing 216. Rainbow的信号

Acwing 216. Rainbow的信号 题意&#xff1a; 给你n个数&#xff0c;在这n个数中&#xff0c;等概率地选取两个数l&#xff0c;r&#xff0c;如果l>r,则交换l,r 把信号中的第 l 个数到第 r 个数取出来&#xff0c;构成一个数列 P。 A 部分对话的密码是数列 P 的 xor 和的…

合肥.NET技术社区首次线下聚会全程回顾【多图】

2019年3月16日对于合肥.NET来说是一个特别的日子&#xff0c;因为这是合肥.NET技术社区首次非正式线下聚会&#xff01;这次聚会受场地限制&#xff08;毕竟是聚餐的形式&#xff09;&#xff0c;即使换成了小椅子后&#xff0c;最多也只能容纳24个人&#xff0c;所以还有一些小…

SignalR第一节-在5分钟内完成通信连接和消息发送

前言首先声明&#xff0c;这又是一个小白从入门到进阶系列。 SignalR 这个项目我关注了很长时间&#xff0c;中间好像还看到过微软即将放弃该项目的消息&#xff0c;然后我也就没有持续关注了&#xff0c;目前的我项目中使用的是自己搭建的 WebSocket &#xff0c;连接管理和消…

【学习笔记】信息学竞赛中的概率与期望小结

信息竞赛——概率与期望事件事件的蕴含、包含事件的互斥事件的对立事件的和&#xff08;并&#xff09;事件的积&#xff08;交&#xff09;事件的差概率事件的独立性全概率公式贝叶斯公式概率DP&#xff08;竞赛中的考察&#xff09;期望&#xff08;竞赛中的考察&#xff09;…

尝试:Script Lab,快速 O365 开发工具//SL01)

《前言》Script Lab 我希望有一个系列&#xff08;连载&#xff09;&#xff0c;可是我挺担心没偿没有能力去驾驭它。虽然早年前己经接触过&#xff0c;但一直未有下决心开始 Office 365 的开发之旅&#xff0c;虽然一直被光标老师所鼓舞&#xff0c;但是我心有旁骛还没有真正做…

Keiichi Tsuchiya the Drift King

Keiichi Tsuchiya the Drift King 题意&#xff1a; 给定一辆小车长宽分别为 b&#xff0c;a&#xff0c;轨道的圆弧部分半径为 r&#xff0c;圆弧对应的角度为 d&#xff0c;求出小车能通过轨道的最小轨道宽度 w。 题解&#xff1a; 我们考虑小车处于什么状态会使弯道最宽…

AspNet Core 下利用普罗米修斯+Grafana构建Metrics和服务器性能的监控

概述Prometheus是一套开源的监控&报警&时间序列数据库的组合,起始是由SoundCloud公司开发的。该项目有非常活跃的社区和开发人员&#xff0c;目前是独立的开源项目&#xff0c;现在最常见的Kubernetes容器管理系统中&#xff0c;通常也会搭配Prometheus进行监控。prome…

软件工程真的是一门什么用都没有的学科么?

软件工程真的是一门什么用都没有的学科么&#xff1f;-----读《构建之法》有感楔子我很惭愧&#xff0c;构建之法这本书已经出版四五年了&#xff0c;我之前却未曾涉猎&#xff0c;还是在通过组织长沙.net技术社区之后&#xff0c;才因为因缘际遇有幸认识邹欣邹老师之后&#x…

Ball Dropping

Ball Dropping 题意&#xff1a; 求&#xff1f;的具体长度 题解&#xff1a; 算一算就出来了 代码&#xff1a; #include<bits/stdc.h> using namespace std; int main(){double r,a,b,h;cin>>r>>a>>b>>h;if(2*r<b&&2*r<…

尝试:Script Lab,开发模式之知识储备//SL02

前期00&#xff1a;深度&#xff1a;从 Office 365 新图标来看微软背后的设计新理念前期01&#xff1a;尝试&#xff1a;Script Lab&#xff0c;快速 Office 365 开发工具 //SL01本期02&#xff1a;尝试&#xff1a;Script Lab&#xff0c;开发模式之知识储备 //SL02项目特点适…

WebApi网关之Bumblebee和Ocelot性能对比

Bumblebee是基于.net core 2.1开发的WebApi网关组件&#xff0c;由于Bumblebee所追求的轻量化和性能&#xff0c;所以它并没有像Ocelot那样从asp.net core上进行扩展&#xff1b;而是构建在BeetleX.FastHttpApi之上&#xff0c;主要原因BeetleX.FastHttpApi有着更轻量化和高性能…

在 .NET Core 中运行 JavaScript

一.前言在 .NET Framework 时&#xff0c;我们可以通过V8.NET等组件来运行 JavaScript&#xff0c;不过目前我看了好几个开源组件包括V8.NET都还不支持 .NET Core &#xff0c;我们如何在 .NET Core 中运行 JavaScript 呢&#xff0c;答案是使用 NodeServices。关于为何有在 .N…