.NET 7.0 EF Core:一、创建Web API 项目基础框架和用户表的增删改查

发布于:2025-06-26 ⋅ 阅读:(23) ⋅ 点赞:(0)

demo 地址: https://github.com/iotjin/Jh.Admin.NETCore
代码不定时更新,请前往github查看最新代码

官方教程

EF Core 官方文档
官方文档: 教程:使用 ASP.NET Core 创建最小 API
EFCore 查询数据
第一个EFCore应用
使用 NuGet 包管理器在 Visual Studio 中安装和管理包
常见的数据库提供程序

NuGet 包网站

本文记录如何使用 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/:控制器,如 UserControllerRoleController
  • Identity/Filters/:请求验证过滤器,如 ValidationFilter.cs
  • ServiceExtensions/:服务注册扩展类(中间件、Swagger、CORS 等)
  • Program.cs:程序主入口
  • appsettings.json:配置文件

2️⃣ Admin.NETCore.Core(核心业务层)

包含系统的业务逻辑、接口定义、DTO/ViewModel,不依赖基础设施层。

主要目录:

  • Interfaces/:接口定义,如 IUserServiceIRoleService
  • 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.CommonAdmin.NETCore.CoreAdmin.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建库建表

第一个EFCore应用

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.CoreAdmin.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();

运行结果

运行后,页面如下

在这里插入图片描述

运行后,页面如下

在这里插入图片描述

至此结束


网站公告

今日签到

点亮在社区的每一天
去签到