Architecture
This document explains how DB TEAM is organized and why.
Layers
┌────────────────────────────────────────────────────────┐
│ DBTeam.App (WPF) │
│ Shell · DI bootstrap · Theme · Localization · Menu │
└────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌──────────────────────────┐ ┌────────────────────────┐
│ Modules.* │ │ DBTeam.UI │
│ ConnectionManager │ │ Shared WPF controls │
│ ObjectExplorer │ │ │
│ QueryEditor │ └────────────────────────┘
│ SchemaCompare / ... │
└──────────────────────────┘
│
▼
┌──────────────────────────┐ ┌────────────────────────┐
│ DBTeam.Data │ │ DBTeam.Core │
│ SqlServerConnection… │◀────│ Models · Abstractions │
│ SqlServerMetadata… │ │ IEventBus · Events │
│ SqlServerQueryExec… │ │ ServiceLocator │
│ DpapiProtector │ │ │
│ JsonConnectionStore │ └────────────────────────┘
└──────────────────────────┘
Dependency direction: App → Modules → Core/Data/UI. Modules never reference each other directly — they communicate through IEventBus.
Composition
The app’s entry point is App.OnStartup:
Services = new ServiceCollection()
.AddSingleton<IConnectionService, SqlServerConnectionService>()
.AddSingleton<IDatabaseMetadataService, SqlServerMetadataService>()
.AddSingleton<IQueryExecutionService, SqlServerQueryExecutionService>()
.AddSingleton<IEventBus, EventBus>()
// ...
.BuildServiceProvider();
ServiceLocator.Services = Services;
Each module exposes a ModuleRegistration.Register(IServiceCollection) that adds its ViewModels and Views to the container. The shell calls them all from ConfigureServices.
Event bus
Cross-module events are defined in DBTeam.Core/Events/AppEvents.cs:
| Event | Published by | Consumed by |
|---|---|---|
ConnectionOpenedEvent |
ConnectionDialog, ConnectionsPanel | ObjectExplorer, MainViewModel, Shell |
ConnectionsChangedEvent |
ConnectionDialog (after save) | ConnectionsPanel |
OpenQueryEditorRequest |
ObjectExplorer, Schema/Data Compare, Table Designer, Data Generator | Shell |
OpenDocumentRequest |
MainViewModel (menu commands) | Shell |
ShowPaneRequest |
MainViewModel (View menu) | Shell |
The shell subscribes in MainWindow.xaml.cs and manipulates AvalonDock accordingly.
Data flow: “Execute a query”
- User types SQL in
QueryEditorView→TextChangedpushes toQueryEditorViewModel.Sql - User presses F5 →
ExecuteCommand - VM calls
IQueryExecutionService.ExecuteAsync(connection, request) SqlServerQueryExecutionServiceopens aSqlConnection, runsExecuteReaderAsync, loads each result set into aDataTable- VM updates
Results,Messages,Elapsed,RowCount,StatusText - View reacts via data binding (ItemsControl over
Results)
Data flow: “Autocomplete”
- User types letter →
TextArea.TextEntered QueryEditorView.ShowCompletion()instantiatesCompletionWindow- Items populated from
SqlCompletionProvider— keywords + live tables (viaIDatabaseMetadataService.GetTablesAsync) cached per(connectionId, database) - After a
., the provider extracts the identifier and fetches columns fromIDatabaseMetadataService.GetColumnsAsync Enter/Tabcommits selection, handled by AvalonEdit’s completion engine
Connection persistence
- Model:
SqlConnectionInfo(Id, Name, Server, Database, AuthMode, User, Password, …) - Serialized to
%AppData%\DBTeam\connections.json Passwordis encrypted withISecretProtector(DPAPI-backedDpapiProtector) before write, decrypted on loadJsonConnectionStorehides the format — swap it to put the store in an encrypted vault / SQLite later
Theme
ThemeServicetogglesModernWpf.ThemeManager.Current.ApplicationTheme- Persisted in
%AppData%\DBTeam\theme.json - Views use
{DynamicResource SystemControl*}brushes from ModernWpf — they react to theme switch without reload
Localization
- One
ResourceDictionaryper language insrc/DBTeam.App/Lang/xx-YY.xamlwith<sys:String x:Key="..."> LocalizationService.SetLanguage("fr-FR")removes the previous merged dictionary and adds the new one via pack URI- Bindings use
{DynamicResource Key}so they live-update
Extension points
Adding a new module (example: “Database Backup”):
// src/Modules/DBTeam.Modules.Backup/ModuleRegistration.cs
public static void Register(IServiceCollection s)
{
s.AddTransient<BackupViewModel>();
s.AddTransient<BackupView>();
}
// src/DBTeam.App/App.xaml.cs
DBTeam.Modules.Backup.ModuleRegistration.Register(s);
// src/DBTeam.App/ViewModels/MainViewModel.cs
[RelayCommand]
private void Backup()
{
var view = App.Services.GetRequiredService<DBTeam.Modules.Backup.Views.BackupView>();
var bus = App.Services.GetRequiredService<IEventBus>();
bus.Publish(new OpenDocumentRequest { Title = "Backup", Content = view });
}
<!-- src/DBTeam.App/Shell/MainWindow.xaml -->
<MenuItem Header="{DynamicResource Menu.Backup}" Command="{Binding BackupCommand}">
<MenuItem.Icon><md:PackIcon Kind="BackupRestore"/></MenuItem.Icon>
</MenuItem>
Done — the module appears as a dockable document tab with full access to connections, metadata, and the event bus.