第二十章:最佳实践与常见问题

一、项目结构最佳实践

1.1 推荐的分层架构

Furion 推荐使用分层架构来组织项目,以下是标准的四层架构:

MyApp.sln
├── MyApp.Web.Entry/           # 入口层(Web 启动项目)
│   ├── Program.cs
│   ├── appsettings.json
│   └── wwwroot/
├── MyApp.Application/         # 应用层(业务逻辑、服务、DTO)
│   ├── Services/
│   ├── Dtos/
│   └── Mappers/
├── MyApp.Core/                # 核心层(实体、枚举、常量、接口定义)
│   ├── Entities/
│   ├── Enums/
│   ├── Constants/
│   └── Interfaces/
├── MyApp.EntityFramework.Core/ # 数据层(数据库上下文、仓储、种子数据)
│   ├── DbContexts/
│   ├── Repositories/
│   ├── Migrations/
│   └── SeedData/
└── MyApp.Tests/               # 测试层
    ├── UnitTests/
    └── IntegrationTests/

1.2 各层职责说明

层 项目 职责 依赖
入口层 Web.Entry 程序入口、中间件配置、启动配置 引用 Application
应用层 Application 业务逻辑、服务实现、动态 API 引用 Core
核心层 Core 实体定义、接口、常量、枚举 仅引用 Furion
数据层 EntityFramework.Core 数据库上下文、仓储实现 引用 Core
测试层 Tests 单元测试、集成测试 引用 Application

1.3 领域驱动设计(DDD)思想

MyApp.sln
├── MyApp.Web.Entry/                # 用户接口层
├── MyApp.Application/              # 应用服务层
│   ├── Commands/                   # 命令处理(写操作)
│   │   ├── CreateOrderCommand.cs
│   │   └── CreateOrderCommandHandler.cs
│   ├── Queries/                    # 查询处理(读操作)
│   │   ├── GetOrderQuery.cs
│   │   └── GetOrderQueryHandler.cs
│   └── EventHandlers/              # 事件处理
├── MyApp.Domain/                   # 领域层
│   ├── Aggregates/                 # 聚合根
│   │   ├── Order/
│   │   │   ├── Order.cs            # 聚合根实体
│   │   │   ├── OrderItem.cs        # 值对象
│   │   │   └── IOrderRepository.cs # 仓储接口
│   │   └── User/
│   ├── Events/                     # 领域事件
│   │   └── OrderCreatedEvent.cs
│   ├── Services/                   # 领域服务
│   │   └── PricingService.cs
│   └── ValueObjects/               # 值对象
│       ├── Money.cs
│       └── Address.cs
├── MyApp.Infrastructure/           # 基础设施层
│   ├── Persistence/
│   │   ├── AppDbContext.cs
│   │   └── Repositories/
│   └── ExternalServices/
└── MyApp.Tests/

1.4 领域实体设计示例

/// <summary>
/// 订单聚合根
/// </summary>
public class Order : EntityBase
{
    /// <summary>
    /// 订单号
    /// </summary>
    public string OrderNo { get; private set; }

    /// <summary>
    /// 订单状态
    /// </summary>
    public OrderStatus Status { get; private set; }

    /// <summary>
    /// 买家ID
    /// </summary>
    public long BuyerId { get; private set; }

    /// <summary>
    /// 订单项列表
    /// </summary>
    private readonly List<OrderItem> _orderItems = new();
    public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly();

    /// <summary>
    /// 订单总金额
    /// </summary>
    public decimal TotalAmount => _orderItems.Sum(item => item.SubTotal);

    /// <summary>
    /// 创建订单
    /// </summary>
    public static Order Create(long buyerId, List<OrderItem> items)
    {
        if (items == null || !items.Any())
            throw new BusinessException("订单项不能为空");

        var order = new Order
        {
            OrderNo = GenerateOrderNo(),
            BuyerId = buyerId,
            Status = OrderStatus.Pending
        };

        foreach (var item in items)
        {
            order._orderItems.Add(item);
        }

        // 发布领域事件
        order.AddDomainEvent(new OrderCreatedEvent(order));
        return order;
    }

    /// <summary>
    /// 支付订单
    /// </summary>
    public void Pay()
    {
        if (Status != OrderStatus.Pending)
            throw new BusinessException("只有待支付的订单才能支付");

        Status = OrderStatus.Paid;
        AddDomainEvent(new OrderPaidEvent(this));
    }

    /// <summary>
    /// 取消订单
    /// </summary>
    public void Cancel(string reason)
    {
        if (Status == OrderStatus.Shipped)
            throw new BusinessException("已发货的订单不能取消");

        Status = OrderStatus.Cancelled;
        AddDomainEvent(new OrderCancelledEvent(this, reason));
    }

    private static string GenerateOrderNo()
    {
        return $"ORD{DateTime.Now:yyyyMMddHHmmss}{Random.Shared.Next(1000, 9999)}";
    }
}

/// <summary>
/// 订单状态枚举
/// </summary>
public enum OrderStatus
{
    Pending = 0,    // 待支付
    Paid = 1,       // 已支付
    Shipped = 2,    // 已发货
    Completed = 3,  // 已完成
    Cancelled = 4   // 已取消
}

二、编码规范

2.1 命名规范

元素 规范 正确示例 错误示例
类名 PascalCase UserService userService
接口 I + PascalCase IUserService UserServiceInterface
方法 PascalCase GetUserById getUserById
属性 PascalCase UserName userName
私有字段 _ + camelCase _userService userService
局部变量 camelCase userName UserName
常量 PascalCase MaxRetryCount MAX_RETRY_COUNT
枚举值 PascalCase OrderStatus.Pending OrderStatus.PENDING
异步方法 以 Async 结尾 GetUserAsync GetUser
DTO 类 以 Dto 结尾 UserDto UserModel
输入模型 以 Input 结尾 CreateUserInput CreateUserRequest

2.2 注释规范

/// <summary>
/// 用户管理服务
/// </summary>
/// <remarks>
/// 提供用户的增删改查功能,包含数据验证和权限检查。
/// </remarks>
public class UserAppService : IDynamicApiController
{
    /// <summary>
    /// 根据 ID 获取用户详情
    /// </summary>
    /// <param name="id">用户 ID</param>
    /// <returns>用户详情信息</returns>
    /// <exception cref="BusinessException">当用户不存在时抛出</exception>
    public async Task<UserDto> GetAsync(long id)
    {
        // 查询用户(包含部门信息)
        var user = await _repository
            .Include(u => u.Department)
            .FirstOrDefaultAsync(u => u.Id == id)
            ?? throw Oops.Oh("用户不存在");

        return user.Adapt<UserDto>();
    }
}

2.3 代码组织规范

/// <summary>
/// 类成员排列顺序规范
/// </summary>
public class SampleService
{
    // 1. 常量
    private const int MaxRetryCount = 3;

    // 2. 静态字段
    private static readonly object _lock = new();

    // 3. 私有字段
    private readonly IUserRepository _userRepository;
    private readonly ILogger<SampleService> _logger;

    // 4. 构造函数
    public SampleService(
        IUserRepository userRepository,
        ILogger<SampleService> logger)
    {
        _userRepository = userRepository;
        _logger = logger;
    }

    // 5. 公有属性
    public string ServiceName => "SampleService";

    // 6. 公有方法
    public async Task<UserDto> GetUserAsync(long id)
    {
        return await _userRepository.GetByIdAsync(id);
    }

    // 7. 受保护方法
    protected virtual void OnUserCreated(User user)
    {
        _logger.LogInformation("用户创建: {UserId}", user.Id);
    }

    // 8. 私有方法
    private bool ValidateInput(CreateUserInput input)
    {
        return !string.IsNullOrWhiteSpace(input.UserName);
    }
}

三、性能优化技巧

3.1 异步编程最佳实践

/// <summary>
/// 异步编程最佳实践
/// </summary>
public class AsyncBestPractices
{
    private readonly IRepository<Order> _orderRepo;
    private readonly IRepository<Product> _productRepo;
    private readonly ICacheService _cache;

    // ✅ 正确:并行执行多个独立的异步操作
    public async Task<DashboardDto> GetDashboardAsync()
    {
        var orderCountTask = _orderRepo.CountAsync();
        var productCountTask = _productRepo.CountAsync();
        var recentOrdersTask = _orderRepo.AsQueryable()
            .OrderByDescending(o => o.CreatedTime)
            .Take(10)
            .ToListAsync();

        await Task.WhenAll(orderCountTask, productCountTask, recentOrdersTask);

        return new DashboardDto
        {
            OrderCount = orderCountTask.Result,
            ProductCount = productCountTask.Result,
            RecentOrders = recentOrdersTask.Result
        };
    }

    // ❌ 错误:同步阻塞异步方法
    public DashboardDto GetDashboardSync_Wrong()
    {
        // 可能导致死锁!
        var orderCount = _orderRepo.CountAsync().Result;
        return new DashboardDto { OrderCount = orderCount };
    }

    // ✅ 正确:使用 ConfigureAwait(false) 在库代码中
    public async Task<int> GetCountInLibraryAsync()
    {
        return await _orderRepo.CountAsync().ConfigureAwait(false);
    }

    // ✅ 正确:避免不必要的 async/await
    public Task<Order> GetOrderAsync(long id)
    {
        // 简单的透传不需要 async/await
        return _orderRepo.FindAsync(id);
    }
}

3.2 批量操作优化

/// <summary>
/// 批量操作优化
/// </summary>
public class BatchOperationService
{
    private readonly IRepository<Product> _repository;

    // ❌ 低效:逐条插入
    public async Task ImportProducts_Slow(List<ProductInput> inputs)
    {
        foreach (var input in inputs)
        {
            var product = input.Adapt<Product>();
            await _repository.InsertAsync(product);
        }
    }

    // ✅ 高效:批量插入
    public async Task ImportProducts_Fast(List<ProductInput> inputs)
    {
        var products = inputs.Select(i => i.Adapt<Product>()).ToList();
        await _repository.InsertRangeAsync(products);
    }

    // ✅ 更高效:使用 EFCore.BulkExtensions
    public async Task ImportProducts_Fastest(List<ProductInput> inputs)
    {
        var products = inputs.Select(i => i.Adapt<Product>()).ToList();
        await _repository.Context.BulkInsertAsync(products);
    }
}

3.3 缓存策略优化

/// <summary>
/// 多级缓存策略
/// </summary>
public class CacheOptimizationService
{
    private readonly IMemoryCache _memoryCache;
    private readonly IDistributedCache _distributedCache;
    private readonly IRepository<Product> _repository;

    public CacheOptimizationService(
        IMemoryCache memoryCache,
        IDistributedCache distributedCache,
        IRepository<Product> repository)
    {
        _memoryCache = memoryCache;
        _distributedCache = distributedCache;
        _repository = repository;
    }

    /// <summary>
    /// 两级缓存查询
    /// </summary>
    public async Task<ProductDto> GetProductAsync(long id)
    {
        var cacheKey = $"product:{id}";

        // 第一级:内存缓存(最快)
        if (_memoryCache.TryGetValue(cacheKey, out ProductDto cached))
        {
            return cached;
        }

        // 第二级:分布式缓存(Redis)
        var json = await _distributedCache.GetStringAsync(cacheKey);
        if (!string.IsNullOrEmpty(json))
        {
            var dto = JsonSerializer.Deserialize<ProductDto>(json);
            // 回填内存缓存
            _memoryCache.Set(cacheKey, dto, TimeSpan.FromMinutes(5));
            return dto;
        }

        // 第三级:数据库查询
        var product = await _repository.FindAsync(id);
        if (product == null) return null;

        var result = product.Adapt<ProductDto>();

        // 回填两级缓存
        _memoryCache.Set(cacheKey, result, TimeSpan.FromMinutes(5));
        await _distributedCache.SetStringAsync(cacheKey,
            JsonSerializer.Serialize(result),
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
            });

        return result;
    }
}

3.4 数据库查询优化

/// <summary>
/// 数据库查询优化示例
/// </summary>
public class QueryOptimizationService
{
    private readonly IRepository<Order> _repository;

    // ✅ 使用投影查询,只查需要的字段
    public async Task<List<OrderListDto>> GetOrderListAsync()
    {
        return await _repository.AsQueryable()
            .Select(o => new OrderListDto
            {
                Id = o.Id,
                OrderNo = o.OrderNo,
                TotalAmount = o.TotalAmount,
                Status = o.Status,
                CreatedTime = o.CreatedTime
            })
            .ToListAsync();
    }

    // ✅ 使用 AsNoTracking 提高查询性能
    public async Task<List<Order>> GetOrdersReadOnlyAsync()
    {
        return await _repository.AsQueryable()
            .AsNoTracking()
            .ToListAsync();
    }

    // ✅ 避免 N+1 查询问题
    public async Task<List<Order>> GetOrdersWithItemsAsync()
    {
        // 使用 Include 预加载关联数据
        return await _repository.AsQueryable()
            .Include(o => o.OrderItems)
            .Include(o => o.Buyer)
            .ToListAsync();
    }

    // ✅ 使用分页查询避免大量数据加载
    public async Task<PagedResult<OrderDto>> GetPagedOrdersAsync(
        int page, int pageSize)
    {
        var query = _repository.AsQueryable()
            .OrderByDescending(o => o.CreatedTime);

        var totalCount = await query.CountAsync();
        var items = await query
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(o => o.Adapt<OrderDto>())
            .ToListAsync();

        return new PagedResult<OrderDto>
        {
            Items = items,
            TotalCount = totalCount,
            Page = page,
            PageSize = pageSize
        };
    }
}

四、安全最佳实践

4.1 输入验证与防护

/// <summary>
/// 安全输入验证
/// </summary>
public class SecurityBestPractices
{
    /// <summary>
    /// ✅ 使用参数化查询防止 SQL 注入
    /// </summary>
    public async Task<List<User>> SearchUsersAsync(string keyword)
    {
        // ✅ 正确:使用参数化查询
        return await _context.Users
            .Where(u => u.UserName.Contains(keyword))
            .ToListAsync();

        // ❌ 危险:拼接 SQL(SQL 注入风险)
        // var sql = $"SELECT * FROM Users WHERE UserName LIKE '%{keyword}%'";
    }

    /// <summary>
    /// ✅ XSS 防护 - 输出编码
    /// </summary>
    public string SanitizeOutput(string input)
    {
        return System.Net.WebUtility.HtmlEncode(input);
    }

    /// <summary>
    /// ✅ 敏感数据加密存储
    /// </summary>
    public class UserEntity
    {
        public long Id { get; set; }
        public string UserName { get; set; }

        // 手机号脱敏存储
        [SensitiveData]
        public string Phone { get; set; }

        // 身份证号加密存储
        [Encrypted]
        public string IdCard { get; set; }
    }
}

4.2 CSRF 防护

// Program.cs - CSRF 防护配置
builder.Services.AddAntiforgery(options =>
{
    options.HeaderName = "X-CSRF-TOKEN";
    options.Cookie.Name = "CSRF-TOKEN";
    options.Cookie.HttpOnly = true;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
});

// 在需要 CSRF 防护的接口上使用
[ValidateAntiForgeryToken]
public async Task<UserDto> CreateUser(CreateUserInput input)
{
    // ...
}

4.3 敏感数据处理

/// <summary>
/// 数据脱敏工具类
/// </summary>
public static class DataMasking
{
    /// <summary>
    /// 手机号脱敏:138****1234
    /// </summary>
    public static string MaskPhone(string phone)
    {
        if (string.IsNullOrEmpty(phone) || phone.Length < 7)
            return phone;
        return phone[..3] + "****" + phone[^4..];
    }

    /// <summary>
    /// 身份证号脱敏:110***********1234
    /// </summary>
    public static string MaskIdCard(string idCard)
    {
        if (string.IsNullOrEmpty(idCard) || idCard.Length < 8)
            return idCard;
        return idCard[..3] + new string('*', idCard.Length - 7) + idCard[^4..];
    }

    /// <summary>
    /// 邮箱脱敏:z***n@example.com
    /// </summary>
    public static string MaskEmail(string email)
    {
        if (string.IsNullOrEmpty(email)) return email;
        var atIndex = email.IndexOf('@');
        if (atIndex <= 1) return email;
        return email[0] + new string('*', Math.Min(atIndex - 1, 3))
            + email[(atIndex - 1)..];
    }
}

五、单元测试与集成测试

5.1 单元测试(xUnit + Moq)

using Xunit;
using Moq;

/// <summary>
/// 用户服务单元测试
/// </summary>
public class UserServiceTests
{
    private readonly Mock<IRepository<User>> _mockRepo;
    private readonly Mock<ILogger<UserAppService>> _mockLogger;
    private readonly UserAppService _service;

    public UserServiceTests()
    {
        _mockRepo = new Mock<IRepository<User>>();
        _mockLogger = new Mock<ILogger<UserAppService>>();
        _service = new UserAppService(_mockRepo.Object, _mockLogger.Object);
    }

    [Fact]
    public async Task GetUser_ExistingId_ReturnsUser()
    {
        // Arrange
        var expectedUser = new User { Id = 1, UserName = "张三" };
        _mockRepo.Setup(r => r.FindAsync(1))
                 .ReturnsAsync(expectedUser);

        // Act
        var result = await _service.GetAsync(1);

        // Assert
        Assert.NotNull(result);
        Assert.Equal("张三", result.UserName);
    }

    [Fact]
    public async Task GetUser_NonExistingId_ThrowsException()
    {
        // Arrange
        _mockRepo.Setup(r => r.FindAsync(999))
                 .ReturnsAsync((User)null);

        // Act & Assert
        await Assert.ThrowsAsync<Exception>(
            () => _service.GetAsync(999));
    }

    [Theory]
    [InlineData("")]
    [InlineData(null)]
    [InlineData("  ")]
    public async Task CreateUser_InvalidName_ThrowsValidation(string name)
    {
        // Arrange
        var input = new CreateUserInput { UserName = name };

        // Act & Assert
        await Assert.ThrowsAsync<ValidationException>(
            () => _service.CreateAsync(input));
    }

    [Fact]
    public async Task CreateUser_ValidInput_ReturnsNewUser()
    {
        // Arrange
        var input = new CreateUserInput
        {
            UserName = "李四",
            Email = "lisi@example.com"
        };

        _mockRepo.Setup(r => r.InsertAsync(It.IsAny<User>()))
                 .ReturnsAsync((User u) => new EntityEntry<User>(u));

        // Act
        var result = await _service.CreateAsync(input);

        // Assert
        Assert.NotNull(result);
        Assert.Equal("李四", result.UserName);
        _mockRepo.Verify(r => r.InsertAsync(It.IsAny<User>()), Times.Once);
    }
}

5.2 集成测试

using Microsoft.AspNetCore.Mvc.Testing;
using System.Net.Http.Json;

/// <summary>
/// API 集成测试
/// </summary>
public class UserApiIntegrationTests
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public UserApiIntegrationTests(
        WebApplicationFactory<Program> factory)
    {
        _client = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                // 替换为测试数据库
                services.RemoveAll(typeof(DbContextOptions<DefaultDbContext>));
                services.AddDbContext<DefaultDbContext>(options =>
                {
                    options.UseInMemoryDatabase("TestDb");
                });
            });
        }).CreateClient();
    }

    [Fact]
    public async Task GetUserList_ReturnsSuccessStatusCode()
    {
        // Act
        var response = await _client.GetAsync("/api/user/list");

        // Assert
        response.EnsureSuccessStatusCode();
        var result = await response.Content
            .ReadFromJsonAsync<RESTfulResult<List<UserDto>>>();
        Assert.True(result.Success);
    }

    [Fact]
    public async Task CreateUser_ValidInput_Returns200()
    {
        // Arrange
        var input = new CreateUserInput
        {
            UserName = "测试用户",
            Email = "test@example.com",
            Phone = "13800138000"
        };

        // Act
        var response = await _client.PostAsJsonAsync("/api/user", input);

        // Assert
        response.EnsureSuccessStatusCode();
        var result = await response.Content
            .ReadFromJsonAsync<RESTfulResult<UserDto>>();
        Assert.True(result.Success);
        Assert.Equal("测试用户", result.Data.UserName);
    }

    [Fact]
    public async Task CreateUser_InvalidInput_Returns400()
    {
        // Arrange
        var input = new CreateUserInput
        {
            UserName = "",  // 空用户名
            Email = "invalid-email"
        };

        // Act
        var response = await _client.PostAsJsonAsync("/api/user", input);

        // Assert
        Assert.Equal(System.Net.HttpStatusCode.BadRequest,
            response.StatusCode);
    }
}

六、API 文档维护

6.1 Swagger 增强配置

// Program.cs - Swagger 增强配置
builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "企业管理系统 API",
        Version = "v1",
        Description = "基于 Furion 框架的企业管理系统",
        Contact = new OpenApiContact
        {
            Name = "技术团队",
            Email = "dev@example.com",
            Url = new Uri("https://example.com")
        },
        License = new OpenApiLicense
        {
            Name = "MIT",
            Url = new Uri("https://opensource.org/licenses/MIT")
        }
    });

    // Bearer Token 认证配置
    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Description = "JWT 认证,在下方输入 Bearer {token}",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey,
        Scheme = "Bearer"
    });

    options.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                }
            },
            Array.Empty<string>()
        }
    });

    // 加载所有 XML 注释文件
    var xmlFiles = Directory.GetFiles(
        AppContext.BaseDirectory, "*.xml");
    foreach (var xmlFile in xmlFiles)
    {
        options.IncludeXmlComments(xmlFile, true);
    }
});

6.2 接口分组与版本管理

/// <summary>
/// 按模块分组的 API 接口
/// </summary>
[ApiDescriptionSettings("系统管理", Name = "User", Order = 100,
    Description = "用户管理相关接口")]
public class UserAppService : IDynamicApiController
{
    /// <summary>
    /// 获取用户列表
    /// </summary>
    /// <remarks>
    /// 示例请求:
    ///
    ///     GET /api/user/list?page=1&amp;pageSize=20
    ///
    /// 注意事项:
    /// - 需要登录后才能访问
    /// - 默认每页返回 20 条数据
    /// - 支持关键字模糊搜索
    /// </remarks>
    [DisplayName("获取用户列表")]
    public async Task<PagedResult<UserDto>> GetListAsync(
        [FromQuery] UserQueryInput input)
    {
        // ...
    }
}

七、版本管理与发布策略

7.1 语义化版本规范

版本号 格式 说明 示例
主版本 X.0.0 不兼容的 API 变更 2.0.0
次版本 0.X.0 向后兼容的新功能 1.1.0
修订版 0.0.X 向后兼容的问题修复 1.0.1
预发布 X.Y.Z-tag 预发布版本标记 1.0.0-beta.1

7.2 Git 分支策略

main(生产分支)
├── develop(开发分支)
│   ├── feature/user-management(功能分支)
│   ├── feature/order-system(功能分支)
│   └── feature/payment(功能分支)
├── release/v1.2.0(发布分支)
└── hotfix/fix-login-bug(热修复分支)
# 功能开发流程
git checkout develop
git checkout -b feature/new-feature
# ... 开发完成 ...
git checkout develop
git merge feature/new-feature
git branch -d feature/new-feature

# 发布流程
git checkout develop
git checkout -b release/v1.2.0
# ... 修复发布前的 bug ...
git checkout main
git merge release/v1.2.0
git tag -a v1.2.0 -m "Release v1.2.0"
git checkout develop
git merge release/v1.2.0
git branch -d release/v1.2.0

# 热修复流程
git checkout main
git checkout -b hotfix/fix-critical-bug
# ... 修复 ...
git checkout main
git merge hotfix/fix-critical-bug
git tag -a v1.2.1 -m "Hotfix v1.2.1"
git checkout develop
git merge hotfix/fix-critical-bug
git branch -d hotfix/fix-critical-bug

八、常见问题汇总

8.1 启动与配置相关

Q1:应用启动失败,提示 “Unable to configure HTTPS endpoint”

// 解决方案:信任开发证书
// 在命令行执行:
// dotnet dev-certs https --trust

// 或在 Program.cs 中禁用 HTTPS(仅开发环境)
if (builder.Environment.IsDevelopment())
{
    builder.WebHost.ConfigureKestrel(options =>
    {
        options.ListenAnyIP(5000); // 仅 HTTP
    });
}

Q2:Furion 服务注册失败,提示找不到程序集

// 解决方案:确保所有项目正确引用 Furion
// 在 Web.Entry 的 Program.cs 中
Serve.Run(RunOptions.Default
    .AddAssemblies(typeof(SomeServiceInOtherProject).Assembly));

Q3:配置文件修改后不生效

// 解决方案:确保启用了配置热重载
builder.Configuration.AddJsonFile(
    "appsettings.json",
    optional: false,
    reloadOnChange: true  // 关键:启用热重载
);

// 使用 IOptionsMonitor 监听配置变更
public class MyService
{
    public MyService(IOptionsMonitor<MyOptions> options)
    {
        options.OnChange(newOptions =>
        {
            Console.WriteLine("配置已更新");
        });
    }
}

8.2 数据库相关

Q4:数据库连接失败

// 检查连接字符串格式
// MySQL:
"Server=localhost;Port=3306;Database=mydb;Uid=root;Pwd=123456;CharSet=utf8mb4;"

// PostgreSQL:
"Host=localhost;Port=5432;Database=mydb;Username=postgres;Password=123456;"

// SQL Server:
"Server=localhost;Database=mydb;User Id=sa;Password=123456;TrustServerCertificate=True;"

Q5:EF Core 迁移失败

# 确保安装了 EF Core 工具
dotnet tool install --global dotnet-ef

# 指定启动项目和迁移项目
dotnet ef migrations add InitialCreate \
    --project MyApp.EntityFramework.Core \
    --startup-project MyApp.Web.Entry

dotnet ef database update \
    --project MyApp.EntityFramework.Core \
    --startup-project MyApp.Web.Entry

Q6:EF Core 查询性能差

// 1. 使用 AsNoTracking(只读查询)
var users = await _context.Users.AsNoTracking().ToListAsync();

// 2. 使用投影查询
var dtos = await _context.Users.Select(u => new UserDto
{
    Id = u.Id,
    Name = u.UserName
}).ToListAsync();

// 3. 避免 N+1 查询
var orders = await _context.Orders
    .Include(o => o.Items)  // 预加载
    .ToListAsync();

8.3 跨域相关

Q7:前端请求跨域被拒绝

// Program.cs - CORS 配置
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowFrontend", policy =>
    {
        policy.WithOrigins(
                "http://localhost:3000",    // React 开发服务器
                "http://localhost:8080",    // Vue 开发服务器
                "https://www.example.com"   // 生产域名
            )
            .AllowAnyHeader()
            .AllowAnyMethod()
            .AllowCredentials();
    });
});

// 注意:UseRouting 之后、UseAuthentication 之前
app.UseRouting();
app.UseCors("AllowFrontend");
app.UseAuthentication();
app.UseAuthorization();

Q8:跨域 Cookie 无法携带

// 确保同时满足以下条件:
// 1. 服务端允许 Credentials
policy.AllowCredentials();

// 2. 前端请求设置 withCredentials
// axios.defaults.withCredentials = true;

// 3. Cookie 设置 SameSite=None 和 Secure
builder.Services.ConfigureApplicationCookie(options =>
{
    options.Cookie.SameSite = SameSiteMode.None;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
});

8.4 认证与授权相关

Q9:JWT Token 验证失败

// 检查 JWT 配置
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = "myapp",  // 必须与生成 Token 时一致
            ValidateAudience = true,
            ValidAudience = "myapp",  // 必须与生成 Token 时一致
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes("your-secret-key-at-least-32-chars")),
            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromMinutes(5)
        };
    });

Q10:接口返回 401 但已传递 Token

// 确保中间件注册顺序正确
app.UseRouting();
app.UseAuthentication();  // 先认证
app.UseAuthorization();   // 后授权
app.MapControllers();

// 检查 Token 格式:Authorization: Bearer eyJhbGciOiJIUzI1NiI...

8.5 性能相关

Q11:接口响应慢

// 排查步骤:
// 1. 启用请求日志,找出慢接口
app.UseSerilogRequestLogging();

// 2. 使用 MiniProfiler 分析性能
builder.Services.AddMiniProfiler(options =>
{
    options.RouteBasePath = "/profiler";
});

// 3. 检查数据库查询
builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(connectionString);
    if (builder.Environment.IsDevelopment())
    {
        options.EnableSensitiveDataLogging();
        options.LogTo(Console.WriteLine, LogLevel.Information);
    }
});

Q12:内存持续增长(内存泄漏)

// 常见原因和解决方案:
// 1. 事件处理器未取消订阅
public class MyService : IDisposable
{
    private readonly EventHandler _handler;

    public MyService()
    {
        _handler = OnEvent;
        SomeEvent += _handler;
    }

    public void Dispose()
    {
        SomeEvent -= _handler;  // ✅ 取消订阅
    }
}

// 2. IDisposable 资源未释放
// 使用 using 语句
using var stream = new FileStream("file.txt", FileMode.Open);

// 3. 静态集合持续增长
// 使用 ConcurrentDictionary + 定期清理

8.6 部署相关

Q13:Docker 容器启动后无法访问

# 检查端口映射
docker run -p 5000:80 myapp  # 主机 5000 → 容器 80

# 确保应用监听 0.0.0.0 而非 localhost
# 设置环境变量:ASPNETCORE_URLS=http://+:80

Q14:Linux 部署后中文乱码

# 安装中文语言包
sudo apt-get install -y locales
sudo locale-gen zh_CN.UTF-8
export LANG=zh_CN.UTF-8

# 安装中文字体(PDF 生成需要)
sudo apt-get install -y fonts-wqy-zenhei fonts-wqy-microhei

Q15:Nginx 反向代理 WebSocket 失败

# WebSocket 代理配置
location /ws {
    proxy_pass http://127.0.0.1:5000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_read_timeout 86400;
}

8.7 其他常见问题

Q16:文件上传大小限制

// Kestrel 配置
builder.WebHost.ConfigureKestrel(options =>
{
    options.Limits.MaxRequestBodySize = 100 * 1024 * 1024; // 100MB
});

// Form 配置
builder.Services.Configure<FormOptions>(options =>
{
    options.MultipartBodyLengthLimit = 100 * 1024 * 1024;
});

Q17:时区问题(UTC vs 本地时间)

// 统一使用 UTC 存储,展示时转换
// 配置 JSON 序列化
builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.Converters.Add(
            new DateTimeJsonConverter("yyyy-MM-dd HH:mm:ss"));
    });

// 数据库存储 UTC
public class BaseEntity
{
    public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
}

Q18:Swagger 页面空白或报错

// 确保 XML 注释文件正确生成
// .csproj 中添加:
// <GenerateDocumentationFile>true</GenerateDocumentationFile>
// <NoWarn>$(NoWarn);1591</NoWarn>

// 检查 Swagger 路由是否冲突
app.UseSwagger();
app.UseSwaggerUI(options =>
{
    options.SwaggerEndpoint("/swagger/v1/swagger.json", "API V1");
    options.RoutePrefix = "swagger";
});

Q19:动态 API 路由不生效

// 确保服务类实现了 IDynamicApiController
public class MyService : IDynamicApiController  // ✅
{
    public string GetName() => "Hello";
}

// 确保 AddDynamicApiControllers 已注册
builder.Services.AddControllers()
    .AddDynamicApiControllers();  // ✅

Q20:依赖注入生命周期问题

// 常见错误:在 Singleton 中注入 Scoped 服务
// ❌ 错误
builder.Services.AddSingleton<MySingletonService>();
builder.Services.AddScoped<MyScopedService>();

public class MySingletonService
{
    // 这会导致 Scoped 服务被提升为 Singleton
    public MySingletonService(MyScopedService scopedService) { }
}

// ✅ 正确:使用 IServiceScopeFactory
public class MySingletonService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public MySingletonService(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    public async Task DoWorkAsync()
    {
        using var scope = _scopeFactory.CreateScope();
        var scopedService = scope.ServiceProvider
            .GetRequiredService<MyScopedService>();
        await scopedService.ExecuteAsync();
    }
}

九、从其他框架迁移到 Furion

9.1 ABP 框架迁移指南

ABP 概念 Furion 对应 迁移要点
ApplicationService IDynamicApiController 继承接口即可
IRepository<T> IRepository<T> 基本一致
AbpModule Startup 配置 改为标准 ASP.NET Core 启动方式
[UnitOfWork] [UnitOfWork] Furion 也支持
EventBus IEventPublisher 接口略有不同
权限系统 自定义或集成 需要重新实现

9.2 传统 WebAPI 迁移指南

// 传统 WebAPI 控制器
[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
    [HttpGet("{id}")]
    public async Task<IActionResult> GetUser(int id)
    {
        var user = await _service.GetByIdAsync(id);
        return Ok(user);
    }
}

// 迁移到 Furion 动态 API
[ApiDescriptionSettings(Name = "User")]
public class UserAppService : IDynamicApiController
{
    public async Task<UserDto> Get(int id)
    {
        return await _service.GetByIdAsync(id);
    }
}
// 效果相同,代码更简洁,自动路由映射

十、Furion 生态项目推荐

10.1 推荐项目

项目名称 说明 GitHub
Admin.NET 基于 Furion 的通用后台管理框架 zuohuaijun/Admin.NET
SimpleAdmin 简洁的后台管理系统 基于 Furion
Furion.Pure Furion 纯净版(无额外依赖) MonkSoul/Furion
Furion.Template 项目模板脚手架 dotnet new furion

10.2 快速创建项目

# 安装 Furion 项目模板
dotnet new install Furion.Template.Api

# 创建新项目
dotnet new furion -n MyApp

# 或使用纯净版模板
dotnet new install Furion.Pure.Template.Api
dotnet new furionpure -n MyApp

十一、学习资源汇总

11.1 官方资源

资源 地址 说明
官方文档 https://furion.net 最全面的文档
GitHub 仓库 https://github.com/MonkSoul/Furion 源码和 Issue
NuGet 包 https://www.nuget.org/packages/Furion 包下载
Gitee 镜像 https://gitee.com/dotnetchina/Furion 国内镜像

11.2 社区资源

资源 说明
QQ 群 Furion 官方交流群
微信群 关注公众号获取入群二维码
B 站教程 搜索 “Furion 教程” 获取视频教程
博客园 搜索 Furion 相关技术博客
CSDN Furion 实战系列文章

11.3 推荐学习路径

第一阶段:基础入门(1-2周)
├── C# 语言基础
├── ASP.NET Core 基础
├── Furion 快速上手
└── 完成第一个 CRUD 应用

第二阶段:核心能力(2-4周)
├── 依赖注入深入理解
├── EF Core 数据操作
├── 数据验证与异常处理
├── 身份认证与授权
└── 完成一个完整的管理系统

第三阶段:进阶提升(4-8周)
├── 缓存与性能优化
├── 事件总线与消息队列
├── 定时任务与后台服务
├── 多租户架构
└── 微服务架构设计

第四阶段:生产实践(持续学习)
├── Docker 容器化部署
├── CI/CD 流水线搭建
├── 监控与日志体系
├── 安全加固
└── 性能调优

十二、未来展望

12.1 Furion 框架发展趋势

Furion 框架作为国产 .NET 开源框架的佼佼者,未来将持续在以下方向发力:

  • 云原生支持:深度集成 Dapr、Service Mesh 等云原生技术
  • .NET 新版本跟进:第一时间适配 .NET 最新版本特性
  • AI 集成:集成大语言模型(LLM)和 AI 辅助开发能力
  • 低代码平台:提供可视化开发工具,降低开发门槛
  • 微服务生态:完善微服务相关组件(服务发现、配置中心、链路追踪)
  • 国产化适配:持续优化对国产操作系统和数据库的支持
  • 性能优化:利用 .NET 最新特性持续提升框架性能
  • 开发者体验:更完善的文档、示例和开发工具

12.2 持续学习建议

/// <summary>
/// 技术成长路线
/// </summary>
public class GrowthPath
{
    // 1. 深入理解 .NET 运行时机制
    // 2. 学习设计模式和架构模式
    // 3. 关注性能优化和安全实践
    // 4. 参与开源社区贡献
    // 5. 关注行业趋势和新技术
}

恭喜你完成了 Furion 框架系列教程的学习!从基础入门到高级特性,从开发实践到部署运维,你已经具备了使用 Furion 框架开发企业级应用的完整能力。希望本教程能够帮助你在实际项目中快速落地,构建高质量的 .NET 应用程序。

百小僧(MonkSoul) 创建的 Furion 框架:让 .NET 开发更简单、更通用、更流行!


上一章: 第19章-部署与DevOps 返回目录