创建一个 asp.net core的项目
-
参考微软文档吧, 很详细: docs.microsoft.com/en-us/aspne…
-
创建完后会有个web项目, 打开后我这面的层级目录如下:
代码解析
//为什么关注这个类, 因为这里有main函数, 一般来说main函数都是程序启动的时候的启动类. 看一下这行代码:
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
看到这个 Main 函数时我顿时有种似曾相识的感觉, 这个不是 console application 中的 Main 函数么, 二者莫非有关联?
在我的理解, 其实asp.net core web application其本质应该就是一个 console application,然后通过内置的 web server 去处理 http 请求。这个内置的 web sever 默认是 Kestre, Web server 一般是需要 host在某个宿主上的, 比较 common 的 host 环境有 IIS, windows servie, Nginx, 或者 Docker等等. 这时候你要问我宿主是干什么用的以及为何要host到宿主上面? 我觉得这就稍微超出我们这篇文章的 scope 了. 简单来说就是宿主是用来启动我们的 asp.net core 的程序的. 并且会监听程序的运行状态能够做到一些配置和审核权限等其他的工作, 并且我们的 web server 可以做成分布式的 server. 它们都分别接收由宿主分发过来的 http 请求, 那么宿主又可以承担作为一个反向代理的角色. 对这些内容如果感兴趣的话可以看看官方的材料, docs.microsoft.com/en-us/aspne…, 这里就不多写了.
那么回头来看 asp.net core 的 main 函数都做了什么?
1.创建 WebHostBuilder 对象
2.让这个 WebHostBuilder 对象 build一个 webhost 并run起来
创建 WebHostBuilder 对象
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
分开来看, 先看第一部分 WebHost.CreateDefaultBuilder(args)
打开源码文件路径(..\AspNetCore-2.2.4\src\DefaultBuilder\src)(因为源码中很多叫WebHost的文件, 容易找不到), 我们来看一下 CreateDefaultBuilder 这个方法的源码:
/// <summary>
/// Initializes a new instance of the <see cref="WebHostBuilder"/> class with pre-configured defaults.
/// </summary>
/// <remarks>
/// The following defaults are applied to the returned <see cref="WebHostBuilder"/>:
/// use Kestrel as the web server and configure it using the application's configuration providers,
/// set the <see cref="IHostingEnvironment.ContentRootPath"/> to the result of <see cref="Directory.GetCurrentDirectory()"/>,
/// load <see cref="IConfiguration"/> from 'appsettings.json' and 'appsettings.[<see cref="IHostingEnvironment.EnvironmentName"/>].json',
/// load <see cref="IConfiguration"/> from User Secrets when <see cref="IHostingEnvironment.EnvironmentName"/> is 'Development' using the entry assembly,
/// load <see cref="IConfiguration"/> from environment variables,
/// load <see cref="IConfiguration"/> from supplied command line args,
/// configure the <see cref="ILoggerFactory"/> to log to the console and debug output,
/// and enable IIS integration.
/// </remarks>
/// <param name="args">The command line args.</param>
/// <returns>The initialized <see cref="IWebHostBuilder"/>.</returns>
public static IWebHostBuilder CreateDefaultBuilder(string[] args)
{
var builder = new WebHostBuilder();
if (string.IsNullOrEmpty(builder.GetSetting(WebHostDefaults.ContentRootKey)))
{
builder.UseContentRoot(Directory.GetCurrentDirectory());
}
if (args != null)
{
builder.UseConfiguration(new ConfigurationBuilder().AddCommandLine(args).Build());
}
builder.UseKestrel((builderContext, options) =>
{
options.Configure(builderContext.Configuration.GetSection("Kestrel"));
})
.ConfigureAppConfiguration((hostingContext, config) =>
{
var env = hostingContext.HostingEnvironment;
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
if (env.IsDevelopment())
{
var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
if (appAssembly != null)
{
config.AddUserSecrets(appAssembly, optional: true);
}
}
config.AddEnvironmentVariables();
if (args != null)
{
config.AddCommandLine(args);
}
})
.ConfigureLogging((hostingContext, logging) =>
{
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole();
logging.AddDebug();
logging.AddEventSourceLogger();
})
.ConfigureServices((hostingContext, services) =>
{
// Fallback
services.PostConfigure<HostFilteringOptions>(options =>
{
if (options.AllowedHosts == null || options.AllowedHosts.Count == 0)
{
// "AllowedHosts": "localhost;127.0.0.1;[::1]"
var hosts = hostingContext.Configuration["AllowedHosts"]?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
// Fall back to "*" to disable.
options.AllowedHosts = (hosts?.Length > 0 ? hosts : new[] { "*" });
}
});
// Change notification
services.AddSingleton<IOptionsChangeTokenSource<HostFilteringOptions>>(
new ConfigurationChangeTokenSource<HostFilteringOptions>(hostingContext.Configuration));
services.AddTransient<IStartupFilter, HostFilteringStartupFilter>();
})
.UseIIS()
.UseIISIntegration()
.UseDefaultServiceProvider((context, options) =>
{
options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
});
return builder;
}
根据源码,我们来总结一下 CreateDefaultBuilder 所做的工作:
UseKestrel:使用Kestrel作为Web server。
UseContentRoot:指定Web host使用的content root(内容根目录),比如Views。默认为当前应用程序根目录。
ConfigureAppConfiguration:设置当前应用程序配置。主要是读取 appsettinggs.json 配置文件、开发环境中配置的UserSecrets、添加环境变量和命令行参数 。
ConfigureLogging:读取配置文件中的Logging节点,配置日志系统。
UseIISIntegration:使用IISIntegration 中间件。
UseDefaultServiceProvider:设置默认的依赖注入容器。
再看第二部分 .UseStartup<Startup>();
我们直接F12
//
// Summary:
// Specify the startup type to be used by the web host.
//
// Parameters:
// hostBuilder:
// The Microsoft.AspNetCore.Hosting.IWebHostBuilder to configure.
//
// Type parameters:
// TStartup:
// The type containing the startup methods for the application.
//
// Returns:
// The Microsoft.AspNetCore.Hosting.IWebHostBuilder.
public static IWebHostBuilder UseStartup<TStartup>(this IWebHostBuilder hostBuilder) where TStartup : class;
当然这些不够, 我们的目的是知道底层是用怎么使用的startup
/// <summary>
/// Specify the startup type to be used by the web host.
/// </summary>
/// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param>
/// <param name="startupType">The <see cref="Type"/> to be used.</param>
/// <returns>The <see cref="IWebHostBuilder"/>.</returns>
public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, Type startupType)
{
var startupAssemblyName = startupType.GetTypeInfo().Assembly.GetName().Name;
return hostBuilder
.UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName)
.ConfigureServices(services =>
{
if (typeof(IStartup).GetTypeInfo().IsAssignableFrom(startupType.GetTypeInfo()))
{
services.AddSingleton(typeof(IStartup), startupType);
}
else
{
services.AddSingleton(typeof(IStartup), sp =>
{
var hostingEnvironment = sp.GetRequiredService<IHostingEnvironment>();
return new ConventionBasedStartup(StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName));
});
}
});
}
可以看到其实UseStartup方法返回的是IWebHostBuilder对象, 其中.UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName) 是将 ApplicationKey和startupAssemblyName 缓存起来. 如下图:
然后.ConfigureServices(Action<IServiceCollection> configureServices) 实际上也是对list集合进行一个缓存操作, 但是注意这个方法的参数是一个委托Action, 实际上调用的时候传递的是一个lambda表达式, 我们需要看看这个表达式里的神奇操作:(我们主要关注 else 里面的部分, if中的没啥可说的.)
services.AddSingleton(typeof(IStartup), sp =>
{
var hostingEnvironment = sp.GetRequiredService<IHostingEnvironment>();
return new ConventionBasedStartup(StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName));
});
services是我们 asp.net core DI的核心ServiceCollection , 然后AddSingleton方法是它内部的一个静态方法
public static IServiceCollection AddSingleton(this IServiceCollection services, Type serviceType, Func<IServiceProvider, object> implementationFactory);
所以我们看到的 sp 实际上是 IServiceProvider, 先暂时理解成是用来搞到 service 的.
然后精髓来了,
StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName)
参数我们差不多都知道代表什么. 但是这个LoadMethods 是干嘛的?? 我们看一下解释:
很长, 但是主要就是用于通过反射技术, 将我们自定义的StartUp.cs文件里面的方法都load到低层来. 然后顺便配置一下每次都有的那个service和创建一个request pipeline of the application
这里我觉得有必要将startup.cs文件中的两个function贴到下面方便我们后续的撸源码:
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
看完这两个方法, 我们都能明白一个事儿就是这两个方法都是运行时调用的. 至于为什么是运行时调用的, 咱们差不多都知道了, 因为底层build的时候反射获取这两个方法, 将两个方法的配置参数什么的配置进底层的service中.
这里有必要解释一下我们的自定义 startup 文件里面的参数含义, 因为这毕竟是暴露给我们开发者使用的, 所以应该多了解一下:
IServiceCollection:当前容器中各服务的配置集合,ASP.NET Core内置的依赖注入容器。
IApplicationBuilder:用于构建应用程序的请求管道。
IHostingEnvironment:提供了当前的 EnvironmentName、WebRootPath 以及 ContentRoot等。
public static StartupMethods LoadMethods(IServiceProvider hostingServiceProvider, Type startupType, string environmentName)
{
var configureMethod = FindConfigureDelegate(startupType, environmentName);
var servicesMethod = FindConfigureServicesDelegate(startupType, environmentName);
var configureContainerMethod = FindConfigureContainerDelegate(startupType, environmentName);
object instance = null;
if (!configureMethod.MethodInfo.IsStatic || (servicesMethod != null && !servicesMethod.MethodInfo.IsStatic))
{
instance = ActivatorUtilities.GetServiceOrCreateInstance(hostingServiceProvider, startupType);
}
// The type of the TContainerBuilder. If there is no ConfigureContainer method we can just use object as it's not
// going to be used for anything.
var type = configureContainerMethod.MethodInfo != null ? configureContainerMethod.GetContainerType() : typeof(object);
var builder = (ConfigureServicesDelegateBuilder) Activator.CreateInstance(
typeof(ConfigureServicesDelegateBuilder<>).MakeGenericType(type),
hostingServiceProvider,
servicesMethod,
configureContainerMethod,
instance);
return new StartupMethods(instance, configureMethod.Build(instance), builder.Build());
}
(反射已经用到了出神入化的地步) 想看怎么实现的就自己去看吧. 反正代码贴到这里, 意思明白了就行.
好了, 目前知道了.ConfigureService 的参数是干嘛的了... 那看看这个方法的底层要这个参数做啥了吧:
private readonly List<Action<WebHostBuilderContext, IServiceCollection>> _configureServicesDelegates;
/// <summary>
/// Adds a delegate for configuring additional services for the host or web application. This may be called
/// multiple times.
/// </summary>
/// <param name="configureServices">A delegate for configuring the <see cref="IServiceCollection"/>.</param>
/// <returns>The <see cref="IWebHostBuilder"/>.</returns>
public IWebHostBuilder ConfigureServices(Action<IServiceCollection> configureServices)
{
if (configureServices == null)
{
throw new ArgumentNullException(nameof(configureServices));
}
return ConfigureServices((_, services) => configureServices(services));
}
/// <summary>
/// Adds a delegate for configuring additional services for the host or web application. This may be called
/// multiple times.
/// </summary>
/// <param name="configureServices">A delegate for configuring the <see cref="IServiceCollection"/>.</param>
/// <returns>The <see cref="IWebHostBuilder"/>.</returns>
public IWebHostBuilder ConfigureServices(Action<WebHostBuilderContext, IServiceCollection> configureServices)
{
if (configureServices == null)
{
throw new ArgumentNullException(nameof(configureServices));
}
_configureServicesDelegates.Add(configureServices);
return this;
}
卧槽槽, 又是一个缓存....行吧, 底层各种搞缓存, 不就是DI的本质吗..这算基本搞懂了UseStartup..
创建 WebHostBuilder 对象完事了
那么WebHostBuilder创建出来后, 里面有两个缓存, 对startup的类型进行缓存之外, 还对startup的services进行缓存. 然后这个builder显然是要进行 build 的, 不然费那么大力气创建干嘛. 所以看下一篇吧, 解读后续操作.