2020/01/29, ASP.NET Core 3.1, VS2019, EntityFrameworkCore 3.1.1, Microsoft.Extensions.Logging.Console 3.1.1, Microsoft.Extensions.Logging.Debug 3.1.1
摘要:基於ASP.NET Core 3.1 WebApi搭建后端多層網站架構【5-網站數據庫實體設計及映射配置】
網站數據庫實體設計,使用EntityFrameworkCore 3.1 FluentAPI映射配置實體,網站啟動時創建數據庫並添加種子數據,開發調試時可以看到執行的具體sql語句
本章節介紹后台管理的網站數據庫實體設計,使用FluentAPI方式配置數據庫字段映射,網站啟動時創建數據庫並添加種子數據
需求分析
首先要實現的功能有用戶登錄、角色管理、日志記錄
大概有四張表:用戶表、密碼表、角色表、日志表
日志表:
用戶表:
密碼表:
角色表:
好像博客園md不支持表格功能?所以只能截圖展示,excel表格上傳至項目docs文件夾中
字段設計說明
- 日志表主鍵Id是數據庫自增的,也就是在向數據庫插入日志時,不用管Id,往里寫入就行
- 用戶表、角色表的Id都是long類型的,也就是使用雪花算法生成的Id
- 密碼表的主鍵是Account,UserId是用戶表外鍵
- 用戶表和角色表擁有StatusCode、Creator、CreateTime、Modifier、ModifyTime,標明該記錄的狀態、創建時間等信息
創建實體類
在MS.Entities
類庫中添加Core文件夾,在Core文件夾中添加IEntity.cs
類:
using System;
namespace MS.Entities.Core
{
//沒有Id主鍵的實體繼承這個
public interface IEntity
{
}
//有Id主鍵的實體繼承這個
public abstract class BaseEntity : IEntity
{
public long Id { get; set; }
public StatusCode StatusCode { get; set; }
public long? Creator { get; set; }
public DateTime? CreateTime { get; set; }
public long? Modifier { get; set; }
public DateTime? ModifyTime { get; set; }
}
}
在Core中新建StatusCode.cs
枚舉:
using System.ComponentModel;
namespace MS.Entities.Core
{
public enum StatusCode
{
[Description("已刪除")]
Deleted = -1,//軟刪除,已刪除的無法恢復,無法看見,暫未使用
[Description("生效")]
Enable = 0,
[Description("失效")]
Disable = 1//失效的還可以改為生效
}
}
日志表
在MS.Entities
類庫中添加Logrecord.cs
類:
using MS.Entities.Core;
using System;
namespace MS.Entities
{
public class Logrecord : IEntity
{
public int Id { get; set; }
public DateTime LogDate { get; set; }
public string LogLevel { get; set; }
public string Logger { get; set; }
public string Message { get; set; }
public string Exception { get; set; }
public string MachineName { get; set; }
public string MachineIp { get; set; }
public string NetRequestMethod { get; set; }
public string NetRequestUrl { get; set; }
public string NetUserIsauthenticated { get; set; }
public string NetUserAuthtype { get; set; }
public string NetUserIdentity { get; set; }
}
}
角色表
在MS.Entities
類庫中添加Role.cs
類:
using MS.Entities.Core;
namespace MS.Entities
{
public class Role : BaseEntity
{
public string Name { get; set; }
public string DisplayName { get; set; }
public string Remark { get; set; }
}
}
用戶表
在MS.Entities
類庫中添加User.cs
類:
using MS.Entities.Core;
namespace MS.Entities
{
public class User : BaseEntity
{
public string Account { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public long RoleId { get; set; }
public Role Role { get; set; }
}
}
密碼表
在MS.Entities
類庫中添加UserLogin.cs
類:
using MS.Entities.Core;
using System;
namespace MS.Entities
{
public class UserLogin : IEntity
{
public long UserId { get; set; }
public string Account { get; set; }
public string HashedPassword { get; set; }
public DateTime? LastLoginTime { get; set; }
public int AccessFailedCount { get; set; }
public bool IsLocked { get; set; }
public DateTime? LockedTime { get; set; }
public User User { get; set; }
}
}
至此,實體類都已完成設計
項目完成后,如下圖
創建映射配置
向MS.DbContexts
類庫添加包引用:
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.1" />
</ItemGroup>
這兩個包給DbContext擴展日志記錄,可以實現查看EFCore生成的sql語句,具體使用方法后文會提到
在MS.DbContexts
類庫中引用MS.Entities
、MS.UnitOfWork
類庫
在MS.DbContexts
類庫中添加Mappings文件夾,在該文件夾中添加 LogrecordMap.cs
、RoleMap.cs
、UserLoginMap.cs
、UserMap.cs
LogrecordMap.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using MS.Entities;
namespace MS.DbContexts
{
public class LogrecordMap : IEntityTypeConfiguration<Logrecord>
{
public void Configure(EntityTypeBuilder<Logrecord> builder)
{
builder.ToTable("TblLogrecords");
builder.HasKey(c => c.Id);//自增主鍵
builder.Property(c => c.LogDate).IsRequired();
builder.Property(u => u.LogLevel).IsRequired().HasMaxLength(50);
builder.Property(u => u.Logger).IsRequired().HasMaxLength(256);
builder.Property(u => u.Message);
builder.Property(u => u.Exception);
builder.Property(u => u.MachineName).HasMaxLength(50);
builder.Property(u => u.MachineIp).HasMaxLength(50);
builder.Property(u => u.NetRequestMethod).HasMaxLength(10);
builder.Property(u => u.NetRequestUrl).HasMaxLength(500);
builder.Property(u => u.NetUserIsauthenticated).HasMaxLength(10);
builder.Property(u => u.NetUserAuthtype).HasMaxLength(50);
builder.Property(u => u.NetUserIdentity).HasMaxLength(50);
}
}
}
RoleMap.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using MS.Entities;
namespace MS.DbContexts
{
public class RoleMap : IEntityTypeConfiguration<Role>
{
public void Configure(EntityTypeBuilder<Role> builder)
{
builder.ToTable("TblRoles");
builder.HasKey(c => c.Id);
builder.Property(c => c.Id).ValueGeneratedNever();
builder.HasIndex(c => c.Name).IsUnique();//指定索引,不能重復
builder.Property(c => c.Name).IsRequired().HasMaxLength(16);
builder.Property(c => c.DisplayName).IsRequired().HasMaxLength(50);
builder.Property(c => c.Remark).HasMaxLength(4000);
builder.Property(c => c.Creator).IsRequired();
builder.Property(c => c.CreateTime).IsRequired();
builder.Property(c => c.Modifier);
builder.Property(c => c.ModifyTime);
//builder.HasQueryFilter(b => b.StatusCode != StatusCode.Deleted);//默認不查詢軟刪除數據
}
}
}
UserLoginMap.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using MS.Entities;
namespace MS.DbContexts
{
public class UserLoginMap : IEntityTypeConfiguration<UserLogin>
{
public void Configure(EntityTypeBuilder<UserLogin> builder)
{
builder.ToTable("TblUserLogins");
builder.HasKey(c => c.Account);
//builder.Property(c => c.UserId).ValueGeneratedNever();
builder.Property(c => c.Account).IsRequired().HasMaxLength(20);
builder.Property(c => c.HashedPassword).IsRequired().HasMaxLength(256);
builder.Property(c => c.LastLoginTime);
builder.Property(c => c.AccessFailedCount).IsRequired().HasDefaultValue(0);
builder.Property(c => c.IsLocked).IsRequired();
builder.Property(c => c.LockedTime);
builder.HasOne(c => c.User);
}
}
}
UserMap.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using MS.Entities;
using MS.Entities.Core;
namespace MS.DbContexts
{
public class UserMap : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.ToTable("TblUsers");
builder.HasKey(c => c.Id);
builder.Property(c => c.Id).ValueGeneratedNever();
builder.HasIndex(c => c.Account).IsUnique();//指定索引
builder.Property(c => c.Account).IsRequired().HasMaxLength(16);
builder.Property(c => c.Name).IsRequired().HasMaxLength(50);
builder.Property(c => c.Email).HasMaxLength(100);
builder.Property(c => c.Phone).HasMaxLength(25);
builder.Property(c => c.RoleId).IsRequired();
builder.Property(c => c.StatusCode).IsRequired().HasDefaultValue(StatusCode.Enable);
builder.Property(c => c.Creator).IsRequired();
builder.Property(c => c.CreateTime).IsRequired();
builder.Property(c => c.Modifier);
builder.Property(c => c.ModifyTime);
builder.HasOne(c => c.Role);
//builder.HasQueryFilter(b => b.StatusCode != StatusCode.Deleted);//默認不查詢軟刪除數據
}
}
}
至此映射配置完成
說明
- User和Role映射中注釋掉了HasQueryFilter全局過濾查詢,如需要可自行開啟
- LogrecordMap中Id僅配置主鍵,所以默認是數據庫自增主鍵
- RoleMap、UserMap中Id設為ValueGeneratedNever,不自動生成值,我們使用雪花算法生成Id賦值
- UserMap中配置了HasOne(Role),表明關聯性,所以RoleId能自動映射為Role表的Id外鍵,UserLoginMap中的UserId也是如此
- UserMap中手動顯式指定了表名為TblUsers,加"Tbl"前綴是為了避免和數據庫默認關鍵字重復
建立DbContext上下文
在MS.DbContexts
類庫中添加MSDbContext.cs
類:
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace MS.DbContexts
{
public class MSDbContext : DbContext
{
//Add-Migration InitialCreate
//Update-Database InitialCreate
public MSDbContext(DbContextOptions<MSDbContext> options)
: base(options)
{
}
//此處用微軟原生的控制台日志記錄,如果使用NLog很可能數據庫還沒創建,造成記錄日志到數據庫性能下降(一直在嘗試連接數據庫,但是數據庫還沒創建)
//此處使用靜態實例,這樣不會為每個上下文實例創建新的 ILoggerFactory 實例,這一點非常重要。 否則會導致內存泄漏和性能下降。
//此處使用了Debug和console兩種日志輸出,會輸出到控制台和調試窗口
public static readonly ILoggerFactory MyLoggerFactory = LoggerFactory.Create(builder => builder.AddDebug().AddConsole());
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.UseLoggerFactory(MyLoggerFactory);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new LogrecordMap());
modelBuilder.ApplyConfiguration(new RoleMap());
modelBuilder.ApplyConfiguration(new UserLoginMap());
modelBuilder.ApplyConfiguration(new UserMap());
base.OnModelCreating(modelBuilder);
}
}
}
說明:
- 使用了微軟原生的控制台日志記錄,如果使用NLog很可能數據庫還沒創建,造成記錄日志到數據庫性能下降(一直在嘗試連接數據庫,但是數據庫還沒創建)
- 使用靜態實例,這樣不會為每個上下文實例創建新的 ILoggerFactory 實例,這一點非常重要。 否則會導致內存泄漏和性能下降。
- 使用了Debug和console兩種日志輸出,會輸出到控制台和調試窗口
至此,數據訪問層創建完畢,項目完成后如下圖所示
創建數據種子
目前我所知道的數據庫的創建有三種(生成sql語句單獨執行創建暫不討論):
- 先創建遷移文件,然后在代碼中自動遷移
- 使用.NET Core CLI命令創建數據庫
- 在代碼中直接創建數據庫
一、三兩種方法的差別我在EFCore自動遷移中寫過,第一種方法有個缺點是如果創建遷移時使用MySQL數據庫,編譯好代碼后,部署的環境必須是同樣的數據庫,而第三種方法沒有這個問題。
第二種方法需要使用到CLI命令工具單獨執行,所以我沒有考慮
我選擇直接創建,項目啟動時,檢查數據庫是否存在,如果不存在則創建,創建成功后開始寫入種子數據。
添加包引用
向MS.WebApi
應用程序中添加MySQL包引用,如果你使用SQL server,安裝Microsoft.EntityFrameworkCore.SqlServer
包即可:
<ItemGroup>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.1" />
</ItemGroup>
我寫本章節時,還是3.1.0版本,但是寫到第8.1章的時候升級了3.1.1,本文改成了3.1.1,代碼中8.1之后的所有分支都改成了最新版本,但是在此之前的分支依然是3.1.0沒有去做更新改動了(其實用起來區別也不大)
添加數據種子方法
在MS.WebApi
應用程序中添加Initialize文件夾,把自帶的Startup.cs
類移至Initialize文件夾中
在Initialize文件夾新建DBSeed.cs
類:
using MS.Common.Security;
using MS.DbContexts;
using MS.Entities;
using MS.Entities.Core;
using MS.UnitOfWork;
using System;
namespace MS.WebApi
{
public static class DBSeed
{
/// <summary>
/// 數據初始化
/// </summary>
/// <param name="unitOfWork"></param>
/// <returns>返回是否創建了數據庫(非遷移)</returns>
public static bool Initialize(IUnitOfWork<MSDbContext> unitOfWork)
{
bool isCreateDb = false;
//直接自動執行遷移,如果它創建了數據庫,則返回true
if (unitOfWork.DbContext.Database.EnsureCreated())
{
isCreateDb = true;
//打印log-創建數據庫及初始化期初數據
long rootUserId = 1219490056771866624;
#region 角色、用戶、登錄
Role rootRole = new Role
{
Id = 1219490056771866625,
Name = "SuperAdmin",
DisplayName = "超級管理員",
Remark = "系統內置超級管理員",
Creator = rootUserId,
CreateTime = DateTime.Now
};
User rootUser = new User
{
Id = rootUserId,
Account = "admin",
Name = "admin",
RoleId = rootRole.Id,
StatusCode = StatusCode.Enable,
Creator = rootUserId,
CreateTime = DateTime.Now,
};
unitOfWork.GetRepository<Role>().Insert(rootRole);
unitOfWork.GetRepository<User>().Insert(rootUser);
unitOfWork.GetRepository<UserLogin>().Insert(new UserLogin
{
UserId = rootUserId,
Account = rootUser.Account,
HashedPassword = Crypto.HashPassword(rootUser.Account),//默認密碼同賬號名
IsLocked = false
});
unitOfWork.SaveChanges();
#endregion
}
return isCreateDb;
}
}
}
上面的DBSeed中:
- EnsureCreated方法確保創建了數據庫(如果數據庫不存在則創建並返回true,存在則返回false)
- 創建了一個超級管理員角色,創建了一個超級管理員用戶admin(密碼同賬號)
添加數據庫連接字符串
在appsettings.json
中添加數據庫連接字符串(具體的連接自行配置):
"ConectionStrings": {
"MSDbContext": "server=192.168.137.10;database=MSDB;user=root;password=mysql@local;"
}
修改后如下圖所示:
開啟EntityFrameworkCore日志
在appsettings.Development.json
的"Logging:LogLevel"節點添加:
"Microsoft.EntityFrameworkCore": "Information"
修改完成后,如下圖所示
為什么要把開啟EntityFrameworkCore日志寫在appsettings.Development.json
文件里呢?
因為appsettings.Development.json
文件是默認開發時使用的配置,也就是只在開發時才開啟EFCore的日志記錄,實際生產環境不開啟
注冊工作單元
在Startup.cs
類,ConfigureServices方法中添加以下代碼:
//using MS.DbContexts;
//using MS.UnitOfWork;
//using Microsoft.EntityFrameworkCore;
//以上添加到using引用
services.AddUnitOfWorkService<MSDbContext>(options => { options.UseMySql(Configuration.GetSection("ConectionStrings:MSDbContext").Value); });
說明:
- 《1-項目結構分層建立》中,
MS.WebApi
應用程序引用了MS.Services
,層層套娃,最終引用了MS.UnitOfWork
,所以可以使用AddUnitOfWorkService方法 - 這里注冊數據庫用的是MySQL,所以是UseMySql方法
修改網站啟動邏輯
在Program.cs
類中,修改Main方法為以下內容(覆蓋原先的Main方法內容):
//using MS.DbContexts;
//using MS.UnitOfWork;
//以上代碼添加到using
public static void Main(string[] args)
{
try
{
var host = CreateHostBuilder(args).Build();
using (IServiceScope scope = host.Services.CreateScope())
{
//初始化數據庫
DBSeed.Initialize(scope.ServiceProvider.GetRequiredService<IUnitOfWork<MSDbContext>>());
}
host.Run();
}
catch (Exception ex)
{
throw;
}
}
至此,所有的修改已完成,網站啟動將執行DBSeed.Initialize方法來初始化數據
項目完成后,如下圖
啟動項目,此時可以看見控制台EntityFramworkCore的日志:
而數據庫中也生成了對應的數據庫: