asp.net core 系列之Dependency injection(依赖注入)

这篇文章主要讲解asp.net core 依赖注入的一些内容。

ASP.NET Core支持依赖注入。这是一种在类和其依赖之间实现控制反转的一种技术(IOC).

一.依赖注入概述

1.原始的代码

依赖就是一个对象的创建需要另一个对象。下面的MyDependency是应用中其他类需要的依赖:

public class MyDependency
{
    public MyDependency()
    {
    }

    public Task WriteMessage(string message)
    {
        Console.WriteLine(
            $"MyDependency.WriteMessage called. Message: {message}");

        return Task.FromResult(0);
    }
}

一个MyDependency类被创建使WriteMessage方法对另一个类可用。MyDependency类是IndexModel类的依赖(即IndexModel类的创建需要用到MyDependency类):

public class IndexModel : PageModel
{
    MyDependency _dependency = new MyDependency();

    public async Task OnGetAsync()
    {
        await _dependency.WriteMessage(
            "IndexModel.OnGetAsync created this message.");
    }
}

2.原始代码分析

IndexModel类创建了MyDependency类,并且直接依赖MyDependency实例。上面的代码依赖是有问题的,并且应该被避免(避免直接创建依赖的实例对象),

原因如下:

  • 需要用一个不同的实现来替换MyDependency,这个类必须被修改
  • 如果MyDependency有依赖,他们必须被这个类配置。在一个有很多类依赖MyDependency的大的项目中,配置代码在应用中会很分散。
  • 这种实现对于单元测试是困难的。对于MyDependency,应用应该使用mock或者stub,用这种方式是不可能的。

依赖注入解决那些问题:

  • 接口的使用抽象了依赖的实现
  • 在service container注册依赖。ASP.NET Core提供了一个内置的service container, IServiceProvider.  Services是在应用的Startup.ConfigureServices中被注册。
  • 一个类是在构造函数中注入service。框架执行着创建一个带依赖的实例的责任,并且当不需要时,释放。

3.下面是改良后的代码

这示例应用中,IMyDependency接口定义了一个方法:

public interface IMyDependency
{
    Task WriteMessage(string message);
}

接口被一个具体的类型,MyDependency实现:

public class MyDependency : IMyDependency
{
    private readonly ILogger<MyDependency> _logger;

    public MyDependency(ILogger<MyDependency> logger)
    {
        _logger = logger;
    }

    public Task WriteMessage(string message)
    {
        _logger.LogInformation(
            "MyDependency.WriteMessage called. Message: {MESSAGE}",
            message);

        return Task.FromResult(0);
    }
}

在示例中,IMydependency实例被请求和用于调用服务的WriteMessage方法:

public class IndexModel : PageModel
{
    private readonly IMyDependency _myDependency;

    public IndexModel(
        IMyDependency myDependency,
        OperationService operationService,
        IOperationTransient transientOperation,
        IOperationScoped scopedOperation,
        IOperationSingleton singletonOperation,
        IOperationSingletonInstance singletonInstanceOperation)
    {
        _myDependency = myDependency;
        OperationService = operationService;
        TransientOperation = transientOperation;
        ScopedOperation = scopedOperation;
        SingletonOperation = singletonOperation;
        SingletonInstanceOperation = singletonInstanceOperation;
    }

    public OperationService OperationService { get; }
    public IOperationTransient TransientOperation { get; }
    public IOperationScoped ScopedOperation { get; }
    public IOperationSingleton SingletonOperation { get; }
    public IOperationSingletonInstance SingletonInstanceOperation { get; }

    public async Task OnGetAsync()
    {
        await _myDependency.WriteMessage(
            "IndexModel.OnGetAsync created this message.");
    }
}

4.改良代码分析及扩展讲解(使用DI)

MyDependency在构造函数中,要求有一个ILogger<TCategoryName>。用一种链式的方法使用依赖注入是很常见的。每个依赖依次再请求它自己需要的依赖。(即:MyDependency是一个依赖,同时,创建MyDependency又需要其他依赖:ILogger<TCategoryName>。)

IMyDependency和ILogger<TCategoryName>必须在service container中注册。IMyDependency是在Startup.ConfigureServices中注册。ILogger<TCategoryName>是被logging abstractions infrastructure注册,所以它是一种默认已经注册的框架提供的服务。(即框架自带的已经注册的服务,不需要再另外注册)

容器解析ILogger<TCategoryName>,通过利用泛型. 消除注册每一种具体的构造类型的需要。(因为在上面的例子中,ILogger中的泛型类型为MyDependency,但是如果在其他类中使用ILogger<>, 类型则是其他类型,这里使用泛型比较方便)

services.AddSingleton(typeof(ILogger<T>), typeof(Logger<T>)); 

这是它的注册的语句(框架实现的),其中的用到泛型,而不是一种具体的类型。

在示例应用中,IMyDependency service是用具体的类型MyDependency来注册的。这个注册包括服务的生命周期(service lifetime)。Service lifetimes随后会讲。

如果服务的构造函数要求一个内置类型,像string,这个类型可以被使用configuration 或者options pattern来注入:

public class MyDependency : IMyDependency
{
    public MyDependency(IConfiguration config)
    {
        var myStringValue = config["MyStringKey"];

        // Use myStringValue
    }

    ...
}

或者 options pattern(注意:不止这些,这里简单举例)

二.框架提供的服务(Framework-provided services)

Startup.ConfigureServices方法有责任定义应用使用的服务,包括平台功能,例如Entity Framework Core和ASP.NET Core MVC。最初,IServiceColletion提供给ConfigureServices下面已经定义的服务(依赖于怎样配置host):

当一个service colletion 扩展方法可以用来注册一个服务,习惯是用一个单独的Add{SERVICE_NAME} 扩展方法来注册服务所需要的所有服务。下面的代码是一个怎么使用扩展方法AddDbContext, AddIdentity,和AddMvc, 添加额外的服务到container:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.AddMvc();
}

更多的信息:ServiceCollection Class

三. 服务生命周期(Service lifetimes)

为每个注册的服务选择一个合适的生命周期。ASP.NET Core服务可以用下面的声明周期配置:

Transient、Scoped、Singleton

Transient(临时的)

临时的生命周期服务是在每次从服务容器中被请求时被创建。这个生命周期对于lightweight(轻量的),stateless(无状态的)服务比较合适。

Scoped(范围)

范围生命周期被创建,一旦每个客户端请求时(connection)

警告:当在中间件中使用范围服务时,注入服务到Invoke或者InvokeAsync方法。不要通过构造函数注入,因为那回强制服务表现的像是singleton(单例)。

Singleton(单独)

单独生命周期在第一次请求时被创建(或者说当ConfigureService运行并且被service registration指定时)。之后每一个请求都使用同一个实例。如果应用要求一个单独行为(singleton behavior),允许service container来管理服务生命周期是被推荐的。不要实现一个单例设计模式并且在类中提供用户代码来管理这个对象的生命周期。

警告:从一个singleton来解析一个范围服务(scoped service)是危险的。它可能会造成服务有不正确的状态,当处理随后的请求时。

构造函数注入行为

服务可以被通过两种机制解析:

  • IServiceProvider
  • ActivatorUtilities : 允许对象创建,可以不通过在依赖注入容器中注入的方式。ActivatorUtilities是使用user-facing abstractions,例如Tag Helpers , MVC controllers 和 model binders.

构造函数可以接受参数,不通过依赖注入提供,但是这些参数必须指定默认值。

当服务被通过IServiceProvider或者ActivatorUtilities解析时,构造函数注入要求一个公共的构造函数。

当服务被ActivatorUtilities解析时,构造函数注入要求一个合适的构造函数存在。构造函数的重载是被支持的,但是只有一个重载可以存在,它的参数可以被依赖注入执行(即:可以被依赖注入执行的,只有一个构造函数的重载)。

四. Entity Framework contexts

Entity Framework contexts 通常使用scoped lifetime ,添加到服务容器中(service container).因为web 应用数据库操作的范围适用于client request(客户端请求)。默认的生命周期是scoped,如果一个生命周期没有被AddDbContext<TContext>重载指定,当注册database context时。给出生命周期的服务不应该使用一个生命周期比服务的生命周期短的database context.

五.Lifetime and registration options

为了说明lifetime和registration options之间的不同,考虑下面的接口:这些接口表示的任务都是带有唯一标识的操作。取决于这些接口的操作服务的生命周期怎么配置,container提供了要么是同一个要么是不同的服务当被一个类请求时:

public interface IOperation
{
    Guid OperationId { get; }
}

public interface IOperationTransient : IOperation
{
}

public interface IOperationScoped : IOperation
{
}

public interface IOperationSingleton : IOperation
{
}

public interface IOperationSingletonInstance : IOperation
{
}

这些接口在一个Operation类中被实现。Operation 构造函数生成了一个GUID,如果GUID没被提供:

public class Operation : IOperationTransient,
    IOperationScoped,
    IOperationSingleton,
    IOperationSingletonInstance
{
    public Operation() : this(Guid.NewGuid())
    {
    }

    public Operation(Guid id)
    {
        OperationId = id;
    }

    public Guid OperationId { get; private set; }
}

OperationService依赖于其他的Operation 类型被注册。当OperationService被通过依赖注入请求,它要么接收每个服务的一个新实例要么接收一个已经存在的实例(在依赖服务的生命周期的基础上)。

  • 当临时服务(transient services)被创建时,当被从容器中请求时,IOperationTransient服务的OperationId是不同的。OperationService接收到一个IOperationTransient类的实例。这个新实例产生一个不同的OperationId.
  • 每个client请求时,scoped services被创建,IOperationScoped service的OperationId是一样的,在一个client request内。跨越client requests,两个service享用一个不同的OperationId的值。
  • 当singleton和singleton-instance服务一旦被创建,并且被使用跨越所有的client requests和所有的服务,则OperationId跨越所有的service requests是一致的。
public class OperationService
{
    public OperationService(
        IOperationTransient transientOperation,
        IOperationScoped scopedOperation,
        IOperationSingleton singletonOperation,
        IOperationSingletonInstance instanceOperation)
    {
        TransientOperation = transientOperation;
        ScopedOperation = scopedOperation;
        SingletonOperation = singletonOperation;
        SingletonInstanceOperation = instanceOperation;
    }

    public IOperationTransient TransientOperation { get; }
    public IOperationScoped ScopedOperation { get; }
    public IOperationSingleton SingletonOperation { get; }
    public IOperationSingletonInstance SingletonInstanceOperation { get; }
}

在Startup.ConfigureServices中,每个类型根据命名的生命周期被添加到容器中:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    services.AddScoped<IMyDependency, MyDependency>();
    services.AddTransient<IOperationTransient, Operation>();
    services.AddScoped<IOperationScoped, Operation>();
    services.AddSingleton<IOperationSingleton, Operation>();
    services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));

    // OperationService depends on each of the other Operation types.
    services.AddTransient<OperationService, OperationService>();
}

IOperationSingletonInstance服务是一个特殊的实例,它的ID是Guid.Empty. 它是清楚的,当这个类型被使用(它的GUID都是0组成的)

示例应用说明了requests内的对象生命周期和两个requests之间的对象生命周期。示例应用的IndexModel请求IOperation的每个类型和OperationService。这个页面展示了所有的这个page model类的和服务的OperationId值,通过属性指定。

public class IndexModel : PageModel
{
    private readonly IMyDependency _myDependency;

    public IndexModel(
        IMyDependency myDependency,
        OperationService operationService,
        IOperationTransient transientOperation,
        IOperationScoped scopedOperation,
        IOperationSingleton singletonOperation,
        IOperationSingletonInstance singletonInstanceOperation)
    {
        _myDependency = myDependency;
        OperationService = operationService;
        TransientOperation = transientOperation;
        ScopedOperation = scopedOperation;
        SingletonOperation = singletonOperation;
        SingletonInstanceOperation = singletonInstanceOperation;
    }

    public OperationService OperationService { get; }
    public IOperationTransient TransientOperation { get; }
    public IOperationScoped ScopedOperation { get; }
    public IOperationSingleton SingletonOperation { get; }
    public IOperationSingletonInstance SingletonInstanceOperation { get; }

    public async Task OnGetAsync()
    {
        await _myDependency.WriteMessage(
            "IndexModel.OnGetAsync created this message.");
    }
}

下面的输出展示了两个请求的结果:

从结果看出:

  • Transient对象总是不同的。Transient OperationId的值对于第一个和第二个客户端请求是在OperationService中不同的,并且跨越client requests. 一个新的实例被提供给每个service request和client request.
  • Scoped对象对于一个client request内部是一样的,跨越client request是不同的。
  • Singleton对象对于每个对象和每个请求都是一样的,不管Operation实例是否在ConfigureServices中被提供了。

可以看出,Transient一直在变;Scoped 同一个client request请求内不变;Singleton一直不变;

六. Call Services from main(在main中调用services)

用IServiceScopeFactory.CreateScope创建一个IServiceScope 来解析一个scoped service在应用的范围内。这个方式是有用的对于在Startup中得到一个scoped service 来运行初始化任务。下面的例子展示了MyScopedServcie怎样包含一个context,在Program.Main中:

public static void Main(string[] args)
{
    var host = CreateWebHostBuilder(args).Build();

    using (var serviceScope = host.Services.CreateScope())
    {
        var services = serviceScope.ServiceProvider;

        try
        {
            var serviceContext = services.GetRequiredService<MyScopedService>();
            // Use the context here
        }
        catch (Exception ex)
        {
            var logger = services.GetRequiredService<ILogger<Program>>();
            logger.LogError(ex, "An error occurred.");
        }
    }

    host.Run();
}

七.Scope validation(范围验证)

当应用在开发环境运行时,默认的service provider 执行检查来验证:

  • Scoped services不是直接或间接的被从root service provider中解析
  • Scoped services 不是直接或间接的被注入为singletons

root service provider 是当BuildServiceProvider被调用时被创建的。Root service provider的生命周期对应于应用/服务器 的生命周期,当provider随着应用启动并且当应用关闭时会被释放。

Scoped服务被创建它们的容器释放。如果scoped service在root container中被创建,服务的生命周期实际上是被提升为singleton,因为它只有当应用或者服务器关闭时才会被root container释放。验证servcie scopes 注意这些场景,当BuildServiceProvider被调用时。

八.Request Services

来自HttpContext的ASP.NET Core request中的可用的services通过HttpContext.RequestServices集合来暴露。

Request Services代表应用中被配置的services和被请求的部分。当对象指定依赖,会被RequestService中的类型满足,而不是ApplicationServices中的。

通常,应用不应该直接使用那些属性。相反的,请求满足那个类型的的这些类,可以通过构造函数并且允许框架注入这些依赖。这使类更容易测试。

注意:请求依赖,通过构造函数参数来得到RequestServices集合更受欢迎。

九. Design services for dependency injection

最佳实践:

  • 设计services使用依赖注入来包含它们的依赖
  • 避免stateful,静态的方法调用
  • 避免在services内直接初始化依赖类。直接初始化是代码关联一个特定的实现
  • 使应用的类small, well-factored,和easily tested.

如果一个类似乎有很多注入的依赖,这通常是它有太多职责的信号,并且违反了Single Responsibility Principle(SRP)单一职责原则。尝试通过移动一些职责到一个新类来重构这个类。记住,Razor Pages page model classes和MVC controller classes应该专注于UI层面。Business rules和data access implementation细节应该在那些合适的分开的关系的类中。

Disposal of services

容器为它创建的类调用IDisposable的Dispose。如果一个实例被用户代码添加到容器中,它不会自动释放。

// Services that implement IDisposable:
public class Service1 : IDisposable {}
public class Service2 : IDisposable {}
public class Service3 : IDisposable {}

public interface ISomeService {}
public class SomeServiceImplementation : ISomeService, IDisposable {}

public void ConfigureServices(IServiceCollection services)
{
    // The container creates the following instances and disposes them automatically:
    services.AddScoped<Service1>();
    services.AddSingleton<Service2>();
    services.AddSingleton<ISomeService>(sp => new SomeServiceImplementation());

    // The container doesn‘t create the following instances, so it doesn‘t dispose of
    // the instances automatically:
    services.AddSingleton<Service3>(new Service3());
    services.AddSingleton(new Service3());
}

即,如果,类是被用户代码添加容器中的,不会自动释放。像下面这种直接new类的。

十.Default service container replacement

内置的service container意味着提供服务来满足框架和大多消费应用的需求。我们建议使用功能内置容器,除非你需要一个特殊的功能,内置容器不支持。有些功能在第三方容器支持,但是内置容器不支持:

  • Property injection
  • Injection based on name
  • Child containers
  • Custom lifetime management
  • Fun<T> support for lazy initializtion

下面的示例,使用Autofac替代内置容器:

  • 安装合适的容器包:
    • Autofac
    • Autofac.Extensions.DependencyInjection
  • 在Startup.ConfigureServices中配置容器,并且返回IServiceProvider:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    // Add other framework services

    // Add Autofac
    var containerBuilder = new ContainerBuilder();
    containerBuilder.RegisterModule<DefaultModule>();
    containerBuilder.Populate(services);
    var container = containerBuilder.Build();
    return new AutofacServiceProvider(container);
}

要使用第三方容器,Startup.ConfigureServices必须返回IServiceProvider.

  • 在DefaultModule中配置Autofac
public class DefaultModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<CharacterRepository>().As<ICharacterRepository>();
    }
}

在运行时,Autofac被用来解析类型和注入依赖。

更多: Autofac documentation.

Thread safety

创建线程安全的单例服务。如果一个单例服务对一个临时的服务有依赖,这个临时的服务可能需要要求线程安全根据它怎样被单例服务使用。

单例服务的工厂方法,例如AddSingleton<TService>(IServiceColletion, Func<IServiceProvider, TService>)的第二个参数,不需要线程安全。像一个类型的构造函数,它一次只能被一个线程调用。

十一.Recommendations

  • Async/await 和 Task 依据service resolution(服务解决)是不支持的。C# 不支持异步的构造函数;因此,推荐的模式是在同步解析服务之后使用异步方法。
  • 避免直接在service container中存储数据和配置。例如,用户的购物车不应该被添加到service container. 配置应该使用option pattern. 相似的,避免data holder对象可接近其他对象。最好是请求实际的item通过DI.
  • 避免静态得到services(例如,静态类型IApplicationBuilder.ApplicationServices的在别处的使用)
  • 避免使用service locator pattern. 例如,当你可以用DI时,不要用GetService来获取一个服务。

错误的:

public void MyMethod()
{
    var options =
        _services.GetService<IOptionsMonitor<MyOptions>>();
    var option = options.CurrentValue.Option;

    ...
}

正确的:

private readonly MyOptions _options;

public MyClass(IOptionsMonitor<MyOptions> options)
{
    _options = options.CurrentValue;
}

public void MyMethod()
{
    var option = _options.Option;

    ...
}
  • 另一个service locator 变量要避免,是注入一个在运行时解析依赖的工厂。那些实践的两者都混合了Inversion of Control策略(即避免依赖注入和其他方式混合使用)。
  • 避免静态得到HttpContext(例如,IHttpContextAccessor.HttpContext)

有时候的场景,可能需要忽略其中的建议。

DI是static/global object access patterns的可替代方式。如果你把它和static object access 方式混合使用,可能不能认识到DI的好处。

参考网址:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.2

原文地址:https://www.cnblogs.com/Vincent-yuan/p/11145717.html

时间: 2024-10-28 19:01:56

asp.net core 系列之Dependency injection(依赖注入)的相关文章

spring in action学习笔记一:DI(Dependency Injection)依赖注入之CI(Constructor Injection)构造器注入

一:这里先说一下DI(Dependency Injection)依赖注入有种表现形式:一种是CI(Constructor Injection)构造方法注入,另一种是SI(Set Injection) set 注入.这篇随笔讲的是第一种构造方法注入(Constructor Injection). 其实DI(Dependency Injection)依赖注入你不妨反过来读:注入依赖也就是把"依赖"注入到一个对象中去.那么何为"依赖"呢?依赖就是讲一个对象初始化或者将实例

ASP.NET Core 新建线程中使用依赖注入的问题

问题来自博问的一个提问 .net core 多线程数据保存的时候DbContext被释放 . TCPService 通过构造函数注入了 ContentService , ContentService 的实例依赖了 AppDbContext (继承自 EF Core 的 DbContext).在 TCPService 中通过 Thread.Start 启动了一个新的线程执行了 TCPService 中的 Receive 方法,在 Receive 方法中通过 ContentService 进行保存数

IoC(Inversion of Control)控制反转和 DI(Dependency Injection)依赖注入

首先想说说IoC(Inversion of Control,控制倒转).这是spring的核心,贯穿始终.所谓IoC,对于spring框架来说,就是由spring来负责控制对象的生命周期和对象间的关系.这是什么意思呢,举个简单的例子,我们是如何找女朋友的?常见的情况是,我们到处去看哪里有长得漂亮身材又好的mm,然后打听她们的兴趣爱好.qq号.电话号.ip号.iq号---,想办法认识她们,投其所好送其所要,然后嘿嘿--这个过程是复杂深奥的,我们必须自己设计和面对每个环节.传统的程序开发也是如此,在

ASP.NET Core 配置文件(无处不在的依赖注入)

前烟: .NET Core 中取消了以往的 XML 节点配置文件,改用了 *.json 格式. 在 Startup.cs 文件中,构造方法 build appsetting.json 文件, 本文主要对解析配置文件的官方工具类做总结: 一.appsettings.json 文件 在新建的 Core Web 项目中,默认会有一个全局的配置变量:IConfigurationRoot 提供了索引器.GetSection 方法: { "Host": "http://localhost

Asp.net Core AutoFac根据程序集实现依赖注入

一.创建一个专门用于依赖注入的接口(IAutoInject), 所有的服务接口皆继承于此接口 namespace DDD.Domain { public interface IAutoInject { } } 二.添加服务接口,需要继承IAutoInject namespace DDD.Domain.Product.Inter { public interface IProductTypeService : IAutoInject { int Add(ProductType entity); }

目录导航「深入浅出ASP.NET Core系列」

希望给你3-5分钟的碎片化学习,可能是坐地铁.等公交,积少成多,水滴石穿,谢谢关注. 入门篇 1.1课程介绍「深入浅出ASP.NET Core系列」 1.2环境安装「深入浅出ASP.NET Core系列」 1.3创建项目「深入浅出ASP.NET Core系列」 1.4部署到IIS「深入浅出ASP.NET Core系列」 1.5准备CentOS和Nginx环境「深入浅出ASP.NET Core系列」 1.6部署到CentOS「深入浅出ASP.NET Core系列」 2.1命令行和JSON的配置「深

ASP.NET CORE系列【五】webapi整理以及RESTful风格化

原文:ASP.NET CORE系列[五]webapi整理以及RESTful风格化 介绍 什么是RESTful?  这里不多做赘述,详情请百度! 哈哈,本来还想巴拉巴拉介绍一些webapi, RESTful的, 还是算了,咱们直接上干货!(原因是懒!哈哈) 使用 以前使用过mvc的人对webapi 应该都很熟悉,先看一段熟悉的代码 大伙发现了什么没?跟以往mvc大多数相同,但有些地方不同 ,我们来一起看看有何区别 1.首先SysUsersController上面有一段代码 [Produces("a

ASP.NET CORE系列【一】搭建ASP.NET CORE项目

原文:ASP.NET CORE系列[一]搭建ASP.NET CORE项目 为什么要使用 ASP.NET Core? NET Core 刚发布的时候根据介绍就有点心里痒痒,微软的尿性都懂的,新东西bug太多,现在2.0也发布很久了,决定研究一下. ASP.NET Core官方文档https://docs.microsoft.com/en-us/aspnet/core/getting-started ASP.NET Core 具有如下优点: 生成 Web UI 和 Web API 的统一场景. 集成

ASP.NET CORE系列【二】使用Entity Framework Core进行增删改查

原文:ASP.NET CORE系列[二]使用Entity Framework Core进行增删改查 介绍 EntityFrameworkCore EF core 是一个轻量级的,可扩展的EF的跨平台版本.对于EF而言 EF core 包含许多提升和新特性,同时 EF core 是一个全新的代码库,并不如 EF6 那么成熟和稳定.EF core 保持了和EF相似的开发体验,大多数顶级API都被保留了下来,所以,如果你用过EF6,那么上手EF core你会觉得非常轻松和熟悉,EF core 构建在一