diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a4e6fae --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +root = true + +[*] +charset = utf-8 +end_of_line = crlf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{cs,csproj,props,targets}] +indent_style = space +indent_size = 4 + +[*.{yml,yaml,json,md}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.cs] +dotnet_analyzer_diagnostic.category-Style.severity = suggestion +dotnet_analyzer_diagnostic.category-Design.severity = warning +csharp_style_namespace_declarations = file_scoped:suggestion diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9ec00c5..d02779e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -24,6 +24,9 @@ - [ ] I have added or updated tests covering my change. - [ ] `dotnet build CaeriusNet.slnx -c Release -p:TreatWarningsAsErrors=true` succeeds locally. - [ ] `dotnet test CaeriusNet.slnx -c Release --filter "FullyQualifiedName!~IntegrationTests"` is green. +- [ ] `pwsh ./eng/ValidatePackage.ps1 -Configuration Release` succeeds for package/public API changes. +- [ ] `cd Documentations && npm install && npm run docs:build` succeeds for documentation changes (or `npm ci` when a lockfile exists). +- [ ] Docker-backed integration tests were run when storage, transactions, SQL, or TVP behaviour changed. - [ ] I have updated [`CHANGELOG.md`](../CHANGELOG.md) under `[Unreleased]` if user-visible. - [ ] I have updated public XML documentation comments for any public API change. - [ ] No new dependencies introduced (or discussed in an issue first). diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bec2c6..bf5f362 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,13 +60,17 @@ jobs: -p:TreatWarningsAsErrors=true \ -p:WarningLevel=4 + - name: Validate NuGet package + shell: pwsh + run: ./eng/ValidatePackage.ps1 -Configuration Release + - name: Run tests with coverage run: | dotnet test CaeriusNet.slnx \ --configuration Release \ --no-restore \ --no-build \ - --filter "FullyQualifiedName!~CaeriusNet.IntegrationTests" \ + --filter "FullyQualifiedName!~IntegrationTests" \ --collect:"XPlat Code Coverage" \ --results-directory ./coverage \ --logger "trx;LogFileName=test-results.trx" \ diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index dd7f8c3..1d72cf1 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,6 +13,10 @@ permissions: contents: read security-events: write +concurrency: + group: codeql-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + env: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true DOTNET_CLI_TELEMETRY_OPTOUT: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..42d8b62 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,52 @@ +name: Docs + +on: + pull_request: + branches: [main] + paths: + - 'Documentations/**' + - '.github/workflows/docs.yml' + push: + branches: [main, 'feature/**'] + paths: + - 'Documentations/**' + - '.github/workflows/docs.yml' + +permissions: + contents: read + +concurrency: + group: docs-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' + +jobs: + build-docs: + name: Build documentation + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: npm + cache-dependency-path: Documentations/package*.json + + - name: Install dependencies + if: hashFiles('Documentations/package-lock.json') == '' + working-directory: Documentations + run: npm install + + - name: Install locked dependencies + if: hashFiles('Documentations/package-lock.json') != '' + working-directory: Documentations + run: npm ci + + - name: Build docs + working-directory: Documentations + run: npm run docs:build diff --git a/.github/workflows/github-pages-deploy.yml b/.github/workflows/github-pages-deploy.yml index cac0034..d92be3b 100644 --- a/.github/workflows/github-pages-deploy.yml +++ b/.github/workflows/github-pages-deploy.yml @@ -8,6 +8,7 @@ on: branches: [main] paths: - 'Documentations/**' + - '.github/workflows/github-pages-deploy.yml' workflow_dispatch: # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages @@ -39,12 +40,17 @@ jobs: with: node-version: 24 cache: npm - cache-dependency-path: Documentations/package.json + cache-dependency-path: Documentations/package*.json - name: Setup Pages uses: actions/configure-pages@v4 - name: Install dependencies + if: hashFiles('Documentations/package-lock.json') == '' working-directory: Documentations run: npm install + - name: Install locked dependencies + if: hashFiles('Documentations/package-lock.json') != '' + working-directory: Documentations + run: npm ci - name: Build with VitePress working-directory: Documentations run: npm run docs:build @@ -64,4 +70,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/nuget-dotnet.yml b/.github/workflows/nuget-dotnet.yml index 09204d8..378b3ea 100644 --- a/.github/workflows/nuget-dotnet.yml +++ b/.github/workflows/nuget-dotnet.yml @@ -8,6 +8,10 @@ on: permissions: contents: read +concurrency: + group: dependabot-version-bump-${{ github.ref }} + cancel-in-progress: false + env: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true DOTNET_CLI_TELEMETRY_OPTOUT: true @@ -47,5 +51,5 @@ jobs: git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add Src/CaeriusNet.csproj - git commit -m "chore: bump version to ${{ steps.bump.outputs.new_version }} [skip ci]" - git push \ No newline at end of file + git commit -m "chore: bump version to ${{ steps.bump.outputs.new_version }}" + git push diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ff201b9..2733c33 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,6 +61,14 @@ jobs: with: dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Setup Node + if: hashFiles('Documentations/package.json') != '' + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: npm + cache-dependency-path: Documentations/package*.json + - name: Cache NuGet packages uses: actions/cache@v5 with: @@ -90,6 +98,20 @@ jobs: echo "tag=v${NEW}" >> "$GITHUB_OUTPUT" echo "🔖 Version bump: ${CURRENT} → ${NEW} (${{ github.event.inputs.version_bump }})" + - name: Validate release metadata + run: | + if [ ! -f CHANGELOG.md ]; then + echo "❌ CHANGELOG.md is required before publishing a release." + exit 1 + fi + if ! grep -q '^## \[Unreleased\]' CHANGELOG.md; then + echo "❌ CHANGELOG.md must contain an [Unreleased] section before release." + exit 1 + fi + if [ -z "${{ github.event.inputs.release_notes }}" ]; then + echo "â„šī¸ No manual release notes supplied; GitHub release notes will be generated from commits." + fi + # ── Build & Test (gates on the new version) ──────────────────────────────── - name: Restore run: dotnet restore CaeriusNet.slnx @@ -102,14 +124,38 @@ jobs: -p:TreatWarningsAsErrors=true \ -p:WarningLevel=4 - - name: Test + - name: Test (non-integration) run: | dotnet test CaeriusNet.slnx \ --configuration Release \ --no-restore \ --no-build \ + --filter "FullyQualifiedName!~IntegrationTests" \ --logger "console;verbosity=normal" + - name: Test (integration, Docker) + run: | + dotnet test Tests/CaeriusNet.IntegrationTests/CaeriusNet.IntegrationTests.csproj \ + --configuration Release \ + --no-restore \ + --no-build \ + --logger "console;verbosity=normal" + + - name: Install docs dependencies + if: hashFiles('Documentations/package.json') != '' && hashFiles('Documentations/package-lock.json') == '' + working-directory: Documentations + run: npm install + + - name: Install locked docs dependencies + if: hashFiles('Documentations/package-lock.json') != '' + working-directory: Documentations + run: npm ci + + - name: Build docs + if: hashFiles('Documentations/package.json') != '' + working-directory: Documentations + run: npm run docs:build + # ── Pack ──────────────────────────────────────────────────────────────────── - name: Pack (nupkg + snupkg) run: | @@ -120,6 +166,10 @@ jobs: -p:IncludeSymbols=true \ -p:SymbolPackageFormat=snupkg + - name: Validate package contents + shell: pwsh + run: ./eng/ValidatePackage.ps1 -Configuration Release -OutputDirectory artifacts/package-validation + - name: List packages run: ls -la ./packages/ diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index de454fb..b6a3b7d 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -8,6 +8,13 @@ on: schedule: - cron: '0 4 * * 1' # Monday 04:00 UTC +permissions: + contents: read + +concurrency: + group: security-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + env: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true DOTNET_CLI_TELEMETRY_OPTOUT: true diff --git a/Analyzer/AnalyzerReleases.Unshipped.md b/Analyzer/AnalyzerReleases.Unshipped.md index dbc84bf..1305eb2 100644 --- a/Analyzer/AnalyzerReleases.Unshipped.md +++ b/Analyzer/AnalyzerReleases.Unshipped.md @@ -7,3 +7,4 @@ CAERIUS003 | CaeriusNet.Generator | Error | Type must declare a primary constructor with parameters. CAERIUS004 | CaeriusNet.Generator | Error | [GenerateTvp] requires a non-empty TvpName. CAERIUS005 | CaeriusNet.Generator | Warning | Parameter type falls back to sql_variant. + CAERIUS006 | CaeriusNet.Generator | Error | Generator target must be a non-generic top-level type. diff --git a/Analyzer/Diagnostics/DiagnosticDescriptors.cs b/Analyzer/Diagnostics/DiagnosticDescriptors.cs index 7001eac..5efe79e 100644 --- a/Analyzer/Diagnostics/DiagnosticDescriptors.cs +++ b/Analyzer/Diagnostics/DiagnosticDescriptors.cs @@ -59,4 +59,14 @@ internal static class DiagnosticDescriptors true, "Falling back to sql_variant works but carries performance, indexing, and type-safety penalties.", HelpLinkBase + "CAERIUS005.md"); -} \ No newline at end of file + + internal static readonly DiagnosticDescriptor UnsupportedGeneratorTarget = new( + "CAERIUS006", + "Generator target shape is not supported", + "'{0}' is decorated with '{1}' but generator targets must be non-generic top-level types", + Category, + DiagnosticSeverity.Error, + true, + "CaeriusNet generators emit companion partial declarations and currently support only non-generic top-level types.", + HelpLinkBase + "CAERIUS006.md"); +} diff --git a/Analyzer/GeneratorUsageAnalyzer.cs b/Analyzer/GeneratorUsageAnalyzer.cs index b71a76c..76b08cb 100644 --- a/Analyzer/GeneratorUsageAnalyzer.cs +++ b/Analyzer/GeneratorUsageAnalyzer.cs @@ -15,7 +15,8 @@ public sealed class GeneratorUsageAnalyzer : DiagnosticAnalyzer DiagnosticDescriptors.MustBePartial, DiagnosticDescriptors.MustHavePrimaryConstructor, DiagnosticDescriptors.TvpNameMustNotBeEmpty, - DiagnosticDescriptors.UnsupportedSqlMapping + DiagnosticDescriptors.UnsupportedSqlMapping, + DiagnosticDescriptors.UnsupportedGeneratorTarget ]; public override void Initialize(AnalysisContext context) @@ -44,7 +45,6 @@ private static void AnalyzeTypeDeclaration( { if (context.Node is not TypeDeclarationSyntax declaration || declaration.AttributeLists.Count == 0 || - !MayContainRelevantAttribute(declaration) || context.SemanticModel.GetDeclaredSymbol(declaration, context.CancellationToken) is not { } typeSymbol) return; @@ -155,6 +155,17 @@ private static bool ReportStructureDiagnostics( hasFatal = true; } + if (!validation.IsTopLevel || !validation.IsNonGeneric) + { + context.ReportDiagnostic( + Diagnostic.Create( + DiagnosticDescriptors.UnsupportedGeneratorTarget, + location, + typeSymbol.Name, + attributeDisplayName)); + hasFatal = true; + } + if (validation.PrimaryConstructorDeclaration is not null) return hasFatal; context.ReportDiagnostic( Diagnostic.Create( @@ -207,25 +218,6 @@ private static void ReportUnsupportedSqlMappings( } } - private static bool MayContainRelevantAttribute(TypeDeclarationSyntax declaration) - { - foreach (var attribute in declaration.AttributeLists.SelectMany(static list => list.Attributes)) - { - var name = attribute.Name switch - { - IdentifierNameSyntax identifierName => identifierName.Identifier.ValueText, - QualifiedNameSyntax qualifiedName => qualifiedName.Right.Identifier.ValueText, - AliasQualifiedNameSyntax aliasQualifiedName => aliasQualifiedName.Name.Identifier.ValueText, - _ => attribute.Name.ToString() - }; - - if (name is "GenerateDto" or "GenerateDtoAttribute" or "GenerateTvp" or "GenerateTvpAttribute") - return true; - } - - return false; - } - private static AttributeData? TryGetDirectAttribute(INamedTypeSymbol typeSymbol, INamedTypeSymbol attributeSymbol) { foreach (var attribute in typeSymbol.GetAttributes()) @@ -255,4 +247,4 @@ private static bool TryGetNamedArgumentSyntax( argumentSyntax = null!; return false; } -} \ No newline at end of file +} diff --git a/Analyzer/GlobalUsings.cs b/Analyzer/GlobalUsings.cs index 34f5f3e..cc793df 100644 --- a/Analyzer/GlobalUsings.cs +++ b/Analyzer/GlobalUsings.cs @@ -7,4 +7,4 @@ global using Microsoft.CodeAnalysis; global using Microsoft.CodeAnalysis.CSharp; global using Microsoft.CodeAnalysis.CSharp.Syntax; -global using Microsoft.CodeAnalysis.Diagnostics; \ No newline at end of file +global using Microsoft.CodeAnalysis.Diagnostics; diff --git a/Analyzer/Helpers/SqlTypeDetector.cs b/Analyzer/Helpers/SqlTypeDetector.cs index e62a365..99577be 100644 --- a/Analyzer/Helpers/SqlTypeDetector.cs +++ b/Analyzer/Helpers/SqlTypeDetector.cs @@ -48,4 +48,4 @@ internal static string GetSqlType(ITypeSymbol type) } }; } -} \ No newline at end of file +} diff --git a/Analyzer/Helpers/TypeStructureValidator.cs b/Analyzer/Helpers/TypeStructureValidator.cs index d79bf45..1a01b43 100644 --- a/Analyzer/Helpers/TypeStructureValidator.cs +++ b/Analyzer/Helpers/TypeStructureValidator.cs @@ -13,6 +13,8 @@ internal static ValidationResult Validate(INamedTypeSymbol typeSymbol) { var isSealed = typeSymbol.IsSealed; var isPartial = false; + var isTopLevel = typeSymbol.ContainingType is null; + var isNonGeneric = typeSymbol.TypeParameters.Length == 0; TypeDeclarationSyntax? primaryCtorDeclaration = null; foreach (var declRef in typeSymbol.DeclaringSyntaxReferences) @@ -44,7 +46,7 @@ internal static ValidationResult Validate(INamedTypeSymbol typeSymbol) primaryCtorDeclaration = decl; } - return new ValidationResult(isSealed, isPartial, primaryCtorDeclaration); + return new ValidationResult(isSealed, isPartial, isTopLevel, isNonGeneric, primaryCtorDeclaration); } internal static Location GetIdentifierLocation(INamedTypeSymbol typeSymbol) @@ -61,10 +63,14 @@ internal readonly struct ValidationResult internal ValidationResult( bool isSealed, bool isPartial, + bool isTopLevel, + bool isNonGeneric, TypeDeclarationSyntax? primaryConstructorDeclaration) { IsSealed = isSealed; IsPartial = isPartial; + IsTopLevel = isTopLevel; + IsNonGeneric = isNonGeneric; PrimaryConstructorDeclaration = primaryConstructorDeclaration; } @@ -72,6 +78,10 @@ internal ValidationResult( internal bool IsPartial { get; } + internal bool IsTopLevel { get; } + + internal bool IsNonGeneric { get; } + internal TypeDeclarationSyntax? PrimaryConstructorDeclaration { get; } } -} \ No newline at end of file +} diff --git a/Benchmark/Data/Generated/BenchmarkItemDto.cs b/Benchmark/Data/Generated/BenchmarkItemDto.cs index 0d15583..20d2c4e 100644 --- a/Benchmark/Data/Generated/BenchmarkItemDto.cs +++ b/Benchmark/Data/Generated/BenchmarkItemDto.cs @@ -5,4 +5,4 @@ /// of ISpMapper<BenchmarkItemDto>.MapFromDataReader(). /// [GenerateDto] -public sealed partial record BenchmarkItemDto(int Id, Guid TraceId, string Name, decimal Price, bool IsActive); \ No newline at end of file +public sealed partial record BenchmarkItemDto(int Id, Guid TraceId, string Name, decimal Price, bool IsActive); diff --git a/Benchmark/Data/Generated/BenchmarkTvpItem.cs b/Benchmark/Data/Generated/BenchmarkTvpItem.cs index 55ab465..7bfca6b 100644 --- a/Benchmark/Data/Generated/BenchmarkTvpItem.cs +++ b/Benchmark/Data/Generated/BenchmarkTvpItem.cs @@ -5,4 +5,4 @@ /// which produces SqlDataRecord[] for streaming to SQL Server. /// [GenerateTvp(TvpName = "tvp_BenchmarkItem", Schema = "dbo")] -public sealed partial record BenchmarkTvpItem(int Id, string Name, decimal Price); \ No newline at end of file +public sealed partial record BenchmarkTvpItem(int Id, string Name, decimal Price); diff --git a/Benchmark/Data/Generated/BenchmarkTvpItem10Col.cs b/Benchmark/Data/Generated/BenchmarkTvpItem10Col.cs index 42bc9f1..8274f85 100644 --- a/Benchmark/Data/Generated/BenchmarkTvpItem10Col.cs +++ b/Benchmark/Data/Generated/BenchmarkTvpItem10Col.cs @@ -16,4 +16,4 @@ public sealed partial record BenchmarkTvpItem10Col( int Quantity, decimal Score, string Description, - Guid TraceId); \ No newline at end of file + Guid TraceId); diff --git a/Benchmark/Data/Generated/BenchmarkTvpItem5Col.cs b/Benchmark/Data/Generated/BenchmarkTvpItem5Col.cs index 7828390..9af7ddf 100644 --- a/Benchmark/Data/Generated/BenchmarkTvpItem5Col.cs +++ b/Benchmark/Data/Generated/BenchmarkTvpItem5Col.cs @@ -10,4 +10,4 @@ public sealed partial record BenchmarkTvpItem5Col( string Name, decimal Price, bool IsActive, - DateTime CreatedDate); \ No newline at end of file + DateTime CreatedDate); diff --git a/Benchmark/Data/Generated/NullableRowDto.cs b/Benchmark/Data/Generated/NullableRowDto.cs index db5aa9e..35abc8a 100644 --- a/Benchmark/Data/Generated/NullableRowDto.cs +++ b/Benchmark/Data/Generated/NullableRowDto.cs @@ -11,4 +11,4 @@ public sealed partial record NullableRowDto( string? Name, decimal? Price, bool? IsActive, - DateTime? CreatedAt); \ No newline at end of file + DateTime? CreatedAt); diff --git a/Benchmark/Data/Generated/WideRowDto.cs b/Benchmark/Data/Generated/WideRowDto.cs index 798fa7a..db4ddf7 100644 --- a/Benchmark/Data/Generated/WideRowDto.cs +++ b/Benchmark/Data/Generated/WideRowDto.cs @@ -16,4 +16,4 @@ public sealed partial record WideRowDto( int Quantity, decimal PriceWithTax, decimal DiscountedPrice, - bool InStock); \ No newline at end of file + bool InStock); diff --git a/Benchmark/Data/Simple/SimpleDto.cs b/Benchmark/Data/Simple/SimpleDto.cs index 43f3e39..ab7d9c6 100644 --- a/Benchmark/Data/Simple/SimpleDto.cs +++ b/Benchmark/Data/Simple/SimpleDto.cs @@ -17,4 +17,4 @@ public SimpleDto() public int Id { get; set; } public Guid Guid { get; set; } public string Name { get; set; } = null!; -} \ No newline at end of file +} diff --git a/Benchmark/Program.cs b/Benchmark/Program.cs index 7ce0ac2..7ff7f25 100644 --- a/Benchmark/Program.cs +++ b/Benchmark/Program.cs @@ -25,4 +25,4 @@ default: RunningBenchmarks.Run_All_Benchmarks(); break; -} \ No newline at end of file +} diff --git a/Benchmark/RunningBenchmarks.cs b/Benchmark/RunningBenchmarks.cs index 7ccbfa3..c019045 100644 --- a/Benchmark/RunningBenchmarks.cs +++ b/Benchmark/RunningBenchmarks.cs @@ -139,4 +139,4 @@ public static void Run_Cache_Benchmarks() typeof(InMemoryCacheBench) ], GetConfig()); } -} \ No newline at end of file +} diff --git a/Benchmark/Usings.cs b/Benchmark/Usings.cs index a0a9638..4b61a5e 100644 --- a/Benchmark/Usings.cs +++ b/Benchmark/Usings.cs @@ -18,4 +18,4 @@ global using CaeriusNet.Attributes.Dto; global using CaeriusNet.Attributes.Tvp; global using Microsoft.Data.SqlClient; -global using Microsoft.Data.SqlClient.Server; \ No newline at end of file +global using Microsoft.Data.SqlClient.Server; diff --git a/Benchmark/Workshops/BenchmarkConfig.cs b/Benchmark/Workshops/BenchmarkConfig.cs index 34d78e7..cb63161 100644 --- a/Benchmark/Workshops/BenchmarkConfig.cs +++ b/Benchmark/Workshops/BenchmarkConfig.cs @@ -44,4 +44,4 @@ public BenchmarkConfig() AddDiagnoser(MemoryDiagnoser.Default); WithOptions(ConfigOptions.DisableOptimizationsValidator); } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/Cache/FrozenCacheBench.cs b/Benchmark/Workshops/Benchs/Cache/FrozenCacheBench.cs index f8ab841..538c278 100644 --- a/Benchmark/Workshops/Benchs/Cache/FrozenCacheBench.cs +++ b/Benchmark/Workshops/Benchs/Cache/FrozenCacheBench.cs @@ -128,4 +128,4 @@ public FrozenDictionary Write_FullRebuild() { return _sourceEntries.ToFrozenDictionary(); } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/Cache/InMemoryCacheBench.cs b/Benchmark/Workshops/Benchs/Cache/InMemoryCacheBench.cs index 39b1920..b5328f3 100644 --- a/Benchmark/Workshops/Benchs/Cache/InMemoryCacheBench.cs +++ b/Benchmark/Workshops/Benchs/Cache/InMemoryCacheBench.cs @@ -53,7 +53,7 @@ namespace CaeriusNet.Benchmark.Workshops.Benchs.Cache; /// [Config(typeof(BenchmarkConfig))] [MemoryDiagnoser] -public class InMemoryCacheBench +public sealed class InMemoryCacheBench : IDisposable { private IMemoryCache _cache = null!; private BenchmarkItemDto[] _entries = null!; @@ -64,6 +64,12 @@ public class InMemoryCacheBench [Params(100, 1_000, 10_000)] public int CacheSize { get; set; } + public void Dispose() + { + _cache?.Dispose(); + GC.SuppressFinalize(this); + } + [GlobalSetup] public void Setup() { @@ -91,7 +97,7 @@ public void Setup() [GlobalCleanup] public void Cleanup() { - _cache.Dispose(); + Dispose(); } /// @@ -152,4 +158,4 @@ public int ReadWrite_GetOrCreate_WarmCache() return hits; } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/CreateCollections/CreateEnumerableToBench.cs b/Benchmark/Workshops/Benchs/CreateCollections/CreateEnumerableToBench.cs index fc9befe..4032e2d 100644 --- a/Benchmark/Workshops/Benchs/CreateCollections/CreateEnumerableToBench.cs +++ b/Benchmark/Workshops/Benchs/CreateCollections/CreateEnumerableToBench.cs @@ -69,4 +69,4 @@ public IEnumerable Create_Array_AsEnumerable() { return _source.AsEnumerable(); } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/CreateCollections/CreateImmutableArrayToBench.cs b/Benchmark/Workshops/Benchs/CreateCollections/CreateImmutableArrayToBench.cs index ca36ab2..cb934ee 100644 --- a/Benchmark/Workshops/Benchs/CreateCollections/CreateImmutableArrayToBench.cs +++ b/Benchmark/Workshops/Benchs/CreateCollections/CreateImmutableArrayToBench.cs @@ -71,4 +71,4 @@ public ImmutableArray Create_FromSpan() { return ImmutableArray.Create(_source.AsSpan()); } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/CreateCollections/CreateListToBench.cs b/Benchmark/Workshops/Benchs/CreateCollections/CreateListToBench.cs index 5a800c4..da8db5a 100644 --- a/Benchmark/Workshops/Benchs/CreateCollections/CreateListToBench.cs +++ b/Benchmark/Workshops/Benchs/CreateCollections/CreateListToBench.cs @@ -65,4 +65,4 @@ public List Create_LinqToList() { return _source.ToList(); } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/CreateCollections/CreateReadOnlyCollectionToBench.cs b/Benchmark/Workshops/Benchs/CreateCollections/CreateReadOnlyCollectionToBench.cs index e5fde24..e8cc417 100644 --- a/Benchmark/Workshops/Benchs/CreateCollections/CreateReadOnlyCollectionToBench.cs +++ b/Benchmark/Workshops/Benchs/CreateCollections/CreateReadOnlyCollectionToBench.cs @@ -54,4 +54,4 @@ public ReadOnlyCollection Create_LinqToList_AsReadOnly() { return _source.ToList().AsReadOnly(); } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/ListCapacity/ListWithCapacityToBench.cs b/Benchmark/Workshops/Benchs/ListCapacity/ListWithCapacityToBench.cs index 5ea57d4..c3849b4 100644 --- a/Benchmark/Workshops/Benchs/ListCapacity/ListWithCapacityToBench.cs +++ b/Benchmark/Workshops/Benchs/ListCapacity/ListWithCapacityToBench.cs @@ -48,4 +48,4 @@ public List Create_ExactCapacity() list.AddRange(_source); return list; } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/ListCapacity/ListWithCapacityWithOverextendToBench.cs b/Benchmark/Workshops/Benchs/ListCapacity/ListWithCapacityWithOverextendToBench.cs index bfc0935..56e32f4 100644 --- a/Benchmark/Workshops/Benchs/ListCapacity/ListWithCapacityWithOverextendToBench.cs +++ b/Benchmark/Workshops/Benchs/ListCapacity/ListWithCapacityWithOverextendToBench.cs @@ -50,4 +50,4 @@ public List Create_OverextendCapacity() list.AddRange(_source); return list; } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/ListCapacity/ListWithLessCapacityThanNeededToBench.cs b/Benchmark/Workshops/Benchs/ListCapacity/ListWithLessCapacityThanNeededToBench.cs index 1b7d287..3505eb7 100644 --- a/Benchmark/Workshops/Benchs/ListCapacity/ListWithLessCapacityThanNeededToBench.cs +++ b/Benchmark/Workshops/Benchs/ListCapacity/ListWithLessCapacityThanNeededToBench.cs @@ -49,4 +49,4 @@ public List Create_UnderCapacity() list.AddRange(_source); return list; } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/ListCapacity/ListWithoutCapacityToBench.cs b/Benchmark/Workshops/Benchs/ListCapacity/ListWithoutCapacityToBench.cs index 25a85e4..6b5f9c7 100644 --- a/Benchmark/Workshops/Benchs/ListCapacity/ListWithoutCapacityToBench.cs +++ b/Benchmark/Workshops/Benchs/ListCapacity/ListWithoutCapacityToBench.cs @@ -48,4 +48,4 @@ public List Create_NoCapacity() list.AddRange(_source); return list; } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/Mapping/DtoMappingBench.cs b/Benchmark/Workshops/Benchs/Mapping/DtoMappingBench.cs index 02fab65..35cc9c9 100644 --- a/Benchmark/Workshops/Benchs/Mapping/DtoMappingBench.cs +++ b/Benchmark/Workshops/Benchs/Mapping/DtoMappingBench.cs @@ -78,4 +78,4 @@ public BenchmarkItemDto[] Map_Via_PreAllocatedArray() result[i] = new BenchmarkItemDto(_ids[i], _traceIds[i], _names[i], _prices[i], _isActives[i]); return result; } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/Mapping/NullableColumnMappingBench.cs b/Benchmark/Workshops/Benchs/Mapping/NullableColumnMappingBench.cs index 3eeb5bf..7e8f6d0 100644 --- a/Benchmark/Workshops/Benchs/Mapping/NullableColumnMappingBench.cs +++ b/Benchmark/Workshops/Benchs/Mapping/NullableColumnMappingBench.cs @@ -124,4 +124,4 @@ public NullableRowDto[] Map_Nullable_DTO_Upfront_Check() return result; } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/Mapping/WideRowDtoMappingBench.cs b/Benchmark/Workshops/Benchs/Mapping/WideRowDtoMappingBench.cs index 12f56ab..e85aa3a 100644 --- a/Benchmark/Workshops/Benchs/Mapping/WideRowDtoMappingBench.cs +++ b/Benchmark/Workshops/Benchs/Mapping/WideRowDtoMappingBench.cs @@ -127,4 +127,4 @@ public WideRowDto[] Map_10Column_DTO_ToArray() _categories[i], _quantities[i], _pricesWithTax[i], _discountedPrices[i], _inStocks[i]); return result; } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/Parameters/AddTvpParameterBench.cs b/Benchmark/Workshops/Benchs/Parameters/AddTvpParameterBench.cs index 4f95926..2d46c69 100644 --- a/Benchmark/Workshops/Benchs/Parameters/AddTvpParameterBench.cs +++ b/Benchmark/Workshops/Benchs/Parameters/AddTvpParameterBench.cs @@ -83,4 +83,4 @@ public StoredProcedureParameters AddTvpParameter_TwoTvps() .AddTvpParameter("@ExistingItems", _itemList) .Build(); } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/Parameters/SpParameterBuilderBench.cs b/Benchmark/Workshops/Benchs/Parameters/SpParameterBuilderBench.cs index da355df..43f7b70 100644 --- a/Benchmark/Workshops/Benchs/Parameters/SpParameterBuilderBench.cs +++ b/Benchmark/Workshops/Benchs/Parameters/SpParameterBuilderBench.cs @@ -41,4 +41,4 @@ public StoredProcedureParameters Build_WithMixedParameters() builder.AddParameter($"@Bit{i}", i % 2 == 0, SqlDbType.Bit); return builder.Build(); } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/ReadCollections/ReadEnumerableToBench.cs b/Benchmark/Workshops/Benchs/ReadCollections/ReadEnumerableToBench.cs index 5a6595c..8360f93 100644 --- a/Benchmark/Workshops/Benchs/ReadCollections/ReadEnumerableToBench.cs +++ b/Benchmark/Workshops/Benchs/ReadCollections/ReadEnumerableToBench.cs @@ -54,4 +54,4 @@ public int Read_LinqSum() { return _data.Sum(item => item.Id); } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/ReadCollections/ReadImmutableArrayToBench.cs b/Benchmark/Workshops/Benchs/ReadCollections/ReadImmutableArrayToBench.cs index 36c7f3e..d5c8565 100644 --- a/Benchmark/Workshops/Benchs/ReadCollections/ReadImmutableArrayToBench.cs +++ b/Benchmark/Workshops/Benchs/ReadCollections/ReadImmutableArrayToBench.cs @@ -54,4 +54,4 @@ public int Read_LinqSum() { return _data.Sum(item => item.Id); } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/ReadCollections/ReadListToBench.cs b/Benchmark/Workshops/Benchs/ReadCollections/ReadListToBench.cs index 5a5ab30..ef245bb 100644 --- a/Benchmark/Workshops/Benchs/ReadCollections/ReadListToBench.cs +++ b/Benchmark/Workshops/Benchs/ReadCollections/ReadListToBench.cs @@ -67,4 +67,4 @@ public int Read_LinqSum() { return _data.Sum(item => item.Id); } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/ReadCollections/ReadReadOnlyCollectionToBench.cs b/Benchmark/Workshops/Benchs/ReadCollections/ReadReadOnlyCollectionToBench.cs index 4e56f11..3505bee 100644 --- a/Benchmark/Workshops/Benchs/ReadCollections/ReadReadOnlyCollectionToBench.cs +++ b/Benchmark/Workshops/Benchs/ReadCollections/ReadReadOnlyCollectionToBench.cs @@ -53,4 +53,4 @@ public int Read_LinqSum() { return _data.Sum(item => item.Id); } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/SqlServer/BatchedVsSingleBench.cs b/Benchmark/Workshops/Benchs/SqlServer/BatchedVsSingleBench.cs index 29f618f..9daf033 100644 --- a/Benchmark/Workshops/Benchs/SqlServer/BatchedVsSingleBench.cs +++ b/Benchmark/Workshops/Benchs/SqlServer/BatchedVsSingleBench.cs @@ -70,4 +70,4 @@ public async Task Insert_Batched_Via_TVP_StoredProcedureCall() cmd.Parameters.Add(tvpParam); await cmd.ExecuteNonQueryAsync(); } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/SqlServer/ConnectionPoolBench.cs b/Benchmark/Workshops/Benchs/SqlServer/ConnectionPoolBench.cs index f3d9d06..6cc8f56 100644 --- a/Benchmark/Workshops/Benchs/SqlServer/ConnectionPoolBench.cs +++ b/Benchmark/Workshops/Benchs/SqlServer/ConnectionPoolBench.cs @@ -25,10 +25,16 @@ namespace CaeriusNet.Benchmark.Workshops.Benchs.SqlServer; /// [Config(typeof(BenchmarkConfig))] [MemoryDiagnoser] -public class ConnectionPoolBench +public sealed class ConnectionPoolBench : IDisposable { private SqlConnection? _persistentConnection; + public void Dispose() + { + _persistentConnection?.Dispose(); + GC.SuppressFinalize(this); + } + [GlobalSetup] public async Task Setup() { @@ -49,7 +55,10 @@ public async Task Setup() public async Task Cleanup() { if (_persistentConnection is not null) + { await _persistentConnection.DisposeAsync(); + _persistentConnection = null; + } } /// @@ -125,4 +134,4 @@ public async Task ExecuteSP_ColdStartConnection() count++; return count; } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/SqlServer/MultiResultSetBench.cs b/Benchmark/Workshops/Benchs/SqlServer/MultiResultSetBench.cs index 4bae6d9..60c727e 100644 --- a/Benchmark/Workshops/Benchs/SqlServer/MultiResultSetBench.cs +++ b/Benchmark/Workshops/Benchs/SqlServer/MultiResultSetBench.cs @@ -62,4 +62,4 @@ public async Task One_MultiResult_Query() return total; } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/SqlServer/SpExecutionBench.cs b/Benchmark/Workshops/Benchs/SqlServer/SpExecutionBench.cs index c1cce0c..45e047e 100644 --- a/Benchmark/Workshops/Benchs/SqlServer/SpExecutionBench.cs +++ b/Benchmark/Workshops/Benchs/SqlServer/SpExecutionBench.cs @@ -39,4 +39,4 @@ public async Task Execute_StoredProcedure_And_Materialise() return count; } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/SqlServer/SpOutputParameterBench.cs b/Benchmark/Workshops/Benchs/SqlServer/SpOutputParameterBench.cs index 562b563..bb63846 100644 --- a/Benchmark/Workshops/Benchs/SqlServer/SpOutputParameterBench.cs +++ b/Benchmark/Workshops/Benchs/SqlServer/SpOutputParameterBench.cs @@ -114,4 +114,4 @@ public async Task Legacy_ScopeIdentity_TwoRoundtrips() var result = await idCmd.ExecuteScalarAsync(); return result is DBNull or null ? -1 : (int)result; } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/SqlServer/SqlBenchmarkGlobalSetup.cs b/Benchmark/Workshops/Benchs/SqlServer/SqlBenchmarkGlobalSetup.cs index 88d9c9b..1662617 100644 --- a/Benchmark/Workshops/Benchs/SqlServer/SqlBenchmarkGlobalSetup.cs +++ b/Benchmark/Workshops/Benchs/SqlServer/SqlBenchmarkGlobalSetup.cs @@ -175,4 +175,4 @@ public static async Task InitialiseAsync() await cmd.ExecuteNonQueryAsync(); } } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/SqlServer/TvpFullRoundtripBench.cs b/Benchmark/Workshops/Benchs/SqlServer/TvpFullRoundtripBench.cs index a17baa2..610fc81 100644 --- a/Benchmark/Workshops/Benchs/SqlServer/TvpFullRoundtripBench.cs +++ b/Benchmark/Workshops/Benchs/SqlServer/TvpFullRoundtripBench.cs @@ -110,4 +110,4 @@ public async Task Manual_TvpFullRoundtrip() return count; } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/Tvp/TvpColumnScalingBench.cs b/Benchmark/Workshops/Benchs/Tvp/TvpColumnScalingBench.cs index de23864..82ba176 100644 --- a/Benchmark/Workshops/Benchs/Tvp/TvpColumnScalingBench.cs +++ b/Benchmark/Workshops/Benchs/Tvp/TvpColumnScalingBench.cs @@ -90,4 +90,4 @@ public int Tvp_10_Columns() count++; return count; } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/Tvp/TvpSerializationBench.cs b/Benchmark/Workshops/Benchs/Tvp/TvpSerializationBench.cs index fbdb7a3..c7c0cd2 100644 --- a/Benchmark/Workshops/Benchs/Tvp/TvpSerializationBench.cs +++ b/Benchmark/Workshops/Benchs/Tvp/TvpSerializationBench.cs @@ -53,4 +53,4 @@ public int Serialize_And_Materialize_ToArray() var records = firstItem.MapAsSqlDataRecords(_items).ToArray(); return records.Length; } -} \ No newline at end of file +} diff --git a/Benchmark/Workshops/Benchs/Tvp/TvpVsDataTableBench.cs b/Benchmark/Workshops/Benchs/Tvp/TvpVsDataTableBench.cs index 081592d..d9fce05 100644 --- a/Benchmark/Workshops/Benchs/Tvp/TvpVsDataTableBench.cs +++ b/Benchmark/Workshops/Benchs/Tvp/TvpVsDataTableBench.cs @@ -21,7 +21,7 @@ namespace CaeriusNet.Benchmark.Workshops.Benchs.Tvp; [Config(typeof(BenchmarkConfig))] [MemoryDiagnoser] [HardwareCounters(HardwareCounter.BranchMispredictions, HardwareCounter.CacheMisses)] -public class TvpVsDataTableBench +public sealed class TvpVsDataTableBench : IDisposable { private static readonly Faker Faker = new Faker() .CustomInstantiator(f => new BenchmarkTvpItem( @@ -37,6 +37,12 @@ public class TvpVsDataTableBench [Params(10, 100, 1_000, 10_000, 50_000, 100_000)] public int RowCount { get; set; } + public void Dispose() + { + _dataTableSchema?.Dispose(); + GC.SuppressFinalize(this); + } + [GlobalSetup] public void Setup() { @@ -71,7 +77,7 @@ public int CaeriusNet_LazyStream_Enumerate() [Benchmark(Description = "DataTable: one DataRow per item + boxing (O(N) alloc)")] public int DataTable_FillRows() { - var dt = _dataTableSchema.Clone(); // Clone schema (columns), no rows + using var dt = _dataTableSchema.Clone(); // Clone schema (columns), no rows foreach (var item in _items) { var row = dt.NewRow(); @@ -92,7 +98,7 @@ public int DataTable_FillRows() [Benchmark(Description = "DataTable: BeginLoadData/EndLoadData optimised fill")] public int DataTable_FillRows_BeginLoadData() { - var dt = _dataTableSchema.Clone(); + using var dt = _dataTableSchema.Clone(); dt.BeginLoadData(); foreach (var item in _items) { @@ -106,4 +112,4 @@ public int DataTable_FillRows_BeginLoadData() dt.EndLoadData(); return dt.Rows.Count; } -} \ No newline at end of file +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..acdad00 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +All notable changes to CaeriusNet are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Initial public changelog for tracking user-visible changes. + +### Changed + +- Documentation now consistently describes the VitePress site, current public APIs, diagnostics, examples, and benchmark methodology. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 28f756d..3a07ae1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,8 +54,8 @@ Tests/CaeriusNet.Tests/ # Pure unit tests (no IO) Tests/CaeriusNet.Generator.Tests/ # Source-generator emit & diagnostic tests Tests/CaeriusNet.IntegrationTests/ # End-to-end tests (Testcontainers MSSQL) Benchmark/ # BenchmarkDotNet suites -Exemples/ # Sample consumer apps -Documentations/ # MkDocs site (GitHub Pages) +Exemples/ # Example consumer apps (directory name retained for compatibility) +Documentations/ # VitePress documentation site .devcontainer/ # Reproducible dev environment .github/ # Workflows, templates, issue forms ``` @@ -89,6 +89,8 @@ migration path in the PR description. | `dotnet build CaeriusNet.slnx -c Release -p:TreatWarningsAsErrors=true` | Mirrors CI. | | `dotnet test CaeriusNet.slnx -c Release --filter "FullyQualifiedName!~IntegrationTests"` | Unit + generator tests (fast, no Docker). | | `dotnet test Tests/CaeriusNet.IntegrationTests` | End-to-end tests; needs Docker. | +| `pwsh ./eng/ValidatePackage.ps1 -Configuration Release` | Packs the NuGet package and smoke-tests a consumer project. | +| `cd Documentations && npm install && npm run docs:build` | Builds the VitePress docs when npm is available; use `npm ci` if a lockfile exists. | | `dotnet test --collect:"XPlat Code Coverage"` | Generates Cobertura coverage. | Coverage is reported on every PR via the CI workflow. Aim to **never decrease** @@ -116,11 +118,13 @@ root cause over a `#pragma warning disable`. 1. **Open an issue first** for non-trivial changes so we can align on direction. 2. Use a focused branch with the prefix described above. 3. Keep PRs small and reviewable. Split refactors from features. -4. **CI must be green** (build, unit tests, CodeQL, dependency review). -5. Update [`CHANGELOG.md`](./CHANGELOG.md) under the `[Unreleased]` section. -6. Update README / docs when public behaviour changes. -7. At least **one approving review** is required before merge. -8. PRs are squash-merged onto `main` with a Conventional Commit title. +4. **CI must be green** (build, non-integration tests, package validation, CodeQL, dependency review, and docs build when docs changed). +5. Run Docker-backed integration tests when storage, transactions, SQL, or TVP behaviour changes. +6. Update [`CHANGELOG.md`](./CHANGELOG.md) under the `[Unreleased]` section. +7. Update README / docs when public behaviour changes. +8. Release PRs must leave clear release notes in the PR or changelog so the manual release workflow can publish a tagged NuGet package with generated or supplied notes. +9. At least **one approving review** is required before merge. +10. PRs are squash-merged onto `main` with a Conventional Commit title. ## Reporting bugs / requesting features diff --git a/Documentations/diagnostics/CAERIUS006.md b/Documentations/diagnostics/CAERIUS006.md new file mode 100644 index 0000000..b7511ad --- /dev/null +++ b/Documentations/diagnostics/CAERIUS006.md @@ -0,0 +1,37 @@ +# CAERIUS006 — Generator target shape is not supported + +**Severity**: Error +**Category**: CaeriusNet.Generator + +## Cause + +A type decorated with `[GenerateDto]` or `[GenerateTvp]` is either a generic type or a nested +type. CaeriusNet generators currently only support non-generic, top-level types. + +## Why + +The source generators emit companion `partial` declarations that must be placed in a well-known +namespace without additional type parameters. Generic and nested types would require reproducing +the enclosing type hierarchy and all type parameters in the generated output, which is not +currently supported. + +## How to fix + +Move the type to the top level and remove any generic type parameters: + +```csharp +// Before — nested type, not supported. +public class Container +{ + [GenerateDto] + public sealed partial record FooDto(int Id, string Name); +} + +// Before — generic type, not supported. +[GenerateDto] +public sealed partial record FooDto(int Id, T Value); + +// After — top-level, non-generic type. +[GenerateDto] +public sealed partial record FooDto(int Id, string Name); +``` diff --git a/Documentations/docs/.vitepress/config.mts b/Documentations/docs/.vitepress/config.mts index 8195ac7..22f43a9 100644 --- a/Documentations/docs/.vitepress/config.mts +++ b/Documentations/docs/.vitepress/config.mts @@ -3,7 +3,7 @@ import { defineConfig } from 'vitepress' // https://vitepress.dev/reference/site-config export default defineConfig({ title: "CaeriusNet", - description: "High-performance .NET 10 / C# 14 micro-ORM for SQL Server Stored Procedures", + description: "SQL Server stored procedure data access for .NET", head: [ ['link', { rel: 'icon', type: 'image/png', href: '/logo.png' }], @@ -16,10 +16,12 @@ export default defineConfig({ nav: [ { text: 'Home', link: '/' }, - { text: 'Get Started', link: '/quickstart/what-is-caeriusnet' }, + { text: 'Quickstart', link: '/quickstart/getting-started' }, { text: 'Guides', items: [ + { text: 'DTO Mapping', link: '/documentation/dto-mapping' }, + { text: 'Source Generators', link: '/documentation/source-generators' }, { text: 'Reading Data', link: '/documentation/reading-data' }, { text: 'Writing Data', link: '/documentation/writing-data' }, { text: 'Table-Valued Parameters', link: '/documentation/tvp' }, @@ -33,18 +35,20 @@ export default defineConfig({ { text: 'Reference', items: [ - { text: 'API Reference', link: '/documentation/api' }, - { text: 'Best Practices', link: '/documentation/best-practices' }, - { text: 'Telemetry & Diagnostics', link: '/documentation/diagnostics' }, + { text: 'API Reference', link: '/documentation/api' }, + { text: 'Best Practices', link: '/documentation/best-practices' }, + { text: 'Telemetry & Diagnostics', link: '/documentation/diagnostics' }, + { text: 'Diagnostic Rules', link: '/diagnostics/' }, ] }, + { text: 'Diagnostics', link: '/diagnostics/' }, { text: 'Examples', link: '/examples/' }, - { text: 'Performance', link: '/benchmarks/' } + { text: 'Benchmarks', link: '/benchmarks/' } ], sidebar: [ { - text: 'Get Started', + text: 'Quickstart', collapsed: false, items: [ { text: 'What is CaeriusNet?', link: '/quickstart/what-is-caeriusnet' }, @@ -78,11 +82,24 @@ export default defineConfig({ text: 'Reference', collapsed: true, items: [ - { text: 'API Reference', link: '/documentation/api' }, - { text: 'Best Practices', link: '/documentation/best-practices' }, - { text: 'Telemetry & Diagnostics', link: '/documentation/diagnostics' } - ] - }, + { text: 'API Reference', link: '/documentation/api' }, + { text: 'Best Practices', link: '/documentation/best-practices' }, + { text: 'Telemetry & Diagnostics', link: '/documentation/diagnostics' }, + { text: 'Diagnostic Rules', link: '/diagnostics/' } + ] + }, + { + text: 'Diagnostics', + collapsed: true, + items: [ + { text: 'Overview', link: '/diagnostics/' }, + { text: 'CAERIUS001', link: '/diagnostics/CAERIUS001' }, + { text: 'CAERIUS002', link: '/diagnostics/CAERIUS002' }, + { text: 'CAERIUS003', link: '/diagnostics/CAERIUS003' }, + { text: 'CAERIUS004', link: '/diagnostics/CAERIUS004' }, + { text: 'CAERIUS005', link: '/diagnostics/CAERIUS005' } + ] + }, { text: 'Examples', collapsed: false, @@ -95,7 +112,7 @@ export default defineConfig({ ] }, { - text: 'Performance', + text: 'Benchmarks', collapsed: false, items: [ { text: 'Overview & Methodology', link: '/benchmarks/' }, diff --git a/Documentations/docs/benchmarks/in-memory.md b/Documentations/docs/benchmarks/in-memory.md index 0a781f1..dfefdbf 100644 --- a/Documentations/docs/benchmarks/in-memory.md +++ b/Documentations/docs/benchmarks/in-memory.md @@ -20,7 +20,7 @@ developers can reason about the library's intrinsic cost vs raw ADO.NET. ### What is measured -CaeriusNet's source generator emits a `MapFromDataReader()` extension method for every `[CaeriusDto]`-annotated record. +CaeriusNet's source generator emits a static `MapFromDataReader(SqlDataReader)` mapper for every `[GenerateDto]`-annotated record. The generated method constructs each DTO via a **positional record constructor** — the fastest C# construction pattern for immutable types because it initialises all fields in a single allocation with no property-setter dispatch overhead. @@ -35,7 +35,7 @@ This benchmark compares three construction strategies against a simulated `IData ### Key insights - The positional constructor path is on-par with hand-written ADO.NET mapping code because the generator produces **identical IL**. -- The pre-allocated array variant consistently saves ~20 % allocation at â‰Ĩ 1 000 rows by avoiding `List`'s internal +- The pre-allocated array variant can reduce allocation at higher row counts by avoiding `List`'s internal capacity-doubling strategy (2× growth on each resize). - At 100 000 rows the difference between the pre-allocated array and `ToList()` is measurable in both allocations (bytes) and Gen0 collection count. @@ -111,7 +111,7 @@ The builder pre-allocates an internal `List` with an initial capac ### Key insights -- For typical stored procedures (3–8 parameters), the builder overhead is **sub-microsecond** — negligible vs the +- For typical stored procedures (3–8 parameters), builder overhead is intended to be small relative to the SQL Server roundtrip. - Pre-allocation avoids resize overhead for the common case. Beyond the pre-allocated capacity, each additional parameter triggers a standard `List` 2× capacity growth. @@ -142,8 +142,7 @@ This benchmark measures **the cost of passing a pre-materialised `List` vs a - The `IList` fast path is **strictly O(1)** in allocation regardless of item count — only a type-check and reference assignment. - The `IEnumerable` slow path allocates a new `List` and copies every element — O(N) allocation. -- The Ratio column will show that at small counts (≤ 100), the difference is negligible; at large counts - (10 000–100 000), the allocation gap becomes significant. +- Use the generated Ratio and Allocated columns to decide where materialising a lazy sequence becomes meaningful for your workload. - **Best practice:** Always pass a `List` (or any `IList`) to `AddTvpParameter` — never a LINQ chain. --- @@ -201,9 +200,9 @@ This benchmark puts both approaches head-to-head at the same row counts: `SqlDataRecord` streaming allocates **O(1)**. - `BeginLoadData/EndLoadData` reduces `DataTable` internal event overhead during load but does not change the fundamental O(N) allocation: every `DataRow` is still heap-allocated. -- The allocation gap between CaeriusNet and `DataTable` widens proportionally with row count — at 100 000 rows, - the difference is several hundred megabytes. -- This is the core reason CaeriusNet's TVP implementation significantly reduces Gen0/Gen1 GC pressure in +- The allocation gap between CaeriusNet and `DataTable` widens proportionally with row count — at high row counts, + the difference can become large. +- This is the core reason CaeriusNet's TVP implementation is designed to reduce Gen0/Gen1 GC pressure in high-throughput batch-insert workloads. --- diff --git a/Documentations/docs/benchmarks/index.md b/Documentations/docs/benchmarks/index.md index 6d4b9cc..917db5d 100644 --- a/Documentations/docs/benchmarks/index.md +++ b/Documentations/docs/benchmarks/index.md @@ -5,13 +5,16 @@ description: CaeriusNet benchmark methodology, BDN configuration, CI vs local mo # Performance & Benchmarks -CaeriusNet is designed from the ground up for high performance. This section documents the measured performance -characteristics of all library operations, structured by concern, using [BenchmarkDotNet](https://benchmarkdotnet.org/) — the industry-standard .NET benchmarking framework. +This section documents CaeriusNet benchmark suites by concern, using [BenchmarkDotNet](https://benchmarkdotnet.org/) to measure CPU, allocation, cache, and SQL Server roundtrip behavior. > **Benchmark environment:** All CI runs execute on **ubuntu-latest** GitHub Actions runners with **.NET 10** > and **SQL Server 2022 Developer** edition (Docker service container). > SQL benchmarks use a live TCP connection to measure real end-to-end latency including connection pooling, TDS framing, and SQL Server execution plans. +::: warning Environment-specific results +Benchmark results are diagnostic data, not performance guarantees. Compare trends and allocations, then run the suites against your own hardware, SQL Server edition, schema, query plans, indexes, network path, and payload sizes. +::: + --- ## Sections @@ -38,7 +41,7 @@ CaeriusNet uses a custom `BenchmarkConfig` class (see `Benchmark/Workshops/Bench | Toolchain | `InProcessEmitToolchain` | No child-process overhead; benchmarks run in the same process as the host | | WarmupCount | `1` | Minimal JIT warm-up — sufficient for in-process execution | | IterationCount | `5` | Enough statistical signal for median/mean without exceeding CI time budgets | -| Exporters | `MarkdownExporter.GitHub`, `JsonExporter.Full` | Produces both the human-readable tables committed to this doc and the machine-readable JSON artifacts | +| Exporters | `MarkdownExporter.GitHub`, `JsonExporter.Full` | Produces human-readable Markdown tables and machine-readable JSON artifacts | **Local Mode** (default when running `dotnet run -c Release`): @@ -55,7 +58,7 @@ All benchmarks that generate data (collection, mapping, TVP) use either: - **`Randomizer.Seed = new Random(42)`** (Bogus-based SQL/mapping benchmarks) — ensures the same sequence of fake records on every run - **`new Random(42)` in `[GlobalSetup]`** (collection benchmarks) — for speed at 100 000-item param sizes where Bogus would add measurable setup cost -This means two runs on the same hardware produce identical inputs and results should differ only from OS scheduling noise. +This means two runs on the same hardware use identical generated inputs. Remaining differences can still come from OS scheduling, CPU frequency changes, background services, container state, SQL Server plan choices, and storage or network variability. ### Benchmark Class Architecture @@ -84,7 +87,7 @@ The `[Params]` attribute drives a matrix run: BDN generates one independent meas --- -## How to Read a BDN Result Table +## How to read a BenchmarkDotNet result table A typical exported GitHub-Markdown table looks like: @@ -107,7 +110,7 @@ A typical exported GitHub-Markdown table looks like: | **Allocated** | Total managed heap allocation per single invocation | > **What to focus on:** For throughput comparisons, look at **Ratio** and **Allocated**. -> A method with Ratio < 1.00 is faster than the baseline; lower **Allocated** means less GC pressure. +> A method with Ratio < 1.00 measured faster than the baseline in that run; lower **Allocated** usually means less GC pressure. > **Error** and **StdDev** indicate measurement confidence — high values suggest the benchmark needs more iterations or the operation is I/O-bound. ### Hardware Counters (Read / Collection Benchmarks) @@ -160,13 +163,9 @@ Results are written to `Benchmark/BenchmarkDotNet.Artifacts/results/` (or to `BE ## CI/CD Integration -Benchmarks run automatically on every [GitHub Release](https://github.com/CaeriusNET/CaeriusNet/releases) via -the `benchmark.yml` GitHub Actions workflow, and can be triggered manually (requires `AriusII` approval via the `production` GitHub Environment). +Benchmarks run from the repository's `benchmark.yml` GitHub Actions workflow for release-oriented runs and can be triggered manually by maintainers with the required environment approval. -After each run, the workflow: -1. Extracts GitHub-Markdown tables from BDN's `*-report-github.md` artefacts -2. Writes one `results/ClassName.md` file per benchmark class to `Documentations/docs/benchmarks/results/` -3. Commits and pushes the updated results, triggering a VitePress rebuild +After each run, the workflow extracts GitHub-Markdown tables from BDN's `*-report-github.md` artefacts. If result files are committed under `Documentations/docs/benchmarks/results/`, VitePress can publish them alongside these methodology pages. The JSON artefacts (`*-report-full.json`) are uploaded as workflow artefacts with 90-day retention for deeper analysis. diff --git a/Documentations/docs/benchmarks/performance.md b/Documentations/docs/benchmarks/performance.md index 82605cb..a445347 100644 --- a/Documentations/docs/benchmarks/performance.md +++ b/Documentations/docs/benchmarks/performance.md @@ -1,15 +1,20 @@ --- title: Performance & Benchmarks -description: CaeriusNet benchmark results - redirected to the new structured documentation. +description: Redirect page for CaeriusNet benchmark documentation. --- # Performance & Benchmarks -This page has been reorganised into dedicated sections for better navigation. -Please use the sidebar or the links below: +The benchmark documentation is organized into focused pages so results and methodology are easier to review. -- [Overview & Methodology](./index) - BDN configuration, how to read tables, CI/CD integration -- [In-Memory Benchmarks](./in-memory) - DTO mapping, TVP serialization, parameter builder -- [Collection Benchmarks](./collections) - Read / create performance, capacity pre-allocation -- [SQL Server Benchmarks](./sql-server) - Stored procedure execution, batched inserts, TVP full roundtrip -- [Cache Benchmarks](./cache) - FrozenDictionary and IMemoryCache throughput +| Page | Scope | +|---|---| +| [Overview & Methodology](./index) | BenchmarkDotNet configuration, local commands, CI mode, and table interpretation. | +| [In-Memory Benchmarks](./in-memory) | DTO mapping, TVP serialization, and parameter-builder CPU/allocation measurements. | +| [Collection Benchmarks](./collections) | Read, create, and capacity behavior for supported collection shapes. | +| [SQL Server Benchmarks](./sql-server) | Stored-procedure execution, batched inserts, TVP roundtrips, output parameters, and connection pooling. | +| [Cache Benchmarks](./cache) | Frozen and in-memory cache read/write behavior. | + +::: warning Interpret benchmark numbers carefully +Benchmark results are not product guarantees. Use them to understand trends, then measure your own workload with your schema, SQL plans, network path, database edition, and deployment hardware. +::: diff --git a/Documentations/docs/benchmarks/sql-server.md b/Documentations/docs/benchmarks/sql-server.md index 8b1d63b..4ae240c 100644 --- a/Documentations/docs/benchmarks/sql-server.md +++ b/Documentations/docs/benchmarks/sql-server.md @@ -7,7 +7,7 @@ description: CaeriusNet SQL Server end-to-end benchmarks — stored procedure ex These benchmarks measure **real end-to-end latency** of CaeriusNet operations against a live SQL Server 2022 instance running inside a Docker service container on Ubuntu. -All metrics include: TCP connection setup (pooled), TDS framing, SQL Server execution plan evaluation, +Metrics include: TCP connection setup (pooled), TDS framing, SQL Server execution plan evaluation, data serialization over the wire, and `SqlDataReader` deserialization on the .NET side. > âš ī¸ **These benchmarks require SQL Server.** @@ -17,8 +17,12 @@ data serialization over the wire, and `SqlDataReader` deserialization on the .NE > `SELECT TOP 100000 ... FROM sys.all_objects a CROSS JOIN sys.all_objects b` cross-join seed, ensuring > a realistic cardinality for all read benchmarks. -> All benchmark results displayed on this page are **real measured values** produced by the CI benchmark workflow. -> Tables are populated automatically after each [GitHub Release](https://github.com/CaeriusNET/CaeriusNet/releases). +> This page explains what the SQL Server benchmark suite measures and how to interpret generated tables. +> If no result table is shown, run the benchmark workflow or the local commands in the overview to produce fresh BenchmarkDotNet artifacts. + +::: warning Environment-specific results +SQL Server benchmark results vary with server edition, CPU, memory, storage, indexes, query plans, network latency, container settings, and connection-pool state. Use these pages to understand methodology and trends, then measure your deployment scenario. +::: --- @@ -41,12 +45,11 @@ scales from 0 rows (no data, pure call overhead) to 50 000 rows (large result se - **At RowCount = 0**, the measurement isolates pure call overhead: TDS command framing + SQL Server parse/compile + empty result set return. This is the irreducible floor for any SP call. - **At small row counts (10–100)**, connection establishment and command preparation dominate over data transfer. - Connection pooling amortises the TCP handshake across calls — the warm-pool cost is dramatically lower + Connection pooling amortises the TCP handshake across calls — the warm-pool cost is lower than a cold-start `SqlConnection`. - **At large row counts (5 000–50 000)**, streaming throughput (rows per microsecond) becomes the binding factor. CaeriusNet's `SqlDataReader` iteration cost grows linearly with rows. -- The **Ratio** column between `RowCount = 0` and large row counts quantifies how much of the measured time - is pure SQL Server work vs .NET deserialization overhead. +- The **Ratio** column between `RowCount = 0` and larger row counts helps estimate how measured time shifts from call overhead to data transfer and deserialization. --- @@ -56,7 +59,7 @@ scales from 0 rows (no data, pure call overhead) to 50 000 rows (large result se ### What is measured -This benchmark is the **core value proposition** of CaeriusNet's TVP support. +This benchmark compares per-row stored-procedure calls with a single TVP-based stored-procedure call. Two insertion strategies are compared at `[Params(10, 100, 500, 1_000, 5_000)]` items: @@ -74,14 +77,13 @@ Two insertion strategies are compared at `[Params(10, 100, 500, 1_000, 5_000)]` - **TVP batch strategy costs O(1) roundtrips.** The entire dataset is serialized into the TVP stream, transmitted in a single TDS message batch, and processed in one server-side INSERT. -- The performance gap between the two strategies widens dramatically with item count: +- The performance gap between the two strategies widens with item count: - At N = 10: the difference exists but is modest (TVP has a fixed setup overhead). - - At N = 1 000: TVP is multiple orders of magnitude faster. - - At N = 5 000: TVP's advantage is so large that single-call strategy is effectively unusable. +- At N = 1 000: TVP avoids per-row roundtrip cost; confirm the measured ratio from your benchmark table. +- At N = 5 000: per-row roundtrip cost often dominates; confirm suitability from your benchmark table and latency budget. -- In **production environments with network latency > 1 ms**, the per-roundtrip cost is even higher — - the numbers on this page represent a local Docker loop with sub-millisecond latency. - Real-world latency multiplies every row's overhead for the single-call strategy. +- In production environments, network latency increases the cost of every roundtrip. + Local Docker numbers should not be treated as remote database latency estimates. --- @@ -131,9 +133,7 @@ Compared against a **manual ADO.NET TVP setup** without the builder — raw `Sql ### Key insights -- The CaeriusNet builder adds **negligible overhead** vs raw ADO.NET assembly — the Ratio should be - within noise margin (< 1 % difference in mean) because the builder is a thin wrapper around - `List.Add()` + a type-check for TVP items. +- Compare the CaeriusNet builder against raw ADO.NET assembly by using the **Ratio**, **Error**, and **StdDev** columns together. Small differences may be measurement noise. - The dominant cost at any row count > 100 is **network I/O + SQL Server execution** — not .NET-side work. - At RowCount = 10 000 the TVP pipeline throughput demonstrates that memory allocation stays constant (O(1) `SqlDataRecord` streaming) even as the SQL-side work grows linearly. @@ -183,17 +183,17 @@ logical `Open/Close` calls. This benchmark quantifies the performance difference |---|---| | `Connect_WarmPool` *(Baseline)* | `Open()` returns a pooled physical connection — no TCP handshake | | `Connect_ColdStart` | `ClearPool(connection)` then `Open()` — forces a new TCP handshake + TDS login | -| `Connect_Persistent` | Hold one physical connection open for the entire benchmark iteration — zero pool overhead | +| `Connect_Persistent` | Hold one physical connection open for the entire benchmark iteration — bypasses pool checkout overhead | ### Key insights - **Warm pool** is the normal production scenario. `SqlConnection.Open()` dequeues an idle physical - connection, resetting its state (`sp_reset_connection`) — typically < 100 Îŧs. + connection and resets its state (`sp_reset_connection`). Measure the expected cost in your environment. - **Cold start** forces a full TCP three-way handshake + TDS pre-login + TDS login sequence. - This is orders of magnitude more expensive than a warm pool checkout. + This is typically much more expensive than a warm pool checkout; use measured results for your environment. `ClearPool()` should **never be called in production** unless connection credentials change. -- **Persistent connection** has zero checkout overhead — useful for understanding the irreducible +- **Persistent connection** bypasses checkout overhead — useful for understanding the irreducible command-execution cost (no pool interaction at all), but not suitable for concurrent workloads. - The Ratio between Warm Pool and Cold Start quantifies the value of the connection pool. - In a typical Docker environment (sub-ms loopback), the cold-start cost is dominated by the TDS - handshake. Over a real network, cold-start latency is 10–100× higher. + In a local Docker environment, the cold-start cost is often dominated by the TDS handshake. + Remote database latency and authentication configuration can change the result substantially. diff --git a/Documentations/docs/diagnostics/CAERIUS001.md b/Documentations/docs/diagnostics/CAERIUS001.md new file mode 100644 index 0000000..f81e632 --- /dev/null +++ b/Documentations/docs/diagnostics/CAERIUS001.md @@ -0,0 +1,30 @@ +# CAERIUS001 — Type must be `sealed` + +**Severity**: Error +**Category**: CaeriusNet.Generator +**Applies to**: `[GenerateDto]`, `[GenerateTvp]` + +## Cause + +A type decorated with `[GenerateDto]` or `[GenerateTvp]` is not declared `sealed`. + +## Why + +CaeriusNet's source generators emit per-type mappers that bypass virtual dispatch and rely on the +exact runtime layout. Allowing inheritance would break the assumption that the generated code can +materialise an instance using the primary constructor with no further polymorphism. Sealing the +type also gives the JIT additional optimisation room for the hot-path mapping. + +## How to fix + +Add the `sealed` modifier: + +```csharp +[GenerateDto] +public sealed partial record FooDto(int Id, string Name); +``` + +## See also + +- [CAERIUS002 — Type must be partial](./CAERIUS002) +- [CAERIUS003 — Type must declare a primary constructor](./CAERIUS003) diff --git a/Documentations/docs/diagnostics/CAERIUS002.md b/Documentations/docs/diagnostics/CAERIUS002.md new file mode 100644 index 0000000..48f492b --- /dev/null +++ b/Documentations/docs/diagnostics/CAERIUS002.md @@ -0,0 +1,24 @@ +# CAERIUS002 — Type must be `partial` + +**Severity**: Error +**Category**: CaeriusNet.Generator +**Applies to**: `[GenerateDto]`, `[GenerateTvp]` + +## Cause + +A type decorated with `[GenerateDto]` or `[GenerateTvp]` is not declared `partial`, so the +source generator cannot extend it with the generated mapper. + +## How to fix + +Add the `partial` modifier: + +```csharp +[GenerateDto] +public sealed partial record FooDto(int Id, string Name); +``` + +## See also + +- [CAERIUS001 — Type must be sealed](./CAERIUS001) +- [CAERIUS003 — Type must declare a primary constructor](./CAERIUS003) diff --git a/Documentations/docs/diagnostics/CAERIUS003.md b/Documentations/docs/diagnostics/CAERIUS003.md new file mode 100644 index 0000000..c37c490 --- /dev/null +++ b/Documentations/docs/diagnostics/CAERIUS003.md @@ -0,0 +1,27 @@ +# CAERIUS003 — Type must declare a primary constructor + +**Severity**: Error +**Category**: CaeriusNet.Generator +**Applies to**: `[GenerateDto]`, `[GenerateTvp]` + +## Cause + +A type decorated with `[GenerateDto]` or `[GenerateTvp]` does not declare a primary constructor. +The generated mapper relies exclusively on the ordered parameters of the primary constructor to +materialise instances — initialisers, properties or secondary constructors are ignored on purpose +to keep the SQL column / object-graph mapping deterministic. + +## How to fix + +Declare a primary constructor whose parameters mirror the SQL result-set columns (DTO) or the +TVP column ordering (TVP): + +```csharp +[GenerateDto] +public sealed partial record FooDto(int Id, string Name, DateTime CreatedAt); +``` + +## See also + +- [DTO Mapping](/documentation/dto-mapping) +- [Source Generators](/documentation/source-generators) diff --git a/Documentations/docs/diagnostics/CAERIUS004.md b/Documentations/docs/diagnostics/CAERIUS004.md new file mode 100644 index 0000000..f16c8fa --- /dev/null +++ b/Documentations/docs/diagnostics/CAERIUS004.md @@ -0,0 +1,26 @@ +# CAERIUS004 — `TvpName` must not be empty + +**Severity**: Error +**Category**: CaeriusNet.Generator +**Applies to**: `[GenerateTvp]` + +## Cause + +`[GenerateTvp(TvpName = "")]` (or any whitespace-only value) is not a valid SQL Server type name. +The generator needs a non-empty identifier to emit the `SqlParameter.TypeName` value. + +## How to fix + +Provide the fully-qualified TVP type name as configured in your SQL Server schema: + +```csharp +[GenerateTvp(TvpName = "tvp_FooBar", Schema = "dbo")] +public sealed partial record FooBarTvp(int Id, string Name); +``` + +The `Schema` argument is optional and defaults to `dbo`. + +## See also + +- [Table-Valued Parameters](/documentation/tvp) +- [Source Generators — GenerateTvp](/documentation/source-generators#generatetvp-tvp-mapper) diff --git a/Documentations/docs/diagnostics/CAERIUS005.md b/Documentations/docs/diagnostics/CAERIUS005.md new file mode 100644 index 0000000..765acdd --- /dev/null +++ b/Documentations/docs/diagnostics/CAERIUS005.md @@ -0,0 +1,47 @@ +# CAERIUS005 — Unmapped CLR type falls back to `sql_variant` + +**Severity**: Warning +**Category**: CaeriusNet.Generator +**Applies to**: `[GenerateDto]`, `[GenerateTvp]` + +## Cause + +A parameter on a `[GenerateDto]` or `[GenerateTvp]` type uses a CLR type that the generator +cannot map to a native SQL Server data type. The generated code falls back to +`SqlDbType.Variant` (`sql_variant`). + +Common offenders: `System.Uri`, `System.Version`, custom value objects, `System.Net.IPAddress`, +arbitrary reference types, etc. + +## Why it matters + +`sql_variant` works, but it disables several SQL Server features: + +- No participation in indexes (no seek/scan optimizations). +- Cannot back computed columns. +- Cannot be referenced from `CHECK` constraints. +- Forces boxing/unboxing on every round-trip. +- ORM/tooling support is limited. + +## How to fix + +Replace the offending property with a natively-mapped CLR type, or expose a conversion at the +DTO/TVP boundary: + +```csharp +// Before — emits sql_variant. +[GenerateDto] +public sealed partial record EndpointDto(int Id, System.Uri Endpoint); + +// After — explicit string mapping; SQL nvarchar works with all features. +[GenerateDto] +public sealed partial record EndpointDto(int Id, string Endpoint); +``` + +If you intentionally want `sql_variant`, suppress the warning with +`#pragma warning disable CAERIUS005` around the declaration. + +## See also + +- [Compiler Diagnostics](/documentation/diagnostics#suppressing-diagnostics) +- [Table-Valued Parameters](/documentation/tvp) diff --git a/Documentations/docs/diagnostics/index.md b/Documentations/docs/diagnostics/index.md new file mode 100644 index 0000000..a44d364 --- /dev/null +++ b/Documentations/docs/diagnostics/index.md @@ -0,0 +1,19 @@ +# Diagnostic rules + +CaeriusNet analyzers validate `[GenerateDto]` and `[GenerateTvp]` contracts at compile time. Diagnostics are emitted by the analyzer included with the `CaeriusNet` package and appear in IDE builds and `dotnet build`. + +Use these pages as the rule reference for generated DTO and TVP mapper issues. + +| ID | Severity | Summary | +|---|---|---| +| [CAERIUS001](./CAERIUS001) | Error | Type must be `sealed` | +| [CAERIUS002](./CAERIUS002) | Error | Type must be `partial` | +| [CAERIUS003](./CAERIUS003) | Error | Type must declare a primary constructor | +| [CAERIUS004](./CAERIUS004) | Error | `TvpName` must not be empty | +| [CAERIUS005](./CAERIUS005) | Warning | Unmapped CLR type falls back to `sql_variant` | + +::: tip Build policy +Keep error diagnostics fixed in source. Treat warning suppressions as design decisions and prefer documenting them near the affected type. +::: + +See also the higher-level [Compiler Diagnostics](/documentation/diagnostics) guide for suppression and severity configuration. diff --git a/Documentations/docs/documentation/advanced-usage.md b/Documentations/docs/documentation/advanced-usage.md index 855cce9..8ff00db 100644 --- a/Documentations/docs/documentation/advanced-usage.md +++ b/Documentations/docs/documentation/advanced-usage.md @@ -32,12 +32,12 @@ public async Task> GetUsersByIdsAndAgeAsync( var tvpItems = userIds.Select(id => new UserIdTvp(id)); - var sp = new StoredProcedureParametersBuilder("dbo", "sp_GetUsers_By_Tvp_Ids_And_Age", capacity: 4096) + var sp = new StoredProcedureParametersBuilder("dbo", "sp_GetUsers_By_Tvp_Ids_And_Age", ResultSetCapacity: 4096) .AddTvpParameter("Ids", tvpItems) .AddParameter("Age", minAge, SqlDbType.Int) .Build(); - return await DbContext.QueryAsIEnumerableAsync(sp, ct) ?? []; + return await DbContext.QueryAsIEnumerableAsync(sp, ct); } ``` @@ -56,7 +56,7 @@ public async Task> GetUsersAsync( else builder.AddFrozenCache("users:all"); // only cache the unfiltered result - return await DbContext.QueryAsIEnumerableAsync(builder.Build(), ct) ?? []; + return await DbContext.QueryAsIEnumerableAsync(builder.Build(), ct); } ``` diff --git a/Documentations/docs/documentation/api.md b/Documentations/docs/documentation/api.md index 23fb998..de8a169 100644 --- a/Documentations/docs/documentation/api.md +++ b/Documentations/docs/documentation/api.md @@ -1,8 +1,12 @@ # API Reference -This page is a practical reference for the public API exposed by CaeriusNet. All examples target **C# 14 / .NET 10** with `Microsoft.Data.SqlClient`. Namespaces are abbreviated below — add the matching `using` directives in your project. +This page is a practical reference for the public API exposed by CaeriusNet. All examples target **C# 14 / .NET 10** with `Microsoft.Data.SqlClient`. Namespaces are abbreviated below — add the matching `using` directives in your project. C# 14 extension-block methods such as `QueryAsIEnumerableAsync`, `ExecuteNonQueryAsync`, and `BeginTransactionAsync` are available after importing their command namespaces. > Examples assume DI is configured via `CaeriusNetBuilder` (see [Installation & Setup](/quickstart/getting-started)). +> +> Common command namespaces: `CaeriusNet.Commands.Reads`, `CaeriusNet.Commands.Writes`, and `CaeriusNet.Commands.Transactions`. +> +> CaeriusNet targets SQL Server stored procedures. Parameter names passed to builder methods are identifiers without the SQL `@` prefix. ## Builders @@ -39,13 +43,18 @@ CaeriusNetBuilder Fluent builder for Stored Procedure execution settings, parameters, and caching. +::: tip Parameter identifiers +Use the parameter name only when calling `AddParameter` or `AddTvpParameter`; do not include the SQL `@` prefix. This keeps C# call sites consistent while SQL definitions continue to use normal SQL Server parameter syntax. +::: + **Constructor:** ```csharp StoredProcedureParametersBuilder( string schemaName, string procedureName, - int resultSetCapacity = 1); + int ResultSetCapacity = 16, + int CommandTimeout = 30); ``` **Parameter methods:** @@ -93,9 +102,9 @@ var sp = new StoredProcedureParametersBuilder("dbo", "sp_GetUsers_By_Tvp_Ids_And Configuration record consumed globally by every command pipeline. ```csharp -public sealed record CaeriusTelemetryOptions +public sealed class CaeriusTelemetryOptions { - public bool CaptureParameterValues { get; init; } = false; + public bool CaptureParameterValues { get; init; } } ``` @@ -156,6 +165,7 @@ Distributed cache adapter used when Redis is configured. ```csharp bool TryGet(string cacheKey, out T? value); void Store (string cacheKey, T value, TimeSpan? expiration) where T : notnull; +void Remove(string cacheKey); ``` ## Mappers diff --git a/Documentations/docs/documentation/aspire.md b/Documentations/docs/documentation/aspire.md index 413e991..bccefbe 100644 --- a/Documentations/docs/documentation/aspire.md +++ b/Documentations/docs/documentation/aspire.md @@ -158,7 +158,7 @@ public sealed record UserRepository(ICaeriusNetDbContext DbContext) .AddRedisCache("users:all", TimeSpan.FromMinutes(5)) .Build(); - return await DbContext.QueryAsIEnumerableAsync(sp, ct) ?? []; + return await DbContext.QueryAsIEnumerableAsync(sp, ct); } } ``` diff --git a/Documentations/docs/documentation/diagnostics.md b/Documentations/docs/documentation/diagnostics.md index ab4ccc9..4306914 100644 --- a/Documentations/docs/documentation/diagnostics.md +++ b/Documentations/docs/documentation/diagnostics.md @@ -1,16 +1,18 @@ # Compiler Diagnostics -CaeriusNet's Roslyn analyzer emits **compile-time diagnostics** for `[GenerateDto]` and `[GenerateTvp]` so contract problems show up in your IDE — never at runtime. The analyzer ships **inside the `CaeriusNet` NuGet package**; no extra reference is required. +CaeriusNet's Roslyn analyzer emits **compile-time diagnostics** for `[GenerateDto]` and `[GenerateTvp]` contract issues. The analyzer ships inside the `CaeriusNet` NuGet package; no extra reference is required. + +Use this page to decide whether a diagnostic must be fixed, can be suppressed, or should be escalated in CI. ## Diagnostic reference | ID | Severity | Title | Triggered when | |---|---|---|---| -| [CAERIUS001](https://github.com/CaeriusNET/CaeriusNet/blob/main/Documentations/diagnostics/CAERIUS001.md) | Error | Type must be `sealed` | A `[GenerateDto]` or `[GenerateTvp]` type is missing the `sealed` modifier | -| [CAERIUS002](https://github.com/CaeriusNET/CaeriusNet/blob/main/Documentations/diagnostics/CAERIUS002.md) | Error | Type must be `partial` | A `[GenerateDto]` or `[GenerateTvp]` type is missing the `partial` modifier | -| [CAERIUS003](https://github.com/CaeriusNET/CaeriusNet/blob/main/Documentations/diagnostics/CAERIUS003.md) | Error | Primary constructor required | The type does not declare a primary constructor (or the constructor has no parameters) | -| [CAERIUS004](https://github.com/CaeriusNET/CaeriusNet/blob/main/Documentations/diagnostics/CAERIUS004.md) | Error | `[GenerateTvp]` requires a non-empty `TvpName` | The attribute sets `TvpName` to an empty or whitespace-only string | -| [CAERIUS005](https://github.com/CaeriusNET/CaeriusNet/blob/main/Documentations/diagnostics/CAERIUS005.md) | Warning | Unmapped CLR type falls back to `sql_variant` | A constructor parameter has a type with no native SQL Server mapping | +| [CAERIUS001](/diagnostics/CAERIUS001) | Error | Type must be `sealed` | A `[GenerateDto]` or `[GenerateTvp]` type is missing the `sealed` modifier | +| [CAERIUS002](/diagnostics/CAERIUS002) | Error | Type must be `partial` | A `[GenerateDto]` or `[GenerateTvp]` type is missing the `partial` modifier | +| [CAERIUS003](/diagnostics/CAERIUS003) | Error | Primary constructor required | The type does not declare a primary constructor (or the constructor has no parameters) | +| [CAERIUS004](/diagnostics/CAERIUS004) | Error | `[GenerateTvp]` requires a non-empty `TvpName` | The attribute sets `TvpName` to an empty or whitespace-only string | +| [CAERIUS005](/diagnostics/CAERIUS005) | Warning | Unmapped CLR type falls back to `sql_variant` | A constructor parameter has a type with no native SQL Server mapping | ## Severity levels @@ -19,6 +21,10 @@ CaeriusNet's Roslyn analyzer emits **compile-time diagnostics** for `[GenerateDt | **Error** | Fails compilation | Must be fixed before the project builds | | **Warning** | Compilation succeeds | Review and fix, or suppress intentionally | +::: tip Full rule pages +For examples and rule-specific guidance, open the individual pages under [Diagnostic rules](/diagnostics/). +::: + ## Suppressing diagnostics ### Per declaration with `#pragma` diff --git a/Documentations/docs/documentation/reading-data.md b/Documentations/docs/documentation/reading-data.md index 2fea166..c1bea02 100644 --- a/Documentations/docs/documentation/reading-data.md +++ b/Documentations/docs/documentation/reading-data.md @@ -47,11 +47,11 @@ Returns `IEnumerable?` (`null` on empty result set). Best for LINQ-pipeline s public async Task> GetUsersOlderThanAsync( byte age, CancellationToken ct) { - var sp = new StoredProcedureParametersBuilder("dbo", "sp_GetUsers_By_Age", capacity: 450) + var sp = new StoredProcedureParametersBuilder("dbo", "sp_GetUsers_By_Age", ResultSetCapacity: 450) .AddParameter("Age", age, SqlDbType.TinyInt) .Build(); - return await DbContext.QueryAsIEnumerableAsync(sp, ct) ?? []; + return await DbContext.QueryAsIEnumerableAsync(sp, ct); } ``` @@ -105,7 +105,7 @@ The third constructor argument of `StoredProcedureParametersBuilder` is `resultS ```csharp // Expecting ~250 rows — pre-allocate to skip List resizing -new StoredProcedureParametersBuilder("dbo", "usp_Get_All_Users", capacity: 250); +new StoredProcedureParametersBuilder("dbo", "usp_Get_All_Users", ResultSetCapacity: 250); ``` ::: tip Capacity tuning @@ -120,7 +120,7 @@ Every read method takes a `CancellationToken`. Propagate it from your controller public async Task> GetAsync(CancellationToken ct) { var sp = new StoredProcedureParametersBuilder("dbo", "usp_Get_All_Users").Build(); - return await DbContext.QueryAsIEnumerableAsync(sp, ct) ?? []; + return await DbContext.QueryAsIEnumerableAsync(sp, ct); } ``` diff --git a/Documentations/docs/documentation/tvp.md b/Documentations/docs/documentation/tvp.md index 2439ba3..f1a4f4d 100644 --- a/Documentations/docs/documentation/tvp.md +++ b/Documentations/docs/documentation/tvp.md @@ -80,7 +80,7 @@ Pass the TVP collection via `AddTvpParameter`: ```csharp var ids = users.Select(u => new UserIdTvp(u.Id)).ToList(); -var sp = new StoredProcedureParametersBuilder("dbo", "sp_GetUsers_By_Tvp_Ids", capacity: 1024) +var sp = new StoredProcedureParametersBuilder("dbo", "sp_GetUsers_By_Tvp_Ids", ResultSetCapacity: 1024) .AddTvpParameter("Ids", ids) .Build(); diff --git a/Documentations/docs/documentation/usage.md b/Documentations/docs/documentation/usage.md index 9a42b49..f112601 100644 --- a/Documentations/docs/documentation/usage.md +++ b/Documentations/docs/documentation/usage.md @@ -58,7 +58,7 @@ public sealed record UserRepository(ICaeriusNetDbContext DbContext) .AddParameter("Age", age, SqlDbType.TinyInt) .Build(); - return await DbContext.QueryAsIEnumerableAsync(sp, ct) ?? []; + return await DbContext.QueryAsIEnumerableAsync(sp, ct); } public async Task UpdateUserAgeAsync( diff --git a/Documentations/docs/examples/index.md b/Documentations/docs/examples/index.md index 873c6bf..fff5fcc 100644 --- a/Documentations/docs/examples/index.md +++ b/Documentations/docs/examples/index.md @@ -1,6 +1,10 @@ # Examples -End-to-end, production-shaped walkthroughs for CaeriusNet. Each page covers **one category** from the SQL Server objects to the C# repository, including the telemetry tags emitted along the way. +End-to-end walkthroughs for CaeriusNet. Each page covers one category from the SQL Server objects to the C# repository, including the telemetry tags emitted along the way. + +::: info SQL Server stored-procedure examples +The examples use SQL Server stored procedures and TVPs. C# parameter builder calls use parameter identifiers without the SQL `@` prefix; the SQL snippets continue to use normal SQL Server syntax. +::: | Page | Scope | |---|---| @@ -14,7 +18,7 @@ End-to-end, production-shaped walkthroughs for CaeriusNet. Each page covers **on - **Repositories** inject `ICaeriusNetDbContext` via primary-constructor DI and expose intent-revealing async methods. - **DTOs and TVPs** use `[GenerateDto]` / `[GenerateTvp]` — sealed partial records with primary constructors. - **All examples** propagate `CancellationToken` and use `await using` for transaction scopes. -- **SQL snippets** are idempotent (`SET NOCOUNT ON`, explicit schema) and match the schema used by the runnable `Exemples/` projects in the repository. +- **SQL snippets** are idempotent (`SET NOCOUNT ON`, explicit schema) and match the schema used by the runnable **Examples** projects in the repository (`Exemples/` path retained for compatibility). - **Telemetry callouts** name the tags emitted by each scenario so you can validate them in the Aspire dashboard. ## Running the examples diff --git a/Documentations/docs/examples/multi-result-sets.md b/Documentations/docs/examples/multi-result-sets.md index c93601f..4611bcb 100644 --- a/Documentations/docs/examples/multi-result-sets.md +++ b/Documentations/docs/examples/multi-result-sets.md @@ -87,7 +87,7 @@ A 3-tuple destructures the three sets directly at the call site. The DTO type at public async Task GetDashboardAsync(CancellationToken ct) { var sp = new StoredProcedureParametersBuilder( - "Users", "usp_Get_Dashboard", capacity: 25) + "Users", "usp_Get_Dashboard", ResultSetCapacity: 25) .Build(); var (users, orders, stats) = await DbContext @@ -116,7 +116,7 @@ public async Task<(IReadOnlyCollection Users, IReadOnlyCollection tvp = userIds.Select(id => new UsersIntTvp(id)); var sp = new StoredProcedureParametersBuilder( - "Users", "usp_Get_Users_With_Orders_By_Tvp", capacity: 25) + "Users", "usp_Get_Users_With_Orders_By_Tvp", ResultSetCapacity: 25) .AddTvpParameter("tvp", tvp) .Build(); diff --git a/Documentations/docs/examples/stored-procedures.md b/Documentations/docs/examples/stored-procedures.md index d561c34..528f589 100644 --- a/Documentations/docs/examples/stored-procedures.md +++ b/Documentations/docs/examples/stored-procedures.md @@ -75,10 +75,10 @@ public sealed record UsersRepository( ```csharp public async Task> GetAllUsersAsync(CancellationToken ct) { - var sp = new StoredProcedureParametersBuilder("Users", "usp_Get_All_Users", capacity: 25) + var sp = new StoredProcedureParametersBuilder("Users", "usp_Get_All_Users", ResultSetCapacity: 25) .Build(); - return await DbContext.QueryAsIEnumerableAsync(sp, ct) ?? []; + return await DbContext.QueryAsIEnumerableAsync(sp, ct); } ``` @@ -129,7 +129,7 @@ On a cache hit, **no SQL command runs and no DB span is created** — only the ` public async Task CreateUserAsync(string userName, CancellationToken ct) { var sp = new StoredProcedureParametersBuilder("Users", "usp_Create_User") - .AddParameter("@UserName", userName, SqlDbType.NVarChar) + .AddParameter("UserName", userName, SqlDbType.NVarChar) .Build(); return await DbContext.ExecuteScalarAsync(sp, ct) ?? 0; @@ -148,7 +148,7 @@ public async Task> GetAllUsersSafeAsync(CancellationToken c var sp = new StoredProcedureParametersBuilder("Users", "usp_Get_All_Users", 25) .Build(); - return await DbContext.QueryAsIEnumerableAsync(sp, ct) ?? []; + return await DbContext.QueryAsIEnumerableAsync(sp, ct); } catch (CaeriusNetSqlException ex) { diff --git a/Documentations/docs/examples/transactions.md b/Documentations/docs/examples/transactions.md index adbeb02..e748a4b 100644 --- a/Documentations/docs/examples/transactions.md +++ b/Documentations/docs/examples/transactions.md @@ -101,16 +101,16 @@ public async Task CreateUserWithFirstOrderAsync( // First write — create the user var createUser = new StoredProcedureParametersBuilder("Users", "usp_Create_User") - .AddParameter("@UserName", userName, SqlDbType.NVarChar) + .AddParameter("UserName", userName, SqlDbType.NVarChar) .Build(); var newUserId = await tx.ExecuteScalarAsync(createUser, ct); // Second write — create their first order, using the ID from the first call var createOrder = new StoredProcedureParametersBuilder("Users", "usp_Create_Order") - .AddParameter("@UserId", newUserId, SqlDbType.Int) - .AddParameter("@Label", orderLabel, SqlDbType.NVarChar) - .AddParameter("@Amount", amount, SqlDbType.Decimal) + .AddParameter("UserId", newUserId, SqlDbType.Int) + .AddParameter("Label", orderLabel, SqlDbType.NVarChar) + .AddParameter("Amount", amount, SqlDbType.Decimal) .Build(); await tx.ExecuteScalarAsync(createOrder, ct); @@ -139,7 +139,7 @@ public async Task DemonstrateClientSideRollbackAsync( .BeginTransactionAsync(IsolationLevel.ReadCommitted, ct); var createUser = new StoredProcedureParametersBuilder("Users", "usp_Create_User") - .AddParameter("@UserName", userName, SqlDbType.NVarChar) + .AddParameter("UserName", userName, SqlDbType.NVarChar) .Build(); await tx.ExecuteScalarAsync(createUser, ct); @@ -168,8 +168,8 @@ public async Task DemonstrateServerSideRollbackAsync( CancellationToken ct) { var sp = new StoredProcedureParametersBuilder("Users", "usp_Create_User_Tx_Safe") - .AddParameter("@UserName", userName, SqlDbType.NVarChar) - .AddParameter("@ForceFailure", true, SqlDbType.Bit) + .AddParameter("UserName", userName, SqlDbType.NVarChar) + .AddParameter("ForceFailure", true, SqlDbType.Bit) .Build(); // This call throws CaeriusNetSqlException because the SP re-raises. diff --git a/Documentations/docs/examples/tvp.md b/Documentations/docs/examples/tvp.md index 1204a81..ed53667 100644 --- a/Documentations/docs/examples/tvp.md +++ b/Documentations/docs/examples/tvp.md @@ -96,7 +96,7 @@ public async Task> GetUsersByTvpIntAsync( IEnumerable ids = [new(1), new(2), new(3), new(4)]; var sp = new StoredProcedureParametersBuilder( - "Users", "usp_Get_Users_From_TvpInt", capacity: 5) + "Users", "usp_Get_Users_From_TvpInt", ResultSetCapacity: 5) .AddTvpParameter("tvp", ids) // matches @tvp in the SP (no leading @) .Build(); @@ -121,7 +121,7 @@ public async Task> GetUsersByTvpGuidAsync( ]; var sp = new StoredProcedureParametersBuilder( - "Users", "usp_Get_Users_From_TvpGuid", capacity: 5) + "Users", "usp_Get_Users_From_TvpGuid", ResultSetCapacity: 5) .AddTvpParameter("tvp", guids) .Build(); @@ -142,11 +142,11 @@ public async Task> GetUsersByTvpIntGuidAsync( ]; var sp = new StoredProcedureParametersBuilder( - "Users", "usp_Get_Users_From_TvpIntGuid", capacity: 5) + "Users", "usp_Get_Users_From_TvpIntGuid", ResultSetCapacity: 5) .AddTvpParameter("tvp", pairs) .Build(); - return await DbContext.QueryAsIEnumerableAsync(sp, ct) ?? []; + return await DbContext.QueryAsIEnumerableAsync(sp, ct); } ``` @@ -185,8 +185,8 @@ public async Task CreateBatchOrdersAsync( var sp = new StoredProcedureParametersBuilder("Users", "usp_Create_Orders_For_Users") .AddTvpParameter("tvp", targetUserIds) - .AddParameter ("@Label", label, SqlDbType.NVarChar) - .AddParameter ("@Amount", amount, SqlDbType.Decimal) + .AddParameter ("Label", label, SqlDbType.NVarChar) + .AddParameter ("Amount", amount, SqlDbType.Decimal) .Build(); return await DbContext.ExecuteScalarAsync(sp, ct) ?? 0; diff --git a/Documentations/docs/index.md b/Documentations/docs/index.md index 70996f3..8b4543f 100644 --- a/Documentations/docs/index.md +++ b/Documentations/docs/index.md @@ -3,51 +3,70 @@ layout: home hero: name: "CaeriusNet" - text: "SQL Server Stored Procedures to C# DTOs" - tagline: A focused, high-performance micro-ORM for C# 14 / .NET 10. Compile-time safe mapping, zero reflection, first-class observability. + text: "SQL Server stored procedures to C# DTOs" + tagline: A focused .NET data-access library for teams that standardize on SQL Server stored procedures, compile-time DTO mapping, and observable database calls. image: alt: CaeriusNet Logo actions: - theme: brand + text: Start with the quickstart + link: /quickstart/getting-started + - theme: alt text: What is CaeriusNet? link: /quickstart/what-is-caeriusnet - theme: alt - text: Get Started - link: /quickstart/getting-started - - theme: alt - text: Examples - link: /examples/ + text: API reference + link: /documentation/api - theme: alt text: GitHub link: https://github.com/CaeriusNET/CaeriusNet features: - icon: đŸ› ī¸ - title: Stored Procedures, first and only - details: Data access lives in T-SQL where it belongs. CaeriusNet binds Stored Procedure result sets to typed C# DTOs with a fluent builder, DI-first design, and a deliberately small API surface — no LINQ translation, no change tracking, no surprises. - - icon: ⚡ - title: Compile-time mapping via Roslyn generators - details: [GenerateDto] and [GenerateTvp] emit ISpMapper<T> / ITvpMapper<T> at build time. A dedicated analyzer enforces the sealed partial + primary-constructor shape, so contract drift surfaces in your IDE — never at runtime. - - icon: 🚀 - title: Ordinal, allocation-aware reads - details: Columns are read by index — never by name — through SqlDataReader with SequentialAccess. Result lists are pre-sized via your declared capacity, populated through CollectionsMarshal, and pooled with ArrayPool<T> for ImmutableArray outputs. + title: SQL Server stored procedures only + details: CaeriusNet intentionally targets Microsoft SQL Server and stored procedures. It does not translate LINQ, generate SQL text, track entities, or abstract multiple database providers. + - icon: âš™ī¸ + title: Compile-time mapper generation + details: [GenerateDto] and [GenerateTvp] emit ISpMapper<T> and ITvpMapper<T> implementations at build time. Analyzer rules validate the sealed partial primary-constructor contract. + - icon: 📖 + title: Ordinal data-reader mapping + details: Result columns are read by index through SqlDataReader with SequentialAccess. Declared result-set capacity lets CaeriusNet pre-size collections for predictable materialization. - icon: đŸ“Ļ - title: Table-Valued Parameters, zero-copy - details: Pass thousands of IDs, GUIDs, or composite keys in a single round-trip. The generator emits a streaming IEnumerable<SqlDataRecord> that reuses a single record instance across rows — no DataTable, no boxing, no IN-list size limits. + title: Table-valued parameters + details: Pass large identifier or composite-key sets with SQL Server TVPs. Generated TVP mappers stream SqlDataRecord values without requiring callers to build DataTable instances. - icon: 🔀 - title: Multiple result sets in one call - details: Return up to five typed result sets from a single Stored Procedure execution. Helpers come in three collection flavours — IEnumerable, ReadOnlyCollection, ImmutableArray — destructured straight into a tuple at the call site. + title: Multiple result sets + details: Read up to five typed result sets from one stored-procedure execution. Return shapes are available for IEnumerable, ReadOnlyCollection, and ImmutableArray. - icon: 🧊 - title: Three-tier opt-in caching - details: Pick the right tier per call directly on the parameters builder. Frozen for static reference data, InMemory for short-TTL hot paths, Redis for shared distributed cache — zero overhead when not used, zero allocation on cache hits. + title: Per-call caching + details: Choose a cache tier on the parameters builder when a call is safe to cache Frozen for process-lifetime reference data, InMemory for expiring local entries, or Redis for shared distributed cache. - icon: 🔐 - title: Atomic transactions with safety nets - details: BeginTransactionAsync returns a thread-safe scope with a strict state machine (Active → Committed / RolledBack / Poisoned). Failures auto-poison and rollback on dispose; child SP spans nest under a parent TX activity for one cohesive trace per unit of work. + title: Transaction scopes + details: BeginTransactionAsync returns an explicit async scope with commit, rollback, poisoned-state handling, and tracing that groups child stored-procedure calls under a transaction activity. - icon: 📡 - title: OpenTelemetry & Aspire, native - details: A built-in ActivitySource and Meter emit OTel-compliant spans (db.system, db.operation, db.statement) and metrics (duration, executions, errors, cache lookups). WithAspireSqlServer / WithAspireRedis wire it into the Aspire dashboard in two lines. + title: Diagnostics and telemetry + details: Built-in ActivitySource, Meter, and source-generated logging expose stored-procedure metadata, durations, failures, cache events, and optional parameter-value capture. --- +::: info Important scope +CaeriusNet is for SQL Server stored procedures. Parameter names passed to StoredProcedureParametersBuilder methods use the identifier only, without the SQL @ prefix. +::: + +::: tip Benchmarks +Benchmark pages document the suites and methodology. Treat any published numbers as environment-specific; SQL Server edition, hardware, network latency, schema, query plans, and payload shape can change results. +::: + +## Documentation map + +| Area | Start here | Use it for | +|---|---|---| +| Quickstart | [Installation & Setup](/quickstart/getting-started) | Install the package, register DI, and run the first stored-procedure query. | +| Guides | [Reading Data](/documentation/reading-data) | Learn DTO mapping, TVPs, result-set shapes, caching, transactions, logging, and Aspire integration. | +| Reference | [API Reference](/documentation/api) | Look up public builders, abstractions, commands, mapper contracts, and exception behavior. | +| Diagnostics | [Diagnostic rules](/diagnostics/) | Resolve analyzer errors and warnings emitted by the source generators. | +| Examples | [Examples overview](/examples/) | Follow end-to-end stored procedure, TVP, multi-result, and transaction scenarios. | +| Benchmarks | [Performance & Benchmarks](/benchmarks/) | Understand the benchmark suites, configuration, limitations, and local run commands. | +