demo 地址: https://github.com/iotjin/Jh.Admin.NETCore
代码不定时更新,请前往github查看最新代码
.NET 7.0 EF Core:一、创建Web API项目
官方教程
EF Core 官方文档
官方文档: 教程:使用 ASP.NET Core 创建最小 API
EFCore 查询数据
第一个EFCore应用
使用 NuGet 包管理器在 Visual Studio 中安装和管理包
常见的数据库提供程序
序
本文记录如何使用 MySQL+ .NET 7 + Entity Framework Core (EF Core) + Code First + Fluent API 搭建一个后台管理系统框架。项目采用常见的分层结构,适合后续扩展用户、角色、菜单管理等模块。
Admin.NETCore.API
:接口入口层(API 层),Web API 入口Admin.NETCore.Core
:核心业务层,Interface、Service、ViewModel、DTO等Admin.NETCore.Infrastructure
:基础设施层,数据库、Fluent API配置、Entity、Migrations等Admin.NETCore.Common
:通用功能层,工具类等
一、项目目录结构
Admin.NETCore
├── Admin.NETCore.API // 接口入口层
├── Admin.NETCore.Core // 核心业务层
├── Admin.NETCore.Infrastructure // 基础设施层
└── Admin.NETCore.Common // 通用功能层
各层职责说明
1️⃣ Admin.NETCore.API(接口层)
负责处理 HTTP 请求和响应,是整个项目的对外入口。
主要目录:
Controllers/
:控制器,如UserController
、RoleController
Identity/Filters/
:请求验证过滤器,如ValidationFilter.cs
ServiceExtensions/
:服务注册扩展类(中间件、Swagger、CORS 等)Program.cs
:程序主入口appsettings.json
:配置文件
2️⃣ Admin.NETCore.Core(核心业务层)
包含系统的业务逻辑、接口定义、DTO/ViewModel,不依赖基础设施层。
主要目录:
Interfaces/
:接口定义,如IUserService
、IRoleService
Services/
:核心业务逻辑的实现ViewModels/
:DTO/ViewModel 传输模型,分为UserDTO
,RoleDTO
,UserVModel
等
3️⃣ Admin.NETCore.Infrastructure(基础设施层)
实现所有对数据库的访问逻辑,包含 EF Core 实体、配置和上下文等。
主要目录:
Entities/
:实体类,如User
,Role
,UserRole
,继承BaseEntity
Configs/
:EF Core 实体映射配置(Fluent API)AppDbContext.cs
:EF Core 数据上下文Migrations/
:EF Core 数据迁移记录(Code First)
4️⃣ Admin.NETCore.Common(通用功能层)
放置整个项目中通用的类和工具方法。
主要目录:
Configs/
:全局配置类,如GlobalConfigs
SqlHelper.cs
:通用数据库操作帮助类(如直接操作 SQL)
项目文件结构图
依赖关系
完整层级
设计的用户表数据结构和返回结构
二、创建Web API项目
先创建一个带swagger的Web API项目然后再对其进行改造
打开 Visual Studio → 新建项目
选择 ASP.NET Core Web API
项目命名为:Admin.NETCore.API ,解决方案命名为:Admin.NETCore
勾选:
✔ 配置 HTTPS(可选)
✔ 勾选“使用控制器(取消选中以使用最小API)”(我们保留默认控制器)
✔ 启用 OpenAPI 支持(Swagger)
创建完成点击运行,可以看到浏览器的swagger页面
三、调整项目结构
右键解决方案,按下面步骤依次创建
Admin.NETCore.Common
,Admin.NETCore.Core
,Admin.NETCore.Infrastructure
,
右键解决方案 → 添加 → 新建项目
选择:类库 (.NET Class Library)
命名为:Admin.NETCore.Common,
选择NET 7.0 (或者创建完成后,右键项目 → 属性 → 将目标框架设置为 .NET 7.0)
创建
四、添加NuGet 包
官方文档:使用 NuGet 包管理器在 Visual Studio 中安装和管理包
使用 EFCore 和MySQL 需要添加以下NuGet包:
Microsoft.EntityFrameworkCore.Tools
EF Core 命令支持,用于数据库生成、迁移、生成表等操作,安装在Admin.NETCore.API
内Pomelo.EntityFrameworkCore.MySql
MySQL支持,安装在Admin.NETCore.Infrastructure
内
如果是其他数据库参考下图 常见的数据库提供程序
五、通过CodeFirst建库建表
1)、先在
Admin.NETCore.Infrastructure
下面创建文件夹和文件
2)、然后通过实体类定义用户表(User)的数据结构,使用 Fluent API 在独立的配置类中集中管理表名、主键、字段约束等数据库映射信息
3)、在数据库上下文(AppDbContext)中统一注册实体集合,并自动应用所有配置类
4)、最后使用 EF Core 提供的迁移与数据库更新命令(Add-Migration 和 Update-Database)即可在数据库中生成对应的 User 表结构,实现从代码到数据库的完整同步
BaseEntity代码
using System.ComponentModel.DataAnnotations.Schema;
namespace Admin.NETCore.Infrastructure.DB.Entities
{
public abstract class BaseEntity
{
//public Guid Id { get; set; } // 使用 Guid 类型,表示 UUID
// Guid.NewGuid().ToString().ToLower() 得到id
//public string Id { get; set; } = null!; // 存36位字符串 char 主键要定义基类中
public DateTime CreateTime { get; set; }
public DateTime UpdateDate { get; set; }
public string? CreateBy { get; set; }
public string? UpdateBy { get; set; }
public bool IsDelete { get; set; } // 是否删除
//[Column(TypeName = "DATETIME(6)")] // MySQL 支持高精度时间
//public DateTime CreateTime { get; set; } = DateTime.UtcNow;
//[Column(TypeName = "DATETIME(6)")]
//public DateTime UpdateDate { get; set; } = DateTime.UtcNow;
//[Column(TypeName = "VARCHAR(10)")] // 明确指定 VARCHAR 类型
//public string CreateBy { get; set; } = "system";
//[Column(TypeName = "VARCHAR(10)")]
//public string UpdateBy { get; set; } = "system";
//[Column(TypeName = "TINYINT(1)")] // MySQL 通常用 TINYINT 表示 bool
//public bool IsDelete { get; set; }
}
}
User代码
namespace Admin.NETCore.Infrastructure.DB.Entities
{
public class User : BaseEntity
{
//public Guid Id { get; set; } // 使用 Guid 类型,表示 UUID
// Guid.NewGuid().ToString().ToLower() 得到id
public string Id { get; set; } = null!; // 存36位字符串 char
public string Name { get; set; } = null!; // 姓名
public string LoginName { get; set; } = null!; // 登录用户名
public string Phone { get; set; } = null!; // 手机号
public int UserNumber { get; set; } // 员工编号
public string DeptId { get; set; } = null!; // 部门ID
public DateTime UserExpiryDate { get; set; } // 用户有效期止
public int Status { get; set; } // 用户状态(1:启用,0:停用)
public int Level { get; set; } // 级别(1:一级,2:二级 。。。)
public double? Money { get; set; } // 补助,可为空
public int? Age { get; set; } // 年龄,可为空
public string? Notes { get; set; } // 备注,最多100个字符
}
}
UserConfig代码
using Admin.NETCore.Infrastructure.DB.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Admin.NETCore.Infrastructure.DB.configs
{
public class UserConfig : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
// 设置表名
builder.ToTable("User");
builder.HasKey(e => e.Id); // 设置主键
// 属性配置
builder.Property(e => e.Id).HasMaxLength(36);
builder.Property(e => e.Name).HasMaxLength(10).IsRequired().HasComment("姓名");
builder.Property(e => e.LoginName).HasMaxLength(10).HasColumnType("varchar(10)").IsRequired();
builder.Property(e => e.Phone).HasMaxLength(11).IsRequired().IsFixedLength().HasAnnotation("PhoneNumber", "^[0-9]{11}$"); // 正则校验(可以自定义更复杂的格式)
builder.Property(e => e.UserNumber).HasMaxLength(8).IsRequired(); // 数值类型(如 int)无法添加长度限制,此处HasMaxLength不生效
builder.Property(e => e.DeptId).IsRequired(); // 不设置长度,类型为longtext
builder.Property(e => e.UserExpiryDate).HasMaxLength(10).IsRequired(); // DateTime类型,无法添加长度限制,此处HasMaxLength不生效
builder.Property(e => e.Status).IsRequired();
builder.Property(e => e.Level).IsRequired();
builder.Property(e => e.Money).IsRequired(false);
builder.Property(e => e.Age).IsRequired(false).HasAnnotation("Age", "^[1-9][0-9]*$");
builder.Property(e => e.Notes).HasMaxLength(100).IsRequired(false).HasComment("这是备注");
// 公共属性配置
builder.Property(e => e.CreateBy).IsRequired(false).HasMaxLength(10).HasColumnName("CreateBy");
builder.Property(e => e.UpdateBy).IsRequired(false).HasMaxLength(10).HasColumnName("UpdateBy");
// 创建时间(只设置一次)
builder.Property(e => e.CreateTime)
.HasColumnName("CreateTime")
.HasColumnType("DATETIME(6)")
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP(6)") // GETDATE() SQL Server / NOW() mysql GETUTCDATE()
.Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Ignore);
// 更新时间(每次更新)
builder.Property(e => e.UpdateDate)
.HasColumnName("UpdateDate")
.HasColumnType("DATETIME(6)")
.ValueGeneratedOnAddOrUpdate()
.HasDefaultValueSql("CURRENT_TIMESTAMP(6)") //
.Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Save);
//builder.Ignore(e => e.CreateTime);// 设置某字段不映射到数据表
//builder.Property(e => e.IsDelete).HasColumnName("delFlag"); // 某字段对应数据表的某字段
}
}
}
/*
EF默认规则是“主键属性不允许为空,引用类型允许为空,可空的值类型long?等允许为空,值类型不允许为空。”
基于“尽量少配置”的原则:如果属性是值类型并且允许为null,就声明成long?等,否则声明成long等;
如果属性属性值是引用类型,只有不允许为空的时候设置IsRequired()。
其他一般不用设置的
主键:this.HasKey(p => p.pId);
某个字段不参与映射数据库:this.Ignore(p => p.Name1);
this.Property(p => p.Name).IsFixedLength(); 是否对应固定长度
this.Property(p => p.Name).IsUnicode(false) 对应的数据库类型是varchar类型,而不是nvarchar
this.Property(p => p.Id).HasColumnName(“Id”); Id列对应数据库中名字为Id的字段
this.Property(p=>p.Id).HasDatabaseGeneratedOption(System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption.Identity) 指定字段是自动增长类型。
*/
AppDbContext代码
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using System.Reflection;
using Admin.NETCore.Infrastructure.DB.Entities;
namespace Admin.NETCore.Infrastructure.DB
{
/// <summary>
/// 应用数据库上下文,用于管理 EF Core 与数据库之间的交互
/// </summary>
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
/*
用户表(User 实体)对应的数据集合,AppDbContext 可以对 User 实体进行增删查改操作
DbSet<T> 是 EF Core 中用于操作某个实体类型的集合
放在 AppDbContext 中,EF Core 会自动识别这些属性,对应到数据库的表,进行 Code First 映射
*/
public DbSet<User> User { get; set; } // DbSet<模型> 表名
public DbSet<Role> Role { get; set; }
public DbSet<UserRole> UserRole { get; set; }
/*
* 数据库配置建议在 Program.cs 中完成(使用依赖注入方式)
* 如果在这里同时使用 OnConfiguring,会导致配置冲突或冗余。
* 所以此处注释掉 OnConfiguring 方法
*/
//protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
//{
// // 数据库连接字符串
// var connectionString = "server=localhost;Database=TestDB;Uid=root;Pwd=root;";
// // 使用 MySQL 数据库,并指定服务器版本
// optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));
// base.OnConfiguring(optionsBuilder);
//}
/// <summary>
/// 模型构建方法,用于配置实体与数据库的映射关系
/// </summary>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// ❎ 下面是手动配置实体属性的示例,仅供参考,实际建议写在单独的配置类中
//modelBuilder.Entity<User>().Property(e => e.Name)
// .IsRequired()
// .HasComment("备注");
//modelBuilder.Entity<User>(entity =>
//{
// entity.ToTable("User");
// // 属性配置
// entity.HasKey(e => e.Id); // 设置主键
// entity.Property(e => e.Id).HasMaxLength(36);
// entity.Property(e => e.Name).HasMaxLength(10).IsRequired().HasComment("姓名");
// entity.Property(e => e.LoginName).HasMaxLength(10).IsRequired();
// entity.Property(e => e.Phone).HasMaxLength(11).IsRequired().IsFixedLength().HasAnnotation("PhoneNumber", "^[0-9]{11}$"); // 正则校验(可以自定义更复杂的格式)
// entity.Property(e => e.UserNumber).HasMaxLength(8).IsRequired();
// entity.Property(e => e.DeptId).IsRequired();
// entity.Property(e => e.UserExpiryDate).HasMaxLength(10).IsRequired();
// entity.Property(e => e.Status).IsRequired();
// entity.Property(e => e.Level).IsRequired();
// entity.Property(e => e.Money).IsRequired(false);
// entity.Property(e => e.Age).IsRequired(false).HasAnnotation("Age", "^[1-9][0-9]*$");
// entity.Property(e => e.Notes).HasMaxLength(100).IsRequired(false).HasComment("这是备注");
//});
base.OnModelCreating(modelBuilder);
//方式一. 分开注册, 逐个添加实体的配置类
//modelBuilder.Configurations.Add(new UserConfig());
//modelBuilder.Configurations.Add(new RoleConfig());
//方式二. 一次性加载所有Fluent API的配置
// ✅ 推荐方式:统一加载所有 IEntityTypeConfiguration 的配置类(如 UserConfig、RoleConfig 等)
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
}
}
}
/*
Migration的一些命令(因为在Program中配置了 MigrationsAssembly对应的项目,这里放在配置的项目路径下)
工具>NuGet 包管理器 > 程序包管理器控制台
Add-Migration Init
update-database
指定 Migration 路径
Add-Migration updateUserTable6 -OutputDir "DB/Migrations"
*/
生成数据库和表
1)、在 Program.cs
中注册 AppDbContext,并指定 EF Core 使用 MySQL 作为数据库
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseMySql(
builder.Configuration.GetConnectionString("DefaultConnection"),
ServerVersion.AutoDetect(builder.Configuration.GetConnectionString("DefaultConnection")),
x => x.MigrationsAssembly("Admin.NETCore.Infrastructure") // 指定迁移程序集
));
var app = builder.Build();
2)、在 appsettings
中添加连接字符串
"ConnectionStrings": {
"DefaultConnection": "server=localhost;Database=TestDB;Uid=root;Pwd=root;"
}
3)、生成Migration
// Install-Package Microsoft.EntityFrameworkCore.Tools
Add-Migration InitialCreate
Update-Database
通过
Add-Migration xxx
命令生成Migration 迁移文件
作用:用于创建数据库迁移文件。根据当前的实体模型和配置类,生成一份数据库结构的变更快照(Migration 迁移文件)
4)、应用到数据库
通过
Update-Database
命令将迁移应用到数据库中,创建相应的数据表结构
六、UserService 实现
1)、先在
Admin.NETCore.Core
和Admin.NETCore.Common
下面创建文件夹和文件
2)、在Service实现代码
Services主要代码
最新代码UserService.cs
UserDTO代码
using Admin.NETCore.Core.ViewModels.Base;
namespace Admin.NETCore.Core.ViewModels
{
public class UserDTO : BaseModel
{
public string Id { get; set; } = null!;
public string Name { get; set; } = null!;
public string LoginName { get; set; } = null!;
public string Phone { get; set; } = null!;
public int UserNumber { get; set; }
public string DeptId { get; set; } = null!;
public DateTime UserExpiryDate { get; set; }
public int Status { get; set; }
public int Level { get; set; }
public double? Money { get; set; }
public int? Age { get; set; }
public string? Notes { get; set; }
}
}
UserVModel代码
using Admin.NETCore.Core.ViewModels.Base;
using System.ComponentModel.DataAnnotations;
namespace Admin.NETCore.Core.ViewModels
{
public class UserVModel : BaseModel
{
public string? Id { get; set; }
[Required(ErrorMessage = "Name不能为空")]
public string Name { get; set; } = null!;
[Required(ErrorMessage = "LoginName不能为空")]
public string LoginName { get; set; } = null!;
//[MaxLength(11, ErrorMessage = "Phone必须为11位")]
//[MinLength(11, ErrorMessage = "Phone必须为11位")]
//[StringLength(11, MinimumLength = 11, ErrorMessage = "Phone长度必须为11位
[Required(ErrorMessage = "Phone不能为空")]
[RegularExpression(@"^1[3-9]\d{9}$", ErrorMessage = "手机号格式不正确")]
public string Phone { get; set; } = null!;
[Required(ErrorMessage = "UserNumber不能为空")]
[RegularExpression(@"^\d{8}$", ErrorMessage = "UserNumber必须为8位纯数字")]
public int UserNumber { get; set; }
[Required(ErrorMessage = "DeptId不能为空")]
public string DeptId { get; set; } = null!;
public DateTime UserExpiryDate { get; set; }
public int Status { get; set; }
[Range(1, 10, ErrorMessage = "Level需在0-10范围内")]
public int Level { get; set; }
public double? Money { get; set; }
[Range(1, int.MaxValue, ErrorMessage = "Age必须是正整数")]
public int? Age { get; set; }
public string? Notes { get; set; }
}
public class UserFilterModel
{
public string? Name { get; set; }
public string? UserNumber { get; set; }
public string? DeptId { get; set; }
public string? StartDate { get; set; }
public string? EndDate { get; set; }
public int Page { get; set; }
public int Limit { get; set; }
}
public class AssignRoleVModel
{
[Required(ErrorMessage = "UserId不能为空")]
public string Id { get; set; } = null!;
public List<string> RoleIds { get; set; } = new();
}
}
IUserService代码
using Admin.NETCore.Core.ViewModels;
using Admin.NETCore.Core.ViewModels.Base;
namespace Admin.NETCore.Core.Interfaces
{
public interface IUserService
{
Task<ApiResult<UserVModel>> CreateUserAsync(UserVModel user);
Task<ApiResult<UserVModel>> UpdateUserAsync(UserVModel model);
Task<ApiResult<UserVModel>> GetUserByIdAsync(string id);
Task<ApiResult<string>> DeleteUserByIdAsync(string id);
Task<PagedResult<UserDTO>> GetUserListAsync(UserFilterModel filter);
}
}
UserService代码
using Microsoft.EntityFrameworkCore;
using Admin.NETCore.Common.Configs;
using Admin.NETCore.Core.Interfaces;
using Admin.NETCore.Core.ViewModels;
using Admin.NETCore.Core.ViewModels.Base;
using Admin.NETCore.Infrastructure.DB;
using Admin.NETCore.Infrastructure.DB.Entities;
namespace Admin.NETCore.Core.Services
{
public class UserService : IUserService
{
private readonly AppDbContext _context;
public UserService(AppDbContext context)
{
_context = context;
}
public async Task<ApiResult<UserVModel>> CreateUserAsync(UserVModel model)
{
//var result = new ApiResult<UserVModel>();
// 检查用户名是否已存在
if (await _context.User.AnyAsync(m => m.LoginName == model.LoginName))
{
return ApiResult<UserVModel>.FailResult("登录名已存在");
}
var dbModel = new User
{
Id = Guid.NewGuid().ToString(),
Name = model.Name,
LoginName = model.LoginName,
Phone = model.Phone,
UserNumber = model.UserNumber,
DeptId = model.DeptId,
UserExpiryDate = model.UserExpiryDate,
Status = model.Status,
Level = model.Level,
Money = model.Money,
Age = model.Age,
Notes = model.Notes,
IsDelete = false
};
await _context.User.AddAsync(dbModel);
await _context.SaveChangesAsync();
var returnModel = model;
returnModel.Id = dbModel.Id;
return ApiResult<UserVModel>.SuccessResult(returnModel, "用户创建成功");
}
public async Task<ApiResult<UserVModel>> UpdateUserAsync(UserVModel model)
{
var user = await _context.User.FindAsync(model.Id);
if (user == null)
{
return ApiResult<UserVModel>.FailResult("用户不存在");
}
// 检查用户名是否与其他用户重复
if (await _context.User.AnyAsync(m => m.LoginName == model.LoginName && m.Id != model.Id))
{
return ApiResult<UserVModel>.FailResult("登录名已存在");
}
user.Name = model.Name;
user.LoginName = model.LoginName;
user.Phone = model.Phone;
user.UserNumber = model.UserNumber;
user.DeptId = model.DeptId;
user.UserExpiryDate = model.UserExpiryDate;
user.Status = model.Status;
user.Level = model.Level;
user.Money = model.Money;
user.Age = model.Age;
user.Notes = model.Notes;
user.IsDelete = false;
await _context.SaveChangesAsync();
var returnModel = model;
returnModel.Id = user.Id;
return ApiResult<UserVModel>.SuccessResult(returnModel, "用户更新成功");
}
public async Task<ApiResult<string>> DeleteUserByIdAsync(string id)
{
var user = await _context.User.FindAsync(id);
if (user == null)
{
return ApiResult<string>.FailResult("用户不存在");
}
_context.User.Remove(user); // 物理删除
//user.IsDelete = true; // 逻辑删除
await _context.SaveChangesAsync();
return ApiResult<string>.SuccessResult("", "用户删除成功");
}
public async Task<ApiResult<UserVModel>> GetUserByIdAsync(string id)
{
var user = await _context.User.FindAsync(id);
if (user == null)
{
return ApiResult<UserVModel>.FailResult("用户不存在");
}
var returnModel = new UserVModel
{
Id = user.Id,
Name = user.Name,
LoginName = user.LoginName,
Phone = user.Phone,
UserNumber = user.UserNumber,
DeptId = user.DeptId,
UserExpiryDate = user.UserExpiryDate,
Status = user.Status,
Level = user.Level,
Money = user.Money,
Age = user.Age,
Notes = user.Notes,
IsDelete = user.IsDelete
};
return ApiResult<UserVModel>.SuccessResult(returnModel);
}
public async Task<PagedResult<UserDTO>> GetUserListAsync(UserFilterModel filter)
{
// 参数校验
filter.Page = filter.Page > 0 ? filter.Page : 1;
filter.Limit = filter.Limit > 0 ? filter.Limit : GlobalConfigs.DefaultPageSize;
var query = _context.User.AsNoTracking().AsQueryable();
/*
EF Core 默认会追踪查询出来的实体(用于之后的更新或删除)
如果只是读取数据,无需修改,可以用 .AsNoTracking() 提高性能
它会 减少内存开销 和 避免不必要的跟踪逻辑
*/
//var query = _context.User.Where(m => m.IsDelete == false).AsNoTracking().AsQueryable();
//var query = _context.User.Where(m => !m.IsDelete).AsNoTracking();
// 姓名模糊查询
if (!string.IsNullOrEmpty(filter.Name))
query = query.Where(m => EF.Functions.Like(m.Name, $"%{filter.Name}%"));
// 员工编号模糊查询
if (!string.IsNullOrEmpty(filter.UserNumber))
query = query.Where(m => m.UserNumber.ToString().Contains(filter.UserNumber));
// 部门ID模糊查询
if (!string.IsNullOrEmpty(filter.DeptId))
query = query.Where(m => m.DeptId == filter.DeptId);
// 处理日期范围
if (DateTime.TryParse(filter.StartDate, out DateTime startDate))
query = query.Where(m => m.CreateTime >= startDate);
if (DateTime.TryParse(filter.EndDate, out DateTime endDate))
query = query.Where(m => m.CreateTime <= endDate);
// 获取总数
int total = await query.CountAsync();
// 分页查询
var users = await query
.OrderByDescending(m => m.UpdateDate)
.Skip((filter.Page - 1) * filter.Limit)
.Take(filter.Limit)
.AsSplitQuery()
.Select(m => new UserDTO
{
Id = m.Id,
Name = m.Name,
LoginName = m.LoginName,
Phone = m.Phone,
UserNumber = m.UserNumber,
DeptId = m.DeptId,
UserExpiryDate = m.UserExpiryDate,
Status = m.Status,
Level = m.Level,
Money = m.Money,
Age = m.Age,
Notes = m.Notes,
IsDelete = m.IsDelete
})
.ToListAsync();
return PagedResult<UserDTO>.SuccessResult(users, total);
}
}
}
七、UserController 实现
1)、先在
Admin.NETCore.API
下面创建UserController
文件
2)、在UserController实现代码
3)、在Program.cs
注册依赖注入
builder.Services.AddScoped<IUserService, UserService>();
var app = builder.Build();
UserController代码
using Microsoft.AspNetCore.Mvc;
using Admin.NETCore.Core.Interfaces;
using Admin.NETCore.Core.ViewModels;
using Admin.NETCore.Core.ViewModels.Base;
namespace Admin.NETCore.API.Controllers
{
[ApiController]
[Route("api/[controller]/[action]")] // RPC风格, api/[控制器名称]/函数名称
//[Route("api/[controller]")] // RESTful 风格
public class UserController : ControllerBase
{
private readonly IUserService _userService;
public UserController(IUserService userService)
{
_userService = userService;
}
[HttpPost]
public async Task<ApiResult<UserVModel>> CreateUserAsync(UserVModel model)
{
if (string.IsNullOrEmpty(model.LoginName))
{
return ApiResult<UserVModel>.FailResult("用户名不能为空");
}
return await _userService.CreateUserAsync(model);
}
[HttpPost]
public async Task<ApiResult<UserVModel>> UpdateUserAsync(UserVModel model)
{
if (string.IsNullOrEmpty(model.LoginName))
{
return ApiResult<UserVModel>.FailResult("用户名不能为空");
}
return await _userService.UpdateUserAsync(model);
}
//[HttpGet("{id}")] // 接口格式为 /api/user/GetUserById/123
[HttpGet] // 接口格式为 /api/user/GetUserById?id=123
public async Task<ApiResult<UserVModel>> GetUserByIdAsync(string id)
{
if (string.IsNullOrEmpty(id))
{
return ApiResult<UserVModel>.FailResult("id不能为空");
}
return await _userService.GetUserByIdAsync(id);
}
[HttpPost]
public async Task<ApiResult<string>> DeleteUserByIdAsync([FromBody] IDRequest request)
{
if (string.IsNullOrEmpty(request.Id))
{
return ApiResult<string>.FailResult("id不能为空");
}
return await _userService.DeleteUserByIdAsync(request.Id);
}
[HttpGet]
public async Task<PagedResult<UserDTO>> GetUserListAsync([FromQuery] UserFilterModel filter)
{
return await _userService.GetUserListAsync(filter);
}
}
}
运行项目后,在swagger点创建用户接口
/api/User/CreateUser
和/api/User/GetUserList
如下
点创建用户接口
/api/User/CreateUser
, 什么都不改,可以看到返回的数据并不是预期的结构,这是因为使用的是默认的校验,没有用自定义校验,在下一步优化
{
"code": 200,
"msg": "success",
"data": {}
}
八、优化Program.cs
现在
Program.cs
完整代码如下,东西都写在一个文件了,以后如果加上像swagger配置、依赖注入、参数校验、中间件等,文件会越来越多,把代码都抽出来,保持主入口简洁
Program.cs原来的代码
using Admin.NETCore.Core.Interfaces;
using Admin.NETCore.Core.Services;
using Admin.NETCore.Infrastructure.DB;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseMySql(
builder.Configuration.GetConnectionString("DefaultConnection"),
ServerVersion.AutoDetect(builder.Configuration.GetConnectionString("DefaultConnection")),
x => x.MigrationsAssembly("Admin.NETCore.Infrastructure") // 指定迁移程序集
));
builder.Services.AddScoped<IUserService, UserService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
先把文件都创建好,然后在调整
ValidationFiter代码
// 自定义异常处理类
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Http;
namespace Admin.NETCore.API.Identity.filters
{
public class ValidationFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
// 提取所有错误信息
var errorMessages = context.ModelState.Values
.Where(x => x.Errors.Count > 0)
.SelectMany(x => x.Errors)
.Select(x => x.ErrorMessage)
.ToList();
// 拼接错误信息为字符串
var msgs = string.Join("; ", errorMessages);
int statusCode = 201;
// 构造自定义响应对象
var response = new
{
code = statusCode,
msg = msgs,
success = false,
data = ""
};
context.Result = new BadRequestObjectResult(response);
// 自定义状态码
//context.Result = new ObjectResult(response)
//{
// StatusCode = statusCode
//};
}
}
}
public class CustomValidationFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (!context.ModelState.IsValid)
{
//var errorMessages = context.ModelState.Values
// .Where(x => x.Errors.Count > 0)
// .SelectMany(x => x.Errors)
// .Select(x => x.ErrorMessage)
// .ToList();
//var msgs = string.Join("; ", errorMessages);
//var response = new
//{
// code = 201, // 可根据需求调整
// msg = msgs,
// success = false,
// data = ""
//};
//context.Result = new BadRequestObjectResult(response);
context.Result = CreateValidationErrorResult(context.ModelState);
return;
}
await next();
}
private static IActionResult CreateValidationErrorResult(ModelStateDictionary modelState)
{
var errorMessages = modelState
.Where(x => x.Value?.Errors.Count > 0)
.ToDictionary(
x => x.Key,
x => x.Value?.Errors
.Select(error =>
!string.IsNullOrEmpty(error.ErrorMessage)
? error.ErrorMessage
: error.Exception?.Message ?? "Unknown error")
.ToArray()
);
var message = string.Join("; ", errorMessages.SelectMany(e => e.Value ?? Array.Empty<string>()));
int statusCode = 201;
var response = new
{
code = statusCode, // 可根据需求调整
msg = message,
errors = errorMessages,
success = false,
data = ""
};
return new BadRequestObjectResult(response);
自定义状态码
//return new ObjectResult(response)
//{
// StatusCode = statusCode
//};
}
}
}
ApplicationBuilderExtensions代码
using Admin.NETCore.Infrastructure.DB;
using Microsoft.EntityFrameworkCore;
namespace Admin.NETCore.API.ServiceExtensions
{
public static class ApplicationBuilderExtensions
{
public static WebApplication ConfigureMiddlewarePipeline(this WebApplication app)
{
// 异常处理应放在最前面
app.UseExceptionHandler("/error");
// Swagger
app.UseCustomSwaggerUI();
//app.UseCustomSwaggerUI22();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
app.UseHttpsRedirection();
}
app.UseRouting();
// 健康检查端点
app.MapHealthChecks("/health");
// 全局日志中间件
app.UseRequestLogging();
app.UseCorsPolicy();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
// 可选:数据库自动迁移
//app.ApplyMigrations<AppDbContext>();
return app;
}
private static IApplicationBuilder UseRequestLogging(this IApplicationBuilder builder)
{
return builder.Use(async (context, next) =>
{
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Request {Method} {Path}", context.Request.Method, context.Request.Path);
await next();
logger.LogInformation("Response {StatusCode}", context.Response.StatusCode);
});
}
public static void ApplyMigrations<TContext>(this WebApplication app) where TContext : DbContext
{
using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<TContext>();
db.Database.Migrate();
}
}
}
CorsExtensions代码
namespace Admin.NETCore.API.ServiceExtensions
{
public static class CorsExtensions
{
public static IServiceCollection AddCorsPolicy(this IServiceCollection services)
{
// 配置跨域
services.AddCors(c => c.AddPolicy("any", p => p.AllowAnyHeader().AllowAnyOrigin().AllowAnyMethod()));
return services;
}
public static IApplicationBuilder UseCorsPolicy(this IApplicationBuilder app)
{
app.UseCors("any");
return app;
}
}
}
ServiceCollectionExtensions代码
using Admin.NETCore.API.Identity.filters;
using Admin.NETCore.Core.Interfaces;
using Admin.NETCore.Core.Services;
using Admin.NETCore.Infrastructure.DB;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Admin.NETCore.API.ServiceExtensions
{
public static class ServiceCollectionExtensions
{
private const string MigrationAssembly = "Admin.NETCore.Infrastructure";
public static IServiceCollection AddBasicServices(this IServiceCollection services, IConfiguration configuration)
{
// Add services to the container.
services.AddControllers();
services.AddHealthChecks();
// 日志配置
services.AddLogging(logging =>
{
logging.AddConfiguration(configuration.GetSection("Logging"));
logging.AddConsole();
logging.AddDebug();
});
services.AddCorsPolicy();
services.AddSwaggerConfig();
// services.AddSwaggerConfig22();
return services;
}
public static IServiceCollection AddDataBaseServices(this IServiceCollection services, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("缺少数据库连接字符串配置");
// services.AddDbContext<AppDbContext>(options =>
services.AddDbContextPool<AppDbContext>(options =>
options.UseMySql(
connectionString,
ServerVersion.AutoDetect(connectionString),
x => x.MigrationsAssembly(MigrationAssembly) // 指定迁移程序集
));
services.AddScoped<IUserService, UserService>();
return services;
}
public static IServiceCollection AddControllerWithValidation(this IServiceCollection services)
{
services.AddControllers(options =>
{
options.Filters.Add<ValidationFilterAttribute>();
options.Filters.Add<CustomValidationFilter>();
});
// 禁用默认的模型验证响应
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
return services;
}
}
}
/*
数据库配置
// 添加数据库上下文
//builder.Services.AddDbContext<AppDbContext>(options =>
// options.UseMySql(
// builder.Configuration.GetConnectionString("DefaultConnection"),
// ServerVersion.AutoDetect(builder.Configuration.GetConnectionString("DefaultConnection"))
// ));
//builder.Services.AddDbContext<AppDbContext>(options =>
// options.UseMySql(builder.Configuration.GetConnectionString("DefaultConnection"),
// new MySqlServerVersion(new Version(8, 0, 21))));
//builder.Services.AddDbContext<AppDbContext>(options =>
// options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// 指定迁移程序集
//builder.Services.AddDbContext<AppDbContext>(options =>
// options.UseMySql(
// builder.Configuration.GetConnectionString("DefaultConnection"),
// ServerVersion.AutoDetect(builder.Configuration.GetConnectionString("DefaultConnection")),
// x => x.MigrationsAssembly(Assembly.GetExecutingAssembly().GetName().Name)
// ));
*/
SwaggerExtensions代码
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
// 通过[ApiExplorerSettings(GroupName = "v1")] 实现
namespace Admin.NETCore.API.ServiceExtensions
{
public static class SwaggerExtensions
{
public static IServiceCollection AddSwaggerConfig(this IServiceCollection services)
{
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
services.AddEndpointsApiExplorer();
//services.AddSwaggerGen();
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "API (V1)",
Version = "v1",
//Description = "API 文档1",
Description = "🚨 此版本已废弃,请使用 v2",
});
options.SwaggerDoc("v2", new OpenApiInfo
{
Title = "API (V2)",
Version = "v2",
Description = "API 文档2"
});
// 核心逻辑:根据 GroupName 过滤接口到对应文档
options.DocInclusionPredicate((docName, apiDesc) =>
{
// 获取 Controller/Action 上的 GroupName
var groupName = apiDesc.ActionDescriptor.EndpointMetadata
.OfType<ApiExplorerSettingsAttribute>()
.FirstOrDefault()?.GroupName;
// 未标记 GroupName 的接口默认显示在所有文档中
if (string.IsNullOrEmpty(groupName)) return true;
// 匹配当前 Swagger 文档名,只有标记了 GroupName 的接口才显示(如 v1/v2)
return groupName == docName;
});
options.OperationFilter<InheritedObsoleteOperationFilter>(); // 注册自定义过滤器
解决 Schema ID 冲突问题
//options.CustomSchemaIds(type => type.FullName); // 使用完整命名空间
});
return services;
}
public static WebApplication UseCustomSwaggerUI(this WebApplication app)
{
// 获取 IConfiguration
var configuration = app.Services.GetRequiredService<IConfiguration>();
// 获取配置EnableSwagger
bool enableSwagger = configuration.GetValue<bool>("EnableSwagger", false);
if (app.Environment.IsDevelopment() && enableSwagger)
{
app.UseSwagger();
//app.UseSwaggerUI();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v2/swagger.json", "versionV2"); // 这个顺序影响swagger select a definition 默认选中
c.SwaggerEndpoint("/swagger/v1/swagger.json", "versionV1 (Deprecated)");
// API 文档的默认展开方式
c.DocExpansion(Swashbuckle.AspNetCore.SwaggerUI.DocExpansion.List); // 展开分组
//c.DocExpansion(Swashbuckle.AspNetCore.SwaggerUI.DocExpansion.None); // 默认折叠
//c.DocExpansion(Swashbuckle.AspNetCore.SwaggerUI.DocExpansion.Full); // 完全展开所有 API 的详细描述(包括请求参数、响应模型等)
//c.DefaultModelsExpandDepth(-1); // -1:完全折叠,不显示任何模型详情。0:展开模型的顶层属性(单层展开)。1或更高:展开到指定层级。
});
}
// 动态处理根路径重定向
app.MapGet("/", context =>
{
if (enableSwagger)
{
// 重定向到 Swagger 页面
context.Response.Redirect("/swagger");
return Task.CompletedTask;
}
else
{
// 返回其他信息(例如 API 状态)
context.Response.ContentType = "text/plain; charset=utf-8";
return context.Response.WriteAsync($"API 运行中({(app.Environment.IsDevelopment() ? "Development" : "Production")}),Swagger 已禁用。");
}
});
//app.Use(async (context, next) =>
//{
// if (context.Request.Path == "/")
// {
// // 返回自定义响应
// context.Response.ContentType = "text/plain; charset=utf-8";
// await context.Response.WriteAsync("API 运行中,Swagger 已禁用");
// return;
// }
// await next();
//});
return app;
}
}
// 若希望所有继承自 V1BaseController 的子类自动标记为废弃,可通过自定义过滤器实现。
public class InheritedObsoleteOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
// 检查控制器是否继承自带有 [Obsolete] 的基类
var controllerType = context.MethodInfo.DeclaringType;
var baseType = controllerType?.BaseType;
if (baseType != null && baseType.GetCustomAttributes(typeof(ObsoleteAttribute), true).Any())
{
operation.Deprecated = true; // 标记操作已废弃
operation.Description += " (已废弃:此接口所属控制器基类已废弃)";
}
}
}
}
最后把
Program.cs
调整一下,把Swagger配置项加到appsettings
"EnableSwagger": true,
修改后的Program.cs代码
using Admin.NETCore.API.ServiceExtensions;
var builder = WebApplication.CreateBuilder(args);
// Services 配置
builder.Services.AddBasicServices(builder.Configuration);
builder.Services.AddDataBaseServices(builder.Configuration);
builder.Services.AddControllerWithValidation();
//if (builder.Environment.IsDevelopment() && builder.Configuration.GetValue<bool>("EnableSwagger", false))
//{
// builder.Services.AddSwaggerConfig();
//}
var app = builder.Build();
app.ConfigureMiddlewarePipeline();
// Swagger
//if (builder.Environment.IsDevelopment() && builder.Configuration.GetValue<bool>("EnableSwagger", false))
//{
// app.UseCustomSwaggerUI();
//}
app.Run();
运行结果
运行后,页面如下
运行后,页面如下
至此结束