| name | arcana-windows-developer-skill |
| description | Windows desktop development guide based on Arcana Windows enterprise architecture. Provides comprehensive support for Clean Architecture, WinUI 3, MVVM UDF pattern, Plugin System with 18 plugin types, CRDT-based offline sync, and enterprise security. Suitable for Windows desktop project development, architecture design, code review, and debugging. |
| allowed-tools | ["Read","Grep","Glob","Bash","Write","Edit"] |
Windows Developer Skill
Professional Windows desktop development skill based on Arcana Windows enterprise architecture.
⚡ Workflow — Always Start From the Reference Project
Every task starts by cloning the complete reference project — NEVER scaffold a new WinUI 3 project from scratch:
git clone https://github.com/jrjohn/arcana-windows.git [new-project-directory]
- Clone the reference project (command above).
- Build + test the UNTOUCHED clone first to establish a green baseline (
dotnet restore && dotnet build && dotnet test) before changing anything.
- Follow 0. Project Setup to rename the solution/namespaces and strip the demo screens — while KEEPING the infrastructure: auth/security layers (
src/Arcana.Infrastructure/Security — PBKDF2 hashing, RBAC, audit logging), caching, offline/sync (CRDT engine + vector clocks), the DI container (src/Arcana.Infrastructure/), the plugin runtime (src/Arcana.Plugins*), and deployment/build configs (.csproj versions, Directory.Build.props).
- Add features by copying the nearest working feature, then adapting (see 🔁 Adding a feature) — the recipe below is the completeness checklist. (File-by-File Feature Recipe)
🔁 Adding a feature = copy the nearest working feature (NOT from scratch)
When you add a new feature/screen, do NOT re-create it from memory by walking the File-by-File Recipe on a blank slate. Copy the nearest already-working feature in the cloned reference, then adapt it.
- Find the closest conformant feature already in the reference — e.g. the example Orders list/detail feature, which already demonstrates the full Model → Repository → Service/UseCase → ViewModel (Input/Output/Effect) → View chain wired through DI.
- Duplicate its ENTIRE file set (domain entity, service interface + implementation, repository interface, repository implementation + UnitOfWork wiring, EF Core configuration + DbContext DbSet, mock data, DI registration, ViewModel, View XAML + code-behind, INavGraph + NavGraph navigation, unit tests) 1:1, keeping every layer.
- Rename + adapt to the new domain (types, navigation, endpoints, DI bindings).
- Diff against the original to confirm nothing was dropped — same layer split, same ViewModel/use-case/repository boundary, same error model, same tests.
Why this is mandatory: the File-by-File Recipe is a checklist of what must exist, not a from-scratch build order. Re-deriving the pattern each time makes every step a chance to skip a layer (the ViewModel, the repository), wire a shortcut (view → data/API directly), or drop the tests — the "vibe-coding" deviations that compile and pass coverage but fail architecture review. Copying a known-good feature carries conformance in by construction; the recipe is then only your verification that nothing is missing.
Supporting files — load on demand
| File | When to read |
|---|
reference.md | Deep-dive architecture reference — layer responsibilities and project conventions |
patterns.md | Extended code patterns beyond those inlined in this file |
patterns/mvvm-udf.md | Detailed MVVM UDF (Input/Output/Effect) ViewModel walkthrough |
examples.md | Complete end-to-end feature examples to copy from |
checklists/production-ready.md | Pre-release checklist before declaring a feature done |
verification/commands.md | Full verification command set (superset of Quick Verification below) |
File-by-File Feature Recipe
Ordered file-by-file recipe for adding a complete feature (example: Orders) through all layers. Create files in this order — each step compiles against the previous ones.
- Domain entity →
src/Arcana.Domain/Entities/Order.cs
— Implements IEntity (and ISyncableEntity if it must sync offline).
- Service interface + implementation →
src/Arcana.Domain/Services/IOrderService.cs + OrderService.cs
— Business rules and validation; only calls repository/UnitOfWork methods that exist on the interfaces.
- Repository interface →
src/Arcana.Domain/Repositories/IOrderRepository.cs
— Extends IRepository<Order> for any custom queries beyond the generic CRUD.
- Repository implementation →
src/Arcana.Data/Repositories/OrderRepository.cs
— Then expose IRepository<Order> Orders { get; } on IUnitOfWork and wire it in UnitOfWork.
- EF Core configuration →
src/Arcana.Data/Configurations/OrderConfiguration.cs
— Entity mapping; register the DbSet<Order> in the DbContext.
- Mock data →
tests/Arcana.App.Tests/Mocks/MockOrderRepository.cs
— NEVER return empty collections; 5-10 varied items, Task.Delay() latency, IDs consistent with other repositories.
- DI registration →
src/Arcana.Infrastructure/DependencyInjection.cs
— Register service + repository; #if DEBUG switch between mock and real where applicable.
- ViewModel →
src/Arcana.App/ViewModels/OrderListViewModel.cs
— MVVM UDF: nested Input records, Output : ObservableObject, Effect records, Subject<Effect> Fx.
- View →
src/Arcana.App/Views/OrderListPage.xaml + OrderListPage.xaml.cs
— Loading/Error/Empty/Content states; subscribe to _viewModel.Fx in code-behind and route Effects to INavGraph.
- Navigation → add
ToOrderList() / ToOrderDetail(string orderId) to INavGraph AND implement them in NavGraph (_frame.Navigate(typeof(OrderListPage))).
- Unit tests →
tests/Arcana.App.Tests/ViewModels/OrderListViewModelTests.cs, tests/Arcana.Data.Tests/Repositories/OrderRepositoryTests.cs.
- Verify →
dotnet build && dotnet test, then run the Quick Verification Commands — every Effect.Navigate* must have a View subscription AND a NavGraph implementation.
Quick Reference Card
New View Checklist:
1. Add Page.xaml to Presentation/Views/
2. Create ViewModel with Input/Output/Effect pattern
3. Add navigation method to INavGraph interface
4. Implement navigation in NavGraph
5. Subscribe to ViewModel.Fx in code-behind
6. Verify mock data returns non-empty values
New Repository Checklist:
1. Interface → Domain/Repositories/IXxxRepository.cs
2. Implementation → Data/Repositories/XxxRepository.cs
3. DI registration → Infrastructure/DependencyInjection.cs
4. Mock data (NEVER return empty collections!)
5. Verify ID consistency across repositories
Quick Diagnosis:
| Symptom | Check Command |
|---|
| Blank screen | `grep "new List<>\ |
| Navigation crash | Compare INavGraph methods vs NavGraph implementations |
| Button does nothing | Check Fx.Subscribe in View code-behind |
Rules Priority
🔴 CRITICAL (Must Fix Immediately)
| Rule | Description | Verification |
|---|
| Zero-Empty Policy | Repository stubs NEVER return empty collections | `grep "Empty\ |
| Navigation Wiring | ALL INavGraph methods MUST be in NavGraph | Compare interface vs impl |
| Effect Handling | ALL ViewModel Effects MUST be subscribed | Check Fx.Subscribe |
| ID Consistency | Cross-repository IDs must match | Check mock data IDs |
🟡 IMPORTANT (Should Fix Before PR)
| Rule | Description | Verification |
|---|
| UI States | Loading/Error/Empty for all views | Check ViewModel Output |
| Mock Data Quality | Realistic, varied values | Review mock data |
| Error Messages | User-friendly messages | Check AppError handling |
| Nullable | Proper null handling | Check ? annotations |
🟢 RECOMMENDED (Nice to Have)
| Rule | Description |
|---|
| Animations | Smooth view transitions |
| Accessibility | AutomationProperties.Name |
| Dark Mode | Theme support |
| Telemetry | Analytics tracking |
Error Handling Pattern
AppException - Unified Error Model
public class AppException : Exception
{
public ErrorCode Code { get; }
public Dictionary<string, object>? Details { get; }
public enum ErrorCode
{
NetworkUnavailable,
Timeout,
ServiceUnavailable,
Unauthorized,
TokenExpired,
NotFound,
ValidationFailed,
Conflict,
InternalError
}
public static AppException NotFound(string message)
=> new(ErrorCode.NotFound, message);
public static AppException Validation(string message, Dictionary<string, object> details)
=> new(ErrorCode.ValidationFailed, message) { Details = details };
}
Error Handling in ViewModel
private async Task LoadDataAsync()
{
IsLoading = true;
Error = null;
try
{
Items = await _repository.GetAllAsync();
}
catch (AppException ex)
{
Error = ex.Message;
if (ex.Code == ErrorCode.Unauthorized)
Fx.OnNext(new Effect.NavigateToLogin());
}
finally
{
IsLoading = false;
}
}
Test Coverage Targets
Coverage by Layer
| Layer | Target | Focus Areas |
|---|
| ViewModel | 90%+ | All intents, state transitions, effects |
| Service | 85%+ | Business logic, edge cases |
| Repository | 80%+ | Data mapping, error handling |
Test Commands
dotnet test --collect:"XPlat Code Coverage"
reportgenerator -reports:coverage.xml -targetdir:coverage
Spec Gap Prediction System
When implementing UI from incomplete specifications, PROACTIVELY predict missing screens and states:
Screen Prediction Matrix
When a spec mentions a feature, predict ALL related screens:
| Feature | Predicted Screens | Check |
|---|
| User Management | UserListPage | ✅ |
| User Management | UserDetailPage | ✅ |
| User Management | UserEditPage | ✅ |
| User Management | UserCreatePage | ✅ |
| User Management | UserDeleteConfirmDialog | ✅ |
UI State Prediction
For every screen, predict required UI states:
Navigation Prediction
When adding a new feature, predict navigation flow:
Dialog Prediction
CRUD operations need confirmation dialogs:
Ask Clarification Prompt
When specs are incomplete, ASK before implementing:
The specification mentions "User Management" but doesn't specify:
1. Should the list support multi-select?
2. What fields are shown in the list vs detail view?
3. Are there bulk operations (delete multiple)?
4. What sorting/filtering options are needed?
Please clarify before I proceed with implementation.
Core Architecture Principles
Clean Architecture - Five Layers
┌─────────────────────────────────────────────────────┐
│ Presentation Layer │
│ WinUI 3 + MVVM UDF + Navigation │
├─────────────────────────────────────────────────────┤
│ Infrastructure Layer │
│ DI + Security + Settings │
├─────────────────────────────────────────────────────┤
│ Domain Layer │
│ Business Entities + Services │
├─────────────────────────────────────────────────────┤
│ Data Layer │
│ Repository + Unit of Work + EF Core │
├─────────────────────────────────────────────────────┤
│ Sync Layer │
│ CRDT Engine + Vector Clocks │
└─────────────────────────────────────────────────────┘
Architecture Ratings
| Category | Score |
|---|
| Clean Architecture | 9.0/10 |
| Plugin System | 9.5/10 |
| MVVM Pattern | 9.5/10 |
| Security | 9.0/10 |
| Overall | 9.0/10 |
Instructions
When handling Windows desktop development tasks, follow these principles:
Quick Verification Commands
Use these commands to quickly check for common issues:
grep -rn "throw.*NotImplementedException\|TODO.*implement" src/
grep -rn "Click=\"\"\|Command=\"{x:Null}\"" src/
echo "NavGraph methods:" && grep -c "public void To\|public void Navigate" src/**/Navigation/NavGraph.cs 2>/dev/null || echo 0
echo "Pages registered:" && grep -c "typeof(.*Page)" src/**/Navigation/NavGraph.cs 2>/dev/null || echo 0
echo "Views:" && find src -name "*View.xaml" 2>/dev/null | wc -l
echo "ViewModels:" && find src -name "*ViewModel.cs" 2>/dev/null | wc -l
dotnet build
dotnet test
echo "=== INavGraph Interface Methods ===" && \
grep -rh "void To[A-Z]\|Task To[A-Z]" src/**/INavGraph.cs | grep -oE "To[A-Za-z]+" | sort -u
echo "=== NavGraph Implemented Methods ===" && \
grep -rh "public void To[A-Z]\|public async Task To[A-Z]" src/**/NavGraph.cs | grep -oE "To[A-Za-z]+" | sort -u
echo "=== ViewModel Effect Types ===" && \
grep -rh "record Navigate\|record Show\|record Open" src/**/ViewModels/*.cs | grep -oE "record [A-Za-z]+" | sort -u
echo "=== Effect Subscriptions in Views ===" && \
grep -rn "Fx.Subscribe\|_viewModel.Fx" src/**/Views/*.cs
grep -rn "OnNavigate\|NavigateTo" src/**/Views/*.xaml.cs | grep -v "INavGraph\|_navGraph"
echo "=== Repository Methods Called in Services ===" && \
grep -roh "_repository\.[A-Za-z]*(\|_unitOfWork\.[A-Za-z]*\.[A-Za-z]*(" src/**/Services/*.cs | sort -u
echo "=== Repository Interface Methods ===" && \
grep -rh "Task<\|[A-Z][a-zA-Z]* [A-Z]" src/**/IRepository.cs | grep -oE "[A-Z][a-zA-Z]+Async\(" | sort -u
echo "=== IRepository Interface Methods ===" && \
grep -rh "Task<\|void " src/**/Repositories/IRepository.cs | grep -oE "[A-Z][a-zA-Z]+\(" | sort -u
echo "=== Repository Implementation Methods ===" && \
grep -rh "public.*async\|public.*override" src/**/Repositories/Repository.cs | grep -oE "[A-Z][a-zA-Z]+\(" | sort -u
⚠️ CRITICAL: All NavGraph navigation methods MUST have corresponding Page types registered. Missing pages cause runtime crashes.
⚠️ NAVIGATION WIRING CRITICAL: Commands #7-#9 detect INavGraph interface methods not implemented in NavGraph, ViewModel Effects not subscribed in Views, and navigation callbacks not wired. A ViewModel can emit Fx.OnNext(new Effect.NavigateToSettings()) but if the View doesn't subscribe and call _navGraph.ToSettings(), nothing happens!
If any of these return results or counts don't match, FIX THEM before completing the task.
📊 Mock Data Requirements for Repository Stubs
The Chart Data Problem
When implementing Repository stubs, NEVER return empty collections for data that powers UI charts or visualizations. This causes:
- Charts that render but show nothing (blank WinUI charts)
- Line charts that skip rendering (e.g.,
if (points.Count < 2) return;)
- Empty state views even when data structure exists
Mock Data Rules
Rule 1: Collection data for charts MUST have at least 7 items
public async Task<WeeklySummary> GetCurrentWeekSummaryAsync(string userId)
{
return new WeeklySummary
{
DailyReports = new List<DailyReport>()
};
}
public async Task<WeeklySummary> GetCurrentWeekSummaryAsync(string userId)
{
var scores = new[] { 72, 78, 85, 80, 76, 88, 82 };
var durations = new[] { 390, 420, 450, 410, 380, 460, 435 };
var mockDailyReports = Enumerable.Range(0, 7)
.Select(i => CreateMockDailyReport(scores[i], durations[i]))
.ToList();
return new WeeklySummary { DailyReports = mockDailyReports };
}
Rule 2: Use realistic, varied sample values
var scores = Enumerable.Repeat(80, 7).ToArray();
var scores = new[] { 72, 78, 85, 80, 76, 88, 82 };
Rule 3: Data must match entity/DTO class exactly
grep -A 20 "class TherapyData" src/**/Entities/*.cs
grep -A 20 "record TherapyData" src/**/DTOs/*.cs
Rule 4: Create helper methods for complex mock data
private DailyReport CreateMockDailyReport(int score, int duration)
{
return new DailyReport
{
Id = Guid.NewGuid().ToString(),
SleepScore = score,
SleepDuration = new SleepDuration { TotalMinutes = duration, ... },
};
}
Quick Verification Commands for Mock Data
grep -rn "new List<\|Enumerable.Empty<\|Array.Empty<" src/**/Repositories/*Repository.cs
grep -rn "DailyReports\|WeeklyData\|ChartData" src/**/Repositories/ | grep -E "new List<.*>\(\)|Empty<"
0. Project Setup - CRITICAL
⚠️ IMPORTANT: This reference project has been validated with tested NuGet settings and library versions. NEVER reconfigure project structure or modify .csproj / Directory.Build.props, or it will cause compilation errors.
Step 1: Clone the reference project
git clone https://github.com/jrjohn/arcana-windows.git [new-project-directory]
cd [new-project-directory]
Step 2: Reinitialize Git (remove original repo history)
rm -rf .git
git init
git add .
git commit -m "Initial commit from arcana-windows template"
Step 3: Modify project name and namespace
Only modify the following required items:
.sln solution file name
<RootNamespace> and <AssemblyName> in each .csproj
- Rename project directories under
src/
- Update namespace declarations in code
Step 4: Clean up example code
The cloned project contains example UI (e.g., Arcana User Management). Clean up and replace with new project screens:
Core architecture files to KEEP (do not delete):
src/Arcana.Infrastructure/ - DI, Security, Settings
src/Arcana.Domain/Services/ - Common Service base classes
src/Arcana.Data/DbContext/ - EF Core base configuration
src/Arcana.Data/Repositories/ - Repository base classes
src/Arcana.Plugins.Contracts/ - Plugin interface definitions
src/Arcana.Plugins/Runtime/ - Plugin runtime
src/Arcana.App/App.xaml - App entry point
src/Arcana.App/Navigation/ - NavGraph configuration (modify routes)
Example files to REPLACE:
src/Arcana.App/Views/ - Delete all example screens, create new project Views
src/Arcana.App/ViewModels/ - Delete example ViewModel, create new ViewModel
src/Arcana.Domain/Entities/ - Delete example Entities, create new Domain Entities
src/Arcana.Data/Configurations/ - Modify EF Core configuration
plugins/ - Delete example Plugin, create new Plugin
Step 5: Verify build
dotnet restore
dotnet build
dotnet test
❌ Prohibited Actions
- DO NOT create new WinUI 3 project from scratch
- DO NOT modify NuGet package version numbers in
.csproj
- DO NOT add or remove NuGet dependencies (unless explicitly required)
- DO NOT modify shared settings in
Directory.Build.props
- DO NOT reconfigure WinUI 3, EF Core, CommunityToolkit, or other library settings
✅ Allowed Modifications
- Add business-related C# code (following existing architecture)
- Add Views, ViewModels
- Add Domain Entities, Repositories
- Modify XAML resource files
- Develop new Plugins (following 18 Plugin types)
1. TDD & Spec-Driven Development Workflow - MANDATORY
⚠️ CRITICAL: All development MUST follow this TDD workflow. Every SRS/SDD requirement must have corresponding tests BEFORE implementation.
🚨 ABSOLUTE RULE: TDD = Tests + Implementation. Writing tests without implementation is INCOMPLETE. Every test file MUST have corresponding production code that passes the tests.
┌─────────────────────────────────────────────────────────────────┐
│ TDD Development Workflow │
├─────────────────────────────────────────────────────────────────┤
│ Step 1: Analyze Spec → Extract all SRS & SDD requirements │
│ Step 2: Create Tests → Write tests for EACH Spec item │
│ Step 3: Verify Coverage → Ensure 100% Spec coverage in tests │
│ Step 4: Implement → Build features to pass tests ⚠️ MANDATORY │
│ Step 5: Mock APIs → Use mock data for unfinished dependencies │
│ Step 6: Run All Tests → ALL tests must pass before completion │
│ Step 7: Verify 100% → Tests written = Features implemented │
└─────────────────────────────────────────────────────────────────┘
⛔ FORBIDDEN: Tests Without Implementation
⛔ Placeholder Page Policy
Placeholder pages are ONLY allowed as a temporary navigation target during active development. They are FORBIDDEN as a final state.
case Route.Training:
_frame.Navigate(typeof(PlaceholderPage), "Training Course");
break;
case Route.Training:
_frame.Navigate(typeof(TrainingPage));
break;
Placeholder Check Command:
grep -rn "PlaceholderPage\|NotImplementedException\|TODO.*implement\|Coming Soon" src/
Step 1: Analyze Spec Documents (SRS & SDD)
Before writing any code, extract ALL requirements from both SRS and SDD:
Step 2: Create Test Cases for Each Spec Item
using Xunit;
using Moq;
using FluentAssertions;
public class LoginViewModelTests
{
private readonly Mock<IAuthService> _mockAuthService;
private readonly LoginViewModel _viewModel;
public LoginViewModelTests()
{
_mockAuthService = new Mock<IAuthService>();
_viewModel = new LoginViewModel(_mockAuthService.Object);
}
[Fact]
public async Task Login_WithValidCredentials_ShouldSucceed()
{
_mockAuthService
.Setup(x => x.LoginAsync("test@test.com", "password123"))
.ReturnsAsync(new AuthResult { Success = true });
_viewModel.OnInput(new LoginViewModel.Input.UpdateEmail("test@test.com"));
_viewModel.OnInput(new LoginViewModel.Input.UpdatePassword("password123"));
await _viewModel.OnInput(new LoginViewModel.Input.Submit());
_viewModel.Out.IsLoginSuccess.Should().BeTrue();
_viewModel.Out.Error.Should().BeNull();
}
[Fact]
public async Task Login_WithInvalidCredentials_ShouldShowError()
{
_mockAuthService
.Setup(x => x.LoginAsync(It.IsAny<string>(), It.IsAny<string>()))
.ThrowsAsync(new AuthException("Invalid credentials"));
await _viewModel.OnInput(new LoginViewModel.Input.Submit());
_viewModel.Out.IsLoginSuccess.Should().BeFalse();
_viewModel.Out.Error.Should().NotBeNull();
}
[Fact]
public void ViewModel_ShouldFollowUDFPattern()
{
typeof(LoginViewModel).Should().HaveNestedType("Input");
typeof(LoginViewModel).GetProperty("Out").Should().NotBeNull();
typeof(LoginViewModel).GetProperty("Fx").Should().NotBeNull();
}
[Fact]
public void PasswordHasher_ShouldUsePBKDF2SHA256()
{
var hasher = new PasswordHasher();
var hash = hasher.HashPassword("password123");
hash.Should().StartWith("pbkdf2:sha256:");
}
}
Step 3: Spec Coverage Verification Checklist
Before implementation, verify ALL SRS and SDD items have tests:
Step 4: Mock External Dependencies - MANDATORY
⚠️ CRITICAL: Every Repository/Service method MUST return valid mock data. NEVER leave methods throwing NotImplementedException.
Rules for Mock Classes:
- ALL methods must return valid mock data
- Use
Task.Delay() to simulate network latency (500-1000ms)
- Mock data must match the entity structure exactly
- Check enum values exist before using them
- Include all required properties for records/classes
For external services or databases not yet available, implement mock classes:
public class MockAuthService : IAuthService
{
private static readonly List<MockUser> MockUsers = new()
{
new("1", "test@test.com", "pbkdf2:sha256:...", "Test User"),
new("2", "demo@demo.com", "pbkdf2:sha256:...", "Demo User")
};
public async Task<AuthResult> LoginAsync(string email, string password)
{
await Task.Delay(500);
var user = MockUsers.FirstOrDefault(u => u.Email == email);
if (user != null && VerifyPassword(password, user.PasswordHash))
{
return new AuthResult
{
Success = true,
AccessToken = $"mock_token_{DateTime.UtcNow.Ticks}",
UserName = user.Name
};
}
throw new AuthException("Invalid email or password");
}
public bool IsLoggedIn() => !string.IsNullOrEmpty(GetAccessToken());
private string? GetAccessToken() =>
Windows.Storage.ApplicationData.Current.LocalSettings.Values["access_token"] as string;
}
public static class ServiceRegistration
{
public static IServiceCollection AddAuthService(this IServiceCollection services)
{
#if DEBUG
services.AddSingleton<IAuthService, MockAuthService>();
#else
services.AddSingleton<IAuthService, AuthService>();
#endif
return services;
}
}
Step 5: Run All Tests Before Completion
dotnet test
dotnet test --collect:"XPlat Code Coverage"
dotnet test tests/Arcana.App.Tests/Arcana.App.Tests.csproj
dotnet test --logger "console;verbosity=detailed"
dotnet test --no-build
Test Directory Structure
tests/
├── Arcana.App.Tests/
│ ├── ViewModels/
│ │ ├── LoginViewModelTests.cs
│ │ ├── RegisterViewModelTests.cs
│ │ ├── DashboardViewModelTests.cs
│ │ └── SplashViewModelTests.cs
│ ├── Services/
│ │ └── AuthServiceTests.cs
│ └── Mocks/
│ ├── MockAuthService.cs
│ └── MockUserRepository.cs
├── Arcana.Domain.Tests/
│ └── Services/
│ └── UserServiceTests.cs
├── Arcana.Data.Tests/
│ └── Repositories/
│ └── UserRepositoryTests.cs
└── Arcana.Infrastructure.Tests/
└── Security/
└── PasswordHasherTests.cs
2. Project Structure
arcana-windows/
├── src/
│ ├── Arcana.App/ # WinUI 3 presentation layer
│ │ ├── Views/ # XAML views
│ │ ├── ViewModels/ # MVVM UDF ViewModels
│ │ └── Navigation/ # NavGraph
│ ├── Arcana.Domain/ # Business entities & services
│ │ ├── Entities/
│ │ └── Services/
│ ├── Arcana.Data/ # Repository + Unit of Work
│ │ ├── Repositories/
│ │ └── DbContext/
│ ├── Arcana.Infrastructure/ # DI, Security, Settings
│ │ ├── Security/
│ │ └── Settings/
│ ├── Arcana.Plugins/ # Plugin runtime
│ │ └── Runtime/
│ └── Arcana.Plugins.Contracts/ # Plugin interfaces
│ └── Interfaces/
├── plugins/ # Built-in plugins
│ └── FlowChartModule/
└── tests/ # 507 unit tests
2. MVVM UDF Pattern (Unidirectional Data Flow)
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Reactive.Subjects;
public partial class UserViewModel : ObservableObject
{
public sealed class Input
{
public record UpdateName(string Name);
public record UpdateEmail(string Email);
public record Submit;
}
public sealed partial class Output : ObservableObject
{
[ObservableProperty]
private string _name = string.Empty;
[ObservableProperty]
private string _email = string.Empty;
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private string? _error;
}
public sealed class Effect
{
public record NavigateBack;
public record ShowSnackbar(string Message);
}
public Output Out { get; } = new();
public Subject<Effect> Fx { get; } = new();
private readonly IUserService _userService;
public UserViewModel(IUserService userService)
{
_userService = userService;
}
public void OnInput(object input)
{
switch (input)
{
case Input.UpdateName updateName:
Out.Name = updateName.Name;
break;
case Input.UpdateEmail updateEmail:
Out.Email = updateEmail.Email;
break;
case Input.Submit:
_ = SubmitAsync();
break;
}
}
private async Task SubmitAsync()
{
Out.IsLoading = true;
Out.Error = null;
try
{
await _userService.UpdateUserAsync(Out.Name, Out.Email);
Fx.OnNext(new Effect.NavigateBack());
}
catch (Exception ex)
{
Out.Error = ex.Message;
}
finally
{
Out.IsLoading = false;
}
}
}
3. Type-Safe Navigation (NavGraph)
public interface INavGraph
{
void ToHome();
void ToUserList();
void ToUserDetail(string userId);
void ToUserEdit(string userId);
void ToOrderList();
void ToOrderDetail(string orderId);
void Back();
}
public class NavGraph : INavGraph
{
private readonly Frame _frame;
private readonly IServiceProvider _serviceProvider;
public NavGraph(Frame frame, IServiceProvider serviceProvider)
{
_frame = frame;
_serviceProvider = serviceProvider;
}
public void ToHome()
{
_frame.Navigate(typeof(HomePage));
}
public void ToUserList()
{
_frame.Navigate(typeof(UserListPage));
}
public void ToUserDetail(string userId)
{
_frame.Navigate(typeof(UserDetailPage), userId);
}
public void ToUserEdit(string userId)
{
_frame.Navigate(typeof(UserEditPage), userId);
}
public void ToOrderList()
{
_frame.Navigate(typeof(OrderListPage));
}
public void ToOrderDetail(string orderId)
{
_frame.Navigate(typeof(OrderDetailPage), orderId);
}
public void Back()
{
if (_frame.CanGoBack)
{
_frame.GoBack();
}
}
}
public interface IPluginNavGraph
{
void ToPluginView(string viewKey);
void ToPluginView(string viewKey, object parameter);
}
public class FlowChartNavGraph : IPluginNavGraph
{
public void ToNewEditor() => ToPluginView("flowchart-editor");
public void ToEditor(string chartId) => ToPluginView("flowchart-editor", chartId);
public void ToPluginView(string viewKey)
{
}
public void ToPluginView(string viewKey, object parameter)
{
}
}
4. Plugin System (18 Plugin Types)
public interface IArcanaPlugin
{
string Key { get; }
string Name { get; }
string Version { get; }
PluginManifest Manifest { get; }
Task OnActivateAsync(IPluginContext context);
Task OnDeactivateAsync();
}
public record PluginManifest
{
public string Key { get; init; } = string.Empty;
public string Name { get; init; } = string.Empty;
public string Version { get; init; } = "1.0.0";
public string Description { get; init; } = string.Empty;
public string Author { get; init; } = string.Empty;
public string[] Dependencies { get; init; } = Array.Empty<string>();
public PluginType Type { get; init; } = PluginType.Module;
public string[] ActivationEvents { get; init; } = Array.Empty<string>();
}
public enum PluginType
{
Menu,
View,
Module,
Theme,
Authentication,
Data,
Command,
Service,
Validator,
Storage,
Export,
Import,
Report,
Notification,
Logging,
Cache,
Search,
Analytics
}
public interface IPluginContext
{
IMessageBus MessageBus { get; }
IEventAggregator EventAggregator { get; }
IStateStore StateStore { get; }
IPluginNavGraph Navigation { get; }
IDialogService DialogService { get; }
INotificationService NotificationService { get; }
ISettingsService SettingsService { get; }
ILoggerFactory LoggerFactory { get; }
IServiceProvider ServiceProvider { get; }
IPluginStorage Storage { get; }
ILocalizationService Localization { get; }
IThemeService ThemeService { get; }
}
[PluginManifest(
Key = "flowchart-module",
Name = "Flow Chart Module",
Version = "1.0.0",
Type = PluginType.Module
)]
public class FlowChartPlugin : IArcanaPlugin
{
public string Key => "flowchart-module";
public string Name => "Flow Chart Module";
public string Version => "1.0.0";
public PluginManifest Manifest { get; } = new()
{
Key = "flowchart-module",
Name = "Flow Chart Module",
Type = PluginType.Module,
ActivationEvents = new[] { "onCommand:flowchart.new" }
};
private IPluginContext? _context;
public async Task OnActivateAsync(IPluginContext context)
{
_context = context;
context.Navigation.RegisterView("flowchart-editor", typeof(FlowChartEditorView));
context.Navigation.RegisterView("flowchart-list", typeof(FlowChartListView));
context.MessageBus.Subscribe<NewFlowChartCommand>(OnNewFlowChart);
context.EventAggregator.Publish(new RegisterMenuItemEvent
{
MenuId = "tools",
Item = new MenuItem("Flow Chart", "flowchart.new")
});
await Task.CompletedTask;
}
public async Task OnDeactivateAsync()
{
await Task.CompletedTask;
}
private void OnNewFlowChart(NewFlowChartCommand command)
{
_context?.Navigation.ToPluginView("flowchart-editor");
}
}
public class PluginRuntime
{
private readonly Dictionary<string, AssemblyLoadContext> _loadContexts = new();
private readonly Dictionary<string, IArcanaPlugin> _plugins = new();
public async Task LoadPluginAsync(string pluginPath)
{
var loadContext = new PluginLoadContext(pluginPath);
var assembly = loadContext.LoadFromAssemblyPath(pluginPath);
var pluginType = assembly.GetTypes()
.FirstOrDefault(t => typeof(IArcanaPlugin).IsAssignableFrom(t));
if (pluginType == null)
throw new InvalidOperationException("No plugin found in assembly");
var plugin = (IArcanaPlugin)Activator.CreateInstance(pluginType)!;
_loadContexts[plugin.Key] = loadContext;
_plugins[plugin.Key] = plugin;
}
public async Task UnloadPluginAsync(string key)
{
if (_plugins.TryGetValue(key, out var plugin))
{
await plugin.OnDeactivateAsync();
_plugins.Remove(key);
}
if (_loadContexts.TryGetValue(key, out var context))
{
context.Unload();
_loadContexts.Remove(key);
}
}
}
Create a New Plugin — File Recipe
- Project → create
plugins/<YourPlugin>/<YourPlugin>.csproj referencing src/Arcana.Plugins.Contracts/ (interfaces only — never reference Arcana.App directly).
- Manifest → define the
PluginManifest (Key, Name, Version, PluginType — one of the 18 types, ActivationEvents).
- Plugin class →
plugins/<YourPlugin>/<YourPlugin>Plugin.cs implementing IArcanaPlugin (Key/Name/Version/Manifest + OnActivateAsync/OnDeactivateAsync).
- Views → add plugin views (e.g.
plugins/<YourPlugin>/Views/) and register them in OnActivateAsync via context.Navigation.RegisterView("<view-key>", typeof(YourView)).
- Commands / menu items → subscribe via
context.MessageBus.Subscribe<YourCommand>(...) and publish RegisterMenuItemEvent via context.EventAggregator.
- Cleanup → unsubscribe and release resources in
OnDeactivateAsync.
- Load + verify → build the plugin, let
PluginRuntime.LoadPluginAsync load it through its isolated AssemblyLoadContext, then test activation/deactivation (no leaked subscriptions after unload).
5. Repository + Unit of Work Pattern
public interface IRepository<T> where T : class, IEntity
{
Task<T?> GetByIdAsync(string id);
Task<IEnumerable<T>> GetAllAsync();
Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate);
Task AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(T entity);
Task SoftDeleteAsync(T entity);
}
public interface IUnitOfWork : IDisposable
{
IRepository<User> Users { get; }
IRepository<Order> Orders { get; }
IRepository<Product> Products { get; }
IRepository<Customer> Customers { get; }
Task<int> SaveChangesAsync();
Task BeginTransactionAsync();
Task CommitAsync();
Task RollbackAsync();
}
public class Repository<T> : IRepository<T> where T : class, IEntity
{
protected readonly AppDbContext _context;
protected readonly DbSet<T> _dbSet;
public Repository(AppDbContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public async Task<T?> GetByIdAsync(string id)
{
return await _dbSet.FindAsync(id);
}
public async Task<IEnumerable<T>> GetAllAsync()
{
return await _dbSet.ToListAsync();
}
public async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate)
{
return await _dbSet.Where(predicate).ToListAsync();
}
public async Task AddAsync(T entity)
{
entity.CreatedAt = DateTime.UtcNow;
entity.UpdatedAt = DateTime.UtcNow;
await _dbSet.AddAsync(entity);
}
public async Task UpdateAsync(T entity)
{
entity.UpdatedAt = DateTime.UtcNow;
_dbSet.Update(entity);
await Task.CompletedTask;
}
public async Task DeleteAsync(T entity)
{
_dbSet.Remove(entity);
await Task.CompletedTask;
}
public async Task SoftDeleteAsync(T entity)
{
entity.IsDeleted = true;
entity.DeletedAt = DateTime.UtcNow;
await UpdateAsync(entity);
}
}
public class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext _context;
private IDbContextTransaction? _transaction;
public IRepository<User> Users { get; }
public IRepository<Order> Orders { get; }
public IRepository<Product> Products { get; }
public IRepository<Customer> Customers { get; }
public UnitOfWork(AppDbContext context)
{
_context = context;
Users = new Repository<User>(context);
Orders = new Repository<Order>(context);
Products = new Repository<Product>(context);
Customers = new Repository<Customer>(context);
}
public async Task<int> SaveChangesAsync()
{
return await _context.SaveChangesAsync();
}
public async Task BeginTransactionAsync()
{
_transaction = await _context.Database.BeginTransactionAsync();
}
public async Task CommitAsync()
{
if (_transaction != null)
{
await _transaction.CommitAsync();
await _transaction.DisposeAsync();
_transaction = null;
}
}
public async Task RollbackAsync()
{
if (_transaction != null)
{
await _transaction.RollbackAsync();
await _transaction.DisposeAsync();
_transaction = null;
}
}
public void Dispose()
{
_transaction?.Dispose();
_context.Dispose();
}
}
6. CRDT Sync Engine
public class VectorClock
{
private readonly Dictionary<string, long> _clock = new();
public void Increment(string nodeId)
{
_clock[nodeId] = _clock.GetValueOrDefault(nodeId, 0) + 1;
}
public void Merge(VectorClock other)
{
foreach (var (nodeId, timestamp) in other._clock)
{
_clock[nodeId] = Math.Max(
_clock.GetValueOrDefault(nodeId, 0),
timestamp
);
}
}
public CausalOrdering Compare(VectorClock other)
{
bool thisGreater = false;
bool otherGreater = false;
var allKeys = _clock.Keys.Union(other._clock.Keys);
foreach (var key in allKeys)
{
var thisValue = _clock.GetValueOrDefault(key, 0);
var otherValue = other._clock.GetValueOrDefault(key, 0);
if (thisValue > otherValue) thisGreater = true;
if (otherValue > thisValue) otherGreater = true;
}
if (thisGreater && otherGreater) return CausalOrdering.Concurrent;
if (thisGreater) return CausalOrdering.After;
if (otherGreater) return CausalOrdering.Before;
return CausalOrdering.Equal;
}
}
public enum CausalOrdering
{
Before,
After,
Equal,
Concurrent
}
public enum ConflictStrategy
{
LastWriteWins,
FirstWriteWins,
FieldLevelMerge,
MultiValue,
Custom
}
public class CrdtSyncManager
{
private readonly IUnitOfWork _unitOfWork;
private readonly VectorClock _localClock;
private readonly string _nodeId;
private readonly ConflictStrategy _defaultStrategy;
public CrdtSyncManager(
IUnitOfWork unitOfWork,
string nodeId,
ConflictStrategy defaultStrategy = ConflictStrategy.LastWriteWins)
{
_unitOfWork = unitOfWork;
_nodeId = nodeId;
_localClock = new VectorClock();
_defaultStrategy = defaultStrategy;
}
public async Task<T> ApplyChangeAsync<T>(
T localEntity,
T remoteEntity,
ConflictStrategy? strategy = null) where T : class, ISyncableEntity
{
var ordering = localEntity.VectorClock.Compare(remoteEntity.VectorClock);
var effectiveStrategy = strategy ?? _defaultStrategy;
return ordering switch
{
CausalOrdering.Before => remoteEntity,
CausalOrdering.After => localEntity,
CausalOrdering.Equal => localEntity,
CausalOrdering.Concurrent => ResolveConflict(
localEntity, remoteEntity, effectiveStrategy),
_ => localEntity
};
}
private T ResolveConflict<T>(
T local,
T remote,
ConflictStrategy strategy) where T : class, ISyncableEntity
{
return strategy switch
{
ConflictStrategy.LastWriteWins =>
local.UpdatedAt > remote.UpdatedAt ? local : remote,
ConflictStrategy.FirstWriteWins =>
local.CreatedAt < remote.CreatedAt ? local : remote,
ConflictStrategy.FieldLevelMerge =>
MergeFields(local, remote),
_ => local
};
}
private T MergeFields<T>(T local, T remote) where T : class, ISyncableEntity
{
var properties = typeof(T).GetProperties()
.Where(p => p.CanRead && p.CanWrite);
foreach (var prop in properties)
{
var localMeta = local.GetFieldMetadata(prop.Name);
var remoteMeta = remote.GetFieldMetadata(prop.Name);
if (remoteMeta.UpdatedAt > localMeta.UpdatedAt)
{
prop.SetValue(local, prop.GetValue(remote));
}
}
local.VectorClock.Merge(remote.VectorClock);
return local;
}
public async Task MarkForSyncAsync<T>(T entity) where T : class, ISyncableEntity
{
_localClock.Increment(_nodeId);
entity.VectorClock = _localClock;
entity.SyncStatus = SyncStatus.Pending;
entity.UpdatedAt = DateTime.UtcNow;
await _unitOfWork.SaveChangesAsync();
}
}
7. Security Architecture
public class PasswordHasher : IPasswordHasher
{
private const int Iterations = 100_000;
private const int SaltSize = 16;
private const int HashSize = 32;
public string HashPassword(string password)
{
var salt = RandomNumberGenerator.GetBytes(SaltSize);
var hash = Rfc2898DeriveBytes.Pbkdf2(
password,
salt,
Iterations,
HashAlgorithmName.SHA256,
HashSize
);
var result = new byte[SaltSize + HashSize];
Buffer.BlockCopy(salt, 0, result, 0, SaltSize);
Buffer.BlockCopy(hash, 0, result, SaltSize, HashSize);
return Convert.ToBase64String(result);
}
public bool VerifyPassword(string password, string hashedPassword)
{
var bytes = Convert.FromBase64String(hashedPassword);
var salt = new byte[SaltSize];
Buffer.BlockCopy(bytes, 0, salt, 0, SaltSize);
var storedHash = new byte[HashSize];
Buffer.BlockCopy(bytes, SaltSize, storedHash, 0, HashSize);
var computedHash = Rfc2898DeriveBytes.Pbkdf2(
password,
salt,
Iterations,
HashAlgorithmName.SHA256,
HashSize
);
return CryptographicOperations.FixedTimeEquals(storedHash, computedHash);
}
}
public interface IAuthorizationService
{
Task<bool> HasPermissionAsync(string userId, string permission);
Task<bool> IsInRoleAsync(string userId, string role);
Task<IEnumerable<string>> GetPermissionsAsync(string userId);
}
public class AuthorizationService : IAuthorizationService
{
private readonly IUnitOfWork _unitOfWork;
public AuthorizationService(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task<bool> HasPermissionAsync(string userId, string permission)
{
var permissions = await GetPermissionsAsync(userId);
return permissions.Contains(permission);
}
public async Task<bool> IsInRoleAsync(string userId, string role)
{
var user = await _unitOfWork.Users.GetByIdAsync(userId);
return user?.Roles.Any(r => r.Name == role) ?? false;
}
public async Task<IEnumerable<string>> GetPermissionsAsync(string userId)
{
var user = await _unitOfWork.Users.GetByIdAsync(userId);
if (user == null) return Enumerable.Empty<string>();
return user.Roles
.SelectMany(r => r.Permissions)
.Select(p => p.Name)
.Distinct();
}
}
public class AuditService : IAuditService
{
private readonly IUnitOfWork _unitOfWork;
public async Task LogAsync(AuditEntry entry)
{
entry.Timestamp = DateTime.UtcNow;
await _unitOfWork.AuditLogs.AddAsync(entry);
await _unitOfWork.SaveChangesAsync();
}
}
public record AuditEntry
{
public string Id { get; init; } = Guid.NewGuid().ToString();
public string UserId { get; init; } = string.Empty;
public string Action { get; init; } = string.Empty;
public string EntityType { get; init; } = string.Empty;
public string EntityId { get; init; } = string.Empty;
public string? OldValue { get; init; }
public string? NewValue { get; init; }
public DateTime Timestamp { get; set; }
}
Navigation Wiring Verification Guide
🚨 The Navigation Wiring Blind Spot
WinUI 3 Views with ViewModels often have navigation Effects that need View subscription:
public partial class SettingsViewModel : ObservableObject
{
public sealed class Effect
{
public record NavigateToAccountInfo;
public record NavigateToChangePassword;
public record NavigateToUserList;
}
public Subject<Effect> Fx { get; } = new();
private void GoToAccountInfo()
{
Fx.OnNext(new Effect.NavigateToAccountInfo());
}
}
Problem: If the View doesn't subscribe to Fx and handle the Effect by calling INavGraph, the button appears functional but does nothing when clicked!
Detection Patterns
grep -rn "record Navigate\|record Show\|record Open" src/**/ViewModels/*.cs
grep -rn "Fx.Subscribe\|_viewModel.Fx" src/**/Views/*.cs
grep -rn "void To[A-Z]" src/**/INavGraph.cs
grep -rn "public void To[A-Z]" src/**/NavGraph.cs
Verification Checklist
-
List Effect types in each ViewModel:
grep -h "record Navigate" src/Arcana.App/ViewModels/SettingsViewModel.cs
-
List Effect handlers in corresponding View:
grep -h "NavigateTo\|Effect.Navigate" src/Arcana.App/Views/SettingsPage.xaml.cs
-
Every Effect MUST have View handler AND NavGraph method! Any missing = dead button
Correct Wiring Example
public partial class SettingsViewModel : ObservableObject
{
public sealed class Effect
{
public record NavigateToAccountInfo;
public record NavigateToChangePassword;
public record NavigateToUserList;
}
public Subject<Effect> Fx { get; } = new();
public void GoToAccountInfo() => Fx.OnNext(new Effect.NavigateToAccountInfo());
public void GoToChangePassword() => Fx.OnNext(new Effect.NavigateToChangePassword());
public void GoToUserList() => Fx.OnNext(new Effect.NavigateToUserList());
}
public sealed partial class SettingsPage : Page
{
private readonly SettingsViewModel _viewModel;
private readonly INavGraph _navGraph;
private readonly IDisposable _effectSubscription;
public SettingsPage(SettingsViewModel viewModel, INavGraph navGraph)
{
_viewModel = viewModel;
_navGraph = navGraph;
InitializeComponent();
_effectSubscription = _viewModel.Fx.Subscribe(effect =>
{
switch (effect)
{
case SettingsViewModel.Effect.NavigateToAccountInfo:
_navGraph.ToAccountInfo();
break;
case SettingsViewModel.Effect.NavigateToChangePassword:
_navGraph.ToChangePassword();
break;
case SettingsViewModel.Effect.NavigateToUserList:
_navGraph.ToUserList();
break;
}
});
}
}
public interface INavGraph
{
void ToAccountInfo();
void ToChangePassword();
void ToUserList();
}
public class NavGraph : INavGraph
{
public void ToAccountInfo() => _frame.Navigate(typeof(AccountInfoPage));
public void ToChangePassword() => _frame.Navigate(typeof(ChangePasswordPage));
public void ToUserList() => _frame.Navigate(typeof(UserListPage));
}
Code Review Checklist
Required Items
Performance Checks
Security Checks
Plugin Checks
Common Issues
WinUI 3 Issues
- Ensure Windows App SDK is properly installed
- Check XAML compilation errors
- Verify resource dictionary merging
EF Core Issues
- Run migrations:
dotnet ef database update
- Check connection string configuration
- Review lazy loading settings
Plugin Loading Issues
- Verify AssemblyLoadContext configuration
- Check plugin dependencies
- Review manifest activation events
Tech Stack Reference
| Technology | Recommended Version |
|---|
| .NET | 10.0+ |
| C# | 14.0+ |
| WinUI 3 | 3.0+ |
| EF Core | 10.0+ |
| SQLite | Latest |
| xUnit | Latest |
| CommunityToolkit.Mvvm | Latest |