【一文详解】知识分享:(ASP.Net Core基础学习及快速入门)

背景知识

相关术语

.Net

.NET是微软的一个开发平台,这个平台的一大特点就是跨语言性,不管是什么语言,c、c++、c#、F#、J#、vb等语言都可以用这个平台合作开发;

.NET,它是微软创建的一个用于构建多种不同类型的应用程序的开发人员平台。

.NET 是一个广泛的术语,用于描述整个 Microsoft 的软件开发生态系统。它包括了多种不同的技术和工具,用于构建不同类型的应用程序,包括 Windows 应用程序、Web 应用程序、移动应用程序等。

在过去几年中,所说的.NET主要是指向的是.NET Framework,它是.NET简称的最早拥有者,有着近20年的历史,开发的软件只能在Windows系统下运行。而在这两年中,人们所说的.NET又慢慢指向了.NET Core。

.Net 和Java有何不同?

.NET是个开发平台;Java是一种编程语言。

两者没啥可比性,硬要比的话应该是C#和Java比。

不过平时口头上(有时候会把C#和.NET等同起来),确实会出现拿这两者作比较,这个时候通常是讲.NET相关技术和Java相关技术比较。

因为两者确实有很多对应的技术,比如.NET有ASP,Java有JSP;桌面端.NET有WinForm、WPF,Java有swing、Javafx等。当然它们各有千秋,有些技术不适应环境已经快被淘汰了。

总之,我认为两者各有千秋,C#在工业领域应用广;Java在企业应用广。从当下国内大环境来讲,Java是热于C#的,不过这不是C#的问题,是生态、历史等多因素造成的。同时,Java求职市场也更卷,国内大厂应用多。


.NET Framework

.NET Framework 是最初的 .NET 实现,主要用于 Windows 平台上的应用程序开发,并且依赖于特定的 Windows 版本。它提供了大量的类库,支持 Web、桌面、移动端等各种应用程序的开发。但由于它只能在 Windows 平台上运行,所以在跨平台方面存在局限性。


.NET Core

.NET Core 是 .NET 的全新实现,旨在解决 .NET Framework 的跨平台问题,它可以在 Windows、Mac 和 Linux 等操作系统上运行。.NET Core 具有模块化和轻量级的特性,适合用于云计算和容器这样的环境。

  • 支持独立部署,不会存在互相影响的问题
  • 彻底模块化
  • 运行效率高
  • 不依赖于IIS
  • 跨平台
  • 符合现代开发理念:依赖注入,单元测试等

.NetCore版本发布

**时间 ** **版本 ** 备注
2016年2月 .Net Core 1.0 RC1
2016年5月 .Net Core 1.0 RC2
2016年6月 .Net Core 1.0
2017年3月 .Net Core 1.1
2017年8月 .Net Core 2.0
2018年5月 .Net Core 2.1(LTS) 长期支持版本
2018年12月 .Net Core 2.2
2019年9月 .Net Core 3.0(Maintenance)
2019年12月 .Net Core 3.1(LTS) 长期支持版本
2020年11月 .NET5.0
2021年11月 .NET6.0(LTS) 长期支持版本
2022年11月 .NET7.0
2023年11月 .NET8.0(LTS) 长期支持版本

其中LTS版本是长期支持版本,相对比较推荐; 目前3.1是推荐使用版本,后续微软计划每一年发布一个版本,其中偶数版本为LTS版本;

因为.Net Core是跨平台的,每一个ASP.NET Core 应用程序,从本质上来说,都是一个独立的控制台应用,并不需要依托IIS(web服务器)来运行,这也是它能实现跨平台的一个基础。


可以这么类比,之前的.Net FrameWork部署在IIS服务器类似于Java之前的SSM阶段部署在Tomcat服务器中。

而现在的.Net Core不用依托IIS服务器类似于现在Java的SpringBoot阶段内嵌了服务器,所以可以单独启动。


.Net Core SDK

SDK 包含构建和运行.NET Core 应用程序所需的一切。

.Net Core Runtime

  • .NET Core Runtime已经包含在 SDK 中。因此,如果您已安装 SDK,则无需安装 .NET Core Runtime
  • .NET Core Runtime 仅包含运行现有.NET Core 应用程序所需的资源。

.NET Standard

.NET Standard 不是一个实际的 .NET 运行时实现,而是定义了一组 API,这些 API 是所有 .NET 实现都需要支持的。通过对 .NET Standard 的支持,开发者可以编写一次代码,并在所有 .NET 平台上运行,提高了代码的复用性。


.NET 5+

.NET 5+(包括 .NET 5、.NET 6 等) 是 .NET Core 的后续版本,它将 .NET Framework 和 .NET Core 合并为一个统一的平台。

即从NET5.0版本开始,将不再区分.NetFrameWork和.NetCore,统一叫.NET,之前之所以叫.NetCore,应该就是为了区分两个版本;

这意味着现在有一个统一的 .NET 平台,可以用于构建跨平台应用程序,而不再需要选择 .NET Framework 或 .NET Core。

.NET 5 支持更多的应用类型和平台,包括桌面应用、Web 应用、云服务、移动设备、游戏、物联网,提供了更好的性能和生产力。

从.NET 5开始,微软开始淡化其他叫法,统一为.NET后续默认.NET指的就是.NET Core.


Asp.Net

ASP.NET 是专门用于构建 Web 应用程序的子集。ASP.NET 应用程序运行在 .NET 运行时之上。ASP.NET 主要关注 Web 开发,提供了专门的工具和框架来简化 Web 应用程序的开发和部署。


Asp.Net Core

ASP.NET Core 是 .NET Core 的一个组件,专门用于构建 Web 应用程序和服务。ASP.NET Core 构建在 .NET Core 之上,利用了 .NET Core 提供的跨平台性和性能优势。总的来说,.NET Core 提供了一个通用的开发平台,而 ASP.NET Core 则是在此基础上专注于 Web 开发,提供了丰富的 Web 开发功能和工具.

对于ASP.NET Core应用程序来说,我们要记住非常重要的一点是:

ASP.NET Core应用程序拥有一个内置的Self-Hosted(自托管)Web Server(Web服务器),用来处理外部请求。

不管是托管还是自托管,都离不开Host(宿主)

在ASP.NET Core应用中通过配置并启动一个Host来完成应用程序的启动和其生命周期的管理。而Host的主要的职责就是Web Server的配置和**Pilpeline(请求处理管道)**的构建。

txt 复制代码
ASP.NET Core应用程序的启动主要包含三个步骤:
     1.CreateDefaultBuilder():创建IWebHostBuilder
     2.Build():IWebHostBuilder负责创建IWebHost
     3.Run():启动IWebHost

所以ASP.NET Core应用的启动本质上是启动作为宿主的WebHost宿主对象。

ASP.NET和ASP.NET Core都是由Microsoft开发的,在.NET平台上运行的框架,用于开发Web应用程序。以下是他们之间的一些主要区别和联系:

  1. 平台兼容性:ASP.NET仅运行在Windows平台上,而ASP.NET Core是跨平台的,可在Windows、Linux和macOS上运行。这使得ASP.NET Core更具灵活性和广泛的应用可能性。
  2. 性能ASP.NET Core被设计为更高效,性能常常超过传统的ASP.NET这主要是由于ASP.NET Core的模块化设计和优化。
  3. 模块化和微服务ASP.NET Core具有模块化的框架设计,这使得ASP.NET Core更加轻便和灵活,架构的应用。而ASP.NET没有这样的设计。
  4. 版本依赖ASP.NET Core的一个重要特点是侧重于应用级别的依赖,这意味着不同的ASP.NET Core应用可以运行在同一系统上,而不会互相影响。而ASP.NET依赖于系统级别的.NET Framework版本。
  5. 集成性ASP.NET Core通过内置的依赖注入、中间件配置来提供更大的控制和更好的集成性。而ASP.NET则需要依赖第三方库 like Unity, Ninject等来实现依赖注入。
  6. API的统一性:ASP.NET分为Web API和MVC两个部分,他们有各自独立的类库和命名空间。而在ASP.NET Core中,Web API和MVC是统一起来的,在构建RESTful服务时会更加方便。
  7. ASP.Net Core可以托管在更多的地方:如IIS, Apache,Docker,自托管等。

在联系方面,两者都是用于开发Web应用程序的框架,并且都使用.NET技术。代码语法、设计原则、编程模式有很多相似之处,对于ASP.NET的开发者来说,转到ASP.NET Core会更容易。

归根结底,选择ASP.NET还是ASP.NET Core,取决于项目需求,平台依赖,性能需求和个人或团队的技能水平。


程序集和库

.dll 或 .exe 文件,其中包含一组可由应用程序或其他程序集调用的 API 。

程序集可以包括接口、类、结构、枚举和委托等类型。 有时,项目的 bin 文件夹中的程序集被称为二进制文件。

而库或框架是由应用或其他库调用的 API 集合。

.NET 库由一个或多个程序集组成。库和框架通常作同义词使用。

什么是CLR和IL

.NET CLR(Common Language Runtime)是.NET的核心组成部分,它为.NET应用程序提供了一个运行环境。


IL(Intermediate Language)是.NET Framework中的一种中间语言,也被称为CIL (Common Intermediate Language) 或 MSIL (Microsoft Intermediate Language)。

在.NET环境中,所有的.NET代码(无论它是由C#、VB.NET还是其他.NET支持的语言编写的)在编译时,都首先被转换为IL。IL是一种面向堆栈的计算机指令集,设计用于由具有高级语言特性的编程语言生成,并可进一步由JIT(Just-In-Time)编译器在运行时转换为本地代码。

这个过程中的几个关键点包括:

  • 平台独立性:通过编译成IL,.NET应用程序可以在任何平台上运行,只要该平台有.NET运行环境即可。
  • 语言互通性:由于所有.NET语言都编译到统一的IL,因此不同语言编写的代码可以轻松地进行交互。
  • 优化:在运行时,CLR的JIT编译器可以根据目标计算机的具体硬件进行优化,以提高应用程序的性能。

一个.NET CLR结构的图片通常会包括以下部分:

  • 最上层是各种**.NET应用程序**,它们是由各种.NET语言(如C#、VB.NET、F#等)编写的。

  • 这些应用程序在运行时都依赖于下一层的Common Language Runtime (CLR)。CLR是运行所有.NET代码的环境。

  • 在CLR之下,可能会有几个子组件,例如:

    • JIT编译器:负责将中间语言(CIL)转换成特定平台上的机器语言。
    • 垃圾收集器:负责自动管理内存,回收不再使用的对象。
    • 安全组件:负责执行访问检查和权限验证等安全操作。
    • Type Checker:负责确保类型的正确性和安全性。
  • 最底层是操作系统,所有的.NET应用程序和CLR最终都运行在这个操作系统上。

注意:

.NET框架的CLR和Java虚拟机在某种程度上可以进行比较。

Java中的虚拟机(Java Virtual Machine - JVM)是Java平台的核心组件,它负责解释和执行Java字节码。JVM允许Java程序在不同的操作系统上运行,并提供了垃圾回收、内存管理和安全性等功能。

类似地,.NET框架也提供了类似的功能。

.NET框架有一个称为Common Language Runtime(CLR)的组件,它负责执行和管理.NET应用程序。CLR解释和执行IL(Intermediate Language)代码,这是.NET应用程序编译后生成的中间语言。

因此,从某种程度上说,.NET框架中的CLR可以类比于Java中的虚拟机。两者都负责解释和执行中间代码,并提供了一些共享功能,如垃圾回收和安全性。

然而,请注意,Java虚拟机和.NET框架之间仍存在一些区别。它们使用不同的中间语言(Java字节码和IL代码)和不同的运行时环境。此外,Java平台和.NET平台也具有不同的生态系统和工具链。

综上所述,虽然.NET框架的CLR在某种程度上可以类比于Java中的虚拟机,但它们仍有一些差异。每个技术栈都有各自的特点和用途,因此选择使用哪种取决于项目需求和开发团队的首选项。

asp.net core启动流程解析

一个WebAPI项目启动方式本质也是一个控制台程序,程序入口都是从Main函数开始,就从里面方法看看大概都做了什么,其中择取几个方法源码简要说明主要功能。

1. Host.CreateDefaultBuilder方法:

c# 复制代码
public static IHostBuilder CreateDefaultBuilder(string[] args)
{
    //实例化一个HostBuilder
    var builder = new HostBuilder();
    //设置根目录
    builder.UseContentRoot(Directory.GetCurrentDirectory());
    //设置 Host相关配置的配置源
    builder.ConfigureHostConfiguration(config =>
    {
        //从环境变量中获取,前缀名为DOTNET_
        config.AddEnvironmentVariables(prefix: "DOTNET_");
        //如果命令行中有参数,可从命令行中读取
        if (args != null)
        {
            config.AddCommandLine(args);
        }
    });
    //设置应用程序配置的配置源
    builder.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() && !string.IsNullOrEmpty(env.ApplicationName))
        {
          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) =>
    {
        //判断操作系统
        var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
        //如果是Windows系统,配置对应的显示级别
        //IMPORTANT: This needs to be added *before* configuration is loaded, this lets
       //the defaults be overridden by the configuration.
        if (isWindows)
        {
        // Default the EventLogLoggerProvider to warning or above
        logging.AddFilter<EventLogLoggerProvider>(level => level >= LogLevel.Warning);
        }
        //获取配置文件中Logging 段相关的配置信息添加到日志配置中
        logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
        //在控制台输出,所以启动程序的时候就看见输出日志
        logging.AddConsole();
        //在Debug窗口输出
        logging.AddDebug();
        logging.AddEventSourceLogger();
        //如果是Windows系统,可以在系统日志中输出日志
        if (isWindows)
        {
          // Add the EventLogLoggerProvider on windows machines
          logging.AddEventLog();
        }
    })//使用默认的依赖注入容器
    .UseDefaultServiceProvider((context, options) =>
    {
        var isDevelopment = context.HostingEnvironment.IsDevelopment();
        options.ValidateScopes = isDevelopment;
        options.ValidateOnBuild = isDevelopment;
    });
    return builder;
}

2. ConfigureWebHostDefaults 方法:

c# 复制代码
public static IHostBuilder ConfigureWebHostDefaults(this IHostBuilder builder, Action<IWebHostBuilder> configure)
{
    return builder.ConfigureWebHost(webHostBuilder =>
    {
        //指定是用的服务器及集成一些默认管道
        WebHost.ConfigureWebDefaults(webHostBuilder);
         //调用传入的委托,这里是外部指定Startup类做服务注册和管道配置
        configure(webHostBuilder);
    });
}

2.1 WebHost.ConfigureWebDefaults方法:

c# 复制代码
internal static void ConfigureWebDefaults(IWebHostBuilder builder)
{
    builder.ConfigureAppConfiguration((ctx, cb) =>
    {
    if (ctx.HostingEnvironment.IsDevelopment())
    {
    //静态文件环境的配置启用
    StaticWebAssetsLoader.UseStaticWebAssets(ctx.HostingEnvironment, ctx.Configuration);
    }
    });
    //指定Kestrel作为默认的Web服务器
    builder.UseKestrel((builderContext, options) =>
    {
    options.Configure(builderContext.Configuration.GetSection("Kestrel"));
    })// 服务中间的注册,包含路的中间件注册
    .ConfigureServices((hostingContext, services) =>
    {
    // 针对配置节点AllowedHosts改变时的回调
    // 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>();
    if (string.Equals("true", hostingContext.Configuration["ForwardedHeaders_Enabled"], StringComparison.OrdinalIgnoreCase))
    {
    services.Configure<ForwardedHeadersOptions>(options =>
    {
    options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    // Only loopback proxies are allowed by default. Clear that restriction because forwarders are
    // being enabled by explicit configuration.
    options.KnownNetworks.Clear();
    options.KnownProxies.Clear();
    });
    services.AddTransient<IStartupFilter, ForwardedHeadersStartupFilter>();
    }
    services.AddRouting();
    })//对使用IIS相关中间件
    .UseIIS()
    .UseIISIntegration();
}

3. Build方法,其实这个方法就是根据之前配置构造出一个IHost对象:

c# 复制代码
public IHost Build()
{
    if (_hostBuilt)
    {
        throw new InvalidOperationException("Build can only be called once.");
    }
    _hostBuilt = true;
    //执行ConfigureHostConfiguration添加的一系列配置回调方法

    BuildHostConfiguration();
    //运行环境相关创建,如ApplicationName、EnvironmentName、ContentRootPath等
    CreateHostingEnvironment();
    //构建HostBuilder
    CreateHostBuilderContext();
    //执行ConfigureAppConfigureation添加的一系列配置回调方法
    BuildAppConfiguration();
    //注入默认服务如:IHost、ILogging等,执行ConfigureServices添加的一系列回调方法
    CreateServiceProvider();
    return _appServices.GetRequiredService<IHost>();
}

4. Run()方法,开启服务器,之后就可以进行请求了:


综上几个关键方法,从其中Host这个关键词出现很多次!

  • 其实在Asp.Net Core应用中是通过配置并启动一个Host来完成应用程序的启动和生命周期的管理。

  • 而Host主要就是对Web Server的配置和请求处理管理的管理,

    简要流程如下图:

NuGet(包管理)

VS的图形界面方式

找到工具 ====> NuGet包管理器 ===> 管理解决方案的NuGet程序包:

按需下载,可以选择版本,在"浏览"中搜索自己所需要的包,然后下载即可,同时可以查看第三方软件包的相关信息。

CLI方式(命令行接口方式)

更新或卸载程序包:

  • 安装: Install-Package XXX
  • 卸载:Uninstall-Package XXX
  • 更新:Updata-Package XXX

一般情况下,CLI方式默认安装的将会是最新版本的软件包,但是可以根据自己需求调整包的版本。

步骤如下:

  1. 在NuGte官网上找到自己想要获取的第三方软件包的详情页面,在Package Manager中复制命令:

  2. 打开NuGet程序包管理器控制台:

  3. 在控制台输入复制的命令即可完成程序包的下载。同时在控制台中,还可选择需要引入程序包的项目:

项目结构

了解项目结构和启动文件和启动顺序

  • 带有这种网络地球图标的才是web项目目录。

  • wwwroot:网站的静态文件目录。

    此目录存放项目中所有公共的、静态的资源,如 css、js 和 图片文件。

    如果不做额外的配置,那么 wwwroot 将是应用程序中唯一可以存放静态文件的位置.

    如果没有此目录,需要手动在web项目目录下新建个wwwroot文件夹,新建完文件夹后若此文件夹的图标没有变化的话,则需在vs中重新打开解决方案文件即可。

  • Controllers:手动创建存放各种控制器Controllers。

  • appsettings.json:配置文件,比如数据库连接字符串等配置信息。

    • 用途appsettings.json 主要用于存储应用程序的配置数据,例如数据库连接字符串、日志设置、应用程序的行为配置等。

      这些配置数据通常是应用程序在运行时需要读取的信息。

      .Net Core中有个IConfiguration类用于读取配置文件

    • 内容appsettings.json 文件通常包含一个JSON格式的配置对象,其中包括键值对,用于指定各种应用程序设置。

    • 环境特定设置 :通常,appsettings.json 文件可以有多个版本,每个版本对应于不同的应用程序环境(例如开发、测试和生产环境)。这些环境特定的配置文件可以命名为appsettings.Development.jsonappsettings.Production.json等,以便在不同环境中使用不同的配置。

  • Program.cs:程序入口文件.

    主应用程序类,包含了Main主函数来运行应用程序

    ASP.NET Core应用程序需要由**Host(宿主)**进行管理,宿主为其提供运行环境并负责启动。

    所以Main函数主要是用来初始化宿主环境,而宿主环境的初始化需要借助WebHostBuilder

    初始化完毕后,调用Run()方法来启动应用程序。

    这个Main()方法配置 ASP.NET Core 并启动它,此时,它成为一个 ASP.NET Core Web 应用程序。

    因此,如果你跟踪一下Main()方法,它会调用 CreateWebHostBuilder()方法传递命令行参数。

    然后你就可以看到,CreateWebHostBuilder()方法返回一个实现 IWebHostBuilder 的对象。 在此对象上,调用Build()方法,会将我们的 ASP.NET Core 应用程序生成并且托管到服务器上。

    在服务器上的程序调用Run() 方法,该方法运行Web 应用程序并开始侦听传入的 HTTP 请求。

    CreateDefaultBuilder()方法执行多项操作来创建服务器.

    • 设置Web服务器
    • 加载主机和应用程序的配置信息
    • 配置日志记录
  • Startup.cs:此文件是 ASP.NET Core 项目的启动文件

    Startup 类是 ASP.NET Core 应用程序启动时默认调用的类,

    其中定义了项目的配置和中间件。

    Program.csStartup.cs 的区别在于 Program.cs 会调用 Startup.csProgram 类会实例化 Startup 类.

    这个可以通过 Program.cs 中的代码看出来

    c# 复制代码
    WebHost.CreateDefaultBuilder(args).UseStartup<Startup>();

    除了 Program.cs 会调用 Startup.csProgram 类会实例化 Startup 类之外。但 Startup 能做的不仅仅是这些,可以说 ASP.NET Core 中的各个组件和中间件都会和 Startup 类打交道。


    Startup.cs 文件中有一个 Startup 类,在这个类中可以配置应用程序,甚至配置配置源。

    Startup 类可以用来定义请求处理管道配置应用程序需要的服务

    Startup类承担应用的启动任务,所以按照约定,起名为Startup,不过你可以修改为任意类名(强烈建议类名为Startup)。

    默认的Startup结构很简单,包含:

    • 构造函数
    • Configuration属性
    • ConfigureServices方法
    • Configure方法 【必需的

    Startup 类必须是公开的,且必须包含以下两个方法:

    • ConfigureServices() 方法:

      此方法用于定义应用程序所需要的服务,例如 ASP.NET Core MVC 、 Entity Framework Core 和 Identity 等等。

      常用的服务有(部分服务框架已默认注册):

      • AddControllers:注册Controller相关服务,内部调用了AddMvcCoreAddApiExplorerAddAuthorizationAddCorsAddDataAnnotationsAddFormatterMappings等多个扩展方法
      • AddOptions:注册Options相关服务,如IOptions<>IOptionsSnapshot<>IOptionsMonitor<>IOptionsFactory<>IOptionsMonitorCache<>等。很多服务都需要Options,所以很多服务注册的扩展方法会在内部调用AddOptions
      • AddRouting:注册路由相关服务,如IInlineConstraintResolverLinkGeneratorIConfigureOptions<RouteOptions>RoutePatternTransformer
      • AddAddLogging:注册Logging相关服务,如ILoggerFactoryILogger<>IConfigureOptions<LoggerFilterOptions>>
      • AddAuthentication:注册身份认证相关服务,以方便后续注册JwtBearer、Cookie等服务
      • AddAuthorization:注册用户授权相关服务
      • AddMvc:注册Mvc相关服务,比如Controllers、Views、RazorPages等
      • AddHealthChecks:注册健康检查相关服务,如HealthCheckServiceIHostedService
    • Configure() 方法

      此方法用于定义请求管道中的中间件 。也就是说,Configure() 方法可以用来定义应用程序如何响应请求。如果希望应用程序的行为不同,需要在 Configure() 方法中添加其他代码来更改请求管道。

    • ConfigureServices()方法配置应用程序所需的服务
    • Configure()方法配置应用程序的请求处理管道

    Startup.cs 文件中默认的内容如下:

    c# 复制代码
    //默认的Startup结构很简单,包含:
    
    //构造函数
    //Configuration属性
    //ConfigureServices方法
    //Configure方法
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Http;
    using Microsoft.Extensions.DependencyInjection;
    
    namespace HelloWorld
    {
        public class Startup
        {
            // 该ConfigureServices方法在运行时被调用。
            // 可以使用该方法将服务添加到容器中
            //该方法在Configure方法之前被调用
            //该方法要么无参数,要么只能有一个参数且类型必须为IServiceCollection
            //该方法内的代码大多是形如Add{Service}的扩展方法
            public void ConfigureServices(IServiceCollection services)
            {
            }
    
            // 该Configure方法在运行时被调用
            // 可以使用该方法来配置 HTTP 请求管道
            //该方法是必须的,该方法用于配置HTTP请求管道,通过向管道添加中间件,应用不同的响应方式。
            //该方法在ConfigureServices方法之后被调用
            //该方法中的参数可以接受任何已注入到DI容器中的服务
            //该方法内的代码大多是形如Use{Middleware}的扩展方法
    		//该方法内中间件的注册顺序与代码的书写顺序是一致的,先注册的先执行,后注册的后执行
    
            public void Configure(IApplicationBuilder app, IHostingEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
    			//Run() 方法不经常见,它是调用中间件的终端。
                //即:如果在 app.Run() 方法的后面继续注册另一个中间件,那么注册的那个中间件永远不会被调用,因为 Run() 方法是注册中间件的终端,在它之后,永远不会调用下一个中间件
                app.Run(async (context) =>
                {
                    await context.Response.WriteAsync("Hello World!");
                });
            }
        }
    }

  • 依赖项:管理项目所依赖的第三方组件的安装,配置,升级

  • Properties:存放了一些 .json 文件用于配置 ASP.NET Core 项目

  • launchSettings.json: 启动配置文件,为一个 ASP.NET Core 应用保存特有的配置标准,用于应用的启动准备工作

    • 用途launchSettings.json 用于配置项目的启动设置,例如应用程序的启动方式、调试设置、环境变量等。

      这个文件通常用于定义开发和调试时的应用程序行为。

    • 内容launchSettings.json 文件包含一个JSON格式的对象,其中包含了各种项目启动相关的配置,如命令行参数、环境变量、启动浏览器、应用程序URL等。

    • 该文件中出现的几个参数:

      • commandName:指定要启动的Web服务器,有三个可选值:
        • Project:启动 Kestrel
        • IISExpress:启动IIS Express
        • IIS:不启用任何Web服务器,使用IIS
      • dotnetRunMessages:bool字符串,指示当使用 dotnet run 命令时,终端能够及时响应并输出消息
      • launchBrowser:bool值,指示当程序启动后,是否打开浏览器
      • launchUrl:默认启动路径
      • applicationUrl:应用程序Url列表,多个URL之间使用分号(;)进行分隔。当launchBrowser为true时,将{applicationUrl}/{launchUrl}作为浏览器默认访问的Url
      • environmentVariables:环境变量集合,在该集合内配置环境变量.

    疑问:appsetting.json文件和launchsetting.json文件的区别和联系?


    1. 联系
      • launchsettings.jsonappsettings.json 都是用于配置应用程序的文件,但它们的职责和用途不同。
    2. 使用场景
      • 在开发过程中,通常会同时使用这两个文件:
        • launchsettings.json 用于配置调试器如何启动应用程序,包括选择要启动的项目、使用的环境变量等。
        • appsettings.json 用于存储应用程序的配置信息,例如数据库连接字符串、日志级别等。
    3. 环境特定的配置
      • appsettings.json 文件可以根据不同的环境(如开发、生产)创建不同的版本(如 appsettings.Development.json)。这样可以在不同环境下使用不同的配置。
    4. 配置管理
      • appsettings.json 中的配置信息可以在应用程序中通过依赖注入或者 Configuration 对象来读取和使用。
      • appsetting,json文件有点类似于java中主要的核心配置文件。

.net core项目两种启动方式

参考链接:

  • [控制台启动.Net Core 3.1 Web应用程序 项目 - 汪小让 - 博客园 (cnblogs.com)](https://www.cnblogs.com/wangxiaorang/p/14459423.html)

    注意:

    • .net core项目编译生成的文件夹,除了命令行用dotnet命令运行dll文件的方式可以启动项目。

    • 还可以在项目编译生成的文件夹中直接运行相应的exe程序文件来启动项目。

  • [.Net Core 项目启动方式 - 贰拾~ - 博客园 (cnblogs.com)](https://www.cnblogs.com/zousc/p/12421131.html)


多环境配置

一款软件,一般要经过需求分析、设计编码,单元测试、集成测试以及系统测试等一系列测试流程,验收,最终上线。那么,就至少需要4套环境来保证系统运行:

  • Development:开发环境,用于开发人员在本地对应用进行调试运行
  • Test:测试环境,用于测试人员对应用进行测试
  • Staging:预发布环境,用于在正式上线之前,对应用进行集成、测试和预览,或用于验收
  • Production:生产环境,应用的正式线上环境

环境配置方式

通过环境变量ASPNETCORE_ENVIRONMENT指定运行环境

注意:如果未指定环境,默认情况下,为 Production

在项目的Properties文件夹里面,有一个"launchSettings.json"文件,该文件是用于配置VS中项目启动的。

.net core读取配置源数据

参考学习文章: 跟我一起学.NetCore之配置初体验 (qq.com)

跟我一起学.NetCore之自定义配置源-热更新-对象绑定 (qq.com)


项目文件(xx.csproj)

我们使用 C#作为编程语言,因此项目文件具有.csproj 扩展名。

.csproj 文件是 ASP.NET Core 项目的核心文件之一,它包含了项目的配置信息和元数据。这个文件使用 XML 格式,定义了项目的结构、依赖项、编译选项等。

作用:

  • 定义项目结构:csproj 文件描述了项目中包含的文件、目录以及它们的相对路径。
  • 管理依赖项:通过引入 NuGet 包或其他项目的引用,来管理项目的依赖关系。
  • 配置编译选项:可以指定编译器选项、目标框架等,以控制项目的编译行为。
  • 构建和发布配置:定义了如何构建和发布项目,包括生成输出路径、发布目标等。

项目文件不包含任何文件夹或文件引用.

简单解释后的意思就是。在以前的 ASP.NET 中,当我们使用解决方案资源管理器 向项目添加文件或文件夹时,项目文件 中会包含对该文件或文件夹的引用。但是在 ASP.NET Core 中,项目文件不包含任何文件夹或文件引用。

在解决方案中,右键单击项目名称并选择"编辑 xxxx.csproj" 文件。

这将在编辑器中打开.csproj 文件。

xml 复制代码
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
    <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App"/>
    <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All"/>
  </ItemGroup>
</Project>
  • <Project Sdk="Microsoft.NET.Sdk.Web"> 指定了项目所使用的 SDK(Software Development Kit),这里使用了 ASP.NET Core 的 Web SDK。
  • <PropertyGroup> 包含了一组属性,用于配置项目的基本信息。
    • <TargetFramework> 定义了项目的目标 .NET Core/.NET 5 版本。
    • <AspNetCoreHostingModel> 指定了 ASP.NET Core 的托管模型(InProcess 或 OutOfProcess)。
    • <RootNamespace> 定义了项目的根命名空间。
    • <OutputPath> 指定了项目编译输出的路径。
  • <ItemGroup> 用于管理项目的依赖项。
    • <PackageReference> 定义了项目引用的 NuGet 包。这里示例中引用了一些 ASP.NET Core 和 Entity Framework Core 的相关包。

中间件

.NET本身就是一个基于中间件(middleware)的框架,它通过一系列的中间件组件来处理HTTP请求和响应。

一个管道可以有一个或多个中间件,而中间件的职责是根据HttpContext处理HTTP请求,然后往Response里填充东西,最后完成整个Response的输出。

Startup 类中的 Configure() 方法用于定义请求管道中的中间件。

中间件控制我们的应用程序如何响应 HTTP 请求,它还可以控制我们的应用程序在发生错误时的显示的内容,它是我们认证和授权用户执行特定操作的关键部分。

中间件是一种装配到应用程序管道以处理请求和响应的组件

每个中间件(组件):

  1. 可以选择是否将请求传递到请求管道中的下一个组件。
  2. 可在调用管道中的下一个组件的前后执行工作。

请求管道中的每个中间件组件负责调用管道中的下一个组件,或在适当情况下使链发生短路,从而避免不必要的工作。

真正理解起来很简单,就是流水化的作业。

管道数据是如何流通的呢?如下图所示:

Request进入Middleware 1,叠加一层处理的逻辑代码到HttpContext(切确说是HttpContext的Response对象),然后调用next()进入到下一个Middleware 2,依次递推,最后所有的逻辑代码叠加完毕后返回前端。


定义中间件:

Startup.cs文件中的Configure()方法可以配置应用程序的请求处理管道以及定义中间件。

**定义: **修改 Startup 类中的 Configure() 方法: 往其中添加如下语句 app.Use某个中间件()即可。

注意:app.run()是一个终端中间件,请求到达这里后就短路了,不会继续再调用下一个中间件。

**所以: **增加其他中间件的app.Usexx()语句需要放在app.run()方法前面此中间件才能定义成功, 因为app.run()是中间件定义的终点了,如果这个语句放在app.run()方法后面,那么定义这个中间件将不会生效。


常用的中间件


常用的中间件:

  • app.UseDeveloperExceptionPage():这个中间件与其它中间件有些不同,其它中间件通常会检查传入的请求并对该请求做出一些响应UseDeveloperExceptionPage中间件不关心传入的请求,因为它总是在管道后发生,它会调用下一个中间件,然后等待管道后面的任何事情是否会产生异常,如果有异常,这个中间件会给返回一个错误页面,并显示有关该异常的详细信息.

    我们还可以从 UseDeveloperExceptionPage 中间件返回的结果中看到原始异常详细信息.

    所有这些信息对开发人员都非常有用

    所以必须尽可能的在管道中提早注入。;

  • app.Use() 注册的中间件会依次在请求管道中执行,每个中间件都可以在请求到达时执行前置逻辑,然后将请求传递给下一个中间件。

    app.Use() 中间件中,必须调用 await next() 才能将请求传递给下一个中间件。否则,请求会在当前中间件中终止。

    1. 在中间件中,如果已经开始给客户端发送Response,请千万不要调用next.Invoke / next(),也不要对Response进行任何更改,否则,将抛出异常。

    app.Use() 用于注册处理中间件链中的中间件,

  • app.Run() 通常被用作管道的最终处理程序,即是终端中间件,它直接处理请求,不会将请求传递给下一个中间件。

    app.Run() 中间件中,请求不会传递给下一个中间件,它直接处理请求。app.Run() 通常用作最终处理程序,直接处理请求。

  • app.UseStaticFiles() 中间件: 处理静态文件

    注意:

    app.useStaticFiles中间件机制:

    静态文件中间件执行的操作是针对给定的请求查看请求路径,然后将此请求路径与文件系统以及文件系统上的内容进行比较:

    1.如果静态文件是一个可以使用的文件,它将返回该文件,而不会尝试调用下一个中间件(即不会继续往下走调用其他已定义的中间件)

    2.如果它没有找到匹配的文件,那么将继续调用下一个中间件(即继续往下走调用其他已定义的中间件)

    3.浏览器上输入项目的根路径/,对应的是wwwroot文件夹;

    eg:浏览器上输入localhost:1215/images/demo.png

    则对应的是wwwroot文件夹里的images文件夹中的demo.png

    无论静态文件放在 wwwroot 的任何地方,一级目录也好,多级子目录也好,任何 JavaScript 文件或 CSS 文件或 HTML 文件,UseStaticFiles中间件都可以找到并返回它们

  • app.UseDefaultFiles()中间件: 指定默认首页打开的文件

    app.UseDefaultFiles()中间件默认查找的文件名如下:

    • index.html
    • index.htm
    • default.html
    • default.htm

    现在有个需求, 打开根路径/就直接匹配到 wwwroot 目录下的 index.html 文件?


    答:只需将定义app.UseDefaultFiles()中间件语句放在定义app.UseStaticFiles()中间件语句前面即可,如下:

    c# 复制代码
    app.UseDefaultFiles();
    app.UseStaticFiles();   

    原理: 由上面app.UseStaticFiles中间件的知识可知,当其找到一个静态文件,它就直接返回该文件,不会继续调用下一个中间件。而app.UseDefaultFiles中间件用来指定默认文件,其放在app.UseStaticFiles中间件的前面,所以相当于用app.UseDefaultFiles先说明要指定默认文件,然后接着就调用app.UseStaticFiles中间件找到对应的静态文件.

    总的来说,就相当于app.UseStaticFiles利用app.UseStaticFiles返回的静态文件作为其指定的默认文件列表。


  • app.UseFileServer()中间件

    如果同时使用 UseDefaultFilesUseStaticFiles 两个中间件,很容易搞混它们之间的顺序

    针对这种情况,ASP.NET Core 提供了 UseFileServer 中间件,这个中间件是对它们的封装。

    使用UseFileServer中间件和同时使用 UseDefaultFiles UseStaticFiles 两个中间件时的效果一模一样。

  • app.UseMvc()中间件

    使用 app.UseMvc() 方法时,你需要手动配置所有的路由规则,包括控制器、动作、参数等。

    使用 app.UseMvc() 时,你完全控制了路由的配置,可以根据项目的需要进行高度定制。

    app.UseMvc() 是在 Startup.cs 文件的 Configure 方法中调用的。它的作用是将 MVC 中间件添加到请求处理管道中,以便它能够处理传入的请求,并将其路由到相应的控制器和动作方法。

    services.AddMvc() 注册了 MVC 相关的服务,而 app.UseMvc() 将 MVC 中间件添加到请求处理管道中,使其能够处理传入的请求。一般情况下,会同时使用这两个方法来启用 MVC 功能.


    总的来说,services.AddMvc()app.UseMvc() 一起协作,为你的应用程序提供了处理 HTTP 请求的 MVC 功能。

    然而,从 ASP.NET Core 3.0 开始,Microsoft 推荐使用终结点路由(Endpoint Routing)替代传统的 app.UseMvc(),这使得配置更为灵活,并且可以兼容传统的基于路由模板的配置方式。

  • app.UseMvcWithDefaultRoute()中间件

    app.UseMvcWithDefaultRoute() 方法会添加一个默认的路由规则,通常使用控制器名称、动作名称和参数来处理请求。

    默认的路由规则是: {controller=Home}/{action=Index}/{id?}

    在大多数情况下,特别是在典型的 web 应用程序中,使用默认路由模板可以简化路由配置,提高开发效率。

    根据具体的项目需求来选择合适的配置方式。

    在大多数情况下,使用默认路由模板是一个不错的起点,如果项目需要更高度的定制,再考虑使用手动配置路由。

  • app.UseWhen:通过该方法针对不同的逻辑条件创建管道分支。

    需要注意的是:

    • 进入了管道分支后,如果管道分支不存在管道短路或终端中间件,则会再次返回到主管道。

    • 当使用PathString时,路径必须以"/"开头,且允许 只有一个'/'字符

    • 支持嵌套,即UseWhen中嵌套UseWhen等

    • 支持同时匹配多个段,如 /get/user

      c# 复制代码
      public class Startup
      {
          public void Configure(IApplicationBuilder app)
          {
              // /get 或 /get/xxx 都会进入该管道分支
              app.UseWhen(context => context.Request.Path.StartsWithSegments("/get"), app =>
              {
                  app.Use(async (context, next) =>
                  {
                      Console.WriteLine("UseWhen:Use");
      
                      await next();
                  });
              });
              
              app.Use(async (context, next) =>
              {
                  Console.WriteLine("Use");
      
                  await next();
              });
      
              app.Run(async context =>
              {
                  Console.WriteLine("Run");
      
                  await context.Response.WriteAsync("Hello World!");
              });
          }
      }
  • app.Map:通过该方法针对不同的请求路径创建管道分支。需要注意的是:

    • 一旦进入了管道分支,则不会再回到主管道。

    • 使用该方法时,会将匹配的路径从HttpRequest.Path 中删除,并将其追加到HttpRequest.PathBase中。

    • 路径必须以"/"开头,且不能 只有一个'/'字符

    • 支持嵌套,即Map中嵌套Map、MapWhen(接下来会讲)等

    • 支持同时匹配多个段,如 /post/user

      c# 复制代码
      public class Startup
      {
          public void Configure(IApplicationBuilder app)
          {
              // 访问 /get 时会进入该管道分支
              // 访问 /get/xxx 时会进入该管道分支
              app.Map("/get", app =>
              {
                  app.Use(async (context, next) =>
                  {
                      Console.WriteLine("Map get: Use");
                      Console.WriteLine($"Request Path: {context.Request.Path}"); 
                      Console.WriteLine($"Request PathBase: {context.Request.PathBase}");
              
                      await next();
                  });
              
                  app.Run(async context =>
                  {
                      Console.WriteLine("Map get: Run");
              
                      await context.Response.WriteAsync("Hello World!");
                  });
              
              });
              
              // 访问 /post/user 时会进入该管道分支
              // 访问 /post/user/xxx 时会进入该管道分支
              app.Map("/post/user", app =>
              {
                  // 访问 /post/user/student 时会进入该管道分支
                  // 访问 /post/user/student/1 时会进入该管道分支
                  app.Map("/student", app =>
                  {
                      app.Run(async context =>
                      {
                          Console.WriteLine("Map /post/user/student: Run");
                          Console.WriteLine($"Request Path: {context.Request.Path}");
                          Console.WriteLine($"Request PathBase: {context.Request.PathBase}");
              
                          await context.Response.WriteAsync("Hello World!");
                      });
                  });
                  
                  app.Use(async (context, next) =>
                  {
                      Console.WriteLine("Map post/user: Use");
                      Console.WriteLine($"Request Path: {context.Request.Path}");
                      Console.WriteLine($"Request PathBase: {context.Request.PathBase}");
                      
                      await next();
                  });
              
                  app.Run(async context =>
                  {
                      Console.WriteLine("Map post/user: Run");
              
                      await context.Response.WriteAsync("Hello World!");
                  });
              });
          }
      }
  • app.MapWhen:与Map类似,只不过MapWhen不是基于路径,而是基于逻辑条件创建管道分支。注意事项如下:

    • 一旦进入了管道分支,则不会再回到主管道。

    • 当使用PathString时,路径必须以"/"开头,且允许 只有一个'/'字符

    • HttpRequest.PathHttpRequest.PathBase不会像Map那样进行特别处理

    • 支持嵌套,即MapWhen中嵌套MapWhen、Map等

    • 支持同时匹配多个段,如 /get/user

      c# 复制代码
      public class Startup
      {
          public void Configure(IApplicationBuilder app)
          {
              // /get 或 /get/xxx 都会进入该管道分支
              app.MapWhen(context => context.Request.Path.StartsWithSegments("/get"), app =>
              {
                  app.MapWhen(context => context.Request.Path.ToString().Contains("user"), app =>
                  {
                      app.Use(async (context, next) =>
                      {
                          Console.WriteLine("MapWhen get user: Use");
      
                          await next();
                      });
                  });
              
                  app.Use(async (context, next) =>
                  {
                      Console.WriteLine("MapWhen get: Use");
              
                      await next();
                  });
              
                  app.Run(async context =>
                  {
                      Console.WriteLine("MapWhen get: Run");
              
                      await context.Response.WriteAsync("Hello World!");
                  });
              });
          }
      }
  • Run用于注册终端中间件,Use用来注册匿名中间件,UseWhenMapMapWhen用于创建管道分支。
  • UseWhen进入管道分支后,如果管道分支中不存在短路或终端中间件,则会返回到主管道。MapMapWhen进入管道分支后,无论如何,都不会再返回到主管道。
  • UseWhenMapWhen基于逻辑条件来创建管道分支,而Map基于请求路径来创建管道分支,且会对HttpRequest.PathHttpRequest.PathBase进行处理。

  • UseRouting:路由中间件,根据Url中的路径导航到对应的Endpoint。必须与UseEndpoints搭配使用。

  • UseEndpoints:执行路由所选择的Endpoint对应的委托。

  • UseAuthentication:身份认证中间件,用于对请求用户的身份进行认证。比如,早晨上班打卡时,管理员认出你是公司员工,那么才允许你进入公司。

  • UseAuthorization:用户授权中间件,用于对请求用户进行授权。比如,虽然你是公司员工,但是你是一名.NET开发工程师,那么你只允许坐在.NET开发工程师区域的工位上,而不能坐在老总的办公室里。

  • UseMvc:Mvc中间件。

  • UseHealthChecks:健康检查中间件。

  • UseMiddleware:用来添加匿名中间件的,通过该方法,可以方便的添加自定义中间件。

  • useAuthentication: 是ASP.NET Core中用于处理身份验证的中间件。它负责在请求处理过程中对用户进行身份验证,以确保用户已经通过认证才能访问受保护的资源。

    app.UseAuthentication 中间件用于启用身份验证处理,在请求到达控制器之前对用户进行认证。它会检查请求的身份信息,如Cookie或Bearer令牌,并将已认证的用户信息附加到HttpContext.User属性中,以便在后续的请求中使用。

    要使用 app.UseAuthentication 中间件,需要确保已经在应用程序中配置了身份验证服务(例如使用ASP.NET Core Identity、OAuth、JWT等)。

    Startup.cs 文件中的 Configure 方法中,将 app.UseAuthentication() 添加到中间件管道中:

    c# 复制代码
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // 其他中间件...
    
        app.UseAuthentication();
    
        // ...
    }
  • UseAuthorization: app.UseAuthorization() 中间件是 ASP.NET Core 中用于处理授权的中间件。它负责在请求处理过程中对用户进行授权,以确定用户是否有权访问特定的资源或执行特定的操作。

    app.UseAuthorization() 中间件用于启用授权处理,它会检查请求的用户是否被授权执行特定操作或访问特定资源。

    要使用 app.UseAuthorization() 中间件,需要确保已经在应用程序中配置了授权策略和规则。

    Startup.cs 文件中的 Configure 方法中,将 app.UseAuthorization() 添加到中间件管道中:

    c# 复制代码
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // 其他中间件...
    
        app.UseAuthorization();
    
        // ...
    }

编写中间件

"约定大于配置",先来个约法三章:

  1. 拥有公共(public)构造函数,且该构造函数至少包含一个类型为RequestDelegate的参数

  2. 拥有名为InvokeInvokeAsync的公共(public)方法,必须包含一个类型为HttpContext的方法参数,且该参数必须位于第一个参数的位置,另外该方法必须返回Task类型。

  3. 构造函数

    中的其他参数可以通过依赖注入(DI)填充,也可以通过

    UseMiddleware
    

    传参进行填充。

    • 通过DI填充时,只能接收 Transient 和 Singleton 的DI参数。这是由于中间件是在应用启动时构造的(而不是按请求构造),所以当出现 Scoped 参数时,构造函数内的DI参数生命周期与其他不共享,如果想要共享,则必须将Scoped DI参数添加到Invoke/InvokeAsync来进行使用。
    • 通过UseMiddleware传参时,构造函数内的DI参数和非DI参数顺序没有要求,传入UseMiddleware内的参数顺序也没有要求,但是我建议将非DI参数放到前面,DI参数放到后面。(这一块感觉微软做的好牛皮)
  4. Invoke/InvokeAsync的其他参数也能够通过依赖注入(DI)填充,可以接收 Transient、Scoped 和 Singleton 的DI参数。

一个简单的中间件如下:

c# 复制代码
public class MyMiddleware
{
    // 用于调用管道中的下一个中间件
    private readonly RequestDelegate _next;

    public MyMiddleware(
        RequestDelegate next,//RequestDelegate
        ITransientService transientService,
        ISingletonService singletonService)
    {
        _next = next;
    }

    public async Task InvokeAsync(//invoke方法,返回Task
        HttpContext context,//context
        ITransientService transientService,
        IScopedService scopedService,
        ISingletonService singletonService)
    {
        // 下一个中间件处理之前的操作
        Console.WriteLine("MyMiddleware Begin");
        
        await _next(context);
        
        // 下一个中间件处理完成后的操作
        Console.WriteLine("MyMiddleware End");
    }
}

然后,你可以通过UseMiddleware方法将其添加到管道中

c# 复制代码
public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseMiddleware<MyMiddleware>();
    }
}

基于工厂的中间件

优势:

  • 按照请求进行激活。这个就是说,上面基于约定的中间件实例是单例的,但是基于工厂的中间件,可以在依赖注入时设置中间件实例的生命周期。
  • 使中间件强类型化(因为其实现了接口IMiddleware

该方式的实现基于IMiddlewareFactoryIMiddleware

先来看一下接口定义:

c# 复制代码
public interface IMiddlewareFactory
{
    IMiddleware? Create(Type middlewareType);

    void Release(IMiddleware middleware);
}

public interface IMiddleware
{
    Task InvokeAsync(HttpContext context, RequestDelegate next);
}

当我们调用UseMiddleware时,它是如何工作的呢?事实上,UseMiddleware扩展方法会先检查中间件是否实现了IMiddleware接口。 如果实现了,则使用容器中注册的IMiddlewareFactory实例来解析该IMiddleware的实例(这下你知道为什么称为"基于工厂的中间件"了吧)。如果没实现,那么就使用基于约定的中间件逻辑来激活中间件。

注意,基于工厂的中间件,在应用的服务容器中一般注册为 Scoped 或 Transient 服务

示例实现一个基于工厂的中间件:

c# 复制代码
public class YourMiddleware : IMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        // 下一个中间件处理之前的操作
        Console.WriteLine("YourMiddleware Begin");

        await next(context);

        // 下一个中间件处理完成后的操作
        Console.WriteLine("YourMiddleware End");
    }
}

public static class AppMiddlewareApplicationBuilderExtensions
{
    public static IApplicationBuilder UseYour(this IApplicationBuilder app) => app.UseMiddleware<YourMiddleware>();
}

注意 : 微软提供了IMiddlewareFactory接口的默认实现,所以咱们不用关心或者去手动书写IMiddlewareFactory接口的实现类。

然后,在ConfigureServices中添加中间件依赖注入:

c# 复制代码
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddTransient<YourMiddleware>();
    }
}

最后,在Configure中使用中间件:

c# 复制代码
public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseYour();
    }
}

可以看到,该工厂使用过DI容器来解析出服务实例的。因此,当使用基于工厂的中间件时,是无法通过UseMiddleware向中间件的构造函数传参的。

基于约定的中间件 VS 基于工厂的中间件

  • 基于约定的中间件实例都是 Singleton;而基于工厂的中间件实例可以是 Singleton、Scoped 和 Transient(当然,不建议注册为 Singleton)
  • 基于约定的中间件实例构造函数中可以通过依赖注入传参,也可以用过UseMiddleware传参;而基于工厂的中间件只能通过依赖注入传参
  • 基于约定的中间件实例可以在Invoke/InvokeAsync中添加更多的依赖注入参数;而基于工厂的中间件只能按照IMiddleware的接口定义进行实现。

服务

Startup.cs文件中的ConfigureServices()方法可以配置应用程序所需的服务。

配置MVC服务

  • services.AddMvc()
C# 复制代码
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
}
  1. 全功能的 MVC 配置
    • AddMvc() 方法添加了完整的 MVC 功能,包括视图引擎、模型绑定、过滤器、路由等。它提供了一套完整的 MVC 架构,适用于大多数 web 应用程序。
  2. 便捷的配置
    • AddMvc() 方法提供了许多内置的默认配置,可以在大多数情况下工作良好,让你可以更快地启动一个新的 MVC 项目。
  3. 包括了许多默认组件
    • 它默认包含了许多常用的组件,比如 Razor 视图引擎、模型绑定、路由等,使得你不必手动添加这些组件。
  • services.AddMvcCore()
c# 复制代码
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvcCore();
}
  1. 基础 MVC 配置
    • AddMvcCore() 方法添加了 MVC 的基本组件,但没有包括一些高级功能,比如 Razor 视图引擎,默认模型绑定等。
  2. 更灵活的配置选项
    • AddMvcCore() 方法提供了更灵活的配置选项,允许你手动选择添加你需要的组件和功能,使得它更适合定制化和特定场景的需求。
  3. 轻量级
    • 由于 AddMvcCore() 方法只添加了基本组件,因此它相对于完整的 MVC 来说更加轻量级,适用于一些对资源要求比较严格的场景。

如何选择?

  • 如果你正在创建一个传统的 web 应用程序,并且你想使用 ASP.NET Core MVC 的全部功能,那么 services.AddMvc() 是一个不错的选择。
  • 如果你正在构建一个特定场景下的 web API 或者单页应用(SPA),并且你想要对组件进行更精确的控制,那么 services.AddMvcCore() 可能更适合你,因为它允许你手动选择添加你需要的组件。

进程内托管和进程外托管

ASP.NET Core 支持两种主要的托管模型:进程内托管 (In-Process Hosting)和进程外托管(Out-of-Process Hosting)。

进程内托管部署(In-Process Hosting):


  1. 意思:进程内托管就像把一个小队的所有成员都放在同一个房间里一样,他们可以直接互相交谈。

  2. 特点

  • 应用程序和服务器共用一个"房间",也就是同一个进程;相当于在同一个大家庭里,成员之间交流非常方便。
  • 在进程内托管模型中,ASP.NET Core 应用程序直接运行在 IIS 或 IIS Express 进程中。
  • 这种模型通常用于小型应用程序或者在单个 IIS 站点中托管的情况。
  1. 优点
  • 性能更高:因为应用程序与 IIS 进程共享同一个进程,所以它们可以直接相互通信,不需要通过网络协议,通信效率高。资源占用少,不需要额外的进程,省内存。
  • 部署简单:只需要将应用程序文件放置在指定目录即可,不需要额外的配置。
  1. 缺点
  • 隔离性差:应用程序与 IIS 进程共享同一个进程,如果应用程序崩溃,可能会影响整个 IIS 进程。即如果其中一个成员出了问题,可能会影响到其他成员。
  • 不适合多租户应用:因为所有租户共享同一个进程,难以实现真正的隔离。

进程外托管部署(Out-of-Process Hosting):

  1. 意思

    进程外托管就像把一个小队的成员们分别安排在不同的房间里,他们需要通过某种方式才能交谈。

  2. 特点

  • 应用程序和服务器分开运行在不同的进程中,就像住在不同的房间里。
  • 需要一些特殊的手段(比如通过网络)才能互相通信。
  1. 优点
  • 更好的隔离性,一个房间出了问题不会影响到其他房间。
  • 适用于大型、高流量的应用,也可以提供更灵活的部署方式。
  1. 缺点
  • 相对占用更多的系统资源,因为需要多个进程。
  • 通信效率可能会稍低,因为需要通过一些手段来进行交流。

总结:

性能和资源消耗:进程内托管通常具有更高的性能和较低的资源消耗,而进程外托管提供了更高的隔离性和灵活性。

选择:根据你的应用程序的要求和特性来选择托管模型。如果你的应用程序比较小、资源要求不高,可以选择进程内托管,而对于大型或者需要更高隔离性的应用程序,进程外托管可能更合适。

部署场景:如果你需要将应用程序部署在特定的Web服务器之后,或者通过反向代理等方式进行部署,进程外托管是一个更好的选择。

总的来说,进程内托管适用于小型应用程序或者资源要求不高的场景,而进程外托管适用于大型、高流量的应用程序,或者需要更灵活部署方式的情况。

IIS和IIS Express和Kestrel

IIS和IIS Express的差别和联系:

  1. 功能差异

    • w3wp.exe 是 正式版本的IIS (Internet Information Services) 的工作进程,用于托管和执行托管在 IIS 上的 ASP.NET 应用程序。

      每个应用程序池(Application Pool)都会在 IIS 中启动一个 w3wp.exe 进程。这样可以隔离不同应用程序池的代码,从而提高了系统的稳定性和安全性。

      应用程序池提供了一个隔离的执行环境,可以保证不同应用程序之间的互不干扰,并且可以通过资源管理和安全性配置来优化服务器的性能和安全性。这使得 IIS 能够有效地托管多个应用程序,并保持系统的稳定性。

    • IIS是一个全功能的Web服务器,适用于生产环境和大规模应用。

    • IIS Express是一个精简版,专注于本地开发和测试,不适合用于生产环境。

      IIS Express 并不直接使用 w3wp.exe。相反,它自身包含了一个简化版的 Web 服务器来处理 HTTP 请求,并提供与标准 IIS 类似的功能。

  2. 使用场景

    • IIS用于生产环境中的实际部署。
    • IIS Express用于开发阶段的本地调试。
  3. 性能

    • 由于IIS是一个全功能的Web服务器,通常比IIS Express更强大,能够处理更大的流量和并发请求。
  4. 部署要求

    • IIS需要在服务器操作系统上手动安装和配置。
    • IIS Express通常会随着Visual Studio一起安装,不需要额外配置。

总的来说,IIS和IIS Express都是用于托管Web应用程序的工具,但它们的主要区别在于使用场景和功能定位。IIS用于生产环境,而IIS Express则是用于开发和测试阶段的本地调试工具。


IIS和Kestrel的区别和联系:

  1. 功能差异

    • w3wp.exe 是 正式版本的IIS (Internet Information Services) 的工作进程,用于托管和执行托管在 IIS 上的 ASP.NET 应用程序。

      每个应用程序池(Application Pool)都会在 IIS 中启动一个 w3wp.exe 进程。这样可以隔离不同应用程序池的代码,从而提高了系统的稳定性和安全性。

      应用程序池提供了一个隔离的执行环境,可以保证不同应用程序之间的互不干扰,并且可以通过资源管理和安全性配置来优化服务器的性能和安全性。这使得 IIS 能够有效地托管多个应用程序,并保持系统的稳定性。

    • Kestrel 是 ASP.NET Core 的默认 Web 服务器,与 IIS 不同,它可以独立地运行,也可以与反向代理(如 IIS)一起使用。

    • Kestrel 不使用 w3wp.exe 进程,而是有自己的进程管理方式。

    • IIS 是一个全功能的 Web 服务器,支持多种 Web 技术,适用于大规模生产环境。

    • Kestrel 是一个轻量级的跨平台 Web 服务器,专注于处理 HTTP 请求。

    • Kestrel它是 Microsoft Visual Studio 集成的一部分,可以通过 Visual Studio 轻松启动

  2. 性能

    • IIS 在处理大量并发请求时通常比 Kestrel 更高效,特别是在生产环境中。
    • Kestrel 在轻负载情况下表现良好,但在高负载情况下可能需要配合反向代理服务器使用。
  3. 部署要求

    • IIS 需要在 Windows 服务器操作系统上手动安装和配置。
    • Kestrel 可以作为一个独立的服务器,也可以与反向代理服务器结合使用。
  4. 适用场景

    • IIS 适用于大规模、高并发的生产环境,支持多种 Web 技术。
    • Kestrel 适用于开发和测试阶段的本地调试,以及一些中小型应用程序的托管需求。

总的来说,选择使用 IIS 还是 Kestrel 取决于你的项目需求和部署场景。通常在生产环境中,会使用 IIS 作为反向代理服务器来托管 ASP.NET Core 应用,以保证高性能和安全性。在开发和测试阶段,可以直接使用 Kestrel 进行本地调试。


IIS Express和Kestrel的区别和联系:

  1. 功能差异

    • Kestrel 是 ASP.NET Core 的默认 Web 服务器,与 IIS 不同,它可以独立地运行,也可以与反向代理(如 IIS)一起使用。

    • Kestrel 不使用 w3wp.exe 进程,而是有自己的进程管理方式。

    • IIS Express 是一款用于本地调试的轻量级 Web 服务器,适用于开发和测试阶段。

    • Kestrel 是一个跨平台的、轻量级的 Web 服务器,可以用于开发、测试和生产环境。

  2. 性能

    • IIS Express 主要用于开发环境,不强调高性能,而是注重便于开发者进行调试。
    • Kestrel 性能较好,特别适合用于一些中小型应用程序和特定场景的高性能需求。
  3. 操作系统支持

    • IIS Express 只能在 Windows 上运行。
    • Kestrel 可以在 Windows、Linux 和 macOS 上运行,具有更强的跨平台能力。
  4. 使用场景

    • IIS Express 适用于开发阶段的本地调试,为开发人员提供一个快速、方便的调试环境。
    • Kestrel 可以用于各个阶段,包括开发、测试和生产环境,也可以与反向代理服务器结合使用,提供高性能的托管解决方案。

总的来说,选择使用 IIS Express 还是 Kestrel 取决于你的项目需求和开发环境。在开发阶段,通常会使用 IIS Express 进行本地调试,而在生产环境中,通常会使用 Kestrel 作为主要的 Web 服务器。

Swagger相关

swagger基础参考的学习文章链接: 跟我一起学.NetCore之Swagger让前后端不再烦恼及界面自定义 (qq.com)

Jwt(json web token)相关

参考的学习文章链接:跟我一起学.NetCore之WebApi接口裸奔有风险(Jwt) (qq.com)

环境变量

在.NET Core中,环境变量是一种配置机制,用于在应用程序中访问外部配置信息或者控制应用程序的行为。

ASPNETCORE_ENVIRONMENT

  • 用于指定应用程序的当前环境。

    常见的值包括 Development(开发环境)、Staging(预发布环境)、Production(生产环境)等。

    根据不同的环境,应用程序可以采取不同的配置和行为。

Entity Framework和Entity Framework Core

Entity Framework(EF)和Entity Framework Core(EF Core)都是由微软开发的对象关系映射(ORM)框架,用于将数据库中的数据映射到.NET应用程序中的对象模型。

Entity Framework / Entity Framework Core(实体框架核心,简称EFCore)和LINQ(语言集成查询)的技术。使用这两者来实现数据访问。

  • 数据持久化:EF Core 提供了将对象持久化到数据库的能力,包括插入、更新、删除等操作。
  • LINQ 查询:可以使用 LINQ (Language Integrated Query) 进行灵活的数据查询。
  • 数据库迁移:可以自动追踪和应用模型变化,更新数据库结构。

EF

Entity Framework(EF)是一款由Microsoft支持的开源对象关系映射(ORM)框架,它允许.NET开发人员在使用数据库时更加专注于业务逻辑,减少输入大量SQL代码的工作量。

以往操作数据的话,是用SQL语句对数据库的表进行操作

这是比较抽象的,就感觉你操作的是存储在数据库中的数据。

EF是以Entity Class为核心的,也叫实体类 ,你操作

的是该类的对象了,从某种程度上来讲你操作的变成一个有数据体了。

以数据体为核心的这一套数据访问框架,称为实体框架。至于后来的EFCore,纯粹是为了与.NET Core配对。

在数据和程序之间出现了一个模型(model).

Model是由实体和上下文组成的。它不是一个类,它是一套东西

而其中与数据库交互的就是上下文。


EF支持的模型开发方式

  • 从已有的数据库中生成model
  • 手工来编写一个匹配数据库的model
  • 一旦创建了一个模型,使用EF Migrations来根据模型创建一个数据库。Migrations允许在模型更改时同时使数据库发生变化。

显然三种方式有各自的特点,对于初学者来说,应该先尝试第二种。

数据库已经建好,通过手写一个model,更能体会其中的一些机理。


DbContext及其派生类

DbContext 是 EF 和 EF Core 中非常重要的一个类,它代表了一个数据库会话,负责管理对数据库的连接以及执行各种数据库操作。

DbContext类作用如下:

  1. 管理数据库连接DbContext 负责管理与数据库的连接。它会在需要时打开连接,在完成操作后关闭连接。
  2. 跟踪实体对象DbContext 能够追踪实体对象的状态(Added、Modified、Deleted 等)。这使得在保存更改时,EF 可以知道应该如何更新数据库。
  3. 查询和操作数据库 :通过 DbContext 可以执行各种查询和操作,例如增加、删除、更新数据。
  4. 定义模型和关系 :在 DbContext 中,你可以定义实体类之间的关系,如一对一、一对多、多对多等。

有了上下文,并且关联了实体之后,你就可以通过上下文对象来操作数据表了。

上下文的生命周期始于实例创建时,终于实例被处理(Dispose)或GC回收。如果你想在块的末尾释放上下文控制的所有资源,请使用using。当你使用using时,编译器会自动创建一个try/finally块,并在finally块中调用dispose。

使用示例:

首先,你需要定义一个继承自 DbContext 的类,以及你要在数据库中映射的实体类。

假设我们有一个简单的博客应用,有两个实体类:PostAuthor

c# 复制代码
//在这个例子中,我们定义了一个 BlogDbContext 类,继承自 DbContext。
//它包含了两个 DbSet 属性,分别对应数据库中 Posts 和 Authors 表。
public class BlogDbContext : DbContext
{
    public DbSet<Post> Posts { get; set; }
    public DbSet<Author> Authors { get; set; }
}

接下来,我们可以使用 BlogDbContext 来进行数据库操作:

c# 复制代码
//在这个示例中,我们创建了一个新的 BlogDbContext 实例。然后,我们创建了一个新的文章和一个新的作者,并将文章与作者关联。最后,通过 SaveChanges 方法将更改保存到数据库。
//这只是一个简单的示例,DbContext 还提供了许多其他功能,如复杂查询、事务管理等。
using (var context = new BlogDbContext())
{
    // 添加一篇新文章
    var newPost = new Post { Title = "Hello World", Content = "This is the first post." };
    context.Posts.Add(newPost);

    // 添加一个作者
    var author = new Author { Name = "John Doe" };
    context.Authors.Add(author);

    // 关联文章和作者
    newPost.Author = author;

    // 保存更改到数据库
    context.SaveChanges();
}

实体类型

上下文(Context的派生类)中包含一种实体类型的DbSet表示该实体存在于EF Core的模型中;

c# 复制代码
internal class MyContext : DbContext
{
	// Blog是实体
    public DbSet<Blog> Blogs { get; set; }
	...
}

通常称这样的类型为实体(Entity)(所以实体就是类,是DbSet xxx中的T,而不是DbSet xxx)。

EF Core能从数据库中读数据至实体的实例或将实体实例写入数据库,并且如果你用的是关系数据库,EF Core能通过migrations为你的实体建表。


在model中添加实体类型

  • 按照约定(指EF Core技术设计使用上的规定),在定义的上下文类中的DbSet属性中暴露的类型T会作为实体被包含在model中。
  • 在OnModelCreating方法中指定的实体类型也会被包含在内
  • 并且由递归探索到的实体类型的导航属性所找到的任何类型也会被包含在内。

在下面代码示例中,包含了上面描述的意思;

c# 复制代码
//Blog实体是被包含在model内的,它在上下文中由DbSet属性暴露;
//Post实体是被包含在model内,它通过Blog.Posts导航属性可以发现;
//AudiEntry是被包含在model内,它在OnModelCreating方法中被指定。
internal class MyContext : DbContext
{
    //DbSet
    public DbSet<Blog> Blogs { get; set; }
	
    //override方法
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        
        modelBuilder.Entity<AuditEntry>();
    }
}

//entity
public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    public List<Post> Posts { get; set; }
}

//entity
public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public Blog Blog { get; set; }
}

//entity
public class AuditEntry
{
    public int AuditEntryId { get; set; }
    public string Username { get; set; }
    public string Action { get; set; }
}

在model中排除实体类型

如果你不想某个类型被包含到模型中,你可以排除它:

c# 复制代码
// 两种方式
// 1. 数据标注方式
[NotMapped]
public class BlogMetadata
{
    public DateTime LoadedFromDatabase { get; set; }
}
// 2. Fluent API方式
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
	modelBuilder.Ignore<BlogMetadata>();
}

配置表名

按照约定,每个实体类型对应的表名将被设置为暴露实体的DbSet属性变量有着相同名称的数据表。若给定的实体不存在DbSet,则使用类名。

同时也可以手动配置实体对应的表名:

c# 复制代码
// 两种方式
// 1. 数据标注
[Table("blogs")]
public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
}

// 2. Fluent API
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .ToTable("blogs");
}

Table schema

感觉中,数据库的关系是这样的, 数据服务器下面有数据库,数据库下面有数据表,表中有字段,仅此而已。

实际上当打开一个数据服务器的连接后,展现的树状图如下:

显然,它们都是在一个叫SCHEMAS的根节点下的。所以这个图标是数据库形状的子节点居然是schema?!

网上说法很多,但是就目前来看,在MySQL中把schema理解成database没有大问题。

当使用关系数据库时,表按照约定会创建到你数据库中默认的schema中。

也可以按照下面方式为每个表指定其对应的schema:

c# 复制代码
// 1. 数据标注
[Table("blogs", Schema = "blogging")]
public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
}

// 2. Fluent API
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .ToTable("blogs", schema: "blogging");
}

除了为每个表指定schema,你也可以使用fluent API在模型上定义默认的schema:

c# 复制代码
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.HasDefaultSchema("blogging");
}

注意:设置默认的schema还会影响其他的数据库对象,例如序列(sequence)。


表备注

可以在数据表上设置任意文本注释:

c# 复制代码
// 1. 数据标注 EF Core 5.0引入的
[Comment("Blogs managed on the website")]
public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
}

// 2. fluent API
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasComment("Blogs managed on the website");
}

若使用的是关系数据库,实体属性将映射到数据表的列。

如何包含或排除属性

按照约定,所有带有getter和setter的公开属性都将被包含在模型中。

可以通过以下方式排除指定的属性:

c# 复制代码
// 1. 数据标注
public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    [NotMapped]
    public DateTime LoadedFromDatabase { get; set; }
}

// 2. fluent API
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Ignore(b => b.LoadedFromDatabase);
}

自定义实体属性对应的列名

按照约定,当使用关系数据库时,实体的属性会映射到和属性有着相同名称的表的列上。当然也可以自定义列名:

c# 复制代码
// 1. 数据标注
public class Blog
{
    [Column("blog_id")]
    public int BlogId { get; set; }

    public string Url { get; set; }
}

// 2. fluent API
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.BlogId)
        .HasColumnName("blog_id");
}

自定义实体属性对应的列数据类型

c# 复制代码
// 1. 数据标注
public class Blog
{
    public int BlogId { get; set; }

    [Column(TypeName = "varchar(200)")]
    public string Url { get; set; }

    [Column(TypeName = "decimal(5, 2)")]
    public decimal Rating { get; set; }
}

// 2. fluent API
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>(
        eb =>
        {
            eb.Property(b => b.Url).HasColumnType("varchar(200)");
            eb.Property(b => b.Rating).HasColumnType("decimal(5, 2)");
        });
}

配置必需的属性和可选的属性(按照约定法)

按照约定,一个属性若它的.NET类型可以包含null则被配置为可选的,反之则是必需/必要属性。

例如,所有带有.NET值类型的属性(int,decimal,bool等等)是必需属性,而所有带有可为null的.NET值类型的属性(int?,decimal?,bool?等等)配置为可选属性。

c# 复制代码
// 1. 禁用NRT
public class CustomerWithoutNullableReferenceTypes
{
    public int Id { get; set; }

    [Required] // Data annotations needed to configure as required
    public string FirstName { get; set; }

    [Required]
    public string LastName { get; set; } // Data annotations needed to configure as required

    public string MiddleName { get; set; } // Optional by convention
}

// 2. 启用NRT
public class Customer
{
    public int Id { get; set; }
    public string FirstName { get; set; } // Required by convention
    public string LastName { get; set; } // Required by convention
    public string? MiddleName { get; set; } // Optional by convention

    // Note the following use of constructor binding, which avoids compiled warnings
    // for uninitialized non-nullable properties.
    public Customer(string firstName, string lastName, string? middleName = null)
    {
        FirstName = firstName;
        LastName = lastName;
        MiddleName = middleName;
    }
}

配置必须的属性和可选的属性(显式配置法)

c# 复制代码
// 1. 数据标注
public class Blog
{
    public int BlogId { get; set; }

    [Required]
    public string Url { get; set; }
}

// 2. fluent API
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Url)
        .IsRequired();
}

配置主键

按照约定,一个属性命名为Id或 Id就会被配置成实体的主键

c# 复制代码
internal class Car
{
	// Id被配置为主键
    public string Id { get; set; }

    public string Make { get; set; }
    public string Model { get; set; }
}

internal class Truck
{
	// TruckId被配置为主键
    public string TruckId { get; set; }

    public string Make { get; set; }
    public string Model { get; set; }
}

也可以按以下方式将单个属性配置为主键:

c# 复制代码
// 1. 数据标注
internal class Car
{
    [Key]
    public string LicensePlate { get; set; }

    public string Make { get; set; }
    public string Model { get; set; }
}

// 2. fluent API
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Car>()			// 模型中取出Car实体
        .HasKey(c => c.LicensePlate);	// 使LicensePlate成为主键
}

也可以将多个属性配置为实体的主键------这被称为组合键(有点超码的概念)。组合键只能用fluent API来配置;

c# 复制代码
// 1. fluent API配置组合键
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Car>()
        .HasKey(c => new { c.State, c.LicensePlate });
}

总结:

实体是个类,它是用来与数据库通信的,它本身不能起通信作用,需要以DbSet来在上下文中暴露才行!


数据库迁移

当使用 Entity Framework (EF) 或 EF Core 时,数据库迁移(migration)是一种用于管理数据库架构变化的工具。它允许你在应用程序的开发过程中轻松地对数据库模型进行更改,而无需手动更新数据库结构。

  1. 保持数据库模式与应用程序模型同步:随着应用程序的不断演进,你可能需要修改实体类(模型),添加新实体、更改字段或关系等。数据库迁移帮助你将这些变化应用到数据库结构,以便它与模型保持一致。
  2. 版本控制数据库结构:每个数据库迁移都是一个版本,它记录了数据库模式的变化。这使你能够追踪数据库结构的历史和演化,还可以轻松地回滚到先前的版本。
  3. 简化数据库部署:使用迁移,你可以将数据库的模式变更作为代码提交到版本控制系统,然后在部署新版本应用程序时,通过运行迁移命令自动更新数据库。

使用示例:

首先,确保你的.NET Core项目中已经安装了Entity Framework(EF)或EF Core的相关包,如果没有,可以使用以下命令安装:

c# 复制代码
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Design

接下来,你需要创建数据库上下文(DbContext)类以及实体类。

c# 复制代码
//假设有一个简单的博客应用,包括文章(Post)和作者(Author)两实体。
//我们创建了一个 BlogDbContext 类,继承自 DbContext,并定义了两个 DbSet 属性,分别对应数据库中的文章和作者。
public class BlogDbContext : DbContext
{
    public DbSet<Post> Posts { get; set; }
    public DbSet<Author> Authors { get; set; }
}

接下来,为了启用数据库迁移,你需要使用以下命令创建一个初始迁移:

c# 复制代码
dotnet ef migrations add InitialMigration

这将生成一个迁移文件,其中包含了将模型映射到数据库结构所需代码。

然后,运行迁移以将这些变更应用到数据库:

c# 复制代码
dotnet ef database update

如果你之后对模型进行更改,可以再次创建并应用迁移,以保持数据库结构的同步:

c# 复制代码
dotnet ef migrations add NewChange
dotnet ef database update

命令会自动生成迁移文件,描述了模型的新状态,并将其应用到数据库。

总之数据库迁移是.NET Core中的EF或EF Core框架的一个强大功能,用于管理数据库模式的变化,使开发和维护数据库变得更加方便和可控。

创建并配置模型

EF/EFCore使用一系列约定来构建基于实体类特征的模型。

同时也可以指定额外的配置来补充或重写约定的内容。

用API配置模型

可以在派生的上下文Context类重写OnModelCreating方法,并使用ModelBuilder API来配置你的模型。

这是最有效的配置方法,它允许你在不修改实体类的情况下指定配置。

注意: fluent API配置有着最高的优先级,并会覆盖约定和数据标注。

c# 复制代码
using Microsoft.EntityFrameworkCore;

namespace EFModeling.EntityProperties.FluentAPI.Required;

//实体类
public class Blog
{
   public int BlogId { get; set; }
   public string Url { get; set; }
}
//派生的上下文Context类
internal class MyContext : DbContext
{
   //DbSet
   public DbSet<Blog> Blogs { get; set; }

   //重写: 派生的上下文类中的OnModelCreating方法
   #region Required
   protected override void OnModelCreating(ModelBuilder modelBuilder)
   {
       modelBuilder.Entity<Blog>()
           .Property(b => b.Url)
           .IsRequired();
   }
   #endregion
}

同时, 为了减小OnModelCreating方法的大小(让里面代码少一点),实体类型的所有配置项可以提取到一个实现了IEntityTypeConfiguration<TEntity>接口的单独的类中。

c# 复制代码
//如下所示:
public class BlogEntityTypeConfiguration : IEntityTypeConfiguration<Blog>
{
    public void Configure(EntityTypeBuilder<Blog> builder)
    {
        builder
            .Property(b => b.Url)
            .IsRequired();
    }
}

然后在OnModelCreating方法中调用上面的Configure方法即可。


用数据标注配置模型

还可以应用特性(也称为数据标注,数据注释,Data Annotations)到

实体类和属性上。

注意: 数据标注会覆盖约定,但会被Fluent API配置项给覆盖。

c# 复制代码
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

namespace EFModeling.EntityProperties.DataAnnotations.Annotations;

//应用数据标注 或者说 特性到实体类上
[Table("Blogs")]
public class Blog
{
    public int BlogId { get; set; }

    ///应用数据标注 或者说 特性到实体类的属性上
    [Required]
    public string Url { get; set; }
}

//派生的上下文Context类
internal class MyContext : DbContext
{
    //DbSet
    public DbSet<Blog> Blogs { get; set; }
}

EF Core

数据库提供程序(DataBase Provider,数据库提供者其实就是数据库和你的应用程序之间的一个中间件,供你访问数据库用的)。

数据库提供程序将其转换为特定数据库的查询语言(例如,关系数据库的SQL)。

对于EF Core使用的数据库提供程序的选择,直接在管理nuget中搜索:

entityframeworkcore.xxx即可。

eg: 在nuget中搜索entityframework.mysql


区别:

Entity Framework Core 是 Entity Framework 的轻量级,可扩展和跨平台的版本.

  1. 兼容性:EF是建立在传统的.NET框架上,而EF Core则是建立在跨平台的.NET Core上。

    所以,EF仅适用于Windows平台,而EF Core可以在多种平台(包括Windows、Linux和macOS)上使用。

  2. 功能:EF拥有更丰富的功能集,例如支持更多类型的数据库引擎、更多的查询操作和性能优化技术。而EF Core在一开始的版本中,功能相对较少,但随着版本的更新,逐渐接近EF的功能水平。

  3. 性能 :由于EF Core是为了跨平台和性能优化而重新设计的,它通常比EF的性能更好。然而,具体的性能表现也取决于使用的数据库引擎、查询操作和应用程序的特定需求。

联系:

  1. 核心概念:EF和EF Core共享类似的核心概念,如DbContext、实体类型(Entity Type)、集合导航属性(Collection Navigation Property)等。因此,从EF迁移到EF Core时,大部分代码可以保持不变。
  2. 数据访问:EF和EF Core都提供了CRUD(创建、读取、更新、删除)操作的API,可以使用LINQ查询语句从数据库中检索和操作数据。
  3. 数据库迁移:EF和EF Core都支持数据库迁移工具,可以根据模型的变化自动更新数据库架构。

总的来说,EF适用于传统的.NET框架和Windows平台,功能丰富而稳定;而EF Core适用于跨平台的.NET Core,性能较好,但功能相对较少。在选择使用时,需要考虑到应用程序的平台需求和性能要求。


EF Core连接Mysql

1: 安装 NuGet 包

首先,你需要安装 Entity Framework Core 和 MySQL 数据库提供程序的 NuGet 包.

c# 复制代码
//Pomelo.EntityFrameworkCore.MySql 是用于连接 MySQL 数据库的 Entity Framework Core 提供程序。
Pomelo.EntityFrameworkCore.MySql

2: 配置数据库连接字符串

appsettings.json 文件中添加数据库连接字符串。例如:

json 复制代码
{
  "ConnectionStrings": {
    "MysqlConnection": "server=localhost;port=3306;user id=root;password=Szcatic@8616;"
      
  }
}

ServerDatabaseUserPassword 替换为你的 MySQL 数据库的实际信息。

3: 配置 DbContext

在你的应用程序中创建一个继承自 DbContext 的类,该类将表示你的数据库上下文。

c# 复制代码
using Microsoft.EntityFrameworkCore;

public class MyDbContext : DbContext
{
    public MyDbContext(DbContextOptions<MyDbContext> options)
        : base(options)
        {
        }

    // DbSet 属性用于表示数据库中的表
    //泛型T则表示实体 或者 表中的一条记录
    public DbSet<T> YourEntities { get; set; }
}

确保 YourEntity 类是你要在数据库中映射的实体类。

4: 注册 DbContext

Startup.cs 文件中的 ConfigureServices 方法中注册你的 DbContext

c# 复制代码
public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.AddDbContext<MyDbContext>(options =>
        options.UseMySql(Configuration.GetConnectionString("MysqlConnection")));
    // ...
}

5: 创建数据库迁移

在终端中执行以下命令来创建数据库迁移:

c# 复制代码
dotnet ef migrations add InitialMigration

6: 应用数据库迁移

运行以下命令将迁移应用到数据库:

c# 复制代码
dotnet ef database update

7: 使用DbContext

现在可以在应用程序中使用 MyDbContext 来访问数据库了。例如:

c# 复制代码
public class YourService
{
    //继承的上下文Context类
    private readonly MyDbContext _context;
	
    //构造函数
    public YourService(MyDbContext context)
    {
        _context = context;
    }
	
    //利用context类对象进行数据访问和操作
    public List<YourEntity> GetEntities()
    {
        return _context.YourEntities.ToList();
    }
}

以上就是在 ASP.NET Core 中使用 Entity Framework Core 连接 MySQL 数据库的基本步骤。如果你的实体模型有任何变化,你可以通过运行 dotnet ef migrations adddotnet ef database update 命令来更新数据库。


仓储模式

仓储模式(Repository Pattern)是一种常用的软件设计模式,用于将数据访问逻辑与业务逻辑分离开来,提高代码的可维护性和可测试性。

仓储模式的基本思想

仓储模式通过引入一个仓储(Repository)层,将数据访问逻辑封装在该层中。这使得业务逻辑可以专注于业务处理,而不必关心数据如何从数据库或其他持久化机制中获取。

仓储模式的元素

在.NET Core 中,一个简单的仓储模式通常包括以下几个元素:

  • 实体类(Entity):表示数据模型,通常对应数据库中的表格或其他持久化机制。
  • 仓储接口(Repository Interface):定义了对实体类进行增删改查的方法。
  • 具体仓储类(Concrete Repository):实现了仓储接口,负责实际的数据访问工作。

仓储模式的分层

.NetCore简单仓储模型,共分为三层:

  1. 仓储层:Repository(类),IRepository(接口,接口添加泛型及约束)

  2. 业务层:Service(类,继承IService, 其构造函数引入IRepository)

    ​ IService(接口,接口添加泛型及约束)

  3. 控制器层:Controller(控制器层,其构造函数引入IService)

如下图所示:

Repository继承IRepository,而IbaseRepository接口里面则写常用的增删改查方法。

注意事项:

一定要在Api的Program.cs配置文件里面添加各个层的注入,如下图:

示例:

假设我们有一个简单的 Product 类:

c# 复制代码
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

仓储接口

c# 复制代码
public interface IProductRepository
{
    Product GetById(int id);
    void Add(Product product);
    void Update(Product product);
    void Delete(int id);
}

具体仓储类(实现仓储接口的类):

c# 复制代码
public class ProductRepository : IProductRepository
{
    //【上下文context类】加readonly是编码习惯,防止被修改
    private readonly DbContext _context;
	//构造函数【传入上下文Context类】
    public ProductRepository(DbContext context)
    {
        _context = context;
    }

    public Product GetById(int id)
    {
        return _context.Products.Find(id);
    }

    public void Add(Product product)
    {
        _context.Products.Add(product);
        _context.SaveChanges();
    }

    public void Update(Product product)
    {
        _context.Products.Update(product);
        _context.SaveChanges();
    }

    public void Delete(int id)
    {
        var product = _context.Products.Find(id);
        if (product != null)
        {
            _context.Products.Remove(product);
            _context.SaveChanges();
        }
    }
}

仓储模式的作用

  • 解耦数据访问和业务逻辑:通过使用仓储模式,业务逻辑代码与具体的数据访问方式解耦,提高了代码的可维护性和可测试性。
  • 方便切换数据访问方式:如果以后需要更换数据访问方式(例如从关系型数据库切换到 NoSQL 数据库),只需要修改具体仓储类的实现,而不需要修改业务逻辑。
  • 减少重复代码:将数据访问逻辑封装在仓储中,可以在多个业务逻辑中共享同一份数据访问代码,避免重复实现相似的数据操作。

Identity框架

ASP.NET Core Identity 是一个用于处理用户认证和授权的框架,它提供了一套通用的解决方案,可以轻松地集成到 ASP.NET Core 应用程序中。它包括了用户管理、角色管理、登录、注册等功能。


Identity框架两个重要组件

Identity 框架提供了很多组件,但最重要的莫过于两个:

SignInManager 和 Identity 中间件。

  1. SignInManager ( 登录管理器,验证用户 )

顾名思义,一旦密码验证通过,SignInManager 就允许用户登录

当然了,SignInManager 还可以用于登出一个用户

如果使用表单身份验证,那么登录和注销通过管理 cookie 来实现。

当我们告诉 SignInManager 允许某个用户登录时, SignInManager 会向用户的浏览器返回一个 cookie,浏览器接下来的每个后续请求中都会发送该 cookie,直到 cookie 过期,我们可以使用该 cookie 来识别用户

  1. Identity 中间件(识别用户)

Identity 中间件读取 SignInManager发送的 cookie 并识别用户,一般情况下,Identity 中间件都是在排在所有其它中间件之后才运行的

要使用该中间件,需要将它配置到我们的应用程序管道中,才能处理 SignInManager 设置的 Cookie


Identity验证特性

验证用户身份的第一步就是要标识 哪些控制器/哪些操作需要验证用户,而这一步操作可以使用 Identity 框架提供的 [Authorize] 特性来解决。

可以把 [Authorize] 特性放在控制器上,那么该控制器的所有方法都必须需要授权才能访问,也可以放在控制器内的动作方法上,那么只有该控制器的该动作方法需要授权访问。

默认情况下,如果不传递其它参数给 Authorize,授权检查的唯一项目就是确认用户是否已经登录;当然也可传递参数来指定自定义授权策略。

c# 复制代码
[Authorize] 
public class HomeController : Controller { 
   //....  
    
    [Authorize] 
    public string index(){}
} 

除了 [Authorize] 外,还有一个特性 [AllowAnonymous]

当想要在使用了 [Authorize] 特性的某个控制器的动作方法上取消保护并允许匿名用户访问时,可以使用该特性。

c# 复制代码
[Authorize]
public ViewResult Index() {
    //....
    
    [AllowAnonymous]  
    public string index(){}
}

注意:
AuthorizeAllowAnonymous 特性都在命名空间 Microsoft.AspNetCore.Authorization中,所以在使用之前先要引入该命名空间。

ASP.NET Core请求过程

如图所示,整个请求流程更加细化,特别是对ASP.NET Core Application进行了放大,其内部包含很重要的两个组件,一个是Kestrel(作为托管的内部服务器,跨平台的),一个是管道,管道包含多个中间件,可以无限扩展。

ASP.NET Core Application

ASP.NET Core Applicaton进一步放大,可以了解到,Kestrel其实在这里并没有做真正的核心处理,只是做一层封装为HttpContext,并往下传。真正处理请求的是管道,管道其实就是RequestDelegate,处理完成后封装成HttpContext进行回传,当然,HttpContext内含HttpRequest和HttpResponse。

Asp NetCore MVC

Introduce

MVC 体系结构模式将应用分成 3 组主要组件:模型、视图和控制器。

此模式有助于实现关注点分离:

  • UI 逻辑位于视图中,在 MVC 应用中,视图仅显示信息。
  • 输入逻辑位于控制器中,控制器处理用户输入和交互并对其进行响应。
  • 业务逻辑位于模型中。

控制器

ASP.NET MVC 应用程序中,所有传入的请求均由控制器处理,并将这些请求映射到控制器相应的方法上.


按照约定 ,控制器的名称应该以 "Controller" 结尾,例如 HomeControllerProductController 等。

MVC的简单控制器,如以下代码:

c# 复制代码
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;

namespace MvcMovie.Controllers;

public class HelloWorldController : Controller
{
    // 指出这是一个 HTTP GET 方法,它通过向基 URL 追加    	/HelloWorld/ 进行调用
    // GET: /HelloWorld/
    public string Index()
    {
        return "This is my default action...";
    }
    // 指定一个 HTTP GET 方法,它通过向 URL 追加 /HelloWorld/Welcome/ 进行调用。
    // GET: /HelloWorld/Welcome/ 
    public string Welcome()
    {
        return "This is the Welcome action method...";
    }
}

控制器中的每个 public 方法均可被 HTTP 请求而调用。


对于控制器,但更常见的做法是从 Microsoft.AspNetCore.Mvc 命名空间中提供的控制器基类中来派生控制器。

Microsoft.AspNetCore.Mvc 命名空间下的基类 Controller 让我们能够访问很多关于 HTTP 请求的上下文信息,以及提供了一些方法帮助我们构建返回给回客户端的结果

返回的响应的结果通常被封装到实现 IActionResult 接口的对象中,有大量的不同类型的结果实现了该接口.

响应结果可以返回 JSON,也可以返回 XML,或者 HTML 视图等。

动作基本上可以返回任意不同类型的动作结果。

它们都有一个共同的基类:ActionResult

下表列出了不同种类的动作结果及其行为:

动作名称( 类 ) 行为
ContentResult 返回一串字符串
ObjectResult 创建对象,返回对象。当返回一个 ObjectResult 时,MVC 框架将访问这个对象,并将这个对象做一些转换,然后作为 HTTP 响应返回给客户端。转换 ObjectResult 对象时,它可能被序列化为 XMLJSON 或其它格式, 至于什么格式,由应用程序启动时向 MVC 提供的配置信息决定。如果没有显式的配置任何东西,那么将使用 JSON 作为默认格式。 即ObjectResult类型是准许内容协商的。
FileContentResult 返回文件的内容
FilePathResult 返回路径文件的内容
FileStreamResult 返回流文件的内容
EmptyResult 返回空
JavaScriptResult 返回一段 JavaScript 代码
JsonResult 返回 JSON 格式的数据
RedirectToResult 重定向到某个 URL
HttpUnauthorizedResult 返回 403 未授权状态码
RedirectToRouteResult 重定向到不同的控制器或方法
ViewResult 从视图引擎中返回一个响应
PartialViewResult 从视图引擎中返回一个响应

demo

c# 复制代码
using System;
using Microsoft.AspNetCore.Mvc;

namespace HelloWorld.Controllers
{
    public class HomeController: Controller
    {
        public ContentResult Index() { 
            return Content("你好,世界! 这条消息来自使用了 Action Result 的 Home 控制器"); 
      } 
    }
}

/*
*我们可以看到,Index() 方法返回了一个 ContentResult 类型的结果。ContentResult 是实现了 ActionResult 接口的不同结果类型之一

在 Index() 方法中,我们将一个字符串传递给 Content()。 Content() 方法会产生一个 ContentResult,也就是说,Index() 方法会返回 ContentResult
*/

模型绑定

模型绑定即:自动将 HTTP 请求中的数据映射到操作方法参数。

这使得我们能够方便地从表单、URL参数或JSON等数据源中提取信息,并将其转换为C#对象。

模型绑定的基本方式

  1. 从路由获取数据

    如果你在路由中定义了参数,比如:

    c# 复制代码
    [Route("api/users/{id}")]
    public IActionResult GetUser(int id)
    {
        // ...
    }

    那么id会自动从路由中提取出来。

  2. 从查询字符串获取数据

    如果数据是作为查询字符串参数传递的,例如:

    GET /api/users?id=10
    

    那么可以直接在方法中添加一个参数:

    c# 复制代码
    public IActionResult GetUser(int id)
    {
        // ...
    }

    模型绑定会自动将id从查询字符串中提取出来。

  3. 从请求体获取数据

    当你发送一个POST请求时,数据通常会包含在请求体中,比如一个表单或一个JSON对象。你可以在方法参数中使用一个类来接收整个对象,模型绑定会自动将请求体中的数据映射到这个类的属性上。

    c# 复制代码
    [HttpPost]
    public IActionResult CreateUser([FromBody] User user)
    {
        // ...
    }

    其中User是一个表示用户的类。

  4. 从路由值获取数据

    有时,数据可能是作为路由的一部分传递的,但是它并不是直接作为参数提供的。在这种情况下,你可以使用[FromRoute]属性来明确指定从路由中获取数据。

    c# 复制代码
    [HttpGet("api/users/{id}")]
    public IActionResult GetUser([FromRoute] int id)
    {
        // ...
    }
  5. 从表单获取数据

    当你发送一个带有表单数据的POST请求时,你可以在方法参数中使用一个类来接收整个对象。

    c# 复制代码
    [HttpPost]
    public IActionResult CreateUser([FromForm] User user)
    {
        // ...
    }
  6. 从头部获取数据

    你可以使用[FromHeader]属性来从HTTP头部中获取数据:

    c# 复制代码
    [HttpGet]
    public IActionResult GetUser([FromHeader] string userAgent)
    {
        // ...
    }
  7. 默认值

    如果没有匹配到任何数据,你可以提供一个默认值:

    c# 复制代码
    public IActionResult GetUser(int id = 1)
    {
        // ...
    }

    在没有传递id的情况下,它会默认为1。

  8. 复合类型

    你也可以在一个方法中同时使用多个以上的绑定方式,比如同时从路由、查询字符串和请求体中获取数据:

    c# 复制代码
    [HttpPost("api/users/{id}")]
    public IActionResult UpdateUser(int id, [FromQuery] string role, [FromBody] User user)
    {
        // ...
    }

以上是.NET Core中模型绑定的基本顺序和原理.


模型绑定顺序


模型验证

在ASP.NET Core中,模型绑定后的数据可能需要进行验证以确保其符合预期的格式和规范。模型验证是一个重要的环节,它可以帮助你确保接收到的数据是有效的。以下是ASP.NET Core中的模型验证:

  1. 数据注解(Data Annotations):

    数据注解是一种常用的模型验证技术,它通过在模型的属性上应用特性来定义验证规则。以下是一些常用的数据注解特性:

    • [Required]: 指示该属性是必需的。
    • [Range]: 指定属性的范围。
    • [StringLength]: 指定字符串属性的最小和最大长度。
    • [RegularExpression]: 指定属性的值必须符合指定的正则表达式。

    例如:

    c# 复制代码
    public class UserModel
    {
        [Required]
        public string UserName { get; set; }
        
        [Range(1, 100)]
        public int Age { get; set; }
        
        [EmailAddress]
        public string Email { get; set; }
    }
  2. 模型状态验证:

    控制器中的操作方法可以使用ModelState.IsValid属性来检查模型的状态。如果模型的数据符合规定,ModelState.IsValid将返回true,否则返回false

    c# 复制代码
    [HttpPost]
    public IActionResult CreateUser(UserModel user)
    {
        if (ModelState.IsValid)
        {
            // 数据验证成功,可以进行后续操作
            return RedirectToAction("Success");
        }
        else
        {
            // 数据验证失败,重新显示表单
            return View(user);
        }
    }

    在上面的例子中,如果user模型的数据验证成功,将会执行成功的逻辑;否则,将会返回包含验证错误信息的视图。

  3. 手动添加错误信息:

    你也可以在控制器中手动添加错误信息到ModelState中:

    ModelState.AddModelError("FieldName", "Error Message");
    

    例如:

    c# 复制代码
    [HttpPost]
    public IActionResult CreateUser(UserModel user)
    {
        if (user.Age < 18)
        {
            ModelState.AddModelError("Age", "Age must be at least 18.");
        }
    
        if (ModelState.IsValid)
        {
            // 数据验证成功,可以进行后续操作
            return RedirectToAction("Success");
        }
        else
        {
            // 数据验证失败,重新显示表单
            return View(user);
        }
    }
  4. 自定义验证:

    你可以创建自定义的验证逻辑,例如在控制器中实现一个自定义的验证方法,并在需要的时候调用它。这可以用于处理一些特殊的业务逻辑验证。

    c# 复制代码
    private bool IsEmailUnique(string email)
    {
        // 自定义逻辑,检查邮箱是否唯一
    }
    
    [HttpPost]
    public IActionResult CreateUser(UserModel user)
    {
        if (!IsEmailUnique(user.Email))
        {
            ModelState.AddModelError("Email", "Email is already in use.");
        }
    
        if (ModelState.IsValid)
        {
            // 数据验证成功,可以进行后续操作
            return RedirectToAction("Success");
        }
        else
        {
            // 数据验证失败,重新显示表单
            return View(user);
        }
    }

模型验证是保障接收到的数据合法性的重要环节。通过合理使用数据注解、模型状态验证和自定义验证,可以有效地保证应用程序接收到的数据符合预期的格式和规范。

路由

需要找到一种方法将这些 HTTP 请求发送到正确的控制器。

ASP.NET Core MVC 中,这个过程称为路由。

路由是指导 HTTP 请求到控制器的过程。

简单来说,路由负责决定如何响应请求,也就是将HTTP请求映射到应用程序中的的执行终结点(例如,一个方法或一个代码块)。

MVC 中间件将根据我们提供的 URL 和一些配置信息做出此决定.

默认路由规则

在Startup.cs文件中的Startup类中Configure()方法中,可以看到UseMvcWithDefaultRoute()中间件,这是启动默认的MVC路由的功能。

这会给ASP.NET Core添加默认路由规则。

默认路由规则: {controller=Home}/{action=Index}/{id?}

即:如果没有指定控制器(controller)或动作(action),它将默认调用名为 Home 的控制器的 Index 动作。问号表示 id 参数可有可无。


//默认映射规则

// {controller}指定映射到相应的控制器,默认是HomeController

// {action}指定映射到控制器的方法 ,默认是Index方法

// {id?}指定映射到控制器方法的参数,称为模型绑定


故举例假设定义的路由模板为: {controller=Home}/{action=Index}/{id};

这意味者它会去寻找HomeController文件的Index方法,并向Index方法的形参传入具体的id值。


如果需要自定义映射规则 ,可以使用UseEndpoints()中间件来设置端点,然后在中间件当中设置如何映射。

c# 复制代码
//在StartUp.cs文件中的Configure方法
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    //去掉原先的app.UseMvcDefaultRoute()
    //app.UseMvcWithDefaultRoute();
    //使用下面的中间件来自定义路由映射规则【lambda表达式】
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",//路由名字
            //设置的路由模板【pattern中等号两边不能有空格】
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}

也可以使用UseMvc()中间件来配置自定义映射规则:

c# 复制代码
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    //去掉原先的app.UseMvcDefaultRoute()
    //app.UseMvcWithDefaultRoute();
    //手动覆盖约定路由【template中等号两边不能有空格】
    app.UseMvc(routes =>
               {
                   routes.MapRoute(
                       name: "default",
                       template: "{controller=Home}/{action=About}/{id?}"
                   );
               });

路由中间件

  • Routing(路由):更准确的应该叫做Endpoint Routing,负责将HTTP请求按照匹配规则选择对应的终结点
  • Endpoint(终结点):负责当HTTP请求到达时,执行代码

路由是通过UseRoutingUseEndpoints两个中间件配合来完成注册的!

UseRouting`与`UseEndpoints`必须同时使用,而且必须先调用`UseRouting`,再调用`UseEndpoints

在调用UseRouting之前,你可以注册一些用于修改路由操作的数据,比如UseRewriter、UseHttpMethodOverride、UsePathBase等。

在调用UseRouting和UseEndpoints之间,可以注册一些用于提前处理路由结果的中间件,如UseAuthentication、UseAuthorization、UseCors等。

UseRouting()中间件

使用UseRouting()中间件启用路由功能。

然后使用UseEndpoints()中间件配置路由。


UseRouting注册了路由中间件,负责根据请求信息匹配路由定义。

路由信息定义包含了路由的处理程序信息,这些信息会在UseEndpoints方法中预先注册生成。

c# 复制代码
//1、启用默认路由
app.UseStaticFiles();
app.UseRouting();  
app.UseMvcWithDefaultRoute();
            
 
 
//2、自定义路由模板
app.UseStaticFiles();
app.UseRouting();
app.UseMvc(routes =>
{
    routes.MapRoute(
    name: "default",
    //注意下面的等号两边不能有空格
    template: "{controller=Home}/{action=Index}/{id?}");
});
 

//3、使用UseEndpoints自定义路由模板 
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
   endpoints.MapControllerRoute(
   name: "default",
   //注意下面的等号两边不能有空格
   pattern: "{controller=Home}/{action=Index}/{id?}");
 
});

约定路由

ASP.NET Core 中,约定路由(Convention-based Routing)是一种基于约定的路由方式,它使用一组命名约定来自动生成路由模板,而无需显式地配置每个路由规则。

约定路由常用于创建一致性强的 RESTful API 或 Web应用程序路由。

(所有的路由定义都可以在同一个地方进行配置,这些规则将被应用到所有的 Controller ,这也造成也其对一些特定的 URI 匹配不够灵活)。

需要定义一些包含一些参数化字符串的模板

例如,/{controller}/{action}/{id},当接受到请求后,会将请求的 URI 与这些模板进行匹配。

此时,MVC 根据入站 URL 中的具体值调用控制器类及其中的操作方法,以及如果涉及到传值,还会将值传递给方法作为方法实参。

特性路由

特性路由提供了一种直观的方式来定义路由规则,这种方式允许你在控制器或动作方法上 直接使用特性来指定路由信息,而无需在全局配置文件设置。

  • 提供更细粒度的控制。
  • 可以将路由信息直接与控制器或动作方法关联。
  • 无论你在特性路由中使用斜杠开头与否,访问相同的 URL 时都会得到相同的结果。
  • 使用特性路由时,还可以通过添加更多的属性来自定义路由的行为,例如约束路由参数的类型、限制HTTP方法等。

使用特性路由的特性写法:

[Route("这里书写路由Url字符串")]


限制Http请求方法的特性写法

  • [HttGet]
  • [HttpPost]
  • [HttpHead]
  • [HttpOptions]
  • [HttpDelete]
  • [HttpPacth]
  • [HttpPut]

这些特性中也可以填写字符串数据,其也是作为路由的组成部分。如:

  • [HttGet("这里书写路由Url字符串")]
  • [HttpPost("这里书写路由Url字符串")]等等。

路由参数:

特性路由也支持路由参数,可以通过使用大括号 {} 来定义参数:

c# 复制代码
[Route("products/{id}")]
//其中 {id} 是占位符用于接收参数。
public IActionResult Details(int id)
{
    // ...
}
//上述代码将 Details 方法映射到了 products/{id} 这个路由,并且定义了一个名为 id 的路由参数,其将传递给Details方法作为实参

路由约束:

可以通过在路由参数后面添加约束来限制路由参数的类型:

通过路由参数约束我们可以将路由模板中的参数限制为指定的类型 ,通用的语法如下所示:{parameter:constraint}

c# 复制代码
//单个参数类型约束条件
//代码中的 :int 约束了 id 参数必须为整数类型。
[Route("api/product/{id:int}")]     
public IHttpActionResult GetProduct(int id){}

//多个参数类型约束条件,之间用:符号进行分割
[Route("api/product/{id:int:min(1)}")]
public IHttpActionResult GetProduct(int id){}

下表罗列了可用的约束:

约束 描述 例子
alpha 将参数约束为大写或者小写的拉丁字母(a-z,A-Z) {x:alpha}
bool 将参数限制为 bool 类型 {x:bool}
datetime 将参数限制为 date {x:date}
decimal 将参数限制为 decimal 类型 {x:decimal}
float 将参数限制为 32 位浮点数类型 {x:float}
double 将参数限制为 64 位浮点数类型 {x:double}
int 将参数限制为 32 整形 {x:int}
long 将参数限制为 64位整形 {x:long}
guid 将参数类型限制为 guid 类型 {x:guid}
length 将参数的长度限制为指定长度或指定范围的长度 {x:length(5)/{x:length(1,10)}
min/max 限制参数(整形)最大或最小值 {x:min(10)}/{x:max(10)}
minlength/maxlength 限制参数的最小长度或最大长度 {x:minlength(1)}/{x:maxlength}
range 限制参数(整形) 的范围,包含两端 {x:range(1,3)}
regex 限制参数必须匹配指定的正则表达式 {x:regex(\expression)}

路由模板中的标记替换:

特性路由支持标记替换,标记替换是特性路由中的一种高级用法,允许在路由模板中使用标记(tokens),这些标记将在运行时被替换为实际的值。这使得你可以根据实际情况动态地构建路由。

标记[action],[area],[controller]会被替换成操作所对应的操作名(方法名),区域名,控制器名。

  • [controller]:这个标记会被替换为控制器名字去掉Controller字符串后的名称。

    例如,如果你有一个名为ProductsController的控制器,那么[controller]将在运行时替换为Products

  • [action]:这个标记会被替换为动作方法的名称。

    例如,如果你有一个名为GetProduct的动作方法,那么[action]将在运行时被替换为GetProduct

  • [area]: 这个标记会被替换为控制器所属的区域名称。

    区域允许你将应用程序分成更小、更可管理的功能模块,每个模块可以拥有自己的控制器、视图和静态资源等。

    例如,如果你的控制器类所属区域为admin,那么[area]将在运行时被替换为admin.

c# 复制代码
//例子
[Route("[controller]/[action]")]
    public class BlogController : Controller
    {
        [HttpGet]//匹配: Blog/GetAll
        public ActionResult GetAll()
        {
            return View();
        }
}

以下是使用特性路由的一些常见示例

  1. 控制器级别的特性路由:

    你可以在控制器类上使用 [Route] 特性来指定控制器的路由:

    c# 复制代码
    //将 ProductsController 映射到了 api/products 这个路由。
    [Route("api/products")]
    public class ProductsController : Controller
    {
        // ...
    }
  2. 控制器方法级别的路由模式:

    你也可以在控制器的动作方法上使用 [Route] 特性来指定动作方法的路由:

    c# 复制代码
    //代码将 List 方法映射到了 list 这个路由,该路由将会是相对于控制器级别的路由的一部分。
    public class ProductsController : Controller
    {
        [Route("list")]
        public IActionResult List()
        {
            // ...
        }
    }
  3. 多个路由:

    可以具有多个路由,这在特定情况下可能会很有用:

    c# 复制代码
    [Route("products/{id:int}")]
    [Route("items/{id:int}")]
    public IActionResult Details(int id)
    {
        // ...
    }
    //上述代码中,Details 方法可以通过 products/{id} 或者 items/{id} 两个路由访问。

约定路由特性路由 对比:

  • 特性路由修饰在Controller和Action上,特性路由有更多灵活性。
  • 特性路由一般用于设置RESTful API控制器的映射。
  • 约定路由和特性路由可以混用。

路由的执行顺序

常规路由会根据定义顺序来执行,与之相比,特性路由会构建一个树形结构,同时匹配所有路由。这种看起来像路由条目被放置在一个理想的顺序中,最具体的路由会在一般的路由之前执行。比如,路由blog/Edit/4 比 blog/{*article} 更加具体。

特性路由使用所有框架提供的路由特有的Order属性来配置顺序,并根据Order属性升序处理路由。默认是0,设置为-1时会在没有设置的路由之前执行。

视图

页面就是一个服务器端返回的数据,但这个数据并不是直接存在的,而是由控制器、模型、视图三大部分相互作用的结果。


默认情况下,ASP.NET Core MVC 项目中视图以 .cshtml 作为扩展名

视图文件夹的对应关系

ASP.NET Core 中,控制器名称和视图文件夹的命名对应关系是由约定来确定的。

这种约定遵循了一套命名规则,以便 ASP.NET Core 可以自动找到与控制器相关联的视图文件。

按照约定,控制器的名称应该以 "Controller" 结尾,例如 HomeControllerProductController 等。

对应的视图文件夹应该位于 Views 文件夹下,文件夹的名称应该与控制器的名称相匹配(但不包括 "Controller" 后缀)。

举个例子:

如果有一个名为 HomeController 的控制器,那么它对应的视图文件夹应该是 Views/Home,且如果HomeController控制器里面有名为IndexAbout的方法,则对应的视图目录结构如下:

/Views
    /Home
        Index.cshtml
        About.cshtml

这样,当你在 HomeController 中返回一个视图时,ASP.NET Core 将会自动查找 Views/Home 文件夹下的视图文件,并根据HomeController里的方法名去查找 Views/Home 文件夹下同名的视图文件。

需要注意的是,你也可以在控制器的方法中通过参数指定视图的名称,以覆盖默认的约定。

举个例子:

c# 复制代码
public IActionResult SomeAction()
{
    //这里的View()方法是属于父类Controller中的
    return View("CustomView"); // 通过参数指定,从而使用名为 CustomView.cshtml 的视图,从而覆盖默认的约定。
}

视图查找搜索顺序及过程:

假设同时存在Pages文件夹和Views文件夹

先查找Pages视图文件夹:

  1. 首先,ASP.NET Core 将会在 Pages 文件夹中按照请求路径查找对应的 Razor Page 文件。比如,对于请求 /Home/Index,它会在 Pages 文件夹下查找以下路径的 Razor Page视图:

    • /Pages/Home/Index.cshtml

    如果找到对应的 Razor Page 文件,则会使用它作为视图。

  2. 如果没有找到与请求路径匹配的 Razor Page,ASP.NET Core 将会在 Pages 文件夹中查找一个名为 _ViewStart.cshtml 的文件作为 fallback 页面。/Pages/_ViewStart.cshtml

    _ViewStart.cshtml 文件可以设置一些全局的视图配置,但通常情况下并不直接用作视图。

再查找Views视图文件夹

  1. 如果在控制器的方法中明确指定了视图的名称,ASP.NET Core 将直接搜索该名称对应的视图文件。

    c# 复制代码
    return View("CustomView"); // 将直接在Views文件夹中搜索 CustomView.cshtml
  2. 如果没有指定视图名称,ASP.NET Core 将按照约定搜索默认的视图文件。默认情况下,其将直接查找与当前操作方法同名的视图文件。

    c# 复制代码
    public IActionResult SomeAction()
    {
        // 在 SomeAction 方法中
    	return View(); // 将搜索 SomeAction.cshtml
    }
  3. 如果没有直接找到与当前操作方法同名的视图文件,ASP.NET Core 将搜索与控制器名称相同的文件夹中的与操作方法同名视图。

    c# 复制代码
    // 如果当前控制器名称为 HomeController且在 SomeAction 方法中
    return View(); // 将搜索 Views/Home/SomeAction.cshtml
  4. 如果以上步骤都没有找到对应的视图,ASP.NET Core 将在 Views/Shared 文件夹中搜索视图文件。

    c# 复制代码
    // 如果没有找到 Views/Home/SomeAction.cshtml,将搜索 Views/Shared/SomeAction.cshtml

    总的来说,ASP.NET Core 通过这套搜索顺序来自动关联控制器和视图,使得开发者可以更加便捷地管理视图文件。如果需要特殊处理,可以在方法中通过参数指定视图的名称,以覆盖默认的约定。


自定义视图路径

ASP.NET Core 中,可以通过 View() 方法来指定返回的视图的路径和名称,从而实现自定义视图路径。

View() 方法有多个重载,可以根据需要传递不同的参数来实现不同的自定义视图路径。

指定完整的视图路径(绝对路径)

  • **需以 / 开头的绝对路径 **:其中/代表的是应用程序的根目录

  • 需指定视图文件的完整路径,包括视图文件名和扩展名

  • 两种路径写法: 以波浪线~开头的视图路径 或 以/开头的视图路径;通常情况下,建议使用带有波浪线的路径,以确保路径始终相对于应用程序的根目录。

1.以~开头的视图路径

~开头的路径可以确保你的路径始终相对于应用程序的根目录,不受当前请求路径的影响。

波浪线路径是一种相对于应用程序根目录的路径表示方式,可以确保路径的准确性和可靠性。

c# 复制代码
return View("~/Views/CustomViews/MyController/MyView.cshtml");

2.以/开头的视图路径

这种写法和使用波浪线路径 ~ 类似,也是相对于应用程序的根目录来引用文件。区别在于,不带波浪线的相对路径可能会受到当前请求路径的影响,而波浪线路径始终相对于应用程序的根目录。

c# 复制代码
return View("/Views/CustomViews/MyView.cshtml");

指定相对于当前控制器或视图所在的文件夹而言的视图路径(相对路径)

  • 不以/开头

  • 需指定视图文件的相对路径,仅需要视图文件名,不需扩展名

c# 复制代码
return View("../../Views/CustomViews/MyView");

使用约定的视图文件夹和视图名称:

如果按照约定命名和组织视图文件,可以省略掉路径的指定:

c# 复制代码
return View(); // 会根据约定搜索视图

从控制器传递数据到视图

ASP.NET Core 中,控制器可以通过两种方式将数据传递给视图:

弱类型和强类型。

弱类型传递数据到视图

ViewData

  • ViewData 是一个字典,可以在控制器和视图之间传递数据。
  • 它使用字符串键和对象值来存储数据。
  • 在控制器中设置数据,然后在视图中使用相应的键来获取数据。

示例:

c# 复制代码
// 在控制器中设置数据
ViewData["Message"] = "Hello from ViewData";

// 在视图中获取数据
<h1>@ViewData["Message"]</h1>

ViewBag

  • ViewBag 是一个动态属性,可以在控制器和视图之间传递数据。
  • 它允许你像使用属性一样设置和获取数据。
  • 可以减少一些键的书写,但是需要注意类型转换。

示例:

c# 复制代码
// 在控制器中设置数据
ViewBag.Message = "Hello from ViewBag";

// 在视图中获取数据
<h1>@ViewBag.Message</h1>

强类型传递数据到视图

使用模型传递数据

  • 定义一个模型类,将需要传递的数据封装在模型中。
  • 在控制器中将模型实例传递给视图。

示例:

c# 复制代码
// 模型类
public class MyViewModel
{
    public string Message { get; set; }
}

// 在控制器中传递模型实例
public IActionResult Index()
{
    MyViewModel model = new MyViewModel
    {
        Message = "Hello from Model"
    };
    return View(model);
}

// 在视图中使用模型
@model MyViewModel

<h1>@Model.Message</h1>

使用ViewModel传递数据

这里的ViewModel可以理解为Java中的DTO对象,

主要就是为了提供给前端或者视图使用的数据。

示例:

  1. 在项目中创建一个用于传递数据的 ViewModel 类。这个类通常包含了你想要传递给视图的属性。

    c# 复制代码
    public class MyViewModel
    {
        public string Message { get; set; }
    }
  2. 在控制器的动作方法中实例化 ViewModel 并将数据赋值给它,然后将 ViewModel 传递给视图。

    c# 复制代码
    public IActionResult Index()
    {
        MyViewModel model = new MyViewModel
        {
            Message = "Hello from ViewModel"
        };
        return View(model);
    }
  3. 在视图中声明视图的模型类型,然后就可以通过 Model 属性来访问 ViewModel 中的属性。

    c# 复制代码
    @model MyViewModel
    
    <h1>@Model.Message</h1>

通过使用 ViewModel,我们可以将需要传递给视图的数据整合在一个类中,并且可以利用类的属性来提供更加具体和类型安全的数据。这种做法使得代码更加清晰、易于维护,并且提升了可读性。


总结来说,弱类型传递数据使用 ViewDataViewBag,而强类型传递数据使用模型类。强类型传递数据更加类型安全,能够提供更好的编译时检查和 IntelliSense 支持。


依赖注入

基本概念

服务(Service):

  • ASP.NET Core 中,服务是指应用程序中的一个组件,它执行特定的功能或提供特定的服务。

    例如,数据库连接、日志记录、配置管理等都可以是服务。

服务容器(Service Container):

  • 服务容器是 ASP.NET Core 框架提供的一个内置容器,它负责管理应用程序中所有的服务和它们的生命周期。

依赖(Dependency):

  • 当一个组件(如类、方法等)需要使用另一个组件时,它依赖于后者。
  • 依赖通常以构造函数参数、方法参数等形式注入。

依赖注入(Dependency Injection):

  • 依赖注入是指将一个组件所依赖的其他组件(服务)通过构造函数、方法参数等方式传递给它,而不是由它自己去创建或管理依赖。

依赖注入解决了这些棘手的问题:

  • 通过接口或基类(包含抽象方法)将依赖关系进行抽象化
  • 将依赖关系存放到服务容器中
  • 由框架负责创建和释放依赖关系的实例,并将实例注入到构造函数、属性或方法中

什么是依赖注入?

工厂模式的思想,知道这样做的好处;提及到依赖注入,通常会关联出两个概念:Ioc(控制反转)和DI(依赖注入)

控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。

其中最常见的方式叫做依赖注入(Dependency Injection,简称DI)。

  • IoC(控制反转): 就是将原先的new对象这个操作交由第三方容器,由容器统一创建对象并管理其创建对象的生命周期;

  • DI(依赖注入): 其中"依赖"有两层意思:

    • 类与类之间的依赖关系;

    • 对象的创建依赖于容器;

      "注入":不用主动从容器中获取对象,由容器根据对象依赖关系自动注入;【注入的前提是要把对象的控制权交给容器】

    依赖注入:程序将对象控制权交给容器,统一依赖容器创建对象,类之间的依赖,也是通过容器自动注入

ASP.NET Core 中的依赖注入(DI)是一个重要的设计模式,它允许你在应用程序中组织和管理组件之间的依赖关系,从而使代码更加模块化、可测试和可维护。


通俗地说,依赖注入就像是把一些工具放在一个工具箱里,当你需要用到某个工具时,只需要向工具箱里拿即可,不用自己去买买买,也不用自己去管理这些工具的生命周期。


这就好像你在做菜的时候,需要用到炒菜的锅、刀、砧板等工具。你不需要自己去买这些工具,而是可以向厨房管理员(ASP.NET Core)提出你需要这些工具,然后他会把它们交给你。你用完后,也不需要自己清洗和管理,你只需要把它们还给管理员即可。


总的来说,ASP.NET Core 的依赖注入机制让我们可以在需要的地方"要求"所需的服务,而不用自己去创建或者管理其生命周期。


依赖注入中的关键类型

NetCore中依赖注入有几个关键的类型,简单介绍一下:

  • IServiceCollection:负责存储注册的服务,可以通过其扩展方法进行服务注册;
  • ServiceDescriptor:服务注册时的信息,如服务类型、实现类型、实例类型、生命周期等;
  • IServiceProvider: 理解是常说的容器,是IServiceCollection创建出来的,用来提供实例的;
  • IServiceScope:表示一个容器的子容器的生命周期;

基本使用步骤

1.注册服务

在程序启动时,我们告诉 ASP.NET Core 我们需要哪些服务,这个过程叫做注册服务。(即是一个往工具箱里放入工具的过程)

Startup.cs 文件的 ConfigureServices 方法中,通过调用 services.AddXXX 方法来注册服务。例如:

c# 复制代码
// 例如,我们注册一个名为 IMyService 的服务,并告诉 ASP.NET Core 它的具体实现类 MyService。
//这里,IMyService 是接口,MyService 是实现了该接口的具体类。
//注:services 是指  IServiceCollection services;
services.AddScoped<IMyService, MyService>();

有三种注册服务的方式 :

ASP.NET Core 中,services.AddSingleton(), services.AddTransient(), 和 services.AddScoped() 是用于将服务注册到依赖注入容器的方法,分别表示单例模式、瞬时模式和作用域模式

  • services.AddSingleton()

services.AddSingleton<TService, TImplementation>() 将一个服务注册为单例模式,也就是在整个应用程序的生命周期中只会创建一个实例,之后的每次请求都会返回同一个实例,其会在应用程序启动时创建,并在应用程序关闭时销毁;单例服务是线程安全的,可以在整个应用程序中共享状态;适用于无状态的服务、共享配置信息等。

  • services.AddTransient()

services.AddTransient<TService, TImplementation>() 将一个服务注册为瞬时模式,也就是每次请求都会创建一个新的实例;瞬时服务在每次请求时都会创建新的实例,适用于无状态的、临时的操作;每次请求都会得到一个全新的实例,不存在线程安全问题。

  • services.AddScoped()

services.AddScoped<TService, TImplementation>() 将一个服务注册为作用域模式,也就是在每一个 HTTP 请求处理过程中都会创建一个新的实例,而在同一个http请求中多次获取该服务,会得到相同的实例;作用域服务在同一个 HTTP 请求处理过程中会保持相同的实例,适用于需要在同一请求中共享状态的场景,比如 DbContext。

范围(或称为作用域),即在某个范围(或作用域内)内,获取的始终是同一个服务实例,而不同范围(或作用域)间获取的是不同的服务实例。对于Web应用,每个请求为一个范围(或作用域)。

2.接收使用服务(依赖注入)

当我们需要用到这个服务时,ASP.NET Core 会自动帮我们把它提供出来,我们只需要在需要的地方使用它。

例如:

c# 复制代码
public class HomeController : Controller
{
    private readonly IMyService _myService;

    // 构造函数中接收 IMyService,ASP.NET Core 会自动帮我们传递进来。
    //所谓"ASP.NET Core 会帮我们把它提供出来",指的是在我们创建 HomeController 实例时,ASP.NET Core 会自动帮我们把 IMyService 这个服务的一个实例(也就是 MyService 的一个实例)传递给 HomeController 的构造函数。
    //这里的 IMyService myService 参数会在创建 HomeController 实例时自动注入。
    public HomeController(IMyService myService)
    {
        _myService = myService;
    }

    // 在需要的地方使用 _myService 即可。
    ...
}

在需使用服务的地方,将服务通过构造函数、方法参数等方式注入。

  • 构造函数注入:
c# 复制代码
public class HomeController : Controller
{
    //在构造函数中将依赖项赋值给私有字段时,将字段标记为 readonly 通常是一个良好的实践。readonly 关键字确保了一旦在构造函数中给字段赋值,就不能再次在类的其他地方更改它的值。
    //总的来说,尽管在这种情况下 readonly 不是必需的,但它是一种良好的实践,可以帮助你保持代码的一致性和可维护性。
    private readonly IMyService _myService;

    public HomeController(IMyService myService)
    {
        _myService = myService;
    }

    // ...
}
  • 方法参数注入
c# 复制代码
//这种方式通常用于在控制器的具体方法中获取所需的服务。
//[FromServices] 是 ASP.NET Core 中用于在控制器的动作方法中获取服务。它告诉 ASP.NET Core 从依赖注入容器中获取指定类型的服务,并将其传递给动作方法。

//例如,如果你在一个控制器的动作方法中需要使用某个服务,可以将该服务的类型作为参数,并在参数前加上 [FromServices] 属性:
//这样,ASP.NET Core 将会自动从依赖注入容器中找到 IMyService 的实现,并传递给 DoSomeing 方法。

//对于方法参数的依赖注入来说,[FromServices] 是一个可选的特性。如果你在方法参数中使用了 [FromServices],ASP.NET Core 将会尝试从依赖注入容器中获取相应的服务并传递给该参数。如果没有使用 [FromServices],你也可以在方法内部使用 HttpContext.RequestServices.GetService<TService>() 来手动获取所需的服务。
public IActionResult DoSomeing([FromServices] IMyService myService)
{
    // ...
}
  • 直接从 HttpContext.RequestServices 中获取:

    如果在非控制器类中需要获取服务,你可以使用 ()HttpContext.RequestServices.GetService(typeof("xx"))

c# 复制代码
//这种方式可以在任何地方通过 HttpContext.RequestServices 来获取所需的服务,但通常不推荐在控制器或服务之外的地方使用。
public class HomeController : Controller
{	
    public IActionResult SomeAction()
    {
        var myService = (IMyService)HttpContext.RequestServices.GetService(typeof(IMyService));
        // ...
    }
}

三种方式可以根据实际情况选择使用,通常情况下,构造函数注入是最常用且推荐的方式,因为它可以保证服务在类被创建时就可用,并且能够提高代码的可测试性和可维护性。其他方式则在特定场景下可能会更为方便。



Replace && Remove 扩展方法

有时我们想要替换或是移除某些服务,这时就需要使用ReplaceRemove了。

c# 复制代码
// 将 IMyService 的实现替换为 MyService1
services.Replace(ServiceDescriptor.Singleton<IMyService, MyService>());
// 移除 IMyService 注册的实现 MyService
services.Remove(ServiceDescriptor.Singleton<IMyService, MyService>());
// 移除 IMyService 的所有注册
services.RemoveAll<IMyService>();
// 清除所有服务注册

Autofac

Autofac 是一个老牌DI组件,可使用Autofac替换ASP.NET Core自带的DI容器。

  1. 安装nuget包:
c# 复制代码
Install-Package Autofac
Install-Package Autofac.Extensions.DependencyInjection
  1. 替换服务提供器工厂
c# 复制代码
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        })
        // 通过此处将默认服务提供器工厂替换为 autofac
        .UseServiceProviderFactory(new AutofacServiceProviderFactory());

具体可参考学习链接: 跟我一起学.NetCore之Asp.NetCore中集成Autofac扩展 (qq.com)

更多内容

可参考学习链接: #跟我一起学.NetCore (qq.com)


最后附上一张.Net修仙图(学习路线)



The End!!创作不易,欢迎点赞/评论!!欢迎关注个人GZH!!

相关推荐
向宇it7 小时前
【从零开始入门unity游戏开发之——C#篇25】C#面向对象动态多态——virtual、override 和 base 关键字、抽象类和抽象方法
java·开发语言·unity·c#·游戏引擎
向宇it8 小时前
【从零开始入门unity游戏开发之——C#篇24】C#面向对象继承——万物之父(object)、装箱和拆箱、sealed 密封类
java·开发语言·unity·c#·游戏引擎
星河梦瑾9 小时前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
长潇若雪11 小时前
《类和对象:基础原理全解析(上篇)》
开发语言·c++·经验分享·类和对象
志-AOX12 小时前
C语言入门指南:从零开始的编程之路
经验分享
WANGWUSAN6613 小时前
Python高频写法总结!
java·linux·开发语言·数据库·经验分享·python·编程
坐井观老天13 小时前
在C#中使用资源保存图像和文本和其他数据并在运行时加载
开发语言·c#
赵谨言15 小时前
基于python+django的外卖点餐系统
经验分享·python·毕业设计
pchmi15 小时前
C# OpenCV机器视觉:模板匹配
opencv·c#·机器视觉
stm 学习ing16 小时前
HDLBits训练3
c语言·经验分享·笔记·算法·fpga·eda·verilog hdl