当前位置: 首页 > news >正文

手把手搭建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 开发环境准备

  1. 安装 .NET 8 SDK: 前往 微软官网 下载并安装。
  2. 安装 IDE: 推荐使用Visual Studio 2022(社区版免费) 并确保安装了“ASP.NET和Web开发”工作负载。或者使用JetBrains RiderVisual Studio Code
  3. 数据库: 确保已安装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.csproj

3.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.csproj

4. 核心层 (Core):定义领域模型与契约

Core 项目应该保持“纯净”,不依赖任何其他项目。它包含系统的核心概念。

4.1 定义领域实体

EnterpriseDemo.Core/Entities/文件夹下创建两个实体类:Product.csUser.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 运行项目

  1. 在终端中,导航到src/EnterpriseDemo.API目录。
  2. 运行dotnet run
  3. 控制台会输出应用监听的地址,通常是https://localhost:5001http://localhost:5000

8.2 使用Swagger UI测试API

  1. 在浏览器中打开https://localhost:5001/swagger(或对应的HTTP地址)。
  2. 你将看到自动生成的API文档,列出了Products的所有端点。
  3. 尝试执行以下操作:
    • 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数据库中的ProductsUsers表,确认数据已持久化。

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返回4041. 控制器路由配置错误。
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. 检查IProductServiceProductService是否在AddApplication中通过AddScoped注册。
2. 确保服务生命周期匹配。通常仓储和服务使用Scoped

10. 最佳实践与工程建议

  1. 异步编程: 如上所示,全程使用async/await,避免阻塞线程,提高Web应用的并发能力。
  2. 日志记录: 在服务层使用ILogger记录关键操作和异常,便于生产环境排查问题。避免在Controller中记录过多业务日志。
  3. 异常处理: 在Controller中进行基本的模型验证和异常捕获,返回合适的HTTP状态码(如404、400)。更复杂的业务异常可以考虑使用自定义异常和全局异常过滤器。
  4. 输入验证: 除了Controller的[ApiController]自动模型验证,在DTO上使用数据注解(如[Required],[StringLength])进行声明式验证。复杂规则应在服务层实现。
  5. 跨域请求 (CORS): 如果前端独立部署,需要在Program.cs中配置CORS策略。
  6. 环境配置: 使用appsettings.Development.jsonappsettings.Production.json管理不同环境的配置(如数据库连接字符串、日志级别)。
  7. 单元测试: 为服务层编写单元测试,使用xUnitMoq模拟仓储依赖,确保业务逻辑正确性。
  8. 迁移管理: 对于生产环境,使用EF Core的代码迁移 (dotnet ef migrations) 来管理数据库架构变更,而不是EnsureCreated
  9. API版本控制: 当API需要重大变更时,考虑引入API版本控制(如Microsoft.AspNetCore.Mvc.Versioning)。
  10. 性能考量: 对于大型数据集,在仓储层实现分页查询,避免一次性加载所有数据。考虑使用AsNoTracking()进行只读查询以提升性能。

至此,你已经拥有了一个结构清晰、职责分明、可测试、可扩展的C#企业级项目骨架。这个项目模板清晰地展示了分层架构、依赖注入、仓储模式、DTO映射等核心概念是如何协同工作的。你可以在此基础上,轻松地添加新的实体、业务逻辑和API端点,例如实现完整的用户认证授权(JWT)、更复杂的查询过滤、文件上传、缓存集成、消息队列等高级功能。真正的企业级项目正是在这样健壮的基础上,通过不断解决具体的业务需求而演化出来的。建议你将此项目作为模板保存,并在未来的开发中反复实践和优化这些模式。

http://www.jsqmd.com/news/1310493/

相关文章:

  • 2026年8月湖南省电信1000M单宽带安装流程 - 找卡家园
  • 2026年8月湖南省电信1000M融合宽带我的真实避坑攻略 - 找卡家园
  • 东莞代理报关公司联系电话汇总,含进口清关等业务 - 品牌排行榜
  • 非机动车闯红灯电动摩托车闯红灯检测数据集VOC+YOLO格式1754张3类别
  • 5分钟免费解锁WeMod专业版:终极游戏修改体验指南
  • 郑州严格管理民办高中盘点:家长择校重点看什么? - 品牌排行榜
  • 树莓派智能魔镜DIY:从硬件选型到MagicMirror²配置全攻略
  • Python零基础实战入门:从环境搭建到项目实战的避坑指南
  • 汉字转拼音工具2.0一款汉字转拼音的功能
  • 2026学术写作一键生成论文工具全榜单:7款实测,哪款论文党看完直接闭眼选?
  • SPI协议深度解析:从基础时序到四种工作模式与实战调试
  • 2026年江苏泰州粉末涂料供货商哪家靠谱?榜单揭晓 - 品牌排行榜
  • 构建高质量跌倒检测数据集:核心要素、主流资源与实战指南
  • 适配“十五五”降碳,哪家电梯节能企业更胜一筹? - 品牌排行榜
  • 2026年8月湖南省电信500M单宽带怎么选不踩坑_一篇说透 - 找卡家园
  • 广州民营企业主经济犯罪律师推荐:【法纳刑辩】赞誉有加 - 松梢月冷
  • Fable 5解禁:AI驱动的交互式叙事与智能体模拟平台实操指南
  • 系统级工具链开发与 Cargo Workspaces 工作区管理:基于 Monorepo 的多 Crates 组织实战
  • 2026 年现阶段白河优秀的小红书+AI内容创作品牌哪家专业,靠这俩货帮我涨了五千粉,原来小红书内容还能这么玩? - 领域鉴赏官
  • 游戏开发视角下的走A技术:原理、价值与实战应用
  • 量子力学基础:从实验危机到态空间语言的核心框架
  • Unity游戏逆向:Il2Cpp元数据损坏的深度修复与重建实战
  • Sketchfab模型下载工具架构解析:基于Firefox的beforescriptexecute事件拦截技术实现
  • AI代码迁移成功率提升73%的关键路径:从Python到Java的自动化迁移框架实操手册
  • 2026年8月湖南省电信500M单宽带怎么选_一篇说透 - 找卡家园
  • 计算机名称修改工具2025更新计算机名可以是任意字符
  • 2026 年更新:海南高性价比钢制闸门实力厂家哪家专业,水库汛期漏洪?原来这套能扛住万吨水压的家伙,才是守护堤坝的隐形屏障 - 行业严选官
  • 2026年8月湖南省电信1000M融合宽带套餐避坑全攻略 - 找卡家园
  • AI语音生成器推荐:免费好用、无需下载网页版、国内合规文字转语音工具怎么挑 - 优企甄选
  • Python 科学计算与高性能编程:基于 NumPy 矢量化与 Numba JIT 的算子加速实战