Database command execution framework for .NET applications
The Syrx.Commanders.Databases ecosystem provides a comprehensive, high-performance database access framework built on top of Dapper. It enables type-safe, configuration-driven database operations with support for multiple database providers, connection management, and command resolution.
- Architecture Overview
- Core Concepts
- Package Ecosystem
- Getting Started
- Configuration
- Advanced Topics
- Performance Considerations
- Migration Guide
- API Reference
- Examples
The Syrx framework follows a layered architecture that separates concerns between application code, command resolution, connection management, and database execution:
┌─────────────────────────────────────────────────────────────┐
│ Application Layer │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Repository │ │ Repository │ ... │
│ │ Class │ │ Class │ │
│ └─────────────────┘ └─────────────────┘ │
└─────────────────┬───────────────┬───────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ Commander Layer │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ ICommander<TRepository> ││
│ │ ││
│ │ ┌─────────────────────────────────────────────────────┐││
│ │ │ DatabaseCommander<TRepository> │││
│ │ └─────────────────────────────────────────────────────┘││
│ └─────────────────────────────────────────────────────────┘│
└─────────────────┬───────────────┬───────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ Configuration Layer │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Command Settings │ │ Connection │ │
│ │ Resolution │ │ Management │ │
│ └──────────────────┘ └──────────────────┘ │
└─────────────────┬───────────────┬───────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ Database Layer │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Dapper │ │ ADO.NET │ │
│ │ (Micro-ORM) │ │ Connections │ │
│ └──────────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
- Separation of Concerns: Each layer has distinct responsibilities
- Configuration-Driven: SQL commands are externalized from code
- Type Safety: Strong typing throughout the execution pipeline
- Performance First: Built on Dapper for optimal database performance
- Provider Agnostic: Support for multiple database providers
- Dependency Injection: First-class DI container support
Repositories define business operations that translate to database commands:
public class UserRepository
{
private readonly ICommander<UserRepository> _commander;
public UserRepository(ICommander<UserRepository> commander)
{
_commander = commander;
}
// Method name automatically maps to configured command
public async Task<User> RetrieveAsync(int id, CancellationToken cancellationToken = default)
{
var result = await _commander.QueryAsync<User>(new { id }, cancellationToken);
return result.FirstOrDefault();
}
}Commands are resolved using the pattern: {Namespace}.{ClassName}.{MethodName}
For the example above:
- Namespace:
MyApp.Repositories - Class:
UserRepository - Method:
RetrieveAsync - Resolved Command:
MyApp.Repositories.UserRepository.RetrieveAsync
Named connection strings are resolved by alias, enabling environment-specific configuration:
{
"Connections": [
{
"Alias": "Primary",
"ConnectionString": "Server=prod-db;Database=MyApp;..."
},
{
"Alias": "ReadOnly",
"ConnectionString": "Server=readonly-db;Database=MyApp;..."
}
]
}- Query Operations: Execute without transactions (read-only)
- Execute Operations: Automatically wrapped in transactions with rollback on failure
The Syrx.Commanders.Databases ecosystem consists of several interconnected packages:
| Package | Purpose | Dependencies |
|---|---|---|
| Syrx.Commanders.Databases | Core command execution engine | Syrx, Dapper |
| Syrx.Commanders.Databases.Connectors | Database connection abstractions | Syrx.Connectors |
| Syrx.Commanders.Databases.Settings | Configuration model definitions | Syrx.Settings |
| Package | Purpose | Use Case |
|---|---|---|
| Syrx.Commanders.Databases.Extensions | DI container registration | Service registration |
| Syrx.Commanders.Databases.Connectors.Extensions | Connector DI extensions | Connector registration |
| Package | Purpose | Configuration Format |
|---|---|---|
| Syrx.Commanders.Databases.Settings.Extensions | Recommended builder pattern APIs | Programmatic |
| Syrx.Commanders.Databases.Settings.Extensions.Json | Optional JSON configuration loader | JSON files |
| Syrx.Commanders.Databases.Settings.Extensions.Xml | Optional XML configuration loader | XML files |
| Package | Purpose | Use Case |
|---|---|---|
| Syrx.Commanders.Databases.Builders | Schema modeling utilities | Code generation, tooling |
| Syrx.Commanders.Databases.Settings.Readers | Internal command resolution | Framework internals |
| Syrx.Commanders.Databases.Settings.Readers.Extensions | Reader DI extensions | Internal DI registration |
Install the core package and the recommended builder configuration package:
# Core framework
dotnet add package Syrx.Commanders.Databases
# Recommended: builder-based configuration
dotnet add package Syrx.Commanders.Databases.Settings.Extensions
# Optional: file-based configuration loaders
dotnet add package Syrx.Commanders.Databases.Settings.Extensions.Json
dotnet add package Syrx.Commanders.Databases.Settings.Extensions.Xml
# Service registration extensions
dotnet add package Syrx.Commanders.Databases.ExtensionsCreate configuration with the fluent builder API:
var settings = new CommanderSettingsBuilder()
.AddConnectionString("DefaultConnection", connectionString)
.AddNamespace("MyApp.Repositories", ns => ns
.AddType("UserRepository", type => type
.AddCommand("RetrieveAsync", cmd => cmd
.UseCommandText("SELECT * FROM Users WHERE Id = @id")
.UseConnectionAlias("DefaultConnection"))
.AddCommand("CreateAsync", cmd => cmd
.UseCommandText("INSERT INTO Users (Name, Email) VALUES (@Name, @Email)")
.UseConnectionAlias("DefaultConnection"))))
.Build();Optional JSON file equivalent:
{
"Connections": [
{
"Alias": "DefaultConnection",
"ConnectionString": "Server=localhost;Database=MyApp;Trusted_Connection=true;"
}
],
"Namespaces": [
{
"Name": "MyApp.Repositories",
"Types": [
{
"Name": "UserRepository",
"Commands": {
"RetrieveAsync": {
"CommandText": "SELECT * FROM Users WHERE Id = @id",
"ConnectionAlias": "DefaultConnection"
},
"CreateAsync": {
"CommandText": "INSERT INTO Users (Name, Email) VALUES (@Name, @Email)",
"ConnectionAlias": "DefaultConnection"
}
}
}
]
}
]
}Register services in your DI container:
public void ConfigureServices(IServiceCollection services)
{
// Recommended: register builder-composed settings
services.AddSingleton<ICommanderSettings>(settings);
services.UseSyrx(_ => { });
// Optional file-based registration
// var configBuilder = new ConfigurationBuilder();
// services.UseSyrx(builder => builder.UseFile("syrx.json", configBuilder));
// Register your repositories
services.AddScoped<UserRepository>();
}Create your repository classes:
public class UserRepository
{
private readonly ICommander<UserRepository> _commander;
public UserRepository(ICommander<UserRepository> commander)
{
_commander = commander;
}
public async Task<User> RetrieveAsync(int id, CancellationToken cancellationToken = default)
{
var result = await _commander.QueryAsync<User>(new { id }, cancellationToken);
return result.FirstOrDefault();
}
public async Task<User> CreateUserAsync(User user, CancellationToken cancellationToken = default)
{
return await _commander.ExecuteAsync(user, cancellationToken) ? user : null;
}
}Configuration follows a hierarchical structure:
CommanderSettings
├── Connections[] # Named connection strings
│ ├── Alias # Connection name/reference
│ └── ConnectionString # Actual connection string
└── Namespaces[] # Namespace-level grouping
├── Name # Namespace name
└── Types[] # Type-level grouping
├── Name # Class name
└── Commands{} # Method-to-command mappings
└── [MethodName]
├── CommandText # SQL command
├── ConnectionAlias # Connection reference
├── CommandTimeout # Timeout in seconds
├── CommandType # Text/StoredProcedure/TableDirect
├── SplitOn # Multi-mapping split columns
└── IsolationLevel # Transaction isolation
{
"Connections": [...],
"Namespaces": [...]
}<CommanderSettings>
<Connections>...</Connections>
<Namespaces>...</Namespaces>
</CommanderSettings>var settings = new CommanderSettingsBuilder()
.AddConnectionString("Default", connectionString)
.AddNamespace("MyApp.Repositories", ns => ns
.AddType("UserRepository", type => type
.AddCommand("RetrieveAsync", cmd => cmd
.UseCommandText("SELECT * FROM Users WHERE Id = @id")
.UseConnectionAlias("Default"))))
.Build();The Syrx.Commanders.Databases framework is fully thread-safe across all components:
- Runtime Components:
DatabaseCommander<T>,DatabaseConnector, andDatabaseCommandReaderuseConcurrentDictionaryfor caching and maintain no mutable shared state - Builder Classes:
CommanderSettingsBuilder,NamespaceSettingBuilder, andTypeSettingBuilderuseConcurrentDictionaryinternally and are safe for concurrent use - Configuration Models: All settings are immutable records after construction
- Service Lifetimes: Can be safely registered as Singleton, Scoped, or Transient
Commands are cached using thread-safe ConcurrentDictionary with the pattern {TypeFullName}.{MethodName}:
// Cached lookup - subsequent calls are extremely fast
var result = await _commander.QueryAsync<User>(new { id = 123 });Connection strings are cached by alias to avoid repeated LINQ operations:
// First call: Searches settings collection
// Subsequent calls: Retrieved from cache
var connection = _connector.CreateConnection(commandSetting);// Recommended: Single instance with cached command resolution
services.AddSingleton<ICommander<UserRepository>, DatabaseCommander<UserRepository>>();
// Alternative: Scoped for web applications
services.AddScoped<ICommander<UserRepository>, DatabaseCommander<UserRepository>>();- Use Async Methods: Prefer
QueryAsyncandExecuteAsyncfor non-blocking operations - Connection Pooling: Configure appropriate connection pool sizes in connection strings
- Command Timeouts: Set realistic timeouts to avoid unnecessary blocking
- Parameter Objects: Use strongly-typed parameter objects for better performance
// Good: Strongly-typed parameters
await _commander.QueryAsync<User>(new { id = 123, active = true });
// Avoid: Anonymous objects with complex expressions
await _commander.QueryAsync<User>(new { id = users.Where(u => u.Active).First().Id });Handle complex object relationships:
// Two-table join
public async Task<IEnumerable<User>> RetrieveWithProfilesAsync(CancellationToken cancellationToken = default)
{
return await _commander.QueryAsync<User, Profile, User>(
(user, profile) =>
{
user.Profile = profile;
return user;
},
cancellationToken: cancellationToken);
}
// Configure split column in settings
{
"SplitOn": "ProfileId"
}Process stored procedures returning multiple result sets:
public async Task<DashboardData> RetrieveDashboardAsync(int userId, CancellationToken cancellationToken = default)
{
var (users, orders, notifications) = await _commander
.QueryMultipleAsync<User, Order, Notification>(new { userId });
return new DashboardData
{
User = users.FirstOrDefault(),
Orders = orders.ToList(),
Notifications = notifications.ToList()
};
}{
"Connections": [
{
"Alias": "Primary",
"ConnectionString": "Server=write-db;..."
},
{
"Alias": "ReadOnly",
"ConnectionString": "Server=read-db;..."
}
]
}// Use different connections for different operations
{
"RetrieveUsersAsync": {
"ConnectionAlias": "ReadOnly"
},
"CreateUserAsync": {
"ConnectionAlias": "Primary"
}
}{
"ProcessOrderAsync": {
"CommandText": "sp_ProcessOrder",
"CommandType": "StoredProcedure",
"CommandTimeout": 300
}
}{
"RetrieveUserTableAsync": {
"CommandText": "Users",
"CommandType": "TableDirect"
}
}- Utilizes ADO.NET connection pooling automatically
- Configure pool settings in connection strings:
"Server=localhost;Database=MyApp;Pooling=true;Max Pool Size=100;Min Pool Size=5;"
- DatabaseCommander caches
CommandSettinglookups per method - Eliminates repeated LINQ operations on settings collections
- Uses
ConcurrentDictionaryfor thread-safe caching
- Leverages Dapper's compiled query caching
- Uses buffered queries by default for better performance
- Supports unbuffered queries for large result sets when needed
-
Use Appropriate Connection Lifetimes
// Scoped for web applications services.AddScoped<UserRepository>(); // Singleton for cached data services.AddSingleton<ConfigurationRepository>();
-
Optimize Query Parameters
// Good: Use specific parameters await _commander.QueryAsync<User>(new { id, isActive = true }); // Avoid: Passing entire objects as parameters when not needed
-
Configure Appropriate Timeouts
{ "CommandTimeout": 30, // Default queries "CommandTimeout": 300 // Long-running operations }
-
Package References
- Update all package references to 3.x versions
- No breaking changes in public APIs
-
Configuration Changes
- Configuration schema remains compatible
- New optional properties available
-
Service Registration
- Registration patterns remain the same
- New extension methods available
// Entity Framework
public async Task<User> RetrieveUserAsync(int id)
{
return await _context.Users.FindAsync(id);
}
// Syrx
public async Task<User> RetrieveUserAsync(int id)
{
var result = await _commander.QueryAsync<User>(new { id });
return result.FirstOrDefault();
}// Raw ADO.NET
public async Task<User> RetrieveUserAsync(int id)
{
using var connection = new SqlConnection(connectionString);
using var command = new SqlCommand("SELECT * FROM Users WHERE Id = @id", connection);
command.Parameters.AddWithValue("@id", id);
// ... manual mapping
}
// Syrx
public async Task<User> RetrieveUserAsync(int id)
{
var result = await _commander.QueryAsync<User>(new { id });
return result.FirstOrDefault();
}- ICommander<TRepository>: Primary interface for repository operations
- IDatabaseConnector: Database connection creation
- ICommanderSettings: Configuration container
- DatabaseCommander<TRepository>: Main command executor
- DatabaseConnector: Connection management
- CommanderSettings: Configuration model
- CommandSetting: Individual command configuration
- ConnectionStringSetting: Connection string configuration
- NamespaceSetting: Namespace-level configuration
For complete working examples, see:
- Basic CRUD Operations
- Multi-mapping Queries
- Multiple Result Sets
- Transaction Management
- Configuration Patterns
- Performance Optimization
See the Contributing Guide for information about:
- Development setup
- Coding standards
- Pull request process
- Testing requirements
- Documentation: Complete documentation
- Issues: GitHub Issues
- Discussions: GitHub Discussions
This project is licensed under the MIT License.