Documentation
AdGuard ConsoleUI Architecture
This document describes the architecture and design patterns used in the AdGuard.ConsoleUI application.
Overview
AdGuard.ConsoleUI is a menu-driven console application that provides a user-friendly interface for the AdGuard DNS API. It follows a service-oriented architecture with dependency injection for loose coupling and testability.
Architecture Diagram
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Program.cs β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Main Entry Point β β
β β - BuildConfiguration() β β
β β - ConfigureServices() β β
β β - Runs ConsoleApplication β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ConsoleApplication β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β - Displays welcome banner β β
β β - Handles API key configuration β β
β β - Main menu loop β β
β β - Routes to menu services β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββΌββββββββββββββββ
β β β
βΌ βΌ βΌ
ββββββββββββββββββββ βββββββββββββββββ ββββββββββββββββββββ
β DeviceMenuServiceβ βDnsServerMenu β βStatisticsMenu β
β β βService β βService β
β - List devices β β β β β
β - View details β β - List serversβ β - 24h stats β
β - Create device β β - View detailsβ β - 7d stats β
β - Delete device β β - Create β β - 30d stats β
β β β - Delete β β - Custom range β
ββββββββββββββββββββ βββββββββββββββββ ββββββββββββββββββββ
β β β
βββββββββββββββββΌββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ApiClientFactory β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β - Manages API configuration β β
β β - Creates API client instances β β
β β - Tests API connectivity β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β AdGuard.ApiClient β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β - AccountApi - DevicesApi β β
β β - DNSServersApi - StatisticsApi β β
β β - QueryLogApi - FilterListsApi β β
β β - WebServicesApi - DedicatedIPAddressesApi β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Design Patterns
1. Dependency Injection (DI)
All services are registered in the DI container and injected via constructors:
// Registration in Program.cs
services.AddSingleton<ApiClientFactory>();
services.AddSingleton<ConsoleApplication>();
services.AddSingleton<DeviceMenuService>();
// Injection in ConsoleApplication.cs
public ConsoleApplication(
ApiClientFactory apiClientFactory,
DeviceMenuService deviceMenu,
DnsServerMenuService dnsServerMenu,
// ... other services
)
Benefits:
- Loose coupling between components
- Easy unit testing with mocks
- Centralized service configuration
2. Factory Pattern
ApiClientFactory implements the Factory pattern for creating API client instances:
public class ApiClientFactory
{
public AccountApi CreateAccountApi()
{
return new AccountApi(GetConfiguration());
}
public DevicesApi CreateDevicesApi()
{
return new DevicesApi(GetConfiguration());
}
// ... other factory methods
}
Benefits:
- Centralized API client creation
- Consistent configuration across all clients
- Easy to modify client creation logic
3. Service Pattern
Each menu service encapsulates a specific domain area:
public class DeviceMenuService
{
private readonly ApiClientFactory _apiClientFactory;
public DeviceMenuService(ApiClientFactory apiClientFactory)
{
_apiClientFactory = apiClientFactory;
}
public async Task ShowAsync()
{
// Menu loop implementation
}
}
Benefits:
- Single Responsibility Principle
- Reusable components
- Easy to extend with new features
Component Descriptions
Program.cs
The entry point of the application responsible for:
- Building configuration from multiple sources
- Configuring the DI container
- Setting up logging
- Running the application
ConsoleApplication
The main application orchestrator that:
- Displays the welcome banner
- Handles initial API key configuration
- Manages the main menu loop
- Routes user selections to appropriate menu services
ApiClientFactory
Central factory for API operations:
- Stores and manages API configuration
- Creates configured API client instances
- Provides connection testing functionality
- Supports both settings-based and manual configuration
Menu Services
Each service handles a specific domain:
| Service | Responsibility |
|---|---|
| DeviceMenuService | Device CRUD operations |
| DnsServerMenuService | DNS server CRUD operations |
| StatisticsMenuService | Statistics retrieval and display |
| QueryLogMenuService | Query log viewing and clearing |
| AccountMenuService | Account limits display |
| FilterListMenuService | Filter list display |
Configuration Flow
βββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ
β appsettings.jsonβ ββΊ β ConfigurationBuilderβ ββΊ β IConfiguration β
βββββββββββββββββββ β β β β
β AddJsonFile() β β ["AdGuard:ApiKey"]
βββββββββββββββββββ β AddEnvironment() β β β
β Environment Varsβ ββΊ β β βββββββββββββββββββ
β ADGUARD_* β ββββββββββββββββββββ β
βββββββββββββββββββ βΌ
βββββββββββββββββββ
β ApiClientFactory β
β β
β ConfigureFrom() β
βββββββββββββββββββ
UI Library: Spectre.Console
The application uses Spectre.Console for rich terminal UI:
Key Components Used
| Component | Usage |
|---|---|
FigletText |
Welcome banner |
SelectionPrompt |
Interactive menus |
TextPrompt |
User input (including secrets) |
Table |
Data display |
Panel |
Detail views |
Rule |
Section separators |
Status |
Loading indicators |
Markup |
Colored text |
Example Usage
// Interactive menu
var choice = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("[green]Main Menu[/]")
.AddChoices(new[] { "Option 1", "Option 2", "Exit" }));
// Loading indicator
var result = await AnsiConsole.Status()
.StartAsync("Loading...", async ctx =>
{
return await api.GetDataAsync();
});
// Table display
var table = new Table()
.Border(TableBorder.Rounded)
.AddColumn("[green]ID[/]")
.AddColumn("[green]Name[/]");
Error Handling Strategy
API Exceptions
All menu services catch and display API exceptions:
try
{
// API operation
}
catch (ApiException ex)
{
AnsiConsole.MarkupLine($"[red]API Error ({ex.ErrorCode}): {ex.Message}[/]");
}
Authentication Errors
The ApiClientFactory.TestConnectionAsync() method handles authentication:
catch (ApiException ex) when (ex.ErrorCode == 401)
{
AnsiConsole.MarkupLine("[red]Authentication failed. Invalid API key.[/]");
return false;
}
General Exceptions
The main menu loop catches and displays unexpected exceptions:
catch (Exception ex)
{
AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything);
}
Testing Strategy
Unit Tests
Focus on testing:
ApiClientFactoryconfiguration and validation- DI container configuration
- Service registration and resolution
Integration Tests
For testing:
- Configuration loading from various sources
- Service dependency chains
Why Some Components Are Not Unit Tested
Menu services heavily depend on AnsiConsole for:
- User input prompts
- Console output
- Interactive selections
These console I/O operations make pure unit testing impractical. Instead:
- Business logic is centralized in
ApiClientFactory - Menu services are thin wrappers around API calls
- Integration testing covers end-to-end scenarios
Extending the Application
Adding a New Menu Service
- Create a new service class:
public class NewMenuService
{
private readonly ApiClientFactory _apiClientFactory;
public NewMenuService(ApiClientFactory apiClientFactory)
{
_apiClientFactory = apiClientFactory;
}
public async Task ShowAsync()
{
// Implementation
}
}
- Register in DI container:
services.AddSingleton<NewMenuService>();
- Inject into
ConsoleApplication:
public ConsoleApplication(
// ... existing services
NewMenuService newMenu)
{
_newMenu = newMenu;
}
- Add menu option in
MainMenuLoopAsync():
case "New Feature":
await _newMenu.ShowAsync();
break;
Adding a New API Client Type
- Add factory method to
ApiClientFactory:
public NewApi CreateNewApi()
{
_logger.LogDebug("Creating NewApi instance");
return new NewApi(GetConfiguration());
}
- Use in menu service:
using var api = _apiClientFactory.CreateNewApi();
var result = await api.OperationAsync();
Best Practices
- Use
usingstatements for API clients to ensure proper disposal - Wrap long operations with
AnsiConsole.Status()for loading feedback - Escape user content with
Markup.Escape()before display - Handle API exceptions gracefully with user-friendly messages
- Keep menu services focused on a single domain area