手把手搭建C#企业级项目:从分层架构到完整API实现
很多C#开发者都有这样的困惑:跟着教程学会了语法,也做过一些小练习,但一到实际工作中,面对一个需要从零开始构建的“标准企业级项目”时,却不知从何下手。Controller、Service、Repository这些层到底该怎么划分?依赖注入怎么配才合理?数据库连接和事务又该如何管理?网上能找到的要么是零散的“学生管理系统”Demo,要么是过于庞大、依赖特定商业框架的解决方案,中间缺少一个清晰、完整、能直接复用到真实工作场景的桥梁。
这篇文章要解决的,正是这个“最后一公里”的问题。我们不只讲语法,而是手把手带你搭建一个具备标准企业级架构雏形的C#项目。你将清晰地看到,一个可维护、可扩展、职责分明的后端项目是如何从文件夹结构开始,一步步构建起来的。我们会从最简单的控制台程序开始,逐步引入ASP.NET Core Web API、Entity Framework Core、分层架构、依赖注入、仓储模式、单元测试等核心概念,并最终落地一个包含用户管理和产品目录的微型业务系统。
读完本文,你将获得一个可以直接作为模板的完整项目结构,理解每一层存在的意义,并掌握如何将新的业务需求填充到这个框架中。无论你是刚学完C#基础想进阶的初学者,还是有一定经验但想系统化工程实践的开发者,这篇文章都将为你提供一条明确的实践路径。
1. 为什么你需要一个“标准”的企业级项目结构?
在开始敲代码之前,我们必须先达成一个共识:为什么不能把所有代码都写在Program.cs或者Controller里?企业级项目结构的价值,远不止于“看起来规范”。
核心价值是应对变化与协作。想象一下,当业务逻辑需要修改时,如果它和数据库访问代码、API接口代码纠缠在一起,你很可能改一处而崩三处。当需要更换数据库(比如从SQL Server迁移到PostgreSQL)时,如果SQL语句散落在上百个业务方法里,这几乎是一项不可能完成的任务。当新同事加入,面对一个没有清晰结构的“意大利面条式”代码库,他的学习成本和犯错概率会急剧上升。
一个标准的分层架构(如经典的三层或领域驱动设计中的分层)通过分离关注点来解决这些问题。每一层有明确的职责:
- 表现层 (Presentation Layer):只负责接收HTTP请求、验证基础格式、返回响应。它不应该知道数据从哪里来。
- 业务逻辑层 (Business Logic Layer / Service Layer):包含核心的业务规则和流程。它是系统的“大脑”,协调数据流转和业务决策。
- 数据访问层 (Data Access Layer / Repository Layer):负责与数据库、文件系统、外部API等数据源打交道。它封装了所有数据持久化的细节。
这样,当UI从Web API变成桌面应用时,你只需替换表现层;当数据库变更时,你只需调整数据访问层。业务逻辑作为最核心的资产,保持稳定。
接下来,我们就从零开始,构建这样一个结构清晰的项目。
2. 项目结构与技术栈选型
我们将创建一个名为EnterpriseDemo的解决方案。技术栈选择当前最主流、最稳定的组合:
- .NET 8: 最新的LTS(长期支持)版本,性能和支持都有保障。
- ASP.NET Core Web API: 构建RESTful API的事实标准。
- Entity Framework Core 8: ORM框架,用于对象关系映射,支持Code First开发模式。
- SQL Server LocalDB / SQLite: 为了演示方便,我们使用轻量级的LocalDB(Windows)或SQLite(跨平台),生产环境可无缝切换至SQL Server、PostgreSQL等。
- xUnit&Moq: 用于编写单元测试和模拟依赖。
- Swagger/OpenAPI: 自动生成API文档。
最终的解决方案结构如下(使用Visual Studio 2022或Rider,或通过CLI创建):
EnterpriseDemo.sln ├── src/ │ ├── EnterpriseDemo.API/ (表现层 - ASP.NET Core Web API 项目) │ ├── EnterpriseDemo.Core/ (核心层 - 实体、枚举、接口、DTO等) │ ├── EnterpriseDemo.Application/ (应用层 - 业务逻辑、服务、映射) │ └── EnterpriseDemo.Infrastructure/ (基础设施层 - 数据访问、外部服务集成) └── tests/ └── EnterpriseDemo.Tests/ (单元测试项目)这种结构清晰地区分了职责,并且通过项目引用(而非DLL)来管理依赖,比如API项目引用Application和Infrastructure,Application引用Core,Infrastructure引用Core和Application(对于接口)。
3. 环境准备与项目创建
3.1 开发环境准备
- 安装 .NET 8 SDK: 前往 微软官网 下载并安装。
- 安装 IDE: 推荐使用Visual Studio 2022(社区版免费) 并确保安装了“ASP.NET和Web开发”工作负载。或者使用JetBrains Rider、Visual Studio Code。
- 数据库: 确保已安装SQL Server Express LocalDB(通常随VS安装) 或SQLite。
3.2 使用 CLI 创建解决方案和项目
打开终端(PowerShell, CMD, 或 Bash),执行以下命令:
# 创建解决方案目录并进入 mkdir EnterpriseDemo cd EnterpriseDemo # 创建解决方案文件 dotnet new sln -n EnterpriseDemo # 创建各个项目 dotnet new webapi -n EnterpriseDemo.API -f net8.0 --no-https -o src/EnterpriseDemo.API dotnet new classlib -n EnterpriseDemo.Core -f net8.0 -o src/EnterpriseDemo.Core dotnet new classlib -n EnterpriseDemo.Application -f net8.0 -o src/EnterpriseDemo.Application dotnet new classlib -n EnterpriseDemo.Infrastructure -f net8.0 -o src/EnterpriseDemo.Infrastructure dotnet new xunit -n EnterpriseDemo.Tests -f net8.0 -o tests/EnterpriseDemo.Tests # 将项目添加到解决方案 dotnet sln add src/EnterpriseDemo.API/EnterpriseDemo.API.csproj dotnet sln add src/EnterpriseDemo.Core/EnterpriseDemo.Core.csproj dotnet sln add src/EnterpriseDemo.Application/EnterpriseDemo.Application.csproj dotnet sln add src/EnterpriseDemo.Infrastructure/EnterpriseDemo.Infrastructure.csproj dotnet sln add tests/EnterpriseDemo.Tests/EnterpriseDemo.Tests.csproj3.3 配置项目依赖关系
这是架构清晰的关键一步,依赖必须单向流动,高层模块不依赖低层模块细节。
# API 层依赖 Application 和 Infrastructure dotnet add src/EnterpriseDemo.API/EnterpriseDemo.API.csproj reference src/EnterpriseDemo.Application/EnterpriseDemo.Application.csproj dotnet add src/EnterpriseDemo.API/EnterpriseDemo.API.csproj reference src/EnterpriseDemo.Infrastructure/EnterpriseDemo.Infrastructure.csproj # Application 层依赖 Core dotnet add src/EnterpriseDemo.Application/EnterpriseDemo.Application.csproj reference src/EnterpriseDemo.Core/EnterpriseDemo.Core.csproj # Infrastructure 层依赖 Core 和 Application (为实现Application中定义的接口) dotnet add src/EnterpriseDemo.Infrastructure/EnterpriseDemo.Infrastructure.csproj reference src/EnterpriseDemo.Core/EnterpriseDemo.Core.csproj dotnet add src/EnterpriseDemo.Infrastructure/EnterpriseDemo.Infrastructure.csproj reference src/EnterpriseDemo.Application/EnterpriseDemo.Application.csproj # Tests 项目依赖所有需要测试的项目 dotnet add tests/EnterpriseDemo.Tests/EnterpriseDemo.Tests.csproj reference src/EnterpriseDemo.API/EnterpriseDemo.API.csproj dotnet add tests/EnterpriseDemo.Tests/EnterpriseDemo.Tests.csproj reference src/EnterpriseDemo.Application/EnterpriseDemo.Application.csproj dotnet add tests/EnterpriseDemo.Tests/EnterpriseDemo.Tests.csproj reference src/EnterpriseDemo.Infrastructure/EnterpriseDemo.Infrastructure.csproj4. 核心层 (Core):定义领域模型与契约
Core 项目应该保持“纯净”,不依赖任何其他项目。它包含系统的核心概念。
4.1 定义领域实体
在EnterpriseDemo.Core/Entities/文件夹下创建两个实体类:Product.cs和User.cs。
// 文件:src/EnterpriseDemo.Core/Entities/Product.cs using System; using System.ComponentModel.DataAnnotations; namespace EnterpriseDemo.Core.Entities { public class Product { [Key] public int Id { get; set; } [Required] [MaxLength(100)] public string Name { get; set; } = string.Empty; [MaxLength(500)] public string? Description { get; set; } [Range(0, double.MaxValue)] public decimal Price { get; set; } public int StockQuantity { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime? UpdatedAt { get; set; } } }// 文件:src/EnterpriseDemo.Core/Entities/User.cs using System; using System.ComponentModel.DataAnnotations; namespace EnterpriseDemo.Core.Entities { public class User { [Key] public int Id { get; set; } [Required] [MaxLength(50)] public string Username { get; set; } = string.Empty; [Required] [EmailAddress] [MaxLength(100)] public string Email { get; set; } = string.Empty; // 注意:实际项目中密码不应以明文存储,这里仅为演示。 // 应使用哈希加盐处理,如 Identity 的 PasswordHasher。 [Required] public string PasswordHash { get; set; } = string.Empty; public bool IsActive { get; set; } = true; public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime? LastLoginAt { get; set; } } }4.2 定义数据访问接口(仓储模式)
仓储模式抽象了数据访问逻辑。我们在Core层定义接口,在Infrastructure层实现。在EnterpriseDemo.Core/Interfaces/下创建通用仓储接口和具体实体的仓储接口。
// 文件:src/EnterpriseDemo.Core/Interfaces/IGenericRepository.cs using System.Linq.Expressions; namespace EnterpriseDemo.Core.Interfaces { public interface IGenericRepository<T> where T : class { Task<T?> GetByIdAsync(int id); Task<IEnumerable<T>> GetAllAsync(); Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate); Task<T> AddAsync(T entity); Task UpdateAsync(T entity); Task DeleteAsync(T entity); Task<bool> ExistsAsync(int id); } }// 文件:src/EnterpriseDemo.Core/Interfaces/IProductRepository.cs using EnterpriseDemo.Core.Entities; namespace EnterpriseDemo.Core.Interfaces { public interface IProductRepository : IGenericRepository<Product> { // 可以定义Product特有的数据访问方法 Task<IEnumerable<Product>> GetProductsByPriceRangeAsync(decimal minPrice, decimal maxPrice); } }同理创建IUserRepository.cs。这样,业务逻辑层(Application)只依赖于这些接口,而不关心底层是用Entity Framework、Dapper还是别的什么实现的。
5. 基础设施层 (Infrastructure):实现数据持久化
这一层负责实现Core层定义的接口,并处理与外部资源(数据库、文件、API等)的交互。
5.1 添加 EF Core 包并配置 DbContext
首先,为EnterpriseDemo.Infrastructure项目添加必要的 NuGet 包。
cd src/EnterpriseDemo.Infrastructure dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Tools # 如果使用SQLite # dotnet add package Microsoft.EntityFrameworkCore.Sqlite然后,创建数据库上下文AppDbContext.cs。
// 文件:src/EnterpriseDemo.Infrastructure/Data/AppDbContext.cs using EnterpriseDemo.Core.Entities; using Microsoft.EntityFrameworkCore; namespace EnterpriseDemo.Infrastructure.Data { public class AppDbContext : DbContext { public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } public DbSet<Product> Products { get; set; } public DbSet<User> Users { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // 这里可以配置实体关系、索引、种子数据等 modelBuilder.Entity<Product>(entity => { entity.HasIndex(p => p.Name).IsUnique(); // 产品名称唯一索引 entity.Property(p => p.Price).HasPrecision(18, 2); // 价格精度 }); modelBuilder.Entity<User>(entity => { entity.HasIndex(u => u.Username).IsUnique(); entity.HasIndex(u => u.Email).IsUnique(); }); // 种子数据 modelBuilder.Entity<Product>().HasData( new Product { Id = 1, Name = "示例产品A", Description = "这是一个种子产品", Price = 99.99m, StockQuantity = 100 }, new Product { Id = 2, Name = "示例产品B", Description = "另一个种子产品", Price = 199.99m, StockQuantity = 50 } ); } } }5.2 实现仓储类
在EnterpriseDemo.Infrastructure/Repositories/下实现具体的仓储。
// 文件:src/EnterpriseDemo.Infrastructure/Repositories/GenericRepository.cs using EnterpriseDemo.Core.Interfaces; using EnterpriseDemo.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using System.Linq.Expressions; namespace EnterpriseDemo.Infrastructure.Repositories { public class GenericRepository<T> : IGenericRepository<T> where T : class { protected readonly AppDbContext _context; protected readonly DbSet<T> _dbSet; public GenericRepository(AppDbContext context) { _context = context; _dbSet = context.Set<T>(); } public virtual async Task<T?> GetByIdAsync(int id) { return await _dbSet.FindAsync(id); } public virtual async Task<IEnumerable<T>> GetAllAsync() { return await _dbSet.ToListAsync(); } public virtual async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate) { return await _dbSet.Where(predicate).ToListAsync(); } public virtual async Task<T> AddAsync(T entity) { await _dbSet.AddAsync(entity); await _context.SaveChangesAsync(); return entity; } public virtual async Task UpdateAsync(T entity) { _dbSet.Update(entity); await _context.SaveChangesAsync(); } public virtual async Task DeleteAsync(T entity) { _dbSet.Remove(entity); await _context.SaveChangesAsync(); } public virtual async Task<bool> ExistsAsync(int id) { var entity = await GetByIdAsync(id); return entity != null; } } }// 文件:src/EnterpriseDemo.Infrastructure/Repositories/ProductRepository.cs using EnterpriseDemo.Core.Entities; using EnterpriseDemo.Core.Interfaces; using EnterpriseDemo.Infrastructure.Data; using Microsoft.EntityFrameworkCore; namespace EnterpriseDemo.Infrastructure.Repositories { public class ProductRepository : GenericRepository<Product>, IProductRepository { public ProductRepository(AppDbContext context) : base(context) { } public async Task<IEnumerable<Product>> GetProductsByPriceRangeAsync(decimal minPrice, decimal maxPrice) { return await _context.Products .Where(p => p.Price >= minPrice && p.Price <= maxPrice) .OrderBy(p => p.Price) .ToListAsync(); } } }UserRepository的实现类似。
5.3 配置依赖注入(Service Extensions)
为了保持Program.cs的整洁,我们创建一个扩展方法来集中注册Infrastructure层的服务。
// 文件:src/EnterpriseDemo.Infrastructure/DependencyInjection.cs using EnterpriseDemo.Core.Interfaces; using EnterpriseDemo.Infrastructure.Data; using EnterpriseDemo.Infrastructure.Repositories; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace EnterpriseDemo.Infrastructure { public static class DependencyInjection { public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) { // 配置数据库上下文 services.AddDbContext<AppDbContext>(options => options.UseSqlServer( configuration.GetConnectionString("DefaultConnection"), b => b.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName))); // 重要:指定迁移程序集 // 注册仓储 services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>)); services.AddScoped<IProductRepository, ProductRepository>(); services.AddScoped<IUserRepository, UserRepository>(); return services; } } }6. 应用层 (Application):实现业务逻辑
Application层是系统的核心,它包含业务规则、工作流和用例。它依赖于Core层的接口,并通过Infrastructure层注入的具体实现来操作数据。
6.1 定义数据传输对象 (DTOs)
Controller不应直接接收或返回实体对象,应使用DTO。在EnterpriseDemo.Application/DTOs/下创建。
// 文件:src/EnterpriseDemo.Application/DTOs/ProductDto.cs using System.ComponentModel.DataAnnotations; namespace EnterpriseDemo.Application.DTOs { public class ProductDto { public int Id { get; set; } [Required] [StringLength(100)] public string Name { get; set; } = string.Empty; [StringLength(500)] public string? Description { get; set; } [Range(0.01, double.MaxValue)] public decimal Price { get; set; } [Range(0, int.MaxValue)] public int StockQuantity { get; set; } } public class CreateProductDto { [Required] [StringLength(100)] public string Name { get; set; } = string.Empty; [StringLength(500)] public string? Description { get; set; } [Range(0.01, double.MaxValue)] public decimal Price { get; set; } [Range(0, int.MaxValue)] public int StockQuantity { get; set; } } public class UpdateProductDto : CreateProductDto { // 更新DTO可能包含ID或其他标识,这里继承自Create } }6.2 定义服务接口与实现
在EnterpriseDemo.Application/Interfaces/和EnterpriseDemo.Application/Services/下创建。
// 文件:src/EnterpriseDemo.Application/Interfaces/IProductService.cs using EnterpriseDemo.Application.DTOs; namespace EnterpriseDemo.Application.Interfaces { public interface IProductService { Task<IEnumerable<ProductDto>> GetAllProductsAsync(); Task<ProductDto?> GetProductByIdAsync(int id); Task<ProductDto> CreateProductAsync(CreateProductDto createProductDto); Task UpdateProductAsync(int id, UpdateProductDto updateProductDto); Task DeleteProductAsync(int id); Task<IEnumerable<ProductDto>> GetProductsByPriceRangeAsync(decimal minPrice, decimal maxPrice); } }// 文件:src/EnterpriseDemo.Application/Services/ProductService.cs using AutoMapper; using EnterpriseDemo.Application.DTOs; using EnterpriseDemo.Application.Interfaces; using EnterpriseDemo.Core.Entities; using EnterpriseDemo.Core.Interfaces; using Microsoft.Extensions.Logging; namespace EnterpriseDemo.Application.Services { public class ProductService : IProductService { private readonly IProductRepository _productRepository; private readonly IMapper _mapper; private readonly ILogger<ProductService> _logger; public ProductService(IProductRepository productRepository, IMapper mapper, ILogger<ProductService> logger) { _productRepository = productRepository; _mapper = mapper; _logger = logger; } public async Task<IEnumerable<ProductDto>> GetAllProductsAsync() { var products = await _productRepository.GetAllAsync(); _logger.LogInformation("获取了所有产品,共 {Count} 条记录", products.Count()); return _mapper.Map<IEnumerable<ProductDto>>(products); } public async Task<ProductDto?> GetProductByIdAsync(int id) { var product = await _productRepository.GetByIdAsync(id); if (product == null) { _logger.LogWarning("未找到ID为 {ProductId} 的产品", id); return null; } return _mapper.Map<ProductDto>(product); } public async Task<ProductDto> CreateProductAsync(CreateProductDto createProductDto) { var productEntity = _mapper.Map<Product>(createProductDto); productEntity.CreatedAt = DateTime.UtcNow; var createdProduct = await _productRepository.AddAsync(productEntity); _logger.LogInformation("创建了新产品,ID: {ProductId}, 名称: {ProductName}", createdProduct.Id, createdProduct.Name); return _mapper.Map<ProductDto>(createdProduct); } public async Task UpdateProductAsync(int id, UpdateProductDto updateProductDto) { var productEntity = await _productRepository.GetByIdAsync(id); if (productEntity == null) { throw new KeyNotFoundException($"未找到ID为 {id} 的产品"); } _mapper.Map(updateProductDto, productEntity); productEntity.UpdatedAt = DateTime.UtcNow; await _productRepository.UpdateAsync(productEntity); _logger.LogInformation("更新了产品,ID: {ProductId}", id); } public async Task DeleteProductAsync(int id) { var productEntity = await _productRepository.GetByIdAsync(id); if (productEntity == null) { throw new KeyNotFoundException($"未找到ID为 {id} 的产品"); } await _productRepository.DeleteAsync(productEntity); _logger.LogInformation("删除了产品,ID: {ProductId}", id); } public async Task<IEnumerable<ProductDto>> GetProductsByPriceRangeAsync(decimal minPrice, decimal maxPrice) { var products = await _productRepository.GetProductsByPriceRangeAsync(minPrice, maxPrice); return _mapper.Map<IEnumerable<ProductDto>>(products); } } }注意:这里使用了AutoMapper进行对象映射。需要为Application项目添加AutoMapper.Extensions.Microsoft.DependencyInjection包。
6.3 配置AutoMapper Profile
在EnterpriseDemo.Application/Common/Mappings/下创建映射配置。
// 文件:src/EnterpriseDemo.Application/Common/Mappings/MappingProfile.cs using AutoMapper; using EnterpriseDemo.Application.DTOs; using EnterpriseDemo.Core.Entities; namespace EnterpriseDemo.Application.Common.Mappings { public class MappingProfile : Profile { public MappingProfile() { CreateMap<Product, ProductDto>().ReverseMap(); CreateMap<Product, CreateProductDto>().ReverseMap(); CreateMap<Product, UpdateProductDto>().ReverseMap(); // 对于Update,我们可能希望部分字段忽略,可以更精细配置 CreateMap<UpdateProductDto, Product>() .ForAllMembers(opts => opts.Condition((src, dest, srcMember) => srcMember != null)); } } }6.4 应用层依赖注入配置
同样,创建一个扩展方法来注册Application层的服务。
// 文件:src/EnterpriseDemo.Application/DependencyInjection.cs using EnterpriseDemo.Application.Common.Mappings; using EnterpriseDemo.Application.Interfaces; using EnterpriseDemo.Application.Services; using Microsoft.Extensions.DependencyInjection; using System.Reflection; namespace EnterpriseDemo.Application { public static class DependencyInjection { public static IServiceCollection AddApplication(this IServiceCollection services) { // 注册AutoMapper,从当前程序集扫描Profile services.AddAutoMapper(Assembly.GetExecutingAssembly()); // 注册应用服务 services.AddScoped<IProductService, ProductService>(); services.AddScoped<IUserService, UserService>(); // 假设有UserService return services; } } }7. 表现层 (API):构建RESTful端点
现在,我们来到最外层,构建供客户端调用的API。
7.1 配置API项目
首先,为EnterpriseDemo.API项目添加对Microsoft.EntityFrameworkCore.Design包的引用,用于迁移。
cd src/EnterpriseDemo.API dotnet add package Microsoft.EntityFrameworkCore.Design然后,修改appsettings.json,添加数据库连接字符串。
// 文件:src/EnterpriseDemo.API/appsettings.json { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=EnterpriseDemoDb;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True" }, "AllowedHosts": "*" }7.2 配置Program.cs
这是应用的入口,负责组装所有部件。
// 文件:src/EnterpriseDemo.API/Program.cs using EnterpriseDemo.Application; using EnterpriseDemo.Infrastructure; var builder = WebApplication.CreateBuilder(args); // 添加服务到容器 builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // 添加Swagger // 注册我们自定义的各层服务 builder.Services.AddApplication(); builder.Services.AddInfrastructure(builder.Configuration); var app = builder.Build(); // 配置HTTP请求管道 if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); // 确保数据库被创建并应用迁移(仅用于开发环境) using (var scope = app.Services.CreateScope()) { var dbContext = scope.ServiceProvider.GetRequiredService<Infrastructure.Data.AppDbContext>(); dbContext.Database.EnsureCreated(); // 或使用 dbContext.Database.Migrate() 如果使用了迁移 } app.Run();7.3 创建Product控制器
在Controllers文件夹下创建ProductsController.cs。
// 文件:src/EnterpriseDemo.API/Controllers/ProductsController.cs using EnterpriseDemo.Application.DTOs; using EnterpriseDemo.Application.Interfaces; using Microsoft.AspNetCore.Mvc; namespace EnterpriseDemo.API.Controllers { [Route("api/[controller]")] [ApiController] public class ProductsController : ControllerBase { private readonly IProductService _productService; public ProductsController(IProductService productService) { _productService = productService; } // GET: api/products [HttpGet] public async Task<ActionResult<IEnumerable<ProductDto>>> GetProducts() { var products = await _productService.GetAllProductsAsync(); return Ok(products); } // GET: api/products/5 [HttpGet("{id}")] public async Task<ActionResult<ProductDto>> GetProduct(int id) { var product = await _productService.GetProductByIdAsync(id); if (product == null) { return NotFound(); } return Ok(product); } // POST: api/products [HttpPost] public async Task<ActionResult<ProductDto>> CreateProduct(CreateProductDto createProductDto) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var createdProduct = await _productService.CreateProductAsync(createProductDto); return CreatedAtAction(nameof(GetProduct), new { id = createdProduct.Id }, createdProduct); } // PUT: api/products/5 [HttpPut("{id}")] public async Task<IActionResult> UpdateProduct(int id, UpdateProductDto updateProductDto) { if (id != updateProductDto.Id) // 假设UpdateProductDto包含Id { return BadRequest("ID不匹配"); } if (!ModelState.IsValid) { return BadRequest(ModelState); } try { await _productService.UpdateProductAsync(id, updateProductDto); } catch (KeyNotFoundException) { return NotFound(); } return NoContent(); } // DELETE: api/products/5 [HttpDelete("{id}")] public async Task<IActionResult> DeleteProduct(int id) { try { await _productService.DeleteProductAsync(id); } catch (KeyNotFoundException) { return NotFound(); } return NoContent(); } // GET: api/products/price-range?min=10&max=100 [HttpGet("price-range")] public async Task<ActionResult<IEnumerable<ProductDto>>> GetProductsByPriceRange([FromQuery] decimal min, [FromQuery] decimal max) { if (min > max) { return BadRequest("最低价格不能高于最高价格"); } var products = await _productService.GetProductsByPriceRangeAsync(min, max); return Ok(products); } } }8. 运行与验证
8.1 运行项目
- 在终端中,导航到
src/EnterpriseDemo.API目录。 - 运行
dotnet run。 - 控制台会输出应用监听的地址,通常是
https://localhost:5001和http://localhost:5000。
8.2 使用Swagger UI测试API
- 在浏览器中打开
https://localhost:5001/swagger(或对应的HTTP地址)。 - 你将看到自动生成的API文档,列出了
Products的所有端点。 - 尝试执行以下操作:
- GET /api/products: 应返回两个种子产品。
- POST /api/products: 点击“Try it out”,填入JSON数据创建新产品。
{ "name": "新测试产品", "description": "通过API创建", "price": 299.99, "stockQuantity": 10 }- GET /api/products/{id}: 使用新创建产品的ID获取它。
- PUT /api/products/{id}: 更新产品信息。
- DELETE /api/products/{id}: 删除产品。
- GET /api/products/price-range?min=50&max=200: 查询价格范围内的产品。
8.3 验证数据库
可以使用SQL Server Object Explorer(VS) 或Azure Data Studio等工具连接到(localdb)\mssqllocaldb,查看EnterpriseDemoDb数据库中的Products和Users表,确认数据已持久化。
9. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
启动时报错:无法找到AppDbContext的构造函数 | 1.AppDbContext未在Program.cs中通过AddDbContext注册。2. 连接字符串配置错误。 | 1. 检查Program.cs中是否调用了AddInfrastructure。2. 检查 appsettings.json中的ConnectionStrings节点。 | 1. 确保services.AddInfrastructure(configuration);被调用。2. 确认连接字符串格式正确,数据库实例存在。 |
运行迁移命令dotnet ef migrations add InitialCreate失败 | 1. 未在启动项目(API)中安装Microsoft.EntityFrameworkCore.Design。2. 未指定启动项目和DbContext所在项目。 | 查看错误信息,通常提示“No DbContext was found”。 | 1. 确保在API项目中安装了Microsoft.EntityFrameworkCore.Design包。2. 使用完整命令: dotnet ef migrations add InitialCreate -s ../EnterpriseDemo.API -p ../EnterpriseDemo.Infrastructure |
| Swagger页面能打开,但调用API返回404 | 1. 控制器路由配置错误。 2. 请求的HTTP方法或URL不正确。 | 1. 检查控制器上的[Route("api/[controller]")]属性。2. 在Swagger UI中查看每个端点的完整路径。 | 1. 确保控制器继承自ControllerBase并具有[ApiController]属性。2. 严格按照Swagger显示的URL和参数格式调用。 |
| AutoMapper映射失败:缺少类型映射配置 | 1.MappingProfile未正确注册到AutoMapper。2. DTO和Entity的属性名或类型不匹配。 | 1. 检查AddApplication方法中是否调用了AddAutoMapper。2. 在服务中注入 IMapper并单步调试,查看映射异常信息。 | 1. 确保AddAutoMapper(Assembly.GetExecutingAssembly())能扫描到包含MappingProfile的程序集。2. 在 MappingProfile中为复杂的映射关系添加自定义CreateMap配置。 |
| 依赖注入错误:无法解析服务 | 1. 服务(如IProductService)未在DI容器中注册。2. 服务生命周期配置错误(如Scoped服务在Singleton中注入)。 | 查看启动时的异常堆栈信息,会明确指出哪个服务无法解析。 | 1. 检查IProductService和ProductService是否在AddApplication中通过AddScoped注册。2. 确保服务生命周期匹配。通常仓储和服务使用 Scoped。 |
10. 最佳实践与工程建议
- 异步编程: 如上所示,全程使用
async/await,避免阻塞线程,提高Web应用的并发能力。 - 日志记录: 在服务层使用
ILogger记录关键操作和异常,便于生产环境排查问题。避免在Controller中记录过多业务日志。 - 异常处理: 在Controller中进行基本的模型验证和异常捕获,返回合适的HTTP状态码(如404、400)。更复杂的业务异常可以考虑使用自定义异常和全局异常过滤器。
- 输入验证: 除了Controller的
[ApiController]自动模型验证,在DTO上使用数据注解(如[Required],[StringLength])进行声明式验证。复杂规则应在服务层实现。 - 跨域请求 (CORS): 如果前端独立部署,需要在
Program.cs中配置CORS策略。 - 环境配置: 使用
appsettings.Development.json和appsettings.Production.json管理不同环境的配置(如数据库连接字符串、日志级别)。 - 单元测试: 为服务层编写单元测试,使用
xUnit和Moq模拟仓储依赖,确保业务逻辑正确性。 - 迁移管理: 对于生产环境,使用EF Core的代码迁移 (
dotnet ef migrations) 来管理数据库架构变更,而不是EnsureCreated。 - API版本控制: 当API需要重大变更时,考虑引入API版本控制(如
Microsoft.AspNetCore.Mvc.Versioning)。 - 性能考量: 对于大型数据集,在仓储层实现分页查询,避免一次性加载所有数据。考虑使用
AsNoTracking()进行只读查询以提升性能。
至此,你已经拥有了一个结构清晰、职责分明、可测试、可扩展的C#企业级项目骨架。这个项目模板清晰地展示了分层架构、依赖注入、仓储模式、DTO映射等核心概念是如何协同工作的。你可以在此基础上,轻松地添加新的实体、业务逻辑和API端点,例如实现完整的用户认证授权(JWT)、更复杂的查询过滤、文件上传、缓存集成、消息队列等高级功能。真正的企业级项目正是在这样健壮的基础上,通过不断解决具体的业务需求而演化出来的。建议你将此项目作为模板保存,并在未来的开发中反复实践和优化这些模式。