This document describes the architecture and design patterns used in the AdGuard.ConsoleUI application.
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.
┌─────────────────────────────────────────────────────────────┐
│ 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 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
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
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
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
The entry point of the application responsible for:
- Building configuration from multiple sources
- Configuring the DI container
- Setting up logging
- Running the application
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
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
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 |
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ appsettings.json│ ─► │ ConfigurationBuilder│ ─► │ IConfiguration │
└─────────────────┘ │ │ │ │
│ AddJsonFile() │ │ ["AdGuard:ApiKey"]
┌─────────────────┐ │ AddEnvironment() │ │ │
│ Environment Vars│ ─► │ │ └─────────────────┘
│ ADGUARD_* │ └──────────────────┘ │
└─────────────────┘ ▼
┌─────────────────┐
│ ApiClientFactory │
│ │
│ ConfigureFrom() │
└─────────────────┘
The application uses Spectre.Console for rich terminal UI:
| 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 |
// 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[/]");All menu services catch and display API exceptions:
try
{
// API operation
}
catch (ApiException ex)
{
AnsiConsole.MarkupLine($"[red]API Error ({ex.ErrorCode}): {ex.Message}[/]");
}The ApiClientFactory.TestConnectionAsync() method handles authentication:
catch (ApiException ex) when (ex.ErrorCode == 401)
{
AnsiConsole.MarkupLine("[red]Authentication failed. Invalid API key.[/]");
return false;
}The main menu loop catches and displays unexpected exceptions:
catch (Exception ex)
{
AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything);
}Focus on testing:
ApiClientFactoryconfiguration and validation- DI container configuration
- Service registration and resolution
For testing:
- Configuration loading from various sources
- Service dependency chains
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
- 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;- 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();- 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