znlgis 博客

GIS开发与技术分享

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

一、项目结构最佳实践

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 开发更简单、更通用、更流行!