Skip to content

本章节详细介绍 Fastdotnet 后端开发的核心概念和最佳实践,包括实体设计、仓储模式、服务层、控制器等。


📖 本章内容


🏗️ 实体设计

Fastdotnet 提供了两个实体基类,所有业务实体都应该继承它们。

1. BaseEntity - 基础实体

文件位置: Fastdotnet.Core.Dtos.Base.BaseEntity

csharp
public class BaseEntity : IBaseEntity, ISoftDelete
{
    /// <summary>
    /// 主键ID (string 类型,支持 GUID/雪花算法)
    /// </summary>
    [SugarColumn(ColumnName = "id", IsPrimaryKey = true)]
    public string Id { get; set; }

    /// <summary>
    /// 创建时间 (自动填充)
    /// </summary>
    [SplitField]
    [SugarColumn(ColumnName = "created_at")]
    public DateTime CreatedAt { get; set; } = DateTime.Now;

    /// <summary>
    /// 更新时间 (自动填充)
    /// </summary>
    [SugarColumn(ColumnName = "updated_at", IsNullable = true)]
    public DateTime? UpdatedAt { get; set; }

    /// <summary>
    /// 是否删除 (软删除标记)
    /// </summary>
    [SugarColumn(ColumnName = "is_deleted")]
    public bool IsDeleted { get; set; } = false;

    /// <summary>
    /// 删除时间 (软删除时自动填充)
    /// </summary>
    [SugarColumn(ColumnName = "deleted_at", IsNullable = true)]
    public DateTime? DeletedAt { get; set; }
}

特性说明:

字段类型说明自动填充
Idstring主键,支持 GUID/雪花ID❌ 需手动设置
CreatedAtDateTime创建时间✅ 是
UpdatedAtDateTime?更新时间✅ 是
IsDeletedbool软删除标记✅ 是
DeletedAtDateTime?删除时间✅ 是

使用示例:

csharp
using Fastdotnet.Core.Dtos.Base;
using SqlSugar;

namespace MyPlugin.Entities
{
    /// <summary>
    /// 任务实体
    /// </summary>
    [SugarTable("my_tasks", "任务表")]
    public class TaskItem : BaseEntity
    {
        /// <summary>
        /// 任务标题
        /// </summary>
        [SugarColumn(Length = 200, IsNullable = false)]
        public string Title { get; set; }

        /// <summary>
        /// 任务描述
        /// </summary>
        [SugarColumn(Length = 1000, IsNullable = true)]
        public string? Description { get; set; }

        /// <summary>
        /// 任务状态:0-待办,1-进行中,2-已完成
        /// </summary>
        public int Status { get; set; } = 0;
    }
}

2. AuditableEntity - 审计实体

文件位置: Fastdotnet.Core.Dtos.Base.AuditableEntity

BaseEntity 基础上增加了操作人信息:

csharp
public class AuditableEntity : BaseEntity, IAuditableEntity
{
    /// <summary>
    /// 创建人ID (自动填充当前用户ID)
    /// </summary>
    [SugarColumn(ColumnName = "created_by", IsNullable = true)]
    public string? CreatedBy { get; set; }

    /// <summary>
    /// 更新人ID (自动填充当前用户ID)
    /// </summary>
    [SugarColumn(ColumnName = "updated_by", IsNullable = true)]
    public string? UpdatedBy { get; set; }

    /// <summary>
    /// 删除人ID (自动填充当前用户ID)
    /// </summary>
    [SugarColumn(ColumnName = "deleted_by", IsNullable = true)]
    public string? DeletedBy { get; set; }
}

何时使用 AuditableEntity?

  • ✅ 需要追踪谁创建了数据
  • ✅ 需要追踪谁修改了数据
  • ✅ 需要追踪谁删除了数据
  • ✅ 业务数据(订单、文章、评论等)

何时使用 BaseEntity?

  • ✅ 不需要追踪操作人
  • ✅ 配置数据、字典数据
  • ✅ 系统内部使用的数据

使用示例:

csharp
/// <summary>
/// 订单实体(需要审计)
/// </summary>
[SugarTable("my_orders", "订单表")]
public class Order : AuditableEntity
{
    [SugarColumn(Length = 50)]
    public string OrderNo { get; set; }
    
    public decimal Amount { get; set; }
    
    public int Status { get; set; }
}

// 创建订单时,CreatedBy 会自动填充为当前登录用户ID
var order = new Order 
{ 
    OrderNo = "ORD20240101001",
    Amount = 99.99m,
    Status = 0
};
await _repository.InsertAsync(order);
// order.CreatedBy 自动设置为当前用户ID

📦 仓储模式

Fastdotnet 提供了强大的通用仓储接口 IRepository<T>;,封装了常用的 CRUD 操作。

1. IRepository 接口概览

文件位置: Fastdotnet.Core.IService.IRepository

csharp
public interface IRepository<T, TKey>; where T : BaseEntity, new()
{
    // 查询操作
    Task<T>; GetByIdAsync(TKey id);
    Task<List<T>;>; GetAllAsync();
    Task<List<T>;>; GetListAsync(Expression<Func<T, bool>;>; where);
    Task<T>; GetFirstAsync(Expression<Func<T, bool>;>; where);
    Task<PageResult<T>;>; GetPageAsync(...);
    Task<bool>; ExistsAsync(Expression<Func<T, bool>;>; where);
    
    // 插入操作
    Task<T>; InsertAsync(T entity);
    Task<List<T>;>; InsertRangeAsync(List<T>; entities);
    
    // 更新操作
    Task UpdateAsync(T entity);
    Task UpdateRangeAsync(List<T>; entities);
    
    // 删除操作
    Task DeleteAsync(TKey id);
    Task DeleteAsync(T entity);
    Task DeleteRangeAsync(List<T>; entities);
    
    // 高级查询
    Task<List<TResult>;>; GetListAsync<TResult>;(
        Expression<Func<T, bool>;>; where,
        Expression<Func<T, TResult>;>; select);
}

2. 查询操作详解

① 根据 ID 查询

csharp
// 单个查询
var task = await _repository.GetByIdAsync("task_id_123");

// 如果不存在返回 null
if (task == null)
{
    throw new BusinessException("任务不存在");
}

② 条件查询

csharp
// 查询所有未完成的任务
var tasks = await _repository.GetListAsync(t => t.Status == 0);

// 复杂条件
var tasks = await _repository.GetListAsync(t => 
    t.Status == 0 && 
    t.CreatedAt > DateTime.Now.AddDays(-7));

③ 投影查询(只查询需要的字段)

csharp
// 只查询 ID 和标题,提高性能
var taskSummaries = await _repository.GetListAsync(
    t => t.Status == 0,
    t => new { t.Id, t.Title }
);

④ 分页查询

csharp
// 基本分页
var pageResult = await _repository.GetPageAsync(
    pageIndex: 1,
    pageSize: 10,
    orderByExpression: t => t.CreatedAt,
    orderByType: OrderByType.Desc
);

// 带条件的分页
var pageResult = await _repository.GetPageAsync(
    whereExpression: t => t.Status == 0,
    pageIndex: 1,
    pageSize: 10,
    orderByExpression: t => t.CreatedAt,
    orderByType: OrderByType.Desc
);

// 返回结果
Console.WriteLine($"总记录数: {pageResult.Total}");
Console.WriteLine($"总页数: {pageResult.Pages}");
Console.WriteLine($"当前页数据: {pageResult.Items.Count}");

⑤ 判断是否存在

csharp
// 检查任务标题是否已存在
bool exists = await _repository.ExistsAsync(t => t.Title == "测试任务");

if (exists)
{
    throw new BusinessException("任务标题已存在");
}

3. 插入操作

csharp
// 单个插入
var task = new TaskItem 
{ 
    Id = Guid.NewGuid().ToString(),
    Title = "新任务",
    Status = 0
};
await _repository.InsertAsync(task);

// 批量插入
var tasks = new List<TaskItem>
{
    new TaskItem { Id = "1", Title = "任务1" },
    new TaskItem { Id = "2", Title = "任务2" },
    new TaskItem { Id = "3", Title = "任务3" }
};
await _repository.InsertRangeAsync(tasks);

4. 更新操作

csharp
// 单个更新
var task = await _repository.GetByIdAsync("task_id");
task.Status = 1;
task.Title = "更新后的标题";
await _repository.UpdateAsync(task);

// 批量更新
var tasks = await _repository.GetListAsync(t => t.Status == 0);
foreach (var task in tasks)
{
    task.Status = 1;
}
await _repository.UpdateRangeAsync(tasks);

5. 删除操作(软删除)

csharp
// 单个删除(软删除)
await _repository.DeleteAsync("task_id");
// 实际上是将 IsDeleted 设置为 true,DeletedAt 设置为当前时间

// 批量删除
var tasks = await _repository.GetListAsync(t => t.Status == 2);
await _repository.DeleteRangeAsync(tasks);

**注意:**物理删除需要直接调用 SqlSugar 的 Delete 方法,请谨慎使用。


🔧 服务层

服务层负责业务逻辑处理,通过依赖注入使用仓储。

1. IBaseService 接口

文件位置: Fastdotnet.Core.IService.IBaseService

csharp
public interface IBaseService<T>; where T : BaseEntity, new()
{
    IRepository<T>; Repository { get; }
    
    // 继承 IRepository 的所有方法
    // 可以添加自定义的业务方法
}

2. 创建服务类

csharp
using Fastdotnet.Core.IService;
using MyPlugin.Entities;

namespace MyPlugin.Services
{
    public interface ITaskService : IBaseService<TaskItem>
    {
        // 自定义业务方法
        Task CompleteTaskAsync(string taskId);
        Task<List<TaskItem>;>; GetOverdueTasksAsync();
    }

    public class TaskService : BaseService<TaskItem>;, ITaskService
    {
        private readonly ICurrentUser _currentUser;

        public TaskService(
            IRepository<TaskItem>; repository,
            ICurrentUser currentUser) 
            : base(repository)
        {
            _currentUser = currentUser;
        }

        /// <summary>
        /// 完成任务
        /// </summary>
        public async Task CompleteTaskAsync(string taskId)
        {
            var task = await Repository.GetByIdAsync(taskId);
            
            if (task == null)
            {
                throw new BusinessException("任务不存在");
            }

            // 权限检查:只能完成自己的任务
            if (task.CreatedBy != _currentUser.Id)
            {
                throw new BusinessException("无权操作此任务");
            }

            task.Status = 2; // 已完成
            await Repository.UpdateAsync(task);
        }

        /// <summary>
        /// 获取逾期任务
        /// </summary>
        public async Task<List<TaskItem>;>; GetOverdueTasksAsync()
        {
            return await Repository.GetListAsync(t => 
                t.Status != 2 && // 未完成
                t.DueDate < DateTime.Now // 已逾期
            );
        }
    }
}

3. 注册服务

在插件的 ConfigureServices 方法中注册:

csharp
public override void ConfigureServices(ContainerBuilder builder)
{
    // 注册服务
    builder.RegisterType<TaskService>()
        .As<ITaskService>()
        .InstancePerLifetimeScope();
}

💾 工作单元(事务管理)

1. IUnitOfWork 接口

文件位置: Fastdotnet.Core.IService.IUnitOfWork

csharp
public interface IUnitOfWork : IDisposable
{
    /// <summary>
    /// 开始事务
    /// </summary>
    void BeginTransaction();

    /// <summary>
    /// 提交事务
    /// </summary>
    Task CommitTransactionAsync();

    /// <summary>
    /// 回滚事务
    /// </summary>
    Task RollbackTransactionAsync();
}

2. 使用事务

csharp
public class OrderService : BaseService<Order>;, IOrderService
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly IRepository<OrderItem>; _orderItemRepository;

    public OrderService(
        IRepository<Order>; repository,
        IRepository<OrderItem>; orderItemRepository,
        IUnitOfWork unitOfWork) 
        : base(repository)
    {
        _orderItemRepository = orderItemRepository;
        _unitOfWork = unitOfWork;
    }

    /// <summary>
    /// 创建订单(包含订单项)
    /// </summary>
    public async Task<string> CreateOrderAsync(CreateOrderDto dto)
    {
        _unitOfWork.BeginTransaction();
        
        try
        {
            // 1. 创建订单
            var order = new Order
            {
                Id = Guid.NewGuid().ToString(),
                OrderNo = GenerateOrderNo(),
                Amount = dto.Items.Sum(i => i.Price * i.Quantity),
                Status = 0
            };
            await Repository.InsertAsync(order);

            // 2. 创建订单项
            var orderItems = dto.Items.Select(item => new OrderItem
            {
                Id = Guid.NewGuid().ToString(),
                OrderId = order.Id,
                ProductId = item.ProductId,
                Quantity = item.Quantity,
                Price = item.Price
            }).ToList();
            await _orderItemRepository.InsertRangeAsync(orderItems);

            // 3. 提交事务
            await _unitOfWork.CommitTransactionAsync();
            
            return order.Id;
        }
        catch (Exception ex)
        {
            // 4. 回滚事务
            await _unitOfWork.RollbackTransactionAsync();
            throw new BusinessException($"创建订单失败: {ex.Message}");
        }
    }
}

🎮 控制器

Fastdotnet 提供了强大的基类控制器,可以快速实现 CRUD API。

1. GenericDtoControllerBase

文件位置: Fastdotnet.Core.Controllers.GenericDtoControllerBase

这是最常用的控制器基类,提供了完整的 CRUD API。

① 最简单的用法

csharp
using Fastdotnet.Core.Controllers;
using Fastdotnet.Core.IService;
using MyPlugin.Entities;

namespace MyPlugin.Controllers
{
    [Route("api/my-plugin/[controller]")]
    public class TaskController : GenericDtoControllerBase<TaskItem>
    {
        public TaskController(IBaseService<TaskItem>; service) 
            : base(service)
        {
        }

        // 自动继承以下 API:
        // GET    /api/my-plugin/task              - 分页列表
        // GET    /api/my-plugin/task/{id}         - 详情
        // POST   /api/my-plugin/task              - 创建
        // PUT    /api/my-plugin/task/{id}         - 更新
        // DELETE /api/my-plugin/task/{id}         - 删除(软删除)
        // POST   /api/my-plugin/task/batch-delete - 批量删除
        // POST   /api/my-plugin/task/search       - 高级搜索
    }
}

② 添加自定义 API

csharp
[Route("api/my-plugin/[controller]")]
public class TaskController : GenericDtoControllerBase<TaskItem>
{
    private readonly ITaskService _taskService;

    public TaskController(IBaseService<TaskItem>; service, ITaskService taskService) 
        : base(service)
    {
        _taskService = taskService;
    }

    /// <summary>
    /// 完成任务
    /// </summary>
    [HttpPost("{id}/complete")]
    public async Task<IActionResult> CompleteTask(string id)
    {
        await _taskService.CompleteTaskAsync(id);
        return Ok(new { success = true });
    }

    /// <summary>
    /// 获取逾期任务
    /// </summary>
    [HttpGet("overdue")]
    public async Task<IActionResult> GetOverdueTasks()
    {
        var tasks = await _taskService.GetOverdueTasksAsync();
        return Ok(tasks);
    }
}

2. AppGenericDtoControllerBase

用于应用端的控制器,功能与 GenericDtoControllerBase 类似,但针对应用端做了优化。

csharp
[Route("api/my-plugin/[controller]")]
[ApiUsageScope(ApiUsageScopeEnum.AppOnly)]  // 仅应用端可访问
public class AppTaskController : AppGenericDtoControllerBase<TaskItem>
{
    public AppTaskController(IBaseService<TaskItem>; service) 
        : base(service)
    {
    }
}

3. 高级搜索功能

GenericDtoControllerBase 内置了强大的动态搜索功能:

http
POST /api/my-plugin/task/search
Content-Type: application/json

{
  "pageIndex": 1,
  "pageSize": 10,
  "conditions": [
    {
      "propertyName": "Status",
      "operator": "Equal",
      "value": 0
    },
    {
      "propertyName": "Title",
      "operator": "Contains",
      "value": "测试"
    },
    {
      "propertyName": "CreatedAt",
      "operator": "Between",
      "value": "2024-01-01",
      "value2": "2024-12-31"
    }
  ],
  "logic": "And",
  "orderBy": "CreatedAt",
  "orderType": "Desc"
}

支持的操作符:

操作符说明示例
Equal等于Status = 0
NotEqual不等于Status != 0
GreaterThan大于Price >; 100
LessThan小于Price < 100
Contains包含Title LIKE '%测试%'
StartsWith开头Title LIKE '测试%'
In在列表中Status IN (0, 1)
Between范围CreatedAt BETWEEN ... AND ...

📋 DTO 设计

1. DTO 命名规范

csharp
// 创建 DTO
public class CreateTaskDto
{
    public string Title { get; set; }
    public string? Description { get; set; }
    public DateTime? DueDate { get; set; }
}

// 更新 DTO
public class UpdateTaskDto
{
    public string Title { get; set; }
    public int Status { get; set; }
}

// 响应 DTO
public class TaskDto
{
    public string Id { get; set; }
    public string Title { get; set; }
    public int Status { get; set; }
    public string CreatedBy { get; set; }
    public DateTime CreatedAt { get; set; }
}

2. 使用 AutoMapper 映射

csharp
// 在 Mappings 目录创建映射配置
public class TaskMappingProfile : Profile
{
    public TaskMappingProfile()
    {
        CreateMap<TaskItem, TaskDto>();
        CreateMap<CreateTaskDto, TaskItem>();
        CreateMap<UpdateTaskDto, TaskItem>()
            .ForAllMembers(opts => opts.Condition((src, dest, srcMember) => srcMember != null));
    }
}

💉 依赖注入

1. 服务注册

在插件入口类中注册服务:

csharp
public class MyPluginPlugin : PluginBase
{
    public override void ConfigureServices(ContainerBuilder builder)
    {
        // 注册仓储(自动注册,无需手动)
        
        // 注册服务
        builder.RegisterType<TaskService>()
            .As<ITaskService>()
            .InstancePerLifetimeScope();
            
        // 注册单例服务
        builder.RegisterType<CacheService>()
            .As<ICacheService>()
            .SingleInstance();
    }
}

2. 构造函数注入

csharp
public class TaskController : ControllerBase
{
    private readonly ITaskService _taskService;
    private readonly ICurrentUser _currentUser;
    private readonly ILogger<TaskController> _logger;

    public TaskController(
        ITaskService taskService,
        ICurrentUser currentUser,
        ILogger<TaskController> logger)
    {
        _taskService = taskService;
        _currentUser = currentUser;
        _logger = logger;
    }
}

🔧 核心服务

Fastdotnet 提供了一系列核心服务,简化常见开发任务。

1. 混合缓存服务 (IHybridCacheService)

文件位置: Fastdotnet.Core.IService.IHybridCacheService

特性说明

  • 双层架构: 本地缓存 + 分布式缓存(Redis)
  • 标签管理: 支持按标签批量清除缓存
  • 过期策略: 灵活的过期时间配置
  • 高性能: 自动优化缓存命中率

基本用法

csharp
public class ProductService : BaseService<Product>;, IProductService
{
    private readonly IHybridCacheService _cache;

    public ProductService(
        IRepository<Product>; repository,
        IHybridCacheService cache) 
        : base(repository)
    {
        _cache = cache;
    }

    /// <summary>
    /// 获取产品详情(带缓存)
    /// </summary>
    public async Task<ProductDto> GetProductAsync(string id)
    {
        var cacheKey = $"product:{id}";
        
        // GetOrCreate: 如果缓存存在则返回,否则执行 factory 并缓存结果
        return await _cache.GetOrCreateAsync(
            cacheKey,
            async () => 
            {
                // 从数据库查询
                var product = await Repository.GetByIdAsync(id);
                return Mapper.Map<ProductDto>(product);
            },
            options: new HybridCacheEntryOptions 
            { 
                Expiration = TimeSpan.FromMinutes(30) 
            },
            tags: new[] { "products", $"product:{id}" }
        );
    }
}

设置缓存

csharp
// 简单设置
await _cache.SetAsync("key", value);

// 带过期时间
await _cache.SetAsync(
    "key", 
    value, 
    new HybridCacheEntryOptions 
    { 
        Expiration = TimeSpan.FromHours(1) 
    }
);

// 带标签
await _cache.SetAsync(
    "key",
    value,
    tags: new[] { "tag1", "tag2" }
);

获取缓存

csharp
// 获取缓存(不存在返回默认值)
var value = await _cache.GetAsync<MyType>("key");

if (value == null)
{
    // 缓存不存在
}

清除缓存

csharp
// 清除单个缓存
await _cache.RemoveAsync("key");

// 根据标签批量清除(强烈推荐)
await _cache.RemoveByTagAsync(new[] { "products" });
// 这会清除所有标记为 "products" 的缓存项

实际应用场景

场景 1: 字典数据缓存

csharp
public async Task<List<DictionaryItem>> GetDictionariesAsync(string type)
{
    return await _cache.GetOrCreateAsync(
        $"dict:{type}",
        async () => await _dictRepository.GetListAsync(d => d.Type == type),
        tags: new[] { "dictionaries", $"dict:{type}" }
    );
}

// 更新字典后清除缓存
public async Task UpdateDictionaryAsync(DictionaryItem item)
{
    await _dictRepository.UpdateAsync(item);
    await _cache.RemoveByTagAsync(new[] { $"dict:{item.Type}" });
}

场景 2: 用户权限缓存

csharp
public async Task<List<string>> GetUserPermissionsAsync(string userId)
{
    return await _cache.GetOrCreateAsync(
        $"user:permissions:{userId}",
        async () => await LoadPermissionsFromDb(userId),
        new HybridCacheEntryOptions 
        { 
            Expiration = TimeSpan.FromHours(2) 
        },
        tags: new[] { $"user:{userId}", "permissions" }
    );
}

// 权限变更后清除
public async Task UpdateUserRoleAsync(string userId, string roleId)
{
    // ... 更新逻辑
    
    // 清除该用户的所有缓存
    await _cache.RemoveByTagAsync(new[] { $"user:{userId}" });
}

场景 3: 统计数据缓存

csharp
public async Task<OrderStatistics> GetOrderStatisticsAsync(DateTime date)
{
    return await _cache.GetOrCreateAsync(
        $"stats:orders:{date:yyyy-MM-dd}",
        async () => await CalculateStatistics(date),
        new HybridCacheEntryOptions 
        { 
            Expiration = TimeSpan.FromHours(1) 
        }
    );
}

最佳实践

csharp
✅ 推荐:
- 使用标签管理缓存,便于批量清除
- 为不同类型的缓存设置合理的过期时间
- 字典、配置等不常变数据可以缓存较长时间
- 用户相关数据使用用户ID作为标签

❌ 避免:
- 缓存大量临时数据
- 忘记清除过期缓存
- 不使用标签导致无法批量清除
- 缓存敏感信息(密码、Token等)

控制器方法缓存特性(⭐ 推荐)

Fastdotnet 提供了两个强大的缓存特性,可以在控制器层面自动缓存 API 响应结果,无需手动编写缓存逻辑。

1. CacheResultAttribute - 方法结果缓存

在控制器方法上添加 [CacheResult] 特性,自动缓存方法的返回结果:

csharp
[Route("api/products")]
public class ProductController : ControllerBase
{
    private readonly IProductService _productService;

    public ProductController(IProductService productService)
    {
        _productService = productService;
    }

    /// <summary>
    /// 获取产品列表(自动缓存 5 分钟)
    /// </summary>
    [HttpGet]
    [CacheResult(ExpirationSeconds = 300)]  // 缓存 5 分钟
    public async Task<IActionResult> GetList()
    {
        var products = await _productService.GetAllAsync();
        return Ok(products);
    }

    /// <summary>
    /// 获取产品详情(自动缓存 30 分钟)
    /// </summary>
    [HttpGet("{id}")]
    [CacheResult(ExpirationSeconds = 1800)]  // 缓存 30 分钟
    public async Task<IActionResult> GetById(string id)
    {
        var product = await _productService.GetByIdAsync(id);
        return Ok(product);
    }
}

特性参数:

参数类型说明默认值
KeyPrefixstring自定义缓存键前缀自动生成
ExpirationSecondsint缓存过期时间(秒)使用配置文件默认值
Enabledbool是否启用缓存true

工作原理:

  1. 自动生成缓存键:根据控制器类型、方法名和参数生成唯一缓存键
  2. 智能参数签名:对请求参数进行哈希处理,确保相同参数的请求命中缓存
  3. 自动序列化/反序列化:使用 Newtonsoft.Json 序列化和反序列化缓存数据
  4. 支持标签管理:配合 CacheTagAttribute 实现批量清除

2. CacheTagAttribute - 缓存标签

为缓存项添加标签,便于批量清除相关缓存:

csharp
[Route("api/products")]
[CacheTag("products")]  // 控制器级别标签
public class ProductController : ControllerBase
{
    /// <summary>
    /// 获取产品列表
    /// </summary>
    [HttpGet]
    [CacheResult(ExpirationSeconds = 300)]
    [CacheTag("product-list")]  // 方法级别标签
    public async Task<IActionResult> GetList()
    {
        // ...
    }

    /// <summary>
    /// 创建产品(清除相关缓存)
    /// </summary>
    [HttpPost]
    public async Task<IActionResult> Create([FromBody] CreateProductDto dto)
    {
        var product = await _productService.CreateAsync(dto);
        
        // 清除所有标记为 "products" 的缓存
        var cacheService = HttpContext.RequestServices.GetService<IHybridCacheService>();
        await cacheService.RemoveByTagAsync(new[] { "products" });
        
        return Ok(product);
    }
}

使用场景:

场景 1: 字典数据缓存

csharp
[Route("api/dictionaries")]
public class DictionaryController : ControllerBase
{
    [HttpGet("{type}")]
    [CacheResult(ExpirationSeconds = 3600)]  // 缓存 1 小时
    [CacheTag("dictionaries")]
    public async Task<IActionResult> GetByType(string type)
    {
        var items = await _dictService.GetByTypeAsync(type);
        return Ok(items);
    }

    [HttpPost]
    public async Task<IActionResult> Create([FromBody] DictionaryItem item)
    {
        await _dictService.CreateAsync(item);
        
        // 清除所有字典缓存
        var cacheService = HttpContext.RequestServices.GetService<IHybridCacheService>();
        await cacheService.RemoveByTagAsync(new[] { "dictionaries" });
        
        return Ok();
    }
}

场景 2: 用户权限缓存

csharp
[Route("api/users")]
public class UserController : ControllerBase
{
    [HttpGet("{userId}/permissions")]
    [CacheResult(ExpirationSeconds = 7200)]  // 缓存 2 小时
    [CacheTag("permissions")]
    public async Task<IActionResult> GetPermissions(string userId)
    {
        var permissions = await _userService.GetPermissionsAsync(userId);
        return Ok(permissions);
    }

    [HttpPost("{userId}/role")]
    public async Task<IActionResult> UpdateRole(string userId, [FromBody] UpdateRoleDto dto)
    {
        await _userService.UpdateRoleAsync(userId, dto.RoleId);
        
        // 清除该用户的权限缓存
        var cacheService = HttpContext.RequestServices.GetService<IHybridCacheService>();
        await cacheService.RemoveByTagAsync(new[] { $"user:{userId}" });
        
        return Ok();
    }
}

场景 3: 统计数据缓存

csharp
[Route("api/statistics")]
public class StatisticsController : ControllerBase
{
    [HttpGet("orders/{date}")]
    [CacheResult(ExpirationSeconds = 3600)]  // 缓存 1 小时
    [CacheTag("statistics")]
    public async Task<IActionResult> GetOrderStatistics(DateTime date)
    {
        var stats = await _statsService.GetOrderStatisticsAsync(date);
        return Ok(stats);
    }
}

最佳实践:

csharp
✅ 推荐:
- 为只读 GET 接口添加缓存特性
- 使用有意义的标签名称,便于管理和清除
- 根据数据更新频率设置合理的过期时间
- 在数据变更时及时清除相关缓存

❌ 避免:
- 不要对 POST/PUT/DELETE 等非幂等操作使用缓存
- 不要缓存包含敏感信息的响应
- 不要忘记在数据变更时清除缓存
- 不要设置过长的过期时间导致数据不一致

2. 日志服务 (ILogService)

文件位置: Fastdotnet.Core.IService.ILogService

日志类型

日志类型方法用途
操作日志AddOperationLogAsync记录用户操作(增删改查)
异常日志AddExceptionLogAsync记录系统异常
调试日志AddDebugLogAsync开发调试信息

操作日志

csharp
public class OrderController : GenericDtoControllerBase<Order>
{
    private readonly ILogService _logService;
    private readonly ICurrentUser _currentUser;

    public OrderController(
        IBaseService<Order>; service,
        ILogService logService,
        ICurrentUser currentUser) 
        : base(service)
    {
        _logService = logService;
        _currentUser = currentUser;
    }

    [HttpPost]
    public async Task<IActionResult> CreateOrder([FromBody] CreateOrderDto dto)
    {
        try
        {
            var order = await _service.InsertAsync(Mapper.Map<Order>(dto));
            
            // 记录操作日志
            await _logService.AddOperationLogAsync(new OperationLog
            {
                UserId = _currentUser.Id,
                UserName = _currentUser.UserName,
                Operation = "创建订单",
                Module = "订单管理",
                Method = "POST",
                Url = "/api/orders",
                IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(),
                RequestData = JsonSerializer.Serialize(dto),
                ResponseData = $"{{\"orderId\":\"{order.Id}\"}}",
                Status = 200,
                Duration = 0, // 可以计算耗时
                CreatedAt = DateTime.Now
            });
            
            return Ok(order);
        }
        catch (Exception ex)
        {
            // 记录异常日志
            await _logService.AddExceptionLogAsync(new ExceptionLog
            {
                UserId = _currentUser.Id,
                UserName = _currentUser.UserName,
                ExceptionType = ex.GetType().Name,
                Message = ex.Message,
                StackTrace = ex.StackTrace,
                Url = "/api/orders",
                Method = "POST",
                RequestData = JsonSerializer.Serialize(dto),
                CreatedAt = DateTime.Now
            });
            
            throw;
        }
    }
}

异步 vs 同步

csharp
// 异步(推荐)
await _logService.AddOperationLogAsync(log);

// 同步(适用于无法使用 async 的场景)
_logService.AddOperationLog(log);

使用中间件自动记录

Fastdotnet 提供了操作日志中间件,可以自动记录所有 API 请求:

csharp
// 在 Program.cs 中配置
app.UseOperationLogMiddleware();

3. 当前用户服务 (ICurrentUser)

文件位置: Fastdotnet.Core.IService.ICurrentUser

接口定义

csharp
public interface ICurrentUser
{
    /// <summary>
    /// 是否已认证
    /// </summary>
    bool IsAuthenticated { get; }

    /// <summary>
    /// 用户ID
    /// </summary>
    string? Id { get; }

    /// <summary>
    /// 用户名
    /// </summary>
    string UserName { get; }

    /// <summary>
    /// 用户类型:Admin 或 App
    /// </summary>
    string UserType { get; }

    /// <summary>
    /// 是否为超级管理员
    /// </summary>
    bool IsSuperAdmin { get; }
}

基本用法

csharp
public class TaskService : BaseService<TaskItem>;, ITaskService
{
    private readonly ICurrentUser _currentUser;

    public TaskService(
        IRepository<TaskItem>; repository,
        ICurrentUser currentUser) 
        : base(repository)
    {
        _currentUser = currentUser;
    }

    public async Task CreateTaskAsync(CreateTaskDto dto)
    {
        var task = new TaskItem
        {
            Id = Guid.NewGuid().ToString(),
            Title = dto.Title,
            CreatedBy = _currentUser.Id,  // 自动填充创建人
            // ... 其他字段
        };
        
        await Repository.InsertAsync(task);
    }

    public async Task DeleteTaskAsync(string taskId)
    {
        var task = await Repository.GetByIdAsync(taskId);
        
        if (task == null)
        {
            throw new BusinessException("任务不存在");
        }

        // 权限检查:只能删除自己的任务(超级管理员除外)
        if (!_currentUser.IsSuperAdmin && task.CreatedBy != _currentUser.Id)
        {
            throw new BusinessException("无权删除此任务");
        }

        await Repository.DeleteAsync(taskId);
    }
}

判断用户类型

csharp
public async Task<object> GetUserInfoAsync()
{
    if (!_currentUser.IsAuthenticated)
    {
        throw new BusinessException("用户未登录");
    }

    return new
    {
        Id = _currentUser.Id,
        UserName = _currentUser.UserName,
        UserType = _currentUser.UserType, // "Admin" or "App"
        IsSuperAdmin = _currentUser.IsSuperAdmin
    };
}

在控制器中使用

csharp
[HttpGet("my-tasks")]
public async Task<IActionResult> GetMyTasks()
{
    // 只查询当前用户的任务
    var tasks = await _repository.GetListAsync(
        t => t.CreatedBy == _currentUser.Id
    );
    
    return Ok(tasks);
}

4. 用户引用填充服务 (IUserRefFiller)

文件位置: Fastdotnet.Core.IService.Sys.IUserRefFiller

功能说明

将 DTO 中的用户ID自动转换为用户姓名,避免手动查询。

UserRefDto 结构

csharp
public class UserRefDto
{
    /// <summary>
    /// 用户ID
    /// </summary>
    public string Id { get; set; } = string.Empty;

    /// <summary>
    /// 用户姓名
    /// </summary>
    public string Name { get; set; } = "未知用户";
}

使用示例

步骤 1: 在 DTO 中添加 UserRefDto 属性

csharp
public class TaskDto
{
    public string Id { get; set; }
    public string Title { get; set; }
    
    // 原始用户ID
    public string CreatedBy { get; set; }
    
    // 用户引用信息(自动填充)
    public UserRefDto CreatedByUser { get; set; }
    
    public string UpdatedBy { get; set; }
    public UserRefDto UpdatedByUser { get; set; }
}

步骤 2: 在控制器中调用 FillNamesAsync

csharp
[HttpGet]
public async Task<IActionResult> GetTasks()
{
    var tasks = await _service.GetListAsync(t => true);
    var dtos = Mapper.Map<List<TaskDto>>(tasks);
    
    // 自动填充用户姓名
    await _userRefFiller.FillNamesAsync(
        dtos,
        SystemCategory.Admin,  // 用户类型:Admin 或 App
        dto => dto.CreatedByUser,  // 指定要填充的字段
        dto => dto.UpdatedByUser
    );
    
    return Ok(dtos);
}

步骤 3: 返回结果

[
  {
    "id": "task_001",
    "title": "测试任务",
    "createdBy": "user_123",
    "createdByUser": {
      "id": "user_123",
      "name": "张三"
    },
    "updatedBy": "user_456",
    "updatedByUser": {
      "id": "user_456",
      "name": "李四"
    }
  }
]

批量填充

csharp
// 分页查询后填充
var pageResult = await _repository.GetPageAsync(1, 10);
var dtos = Mapper.Map<List<TaskDto>>(pageResult.Items);

// 批量填充(性能优化:一次性查询所有用户)
await _userRefFiller.FillNamesAsync(
    dtos,
    SystemCategory.Admin,
    dto => dto.CreatedByUser
);

多个用户字段

csharp
public class OrderDto
{
    public string Id { get; set; }
    
    public string CreatedBy { get; set; }
    public UserRefDto CreatedByUser { get; set; }
    
    public string ApprovedBy { get; set; }
    public UserRefDto ApprovedByUser { get; set; }
    
    public string CancelledBy { get; set; }
    public UserRefDto CancelledByUser { get; set; }
}

// 一次性填充所有用户字段
await _userRefFiller.FillNamesAsync(
    orders,
    SystemCategory.Admin,
    dto => dto.CreatedByUser,
    dto => dto.ApprovedByUser,
    dto => dto.CancelledByUser
);

优势

csharp
✅ 传统方式(N+1 查询问题):
foreach (var task in tasks)
{
    var user = await _userRepository.GetByIdAsync(task.CreatedBy);  // N次查询
    task.CreatedByName = user.Name;
}

✅ 使用 IUserRefFiller(优化为 1 次查询):
await _userRefFiller.FillNamesAsync(tasks, SystemCategory.Admin, t => t.CreatedByUser);
// 内部实现:
// 1. 收集所有 UserID
// 2. 一次性查询所有用户
// 3. 映射到对应的 DTO

5. 数据脱敏服务 ⭐

文件位置:

  • Fastdotnet.Core.Attributes.SensitiveDataAttribute
  • Fastdotnet.Core.Extensions.AutoMapperSensitiveDataExtensions

功能说明

在数据传输过程中自动对敏感信息进行脱敏处理,保护用户隐私和数据安全。

SensitiveDataAttribute

通过特性标记需要脱敏的字段,支持多种预定义的数据类型:

csharp
public enum SensitiveDataType
{
    Phone,      // 手机号
    Email,      // 邮箱
    IdCard,     // 身份证
    BankCard,   // 银行卡
    Name,       // 姓名
    Custom      // 自定义规则
}

使用示例

步骤 1: 在 DTO 中标记敏感字段

csharp
public class UserDto
{
    public string Id { get; set; }
    
    [SensitiveData(SensitiveDataType.Name)]
    public string Name { get; set; }
    
    [SensitiveData(SensitiveDataType.Phone)]
    public string Phone { get; set; }
    
    [SensitiveData(SensitiveDataType.Email)]
    public string Email { get; set; }
    
    [SensitiveData(SensitiveDataType.IdCard)]
    public string IdCard { get; set; }
    
    [SensitiveData(SensitiveDataType.BankCard)]
    public string BankCard { get; set; }
    
    // 自定义脱敏规则
    [SensitiveData(SensitiveDataType.Custom, 
        CustomPattern = @"\d{4}(\d{4})\d{4}", 
        CustomReplacement = "****$1****")]
    public string CustomField { get; set; }
}

步骤 2: 在 AutoMapper 映射中启用脱敏

csharp
public class UserProfile : Profile
{
    public UserProfile()
    {
        CreateMap<User, UserDto>()
            .MaskSensitiveData<User, UserDto>();  // 启用脱敏
    }
}

步骤 3: 返回结果

json
{
  "id": "user_123",
  "name": "张*",           // 姓名脱敏
  "phone": "138****5678",   // 手机号脱敏
  "email": "ex***le@domain.com",  // 邮箱脱敏
  "idCard": "110101********1234",   // 身份证脱敏
  "bankCard": "622202******7890"    // 银行卡脱敏
}

脱敏规则

数据类型示例脱敏后
Phone13812345678138****5678
Emailexample@domain.comex***le@domain.com
IdCard110101199001011234110101********1234
BankCard6222021234567890622202******7890
Name张三张*

自定义脱敏参数

对于更精细的脱敏需求,可以使用自定义参数:

csharp
public class CustomUserDto
{
    [SensitiveData(SensitiveDataType.Phone, 
        PrefixKeep = 2,     // 保留前缀2位
        SuffixKeep = 2,     // 保留后缀2位
        MaskChar = '#')]    // 使用 # 作为脱敏字符
    public string Phone { get; set; }
    
    [SensitiveData(SensitiveDataType.Name, 
        PrefixKeep = 1,     // 保留前缀1位
        SuffixKeep = 0,     // 不保留后缀
        MaskChar = '*',
        MaskLength = 2)]    // 脱敏部分长度为2
    public string Name { get; set; }
}

自定义脱敏规则

使用正则表达式定义完全自定义的脱敏规则:

csharp
public class CustomFieldDto
{
    [SensitiveData(SensitiveDataType.Custom,
        CustomPattern = @"^(.{3}).*(.{4})$",     // 匹配前后固定字符
        CustomReplacement = "$1****$2")]        // 替换为保留前后字符,中间用****
    public string Field { get; set; }
}

在服务层使用

csharp
public class UserService : BaseService<User>;, IUserService
{
    public async Task<UserDto> GetUserAsync(string id)
    {
        var user = await Repository.GetByIdAsync(id);
        
        // 自动应用脱敏规则
        var dto = Mapper.Map<UserDto>(user);
        
        return dto;
    }
    
    public async Task<List<UserDto>> GetUsersAsync()
    {
        var users = await Repository.GetListAsync(_ => true);
        
        // 批量脱敏
        var dtos = users.Select(user => Mapper.Map<UserDto>(user)).ToList();
        
        return dtos;
    }
}

优势

csharp
✅ 集中式管理:通过特性统一配置脱敏规则
✅ 类型安全:编译时检查脱敏配置
✅ 自动应用:映射时自动执行脱敏
✅ 灵活配置:支持自定义参数和规则
✅ 性能良好:脱敏操作在映射阶段完成

❌ 避免手动脱敏:
// 不推荐
userDto.Phone = user.Phone.Substring(0, 3) + "****" + user.Phone.Substring(7);

最佳实践

csharp
✅ 推荐:
- 在 DTO 层标记敏感字段
- 使用预定义的数据类型
- 在 AutoMapper 配置中启用脱敏
- 定期审查敏感字段标记

⚠️ 注意:
- 仅对需要对外暴露的敏感数据进行脱敏
- 不要在数据库层进行脱敏存储
- 脱敏仅用于数据传输,原始数据保持完整

💡 最佳实践

1. 实体设计规范

csharp
✅ 推荐:
- 继承 BaseEntity 或 AuditableEntity
- 使用 [SugarColumn] 明确指定列名
- 字符串字段指定 Length
- 可空字段使用 nullable 类型
- 添加清晰的注释

❌ 避免:
- 不继承基类
- 使用魔法数字
- 忽略索引优化

2. 查询优化

csharp
✅ 推荐:
// 只查询需要的字段
var titles = await _repository.GetListAsync(
    t => t.Status == 0,
    t => t.Title
);

// 使用分页
var page = await _repository.GetPageAsync(1, 10);

// 添加索引
[SugarIndex("idx_status", nameof(Status), OrderByType.Asc)]

❌ 避免:
// 查询所有字段
var all = await _repository.GetAllAsync();

// 不分页查询大量数据
var all = await _repository.GetListAsync(_ => true);

3. 事务使用

csharp
✅ 推荐:
- 只在必要时使用事务
- 保持事务简短
- 及时提交或回滚
- 使用 try-catch 处理异常

❌ 避免:
- 在事务中进行耗时操作
- 嵌套事务
- 忘记回滚

4. 错误处理

csharp
✅ 推荐:
try
{
    await _service.DoSomething();
}
catch (BusinessException ex)
{
    return BadRequest(new { error = ex.Message });
}
catch (Exception ex)
{
    _logger.LogError(ex, "操作失败");
    return StatusCode(500, new { error = "Internal server error" });
}

❌ 避免:
catch (Exception ex)
{
    // 吞掉异常
}

❓ 常见问题

Q: 如何选择 BaseEntity 还是 AuditableEntity?

A:

  • 需要追踪操作人 → AuditableEntity
  • 不需要追踪 → BaseEntity

Q: 软删除的数据如何查询?

A:

csharp
// 默认查询会过滤已删除的数据
var tasks = await _repository.GetListAsync(t => t.Status == 0);

// 查询包括已删除的数据
var allTasks = await _db.Queryable<TaskItem>()
    .IgnoreQueryFilters()  // 忽略全局过滤器
    .ToListAsync();

Q: 如何实现复杂的业务逻辑?

A:

  1. 在服务层实现业务逻辑
  2. 使用工作单元管理事务
  3. 在控制器中调用服务
  4. 保持控制器轻薄

Q: 如何优化查询性能?

A:

  1. 使用投影查询(只查需要的字段)
  2. 添加合适的数据库索引
  3. 使用分页避免大数据量
  4. 缓存常用数据

🔗 相关链接

Released under the MIT License.