请推荐基于.NET Framework(非.NET Core)的ASP.NET WebApi现代应用架构及配套技术方案
针对你的需求,我整理了一套基于.NET Framework的ASP.NET WebAPI现代架构方案,涵盖架构设计、ORM选型、数据库迁移、AD认证以及代码示例,同时解决你提到的EF Migrations数据丢失问题:
推荐方案:基于.NET Framework的ASP.NET WebAPI现代架构(Oracle + AD认证)
一、核心架构设计(分层+关键模式)
这套架构兼顾了业务解耦、可测试性和可维护性,完全适配你的技术选型需求:
- 分层架构:
- Controller:仅处理HTTP请求参数校验、响应封装,依赖Manager层完成业务逻辑,不直接操作数据库
- Manager:业务逻辑核心层,协调多个Repository操作,通过Unit of Work管控事务
- Repository:数据访问层,封装Oracle数据库CRUD操作,对外暴露业务友好的数据接口
- Unit of Work模式:统一管理EF上下文的事务,确保多个Repository的操作要么全部提交,要么全部回滚,避免数据不一致
- 辅助工具集成:
- Automapper:简化DTO与实体模型之间的映射,消除重复的对象转换代码
- Serilog:实现结构化日志输出,支持写入文件、Oracle数据库等,便于线上问题排查
- xUnit:搭配Moq框架对Manager、Repository层做单元/集成测试,保障代码质量
二、ORM选型与数据丢失问题解决
1. 优先选型:Entity Framework 6(EF6)+ Oracle Provider
针对.NET Framework,EF6对Oracle的支持最为成熟,推荐使用Oracle Data Provider for .NET (ODP.NET) 或第三方的Devart dotConnect for Oracle(稳定性更强)。
2. 优化EF Migrations流程,避免数据丢失
你之前的流程确实存在数据丢失风险——EF自动生成的迁移脚本可能会误删字段、清空表数据。我建议调整为手动控制迁移脚本的安全流程:
优化后流程:
- Code First模型设计:用Fluent API定义实体与Oracle表的映射(比DataAnnotation更灵活)
- 生成空白迁移类:执行
Add-Migration MigrationName -Empty生成空迁移,手动编写SQL变更脚本(比如新增字段、修改约束时,先备份数据再执行变更)- 本地验证:在开发库执行手动编写的迁移脚本,确认数据安全、结构符合预期
- 集成Liquibase:将验证后的脚本导入Liquibase做版本管理,用于CI/CD部署
- 回滚准备:为每个迁移脚本编写对应的回滚脚本,Liquibase可自动管理版本回滚
3. 备选ORM:Dapper
如果对性能要求极高,或需要灵活控制复杂Oracle查询,Dapper是轻量型首选。它可以和Unit of Work模式结合,实现高效的数据访问。
三、Oracle数据库迁移体系(支持异步)
1. 主方案:EF6手动迁移脚本 + Liquibase
- Liquibase优势:原生支持Oracle,版本化管理迁移脚本,支持异步执行(通过CLI异步命令或.NET API调用),自带回滚机制
- 集成流程:
- 每个EF手动迁移对应一个Liquibase变更集,将SQL脚本写入
changelog.xml - CI/CD管道中,用Liquibase CLI异步执行变更集,避免阻塞部署流程
- 用Liquibase的标签功能标记数据库版本,快速定位回滚节点
- 每个EF手动迁移对应一个Liquibase变更集,将SQL脚本写入
2. 备选方案:Oracle SQL Developer + 数据库项目
如果你更习惯传统脚本管理,可使用Oracle SQL Developer的版本控制功能,配合Visual Studio的Oracle数据库项目插件(第三方)对比生成变更脚本,再通过SQL*Plus异步命令完成部署。
四、AD/Windows认证实现
1. Web.config配置
<system.web> <authentication mode="Windows" /> <authorization> <deny users="?" /> <!-- 拒绝匿名访问 --> </authorization> </system.web> <system.webServer> <security> <authentication> <windowsAuthentication enabled="true" /> <anonymousAuthentication enabled="false" /> </authentication> </security> </system.webServer>
2. Controller中获取AD用户信息
[Authorize] public class UserController : ApiController { public IHttpActionResult GetCurrentUser() { var adUser = User.Identity.Name; // 格式:DOMAIN\Username return Ok(new { AdUserName = adUser }); } }
五、核心代码示例
1. Unit of Work + Repository(EF6)
// IUnitOfWork接口 public interface IUnitOfWork : IDisposable { IEmployeeRepository Employees { get; } Task<int> SaveChangesAsync(); } // UnitOfWork实现 public class UnitOfWork : IUnitOfWork { private readonly OracleDbContext _context; private IEmployeeRepository _employeeRepo; public UnitOfWork(OracleDbContext context) => _context = context; public IEmployeeRepository Employees => _employeeRepo ??= new EmployeeRepository(_context); public async Task<int> SaveChangesAsync() => await _context.SaveChangesAsync(); public void Dispose() => _context.Dispose(); } // Repository接口与实现 public interface IEmployeeRepository { Task<Employee> GetByIdAsync(int id); Task AddAsync(Employee employee); } public class EmployeeRepository : IEmployeeRepository { private readonly OracleDbContext _context; public EmployeeRepository(OracleDbContext context) => _context = context; public async Task<Employee> GetByIdAsync(int id) => await _context.Employees.FindAsync(id); public async Task AddAsync(Employee employee) => await _context.Employees.AddAsync(employee); } // Oracle DbContext配置 public class OracleDbContext : DbContext { public OracleDbContext() : base("OracleConnectionString") { Configuration.LazyLoadingEnabled = false; Configuration.ProxyCreationEnabled = false; } public DbSet<Employee> Employees { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { // Fluent API映射Oracle表 modelBuilder.Entity<Employee>() .ToTable("EMPLOYEES") .HasKey(e => e.Id); modelBuilder.Entity<Employee>() .Property(e => e.Id).HasColumnName("EMP_ID"); modelBuilder.Entity<Employee>() .Property(e => e.Name).HasColumnName("EMP_NAME").IsRequired().HasMaxLength(100); } }
2. Manager层示例(含Automapper)
public interface IEmployeeManager { Task<EmployeeDto> GetEmployeeByIdAsync(int id); Task<int> CreateEmployeeAsync(EmployeeDto dto); } public class EmployeeManager : IEmployeeManager { private readonly IUnitOfWork _unitOfWork; private readonly IMapper _mapper; public EmployeeManager(IUnitOfWork unitOfWork, IMapper mapper) { _unitOfWork = unitOfWork; _mapper = mapper; } public async Task<EmployeeDto> GetEmployeeByIdAsync(int id) { var employee = await _unitOfWork.Employees.GetByIdAsync(id); if (employee == null) throw new KeyNotFoundException($"Employee {id} not found"); return _mapper.Map<EmployeeDto>(employee); } public async Task<int> CreateEmployeeAsync(EmployeeDto dto) { var employee = _mapper.Map<Employee>(dto); await _unitOfWork.Employees.AddAsync(employee); await _unitOfWork.SaveChangesAsync(); return employee.Id; } } // Automapper配置 public class MappingProfile : Profile { public MappingProfile() { CreateMap<Employee, EmployeeDto>(); CreateMap<EmployeeDto, Employee>(); } }
3. Serilog初始化(Global.asax)
protected void Application_Start() { // 配置Serilog输出到文件和Oracle数据库 Log.Logger = new LoggerConfiguration() .WriteTo.File("logs/log-.txt", rollingInterval: RollingInterval.Day) .WriteTo.Oracle(cfg => cfg.ConnectionString("OracleConnectionString") .TableName("WEBAPI_LOGS") .ColumnOptions(o => { o.AddStandardColumns(StandardColumn.LogEvent); o.AddColumn("UserName", col => col.PropertyValue(x => x.Properties["UserName"])); })) .CreateLogger(); GlobalConfiguration.Configure(WebApiConfig.Register); // 初始化Automapper AutoMapper.Mapper.Initialize(cfg => cfg.AddProfile<MappingProfile>()); }
六、关键注意事项
- Oracle连接字符串:使用ODP.NET格式,例如:
Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=oracle-server)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=ORCL)));User Id=api_user;Password=xxx; - 异步优先:所有数据访问、业务逻辑方法都使用
async/await,提升WebAPI并发能力 - 数据备份:迁移前通过Liquibase预执行脚本备份涉及变更的表,避免数据丢失
- 单元测试:用xUnit+Moq模拟Repository和Unit of Work,对Manager层做无依赖测试
内容的提问来源于stack exchange,提问作者Boris Gappov
相关产品推荐
相关产品推荐

