Appearance
非 Web 应用中的 EF Core
概述
EF Core 不仅适用于 Web 应用,还可以在 Console 应用、Worker Service、Desktop 应用(WPF/WinForms)、MAUI 移动应用等非 Web 场景中使用。这些环境下的 DbContext 生命周期管理和依赖注入配置有其特殊性。
应用场景对比
| 场景 | 特点 | 生命周期策略 |
|---|---|---|
| Console 应用 | 短期运行,一次性任务 | 手动管理或使用 DI |
| Worker Service | 长期运行,后台任务 | Scoped 或工厂模式 |
| WPF/WinForms | GUI 应用,用户交互 | 工厂模式 + UI 线程安全 |
| MAUI | 移动/桌面跨平台 | 工厂模式 + 异步优先 |
| 单元测试 | 隔离测试 | InMemory 或 SQLite |
Console 应用
基础配置
csharp
// Program.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
// 添加 DbContext
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// 添加业务服务
builder.Services.AddScoped<IProductService, ProductService>();
var host = builder.Build();
// 使用服务
using (var scope = host.Services.CreateScope())
{
var productService = scope.ServiceProvider.GetRequiredService<IProductService>();
await productService.ProcessProductsAsync();
}
await host.RunAsync();命令行工具示例
csharp
// ProductCliTool.cs
public class ProductCliTool
{
private readonly IServiceProvider _serviceProvider;
public ProductCliTool(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task RunAsync(string[] args)
{
if (args.Length == 0)
{
Console.WriteLine("Usage: product-cli [command]");
Console.WriteLine("Commands: list, add, update, delete");
return;
}
var command = args[0].ToLower();
using var scope = _serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
switch (command)
{
case "list":
await ListProducts(context);
break;
case "add":
await AddProduct(context, args);
break;
case "update":
await UpdateProduct(context, args);
break;
case "delete":
await DeleteProduct(context, args);
break;
default:
Console.WriteLine($"Unknown command: {command}");
break;
}
}
private async Task ListProducts(AppDbContext context)
{
var products = await context.Products.ToListAsync();
foreach (var product in products)
{
Console.WriteLine($"{product.Id}: {product.Name} - ${product.Price}");
}
}
private async Task AddProduct(AppDbContext context, string[] args)
{
if (args.Length < 3)
{
Console.WriteLine("Usage: product-cli add <name> <price>");
return;
}
var product = new Product
{
Name = args[1],
Price = decimal.Parse(args[2])
};
context.Products.Add(product);
await context.SaveChangesAsync();
Console.WriteLine($"Product added: {product.Name}");
}
private async Task UpdateProduct(AppDbContext context, string[] args)
{
// 实现更新逻辑
}
private async Task DeleteProduct(AppDbContext context, string[] args)
{
// 实现删除逻辑
}
}
// Program.cs
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
var host = builder.Build();
var tool = ActivatorUtilities.CreateInstance<ProductCliTool>(host.Services);
await tool.RunAsync(args);Worker Service
基础配置
csharp
// Program.cs
using Microsoft.EntityFrameworkCore;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddHostedService<DataProcessingWorker>();
var host = builder.Build();
await host.RunAsync();后台工作器
csharp
// Workers/DataProcessingWorker.cs
public class DataProcessingWorker : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<DataProcessingWorker> _logger;
private readonly TimeSpan _interval = TimeSpan.FromMinutes(5);
public DataProcessingWorker(
IServiceProvider serviceProvider,
ILogger<DataProcessingWorker> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Data processing worker started");
while (!stoppingToken.IsCancellationRequested)
{
try
{
// ✅ 每个周期创建新的 Scope
using var scope = _serviceProvider.CreateScope();
var context = scope.ServiceProvider
.GetRequiredService<AppDbContext>();
await ProcessDataAsync(context, stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during data processing");
}
// 等待下一个周期
await Task.Delay(_interval, stoppingToken);
}
_logger.LogInformation("Data processing worker stopped");
}
private async Task ProcessDataAsync(AppDbContext context, CancellationToken ct)
{
_logger.LogInformation("Processing data...");
// 示例: 处理过期订单
var expiredOrders = await context.Orders
.Where(o => o.Status == OrderStatus.Pending &&
o.CreatedAt < DateTime.UtcNow.AddDays(-7))
.ToListAsync(ct);
foreach (var order in expiredOrders)
{
order.Status = OrderStatus.Cancelled;
_logger.LogInformation("Cancelled expired order {OrderId}", order.Id);
}
await context.SaveChangesAsync(ct);
_logger.LogInformation("Processed {Count} orders", expiredOrders.Count);
}
}多工作器协调
csharp
// Workers/OrderCleanupWorker.cs
public class OrderCleanupWorker : BackgroundService
{
private readonly IDbContextFactory<AppDbContext> _factory;
private readonly ILogger<OrderCleanupWorker> _logger;
public OrderCleanupWorker(
IDbContextFactory<AppDbContext> factory,
ILogger<OrderCleanupWorker> logger)
{
_factory = factory;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
// ✅ 使用工厂创建独立实例
using var context = _factory.CreateDbContext();
var deletedCount = await context.Orders
.Where(o => o.Status == OrderStatus.Cancelled &&
o.UpdatedAt < DateTime.UtcNow.AddYears(-2))
.ExecuteDeleteAsync(stoppingToken);
_logger.LogInformation("Cleaned up {Count} old orders", deletedCount);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during order cleanup");
}
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
}
}
}
// Program.cs - 注册多个工作器
builder.Services.AddDbContextFactory<AppDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddHostedService<DataProcessingWorker>();
builder.Services.AddHostedService<OrderCleanupWorker>();
builder.Services.AddHostedService<ReportGenerationWorker>();WPF 应用
配置依赖注入
csharp
// App.xaml.cs
public partial class App : Application
{
public static IServiceProvider ServiceProvider { get; private set; } = null!;
protected override void OnStartup(StartupEventArgs e)
{
var builder = Host.CreateApplicationBuilder();
// 配置 DbContext 工厂
builder.Services.AddDbContextFactory<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// 注册 ViewModels
builder.Services.AddTransient<MainViewModel>();
builder.Services.AddTransient<ProductViewModel>();
var host = builder.Build();
ServiceProvider = host.Services;
base.OnStartup(e);
}
}ViewModel 中使用
csharp
// ViewModels/ProductViewModel.cs
public class ProductViewModel : ObservableObject
{
private readonly IDbContextFactory<AppDbContext> _factory;
private ObservableCollection<Product> _products = new();
public ObservableCollection<Product> Products
{
get => _products;
set => SetProperty(ref _products, value);
}
public ProductViewModel(IDbContextFactory<AppDbContext> factory)
{
_factory = factory;
}
public async Task LoadProductsAsync()
{
try
{
// ✅ 使用工厂创建新实例
using var context = _factory.CreateDbContext();
var products = await context.Products
.OrderBy(p => p.Name)
.ToListAsync();
// ⚠️ 切换到 UI 线程更新集合
await Application.Current.Dispatcher.InvokeAsync(() =>
{
Products.Clear();
foreach (var product in products)
{
Products.Add(product);
}
});
}
catch (Exception ex)
{
MessageBox.Show($"Error loading products: {ex.Message}");
}
}
public async Task AddProductAsync(Product product)
{
using var context = _factory.CreateDbContext();
context.Products.Add(product);
await context.SaveChangesAsync();
await LoadProductsAsync(); // 重新加载
}
public async Task DeleteProductAsync(Product product)
{
using var context = _factory.CreateDbContext();
context.Products.Remove(product);
await context.SaveChangesAsync();
await LoadProductsAsync();
}
}线程安全注意事项
csharp
// ❌ 错误: 在后台线程直接更新 UI 绑定集合
public async Task LoadProductsAsync()
{
using var context = _factory.CreateDbContext();
var products = await context.Products.ToListAsync();
Products.Clear(); // ⚠️ 在非 UI 线程执行
foreach (var product in products)
{
Products.Add(product); // ⚠️ 可能导致异常
}
}
// ✅ 正确: 切换到 UI 线程
public async Task LoadProductsAsync()
{
using var context = _factory.CreateDbContext();
var products = await context.Products.ToListAsync();
await Application.Current.Dispatcher.InvokeAsync(() =>
{
Products.Clear();
foreach (var product in products)
{
Products.Add(product);
}
});
}MAUI 应用
配置
csharp
// MauiProgram.cs
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// 配置 SQLite 数据库
var dbPath = Path.Combine(
FileSystem.AppDataDirectory,
"app.db");
builder.Services.AddDbContextFactory<AppDbContext>(options =>
options.UseSqlite($"Data Source={dbPath}"));
// 注册 ViewModels
builder.Services.AddTransient<ProductsViewModel>();
builder.Services.AddTransient<ProductDetailViewModel>();
// 注册 Pages
builder.Services.AddTransient<ProductsPage>();
builder.Services.AddTransient<ProductDetailPage>();
return builder.Build();
}
}ViewModel 实现
csharp
// ViewModels/ProductsViewModel.cs
public class ProductsViewModel : ObservableObject, IQueryAttributable
{
private readonly IDbContextFactory<AppDbContext> _factory;
private ObservableCollection<Product> _products = new();
public ObservableCollection<Product> Products
{
get => _products;
set => SetProperty(ref _products, value);
}
public ICommand AddCommand { get; }
public ICommand RefreshCommand { get; }
public ProductsViewModel(IDbContextFactory<AppDbContext> factory)
{
_factory = factory;
AddCommand = new Command(async () => await AddProductAsync());
RefreshCommand = new Command(async () => await LoadProductsAsync());
}
public async void ApplyQueryAttributes(IDictionary<string, object> query)
{
await LoadProductsAsync();
}
private async Task LoadProductsAsync()
{
try
{
using var context = _factory.CreateDbContext();
var products = await context.Products
.OrderByDescending(p => p.CreatedAt)
.ToListAsync();
MainThread.BeginInvokeOnMainThread(() =>
{
Products.Clear();
foreach (var product in products)
{
Products.Add(product);
}
});
}
catch (Exception ex)
{
await Shell.Current.DisplayAlert("Error", ex.Message, "OK");
}
}
private async Task AddProductAsync()
{
await Shell.Current.GoToAsync(nameof(ProductDetailPage));
}
}页面导航
csharp
// Pages/ProductsPage.xaml.cs
public partial class ProductsPage : ContentPage
{
private readonly ProductsViewModel _viewModel;
public ProductsPage(ProductsViewModel viewModel)
{
InitializeComponent();
_viewModel = viewModel;
BindingContext = _viewModel;
}
protected override async void OnAppearing()
{
base.OnAppearing();
await _viewModel.LoadProductsAsync();
}
}
// Pages/ProductDetailPage.xaml.cs
public partial class ProductDetailPage : ContentPage
{
private readonly IDbContextFactory<AppDbContext> _factory;
public ProductDetailPage(IDbContextFactory<AppDbContext> factory)
{
InitializeComponent();
_factory = factory;
}
private async void OnSaveClicked(object sender, EventArgs e)
{
var product = BindingContext as Product;
if (product == null) return;
try
{
using var context = _factory.CreateDbContext();
if (product.Id == 0)
{
context.Products.Add(product);
}
else
{
context.Products.Update(product);
}
await context.SaveChangesAsync();
await DisplayAlert("Success", "Product saved", "OK");
await Navigation.PopAsync();
}
catch (Exception ex)
{
await DisplayAlert("Error", ex.Message, "OK");
}
}
}控制台应用的依赖注入
不使用 Host 的简化方案
csharp
// Program.cs - 简单 Console 应用
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
// 配置 DbContext
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer("Server=localhost;Database=MyApp;Trusted_Connection=True;"));
// 配置服务
services.AddScoped<IProductService, ProductService>();
var serviceProvider = services.BuildServiceProvider();
// 使用服务
using (var scope = serviceProvider.CreateScope())
{
var productService = scope.ServiceProvider.GetRequiredService<IProductService>();
await productService.ProcessAsync();
}
// 清理
await serviceProvider.DisposeAsync();Top-level Statements 示例
csharp
// Program.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
await using var services = new ServiceCollection()
.AddDbContext<AppDbContext>(options =>
options.UseSqlite("Data Source=app.db"))
.AddScoped<IMyService, MyService>()
.BuildServiceProvider();
await using var scope = services.CreateScope();
var myService = scope.ServiceProvider.GetRequiredService<IMyService>();
await myService.DoWorkAsync();最佳实践
✅ 推荐做法
1. 始终使用 Scope 管理生命周期
csharp
// ✅ 正确: 使用 Scope
using var scope = serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await UseContextAsync(context);
// Scope 结束时自动释放 DbContext
// ❌ 错误: 直接从根容器解析
var context = serviceProvider.GetRequiredService<AppDbContext>(); // ⚠️ 内存泄漏2. 长期运行的任务使用工厂
csharp
// ✅ Worker Service 推荐
public class MyWorker : BackgroundService
{
private readonly IDbContextFactory<AppDbContext> _factory;
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
using var context = _factory.CreateDbContext();
await DoWorkAsync(context);
await Task.Delay(TimeSpan.FromMinutes(5), ct);
}
}
}3. UI 应用注意线程安全
csharp
// ✅ WPF/MAUI: 切换到 UI 线程
await Dispatcher.InvokeAsync(() =>
{
// 更新 UI 绑定集合
});
// ✅ MAUI
MainThread.BeginInvokeOnMainThread(() =>
{
// 更新 UI
});❌ 常见陷阱
1. DbContext 生命周期过长
csharp
// ❌ 错误: 单例持有 DbContext
public class SingletonService
{
private readonly AppDbContext _context; // ⚠️ 长期持有
public SingletonService(AppDbContext context)
{
_context = context;
}
}
// ✅ 正确: 使用工厂
public class SingletonService
{
private readonly IDbContextFactory<AppDbContext> _factory;
public SingletonService(IDbContextFactory<AppDbContext> factory)
{
_factory = factory;
}
public async Task DoWorkAsync()
{
using var context = _factory.CreateDbContext();
// 使用完立即释放
}
}2. 忘记释放 Scope
csharp
// ❌ 错误
var scope = serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// 忘记调用 scope.Dispose()
// ✅ 正确: 使用 using
using var scope = serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// 自动释放总结
不同应用的 DbContext 策略
| 应用类型 | 推荐策略 | 原因 |
|---|---|---|
| Console | AddDbContext + Scope | 短期运行,简单明了 |
| Worker | AddDbContextFactory | 长期运行,避免内存泄漏 |
| WPF | AddDbContextFactory + Dispatcher | UI 线程安全 |
| MAUI | AddDbContextFactory + MainThread | 跨平台线程安全 |
| 测试 | AddDbContext(InMemory) | 隔离快速 |
核心要点
- 理解生命周期: 根据应用类型选择合适的策略
- 及时释放: 使用
using或 Scope 确保释放 - 线程安全: UI 应用注意线程切换
- 工厂模式: 长期运行的任务优先使用工厂
- 避免单例持有: 不要将 DbContext 注入到单例服务