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

目录
一、WebHost
    WebHostOptions
    构建管道的三个步骤
二、WebHostBuilder
    WebHost的创建
    几个常用的扩展方法

一、WebHost

顾名思义,WebHost被作为Web应用的宿主,应用的启动和关闭都是通过启动或者关闭对应WebHost的方式来实现的。这里所说的WebHost是对所有实现了IWebHost接口的所有类型及其对应对象的统称。IWebHost接口具有如下三个基本成员,其中Start方法用于启动宿主程序。我们编程中通常会调用它的一个扩展方法Run来启动WebHost,实际上背后调用的其实还是这个Start方法。当WebHost启动之后,注册的服务器变开始了针对请求的监听,所以WebHost需要具有与服务器相关的一些特性,这些特性就保存在通过属性ServerFeatures返回的特性集合中。

  1. 1: public interface IWebHost : IDisposable

  1. 2: {

  1. 3: void Start();

  1. 4: IFeatureCollection ServerFeatures { get; }

  1. 5: IServiceProvider Services { get; }

  1. 6: }

我们多次提到ASP.NET Core管道在构建和进行请求处理过程中广泛使用到了依赖注入。依赖注入只要体现在:ASP.NET Core框架以及应用程序会根据需要注册一系列的服务,这些服务会在WebHost启动的时候被用来创建一个ServiceProvider对象,管道在进行请求处理过程所需的任何服务对象都可以从这个ServiceProvider对象中获取。IWebHost接口的Services属性返回的就是这么一个ServiceProvider对象。

具有如下定义的WebHost类是对IWebHost接口的默认实现,我们默认使用的WebHost就是这么一个对象。一般来说,WebHost是通过对应的WebHostBuilder创建的,当后者通过调用构造函数创建一个WebHost对象的时候,需要提供四个参数,它们分别是直接注册到WebHostBuilder上面的服务(appServices)和由此创建的ServiceProvider(hostingServiceProvider),针对WebHost的选项设置(options)和配置(config)。

  1. 1: public class WebHost : IWebHost

  1. 2: {

  1. 3: public IFeatureCollection ServerFeatures { get; }

  1. 4: public IServiceProvider Services { get; }

  1. 5: 

  1. 6: public WebHost(

  1. 7: IServiceCollection appServices,

  1. 8: IServiceProvider hostingServiceProvider,

  1. 9: WebHostOptions options,

  1. 10: IConfiguration config);

  1. 11: 

  1. 12: public void Dispose();

  1. 13: public void Start();

  1. 14: }

WebHostOptions

顾名思义,一个WebHostOptions对象为构建的WebHost对象提供一些预定义的选项设置。这些选项设置很重要,它们决定由WebHost构建的管道进行内容加载以及异常处理等方面的行为。至于它具体携带着哪些选项设置,我们只需要看看这个类型具有怎样的属性成员。

  1. 1: public class WebHostOptions

  1. 2: {

  1. 3: public string ApplicationName { get; set; }

  1. 4: public bool DetailedErrors { get; set; }

  1. 5: public bool CaptureStartupErrors { get; set; }

  1. 6: public string Environment { get; set; }

  1. 7: public string StartupAssembly { get; set; }

  1. 8: public string WebRoot { get; set; }

  1. 9: public string ContentRootPath { get; set; }

  1. 10: 

  1. 11: public WebHostOptions()

  1. 12: public WebHostOptions(IConfiguration configuration)

  1. 13: }

如下面的代码片段所示,WebHostOptions具有七个属性成员。这些属性都是可读可写的,我们可以调用默认无参构造函数创建一个空的WebHostOptions对象,通过手工为这些属性赋值的方式来设置对应的选项。除此之外,我们可以将这些选项设置定义在配置中,并利用对应的Configuration对象来创建一个WebHostOptions对象。

构建管道的三个步骤

一般我们开启了作为应用宿主的WebHost,由注册的服务器和中间件构成的整个管道被构建起来,服务器开始绑定到基地址进行请求的监听。接下来我们就来着重聊聊WebHost在开启过程中都做了些什么。总的来说,WebHost的整个开启过程大体上可以分为如下三个步骤:

  • 注册服务:获取Startup对象并利用它完成服务的注册。

  • 中间件注册:利用获取的Startup对象完成中间件的注册。
  • 设置并开启服务器:获取注册到WebHostBuilder上的服务器并为之设置监听地址,最后启动服务器。

接下来我们按照这个步骤定义一个同名的类型来模式真实WebHost的实现逻辑。如下面的代码片段所示,这个模拟的WebHost和真正的WebHost的构造函数具有完全一致的参数列表,我们定义了对应的字段来保存这些参数值。除此之外,我们会创建一个ApplicationLifetime对象并将其注册到提供个ServiceCollection,在WebHost开启和关闭之后我们会利用它发送相应的通知。

  1. 1: public class WebHost : IWebHost

  1. 2: {

  1. 3: private IServiceCollection _appServices;

  1. 4: private IServiceProvider _hostingServiceProvider;

  1. 5: private WebHostOptions _options;

  1. 6: private IConfiguration _config;

  1. 7: private ApplicationLifetime _applicationLifetime;

  1. 8: 

  1. 9: public WebHost(IServiceCollection appServices, IServiceProvider hostingServiceProvider, WebHostOptions options, IConfiguration config)

  1. 10: {

  1. 11: _appServices = appServices;

  1. 12: _hostingServiceProvider = hostingServiceProvider;

  1. 13: _options = options;

  1. 14: _config = config;

  1. 15: _applicationLifetime = new ApplicationLifetime();

  1. 16: appServices.AddSingleton<IApplicationLifetime>(_applicationLifetime);

  1. 17: }

  1. 18:

  1. 19: }

  1. 20: 

我们接下来看WebHost除Start方法之外的其他成员的定义。只读属性Services返回一个ServiceProvider对象,我们将在完成所有服务注册工作之后利用ServiceCollection对象创建这个对象,所以只要实现具有相关的服务注册,我们就能够利用它得到对应的服务对象。只读属性ServerFeatures返回服务器的特性集合,而服务器本身则直接利用上述这个ServiceProvider获得。当MyWebHost对象因Dispose方法的调用而被回收之后,我们会对ServiceProvider实施回收 工作。在实施回收的前后,我们利用ApplicationLifetime发送相应的信号。

  1. 1: public class WebHost : IWebHost

  1. 2: {

  1. 3: private ApplicationLifetime _applicationLifetime;

  1. 4: public IServiceProvider Services { get; private set; }

  1. 5: public IFeatureCollection ServerFeatures

  1. 6: {

  1. 7: get { return this.Services.GetRequiredService<IServer>()?.Features; }

  1. 8: }

  1. 9: public void Dispose()

  1. 10: {

  1. 11: _applicationLifetime.StopApplication();

  1. 12: (this.Services as IDisposable)?.Dispose();

  1. 13: _applicationLifetime.NotifyStopped();

  1. 14: }

  1. 15: }

  1. 16: 

真正开启WebHost的实现体现在如下所示的代码片段中。我们直接利用WebHostBuilder提供ServiceProvider获取一个Startup对象,并调用其ConfigureServices方法完成服务的注册,作为参数的ServiceCollection对象也是由WebHostBuilder提供的。当所有的服务注册工作完成之后,我们利用最新的ServiceCollection对象创建一个ServiceProvider对象,并利用此对象对Services属性进行赋值。在后续管道构建过程,以及管道在处理请求过程中所使用的服务均是从这个ServiceProvider中提取的。

  1. 1: public class WebHost : IWebHost

  1. 2: {

  1. 3: private IServiceCollection _appServices;

  1. 4: private IServiceProvider _hostingServiceProvider;

  1. 5: private WebHostOptions _options;

  1. 6: private IConfiguration _config;

  1. 7: private ApplicationLifetime _applicationLifetime;

  1. 8: 

  1. 9: public void Start()

  1. 10: {

  1. 11: //注册服务

  1. 12: IStartup startup = _hostingServiceProvider.GetRequiredService<IStartup>();

  1. 13: this.Services = startup.ConfigureServices(_appServices);

  1. 14:

  1. 15: //注册中间件

  1. 16: Action<IApplicationBuilder> configure = startup.Configure;

  1. 17: configure = this.Services.GetServices<IStartupFilter>().Reverse().Aggregate(configure, (next, current) => current.Configure(next));

  1. 18: IApplicationBuilder appBuilder = this.Services.GetRequiredService<IApplicationBuilder>();

  1. 19: configure(appBuilder);

  1. 20: 

  1. 21: //为服务器设置监听地址

  1. 22: IServer server = this.Services.GetRequiredService<IServer>();

  1. 23: IServerAddressesFeature addressesFeature = server.Features.Get<IServerAddressesFeature>();

  1. 24: if (null != addressesFeature && !addressesFeature.Addresses.Any())

  1. 25: {

  1. 26: string addresses = _config["urls"] ?? "http://localhost:5000";

  1. 27: foreach (string address in addresses.Split(';'))

  1. 28: {

  1. 29: addressesFeature.Addresses.Add(address);

  1. 30: }

  1. 31: }

  1. 32: 

  1. 33: //启动服务器

  1. 34: RequestDelegate application = appBuilder.Build();

  1. 35: ILogger logger = this.Services.GetRequiredService <ILogger<MyWebHost>>();

  1. 36: DiagnosticSource diagnosticSource = this.Services.GetRequiredService<DiagnosticSource>();

  1. 37: IHttpContextFactory httpContextFactory = this.Services.GetRequiredService<IHttpContextFactory>();

  1. 38: server.Start(new HostingApplication(application, logger, diagnosticSource, httpContextFactory));

  1. 39: 

  1. 40: //对外发送通知

  1. 41: _applicationLifetime.NotifyStarted();

  1. 42: }

  1. 43: }

  1. 44: 

当服务注册结束并成功创建出ServiceProvider之后,接下来的工作就是注册中间件了。通过上面的介绍我们知道,中间件的注册既可以利用Startup来完成,也可以利用注册的StartupFilter来实现,为此我们利用最新构建的ServiceProvider获取所有注册的StartupFilter,并结合之前提取的Startup对象创建了一个用于注册中间的委托链(最终体现为一个Action<IApplicationBuilder>对象)。我们最终执行这个委托链完成了对所有中间件的注册,执行过程中作为参数的ApplicationBuilder对象同样是通过ServiceProvider提取出来的。

再此之后,我们利用ServiceProvider提取出注册在WebHostBuiler上的服务器。如果服务器的监听地址尚未指定,我们在开启服务器之前必须指定。通过前面对服务器的介绍,我们知道监听地址保存在服务器的一个名为ServerAddressesFeature的特性中,而用户设置的监听地址则保存在配置中,对应的Key为“urls”,所以我们将从配置中提取的地址列表添加到ServerAddressesFeature特性中。如果监听地址不曾配置,我们会为之指定一个默认的地址,即“http://localhost:5000”。

一切就绪的服务器通过调用Start方法开启,该方法接收一个HttpApplication对象作为参数。通过前面的介绍我们知道这个HttpApplication对象可以视为对所有注册中间件和应用的封装,服务器将接收到的请求传递给它作后续处理。我们默认创建的HttpApplication是一个HostingApplication对象,而构建过程中需要提供四个对象,它们分别是代表中间件链表的RequestDelegate对象,用于日志记录和诊断的Logger和DiagnosticSource,以及用来创建HTTP上下文的HttpContextFactory,除了第一个通过调用ApplicationBuilder的Build方法创建之外,其余的都是通过ServiceProvider提取的。在服务器被成功开启之后,我们利用ApplicationLifetime对外发送应用启动的通知。

二、WebHostBuilder

顾名思义,WebHostBuilder就是WebHost的创建者,所谓的WebHostBuilder是对所有实现了IWebHostBuilder接口的类型以及对应对象的统称。如下面的代码片段所示,IWebHostBuilder接口除了用来创建WebHost的核心方法Build之外,还具有其他一些额外的方法。

  1. 1: public interface IWebHostBuilder

  1. 2: {

  1. 3: IWebHost Build();

  1. 4: IWebHostBuilder ConfigureServices(Action<IServiceCollection> configureServices);

  1. 5: IWebHostBuilder UseLoggerFactory(ILoggerFactory loggerFactory);

  1. 6: IWebHostBuilder ConfigureLogging(Action<ILoggerFactory> configureLogging);

  1. 7: string GetSetting(string key);

  1. 8: IWebHostBuilder UseSetting(string key, string value);

  1. 9: }

ConfigureServices方法让我们可以直接将我们所需的服务注册到WebHostBuilder上面。ASP.NET Core具有两种注册服务的途径,一种是将服务注册实现在启动类的ConfigureServices方法中,另一种服务注册的方式就是调用这个方法。对于前者,服务实际上是在开启WebHost的时候调用Startup对象的ConfigureServices进行注册的;至于后者,注册的服务将直接提供给创建的WebHost。UseLoggerFactory 和ConfigureLogging方法与日志记录有关,前者帮助我们设置一个默认的LoggerFactory,后者则对LoggerFactory进行相关设置,最重要的设置就是添加相应的LoggerProvider。GetSetting和UseSetting以键值对的形式获取和设置一些配置。

WebHost的创建

ASP.NET Core定义了一个名为WebHostBuilder的类型作为对IWebHostBuilder接口的默认实现,我们同样采用定义模拟类型的形式来说明WebHostBuilder创建WebHost的实现原理。我们将这个模拟类型命名为,如下的代码片段展示了除Build方法之外的所有成员的定义。

  1. 1: public class WebHostBuilder : IWebHostBuilder

  1. 2: {

  1. 3: private List<Action<ILoggerFactory>> _configureLoggingDelegates = new List<Action<ILoggerFactory>>();

  1. 4: private List<Action<IServiceCollection>> _configureServicesDelegates = new List<Action<IServiceCollection>>();

  1. 5: private ILoggerFactory _loggerFactory = new LoggerFactory();

  1. 6: private IConfiguration _config = new ConfigurationBuilder().AddEnvironmentVariables("ASPNETCORE_").Build();

  1. 7: 

  1. 8: public IWebHostBuilder ConfigureLogging(Action<ILoggerFactory> configureLogging)

  1. 9: {

  1. 10: _configureLoggingDelegates.Add(configureLogging);

  1. 11: return this;

  1. 12: }

  1. 13: 

  1. 14: public IWebHostBuilder ConfigureServices(Action<IServiceCollection> configureServices)

  1. 15: {

  1. 16: _configureServicesDelegates.Add(configureServices);

  1. 17: return this;

  1. 18: }

  1. 19: 

  1. 20: public string GetSetting(string key)

  1. 21: {

  1. 22: return _config[key];

  1. 23: }

  1. 24: 

  1. 25: public IWebHostBuilder UseLoggerFactory(ILoggerFactory loggerFactory)

  1. 26: {

  1. 27: _loggerFactory = loggerFactory;

  1. 28: return this;

  1. 29: }

  1. 30: 

  1. 31: public IWebHostBuilder UseSetting(string key, string value)

  1. 32: {

  1. 33: _config[key] = value;

  1. 34: return this;

  1. 35: }

  1. 36: ...

  1. 37: }

  1. 38: 

如上面的代码片段所示,我们创建了一个Configuration类型的字段(_config)来体现应用默认使用的配置,它默认采用环境变量(用于过滤环境变量的前缀为“ASPNETCORE_”)作为配置源,GetSetting和UseSetting方法操作的均为这个对象。另一个字段_loggerFactory表示默认使用的LoggerFactory,UseLoggerFactory方法指定的LoggerFactory用来对这个字段进行赋值。ConfigureLogging和ConfigureServices方法具有类似的定义,调用它们提供的委托对象都保存在一个集合之中,以待后用。

我们实现WebHostBuilder的核心方法Build来创建一个WebHost对象。通过上面的定义我们知道一个WebHostBuilder能够最终运行起来需要从ServiceProvider提供很多必需的服务,而这些服务最初都必需通过WebHostBuilder来注册,所以Build方法除了调用构造函数创建并返回一个WebHost对象之外,余下的工作就是注册这些必需的服务。我们可以简单列一列那些服务是必需的,如下所示的是一个不完全列表。

  • 用于注册服务和中间件的Startup对象。

  • 用来创建Logger的LoggerFactory对象
  • 构建中间件链表的ApplicationBuilder对象
  • 创建HTTP上下文的HttpContextFactory对象
  • 用户实现诊断功能的DiagnosticSource对象
  • 用来保存承载环境的HostingEnvironment对象

如下所示的定义在WebHostBuilder中的Build方法的定义。在这个方法中,我们按照上述这些系统服务以及用户服务(通过调用ConfigureServices方法注册的服务)的注册之后,创建并返回了一个WebHost对象。

  1. 1: public class WebHostBuilder : IWebHostBuilder

  1. 2: {

  1. 3: private List<Action<ILoggerFactory>> _configureLoggingDelegates = new List<Action<ILoggerFactory>>();

  1. 4: private List<Action<IServiceCollection>> _configureServicesDelegates = new List<Action<IServiceCollection>>();

  1. 5: private ILoggerFactory _loggerFactory = new LoggerFactory();

  1. 6: private IConfiguration _config = new ConfigurationBuilder().AddInMemoryCollection().Build();

  1. 7: 

  1. 8: public IWebHost Build()

  1. 9: {

  1. 10: //根据配置创建WebHostOptions

  1. 11: WebHostOptions options = new WebHostOptions(_config);

  1. 12: 

  1. 13: //注册服务IStartup

  1. 14: IServiceCollection services = new ServiceCollection();

  1. 15: if (!string.IsNullOrEmpty(options.StartupAssembly))

  1. 16: {

  1. 17: Type startupType = StartupLoader.FindStartupType(options.StartupAssembly, options.Environment);

  1. 18: if (typeof(IStartup).GetTypeInfo().IsAssignableFrom(startupType))

  1. 19: {

  1. 20: services.AddSingleton(typeof(IStartup), startupType);

  1. 21: }

  1. 22: else

  1. 23: {

  1. 24: services.AddSingleton<IStartup>(_ => new ConventionBasedStartup(StartupLoader.LoadMethods(_, startupType, options.Environment)));

  1. 25: }

  1. 26: }

  1. 27: 

  1. 28: //注册ILoggerFactory

  1. 29: foreach (var configureLogging in _configureLoggingDelegates)

  1. 30: {

  1. 31: configureLogging(_loggerFactory);

  1. 32: }

  1. 33: services.AddSingleton<ILoggerFactory>(_loggerFactory);

  1. 34: 

  1. 35: //注册服务IApplicationBuilder,DiagnosticSource和IHttpContextFactory

  1. 36: services

  1. 37: .AddSingleton<IApplicationBuilder>(_ => new ApplicationBuilder(_))

  1. 38: .AddSingleton<DiagnosticSource>(new DiagnosticListener("Microsoft.AspNetCore"))

  1. 39: .AddSingleton<IHttpContextFactory, HttpContextFactory>()

  1. 40: .AddOptions()

  1. 41: .AddLogging()

  1. 42: .AddSingleton<IHostingEnvironment, HostingEnvironment>()

  1. 43: .AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();

  1. 44:

  1. 45: //注册用户调用ConfigureServices方法设置的服务

  1. 46: foreach (var configureServices in _configureServicesDelegates)

  1. 47: {

  1. 48: configureServices(services);

  1. 49: }

  1. 50: 

  1. 51: //创建MyWebHost

  1. 52: return new WebHost(services, services.BuildServiceProvider(), options, _config);

  1. 53: }

  1. 54: }

  1. 55: 

虽然上面提供的WebHost和WebHostBuilder仅仅是WebHost和WebHostBuilder的模拟类。为了让读更加易于理解,我们刻意剔除了很多细节的东西,但是两者从实现原理角度来讲是完全一致的。不仅如此,我们自定义的这两个类型甚至可以执行运行的。

几个常用的扩展方法

WebHostBuilder在内部使用了配置,环境变量是默认采用的配置源,它的两个方法GetSetting和UseSetting以键值对的形式实现对配置项的获取和设置。除了UseSettings方法之外,我们还可以调用WebHostBuilder如下这个扩展方法UseConfiguration来进行配置项的设置,这个方法会将保存在指定Configuration中的配置原封不动地拷贝过来,它最终调用的依旧是UseSettings方法。

  1. 1: public static class HostingAbstractionsWebHostBuilderExtensions

  1. 2: {

  1. 3: public static IWebHostBuilder UseConfiguration(this IWebHostBuilder hostBuilder, IConfiguration configuration);

  1. 4: }

WebHostBuilder在创建WebHost的时候需要提供一个WebHostOptions对象,该对象最初是根据当前配置创建的。为了方便设置针对WebHostOptions的配置项,ASP.NET Core为我们定义了如下一系列的扩展方法,这些方法最终调用的也是这个UseSettings方法。

  1. 1: public static class HostingAbstractionsWebHostBuilderExtensions

  1. 2: {

  1. 3: public static IWebHostBuilder CaptureStartupErrors(this IWebHostBuilder hostBuilder, bool captureStartupErrors);

  1. 4: public static IWebHostBuilder UseContentRoot(this IWebHostBuilder hostBuilder, string contentRoot);

  1. 5: public static IWebHostBuilder UseEnvironment(this IWebHostBuilder hostBuilder, string environment);

  1. 6: public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, string startupAssemblyName);

  1. 7: public static IWebHostBuilder UseWebRoot(this IWebHostBuilder hostBuilder, string webRoot);

  1. 8: public static IWebHostBuilder UseUrls(this IWebHostBuilder hostBuilder, params string[] urls);

  1. 9: }

  1. 10: 

虽然服务器是必需的,但是WebHostBuilder并没有专门定义一个用于注册服务的方法,这是因为服务器也是作为一项基本的服务进行注册的。但是我们可以调用如下一个扩展方法UseServer实现针对服务器的注册,至于另一个扩展方法UseUrls,我们可以调用它来为注册的服务器设置监听地址。

  1. 1: public static class HostingAbstractionsWebHostBuilderExtensions

  1. 2: {

  1. 3: public static IWebHostBuilder UseServer(this IWebHostBuilder hostBuilder, IServer server);

  1. 4: public static IWebHostBuilder UseUrls(this IWebHostBuilder hostBuilder, params string[] urls);

  1. 5: }

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

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

    中间件的注册除了可以借助Startup对象(DelegateStartup或者ConventionBasedStartup)来完成之外,也可以利用另一个叫做StartupFilter的对象来实现.所谓 ...

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

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

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

    我们在<服务器在管道中的"龙头"地位>中对ASP.NET Core默认提供的具有跨平台能力的KestrelServer进行了介绍,为了让读者朋友们对管道中的服务器具有更 ...

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

    ASP.NET Core管道由注册的服务器和一系列中间件构成.我们在上一篇中深入剖析了中间件,现在我们来了解一下服务器.服务器是ASP .NET Core管道的第一个节点,它负责完整请求的监听和接收, ...

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

    ASP.NET Core管道虽然在结构组成上显得非常简单,但是在具体实现上却涉及到太多的对象,所以我们在 "通过重建Hosting系统理解HTTP请求在ASP.NET Core管道中的处理流 ...

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

    ASP.NET Core的核心是通过一个Server和若干注册的Middleware构成的管道,不论是管道自身的构建,还是Server和Middleware自身的实现,以及构建在这个管道的应用,都需要 ...

  7. 学习ASP.NET Core Razor 编程系列二——添加一个实体

    在Razor页面应用程序中添加一个实体 在本篇文章中,学习添加用于管理数据库中的书籍的实体类.通过实体框架(EF Core)使用这些类来处理数据库.EF Core是一个对象关系映射(ORM)框架,它简 ...

  8. 学习ASP.NET Core Razor 编程系列四——Asp.Net Core Razor列表模板页面

    学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...

  9. 学习ASP.NET Core Razor 编程系列五——Asp.Net Core Razor新建模板页面

    学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...

随机推荐

  1. java中的字符串相关知识整理

    字符串为什么这么重要 写了多年java的开发应该对String不陌生,但是我却越发觉得它陌生.每学一门编程语言就会与字符串这个关键词打不少交道.看来它真的很重要. 字符串就是一系列的字符组合的串,如果 ...

  2. LeetCode-4MedianofTwoSortedArrays(C#)

    # 题目 4. Median of Two Sorted Arrays There are two sorted arrays nums1 and nums2 of size m and n resp ...

  3. Ubuntu 16.10 安装byzanz截取动态效果图工具

    1.了解byzanz截取动态效果图工具 byzanz能制作文件小,清晰的GIF动态效果图,不足就是,目前只能通过输入命令方式来录制. byzanz主要的参数选项有: -d, --duration=SE ...

  4. [转]Patch文件结构详解

    N久不来 于是不知道扔在哪儿于是放这里先 如果你觉得碍事的话 帮我扔到合适的版块去.. 导读这是一篇说明文 它介绍了标准冒险岛更新文件(*.patch;*.exe)的格式文章的最后附了一段C#的参考代 ...

  5. Mybatis批量删除

    <delete id="deleteByStandardIds"> delete from t_standard_catalog where standard_id i ...

  6. BPM公文管理解决方案分享

    一.方案概述 公文作为一种规范性文书,具有法律性.指导性.政令性强的特点,是企事业单位政令上通下达的重要方式.及时.准确.安全地处理.控制和管理公文,方能保障企事业单位正常运转,确保组织权威和政令畅通 ...

  7. docker4dotnet #1 – 前世今生 & 世界你好

    作为一名.NET Developer,这几年看着docker的流行实在是有些眼馋.可惜的是,Docker是基于Linux环境的,眼瞧着那些 java, python, node.js, go 甚至连p ...

  8. 开始mono开发

    使用mono框架开发android程序,第一步当然是构建开发环境,严格意义上说是使用 mono for android开发android程序. 参考Mono for Android安装配置破解  mo ...

  9. linux系统下基于mono部署asp.net,使用ef6与mysql出现的问题【索引】

    git clone github.com/mono的源码,日期:2014-06-19,百度网盘链接:http://pan.baidu.com/s/1kTG9EUb 关于asp.net利用mono部署到 ...

  10. 【已解决】Https请求——基础连接已经关闭 发送时发生错误

    本人在做商用项目的推送消息功能时,借助第三方推送服务.这里避免有打广告的嫌疑,就不报名字了.由于是通过调用API接口,所以Post方法是自己写的,但是在开发环境是可以正常推送的,但是一上线就出各种问题 ...