学习ASP.NET Core, 怎能不了解请求处理管道[4]: 应用的入口——Startup

一个ASP.NET
Core应用被启动之后就具有了针对请求的处理能力,而这个能力是由管道赋予的,所以应用的启动同时意味着管道的成功构建。由于管道是由注册的服务器和若干中间件构成的,所以应用启动过程中一个核心的工作就是完成中间节的注册。由于依赖注入在ASP.NET

Core应用这得到非常广泛的应用,框架绝大部分的工作都会分配给我们预先注册的服务,所以服务注册也是启动WebHost过程的另一项核心工作。这两项在启动过程中必须完成的核心工作通过一个名为Startup的对象来承载。
[本文已经同步到《ASP.NET Core框架揭秘》之中]

目录

一、 DelegateStartup
二、ConventionBasedStartup
    StartupMethods
    StartupLoader
    如何选择启动类型
    如何选择服务注册方法和中间件注册方法
    StartupMethods对象的创建
    UseStartup方法究竟做了些什么?
三、选择哪一个Startup

这里所谓的Startup实际上是对所有实现了IStartup接口的所有类型以及对应对象的统称。如下面的代码片段所示,服务注册由ConfigureServices方法来实现,它返回一个ServiceProvider对象,至于另一个方法Configure则负责完成中间件的注册,方法输入参数是一个ApplicationBuilder对象。

   1: public interface IStartup
   2: {
   3:     IServiceProvider ConfigureServices(IServiceCollection services);
   4:     void Configure(IApplicationBuilder app);
   5: }

IStartup接口所在的NuGet包中还定义了另一个实现了这个接口的抽象类StartupBase。如下面的代码片段所示,StartupBase实现了抽象方法ConfigureServices,该方法直接利用提供的ServiceCollection对象创建返回的ServiceProvider。换句话说,派生于StartupBase的Startup类型如果没用重写ConfigureServices方法,它们实际上只关心中间件的注册,而不需要注册额外的服务。

   1: public abstract class StartupBase : IStartup
   2: {
   3:     public abstract void Configure(IApplicationBuilder app);
   4:     public virtual IServiceProvider ConfigureServices(IServiceCollection services)
   5:     {
   6:         return services.BuildServiceProvider();
   7:     }
   8: }

一、 DelegateStartup

我们来想想具体的应用中是如何注册中间件和服务的。中间件的注册可以采用两种方式,最简单的方式就是直接调用IWebHostBuilder的Configure方法。如下面的代码片段所示,这个方法直接上是借助于一个类型为Action<IApplicationBuilder>的委托对象将中间件注册到提供的ApplicationBuilder对象上。

   1: public static class WebHostBuilderExtensions
   2: {
   3:     public static IWebHostBuilder Configure(this IWebHostBuilder hostBuilder, Action<IApplicationBuilder> configureApp);
   4: }
   5:  
   6: new WebHostBuilder()
   7:     .Configure(app => app
   8:         .UseExceptionHandler("/Home/Error")
   9:         .UseStaticFiles()
  10:         .UseIdentity()
  11:         .UseMvc())
  12:     …

如果我们在应用中通过调用上面这个Configure方法来注册所需的中间件,WebHost在启动的时候会创建一个类型为DelegateStartup的Startup对象来完成真正的中间件注册工作。如下面的代码片段所示,DelegateStartup派生于StartupBase这个抽象类,它利用一个在构造时提供的Action<IApplicationBuilder>对象实现了Configure方法。很明显,我们调用IWebHostBuilder的Configure方法指定的Action<IApplicationBuilder>对象将用来创建这个DelegateStartup对象。

   1: public class DelegateStartup : StartupBase
   2: {
   3:     private Action<IApplicationBuilder> _configureApp;
   4:  
   5:     public DelegateStartup(Action<IApplicationBuilder> configureApp)
   6:     {
   7:         _configureApp = configureApp;
   8:     }
   9:  
  10:     public override void Configure(IApplicationBuilder app)
  11:     {
  12:         _configureApp(app);
  13:     }
  14: }

如下的代码片段体现了
IWebHostBuilder的扩展方法Configure的实现逻辑。如下面的代码片段所示,这个方法根据提供的Action<IApplicationBuilder>对象创建了一个DelegateStartup对象,并调用ConfigureServices方法以淡例模式注册到WebHostBuilder的服务集合中。这段代码还体现了另一个细节,除了进行Startup的服务注册之外,该方法还对“ApplicationName”选项(对应WebHostOptions的ApplicationName)进行了设置。

   1: public static class WebHostBuilderExtensions
   2: {    
   3:     public static IWebHostBuilder Configure(this IWebHostBuilder hostBuilder, Action<IApplicationBuilder> configureApp)
   4:     {
   5:         var startupAssemblyName = configureApp.GetMethodInfo().DeclaringType.GetTypeInfo().Assembly.GetName().Name;
   6:         return hostBuilder
   7:             .UseSetting("applicationName", startupAssemblyName)
   8:             .ConfigureServices(svcs => svcs.AddSingleton<IStartup>(new DelegateStartup(configureApp));
   9:     }
  10: }

二、ConventionBasedStartup

我们知道应用中最常见的服务和中间件注册代码都定义在一个单独的类中,通常直接将其命名为Startup。为了与IStartup接口代表的Startup相区别,我们使用
“启动类(型)”
来称呼这个类。按照约定,启动类中会分别定义一个ConfigureServices和Configure方法来注册服务和中间件。一般情况下,这样的类型一般需要通过调用UseStartup<T>这个扩展方法注册到WebHostBuilder上。

   1: public class Startup
   2: {
   3:     public void ConfigureServies(IServiceCollection services);
   4:     public void Configure(IApplicationBuilder app);
   5: }
   6:  
   7: new WebHostBuilder()
   8:     .UseStartup<Startup>()
   9:     …

如果我们在应用中将服务和中间件注册的实现定义在启动类型中,当WebHost被启动的时候,ASP.NET
Core会创建一个类型为ConventionBasedStartup的Startup对象。这个Startup类型之所以采用这样的命名方式,是因为ASP.NET

Core并没有采用接口实现的方式为启动类型做强制性的约束,而仅仅是为作为启动类型的定义提供了一个约定而已,至于具体采用怎样的约定,我们将在后续部分进行详细介绍。

StartupMethods

在了解了启动类型的约定以及常见的定义形式之外,我们现在来讨论这对这个类型创建的ConventionBasedStartup就是怎样的对象。从下面的代码片段可以看出,一个ConventionBasedStartup对象是根据一个类型为StartupMethods对象创建的。顾名思义,StartupMethods只在提供两个用户注册服务和中间件的方法,这两个方法体现在由它的两个属性(ConfigureServicesDelegate和ConfigureDelegate)提供的两个委托对象,这两个委托对象分别实现了定义在ConventionBasedStartup的ConfigureServices和Configure方法。

   1: public class ConventionBasedStartup : IStartup
   2: {
   3:     public ConventionBasedStartup(StartupMethods methods);
   4:     public IServiceProvider ConfigureServices(IServiceCollection services); 
   5:     public void Configure(IApplicationBuilder app);
   6: }
   7:  
   8: public class StartupMethods
   9: {      
  10:     public Func<IServiceCollection, IServiceProvider>     ConfigureServicesDelegate {  get; }
  11:     public Action<IApplicationBuilder>                    ConfigureDelegate {  get; }
  12:  
  13:     public StartupMethods(Action<IApplicationBuilder> configure);
  14:     public StartupMethods(Action<IApplicationBuilder> configure, Func<IServiceCollection, IServiceProvider> configureServices);   
  15: }

StartupLoader

既然ConventionBasedStartup对象是根据提供的一个StartupMethods对象创建的,那么现在的核心问题则变成了这个StartupMethods对象如何根据启动类型创建的。StartupMethods的创建者是一个类型为StartupLoader的对象,如下面的代码片段所示,StartupLoader定了两个名为FindStartupType和LoadMethods静态方法,前者用于启动类型的解析,后者则实现了StartupMethods对象的创建。

   1: public class StartupLoader
   2: {
   3:     public static Type FindStartupType(string startupAssemblyName, string environmentName);
   4:     public static StartupMethods LoadMethods(IServiceProvider services, Type startupType, string environmentName);
   5: }

如何选择启动类型

如果启动类型没有通过调用WebHostBuilder的如下两个扩展方法UseStartup/UseStartup<TStartup>的显式注册,那么StartupLoader的FindStartupType方法会被调用来解析出正确的启动类型。这个方法具有两个参数,分别代表启动类型所在的程序集名称和当前环境名称,它们实际上对应着WebHostOptions的两个同名属性。当FindStartupType方法被执行并成功加载了提供的程序集之后,它会按照约定的启动类型全名从该程序集中加载启动类型,候选的启动类型全名按照选择优先级排列如下:

  • Startup{EnvironmentName} (无命名空间)
  • {StartupAssemblyName}.Startup{EnvironmentName}
  • Startup(无命名空间)
  • {StartupAssemblyName}.Startup{EnvironmentName}
  • **. Startup{EnvironmentName}(任意命名空间)
  • **. Startup(任意命名空间)

这个列表体现了启动类型解析过程中选择有效类型名称的一个基本策略,即“环境名称优先”和“无命名空间优先”。我们可以通过一个简单的实例来证明这个策略的存在。我们在一个ASP.NET

Core控制台应用中添加一个名为“StartupLib”(程序集也采用这个名称)的类库项目,然后在这个项目中定义如下两组启动类,其中一组具有命名空间,另一组则采用程序集名称作为命名空间。这些启动类都派生于我们自定义的基类StartupBase,后者的Configure方法中注册了一个中间件将自身的类型作为响应内容。对于每组中的三个启动类,一个命名为Startup,另外两个则分别以环境名称(
“Development” 和 “Production” )作为后缀。

   1: public class StartupBase
   2: {
   3:     public void ConfigureServices(IServiceCollection services){}
   4:     public void Configure(IApplicationBuilder app)
   5:     {
   6:         app.Run(async context => await context.Response.WriteAsync(this.GetType().FullName));
   7:     }
   8: }
   9:  
  10: public class Startup                : StartupBase{}
  11: public class StartupDevelopment     : StartupBase{}
  12: public class StartProduction        : StartupBase{}
  13:  
  14: namespace StartupLib
  15: {
  16:     public class Startup               : StartupBase{}
  17:     public class StartupDevelopment    : StartupBase{}
  18:     public class StartProduction       : StartupBase{}
  19: }

我们采用如下的程序来启动一个ASP.NET
Core应用。如下面的代码代码片段所示,我们在利用WebHostBuilder创建并启动WebHost之前,调用UseSettings方法以配置的形式指定了启动程序集(“StartupLib”)和当前运行环境(“Development”)的名称。

   1: public class Program
   2: {
   3:     public static void Main()
   4:     {
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             .UseSetting("startupAssembly", "StartupLib")
   8:             .UseSetting("environment", "Development")
   9:             .Build()
  10:             .Run();
  11:     }
  12: }

根据上述的启动类型解析规则,对于六个候选的启动类型,最终被选择的是不具有命名空间的StartupDevelopment类型。当应用启动之后,我们利用浏览器请求应用监听地址(“http://localhost:5000”),这个被选择的启动程序的名称将会以如下的形式直接显示出来。

如何选择服务注册方法和中间件注册方法

在了解了ASP.NET
Core针对启动类型命名的约定之后,我们来讨论一下定义在启动类中用于注册服务和中间件的两个方法的约定。这两个方法可以是静态方法,也可以是实例方法。从方法命名来看,这两个方法除了命名为“ConfigureServices”和“Configure”之外,方法名还可以携带运行环境名称,具体采用的格式分别为“Configure{EnvironmentName}Services”和“Configure{EnvironmentName}”,后者具有更高的选择优先级。

ConfigureServices/Configure{EnvironmentName}Services方法具有一个类型为IServiceCollection接口的参数,表示存放注册服务的ServiceCollection对象。如过这个方法没有定义任何参数,它依然是合法的。一般来说,这个方法不具有返回值(返回类型为void),但是它也可以定义成一个返回类型为IServiceProvider的方法。如果这个方法返回一个ServiceProvider对象,后续过程中获取的所有服务将从这个ServiceProvider中提取。对于没有返回值的情况,系统会根据当前注册的服务创建一个ServiceProvider。

Configure/Configure{EnvironmentName}方法只要求只要求第一个参数类型采用IApplicationBuilder接口,至于这个方法可以包含多少个参数,各个参数应该具有怎样的类型,并未做任何规定。实际上我们为这个方法定义任意后续参数都是合法的。当ConventionBasedStartup在调用这个方法的时候,同样是采用依赖注入的方式来提供这些参数。如下面的代码片段所示,我们为启动类的Configure方法定义相应的参数来直接使用在ConfigureServices方法上注册的三个服务。

   1: new WebHostBuilder()
   2:     .ConfigureServices(services => services.AddSingleton<IFoobar, Foobar>())
   3:     …
   4:  
   5: public class Startup
   6: {
   7:     public Startup(IFoobar foobar)
   8:     {
   9:         Debug.Assert(foobar.GetType() == typeof(Foobar));
  10:     }
  11:     public void ConfigureServies(IServiceCollection services) ;
  12:     public void Configure(IApplicationBuilder app) ;
  13: }

StartupMethods对象的创建

除此之外,对于定义成实例类型的启动类,我们并不要求它具有一个默认无参的构造函数。如果构造函数具有参数,ConventionBasedStartup在实例化的时候会采用构造函数注入的方式来提供构造函数的参数。至于提供参数所用的ServiceProvider,就是WebHostBuilder在创建WebHost对象时作为构造函数参数提供的那个ServiceProvider。如下面的代码片段所示,我们利用WebHostBuilder创建并启动WebHost之前,调用他的ConfigureServices方法针对接口IFoobar注册了一个服务,那么注册为启动类的Startup类可以在构造函数中以注入的形式使用这个服务对象。

   1: public class StartupLoader
   2: {
   3:     public static StartupMethods LoadMethods(IServiceProvider serviceProvider, Type startupType, string environmentName)
   4:     {
   5:         return new StartupMethods(BuildConfigureDelegate(serviceProvider, startupType, environmentName), 
   6:             BuildConfigureServicesDelegate(serviceProvider, startupType,   environmentName));
   7:     }
   8:  
   9:     private static Func<IServiceCollection, IServiceProvider> BuildConfigureServicesDelegate(IServiceProvider serviceProvider, Type startupType, string environmentName)
  10:     {
  11:         MethodInfo method = FindMethod(startupType, $"Configure{environmentName}Services", "ConfigureServices");        
  12:         object instance = method.IsStatic ? null : ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, startupType);        
  13:         return services =>
  14:         {
  15:             object[] arguments = method.GetParameters().Length > 0 ? new object[] { services } : new object[0];
  16:             return (method.Invoke(instance, arguments) as IServiceProvider) ?? services.BuildServiceProvider();
  17:         };
  18:     }
  19:  
  20:     private static Action<IApplicationBuilder> BuildConfigureDelegate(IServiceProvider serviceProvider, Type startupType, string environmentName)
  21:     {
  22:         MethodInfo method = FindMethod(startupType, $"Configure{environmentName}", "Configure");        
  23:         object instance = method.IsStatic ? null : ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, startupType);        
  24:         object[] arguments = method.GetParameters().Select(p => serviceProvider.GetService(p.ParameterType)).ToArray();        
  25:         return app =>
  26:         {
  27:             arguments[0] = app;
  28:             method.Invoke(instance, arguments);
  29:         };
  30:     }
  31:  
  32:     private static MethodInfo FindMethod(Type startupType, string method1, string method2)
  33:     {    
  34:         BindingFlags bindAttribute = BindingFlags.Public | BindingFlags.Instance |    BindingFlags.Static;
  35:         return startupType.GetMethod(method1, bindAttribute)?? startupType.GetMethod(method2, bindAttribute);    
  36:      }
  37: }

UseStartup方法究竟做了些什么?

当我们调用IWebHostBuilder接口的扩展方法UseStartup/UseStartup<TStartup>注册某个启动类的时候,该方法会按照如下的形式创建一个ConventionBasedStartup对象并注册到WebHostBuilder的服务集合上。和上面介绍的Configure方法一样,UseStartup方法同样会设置
“ApplicationName”
选项。除此之外,这段还体现了另一个细节,那就是如果我们直接定义一个实现了IStartup接口的启动类,UseStartup方法会直接注册这个类型,而不会再多此一举地创建一个ConventionBasedStartup对象。

   1: public static class WebHostBuilderExtensions
   2: {
   3:     public static IWebHostBuilder UseStartup<TStartup>(this IWebHostBuilder hostBuilder) where TStartup : class
   4:     {
   5:         return UseStartup(hostBuilder, typeof(TStartup));
   6:     }
   7:  
   8:     public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, Type startupType)
   9:     {
  10:         var startupAssemblyName = startupType.GetTypeInfo().Assembly.GetName().Name;
  11:         return hostBuilder
  12:             .UseSetting("ApplicationName", startupAssemblyName)
  13:             .ConfigureServices(svcs =>
  14:             {
  15:                 if (typeof(IStartup).IsAssignableFrom(startupType))
  16:                 {
  17:                     svcs.AddSingleton(typeof(IStartup), startupType);
  18:                 }
  19:                 else
  20:                 {
  21:                     svcs.AddSingleton<IStartup>(sp => new ConventionBasedStartup(StartupLoader.LoadMethods(sp, startupType, sp.GetService<IHostingEnvironment>().EnvironmentName)));
  22:                 }
  23:             });           
  24:     }
  25: }

三、选择哪一个Startup

应用启动的时候,Startup帮助我们完成所需服务和中间件的注册,而Startup对象自身也是服务的形式被注册到WebHostBuilder或者WebHost的服务集合中。总的来说,Startup的注册具有如下三种途径:

  • 调用IWebHostBuilder的扩展方法Configure方法创建并注册一个DelegateStartup对象。
  • 调用IWebHostBuilder的扩展方法UseStartup或者UseStartup<TStartup>创建并注册一个ConventionBasedStartup对象。
  • 如果设置了启动程序集名称(对应WebHostOptions的StartupAssembly属性)并且对应的程序集中存在一个满足约定的启动类型,也会创建并注册一个ConventionBasedStartup对象。

那么现在的问题来说,如果我们采用上述这三种途径创建并注册了多个Startup,那么系统是只选择其中一个,还是所有的Startup均有效呢?就如下这段程序来说,如果当前程序集同时定义了三个有效的Startup类型(Startup、Startup1和Startup2),最终将会有五个Startup对象被注册,其中两个是通过Configure方法注册的DelegateStartup对象,对于额外三个ConventionBasedStartup对象来说,

其中两个针对显式指定的启动类型(Startup1和Startup2),另外一个则是针对默认的约定解析出来的启动类型(Startup)。对于这个五个Startup对象,究竟哪些是有效的呢?

   1: new WebHostBuilder()
   2:     .Configure(app => {})
   3:     .Configure(app => {})
   4:     .UseStartup<Startup1>()
   5:     .UseStartup<Startup2>()
   6:     .UseSetting("startupAssembly", Assembly.GetEntryAssembly().FullName)
   7: …

不论我们注册多少个Startup,系统最终都只会其中一个来注册服务和中间件。由于WebHost会直接利用ServiceProvider来获取Startup对象,根据 “后来居上”

的原则,最终选择的总是最后注册的那个Startup。由于Configure方法和UseStartup方法最终都是调用WebHostBuilder的ConfigureServices方法进行服务注册,所以最后调用的方法具有最高的优先级。至于根据指定启动程序集名称而创建出来的ConventionBasedStartup,针对它的注册信息会放在最前面,所以具有最低优先级。根据这个策略,上面这段程序最终选择的启动类是Startup2。

作者:蒋金楠
微信公众账号:大内老A
微博:www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

原文链接

时间: 2024-09-08 21:11:19

学习ASP.NET Core, 怎能不了解请求处理管道[4]: 应用的入口——Startup的相关文章

学习ASP.NET Core,怎能不了解请求处理管道[1]: 中间件究竟是个什么东西?

ASP.NET Core管道虽然在结构组成上显得非常简单,但是在具体实现上却涉及到太多的对象,所以我们在 "通过重建Hosting系统理解HTTP请求在ASP.NET Core管道中的处理流程"(上篇.中篇.下篇) 中围绕着一个经过极度简化的模拟管道讲述了真实管道构建的方式以及处理HTTP请求的流程.在本系列 中,我们会还原构建模拟管道时可以舍弃和改写的部分,向读者朋友们呈现一个真是的HTTP请求处理管道. ASP.NET Core 的请求处理管道由一个服务器与一组有序排列的中间件构成

学习ASP.NET Core, 怎能不了解请求处理管道[6]: 管道是如何随着WebHost的开启被构建出来的?

注册的服务器和中间件共同构成了ASP.NET Core用于处理请求的管道, 这样一个管道是在我们启动作为应用宿主的WebHost时构建出来的.要深刻了解这个管道是如何被构建出来的,我们就必须对WebHost和它的创建者WebHostBuilder这个重要的对象具有深刻的理解.[本文已经同步到<ASP.NET Core框架揭秘>之中] 目录 一.WebHost     WebHostOptions     构建管道的三个步骤 二.WebHostBuilder     WebHost的创建    

学习ASP.NET Core, 怎能不了解请求处理管道[3]: 自定义一个服务器感受一下管道是如何监听、接收和响应请求的

我们在<服务器在管道中的"龙头"地位>中对ASP.NET Core默认提供的具有跨平台能力的KestrelServer进行了介绍,为了让读者朋友们对管道中的服务器具有更加深刻的认识,接下来我们采用实例演示的形式创建一个自定义的服务器.这个自定义的服务器直接利用HttpListener来完成针对请求的监听.接收和响应,我们将其命名为HttpListenerServer.在正式介绍HttpListenerServer的设计和实现之前,我们先来显示一下如何将它应用到 一个具体的W

学习ASP.NET Core,怎能不了解请求处理管道[2]: 服务器在管道中的“龙头”地位

ASP.NET Core管道由注册的服务器和一系列中间件构成.我们在上一篇中深入剖析了中间件,现在我们来了解一下服务器.服务器是ASP .NET Core管道的第一个节点,它负责完整请求的监听和接收,最终对请求的响应同样也由它完成.[本文已经同步到<ASP.NET Core框架揭秘>之中] 服务器是我们对所有实现了IServer接口的所有类型以及对应对象的统称.如下面的代码片段所示,这个接口具有一个只读属性Features返回描述自身特性集合的FeatureCollection对象,另一个St

学习ASP.NET Core, 怎能不了解请求处理管道[5]: 中间件注册可以除了可以使用Startup之外,还可以选择StartupFilter

中间件的注册除了可以借助Startup对象(DelegateStartup或者ConventionBasedStartup)来完成之外,也可以利用另一个叫做StartupFilter的对象来实现.所谓的StartupFilter是对所有实现了IStartupFilter接口的类型及其对象的统称.IStartupFilter接口定义了如下一个唯一的方法Configure,该方法的参数next返回的Action<IApplicationBuilder>对象体现了后续StartupFilter和St

学习ASP.NET Core,你必须了解无处不在的“依赖注入”

ASP.NET Core的核心是通过一个Server和若干注册的Middleware构成的管道,不论是管道自身的构建,还是Server和Middleware自身的实现,以及构建在这个管道的应用,都需要相应的服务提供支持,ASP.NET Core自身提供了一个DI容器来实现针对服务的注册和消费.换句话说,不只是ASP.NET Core底层框架使用的服务是由这个DI容器来注册和提供,应用级别的服务的注册和提供也需要以来这个DI容器,所以正如本文标题所说的--学习ASP.NET Core,你必须了解无

牛腩学ASP.NET CORE做博客(视频)

牛腩学习ASP.NET CORE做的项目,边学边做. 目录: 01-dotnetcore网站部署到centos7系统上(时长 2:03:16) 02-前期准备及项目搭建 (时长:0:23:35) 03-数据库设计及Dapper使用(时长:0:50:47) 04-后台博客文章增删改(时长:1:16:43) 05-LayUI分页的使用(时长:1:07:07) 06-博客查询功能和LayUI编辑器的使用(时长:1:16:11) 07-后台登录及前台整合(时长:1:00:37) 08-前台整合2(时长:

ASP.NET Core MVC之Serilog日志处理,你了解多少?

前言 本节我们来看看ASP.NET Core MVC中比较常用的功能,对于导入和导出目前仍在探索中,项目需要自定义列合并,所以事先探索了如何在ASP.NET Core MVC进行导入.导出,更高级的内容还需等我学习再给出. EntityFramework Core 在学习ASP.NET Core MVC之前我们来看看在EF Core中如何更新对象指定属性,这个问题之前我们已经探讨过,但是还是存在一点问题,请往下看. public void Update(T entity, params Expr

ASP.NET Core MVC/WebAPi 模型绑定探索

前言 相信一直关注我的园友都知道,我写的博文都没有特别枯燥理论性的东西,主要是当每开启一门新的技术之旅时,刚开始就直接去看底层实现原理,第一会感觉索然无味,第二也不明白到底为何要这样做,所以只有当你用到了,你再去看理论性的文章时才会豁然开朗,这是我一直以来学习技术的方法.本文我们来讲解.NET Core中的模型绑定. 话题 在ASP.NET Core之前MVC和Web APi被分开,也就说其请求管道是独立的,而在ASP.NET Core中,WebAPi和MVC的请求管道被合并在一起,当我们建立控