Skip to content

非 Web 应用中的 EF Core ​

概述 ​

EF Core 不仅适用于 Web 应用,还可以在 Console 应用、Worker Service、Desktop 应用(WPF/WinForms)、MAUI 移动应用等非 Web 场景中使用。这些环境下的 DbContext 生命周期管理和依赖注入配置有其特殊性。

应用场景对比 ​

场景特点生命周期策略
Console 应用短期运行,一次性任务手动管理或使用 DI
Worker Service长期运行,后台任务Scoped 或工厂模式
WPF/WinFormsGUI 应用,用户交互工厂模式 + 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 策略 ​

应用类型推荐策略原因
ConsoleAddDbContext + Scope短期运行,简单明了
WorkerAddDbContextFactory长期运行,避免内存泄漏
WPFAddDbContextFactory + DispatcherUI 线程安全
MAUIAddDbContextFactory + MainThread跨平台线程安全
测试AddDbContext(InMemory)隔离快速

核心要点 ​

  1. 理解生命周期: 根据应用类型选择合适的策略
  2. 及时释放: 使用 using 或 Scope 确保释放
  3. 线程安全: UI 应用注意线程切换
  4. 工厂模式: 长期运行的任务优先使用工厂
  5. 避免单例持有: 不要将 DbContext 注入到单例服务

基于 MIT 许可发布