概要
C#でWPFアプリケーションを作成している時に、
ViewModel(以下、VM)のフィールドにService(ロジッククラス)をDIし、
そのServiceのフィールドにもDbContextをDIしたら、
DI Containerで管理されているはずのDbContextが破棄されなくて困った話です。
自分が数年WebAPIの作成をしてからの初WPFだったので、よりハマった感じがします。
環境
WPF MVVMパターン
.Net Framework 4.8.1
EntityFramework 6.5.1
Prism.Core 8.1.97
Prism.DryIoc 8.1.97
Prism.WPF 8.1.97
ReactiveProperty 9.6.0
(先に) 結論
WPFでは、Viewを再利用する傾向にあり、Viewが破棄されず、
Viewに紐づくVMが破棄されません。
そのため、VMのフィールドに格納したServiceが破棄されず、
Serviceのフィールドに格納したDbContextが破棄されませんでした。
今回は以下の修正により解決しました。
- VMのフィールドにServiceを格納しない
- Serviceを使用する際にContainerのScopeを作成し、Scope内でServiceをDIで解決して取得する
作成したScopeが終わり次第、ServiceやServiceの中でDIしているDbContextが破棄される様になりました。
自分がWPF初心者なこともあり、もっと良い対応があったかもしれませんが、今回はこちらで対応を行いました。
もっと良い方法をご存じの場合は、コメントいただければ幸いです。
以下 詳細
どういう実装をしたか
DI Container にDbContextとServiceを登録
public partial class App : Prism.DryIoc.PrismApplication
{
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.Register<IHogeService, HogeService>();
containerRegistry.RegisterScoped<HogeDbContext>();
}
//・・・
}
DbContextは同じスコープ内だと使いまわししたいため、RegisterScoped、
Serviceは使用する側が使用される側の状況によらない方がいいはずなので、Register
で登録しました。
ServiceでDbContxtをDIして、フィールドに格納
public interface IHogeService
{
Task HogeAsync();
}
public class HogeService : IHogeService
{
private readonly HogeDbContext _context;
public HogeService(HogeDbContext context)
{
_context = context;
}
public async Task HogeAsync()
{
var entity = await _context.MHogeEntities.FirstOrDefaultAsync();
// 処理・・・
}
}
VMでServiceをDIして、フィールドに格納・Commandからサービスを呼び出す
public class HogeViewModel : ViewModelBase
{
private readonly IHogeService _hogeService;
public AsyncReactiveCommand HogeCommand { get; }
public HogeViewModel(IHogeService hogeService)
{
_hogeService = hogeService;
HogeCommand = new AsyncReactiveCommand()
.WithSubscribe(HogeCoreAsync)
.AddTo(Disposables);
}
private async Task HogeCoreAsync()
{
await _hogeService.HogeAsync();
}
}
ViewModelBaseの実装
ViewModelBaseはIDisposableを実装しています。
public class ViewModelBase : Prism.Mvvm.BindableBase, IDisposable
{
protected bool DisposedValue { get; set; } = false;
protected readonly CompositeDisposable Disposables = new CompositeDisposable();
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!DisposedValue)
{
if (disposing)
{
this.Disposables.Dispose();
}
DisposedValue = true;
}
}
}
挙動
DbContextのConstructorとDisposeにログを仕込んだところ、
VMのConstructorで、ServiceをDIしているので、DbContextのConstructorが呼ばれ、
ボタンクリック等によるコマンドを実行し、処理が終了しても、DbContextの破棄は行われませんでした。
検証
この時点で、VMがViewにずっと紐づいていて、ボタンクリック時に生成され、処理終了後に破棄されているわけではないことに思い当たりました。
(これまでWebAPIを作成してきて、VMがControllerの様なものとの認識だったので、ひっかかってしまったところになります。)
そこで、いくらかパターン検証したので、ざっくり概要と結果をまとめます。
- コマンド内でScopeを作成せず、ServiceをDIし、Serviceを使用する
挙動変わらず
Scopeを作成せずとも、コマンドが実行するメソッド内のScopeになるかと考えたのですが、ViewModel基準のScopeになるのか破棄されませんでした - ServiceはVMでDIし、Serviceのメソッド内で、DbContextを作成し、usingにより破棄する
usingのScopeが終了した時点で正しく破棄されました
ただし、Serviceから別のServiceを呼ぶ場合に同じDbContextの使いまわしが難しく、またusingを各Serviceで使用することになり、複数のServiceをまたぐ場合に、破棄がうまくいきませんでした
(再度) 結論
VMのフィールドにServiceを格納せず、VMからServiceを使用する場合は、ContainerのScopeを作成し、Scope内でServiceをDIで解決して取得することにより、Scopeが終わり次第、ServiceやServiceの中でDIしているDbContextが破棄される様になりました。
もっと良い対応があったかもしれませんが、今回はこちらで対応を行いました。
もっと良い方法をご存じの場合は、コメントいただければ幸いです。