feat(worker,ui): wire EF Core into DI and update all consumers to IDbContextFactory

Worker and App Program.cs: replace SqliteConnectionFactory+SchemaInitializer
with AddDbContextFactory<ClaudeDoDbContext> + Database.Migrate(). Repos
changed from AddSingleton to AddScoped.

All singleton services (QueueService, StaleTaskRecovery, WorktreeManager,
TaskRunner) and singleton ViewModels (MainWindowViewModel, TaskDetailViewModel,
TaskListViewModel, TaskEditorViewModel) now take IDbContextFactory<ClaudeDoDbContext>
and create short-lived contexts per operation.

Test infrastructure: DbFixture now uses EF migrations instead of SchemaInitializer;
all test classes create contexts via DbFixture.CreateContext().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-04-16 08:59:24 +02:00
parent b7be52a623
commit 36484ed45a
18 changed files with 479 additions and 232 deletions

View File

@@ -2,21 +2,21 @@ using System.Collections.ObjectModel;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.EntityFrameworkCore;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Ui.ViewModels;
public partial class TaskListViewModel : ViewModelBase
{
private readonly TaskRepository _taskRepo;
private readonly TagRepository _tagRepo;
private readonly ListRepository _listRepo;
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
private readonly WorkerClient _worker;
private readonly Func<TaskEditorViewModel> _editorFactory;
private readonly Action<string> _showMessage;
@@ -33,13 +33,10 @@ public partial class TaskListViewModel : ViewModelBase
partial void OnSelectedTaskChanged(TaskItemViewModel? value) =>
SelectedTaskChanged?.Invoke(value);
public TaskListViewModel(TaskRepository taskRepo, TagRepository tagRepo,
ListRepository listRepo, WorkerClient worker,
public TaskListViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, WorkerClient worker,
Func<TaskEditorViewModel> editorFactory, Action<string> showMessage)
{
_taskRepo = taskRepo;
_tagRepo = tagRepo;
_listRepo = listRepo;
_dbFactory = dbFactory;
_worker = worker;
_editorFactory = editorFactory;
_showMessage = showMessage;
@@ -77,7 +74,9 @@ public partial class TaskListViewModel : ViewModelBase
if (listId is not null)
{
var list = await _listRepo.GetByIdAsync(listId);
using var context = _dbFactory.CreateDbContext();
var listRepo = new ListRepository(context);
var list = await listRepo.GetByIdAsync(listId);
ListName = list?.Name ?? "Tasks";
}
else
@@ -89,10 +88,12 @@ public partial class TaskListViewModel : ViewModelBase
try
{
var entities = await _taskRepo.GetByListAsync(listId);
using var context = _dbFactory.CreateDbContext();
var taskRepo = new TaskRepository(context);
var entities = await taskRepo.GetByListIdAsync(listId);
foreach (var e in entities)
{
var tags = await _taskRepo.GetEffectiveTagsAsync(e.Id);
var tags = await taskRepo.GetEffectiveTagsAsync(e.Id);
Tasks.Add(new TaskItemViewModel(e, tags, RunNowAsync, () => _worker.IsConnected, ToggleDoneAsync));
}
}
@@ -110,8 +111,13 @@ public partial class TaskListViewModel : ViewModelBase
var title = InlineAddTitle.Trim();
if (string.IsNullOrEmpty(title) || CurrentListId is null) return;
var list = await _listRepo.GetByIdAsync(CurrentListId);
var defaultCommitType = list?.DefaultCommitType ?? "chore";
string defaultCommitType;
using (var context = _dbFactory.CreateDbContext())
{
var listRepo = new ListRepository(context);
var list = await listRepo.GetByIdAsync(CurrentListId);
defaultCommitType = list?.DefaultCommitType ?? "chore";
}
var entity = new TaskEntity
{
@@ -125,8 +131,10 @@ public partial class TaskListViewModel : ViewModelBase
try
{
await _taskRepo.AddAsync(entity);
var tags = await _taskRepo.GetEffectiveTagsAsync(entity.Id);
using var context = _dbFactory.CreateDbContext();
var taskRepo = new TaskRepository(context);
await taskRepo.AddAsync(entity);
var tags = await taskRepo.GetEffectiveTagsAsync(entity.Id);
var vm = new TaskItemViewModel(entity, tags, RunNowAsync, () => _worker.IsConnected, ToggleDoneAsync);
Tasks.Add(vm);
SelectedTask = vm;
@@ -141,9 +149,13 @@ public partial class TaskListViewModel : ViewModelBase
[RelayCommand(CanExecute = nameof(CanAddTask))]
private async Task AddTask()
{
// Get list default commit type
var list = await _listRepo.GetByIdAsync(CurrentListId);
var defaultCommitType = list?.DefaultCommitType ?? "chore";
string defaultCommitType;
using (var context = _dbFactory.CreateDbContext())
{
var listRepo = new ListRepository(context);
var list = await listRepo.GetByIdAsync(CurrentListId);
defaultCommitType = list?.DefaultCommitType ?? "chore";
}
var editor = _editorFactory();
await editor.LoadAgentsAsync(_worker);
@@ -159,15 +171,18 @@ public partial class TaskListViewModel : ViewModelBase
try
{
await _taskRepo.AddAsync(saved);
using var context = _dbFactory.CreateDbContext();
var taskRepo = new TaskRepository(context);
var tagRepo = new TagRepository(context);
await taskRepo.AddAsync(saved);
foreach (var tagName in editor.SelectedTagNames)
{
var tagId = await _tagRepo.GetOrCreateAsync(tagName);
await _taskRepo.AddTagAsync(saved.Id, tagId);
var tagId = await tagRepo.GetOrCreateAsync(tagName);
await taskRepo.AddTagAsync(saved.Id, tagId);
}
var tags = await _taskRepo.GetEffectiveTagsAsync(saved.Id);
var tags = await taskRepo.GetEffectiveTagsAsync(saved.Id);
Tasks.Add(new TaskItemViewModel(saved, tags, RunNowAsync, () => _worker.IsConnected, ToggleDoneAsync));
// Auto wake-queue if agent+queued
@@ -188,10 +203,17 @@ public partial class TaskListViewModel : ViewModelBase
private async Task EditTask()
{
if (SelectedTask is null || CurrentListId is null) return;
var entity = await _taskRepo.GetByIdAsync(SelectedTask.Id);
if (entity is null) return;
var taskTags = await _taskRepo.GetTagsAsync(entity.Id);
TaskEntity? entity;
List<TagEntity> taskTags;
using (var context = _dbFactory.CreateDbContext())
{
var taskRepo = new TaskRepository(context);
entity = await taskRepo.GetByIdAsync(SelectedTask.Id);
if (entity is null) return;
taskTags = await taskRepo.GetTagsAsync(entity.Id);
}
var editor = _editorFactory();
await editor.LoadAgentsAsync(_worker);
await editor.InitForEditAsync(entity, taskTags);
@@ -206,18 +228,21 @@ public partial class TaskListViewModel : ViewModelBase
try
{
await _taskRepo.UpdateAsync(saved);
using var context = _dbFactory.CreateDbContext();
var taskRepo = new TaskRepository(context);
var tagRepo = new TagRepository(context);
await taskRepo.UpdateAsync(saved);
var existingTags = await _taskRepo.GetTagsAsync(saved.Id);
var existingTags = await taskRepo.GetTagsAsync(saved.Id);
foreach (var old in existingTags)
await _taskRepo.RemoveTagAsync(saved.Id, old.Id);
await taskRepo.RemoveTagAsync(saved.Id, old.Id);
foreach (var tagName in editor.SelectedTagNames)
{
var tagId = await _tagRepo.GetOrCreateAsync(tagName);
await _taskRepo.AddTagAsync(saved.Id, tagId);
var tagId = await tagRepo.GetOrCreateAsync(tagName);
await taskRepo.AddTagAsync(saved.Id, tagId);
}
var newTags = await _taskRepo.GetEffectiveTagsAsync(saved.Id);
var newTags = await taskRepo.GetEffectiveTagsAsync(saved.Id);
SelectedTask.Refresh(saved, newTags);
}
catch (Exception ex)
@@ -232,7 +257,9 @@ public partial class TaskListViewModel : ViewModelBase
if (SelectedTask is null) return;
try
{
await _taskRepo.DeleteAsync(SelectedTask.Id);
using var context = _dbFactory.CreateDbContext();
var taskRepo = new TaskRepository(context);
await taskRepo.DeleteAsync(SelectedTask.Id);
Tasks.Remove(SelectedTask);
SelectedTask = null;
}
@@ -244,14 +271,16 @@ public partial class TaskListViewModel : ViewModelBase
public async Task RefreshSingleAsync(string taskId)
{
var entity = await _taskRepo.GetByIdAsync(taskId);
using var context = _dbFactory.CreateDbContext();
var taskRepo = new TaskRepository(context);
var entity = await taskRepo.GetByIdAsync(taskId);
var existing = Tasks.FirstOrDefault(t => t.Id == taskId);
if (entity is null)
{
if (existing is not null) Tasks.Remove(existing);
return;
}
var tags = await _taskRepo.GetEffectiveTagsAsync(taskId);
var tags = await taskRepo.GetEffectiveTagsAsync(taskId);
if (existing is not null)
existing.Refresh(entity, tags);
}
@@ -270,14 +299,16 @@ public partial class TaskListViewModel : ViewModelBase
private async Task ToggleDoneAsync(string taskId)
{
var entity = await _taskRepo.GetByIdAsync(taskId);
using var context = _dbFactory.CreateDbContext();
var taskRepo = new TaskRepository(context);
var entity = await taskRepo.GetByIdAsync(taskId);
if (entity is null) return;
entity.Status = entity.Status == TaskStatus.Done ? TaskStatus.Manual : TaskStatus.Done;
if (entity.Status == TaskStatus.Done)
entity.FinishedAt = DateTime.UtcNow;
await _taskRepo.UpdateAsync(entity);
await taskRepo.UpdateAsync(entity);
await RefreshSingleAsync(taskId);
}