У попередній статті ми навчилися працювати з SQLite через DbContext безпосередньо у ViewModel. Це працює для простих додатків, але швидко стає проблемою:
Рішення — Repository Pattern. Репозиторій — це абстракція над доступом до даних. ViewModel працює з IUserRepository, не знаючи, що всередині — DbContext, API, або файли. Це дає тестованість (mock репозиторій), гнучкість (заміна реалізації), та чистоту коду.
Unit of Work координує роботу кількох репозиторіїв у одній транзакції. Замість SaveChangesAsync() після кожної операції, ви викликаєте UnitOfWork.CommitAsync() один раз для всіх змін.
У цій статті ми побудуємо повноцінну архітектуру: IRepository<T> → GenericRepository<T> → IUserRepository → UserRepository, та IUnitOfWork для координації. Також розглянемо User Settings через JSON для зберігання налаштувань додатку.
// ❌ Погана практика
public class UserListViewModel : ViewModelBase
{
private readonly AppDbContext _context;
public UserListViewModel(AppDbContext context)
{
_context = context;
}
private async Task LoadUsersAsync()
{
// ViewModel знає про EF Core, LINQ, Include
var users = await _context.Users
.Include(u => u.Tasks)
.Where(u => u.IsActive)
.OrderBy(u => u.Name)
.ToListAsync();
Users = new ObservableCollection<User>(users);
}
}
Проблеми:
// ✅ Хороша практика
public class UserListViewModel : ViewModelBase
{
private readonly IUserRepository _userRepository;
public UserListViewModel(IUserRepository userRepository)
{
_userRepository = userRepository;
}
private async Task LoadUsersAsync()
{
// ViewModel не знає про DbContext, EF Core, LINQ
var users = await _userRepository.GetActiveUsersAsync();
Users = new ObservableCollection<User>(users);
}
}
Переваги:
GetActiveUsersAsync() в одному місціIRepository<T> // Базовий інтерфейс з CRUD
↓
GenericRepository<T> // Базова реалізація через DbContext
↓
IUserRepository // Специфічний інтерфейс для User
↓
UserRepository // Реалізація з додатковими методами
public interface IRepository<T> where T : class
{
// Читання
Task<T?> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate);
// Запис
Task AddAsync(T entity);
Task AddRangeAsync(IEnumerable<T> entities);
// Оновлення
void Update(T entity);
void UpdateRange(IEnumerable<T> entities);
// Видалення
void Remove(T entity);
void RemoveRange(IEnumerable<T> entities);
// Підрахунок
Task<int> CountAsync();
Task<int> CountAsync(Expression<Func<T, bool>> predicate);
// Перевірка існування
Task<bool> AnyAsync(Expression<Func<T, bool>> predicate);
}
Чому деякі методи не async?
Update(), Remove() — лише маркують сутність як змінену/видаленуSaveChangesAsync() (через Unit of Work)public class GenericRepository<T> : IRepository<T> where T : class
{
protected readonly AppDbContext _context;
protected readonly DbSet<T> _dbSet;
public GenericRepository(AppDbContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public virtual async Task<T?> GetByIdAsync(int id)
{
return await _dbSet.FindAsync(id);
}
public virtual async Task<IEnumerable<T>> GetAllAsync()
{
return await _dbSet.ToListAsync();
}
public virtual async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate)
{
return await _dbSet.Where(predicate).ToListAsync();
}
public virtual async Task AddAsync(T entity)
{
await _dbSet.AddAsync(entity);
}
public virtual async Task AddRangeAsync(IEnumerable<T> entities)
{
await _dbSet.AddRangeAsync(entities);
}
public virtual void Update(T entity)
{
_dbSet.Update(entity);
}
public virtual void UpdateRange(IEnumerable<T> entities)
{
_dbSet.UpdateRange(entities);
}
public virtual void Remove(T entity)
{
_dbSet.Remove(entity);
}
public virtual void RemoveRange(IEnumerable<T> entities)
{
_dbSet.RemoveRange(entities);
}
public virtual async Task<int> CountAsync()
{
return await _dbSet.CountAsync();
}
public virtual async Task<int> CountAsync(Expression<Func<T, bool>> predicate)
{
return await _dbSet.CountAsync(predicate);
}
public virtual async Task<bool> AnyAsync(Expression<Func<T, bool>> predicate)
{
return await _dbSet.AnyAsync(predicate);
}
}
Чому virtual?
GetByIdAsync() може включати пов'язані дані через Include()public interface IUserRepository : IRepository<User>
{
// Додаткові методи, специфічні для User
Task<User?> GetByEmailAsync(string email);
Task<IEnumerable<User>> GetActiveUsersAsync();
Task<IEnumerable<User>> GetUsersWithTasksAsync();
Task<User?> GetUserWithTasksAsync(int userId);
Task<bool> EmailExistsAsync(string email);
Task<IEnumerable<User>> SearchUsersAsync(string searchTerm);
Task<IEnumerable<User>> GetUsersPageAsync(int pageNumber, int pageSize);
}
Чому окремий інтерфейс?
IRepository<T>public class UserRepository : GenericRepository<User>, IUserRepository
{
public UserRepository(AppDbContext context) : base(context)
{
}
// Перевизначення базового методу з Include
public override async Task<User?> GetByIdAsync(int id)
{
return await _dbSet
.Include(u => u.Tasks)
.FirstOrDefaultAsync(u => u.Id == id);
}
public async Task<User?> GetByEmailAsync(string email)
{
return await _dbSet
.FirstOrDefaultAsync(u => u.Email == email);
}
public async Task<IEnumerable<User>> GetActiveUsersAsync()
{
return await _dbSet
.Where(u => u.IsActive)
.OrderBy(u => u.Name)
.ToListAsync();
}
public async Task<IEnumerable<User>> GetUsersWithTasksAsync()
{
return await _dbSet
.Include(u => u.Tasks)
.OrderBy(u => u.Name)
.ToListAsync();
}
public async Task<User?> GetUserWithTasksAsync(int userId)
{
return await _dbSet
.Include(u => u.Tasks)
.FirstOrDefaultAsync(u => u.Id == userId);
}
public async Task<bool> EmailExistsAsync(string email)
{
return await _dbSet.AnyAsync(u => u.Email == email);
}
public async Task<IEnumerable<User>> SearchUsersAsync(string searchTerm)
{
return await _dbSet
.Where(u => u.Name.Contains(searchTerm) || u.Email.Contains(searchTerm))
.OrderBy(u => u.Name)
.ToListAsync();
}
public async Task<IEnumerable<User>> GetUsersPageAsync(int pageNumber, int pageSize)
{
return await _dbSet
.OrderBy(u => u.Name)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
}
}
public interface ITaskRepository : IRepository<TaskItem>
{
Task<IEnumerable<TaskItem>> GetTasksByUserIdAsync(int userId);
Task<IEnumerable<TaskItem>> GetCompletedTasksAsync();
Task<IEnumerable<TaskItem>> GetPendingTasksAsync();
Task<IEnumerable<TaskItem>> GetOverdueTasksAsync();
Task<int> GetCompletedTasksCountAsync(int userId);
}
public class TaskRepository : GenericRepository<TaskItem>, ITaskRepository
{
public TaskRepository(AppDbContext context) : base(context)
{
}
public async Task<IEnumerable<TaskItem>> GetTasksByUserIdAsync(int userId)
{
return await _dbSet
.Where(t => t.UserId == userId)
.OrderBy(t => t.DueDate)
.ToListAsync();
}
public async Task<IEnumerable<TaskItem>> GetCompletedTasksAsync()
{
return await _dbSet
.Where(t => t.IsCompleted)
.OrderByDescending(t => t.CompletedAt)
.ToListAsync();
}
public async Task<IEnumerable<TaskItem>> GetPendingTasksAsync()
{
return await _dbSet
.Where(t => !t.IsCompleted)
.OrderBy(t => t.DueDate)
.ToListAsync();
}
public async Task<IEnumerable<TaskItem>> GetOverdueTasksAsync()
{
var now = DateTime.UtcNow;
return await _dbSet
.Where(t => !t.IsCompleted && t.DueDate < now)
.OrderBy(t => t.DueDate)
.ToListAsync();
}
public async Task<int> GetCompletedTasksCountAsync(int userId)
{
return await _dbSet
.CountAsync(t => t.UserId == userId && t.IsCompleted);
}
}
Unit of Work координує роботу кількох репозиторіїв у одній транзакції.
public interface IUnitOfWork : IDisposable
{
// Репозиторії
IUserRepository Users { get; }
ITaskRepository Tasks { get; }
// Збереження змін
Task<int> CommitAsync();
Task<int> CommitAsync(CancellationToken cancellationToken);
// Транзакції
Task BeginTransactionAsync();
Task CommitTransactionAsync();
Task RollbackTransactionAsync();
}
public class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext _context;
private IDbContextTransaction? _transaction;
// Lazy initialization для репозиторіїв
private IUserRepository? _userRepository;
private ITaskRepository? _taskRepository;
public UnitOfWork(AppDbContext context)
{
_context = context;
}
public IUserRepository Users
{
get
{
_userRepository ??= new UserRepository(_context);
return _userRepository;
}
}
public ITaskRepository Tasks
{
get
{
_taskRepository ??= new TaskRepository(_context);
return _taskRepository;
}
}
public async Task<int> CommitAsync()
{
return await _context.SaveChangesAsync();
}
public async Task<int> CommitAsync(CancellationToken cancellationToken)
{
return await _context.SaveChangesAsync(cancellationToken);
}
public async Task BeginTransactionAsync()
{
_transaction = await _context.Database.BeginTransactionAsync();
}
public async Task CommitTransactionAsync()
{
if (_transaction == null)
throw new InvalidOperationException("Transaction not started");
try
{
await _context.SaveChangesAsync();
await _transaction.CommitAsync();
}
catch
{
await RollbackTransactionAsync();
throw;
}
finally
{
await _transaction.DisposeAsync();
_transaction = null;
}
}
public async Task RollbackTransactionAsync()
{
if (_transaction != null)
{
await _transaction.RollbackAsync();
await _transaction.DisposeAsync();
_transaction = null;
}
}
public void Dispose()
{
_transaction?.Dispose();
_context.Dispose();
}
}
private void ConfigureServices(IServiceCollection services)
{
// DbContext
services.AddDbContext<AppDbContext>(options =>
{
var dbPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"MyApp",
"app.db"
);
options.UseSqlite($"Data Source={dbPath}");
});
// Unit of Work (Scoped)
services.AddScoped<IUnitOfWork, UnitOfWork>();
// Або окремі репозиторії (якщо не використовуєте UoW)
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<ITaskRepository, TaskRepository>();
// ViewModels
services.AddTransient<MainViewModel>();
}
public class UserListViewModel : ViewModelBase
{
private readonly IUnitOfWork _unitOfWork;
private ObservableCollection<User> _users = new();
public UserListViewModel(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
LoadUsersCommand = new AsyncRelayCommand(LoadUsersAsync);
AddUserCommand = new AsyncRelayCommand(AddUserAsync);
UpdateUserCommand = new AsyncRelayCommand<User>(UpdateUserAsync);
DeleteUserCommand = new AsyncRelayCommand<User>(DeleteUserAsync);
}
public ObservableCollection<User> Users
{
get => _users;
set => SetProperty(ref _users, value);
}
public IAsyncRelayCommand LoadUsersCommand { get; }
public IAsyncRelayCommand AddUserCommand { get; }
public IAsyncRelayCommand<User> UpdateUserCommand { get; }
public IAsyncRelayCommand<User> DeleteUserCommand { get; }
private async Task LoadUsersAsync()
{
var users = await _unitOfWork.Users.GetActiveUsersAsync();
Users = new ObservableCollection<User>(users);
}
private async Task AddUserAsync()
{
var newUser = new User
{
Name = "New User",
Email = $"user{DateTime.Now.Ticks}@example.com",
IsActive = true,
CreatedAt = DateTime.UtcNow
};
await _unitOfWork.Users.AddAsync(newUser);
await _unitOfWork.CommitAsync();
Users.Add(newUser);
}
private async Task UpdateUserAsync(User? user)
{
if (user == null) return;
user.UpdatedAt = DateTime.UtcNow;
_unitOfWork.Users.Update(user);
await _unitOfWork.CommitAsync();
}
private async Task DeleteUserAsync(User? user)
{
if (user == null) return;
_unitOfWork.Users.Remove(user);
await _unitOfWork.CommitAsync();
Users.Remove(user);
}
}
public class TransferTasksViewModel : ViewModelBase
{
private readonly IUnitOfWork _unitOfWork;
public TransferTasksViewModel(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
TransferCommand = new AsyncRelayCommand(TransferTasksAsync);
}
public int FromUserId { get; set; }
public int ToUserId { get; set; }
public IAsyncRelayCommand TransferCommand { get; }
private async Task TransferTasksAsync()
{
try
{
// Початок транзакції
await _unitOfWork.BeginTransactionAsync();
// Отримуємо задачі користувача
var tasks = await _unitOfWork.Tasks.GetTasksByUserIdAsync(FromUserId);
// Переносимо на іншого користувача
foreach (var task in tasks)
{
task.UserId = ToUserId;
_unitOfWork.Tasks.Update(task);
}
// Commit транзакції
await _unitOfWork.CommitTransactionAsync();
await ShowSuccessAsync($"Transferred {tasks.Count()} tasks");
}
catch (Exception ex)
{
await _unitOfWork.RollbackTransactionAsync();
await ShowErrorAsync($"Transfer failed: {ex.Message}");
}
}
}
public class UserDetailsViewModel : ViewModelBase
{
private readonly IUnitOfWork _unitOfWork;
private User? _selectedUser;
private ObservableCollection<TaskItem> _userTasks = new();
public UserDetailsViewModel(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
LoadUserCommand = new AsyncRelayCommand<int>(LoadUserAsync);
CompleteAllTasksCommand = new AsyncRelayCommand(CompleteAllTasksAsync);
}
public User? SelectedUser
{
get => _selectedUser;
set => SetProperty(ref _selectedUser, value);
}
public ObservableCollection<TaskItem> UserTasks
{
get => _userTasks;
set => SetProperty(ref _userTasks, value);
}
public IAsyncRelayCommand<int> LoadUserCommand { get; }
public IAsyncRelayCommand CompleteAllTasksCommand { get; }
private async Task LoadUserAsync(int userId)
{
// Завантажуємо користувача
SelectedUser = await _unitOfWork.Users.GetByIdAsync(userId);
if (SelectedUser == null) return;
// Завантажуємо його задачі
var tasks = await _unitOfWork.Tasks.GetTasksByUserIdAsync(userId);
UserTasks = new ObservableCollection<TaskItem>(tasks);
}
private async Task CompleteAllTasksAsync()
{
if (SelectedUser == null) return;
try
{
await _unitOfWork.BeginTransactionAsync();
// Позначаємо всі задачі як виконані
foreach (var task in UserTasks.Where(t => !t.IsCompleted))
{
task.IsCompleted = true;
task.CompletedAt = DateTime.UtcNow;
_unitOfWork.Tasks.Update(task);
}
// Оновлюємо статистику користувача
SelectedUser.CompletedTasksCount = await _unitOfWork.Tasks
.GetCompletedTasksCountAsync(SelectedUser.Id);
_unitOfWork.Users.Update(SelectedUser);
await _unitOfWork.CommitTransactionAsync();
// Оновлюємо UI
OnPropertyChanged(nameof(UserTasks));
}
catch (Exception ex)
{
await _unitOfWork.RollbackTransactionAsync();
await ShowErrorAsync($"Failed to complete tasks: {ex.Message}");
}
}
}
❌ Погано:
// Занадто загальний метод
Task<IEnumerable<T>> GetAsync(
Expression<Func<T, bool>>? filter = null,
Func<IQueryable<T>, IOrderedQueryable<T>>? orderBy = null,
string includeProperties = "",
int? skip = null,
int? take = null
);
✅ Добре:
// Специфічні методи з чіткою метою
Task<IEnumerable<User>> GetActiveUsersAsync();
Task<IEnumerable<User>> GetUsersPageAsync(int pageNumber, int pageSize);
Task<User?> GetUserWithTasksAsync(int userId);
❌ Погано:
public IQueryable<User> GetUsers()
{
return _dbSet.AsQueryable(); // Витік абстракції
}
// ViewModel може робити що завгодно
var users = repository.GetUsers()
.Include(u => u.Tasks)
.Where(u => u.IsActive)
.ToList(); // ViewModel знає про EF Core!
✅ Добре:
public async Task<IEnumerable<User>> GetActiveUsersAsync()
{
return await _dbSet
.Where(u => u.IsActive)
.ToListAsync(); // Повертаємо матеріалізовану колекцію
}
public async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate)
{
return await _dbSet.Where(predicate).ToListAsync();
}
// Використання
var activeUsers = await repository.FindAsync(u => u.IsActive);
var usersByEmail = await repository.FindAsync(u => u.Email.Contains("@example.com"));
public class UnitOfWork : IUnitOfWork
{
private IUserRepository? _userRepository;
public IUserRepository Users
{
get
{
// Створюємо лише при першому доступі
_userRepository ??= new UserRepository(_context);
return _userRepository;
}
}
}
❌ Погано:
public async Task<User> CreateUserAsync(string name, string email)
{
// Бізнес-логіка у репозиторії
if (string.IsNullOrEmpty(name))
throw new ArgumentException("Name is required");
if (await EmailExistsAsync(email))
throw new InvalidOperationException("Email already exists");
var user = new User { Name = name, Email = email };
await AddAsync(user);
await _context.SaveChangesAsync();
return user;
}
✅ Добре:
// Репозиторій — лише доступ до даних
public async Task<bool> EmailExistsAsync(string email)
{
return await _dbSet.AnyAsync(u => u.Email == email);
}
// Бізнес-логіка у Service або ViewModel
public class UserService
{
private readonly IUnitOfWork _unitOfWork;
public async Task<User> CreateUserAsync(string name, string email)
{
if (string.IsNullOrEmpty(name))
throw new ArgumentException("Name is required");
if (await _unitOfWork.Users.EmailExistsAsync(email))
throw new InvalidOperationException("Email already exists");
var user = new User { Name = name, Email = email };
await _unitOfWork.Users.AddAsync(user);
await _unitOfWork.CommitAsync();
return user;
}
}
public interface ISpecification<T>
{
Expression<Func<T, bool>> Criteria { get; }
List<Expression<Func<T, object>>> Includes { get; }
Expression<Func<T, object>>? OrderBy { get; }
Expression<Func<T, object>>? OrderByDescending { get; }
}
public class ActiveUsersSpecification : ISpecification<User>
{
public Expression<Func<User, bool>> Criteria => u => u.IsActive;
public List<Expression<Func<User, object>>> Includes { get; } = new();
public Expression<Func<User, object>>? OrderBy => u => u.Name;
public Expression<Func<User, object>>? OrderByDescending => null;
}
// Використання
public async Task<IEnumerable<T>> FindAsync(ISpecification<T> specification)
{
var query = _dbSet.AsQueryable();
if (specification.Criteria != null)
query = query.Where(specification.Criteria);
query = specification.Includes
.Aggregate(query, (current, include) => current.Include(include));
if (specification.OrderBy != null)
query = query.OrderBy(specification.OrderBy);
if (specification.OrderByDescending != null)
query = query.OrderByDescending(specification.OrderByDescending);
return await query.ToListAsync();
}
public class AppSettings
{
public string Theme { get; set; } = "Light";
public string Language { get; set; } = "en";
public bool AutoSave { get; set; } = true;
public int AutoSaveInterval { get; set; } = 300; // секунди
public WindowSettings Window { get; set; } = new();
}
public class WindowSettings
{
public double Width { get; set; } = 1024;
public double Height { get; set; } = 768;
public double Left { get; set; } = 100;
public double Top { get; set; } = 100;
public bool IsMaximized { get; set; } = false;
}
public interface ISettingsService
{
Task<AppSettings> LoadAsync();
Task SaveAsync(AppSettings settings);
AppSettings Current { get; }
}
public class SettingsService : ISettingsService
{
private readonly string _settingsPath;
private AppSettings _current;
public SettingsService()
{
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var appFolder = Path.Combine(appDataPath, "MyApp");
Directory.CreateDirectory(appFolder);
_settingsPath = Path.Combine(appFolder, "settings.json");
_current = new AppSettings();
}
public AppSettings Current => _current;
public async Task<AppSettings> LoadAsync()
{
try
{
if (File.Exists(_settingsPath))
{
var json = await File.ReadAllTextAsync(_settingsPath);
_current = JsonSerializer.Deserialize<AppSettings>(json) ?? new AppSettings();
}
else
{
_current = new AppSettings();
await SaveAsync(_current);
}
}
catch (Exception ex)
{
// Логування помилки
Console.WriteLine($"Failed to load settings: {ex.Message}");
_current = new AppSettings();
}
return _current;
}
public async Task SaveAsync(AppSettings settings)
{
try
{
_current = settings;
var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var json = JsonSerializer.Serialize(settings, options);
await File.WriteAllTextAsync(_settingsPath, json);
}
catch (Exception ex)
{
Console.WriteLine($"Failed to save settings: {ex.Message}");
throw;
}
}
}
private void ConfigureServices(IServiceCollection services)
{
// Settings як Singleton
services.AddSingleton<ISettingsService, SettingsService>();
// ViewModels
services.AddTransient<SettingsViewModel>();
}
public class SettingsViewModel : ViewModelBase
{
private readonly ISettingsService _settingsService;
private AppSettings _settings;
public SettingsViewModel(ISettingsService settingsService)
{
_settingsService = settingsService;
_settings = settingsService.Current;
SaveCommand = new AsyncRelayCommand(SaveAsync);
ResetCommand = new RelayCommand(Reset);
}
public string Theme
{
get => _settings.Theme;
set
{
if (_settings.Theme != value)
{
_settings.Theme = value;
OnPropertyChanged();
}
}
}
public bool AutoSave
{
get => _settings.AutoSave;
set
{
if (_settings.AutoSave != value)
{
_settings.AutoSave = value;
OnPropertyChanged();
}
}
}
public IAsyncRelayCommand SaveCommand { get; }
public IRelayCommand ResetCommand { get; }
private async Task SaveAsync()
{
await _settingsService.SaveAsync(_settings);
await ShowSuccessAsync("Settings saved");
}
private void Reset()
{
_settings = new AppSettings();
OnPropertyChanged(nameof(Theme));
OnPropertyChanged(nameof(AutoSave));
}
}
public partial class MainWindow : Window
{
private readonly ISettingsService _settingsService;
public MainWindow(MainViewModel viewModel, ISettingsService settingsService)
{
InitializeComponent();
DataContext = viewModel;
_settingsService = settingsService;
Loaded += MainWindow_Loaded;
Closing += MainWindow_Closing;
}
private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
var settings = await _settingsService.LoadAsync();
Width = settings.Window.Width;
Height = settings.Window.Height;
Left = settings.Window.Left;
Top = settings.Window.Top;
if (settings.Window.IsMaximized)
WindowState = WindowState.Maximized;
}
private async void MainWindow_Closing(object? sender, CancelEventArgs e)
{
var settings = _settingsService.Current;
if (WindowState == WindowState.Maximized)
{
settings.Window.IsMaximized = true;
}
else
{
settings.Window.Width = Width;
settings.Window.Height = Height;
settings.Window.Left = Left;
settings.Window.Top = Top;
settings.Window.IsMaximized = false;
}
await _settingsService.SaveAsync(settings);
}
}
// Встановлення пакету
// dotnet add package Microsoft.Extensions.Options
// Реєстрація
services.Configure<AppSettings>(configuration.GetSection("AppSettings"));
// Використання
public class SettingsViewModel : ViewModelBase
{
private readonly IOptions<AppSettings> _options;
public SettingsViewModel(IOptions<AppSettings> options)
{
_options = options;
}
public string Theme => _options.Value.Theme;
}
Переваги IOptions:
Недоліки:
// ❌ Синхронний код блокує UI
private void LoadUsers_Click(object sender, RoutedEventArgs e)
{
var users = _context.Users.ToList(); // Блокує UI на час запиту
UsersListBox.ItemsSource = users;
}
Що відбувається:
// ✅ Асинхронний код не блокує UI
private async void LoadUsers_Click(object sender, RoutedEventArgs e)
{
var users = await _context.Users.ToListAsync(); // Не блокує UI
UsersListBox.ItemsSource = users;
}
Що відбувається:
// Синхронні (блокують UI)
var users = context.Users.ToList();
var user = context.Users.Find(id);
var count = context.Users.Count();
context.SaveChanges();
// Асинхронні (не блокують UI)
var users = await context.Users.ToListAsync();
var user = await context.Users.FindAsync(id);
var count = await context.Users.CountAsync();
await context.SaveChangesAsync();
Мета: Зрозуміти структуру Repository Pattern без прив'язки до бази даних.
Завдання:
Створіть In-Memory репозиторій для TodoItem:
TodoItem модель (Id, Title, IsCompleted, CreatedAt)IRepository<T> інтерфейсInMemoryRepository<T> реалізація через List<T>List<T> (не база даних)Критерії успіху:
Підказка:
public class InMemoryRepository<T> : IRepository<T> where T : class, IEntity
{
private readonly List<T> _data = new();
private int _nextId = 1;
public Task<T?> GetByIdAsync(int id)
{
var entity = _data.FirstOrDefault(e => e.Id == id);
return Task.FromResult(entity);
}
public Task<IEnumerable<T>> GetAllAsync()
{
return Task.FromResult<IEnumerable<T>>(_data.ToList());
}
public Task AddAsync(T entity)
{
entity.Id = _nextId++;
_data.Add(entity);
return Task.CompletedTask;
}
// ... інші методи
}
Мета: Реалізувати повноцінний Repository Pattern з SQLite.
Завдання:
Створіть додаток для управління книгами:
Book модель (Id, Title, Author, ISBN, PublishedYear)IRepository<T> та GenericRepository<T>IBookRepository та BookRepositoryAppDbContext з DbSet<Book>GetBooksByAuthorAsync(string author)SearchBooksAsync(string searchTerm)GetBooksPageAsync(int pageNumber, int pageSize)IsbnExistsAsync(string isbn)Критерії успіху:
Мета: Побудувати повноцінний додаток з чистою архітектурою.
Завдання:
Створіть Task Manager додаток:
User (Id, Name, Email)TaskItem (Id, Title, Description, DueDate, IsCompleted, UserId)IRepository<T>, GenericRepository<T>IUserRepository, UserRepositoryITaskRepository, TaskRepositoryIUnitOfWork, UnitOfWorkКритерії успіху:
Підказка (структура проєкту):
MyApp/
├── Models/
│ ├── User.cs
│ └── TaskItem.cs
├── Data/
│ ├── AppDbContext.cs
│ ├── Repositories/
│ │ ├── IRepository.cs
│ │ ├── GenericRepository.cs
│ │ ├── IUserRepository.cs
│ │ ├── UserRepository.cs
│ │ ├── ITaskRepository.cs
│ │ └── TaskRepository.cs
│ └── UnitOfWork/
│ ├── IUnitOfWork.cs
│ └── UnitOfWork.cs
├── Services/
│ ├── ISettingsService.cs
│ └── SettingsService.cs
├── ViewModels/
│ ├── MainViewModel.cs
│ ├── UserListViewModel.cs
│ ├── TaskListViewModel.cs
│ └── SettingsViewModel.cs
└── Views/
├── MainWindow.xaml
├── UserListView.xaml
├── TaskListView.xaml
└── SettingsView.xaml
Repository Pattern та Unit of Work створюють чисту архітектуру доступу до даних. ViewModel не знає про DbContext, легко тестується через mock-репозиторії, бізнес-логіка відокремлена від доступу до даних.
Ключові висновки:
🏗️ Ієрархія
🔄 Unit of Work
🧪 Тестованість
⚡ Async/Await
⚙️ Settings
📐 Best Practices
Що далі?
Наступна стаття — ViewModel Testing покаже, як тестувати ViewModels з mock-репозиторіями.
SQLite та EF Core у десктопних додатках
Локальне зберігання даних через SQLite. DbContext, міграції, seeding. Інтеграція з DI. IDbContextFactory для багатопоточності.
Тестування ViewModels
Unit-тести для MVVM. xUnit, NSubstitute для mock-об'єктів. Тестування Properties, Commands, Validation, Messenger. Arrange-Act-Assert pattern.