Best Practices and Guidelines
This guide distills practical recommendations for building reliable, secure, and high‑performance applications with Caerius.NET. It complements the Quickstart, Usage, and Advanced Usage chapters.
Applies to: C# 13 / .NET 10, SQL Server 2019+, Microsoft.Data.SqlClient.
Architecture and Patterns
- Prefer the Repository pattern. Keep data access isolated in repositories behind interfaces.
- Use Dependency Injection to obtain ICaeriusNetDbContext where needed.
- Favor sealed records for DTOs and TVPs. They are immutable, lightweight, and great with source generators.
- Prefer source generators:
csharp
[GenerateDto] // to auto-generate ISpMapper<T> for DTOs.
[GenerateTvp(Schema = "", TvpName = "")] // to auto-generate ITvpMapper<T> for TVPs.- Keep mapping and database concerns out of controllers/services. Services orchestrate repositories.
DTO Mapping Guidelines
- Mapping is ordinal-based. The constructor parameter order of your DTO must match the column order in the SQL result set.
- Column names don’t affect mapping. Aliases can improve readability but are not required for mapping.
- Use nullable types for columns that may return NULL from SQL Server.
- Keep DTOs minimal and purpose‑specific. Avoid large catch‑all DTOs.
- Example (manual mapping):
csharp
public sealed record UserDto(int Id, string Name, byte? Age)
: ISpMapper<UserDto>
{
public static UserDto MapFromDataReader(SqlDataReader reader)
=> new(reader.GetInt32(0), reader.GetString(1), reader.IsDBNull(2) ? null : reader.GetByte(2));
}Stored Procedure Guidelines (T/SQL)
- Use dedicated schemas (e.g., App, Users, Sales). Avoid dbo for application code where feasible.
- Always SET NOCOUNT ON inside procedures to avoid extra result sets.
- Keep result shapes stable. Any change in cardinality or order requires a matching DTO change.
- Avoid SELECT *. Explicitly list columns in the exact order your DTO expects.
- Prefer parameterized procedures for all inputs. Avoid dynamic SQL unless absolutely required.
- Use TRY/CATCH with explicit COMMIT/ROLLBACK for transactional procedures when necessary.
- Keep procedure names task‑based and consistent: schema.sp_Action_Subject_By_Filter.
TVP (Table‑Valued Parameter) Guidance
- Create SQL types with the minimal necessary columns and indexes where relevant.
- Use [GenerateTvp] when possible. It generates TvpTypeName and mapping boilerplate for you.
- Ensure the .NET TVP columns (constructor parameters) match the SQL TVP type definition exactly.
- Keep TVP payload sizes reasonable. Extremely large TVPs can increase CPU and memory usage on both ends.
- Pass TVPs read‑only (as required by SQL Server) and consider batching if sets are very large.
Caching Strategy
Choose the right cache per call via StoredProcedureParametersBuilder:
- Frozen cache
- In‑process, immutable, fastest.
- Use only for static reference data that rarely/never changes (e.g., lookup tables).
- No expiration, cleared when the process restarts.
- In‑memory cache
- In‑process with expiration. Good for hot paths where staleness is acceptable.
- Always set a sensible expiration.
- Redis cache
- Distributed, optional. Use in multi‑instance deployments for shared caching.
- Secure with TLS and auth. Set expirations aligned with your invalidation strategy.
Cache key design:
- Deterministic, short, and descriptive (e.g., users:age:>=30).
- Include input parameters and stable identifiers.
- Prefer lowercase and colon separators.
Invalidation:
- Prefer time‑based expiry for mutable data.
- For Frozen cache, only cache immutable data to avoid invalidation complexity.
Performance Tips
- Set ResultSetCapacity accurately to minimize list/array resizing.
- Return only required columns. Avoid over‑fetching.
- Avoid unnecessary allocations in hot paths; use ReadOnlyCollection/ImmutableArray variants where appropriate.
- Benchmark critical flows. Keep an eye on memory pressure for Frozen and In‑Memory caches.
- For very large multi‑result queries, use the MultiIEnumerable APIs to stream sets efficiently.
Async, Cancellation, and Reliability
- All APIs are asynchronous by design. Don’t block on async calls.
- Propagate CancellationToken from the request boundary to database calls.
- Configure reasonable command/connection timeouts in connection strings or command options.
- Open one connection per operation via ICaeriusNetDbContext.DbConnection(), and dispose it promptly (handled by library helpers).
Error Handling and Troubleshooting
Common issues and resolutions:
- Connection issues (Aspire)
- Ensure WithAspireSqlServer("name") matches your AppHost AddSqlServer/AddDatabase name.
- Confirm the connection string is available at runtime (AppHost injection).
- TVP type mismatch
- Verify schema and type name (Schema.TvpName) match between SQL and .NET.
- Ensure constructor parameters and SQL TVP columns align (order and types).
- Mapping errors
- InvalidCastException typically indicates an ordinal/type mismatch. Check DTO constructor order and SQL SELECT order.
- Cache misses
- Verify identical cache keys and parameters across calls. For Redis, check connectivity and configuration.
- Memory growth
- Frozen cache is immutable and monotonic. Use only for true constants.
- Tune In‑Memory cache expiration and avoid caching very large payloads unnecessarily.
Security Considerations
- Use stored procedures with parameters to avoid SQL injection.
- Do not cache sensitive data unless necessary and lawful. Prefer short expirations and encryption at rest/in‑transit.
- Secure Redis with TLS and authentication; restrict network access to trusted environments.
- Limit SQL permissions for the application user to the minimum required.
Versioning and Migrations
- Treat stored procedures and DTOs as a contract. Version them when breaking changes are needed.
- Add new procedures and DTOs alongside existing ones during migrations, then deprecate old versions.
Examples
- Read with caching (In‑memory):
csharp
var sp = new StoredProcedureParametersBuilder("Users", "usp_Get_All_Users", 250)
.AddInMemoryCache("users:all", TimeSpan.FromMinutes(2))
.Build();
var users = await dbContext.QueryAsReadOnlyCollectionAsync<UserDto>(sp, cancellationToken);- Write with affected rows:
csharp
var sp = new StoredProcedureParametersBuilder("dbo", "sp_UpdateUserAge_By_Guid")
.AddParameter("Guid", guid, SqlDbType.UniqueIdentifier)
.AddParameter("Age", age, SqlDbType.TinyInt)
.Build();
int rows = await dbContext.ExecuteNonQueryAsync(sp, cancellationToken);- TVP example:
csharp
var tvpItems = new List<UsersIdsTvp> { new(1), new(2), new(3) };
var sp = new StoredProcedureParametersBuilder("dbo", "sp_GetUsers_By_Tvp_Ids", 256)
.AddTvpParameter("Ids", tvpItems)
.Build();
var users = await dbContext.QueryAsIEnumerableAsync<UserDto>(sp, cancellationToken);Use this page as a checklist when designing new queries or optimizing existing ones. For detailed APIs and examples, see Usage, Advanced Usage, Caching, and API Reference.
