diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bc88fbf..d64baa2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -45,6 +45,9 @@ jobs:
- name: Restore dependencies
run: dotnet restore
+ - name: Verify formatting
+ run: dotnet format --verify-no-changes --no-restore
+
- name: Build
run: dotnet build --configuration ${{ env.CONFIGURATION }} --no-restore
diff --git a/BuildContracts.ps1 b/BuildContracts.ps1
deleted file mode 100644
index 986742f..0000000
--- a/BuildContracts.ps1
+++ /dev/null
@@ -1,71 +0,0 @@
-#!/usr/bin/env pwsh
-<#
-.SYNOPSIS
- Build only the Contracts package
-
-.DESCRIPTION
- This script builds only the Mistruna.Core.Contracts project.
- Useful for scenarios where you only need the interfaces without the full implementation.
-
-.PARAMETER Configuration
- Build configuration (Debug or Release). Default is Release.
-
-.EXAMPLE
- ./BuildContracts.ps1
-
-.EXAMPLE
- ./BuildContracts.ps1 -Configuration Debug
-#>
-
-[CmdletBinding()]
-param(
- [ValidateSet('Debug', 'Release')]
- [string]$Configuration = 'Release'
-)
-
-$ErrorActionPreference = 'Stop'
-
-$artifacts = Join-Path $PSScriptRoot 'artifacts'
-$contractsProject = Join-Path $PSScriptRoot 'src\Mistruna.Core.Contracts\Mistruna.Core.Contracts.csproj'
-
-Write-Host "Building Mistruna.Core.Contracts..." -ForegroundColor Cyan
-Write-Host "Configuration: $Configuration" -ForegroundColor Gray
-
-# Clean artifacts directory
-if (Test-Path $artifacts) {
- Write-Host "Cleaning artifacts directory..." -ForegroundColor Yellow
- Remove-Item $artifacts -Force -Recurse
-}
-
-# Restore packages
-Write-Host "Restoring packages..." -ForegroundColor Yellow
-dotnet restore $contractsProject
-
-if ($LASTEXITCODE -ne 0) {
- throw "Restore failed with exit code $LASTEXITCODE"
-}
-
-# Build project
-Write-Host "Building project..." -ForegroundColor Yellow
-dotnet build $contractsProject --configuration $Configuration --no-restore
-
-if ($LASTEXITCODE -ne 0) {
- throw "Build failed with exit code $LASTEXITCODE"
-}
-
-# Create package
-Write-Host "Creating NuGet package..." -ForegroundColor Yellow
-dotnet pack $contractsProject --configuration $Configuration --no-build --output $artifacts
-
-if ($LASTEXITCODE -ne 0) {
- throw "Pack failed with exit code $LASTEXITCODE"
-}
-
-Write-Host ""
-Write-Host "Build completed successfully!" -ForegroundColor Green
-Write-Host "Package is available in: $artifacts" -ForegroundColor Gray
-
-# List created package
-Get-ChildItem $artifacts -Filter "*.nupkg" | ForEach-Object {
- Write-Host " - $($_.Name)" -ForegroundColor Cyan
-}
diff --git a/Directory.Build.props b/Directory.Build.props
index 1341057..96d9b38 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -58,11 +58,11 @@
-
+
-
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 0000000..d415df2
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,48 @@
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Mistruna.Core.sln b/Mistruna.Core.sln
index 0294005..8963c10 100644
--- a/Mistruna.Core.sln
+++ b/Mistruna.Core.sln
@@ -15,9 +15,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.gitattributes = .gitattributes
.gitignore = .gitignore
Build.ps1 = Build.ps1
- BuildContracts.ps1 = BuildContracts.ps1
CONTRIBUTING.md = CONTRIBUTING.md
Directory.Build.props = Directory.Build.props
+ Directory.Packages.props = Directory.Packages.props
GenerateKey.ps1 = GenerateKey.ps1
LICENSE = LICENSE
NuGet.Config = NuGet.Config
@@ -28,42 +28,228 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mistruna.Core", "src\Mistruna.Core\Mistruna.Core.csproj", "{C16A8634-36D6-4353-B246-D6F43E49D92A}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mistruna.Core.Contracts", "src\Mistruna.Core.Contracts\Mistruna.Core.Contracts.csproj", "{D27B9745-47E7-4464-C357-E7A54F5AE83B}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mistruna.Core.Tests", "test\Mistruna.Core.Tests\Mistruna.Core.Tests.csproj", "{E38CA856-58F8-5575-D468-F8A65A6BF94C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mistruna.Core.Samples.BasicApi", "samples\Mistruna.Core.Samples.BasicApi\Mistruna.Core.Samples.BasicApi.csproj", "{F49DB967-69F9-6686-E579-A9176A7CAA5D}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mistruna.Core.Abstractions", "src\Mistruna.Core.Abstractions\Mistruna.Core.Abstractions.csproj", "{1A47CB5B-C85A-4902-BEB0-62A5B30EF0EE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mistruna.Core.AspNetCore", "src\Mistruna.Core.AspNetCore\Mistruna.Core.AspNetCore.csproj", "{F4CFB8FD-3423-426B-A4D6-A49C96DEB733}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mistruna.Core.EfCore", "src\Mistruna.Core.EfCore\Mistruna.Core.EfCore.csproj", "{142806A6-80F0-4283-975D-0EBA72AD2A7E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mistruna.Core.Caching.Redis", "src\Mistruna.Core.Caching.Redis\Mistruna.Core.Caching.Redis.csproj", "{4D0F006F-ED0E-443A-9BD5-F1427BBD696C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mistruna.Core.Messaging.RabbitMq", "src\Mistruna.Core.Messaging.RabbitMq\Mistruna.Core.Messaging.RabbitMq.csproj", "{E47A7412-3775-4A11-B867-D2857C0C18C5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mistruna.Core.Monetization", "src\Mistruna.Core.Monetization\Mistruna.Core.Monetization.csproj", "{37AE8F50-445E-4D6C-A933-501AE168F59A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mistruna.Core.Observability", "src\Mistruna.Core.Observability\Mistruna.Core.Observability.csproj", "{CEDB658E-54A5-476D-A30A-88EE694FD782}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mistruna.Core.Resilience", "src\Mistruna.Core.Resilience\Mistruna.Core.Resilience.csproj", "{BBA18D08-124E-4786-8EC9-AC30899B9FC5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mistruna.Core.Testing", "src\Mistruna.Core.Testing\Mistruna.Core.Testing.csproj", "{3B2A82FC-357E-47E5-BC1D-A1C8D1D8EA61}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mistruna.Core.Samples.WorkerRabbit", "samples\Mistruna.Core.Samples.WorkerRabbit\Mistruna.Core.Samples.WorkerRabbit.csproj", "{C52F80ED-8895-4CBB-B74B-63AE44D38DCF}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mistruna.Core.Samples.ApiWithOtel", "samples\Mistruna.Core.Samples.ApiWithOtel\Mistruna.Core.Samples.ApiWithOtel.csproj", "{1249DB07-16DF-4C65-9984-93CD5D62AAAF}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{C16A8634-36D6-4353-B246-D6F43E49D92A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C16A8634-36D6-4353-B246-D6F43E49D92A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C16A8634-36D6-4353-B246-D6F43E49D92A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C16A8634-36D6-4353-B246-D6F43E49D92A}.Debug|x64.Build.0 = Debug|Any CPU
+ {C16A8634-36D6-4353-B246-D6F43E49D92A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C16A8634-36D6-4353-B246-D6F43E49D92A}.Debug|x86.Build.0 = Debug|Any CPU
{C16A8634-36D6-4353-B246-D6F43E49D92A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C16A8634-36D6-4353-B246-D6F43E49D92A}.Release|Any CPU.Build.0 = Release|Any CPU
- {D27B9745-47E7-4464-C357-E7A54F5AE83B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D27B9745-47E7-4464-C357-E7A54F5AE83B}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D27B9745-47E7-4464-C357-E7A54F5AE83B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D27B9745-47E7-4464-C357-E7A54F5AE83B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C16A8634-36D6-4353-B246-D6F43E49D92A}.Release|x64.ActiveCfg = Release|Any CPU
+ {C16A8634-36D6-4353-B246-D6F43E49D92A}.Release|x64.Build.0 = Release|Any CPU
+ {C16A8634-36D6-4353-B246-D6F43E49D92A}.Release|x86.ActiveCfg = Release|Any CPU
+ {C16A8634-36D6-4353-B246-D6F43E49D92A}.Release|x86.Build.0 = Release|Any CPU
{E38CA856-58F8-5575-D468-F8A65A6BF94C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E38CA856-58F8-5575-D468-F8A65A6BF94C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E38CA856-58F8-5575-D468-F8A65A6BF94C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E38CA856-58F8-5575-D468-F8A65A6BF94C}.Debug|x64.Build.0 = Debug|Any CPU
+ {E38CA856-58F8-5575-D468-F8A65A6BF94C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E38CA856-58F8-5575-D468-F8A65A6BF94C}.Debug|x86.Build.0 = Debug|Any CPU
{E38CA856-58F8-5575-D468-F8A65A6BF94C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E38CA856-58F8-5575-D468-F8A65A6BF94C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E38CA856-58F8-5575-D468-F8A65A6BF94C}.Release|x64.ActiveCfg = Release|Any CPU
+ {E38CA856-58F8-5575-D468-F8A65A6BF94C}.Release|x64.Build.0 = Release|Any CPU
+ {E38CA856-58F8-5575-D468-F8A65A6BF94C}.Release|x86.ActiveCfg = Release|Any CPU
+ {E38CA856-58F8-5575-D468-F8A65A6BF94C}.Release|x86.Build.0 = Release|Any CPU
{F49DB967-69F9-6686-E579-A9176A7CAA5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F49DB967-69F9-6686-E579-A9176A7CAA5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F49DB967-69F9-6686-E579-A9176A7CAA5D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F49DB967-69F9-6686-E579-A9176A7CAA5D}.Debug|x64.Build.0 = Debug|Any CPU
+ {F49DB967-69F9-6686-E579-A9176A7CAA5D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F49DB967-69F9-6686-E579-A9176A7CAA5D}.Debug|x86.Build.0 = Debug|Any CPU
{F49DB967-69F9-6686-E579-A9176A7CAA5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F49DB967-69F9-6686-E579-A9176A7CAA5D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F49DB967-69F9-6686-E579-A9176A7CAA5D}.Release|x64.ActiveCfg = Release|Any CPU
+ {F49DB967-69F9-6686-E579-A9176A7CAA5D}.Release|x64.Build.0 = Release|Any CPU
+ {F49DB967-69F9-6686-E579-A9176A7CAA5D}.Release|x86.ActiveCfg = Release|Any CPU
+ {F49DB967-69F9-6686-E579-A9176A7CAA5D}.Release|x86.Build.0 = Release|Any CPU
+ {1A47CB5B-C85A-4902-BEB0-62A5B30EF0EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1A47CB5B-C85A-4902-BEB0-62A5B30EF0EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1A47CB5B-C85A-4902-BEB0-62A5B30EF0EE}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1A47CB5B-C85A-4902-BEB0-62A5B30EF0EE}.Debug|x64.Build.0 = Debug|Any CPU
+ {1A47CB5B-C85A-4902-BEB0-62A5B30EF0EE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1A47CB5B-C85A-4902-BEB0-62A5B30EF0EE}.Debug|x86.Build.0 = Debug|Any CPU
+ {1A47CB5B-C85A-4902-BEB0-62A5B30EF0EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1A47CB5B-C85A-4902-BEB0-62A5B30EF0EE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1A47CB5B-C85A-4902-BEB0-62A5B30EF0EE}.Release|x64.ActiveCfg = Release|Any CPU
+ {1A47CB5B-C85A-4902-BEB0-62A5B30EF0EE}.Release|x64.Build.0 = Release|Any CPU
+ {1A47CB5B-C85A-4902-BEB0-62A5B30EF0EE}.Release|x86.ActiveCfg = Release|Any CPU
+ {1A47CB5B-C85A-4902-BEB0-62A5B30EF0EE}.Release|x86.Build.0 = Release|Any CPU
+ {F4CFB8FD-3423-426B-A4D6-A49C96DEB733}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F4CFB8FD-3423-426B-A4D6-A49C96DEB733}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F4CFB8FD-3423-426B-A4D6-A49C96DEB733}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F4CFB8FD-3423-426B-A4D6-A49C96DEB733}.Debug|x64.Build.0 = Debug|Any CPU
+ {F4CFB8FD-3423-426B-A4D6-A49C96DEB733}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F4CFB8FD-3423-426B-A4D6-A49C96DEB733}.Debug|x86.Build.0 = Debug|Any CPU
+ {F4CFB8FD-3423-426B-A4D6-A49C96DEB733}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F4CFB8FD-3423-426B-A4D6-A49C96DEB733}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F4CFB8FD-3423-426B-A4D6-A49C96DEB733}.Release|x64.ActiveCfg = Release|Any CPU
+ {F4CFB8FD-3423-426B-A4D6-A49C96DEB733}.Release|x64.Build.0 = Release|Any CPU
+ {F4CFB8FD-3423-426B-A4D6-A49C96DEB733}.Release|x86.ActiveCfg = Release|Any CPU
+ {F4CFB8FD-3423-426B-A4D6-A49C96DEB733}.Release|x86.Build.0 = Release|Any CPU
+ {142806A6-80F0-4283-975D-0EBA72AD2A7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {142806A6-80F0-4283-975D-0EBA72AD2A7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {142806A6-80F0-4283-975D-0EBA72AD2A7E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {142806A6-80F0-4283-975D-0EBA72AD2A7E}.Debug|x64.Build.0 = Debug|Any CPU
+ {142806A6-80F0-4283-975D-0EBA72AD2A7E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {142806A6-80F0-4283-975D-0EBA72AD2A7E}.Debug|x86.Build.0 = Debug|Any CPU
+ {142806A6-80F0-4283-975D-0EBA72AD2A7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {142806A6-80F0-4283-975D-0EBA72AD2A7E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {142806A6-80F0-4283-975D-0EBA72AD2A7E}.Release|x64.ActiveCfg = Release|Any CPU
+ {142806A6-80F0-4283-975D-0EBA72AD2A7E}.Release|x64.Build.0 = Release|Any CPU
+ {142806A6-80F0-4283-975D-0EBA72AD2A7E}.Release|x86.ActiveCfg = Release|Any CPU
+ {142806A6-80F0-4283-975D-0EBA72AD2A7E}.Release|x86.Build.0 = Release|Any CPU
+ {4D0F006F-ED0E-443A-9BD5-F1427BBD696C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4D0F006F-ED0E-443A-9BD5-F1427BBD696C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4D0F006F-ED0E-443A-9BD5-F1427BBD696C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {4D0F006F-ED0E-443A-9BD5-F1427BBD696C}.Debug|x64.Build.0 = Debug|Any CPU
+ {4D0F006F-ED0E-443A-9BD5-F1427BBD696C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4D0F006F-ED0E-443A-9BD5-F1427BBD696C}.Debug|x86.Build.0 = Debug|Any CPU
+ {4D0F006F-ED0E-443A-9BD5-F1427BBD696C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4D0F006F-ED0E-443A-9BD5-F1427BBD696C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4D0F006F-ED0E-443A-9BD5-F1427BBD696C}.Release|x64.ActiveCfg = Release|Any CPU
+ {4D0F006F-ED0E-443A-9BD5-F1427BBD696C}.Release|x64.Build.0 = Release|Any CPU
+ {4D0F006F-ED0E-443A-9BD5-F1427BBD696C}.Release|x86.ActiveCfg = Release|Any CPU
+ {4D0F006F-ED0E-443A-9BD5-F1427BBD696C}.Release|x86.Build.0 = Release|Any CPU
+ {E47A7412-3775-4A11-B867-D2857C0C18C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E47A7412-3775-4A11-B867-D2857C0C18C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E47A7412-3775-4A11-B867-D2857C0C18C5}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E47A7412-3775-4A11-B867-D2857C0C18C5}.Debug|x64.Build.0 = Debug|Any CPU
+ {E47A7412-3775-4A11-B867-D2857C0C18C5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E47A7412-3775-4A11-B867-D2857C0C18C5}.Debug|x86.Build.0 = Debug|Any CPU
+ {E47A7412-3775-4A11-B867-D2857C0C18C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E47A7412-3775-4A11-B867-D2857C0C18C5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E47A7412-3775-4A11-B867-D2857C0C18C5}.Release|x64.ActiveCfg = Release|Any CPU
+ {E47A7412-3775-4A11-B867-D2857C0C18C5}.Release|x64.Build.0 = Release|Any CPU
+ {E47A7412-3775-4A11-B867-D2857C0C18C5}.Release|x86.ActiveCfg = Release|Any CPU
+ {E47A7412-3775-4A11-B867-D2857C0C18C5}.Release|x86.Build.0 = Release|Any CPU
+ {37AE8F50-445E-4D6C-A933-501AE168F59A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {37AE8F50-445E-4D6C-A933-501AE168F59A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {37AE8F50-445E-4D6C-A933-501AE168F59A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {37AE8F50-445E-4D6C-A933-501AE168F59A}.Debug|x64.Build.0 = Debug|Any CPU
+ {37AE8F50-445E-4D6C-A933-501AE168F59A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {37AE8F50-445E-4D6C-A933-501AE168F59A}.Debug|x86.Build.0 = Debug|Any CPU
+ {37AE8F50-445E-4D6C-A933-501AE168F59A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {37AE8F50-445E-4D6C-A933-501AE168F59A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {37AE8F50-445E-4D6C-A933-501AE168F59A}.Release|x64.ActiveCfg = Release|Any CPU
+ {37AE8F50-445E-4D6C-A933-501AE168F59A}.Release|x64.Build.0 = Release|Any CPU
+ {37AE8F50-445E-4D6C-A933-501AE168F59A}.Release|x86.ActiveCfg = Release|Any CPU
+ {37AE8F50-445E-4D6C-A933-501AE168F59A}.Release|x86.Build.0 = Release|Any CPU
+ {CEDB658E-54A5-476D-A30A-88EE694FD782}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CEDB658E-54A5-476D-A30A-88EE694FD782}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CEDB658E-54A5-476D-A30A-88EE694FD782}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {CEDB658E-54A5-476D-A30A-88EE694FD782}.Debug|x64.Build.0 = Debug|Any CPU
+ {CEDB658E-54A5-476D-A30A-88EE694FD782}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {CEDB658E-54A5-476D-A30A-88EE694FD782}.Debug|x86.Build.0 = Debug|Any CPU
+ {CEDB658E-54A5-476D-A30A-88EE694FD782}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CEDB658E-54A5-476D-A30A-88EE694FD782}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CEDB658E-54A5-476D-A30A-88EE694FD782}.Release|x64.ActiveCfg = Release|Any CPU
+ {CEDB658E-54A5-476D-A30A-88EE694FD782}.Release|x64.Build.0 = Release|Any CPU
+ {CEDB658E-54A5-476D-A30A-88EE694FD782}.Release|x86.ActiveCfg = Release|Any CPU
+ {CEDB658E-54A5-476D-A30A-88EE694FD782}.Release|x86.Build.0 = Release|Any CPU
+ {BBA18D08-124E-4786-8EC9-AC30899B9FC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BBA18D08-124E-4786-8EC9-AC30899B9FC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BBA18D08-124E-4786-8EC9-AC30899B9FC5}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {BBA18D08-124E-4786-8EC9-AC30899B9FC5}.Debug|x64.Build.0 = Debug|Any CPU
+ {BBA18D08-124E-4786-8EC9-AC30899B9FC5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {BBA18D08-124E-4786-8EC9-AC30899B9FC5}.Debug|x86.Build.0 = Debug|Any CPU
+ {BBA18D08-124E-4786-8EC9-AC30899B9FC5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BBA18D08-124E-4786-8EC9-AC30899B9FC5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BBA18D08-124E-4786-8EC9-AC30899B9FC5}.Release|x64.ActiveCfg = Release|Any CPU
+ {BBA18D08-124E-4786-8EC9-AC30899B9FC5}.Release|x64.Build.0 = Release|Any CPU
+ {BBA18D08-124E-4786-8EC9-AC30899B9FC5}.Release|x86.ActiveCfg = Release|Any CPU
+ {BBA18D08-124E-4786-8EC9-AC30899B9FC5}.Release|x86.Build.0 = Release|Any CPU
+ {3B2A82FC-357E-47E5-BC1D-A1C8D1D8EA61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3B2A82FC-357E-47E5-BC1D-A1C8D1D8EA61}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3B2A82FC-357E-47E5-BC1D-A1C8D1D8EA61}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3B2A82FC-357E-47E5-BC1D-A1C8D1D8EA61}.Debug|x64.Build.0 = Debug|Any CPU
+ {3B2A82FC-357E-47E5-BC1D-A1C8D1D8EA61}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3B2A82FC-357E-47E5-BC1D-A1C8D1D8EA61}.Debug|x86.Build.0 = Debug|Any CPU
+ {3B2A82FC-357E-47E5-BC1D-A1C8D1D8EA61}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3B2A82FC-357E-47E5-BC1D-A1C8D1D8EA61}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3B2A82FC-357E-47E5-BC1D-A1C8D1D8EA61}.Release|x64.ActiveCfg = Release|Any CPU
+ {3B2A82FC-357E-47E5-BC1D-A1C8D1D8EA61}.Release|x64.Build.0 = Release|Any CPU
+ {3B2A82FC-357E-47E5-BC1D-A1C8D1D8EA61}.Release|x86.ActiveCfg = Release|Any CPU
+ {3B2A82FC-357E-47E5-BC1D-A1C8D1D8EA61}.Release|x86.Build.0 = Release|Any CPU
+ {C52F80ED-8895-4CBB-B74B-63AE44D38DCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C52F80ED-8895-4CBB-B74B-63AE44D38DCF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C52F80ED-8895-4CBB-B74B-63AE44D38DCF}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C52F80ED-8895-4CBB-B74B-63AE44D38DCF}.Debug|x64.Build.0 = Debug|Any CPU
+ {C52F80ED-8895-4CBB-B74B-63AE44D38DCF}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C52F80ED-8895-4CBB-B74B-63AE44D38DCF}.Debug|x86.Build.0 = Debug|Any CPU
+ {C52F80ED-8895-4CBB-B74B-63AE44D38DCF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C52F80ED-8895-4CBB-B74B-63AE44D38DCF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C52F80ED-8895-4CBB-B74B-63AE44D38DCF}.Release|x64.ActiveCfg = Release|Any CPU
+ {C52F80ED-8895-4CBB-B74B-63AE44D38DCF}.Release|x64.Build.0 = Release|Any CPU
+ {C52F80ED-8895-4CBB-B74B-63AE44D38DCF}.Release|x86.ActiveCfg = Release|Any CPU
+ {C52F80ED-8895-4CBB-B74B-63AE44D38DCF}.Release|x86.Build.0 = Release|Any CPU
+ {1249DB07-16DF-4C65-9984-93CD5D62AAAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1249DB07-16DF-4C65-9984-93CD5D62AAAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1249DB07-16DF-4C65-9984-93CD5D62AAAF}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1249DB07-16DF-4C65-9984-93CD5D62AAAF}.Debug|x64.Build.0 = Debug|Any CPU
+ {1249DB07-16DF-4C65-9984-93CD5D62AAAF}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1249DB07-16DF-4C65-9984-93CD5D62AAAF}.Debug|x86.Build.0 = Debug|Any CPU
+ {1249DB07-16DF-4C65-9984-93CD5D62AAAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1249DB07-16DF-4C65-9984-93CD5D62AAAF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1249DB07-16DF-4C65-9984-93CD5D62AAAF}.Release|x64.ActiveCfg = Release|Any CPU
+ {1249DB07-16DF-4C65-9984-93CD5D62AAAF}.Release|x64.Build.0 = Release|Any CPU
+ {1249DB07-16DF-4C65-9984-93CD5D62AAAF}.Release|x86.ActiveCfg = Release|Any CPU
+ {1249DB07-16DF-4C65-9984-93CD5D62AAAF}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{C16A8634-36D6-4353-B246-D6F43E49D92A} = {3E1E8F48-6D0E-4F3E-A1B9-8C2A5B4D7E9F}
- {D27B9745-47E7-4464-C357-E7A54F5AE83B} = {3E1E8F48-6D0E-4F3E-A1B9-8C2A5B4D7E9F}
{E38CA856-58F8-5575-D468-F8A65A6BF94C} = {4F2F9A59-7E1F-42A4-B2C0-9D3B6C5E8FA0}
{F49DB967-69F9-6686-E579-A9176A7CAA5D} = {5A3B0A6A-8F2A-43A5-C3D1-AE4C7D6F9AB1}
+ {1A47CB5B-C85A-4902-BEB0-62A5B30EF0EE} = {3E1E8F48-6D0E-4F3E-A1B9-8C2A5B4D7E9F}
+ {F4CFB8FD-3423-426B-A4D6-A49C96DEB733} = {3E1E8F48-6D0E-4F3E-A1B9-8C2A5B4D7E9F}
+ {142806A6-80F0-4283-975D-0EBA72AD2A7E} = {3E1E8F48-6D0E-4F3E-A1B9-8C2A5B4D7E9F}
+ {4D0F006F-ED0E-443A-9BD5-F1427BBD696C} = {3E1E8F48-6D0E-4F3E-A1B9-8C2A5B4D7E9F}
+ {E47A7412-3775-4A11-B867-D2857C0C18C5} = {3E1E8F48-6D0E-4F3E-A1B9-8C2A5B4D7E9F}
+ {37AE8F50-445E-4D6C-A933-501AE168F59A} = {3E1E8F48-6D0E-4F3E-A1B9-8C2A5B4D7E9F}
+ {CEDB658E-54A5-476D-A30A-88EE694FD782} = {3E1E8F48-6D0E-4F3E-A1B9-8C2A5B4D7E9F}
+ {BBA18D08-124E-4786-8EC9-AC30899B9FC5} = {3E1E8F48-6D0E-4F3E-A1B9-8C2A5B4D7E9F}
+ {3B2A82FC-357E-47E5-BC1D-A1C8D1D8EA61} = {3E1E8F48-6D0E-4F3E-A1B9-8C2A5B4D7E9F}
+ {C52F80ED-8895-4CBB-B74B-63AE44D38DCF} = {5A3B0A6A-8F2A-43A5-C3D1-AE4C7D6F9AB1}
+ {1249DB07-16DF-4C65-9984-93CD5D62AAAF} = {5A3B0A6A-8F2A-43A5-C3D1-AE4C7D6F9AB1}
EndGlobalSection
EndGlobal
diff --git a/README.md b/README.md
index 1102492..fc54309 100644
--- a/README.md
+++ b/README.md
@@ -2,180 +2,109 @@
[](https://github.com/mistruna/Mistruna.Core/actions?query=workflow%3ACI)
[](https://www.nuget.org/packages/Mistruna.Core)
-[](https://www.nuget.org/packages/Mistruna.Core)
-[](https://opensource.org/licenses/MIT)
+[](LICENSE)
-**Production-oriented SDK for ASP.NET Core microservices** — MediatR CQRS, validation, JWT, RabbitMQ, Redis, health checks, rate limiting, idempotency, usage metering, and consistent API primitives.
+Production-oriented .NET building blocks for ASP.NET Core microservices: CQRS, validation, ProblemDetails, persistence, Redis, RabbitMQ, observability, resilience, and optional monetization primitives.
-[Quick start](#quick-start) • [Packages](#packages) • [CQRS](#cqrs-icommand--iquery) • [Features](#features) • [Samples](samples/)
-
----
+Mistruna.Core v5 is a package-based redesign. Version 4 is frozen and receives no new features; see the [v5 migration guide](docs/migration-v5.md).
## Packages
-| Package | Target | Purpose |
-|---------|--------|---------|
-| `Mistruna.Core` | .NET 10 | Full SDK implementation |
-| `Mistruna.Core.Contracts` | .NET Standard 2.0 | Interfaces, entities, `Result`, specifications |
+| Package | TFM | Responsibility |
+|---------|-----|----------------|
+| `Mistruna.Core.Abstractions` | `net8.0;net10.0` | Results, CQRS markers, entity/value-object bases, persistence contracts, domain events, and pagination DTOs |
+| `Mistruna.Core` | `net10.0` | MediatR registration, validation and logging behaviors, exceptions, and core DI |
+| `Mistruna.Core.AspNetCore` | `net10.0` | ProblemDetails, health, versioning, CORS, JWT, and IP rate limiting |
+| `Mistruna.Core.EfCore` | `net10.0` | EF repositories, unit of work, and conventions |
+| `Mistruna.Core.Caching.Redis` | `net10.0` | Redis caching and health check |
+| `Mistruna.Core.Messaging.RabbitMq` | `net10.0` | RabbitMQ.Client 7 asynchronous bus |
+| `Mistruna.Core.Monetization` | `net10.0` | API keys, plan authorization, tiered rate limits, idempotency, and metering |
+| `Mistruna.Core.Observability` | `net10.0` | OpenTelemetry registration and MediatR activity enrichment |
+| `Mistruna.Core.Resilience` | `net10.0` | HttpClient resilience presets and marked-command retry behavior |
+| `Mistruna.Core.Testing` | `net10.0` | EF async providers and consumer test helpers |
+
+There is no umbrella meta-package. Add only the packages your service uses:
```bash
dotnet add package Mistruna.Core
-# optional contracts-only package for shared libraries:
-dotnet add package Mistruna.Core.Contracts
+dotnet add package Mistruna.Core.AspNetCore
+dotnet add package Mistruna.Core.EfCore
```
----
-
## Quick start
```csharp
-// Program.cs
-builder.Services.AddCore(typeof(Program).Assembly);
-builder.Services.AddCoreHealthChecks();
-
-var app = builder.Build();
-app.UseCoreMiddlewares();
-app.MapHealthChecks("/health");
-```
-
-Define handlers with CQRS markers:
-
-```csharp
-public sealed record GetUserQuery(Guid Id) : IQuery;
+using Mistruna.Core.DependencyInjection;
+using Mistruna.Core.AspNetCore.DependencyInjection;
+using Mistruna.Core.EfCore;
-public sealed class CreateUserCommand : ICommand
+builder.Services.AddMistrunaCore(options =>
{
- public string Email { get; init; } = string.Empty;
-}
-```
-
-Run the sample API:
+ options.RegisterAssemblies(typeof(Program).Assembly);
+ options.AddValidation();
+ options.AddLoggingBehavior();
+});
+builder.Services.AddMistrunaAspNetCore();
+builder.Services.AddMistrunaEfCore();
-```bash
-dotnet run --project samples/Mistruna.Core.Samples.BasicApi
+var app = builder.Build();
+app.UseMistrunaExceptionHandler();
```
----
-
-## CQRS (`ICommand` / `IQuery`)
-
-Mistruna.Core ships MediatR marker interfaces in `Mistruna.Core.Abstractions`:
-
-- **`ICommand` / `ICommand`** — write operations (create, update, delete, auth flows)
-- **`IQuery`** — read-only operations
-
-Use `RequestKind` in custom pipeline behaviors:
+Optional integrations compose independently:
```csharp
-if (!RequestKind.IsCommand(typeof(TRequest)))
- return await next();
+builder.Services.AddMistrunaRedis(builder.Configuration);
+builder.Services.AddMistrunaRabbitMq(builder.Configuration);
+builder.Services.AddMistrunaObservability(builder.Configuration);
+builder.Services.AddMistrunaResilience();
+builder.Services.AddMistrunaMonetization();
+
+app.UseMistrunaTieredRateLimiting();
+app.UseMistrunaIdempotency();
+app.UseMistrunaUsageMetering();
```
-This keeps audit logging, metering, and side effects off read paths.
-
----
-
-## Features
-
-### Application core
-- MediatR registration (`AddCore`)
-- FluentValidation pipeline (`RequestValidationBehavior`)
-- Structured request logging (`LoggingBehavior`)
-- Centralized exception middleware (`UseCoreMiddlewares`)
-- Custom exceptions mapped to HTTP status codes
-
-### Domain & data (Contracts + Core)
-- `Result` / `Error` functional errors
-- Entity base types (`Entity`, `AuditableEntity`, `SoftDeletableEntity`)
-- Value objects (`Email`, `PhoneNumber`, `Money`, `Address`, `DateRange`)
-- Specification pattern + `EfGenericRepository` / `EfUnitOfWork`
-- Domain event contracts
-
-### Infrastructure
-- JWT authentication helpers
-- RabbitMQ integration
-- Redis caching (`AddRedisCaching`)
-- Health checks (`AddCoreHealthChecks`, `AddDatabaseHealthCheck`, `AddRedisHealthCheck`)
-- IP rate limiting (`UseRateLimiting`)
-
-### Monetization primitives (optional, compose as needed)
-- API key authentication (`AddMistrunaApiKeyAuthentication`)
-- Plan-based authorization with HTTP 402 (`AddMistrunaPlanAuthorization`, `[RequiresPlan]`)
-- Tiered rate limiting (`AddMistrunaTieredRateLimiting`, `UseTieredRateLimiting`)
-- Idempotency middleware (`AddMistrunaIdempotency`, `UseIdempotency`)
-- Usage metering (`AddMistrunaUsageMetering`, `UseUsageMetering`)
-
-### Testing helpers
-- `TestAsyncQueryProvider` / `TestAsyncEnumerable` — async EF Core repository tests without a database
-
----
-
-## Exception handling
-
-`UseCoreMiddlewares()` maps exceptions to HTTP responses:
-
-| Exception | Status |
-|-----------|--------|
-| `ValidationException` | 400 |
-| `NotFoundException` | 404 |
-| `UnauthorizedAccessException` | 401 |
-| `ForbiddenAccessException` | 403 |
-| `ConflictException` | 409 |
-| `TimeoutException` | 408 |
-
----
-
-## Health checks
+Define requests with the markers from `Mistruna.Core.Abstractions`:
```csharp
-builder.Services
- .AddCoreHealthChecks(builder => builder
- .AddDatabaseHealthCheck()
- .AddRedisHealthCheck());
+public sealed record GetUserQuery(Guid Id) : IQuery;
+public sealed record CreateUserCommand(string Email) : ICommand;
```
----
+## Foundation features
-## Building from source
+- System.Text.Json-only serialization and RFC 7807 ProblemDetails
+- MediatR command/query markers with opt-in validation and logging behaviors
+- EF Core repository, specification, and unit-of-work implementations
+- Redis caching and health checks
+- RabbitMQ.Client 7 async publishing via `IMistrunaRabbitBus.PublishAsync`
+- OpenTelemetry tracing, metrics, and MediatR activity enrichment
+- HttpClient resilience presets and optional marked-command retries
+- API key authentication, plan authorization, rate limits, idempotency, and metering
+- EF async query providers and result assertions for consumer tests
-**Prerequisites:** [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
+## Samples
```powershell
-./Build.ps1 # build, test, pack
-./BuildContracts.ps1 # contracts package only
-./Push.ps1 -ApiKey "your-api-key"
+dotnet run --project samples/Mistruna.Core.Samples.BasicApi
+dotnet run --project samples/Mistruna.Core.Samples.WorkerRabbit
+dotnet run --project samples/Mistruna.Core.Samples.ApiWithOtel
```
-Place the NuGet icon at `assets/logo/mistruna_128x128.png` before publishing packages (optional for local builds).
+The RabbitMQ sample requires a broker. See [samples/README.md](samples/README.md) for configuration.
----
+## Build
-## Project structure
+Prerequisite: [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
-```
-Mistruna.Core/
-├── src/
-│ ├── Mistruna.Core/ # Main SDK
-│ │ ├── Abstractions/ # ICommand, IQuery, RequestKind
-│ │ ├── Authentication/ # API key auth
-│ │ ├── Authorization/ # Plan-based authorization
-│ │ ├── Base/ # EF repository helpers
-│ │ ├── Extensions/ # DI and middleware registration
-│ │ ├── Filters/ # MediatR pipeline behaviors
-│ │ ├── HealthChecks/ # DB / Redis health checks
-│ │ ├── Idempotency/ # Idempotency middleware
-│ │ ├── Metering/ # Usage metering
-│ │ ├── Microservices/ # JWT, RabbitMQ, Swagger
-│ │ ├── Middlewares/ # Exception handling
-│ │ ├── Providers/ # EF async test helpers
-│ │ └── RateLimiting/ # IP + tiered rate limits
-│ └── Mistruna.Core.Contracts/ # Shared contracts
-├── test/Mistruna.Core.Tests/
-├── samples/Mistruna.Core.Samples.BasicApi/
-└── assets/logo/ # Package icon (add before publish)
+```powershell
+dotnet format --verify-no-changes
+dotnet test
+dotnet pack --configuration Release --output ./artifacts
```
----
+`./Build.ps1` runs restore, build, test, and pack. Packages are written to `artifacts/`.
## Contributing
diff --git a/docs/superpowers/specs/2026-07-23-mistruna-core-foundation-v5-design.md b/docs/superpowers/specs/2026-07-23-mistruna-core-foundation-v5-design.md
new file mode 100644
index 0000000..6dcc547
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-23-mistruna-core-foundation-v5-design.md
@@ -0,0 +1,258 @@
+# Mistruna.Core Foundation v5 — Design Spec
+
+**Date:** 2026-07-23
+**Status:** Approved (brainstorm)
+**Scope:** Foundation wave only (modular hard-cut rewrite)
+**Out of scope:** Brand/docs site wave, consumer service migration PRs, source generators / deep platform features
+
+---
+
+## 1. Goals
+
+Ship an open-source–grade **v5** of Mistruna.Core that millions of ASP.NET microservice developers can adopt:
+
+- Clear package boundaries (pay only for what you use)
+- Modern .NET 10 / System.Text.Json baseline
+- Aggressive removal of domain junk and legacy abstractions
+- Hard-cut public API with a migration guide
+- Essential platform capabilities: OpenTelemetry + HTTP resilience as opt-in packages
+
+Non-goals for this wave: visual rebrand, DocFX/site, migrating `user-service` / `currency-service` in-repo, MassTransit-class broker abstraction, Roslyn analyzers pack.
+
+---
+
+## 2. Decisions (locked)
+
+| Topic | Choice |
+|-------|--------|
+| First milestone | Foundation (structure, API, deps, tests, DX) |
+| Breaking changes | Hard cut (no compatibility shims) |
+| Packaging | Thin core + opt-in packages |
+| Package IDs | Keep `Mistruna.*` |
+| Trim policy | Aggressive — universal microservice primitives only |
+| Stack baseline | net10 host packages; Abstractions `net8.0;net10.0`; STJ; RabbitMQ.Client 7.x |
+| New capabilities in Foundation | Observability + Resilience opt-in packages |
+| Implementation approach | Package-first rewrite (no strangler meta-package) |
+
+---
+
+## 3. Package architecture
+
+### 3.1 Packages
+
+| Package | TFM | Responsibility |
+|---------|-----|----------------|
+| `Mistruna.Core.Abstractions` | `net8.0;net10.0` | `Result`/`Error`, CQRS markers, entity/VO bases, persistence interfaces, domain events, pagination DTOs |
+| `Mistruna.Core` | `net10.0` | MediatR registration, FluentValidation + logging pipeline behaviors, exception types, core DI |
+| `Mistruna.Core.AspNetCore` | `net10.0` | ProblemDetails exception handling, health/versioning/CORS/JWT helpers, IP rate limiting |
+| `Mistruna.Core.EfCore` | `net10.0` | `EfGenericRepository`, `EfUnitOfWork`, EF conventions helpers |
+| `Mistruna.Core.Caching.Redis` | `net10.0` | Redis cache implementation + health check |
+| `Mistruna.Core.Messaging.RabbitMq` | `net10.0` | Async RabbitMQ.Client 7 bus (rewrite) |
+| `Mistruna.Core.Monetization` | `net10.0` | API key auth, plan authorization (402), tiered rate limits, idempotency, usage metering |
+| `Mistruna.Core.Observability` | `net10.0` | OpenTelemetry setup hooks + MediatR Activity enrichment |
+| `Mistruna.Core.Resilience` | `net10.0` | HttpClient resilience presets + optional marked-command MediatR retry |
+| `Mistruna.Core.Testing` | `net10.0` | EF async test providers and consumer test helpers |
+
+No umbrella meta-package that references everything.
+
+### 3.2 Dependency graph
+
+```
+Mistruna.Core.Abstractions
+ ↑
+ Mistruna.Core
+ ↑
+ Mistruna.Core.AspNetCore
+ ↑
+ EfCore / Caching.Redis / Messaging.RabbitMq /
+ Monetization / Observability / Resilience
+ ↑
+ Mistruna.Core.Testing (refs Core + EfCore as needed)
+```
+
+### 3.3 Removed from SDK (not ported)
+
+- `Currency` enum, `CurrencyExtensions`, `CurrencyInfo` (domain; belongs in currency-service)
+- `AuthResponse` and other app-specific DTOs currently in Contracts
+- Newtonsoft.Json usage and package references
+- Legacy sync `RabbitManager` / `IModel` pool design (replaced by new async API)
+- BCL duplicate extension methods (`DistinctBy`, `ToHashSet`, `Append`/`Prepend` where BCL already provides them)
+- Untested / unused DI `TryAdd*` / `Decorate` helpers (keep only if tests + sample prove need)
+- Custom `ErrorResponse` JSON envelope (replaced by ProblemDetails)
+
+### 3.4 Kept (possibly relocated)
+
+- Value objects: `Email`, `PhoneNumber`, `Money`, `Address`, `DateRange` → Abstractions
+- Specification + repository interfaces → Abstractions; EF implementations → EfCore
+- Monetization feature set → dedicated package (cleaned `AddMistruna*` surface)
+
+---
+
+## 4. Public API & DI surface
+
+### 4.1 Naming
+
+- Prefer `AddMistruna*` / `UseMistruna*` over generic `AddCore` / `UseCoreMiddlewares`
+- Namespaces align with packages (e.g. `Mistruna.Core.DependencyInjection`, `Mistruna.Core.Abstractions.Results`)
+- Former `Mistruna.Core.Contracts.*` becomes `Mistruna.Core.Abstractions.*`
+
+### 4.2 Canonical composition
+
+```csharp
+builder.Services.AddMistrunaCore(opts =>
+{
+ opts.RegisterAssemblies(typeof(Program).Assembly);
+ opts.AddValidation();
+ opts.AddLoggingBehavior();
+});
+
+builder.Services.AddMistrunaAspNetCore();
+builder.Services.AddMistrunaEfCore();
+builder.Services.AddMistrunaRedis(configuration);
+builder.Services.AddMistrunaRabbitMq(configuration);
+builder.Services.AddMistrunaObservability(configuration);
+builder.Services.AddMistrunaResilience();
+// builder.Services.AddMistrunaMonetization(); // optional
+
+var app = builder.Build();
+app.UseMistrunaExceptionHandler();
+```
+
+No single `UseMistruna()` that enables everything implicitly.
+
+### 4.3 CQRS
+
+- `ICommand`, `ICommand`, `IQuery` live in Abstractions
+- `RequestKind` retained for pipeline targeting
+- MediatR remains the mediator; Core does not fork MediatR APIs
+- FluentValidation behavior maps failures to HTTP 400 ProblemDetails via exception handler
+
+### 4.4 Result vs exceptions
+
+- Expected domain failures → `Result` / `Result`
+- HTTP/infrastructure boundary → typed exceptions and/or `Result`→ProblemDetails helpers in AspNetCore
+- AspNetCore provides `ToProblemDetails()` / minimal-API result helpers
+
+### 4.5 Serialization & OpenAPI
+
+- System.Text.Json only
+- OpenAPI: keep Swashbuckle on STJ for Foundation (re-evaluate OpenAPI.NET in a later wave if needed)
+- JWT helpers stay in AspNetCore; API keys / plans stay in Monetization
+
+### 4.6 API quality rules
+
+- Public members require XML docs
+- Untested public surface is deleted or made internal
+- `InternalsVisibleTo` limited to Testing package + unit test assembly
+- Prefer extension-method registration; middleware types not part of casual public surface unless needed for advanced scenarios (`EditorBrowsable` / docs)
+
+---
+
+## 5. Observability
+
+Package: `Mistruna.Core.Observability`
+
+- `AddMistrunaObservability(IConfiguration)` wires OpenTelemetry tracing + metrics using standard ASP.NET / Http instrumentation packages
+- Exporters are configuration-driven; do not hard-depend on a single vendor exporter in the library package graph beyond what is required to compile samples
+- MediatR `ActivitySource` name: `mistruna.mediator`
+- Activity tags: `request.type`, `request.kind` (`command`|`query`), outcome/error
+- Exception handler includes `traceId` in ProblemDetails extensions
+
+---
+
+## 6. Resilience
+
+Package: `Mistruna.Core.Resilience`
+
+- Built on `Microsoft.Extensions.Http.Resilience` / Polly v8
+- `AddMistrunaResilience()` registers named HttpClient pipeline presets: `Standard`, `Aggressive`, `Disable`
+- Optional MediatR behavior retries only requests marked with `[Resilient]` or a marker interface — never global retry on all commands
+- All behavior is opt-in and documented
+
+---
+
+## 7. Messaging rewrite (RabbitMQ)
+
+Package: `Mistruna.Core.Messaging.RabbitMq`
+
+- RabbitMQ.Client **7.x**, async-first (`IConnection` / `IChannel`)
+- STJ serialization
+- Replace legacy `IRabbitManager` sync API with a small explicit bus surface (publish + consume registration)
+- Unit-test topology/serialization without requiring a broker in CI; Testcontainers optional follow-up
+
+---
+
+## 8. Samples (Foundation minimum)
+
+1. `samples/Mistruna.Core.Samples.BasicApi` — Core + AspNetCore + CQRS + ProblemDetails
+2. `samples/Mistruna.Core.Samples.WorkerRabbit` — publish/consume
+3. `samples/Mistruna.Core.Samples.ApiWithOtel` — Observability + Resilience HttpClient
+
+Full documentation site is Wave Brand, not Foundation. Foundation ships root README package table + `docs/migration-v5.md`.
+
+---
+
+## 9. Testing
+
+- Single test project `tests/Mistruna.Core.Tests` with folder-per-package layout
+- Required coverage areas:
+ - Result/Error
+ - Validation + logging behaviors
+ - Exception handler → ProblemDetails (status, errorCode, traceId)
+ - EfCore repository/UoW (InMemory or SQLite)
+ - Redis via fake/`IDistributedCache` (no mandatory Redis in CI)
+ - Rabbit serialization/topology unit tests
+ - Monetization middleware suite (port existing)
+ - Observability Activity tags
+ - Resilience HttpClient registration
+- Testing package smoke-tested
+- Keep `TreatWarningsAsErrors`; reduce blanket nullability `NoWarn` on public API
+- Recommended: `PublicApiAnalyzers` baselines per packable project
+
+---
+
+## 10. CI / Release / Versioning
+
+- CI: restore → `dotnet format --verify-no-changes` → build → test (coverage upload) → pack all packable projects
+- Fail if packable projects lack required package metadata (README/license/icon as applicable)
+- Release on tag `v5.0.0` (MinVer prefix `v`) pushes all nupkgs + snupkgs
+- All modules share the same release-train version
+- v4.x line: frozen on NuGet; README states migrate to v5 packages
+- Dependabot retained; group Microsoft.* / OpenTelemetry.* where practical
+
+---
+
+## 11. Migration path
+
+Mandatory doc: `docs/migration-v5.md` covering:
+
+1. Old → new package reference map
+2. Namespace / API rename table (`AddCore` → `AddMistrunaCore`, etc.)
+3. Newtonsoft → STJ
+4. Currency removal guidance
+5. Rabbit old manager → new async bus
+6. `ErrorResponse` → ProblemDetails
+7. Checklist for `user-service` / `currency-service` (executed as follow-up outside this Core Foundation merge)
+
+Consumer migrations are **not** part of the Foundation implementation merge in Mistruna.Core.
+
+---
+
+## 12. Success criteria
+
+Foundation is done when:
+
+- [ ] Solution builds on .NET 10; all tests green
+- [ ] All section 3.1 packages exist, pack, and appear in README table
+- [ ] Currency / Newtonsoft / legacy Rabbit manager are gone
+- [ ] BasicApi + WorkerRabbit + ApiWithOtel samples run
+- [ ] `docs/migration-v5.md` complete
+- [ ] Repository is tag-ready for `v5.0.0`
+
+---
+
+## 13. Follow-up waves (not this spec)
+
+1. **Brand & Docs** — identity, docs site, contributor UX polish
+2. **Platform+** — source generators, analyzers, deeper conventions
+3. **Ecosystem** — migrate GitLab services to v5, community/release communication
diff --git a/samples/Mistruna.Core.Samples.ApiWithOtel/Mistruna.Core.Samples.ApiWithOtel.csproj b/samples/Mistruna.Core.Samples.ApiWithOtel/Mistruna.Core.Samples.ApiWithOtel.csproj
new file mode 100644
index 0000000..41bc374
--- /dev/null
+++ b/samples/Mistruna.Core.Samples.ApiWithOtel/Mistruna.Core.Samples.ApiWithOtel.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net10.0
+ false
+ false
+
+
+
+
+
+
+
+
+
diff --git a/samples/Mistruna.Core.Samples.ApiWithOtel/Program.cs b/samples/Mistruna.Core.Samples.ApiWithOtel/Program.cs
new file mode 100644
index 0000000..19b7918
--- /dev/null
+++ b/samples/Mistruna.Core.Samples.ApiWithOtel/Program.cs
@@ -0,0 +1,33 @@
+using Mistruna.Core.DependencyInjection;
+using Mistruna.Core.Observability.DependencyInjection;
+using Mistruna.Core.Resilience.DependencyInjection;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddMistrunaCore(options =>
+ options.RegisterAssemblies(typeof(Program).Assembly));
+builder.Services.AddMistrunaObservability(builder.Configuration);
+builder.Services.AddMistrunaResilience();
+builder.Services
+ .AddHttpClient("upstream", client =>
+ client.BaseAddress = new Uri("https://example.com"))
+ .AddMistrunaResilienceHandler();
+
+var app = builder.Build();
+
+app.MapGet("/", () => Results.Ok(new
+{
+ message = "Mistruna observability and resilience sample",
+ upstreamEndpoint = "/upstream"
+}));
+
+app.MapGet("/upstream", async (
+ IHttpClientFactory httpClientFactory,
+ CancellationToken cancellationToken) =>
+{
+ var client = httpClientFactory.CreateClient("upstream");
+ var content = await client.GetStringAsync("/", cancellationToken);
+ return Results.Text(content, "text/html");
+});
+
+await app.RunAsync();
diff --git a/samples/Mistruna.Core.Samples.ApiWithOtel/appsettings.json b/samples/Mistruna.Core.Samples.ApiWithOtel/appsettings.json
new file mode 100644
index 0000000..ecd0de3
--- /dev/null
+++ b/samples/Mistruna.Core.Samples.ApiWithOtel/appsettings.json
@@ -0,0 +1,15 @@
+{
+ "OpenTelemetry": {
+ "ServiceName": "Mistruna.Core.Samples.ApiWithOtel",
+ "Otlp": {
+ "Endpoint": "http://localhost:4317"
+ }
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/samples/Mistruna.Core.Samples.BasicApi/Features/Counter/IncrementCounterCommand.cs b/samples/Mistruna.Core.Samples.BasicApi/Features/Counter/IncrementCounterCommand.cs
index a93a1b2..4b28927 100644
--- a/samples/Mistruna.Core.Samples.BasicApi/Features/Counter/IncrementCounterCommand.cs
+++ b/samples/Mistruna.Core.Samples.BasicApi/Features/Counter/IncrementCounterCommand.cs
@@ -1,5 +1,5 @@
using MediatR;
-using Mistruna.Core.Abstractions;
+using Mistruna.Core.Abstractions.Cqrs;
namespace Mistruna.Core.Samples.BasicApi.Features.Counter;
diff --git a/samples/Mistruna.Core.Samples.BasicApi/Features/Ping/PingQuery.cs b/samples/Mistruna.Core.Samples.BasicApi/Features/Ping/PingQuery.cs
index c4ff4bb..a4d3466 100644
--- a/samples/Mistruna.Core.Samples.BasicApi/Features/Ping/PingQuery.cs
+++ b/samples/Mistruna.Core.Samples.BasicApi/Features/Ping/PingQuery.cs
@@ -1,5 +1,5 @@
using MediatR;
-using Mistruna.Core.Abstractions;
+using Mistruna.Core.Abstractions.Cqrs;
namespace Mistruna.Core.Samples.BasicApi.Features.Ping;
diff --git a/samples/Mistruna.Core.Samples.BasicApi/Mistruna.Core.Samples.BasicApi.csproj b/samples/Mistruna.Core.Samples.BasicApi/Mistruna.Core.Samples.BasicApi.csproj
index 230fcb9..e57861a 100644
--- a/samples/Mistruna.Core.Samples.BasicApi/Mistruna.Core.Samples.BasicApi.csproj
+++ b/samples/Mistruna.Core.Samples.BasicApi/Mistruna.Core.Samples.BasicApi.csproj
@@ -14,13 +14,15 @@
+
+
-
-
-
+
+
+
diff --git a/samples/Mistruna.Core.Samples.BasicApi/Program.cs b/samples/Mistruna.Core.Samples.BasicApi/Program.cs
index c78f8bc..1fdae51 100644
--- a/samples/Mistruna.Core.Samples.BasicApi/Program.cs
+++ b/samples/Mistruna.Core.Samples.BasicApi/Program.cs
@@ -1,5 +1,7 @@
using MediatR;
-using Mistruna.Core.Extensions;
+using Mistruna.Core.AspNetCore.DependencyInjection;
+using Mistruna.Core.AspNetCore.HealthChecks;
+using Mistruna.Core.DependencyInjection;
using Mistruna.Core.Samples.BasicApi.Features.Counter;
using Mistruna.Core.Samples.BasicApi.Features.Ping;
@@ -14,14 +16,20 @@ public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
- builder.Services.AddCore(typeof(Program).Assembly);
- builder.Services.AddCoreHealthChecks();
+ builder.Services.AddMistrunaCore(options =>
+ {
+ options.RegisterAssemblies(typeof(Program).Assembly);
+ options.AddValidation();
+ options.AddLoggingBehavior();
+ });
+ builder.Services.AddMistrunaAspNetCore();
+ builder.Services.AddMistrunaHealthChecks();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
- app.UseCoreMiddlewares();
+ app.UseMistrunaExceptionHandler();
if (app.Environment.IsDevelopment())
{
@@ -46,7 +54,7 @@ public static void Main(string[] args)
app.MapGet("/errors/not-found", () =>
{
- throw new Exceptions.NotFoundException("Sample resource not found");
+ throw new Exceptions.NotFoundException("Sample resource not found", "X.NotFound");
});
app.Run();
diff --git a/samples/Mistruna.Core.Samples.WorkerRabbit/Mistruna.Core.Samples.WorkerRabbit.csproj b/samples/Mistruna.Core.Samples.WorkerRabbit/Mistruna.Core.Samples.WorkerRabbit.csproj
new file mode 100644
index 0000000..fa90b4e
--- /dev/null
+++ b/samples/Mistruna.Core.Samples.WorkerRabbit/Mistruna.Core.Samples.WorkerRabbit.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net10.0
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/Mistruna.Core.Samples.WorkerRabbit/Program.cs b/samples/Mistruna.Core.Samples.WorkerRabbit/Program.cs
new file mode 100644
index 0000000..9d6c6b6
--- /dev/null
+++ b/samples/Mistruna.Core.Samples.WorkerRabbit/Program.cs
@@ -0,0 +1,20 @@
+using Mistruna.Core.Messaging.RabbitMq.DependencyInjection;
+using Mistruna.Core.Samples.WorkerRabbit;
+
+var builder = Host.CreateApplicationBuilder(args);
+
+builder.Services.AddMistrunaRabbitMq(builder.Configuration);
+
+if (builder.Configuration.GetValue("Mistruna:RabbitMq:EnableConsumer", false))
+{
+ builder.Services.AddMistrunaRabbitConsumer(
+ queue: "sample.orders",
+ exchange: "sample",
+ routingKey: "orders.submitted");
+}
+else
+{
+ builder.Services.AddHostedService();
+}
+
+await builder.Build().RunAsync();
diff --git a/samples/Mistruna.Core.Samples.WorkerRabbit/RabbitWorker.cs b/samples/Mistruna.Core.Samples.WorkerRabbit/RabbitWorker.cs
new file mode 100644
index 0000000..b35e038
--- /dev/null
+++ b/samples/Mistruna.Core.Samples.WorkerRabbit/RabbitWorker.cs
@@ -0,0 +1,36 @@
+using Microsoft.Extensions.Options;
+using Mistruna.Core.Messaging.RabbitMq;
+
+namespace Mistruna.Core.Samples.WorkerRabbit;
+
+public sealed record OrderSubmitted(Guid OrderId, DateTimeOffset SubmittedAt);
+
+public sealed class OrderSubmittedHandler(ILogger logger)
+ : IMistrunaRabbitMessageHandler
+{
+ public Task HandleAsync(
+ OrderSubmitted message,
+ CancellationToken cancellationToken = default)
+ {
+ logger.LogInformation(
+ "Received order {OrderId} submitted at {SubmittedAt}.",
+ message.OrderId,
+ message.SubmittedAt);
+ return Task.CompletedTask;
+ }
+}
+
+public sealed class ConfigurationReadyWorker(
+ IOptions options,
+ ILogger logger)
+ : BackgroundService
+{
+ protected override Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ logger.LogInformation(
+ "RabbitMQ configuration loaded for {HostName}:{Port}. Set Mistruna__RabbitMq__EnableConsumer=true to connect.",
+ options.Value.HostName,
+ options.Value.Port);
+ return Task.Delay(Timeout.InfiniteTimeSpan, stoppingToken);
+ }
+}
diff --git a/samples/Mistruna.Core.Samples.WorkerRabbit/appsettings.json b/samples/Mistruna.Core.Samples.WorkerRabbit/appsettings.json
new file mode 100644
index 0000000..d8bd346
--- /dev/null
+++ b/samples/Mistruna.Core.Samples.WorkerRabbit/appsettings.json
@@ -0,0 +1,13 @@
+{
+ "Mistruna": {
+ "RabbitMq": {
+ "EnableConsumer": false,
+ "HostName": "localhost",
+ "Port": 5672,
+ "UserName": "guest",
+ "Password": "guest",
+ "VirtualHost": "/",
+ "ClientProvidedName": "Mistruna.Core.Samples.WorkerRabbit"
+ }
+ }
+}
diff --git a/samples/README.md b/samples/README.md
index 59e0f41..6435389 100644
--- a/samples/README.md
+++ b/samples/README.md
@@ -6,10 +6,10 @@ Sample projects demonstrating Mistruna.Core usage.
Demonstrates:
-- `AddCore()` — MediatR + FluentValidation + pipeline behaviors
+- `AddMistrunaCore()` — MediatR + FluentValidation + pipeline behaviors
- CQRS markers — `IQuery` and `ICommand`
-- `UseCoreMiddlewares()` — centralized exception handling
-- `AddCoreHealthChecks()` — `/health` endpoint
+- `UseMistrunaExceptionHandler()` — ProblemDetails exception handling
+- `AddMistrunaHealthChecks()` — `/health` endpoint
- Swagger in Development
### Run
@@ -23,3 +23,44 @@ Try:
- `GET /ping/World`
- `POST /counter/increment?step=2`
- `GET /errors/not-found`
+
+## WorkerRabbit
+
+Demonstrates:
+
+- `AddMistrunaRabbitMq()` — binds `Mistruna:RabbitMq` configuration
+- `AddMistrunaRabbitConsumer()` — typed message handling
+- Broker-free startup by default
+
+### Run
+
+```bash
+dotnet run --project samples/Mistruna.Core.Samples.WorkerRabbit
+```
+
+The default configuration binds RabbitMQ options without opening a connection. To
+start the consumer, provide a reachable broker and set
+`Mistruna__RabbitMq__EnableConsumer=true`. Other settings use the same environment
+variable pattern, for example `Mistruna__RabbitMq__HostName`.
+
+## ApiWithOtel
+
+Demonstrates:
+
+- `AddMistrunaObservability()` — ASP.NET Core, HTTP client, and MediatR instrumentation
+- `AddMistrunaResilience()` — shared resilience registrations
+- `AddMistrunaResilienceHandler()` — a resilient outbound HTTP client
+
+### Run
+
+```bash
+dotnet run --project samples/Mistruna.Core.Samples.ApiWithOtel
+```
+
+Try:
+
+- `GET /`
+- `GET /upstream`
+
+The sample produces OpenTelemetry telemetry. Add the exporter package and exporter
+configuration required by your deployment, such as OTLP, in the host application.
diff --git a/src/Mistruna.Core.Abstractions/Cqrs/ICommand.cs b/src/Mistruna.Core.Abstractions/Cqrs/ICommand.cs
new file mode 100644
index 0000000..f2fa7ea
--- /dev/null
+++ b/src/Mistruna.Core.Abstractions/Cqrs/ICommand.cs
@@ -0,0 +1,10 @@
+using MediatR;
+
+namespace Mistruna.Core.Abstractions.Cqrs;
+
+/// Marks a command that does not return a value.
+public interface ICommand : IRequest;
+
+/// Marks a command that returns a response.
+/// The response type.
+public interface ICommand : IRequest;
diff --git a/src/Mistruna.Core.Abstractions/Cqrs/IQuery.cs b/src/Mistruna.Core.Abstractions/Cqrs/IQuery.cs
new file mode 100644
index 0000000..76fd5a0
--- /dev/null
+++ b/src/Mistruna.Core.Abstractions/Cqrs/IQuery.cs
@@ -0,0 +1,7 @@
+using MediatR;
+
+namespace Mistruna.Core.Abstractions.Cqrs;
+
+/// Marks a read-only query that returns a response.
+/// The response type.
+public interface IQuery : IRequest;
diff --git a/src/Mistruna.Core/Abstractions/RequestKind.cs b/src/Mistruna.Core.Abstractions/Cqrs/RequestKind.cs
similarity index 61%
rename from src/Mistruna.Core/Abstractions/RequestKind.cs
rename to src/Mistruna.Core.Abstractions/Cqrs/RequestKind.cs
index 5636325..d324df4 100644
--- a/src/Mistruna.Core/Abstractions/RequestKind.cs
+++ b/src/Mistruna.Core.Abstractions/Cqrs/RequestKind.cs
@@ -1,30 +1,20 @@
-namespace Mistruna.Core.Abstractions;
+namespace Mistruna.Core.Abstractions.Cqrs;
-///
-/// Helpers for distinguishing CQRS request types in MediatR pipeline behaviors.
-///
+/// Classifies CQRS request types.
public static class RequestKind
{
- ///
- /// Returns when implements
- /// or .
- ///
+ /// Returns whether a type implements a command marker.
public static bool IsCommand(Type requestType)
{
ArgumentNullException.ThrowIfNull(requestType);
-
return typeof(ICommand).IsAssignableFrom(requestType) ||
requestType.GetInterfaces().Any(IsCommandInterface);
}
- ///
- /// Returns when implements
- /// .
- ///
+ /// Returns whether a type implements a query marker.
public static bool IsQuery(Type requestType)
{
ArgumentNullException.ThrowIfNull(requestType);
-
return requestType.GetInterfaces().Any(IsQueryInterface);
}
diff --git a/src/Mistruna.Core.Abstractions/Entities/CommonValueObjects.cs b/src/Mistruna.Core.Abstractions/Entities/CommonValueObjects.cs
new file mode 100644
index 0000000..e481a1c
--- /dev/null
+++ b/src/Mistruna.Core.Abstractions/Entities/CommonValueObjects.cs
@@ -0,0 +1,261 @@
+using System.Text.RegularExpressions;
+
+namespace Mistruna.Core.Abstractions.Entities;
+
+/// Represents an email address.
+public sealed class Email : ValueObject
+{
+ private static readonly Regex Pattern = new(
+ @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
+ RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+ private Email(string value) => Value = value.ToLowerInvariant();
+
+ /// Gets the normalized email address.
+ public string Value { get; }
+
+ /// Creates an email address.
+ public static Email Create(string email)
+ {
+ if (string.IsNullOrWhiteSpace(email))
+ throw new ArgumentException("Email cannot be null or empty.", nameof(email));
+ if (!Pattern.IsMatch(email))
+ throw new ArgumentException("Invalid email format.", nameof(email));
+ return new Email(email);
+ }
+
+ /// Attempts to create an email address.
+ public static bool TryCreate(string email, out Email? result)
+ {
+ result = null;
+ if (string.IsNullOrWhiteSpace(email) || !Pattern.IsMatch(email))
+ return false;
+ result = new Email(email);
+ return true;
+ }
+
+ ///
+ protected override IEnumerable