structuring-wpf-projects

WPF Solution and Project Structure

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "structuring-wpf-projects" with this command: npx skills add christian289/dotnet-with-claudecode/christian289-dotnet-with-claudecode-structuring-wpf-projects

WPF Solution and Project Structure

A guide for designing WPF project solution and project structure based on Clean Architecture.

Template Project

The templates folder contains a Clean Architecture WPF solution example (use latest .NET per version mapping).

templates/ ├── GameDataTool.sln ← Solution file ├── Directory.Build.props ← Common build settings ├── src/ │ ├── GameDataTool.Domain/ ← 🔵 Core - Pure domain models │ ├── GameDataTool.Application/ ← 🟢 Core - Use Cases │ ├── GameDataTool.Infrastructure/ ← 🟡 Infrastructure - External systems │ ├── GameDataTool.ViewModels/ ← 🟠 Presentation - ViewModel │ ├── GameDataTool.WpfServices/ ← 🟠 Presentation - WPF services │ ├── GameDataTool.UI/ ← 🔴 Presentation - Custom Controls │ └── GameDataTool.WpfApp/ ← 🔴 Composition Root └── tests/ ├── GameDataTool.Domain.Tests/ ├── GameDataTool.Application.Tests/ └── GameDataTool.ViewModels.Tests/

Solution Structure Principles

Solution name is the application name

  • Example: GameDataTool solution = executable .NET Assembly name

Project Structure (Clean Architecture)

SolutionName/ ├── src/ │ │ │ │ ══════════════ Core (No Dependencies) ══════════════ │ │ │ ├── SolutionName.Domain // 🔵 Entities - Pure domain models │ │ ├── Entities/ │ │ ├── ValueObjects/ │ │ └── Interfaces/ // - Domain service interfaces only │ │ │ ├── SolutionName.Application // 🟢 Use Cases - Business logic coordination │ │ ├── Interfaces/ // - IRepository, IExternalService, etc. │ │ ├── Services/ // - Application services │ │ └── DTOs/ │ │ │ │ ══════════════ Infrastructure ══════════════ │ │ │ ├── SolutionName.Infrastructure // 🟡 External system implementation │ │ ├── Persistence/ // - Data access implementation │ │ ├── FileSystem/ // - File system access │ │ └── ExternalServices/ // - HTTP, API, etc. │ │ │ │ ══════════════ Presentation (WPF) ══════════════ │ │ │ ├── SolutionName.ViewModels // 🟠 ViewModels (Interface Adapter role) │ │ └── (Depends on Application only) │ │ │ ├── SolutionName.WpfServices // 🟠 WPF-specific services │ │ └── (Dialog, Navigation, etc.) │ │ │ ├── SolutionName.UI // 🔴 Custom Controls & Styles │ │ │ └── SolutionName.WpfApp // 🔴 Composition Root (Entry point) │ └── App.xaml.cs // - DI setup, connect all implementations │ └── tests/ ├── SolutionName.Domain.Tests ├── SolutionName.Application.Tests └── SolutionName.ViewModels.Tests

Roles by Project Type

Core Layer (No Dependencies)

Project Role Contents

.Domain

🔵 Entities Pure domain models, ValueObjects, domain interfaces

.Application

🟢 Use Cases Business logic coordination, IRepository/IService interfaces, DTOs

Infrastructure Layer

Project Role Contents

.Infrastructure

🟡 External Systems Repository implementation, file system, HTTP/API clients

Presentation Layer (WPF)

Project Role Contents

.ViewModels

🟠 Interface Adapter MVVM ViewModel (depends on Application only, no WPF reference)

.WpfServices

🟠 WPF Services DialogService, NavigationService, WindowService

.UI

🔴 Custom Controls ResourceDictionary, CustomControl, Themes

.WpfApp

🔴 Composition Root App.xaml, DI setup, Views, entry point

Project Dependency Hierarchy

                ┌─────────────────────────────────────┐
                │         SolutionName.WpfApp         │  ← Composition Root
                │   (References all projects, DI setup)   │
                └─────────────────────────────────────┘
                                  │
      ┌───────────────────────────┼───────────────────────────┐
      ▼                           ▼                           ▼

┌─────────────────┐ ┌─────────────────────┐ ┌─────────────────┐ │ .ViewModels │ │ .Infrastructure │ │ .WpfServices │ │ (Application │ │ (Application ref) │ │ (Application │ │ ref) │ │ │ │ ref) │ └────────┬────────┘ └──────────┬──────────┘ └────────┬────────┘ │ │ │ └───────────────────────────┼───────────────────────────┘ ▼ ┌─────────────────────────────────────┐ │ SolutionName.Application │ ← Use Cases │ (Domain ref only) │ └─────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────┐ │ SolutionName.Domain │ ← Core (No dependencies) │ (No references) │ └─────────────────────────────────────┘

Detailed Layer Descriptions

Domain Layer (Pure Domain)

// Domain/Entities/User.cs namespace GameDataTool.Domain.Entities;

public sealed class User { public Guid Id { get; init; } public string Name { get; private set; } = string.Empty; public Email Email { get; private set; } = null!;

public void UpdateName(string name)
{
    // Domain business rule validation
    if (string.IsNullOrWhiteSpace(name))
        throw new DomainException("Name is required.");

    Name = name;
}

}

// Domain/ValueObjects/Email.cs namespace GameDataTool.Domain.ValueObjects;

public sealed record Email { public string Value { get; }

public Email(string value)
{
    if (!IsValid(value))
        throw new DomainException("Invalid email format.");

    Value = value;
}

private static bool IsValid(string email) =>
    !string.IsNullOrWhiteSpace(email) && email.Contains('@');

}

Application Layer (Use Cases)

// Application/Interfaces/IUserRepository.cs namespace GameDataTool.Application.Interfaces;

public interface IUserRepository { Task<User?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default); Task<IReadOnlyList<User>> GetAllAsync(CancellationToken cancellationToken = default); Task AddAsync(User user, CancellationToken cancellationToken = default); Task UpdateAsync(User user, CancellationToken cancellationToken = default); }

// Application/Services/UserService.cs namespace GameDataTool.Application.Services;

public sealed class UserService(IUserRepository userRepository) { private readonly IUserRepository _userRepository = userRepository;

public async Task&#x3C;UserDto?> GetUserAsync(Guid id, CancellationToken cancellationToken = default)
{
    var user = await _userRepository.GetByIdAsync(id, cancellationToken);
    return user is null ? null : new UserDto(user.Id, user.Name, user.Email.Value);
}

public async Task UpdateUserNameAsync(Guid id, string newName, CancellationToken cancellationToken = default)
{
    var user = await _userRepository.GetByIdAsync(id, cancellationToken)
        ?? throw new NotFoundException("User not found.");

    user.UpdateName(newName);
    await _userRepository.UpdateAsync(user, cancellationToken);
}

}

// Application/DTOs/UserDto.cs namespace GameDataTool.Application.DTOs;

public sealed record UserDto(Guid Id, string Name, string Email);

Infrastructure Layer (External System Implementation)

// Infrastructure/Persistence/UserRepository.cs namespace GameDataTool.Infrastructure.Persistence;

public sealed class UserRepository(AppDbContext dbContext) : IUserRepository { private readonly AppDbContext _dbContext = dbContext;

public async Task&#x3C;User?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default) =>
    await _dbContext.Users.FindAsync([id], cancellationToken);

public async Task&#x3C;IReadOnlyList&#x3C;User>> GetAllAsync(CancellationToken cancellationToken = default) =>
    await _dbContext.Users.ToListAsync(cancellationToken);

public async Task AddAsync(User user, CancellationToken cancellationToken = default)
{
    await _dbContext.Users.AddAsync(user, cancellationToken);
    await _dbContext.SaveChangesAsync(cancellationToken);
}

public async Task UpdateAsync(User user, CancellationToken cancellationToken = default)
{
    _dbContext.Users.Update(user);
    await _dbContext.SaveChangesAsync(cancellationToken);
}

}

ViewModels Layer (Presentation - Depends on Application Only)

// ViewModels/UserViewModel.cs namespace GameDataTool.ViewModels;

public sealed partial class UserViewModel(UserService userService) : ObservableObject { private readonly UserService _userService = userService;

[ObservableProperty] private string _userName = string.Empty;
[ObservableProperty] private string _userEmail = string.Empty;

[RelayCommand]
private async Task LoadUserAsync(Guid userId)
{
    var user = await _userService.GetUserAsync(userId);
    if (user is null) return;

    UserName = user.Name;
    UserEmail = user.Email;
}

}

WpfApp Layer (Composition Root - DI Setup)

// WpfApp/App.xaml.cs namespace GameDataTool.WpfApp;

public partial class App : Application { private readonly IHost _host;

public App()
{
    _host = Host.CreateDefaultBuilder()
        .ConfigureServices((context, services) =>
        {
            // Domain - No registration needed (pure models)

            // Application Layer
            services.AddTransient&#x3C;UserService>();

            // Infrastructure Layer
            services.AddDbContext&#x3C;AppDbContext>();
            services.AddScoped&#x3C;IUserRepository, UserRepository>();

            // Presentation Layer
            services.AddTransient&#x3C;UserViewModel>();
            services.AddTransient&#x3C;MainViewModel>();

            // WPF Services
            services.AddSingleton&#x3C;IDialogService, DialogService>();
            services.AddSingleton&#x3C;INavigationService, NavigationService>();

            // Views
            services.AddSingleton&#x3C;MainWindow>();
        })
        .Build();
}

protected override async void OnStartup(StartupEventArgs e)
{
    await _host.StartAsync();
    _host.Services.GetRequiredService&#x3C;MainWindow>().Show();
    base.OnStartup(e);
}

protected override async void OnExit(ExitEventArgs e)
{
    using (_host)
    {
        await _host.StopAsync();
    }
    base.OnExit(e);
}

}

Actual Folder Structure Example

GameDataTool/ ├── src/ │ ├── GameDataTool.Domain/ │ │ ├── Entities/ │ │ │ ├── User.cs │ │ │ └── GameData.cs │ │ ├── ValueObjects/ │ │ │ ├── Email.cs │ │ │ └── GameVersion.cs │ │ ├── Interfaces/ │ │ │ └── IDomainEventPublisher.cs │ │ ├── Exceptions/ │ │ │ └── DomainException.cs │ │ ├── GlobalUsings.cs │ │ └── GameDataTool.Domain.csproj │ │ │ ├── GameDataTool.Application/ │ │ ├── Interfaces/ │ │ │ ├── IUserRepository.cs │ │ │ ├── IGameDataRepository.cs │ │ │ └── IFileExportService.cs │ │ ├── Services/ │ │ │ ├── UserService.cs │ │ │ └── GameDataService.cs │ │ ├── DTOs/ │ │ │ ├── UserDto.cs │ │ │ └── GameDataDto.cs │ │ ├── Exceptions/ │ │ │ └── NotFoundException.cs │ │ ├── GlobalUsings.cs │ │ └── GameDataTool.Application.csproj │ │ │ ├── GameDataTool.Infrastructure/ │ │ ├── Persistence/ │ │ │ ├── AppDbContext.cs │ │ │ ├── UserRepository.cs │ │ │ └── GameDataRepository.cs │ │ ├── FileSystem/ │ │ │ └── ExcelExportService.cs │ │ ├── ExternalServices/ │ │ │ └── ApiClient.cs │ │ ├── GlobalUsings.cs │ │ └── GameDataTool.Infrastructure.csproj │ │ │ ├── GameDataTool.ViewModels/ │ │ ├── MainViewModel.cs │ │ ├── UserViewModel.cs │ │ ├── GameDataViewModel.cs │ │ ├── GlobalUsings.cs │ │ └── GameDataTool.ViewModels.csproj │ │ │ ├── GameDataTool.WpfServices/ │ │ ├── DialogService.cs │ │ ├── NavigationService.cs │ │ ├── WindowService.cs │ │ ├── GlobalUsings.cs │ │ └── GameDataTool.WpfServices.csproj │ │ │ ├── GameDataTool.UI/ │ │ ├── Themes/ │ │ │ ├── Generic.xaml │ │ │ └── CustomButton.xaml │ │ ├── CustomControls/ │ │ │ └── CustomButton.cs │ │ ├── Properties/ │ │ │ └── AssemblyInfo.cs │ │ └── GameDataTool.UI.csproj │ │ │ └── GameDataTool.WpfApp/ │ ├── Views/ │ │ ├── MainWindow.xaml │ │ ├── MainWindow.xaml.cs │ │ ├── UserView.xaml │ │ └── UserView.xaml.cs │ ├── App.xaml │ ├── App.xaml.cs │ ├── Mappings.xaml │ ├── GlobalUsings.cs │ └── GameDataTool.WpfApp.csproj │ ├── tests/ │ ├── GameDataTool.Domain.Tests/ │ │ ├── Entities/ │ │ │ └── UserTests.cs │ │ └── GameDataTool.Domain.Tests.csproj │ │ │ ├── GameDataTool.Application.Tests/ │ │ ├── Services/ │ │ │ └── UserServiceTests.cs │ │ └── GameDataTool.Application.Tests.csproj │ │ │ └── GameDataTool.ViewModels.Tests/ │ ├── UserViewModelTests.cs │ └── GameDataTool.ViewModels.Tests.csproj │ ├── GameDataTool.sln └── Directory.Build.props

Assembly Reference Rules

Domain Project

  • ❌ Does not reference any project

  • ✅ Uses pure C# BCL only

Application Project

  • ✅ References Domain only

  • ❌ Cannot reference Infrastructure, Presentation

Infrastructure Project

  • ✅ References Domain, Application

  • ✅ Can use external NuGet packages (EF Core, HttpClient, etc.)

ViewModels Project

  • ✅ References Application only

  • ❌ Cannot reference WPF assemblies (WindowsBase, PresentationFramework, etc.)

  • ✅ Can use CommunityToolkit.Mvvm

WpfApp Project (Composition Root)

  • ✅ References all projects

  • ✅ Connects all implementations in DI container

Clean Architecture Advantages

  • Independence: Core layer is independent from external frameworks

  • Testability: Each layer can be tested independently

  • Maintainability: Clear scope of change impact

  • Flexibility: Easy to replace Infrastructure (DB, API, etc.)

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

converting-html-css-to-wpf-xaml

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

publishing-wpf-apps

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

using-xaml-property-element-syntax

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

managing-styles-resourcedictionary

No summary provided by upstream source.

Repository SourceNeeds Review