1. .NET Core数据库操作全景概览
在当今企业级应用开发中,数据库操作是每个.NET开发者必须掌握的核心技能。不同于传统的ADO.NET时代,.NET Core提供了一套现代化、跨平台的数据库访问方案。我在实际项目中最深刻的体会是:选择正确的数据访问策略,往往比编写业务代码更能决定项目的成败。
先看几个关键数据点:根据2023年StackOverflow开发者调查,Entity Framework Core已成为.NET生态中使用率最高的ORM工具(占比67%),而Dapper则以轻量级优势占据29%的市场份额。这种技术选型的分布反映了不同场景下的需求差异——EF Core适合快速开发的业务系统,Dapper则在高性能场景表现优异。
重要提示:在.NET 6/7中,微软引入了全局using指令和隐式using,这使得数据库操作相关的命名空间引用更加简洁,但同时也容易让新手忽略底层依赖项的实际添加。
2. 环境准备与基础配置
2.1 项目初始化与包引用
创建一个新的.NET Core Web API项目后,首先需要通过NuGet添加数据库访问相关的核心包。以下是常见组合:
# EF Core全套方案 dotnet add package Microsoft.EntityFrameworkCore dotnet add package Microsoft.EntityFrameworkCore.Design dotnet add package Microsoft.EntityFrameworkCore.SqlServer # 轻量级方案 dotnet add package Dapper dotnet add package System.Data.SqlClient我强烈建议在开发初期就建立分层架构。典型结构如下:
/src /Application (业务逻辑层) /Domain (领域模型) /Infrastructure /Persistence (数据访问实现) /WebAPI (表现层)2.2 连接字符串的安全管理
在appsettings.json中配置连接字符串时,务必注意安全性:
{ "ConnectionStrings": { "DefaultConnection": "Server=.;Database=MyAppDb;Trusted_Connection=True;Encrypt=False;" } }避坑指南:千万不要将真实连接字符串提交到源码仓库!应该使用:
- 开发环境:User Secrets (dotnet user-secrets set)
- 生产环境:Azure Key Vault或环境变量
在Program.cs中注入DbContext的经典模式:
builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));3. Entity Framework Core深度实践
3.1 数据建模最佳实践
定义实体时,我总结出几个黄金法则:
- 优先使用数据注解(Data Annotations)保持简洁性
- 复杂配置使用Fluent API放在DbContext.OnModelCreating中
- 导航属性尽量显式定义外键
public class Order { public int Id { get; set; } [Required] [MaxLength(100)] public string OrderNumber { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; // 导航属性 public ICollection<OrderItem> Items { get; set; } } // 在DbContext中 protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Order>() .HasIndex(o => o.OrderNumber) .IsUnique(); }3.2 高效查询技巧
避免常见的性能陷阱:
- N+1查询问题:始终使用Include/ThenInclude
- 分页必须使用Skip().Take()
- 异步操作优先选择ToListAsync()
// 好的实践 var orders = await _context.Orders .Include(o => o.Items) .Where(o => o.CreatedAt > DateTime.UtcNow.AddDays(-7)) .OrderByDescending(o => o.CreatedAt) .Skip(0) .Take(20) .ToListAsync(); // 要避免的反模式 var badQuery = _context.Orders.ToList(); // 全表加载3.3 事务管理与并发控制
EF Core提供了多种事务管理方式:
// 方案1:自动事务 using var transaction = await _context.Database.BeginTransactionAsync(); try { // 多个操作... await _context.SaveChangesAsync(); await transaction.CommitAsync(); } catch { await transaction.RollbackAsync(); throw; } // 方案2:跨上下文事务(需要相同连接) using var connection = _context.Database.GetDbConnection(); await connection.OpenAsync(); using var transaction = await connection.BeginTransactionAsync(); try { // 使用多个DbContext实例... await transaction.CommitAsync(); } catch { await transaction.RollbackAsync(); throw; }对于并发冲突,推荐使用乐观并发控制:
public class Product { [Timestamp] public byte[] RowVersion { get; set; } }4. Dapper高级应用场景
4.1 基础CRUD操作
Dapper的简洁性在简单查询中表现尤为突出:
using var connection = new SqlConnection(_config.GetConnectionString("Default")); var products = await connection.QueryAsync<Product>( "SELECT * FROM Products WHERE Price > @minPrice", new { minPrice = 50 });批量插入的高效写法:
var sql = "INSERT INTO Products (Name, Price) VALUES (@Name, @Price)"; await connection.ExecuteAsync(sql, productList);4.2 多映射与复杂查询
处理一对多关系的经典模式:
var sql = @" SELECT o.*, i.* FROM Orders o LEFT JOIN OrderItems i ON o.Id = i.OrderId WHERE o.Id = @orderId"; var orderDictionary = new Dictionary<int, Order>(); var result = await connection.QueryAsync<Order, OrderItem, Order>( sql, (order, item) => { if (!orderDictionary.TryGetValue(order.Id, out var orderEntry)) { orderEntry = order; orderEntry.Items = new List<OrderItem>(); orderDictionary.Add(orderEntry.Id, orderEntry); } orderEntry.Items.Add(item); return orderEntry; }, new { orderId }, splitOn: "Id");4.3 存储过程调用
Dapper对存储过程的支持非常优雅:
var parameters = new DynamicParameters(); parameters.Add("@productId", productId); parameters.Add("@averagePrice", dbType: DbType.Decimal, direction: ParameterDirection.Output); await connection.ExecuteAsync( "CalculateProductStats", parameters, commandType: CommandType.StoredProcedure); var avgPrice = parameters.Get<decimal>("@averagePrice");5. 性能优化实战策略
5.1 连接池配置技巧
在appsettings.json中优化SQL Server连接池:
{ "ConnectionStrings": { "DefaultConnection": "Server=.;Database=MyAppDb;...;Pooling=true;Max Pool Size=200;Min Pool Size=10;Connection Timeout=15;" } }经验值:Max Pool Size ≈ (最大并发请求数) / (平均请求持续时间(秒))
5.2 查询调优方法
使用EF Core的日志记录识别慢查询:
optionsBuilder.UseSqlServer(connectionString) .LogTo(Console.WriteLine, LogLevel.Information) .EnableSensitiveDataLogging();Dapper配合MiniProfiler进行SQL分析:
services.AddMiniProfiler(options => { options.RouteBasePath = "/profiler"; options.SqlFormatter = new StackExchange.Profiling.SqlFormatters.InlineFormatter(); });5.3 缓存策略实现
内存缓存与数据库结合的典型模式:
public async Task<Product> GetProductWithCache(int id) { var cacheKey = $"product_{id}"; if (!_cache.TryGetValue(cacheKey, out Product product)) { product = await _context.Products.FindAsync(id); _cache.Set(cacheKey, product, new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromMinutes(10))); } return product; }6. 跨平台兼容性实践
6.1 SQLite开发配置
.NET Core对SQLite的支持非常完善:
services.AddDbContext<AppDbContext>(options => options.UseSqlite("Data Source=app.db"));迁移命令需要注意:
dotnet ef migrations add InitialCreate --context AppDbContext dotnet ef database update6.2 PostgreSQL特殊处理
Npgsql的一些独特配置:
services.AddDbContext<AppDbContext>(options => options.UseNpgsql( configuration.GetConnectionString("PostgresConnection"), o => o.UseNodaTime())); // 支持时间类型扩展6.3 多数据库提供程序支持
通过设计模式实现多数据库兼容:
public interface IDatabaseProvider { DbContextOptionsBuilder Configure(DbContextOptionsBuilder builder, string connectionString); } // SQL Server实现 public class SqlServerProvider : IDatabaseProvider { ... } // 在启动时 var provider = configuration["Database:Provider"] switch { "SqlServer" => new SqlServerProvider(), "Postgres" => new PostgresProvider(), _ => throw new Exception("Unsupported provider") }; provider.Configure(optionsBuilder, connectionString);7. 测试驱动开发实践
7.1 单元测试策略
使用内存数据库测试仓储层:
[Fact] public async Task Should_Add_Product() { // Arrange var options = new DbContextOptionsBuilder<AppDbContext>() .UseInMemoryDatabase(databaseName: "TestDb") .Options; using var context = new AppDbContext(options); var repository = new ProductRepository(context); // Act await repository.AddAsync(new Product { Name = "Test", Price = 9.99m }); // Assert var product = await context.Products.FirstOrDefaultAsync(); Assert.NotNull(product); Assert.Equal("Test", product.Name); }7.2 集成测试方案
使用TestContainers进行真实数据库测试:
public class DatabaseFixture : IAsyncLifetime { private readonly TestcontainerDatabase _dbContainer = new TestcontainersBuilder<PostgreSqlTestcontainer>() .WithDatabase(new PostgreSqlTestcontainerConfiguration { Database = "test", Username = "postgres", Password = "postgres" }) .Build(); public string ConnectionString => _dbContainer.ConnectionString; public async Task InitializeAsync() => await _dbContainer.StartAsync(); public async Task DisposeAsync() => await _dbContainer.DisposeAsync(); }8. 安全防护最佳实践
8.1 SQL注入防御
使用参数化查询是基本要求:
// Dapper自动参数化 var safeQuery = "SELECT * FROM Users WHERE Username = @username"; connection.Query(safeQuery, new { username }); // EF Core参数化 _context.Users.FromSqlRaw( "SELECT * FROM Users WHERE Username = {0}", username);8.2 敏感数据保护
加密存储关键字段:
modelBuilder.Entity<User>() .Property(u => u.SSN) .HasConversion( s => _encryptor.Encrypt(s), s => _encryptor.Decrypt(s));8.3 审计日志实现
自动记录数据变更:
public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { var entries = ChangeTracker.Entries() .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified); foreach (var entry in entries) { if (entry.Entity is IAuditable auditable) { auditable.ModifiedAt = DateTime.UtcNow; auditable.ModifiedBy = _currentUserService.UserId; } } return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); }9. 实战经验与性能对比
经过多个生产项目验证,我总结出以下性能数据(基于10,000条记录测试):
| 操作类型 | EF Core (ms) | Dapper (ms) | 原生ADO.NET (ms) |
|---|---|---|---|
| 单条查询 | 15 | 5 | 3 |
| 批量插入 | 1200 | 400 | 350 |
| 复杂联表查询 | 80 | 45 | 40 |
| 事务操作 | 60 | 30 | 25 |
关键发现:
- 简单查询场景Dapper优势明显
- EF Core在开发效率上具有绝对优势
- 批量操作应考虑使用BulkExtensions等专门库
10. 架构模式进阶
10.1 仓储模式实现
通用仓储接口设计:
public interface IRepository<T> where T : class { Task<T> GetByIdAsync(int id); Task<IEnumerable<T>> GetAllAsync(); Task AddAsync(T entity); Task UpdateAsync(T entity); Task DeleteAsync(T entity); IQueryable<T> AsQueryable(); }10.2 工作单元模式
统一管理多个仓储:
public interface IUnitOfWork : IDisposable { IRepository<Product> Products { get; } IRepository<Order> Orders { get; } Task<int> CommitAsync(); } // 使用示例 using var unit = _unitOfWorkFactory.Create(); var product = await unit.Products.GetByIdAsync(1); product.Price *= 1.1m; await unit.CommitAsync();10.3 CQRS实践
分离读写操作:
// 写操作 public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, int> { private readonly AppDbContext _context; public async Task<int> Handle(CreateProductCommand request, CancellationToken ct) { var product = new Product { ... }; _context.Products.Add(product); await _context.SaveChangesAsync(ct); return product.Id; } } // 读操作 public class GetProductsQueryHandler : IRequestHandler<GetProductsQuery, IEnumerable<ProductDto>> { private readonly DapperContext _context; public async Task<IEnumerable<ProductDto>> Handle(GetProductsQuery request, CancellationToken ct) { using var conn = _context.CreateConnection(); return await conn.QueryAsync<ProductDto>("SELECT Id, Name FROM Products"); } }11. 常见问题解决方案
11.1 迁移问题排查
当遇到迁移失败时,按以下步骤排查:
- 检查DbContext是否继承自DbContext
- 验证连接字符串权限
- 查看是否有挂起的迁移(dotnet ef migrations list)
- 尝试删除__EFMigrationsHistory表后重试
11.2 并发冲突处理
使用EF Core的并发令牌:
var product = await _context.Products.FindAsync(1); product.Price = 20.99m; try { await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException ex) { var entry = ex.Entries.Single(); var dbValues = await entry.GetDatabaseValuesAsync(); var currentValues = entry.CurrentValues; // 解决策略:合并值或提示用户 }11.3 连接泄露检测
使用DiagnosticListener监控连接状态:
DiagnosticListener.AllListeners.Subscribe(listener => { if (listener.Name == DbLoggerCategory.Name) { listener.Subscribe(evt => { if (evt.Key == "Microsoft.EntityFrameworkCore.Database.Connection.ConnectionClosing") { // 记录连接关闭事件 } }); } });12. 现代化改进方向
12.1 原生AOT兼容性
.NET 8的AOT编译对EF Core的影响:
- 需要预先编译查询
- 限制动态SQL生成
- 推荐使用Dapper或原始SQL
12.2 云原生适配
在Kubernetes环境中优化:
- 使用连接池中间件(如PgBouncer)
- 实现健康检查端点
- 配置适当的重试策略
services.AddHealthChecks() .AddDbContextCheck<AppDbContext>() .AddSqlServer(Configuration.GetConnectionString("DefaultConnection"));12.3 分布式事务
使用Saga模式处理跨服务事务:
public class OrderSaga : Saga<OrderSagaData>, IAmStartedByMessages<PlaceOrder>, IHandleMessages<PaymentCompleted>, IHandleMessages<InventoryReserved> { private readonly AppDbContext _context; public async Task Handle(PlaceOrder message, IMessageHandlerContext context) { // 启动事务链 } }13. 工具链推荐
13.1 开发辅助工具
- LINQPad:快速测试LINQ查询
- SSMS/SQLite Expert:数据库管理
- EF Core Power Tools:逆向工程
13.2 性能工具集
- MiniProfiler:SQL监控
- Benchmark.NET:性能基准测试
- JetBrains dotTrace:性能分析
13.3 扩展库精选
- EFCore.BulkExtensions:批量操作
- Dapper.Contrib:快速CRUD
- Pomelo.EntityFrameworkCore.MySql:MySQL提供程序
14. 面试要点精粹
根据近期技术面试经验,高频问题包括:
- EF Core延迟加载与立即加载的区别
- Dapper如何处理多表联查
- 工作单元模式的优势
- 乐观并发与悲观并发的实现
- 迁移冲突的解决流程
典型代码题:
// 以下代码有什么问题? public List<Product> GetExpensiveProducts() { return _context.Products .Where(p => p.Price > 100) .ToList(); }答案:应使用异步方法ToListAsync()避免阻塞线程,且不应在仓储层直接返回List
15. 项目实战心得
在最近一个电商项目中,我们经历了从EF Core到Dapper的架构演变。初期使用EF Core快速实现了80%的功能,但在促销活动期间遇到了性能瓶颈。通过以下优化获得了10倍性能提升:
- 将热点查询改为Dapper实现
- 使用存储过程处理复杂统计
- 引入二级缓存
- 优化批量导入改用BulkInsert
关键教训:没有放之四海而皆准的方案,应该根据场景灵活组合技术栈。对于常规管理后台,EF Core的快速开发优势无可替代;而对于高并发接口,Dapper+存储过程往往更可靠。