ASP.NET Core 中的 ORM 之 Entity Framework

EF Core 简介

Entity Framework Core 是微软自家的 ORM 框架。作为 .Net Core 生态中的一个重要组成部分,它是一个支持跨平台的全新版本,用三个词来概况 EF Core 的特点:轻量级、可扩展、跨平台。

目前 EF Core 支持的数据库:

  • Microsoft SQL Server
  • SQLite
  • Postgres (Npgsql)
  • SQL Server Compact Edition
  • InMemory (for testing purposes)
  • MySQL
  • IBM DB2
  • Oracle
  • Firebird

使用 EF Core(Code First)

  1. 新建一个 WebAPI 项目

  2. 通过 Nuget 安装 EF Core 引用

    // SQL Server
    Install-Package Microsoft.EntityFrameworkCore.SqlServer
    

    其他数据库请查看:https://docs.microsoft.com/zh-cn/ef/core/providers/

  3. 添加实体

    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
        public int Rating { get; set; }
    
        public List<Post> Posts { get; set; }
    }
    
    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
    
        public int BlogId { get; set; }
        public Blog Blog { get; set; }
    }
    
  4. 添加数据库上下文

    public class BloggingContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
    }
    

    有两种方式配置数据库连接,一种是注册 Context 的时候提供 options。比较推荐这种方式。

    public class BloggingContext : DbContext
    {
        public BloggingContext(DbContextOptions<BloggingContext> options)
            : base(options)
        { }
    
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
    }
    

    在 Startup 中配置

    public void ConfigureServices(IServiceCollection services)
    {
        var connectionString = @"Server=.;Database=Blogging;Trusted_Connection=True;";
        services.AddDbContext<BloggingContext>(o => o.UseSqlServer(connectionString));
    
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    }
    

    一种是重载 OnConfiguring 方法提供连接字符串:

    public class BloggingContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
        
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Server=.;Database=Blogging;Trusted_Connection=True;");
            base.OnConfiguring(optionsBuilder);
        }
    }
    
  5. 在Controller 中使用 Context

    public class BlogsController : ControllerBase
    {
        private readonly BloggingContext _context;
    
        public BlogsController(BloggingContext context)
        {
            _context = context;
        }
    
        // GET: api/Blogs
        [HttpGet]
        public IEnumerable<Blog> GetBlogs()
        {
            return _context.Blogs;
        }
    }
    

迁移 Migration

  1. 通过 Nuget 引入EF Core Tool 的引用

    Install-Package Microsoft.EntityFrameworkCore.Tools
    

    如果需要使用 dotnet ef 命令, 请添加 Microsoft.EntityFrameworkCore.Tools.DotNet

  2. 生成迁移

    打开Package Manager Console,执行命令 Add-Migration InitialCreate
    执行成功后会在项目下生成一个 Migrations目录,包含两个文件:

    • BloggingContextModelSnapshot:当前Model的快照(状态)。
    • 20180828074905_InitialCreate:这里面包含着migration builder需要的代码,用来迁移这个版本的数据库。里面有Up方法,就是从当前版本升级到下一个版本;还有Down方法,就是从下一个版本再退回到当前版本。
  3. 更新迁移到数据库

    执行命令 Update-Database
    如果执行成功,数据库应该已经创建成功了。现在可以测试刚才创建的WebAPI应用了。

    使用代码 Database.Migrate(); 可以达到同样的目的

        public BloggingContext(DbContextOptions<BloggingContext> options)
            : base(options)
        {
            Database.Migrate();
        }
    

EF Core 中的一些常用知识点

实体建模

EF 根据对 Model 的配置生成表和字段,主要有三种配置方式:

  • 约定 根据约定(Id 或者 Id)会被视为映射表的主键,并且该主键是自增的。

  • Data Annotation 数据注解

    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    
    public class Blog
    {
        [Key]
        [Column("BlogId")]
        public int BlogId { get; set; }
        [Required]
        [MaxLength(500)]
        public string Url { get; set; }
        public int Rating { get; set; }
    
        public List<Post> Posts { get; set; }
    }
    
    • Key: 主键
    • Required:不能为空
    • MinLength:字符串最小长度
    • MaxLength:字符串最大长度
    • StringLength:字符串最大长度
    • Timestamp:rowversion,时间戳列
    • ConcurrencyCheck 乐观并发检查列
    • Table 表名
    • Column 字段名
    • Index 索引
    • ForeignKey 外键
    • NotMapped 不映射数据库中的任何列
    • InverseProperty 指定导航属性和实体关系的对应,用于实体中有多个关系映射。
  • Fluent API

    通过 Fluent API 在 IEntityTypeConfiguration 实现类里面配置实体:

    using Microsoft.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore.Metadata.Builders;
    
    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
        public int Rating { get; set; }
    
        public List<Post> Posts { get; set; }
    }
    
    public class BlogConfiguration : IEntityTypeConfiguration<Blog>
    {
        public void Configure(EntityTypeBuilder<Blog> builder)
        {
            builder.HasKey(t => t.BlogId);
    
            builder.Property(t => t.Url).IsRequired().HasMaxLength(500);
        }
    }
    

    并在 Context 的 OnModelCreating 方法里面应用:

    public class BloggingContext : DbContext
    {
        public BloggingContext(DbContextOptions<BloggingContext> options)
            : base(options)
        {}
    
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
    
            modelBuilder.ApplyConfiguration(new BlogConfiguration());
        }
    }
    

    Fluent API 比数据注解有更高的优先级。

实体关系

  • 一对多关系

    Blog 和 Post 是一对多关系,在 PostConfiguration 里面添加如下配置:

    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
        public int Rating { get; set; }
    
        public List<Post> Posts { get; set; }
    }
    
    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
    
        public int BlogId { get; set; }
        public Blog Blog { get; set; }
    }
    
    public class PostConfiguration : IEntityTypeConfiguration<Post>
    {
        public void Configure(EntityTypeBuilder<Post> builder)
        {
            builder.HasOne<Blog>(p => p.Blog)
                .WithMany(b => b.Posts)
                .HasForeignKey(p => p.BlogId)
                .OnDelete(DeleteBehavior.Cascade);
        }
    }
    
  • 一对一关系

    创建一个实体类 PostExtension 做为 Post 的扩展表,它们之间是一对一关系。
    如果两个实体相互包括了对方的引用导航属性(本例中是 PostExtension ExtensionPost Post)和外键属性 (本例中是 PostExtension 中的 PostId),那 EF Core 会默认配置一对一关系的,当然也可以手动写语句(如注释的部分)。

    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
    
        public PostExtension Extension { get; set; }
    }
    
    public class PostExtension
    {
        public int PostId { get; set; }
        public string ExtensionField1 { get; set; }
    
        public Post Post { get; set; }
    }
    
    public class PostExtensionConfiguration : IEntityTypeConfiguration<PostExtension>
    {
        public PostExtensionConfiguration()
        {
    
        }
    
        public void Configure(EntityTypeBuilder<PostExtension> builder)
        {
            builder.HasKey(t => t.PostId);
    
            //builder.HasOne(e => e.Post)
            //    .WithOne(p => p.Extension)
            //    .HasForeignKey<PostExtension>(e => e.PostId)
            //    .OnDelete(DeleteBehavior.Cascade);
        }
    }
    
  • 多对多关系

    创建一个实体类 Tag, 和 Blog 是多对多关系。一个 Blog 可以有多个不同 Tag,同时一个 Tag 可以用多个 Blog。
    EF Core 中创建多对多关系必须要声明一个映射的关系实体,所以我们创建 BlogTag 实体,并在 BlogTagConfiguration 配置了多对多关系。

    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
        public int Rating { get; set; }
    
        public IList<BlogTag> BlogTags { get; set; }
    }
    
    public class Tag
    {
        public int TagId { get; set; }
        public string TagName { get; set; }
    
        public IList<BlogTag> BlogTags { get; set; }
    }
    
    public class BlogTag
    {
        public int BlogId { get; set; }
        public Blog Blog { get; set; }
    
        public int TagId { get; set; }
        public Tag Tag { get; set; }
    }
    
    public class BlogTagConfiguration : IEntityTypeConfiguration<BlogTag>
    {
        public void Configure(EntityTypeBuilder<BlogTag> builder)
        {
            builder.HasKey(bt => new { bt.BlogId, bt.TagId });
    
            builder.HasOne<Blog>(bt => bt.Blog)
                .WithMany(b => b.BlogTags)
                .HasForeignKey(bt => bt.BlogId);
    
            builder.HasOne<Tag>(bt => bt.Tag)
                .WithMany(t => t.BlogTags)
                .HasForeignKey(bt => bt.TagId);
        }
    }
    

种子数据

填充种子数据可以让我们在首次使用应用之前向数据库中插入一些初始化数据。有两种方法:

  • 通过实体类配置实现
    在配置实体的时候可以通过HasData方法预置数据,在执行Update-Database命令时候会写入数据库。

    public class BlogConfiguration : IEntityTypeConfiguration<Blog>
    {
        public void Configure(EntityTypeBuilder<Blog> builder)
        {
            //Data Seeding
            builder.HasData(new Blog { BlogId = 1, Url = "http://sample.com/1", Rating = 0 });
        }
    }
    
  • 统一配置
    创建一个统一配置 SeedData 类, 然后在 Program.cs 中的 Main 中调用它。

    public static class SeedData
    {
        public static void Initialize(IServiceProvider serviceProvider)
        {
            using (var context = new BloggingContext(
                serviceProvider.GetRequiredService<DbContextOptions<BloggingContext>>()))
            {
                if (context.Blogs.Any())
                    return; // DB has been seeded
    
                var blogs = new List<Blog>
                {
                    new Blog
                    {
                        Url = "http://sample.com/2",
                        Rating = 0
                    },
                    new Blog
                    {
                        Url = "http://sample.com/3",
                        Rating = 0
                    },
                    new Blog
                    {
                        Url = "http://sample.com/4",
                        Rating = 0
                    }
                };
    
                context.Blogs.AddRange(blogs);
                context.SaveChanges();
            }
        }
    }
    
    public class Program
    {
        public static void Main(string[] args)
        {
            //CreateWebHostBuilder(args).Build().Run();
            var host = CreateWebHostBuilder(args).Build();
    
            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;
                try
                {
                    SeedData.Initialize(services);
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "An error occurred seeding the DB.");
                }
            }
    
            host.Run();
        }
    
        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>();
    }
    

并发管理

数据库并发指的是多个进程或用户同时访问或更改数据库中的相同数据的情况。 并发控制指的是用于在发生并发更改时确保数据一致性的特定机制。

  • 乐观并发:无论何时从数据库请求数据,数据都会被读取并保存到应用内存中。数据库级别没有放置任何显式锁。数据操作会按照数据层接收到的顺序执行。
  • 悲观并发:无论何时从数据库请求数据,数据都会被读取,然后该数据上就会加锁,因此没有人能访问该数据。这会降低并发相关问题的机会,缺点是加锁是一个昂贵的操作,会降低整个应用程序的性能。

EF Core 默认支持乐观并发控制,这意味着它将允许多个进程或用户独立进行更改而不产生同步或锁定的开销。 在理想情况下,这些更改将不会相互影响,因此能够成功。 在最坏的情况下,两个或更多进程将尝试进行冲突更改,其中只有一个进程应该成功。

  • ConcurrencyCheck / IsConcurrencyToken
    ConcurrencyCheck 特性可以应用到领域类的属性中。当EF执行更新或删除操作时,EF Core 会将配置的列放在 where 条件语句中。执行这些语句后,EF Core 会读取受影响的行数。如果未影响任何行,将检测到并发冲突引发 DbUpdateConcurrencyException。

    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
    
        [ConcurrencyCheck]
        public int Rating { get; set; }
    }
    
    [HttpPut("{id}")]
    public async Task<IActionResult> PutBlog([FromRoute] int id, [FromBody] Blog blog)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
    
        var dbModel = await _context.Blogs.FindAsync(id);
        dbModel.Url = blog.Url;
        dbModel.Rating = blog.Rating;
    
        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            //todo: handle DbUpdateConcurrencyException
            throw ex;
        }
    
        return NoContent();
    }
    

    通过 SQL Server Profiler 查看生成的 SQL Update 语句。

    exec sp_executesql N'SET NOCOUNT ON;
    UPDATE [Blogs] SET [Rating] = @p0, [Url] = @p1
    WHERE [BlogId] = @p2 AND [Rating] = @p3;
    SELECT @@ROWCOUNT;
    
    ',N'@p2 int,@p0 int,@p3 int,@p1 nvarchar(500)',@p2=1,@p0=999,@p3=20,@p1=N'http://sample.com/1'
    
  • Timestamp / IsRowVersion
    TimeStamp特性可以应用到领域类中,只有一个字节数组的属性上面。每次插入或更新行时,由数据库生成一个新的值做为并发标记。

    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
        public int Rating { get; set; }
    
        [Timestamp]
        public byte[] Timestamp { get; set; }
    }
    

    通过 SQL Server Profiler 查看生成的 SQL Update 语句。

    exec sp_executesql N'SET NOCOUNT ON;
    UPDATE [Blogs] SET [Rating] = @p0
    WHERE [BlogId] = @p1 AND [Timestamp] = @p2;
    SELECT [Timestamp]
    FROM [Blogs]
    WHERE @@ROWCOUNT = 1 AND [BlogId] = @p1;
    
    ',N'@p1 int,@p0 int,@p2 varbinary(8)',@p1=1,@p0=8888,@p2=0x00000000000007D1
    

处理冲突的策略:

  • 忽略冲突并强制更新:这种策略是让所有的用户更改相同的数据集,然后所有的修改都会经过数据库,这就意味着数据库会显示最后一次更新的值。这种策略会导致潜在的数据丢失,因为许多用户的更改都丢失了,只有最后一个用户的更改是可见的。
  • 部分更新:在这种情况中,我们也允许所有的更改,但是不会更新完整的行,只有特定用户拥有的列更新了。这就意味着,如果两个用户更新相同的记录但却不同的列,那么这两个更新都会成功,而且来自这两个用户的更改都是可见的。
  • 拒绝更改:当一个用户尝试更新一个记录时,但是该记录自从他读取之后已经被别人修改了,此时告诉该用户不允许更新该数据,因为数据已经被某人更新了。
  • 警告询问用户:当一个用户尝试更新一个记录时,但是该记录自从他读取之后已经被别人修改了,这时应用程序就会警告该用户该数据已经被某人更改了,然后询问他是否仍然要重写该数据还是首先检查已经更新的数据。

执行 SQL 语句和存储过程

EF Core 使用以下方法执行 SQL 语句和存储过程:

  • DbSet.FromSql()

    DbSet<TEntity>.FromSql() 返回值为IQueryable,可以与Linq扩展方法配合使用。注意:

    1. SQL 查询必须返回实体或查询类型的所有属性的数据
    2. 结果集中的列名必须与属性映射到的列名称匹配。
    3. SQL 查询不能包含相关数据。 但是可以使用 Include 运算符返回相关数据。
    4. 不要使用 TOP 100 PERCENT 或 ORDER BY 等子句。可以通过 Linq 在代码里面编写。

    基本 SQL 查询

    var blogs = _context.Blogs.FromSql($"select * from Blogs").ToList();
    

    带有参数的查询:

    var blog = _context.Blogs.FromSql($"select * from Blogs where BlogId = {id}");
    

    使用 LINQ:

    var blogs = _context.Blogs.FromSql($"select * from Blogs")
                .OrderByDescending(r => r.Rating)
                .Take(2)
                .ToList();
    

    通过 SQL Server Profiler 查看 SQL 语句,可以发现 EF Core 是把手工写的 SQL 语句和 Linq 合并生成了一条语句:

    exec sp_executesql N'SELECT TOP(@__p_1) [r].[BlogId], [r].[Rating], [r].[Timestamp], [r].[Url]
    FROM (
        select * from Blogs
    ) AS [r]
    ORDER BY [r].[Rating] DESC',N'@__p_1 int',@__p_1=2
    

    使用 Include 包括相关数据

    var blogs = _context.Blogs.FromSql($"select * from Blogs").Include(r => r.Posts).ToList();
    

    通过 SQL Server Profiler 查看 SQL 语句:

    SELECT [b].[BlogId], [b].[Rating], [b].[Timestamp], [b].[Url]
    FROM (
        select * from Blogs
    ) AS [b]
    ORDER BY [b].[BlogId]
    
    SELECT [b.Posts].[PostId], [b.Posts].[BlogId], [b.Posts].[Content], [b.Posts].[Title]
    FROM [Posts] AS [b.Posts]
    INNER JOIN (
        SELECT [b0].[BlogId]
        FROM (
            select * from Blogs
        ) AS [b0]
    ) AS [t] ON [b.Posts].[BlogId] = [t].[BlogId]
    ORDER BY [t].[BlogId]
    
  • DbContext.Database.ExecuteSqlCommand()

    ExecuteSqlCommand方法返回一个整数,表示执行的SQL语句影响的行数。有效的操作是 INSERT、UPDATE 和 DELETE,不能用于返回实体。

    测试一下 INSERT:

    int affectRows = _context.Database.ExecuteSqlCommand($"Insert into Blogs([Url],[Rating])Values({blog.Url}, {blog.Rating})");
    

    通过 SQL Server Profiler 查看 SQL 语句:

    exec sp_executesql N'Insert into Blogs([Url],[Rating])Values(@p0, @p1)',N'@p0 nvarchar(4000),@p1 int',@p0=N'testurl',@p1=3
    

延迟加载和预先加载

EF Core 通过在模型中使用导航属性来加载相关实体。 有三种常见模式可用于加载相关数据。

  • 预先加载
    表示从数据库中加载相关数据,作为初始查询的一部分。使用 Include方法实现预加载,使用 ThenInclude 实现多级预加载。

    var blogs = _context.Blogs.Include(r => r.Posts).ToList();
    

    当需要 JSON 序列化 blogs 对象时候,ASP.NET Core 自带的序列化库 Newtonsoft.Json 可能会抛出自引用循环异常。请在 Startup 的 ConfigureServices 方法中配置以下代码解决。

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc()
            .AddJsonOptions(options => options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore);
    }
    
  • 显式加载
    表示稍后从数据库中显式加载相关数据。

    var blog = await _context.Blogs.FindAsync(id);
    
    _context.Entry(blog)
        .Collection(b => b.Posts)
        .Load();
    
  • 延迟加载
    表示在访问导航属性时,才从数据库中加载相关数据。在 EF Core 2.1 中才引入此功能。

    1. Nuget 安装 Microsoft.EntityFrameworkCore.Proxies

    2. 调用 UseLazyLoadingProxies 来启用延迟加载。

      services.AddDbContext<BloggingContext>(option => option.UseLazyLoadingProxies().UseSqlServer(connectionString));
      
    3. 导航属性添加 virtual 修饰符。

      public class Blog
      {
          public int BlogId { get; set; }
          public string Url { get; set; }
          public int Rating { get; set; }
      
          public virtual IList<Post> Posts { get; set; }
      }
      
      public class Post
      {
          public int PostId { get; set; }
          public string Title { get; set; }
          public string Content { get; set; }
      
          public int BlogId { get; set; }
          public virtual Blog Blog { get; set; }
      }
      
    4. 测试,当代码执行到var posts = blog.Posts时候,会去数据库里面查询Posts记录。

      var blog = await _context.Blogs.FindAsync(id);
      var posts = blog.Posts;
      

      尽量避免在循环时候使用延迟加载,会导致每次循环都去访问数据库。

IQueryable 和 IEnumerable

直接通过一个实例测试一下:

var testIQueryable = _context.Blogs.Where(r => r.Rating > 10);
var testIEnumerable = _context.Blogs.AsEnumerable().Where(r => r.Rating > 10);

var testIQueryableList = testIQueryable.ToList();
var testIEnumerableList = testIEnumerable.ToList();

查看生产的 SQL 语句

  • IQueryable

    SELECT [r].[BlogId], [r].[Rating], [r].[Timestamp], [r].[Url]
    FROM [Blogs] AS [r]
    WHERE [r].[Rating] > 10
    
  • IEnumerable

    SELECT [b].[BlogId], [b].[Rating], [b].[Timestamp], [b].[Url]
    FROM [Blogs] AS [b]
    

IQueryable 是将 Linq 表达式翻译成 T-SQL 语句之后再向 SQL 服务器发送命令.
IEnumerable 是在调用自己的 Linq 方法之前先从 SQL 服务器取到数据并加载到本地内存中。

生成迁移 SQL 脚本

EF Core 将迁移更新到生产环境可以使用 Script-Migration 命令生成sql脚本,然后到生产数据库执行.

此命令有几个选项。

  • -From <String> 迁移应是运行该脚本前应用到数据库的最后一个迁移。 如果未应用任何迁移,请指定 0(默认值)。
  • -To <String> 迁移是运行该脚本后应用到数据库的最后一个迁移。 它默认为项目中的最后一个迁移。
  • -Idempotent 此脚本仅会应用尚未应用到数据库的迁移。 如果不确知应用到数据库的最后一个迁移或需要部署到多个可能分别处于不同迁移的数据库,此脚本非常有用。

待补充...

SQL 监视工具

有几种方法可以监视 EF Core 自动生成的 SQL 语句:

  • 内置日志

  • 数据库监视工具

  • Miniprofiler

  • 内置日志: 在调试模式下,EF Core 会使用 ASP.NET Core 的内置日志记录功能把生成的 SQL 语句显示在输出窗口,大概如下:

    Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (50ms) [Parameters=[@__get_Item_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
    SELECT TOP(1) [e].[Id], [e].[Rating], [e].[Url]
    FROM [Blogs] AS [e]
    WHERE [e].[Id] = @__get_Item_0
    

    如果想查看敏感数据比如@__get_Item_0='?',请在 Context 类的 OnConfiguring 方法里面配置optionsBuilder.EnableSensitiveDataLogging();

  • 数据库监视工具: 也可以通过数据库的监视工具,比如用于监视 MS SQL 的工具 SQL Server Profiler 查看执行的 SQL 语句,大概如下:

    exec sp_executesql N'SELECT TOP(1) [e].[Id], [e].[Rating], [e].[Url]
    FROM [Blogs] AS [e]
    WHERE [e].[Id] = @__get_Item_0',N'@__get_Item_0 int',@__get_Item_0=1
    
  • MiniprofilerMiniProfiler/dotnet是一款简单而有效的性能分析的轻量级程序,可以监控页面,也可以监控 EF Core 执行的 SQL 语句。

    MiniProfiler 一般用于 MVC 项目,但也可以结合 Swagger 用于 Web API项目。Swagger 的安装和使用在本篇不做讨论,详细请参考Swashbuckle.AspNetCore

    1. Nuget 安装 MiniProfiler 引用

      Install-Package MiniProfiler.AspNetCore.Mvc
      Install-Package MiniProfiler.EntityFrameworkCore
      
    2. 修改 SwaggerUI/index.html 页面: 在项目下面新建一个文件 SwaggerIndex.html 并复制以下代码,设置编译为 Embedded resource

      <script async="async" id="mini-profiler" src="/profiler/includes.min.js?v=4.0.138+gcc91adf599" data-version="4.0.138+gcc91adf599" data-path="/profiler/" data-current-id="4ec7c742-49d4-4eaf-8281-3c1e0efa748a" data-ids="" data-position="Left" data-authorized="true" data-max-traces="15" data-toggle-shortcut="Alt+P" data-trivial-milliseconds="2.0" data-ignored-duplicate-execute-types="Open,OpenAsync,Close,CloseAsync"></script>
      
      <!-- HTML for static distribution bundle build -->
      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>%(DocumentTitle)</title>
          <link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700" rel="stylesheet">
          <link rel="stylesheet" type="text/css" href="./swagger-ui.css">
          <link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
          <link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
          <style>
              html {
                  box-sizing: border-box;
                  overflow: -moz-scrollbars-vertical;
                  overflow-y: scroll;
              }
      
              *,
              *:before,
              *:after {
                  box-sizing: inherit;
              }
      
              body {
                  margin: 0;
                  background: #fafafa;
              }
          </style>
          %(HeadContent)
      </head>
      
      <body>
      
          <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0">
              <defs>
                  <symbol viewBox="0 0 20 20" id="unlocked">
                      <path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path>
                  </symbol>
      
                  <symbol viewBox="0 0 20 20" id="locked">
                      <path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z" />
                  </symbol>
      
                  <symbol viewBox="0 0 20 20" id="close">
                      <path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z" />
                  </symbol>
      
                  <symbol viewBox="0 0 20 20" id="large-arrow">
                      <path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z" />
                  </symbol>
      
                  <symbol viewBox="0 0 20 20" id="large-arrow-down">
                      <path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z" />
                  </symbol>
      
      
                  <symbol viewBox="0 0 24 24" id="jump-to">
                      <path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z" />
                  </symbol>
      
                  <symbol viewBox="0 0 24 24" id="expand">
                      <path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z" />
                  </symbol>
      
              </defs>
          </svg>
      
          <div id="swagger-ui"></div>
      
          <!-- Workaround for https://github.com/swagger-api/swagger-editor/issues/1371 -->
          <script>
              if (window.navigator.userAgent.indexOf("Edge") > -1) {
                  console.log("Removing native Edge fetch in favor of swagger-ui's polyfill")
                  window.fetch = undefined;
              }
          </script>
      
          <script src="./swagger-ui-bundle.js"></script>
          <script src="./swagger-ui-standalone-preset.js"></script>
          <script>
              window.onload = function () {
                  var configObject = JSON.parse('%(ConfigObject)');
                  var oauthConfigObject = JSON.parse('%(OAuthConfigObject)');
                  // Apply mandatory parameters
                  configObject.dom_id = "#swagger-ui";
                  configObject.presets = [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset];
                  configObject.layout = "StandaloneLayout";
                  // If oauth2RedirectUrl isn't specified, use the built-in default
                  if (!configObject.hasOwnProperty("oauth2RedirectUrl"))
                      configObject.oauth2RedirectUrl = window.location.href.replace("index.html", "oauth2-redirect.html");
                  // Build a system
                  const ui = SwaggerUIBundle(configObject);
                  // Apply OAuth config
                  ui.initOAuth(oauthConfigObject);
              }
          </script>
      </body>
      
      </html>
      
      
        <ItemGroup>
          <EmbeddedResource Include="SwaggerIndex.html" />
        </ItemGroup>
      
    3. 在 Startup 中配置 MiniProfiler: 在 ConfigureServices 里面添加services.AddMiniProfiler().AddEntityFramework(), 在 Configure 里面添加app.UseMiniProfiler(); 并配置 Swagger 的 IndexStream.

      public void ConfigureServices(IServiceCollection services)
      {
          services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
      
          //Swagger
          services.AddSwaggerGen(options =>
          {
              options.DescribeAllEnumsAsStrings();
              options.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info
              {
                  Title = "API Docs",
                  Version = "v1",
              });
          });
          
          //Profiling
          services.AddMiniProfiler(options =>
              options.RouteBasePath = "/profiler"
          ).AddEntityFramework();
      }
      
      // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
      public void Configure(IApplicationBuilder app, IHostingEnvironment env)
      {
          if (env.IsDevelopment())
          {
              app.UseDeveloperExceptionPage();
      
              // profiling, url to see last profile check: http://localhost:56775/profiler/results
              app.UseMiniProfiler();
          }
      
          app.UseSwagger();
      
          app.UseSwagger().UseSwaggerUI(c =>
          {
              c.SwaggerEndpoint("/swagger/v1/swagger.json", "API V1");
              // index.html customizable downloadable here: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/src/Swashbuckle.AspNetCore.SwaggerUI/index.html
              // this custom html has miniprofiler integration
              c.IndexStream = () => GetType().GetTypeInfo().Assembly.GetManifestResourceStream("ORMDemo.EFWithRepository.SwaggerIndex.html");
          });
      
          app.UseMvc();
      }
      
    4. 运行项目,MiniProfiler 监控页面应该已经出现在 Swagger UI 页面的左上角了。

仓储模式和工作单元模式

仓储模式(Repository)是用来解耦的(通过在数据访问层和业务逻辑层之间创建抽象层)。
但仓储只关注于单一聚合的持久化,而业务用例却常常会涉及多个聚合的更改,为了确保业务用例的一致型,我们需要引入工作单元来管理多个聚合。

工作单元模式(unit of work)的作用就是在业务用例的操作中跟踪对象的所有更改(增加、删除和更新),并将所有更改的对象保存在其维护的列表中。在业务用例的终点,通过事务,一次性提交所有更改,以确保数据的完整性和有效性。总而言之,UOW协调这些对象的持久化及并发问题。

在 EF Core 中 DBContext 已经实现了工作单元模式,同时也比较容易更换统一的数据存储介质(通过支持的数据库驱动)。那么还有没有必要在 EF Core 上面再封装一层实现自己的仓储和工作单元呢?

  • 如果项目比较简单,业务逻辑并不复杂。特别是在实现一些微服务的时候,每个项目(服务)都只负责一部分小的并且功能内聚的业务。这个时候或许保持代码简单最好,没有必要过度设计。
  • 当然,如果项目比较复杂,没有采用微服务架构而是多个模块都在一起的单体架构,可能同时需要多种数据存储介质和途径,用到了多种的数据访问和持久化技术,那么可能就需要好好设计一个适合项目的仓储和工作单元模式了。

下面实现一个简单的仓储和工作单元模式:

  • 定义实体基类

    public abstract class BaseEntity<TKey>
    {
        public virtual TKey Id { get; set; }
    }
    
  • 定义仓储基类

    public interface IRepository<TDbContext, TEntity, TKey> where TEntity : BaseEntity<TKey> where TDbContext : DbContext
    {
        Task<TEntity> GetByKeyAsync(TKey id);
    
        Task<IList<TEntity>> GetAsync(
            Expression<Func<TEntity, bool>> predicate = null,
            Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
            List<Expression<Func<TEntity, object>>> includes = null);
    
        Task<TEntity> AddAsync(TEntity entity);
    
        TEntity Update(TEntity entity);
    
        void Delete(TKey id);
    
        void Delete(TEntity entity);
    }
    
    public class EFRepository<TDbContext, TEntity, TKey> : IRepository<TDbContext, TEntity, TKey> where TEntity : BaseEntity<TKey> where TDbContext : DbContext
    {
        protected readonly TDbContext _context;
        protected readonly DbSet<TEntity> dbSet;
    
        public EFRepository(TDbContext context)
        {
            this._context = context;
            this.dbSet = context.Set<TEntity>();
        }
    
        public virtual async Task<TEntity> GetByKeyAsync(TKey id)
        {
            return await dbSet.FindAsync(id);
        }
    
        public virtual async Task<IList<TEntity>> GetAsync(
            Expression<Func<TEntity, bool>> predicate = null,
            Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
            List<Expression<Func<TEntity, object>>> includes = null)
        {
            IQueryable<TEntity> query = dbSet;
    
            if (includes != null)
            {
                query = includes.Aggregate(query, (current, include) => current.Include(include));
            }
            if (orderBy != null)
            {
                query = orderBy(query);
            }
            if (predicate != null)
            {
                query = query.Where(predicate);
            }
    
            return await query.ToListAsync();
        }
    
        public virtual async Task<TEntity> AddAsync(TEntity entity)
        {
            var result = await dbSet.AddAsync(entity);
            return result.Entity;
        }
    
        public virtual TEntity Update(TEntity entity)
        {
            AttachIfNot(entity);
            this._context.Entry(entity).State = EntityState.Modified;
            return entity;
        }
    
        public virtual void Delete(TKey id)
        {
            TEntity entity = dbSet.Find(id);
            Delete(entity);
        }
    
        public virtual void Delete(TEntity entity)
        {
            AttachIfNot(entity);
            dbSet.Remove(entity);
        }
    
        protected virtual void AttachIfNot(TEntity entity)
        {
            if (this._context.Entry(entity).State == EntityState.Detached)
            {
                dbSet.Attach(entity);
            }
        }
    }
    

    可以根据需求扩展更多的方法。

  • 定义工作单元基类

    public interface IUnitOfWork<TDbContext> where TDbContext : DbContext
    {
        Task<int> SaveChangesAsync();
    }
    
    public class UnitOfWork<TDbContext> : IUnitOfWork<TDbContext> where TDbContext : DbContext
    {
        private readonly TDbContext _dbContext;
    
        public UnitOfWork(TDbContext context)
        {
            _dbContext = context ?? throw new ArgumentNullException(nameof(context));
        }
    
        public async Task<int> SaveChangesAsync()
        {
            return await _dbContext.SaveChangesAsync();
        }
    }
    
  • 定义 BloggingContext 并定义基于 BloggingContext 的仓储基类和工作单元基类

    public class BloggingContext : DbContext
    {
        public BloggingContext(DbContextOptions<BloggingContext> options)
            : base(options)
        {
        }
    
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
    
            modelBuilder.ApplyConfiguration(new BlogConfiguration());
            modelBuilder.ApplyConfiguration(new PostConfiguration());
        }
    }
    
    public interface IBlogggingRepositoryBase<TEntity, TKey> : IRepository<BloggingContext, TEntity, TKey> where TEntity : BaseEntity<TKey>
    {
    }
    
    public class BlogggingRepositoryBase<TEntity, TKey> : EFRepository<BloggingContext, TEntity, TKey>, IBlogggingRepositoryBase<TEntity, TKey> where TEntity : BaseEntity<TKey>
    {
        public BlogggingRepositoryBase(BloggingContext dbContext) : base(dbContext)
        {
        }
    }
    
    public class BloggingUnitOfWork : UnitOfWork<BloggingContext>
    {
        public BloggingUnitOfWork(BloggingContext dbContext) : base(dbContext)
        {
        }
    }
    
  • 在 Startup 的 ConfigureServices 里面注册相关服务

    public void ConfigureServices(IServiceCollection services)
    {
        var connectionString = @"Server=.;Database=BloggingWithRepository;Trusted_Connection=True;";
        services.AddDbContext<BloggingContext>(option => option.UseSqlServer(connectionString));
        services.AddScoped<BloggingUnitOfWork>();
        services.AddTransient(typeof(IBlogggingRepositoryBase<,>), typeof(BlogggingRepositoryBase<,>));
    }
    

    这里 BloggingContext 和 UnitOfWork 的生命周期为 Scoped。

  • 在 Controller 里面调用并测试

    public class BlogsController : ControllerBase
    {
        private readonly IBlogggingRepositoryBase<Blog, int> _blogRepository;
        private readonly IBlogggingRepositoryBase<Post, int> _postRepository;
        private readonly BloggingUnitOfWork _unitOfWork;
    
        public BlogsController(IBlogggingRepositoryBase<Blog, int> blogRepository, IBlogggingRepositoryBase<Post, int> postRepository, BloggingUnitOfWork unitOfWork)
        {
            _blogRepository = blogRepository;
            _postRepository = postRepository;
            _unitOfWork = unitOfWork;
        }
        
        [HttpGet]
        public async Task<IActionResult> GetBlogs()
        {
            var blogs = await _blogRepository.GetAsync();
            return Ok(blogs);
        }
        
        [HttpPost]
        public async Task<IActionResult> PostBlog([FromBody] Blog blog)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            
            //await _blogRepository.AddAsync(new Blog { Url = "http://sample.com/4", Rating = 0 });
            //await _postRepository.AddAsync(new Post { Title = "Title4", Content = "BlogId_1 Post_3", BlogId = 1 });
    
            var result = await _blogRepository.AddAsync(blog);
            await _unitOfWork.SaveChangesAsync();
    
            return CreatedAtAction("GetBlog", new { id = blog.Id }, blog);
        }
    }
    

使用 EF Core(DB First)

EF Core 的 DB First 是通过 Scaffold-DbContext 命令根据已经存在的数据库创建实体类和context类。

可以通过PM> get-help scaffold-dbcontext –detailed查看命令的详细参数

Scaffold-DbContext [-Connection] <String> [-Provider] <String> [-OutputDir <String>] [-ContextDir <String>] [-Context <String>] [-Schemas <String[]>] [-Tables <String[]>] [-DataAnnotations] [-UseDatabaseNames] [-Force] 
[-Project <String>] [-StartupProject <String>] [<CommonParameters>]

使用之前创建的 blogging 数据库简单的测试一下:

  1. 新建一个项目,然后通过 Nuget 安装 EF Core 引用

    Install-Package Microsoft.EntityFrameworkCore.SqlServer
    Install-Package Microsoft.EntityFrameworkCore.Tools
    
  2. 执行命令创建实体

    Scaffold-DbContext "Server=CD02SZV3600503\SQLEXPRESS;Database=BloggingWithRepository;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models
    

执行成功后可以看到在 Models 文件夹下面创建的实体类和 Context 类。

源代码

Github

参考

posted @ 2018-09-21 15:15  车骑  阅读(1551)  评论(4编辑  收藏  举报