Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
This article shows how to apply proven architecture patterns to WinUI 3 desktop apps built with the Windows App SDK. You'll learn how to set up dependency injection, manage configuration, and structure code for enterprise line-of-business (LOB) scenarios.
Prerequisites
- Windows App SDK 1.5 or later
- .NET 8 or later
- Visual Studio 2022 version 17.10 or later with the .NET desktop development and Windows application development workloads
Dependency injection
WinUI 3 desktop apps don't include a built-in dependency injection (DI) container like ASP.NET Core does, but you can add one using the same Microsoft.Extensions.DependencyInjection NuGet package. DI makes your code testable, loosely coupled, and easier to maintain.
Set up a DI container
Install the NuGet package:
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Hosting
Configure the host and services in your App.xaml.cs:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.UI.Xaml;
public partial class App : Application
{
public IHost Host { get; }
public static T GetService<T>() where T : class
{
if ((App.Current as App)!.Host.Services.GetService(typeof(T)) is not T service)
{
throw new ArgumentException(
$"{typeof(T)} needs to be registered in ConfigureServices.");
}
return service;
}
public App()
{
InitializeComponent();
Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder()
.UseContentRoot(AppContext.BaseDirectory)
.ConfigureServices((context, services) =>
{
// Services
services.AddSingleton<INavigationService, NavigationService>();
services.AddSingleton<IDataService, DataService>();
services.AddTransient<IDialogService, DialogService>();
// ViewModels
services.AddTransient<MainViewModel>();
services.AddTransient<SettingsViewModel>();
// Views
services.AddTransient<MainPage>();
services.AddTransient<SettingsPage>();
})
.Build();
}
}
Note
If you forget to register a service, the GetService<T>() helper above throws an ArgumentException at runtime with the missing type name. Run the app and navigate to each page during development to verify all registrations are correct.
Inject dependencies into ViewModels
With the container configured, your ViewModels receive dependencies through constructor injection:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public partial class MainViewModel : ObservableObject
{
private readonly IDataService _dataService;
private readonly INavigationService _navigationService;
public MainViewModel(IDataService dataService, INavigationService navigationService)
{
_dataService = dataService;
_navigationService = navigationService;
}
[ObservableProperty]
private string _statusMessage = string.Empty;
[RelayCommand]
private async Task LoadDataAsync()
{
StatusMessage = "Loading...";
var items = await _dataService.GetItemsAsync();
StatusMessage = $"Loaded {items.Count} items";
}
}
Service lifetimes
Choose the appropriate lifetime when registering services:
| Lifetime | Method | Use for |
|---|---|---|
| Singleton | AddSingleton<T>() |
Navigation, app-wide state, caches |
| Scoped | AddScoped<T>() |
Per-window or per-dialog contexts |
| Transient | AddTransient<T>() |
ViewModels, stateless services |
Tip
Register ViewModels as Transient so each navigation creates a fresh instance. Register services that hold app-wide state as Singleton.
Configuration management
Use Microsoft.Extensions.Configuration to manage app settings in desktop apps, the same pattern used in ASP.NET Core.
Add configuration support
Install the required packages:
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Options
Create an appsettings.json file in your project root. In Solution Explorer, right-click the file, select Properties, and set Copy to Output Directory to Copy if newer. Alternatively, add <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> to the file's entry in your .csproj.
{
"AppSettings": {
"ApiBaseUrl": "https://api.contoso.com/v2",
"MaxRetryCount": 3,
"EnableTelemetry": true
},
"Logging": {
"LogLevel": "Information"
}
}
Bind the configuration in your DI setup:
.ConfigureServices((context, services) =>
{
// Bind settings to a strongly-typed class
services.Configure<AppSettings>(
context.Configuration.GetSection("AppSettings"));
// Inject IOptions<AppSettings> into services
services.AddSingleton<IApiClient, ApiClient>();
})
Use configuration in services
using Microsoft.Extensions.Options;
public class ApiClient : IApiClient
{
private readonly AppSettings _settings;
private readonly HttpClient _httpClient;
public ApiClient(IOptions<AppSettings> options)
{
_settings = options.Value;
_httpClient = new HttpClient
{
BaseAddress = new Uri(_settings.ApiBaseUrl)
};
}
}
User settings persistence
For per-user settings that survive app updates, use Windows.Storage.ApplicationData (packaged apps) or a local JSON file (unpackaged apps):
public class UserSettingsService : IUserSettingsService
{
private readonly string _settingsPath;
public UserSettingsService()
{
var localAppData = Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData);
_settingsPath = Path.Combine(localAppData, "Contoso", "MyApp", "settings.json");
}
public async Task SaveAsync<T>(string key, T value)
{
var settings = await LoadAllAsync();
settings[key] = JsonSerializer.Serialize(value);
Directory.CreateDirectory(Path.GetDirectoryName(_settingsPath)!);
await File.WriteAllTextAsync(
_settingsPath, JsonSerializer.Serialize(settings));
}
}
Note
Packaged apps (MSIX) can use ApplicationData.Current.LocalSettings for simple key-value pairs. Unpackaged apps must manage their own storage location.
Feature flags
Implement feature flags to enable gradual rollout and A/B testing without redeployment.
Local feature flags with configuration
public interface IFeatureFlagService
{
bool IsEnabled(string featureName);
}
public class FeatureFlagService : IFeatureFlagService
{
private readonly Dictionary<string, bool> _flags;
public FeatureFlagService(IConfiguration configuration)
{
_flags = configuration.GetSection("FeatureFlags")
.Get<Dictionary<string, bool>>() ?? new();
}
public bool IsEnabled(string featureName) =>
_flags.TryGetValue(featureName, out var enabled) && enabled;
}
Azure App Configuration integration
For cloud-managed feature flags, use Azure App Configuration:
dotnet add package Microsoft.Extensions.Configuration.AzureAppConfiguration
dotnet add package Microsoft.FeatureManagement
using Azure.Identity;
Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder()
.ConfigureAppConfiguration((context, config) =>
{
config.AddAzureAppConfiguration(options =>
{
options.Connect(
new Uri("https://<your-store>.azconfig.io"),
new DefaultAzureCredential())
.UseFeatureFlags(flagOptions =>
{
flagOptions.CacheExpirationInterval = TimeSpan.FromMinutes(5);
});
});
})
.ConfigureServices((context, services) =>
{
services.AddFeatureManagement(context.Configuration);
})
.Build();
Note
For local development, you can use a connection string instead of DefaultAzureCredential. Store the connection string in an environment variable or Windows Credential Manager — never in source control:
options.Connect(Environment.GetEnvironmentVariable("APP_CONFIG_CONNECTION_STRING"))
Tip
For Store-based gradual rollout at the package level, see Gradual package rollout.
Enterprise and LOB patterns
Line-of-business apps have additional requirements around identity, data protection, and device management.
Identity and conditional access
Use MSAL (Microsoft Authentication Library) for enterprise authentication:
services.AddSingleton<IAuthService>(sp =>
{
var app = PublicClientApplicationBuilder
.Create("your-client-id")
.WithAuthority(AzureCloudInstance.AzurePublic, "your-tenant-id")
.WithRedirectUri("http://localhost")
.Build();
return new AuthService(app);
});
Important
The http://localhost redirect URI is suitable for development. For production desktop apps, use the Windows broker (WAM) instead, which provides SSO with the user's Windows account and stronger token protection.
Enterprise apps deployed through Intune can enforce conditional access policies that require:
- Device compliance (encryption, PIN, OS version)
- Multi-factor authentication
- Network location restrictions
Offline data and caching
Desktop LOB apps frequently need to work offline. Implement a repository pattern with local caching:
public class CachedRepository<T> : IRepository<T> where T : class, IEntity
{
private readonly IApiClient _apiClient;
private readonly ILocalDatabase _localDb;
public async Task<IReadOnlyList<T>> GetAllAsync(bool forceRefresh = false)
{
if (!forceRefresh)
{
var cached = await _localDb.GetAllAsync<T>();
if (cached.Any())
return cached;
}
try
{
var items = await _apiClient.GetAsync<List<T>>();
await _localDb.UpsertAllAsync(items);
return items;
}
catch (HttpRequestException)
{
// Offline fallback
return await _localDb.GetAllAsync<T>();
}
}
}
Data protection
Use Windows.Security.Cryptography.DataProtection (packaged apps) or the .NET DataProtectionProvider for encrypting sensitive local data.
Install the required package:
dotnet add package Microsoft.AspNetCore.DataProtection.Extensions
Then register data protection in your DI container:
using Microsoft.AspNetCore.DataProtection;
services.AddDataProtection()
.SetApplicationName("Contoso.LOBApp")
.ProtectKeysWithDpapi();
Layered architecture
Structure your WinUI 3 app in layers to keep dependencies flowing in one direction:
┌─────────────────────────────┐
│ Views (XAML + code-behind)│ ← UI layer, no business logic
├─────────────────────────────┤
│ ViewModels (MVVM Toolkit) │ ← Presentation logic, commands
├─────────────────────────────┤
│ Services / Use Cases │ ← Business rules, orchestration
├─────────────────────────────┤
│ Repositories / Data │ ← Data access, API clients, caching
└─────────────────────────────┘
Rules:
- Each layer depends only on the layer directly below it.
- ViewModels never reference UI types (
Page,Window,ContentDialog). - Services define interfaces; implementations live in the data layer.
- Register all cross-layer dependencies in the DI container.
Backward compatibility and versioning
When you release new versions of your app, consider:
- Data migration: Version your local database schema. Use a migration runner at startup to upgrade from any previous schema to the current one.
- Settings migration: Store a schema version in your settings file. On load, apply transforms from old formats to new.
- Side-by-side installs: MSIX upgrades a package in place by default. To run multiple major versions side by side, assign each version a distinct package family name at design time.
public class DatabaseMigrator
{
public async Task MigrateAsync(SqliteConnection db)
{
var currentVersion = await GetSchemaVersionAsync(db);
if (currentVersion < 2)
await ApplyMigration_v2(db);
if (currentVersion < 3)
await ApplyMigration_v3(db);
await SetSchemaVersionAsync(db, LatestVersion);
}
}
Verify your setup
Run the app and navigate to each page to verify services resolve correctly. If a service is not registered, you will see an InvalidOperationException at runtime with the missing type name. Also confirm that:
- Configuration values load from
appsettings.json(check a bound property in the debugger). - Feature flags evaluate as expected (toggle a flag and restart).
- Offline caching returns data when the network is unavailable.
Related content
Windows developer