【X23】 01 项目搭建
前言
知识点:
EntityFrameworkCore 是轻量化、可扩展、开源和跨平台版的常用数据访问技术,可用作对象关系映射程序 (O/RM),支持多个数据库引擎。
Repository 一种概念,仓储模块(作为一个数据库管理员,直接操作数据库,实体模型)
AutoMapper 是对象到对象的映射工具,例如数据库层与业务层对象映射
Autofac 一个优秀的 .NET IOC框架
JWT 可校验的字符串
Swagger 自动生成接口文档工具
准备工具: VS2019、 Mysql or SqlSever
目的: 搭建X23 项目结构,封装增删改查、权限验证、数据库ORM。完成本章内容可搭建一个可用的后端小Demo。
1 简单介绍
本项目结构如下:
X23
│ README.md
└───X23:webapi层
└───X23.Application:服务层、应用层
└───X23.EFCore:ORM、仓储层
└───X23.Model:实体类库
└───X23.Util:公共工具类
2 构建 X23.Model 实体类库
目标:创建用户信息表 User
其中 BaseAuditedEntity类 包含了审计(创建人、创建时间、修改人、修改时间)、软删除、备注等信息 详情查看源码
// User.cs
/// <summary>
/// 用户信息
/// </summary>
public class User: BaseAuditedEntity
{
/// <summary>
/// 用户姓名
/// </summary>
public string Name { get; set; }
/// <summary>
/// 账号
/// </summary>
public string Account { get; set; }
/// <summary>
/// 密码
/// </summary>
public string Password { get; set; }
/// <summary>
/// 微信账号
/// </summary>
public string WeChatId { get; set; }
}
3 构建 X23.EFCore
目标:通过EntityFrameworkCore实现ORM仓储,数据表结构映射,CRUDService(增删查改服务基础)
文档资料:Entity Framework Core
3.1 Nuget包
<ItemGroup>
<PackageReference Include="AutoMapper" Version="11.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.15" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.15" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.15" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="5.0.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="5.0.4" />
</ItemGroup>
3.2 构建EFCoreContext,迁移数据库
// EFCoreContext.cs
public class EFCoreContext : DbContext
{
public static readonly ILoggerFactory MyLoggerFactory = LoggerFactory.Create(builder => { builder.AddConsole(); });
public EFCoreContext(DbContextOptions<EFCoreContext> options) : base(options)
{
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseLoggerFactory(MyLoggerFactory);//输出日志
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//通过程序集注入方式 使用 fluent API 配置模型
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
//更改表映射 RenameSnakeCase()将大驼峰命名转为蛇形命名
//eg. User.cs => 数据库表 user
// 字段User.WeChatId => 数据库表 user表 字段 wx_chat_id
foreach (var entity in modelBuilder.Model.GetEntityTypes())
{
modelBuilder.Entity(entity.Name, builder =>
{
builder.ToTable(entity.ClrType.Name.RenameSnakeCase());
foreach (var property in entity.GetProperties())
{
builder.Property(property.Name).HasColumnName(property.Name.RenameSnakeCase());
}
});
}
}
}
使用 fluent API 配置模型 构建映射关系
// Mapping.cs 映射基类 因为每个表都有审计信息(创建人、创建时间、修改人、修改时间)等
public class Mapping<T> : IEntityTypeConfiguration<T> where T : BaseAuditedEntity
{
public void Configure(EntityTypeBuilder<T> builder)
{
builder.Property(x => x.Remark).HasMaxLength(500);
builder.Property(x => x.CreateBy).HasMaxLength(20);
builder.Property(x => x.UpdateBy).HasMaxLength(20);
OtherConfigure(builder);
}
public virtual void OtherConfigure(EntityTypeBuilder<T> builder)
{
}
}
// UserMapping.cs 用户表的映射
public class UserMapping : Mapping<User>
{
public override void OtherConfigure(EntityTypeBuilder<User> builder)
{
//用户表 名称、账号、密码等字段都是20个字符
builder.Property(x => x.Name).HasMaxLength(20);
builder.Property(x => x.Account).HasMaxLength(20);
builder.Property(x => x.Password).HasMaxLength(20);
builder.Property(x => x.WeChatId).HasMaxLength(20);
}
}
编写启动扩展类 注册EFCoreContext 可选择连接mysql或者sqlsever
// StartupExtensions.cs
/// <summary>
/// 注册DbContext
/// </summary>
/// <param name="services"></param>
/// <param name="connectionString">连接字符串</param>
/// <param name="dBType">数据库类型(1mysql 2sqlsever)</param>
public static void AddDbContext(this IServiceCollection services, string connectionString, DBType dBType = DBType.MySQL)
{
if (dBType == DBType.MySQL)
{
//https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql
var serverVersion = new MySqlServerVersion(new Version(8, 0, 27));
services.AddDbContext<EFCoreContext>(dbContextOptions => dbContextOptions.UseMySql(connectionString, serverVersion));
}
else if (dBType == DBType.SQLServer)
{
services.AddDbContext<EFCoreContext>(options => options.UseSqlServer(connectionString));
}
else
{
throw new Exception("只支持mysql和sqlsever数据库!");
}
}
//将AddDbContext注册到Startup.cs
//Startup.cs
public void ConfigureServices(IServiceCollection services)
{
...省略
services.AddDbContext("server=192.168.152.128;user=root;password=123456;database=x23");//mysql连接
//services.AddDbContext("server=.;database=X23;uid=sa;password=123456;",Model.Const.Enum.DBType.SQLServer);//sqlsever连接
}
迁移数据库,在程序包管理器控制台选择项目 X23.EFCore 分别执行下列两行命令
Add-Migration init 【执行此命令项目生成一个目录(Migration)】
Update-Database init
此时对应数据库已建立成功!
3.3 构建 Repositoryc仓储
仓储目的:解耦服务层(业务层)和数据库之间,可实现无差别切换ORM,或者说在服务层与数据库表交互过程中无感,比如分库分表、过滤权限等等。
// IRepository.cs
public interface IRepository<T> where T : BaseAuditedEntity
{
//基本的增删改查
Task<bool> AddAsync(T entity);
Task<bool> UpdateAsync(T entity);
Task<T> GetByIdAsync(object id);
Task<bool> DeleteAsync(object id, bool isPhysicalDelete = false);
IQueryable<T> Table { get; }
IQueryable<T> TableNoTracking { get; }
}
// Repository.cs
// IHttpContextAccessors 使用需要在Startup.cs进行注册 services.AddHttpContextAccessor();
public class Repository<T> : IRepository<T> where T : BaseAuditedEntity
{
private DbSet<T> _entities;
private readonly string _currentUserName;
private readonly EFCoreContext _dbContext;
public Repository(EFCoreContext eFCoreContext, IHttpContextAccessor httpContextAccessor)
{
_currentUserName = httpContextAccessor.HttpContext?.User.Identity?.Name;
_dbContext = eFCoreContext;
}
public DbSet<T> Entities
{
get
{
if (_entities == null)
_entities = _dbContext.Set<T>();
return _entities;
}
}
public async Task<bool> AddAsync(T entity)
{
entity.CreateBy = _currentUserName;
await Entities.AddAsync(entity);
var result = await _dbContext.SaveChangesAsync();
return result > 0;
}
...省略
}
3.4 构建 CRUDService增删查改服务基础 【可不需要】
目标:构建基本的增删改查接口,减少重复编写基础服务操作。可跳过。
// ICRUDService
public interface ICRUDService<TDto, T>
where TDto : class
where T : BaseAuditedEntity
{
/// <summary>
/// 添加
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
Task<TDto> AddAsync(TDto entity);
...省略
}
// CRUDService.cs
public class CRUDService<TDto,T> : ICRUDService<TDto,T>
where TDto : class
where T : BaseAuditedEntity
{
private readonly IMapper _mapper;
private readonly IRepository<T> _repository;
public CRUDService(IRepository<T> repository, IMapper mapper)
{
_mapper = mapper;
_repository = repository;
}
public async virtual Task<TDto> AddAsync(TDto entity)
{
var data = _mapper.Map<T>(entity);
if (await _repository.AddAsync(data))
{
return _mapper.Map<TDto>(data);
}
throw new Exception("添加错误");
}
...省略
}
4 构建 X23.Application (应用层)
Application 主要包含整体业务逻辑
<!-- 层次结构 -->
X23.Application
└───AutoMapperConfigs:AutoMapper 关系映射
└───UserManagement
└───Dto:相关Dto文件
└───IUserManagementService.cs
└───UserManagementService.cs
└───XXXXXXXX
└───Dto:相关Dto文件
└───IXXXXXXXXService.cs
└───XXXXXXXXService.cs
此处先写一个用户管理的接口(可继承ICRUDService)
// IUserManagementService.cs
public interface IUserManagementService : ICRUDService<UserDto, User>
{
/// <summary>
/// 测试 通过userId获取用户信息
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
Task<string> Test(int userId);
}
// UserManagementService.cs
public class UserManagementService : CRUDService<UserDto, User>, IUserManagementService
{
private readonly IRepository<User> _userRepository;
private readonly IMapper _mapper;
public UserManagementService(IRepository<User> userRepository, IMapper mapper) : base(userRepository, mapper)
{
_userRepository = userRepository;
_mapper = mapper;
}
/// <summary>
/// 测试 通过userId获取用户信息
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<string> Test(int userId)
{
return (await _userRepository.GetByIdAsync(userId)).ToJson();
}
}
数据传输对象(DTO)(Data Transfer Object),是一种设计模式之间传输数据的软件应用系统。数据传输目标往往是数据访问对象从数据库中检索数据。数据传输对象与数据交互对象或数据访问对象之间的差异是一个以不具有任何行为除了存储和检索的数据(访问和存取器)。
同时 一般来说用户得到的信息和数据库查出来的信息模型会不一致。此处引入AutoMapper进行相关数据的映射。
引入NuGet包 AutoMapper.Extensions.Microsoft.DependencyInjection
建立 AutoMapperConfigs 文件夹,新建用户信息相关映射
// UserManagementCfgs.cs
public class UserManagementCfgs : Profile
{
public UserManagementCfgs()
{
CreateMap<User, UserDto>().ReverseMap();
CreateMap<User, UserInfoDto>().ReverseMap();
}
}
同时 在 Startup.cs 注册AutoMapper
//Startup.cs
public void ConfigureServices(IServiceCollection services)
{
...省略
//通过程序集方式注册AutoMapper
services.AddAutoMapper(Assembly.Load("X23.Application"));
}
5 构建 X23 (webapi层)
5.1 编写webapi
在Controllers下新建控制器基类,封装一些公共方法,比如获取当前用户信息
[AllowAnonymous] 匿名访问
[Authorize] 需要授权才能访问
// BaseApiController.cs
[Authorize]//添加授权,只要继承该类就需要鉴权,
public class BaseApiController : ControllerBase
{
/// <summary>
/// 当前用户信息
/// </summary>
public UserDto CurrentUser
{
get
{
...省略
}
}
}
新建用户控制器
// UserController.cs
/// <summary>
/// 用户管理
/// </summary>
[Route("api/[controller]/[action]")]
[ApiController]
public class UserController : BaseApiController
{
private readonly IUserManagementService _userManagementService;
public UserController(IUserManagementService userManagementService)
{
_userManagementService = userManagementService;
}
/// <summary>
/// 测试 通过userId获取用户信息
/// </summary>
/// <param name="userId">用户id</param>
/// <returns></returns>
[HttpGet]
[AllowAnonymous]
public async Task<string> Test(int userId)
{
return await _userManagementService.Test(userId);
}
/// <summary>
/// 登录
/// </summary>
/// <param name="userDto"></param>
/// <returns></returns>
[HttpPost]
[AllowAnonymous]
public async Task<UserInfoDto> Login(UserDto userDto)
{
return await _userManagementService.Login(userDto);
}
/// <summary>
/// 添加用户
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
[HttpPost]
public async Task<UserDto> AddUser(UserDto user)
{
return await _userManagementService.AddAsync(user);
}
}
5.2 IoC(控制反转)和DI(依赖注入)
资料:ASP.NET Core 依赖注入
此处引用Autofac来进行依赖注入,NuGet包 Autofac.Extensions.DependencyInjection
IOC(控制反转):全称为:Inverse of Control。从字面上理解就是控制反转了,将对在自身对象中的一个内置对象的控制反转,反转后不再由自己本身的对象进行控制这个内置对象的创建,而是由第三方系统去控制这个内置对象的创建。
DI(依赖注入):全称为Dependency Injection,意思自身对象中的内置对象是通过注入的方式进行创建。
IOC就是一种软件设计思想,DI是这种软件设计思想的一个实现。
// Startup.cs
/// <summary>
/// AutofacContainer
/// </summary>
/// <param name="builder"></param>
public void ConfigureContainer(ContainerBuilder builder)
{
builder.RegisterAssemblyTypes(Assembly.Load("X23.Application")).AsImplementedInterfaces();
builder.RegisterGeneric(typeof(Repository<>)).As(typeof(IRepository<>)).InstancePerDependency();//注册仓储泛型
}
// Program.cs
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacServiceProviderFactory())//添加Autofac服务
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
5.3 使用JWT和完善Swagger
资料:JWT
通俗地说,JWT的本质就是一个字符串,它是将用户信息保存到一个Json字符串中,然后进行编码后得到一个JWT token,并且这个JWT token带有签名信息,接收后可以校验是否被篡改,所以可以用于在各方之间安全地将信息作为Json对象传输。
// 添加Token授权
// UserManagementService.cs
/// <summary>
/// 登录
/// </summary>
/// <param name="userDto"></param>
/// <returns></returns>
public async Task<UserInfoDto> Login(UserDto userDto)
{
if (string.IsNullOrEmpty(userDto.Account)|| string.IsNullOrEmpty(userDto.Password))
{
throw new Exception("账号或密码为空!");
}
var user = _userRepository.TableNoTracking.Where(x => x.Account == userDto.Account && x.Password == userDto.Password).FirstOrDefault();
if (user != null)
{
return await GetTokenByUserId(user.Id);
}
else
{
throw new Exception("账号密码错误!");
}
}
/// <summary>
/// 根据用户id获取token
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<UserInfoDto> GetTokenByUserId(int userId)
{
var user = await _userRepository.GetByIdAsync(userId);
var expiresTime = DateTime.Now.AddHours(5);
var claims = new[]
{
new Claim(ClaimTypes.Name, user.Name),
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration[AppSettings.JwtSymmetricSecurityKey]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var jwt = new JwtSecurityToken(
issuer: _configuration[AppSettings.JwtIssuer],
audience: _configuration[AppSettings.JwtAudience],
claims: claims,
expires: expiresTime,
signingCredentials: creds);
var token = new JwtSecurityTokenHandler().WriteToken(jwt);
var resut = _mapper.Map<UserInfoDto>(user);
resut.Token = token;
resut.ExpiresTime = expiresTime;
return resut;
}
添加Jwt,完善Swagger小绿锁(添加授权)
// StartupExtensions.cs
/// <summary>
/// 注册Swagger
/// </summary>
/// <param name="services"></param>
public static void AddSwagger(this IServiceCollection services)
{
//获取程序集解析程序用于探测程序集的基目录的文件路径。
var BasePath = AppContext.BaseDirectory;
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "X23", Version = "v1" });
c.IncludeXmlComments(Path.Combine(BasePath, "X23.Model.xml"));
c.IncludeXmlComments(Path.Combine(BasePath, "X23.Application.xml"));
c.IncludeXmlComments(Path.Combine(BasePath, "X23.xml"), true);//默认的第二个参数是false,这个是controller的注释
#region Token绑定到ConfigureServices
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT授权(数据将在请求头中进行传输) 直接在下框中输入Bearer {token}(注意两者之间是一个空格)",
Name = "Authorization",//jwt默认的参数名称
In = ParameterLocation.Header,//jwt默认存放Authorization信息的位置(请求头中)
Type = SecuritySchemeType.ApiKey,
BearerFormat = "JWT",
Scheme = "Bearer"
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference {
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
});
#endregion
});
}
/// <summary>
/// 添加Jwt
/// </summary>
/// <param name="services"></param>
/// <param name="configuration"></param>
public static void AddJWT(this IServiceCollection services, IConfigurationconfiguration)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters
{
ClockSkew = TimeSpan.FromSeconds(10),//缓冲时间:真正过期时间是 过期时间+缓冲时间
ValidateIssuer = true,//是否验证Issuer
ValidateAudience = true,//是否验证Audience
ValidateLifetime = true,//是否验证失效时间
ValidateIssuerSigningKey = true,//是否验证SecurityKey
ValidAudience = configuration[AppSettings.JwtAudience],//Audience
ValidIssuer = configuration[AppSettings.JwtIssuer],//Issuer,这两项和前面签发jwt的设置一致
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration[AppSettings.JwtSymmetricSecurityKey]))//拿到SecurityKey
};
});
}
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
...省略
services.AddSwagger();
services.AddJWT(Configuration);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "X23 v1"));
}
app.UseRouting();
app.UseAuthentication();//认证 明确是你谁,确认是不是合法用户。
app.UseAuthorization();//授权 明确你是否有某个权限。当用户需要使用某个功能的时候,系统需要校验用户是否需要这个功能的权限
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
给Swagger绑定相关注释
分别给X23.Model、X23.Application、X23 添加xml
并设置xml文件 复制到输出目录=>始终复制
6 大功告成!源码下载
把项目运行起来。