WPF中Simple Injector结合EF的仓储注入及命令模式替代方案问询
Hey Danil, let's break down your problems and work through practical, straightforward solutions:
First: Resolve the DbContext Already Disposed Exception
The root cause of your error is that the ThreadScopedLifestyle scope you created in OnStartup gets disposed as soon as the wrapping using block finishes. But your MainWindow and its ViewModel have a much longer lifecycle—so when you try to use the repository later, the DbContext tied to that initial scope is already gone.
Correct Scope Management for WPF
WPF doesn't have built-in "request boundaries" like web apps, so we need to align the scope lifecycle with your database operations. Here are two solid approaches:
Option 1: Create a Scope Per Database Operation (Simple & Direct)
You don't need to pass the container to services—instead, wrap each database call in a scope where you invoke the service:
// In your ViewModel or UI event handler private void RunTestDatabaseOperation() { using (ThreadScopedLifestyle.BeginScope(App.appContainer)) { var service = App.appContainer.GetInstance<IMainAppSerivce>(); service.TestWorkWithDb(new int[] { 1, 2, 3 }); } }
This ensures every database interaction runs within a valid scope, so the DbContext won't be disposed prematurely.
Option 2: Switch to AsyncScopedLifestyle (Recommended for Modern WPF)
ThreadScopedLifestyle can break if you use background threads (like Task.Run), since it's tied strictly to the current thread. AsyncScopedLifestyle works better with async/await patterns, which are critical for keeping your WPF UI responsive:
private void ConfigureContainer() { appContainer = new Container(); // Replace ThreadScoped with AsyncScoped appContainer.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle(); appContainer.RegisterSingleton<INLogger, LoggerNlog>(); appContainer.Register<DbContext>(() => { string connectionString = System.Configuration.ConfigurationSettings.AppSettings["ConnectionString"].ToString(); return new ExcelSlicerContext(connectionString); }, Lifestyle.Scoped); appContainer.Register(typeof(IRepository<>), typeof(BaseRepository<>), Lifestyle.Scoped); appContainer.Register<IMainAppSerivce, SlicerMainService>(); appContainer.Register<MainViewModel>(); appContainer.Verify(); }
Then use it in async operations:
private async Task LoadDataAsync() { using (AsyncScopedLifestyle.BeginScope(App.appContainer)) { var service = App.appContainer.GetInstance<IMainAppSerivce>(); await Task.Run(() => service.TestWorkWithDb(new int[] { 1, 2, 3 })); } }
Second: Alternative to Command Pattern (Avoid Class Explosion)
It makes total sense that creating a command and handler for every tiny operation feels like overkill. A more straightforward approach for your scenario is to create entity-specific business services—these wrap all CRUD and business logic for a single entity in one cohesive class.
Step 1: Create an Entity-Specific Service Interface
For FactModel, define an IFactService that exposes all the operations you need:
public interface IFactService { IEnumerable<FactModel> GetFactsWithRowGreaterThan(int minRow); void AddSingleFact(FactModel fact); void AddBatchOfFacts(IEnumerable<FactModel> facts); // Add any other business-specific methods here }
Step 2: Implement the Service
Inject your repository and logger into the service, then encapsulate the logic:
public class FactService : IFactService { private readonly IRepository<FactModel> _factRepository; private readonly INLogger _logger; public FactService(IRepository<FactModel> factRepository, INLogger logger) { _factRepository = factRepository; _logger = logger; } public IEnumerable<FactModel> GetFactsWithRowGreaterThan(int minRow) { try { return _factRepository.Get(x => x.Row > minRow).ToList(); } catch (Exception ex) { _logger.Error(ex, "Failed to retrieve fact data"); throw; // Re-throw so the caller can handle it gracefully } } public void AddSingleFact(FactModel fact) { _factRepository.Create(fact); } public void AddBatchOfFacts(IEnumerable<FactModel> facts) { _factRepository.CreateFromRange(facts); } }
Step 3: Register the Service in Simple Injector
Add this line to your ConfigureContainer method:
appContainer.Register<IFactService, FactService>(Lifestyle.Scoped);
Step 4: Use the Service in Your ViewModel/Other Services
Inject the service directly into your ViewModel (no container references needed):
public class MainViewModel { private readonly IFactService _factService; public MainViewModel(IFactService factService) { _factService = factService; } private void LoadFactData() { using (AsyncScopedLifestyle.BeginScope(App.appContainer)) { var facts = _factService.GetFactsWithRowGreaterThan(6); // Bind facts to your UI controls here } } }
Why This Works Better for Your Case
- No more class explosion: All logic for
FactModellives in one place instead of dozens of command/handler pairs - Clear separation of concerns: Data access stays in the repository, business logic stays in the service
- Maintains DI benefits: You still get all the dependency injection perks from Simple Injector, with proper scope management
Bonus Optimizations
- Stop calling
SaveChangesin the repository: YourBaseRepositorycallsSaveChangesin methods likeCreate—this forces a transaction per operation. Instead, let the service layer control when to save, or implement a Unit of Work pattern to batch changes. - Go async with EF: EF Core (and EF6) supports async operations. Update your repository and service methods to use
GetAsync,AddAsync, etc., to keep your WPF UI responsive. - Avoid container calls in ViewModels: Use a ViewModel Locator pattern to inject services into ViewModels automatically, so you don't have to call
GetInstancedirectly.
内容的提问来源于stack exchange,提问作者DanilGholtsman

