diff --git a/.gitignore b/.gitignore index ec39d478..d7fc5cb8 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,7 @@ _ReSharper*/ # JetBrains Rider .idea/ -*.sln.iml \ No newline at end of file +*.sln.iml + +#nuke.build +.nuke/temp diff --git a/.nuke b/.nuke deleted file mode 100644 index 3287f53f..00000000 --- a/.nuke +++ /dev/null @@ -1 +0,0 @@ -Backend.Fx.sln \ No newline at end of file diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json new file mode 100644 index 00000000..c172200f --- /dev/null +++ b/.nuke/build.schema.json @@ -0,0 +1,120 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Build Schema", + "$ref": "#/definitions/build", + "definitions": { + "build": { + "type": "object", + "properties": { + "Configuration": { + "type": "string", + "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)", + "enum": [ + "Debug", + "Release" + ] + }, + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "type": "string", + "description": "Host for execution. Default is 'automatic'", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "Partition": { + "type": "string", + "description": "Partition to use on CI" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "type": "string", + "enum": [ + "Clean", + "Compile", + "Pack", + "Publish", + "Restore", + "StartDependencies", + "StopDependencies", + "Test" + ] + } + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded" + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "type": "string", + "enum": [ + "Clean", + "Compile", + "Pack", + "Publish", + "Restore", + "StartDependencies", + "StopDependencies", + "Test" + ] + } + }, + "Verbosity": { + "type": "string", + "description": "Logging verbosity during build execution. Default is 'Normal'", + "enum": [ + "Minimal", + "Normal", + "Quiet", + "Verbose" + ] + } + } + } + } +} \ No newline at end of file diff --git a/.nuke/parameters.json b/.nuke/parameters.json new file mode 100644 index 00000000..b1a9a717 --- /dev/null +++ b/.nuke/parameters.json @@ -0,0 +1,4 @@ +{ + "$schema": "./build.schema.json", + "Solution": "Backend.Fx.sln" +} \ No newline at end of file diff --git a/.tmp/build-attempt.log b/.tmp/build-attempt.log deleted file mode 100644 index c70b1d97..00000000 --- a/.tmp/build-attempt.log +++ /dev/null @@ -1,7 +0,0 @@ -884bc421701d4c4e7e62c0eca741ed63 -Clean -Restore -Compile -Test -Pack -Publish diff --git a/Backend.Fx.sln b/Backend.Fx.sln index 8353da85..58f5ec2f 100644 --- a/Backend.Fx.sln +++ b/Backend.Fx.sln @@ -15,8 +15,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{C7885592 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Backend.Fx.Tests", "tests\Backend.Fx.Tests\Backend.Fx.Tests.csproj", "{3706F748-43F6-41BD-8875-81FA679220C7}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Backend.Fx.EfCorePersistence.Tests", "tests\Backend.Fx.EfCorePersistence.Tests\Backend.Fx.EfCorePersistence.Tests.csproj", "{4BB72B85-61F2-4C7F-9079-EA43492FCD44}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "abstractions", "abstractions", "{A742F814-725A-44ED-95E6-98E142738E9D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "environments", "environments", "{56ACAE69-F7F0-4FF2-BEE6-4B079481CF9A}" @@ -25,22 +23,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "implementations", "implemen EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Backend.Fx", "src\abstractions\Backend.Fx\Backend.Fx.csproj", "{581DCC00-9246-4A2E-AE31-206742B2746A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Backend.Fx.EfCorePersistence", "src\implementations\Backend.Fx.EfCorePersistence\Backend.Fx.EfCorePersistence.csproj", "{A60B7952-D92C-403D-9710-65BE13963C7E}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Backend.Fx.RabbitMq", "src\implementations\Backend.Fx.RabbitMq\Backend.Fx.RabbitMq.csproj", "{2C826FC0-443A-4874-B213-C35BFDEA200A}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Backend.Fx.SimpleInjectorDependencyInjection", "src\implementations\Backend.Fx.SimpleInjetorDependencyInjection\Backend.Fx.SimpleInjectorDependencyInjection.csproj", "{FF042FB5-BA44-4655-8903-2644FE549810}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Backend.Fx.AspNetCore", "src\environments\Backend.Fx.AspNetCore\Backend.Fx.AspNetCore.csproj", "{25746028-5116-4600-A0C4-35DE0C468A8F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Backend.Fx.SimpleInjectorDependencyInjection.Tests", "tests\Backend.Fx.SimpleInjectorDependencyInjection.Tests\Backend.Fx.SimpleInjectorDependencyInjection.Tests.csproj", "{D98AED23-ABB8-4130-9612-54AEFE9D2272}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Backend.Fx.InMemoryPersistence", "src\implementations\Backend.Fx.InMemoryPersistence\Backend.Fx.InMemoryPersistence.csproj", "{0B8F13CA-1347-4655-9D41-AED21B1AFAC4}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backend.Fx.RabbitMq.Tests", "tests\Backend.Fx.RabbitMq.Tests\Backend.Fx.RabbitMq.Tests.csproj", "{6D0A5E9D-2FA5-4CC9-96B0-C2C871335E3A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backend.Fx.AspNetCore.Tests", "tests\Backend.Fx.AspNetCore.Tests\Backend.Fx.AspNetCore.Tests.csproj", "{DF40E1E8-FB19-455E-9CED-212C544AA8BC}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{16EBBF6D-EA66-4E14-BE2D-1900CBC747F7}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{FD21EC43-FC48-433A-8C4F-5CCFC1A2B35E}" @@ -58,6 +48,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backend.Fx.EfCore6Persisten EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backend.Fx.EfCore6Persistence.Tests", "tests\Backend.Fx.EfCore6Persistence.Tests\Backend.Fx.EfCore6Persistence.Tests.csproj", "{E50D7E8D-D012-4683-BA05-C877BAA25230}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backend.Fx.MicrosoftDependencyInjection", "src\implementations\Backend.Fx.MicrosoftDependencyInjection\Backend.Fx.MicrosoftDependencyInjection.csproj", "{B4791DB0-F8DD-4248-86CB-407E46F55B13}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backend.Fx.TestUtil", "tests\Backend.Fx.TestUtil\Backend.Fx.TestUtil.csproj", "{3AD4F223-DC1D-40B7-9DB9-DC88FDD0178D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -71,18 +65,10 @@ Global {3706F748-43F6-41BD-8875-81FA679220C7}.Debug|Any CPU.Build.0 = Debug|Any CPU {3706F748-43F6-41BD-8875-81FA679220C7}.Release|Any CPU.ActiveCfg = Release|Any CPU {3706F748-43F6-41BD-8875-81FA679220C7}.Release|Any CPU.Build.0 = Release|Any CPU - {4BB72B85-61F2-4C7F-9079-EA43492FCD44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4BB72B85-61F2-4C7F-9079-EA43492FCD44}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4BB72B85-61F2-4C7F-9079-EA43492FCD44}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4BB72B85-61F2-4C7F-9079-EA43492FCD44}.Release|Any CPU.Build.0 = Release|Any CPU {581DCC00-9246-4A2E-AE31-206742B2746A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {581DCC00-9246-4A2E-AE31-206742B2746A}.Debug|Any CPU.Build.0 = Debug|Any CPU {581DCC00-9246-4A2E-AE31-206742B2746A}.Release|Any CPU.ActiveCfg = Release|Any CPU {581DCC00-9246-4A2E-AE31-206742B2746A}.Release|Any CPU.Build.0 = Release|Any CPU - {A60B7952-D92C-403D-9710-65BE13963C7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A60B7952-D92C-403D-9710-65BE13963C7E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A60B7952-D92C-403D-9710-65BE13963C7E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A60B7952-D92C-403D-9710-65BE13963C7E}.Release|Any CPU.Build.0 = Release|Any CPU {2C826FC0-443A-4874-B213-C35BFDEA200A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2C826FC0-443A-4874-B213-C35BFDEA200A}.Debug|Any CPU.Build.0 = Debug|Any CPU {2C826FC0-443A-4874-B213-C35BFDEA200A}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -95,22 +81,10 @@ Global {25746028-5116-4600-A0C4-35DE0C468A8F}.Debug|Any CPU.Build.0 = Debug|Any CPU {25746028-5116-4600-A0C4-35DE0C468A8F}.Release|Any CPU.ActiveCfg = Release|Any CPU {25746028-5116-4600-A0C4-35DE0C468A8F}.Release|Any CPU.Build.0 = Release|Any CPU - {D98AED23-ABB8-4130-9612-54AEFE9D2272}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D98AED23-ABB8-4130-9612-54AEFE9D2272}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D98AED23-ABB8-4130-9612-54AEFE9D2272}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D98AED23-ABB8-4130-9612-54AEFE9D2272}.Release|Any CPU.Build.0 = Release|Any CPU - {0B8F13CA-1347-4655-9D41-AED21B1AFAC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0B8F13CA-1347-4655-9D41-AED21B1AFAC4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0B8F13CA-1347-4655-9D41-AED21B1AFAC4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0B8F13CA-1347-4655-9D41-AED21B1AFAC4}.Release|Any CPU.Build.0 = Release|Any CPU {6D0A5E9D-2FA5-4CC9-96B0-C2C871335E3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6D0A5E9D-2FA5-4CC9-96B0-C2C871335E3A}.Debug|Any CPU.Build.0 = Debug|Any CPU {6D0A5E9D-2FA5-4CC9-96B0-C2C871335E3A}.Release|Any CPU.ActiveCfg = Release|Any CPU {6D0A5E9D-2FA5-4CC9-96B0-C2C871335E3A}.Release|Any CPU.Build.0 = Release|Any CPU - {DF40E1E8-FB19-455E-9CED-212C544AA8BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DF40E1E8-FB19-455E-9CED-212C544AA8BC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DF40E1E8-FB19-455E-9CED-212C544AA8BC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DF40E1E8-FB19-455E-9CED-212C544AA8BC}.Release|Any CPU.Build.0 = Release|Any CPU {38034961-CE3B-4286-A9EB-496DECA39632}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {38034961-CE3B-4286-A9EB-496DECA39632}.Debug|Any CPU.Build.0 = Debug|Any CPU {38034961-CE3B-4286-A9EB-496DECA39632}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -119,32 +93,37 @@ Global {E50D7E8D-D012-4683-BA05-C877BAA25230}.Debug|Any CPU.Build.0 = Debug|Any CPU {E50D7E8D-D012-4683-BA05-C877BAA25230}.Release|Any CPU.ActiveCfg = Release|Any CPU {E50D7E8D-D012-4683-BA05-C877BAA25230}.Release|Any CPU.Build.0 = Release|Any CPU + {B4791DB0-F8DD-4248-86CB-407E46F55B13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B4791DB0-F8DD-4248-86CB-407E46F55B13}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B4791DB0-F8DD-4248-86CB-407E46F55B13}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B4791DB0-F8DD-4248-86CB-407E46F55B13}.Release|Any CPU.Build.0 = Release|Any CPU + {3AD4F223-DC1D-40B7-9DB9-DC88FDD0178D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3AD4F223-DC1D-40B7-9DB9-DC88FDD0178D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3AD4F223-DC1D-40B7-9DB9-DC88FDD0178D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3AD4F223-DC1D-40B7-9DB9-DC88FDD0178D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {3706F748-43F6-41BD-8875-81FA679220C7} = {C7885592-A4B8-4BA8-8D3A-1EDA4025D17A} - {4BB72B85-61F2-4C7F-9079-EA43492FCD44} = {C7885592-A4B8-4BA8-8D3A-1EDA4025D17A} {A742F814-725A-44ED-95E6-98E142738E9D} = {53D4501E-953C-4A7C-97C4-1F9DE04BD092} {56ACAE69-F7F0-4FF2-BEE6-4B079481CF9A} = {53D4501E-953C-4A7C-97C4-1F9DE04BD092} {739A7296-579F-4D9A-BC73-DCECD260D7A0} = {53D4501E-953C-4A7C-97C4-1F9DE04BD092} {581DCC00-9246-4A2E-AE31-206742B2746A} = {A742F814-725A-44ED-95E6-98E142738E9D} {25746028-5116-4600-A0C4-35DE0C468A8F} = {56ACAE69-F7F0-4FF2-BEE6-4B079481CF9A} - {D98AED23-ABB8-4130-9612-54AEFE9D2272} = {C7885592-A4B8-4BA8-8D3A-1EDA4025D17A} {6D0A5E9D-2FA5-4CC9-96B0-C2C871335E3A} = {C7885592-A4B8-4BA8-8D3A-1EDA4025D17A} - {DF40E1E8-FB19-455E-9CED-212C544AA8BC} = {C7885592-A4B8-4BA8-8D3A-1EDA4025D17A} {16EBBF6D-EA66-4E14-BE2D-1900CBC747F7} = {6B64354E-D95B-4711-BAF6-B32049C90CD9} {FD21EC43-FC48-433A-8C4F-5CCFC1A2B35E} = {16EBBF6D-EA66-4E14-BE2D-1900CBC747F7} {ADC35CAD-F5B1-42B6-A0CC-B96974C11F11} = {739A7296-579F-4D9A-BC73-DCECD260D7A0} - {A60B7952-D92C-403D-9710-65BE13963C7E} = {ADC35CAD-F5B1-42B6-A0CC-B96974C11F11} - {0B8F13CA-1347-4655-9D41-AED21B1AFAC4} = {ADC35CAD-F5B1-42B6-A0CC-B96974C11F11} {8BC1C02F-0785-4161-BC37-7D462BD6F42D} = {739A7296-579F-4D9A-BC73-DCECD260D7A0} {2C826FC0-443A-4874-B213-C35BFDEA200A} = {8BC1C02F-0785-4161-BC37-7D462BD6F42D} {22E4DE95-C3E5-49E6-83BF-BF30905A746B} = {739A7296-579F-4D9A-BC73-DCECD260D7A0} {FF042FB5-BA44-4655-8903-2644FE549810} = {22E4DE95-C3E5-49E6-83BF-BF30905A746B} {38034961-CE3B-4286-A9EB-496DECA39632} = {ADC35CAD-F5B1-42B6-A0CC-B96974C11F11} {E50D7E8D-D012-4683-BA05-C877BAA25230} = {C7885592-A4B8-4BA8-8D3A-1EDA4025D17A} + {B4791DB0-F8DD-4248-86CB-407E46F55B13} = {22E4DE95-C3E5-49E6-83BF-BF30905A746B} + {3AD4F223-DC1D-40B7-9DB9-DC88FDD0178D} = {C7885592-A4B8-4BA8-8D3A-1EDA4025D17A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {45648557-C751-44AD-9C87-0F12EB673969} diff --git a/Backend.Fx.sln.DotSettings b/Backend.Fx.sln.DotSettings index afc9e37f..96ad1905 100644 --- a/Backend.Fx.sln.DotSettings +++ b/Backend.Fx.sln.DotSettings @@ -1,7 +1,10 @@  + DI True True + True True True True + True True \ No newline at end of file diff --git a/build.cmd b/build.cmd index 8b8b89dc..b08cc590 100755 --- a/build.cmd +++ b/build.cmd @@ -4,4 +4,4 @@ :; exit $? @ECHO OFF -powershell -ExecutionPolicy ByPass -NoProfile "%~dp0build.ps1" %* +powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %* diff --git a/build.ps1 b/build.ps1 index e5c8a44e..8c52d631 100644 --- a/build.ps1 +++ b/build.ps1 @@ -14,7 +14,7 @@ $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent ########################################################################### $BuildProjectFile = "$PSScriptRoot\build\_build.csproj" -$TempDirectory = "$PSScriptRoot\\.tmp" +$TempDirectory = "$PSScriptRoot\\.nuke\temp" $DotNetGlobalFile = "$PSScriptRoot\\global.json" $DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1" @@ -56,14 +56,14 @@ else { # Install by channel or version $DotNetDirectory = "$TempDirectory\dotnet-win" if (!(Test-Path variable:DotNetVersion)) { - ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } } else { - ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } } $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" } -Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)" +Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } diff --git a/build.sh b/build.sh index 3d526432..1f3ba09e 100755 --- a/build.sh +++ b/build.sh @@ -10,7 +10,7 @@ SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) ########################################################################### BUILD_PROJECT_FILE="$SCRIPT_DIR/build/_build.csproj" -TEMP_DIRECTORY="$SCRIPT_DIR//.tmp" +TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp" DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" @@ -56,7 +56,7 @@ else export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" fi -echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)" +echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" "$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet "$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" diff --git a/build/Build.cs b/build/Build.cs index 8777529b..f58dd3cc 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -1,7 +1,6 @@ using System; using Nuke.Common; using Nuke.Common.CI; -using Nuke.Common.Execution; using Nuke.Common.Git; using Nuke.Common.IO; using Nuke.Common.ProjectModel; @@ -11,9 +10,8 @@ using static Nuke.Common.IO.FileSystemTasks; using static Nuke.Common.Tools.DotNet.DotNetTasks; -[CheckBuildProjectConfigurations] [ShutdownDotNetAfterServerBuild] -class Build : NukeBuild +partial class Build : NukeBuild { public static int Main() => Execute(x => x.Publish); @@ -53,6 +51,7 @@ class Build : NukeBuild .DependsOn(Restore) .Executes(() => { + Serilog.Log.Information("Version {Version}", GitVersion?.SemVer ?? "no version!"); DotNetBuild(s => s .SetProjectFile(Solution) .SetConfiguration(Configuration) @@ -63,14 +62,16 @@ class Build : NukeBuild }); Target Test => _ => _ - .DependsOn(Compile) + .DependsOn(Compile, StartDependencies) .Executes(() => { DotNetTest(s => s .SetProjectFile(Solution) .SetConfiguration(Configuration) .EnableNoRestore()); - }); + }) + .ProceedAfterFailure() + .Triggers(StopDependencies); Target Pack => _ => _ .DependsOn(Test) diff --git a/build/Build.deps.cs b/build/Build.deps.cs new file mode 100644 index 00000000..89bbf082 --- /dev/null +++ b/build/Build.deps.cs @@ -0,0 +1,40 @@ +using System.Linq; +using Nuke.Common; +using Nuke.Common.Tools.Docker; +using static Nuke.Common.Tools.Docker.DockerTasks; + +partial class Build +{ + const string RabbitMqImageName = "docker.io/library/rabbitmq:latest"; + const string RabbitMqContainerName = "backendfx-rabbitmq"; + + Target StartDependencies + => _ => _ + .Executes(() => + { + var existingContainers = DockerPs(s => s + .SetFilter($"name={RabbitMqContainerName}") + .EnableAll() + .EnableQuiet()); + if (existingContainers.Any()) + { + DockerRm(s => s + .AddContainers(existingContainers.Select(c => c.Text)) + .EnableForce()); + } + + DockerPull(x => x.SetName(RabbitMqImageName).EnableQuiet()); + + DockerRun(x => x + .SetImage(RabbitMqImageName) + .SetName(RabbitMqContainerName) + .SetEnv("RABBITMQ_DEFAULT_USER=test", "RABBITMQ_DEFAULT_PASS=password") + .SetPublish("5672:5672") + .EnableDetach()); + }); + + Target StopDependencies + => _ => _ + .AssuredAfterFailure() + .Executes(() => DockerStop(x => x.SetContainers(RabbitMqContainerName))); +} diff --git a/build/_build.csproj b/build/_build.csproj index 565047d2..e7b7cf82 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -2,16 +2,17 @@ Exe - net5.0 + net6.0 CS0649;CS0169 .. .. + 1 - - + + diff --git a/src/abstractions/Backend.Fx/Backend.Fx.csproj b/src/abstractions/Backend.Fx/Backend.Fx.csproj index 8e4bc2c5..a798f7e7 100644 --- a/src/abstractions/Backend.Fx/Backend.Fx.csproj +++ b/src/abstractions/Backend.Fx/Backend.Fx.csproj @@ -7,7 +7,8 @@ false false false - false + false + 9 @@ -24,21 +25,16 @@ - - - - - - - - - - - - + + + + + + + + - - + \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Backend.Fx.csproj.DotSettings b/src/abstractions/Backend.Fx/Backend.Fx.csproj.DotSettings new file mode 100644 index 00000000..c2f4bbdb --- /dev/null +++ b/src/abstractions/Backend.Fx/Backend.Fx.csproj.DotSettings @@ -0,0 +1,8 @@ + + Library + \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/BackendFxApplication.cs b/src/abstractions/Backend.Fx/BackendFxApplication.cs new file mode 100644 index 00000000..8c7647b1 --- /dev/null +++ b/src/abstractions/Backend.Fx/BackendFxApplication.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.DependencyInjection; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Features; +using Backend.Fx.Logging; +using Backend.Fx.Util; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx +{ + /// + /// The root object of the whole backend fx application framework + /// + [PublicAPI] + public interface IBackendFxApplication : IDisposable + { + /// + /// The invoker runs a given action asynchronously in an application scope with injection facilities + /// + IBackendFxApplicationInvoker Invoker { get; } + + /// + /// The composition root of the dependency injection framework + /// + ICompositionRoot CompositionRoot { get; } + + /// + /// The global exception logger of this application + /// + IExceptionLogger ExceptionLogger { get; } + + Assembly[] Assemblies { get; } + + /// + /// allows synchronously awaiting application startup + /// + Task WaitForBootAsync(CancellationToken cancellationToken = default); + + /// + /// Initializes and starts the application (async) + /// + /// + Task BootAsync(CancellationToken cancellationToken = default); + + /// + /// Enables an optional feature. Must be done before calling . + /// + /// + void EnableFeature(Feature feature); + + void RequireDependantFeature() where TFeature : Feature; + } + + + [PublicAPI] + public class BackendFxApplication : IBackendFxApplication + { + private readonly ILogger _logger = Log.Create(); + private readonly List _features = new(); + private readonly Lazy _bootAction; + + /// + /// Initializes the application's runtime instance + /// + /// The composition root of the dependency injection framework + /// + /// + public BackendFxApplication(ICompositionRoot compositionRoot, IExceptionLogger exceptionLogger, + params Assembly[] assemblies) + { + assemblies ??= Array.Empty(); + + _logger.LogInformation( + "Initializing application with {CompositionRoot} providing services from [{Assemblies}]", + compositionRoot.GetType().GetDetailedTypeName(), + string.Join(", ", assemblies.Select(ass => ass.GetName().Name))); + + var invoker = new BackendFxApplicationInvoker(this); + + Invoker = new ExceptionLoggingInvoker(exceptionLogger, invoker); + + CompositionRoot = new LogRegistrationsDecorator(compositionRoot); + ExceptionLogger = exceptionLogger; + Assemblies = assemblies; + CompositionRoot.RegisterModules(new ExecutionPipelineModule(withFrozenClockDuringExecution: true)); + + _bootAction = new Lazy(async () => + { + _logger.LogInformation("Booting application"); + CompositionRoot.Verify(); + + foreach (Feature feature in _features) + { + if (feature is IBootableFeature bootableFeature) + { + await bootableFeature.BootAsync(this).ConfigureAwait(false); + } + } + }); + } + + public Assembly[] Assemblies { get; } + + public IBackendFxApplicationInvoker Invoker { get; } + + public ICompositionRoot CompositionRoot { get; } + + public IExceptionLogger ExceptionLogger { get; } + + public virtual void EnableFeature(Feature feature) + { + if (_bootAction.IsValueCreated) + { + throw new InvalidOperationException("Features must be enabled before booting the application"); + } + + feature.Enable(this); + _features.Add(feature); + } + + public void RequireDependantFeature() where TFeature : Feature + { + if (!_features.OfType().Any()) + { + throw new InvalidOperationException( + $"This feature requires the {typeof(TFeature).Name} to be enabled first"); + } + } + + public async Task BootAsync(CancellationToken cancellationToken = default) + { + await _bootAction.Value.ConfigureAwait(false); + } + + public async Task WaitForBootAsync(CancellationToken cancellationToken = default) + { + await Task.Run(async () => + { + do + { + if (cancellationToken.IsCancellationRequested || + _bootAction.IsValueCreated && _bootAction.Value.Status is TaskStatus.Canceled or TaskStatus.Faulted or TaskStatus.RanToCompletion) + { + return; + } + + await Task.Delay(50, cancellationToken).ConfigureAwait(false); + } while (true); + }, cancellationToken).ConfigureAwait(false); + } + + public void Dispose() + { + _logger.LogInformation("Application shut down initialized"); + foreach (Feature feature in _features) + { + feature.Dispose(); + } + + CompositionRoot?.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/BuildingBlocks/AggregateRoot.cs b/src/abstractions/Backend.Fx/BuildingBlocks/AggregateRoot.cs deleted file mode 100644 index e22101f1..00000000 --- a/src/abstractions/Backend.Fx/BuildingBlocks/AggregateRoot.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Backend.Fx.BuildingBlocks -{ - /// - /// A collection of objects that are bound together by a root entity - /// See https://en.wikipedia.org/wiki/Domain-driven_design#Building_blocks - /// - public abstract class AggregateRoot : Entity - { - protected AggregateRoot() - { - } - - protected AggregateRoot(int id) : base(id) - { - } - - public int TenantId { get; set; } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/BuildingBlocks/Entity.cs b/src/abstractions/Backend.Fx/BuildingBlocks/Entity.cs deleted file mode 100644 index 5ddc4d22..00000000 --- a/src/abstractions/Backend.Fx/BuildingBlocks/Entity.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using Backend.Fx.Extensions; -using JetBrains.Annotations; - -namespace Backend.Fx.BuildingBlocks -{ - /// - /// An object that is not defined by its attributes, but rather by a thread of continuity and its identity. - /// https://en.wikipedia.org/wiki/Domain-driven_design#Building_blocks - /// - public abstract class Entity : Identified - { - protected Entity() - { - } - - protected Entity(int id) - { - Id = id; - } - - public DateTime CreatedOn { get; protected set; } - - [StringLength(100)] public string CreatedBy { get; protected set; } - - public DateTime? ChangedOn { get; protected set; } - - [StringLength(100)] public string ChangedBy { get; protected set; } - - public void SetCreatedProperties([NotNull] string createdBy, DateTime createdOn) - { - if (createdBy == null) - { - throw new ArgumentNullException(nameof(createdBy)); - } - - if (createdBy == string.Empty) - { - throw new ArgumentException(nameof(createdBy)); - } - - CreatedBy = createdBy.Cut(100); - CreatedOn = createdOn; - } - - public void SetModifiedProperties([NotNull] string changedBy, DateTime changedOn) - { - if (changedBy == null) - { - throw new ArgumentNullException(nameof(changedBy)); - } - - if (changedBy == string.Empty) - { - throw new ArgumentException(nameof(changedBy)); - } - - ChangedBy = changedBy.Cut(100); - ChangedOn = changedOn; - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/BuildingBlocks/IAsyncRepository.cs b/src/abstractions/Backend.Fx/BuildingBlocks/IAsyncRepository.cs deleted file mode 100644 index 9365d5c0..00000000 --- a/src/abstractions/Backend.Fx/BuildingBlocks/IAsyncRepository.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace Backend.Fx.BuildingBlocks -{ - /// - /// Encapsulates methods for retrieving domain objects - /// See https://en.wikipedia.org/wiki/Domain-driven_design#Building_blocks - /// - /// - public interface IAsyncRepository where TAggregateRoot : AggregateRoot - { - Task SingleAsync(int id, CancellationToken cancellationToken = default); - Task SingleOrDefaultAsync(int id, CancellationToken cancellationToken = default); - Task GetAllAsync(CancellationToken cancellationToken = default); - Task AnyAsync(CancellationToken cancellationToken = default); - Task ResolveAsync(IEnumerable ids, CancellationToken cancellationToken = default); - IQueryable AggregateQueryable { get; } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/BuildingBlocks/IRepository.cs b/src/abstractions/Backend.Fx/BuildingBlocks/IRepository.cs deleted file mode 100644 index 19ba3187..00000000 --- a/src/abstractions/Backend.Fx/BuildingBlocks/IRepository.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace Backend.Fx.BuildingBlocks -{ - /// - /// Encapsulates methods for retrieving domain objects - /// See https://en.wikipedia.org/wiki/Domain-driven_design#Building_blocks - /// - /// - public interface IRepository where TAggregateRoot : AggregateRoot - { - TAggregateRoot Single(int id); - TAggregateRoot SingleOrDefault(int id); - TAggregateRoot[] GetAll(); - void Delete(TAggregateRoot aggregateRoot); - void Add(TAggregateRoot aggregateRoot); - void AddRange(TAggregateRoot[] aggregateRoots); - bool Any(); - TAggregateRoot[] Resolve(IEnumerable ids); - IQueryable AggregateQueryable { get; } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/BuildingBlocks/IView.cs b/src/abstractions/Backend.Fx/BuildingBlocks/IView.cs deleted file mode 100644 index 0121addb..00000000 --- a/src/abstractions/Backend.Fx/BuildingBlocks/IView.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; - -namespace Backend.Fx.BuildingBlocks -{ - public interface IView : IQueryable - { - } - - public abstract class View : IView - { - private readonly IQueryable _viewImplementation; - - protected View(IQueryable viewImplementation) - { - _viewImplementation = viewImplementation; - } - - public IEnumerator GetEnumerator() - { - return _viewImplementation.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return ((IEnumerable) _viewImplementation).GetEnumerator(); - } - - public Type ElementType => _viewImplementation.ElementType; - - public Expression Expression => _viewImplementation.Expression; - - public IQueryProvider Provider => _viewImplementation.Provider; - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/BuildingBlocks/Identified.cs b/src/abstractions/Backend.Fx/BuildingBlocks/Identified.cs deleted file mode 100644 index f37a3bde..00000000 --- a/src/abstractions/Backend.Fx/BuildingBlocks/Identified.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.Diagnostics; -using JetBrains.Annotations; - -namespace Backend.Fx.BuildingBlocks -{ - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + "}")] - public abstract class Identified : IEquatable - { - [Key] public int Id { get; set; } - - [UsedImplicitly] public string DebuggerDisplay => $"{GetType().Name}[{Id}]"; - - public bool Equals(Identified other) - { - if (other == null || other.GetType() != GetType()) - { - return false; - } - - return Id.Equals(other.Id); - } - - public override bool Equals(object obj) - { - var other = obj as Identified; - if (other == null) - { - return false; - } - - return Equals(other); - } - - public override int GetHashCode() - { - // ReSharper disable NonReadonlyMemberInGetHashCode - if (Id != 0) - { - return Id.GetHashCode(); - } - // ReSharper enable NonReadonlyMemberInGetHashCode - - // ReSharper disable once BaseObjectGetHashCodeCallInGetHashCode - return base.GetHashCode(); - } - - public static bool operator ==(Identified x, Identified y) - { - return Equals(x, y); - } - - public static bool operator !=(Identified x, Identified y) - { - return !(x == y); - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/BuildingBlocks/Repository.cs b/src/abstractions/Backend.Fx/BuildingBlocks/Repository.cs deleted file mode 100644 index 66be9b8b..00000000 --- a/src/abstractions/Backend.Fx/BuildingBlocks/Repository.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Exceptions; -using Backend.Fx.Extensions; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.Authorization; -using Backend.Fx.Patterns.DependencyInjection; -using JetBrains.Annotations; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.BuildingBlocks -{ - public abstract class Repository : IRepository where TAggregateRoot : AggregateRoot - { - private static readonly ILogger Logger = Log.Create>(); - private readonly IAggregateAuthorization _aggregateAuthorization; - private readonly ICurrentTHolder _tenantIdHolder; - - protected Repository(ICurrentTHolder tenantIdHolder, IAggregateAuthorization aggregateAuthorization) - { - Logger.LogTrace( - "Instantiating a new Repository<{AggregateTypeName}> for tenant [{TenantId}]", - AggregateTypeName, - tenantIdHolder.Current.HasValue ? tenantIdHolder.Current.Value.ToString() : "null"); - _tenantIdHolder = tenantIdHolder; - _aggregateAuthorization = aggregateAuthorization; - } - - protected static string AggregateTypeName => typeof(TAggregateRoot).Name; - - protected abstract IQueryable RawAggregateQueryable { get; } - - public IQueryable AggregateQueryable - { - get - { - if (_tenantIdHolder.Current.HasValue) - { - return _aggregateAuthorization.Filter(RawAggregateQueryable - .Where(agg => agg.TenantId == _tenantIdHolder.Current.Value)); - } - - return RawAggregateQueryable.Where(agg => false); - } - } - - public TAggregateRoot Single(int id) - { - Logger.LogDebug("Getting single {AggregateTypeName}[{Id}]", AggregateTypeName, id); - TAggregateRoot aggregateRoot = AggregateQueryable.FirstOrDefault(agg => agg.Id.Equals(id)); - if (aggregateRoot == null) - { - throw new NotFoundException(id); - } - - return aggregateRoot; - } - - public TAggregateRoot SingleOrDefault(int id) - { - Logger.LogDebug("Getting single or default {AggregateTypeName}[{Id}]", AggregateTypeName, id); - return AggregateQueryable.FirstOrDefault(agg => agg.Id.Equals(id)); - } - - public TAggregateRoot[] GetAll() - { - return AggregateQueryable.ToArray(); - } - - public void Delete([NotNull] TAggregateRoot aggregateRoot) - { - if (aggregateRoot == null) - { - throw new ArgumentNullException(nameof(aggregateRoot)); - } - - Logger.LogDebug("Deleting {AggregateTypeName}[{Id}]", AggregateTypeName, aggregateRoot.Id); - if (aggregateRoot.TenantId != _tenantIdHolder.Current.Value || !_aggregateAuthorization.CanDelete(aggregateRoot)) - { - throw new ForbiddenException($"You are not allowed to delete {typeof(TAggregateRoot).Name}[{aggregateRoot.Id}]"); - } - - DeletePersistent(aggregateRoot); - } - - public void Add([NotNull] TAggregateRoot aggregateRoot) - { - if (aggregateRoot == null) - { - throw new ArgumentNullException(nameof(aggregateRoot)); - } - - if (_aggregateAuthorization.CanCreate(aggregateRoot)) - { - Logger.LogDebug("Adding {AggregateTypeName}[{Id}]", AggregateTypeName, aggregateRoot.Id); - aggregateRoot.TenantId = _tenantIdHolder.Current.Value; - AddPersistent(aggregateRoot); - } - else - { - throw new ForbiddenException($"You are not allowed to create records of type {typeof(TAggregateRoot).Name}"); - } - } - - public void AddRange([NotNull] TAggregateRoot[] aggregateRoots) - { - if (aggregateRoots == null) - { - throw new ArgumentNullException(nameof(aggregateRoots)); - } - - aggregateRoots.ForAll(agg => - { - if (!_aggregateAuthorization.CanCreate(agg)) - { - throw new ForbiddenException($"You are not allowed to create records of type {typeof(TAggregateRoot).Name}"); - } - }); - - Logger.LogDebug("Adding {Count} items of type {AggregateTypeName}", aggregateRoots.Length, AggregateTypeName); - - aggregateRoots.ForAll(agg => agg.TenantId = _tenantIdHolder.Current.Value); - - AddRangePersistent(aggregateRoots); - } - - public bool Any() - { - return AggregateQueryable.Any(); - } - - public TAggregateRoot[] Resolve(IEnumerable ids) - { - if (ids == null) - { - return Array.Empty(); - } - - int[] idsToResolve = ids as int[] ?? ids.ToArray(); - TAggregateRoot[] resolved = AggregateQueryable.Where(agg => idsToResolve.Contains(agg.Id)).ToArray(); - if (resolved.Length != idsToResolve.Length) - { - throw new ArgumentException($"The following {AggregateTypeName} ids could not be resolved: {string.Join(", ", idsToResolve.Except(resolved.Select(agg => agg.Id)))}"); - } - return resolved; - } - - protected abstract void AddPersistent(TAggregateRoot aggregateRoot); - - protected abstract void AddRangePersistent(TAggregateRoot[] aggregateRoots); - - protected abstract void DeletePersistent(TAggregateRoot aggregateRoot); - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/ConfigurationSettings/ISettingSerializer.cs b/src/abstractions/Backend.Fx/ConfigurationSettings/ISettingSerializer.cs deleted file mode 100644 index 4fc9868d..00000000 --- a/src/abstractions/Backend.Fx/ConfigurationSettings/ISettingSerializer.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Globalization; -using JetBrains.Annotations; - -namespace Backend.Fx.ConfigurationSettings -{ - public interface ISettingSerializer - { - } - - public interface ISettingSerializer : ISettingSerializer - { - string Serialize(T setting); - T Deserialize(string value); - } - - [UsedImplicitly] - public class StringSerializer : ISettingSerializer - { - public string Serialize(string setting) - { - return setting; - } - - public string Deserialize(string value) - { - return value; - } - } - - [UsedImplicitly] - public class IntegerSerializer : ISettingSerializer - { - public string Serialize(int? setting) - { - return setting?.ToString(CultureInfo.InvariantCulture); - } - - public int? Deserialize(string value) - { - return string.IsNullOrWhiteSpace(value) ? (int?) null : int.Parse(value, CultureInfo.InvariantCulture); - } - } - - [UsedImplicitly] - public class DoubleSerializer : ISettingSerializer - { - public string Serialize(double? setting) - { - return setting?.ToString("r", CultureInfo.InvariantCulture); - } - - public double? Deserialize(string value) - { - return string.IsNullOrWhiteSpace(value) ? (double?) null : double.Parse(value, CultureInfo.InvariantCulture); - } - } - - [UsedImplicitly] - public class BooleanSerializer : ISettingSerializer - { - public string Serialize(bool? setting) - { - return setting?.ToString(); - } - - public bool? Deserialize(string value) - { - return string.IsNullOrWhiteSpace(value) ? (bool?) null : bool.Parse(value); - } - } - - [UsedImplicitly] - public class DateTimeSerializer : ISettingSerializer - { - public string Serialize(DateTime? setting) - { - return setting?.ToString("O", CultureInfo.InvariantCulture); - } - - public DateTime? Deserialize(string value) - { - return string.IsNullOrWhiteSpace(value) ? (DateTime?) null : DateTime.Parse(value, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/ConfigurationSettings/Setting.cs b/src/abstractions/Backend.Fx/ConfigurationSettings/Setting.cs deleted file mode 100644 index c1e55a92..00000000 --- a/src/abstractions/Backend.Fx/ConfigurationSettings/Setting.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Backend.Fx.BuildingBlocks; -using JetBrains.Annotations; - -namespace Backend.Fx.ConfigurationSettings -{ - public class Setting : AggregateRoot - { - [UsedImplicitly] - private Setting() - { - } - - public Setting(int id, string key) : base(id) - { - Key = key; - } - - public string Key { get; [UsedImplicitly] private set; } - public string SerializedValue { get; private set; } - - public T GetValue(ISettingSerializer serializer) - { - return serializer.Deserialize(SerializedValue); - } - - public void SetValue(ISettingSerializer serializer, T value) - { - SerializedValue = serializer.Serialize(value); - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/ConfigurationSettings/SettingsService.cs b/src/abstractions/Backend.Fx/ConfigurationSettings/SettingsService.cs deleted file mode 100644 index 26476479..00000000 --- a/src/abstractions/Backend.Fx/ConfigurationSettings/SettingsService.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Linq; -using Backend.Fx.BuildingBlocks; -using Backend.Fx.Patterns.IdGeneration; - -namespace Backend.Fx.ConfigurationSettings -{ - public abstract class SettingsService - { - private readonly string _category; - private readonly IEntityIdGenerator _idGenerator; - private readonly IRepository _settingRepository; - private readonly ISettingSerializerFactory _settingSerializerFactory; - - protected SettingsService(string category, IEntityIdGenerator idGenerator, IRepository settingRepository, ISettingSerializerFactory settingSerializerFactory) - { - _category = category; - _idGenerator = idGenerator; - _settingRepository = settingRepository; - _settingSerializerFactory = settingSerializerFactory; - } - - protected T ReadSetting(string key) - { - var categoryKey = _category + "." + key; - var setting = _settingRepository.AggregateQueryable.SingleOrDefault(s => s.Key == categoryKey); - if (setting == null) - { - return default(T); - } - - var serializer = _settingSerializerFactory.GetSerializer(); - return setting.GetValue(serializer); - } - - protected void WriteSetting(string key, T value) - { - var categoryKey = _category + "." + key; - var setting = _settingRepository.AggregateQueryable.SingleOrDefault(s => s.Key == categoryKey); - if (setting == null) - { - setting = new Setting(_idGenerator.NextId(), categoryKey); - _settingRepository.Add(setting); - } - - var serializer = _settingSerializerFactory.GetSerializer(); - setting.SetValue(serializer, value); - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/DependencyInjection/CompositionRoot.cs b/src/abstractions/Backend.Fx/DependencyInjection/CompositionRoot.cs new file mode 100644 index 00000000..2acc5663 --- /dev/null +++ b/src/abstractions/Backend.Fx/DependencyInjection/CompositionRoot.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using Backend.Fx.Logging; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.DependencyInjection +{ + /// + /// Encapsulates the injection framework of choice. The implementation follows the Register/Resolve/Release pattern. + /// Usage of this interface is only allowed for framework integration (or tests). NEVER (!) access the injector from + /// the domain or application logic, this would result in the Service Locator anti pattern, described here: + /// http://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/ + /// + [PublicAPI] + public interface ICompositionRoot : IDisposable + { + void Verify(); + + void RegisterModules(params IModule[] modules); + + void Register(ServiceDescriptor serviceDescriptor); + + void RegisterDecorator(ServiceDescriptor serviceDescriptor); + + void RegisterCollection(IEnumerable serviceDescriptors); + + IServiceScope BeginScope(); + + /// + /// Access to the container's resolution functionality + /// + IServiceProvider ServiceProvider { get; } + } + + [PublicAPI] + public abstract class CompositionRoot : ICompositionRoot + { + private readonly ILogger _logger = Log.Create(); + + public abstract IServiceProvider ServiceProvider { get; } + + public abstract void Verify(); + + public virtual void RegisterModules(params IModule[] modules) + { + foreach (IModule module in modules) + { + _logger.LogInformation("Registering {@Module}", module); + module.Register(this); + } + } + + public abstract void Register(ServiceDescriptor serviceDescriptor); + + public abstract void RegisterDecorator(ServiceDescriptor serviceDescriptor); + + public abstract void RegisterCollection(IEnumerable serviceDescriptors); + + public abstract IServiceScope BeginScope(); + + protected abstract void Dispose(bool disposing); + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/IModule.cs b/src/abstractions/Backend.Fx/DependencyInjection/IModule.cs similarity index 67% rename from src/abstractions/Backend.Fx/Patterns/DependencyInjection/IModule.cs rename to src/abstractions/Backend.Fx/DependencyInjection/IModule.cs index 762f2042..97647018 100644 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/IModule.cs +++ b/src/abstractions/Backend.Fx/DependencyInjection/IModule.cs @@ -1,8 +1,11 @@ -namespace Backend.Fx.Patterns.DependencyInjection +using JetBrains.Annotations; + +namespace Backend.Fx.DependencyInjection { /// /// A logically cohesive bunch of services /// + [PublicAPI] public interface IModule { void Register(ICompositionRoot compositionRoot); diff --git a/src/abstractions/Backend.Fx/DependencyInjection/LogRegistrationsDecorator.cs b/src/abstractions/Backend.Fx/DependencyInjection/LogRegistrationsDecorator.cs new file mode 100644 index 00000000..0c8ac4a7 --- /dev/null +++ b/src/abstractions/Backend.Fx/DependencyInjection/LogRegistrationsDecorator.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Backend.Fx.Logging; +using Backend.Fx.Util; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.DependencyInjection +{ + public class LogRegistrationsDecorator : ICompositionRoot + { + private readonly ILogger _logger = Log.Create(); + private readonly ICompositionRoot _compositionRoot; + + public LogRegistrationsDecorator(ICompositionRoot compositionRoot) + { + _compositionRoot = compositionRoot; + } + + public void Dispose() + { + _compositionRoot.Dispose(); + } + + public void Verify() + { + _compositionRoot.Verify(); + } + + public void RegisterModules(params IModule[] modules) + { + _compositionRoot.RegisterModules(modules); + } + + public void Register(ServiceDescriptor serviceDescriptor) + { + LogDetails("Adding", "registration", serviceDescriptor); + _compositionRoot.Register(serviceDescriptor); + } + + public void RegisterDecorator(ServiceDescriptor serviceDescriptor) + { + LogDetails("Adding", "decorator", serviceDescriptor); + _compositionRoot.RegisterDecorator(serviceDescriptor); + } + + public void RegisterCollection(IEnumerable serviceDescriptors) + { + serviceDescriptors = serviceDescriptors as ServiceDescriptor[] ?? serviceDescriptors.ToArray(); + LogAddCollectionRegistration(serviceDescriptors); + if (serviceDescriptors.GroupBy(sd => sd.ServiceType).Count() > 1) + { + _logger.LogError("Attempt to register a collection of services for different service types"); + } + _compositionRoot.RegisterCollection(serviceDescriptors); + } + + private void LogAddCollectionRegistration(IEnumerable serviceDescriptors) + { + serviceDescriptors = serviceDescriptors as ServiceDescriptor[] ?? serviceDescriptors.ToArray(); + _logger.LogDebug("{Verb} {Lifetime} {RegistrationType} for {ServiceType}: {ImplementationType}", + "Adding", + serviceDescriptors.First().Lifetime.ToString().ToLowerInvariant(), + "collection registration", + serviceDescriptors.First().ServiceType.GetDetailedTypeName(), + string.Join(", ", serviceDescriptors.Select(sd => sd.GetImplementationTypeDescription()))); + } + + public IServiceScope BeginScope() + { + return _compositionRoot.BeginScope(); + } + + public IServiceProvider ServiceProvider => _compositionRoot.ServiceProvider; + + + private void LogDetails(string verb, string registrationType, ServiceDescriptor serviceDescriptor) + { + _logger.LogDebug("{Verb} {Lifetime} {RegistrationType} for {ServiceType}: {ImplementationType}", + verb, + serviceDescriptor.Lifetime.ToString().ToLowerInvariant(), + registrationType, + serviceDescriptor.ServiceType.GetDetailedTypeName(), + serviceDescriptor.GetImplementationTypeDescription()); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/DependencyInjection/ServiceDescriptorEx.cs b/src/abstractions/Backend.Fx/DependencyInjection/ServiceDescriptorEx.cs new file mode 100644 index 00000000..cfa36738 --- /dev/null +++ b/src/abstractions/Backend.Fx/DependencyInjection/ServiceDescriptorEx.cs @@ -0,0 +1,30 @@ +using Backend.Fx.Util; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.DependencyInjection +{ + [PublicAPI] + public static class ServiceDescriptorEx + { + public static string GetImplementationTypeDescription(this ServiceDescriptor serviceDescriptor) + { + if (serviceDescriptor.ImplementationFactory != null) + { + return serviceDescriptor.ImplementationFactory.GetType().GetDetailedTypeName(); + } + + if (serviceDescriptor.ImplementationType != null) + { + return serviceDescriptor.ImplementationType.GetDetailedTypeName(); + } + + if (serviceDescriptor.ImplementationInstance != null) + { + return serviceDescriptor.ImplementationInstance.GetType().GetDetailedTypeName(); + } + + return "Unknown"; + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Domain/IAggregateRoot.cs b/src/abstractions/Backend.Fx/Domain/IAggregateRoot.cs new file mode 100644 index 00000000..11e7725a --- /dev/null +++ b/src/abstractions/Backend.Fx/Domain/IAggregateRoot.cs @@ -0,0 +1,18 @@ +using System; + +namespace Backend.Fx.Domain +{ + /// + /// The root of an aggregate. + /// + public interface IAggregateRoot {} + + /// + /// The root of an aggregate, identified by an id of type . + /// + public interface IAggregateRoot : IAggregateRoot + where TId : IEquatable + { + public TId Id { get; } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Domain/Identified.cs b/src/abstractions/Backend.Fx/Domain/Identified.cs new file mode 100644 index 00000000..1eee10d3 --- /dev/null +++ b/src/abstractions/Backend.Fx/Domain/Identified.cs @@ -0,0 +1,59 @@ +using System; +using System.Diagnostics; +using JetBrains.Annotations; + +namespace Backend.Fx.Domain +{ + [PublicAPI] + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + "}")] + public abstract class Identified : IEquatable> + { + public TId Id { get; init; } + + /// + /// DON'T USE! + /// This ctor is only here to allow O/R-Mappers to materialize an object coming from a persistent + /// store using reflection. + /// + protected Identified() + { + } + + protected Identified(TId id) + { + Id = id; + } + + [UsedImplicitly] public string DebuggerDisplay => $"{GetType().Name}[{Id}]"; + + public bool Equals(Identified other) + { + return other != null && Id.Equals(other.Id); + } + + public override bool Equals(object obj) + { + var other = obj as Identified; + return other != null && Id.Equals(other.Id); + } + + public override int GetHashCode() + { + return Id.GetHashCode(); + } + + public static bool operator ==(Identified left, Identified right) + { + if (ReferenceEquals(left, null) && ReferenceEquals(right, null)) return true; + if (ReferenceEquals(left, null) || ReferenceEquals(right, null)) return false; + + return ReferenceEquals(left, right) || right.Id.Equals(left.Id); + + } + + public static bool operator !=(Identified left, Identified right) + { + return !(left == right); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/BuildingBlocks/ValueObject.cs b/src/abstractions/Backend.Fx/Domain/ValueObject.cs similarity index 68% rename from src/abstractions/Backend.Fx/BuildingBlocks/ValueObject.cs rename to src/abstractions/Backend.Fx/Domain/ValueObject.cs index f4d5a5f3..50757d5a 100644 --- a/src/abstractions/Backend.Fx/BuildingBlocks/ValueObject.cs +++ b/src/abstractions/Backend.Fx/Domain/ValueObject.cs @@ -2,13 +2,13 @@ using System.Collections.Generic; using System.Linq; -namespace Backend.Fx.BuildingBlocks +namespace Backend.Fx.Domain { /// /// An object that contains attributes but has no conceptual identity. /// https://en.wikipedia.org/wiki/Domain-driven_design#Building_blocks /// - public abstract class ValueObject + public abstract class ValueObject : IEquatable { /// /// When overriden in a derived class, returns all components of a value objects which constitute its identity. @@ -16,6 +16,14 @@ public abstract class ValueObject /// An ordered list of equality components. protected abstract IEnumerable GetEqualityComponents(); + public bool Equals(ValueObject other) + { + if (ReferenceEquals(this, other)) return true; + if (ReferenceEquals(null, other)) return false; + + return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents()); + } + public override bool Equals(object obj) { if (ReferenceEquals(this, obj)) return true; @@ -29,7 +37,7 @@ public override int GetHashCode() unchecked { var hash = 17; - foreach (object obj in GetEqualityComponents()) + foreach (var obj in GetEqualityComponents()) { hash = hash * 23 + (obj != null ? obj.GetHashCode() : 0); } @@ -74,35 +82,33 @@ protected int CompareTo(ComparableValueObject other) { return 1; } - - using (var thisComponents = GetComparableComponents().GetEnumerator()) - using (var otherComponents = other.GetComparableComponents().GetEnumerator()) + + using var thisComponents = GetComparableComponents().GetEnumerator(); + using var otherComponents = other.GetComparableComponents().GetEnumerator(); + while (true) { - while (true) + var x = thisComponents.MoveNext(); + var y = otherComponents.MoveNext(); + if (x != y) { - var x = thisComponents.MoveNext(); - var y = otherComponents.MoveNext(); - if (x != y) - { - throw new InvalidOperationException(); - } + throw new InvalidOperationException(); + } - if (x) - { - var c = thisComponents.Current?.CompareTo(otherComponents.Current) ?? 0; - if (c != 0) - { - return c; - } - } - else + if (x) + { + var c = thisComponents.Current?.CompareTo(otherComponents.Current) ?? 0; + if (c != 0) { - break; + return c; } } - - return 0; + else + { + break; + } } + + return 0; } } diff --git a/src/abstractions/Backend.Fx/Environment/Authentication/AnonymousIdentity.cs b/src/abstractions/Backend.Fx/Environment/Authentication/AnonymousIdentity.cs deleted file mode 100644 index 3ef29a26..00000000 --- a/src/abstractions/Backend.Fx/Environment/Authentication/AnonymousIdentity.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Security.Principal; - -namespace Backend.Fx.Environment.Authentication -{ - public class AnonymousIdentity : IIdentity - { - public string Name => "ANONYMOUS"; - - public string AuthenticationType => string.Empty; - - public bool IsAuthenticated => false; - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/Authentication/SystemIdentity.cs b/src/abstractions/Backend.Fx/Environment/Authentication/SystemIdentity.cs deleted file mode 100644 index cb388295..00000000 --- a/src/abstractions/Backend.Fx/Environment/Authentication/SystemIdentity.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Security.Principal; - -namespace Backend.Fx.Environment.Authentication -{ - public class SystemIdentity : IIdentity - { - public string Name => "SYSTEM"; - - public string AuthenticationType => "system internal"; - - public bool IsAuthenticated => true; - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/DateAndTime/AdjustableClock.cs b/src/abstractions/Backend.Fx/Environment/DateAndTime/AdjustableClock.cs deleted file mode 100644 index f9572972..00000000 --- a/src/abstractions/Backend.Fx/Environment/DateAndTime/AdjustableClock.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using Backend.Fx.Logging; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.Environment.DateAndTime -{ - public class AdjustableClock : IClock - { - private static readonly ILogger Logger = Log.Create(); - - private readonly IClock _clockImplementation; - private DateTime? _overriddenUtcNow; - - public AdjustableClock(IClock clockImplementation) - { - _clockImplementation = clockImplementation; - } - - public DateTime UtcNow => _overriddenUtcNow ?? _clockImplementation.UtcNow; - - public void OverrideUtcNow(DateTime utcNow) - { - Logger.LogTrace("Adjusting clock to {UtcNow}", utcNow); - _overriddenUtcNow = utcNow; - } - - public DateTime Advance(TimeSpan timespan) - { - _overriddenUtcNow = _overriddenUtcNow ?? _clockImplementation.UtcNow; - Logger.LogTrace("Advancing clock by {TimeSpan}", timespan); - _overriddenUtcNow = _overriddenUtcNow.Value.Add(timespan); - return _overriddenUtcNow.Value; - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/DateAndTime/FrozenClock.cs b/src/abstractions/Backend.Fx/Environment/DateAndTime/FrozenClock.cs deleted file mode 100644 index 22f3244a..00000000 --- a/src/abstractions/Backend.Fx/Environment/DateAndTime/FrozenClock.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using Backend.Fx.Logging; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.Environment.DateAndTime -{ - /// - /// Best practice for web (service) applications: time does not advance during a single request - /// - public class FrozenClock : IClock - { - private static readonly ILogger Logger = Log.Create(); - - // ReSharper disable once UnusedParameter.Local - public FrozenClock(IClock clock) - { - UtcNow = DateTime.UtcNow; - Logger.LogTrace("Freezing clock at {UtcNow}", UtcNow); - } - - public DateTime UtcNow { get; } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/DateAndTime/IClock.cs b/src/abstractions/Backend.Fx/Environment/DateAndTime/IClock.cs deleted file mode 100644 index dcd4a886..00000000 --- a/src/abstractions/Backend.Fx/Environment/DateAndTime/IClock.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace Backend.Fx.Environment.DateAndTime -{ - /// - /// Wraps access to DateTime.UtcNow. By means of this interface the current time can be mocked. - /// the database should only store universal date and time values, that could be translated into user's time by applying a UtcOffset - /// - public interface IClock - { - DateTime UtcNow { get; } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/DateAndTime/WallClock.cs b/src/abstractions/Backend.Fx/Environment/DateAndTime/WallClock.cs deleted file mode 100644 index 5ca43a75..00000000 --- a/src/abstractions/Backend.Fx/Environment/DateAndTime/WallClock.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Backend.Fx.Environment.DateAndTime -{ - /// - /// The real system clock - /// - public class WallClock : IClock - { - public DateTime UtcNow => DateTime.UtcNow; - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/AllTenantBackendFxApplicationInvoker.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/AllTenantBackendFxApplicationInvoker.cs deleted file mode 100644 index 818a558f..00000000 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/AllTenantBackendFxApplicationInvoker.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Linq; -using Backend.Fx.Environment.Authentication; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.Environment.MultiTenancy -{ - public class AllTenantBackendFxApplicationInvoker - { - private static readonly ILogger Logger = Log.Create(); - private readonly ITenantIdProvider _tenantIdProvider; - private readonly IBackendFxApplicationInvoker _invoker; - - public AllTenantBackendFxApplicationInvoker(ITenantIdProvider tenantIdProvider, IBackendFxApplicationInvoker invoker) - { - _tenantIdProvider = tenantIdProvider; - _invoker = invoker; - } - - public void Invoke(Action action) - { - var correlationId = Guid.NewGuid(); - TenantId[] tenantIds = _tenantIdProvider.GetActiveDemonstrationTenantIds().Concat(_tenantIdProvider.GetActiveProductionTenantIds()).ToArray(); - Logger.LogDebug("Action will be called in tenants: {TenantIds}", string.Join(",", tenantIds.Select(t => t.ToString()))); - foreach (TenantId tenantId in tenantIds) - { - _invoker.Invoke(action, new SystemIdentity(), tenantId, correlationId); - } - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/ITenantIdProvider.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/ITenantIdProvider.cs deleted file mode 100644 index 44873e50..00000000 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/ITenantIdProvider.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Backend.Fx.Environment.MultiTenancy -{ - /// - /// By means of this instance, the IBackendFxApplication gains insight about all active tenants. This is required, when for example a job - /// should be executed for all tenants or data should be generated for all tenants during startup. - /// The can provide such implementation, but this can only be done in process. When the tenant service is - /// running in another process, the implementation must be done using a suitable remoting technology. - /// - public interface ITenantIdProvider - { - TenantId[] GetActiveTenantIds(); - TenantId[] GetActiveDemonstrationTenantIds(); - TenantId[] GetActiveProductionTenantIds(); - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/ITenantRepository.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/ITenantRepository.cs deleted file mode 100644 index 2f8292f5..00000000 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/ITenantRepository.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Backend.Fx.Environment.MultiTenancy -{ - public interface ITenantRepository - { - void SaveTenant(Tenant tenant); - - Tenant[] GetTenants(); - - Tenant GetTenant(TenantId tenantId); - - void DeleteTenant(TenantId tenantId); - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/SingleTenantApplication.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/SingleTenantApplication.cs deleted file mode 100644 index 640c3a38..00000000 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/SingleTenantApplication.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Linq; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.EventAggregation.Integration; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.Environment.MultiTenancy -{ - public class SingleTenantApplication - { - private static readonly ILogger Logger = Log.Create(); - private readonly ITenantService _tenantService; - private readonly bool _singleTenantIsDemoTenant; - private readonly object _padlock = new object(); - - public SingleTenantApplication( - IMessageBus messageBus, - ITenantRepository tenantRepository, - bool singleTenantIsDemoTenant) - { - _tenantService = new TenantService(messageBus, tenantRepository); - _singleTenantIsDemoTenant = singleTenantIsDemoTenant; - } - - public TenantId TenantId { get; private set; } - - public ITenantIdProvider TenantProvider => _tenantService.TenantIdProvider; - - public void Boot() - { - lock (_padlock) - { - Logger.Info("Ensuring existence of single tenant"); - TenantId = _tenantService.GetActiveTenants().SingleOrDefault()?.GetTenantId() - ?? _tenantService.CreateTenant("Single Tenant", - "This application runs in single tenant mode", - _singleTenantIsDemoTenant); - } - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantActivated.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantActivated.cs deleted file mode 100644 index ad2a5a9a..00000000 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantActivated.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Backend.Fx.Environment.MultiTenancy -{ - public class TenantActivated : TenantEvent - { - public TenantActivated(int tenantId, string name, string description, bool isDemoTenant) - : base(tenantId, name, description, isDemoTenant) - { - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantDeactivated.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantDeactivated.cs deleted file mode 100644 index c014cfca..00000000 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantDeactivated.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Backend.Fx.Environment.MultiTenancy -{ - public class TenantDeactivated : TenantEvent - { - public TenantDeactivated(int tenantId, string name, string description, bool isDemoTenant) - : base(tenantId, name, description, isDemoTenant) - { - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantDeleted.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantDeleted.cs deleted file mode 100644 index e845d341..00000000 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantDeleted.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Backend.Fx.Environment.MultiTenancy -{ - public class TenantDeleted : TenantEvent - { - public TenantDeleted(int tenantId, string name, string description, bool isDemoTenant) - : base(tenantId, name, description, isDemoTenant) - { - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantEvent.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantEvent.cs deleted file mode 100644 index ec6590c4..00000000 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantEvent.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Backend.Fx.Patterns.EventAggregation.Integration; - -namespace Backend.Fx.Environment.MultiTenancy -{ - public abstract class TenantEvent : IntegrationEvent - { - protected TenantEvent(int tenantId, string name, string description, bool isDemoTenant) : base() - { - Name = name; - Description = description; - IsDemoTenant = isDemoTenant; - } - - public string Name { get; } - - public string Description { get; } - - public bool IsDemoTenant { get; } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantId.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantId.cs deleted file mode 100644 index fbcd5ffd..00000000 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantId.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using Backend.Fx.BuildingBlocks; - -namespace Backend.Fx.Environment.MultiTenancy -{ - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + "}")] - public class TenantId : ValueObject - { - private readonly int? _id; - - public TenantId(int? id) - { - _id = id; - } - - /// - /// Throws on null id - /// - public int Value - { - get - { - if (_id == null) - { - throw new InvalidOperationException("You must not access the Value property when the tenant id is null"); - } - - return _id.Value; - } - } - - public bool HasValue => _id.HasValue; - - protected string DebuggerDisplay - { - get - { - if (HasValue) - { - return $"TenantId: {Value}"; - } - - return "TenantId: null"; - } - } - - public override string ToString() - { - return _id?.ToString() ?? "NULL"; - } - - protected override IEnumerable GetEqualityComponents() - { - yield return _id; - } - - public static explicit operator int(TenantId tid) => tid.Value; - public static explicit operator TenantId(int id) => new TenantId(id); - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantService.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantService.cs deleted file mode 100644 index b5959451..00000000 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantService.cs +++ /dev/null @@ -1,192 +0,0 @@ -using System; -using System.Linq; -using Backend.Fx.Exceptions; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.EventAggregation.Integration; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.Environment.MultiTenancy -{ - /// - /// Encapsulates the management of tenants - /// Note that this should not use repositories and other building blocks, but access the persistence layer directly - /// - public interface ITenantService - { - /// - /// The tenant service can also provide an . Keep in mind that this instance uses a direct - /// database connection. When multiple microservices do not share the same database, this instance cannot be used, but must - /// be implemented by a client to the master tenant service, probably using a remoting technology like RESTful Service, HTTP, - /// gRPC or SOAP web service - /// - ITenantIdProvider TenantIdProvider { get; } - - TenantId CreateTenant(string name, string description, bool isDemonstrationTenant, string configuration = null); - void ActivateTenant(TenantId tenantId); - void DeactivateTenant(TenantId tenantId); - void DeleteTenant(TenantId tenantId); - Tenant UpdateTenant(TenantId tenantId, string name, string description, string configuration); - - Tenant[] GetTenants(); - Tenant[] GetActiveTenants(); - Tenant[] GetActiveDemonstrationTenants(); - Tenant[] GetActiveProductionTenants(); - Tenant GetTenant(TenantId tenantId); - } - - public class TenantService : ITenantService - { - private static readonly ILogger Logger = Log.Create(); - private readonly IMessageBus _messageBus; - private readonly ITenantRepository _tenantRepository; - - public ITenantIdProvider TenantIdProvider { get; } - - public TenantService(IMessageBus messageBus, ITenantRepository tenantRepository) - { - _messageBus = messageBus; - _tenantRepository = tenantRepository; - TenantIdProvider = new TenantServiceTenantIdProvider(this); - } - - public TenantId CreateTenant(string name, string description, bool isDemonstrationTenant, string configuration = null) - { - Logger.LogInformation("Creating tenant: {Name}", name); - - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); - } - - if (_tenantRepository.GetTenants().Any(t => t.Name != null && t.Name.ToLowerInvariant() == name.ToLowerInvariant())) - { - throw new ArgumentException($"There is already a tenant named {name}"); - } - - var tenant = new Tenant(name, description, isDemonstrationTenant) { Configuration = configuration }; - _tenantRepository.SaveTenant(tenant); - var tenantId = new TenantId(tenant.Id); - _messageBus.Publish(new TenantActivated(tenant.Id, tenant.Name, tenant.Description, tenant.IsDemoTenant)); - return tenantId; - } - - public void ActivateTenant(TenantId tenantId) - { - Logger.LogInformation("Activating tenant: {TenantId}", tenantId); - Tenant tenant = _tenantRepository.GetTenant(tenantId); - tenant.State = TenantState.Active; - _tenantRepository.SaveTenant(tenant); - _messageBus.Publish(new TenantActivated(tenant.Id, tenant.Name, tenant.Description, tenant.IsDemoTenant)); - } - - public void DeactivateTenant(TenantId tenantId) - { - Logger.LogInformation("Deactivating tenant: {TenantId}", tenantId); - Tenant tenant = _tenantRepository.GetTenant(tenantId); - tenant.State = TenantState.Inactive; - _tenantRepository.SaveTenant(tenant); - _messageBus.Publish(new TenantDeactivated(tenant.Id, tenant.Name, tenant.Description, tenant.IsDemoTenant)); - } - - public void DeleteTenant(TenantId tenantId) - { - Logger.LogInformation("Deleting tenant: {TenantId}", tenantId); - Tenant tenant = _tenantRepository.GetTenant(tenantId); - if (tenant.State != TenantState.Inactive) - { - throw new UnprocessableException($"Attempt to delete active tenant[{tenantId.Value}]") - .AddError("You cannot delete an active tenant. Please make sure to deactivate it first."); - } - - _tenantRepository.DeleteTenant(tenantId); - _messageBus.Publish(new TenantDeactivated(tenant.Id, tenant.Name, tenant.Description, tenant.IsDemoTenant)); - } - - public Tenant GetTenant(TenantId tenantId) - { - return _tenantRepository.GetTenant(tenantId); - } - - public Tenant UpdateTenant(TenantId tenantId, string name, string description, string configuration) - { - var tenant = _tenantRepository.GetTenant(tenantId); - tenant.Name = name; - tenant.Description = description; - tenant.Configuration = configuration; - _tenantRepository.SaveTenant(tenant); - _messageBus.Publish(new TenantUpdated(tenant.Id, name, description, tenant.IsDemoTenant)); - return tenant; - } - - public Tenant[] GetTenants() - { - var tenants = _tenantRepository.GetTenants(); - Logger.LogTrace("TenantIds: {TenantIds}", string.Join(",", tenants.Select(t => t.ToString()))); - return tenants; - } - - public Tenant[] GetActiveTenants() - { - var activeTenants = _tenantRepository - .GetTenants() - .Where(t => t.State == TenantState.Active) - .ToArray(); - Logger.LogTrace("Active TenantIds: {TenantIds}", string.Join(",", activeTenants.Select(t => t.ToString()))); - return activeTenants; - } - - public Tenant[] GetActiveDemonstrationTenants() - { - var activeDemonstrationTenants = _tenantRepository - .GetTenants() - .Where(t => t.State == TenantState.Active && t.IsDemoTenant) - .ToArray(); - Logger.LogTrace("Active Demonstration TenantIds: {TenantIds}", - string.Join(",", activeDemonstrationTenants.Select(t => t.ToString()))); - return activeDemonstrationTenants; - } - - public Tenant[] GetActiveProductionTenants() - { - var activeProductionTenants = _tenantRepository - .GetTenants() - .Where(t => t.State == TenantState.Active && !t.IsDemoTenant) - .ToArray(); - Logger.LogTrace("Active Production TenantIds: {TenantIds}", - string.Join(",", activeProductionTenants.Select(t => t.ToString()))); - return activeProductionTenants; - } - - private class TenantServiceTenantIdProvider : ITenantIdProvider - { - private readonly ITenantService _tenantService; - - public TenantServiceTenantIdProvider(ITenantService tenantService) - { - _tenantService = tenantService; - } - - public TenantId[] GetActiveDemonstrationTenantIds() - { - return _tenantService.GetActiveDemonstrationTenants() - .Select(t => new TenantId(t.Id)) - .ToArray(); - } - - public TenantId[] GetActiveProductionTenantIds() - { - return _tenantService.GetActiveProductionTenants() - .Select(t => new TenantId(t.Id)) - .ToArray(); - } - - public TenantId[] GetActiveTenantIds() - { - return _tenantService.GetActiveTenants() - .Select(t => new TenantId(t.Id)) - .ToArray(); - } - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantUpdated.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantUpdated.cs deleted file mode 100644 index 509b7d89..00000000 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantUpdated.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Backend.Fx.Environment.MultiTenancy -{ - public class TenantUpdated : TenantEvent - { - public TenantUpdated(int tenantId, string name, string description, bool isDemoTenant) - : base(tenantId, name, description, isDemoTenant) - { - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/Persistence/BackendFxDbApplication.cs b/src/abstractions/Backend.Fx/Environment/Persistence/BackendFxDbApplication.cs deleted file mode 100644 index 501273cb..00000000 --- a/src/abstractions/Backend.Fx/Environment/Persistence/BackendFxDbApplication.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.Patterns.EventAggregation.Integration; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.Environment.Persistence -{ - public class BackendFxDbApplication : IBackendFxApplication - { - private static readonly ILogger Logger = Log.Create(); - - private readonly IDatabaseAvailabilityAwaiter _databaseAvailabilityAwaiter; - private readonly IBackendFxApplication _backendFxApplication; - private readonly IDatabaseBootstrapper _databaseBootstrapper; - - public BackendFxDbApplication(IDatabaseBootstrapper databaseBootstrapper, - IDatabaseAvailabilityAwaiter databaseAvailabilityAwaiter, - IBackendFxApplication backendFxApplication) - { - _databaseBootstrapper = databaseBootstrapper; - _databaseAvailabilityAwaiter = databaseAvailabilityAwaiter; - _backendFxApplication = backendFxApplication; - } - - public void Dispose() - { - Logger.LogTrace("Disposing..."); - _backendFxApplication.Dispose(); - } - - public IBackendFxApplicationAsyncInvoker AsyncInvoker => _backendFxApplication.AsyncInvoker; - - public ICompositionRoot CompositionRoot => _backendFxApplication.CompositionRoot; - - public IBackendFxApplicationInvoker Invoker => _backendFxApplication.Invoker; - - public IMessageBus MessageBus => _backendFxApplication.MessageBus; - - public bool WaitForBoot(int timeoutMilliSeconds = Int32.MaxValue, CancellationToken cancellationToken = default) - { - Logger.LogTrace("Waiting for boot..."); - return _backendFxApplication.WaitForBoot(timeoutMilliSeconds, cancellationToken); - } - - public Task Boot(CancellationToken cancellationToken = default) => BootAsync(cancellationToken); - public async Task BootAsync(CancellationToken cancellationToken = default) - { - Logger.LogTrace("Booting..."); - await _databaseAvailabilityAwaiter.WaitForDatabase(cancellationToken); - _databaseBootstrapper.EnsureDatabaseExistence(); - await _backendFxApplication.BootAsync(cancellationToken); - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/Persistence/DbConnectionOperationDecorator.cs b/src/abstractions/Backend.Fx/Environment/Persistence/DbConnectionOperationDecorator.cs deleted file mode 100644 index 93d6958b..00000000 --- a/src/abstractions/Backend.Fx/Environment/Persistence/DbConnectionOperationDecorator.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Data; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.Environment.Persistence -{ - public class DbConnectionOperationDecorator : IOperation - { - private static readonly ILogger Logger = Log.Create(); - private IDisposable _connectionLifetimeLogger; - public DbConnectionOperationDecorator(IDbConnection dbConnection, IOperation operation) - { - DbConnection = dbConnection; - Operation = operation; - } - - public IOperation Operation { get; } - - public IDbConnection DbConnection { get; } - - public void Begin() - { - Logger.LogDebug("Opening database connection"); - DbConnection.Open(); - _connectionLifetimeLogger = Logger.LogDebugDuration("Database connection open", "Database connection closed"); - Operation.Begin(); - } - - public void Complete() - { - Operation.Complete(); - Logger.LogDebug("Closing database connection"); - DbConnection.Close(); - _connectionLifetimeLogger?.Dispose(); - } - - public void Cancel() - { - Operation.Cancel(); - Logger.LogDebug("Closing database connection"); - DbConnection.Close(); - _connectionLifetimeLogger?.Dispose(); - - // note: we do not dispose the DbConnection here, because we did not instantiate it. Disposing is always up to the creator of - // the instance, that is in this case the injection container. - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/Persistence/FlushDomainEventAggregatorDecorator.cs b/src/abstractions/Backend.Fx/Environment/Persistence/FlushDomainEventAggregatorDecorator.cs deleted file mode 100644 index d9c70632..00000000 --- a/src/abstractions/Backend.Fx/Environment/Persistence/FlushDomainEventAggregatorDecorator.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Backend.Fx.Logging; -using Backend.Fx.Patterns.EventAggregation.Domain; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.Environment.Persistence -{ - public class FlushDomainEventAggregatorDecorator : IDomainEventAggregator - { - private static readonly ILogger Logger = Log.Create(); - - private readonly ICanFlush _canFlush; - private readonly IDomainEventAggregator _domainEventAggregatorImplementation; - - public FlushDomainEventAggregatorDecorator(ICanFlush canFlush, IDomainEventAggregator domainEventAggregatorImplementation) - { - _canFlush = canFlush; - _domainEventAggregatorImplementation = domainEventAggregatorImplementation; - } - - public void PublishDomainEvent(TDomainEvent domainEvent) where TDomainEvent : IDomainEvent - { - _domainEventAggregatorImplementation.PublishDomainEvent(domainEvent); - } - - public void RaiseEvents() - { - Logger.LogDebug("Flushing before raising domain events"); - _canFlush.Flush(); - _domainEventAggregatorImplementation.RaiseEvents(); - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/Persistence/FlushOperationDecorator.cs b/src/abstractions/Backend.Fx/Environment/Persistence/FlushOperationDecorator.cs deleted file mode 100644 index 3f3d00c7..00000000 --- a/src/abstractions/Backend.Fx/Environment/Persistence/FlushOperationDecorator.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.Environment.Persistence -{ - public class FlushOperationDecorator : IOperation - { - private static readonly ILogger Logger = Log.Create(); - private readonly IOperation _operationImplementation; - private readonly ICanFlush _canFlush; - - public FlushOperationDecorator(ICanFlush canFlush, IOperation operationImplementation) - { - _operationImplementation = operationImplementation; - _canFlush = canFlush; - } - - public void Begin() - { - _operationImplementation.Begin(); - } - - public void Complete() - { - Logger.LogDebug("Flushing before completion of operation"); - _canFlush.Flush(); - _operationImplementation.Complete(); - } - - public void Cancel() - { - _operationImplementation.Cancel(); - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/Persistence/ICanFlush.cs b/src/abstractions/Backend.Fx/Environment/Persistence/ICanFlush.cs deleted file mode 100644 index 7e27683e..00000000 --- a/src/abstractions/Backend.Fx/Environment/Persistence/ICanFlush.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Backend.Fx.Environment.Persistence -{ - public interface ICanFlush - { - void Flush(); - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/Persistence/ReadonlyDbTransactionOperationDecorator.cs b/src/abstractions/Backend.Fx/Environment/Persistence/ReadonlyDbTransactionOperationDecorator.cs deleted file mode 100644 index a33b85c3..00000000 --- a/src/abstractions/Backend.Fx/Environment/Persistence/ReadonlyDbTransactionOperationDecorator.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.Environment.Persistence -{ - public class ReadonlyDbTransactionOperationDecorator : IOperation - { - private static readonly ILogger Logger = Log.Create(); - private readonly IOperation _operationImplementation; - - public ReadonlyDbTransactionOperationDecorator(IOperation operationImplementation) - { - _operationImplementation = operationImplementation; - } - - public void Begin() - { - _operationImplementation.Begin(); - } - - public void Complete() - { - Logger.LogDebug("Canceling operation instead of completing it due to classification as readonly operation"); - _operationImplementation.Cancel(); - } - - public void Cancel() - { - _operationImplementation.Cancel(); - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Exceptions/ClientException.cs b/src/abstractions/Backend.Fx/Exceptions/ClientException.cs index dc32894d..5d716361 100644 --- a/src/abstractions/Backend.Fx/Exceptions/ClientException.cs +++ b/src/abstractions/Backend.Fx/Exceptions/ClientException.cs @@ -5,6 +5,7 @@ namespace Backend.Fx.Exceptions { + [PublicAPI] public class ClientException : Exception { public ClientException() @@ -25,12 +26,13 @@ public ClientException(string message) /// to the client to not provide internal details to an attacker. Write the exception message with a developer in mind, since /// the application log will contain the message. To provide the user with functional feedback to correct their input, use /// the AddError(s) overloads. + /// public ClientException(string message, Exception innerException) : base(message, innerException) { } - public Errors Errors { get; } = new Errors(); + public Errors Errors { get; } = new(); public bool HasErrors() { @@ -49,11 +51,11 @@ public override string ToString() string innerException = InnerException != null ? " ---> " + InnerException - + System.Environment.NewLine + + Environment.NewLine + " End of inner exception stack trace" : null; - return string.Join(System.Environment.NewLine, + return string.Join(Environment.NewLine, new[] {message, Errors.ToString(), innerException, StackTrace}.Where(s => s != null)); } @@ -68,6 +70,7 @@ public static IExceptionBuilder UseBuilder() } } + [PublicAPI] public static class ClientExceptionEx { public static TEx AddError(this TEx clientException, [LocalizationRequired] string errorMessage) where TEx : ClientException diff --git a/src/abstractions/Backend.Fx/Exceptions/ConflictedException.cs b/src/abstractions/Backend.Fx/Exceptions/ConflictedException.cs index 541d5ef9..b2c042cc 100644 --- a/src/abstractions/Backend.Fx/Exceptions/ConflictedException.cs +++ b/src/abstractions/Backend.Fx/Exceptions/ConflictedException.cs @@ -1,7 +1,9 @@ using System; +using JetBrains.Annotations; namespace Backend.Fx.Exceptions { + [PublicAPI] public class ConflictedException : ClientException { public ConflictedException() diff --git a/src/abstractions/Backend.Fx/Exceptions/Errors.cs b/src/abstractions/Backend.Fx/Exceptions/Errors.cs index 1aeae363..ca030628 100644 --- a/src/abstractions/Backend.Fx/Exceptions/Errors.cs +++ b/src/abstractions/Backend.Fx/Exceptions/Errors.cs @@ -2,14 +2,43 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using JetBrains.Annotations; namespace Backend.Fx.Exceptions { + /// + /// A structure to collect general or key related errors on a + /// + [PublicAPI] public class Errors : IReadOnlyDictionary { - private const string GenericErrorKey = ""; - private readonly IDictionary> _dictionaryImplementation = new Dictionary>(); + public const string GenericErrorKey = ""; + private readonly IDictionary> _dictionaryImplementation = + new Dictionary>(); + + public Errors() + { } + + public Errors(IDictionary dictionary) : this(null, dictionary) + { } + + public Errors(string genericError, IDictionary dictionary = null) + { + if (genericError != null) + { + Add(genericError); + } + + if (dictionary != null) + { + foreach (var kvp in dictionary) + { + Add(kvp.Key, kvp.Value); + } + } + } + public bool ContainsKey(string key) { return _dictionaryImplementation.ContainsKey(key); @@ -36,19 +65,19 @@ public IEnumerable Values get { return _dictionaryImplementation.Values.Select(errors => errors.ToArray()); } } - public Errors Add(string errorMessage) + internal Errors Add(string errorMessage) { Add(GenericErrorKey, errorMessage); return this; } - public Errors Add(IEnumerable errorMessages) + internal Errors Add(IEnumerable errorMessages) { Add(GenericErrorKey, errorMessages); return this; } - public Errors Add(string key, IEnumerable errorMessages) + internal Errors Add(string key, IEnumerable errorMessages) { if (!_dictionaryImplementation.ContainsKey(key)) { @@ -62,7 +91,7 @@ public Errors Add(string key, IEnumerable errorMessages) return this; } - public Errors Add(string key, string error) + internal Errors Add(string key, string error) { if (!_dictionaryImplementation.ContainsKey(key)) { @@ -73,10 +102,11 @@ public Errors Add(string key, string error) return this; } - + public IEnumerator> GetEnumerator() { - return _dictionaryImplementation.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.ToArray())).GetEnumerator(); + return _dictionaryImplementation + .Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.ToArray())).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() @@ -96,7 +126,7 @@ public override string ToString() foreach (var keyValuePair in this) { b.Append(" "); - b.Append(string.IsNullOrEmpty(keyValuePair.Key) ? "(generic)": keyValuePair.Key); + b.Append(keyValuePair.Key == GenericErrorKey ? "(generic)": keyValuePair.Key); b.AppendLine(); for (var index = 0; index < keyValuePair.Value.Length; index++) { diff --git a/src/abstractions/Backend.Fx/Exceptions/ExceptionBuilder.cs b/src/abstractions/Backend.Fx/Exceptions/ExceptionBuilder.cs index 79084a40..1139a784 100644 --- a/src/abstractions/Backend.Fx/Exceptions/ExceptionBuilder.cs +++ b/src/abstractions/Backend.Fx/Exceptions/ExceptionBuilder.cs @@ -1,7 +1,9 @@ using System; +using JetBrains.Annotations; namespace Backend.Fx.Exceptions { + [PublicAPI] public interface IExceptionBuilder : IDisposable { void Add(string error); @@ -11,9 +13,10 @@ public interface IExceptionBuilder : IDisposable void AddIf(string key, bool condition, string error); } + [PublicAPI] public class ExceptionBuilder : IExceptionBuilder where TEx : ClientException, new() { - private readonly TEx _clientException = new TEx(); + private readonly TEx _clientException = new(); public void Add(string error) { diff --git a/src/abstractions/Backend.Fx/Exceptions/ForbiddenException.cs b/src/abstractions/Backend.Fx/Exceptions/ForbiddenException.cs index e5db75a1..2bc6f195 100644 --- a/src/abstractions/Backend.Fx/Exceptions/ForbiddenException.cs +++ b/src/abstractions/Backend.Fx/Exceptions/ForbiddenException.cs @@ -1,11 +1,13 @@ using System; +using JetBrains.Annotations; namespace Backend.Fx.Exceptions { + [PublicAPI] public class ForbiddenException : ClientException { public ForbiddenException() - : base("Unauthorized") + : base("Forbidden") { } diff --git a/src/abstractions/Backend.Fx/Exceptions/NotFoundException.cs b/src/abstractions/Backend.Fx/Exceptions/NotFoundException.cs index cc3b2350..bd6d5fa9 100644 --- a/src/abstractions/Backend.Fx/Exceptions/NotFoundException.cs +++ b/src/abstractions/Backend.Fx/Exceptions/NotFoundException.cs @@ -1,5 +1,8 @@ -namespace Backend.Fx.Exceptions +using JetBrains.Annotations; + +namespace Backend.Fx.Exceptions { + [PublicAPI] public class NotFoundException : ClientException { public string EntityName { get; } @@ -17,8 +20,18 @@ public NotFoundException(string entityName, object id) EntityName = entityName; Id = id; } + + /// + /// Used to build a with multiple possible error messages. The builder will throw on disposal + /// when at least one error was added. Using the AddIf methods is quite comfortable when there are several criteria to be validated + /// before executing a business case. + /// + public new static IExceptionBuilder UseBuilder() + { + return new ExceptionBuilder(); + } } - + public class NotFoundException : NotFoundException { public NotFoundException(object id) diff --git a/src/abstractions/Backend.Fx/Exceptions/TooManyRequestsException.cs b/src/abstractions/Backend.Fx/Exceptions/TooManyRequestsException.cs index 42dd5f7c..0bf02898 100644 --- a/src/abstractions/Backend.Fx/Exceptions/TooManyRequestsException.cs +++ b/src/abstractions/Backend.Fx/Exceptions/TooManyRequestsException.cs @@ -1,7 +1,9 @@ using System; +using JetBrains.Annotations; namespace Backend.Fx.Exceptions { + [PublicAPI] public class TooManyRequestsException : ClientException { public TooManyRequestsException(int retryAfter) diff --git a/src/abstractions/Backend.Fx/Exceptions/UnauthorizedException.cs b/src/abstractions/Backend.Fx/Exceptions/UnauthorizedException.cs index f365a9f2..f97816c0 100644 --- a/src/abstractions/Backend.Fx/Exceptions/UnauthorizedException.cs +++ b/src/abstractions/Backend.Fx/Exceptions/UnauthorizedException.cs @@ -1,7 +1,9 @@ using System; +using JetBrains.Annotations; namespace Backend.Fx.Exceptions { + [PublicAPI] public class UnauthorizedException : ClientException { public UnauthorizedException() diff --git a/src/abstractions/Backend.Fx/Exceptions/UnprocessableException.cs b/src/abstractions/Backend.Fx/Exceptions/UnprocessableException.cs index 018108a6..23b04088 100644 --- a/src/abstractions/Backend.Fx/Exceptions/UnprocessableException.cs +++ b/src/abstractions/Backend.Fx/Exceptions/UnprocessableException.cs @@ -1,7 +1,9 @@ using System; +using JetBrains.Annotations; namespace Backend.Fx.Exceptions { + [PublicAPI] public class UnprocessableException : ClientException { public UnprocessableException() diff --git a/src/abstractions/Backend.Fx/ExecutionPipeline/AnonymousIdentity.cs b/src/abstractions/Backend.Fx/ExecutionPipeline/AnonymousIdentity.cs new file mode 100644 index 00000000..ee54c2fc --- /dev/null +++ b/src/abstractions/Backend.Fx/ExecutionPipeline/AnonymousIdentity.cs @@ -0,0 +1,30 @@ +using System.Security.Principal; +using JetBrains.Annotations; + +namespace Backend.Fx.ExecutionPipeline +{ + [PublicAPI] + public readonly struct AnonymousIdentity : IIdentity + { + public string Name => "ANONYMOUS"; + + public string AuthenticationType => null; + + public bool IsAuthenticated => false; + + public override bool Equals(object obj) + { + return obj is AnonymousIdentity; + } + + public override int GetHashCode() + { + return 1564925492; + } + + public bool Equals(AnonymousIdentity other) + { + return true; + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/ExecutionPipeline/BackendFxApplicationInvoker.cs b/src/abstractions/Backend.Fx/ExecutionPipeline/BackendFxApplicationInvoker.cs new file mode 100644 index 00000000..fb9ddad4 --- /dev/null +++ b/src/abstractions/Backend.Fx/ExecutionPipeline/BackendFxApplicationInvoker.cs @@ -0,0 +1,96 @@ +using System; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Logging; +using Backend.Fx.Util; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.ExecutionPipeline +{ + public interface IBackendFxApplicationInvoker + { + /// + /// Run a delegate through the full execution pipeline, having its separate injection scope + /// + /// The async action to be invoked by the application + /// The acting identity + /// Pass an existing cancellation token (e.g. HttpContext.RequestAborted) to + /// enable cancellation of the async invocation. + /// The representing the async invocation. + Task InvokeAsync( + Func awaitableAsyncAction, + IIdentity identity = null, + CancellationToken cancellationToken = default); + + /// + /// Run a delegate through the full execution pipeline, having its separate injection scope + /// + /// The async action to be invoked by the application + /// The acting identity + /// The representing the async invocation. + Task InvokeAsync( + Func awaitableAsyncAction, + IIdentity identity = null); + } + + + internal class BackendFxApplicationInvoker : IBackendFxApplicationInvoker + { + private readonly IBackendFxApplication _application; + private readonly ILogger _logger = Log.Create(); + + public BackendFxApplicationInvoker(IBackendFxApplication application) + { + _application = application; + } + + public async Task InvokeAsync( + Func awaitableAsyncAction, + IIdentity identity = null, + CancellationToken cancellationToken = default) + { + identity ??= new AnonymousIdentity(); + _logger.LogInformation("Invoking action as {Identity}", identity.Name); + using IServiceScope serviceScope = BeginScope(identity); + using IDisposable durationLogger = UseDurationLogger(serviceScope); + var operation = serviceScope.ServiceProvider.GetRequiredService(); + try + { + await operation.BeginAsync(serviceScope, cancellationToken).ConfigureAwait(false); + await awaitableAsyncAction.Invoke(serviceScope.ServiceProvider, cancellationToken).ConfigureAwait(false); + await operation.CompleteAsync(cancellationToken).ConfigureAwait(false); + } + catch + { + await operation.CancelAsync(cancellationToken).ConfigureAwait(false); + throw; + } + } + + public Task InvokeAsync(Func awaitableAsyncAction, IIdentity identity = null) + => InvokeAsync((sp, _) => awaitableAsyncAction.Invoke(sp), identity); + + + private IServiceScope BeginScope(IIdentity identity) + { + IServiceScope serviceScope = _application.CompositionRoot.BeginScope(); + + identity ??= new AnonymousIdentity(); + serviceScope.ServiceProvider.GetRequiredService>().ReplaceCurrent(identity); + + return serviceScope; + } + + + private IDisposable UseDurationLogger(IServiceScope serviceScope) + { + IIdentity identity = serviceScope.ServiceProvider.GetRequiredService>().Current; + Correlation correlation = serviceScope.ServiceProvider.GetRequiredService>().Current; + return _logger.LogInformationDuration( + $"Starting invocation (correlation [{correlation.Id}]) for {identity.Name}", + $"Ended invocation (correlation [{correlation.Id}]) for {identity.Name}"); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/Correlation.cs b/src/abstractions/Backend.Fx/ExecutionPipeline/Correlation.cs similarity index 64% rename from src/abstractions/Backend.Fx/Patterns/DependencyInjection/Correlation.cs rename to src/abstractions/Backend.Fx/ExecutionPipeline/Correlation.cs index ec5f6d23..f0b8dfd1 100644 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/Correlation.cs +++ b/src/abstractions/Backend.Fx/ExecutionPipeline/Correlation.cs @@ -1,24 +1,25 @@ using System; using Backend.Fx.Logging; +using JetBrains.Annotations; using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; -namespace Backend.Fx.Patterns.DependencyInjection +namespace Backend.Fx.ExecutionPipeline { /// /// A guid that is unique for an invocation. In case of an invocation as result of handling an integration event, the correlation /// is stable, that is, the correlation can be used to track a logical action over different systems. /// - public class Correlation + [PublicAPI] + public sealed class Correlation { - private static readonly ILogger Logger = Log.Create(); + private readonly ILogger _logger = Log.Create(); public Guid Id { get; private set; } = Guid.NewGuid(); public void Resume(Guid correlationId) { Id = correlationId; - Logger.LogInformation("Resuming correlation {@Correlation}", this); + _logger.LogInformation("Resuming correlation {Correlation}", Id); } } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/CurrentCorrelationHolder.cs b/src/abstractions/Backend.Fx/ExecutionPipeline/CurrentCorrelationHolder.cs similarity index 60% rename from src/abstractions/Backend.Fx/Patterns/DependencyInjection/CurrentCorrelationHolder.cs rename to src/abstractions/Backend.Fx/ExecutionPipeline/CurrentCorrelationHolder.cs index 70a9f67e..9e406ac4 100644 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/CurrentCorrelationHolder.cs +++ b/src/abstractions/Backend.Fx/ExecutionPipeline/CurrentCorrelationHolder.cs @@ -1,6 +1,10 @@ -namespace Backend.Fx.Patterns.DependencyInjection +using Backend.Fx.Util; +using JetBrains.Annotations; + +namespace Backend.Fx.ExecutionPipeline { - public class CurrentCorrelationHolder : CurrentTHolder + [PublicAPI] + public sealed class CurrentCorrelationHolder : CurrentTHolder { public override Correlation ProvideInstance() { diff --git a/src/abstractions/Backend.Fx/Environment/Authentication/CurrentIdentityHolder.cs b/src/abstractions/Backend.Fx/ExecutionPipeline/CurrentIdentityHolder.cs similarity index 61% rename from src/abstractions/Backend.Fx/Environment/Authentication/CurrentIdentityHolder.cs rename to src/abstractions/Backend.Fx/ExecutionPipeline/CurrentIdentityHolder.cs index 9799769d..7447c25e 100644 --- a/src/abstractions/Backend.Fx/Environment/Authentication/CurrentIdentityHolder.cs +++ b/src/abstractions/Backend.Fx/ExecutionPipeline/CurrentIdentityHolder.cs @@ -1,9 +1,11 @@ using System.Security.Principal; -using Backend.Fx.Patterns.DependencyInjection; +using Backend.Fx.Util; +using JetBrains.Annotations; -namespace Backend.Fx.Environment.Authentication +namespace Backend.Fx.ExecutionPipeline { - public class CurrentIdentityHolder : CurrentTHolder + [PublicAPI] + public sealed class CurrentIdentityHolder : CurrentTHolder { public CurrentIdentityHolder() { } @@ -18,13 +20,10 @@ public override IIdentity ProvideInstance() protected override string Describe(IIdentity instance) { - if (instance == null) - { - return ""; - } - - string auth = instance.IsAuthenticated ? $"authenticated via {instance.AuthenticationType}" : "not authenticated"; - return $"Identity: {instance.Name}, {auth}"; + var auth = instance?.IsAuthenticated == true + ? $"authenticated via {instance.AuthenticationType}" + : "not authenticated"; + return $"Identity: {instance?.Name ?? ""}, {auth}"; } public static ICurrentTHolder CreateSystem() diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ExceptionLoggingAndHandlingInvoker.cs b/src/abstractions/Backend.Fx/ExecutionPipeline/ExceptionLoggingAndHandlingInvoker.cs similarity index 53% rename from src/abstractions/Backend.Fx/Patterns/DependencyInjection/ExceptionLoggingAndHandlingInvoker.cs rename to src/abstractions/Backend.Fx/ExecutionPipeline/ExceptionLoggingAndHandlingInvoker.cs index 2616f0aa..e566378d 100644 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ExceptionLoggingAndHandlingInvoker.cs +++ b/src/abstractions/Backend.Fx/ExecutionPipeline/ExceptionLoggingAndHandlingInvoker.cs @@ -1,9 +1,10 @@ using System; using System.Security.Principal; -using Backend.Fx.Environment.MultiTenancy; +using System.Threading; +using System.Threading.Tasks; using Backend.Fx.Logging; -namespace Backend.Fx.Patterns.DependencyInjection +namespace Backend.Fx.ExecutionPipeline { public class ExceptionLoggingAndHandlingInvoker : IBackendFxApplicationInvoker { @@ -16,16 +17,20 @@ public ExceptionLoggingAndHandlingInvoker(IExceptionLogger exceptionLogger, IBac _invoker = invoker; } - public void Invoke(Action action, IIdentity identity, TenantId tenantId, Guid? correlationId = null) + public async Task InvokeAsync(Func awaitableAsyncAction, + IIdentity identity, CancellationToken cancellationToken = default) { try { - _invoker.Invoke(action, identity, tenantId, correlationId); + await _invoker.InvokeAsync(awaitableAsyncAction, identity, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { _exceptionLogger.LogException(ex); } } + + public Task InvokeAsync(Func awaitableAsyncAction, IIdentity identity = null) + => InvokeAsync((sp, _) => awaitableAsyncAction.Invoke(sp), identity); } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/ExecutionPipeline/ExceptionLoggingInvoker.cs b/src/abstractions/Backend.Fx/ExecutionPipeline/ExceptionLoggingInvoker.cs new file mode 100644 index 00000000..dfd17635 --- /dev/null +++ b/src/abstractions/Backend.Fx/ExecutionPipeline/ExceptionLoggingInvoker.cs @@ -0,0 +1,37 @@ +using System; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Logging; + +namespace Backend.Fx.ExecutionPipeline +{ + internal class ExceptionLoggingInvoker : IBackendFxApplicationInvoker + { + private readonly IExceptionLogger _exceptionLogger; + private readonly IBackendFxApplicationInvoker _invoker; + + public ExceptionLoggingInvoker(IExceptionLogger exceptionLogger, IBackendFxApplicationInvoker invoker) + { + _exceptionLogger = exceptionLogger; + _invoker = invoker; + } + + public async Task InvokeAsync(Func awaitableAsyncAction, + IIdentity identity, CancellationToken cancellationToken = default) + { + try + { + await _invoker.InvokeAsync(awaitableAsyncAction, identity, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _exceptionLogger.LogException(ex); + throw; + } + } + + public Task InvokeAsync(Func awaitableAsyncAction, IIdentity identity = null) + => InvokeAsync((sp, _) => awaitableAsyncAction.Invoke(sp), identity); + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/ExecutionPipeline/ExecutionPipelineModule.cs b/src/abstractions/Backend.Fx/ExecutionPipeline/ExecutionPipelineModule.cs new file mode 100644 index 00000000..42d3e6c0 --- /dev/null +++ b/src/abstractions/Backend.Fx/ExecutionPipeline/ExecutionPipelineModule.cs @@ -0,0 +1,42 @@ +using System.Security.Principal; +using Backend.Fx.DependencyInjection; +using Backend.Fx.Util; +using Microsoft.Extensions.DependencyInjection; +using NodaTime; + +namespace Backend.Fx.ExecutionPipeline +{ + internal class ExecutionPipelineModule : IModule + { + private readonly bool _withFrozenClockDuringExecution; + + public ExecutionPipelineModule(bool withFrozenClockDuringExecution = true) + { + _withFrozenClockDuringExecution = withFrozenClockDuringExecution; + } + + public void Register(ICompositionRoot compositionRoot) + { + compositionRoot.Register( + ServiceDescriptor.Singleton(_ => SystemClock.Instance)); + + if (_withFrozenClockDuringExecution) + { + compositionRoot.RegisterDecorator( + ServiceDescriptor.Scoped()); + } + + compositionRoot.Register( + ServiceDescriptor.Singleton()); + + compositionRoot.Register( + ServiceDescriptor.Scoped()); + + compositionRoot.Register( + ServiceDescriptor.Scoped, CurrentIdentityHolder>()); + + compositionRoot.Register( + ServiceDescriptor.Scoped, CurrentCorrelationHolder>()); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/ExecutionPipeline/FrozenClock.cs b/src/abstractions/Backend.Fx/ExecutionPipeline/FrozenClock.cs new file mode 100644 index 00000000..95813a70 --- /dev/null +++ b/src/abstractions/Backend.Fx/ExecutionPipeline/FrozenClock.cs @@ -0,0 +1,27 @@ +using Backend.Fx.Logging; +using Microsoft.Extensions.Logging; +using NodaTime; + +namespace Backend.Fx.ExecutionPipeline +{ + /// + /// Best practice for web (service) applications: time does not advance during an invocation + /// + public class FrozenClock : IClock + { + private readonly ILogger _logger = Log.Create(); + private readonly Instant _frozenInstant; + + public FrozenClock(IClock clock) + { + _frozenInstant = clock.GetCurrentInstant(); + _logger.LogTrace("Freezing clock at {Instant}", _frozenInstant); + } + + + public Instant GetCurrentInstant() + { + return _frozenInstant; + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/ExecutionPipeline/IdentityEx.cs b/src/abstractions/Backend.Fx/ExecutionPipeline/IdentityEx.cs new file mode 100644 index 00000000..d864104e --- /dev/null +++ b/src/abstractions/Backend.Fx/ExecutionPipeline/IdentityEx.cs @@ -0,0 +1,17 @@ +using System.Security.Principal; + +namespace Backend.Fx.ExecutionPipeline +{ + public static class IdentityEx + { + public static bool IsAnonymous(this IIdentity identity) + { + return identity is AnonymousIdentity; + } + + public static bool IsSystem(this IIdentity identity) + { + return identity is SystemIdentity; + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/ExecutionPipeline/Operation.cs b/src/abstractions/Backend.Fx/ExecutionPipeline/Operation.cs new file mode 100644 index 00000000..0f23250f --- /dev/null +++ b/src/abstractions/Backend.Fx/ExecutionPipeline/Operation.cs @@ -0,0 +1,75 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Logging; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.ExecutionPipeline +{ + /// + /// The basic interface of an operation invoked by the . + /// Decorate this interface to provide operation specific infrastructure services (like a database connection, a database transaction + /// an entry-exit logging etc.) + /// + [PublicAPI] + public interface IOperation + { + Task BeginAsync(IServiceScope serviceScope, CancellationToken cancellationToken = default); + + Task CompleteAsync(CancellationToken cancellationToken = default); + + Task CancelAsync(CancellationToken cancellationToken = default); + } + + + [UsedImplicitly] + internal sealed class Operation : IOperation + { + private readonly ILogger _logger = Log.Create(); + private readonly int _instanceId; + private bool? _isActive; + private IDisposable _lifetimeLogger; + + public Operation(OperationCounter operationCounter) + { + _instanceId = operationCounter.Count(); + } + + public Task BeginAsync(IServiceScope serviceScope, CancellationToken cancellationToken = default) + { + if (_isActive != null) + { + throw new InvalidOperationException($"Cannot begin an operation that is {(_isActive.Value ? "active" : "terminated")}"); + } + + _lifetimeLogger = _logger.LogDebugDuration($"Beginning operation #{_instanceId}", $"Terminating operation #{_instanceId}"); + _isActive = true; + return Task.CompletedTask; + } + + public Task CompleteAsync(CancellationToken cancellationToken = default) + { + _logger.LogInformation("Completing operation #{OperationId}", _instanceId); + if (_isActive != true) + { + throw new InvalidOperationException($"Cannot complete an operation that is {(_isActive == false ? "terminated" : "not active")}"); + } + + _isActive = false; + _lifetimeLogger?.Dispose(); + _lifetimeLogger = null; + return Task.CompletedTask; + } + + public Task CancelAsync(CancellationToken cancellationToken = default) + { + _logger.LogInformation("Canceling operation #{OperationId}", _instanceId); + _isActive = false; + _lifetimeLogger?.Dispose(); + _lifetimeLogger = null; + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/ExecutionPipeline/OperationCounter.cs b/src/abstractions/Backend.Fx/ExecutionPipeline/OperationCounter.cs new file mode 100644 index 00000000..67ce2bec --- /dev/null +++ b/src/abstractions/Backend.Fx/ExecutionPipeline/OperationCounter.cs @@ -0,0 +1,14 @@ +using System.Threading; + +namespace Backend.Fx.ExecutionPipeline +{ + public class OperationCounter + { + private int _count; + + public int Count() + { + return Interlocked.Increment(ref _count); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/ExecutionPipeline/SystemIdentity.cs b/src/abstractions/Backend.Fx/ExecutionPipeline/SystemIdentity.cs new file mode 100644 index 00000000..f0897677 --- /dev/null +++ b/src/abstractions/Backend.Fx/ExecutionPipeline/SystemIdentity.cs @@ -0,0 +1,30 @@ +using System.Security.Principal; +using JetBrains.Annotations; + +namespace Backend.Fx.ExecutionPipeline +{ + [PublicAPI] + public readonly struct SystemIdentity : IIdentity + { + public string Name => "SYSTEM"; + + public string AuthenticationType => "Internal"; + + public bool IsAuthenticated => true; + + public override bool Equals(object obj) + { + return obj is SystemIdentity; + } + + public override int GetHashCode() + { + return 542451621; + } + + public bool Equals(SystemIdentity other) + { + return true; + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Extensions/DateTimeEx.cs b/src/abstractions/Backend.Fx/Extensions/DateTimeEx.cs deleted file mode 100644 index 0d7ad38d..00000000 --- a/src/abstractions/Backend.Fx/Extensions/DateTimeEx.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace Backend.Fx.Extensions -{ - using System; - - public static class DateTimeEx - { - /// - /// Gets the related start of week of the DateTime at midnight. - /// - /// - /// Specify the DayOfWeek that you consider as first day of week. Default value: Monday - /// - public static DateTime StartOfWeek(this DateTime dt, DayOfWeek startOfWeek = DayOfWeek.Monday) - { - int diff = dt.DayOfWeek - startOfWeek; - if (diff < 0) - { - diff += 7; - } - - return dt.AddDays(-1 * diff).Date; - } - - public static DateTime GetWeekDay(this DateTime dt, DayOfWeek dayOfWeek, DayOfWeek startOfWeek = DayOfWeek.Monday) - { - dt = dt.StartOfWeek(startOfWeek); - while (dt.DayOfWeek != dayOfWeek) - { - dt = dt.AddDays(1); - } - - return dt; - } - - public static long ToUnixEpochDate(this DateTime utcDate) - { - return (long) Math.Round((utcDate - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds); - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Extensions/ReflectionEx.cs b/src/abstractions/Backend.Fx/Extensions/ReflectionEx.cs deleted file mode 100644 index 1d357fa8..00000000 --- a/src/abstractions/Backend.Fx/Extensions/ReflectionEx.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; - -namespace Backend.Fx.Extensions -{ - public static class ReflectionEx - { - public static bool IsImplementationOfOpenGenericInterface(this Type t, Type openGenericInterface) - { - return t.GetInterfaces().Any(x => x.GetTypeInfo().IsGenericType && x.GetGenericTypeDefinition() == openGenericInterface); - } - - public static string GetDetailedTypeName(this Type t) - { - string detailedTypeName = t.Name; - if (t.GetTypeInfo().IsGenericType) - { - var genericNameWithoutArgCount = t.Name.Substring(0, t.Name.IndexOf('`')); - var typeArgNames = t.GenericTypeArguments.Select(a => a.Name); - detailedTypeName = $"{genericNameWithoutArgCount}<{string.Join(",", typeArgNames)}>"; - } - - return detailedTypeName; - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/Authorization/AllowAll.cs b/src/abstractions/Backend.Fx/Features/Authorization/AllowAll.cs similarity index 61% rename from src/abstractions/Backend.Fx/Patterns/Authorization/AllowAll.cs rename to src/abstractions/Backend.Fx/Features/Authorization/AllowAll.cs index 6b416d79..afc1e193 100644 --- a/src/abstractions/Backend.Fx/Patterns/Authorization/AllowAll.cs +++ b/src/abstractions/Backend.Fx/Features/Authorization/AllowAll.cs @@ -1,10 +1,11 @@ using System; using System.Linq.Expressions; -using Backend.Fx.BuildingBlocks; +using Backend.Fx.Domain; -namespace Backend.Fx.Patterns.Authorization +namespace Backend.Fx.Features.Authorization { - public class AllowAll : AggregateAuthorization where TAggregateRoot : AggregateRoot + public class AllowAll : AuthorizationPolicy + where TAggregateRoot : IAggregateRoot { public override Expression> HasAccessExpression { diff --git a/src/abstractions/Backend.Fx/Features/Authorization/AuthorizationFeature.cs b/src/abstractions/Backend.Fx/Features/Authorization/AuthorizationFeature.cs new file mode 100644 index 00000000..e586836b --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Authorization/AuthorizationFeature.cs @@ -0,0 +1,28 @@ +using System.Linq; +using Backend.Fx.Domain; +using Backend.Fx.Exceptions; +using Backend.Fx.Features.Persistence; +using JetBrains.Annotations; + +namespace Backend.Fx.Features.Authorization +{ + /// + /// The feature "Authorization" obligates you the implementation of an + /// for every . Instances of these policy classes are applied to the repositories, so + /// that on every read or write operation on it, the policy is automatically enforced. Denied reads won't fail but + /// just appear invisible, while a denied write throws a . + /// Note that this feature implicitly depends on the persistence feature, more specific on a persistence + /// implementation that provides s to the repository. + /// While implementing policies, you can start by deriving from or + /// . + /// + [PublicAPI] + public class AuthorizationFeature : Feature + { + public override void Enable(IBackendFxApplication application) + { + application.RequireDependantFeature(); + application.CompositionRoot.RegisterModules(new AuthorizationModule(application.Assemblies)); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Authorization/AuthorizationModule.cs b/src/abstractions/Backend.Fx/Features/Authorization/AuthorizationModule.cs new file mode 100644 index 00000000..3e4152e5 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Authorization/AuthorizationModule.cs @@ -0,0 +1,79 @@ +using System; +using System.Linq; +using System.Reflection; +using Backend.Fx.DependencyInjection; +using Backend.Fx.Domain; +using Backend.Fx.Features.Persistence; +using Backend.Fx.Logging; +using Backend.Fx.Util; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.Features.Authorization +{ + internal class AuthorizationModule : IModule + { + private readonly ILogger _logger = Log.Create(); + private readonly Assembly[] _assemblies; + + public AuthorizationModule(Assembly[] assemblies) + { + _assemblies = assemblies; + } + + public void Register(ICompositionRoot compositionRoot) + { + RegisterAuthorizingDecorators(compositionRoot); + RegisterAuthorizationPolicies(compositionRoot); + } + + private void RegisterAuthorizingDecorators(ICompositionRoot compositionRoot) + { + _logger.LogDebug("Registering authorization decorators"); + compositionRoot.RegisterDecorator(ServiceDescriptor.Scoped(typeof(IQueryable<>), + typeof(AuthorizingQueryable<>))); + compositionRoot.RegisterDecorator(ServiceDescriptor.Scoped(typeof(IRepository<,>), + typeof(AuthorizingRepository<,>))); + } + + private void RegisterAuthorizationPolicies(ICompositionRoot compositionRoot) + { + // ReSharper disable once CoVariantArrayConversion + _logger.LogDebug("Registering authorization services from {Assemblies}", _assemblies); + + var aggregateRootTypes = _assemblies + .SelectMany(ass => ass.GetTypes()) + .Where(t => t.IsImplementationOfOpenGenericInterface(typeof(IAggregateRoot<>))) + .ToArray(); + + foreach (Type aggregateRootType in aggregateRootTypes) + { + Type authorizationPolicyInterfaceType = + typeof(IAuthorizationPolicy<>).MakeGenericType(aggregateRootType); + var authorizationPolicyTypes = _assemblies + .GetImplementingTypes(authorizationPolicyInterfaceType) + .ToArray(); + + if (authorizationPolicyTypes.Length == 0) + { + _logger.LogWarning( + "No authorization policies for {AggregateRootType} found", aggregateRootType); + return; + } + + if (authorizationPolicyTypes.Length > 1) + { + throw new InvalidOperationException( + $"Multiple authorization policies found for {aggregateRootType.Name}: " + + $"[{string.Join(", ", authorizationPolicyTypes.Select(t => t.Name))}]"); + } + + _logger.LogInformation( + "Registering scoped authorization service {ServiceType} with implementation {ImplementationType}", + authorizationPolicyInterfaceType, + authorizationPolicyTypes[0]); + compositionRoot.Register(ServiceDescriptor.Scoped(authorizationPolicyInterfaceType, authorizationPolicyTypes[0])); + } + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Authorization/AuthorizationPolicy.cs b/src/abstractions/Backend.Fx/Features/Authorization/AuthorizationPolicy.cs new file mode 100644 index 00000000..4c49f40c --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Authorization/AuthorizationPolicy.cs @@ -0,0 +1,40 @@ +using System; +using System.Linq.Expressions; +using Backend.Fx.Domain; +using Backend.Fx.Logging; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.Features.Authorization +{ + public abstract class AuthorizationPolicy : IAuthorizationPolicy + where TAggregateRoot : IAggregateRoot + { + private readonly ILogger _logger = Log.Create>(); + + /// > + public abstract Expression> HasAccessExpression { get; } + + /// > + public abstract bool CanCreate(TAggregateRoot t); + + /// + /// Implement a guard that might disallow modifying an existing aggregate. + /// This overload is called directly before saving modification of an instance, so that you can use the instance's state for deciding. + /// This default implementation forwards to + /// + public virtual bool CanModify(TAggregateRoot t) + { + var canModify = CanCreate(t); + _logger.LogTrace("CanModify({@Aggregate}): {CanModify}", t, canModify); + return canModify; + } + + /// > + public virtual bool CanDelete(TAggregateRoot t) + { + var canDelete = CanModify(t); + _logger.LogTrace("CanDelete({@Aggregate}): {CanDelete}", t, canDelete); + return canDelete; + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Authorization/AuthorizingQueryable.cs b/src/abstractions/Backend.Fx/Features/Authorization/AuthorizingQueryable.cs new file mode 100644 index 00000000..2f564090 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Authorization/AuthorizingQueryable.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Backend.Fx.Domain; + +namespace Backend.Fx.Features.Authorization +{ + /// + /// Applies the authorization policy expression to the queryable via decoration + /// + internal class AuthorizingQueryable : IQueryable + where TAggregateRoot : IAggregateRoot + { + private readonly IAuthorizationPolicy _authorizationPolicy; + private readonly IQueryable _aggregateQueryable; + + public AuthorizingQueryable(IAuthorizationPolicy authorizationPolicy, IQueryable aggregateQueryable) + { + _authorizationPolicy = authorizationPolicy; + _aggregateQueryable = aggregateQueryable; + } + + public Type ElementType => _aggregateQueryable.ElementType; + + public Expression Expression + { + get + { + // expression tree manipulation: apply the HasAccessExpression to the basic Queryable Expression using "Where" + MethodCallExpression queryableWhereExpression = Expression.Call( + typeof(Queryable), + "Where", + new [] { ElementType }, + _aggregateQueryable.Expression, + Expression.Quote(_authorizationPolicy.HasAccessExpression)); + + return queryableWhereExpression; + } + } + + public IQueryProvider Provider => _aggregateQueryable.Provider; + + public IEnumerator GetEnumerator() + { + return _aggregateQueryable.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_aggregateQueryable).GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Authorization/AuthorizingRepository.cs b/src/abstractions/Backend.Fx/Features/Authorization/AuthorizingRepository.cs new file mode 100644 index 00000000..4b753f23 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Authorization/AuthorizingRepository.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Domain; +using Backend.Fx.Exceptions; +using Backend.Fx.Features.Persistence; + +namespace Backend.Fx.Features.Authorization +{ + /// + /// Checks the authorization policy for write operations + /// + internal class AuthorizingRepository : IRepository + where TAggregateRoot : IAggregateRoot + where TId : IEquatable + { + private readonly IAuthorizationPolicy _authorizationPolicy; + private readonly IRepository _repository; + + public AuthorizingRepository(IAuthorizationPolicy authorizationPolicy, IRepository repository) + { + _authorizationPolicy = authorizationPolicy; + _repository = repository; + } + + public Task GetAsync(TId id, CancellationToken cancellationToken = default) + { + return _repository.GetAsync(id, cancellationToken); + } + + public Task FindAsync(TId id, CancellationToken cancellationToken = default) + { + return _repository.FindAsync(id, cancellationToken); + } + + public Task GetAllAsync(CancellationToken cancellationToken = default) + { + return _repository.GetAllAsync(cancellationToken); + } + + public Task AnyAsync(CancellationToken cancellationToken = default) + { + return _repository.AnyAsync(cancellationToken); + } + + public Task ResolveAsync(IEnumerable ids, CancellationToken cancellationToken = default) + { + return _repository.ResolveAsync(ids, cancellationToken); + } + + public async Task DeleteAsync(TAggregateRoot aggregateRoot, CancellationToken cancellationToken = default) + { + if (_authorizationPolicy.CanDelete(aggregateRoot)) + { + await _repository.DeleteAsync(aggregateRoot, cancellationToken).ConfigureAwait(false); + } + else + { + throw new ForbiddenException("You are not allowed to delete this record"); + } + } + + public async Task AddAsync(TAggregateRoot aggregateRoot, CancellationToken cancellationToken = default) + { + if (_authorizationPolicy.CanCreate(aggregateRoot)) + { + await _repository.AddAsync(aggregateRoot, cancellationToken).ConfigureAwait(false); + } + else + { + throw new ForbiddenException("You are not allowed to create such a record"); + } + } + + public async Task AddRangeAsync(TAggregateRoot[] aggregateRoots, CancellationToken cancellationToken = default) + { + if (aggregateRoots.Any(ar => !_authorizationPolicy.CanCreate(ar))) + { + throw new ForbiddenException("You are not allowed to create such a record"); + } + + await _repository.AddRangeAsync(aggregateRoots, cancellationToken).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/Authorization/DenyAll.cs b/src/abstractions/Backend.Fx/Features/Authorization/DenyAll.cs similarity index 61% rename from src/abstractions/Backend.Fx/Patterns/Authorization/DenyAll.cs rename to src/abstractions/Backend.Fx/Features/Authorization/DenyAll.cs index 3e99e961..ec418805 100644 --- a/src/abstractions/Backend.Fx/Patterns/Authorization/DenyAll.cs +++ b/src/abstractions/Backend.Fx/Features/Authorization/DenyAll.cs @@ -1,10 +1,11 @@ using System; using System.Linq.Expressions; -using Backend.Fx.BuildingBlocks; +using Backend.Fx.Domain; -namespace Backend.Fx.Patterns.Authorization +namespace Backend.Fx.Features.Authorization { - public class DenyAll : AggregateAuthorization where TAggregateRoot : AggregateRoot + public class DenyAll : AuthorizationPolicy + where TAggregateRoot : IAggregateRoot { public override Expression> HasAccessExpression { diff --git a/src/abstractions/Backend.Fx/Patterns/Authorization/IAggregateAuthorization.cs b/src/abstractions/Backend.Fx/Features/Authorization/IAuthorizationPolicy.cs similarity index 65% rename from src/abstractions/Backend.Fx/Patterns/Authorization/IAggregateAuthorization.cs rename to src/abstractions/Backend.Fx/Features/Authorization/IAuthorizationPolicy.cs index 0eb6773b..cddda868 100644 --- a/src/abstractions/Backend.Fx/Patterns/Authorization/IAggregateAuthorization.cs +++ b/src/abstractions/Backend.Fx/Features/Authorization/IAuthorizationPolicy.cs @@ -1,25 +1,22 @@ -using System.Linq; +using System; using System.Linq.Expressions; -using Backend.Fx.BuildingBlocks; +using Backend.Fx.Domain; +using JetBrains.Annotations; -namespace Backend.Fx.Patterns.Authorization +namespace Backend.Fx.Features.Authorization { /// - /// Implements permissions on aggregate level. The respective instance is applied when creating an , + /// Implements permissions on aggregate level. The respective instance is applied when creating an , /// so that the repository never allows reading or writing of an aggregate without permissions. /// - /// - public interface IAggregateAuthorization where TAggregateRoot : AggregateRoot + [PublicAPI] + public interface IAuthorizationPolicy + where TAggregateRoot : IAggregateRoot { /// /// Express a filter for repository queryable /// - Expression> HasAccessExpression { get; } - - /// - /// Only if the filter expression is not sufficient, you can override this method to apply the filtering to the queryable directly. - /// - IQueryable Filter(IQueryable queryable); + Expression> HasAccessExpression { get; } /// /// Implement a guard that might disallow adding to the repository. diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/ConfigurationSettingsFeature.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/ConfigurationSettingsFeature.cs new file mode 100644 index 00000000..8f5a7b4c --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/ConfigurationSettingsFeature.cs @@ -0,0 +1,31 @@ +using JetBrains.Annotations; + +namespace Backend.Fx.Features.ConfigurationSettings +{ + /// + /// The feature "Configuration Settings" provides a simple abstraction over an arbitrary key/value configuration + /// setting store. The default already provides serialization to and from + /// string for various configuration setting types, but you can provide your own implementation to extend the + /// functionality. + /// + /// The abstraction over your key/value store. Instances of this type will + /// be injected with a scoped lifetime. + [PublicAPI] + public class ConfigurationSettingsFeature : Feature + where TSettingRepository : class, ISettingRepository + { + private readonly SettingSerializerFactory _settingSerializerFactory; + + /// The factory that provides serializers. A singleton instance is being held. + public ConfigurationSettingsFeature(SettingSerializerFactory settingSerializerFactory = null) + { + _settingSerializerFactory = settingSerializerFactory ?? new SettingSerializerFactory(); + } + + public override void Enable(IBackendFxApplication application) + { + application.CompositionRoot.RegisterModules( + new ConfigurationSettingsModule(_settingSerializerFactory, application.Assemblies)); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/ConfigurationSettingsModule.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/ConfigurationSettingsModule.cs new file mode 100644 index 00000000..4b4b54f0 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/ConfigurationSettingsModule.cs @@ -0,0 +1,36 @@ +using System; +using System.Reflection; +using Backend.Fx.DependencyInjection; +using Backend.Fx.Util; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.Features.ConfigurationSettings +{ + internal class ConfigurationSettingsModule : IModule + where TSettingRepository : class, ISettingRepository + { + private readonly SettingSerializerFactory _settingSerializerFactory; + private readonly Assembly[] _assemblies; + + public ConfigurationSettingsModule(SettingSerializerFactory settingSerializerFactory, Assembly[] assemblies) + { + _settingSerializerFactory = settingSerializerFactory; + _assemblies = assemblies; + } + + public void Register(ICompositionRoot compositionRoot) + { + compositionRoot.Register( + ServiceDescriptor.Singleton(_settingSerializerFactory)); + + compositionRoot.Register( + ServiceDescriptor.Scoped()); + + foreach (Type settingsCategoryType in _assemblies.GetImplementingTypes()) + { + compositionRoot.Register(ServiceDescriptor.Scoped(settingsCategoryType, settingsCategoryType)); + } + + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/ISettingRepository.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/ISettingRepository.cs new file mode 100644 index 00000000..14db94f5 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/ISettingRepository.cs @@ -0,0 +1,25 @@ +using JetBrains.Annotations; + +namespace Backend.Fx.Features.ConfigurationSettings +{ + [PublicAPI] + public interface ISettingRepository + { + /// + /// Gets the serialized string value for a specific setting key in a specific category + /// + /// The category of the setting + /// The key of the setting + /// The serialized value of the configuration setting, or null when not configured. + [CanBeNull] + string GetSerializedValue([NotNull] string category, [NotNull] string key); + + /// + /// Writes the serialized string value for a specific setting key in a specific category + /// + /// The category of the setting + /// The key of the setting + /// The serialized value of the configuration setting + void WriteSerializedValue([NotNull] string category, [NotNull] string key, [CanBeNull] string serializedValue); + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/ISettingSerializer.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/ISettingSerializer.cs new file mode 100644 index 00000000..ad3ec434 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/ISettingSerializer.cs @@ -0,0 +1,12 @@ +namespace Backend.Fx.Features.ConfigurationSettings +{ + public interface ISettingSerializer + { + } + + public interface ISettingSerializer : ISettingSerializer + { + string Serialize(T setting); + T Deserialize(string value); + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/InMem/InMemorySettingRepository.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/InMem/InMemorySettingRepository.cs new file mode 100644 index 00000000..de786f9d --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/InMem/InMemorySettingRepository.cs @@ -0,0 +1,28 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using JetBrains.Annotations; + +namespace Backend.Fx.Features.ConfigurationSettings.InMem +{ + [PublicAPI] + public abstract class InMemorySettingRepository : ISettingRepository + { + protected abstract ConcurrentDictionary> SettingsStore { get; } + + public string GetSerializedValue(string category, string key) + { + if (SettingsStore.TryGetValue(category, out var categorizedValues) && categorizedValues.TryGetValue(key, out var value)) + { + return value; + } + + return null; + } + + public void WriteSerializedValue(string category, string key, string serializedValue) + { + var categorizedValues = SettingsStore.GetOrAdd(category, _ => new Dictionary()); + categorizedValues[key] = serializedValue; + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/AnnualDateSerializer.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/AnnualDateSerializer.cs new file mode 100644 index 00000000..ac56555b --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/AnnualDateSerializer.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; +using NodaTime; +using NodaTime.Text; + +namespace Backend.Fx.Features.ConfigurationSettings.Serializers +{ + [UsedImplicitly] + public class AnnualDateSerializer : NodaTimePatternSerializer + { + public AnnualDateSerializer() : base(AnnualDatePattern.Iso) { } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/BooleanSerializer.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/BooleanSerializer.cs new file mode 100644 index 00000000..2d4413e1 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/BooleanSerializer.cs @@ -0,0 +1,18 @@ +using JetBrains.Annotations; + +namespace Backend.Fx.Features.ConfigurationSettings.Serializers +{ + [UsedImplicitly] + public class BooleanSerializer : ISettingSerializer + { + public string Serialize(bool? setting) + { + return setting?.ToString(); + } + + public bool? Deserialize(string value) + { + return string.IsNullOrWhiteSpace(value) ? null : bool.Parse(value); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/DateTimeOffsetSerializer.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/DateTimeOffsetSerializer.cs new file mode 100644 index 00000000..10ca70a3 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/DateTimeOffsetSerializer.cs @@ -0,0 +1,20 @@ +using System; +using System.Globalization; +using JetBrains.Annotations; + +namespace Backend.Fx.Features.ConfigurationSettings.Serializers +{ + [UsedImplicitly] + public class DateTimeOffsetSerializer : ISettingSerializer + { + public string Serialize(DateTimeOffset? setting) + { + return setting?.ToString("O", CultureInfo.InvariantCulture); + } + + public DateTimeOffset? Deserialize(string value) + { + return string.IsNullOrWhiteSpace(value) ? null : DateTimeOffset.Parse(value, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/DateTimeSerializer.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/DateTimeSerializer.cs new file mode 100644 index 00000000..d9bc5d49 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/DateTimeSerializer.cs @@ -0,0 +1,20 @@ +using System; +using System.Globalization; +using JetBrains.Annotations; + +namespace Backend.Fx.Features.ConfigurationSettings.Serializers +{ + [UsedImplicitly] + public class DateTimeSerializer : ISettingSerializer + { + public string Serialize(DateTime? setting) + { + return setting?.ToString("O", CultureInfo.InvariantCulture); + } + + public DateTime? Deserialize(string value) + { + return string.IsNullOrWhiteSpace(value) ? null : DateTime.Parse(value, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/DecimalSerializer.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/DecimalSerializer.cs new file mode 100644 index 00000000..026a0eae --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/DecimalSerializer.cs @@ -0,0 +1,19 @@ +using System.Globalization; +using JetBrains.Annotations; + +namespace Backend.Fx.Features.ConfigurationSettings.Serializers +{ + [UsedImplicitly] + public class DecimalSerializer : ISettingSerializer + { + public string Serialize(decimal? setting) + { + return setting?.ToString("G", CultureInfo.InvariantCulture); + } + + public decimal? Deserialize(string value) + { + return string.IsNullOrWhiteSpace(value) ? null : decimal.Parse(value, CultureInfo.InvariantCulture); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/DoubleSerializer.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/DoubleSerializer.cs new file mode 100644 index 00000000..0bfad272 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/DoubleSerializer.cs @@ -0,0 +1,19 @@ +using System.Globalization; +using JetBrains.Annotations; + +namespace Backend.Fx.Features.ConfigurationSettings.Serializers +{ + [UsedImplicitly] + public class DoubleSerializer : ISettingSerializer + { + public string Serialize(double? setting) + { + return setting?.ToString("r", CultureInfo.InvariantCulture); + } + + public double? Deserialize(string value) + { + return string.IsNullOrWhiteSpace(value) ? null : double.Parse(value, CultureInfo.InvariantCulture); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/DurationSerializer.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/DurationSerializer.cs new file mode 100644 index 00000000..db9135dc --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/DurationSerializer.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; +using NodaTime; +using NodaTime.Text; + +namespace Backend.Fx.Features.ConfigurationSettings.Serializers +{ + [UsedImplicitly] + public class DurationSerializer : NodaTimePatternSerializer + { + public DurationSerializer() : base(DurationPattern.Roundtrip) { } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/FloatSerializer.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/FloatSerializer.cs new file mode 100644 index 00000000..f188dadf --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/FloatSerializer.cs @@ -0,0 +1,19 @@ +using System.Globalization; +using JetBrains.Annotations; + +namespace Backend.Fx.Features.ConfigurationSettings.Serializers +{ + [UsedImplicitly] + public class FloatSerializer : ISettingSerializer + { + public string Serialize(float? setting) + { + return setting?.ToString("r", CultureInfo.InvariantCulture); + } + + public float? Deserialize(string value) + { + return string.IsNullOrWhiteSpace(value) ? null : float.Parse(value, CultureInfo.InvariantCulture); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/InstantSerializer.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/InstantSerializer.cs new file mode 100644 index 00000000..032d947c --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/InstantSerializer.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; +using NodaTime; +using NodaTime.Text; + +namespace Backend.Fx.Features.ConfigurationSettings.Serializers +{ + [UsedImplicitly] + public class InstantSerializer : NodaTimePatternSerializer + { + public InstantSerializer() : base(InstantPattern.ExtendedIso) { } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/IntegerSerializer.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/IntegerSerializer.cs new file mode 100644 index 00000000..8cfcdc41 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/IntegerSerializer.cs @@ -0,0 +1,19 @@ +using System.Globalization; +using JetBrains.Annotations; + +namespace Backend.Fx.Features.ConfigurationSettings.Serializers +{ + [UsedImplicitly] + public class IntegerSerializer : ISettingSerializer + { + public string Serialize(int? setting) + { + return setting?.ToString(CultureInfo.InvariantCulture); + } + + public int? Deserialize(string value) + { + return string.IsNullOrWhiteSpace(value) ? null : int.Parse(value, CultureInfo.InvariantCulture); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/LocalDateSerializer.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/LocalDateSerializer.cs new file mode 100644 index 00000000..9c38535d --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/LocalDateSerializer.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; +using NodaTime; +using NodaTime.Text; + +namespace Backend.Fx.Features.ConfigurationSettings.Serializers +{ + [UsedImplicitly] + public class LocalDateSerializer : NodaTimePatternSerializer + { + public LocalDateSerializer() : base(LocalDatePattern.FullRoundtrip) { } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/LocalDateTimeSerializer.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/LocalDateTimeSerializer.cs new file mode 100644 index 00000000..92e02788 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/LocalDateTimeSerializer.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; +using NodaTime; +using NodaTime.Text; + +namespace Backend.Fx.Features.ConfigurationSettings.Serializers +{ + [UsedImplicitly] + public class LocalDateTimeSerializer : NodaTimePatternSerializer + { + public LocalDateTimeSerializer() : base(LocalDateTimePattern.BclRoundtrip) { } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/LocalTimeSerializer.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/LocalTimeSerializer.cs new file mode 100644 index 00000000..b247914f --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/LocalTimeSerializer.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; +using NodaTime; +using NodaTime.Text; + +namespace Backend.Fx.Features.ConfigurationSettings.Serializers +{ + [UsedImplicitly] + public class LocalTimeSerializer : NodaTimePatternSerializer + { + public LocalTimeSerializer() : base(LocalTimePattern.ExtendedIso) { } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/LongSerializer.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/LongSerializer.cs new file mode 100644 index 00000000..ea1b1bff --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/LongSerializer.cs @@ -0,0 +1,19 @@ +using System.Globalization; +using JetBrains.Annotations; + +namespace Backend.Fx.Features.ConfigurationSettings.Serializers +{ + [UsedImplicitly] + public class LongSerializer : ISettingSerializer + { + public string Serialize(long? setting) + { + return setting?.ToString(CultureInfo.InvariantCulture); + } + + public long? Deserialize(string value) + { + return string.IsNullOrWhiteSpace(value) ? null : long.Parse(value, CultureInfo.InvariantCulture); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/NodaTimePatternSerializer.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/NodaTimePatternSerializer.cs new file mode 100644 index 00000000..b6e4517a --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/NodaTimePatternSerializer.cs @@ -0,0 +1,29 @@ +using JetBrains.Annotations; +using NodaTime.Text; + +namespace Backend.Fx.Features.ConfigurationSettings.Serializers +{ + [UsedImplicitly] + public abstract class NodaTimePatternSerializer : ISettingSerializer where T : struct + { + private readonly IPattern _pattern; + + protected NodaTimePatternSerializer(IPattern pattern) + { + _pattern = pattern; + } + + public string Serialize(T? setting) + { + return setting == null ? string.Empty : _pattern.Format(setting.Value); + } + + public T? Deserialize(string value) + { + return string.IsNullOrWhiteSpace(value) + ? null + : _pattern.Parse(value).GetValueOrThrow(); + } + + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/OffsetDateSerializer.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/OffsetDateSerializer.cs new file mode 100644 index 00000000..0ea59c01 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/OffsetDateSerializer.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; +using NodaTime; +using NodaTime.Text; + +namespace Backend.Fx.Features.ConfigurationSettings.Serializers +{ + [UsedImplicitly] + public class OffsetDateSerializer : NodaTimePatternSerializer + { + public OffsetDateSerializer() : base(OffsetDatePattern.FullRoundtrip) { } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/OffsetDateTimeSerializer.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/OffsetDateTimeSerializer.cs new file mode 100644 index 00000000..5b7ef940 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/OffsetDateTimeSerializer.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; +using NodaTime; +using NodaTime.Text; + +namespace Backend.Fx.Features.ConfigurationSettings.Serializers +{ + [UsedImplicitly] + public class OffsetDateTimeSerializer : NodaTimePatternSerializer + { + public OffsetDateTimeSerializer() : base(OffsetDateTimePattern.FullRoundtrip) { } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/OffsetSerializer.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/OffsetSerializer.cs new file mode 100644 index 00000000..337248fe --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/OffsetSerializer.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; +using NodaTime; +using NodaTime.Text; + +namespace Backend.Fx.Features.ConfigurationSettings.Serializers +{ + [UsedImplicitly] + public class OffsetSerializer : NodaTimePatternSerializer + { + public OffsetSerializer() : base(OffsetPattern.GeneralInvariant) { } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/OffsetTimeSerializer.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/OffsetTimeSerializer.cs new file mode 100644 index 00000000..49760b06 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/OffsetTimeSerializer.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; +using NodaTime; +using NodaTime.Text; + +namespace Backend.Fx.Features.ConfigurationSettings.Serializers +{ + [UsedImplicitly] + public class OffsetTimeSerializer : NodaTimePatternSerializer + { + public OffsetTimeSerializer() : base(OffsetTimePattern.ExtendedIso) { } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/ShortSerializer.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/ShortSerializer.cs new file mode 100644 index 00000000..cff99c7b --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/ShortSerializer.cs @@ -0,0 +1,19 @@ +using System.Globalization; +using JetBrains.Annotations; + +namespace Backend.Fx.Features.ConfigurationSettings.Serializers +{ + [UsedImplicitly] + public class ShortSerializer : ISettingSerializer + { + public string Serialize(short? setting) + { + return setting?.ToString(CultureInfo.InvariantCulture); + } + + public short? Deserialize(string value) + { + return string.IsNullOrWhiteSpace(value) ? null : short.Parse(value, CultureInfo.InvariantCulture); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/StringSerializer.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/StringSerializer.cs new file mode 100644 index 00000000..43ddf9a0 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/StringSerializer.cs @@ -0,0 +1,18 @@ +using JetBrains.Annotations; + +namespace Backend.Fx.Features.ConfigurationSettings.Serializers +{ + [UsedImplicitly] + public class StringSerializer : ISettingSerializer + { + public string Serialize(string setting) + { + return setting; + } + + public string Deserialize(string value) + { + return value; + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/TimeSpanSerializer.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/TimeSpanSerializer.cs new file mode 100644 index 00000000..4b7409dc --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/Serializers/TimeSpanSerializer.cs @@ -0,0 +1,20 @@ +using System; +using System.Globalization; +using JetBrains.Annotations; + +namespace Backend.Fx.Features.ConfigurationSettings.Serializers +{ + [UsedImplicitly] + public class TimeSpanSerializer : ISettingSerializer + { + public string Serialize(TimeSpan? setting) + { + return setting?.ToString("g", CultureInfo.InvariantCulture); + } + + public TimeSpan? Deserialize(string value) + { + return string.IsNullOrWhiteSpace(value) ? null : TimeSpan.Parse(value, CultureInfo.InvariantCulture); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/ConfigurationSettings/SettingSerializerFactory.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/SettingSerializerFactory.cs similarity index 96% rename from src/abstractions/Backend.Fx/ConfigurationSettings/SettingSerializerFactory.cs rename to src/abstractions/Backend.Fx/Features/ConfigurationSettings/SettingSerializerFactory.cs index a270989c..8e2fdf6e 100644 --- a/src/abstractions/Backend.Fx/ConfigurationSettings/SettingSerializerFactory.cs +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/SettingSerializerFactory.cs @@ -4,13 +4,14 @@ using System.Reflection; using JetBrains.Annotations; -namespace Backend.Fx.ConfigurationSettings +namespace Backend.Fx.Features.ConfigurationSettings { public interface ISettingSerializerFactory { ISettingSerializer GetSerializer(); } + [PublicAPI] public class SettingSerializerFactory : ISettingSerializerFactory { protected Dictionary Serializers { get; } diff --git a/src/abstractions/Backend.Fx/Features/ConfigurationSettings/SettingsCategory.cs b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/SettingsCategory.cs new file mode 100644 index 00000000..dafa9bcb --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/ConfigurationSettings/SettingsCategory.cs @@ -0,0 +1,51 @@ +using JetBrains.Annotations; + +namespace Backend.Fx.Features.ConfigurationSettings +{ + /// + /// Base class to implement a category of settings. + /// A typical setting would be implemented as a read/write property. + /// + /// + /// public int MyIntegerSetting + /// { + /// get => ReadSetting<int?>(nameof(MyIntegerSetting)) ?? 0; + /// set => WriteSetting<int>>(nameof(MyIntegerSetting), value); + /// } + /// + /// + /// + [PublicAPI] + public abstract class SettingsCategory + { + private readonly string _category; + private readonly ISettingRepository _settingRepository; + private readonly ISettingSerializerFactory _settingSerializerFactory; + + protected SettingsCategory(string category, ISettingRepository settingRepository, ISettingSerializerFactory settingSerializerFactory) + { + _category = category; + _settingRepository = settingRepository; + _settingSerializerFactory = settingSerializerFactory; + } + + protected T ReadSetting(string key) + { + var serializedValue = _settingRepository.GetSerializedValue(_category, key); + if (serializedValue == null) + { + return default; + } + + var serializer = _settingSerializerFactory.GetSerializer(); + return serializer.Deserialize(serializedValue); + } + + protected void WriteSetting(string key, T value) + { + var serializer = _settingSerializerFactory.GetSerializer(); + var serializedValue = serializer.Serialize(value); + _settingRepository.WriteSerializedValue(_category, key, serializedValue); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/DataGeneration/BackendFxApplicationEx.cs b/src/abstractions/Backend.Fx/Features/DataGeneration/BackendFxApplicationEx.cs new file mode 100644 index 00000000..6d594df5 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/DataGeneration/BackendFxApplicationEx.cs @@ -0,0 +1,16 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.Features.DataGeneration +{ + public static class BackendFxApplicationEx + { + public static async Task GenerateData(this IBackendFxApplication application, CancellationToken cancellationToken = default) + { + var dataGenerationContext = application.CompositionRoot.ServiceProvider.GetRequiredService(); + var dataGeneratorTypes = await dataGenerationContext.GetDataGeneratorTypesAsync(application.Invoker).ConfigureAwait(false); + await dataGenerationContext.GenerateDataAsync(application.Invoker, dataGeneratorTypes, cancellationToken).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/DataGeneration/DataGenerationFeature.cs b/src/abstractions/Backend.Fx/Features/DataGeneration/DataGenerationFeature.cs new file mode 100644 index 00000000..33d74b28 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/DataGeneration/DataGenerationFeature.cs @@ -0,0 +1,41 @@ +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Backend.Fx.Features.DataGeneration +{ + /// + /// The extension "Data Generation" makes sure that all implemented data generators are executed on application boot + /// + [PublicAPI] + public class DataGenerationFeature : Feature, IBootableFeature, IMultiTenancyFeature + { + private readonly bool _allowDemoDataGeneration; + + /// + /// Controls, whether demo data generators should run. In case of a multi tenancy application, this flag must + /// be set to true to allow specific tenants to contain demo data, following the respective tenant + /// configuration. + /// + public DataGenerationFeature(bool allowDemoDataGeneration = true) + { + _allowDemoDataGeneration = allowDemoDataGeneration; + } + + public override void Enable(IBackendFxApplication application) + { + application.CompositionRoot.RegisterModules( + new DataGenerationModule(application.Assemblies, _allowDemoDataGeneration)); + } + + public void EnableMultiTenancyServices(IBackendFxApplication application) + { + application.CompositionRoot.RegisterModules(new MultiTenancyDataGenerationModule()); + } + + public async Task BootAsync(IBackendFxApplication application, CancellationToken cancellationToken = default) + { + await application.GenerateData(cancellationToken).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/DataGeneration/DataGenerationModule.cs b/src/abstractions/Backend.Fx/Features/DataGeneration/DataGenerationModule.cs new file mode 100644 index 00000000..6cb6db47 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/DataGeneration/DataGenerationModule.cs @@ -0,0 +1,45 @@ +using System; +using System.Linq; +using System.Reflection; +using Backend.Fx.DependencyInjection; +using Backend.Fx.Util; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.Features.DataGeneration +{ + internal class DataGenerationModule : IModule + { + private readonly Assembly[] _assemblies; + private readonly bool _allowDemoDataGeneration; + + public DataGenerationModule(Assembly[] assemblies, bool allowDemoDataGeneration) + { + _assemblies = assemblies; + _allowDemoDataGeneration = allowDemoDataGeneration; + } + + public void Register(ICompositionRoot compositionRoot) + { + Type serviceType = _allowDemoDataGeneration + ? typeof(IDataGenerator) + : typeof(IProductiveDataGenerator); + + compositionRoot.RegisterCollection( + _assemblies + .GetImplementingTypes(serviceType) + .Select(t => new ServiceDescriptor(typeof(IDataGenerator), t, ServiceLifetime.Scoped))); + + compositionRoot.Register( + ServiceDescriptor.Singleton()); + } + } + + internal class MultiTenancyDataGenerationModule : IModule + { + public void Register(ICompositionRoot compositionRoot) + { + compositionRoot.RegisterDecorator( + ServiceDescriptor.Singleton()); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DataGeneration/DataGenerator.cs b/src/abstractions/Backend.Fx/Features/DataGeneration/DataGenerator.cs similarity index 63% rename from src/abstractions/Backend.Fx/Patterns/DataGeneration/DataGenerator.cs rename to src/abstractions/Backend.Fx/Features/DataGeneration/DataGenerator.cs index 301c1383..1560475c 100644 --- a/src/abstractions/Backend.Fx/Patterns/DataGeneration/DataGenerator.cs +++ b/src/abstractions/Backend.Fx/Features/DataGeneration/DataGenerator.cs @@ -1,9 +1,12 @@ -using Microsoft.Extensions.Logging; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Logging; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; -namespace Backend.Fx.Patterns.DataGeneration +namespace Backend.Fx.Features.DataGeneration { - using Logging; - + [PublicAPI] public interface IDataGenerator { /// @@ -11,7 +14,7 @@ public interface IDataGenerator /// int Priority { get; } - void Generate(); + Task GenerateAsync(CancellationToken cancellationToken = default); } /// @@ -21,33 +24,36 @@ public interface IDataGenerator /// Any implementation is automatically picked up by the injection container, so no extra plumbing is required. /// You can require any application or domain service including repositories via constructor parameter. /// + + [PublicAPI] public abstract class DataGenerator : IDataGenerator { - private static readonly Microsoft.Extensions.Logging.ILogger Logger = Log.Create(); + private readonly ILogger _logger = Log.Create(); /// /// simple way of ordering the execution of DataGenerators. Priority 0 will be executed first. /// public abstract int Priority { get; } - public void Generate() + public async Task GenerateAsync(CancellationToken cancellationToken = default) { if (ShouldRun()) { Initialize(); - Logger.LogInformation("{DataGeneratorTypeName} is now generating initial data", GetType().FullName); - GenerateCore(); + _logger.LogInformation("{DataGeneratorTypeName} is now generating initial data", GetType().FullName); + await GenerateCoreAsync(cancellationToken).ConfigureAwait(false); } else { - Logger.LogInformation("No need to run {DataGeneratorTypeName}", GetType().FullName); + _logger.LogInformation("No need to run {DataGeneratorTypeName}", GetType().FullName); } } /// /// Implement your generate Logic here /// - protected abstract void GenerateCore(); + /// + protected abstract Task GenerateCoreAsync(CancellationToken cancellationToken); /// /// Implement your initial logic here (e.g. loading from external source) @@ -56,7 +62,7 @@ public void Generate() /// /// return true, if the generator should be executed. Generators must be implemented idempotent, - /// since they're all executed on application start + /// since they're all executed on each application start /// /// protected abstract bool ShouldRun(); diff --git a/src/abstractions/Backend.Fx/Features/DataGeneration/ForEachTenantDataGenerationContext.cs b/src/abstractions/Backend.Fx/Features/DataGeneration/ForEachTenantDataGenerationContext.cs new file mode 100644 index 00000000..e967513b --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/DataGeneration/ForEachTenantDataGenerationContext.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Features.MultiTenancy; + +namespace Backend.Fx.Features.DataGeneration +{ + public class ForEachTenantDataGenerationContext : IDataGenerationContext + { + private readonly ITenantEnumerator _tenantEnumerator; + private readonly ITenantWideMutexManager _tenantWideMutexManager; + private readonly IDataGenerationContext _dataGenerationContext; + + public ForEachTenantDataGenerationContext( + ITenantEnumerator tenantEnumerator, + ITenantWideMutexManager tenantWideMutexManager, + IDataGenerationContext dataGenerationContext) + { + _tenantEnumerator = tenantEnumerator; + _tenantWideMutexManager = tenantWideMutexManager; + _dataGenerationContext = dataGenerationContext; + } + + public Task GetDataGeneratorTypesAsync(IBackendFxApplicationInvoker invoker) + { + return _dataGenerationContext.GetDataGeneratorTypesAsync(invoker); + } + + public async Task GenerateDataAsync(IBackendFxApplicationInvoker invoker, IEnumerable dataGeneratorTypes, + CancellationToken cancellationToken = default) + { + dataGeneratorTypes = dataGeneratorTypes as Type[] ?? dataGeneratorTypes.ToArray(); + + await _dataGenerationContext.GenerateDataAsync( + new ForEachTenantIdInvoker( + _tenantEnumerator.GetActiveTenantIds(), + _tenantWideMutexManager, + "DataGeneration", + invoker), + dataGeneratorTypes.Where(t => typeof(IProductiveDataGenerator).IsAssignableFrom(t)), + cancellationToken) + .ConfigureAwait(false); + + await _dataGenerationContext.GenerateDataAsync( + new ForEachTenantIdInvoker( + _tenantEnumerator.GetActiveDemoTenantIds(), + _tenantWideMutexManager, + "DataGeneration", + invoker), + dataGeneratorTypes.Where(t => typeof(IDemoDataGenerator).IsAssignableFrom(t)), + cancellationToken) + .ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/DataGeneration/IDataGenerationContext.cs b/src/abstractions/Backend.Fx/Features/DataGeneration/IDataGenerationContext.cs new file mode 100644 index 00000000..d346e7b7 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/DataGeneration/IDataGenerationContext.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.ExecutionPipeline; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.Features.DataGeneration +{ + public interface IDataGenerationContext + { + Task GetDataGeneratorTypesAsync(IBackendFxApplicationInvoker invoker); + Task GenerateDataAsync(IBackendFxApplicationInvoker invoker, IEnumerable dataGeneratorTypes, CancellationToken cancellationToken = default); + } + + [UsedImplicitly] + public class DataGenerationContext : IDataGenerationContext + { + public async Task GetDataGeneratorTypesAsync(IBackendFxApplicationInvoker invoker) + { + var dataGeneratorTypes = Type.EmptyTypes; + await invoker.InvokeAsync(sp => + { + dataGeneratorTypes = sp + .GetServices() + .OrderBy(dg => dg.Priority) + .Select(dg => dg.GetType()) + .ToArray(); + return Task.CompletedTask; + }, new SystemIdentity()).ConfigureAwait(false); + + return dataGeneratorTypes; + } + + public async Task GenerateDataAsync(IBackendFxApplicationInvoker invoker, IEnumerable dataGeneratorTypes, + CancellationToken cancellationToken = default) + { + foreach (Type dataGeneratorType in dataGeneratorTypes) + { + await invoker.InvokeAsync(async (sp, ct) => + { + IDataGenerator dataGenerator = sp.GetServices().Single(dgt => dgt.GetType() == dataGeneratorType); + await dataGenerator.GenerateAsync(ct).ConfigureAwait(false); + }, new SystemIdentity(), cancellationToken).ConfigureAwait(false); + } + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DataGeneration/IDemoDataGenerator.cs b/src/abstractions/Backend.Fx/Features/DataGeneration/IDemoDataGenerator.cs similarity index 80% rename from src/abstractions/Backend.Fx/Patterns/DataGeneration/IDemoDataGenerator.cs rename to src/abstractions/Backend.Fx/Features/DataGeneration/IDemoDataGenerator.cs index 6908aba5..63f28de0 100644 --- a/src/abstractions/Backend.Fx/Patterns/DataGeneration/IDemoDataGenerator.cs +++ b/src/abstractions/Backend.Fx/Features/DataGeneration/IDemoDataGenerator.cs @@ -1,4 +1,4 @@ -namespace Backend.Fx.Patterns.DataGeneration +namespace Backend.Fx.Features.DataGeneration { /// /// Marks an as active in development environments only diff --git a/src/abstractions/Backend.Fx/Patterns/DataGeneration/IProductiveDataGenerator.cs b/src/abstractions/Backend.Fx/Features/DataGeneration/IProductiveDataGenerator.cs similarity index 79% rename from src/abstractions/Backend.Fx/Patterns/DataGeneration/IProductiveDataGenerator.cs rename to src/abstractions/Backend.Fx/Features/DataGeneration/IProductiveDataGenerator.cs index 17733651..255fd759 100644 --- a/src/abstractions/Backend.Fx/Patterns/DataGeneration/IProductiveDataGenerator.cs +++ b/src/abstractions/Backend.Fx/Features/DataGeneration/IProductiveDataGenerator.cs @@ -1,4 +1,4 @@ -namespace Backend.Fx.Patterns.DataGeneration +namespace Backend.Fx.Features.DataGeneration { /// /// Marks an as active in all environments diff --git a/src/abstractions/Backend.Fx/Features/DomainEvents/DomainEventAggregator.cs b/src/abstractions/Backend.Fx/Features/DomainEvents/DomainEventAggregator.cs new file mode 100644 index 00000000..b4d62035 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/DomainEvents/DomainEventAggregator.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Logging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.Features.DomainEvents +{ + public class DomainEventAggregator : IDomainEventAggregator, IDomainEventAggregatorScope + { + private readonly ILogger _logger = Log.Create(); + private readonly IServiceProvider _serviceProvider; + private readonly ConcurrentQueue _domainEvents = new(); + + public DomainEventAggregator(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + /// + /// Publish a domain event that is handled by all handlers synchronously in the same scope/transaction. + /// Possible exceptions are not caught, so that your action might fail due to a failing event handler. + /// + /// + /// + public void PublishDomainEvent(TDomainEvent domainEvent) where TDomainEvent : IDomainEvent + { + _logger.LogDebug( + "Domain event {DomainEvent} registered. It will be raised on completion of operation", + typeof(TDomainEvent).Name); + _domainEvents.Enqueue(domainEvent); + } + + public async Task RaiseEventsAsync(CancellationToken cancellationToken) + { + while (_domainEvents.TryDequeue(out IDomainEvent domainEvent)) + { + Type eventType = domainEvent.GetType(); + var handlerType = typeof(IDomainEventHandler<>).MakeGenericType(eventType); + foreach (object injectedHandler in _serviceProvider.GetServices(handlerType)) + { + try + { + MethodInfo handleMethod = handlerType.GetMethod("HandleAsync"); + Debug.Assert(handleMethod != null, nameof(handleMethod) + " != null"); + var task = (Task)handleMethod.Invoke(injectedHandler, new object[] { domainEvent, cancellationToken }); + await task.ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Handling of {DomainEvent} by {HandlerTypeName} failed", + eventType.Name, + handlerType.Name); + throw; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/DomainEvents/DomainEventsFeature.cs b/src/abstractions/Backend.Fx/Features/DomainEvents/DomainEventsFeature.cs new file mode 100644 index 00000000..b4a03974 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/DomainEvents/DomainEventsFeature.cs @@ -0,0 +1,22 @@ +using Backend.Fx.ExecutionPipeline; +using JetBrains.Annotations; + +namespace Backend.Fx.Features.DomainEvents +{ + /// + /// The feature "Domain Events" provides you with a domain event aggregator, that will be injected as a scoped + /// instance and generic domain event handlers that will also be injected as scoped instances. You can publish + /// arbitrary domain events using the instance, but domain events won't be + /// raised until the is completing. + /// Failures when handling domain events will result in canceling the whole operation, thus in rolling back a + /// possible transaction. + /// + [PublicAPI] + public class DomainEventsFeature : Feature + { + public override void Enable(IBackendFxApplication application) + { + application.CompositionRoot.RegisterModules(new DomainEventsModule(application.Assemblies)); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/DomainEvents/DomainEventsModule.cs b/src/abstractions/Backend.Fx/Features/DomainEvents/DomainEventsModule.cs new file mode 100644 index 00000000..cdefef70 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/DomainEvents/DomainEventsModule.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq; +using System.Reflection; +using Backend.Fx.DependencyInjection; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Logging; +using Backend.Fx.Util; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.Features.DomainEvents +{ + internal class DomainEventsModule : IModule + { + private readonly ILogger _logger = Log.Create(); + private readonly Assembly[] _assemblies; + + public DomainEventsModule(params Assembly[] assemblies) + { + _assemblies = assemblies; + } + + public void Register(ICompositionRoot compositionRoot) + { + compositionRoot.Register(ServiceDescriptor.Scoped(sp => new DomainEventAggregator(sp))); + compositionRoot.Register(ServiceDescriptor.Scoped(sp => sp.GetRequiredService())); + compositionRoot.Register(ServiceDescriptor.Scoped(sp => sp.GetRequiredService())); + compositionRoot.RegisterDecorator(ServiceDescriptor.Scoped()); + RegisterDomainEventHandlers(compositionRoot); + } + + private void RegisterDomainEventHandlers(ICompositionRoot compositionRoot) + { + foreach (Type domainEventType in _assemblies.GetImplementingTypes(typeof(IDomainEvent))) + { + Type handlerTypeForThisDomainEventType = typeof(IDomainEventHandler<>).MakeGenericType(domainEventType); + + var serviceDescriptors = _assemblies + .GetImplementingTypes(handlerTypeForThisDomainEventType) + .Select(t => new ServiceDescriptor(handlerTypeForThisDomainEventType, t, ServiceLifetime.Scoped)) + .ToArray(); + + if (serviceDescriptors.Any()) + { + compositionRoot.RegisterCollection(serviceDescriptors); + } + else + { + _logger.LogInformation("No handlers for {DomainEventType} found", domainEventType); + } + } + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Domain/IDomainEvent.cs b/src/abstractions/Backend.Fx/Features/DomainEvents/IDomainEvent.cs similarity index 82% rename from src/abstractions/Backend.Fx/Patterns/EventAggregation/Domain/IDomainEvent.cs rename to src/abstractions/Backend.Fx/Features/DomainEvents/IDomainEvent.cs index 8f9af72b..9a5f5a5c 100644 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Domain/IDomainEvent.cs +++ b/src/abstractions/Backend.Fx/Features/DomainEvents/IDomainEvent.cs @@ -1,4 +1,4 @@ -namespace Backend.Fx.Patterns.EventAggregation.Domain +namespace Backend.Fx.Features.DomainEvents { /// /// Marker interface for domain events that must be handled in the same scope and transaction of the publishing logic. diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Domain/IDomainEventAggregator.cs b/src/abstractions/Backend.Fx/Features/DomainEvents/IDomainEventAggregator.cs similarity index 60% rename from src/abstractions/Backend.Fx/Patterns/EventAggregation/Domain/IDomainEventAggregator.cs rename to src/abstractions/Backend.Fx/Features/DomainEvents/IDomainEventAggregator.cs index 42891ec5..fdd9b346 100644 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Domain/IDomainEventAggregator.cs +++ b/src/abstractions/Backend.Fx/Features/DomainEvents/IDomainEventAggregator.cs @@ -1,12 +1,19 @@ -namespace Backend.Fx.Patterns.EventAggregation.Domain +using System.Threading; +using System.Threading.Tasks; + +namespace Backend.Fx.Features.DomainEvents { + public interface IDomainEventAggregatorScope + { + void PublishDomainEvent(TDomainEvent domainEvent) where TDomainEvent : IDomainEvent; + } + /// /// Channel events from multiple objects into a single object to simplify registration for clients. /// https://martinfowler.com/eaaDev/EventAggregator.html /// public interface IDomainEventAggregator { - void PublishDomainEvent(TDomainEvent domainEvent) where TDomainEvent : IDomainEvent; - void RaiseEvents(); + Task RaiseEventsAsync(CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/DomainEvents/IDomainEventHandler.cs b/src/abstractions/Backend.Fx/Features/DomainEvents/IDomainEventHandler.cs new file mode 100644 index 00000000..3dc9d1f7 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/DomainEvents/IDomainEventHandler.cs @@ -0,0 +1,10 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Backend.Fx.Features.DomainEvents +{ + public interface IDomainEventHandler where TDomainEvent : IDomainEvent + { + Task HandleAsync(TDomainEvent domainEvent, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/DomainEvents/RaiseDomainEventsOperationDecorator.cs b/src/abstractions/Backend.Fx/Features/DomainEvents/RaiseDomainEventsOperationDecorator.cs new file mode 100644 index 00000000..28f6ed53 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/DomainEvents/RaiseDomainEventsOperationDecorator.cs @@ -0,0 +1,39 @@ +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.ExecutionPipeline; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.Features.DomainEvents +{ + [UsedImplicitly] + public class RaiseDomainEventsOperationDecorator : IOperation + { + private readonly IDomainEventAggregator _domainEventAggregator; + private readonly IOperation _operation; + + public RaiseDomainEventsOperationDecorator( + IDomainEventAggregator domainEventAggregator, + IOperation operation) + { + _domainEventAggregator = domainEventAggregator; + _operation = operation; + } + + public Task BeginAsync(IServiceScope serviceScope, CancellationToken cancellationToken = default) + { + return _operation.BeginAsync(serviceScope, cancellationToken); + } + + public async Task CompleteAsync(CancellationToken cancellationToken = default) + { + await _domainEventAggregator.RaiseEventsAsync(cancellationToken).ConfigureAwait(false); + await _operation.CompleteAsync(cancellationToken).ConfigureAwait(false); + } + + public Task CancelAsync(CancellationToken cancellationToken = default) + { + return _operation.CancelAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/DomainServices/DomainServicesFeature.cs b/src/abstractions/Backend.Fx/Features/DomainServices/DomainServicesFeature.cs new file mode 100644 index 00000000..1ffdfe09 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/DomainServices/DomainServicesFeature.cs @@ -0,0 +1,17 @@ +using JetBrains.Annotations; + +namespace Backend.Fx.Features.DomainServices +{ + /// + /// The feature "Domain Services" makes sure that all implementations of and + /// are injected as scoped instances. + /// + [PublicAPI] + public class DomainServicesFeature : Feature + { + public override void Enable(IBackendFxApplication application) + { + application.CompositionRoot.RegisterModules(new DomainServicesModule(application.Assemblies)); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/DomainServices/DomainServicesModule.cs b/src/abstractions/Backend.Fx/Features/DomainServices/DomainServicesModule.cs new file mode 100644 index 00000000..43265911 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/DomainServices/DomainServicesModule.cs @@ -0,0 +1,52 @@ +using System.Linq; +using System.Reflection; +using Backend.Fx.DependencyInjection; +using Backend.Fx.Logging; +using Backend.Fx.Util; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.Features.DomainServices +{ + internal class DomainServicesModule : IModule + { + private readonly ILogger _logger = Log.Create(); + private readonly Assembly[] _assemblies; + private readonly string _assembliesForLogging; + + public DomainServicesModule(params Assembly[] assemblies) + { + _assemblies = assemblies; + _assembliesForLogging = string.Join(",", _assemblies.Select(ass => ass.GetName().Name)); + } + + public void Register(ICompositionRoot compositionRoot) + { + RegisterDomainAndApplicationServices(compositionRoot); + } + + private void RegisterDomainAndApplicationServices(ICompositionRoot container) + { + _logger.LogDebug("Registering domain and application services from {Assemblies}", _assembliesForLogging); + + var serviceDescriptors = _assemblies.GetImplementingTypes(typeof(IDomainService)) + .Concat(_assemblies.GetImplementingTypes(typeof(IApplicationService))) + .SelectMany(type => + type.GetTypeInfo() + .ImplementedInterfaces + .Where(i => typeof(IDomainService) != i + && typeof(IApplicationService) != i + && _assemblies.Contains(i.GetTypeInfo().Assembly)) + .Select(service => new ServiceDescriptor(service, type, ServiceLifetime.Scoped))); + + + foreach (ServiceDescriptor serviceDescriptor in serviceDescriptors) + { + _logger.LogDebug("Registering scoped service {ServiceType} with implementation {ImplementationType}", + serviceDescriptor.ServiceType.Name, + serviceDescriptor.ImplementationType.Name); + container.Register(serviceDescriptor); + } + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/BuildingBlocks/IApplicationService.cs b/src/abstractions/Backend.Fx/Features/DomainServices/IApplicationService.cs similarity index 80% rename from src/abstractions/Backend.Fx/BuildingBlocks/IApplicationService.cs rename to src/abstractions/Backend.Fx/Features/DomainServices/IApplicationService.cs index 832d8777..cdd65a8b 100644 --- a/src/abstractions/Backend.Fx/BuildingBlocks/IApplicationService.cs +++ b/src/abstractions/Backend.Fx/Features/DomainServices/IApplicationService.cs @@ -1,4 +1,4 @@ -namespace Backend.Fx.BuildingBlocks +namespace Backend.Fx.Features.DomainServices { /// /// A marker interface to identify application services to be auto registered in the container on boot diff --git a/src/abstractions/Backend.Fx/BuildingBlocks/IDomainService.cs b/src/abstractions/Backend.Fx/Features/DomainServices/IDomainService.cs similarity index 80% rename from src/abstractions/Backend.Fx/BuildingBlocks/IDomainService.cs rename to src/abstractions/Backend.Fx/Features/DomainServices/IDomainService.cs index ddcc120d..633399b5 100644 --- a/src/abstractions/Backend.Fx/BuildingBlocks/IDomainService.cs +++ b/src/abstractions/Backend.Fx/Features/DomainServices/IDomainService.cs @@ -1,4 +1,4 @@ -namespace Backend.Fx.BuildingBlocks +namespace Backend.Fx.Features.DomainServices { /// /// A marker interface to domain application services to be auto registered in the container on boot diff --git a/src/abstractions/Backend.Fx/Features/Feature.cs b/src/abstractions/Backend.Fx/Features/Feature.cs new file mode 100644 index 00000000..972a3d12 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Feature.cs @@ -0,0 +1,27 @@ +using System; +using JetBrains.Annotations; + +namespace Backend.Fx.Features +{ + /// + /// Base class for optional features that can be added to the Backend.Fx execution pipeline + /// + [PublicAPI] + public abstract class Feature : IDisposable + { + public abstract void Enable(IBackendFxApplication application); + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/IBootableFeature.cs b/src/abstractions/Backend.Fx/Features/IBootableFeature.cs new file mode 100644 index 00000000..5754cd94 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/IBootableFeature.cs @@ -0,0 +1,13 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Backend.Fx.Features +{ + /// + /// Marks a to require stuff done during startup of the application + /// + public interface IBootableFeature + { + public Task BootAsync(IBackendFxApplication application, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/IMultiTenancyFeature.cs b/src/abstractions/Backend.Fx/Features/IMultiTenancyFeature.cs new file mode 100644 index 00000000..bc812098 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/IMultiTenancyFeature.cs @@ -0,0 +1,10 @@ +namespace Backend.Fx.Features +{ + /// + /// Marks a to add behavior in case of multi tenancy enabled + /// + public interface IMultiTenancyFeature + { + void EnableMultiTenancyServices(IBackendFxApplication application); + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/IdGeneration/HiLoIdGenerator.cs b/src/abstractions/Backend.Fx/Features/IdGeneration/HiLoIdGenerator.cs new file mode 100644 index 00000000..bd92ba90 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/IdGeneration/HiLoIdGenerator.cs @@ -0,0 +1,96 @@ +using System.Threading; +using Backend.Fx.Logging; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.Features.IdGeneration +{ + public abstract class HiLoIdGenerator : IIdGenerator + { + private readonly object _mutex = new(); + + public TId NextId() + { + lock (_mutex) + { + EnsureValidLowAndHiId(); + return GetNextId(); + } + } + + protected abstract void EnsureValidLowAndHiId(); + + protected abstract TId GetNextId(); + + + protected abstract TId GetNextBlockStart(); + + protected abstract TId BlockSize { get; } + } + + [PublicAPI] + public abstract class HiLoIntIdGenerator : HiLoIdGenerator + { + private readonly ILogger _logger = Log.Create(); + private int _highId = -1; + private int _lowId = -1; + private readonly bool _isTraceEnabled; + + protected HiLoIntIdGenerator() + { + _isTraceEnabled = _logger.IsEnabled(LogLevel.Trace); + } + + + protected override void EnsureValidLowAndHiId() + { + if (_lowId == -1 || _lowId > _highId) + { + // first fetch from sequence in life time + _lowId = GetNextBlockStart(); + _highId = _lowId + BlockSize- 1; + } + } + + protected override int GetNextId() + { + var nextId = _lowId; + Interlocked.Increment(ref _lowId); + if (_isTraceEnabled) _logger.LogTrace("Providing id {NextId}", nextId); + return nextId; + } + } + + [PublicAPI] + public abstract class HiLoLongIdGenerator : HiLoIdGenerator + { + private readonly ILogger _logger = Log.Create(); + private long _highId = -1; + private long _lowId = -1; + private readonly bool _isTraceEnabled; + + protected HiLoLongIdGenerator() + { + _isTraceEnabled = _logger.IsEnabled(LogLevel.Trace); + } + + + protected override void EnsureValidLowAndHiId() + { + if (_lowId == -1 || _lowId > _highId) + { + // first fetch from sequence in life time + _lowId = GetNextBlockStart(); + _highId = _lowId + BlockSize- 1; + } + } + + protected override long GetNextId() + { + var nextId = _lowId; + Interlocked.Increment(ref _lowId); + if (_isTraceEnabled) _logger.LogTrace("Providing id {NextId}", nextId); + return nextId; + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/IdGeneration/IEntityIdGenerator.cs b/src/abstractions/Backend.Fx/Features/IdGeneration/IEntityIdGenerator.cs new file mode 100644 index 00000000..02ca22e3 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/IdGeneration/IEntityIdGenerator.cs @@ -0,0 +1,21 @@ +namespace Backend.Fx.Features.IdGeneration +{ + public interface IEntityIdGenerator : IIdGenerator + { + } + + public class EntityIdGenerator : IEntityIdGenerator + { + private readonly IIdGenerator _idGenerator; + + public EntityIdGenerator(IIdGenerator idGenerator) + { + _idGenerator = idGenerator; + } + + public TId NextId() + { + return _idGenerator.NextId(); + } + } +} diff --git a/src/abstractions/Backend.Fx/Features/IdGeneration/IIdGenerator.cs b/src/abstractions/Backend.Fx/Features/IdGeneration/IIdGenerator.cs new file mode 100644 index 00000000..e751d8b1 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/IdGeneration/IIdGenerator.cs @@ -0,0 +1,7 @@ +namespace Backend.Fx.Features.IdGeneration +{ + public interface IIdGenerator + { + TId NextId(); + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/IdGeneration/ISequence.cs b/src/abstractions/Backend.Fx/Features/IdGeneration/ISequence.cs new file mode 100644 index 00000000..05ff5472 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/IdGeneration/ISequence.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; + +namespace Backend.Fx.Features.IdGeneration +{ + [PublicAPI] + public interface ISequence + { + void EnsureSequence(); + TId GetNextValue(); + TId Increment { get; } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/IdGeneration/IdGenerationFeature.cs b/src/abstractions/Backend.Fx/Features/IdGeneration/IdGenerationFeature.cs new file mode 100644 index 00000000..3b03aa50 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/IdGeneration/IdGenerationFeature.cs @@ -0,0 +1,46 @@ +using JetBrains.Annotations; + +namespace Backend.Fx.Features.IdGeneration +{ + /// + /// use this feature type, when you want your non generic type of to be injected + /// + /// + /// + [PublicAPI] + public class IdGenerationFeature : Feature + where TIdGenerator : class, IEntityIdGenerator + { + private readonly TIdGenerator _entityIdGenerator; + + public IdGenerationFeature(TIdGenerator entityIdGenerator) + { + _entityIdGenerator = entityIdGenerator; + } + + public override void Enable(IBackendFxApplication application) + { + application.CompositionRoot.RegisterModules(new IdGenerationModule(_entityIdGenerator)); + } + } + + /// + /// use this feature type, when you want the generic to be injected + /// + /// + [PublicAPI] + public class IdGenerationFeature : Feature + { + private readonly IEntityIdGenerator _entityIdGenerator; + + public IdGenerationFeature(IEntityIdGenerator entityIdGenerator) + { + _entityIdGenerator = entityIdGenerator; + } + + public override void Enable(IBackendFxApplication application) + { + application.CompositionRoot.RegisterModules(new IdGenerationModule>(_entityIdGenerator)); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/IdGeneration/IdGenerationModule.cs b/src/abstractions/Backend.Fx/Features/IdGeneration/IdGenerationModule.cs new file mode 100644 index 00000000..6200e19d --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/IdGeneration/IdGenerationModule.cs @@ -0,0 +1,21 @@ +using Backend.Fx.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.Features.IdGeneration +{ + public class IdGenerationModule : IModule + where TIdGenerator : class, IEntityIdGenerator + { + private readonly TIdGenerator _entityIdGenerator; + + public IdGenerationModule(TIdGenerator entityIdGenerator) + { + _entityIdGenerator = entityIdGenerator; + } + + public void Register(ICompositionRoot compositionRoot) + { + compositionRoot.Register(ServiceDescriptor.Singleton(_entityIdGenerator)); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/IdGeneration/InMem/InMemorySequence.cs b/src/abstractions/Backend.Fx/Features/IdGeneration/InMem/InMemorySequence.cs new file mode 100644 index 00000000..7006efef --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/IdGeneration/InMem/InMemorySequence.cs @@ -0,0 +1,27 @@ +namespace Backend.Fx.Features.IdGeneration.InMem +{ + public class InMemorySequence : ISequence + { + private int _currentValue = 1; + + public InMemorySequence(int increment = 1) + { + Increment = increment; + } + + public void EnsureSequence() + { } + + public int GetNextValue() + { + lock (this) + { + var nextValue = _currentValue; + _currentValue += Increment; + return nextValue; + } + } + + public int Increment { get; } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/IdGeneration/SequenceHiLoIdGenerator.cs b/src/abstractions/Backend.Fx/Features/IdGeneration/SequenceHiLoIdGenerator.cs new file mode 100644 index 00000000..eaa2c60e --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/IdGeneration/SequenceHiLoIdGenerator.cs @@ -0,0 +1,54 @@ +using System.Threading; +using Backend.Fx.Logging; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.Features.IdGeneration +{ + public abstract class SequenceHiLoIdGenerator : HiLoIdGenerator + { + private readonly ISequence _sequence; + + protected SequenceHiLoIdGenerator(ISequence sequence) + { + _sequence = sequence; + } + + protected override TId GetNextBlockStart() + { + return _sequence.GetNextValue(); + } + + protected override TId BlockSize => _sequence.Increment; + } + + public class SequenceHiLoIntIdGenerator : SequenceHiLoIdGenerator + { + private readonly ILogger _logger = Log.Create(); + private int _highId = -1; + private int _lowId = -1; + private readonly bool _isTraceEnabled; + + public SequenceHiLoIntIdGenerator(ISequence sequence) : base(sequence) + { + _isTraceEnabled = _logger.IsEnabled(LogLevel.Trace); + } + + protected override void EnsureValidLowAndHiId() + { + if (_lowId == -1 || _lowId > _highId) + { + // first fetch from sequence in life time + _lowId = GetNextBlockStart(); + _highId = _lowId + BlockSize- 1; + } + } + + protected override int GetNextId() + { + var nextId = _lowId; + Interlocked.Increment(ref _lowId); + if (_isTraceEnabled) _logger.LogTrace("Providing id {NextId}", nextId); + return nextId; + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/IdGeneration/SequenceIdGenerator.cs b/src/abstractions/Backend.Fx/Features/IdGeneration/SequenceIdGenerator.cs new file mode 100644 index 00000000..f3150e91 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/IdGeneration/SequenceIdGenerator.cs @@ -0,0 +1,17 @@ +namespace Backend.Fx.Features.IdGeneration +{ + public class SequenceIdGenerator : IIdGenerator + { + private readonly ISequence _sequence; + + public SequenceIdGenerator(ISequence sequence) + { + _sequence = sequence; + } + + public TId NextId() + { + return _sequence.GetNextValue(); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Jobs/BackendFxApplicationEx.cs b/src/abstractions/Backend.Fx/Features/Jobs/BackendFxApplicationEx.cs new file mode 100644 index 00000000..aaabbc5d --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Jobs/BackendFxApplicationEx.cs @@ -0,0 +1,16 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.Features.Jobs +{ + public static class BackendFxApplicationEx + { + public static async Task ExecuteJob(this IBackendFxApplication application, CancellationToken cancellationToken = default) + where TJob : IJob + { + var jobExecutor = application.CompositionRoot.ServiceProvider.GetRequiredService(); + await jobExecutor.ExecuteAsync(application, cancellationToken: cancellationToken).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Jobs/ForEachTenantJobExecutor.cs b/src/abstractions/Backend.Fx/Features/Jobs/ForEachTenantJobExecutor.cs new file mode 100644 index 00000000..f1d96550 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Jobs/ForEachTenantJobExecutor.cs @@ -0,0 +1,43 @@ +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Features.MultiTenancy; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.Features.Jobs +{ + [UsedImplicitly] + public class ForEachTenantJobExecutor : IJobExecutor + { + private readonly ITenantEnumerator _tenantEnumerator; + + public ForEachTenantJobExecutor( + ITenantEnumerator tenantEnumerator, + IJobExecutor _) + { + _tenantEnumerator = tenantEnumerator; + } + + public async Task ExecuteAsync( + IBackendFxApplication application, + IIdentity identity = null, + CancellationToken cancellationToken = default) + where TJob : IJob + { + var tenantWideMutexManager = + application.CompositionRoot.ServiceProvider.GetRequiredService(); + + await new ForEachTenantIdInvoker( + _tenantEnumerator.GetActiveTenantIds(), + tenantWideMutexManager, + typeof(TJob).FullName, application.Invoker) + .InvokeAsync( + async (sp, ct) => await sp.GetRequiredService().RunAsync(ct).ConfigureAwait(false), + identity ?? new SystemIdentity(), + cancellationToken) + .ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Jobs/IJob.cs b/src/abstractions/Backend.Fx/Features/Jobs/IJob.cs new file mode 100644 index 00000000..e7a239f0 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Jobs/IJob.cs @@ -0,0 +1,15 @@ +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Backend.Fx.Features.Jobs +{ + /// + /// This interface describes a job that can be executed directly or by a scheduler. + /// + [PublicAPI] + public interface IJob + { + Task RunAsync(CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Jobs/IJobExecutor.cs b/src/abstractions/Backend.Fx/Features/Jobs/IJobExecutor.cs new file mode 100644 index 00000000..395bd6e6 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Jobs/IJobExecutor.cs @@ -0,0 +1,36 @@ +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.Features.Jobs +{ + public interface IJobExecutor + { + Task ExecuteAsync(IBackendFxApplication application, + IIdentity identity = null, + CancellationToken cancellationToken = default) + where TJob : IJob; + } + + [UsedImplicitly] + public class JobExecutor : IJobExecutor + { + public async Task ExecuteAsync(IBackendFxApplication application, + IIdentity identity = null, + CancellationToken cancellationToken = default) + where TJob : IJob + { + await application + .Invoker + .InvokeAsync(async (sp, ct) => + await sp + .GetRequiredService() + .RunAsync(ct) + .ConfigureAwait(false) + , identity, cancellationToken) + .ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Jobs/JobsFeature.cs b/src/abstractions/Backend.Fx/Features/Jobs/JobsFeature.cs new file mode 100644 index 00000000..20529d98 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Jobs/JobsFeature.cs @@ -0,0 +1,21 @@ +using JetBrains.Annotations; + +namespace Backend.Fx.Features.Jobs +{ + /// + /// The feature "Jobs" makes sure, that all implementations of are injected as scoped instances. + /// + [PublicAPI] + public class JobsFeature : Feature, IMultiTenancyFeature + { + public override void Enable(IBackendFxApplication application) + { + application.CompositionRoot.RegisterModules(new JobsModule(application.Assemblies)); + } + + public void EnableMultiTenancyServices(IBackendFxApplication application) + { + application.CompositionRoot.RegisterModules(new MultiTenancyJobsModule()); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Jobs/JobsModule.cs b/src/abstractions/Backend.Fx/Features/Jobs/JobsModule.cs new file mode 100644 index 00000000..408aafe2 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Jobs/JobsModule.cs @@ -0,0 +1,40 @@ +using System; +using System.Reflection; +using Backend.Fx.DependencyInjection; +using Backend.Fx.Util; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.Features.Jobs +{ + internal class JobsModule : IModule + { + private readonly Assembly[] _assemblies; + + public JobsModule(Assembly[] assemblies) + { + _assemblies = assemblies; + } + + public void Register(ICompositionRoot compositionRoot) + { + // all jobs are dynamically registered + foreach (Type jobType in _assemblies.GetImplementingTypes(typeof(IJob))) + { + compositionRoot.Register( + new ServiceDescriptor(jobType, jobType, ServiceLifetime.Scoped)); + } + + compositionRoot.Register( + ServiceDescriptor.Singleton()); + } + } + + internal class MultiTenancyJobsModule : IModule + { + public void Register(ICompositionRoot compositionRoot) + { + compositionRoot.RegisterDecorator( + ServiceDescriptor.Singleton()); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/MessageBus/IIntegrationEventHandler.cs b/src/abstractions/Backend.Fx/Features/MessageBus/IIntegrationEventHandler.cs new file mode 100644 index 00000000..305756aa --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MessageBus/IIntegrationEventHandler.cs @@ -0,0 +1,12 @@ +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Backend.Fx.Features.MessageBus +{ + [PublicAPI] + public interface IIntegrationEventHandler + { + Task HandleAsync(T integrationEvent, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/MessageBus/InProc/InProcMessageBus.cs b/src/abstractions/Backend.Fx/Features/MessageBus/InProc/InProcMessageBus.cs new file mode 100644 index 00000000..9335b5fc --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MessageBus/InProc/InProcMessageBus.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Backend.Fx.Features.MessageBus.InProc +{ + public class InProcMessageBus : MessageBus + { + private readonly InProcMessageBusChannel _channel; + private readonly HashSet _subscribedEventTypeNames = new(); + + public InProcMessageBus(InProcMessageBusChannel channel) + { + _channel = channel; + } + + public override void Connect() + { + _channel.MessageReceived += ChannelOnMessageReceived; + } + + protected override void SubscribeToEventMessage(string eventTypeName) + { + _subscribedEventTypeNames.Add(eventTypeName); + } + + protected override Task PublishMessageAsync(SerializedMessage serializedMessage) + { + _channel.Publish(serializedMessage); + return Task.CompletedTask; + } + + private async void ChannelOnMessageReceived( + object sender, + InProcMessageBusChannel.MessageReceivedEventArgs eventArgs) + { + if (_subscribedEventTypeNames.Contains(eventArgs.SerializedMessage.EventTypeName)) + { + await ProcessAsync(eventArgs.SerializedMessage).ConfigureAwait(false); + } + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/MessageBus/InProc/InProcMessageBusChannel.cs b/src/abstractions/Backend.Fx/Features/MessageBus/InProc/InProcMessageBusChannel.cs new file mode 100644 index 00000000..a2ab4a71 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MessageBus/InProc/InProcMessageBusChannel.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; + +namespace Backend.Fx.Features.MessageBus.InProc +{ + public class InProcMessageBusChannel : IAsyncDisposable + { + private readonly ConcurrentBag _messageHandlingTasks = new(); + + internal event EventHandler MessageReceived; + + internal void Publish(SerializedMessage serializedMessage) + { + var eventArgs = new MessageReceivedEventArgs { SerializedMessage = serializedMessage }; + _messageHandlingTasks.Add(Task.Run(() => MessageReceived?.Invoke(this, eventArgs))); + } + + public async Task FinishHandlingAllMessagesAsync() + { + while (_messageHandlingTasks.TryTake(out Task messageHandlingTask)) + { + await messageHandlingTask.ConfigureAwait(false); + } + } + + internal class MessageReceivedEventArgs + { + public SerializedMessage SerializedMessage { get; init; } + } + + public async ValueTask DisposeAsync() + { + await FinishHandlingAllMessagesAsync().ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/MessageBus/IntegrationEvent.cs b/src/abstractions/Backend.Fx/Features/MessageBus/IntegrationEvent.cs new file mode 100644 index 00000000..24b17dda --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MessageBus/IntegrationEvent.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using JetBrains.Annotations; +using NodaTime; + +namespace Backend.Fx.Features.MessageBus +{ + [PublicAPI] + public interface IIntegrationEvent + { + Guid Id { get; } + Instant CreationDate { get; } + Guid CorrelationId { get; } + + Dictionary Properties { get; } + } + + /// + /// Events that should be handled in a separate context. Might be persisted as well using an external message bus. + /// See https://blogs.msdn.microsoft.com/cesardelatorre/2017/02/07/domain-events-vs-integration-events-in-domain-driven-design-and-microservices-architectures/ + /// + public abstract class IntegrationEvent : IIntegrationEvent + { + [JsonInclude] + public Guid Id { get; private set; } = Guid.NewGuid(); + + [JsonInclude] + public Instant CreationDate { get; private set; } = SystemClock.Instance.GetCurrentInstant(); + + [JsonInclude] + public Guid CorrelationId { get; internal set; } + + [JsonInclude] + public Dictionary Properties { get; private set; } = new(); + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/MessageBus/IntegrationEventHandlingInvoker.cs b/src/abstractions/Backend.Fx/Features/MessageBus/IntegrationEventHandlingInvoker.cs new file mode 100644 index 00000000..51889eb0 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MessageBus/IntegrationEventHandlingInvoker.cs @@ -0,0 +1,42 @@ +using System; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.ExecutionPipeline; + +namespace Backend.Fx.Features.MessageBus +{ + /// + /// Ensures events to be handled sequentially + /// + public class IntegrationEventHandlingInvoker : IBackendFxApplicationInvoker + { + private readonly object _syncLock = new(); + private readonly IBackendFxApplicationInvoker _invoker; + + public IntegrationEventHandlingInvoker(IBackendFxApplicationInvoker invoker) + { + _invoker = invoker; + } + + + public async Task InvokeAsync(Func awaitableAsyncAction, + IIdentity identity, CancellationToken cancellationToken = default) + { + Monitor.Enter(_syncLock); + try + { + await _invoker + .InvokeAsync(awaitableAsyncAction, identity, cancellationToken) + .ConfigureAwait(false); + } + finally + { + Monitor.Exit(_syncLock); + } + } + + public Task InvokeAsync(Func awaitableAsyncAction, IIdentity identity = null) + => InvokeAsync((sp, _) => awaitableAsyncAction.Invoke(sp), identity); + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/MessageBus/IntegrationEventMessageSerializer.cs b/src/abstractions/Backend.Fx/Features/MessageBus/IntegrationEventMessageSerializer.cs new file mode 100644 index 00000000..3d5c2814 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MessageBus/IntegrationEventMessageSerializer.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using NodaTime; +using NodaTime.Serialization.SystemTextJson; + +namespace Backend.Fx.Features.MessageBus +{ + public interface IIntegrationEventMessageSerializer + { + Task SerializeAsync(IIntegrationEvent integrationEvent); + Task DeserializeAsync(SerializedMessage serializedMessage); + } + + public class IntegrationEventMessageMessageSerializer : IIntegrationEventMessageSerializer + { + private readonly IDictionary _typeMap; + + private static readonly JsonSerializerOptions SerializerOptions + = new JsonSerializerOptions + { +#if DEBUG + WriteIndented = true +#endif + } + .ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); + + public IntegrationEventMessageMessageSerializer(IEnumerable eventTypesToSubscribe) + { + _typeMap = eventTypesToSubscribe.ToDictionary( + evt => evt.Name, + evt => evt); + } + + public async Task SerializeAsync(IIntegrationEvent integrationEvent) + { + using var memoryStream = new MemoryStream(4096); + await JsonSerializer.SerializeAsync(memoryStream, integrationEvent, SerializerOptions) + .ConfigureAwait(false); + memoryStream.Seek(0, SeekOrigin.Begin); + return new SerializedMessage(integrationEvent.GetType().Name, memoryStream.ToArray()); + } + + public async Task DeserializeAsync(SerializedMessage serializedMessage) + { + using var memoryStream = new MemoryStream(serializedMessage.MessagePayload, false); + if (_typeMap.TryGetValue(serializedMessage.EventTypeName, out Type returnType)) + { + var integrationEvent = (IIntegrationEvent)await JsonSerializer + .DeserializeAsync(memoryStream, returnType, SerializerOptions) + .ConfigureAwait(false); + + return integrationEvent; + } + + throw new InvalidOperationException( + $"The message type {serializedMessage.EventTypeName} is not in the list of supported types to deserialize ({string.Join(",", _typeMap.Keys)}) "); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/MessageBus/MessageBus.cs b/src/abstractions/Backend.Fx/Features/MessageBus/MessageBus.cs new file mode 100644 index 00000000..86885cea --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MessageBus/MessageBus.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.DependencyInjection; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Logging; +using Backend.Fx.Util; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.Features.MessageBus +{ + [PublicAPI] + public interface IMessageBus : IDisposable + { + void Connect(); + + Task PublishAsync(TIntegrationEvent integrationEvent) + where TIntegrationEvent : IIntegrationEvent; + + void Subscribe(Type eventType); + void Integrate(IBackendFxApplication application); + } + + public abstract class MessageBus : IMessageBus + { + private readonly ILogger _logger = Log.Create(); + private readonly ILogger _messageLogger = Log.Create(typeof(MessageBus).FullName + ".Messages"); + private readonly CancellationToken _cancellationToken; + + private IBackendFxApplicationInvoker _invoker; + + private ICompositionRoot _compositionRoot; + + public abstract void Connect(); + + public async Task PublishAsync(TIntegrationEvent integrationEvent) + where TIntegrationEvent : IIntegrationEvent + { + var serializer = _compositionRoot.ServiceProvider.GetRequiredService(); + SerializedMessage serializedMessage = + await serializer.SerializeAsync(integrationEvent).ConfigureAwait(false); + + if (_messageLogger.IsEnabled(LogLevel.Debug)) + { + _messageLogger.LogDebug("Sending {EventTypeName} payload: {Payload}", serializedMessage.EventTypeName, Encoding.UTF8.GetString(serializedMessage.MessagePayload)); + } + + await PublishMessageAsync(serializedMessage).ConfigureAwait(false); + } + + public void Subscribe(Type eventType) + { + SubscribeToEventMessage(eventType.Name); + } + + public void Integrate(IBackendFxApplication application) + { + _compositionRoot = application.CompositionRoot; + _invoker = new IntegrationEventHandlingInvoker(application.Invoker); + } + + protected async Task ProcessAsync(SerializedMessage serializedMessage) + { + _logger.LogInformation("Processing a {EventTypeName} message", serializedMessage.EventTypeName); + + if (_messageLogger.IsEnabled(LogLevel.Debug)) + { + _messageLogger.LogDebug("Received {EventTypeName} payload: {Payload}", serializedMessage.EventTypeName, Encoding.UTF8.GetString(serializedMessage.MessagePayload)); + } + + await _invoker.InvokeAsync(async (sp, ct) => + { + var serializer = sp.GetRequiredService(); + IIntegrationEvent integrationEvent = await serializer.DeserializeAsync(serializedMessage).ConfigureAwait(false); + sp.GetRequiredService>().Current.Resume(integrationEvent.CorrelationId); + + Type handlerType = typeof(IIntegrationEventHandler<>).MakeGenericType(integrationEvent.GetType()); + Type handlerTypeCollectionType = typeof(IEnumerable<>).MakeGenericType(handlerType); + var handlers = (IEnumerable)sp.GetRequiredService(handlerTypeCollectionType); + foreach (var handler in handlers) + { + const string methodName = nameof(IIntegrationEventHandler.HandleAsync); + MethodInfo handleAsyncMethod = handlerType.GetMethod(methodName, new[] { integrationEvent.GetType(), typeof(CancellationToken) }); + Debug.Assert(handleAsyncMethod != null, nameof(handleAsyncMethod) + " != null"); + var task = (Task)handleAsyncMethod.Invoke(handler, new object[] { integrationEvent, ct }); + await task.ConfigureAwait(false); + } + + }, new SystemIdentity(), _cancellationToken).ConfigureAwait(false); + } + + + protected abstract void SubscribeToEventMessage(string eventTypeName); + + protected abstract Task PublishMessageAsync(SerializedMessage serializedMessage); + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _cancellationToken.ThrowIfCancellationRequested(); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/MessageBus/MessageBusFeature.cs b/src/abstractions/Backend.Fx/Features/MessageBus/MessageBusFeature.cs new file mode 100644 index 00000000..a4973db9 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MessageBus/MessageBusFeature.cs @@ -0,0 +1,49 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Backend.Fx.Features.MessageBus +{ + /// + /// The extension "Message Bus" adds integration message sending and handling of received integration messages to the + /// application. If the feature "Multi Tenancy" has been activated, this feature takes care of adding a tenant id + /// to all outgoing messages and handling incoming messages in the respective tenant. + /// + public class MessageBusFeature : Feature, IBootableFeature, IMultiTenancyFeature + { + private readonly IMessageBus _messageBus; + private MessageBusModule _messageBusModule; + + public MessageBusFeature(IMessageBus messageBus) + { + _messageBus = messageBus; + } + + public override void Enable(IBackendFxApplication application) + { + _messageBus.Integrate(application); + _messageBusModule = new MessageBusModule(_messageBus, application); + application.CompositionRoot.RegisterModules(_messageBusModule); + } + + public Task BootAsync(IBackendFxApplication application, CancellationToken cancellationToken = default) + { + _messageBus.Connect(); + _messageBusModule.SubscribeToAllEvents(); + return Task.CompletedTask; + } + + public void EnableMultiTenancyServices(IBackendFxApplication application) + { + application.CompositionRoot.RegisterModules(new MultiTenancyMessageBusModule()); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _messageBus.Dispose(); + } + base.Dispose(disposing); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/MessageBus/MessageBusModule.cs b/src/abstractions/Backend.Fx/Features/MessageBus/MessageBusModule.cs new file mode 100644 index 00000000..1cd79c65 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MessageBus/MessageBusModule.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Backend.Fx.DependencyInjection; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Logging; +using Backend.Fx.Util; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.Features.MessageBus +{ + internal class MessageBusModule : IModule + { + private readonly ILogger _logger = Log.Create(); + private readonly IMessageBus _messageBus; + private readonly IBackendFxApplication _application; + private readonly List _eventTypesToSubscribe = new(); + + public MessageBusModule(IMessageBus messageBus, IBackendFxApplication application) + { + _messageBus = messageBus; + _application = application; + } + + public void Register(ICompositionRoot compositionRoot) + { + // note tht there should be no reason to access the singleton message bus instance from the service provider + + // register the message bus scope + compositionRoot.Register( + ServiceDescriptor.Scoped( + sp => new MessageBusScope( + _messageBus, + sp.GetRequiredService>()))); + + // make sure all integration events are raised after completing an operation, but before ending the scope + compositionRoot.RegisterDecorator( + ServiceDescriptor.Scoped()); + + compositionRoot.Register( + ServiceDescriptor.Scoped, CurrentCorrelationHolder>()); + + RegisterIntegrationEventHandlers(compositionRoot); + + compositionRoot.Register( + ServiceDescriptor.Singleton(new IntegrationEventMessageMessageSerializer(_eventTypesToSubscribe))); + } + + public void SubscribeToAllEvents() + { + foreach (Type eventType in _eventTypesToSubscribe) + { + _messageBus.Subscribe(eventType); + } + } + + private void RegisterIntegrationEventHandlers(ICompositionRoot compositionRoot) + { + foreach (Type integrationEventType in _application.Assemblies.GetImplementingTypes(typeof(IIntegrationEvent))) + { + Type handlerTypeForThisIntegrationEventType = + typeof(IIntegrationEventHandler<>).MakeGenericType(integrationEventType); + + var serviceDescriptors = _application.Assemblies + .GetImplementingTypes(handlerTypeForThisIntegrationEventType) + .Select(t => + new ServiceDescriptor(handlerTypeForThisIntegrationEventType, t, ServiceLifetime.Scoped)) + .ToArray(); + + if (serviceDescriptors.Any()) + { + _logger.LogInformation("Registering {Count} handlers for {IntegrationEventType}", + serviceDescriptors.Length, + integrationEventType); + compositionRoot.RegisterCollection(serviceDescriptors); + _eventTypesToSubscribe.Add(integrationEventType); + } + else + { + _logger.LogInformation("No handlers for {IntegrationEventType} found", integrationEventType); + } + } + } + } + + internal class MultiTenancyMessageBusModule : IModule + { + public void Register(ICompositionRoot compositionRoot) + { + // enrich the integration event with a TenantId property + compositionRoot.RegisterDecorator( + ServiceDescriptor.Scoped()); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/MessageBusScope.cs b/src/abstractions/Backend.Fx/Features/MessageBus/MessageBusScope.cs similarity index 53% rename from src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/MessageBusScope.cs rename to src/abstractions/Backend.Fx/Features/MessageBus/MessageBusScope.cs index 396513a5..8c62fe84 100644 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/MessageBusScope.cs +++ b/src/abstractions/Backend.Fx/Features/MessageBus/MessageBusScope.cs @@ -1,11 +1,10 @@ -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Patterns.DependencyInjection; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Util; -namespace Backend.Fx.Patterns.EventAggregation.Integration +namespace Backend.Fx.Features.MessageBus { - using System.Collections.Concurrent; - using System.Threading.Tasks; - public interface IMessageBusScope { /// @@ -15,38 +14,34 @@ public interface IMessageBusScope /// void Publish(IIntegrationEvent integrationEvent); - Task RaiseEvents(); + Task RaiseEventsAsync(); } - public class MessageBusScope : IMessageBusScope + internal class MessageBusScope : IMessageBusScope { - private readonly ConcurrentQueue _integrationEvents = new ConcurrentQueue(); + private readonly ConcurrentQueue _integrationEvents = new(); private readonly IMessageBus _messageBus; private readonly ICurrentTHolder _correlationHolder; - private readonly ICurrentTHolder _tenantIdHolder; - + public MessageBusScope( IMessageBus messageBus, - ICurrentTHolder correlationHolder, - ICurrentTHolder tenantIdHolder) + ICurrentTHolder correlationHolder) { _messageBus = messageBus; _correlationHolder = correlationHolder; - _tenantIdHolder = tenantIdHolder; } void IMessageBusScope.Publish(IIntegrationEvent integrationEvent) { - ((IntegrationEvent) integrationEvent).SetCorrelationId(_correlationHolder.Current.Id); - ((IntegrationEvent) integrationEvent).SetTenantId(_tenantIdHolder.Current); + ((IntegrationEvent) integrationEvent).CorrelationId = _correlationHolder.Current.Id; _integrationEvents.Enqueue(integrationEvent); } - public async Task RaiseEvents() + public async Task RaiseEventsAsync() { while (_integrationEvents.TryDequeue(out IIntegrationEvent integrationEvent)) { - await _messageBus.Publish(integrationEvent); + await _messageBus.PublishAsync(integrationEvent).ConfigureAwait(false); } } } diff --git a/src/abstractions/Backend.Fx/Features/MessageBus/MultiTenantIntegrationEventSerializer.cs b/src/abstractions/Backend.Fx/Features/MessageBus/MultiTenantIntegrationEventSerializer.cs new file mode 100644 index 00000000..4308d9dc --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MessageBus/MultiTenantIntegrationEventSerializer.cs @@ -0,0 +1,48 @@ +using System.Globalization; +using System.Threading.Tasks; +using Backend.Fx.Exceptions; +using Backend.Fx.Features.MultiTenancy; +using Backend.Fx.Util; + +namespace Backend.Fx.Features.MessageBus +{ + public class MultiTenancyIntegrationEventMessageSerializer : IIntegrationEventMessageSerializer + { + private const string TenantIdPropertyKey = nameof(TenantId); + private readonly ICurrentTHolder _tenantIdHolder; + private readonly IIntegrationEventMessageSerializer _messageSerializer; + + public MultiTenancyIntegrationEventMessageSerializer(IIntegrationEventMessageSerializer messageSerializer, ICurrentTHolder tenantIdHolder) + { + _messageSerializer = messageSerializer; + _tenantIdHolder = tenantIdHolder; + } + + public Task SerializeAsync(IIntegrationEvent integrationEvent) + { + integrationEvent.Properties[TenantIdPropertyKey] = + _tenantIdHolder.Current.Value.ToString(CultureInfo.InvariantCulture); + return _messageSerializer.SerializeAsync(integrationEvent); + } + + public async Task DeserializeAsync(SerializedMessage serializedMessage) + { + IIntegrationEvent integrationEvent = await _messageSerializer.DeserializeAsync(serializedMessage).ConfigureAwait(false); + + if (!integrationEvent.Properties.TryGetValue(TenantIdPropertyKey, out var tenantIdString)) + { + throw new UnprocessableException("Received an integration event message without TenantId property"); + } + + if (!int.TryParse(tenantIdString, out var tenantId)) + { + throw new UnprocessableException( + $"Received an integration event message with an invalid TenantId property value: {tenantIdString}"); + } + + _tenantIdHolder.ReplaceCurrent(new TenantId(tenantId)); + + return integrationEvent; + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/MessageBus/RaiseIntegrationEventsWhenOperationCompleted.cs b/src/abstractions/Backend.Fx/Features/MessageBus/RaiseIntegrationEventsWhenOperationCompleted.cs new file mode 100644 index 00000000..367fc3f6 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MessageBus/RaiseIntegrationEventsWhenOperationCompleted.cs @@ -0,0 +1,39 @@ +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.ExecutionPipeline; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.Features.MessageBus +{ + [UsedImplicitly] + internal class RaiseIntegrationEventsWhenOperationCompleted : IOperation + { + private readonly IMessageBusScope _messageBusScope; + private readonly IOperation _operation; + + public RaiseIntegrationEventsWhenOperationCompleted( + IMessageBusScope messageBusScope, + IOperation operation) + { + _messageBusScope = messageBusScope; + _operation = operation; + } + + public Task BeginAsync(IServiceScope serviceScope, CancellationToken cancellationToken = default) + { + return _operation.BeginAsync(serviceScope, cancellationToken); + } + + public async Task CompleteAsync(CancellationToken cancellationToken = default) + { + await _operation.CompleteAsync(cancellationToken).ConfigureAwait(false); + await _messageBusScope.RaiseEventsAsync().ConfigureAwait(false); + } + + public Task CancelAsync(CancellationToken cancellationToken = default) + { + return _operation.CancelAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/MessageBus/SerializedMessage.cs b/src/abstractions/Backend.Fx/Features/MessageBus/SerializedMessage.cs new file mode 100644 index 00000000..1bcc2aa5 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MessageBus/SerializedMessage.cs @@ -0,0 +1,14 @@ +namespace Backend.Fx.Features.MessageBus +{ + public struct SerializedMessage + { + public SerializedMessage(string eventTypeName, byte[] messagePayload) + { + EventTypeName = eventTypeName; + MessagePayload = messagePayload; + } + + public string EventTypeName { get; } + public byte[] MessagePayload { get; } + } +} diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/CurrentTenantIdHolder.cs b/src/abstractions/Backend.Fx/Features/MultiTenancy/CurrentTenantIdHolder.cs similarity index 69% rename from src/abstractions/Backend.Fx/Environment/MultiTenancy/CurrentTenantIdHolder.cs rename to src/abstractions/Backend.Fx/Features/MultiTenancy/CurrentTenantIdHolder.cs index c5c4f1c2..952f26fe 100644 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/CurrentTenantIdHolder.cs +++ b/src/abstractions/Backend.Fx/Features/MultiTenancy/CurrentTenantIdHolder.cs @@ -1,9 +1,11 @@ -using Backend.Fx.Patterns.DependencyInjection; +using Backend.Fx.Util; +using JetBrains.Annotations; -namespace Backend.Fx.Environment.MultiTenancy +namespace Backend.Fx.Features.MultiTenancy { public class CurrentTenantIdHolder : CurrentTHolder { + [UsedImplicitly] public CurrentTenantIdHolder() { } @@ -29,17 +31,7 @@ public override TenantId ProvideInstance() protected override string Describe(TenantId instance) { - if (instance == null) - { - return ""; - } - - if (instance.HasValue) - { - return $"TenantId: {instance.Value}"; - } - - return "TenantId: null"; + return instance.HasValue ? $"TenantId: {instance.Value}" : "TenantId: null"; } } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/MultiTenancy/ForEachTenantIdInvoker.cs b/src/abstractions/Backend.Fx/Features/MultiTenancy/ForEachTenantIdInvoker.cs new file mode 100644 index 00000000..adff9084 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MultiTenancy/ForEachTenantIdInvoker.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Util; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.Features.MultiTenancy +{ + public class ForEachTenantIdInvoker : IBackendFxApplicationInvoker + { + private readonly IEnumerable _tenantIds; + private readonly ITenantWideMutexManager _tenantWideMutexManager; + private readonly string _mutexKey; + private readonly IBackendFxApplicationInvoker _invoker; + + public ForEachTenantIdInvoker( + IEnumerable tenantIds, + ITenantWideMutexManager tenantWideMutexManager, + string mutexKey, + IBackendFxApplicationInvoker invoker) + { + _tenantIds = tenantIds; + _tenantWideMutexManager = tenantWideMutexManager; + _mutexKey = mutexKey; + _invoker = invoker; + } + + public async Task InvokeAsync(Func awaitableAsyncAction, + IIdentity identity, CancellationToken cancellationToken = default) + { + foreach (TenantId tenantId in _tenantIds) + { + if (_tenantWideMutexManager.TryAcquire(tenantId, _mutexKey, out ITenantWideMutex mutex)) + { + try + { + await _invoker.InvokeAsync((sp, ct) => + { + sp.GetRequiredService>().ReplaceCurrent(tenantId); + return awaitableAsyncAction(sp, ct); + }, identity, cancellationToken).ConfigureAwait(false); + } + finally + { + mutex.Dispose(); + } + } + } + } + + public Task InvokeAsync(Func awaitableAsyncAction, IIdentity identity = null) + => InvokeAsync((sp, _) => awaitableAsyncAction.Invoke(sp), identity); + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/MultiTenancy/ICurrentTenantIdSelector.cs b/src/abstractions/Backend.Fx/Features/MultiTenancy/ICurrentTenantIdSelector.cs new file mode 100644 index 00000000..794c97c8 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MultiTenancy/ICurrentTenantIdSelector.cs @@ -0,0 +1,7 @@ +namespace Backend.Fx.Features.MultiTenancy +{ + public interface ICurrentTenantIdSelector + { + TenantId GetCurrentTenantId(); + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/MultiTenancy/ITenantEnumerator.cs b/src/abstractions/Backend.Fx/Features/MultiTenancy/ITenantEnumerator.cs new file mode 100644 index 00000000..6a5f6c7d --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MultiTenancy/ITenantEnumerator.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using JetBrains.Annotations; + +namespace Backend.Fx.Features.MultiTenancy +{ + [PublicAPI] + public interface ITenantEnumerator + { + IEnumerable GetActiveDemoTenantIds(); + IEnumerable GetActiveProductiveTenantIds(); + IEnumerable GetActiveTenantIds(); + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/ITenantWideMutex.cs b/src/abstractions/Backend.Fx/Features/MultiTenancy/ITenantWideMutex.cs similarity index 62% rename from src/abstractions/Backend.Fx/Environment/MultiTenancy/ITenantWideMutex.cs rename to src/abstractions/Backend.Fx/Features/MultiTenancy/ITenantWideMutex.cs index 1be237b7..015cc902 100644 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/ITenantWideMutex.cs +++ b/src/abstractions/Backend.Fx/Features/MultiTenancy/ITenantWideMutex.cs @@ -1,6 +1,6 @@ using System; -namespace Backend.Fx.Environment.MultiTenancy +namespace Backend.Fx.Features.MultiTenancy { public interface ITenantWideMutex : IDisposable { } diff --git a/src/abstractions/Backend.Fx/Features/MultiTenancy/ITenantWideMutexManager.cs b/src/abstractions/Backend.Fx/Features/MultiTenancy/ITenantWideMutexManager.cs new file mode 100644 index 00000000..789aa063 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MultiTenancy/ITenantWideMutexManager.cs @@ -0,0 +1,7 @@ +namespace Backend.Fx.Features.MultiTenancy +{ + public interface ITenantWideMutexManager + { + bool TryAcquire(TenantId tenantId, string key, out ITenantWideMutex mutex); + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/ITenantWideMutexManager.cs b/src/abstractions/Backend.Fx/Features/MultiTenancy/InProc/InProcTenantWideMutexManager.cs similarity index 59% rename from src/abstractions/Backend.Fx/Environment/MultiTenancy/ITenantWideMutexManager.cs rename to src/abstractions/Backend.Fx/Features/MultiTenancy/InProc/InProcTenantWideMutexManager.cs index e06b7ec8..52d12b04 100644 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/ITenantWideMutexManager.cs +++ b/src/abstractions/Backend.Fx/Features/MultiTenancy/InProc/InProcTenantWideMutexManager.cs @@ -3,23 +3,17 @@ using System.Threading; using JetBrains.Annotations; -namespace Backend.Fx.Environment.MultiTenancy +namespace Backend.Fx.Features.MultiTenancy.InProc { - public interface ITenantWideMutexManager - { - bool TryAcquire(TenantId tenantId, string key, out ITenantWideMutex mutex); - } - /// /// If an instance of this class is being hold as singleton it can be used to manage tenant wide /// mutexes in a single process. When multiple processes are running, another locking mechanism /// must be implemented (e.g. using MS SQL`s sp_getapplock or Postgres` advisory locks) /// - public class InMemoryTenantWideMutexManager : ITenantWideMutexManager + public class InProcTenantWideMutexManager : ITenantWideMutexManager { - private readonly ConcurrentDictionary> _mutexes = - new ConcurrentDictionary>(); - + private readonly ConcurrentDictionary> _mutexes = new(); + public bool TryAcquire(TenantId tenantId, string key, out ITenantWideMutex tenantWideMutex) { if (!tenantId.HasValue) @@ -29,30 +23,28 @@ public bool TryAcquire(TenantId tenantId, string key, out ITenantWideMutex tenan lock (this) { - var subDictionary = _mutexes.GetOrAdd(tenantId.Value, i => new ConcurrentDictionary()); - var mutex = subDictionary.GetOrAdd(key, s => new Mutex()); + var subDictionary = _mutexes.GetOrAdd(tenantId.Value, _ => new ConcurrentDictionary()); + Mutex mutex = subDictionary.GetOrAdd(key, _ => new Mutex()); if (mutex.WaitOne(300)) { - tenantWideMutex = new InMemoryTenantWideMutex(() => mutex.ReleaseMutex()); + tenantWideMutex = new InProcTenantWideMutex(() => mutex.ReleaseMutex()); return true; } - else - { - tenantWideMutex = null; - return false; - } + + tenantWideMutex = null; + return false; } } - - private class InMemoryTenantWideMutex : ITenantWideMutex + + private class InProcTenantWideMutex : ITenantWideMutex { private readonly Action _dispose; - public InMemoryTenantWideMutex([NotNull] Action dispose) + public InProcTenantWideMutex([NotNull] Action dispose) { _dispose = dispose ?? throw new ArgumentNullException(nameof(dispose)); } - + public void Dispose() { _dispose.Invoke(); diff --git a/src/abstractions/Backend.Fx/Features/MultiTenancy/MultiTenancyModule.cs b/src/abstractions/Backend.Fx/Features/MultiTenancy/MultiTenancyModule.cs new file mode 100644 index 00000000..1f8ed352 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MultiTenancy/MultiTenancyModule.cs @@ -0,0 +1,38 @@ +using Backend.Fx.DependencyInjection; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Util; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.Features.MultiTenancy +{ + internal class MultiTenancyModule : IModule + where TCurrentTenantIdSelector : class, ICurrentTenantIdSelector + { + private readonly ITenantEnumerator _tenantEnumerator; + private readonly ITenantWideMutexManager _tenantWideMutexManager; + + public MultiTenancyModule(ITenantEnumerator tenantEnumerator, ITenantWideMutexManager tenantWideMutexManager) + { + _tenantEnumerator = tenantEnumerator; + _tenantWideMutexManager = tenantWideMutexManager; + } + + public void Register(ICompositionRoot compositionRoot) + { + compositionRoot.Register( + ServiceDescriptor.Singleton(_tenantEnumerator)); + + compositionRoot.Register( + ServiceDescriptor.Singleton(_tenantWideMutexManager)); + + compositionRoot.Register( + ServiceDescriptor.Scoped()); + + compositionRoot.RegisterDecorator( + ServiceDescriptor.Scoped()); + + compositionRoot.Register( + ServiceDescriptor.Scoped, CurrentTenantIdHolder>()); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/MultiTenancy/TenantId.cs b/src/abstractions/Backend.Fx/Features/MultiTenancy/TenantId.cs new file mode 100644 index 00000000..4c646618 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MultiTenancy/TenantId.cs @@ -0,0 +1,100 @@ +using System; +using System.Diagnostics; + +namespace Backend.Fx.Features.MultiTenancy +{ + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + "}")] + public readonly struct TenantId + { + private readonly int? _id; + + public TenantId(int? id) + { + _id = id; + } + + /// + /// Throws on null id + /// + public int Value + { + get + { + if (_id == null) + { + throw new InvalidOperationException("You must not access the Value property when the tenant id is null"); + } + + return _id.Value; + } + } + + public bool HasValue => _id.HasValue; + + public string DebuggerDisplay + { + get + { + if (HasValue) + { + return $"TenantId: {Value}"; + } + + return "TenantId: NULL"; + } + } + + public override string ToString() + { + return _id?.ToString() ?? "NULL"; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (GetType() != obj.GetType()) return false; + if (obj is TenantId tenantId) + { + if (!HasValue && !tenantId.HasValue) + { + return true; + } + + if (HasValue && tenantId.HasValue) + { + return Equals(Value, tenantId.Value); + } + } + + return false; + } + + public override int GetHashCode() + { + return HasValue ? Value.GetHashCode() : 487623523.GetHashCode(); + } + + public static explicit operator int(TenantId tid) => tid.Value; + public static explicit operator TenantId(int id) => new(id); + + public static bool operator ==(TenantId left, TenantId right) + { + if (!left.HasValue && !right.HasValue) + { + return true; + } + + if (left.HasValue && right.HasValue) + { + return Equals(left.Value, right.Value); + } + + return false; + } + + public static bool operator !=(TenantId left, TenantId right) + { + return !(left == right); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/MultiTenancy/TenantOperationDecorator.cs b/src/abstractions/Backend.Fx/Features/MultiTenancy/TenantOperationDecorator.cs new file mode 100644 index 00000000..6a425bdd --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MultiTenancy/TenantOperationDecorator.cs @@ -0,0 +1,38 @@ +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Util; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.Features.MultiTenancy +{ + [UsedImplicitly] + public class TenantOperationDecorator : IOperation + { + private readonly IOperation _operation; + + public TenantOperationDecorator(IOperation operation) + { + _operation = operation; + } + public async Task BeginAsync(IServiceScope serviceScope, CancellationToken cancellationToken = default) + { + var currentTenantIdSelector = serviceScope.ServiceProvider.GetRequiredService(); + TenantId currentTenantId = currentTenantIdSelector.GetCurrentTenantId(); + var tenantIdHolder = serviceScope.ServiceProvider.GetRequiredService>(); + tenantIdHolder.ReplaceCurrent(currentTenantId); + await _operation.BeginAsync(serviceScope, cancellationToken).ConfigureAwait(false); + } + + public Task CompleteAsync(CancellationToken cancellationToken = default) + { + return _operation.CompleteAsync(cancellationToken); + } + + public Task CancelAsync(CancellationToken cancellationToken = default) + { + return _operation.CancelAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/MultiTenancyAdmin/DirectTenantEnumerator.cs b/src/abstractions/Backend.Fx/Features/MultiTenancyAdmin/DirectTenantEnumerator.cs new file mode 100644 index 00000000..25cf24b2 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MultiTenancyAdmin/DirectTenantEnumerator.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Linq; +using Backend.Fx.Features.MultiTenancy; + +namespace Backend.Fx.Features.MultiTenancyAdmin +{ + public class DirectTenantEnumerator : ITenantEnumerator + { + private readonly ITenantRepository _tenantRepository; + + public DirectTenantEnumerator(ITenantRepository tenantRepository) + { + _tenantRepository = tenantRepository; + } + public IEnumerable GetActiveDemoTenantIds() + { + return _tenantRepository.GetTenants().Where(t => t.IsActive && t.IsDemoTenant).Select(t => t.TenantId).ToArray(); + } + + public IEnumerable GetActiveProductiveTenantIds() + { + return _tenantRepository.GetTenants().Where(t => t.IsActive && !t.IsDemoTenant).Select(t => t.TenantId).ToArray(); + } + + public IEnumerable GetActiveTenantIds() + { + return _tenantRepository.GetTenants().Where(t => t.IsActive).Select(t => t.TenantId).ToArray(); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/MultiTenancyAdmin/ITenantRepository.cs b/src/abstractions/Backend.Fx/Features/MultiTenancyAdmin/ITenantRepository.cs new file mode 100644 index 00000000..1350fb26 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MultiTenancyAdmin/ITenantRepository.cs @@ -0,0 +1,15 @@ +namespace Backend.Fx.Features.MultiTenancyAdmin +{ + public interface ITenantRepository + { + void SaveTenant(Tenant tenant); + + Tenant[] GetTenants(); + + Tenant GetTenant(int tenantId); + + void DeleteTenant(int tenantId); + + int GetNextTenantId(); + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/MultiTenancyAdmin/InMem/InMemoryTenantRepository.cs b/src/abstractions/Backend.Fx/Features/MultiTenancyAdmin/InMem/InMemoryTenantRepository.cs new file mode 100644 index 00000000..fe014b39 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MultiTenancyAdmin/InMem/InMemoryTenantRepository.cs @@ -0,0 +1,43 @@ +using System.Collections.Concurrent; +using System.Linq; +using Backend.Fx.Exceptions; + +namespace Backend.Fx.Features.MultiTenancyAdmin.InMem +{ + public class InMemoryTenantRepository : ITenantRepository + { + private readonly ConcurrentDictionary _tenantsDictionary = new(); + + public void SaveTenant(Tenant tenant) + { + _tenantsDictionary[tenant.Id] = tenant; + } + + public Tenant[] GetTenants() + { + return _tenantsDictionary.Values.ToArray(); + } + + public Tenant GetTenant(int tenantId) + { + return _tenantsDictionary.ContainsKey(tenantId) + ? _tenantsDictionary[tenantId] + : throw new NotFoundException(tenantId); + } + + public void DeleteTenant(int tenantId) + { + _tenantsDictionary.TryRemove(tenantId, out _); + } + + public int GetNextTenantId() + { + if (_tenantsDictionary.IsEmpty) + { + return 1; + } + + return _tenantsDictionary.Keys.Max() + 1; + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/MultiTenancyAdmin/MultiTenancyAdminFeature.cs b/src/abstractions/Backend.Fx/Features/MultiTenancyAdmin/MultiTenancyAdminFeature.cs new file mode 100644 index 00000000..c7a22b0f --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MultiTenancyAdmin/MultiTenancyAdminFeature.cs @@ -0,0 +1,40 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.Features.MultiTenancyAdmin +{ + [PublicAPI] + public class MultiTenancyAdminFeature : Feature, IBootableFeature + { + private readonly ITenantRepository _tenantRepository; + private readonly bool _ensureDemoTenantOnBoot; + + public MultiTenancyAdminFeature(ITenantRepository tenantRepository, bool ensureDemoTenantOnBoot = false) + { + _tenantRepository = tenantRepository; + _ensureDemoTenantOnBoot = ensureDemoTenantOnBoot; + } + + public override void Enable(IBackendFxApplication application) + { + application.CompositionRoot.RegisterModules(new MultiTenancyAdminModule(_tenantRepository)); + } + + public Task BootAsync(IBackendFxApplication application, CancellationToken cancellationToken = default) + { + if (_ensureDemoTenantOnBoot) + { + var tenantService = application.CompositionRoot.ServiceProvider.GetRequiredService(); + if (!tenantService.GetTenants().Any(t => t.IsDemoTenant)) + { + tenantService.CreateTenant("demo", "Demonstration Tenant", true); + } + } + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/MultiTenancyAdmin/MultiTenancyAdminModule.cs b/src/abstractions/Backend.Fx/Features/MultiTenancyAdmin/MultiTenancyAdminModule.cs new file mode 100644 index 00000000..dcc517fc --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MultiTenancyAdmin/MultiTenancyAdminModule.cs @@ -0,0 +1,21 @@ +using Backend.Fx.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.Features.MultiTenancyAdmin +{ + internal class MultiTenancyAdminModule : IModule + { + private readonly ITenantRepository _tenantRepository; + + public MultiTenancyAdminModule(ITenantRepository tenantRepository) + { + _tenantRepository = tenantRepository; + } + + public void Register(ICompositionRoot compositionRoot) + { + compositionRoot.Register(ServiceDescriptor.Singleton(_tenantRepository)); + compositionRoot.Register(ServiceDescriptor.Singleton()); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/Tenant.cs b/src/abstractions/Backend.Fx/Features/MultiTenancyAdmin/Tenant.cs similarity index 55% rename from src/abstractions/Backend.Fx/Environment/MultiTenancy/Tenant.cs rename to src/abstractions/Backend.Fx/Features/MultiTenancyAdmin/Tenant.cs index b226562f..948890d3 100644 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/Tenant.cs +++ b/src/abstractions/Backend.Fx/Features/MultiTenancyAdmin/Tenant.cs @@ -1,12 +1,13 @@ using System; -using System.ComponentModel.DataAnnotations; +using Backend.Fx.Features.MultiTenancy; using JetBrains.Annotations; -namespace Backend.Fx.Environment.MultiTenancy +namespace Backend.Fx.Features.MultiTenancyAdmin { /// /// Represents a tenant in the application /// + [PublicAPI] public class Tenant { [UsedImplicitly] @@ -14,40 +15,32 @@ private Tenant() { } - public Tenant([NotNull] string name, string description, bool isDemoTenant, string configuration = null) + public Tenant(int id, [NotNull] string name, string description, bool isDemoTenant, string configuration = null) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); + Id = id; Name = name; Description = description; IsDemoTenant = isDemoTenant; Configuration = configuration; - State = TenantState.Active; + IsActive = true; } - [Key] public int Id { get; set; } + public int Id { get; } - [Required] public string Name { get; set; } + public string Name { get; set; } public string Description { get; set; } - public bool IsDemoTenant { get; set; } + public bool IsDemoTenant { get; } - public TenantState State { get; set; } + public bool IsActive { get; set; } /// /// optional: a generic field to store your arbitrary config data /// public string Configuration { get; set; } - public TenantId GetTenantId() - { - return new TenantId(Id); - } - } - - public enum TenantState - { - Active = 2, - Inactive = -1 + public TenantId TenantId => new(Id); } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/MultiTenancyAdmin/TenantService.cs b/src/abstractions/Backend.Fx/Features/MultiTenancyAdmin/TenantService.cs new file mode 100644 index 00000000..04db581d --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/MultiTenancyAdmin/TenantService.cs @@ -0,0 +1,144 @@ +using System; +using System.Linq; +using Backend.Fx.Exceptions; +using Backend.Fx.Logging; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.Features.MultiTenancyAdmin +{ + /// + /// Encapsulates the management of tenants + /// + [PublicAPI] + public interface ITenantService + { + Tenant CreateTenant(string name, string description, bool isDemonstrationTenant, string configuration = null); + void ActivateTenant(int tenantId); + void DeactivateTenant(int tenantId); + void DeleteTenant(int tenantId); + Tenant UpdateTenant(int tenantId, string name, string description, string configuration); + + Tenant[] GetTenants(); + Tenant[] GetActiveTenants(); + Tenant[] GetActiveDemonstrationTenants(); + Tenant[] GetActiveProductionTenants(); + Tenant GetTenant(int tenantId); + } + + public class TenantService : ITenantService + { + private readonly ILogger _logger = Log.Create(); + private readonly ITenantRepository _tenantRepository; + + public TenantService(ITenantRepository tenantRepository) + { + _tenantRepository = tenantRepository; + } + + public Tenant CreateTenant(string name, string description, bool isDemonstrationTenant, + string configuration = null) + { + _logger.LogInformation("Creating tenant: {Name}", name); + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); + } + + if (_tenantRepository.GetTenants() + .Any(t => t.Name != null && string.Equals(t.Name, name))) + { + throw new ArgumentException($"There is already a tenant named {name}"); + } + + var tenant = new Tenant(_tenantRepository.GetNextTenantId(), name, description, isDemonstrationTenant) { Configuration = configuration }; + _tenantRepository.SaveTenant(tenant); + + return tenant; + } + + public void ActivateTenant(int tenantId) + { + _logger.LogInformation("Activating tenant: {TenantId}", tenantId); + Tenant tenant = _tenantRepository.GetTenant(tenantId); + tenant.IsActive = true; + _tenantRepository.SaveTenant(tenant); + } + + public void DeactivateTenant(int tenantId) + { + _logger.LogInformation("Deactivating tenant: {TenantId}", tenantId); + Tenant tenant = _tenantRepository.GetTenant(tenantId); + tenant.IsActive = false; + _tenantRepository.SaveTenant(tenant); + } + + public void DeleteTenant(int tenantId) + { + _logger.LogInformation("Deleting tenant: {TenantId}", tenantId); + Tenant tenant = _tenantRepository.GetTenant(tenantId); + if (tenant.IsActive) + { + throw new UnprocessableException($"Attempt to delete active tenant[{tenantId}]") + .AddError("You cannot delete an active tenant. Please make sure to deactivate it first."); + } + + _tenantRepository.DeleteTenant(tenantId); + } + + public Tenant GetTenant(int tenantId) + { + return _tenantRepository.GetTenant(tenantId); + } + + public Tenant UpdateTenant(int tenantId, string name, string description, string configuration) + { + Tenant tenant = _tenantRepository.GetTenant(tenantId); + tenant.Name = name; + tenant.Description = description; + tenant.Configuration = configuration; + _tenantRepository.SaveTenant(tenant); + return tenant; + } + + public Tenant[] GetTenants() + { + var tenants = _tenantRepository.GetTenants(); + _logger.LogTrace("TenantIds: {TenantIds}", string.Join(",", tenants.Select(t => t.ToString()))); + return tenants; + } + + public Tenant[] GetActiveTenants() + { + var activeTenants = _tenantRepository + .GetTenants() + .Where(t => t.IsActive) + .ToArray(); + _logger.LogTrace("Active TenantIds: {TenantIds}", string.Join(",", activeTenants.Select(t => t.ToString()))); + return activeTenants; + } + + public Tenant[] GetActiveDemonstrationTenants() + { + var activeDemonstrationTenants = _tenantRepository + .GetTenants() + .Where(t => t.IsActive && t.IsDemoTenant) + .ToArray(); + _logger.LogTrace("Active Demonstration TenantIds: {TenantIds}", + string.Join(",", activeDemonstrationTenants.Select(t => t.ToString()))); + return activeDemonstrationTenants; + } + + public Tenant[] GetActiveProductionTenants() + { + var activeProductionTenants = _tenantRepository + .GetTenants() + .Where(t => t.IsActive && !t.IsDemoTenant) + .ToArray(); + _logger.LogTrace("Active Production TenantIds: {TenantIds}", + string.Join(",", activeProductionTenants.Select(t => t.ToString()))); + return activeProductionTenants; + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Persistence/AdoNet/AdoNetPersistenceModule.cs b/src/abstractions/Backend.Fx/Features/Persistence/AdoNet/AdoNetPersistenceModule.cs new file mode 100644 index 00000000..10166b7a --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Persistence/AdoNet/AdoNetPersistenceModule.cs @@ -0,0 +1,43 @@ +using System.Data; +using Backend.Fx.DependencyInjection; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Util; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.Features.Persistence.AdoNet +{ + public abstract class AdoNetPersistenceModule : PersistenceModule + { + private readonly IDbConnectionFactory _dbConnectionFactory; + + protected AdoNetPersistenceModule(IDbConnectionFactory dbConnectionFactory) + { + _dbConnectionFactory = dbConnectionFactory; + } + + public override void Register(ICompositionRoot compositionRoot) + { + base.Register(compositionRoot); + + // the DbConnectionFactory is registered as a singleton + compositionRoot.Register(ServiceDescriptor.Singleton(_dbConnectionFactory)); + + // by letting the container create the connection we can be sure, that only one connection per scope is used, and disposing is done accordingly + compositionRoot.Register(ServiceDescriptor.Scoped(_ => _dbConnectionFactory.Create())); + + // keeping a reference to the current transaction + compositionRoot.Register(ServiceDescriptor.Scoped, CurrentDbTransactionHolder>()); + + // wrapping the operation: + // invoke -> connection.open -> transaction.begin ---+ + // | + // v + // operation + // | + // | + // end invoke <- connection.close <- transaction.commit <-+ + compositionRoot.RegisterDecorator(ServiceDescriptor.Scoped()); + compositionRoot.RegisterDecorator(ServiceDescriptor.Scoped()); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Persistence/AdoNet/CurrentDbTransactionHolder.cs b/src/abstractions/Backend.Fx/Features/Persistence/AdoNet/CurrentDbTransactionHolder.cs new file mode 100644 index 00000000..3a0098d5 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Persistence/AdoNet/CurrentDbTransactionHolder.cs @@ -0,0 +1,20 @@ +using System.Data; +using Backend.Fx.Util; +using JetBrains.Annotations; + +namespace Backend.Fx.Features.Persistence.AdoNet +{ + [UsedImplicitly] + public class CurrentDbTransactionHolder : CurrentTHolder, ICurrentTHolder + { + public override IDbTransaction ProvideInstance() + { + return null; + } + + protected override string Describe(IDbTransaction instance) + { + return instance == null ? "" : "DbTransaction"; + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Persistence/AdoNet/DbConnectionOperationDecorator.cs b/src/abstractions/Backend.Fx/Features/Persistence/AdoNet/DbConnectionOperationDecorator.cs new file mode 100644 index 00000000..8673e8d2 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Persistence/AdoNet/DbConnectionOperationDecorator.cs @@ -0,0 +1,54 @@ +using System; +using System.Data; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Logging; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.Features.Persistence.AdoNet +{ + [UsedImplicitly] + public class DbConnectionOperationDecorator : IOperation + { + private readonly ILogger _logger = Log.Create(); + private IDisposable _connectionLifetimeLogger; + private readonly IOperation _operation; + private readonly IDbConnection _dbConnection; + + public DbConnectionOperationDecorator(IDbConnection dbConnection, IOperation operation) + { + _dbConnection = dbConnection; + _operation = operation; + } + + public async Task BeginAsync(IServiceScope serviceScope, CancellationToken cancellationToken = default) + { + _logger.LogDebug("Opening database connection"); + _dbConnection.Open(); + _connectionLifetimeLogger = _logger.LogDebugDuration("Database connection open", "Database connection closed"); + await _operation.BeginAsync(serviceScope, cancellationToken).ConfigureAwait(false); + } + + public async Task CompleteAsync(CancellationToken cancellationToken = default) + { + await _operation.CompleteAsync(cancellationToken).ConfigureAwait(false); + _logger.LogDebug("Closing database connection"); + _dbConnection.Close(); + _connectionLifetimeLogger?.Dispose(); + } + + public async Task CancelAsync(CancellationToken cancellationToken = default) + { + await _operation.CancelAsync(cancellationToken).ConfigureAwait(false); + _logger.LogDebug("Closing database connection"); + _dbConnection.Close(); + _connectionLifetimeLogger?.Dispose(); + + // note: we do not dispose the DbConnection here, because we did not instantiate it. Disposing is always + // up to the creator of the instance, that is in this case the injection container. + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/Persistence/DbTransactionOperationDecorator.cs b/src/abstractions/Backend.Fx/Features/Persistence/AdoNet/DbTransactionOperationDecorator.cs similarity index 62% rename from src/abstractions/Backend.Fx/Environment/Persistence/DbTransactionOperationDecorator.cs rename to src/abstractions/Backend.Fx/Features/Persistence/AdoNet/DbTransactionOperationDecorator.cs index 3960d689..00eebed4 100644 --- a/src/abstractions/Backend.Fx/Environment/Persistence/DbTransactionOperationDecorator.cs +++ b/src/abstractions/Backend.Fx/Features/Persistence/AdoNet/DbTransactionOperationDecorator.cs @@ -1,34 +1,41 @@ using System; using System.Data; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.ExecutionPipeline; using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; +using Backend.Fx.Util; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; -namespace Backend.Fx.Environment.Persistence +namespace Backend.Fx.Features.Persistence.AdoNet { /// /// Enriches the operation to use a database transaction during lifetime. The transaction gets started, before IOperation.Begin() /// is being called and gets committed after IOperation.Complete() is being called. /// + [PublicAPI] public class DbTransactionOperationDecorator : IOperation { - private static readonly ILogger Logger = Log.Create(); + private readonly ILogger _logger = Log.Create(); private readonly IDbConnection _dbConnection; + private readonly ICurrentTHolder _currentTransactionHolder; private readonly IOperation _operation; private bool _shouldHandleConnectionState; private IsolationLevel _isolationLevel = IsolationLevel.Unspecified; private IDisposable _transactionLifetimeLogger; private TxState _state = TxState.NotStarted; - public DbTransactionOperationDecorator(IDbConnection dbConnection, IOperation operation) + public DbTransactionOperationDecorator(IDbConnection dbConnection, ICurrentTHolder currentTransactionHolder, IOperation operation) { _dbConnection = dbConnection; + _currentTransactionHolder = currentTransactionHolder; _operation = operation; } - public virtual void Begin() + public virtual async Task BeginAsync(IServiceScope serviceScope, CancellationToken cancellationToken = default) { if (_state != TxState.NotStarted) { @@ -38,56 +45,54 @@ public virtual void Begin() _shouldHandleConnectionState = ShouldHandleConnectionState(); if (_shouldHandleConnectionState) { - Logger.LogDebug("Opening connection"); + _logger.LogDebug("Opening connection"); _dbConnection.Open(); } - Logger.LogDebug("Beginning transaction"); - CurrentTransaction = _dbConnection.BeginTransaction(_isolationLevel); - _transactionLifetimeLogger = Logger.LogDebugDuration("Transaction open", "Transaction terminated"); + _logger.LogDebug("Beginning transaction"); + _currentTransactionHolder.ReplaceCurrent(_dbConnection.BeginTransaction(_isolationLevel)); + _transactionLifetimeLogger = _logger.LogDebugDuration("Transaction open", "Transaction terminated"); _state = TxState.Active; - _operation.Begin(); + await _operation.BeginAsync(serviceScope, cancellationToken).ConfigureAwait(false); } - public IDbTransaction CurrentTransaction { get; private set; } - - public void Complete() + public async Task CompleteAsync(CancellationToken cancellationToken = default) { + await _operation.CompleteAsync(cancellationToken).ConfigureAwait(false); + if (_state != TxState.Active) { throw new InvalidOperationException($"A transaction cannot be committed when it is {_state}."); } - _operation.Complete(); - - Logger.LogDebug("Committing transaction"); - CurrentTransaction.Commit(); - CurrentTransaction.Dispose(); - CurrentTransaction = null; + _logger.LogDebug("Committing transaction"); + _currentTransactionHolder.Current.Commit(); + _currentTransactionHolder.Current.Dispose(); + _currentTransactionHolder.ReplaceCurrent(null); _transactionLifetimeLogger?.Dispose(); _transactionLifetimeLogger = null; if (_shouldHandleConnectionState) { - Logger.LogDebug("Closing connection"); + _logger.LogDebug("Closing connection"); _dbConnection.Close(); } _state = TxState.Committed; } - public void Cancel() + public async Task CancelAsync(CancellationToken cancellationToken = default) { - Logger.LogDebug("rolling back transaction"); + _logger.LogDebug("rolling back transaction"); if (_state != TxState.Active) { throw new InvalidOperationException($"Cannot roll back a transaction that is {_state}"); } - _operation.Cancel(); + await _operation.CancelAsync(cancellationToken).ConfigureAwait(false); - CurrentTransaction.Rollback(); - CurrentTransaction.Dispose(); - CurrentTransaction = null; + _currentTransactionHolder.Current.Rollback(); + _currentTransactionHolder.Current.Dispose(); + _currentTransactionHolder.ReplaceCurrent(null); _transactionLifetimeLogger?.Dispose(); _transactionLifetimeLogger = null; diff --git a/src/implementations/Backend.Fx.EfCorePersistence/Bootstrapping/IDbConnectionFactory.cs b/src/abstractions/Backend.Fx/Features/Persistence/AdoNet/IDbConnectionFactory.cs similarity index 67% rename from src/implementations/Backend.Fx.EfCorePersistence/Bootstrapping/IDbConnectionFactory.cs rename to src/abstractions/Backend.Fx/Features/Persistence/AdoNet/IDbConnectionFactory.cs index df953115..64501e62 100644 --- a/src/implementations/Backend.Fx.EfCorePersistence/Bootstrapping/IDbConnectionFactory.cs +++ b/src/abstractions/Backend.Fx/Features/Persistence/AdoNet/IDbConnectionFactory.cs @@ -1,6 +1,6 @@ using System.Data; -namespace Backend.Fx.EfCorePersistence.Bootstrapping +namespace Backend.Fx.Features.Persistence.AdoNet { public interface IDbConnectionFactory { diff --git a/src/abstractions/Backend.Fx/Features/Persistence/AdoNet/MsSqlSequence.cs b/src/abstractions/Backend.Fx/Features/Persistence/AdoNet/MsSqlSequence.cs new file mode 100644 index 00000000..8e2f44b7 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Persistence/AdoNet/MsSqlSequence.cs @@ -0,0 +1,96 @@ +using System; +using System.Data; +using Backend.Fx.Features.IdGeneration; +using Backend.Fx.Logging; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.Features.Persistence.AdoNet +{ + [PublicAPI] + public abstract class MsSqlSequence : ISequence + { + private readonly ILogger _logger = Log.Create>(); + private readonly IDbConnectionFactory _dbConnectionFactory; + private readonly int _startWith; + + protected MsSqlSequence(IDbConnectionFactory dbConnectionFactory, int startWith = 1) + { + _dbConnectionFactory = dbConnectionFactory; + _startWith = startWith; + } + + protected abstract string SequenceName { get; } + protected virtual string SchemaName { get; } = "dbo"; + + public void EnsureSequence() + { + _logger.LogInformation("Ensuring existence of mssql sequence {SchemaName}.{SequenceName}", SchemaName, SequenceName); + using IDbConnection dbConnection = _dbConnectionFactory.Create(); + dbConnection.Open(); + bool sequenceExists; + using (IDbCommand cmd = dbConnection.CreateCommand()) + { + cmd.CommandText = $"SELECT count(*) FROM sys.sequences seq join sys.schemas s on s.schema_id = seq.schema_id WHERE seq.name = '{SequenceName}' and s.name = '{SchemaName}'"; + sequenceExists = (int) cmd.ExecuteScalar() == 1; + } + + if (sequenceExists) + { + _logger.LogInformation("Sequence {SchemaName}.{SequenceName} exists", SchemaName, SequenceName); + } + else + { + _logger.LogInformation("Sequence {SchemaName}.{SequenceName} does not exist yet and will be created now", SchemaName, SequenceName); + using IDbCommand cmd = dbConnection.CreateCommand(); + cmd.CommandText = $"CREATE SEQUENCE [{SchemaName}].[{SequenceName}] START WITH {_startWith} INCREMENT BY {Increment}"; + cmd.ExecuteNonQuery(); + _logger.LogInformation("Sequence {SchemaName}.{SequenceName} created", SchemaName, SequenceName); + } + } + + public TId GetNextValue() + { + using IDbConnection dbConnection = _dbConnectionFactory.Create(); + dbConnection.Open(); + using IDbCommand selectNextValCommand = dbConnection.CreateCommand(); + selectNextValCommand.CommandText = $"SELECT next value FOR {SchemaName}.{SequenceName}"; + TId nextValue = ConvertNextValueFromSequence(selectNextValCommand.ExecuteScalar()); + _logger.LogDebug("{SchemaName}.{SequenceName} served {NextValue} as next value", SchemaName, SequenceName, nextValue); + + return nextValue; + } + + public abstract TId Increment { get; } + + protected abstract TId ConvertNextValueFromSequence(object valueFromSequence); + } + + [PublicAPI] + public abstract class MsSqlIntSequence : MsSqlSequence + { + protected MsSqlIntSequence(IDbConnectionFactory dbConnectionFactory, int startWith = 1) + : base(dbConnectionFactory, startWith) + { + } + + protected override int ConvertNextValueFromSequence(object valueFromSequence) + { + return Convert.ToInt32(valueFromSequence); + } + } + + [PublicAPI] + public abstract class MsSqlLongSequence : MsSqlSequence + { + protected MsSqlLongSequence(IDbConnectionFactory dbConnectionFactory, int startWith = 1) + : base(dbConnectionFactory, startWith) + { + } + + protected override long ConvertNextValueFromSequence(object valueFromSequence) + { + return Convert.ToInt64(valueFromSequence); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Persistence/AdoNet/OracleSequence.cs b/src/abstractions/Backend.Fx/Features/Persistence/AdoNet/OracleSequence.cs new file mode 100644 index 00000000..2cb8b583 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Persistence/AdoNet/OracleSequence.cs @@ -0,0 +1,112 @@ +using System; +using System.Data; +using Backend.Fx.Features.IdGeneration; +using Backend.Fx.Logging; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.Features.Persistence.AdoNet +{ + public abstract class OracleSequence : ISequence + { + private readonly ILogger _logger = Log.Create>(); + private readonly IDbConnectionFactory _dbConnectionFactory; + private readonly int _startWith; + + protected OracleSequence(IDbConnectionFactory dbConnectionFactory, int startWith = 1) + { + _dbConnectionFactory = dbConnectionFactory; + _startWith = startWith; + } + + protected abstract string SequenceName { get; } + protected abstract string SchemaName { get; } + + private string SchemaPrefix + { + get + { + if (string.IsNullOrEmpty(SchemaName)) return string.Empty; + + return SchemaName + "."; + } + } + + public void EnsureSequence() + { + _logger.LogInformation("Ensuring existence of oracle sequence {SchemaPrefix}.{SequenceName}", SchemaPrefix, SequenceName); + + using IDbConnection dbConnection = _dbConnectionFactory.Create(); + dbConnection.Open(); + bool sequenceExists; + using (IDbCommand command = dbConnection.CreateCommand()) + { + command.CommandText = $"SELECT count(*) FROM user_sequences WHERE sequence_name = '{SequenceName}'"; + sequenceExists = (decimal)command.ExecuteScalar() == 1; + } + + if (sequenceExists) + { + _logger.LogInformation("Oracle sequence {SchemaPrefix}.{SequenceName} exists", SchemaPrefix, SequenceName); + } + else + { + _logger.LogInformation("Oracle sequence {SchemaPrefix}.{SequenceName} does not exist yet and will be created now", + SchemaPrefix, + SequenceName); + using IDbCommand cmd = dbConnection.CreateCommand(); + cmd.CommandText = $"CREATE SEQUENCE {SchemaPrefix}{SequenceName} START WITH {_startWith} INCREMENT BY {Increment}"; + cmd.ExecuteNonQuery(); + _logger.LogInformation("Oracle sequence {SchemaPrefix}.{SequenceName} created", SchemaPrefix, SequenceName); + } + } + + public TId GetNextValue() + { + using IDbConnection dbConnection = _dbConnectionFactory.Create(); + dbConnection.Open(); + + using IDbCommand command = dbConnection.CreateCommand(); + command.CommandText = $"SELECT {SchemaPrefix}{SequenceName}.NEXTVAL FROM dual"; + TId nextValue = ConvertNextValueFromSequence(command.ExecuteScalar()); + _logger.LogDebug("Oracle sequence {SchemaPrefix}.{SequenceName} served {NextValue} as next value", + SchemaPrefix, + SequenceName, + nextValue); + + return nextValue; + } + + public abstract TId Increment { get; } + + protected abstract TId ConvertNextValueFromSequence(object valueFromSequence); + } + + [PublicAPI] + public abstract class OracleIntSequence : OracleSequence + { + protected OracleIntSequence(IDbConnectionFactory dbConnectionFactory, int startWith = 1) + : base(dbConnectionFactory, startWith) + { + } + + protected override int ConvertNextValueFromSequence(object valueFromSequence) + { + return Convert.ToInt32(valueFromSequence); + } + } + + [PublicAPI] + public abstract class OracleLongSequence : OracleSequence + { + protected OracleLongSequence(IDbConnectionFactory dbConnectionFactory, int startWith = 1) + : base(dbConnectionFactory, startWith) + { + } + + protected override long ConvertNextValueFromSequence(object valueFromSequence) + { + return Convert.ToInt64(valueFromSequence); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Persistence/AdoNet/PostgresSequence.cs b/src/abstractions/Backend.Fx/Features/Persistence/AdoNet/PostgresSequence.cs new file mode 100644 index 00000000..d6be12f2 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Persistence/AdoNet/PostgresSequence.cs @@ -0,0 +1,100 @@ +using System; +using System.Data; +using Backend.Fx.Features.IdGeneration; +using Backend.Fx.Logging; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.Features.Persistence.AdoNet +{ + public abstract class PostgresSequence : ISequence + { + private readonly ILogger _logger = Log.Create>(); + private readonly IDbConnectionFactory _dbConnectionFactory; + private readonly int _startWith; + + protected PostgresSequence(IDbConnectionFactory dbConnectionFactory, int startWith = 1) + { + _dbConnectionFactory = dbConnectionFactory; + _startWith = startWith; + } + + protected abstract string SequenceName { get; } + protected abstract string SchemaName { get; } + + public void EnsureSequence() + { + _logger.LogInformation("Ensuring existence of postgres sequence {SchemaName}.{SequenceName}", SchemaName, SequenceName); + + using IDbConnection dbConnection = _dbConnectionFactory.Create(); + dbConnection.Open(); + bool sequenceExists; + using (IDbCommand command = dbConnection.CreateCommand()) + { + command.CommandText = $"SELECT count(*) FROM information_schema.sequences WHERE sequence_name = '{SequenceName}' AND sequence_schema = '{SchemaName}'"; + sequenceExists = (long) command.ExecuteScalar() == 1L; + } + + if (sequenceExists) + { + _logger.LogInformation("Sequence {SchemaName}.{SequenceName} exists", SchemaName, SequenceName); + } + else + { + _logger.LogInformation( + "Sequence {SchemaName}.{SequenceName} does not exist yet and will be created now", + SchemaName, + SequenceName); + using IDbCommand cmd = dbConnection.CreateCommand(); + cmd.CommandText = $"CREATE SEQUENCE {SchemaName}.{SequenceName} START WITH {_startWith} INCREMENT BY {Increment}"; + cmd.ExecuteNonQuery(); + _logger.LogInformation("Sequence {SchemaName}.{SequenceName} created", SchemaName, SequenceName); + } + } + + public TId GetNextValue() + { + using IDbConnection dbConnection = _dbConnectionFactory.Create(); + dbConnection.Open(); + + using IDbCommand command = dbConnection.CreateCommand(); + command.CommandText = $"SELECT nextval('{SchemaName}.{SequenceName}');"; + TId nextValue = ConvertNextValueFromSequence(command.ExecuteScalar()); + _logger.LogDebug("{SchemaName}.{SequenceName} served {2} as next value", SchemaName, SequenceName, nextValue); + + return nextValue; + } + + public abstract TId Increment { get; } + + protected abstract TId ConvertNextValueFromSequence(object valueFromSequence); + } + + [PublicAPI] + public abstract class PostgresIntSequence : PostgresSequence + { + protected PostgresIntSequence(IDbConnectionFactory dbConnectionFactory, int startWith = 1) + : base(dbConnectionFactory, startWith) + { + } + + protected override int ConvertNextValueFromSequence(object valueFromSequence) + { + return Convert.ToInt32(valueFromSequence); + } + } + + [PublicAPI] + public abstract class PostgresLongSequence : PostgresSequence + { + protected PostgresLongSequence(IDbConnectionFactory dbConnectionFactory, int startWith = 1) + : base(dbConnectionFactory, startWith) + { + } + + protected override long ConvertNextValueFromSequence(object valueFromSequence) + { + return Convert.ToInt64(valueFromSequence); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Persistence/FlushDomainEventAggregatorDecorator.cs b/src/abstractions/Backend.Fx/Features/Persistence/FlushDomainEventAggregatorDecorator.cs new file mode 100644 index 00000000..1220f6cc --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Persistence/FlushDomainEventAggregatorDecorator.cs @@ -0,0 +1,29 @@ +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Features.DomainEvents; +using Backend.Fx.Logging; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.Features.Persistence +{ + public class FlushDomainEventAggregatorDecorator : IDomainEventAggregator + { + private readonly ILogger _logger = Log.Create(); + + private readonly ICanFlush _canFlush; + private readonly IDomainEventAggregator _domainEventAggregator; + + public FlushDomainEventAggregatorDecorator(ICanFlush canFlush, IDomainEventAggregator domainEventAggregator) + { + _canFlush = canFlush; + _domainEventAggregator = domainEventAggregator; + } + + public async Task RaiseEventsAsync(CancellationToken cancellationToken = default) + { + _logger.LogDebug("Flushing before raising domain events"); + _canFlush.Flush(); + await _domainEventAggregator.RaiseEventsAsync(cancellationToken).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Persistence/FlushOperationDecorator.cs b/src/abstractions/Backend.Fx/Features/Persistence/FlushOperationDecorator.cs new file mode 100644 index 00000000..92c4f5f8 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Persistence/FlushOperationDecorator.cs @@ -0,0 +1,39 @@ +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Logging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.Features.Persistence +{ + public class FlushOperationDecorator : IOperation + { + private readonly ILogger _logger = Log.Create(); + private readonly IOperation _operationImplementation; + private readonly ICanFlush _canFlush; + + public FlushOperationDecorator(ICanFlush canFlush, IOperation operationImplementation) + { + _operationImplementation = operationImplementation; + _canFlush = canFlush; + } + + public Task BeginAsync(IServiceScope serviceScope, CancellationToken cancellationToken = default) + { + return _operationImplementation.BeginAsync(serviceScope, cancellationToken); + } + + public async Task CompleteAsync(CancellationToken cancellationToken = default) + { + _logger.LogDebug("Flushing before completion of operation"); + _canFlush.Flush(); + await _operationImplementation.CompleteAsync(cancellationToken).ConfigureAwait(false); + } + + public Task CancelAsync(CancellationToken cancellationToken = default) + { + return _operationImplementation.CancelAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Persistence/ICanFlush.cs b/src/abstractions/Backend.Fx/Features/Persistence/ICanFlush.cs new file mode 100644 index 00000000..a783a1c8 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Persistence/ICanFlush.cs @@ -0,0 +1,16 @@ +using JetBrains.Annotations; + +namespace Backend.Fx.Features.Persistence +{ + public interface ICanFlush + { + void Flush(); + } + + [UsedImplicitly] + internal class DefaultFlush : ICanFlush + { + public void Flush() + { } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/Persistence/IDatabaseAvailabilityAwaiter.cs b/src/abstractions/Backend.Fx/Features/Persistence/IDatabaseAvailabilityAwaiter.cs similarity index 80% rename from src/abstractions/Backend.Fx/Environment/Persistence/IDatabaseAvailabilityAwaiter.cs rename to src/abstractions/Backend.Fx/Features/Persistence/IDatabaseAvailabilityAwaiter.cs index 7b21926e..40e9b2ce 100644 --- a/src/abstractions/Backend.Fx/Environment/Persistence/IDatabaseAvailabilityAwaiter.cs +++ b/src/abstractions/Backend.Fx/Features/Persistence/IDatabaseAvailabilityAwaiter.cs @@ -1,7 +1,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Backend.Fx.Environment.Persistence +namespace Backend.Fx.Features.Persistence { public interface IDatabaseAvailabilityAwaiter { diff --git a/src/abstractions/Backend.Fx/Environment/Persistence/IDatabaseBootstrapper.cs b/src/abstractions/Backend.Fx/Features/Persistence/IDatabaseBootstrapper.cs similarity index 59% rename from src/abstractions/Backend.Fx/Environment/Persistence/IDatabaseBootstrapper.cs rename to src/abstractions/Backend.Fx/Features/Persistence/IDatabaseBootstrapper.cs index 65f1049a..2ef8d556 100644 --- a/src/abstractions/Backend.Fx/Environment/Persistence/IDatabaseBootstrapper.cs +++ b/src/abstractions/Backend.Fx/Features/Persistence/IDatabaseBootstrapper.cs @@ -1,12 +1,14 @@ using System; +using System.Threading; +using System.Threading.Tasks; -namespace Backend.Fx.Environment.Persistence +namespace Backend.Fx.Features.Persistence { /// /// Encapsulates database bootstrapping. This interface hides the implementation details for creating/migrating the database /// public interface IDatabaseBootstrapper : IDisposable { - void EnsureDatabaseExistence(); + Task EnsureDatabaseExistenceAsync(CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Persistence/IRepository.cs b/src/abstractions/Backend.Fx/Features/Persistence/IRepository.cs new file mode 100644 index 00000000..47769a7a --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Persistence/IRepository.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Domain; +using Backend.Fx.Exceptions; +using JetBrains.Annotations; + +namespace Backend.Fx.Features.Persistence +{ + /// + /// Encapsulates methods for retrieving domain objects + /// See https://en.wikipedia.org/wiki/Domain-driven_design#Building_blocks + /// + [PublicAPI] + public interface IRepository where TAggregateRoot : IAggregateRoot + where TId : IEquatable + { + /// + /// Throws a when nothing matches the given id + /// + /// + /// + /// + Task GetAsync(TId id, CancellationToken cancellationToken = default); + + /// + /// Returns null when nothing matches the given id + /// + /// + /// + /// + Task FindAsync(TId id, CancellationToken cancellationToken = default); + + Task GetAllAsync(CancellationToken cancellationToken = default); + + Task AnyAsync(CancellationToken cancellationToken = default); + + Task ResolveAsync(IEnumerable ids, CancellationToken cancellationToken = default); + + Task DeleteAsync(TAggregateRoot aggregateRoot, CancellationToken cancellationToken = default); + + Task AddAsync(TAggregateRoot aggregateRoot, CancellationToken cancellationToken = default); + + Task AddRangeAsync(TAggregateRoot[] aggregateRoots, CancellationToken cancellationToken = default); + } + + public static class RepositoryEx + { + public static async Task ResolveAsync( + this IRepository repository, + IEnumerable ids, + CancellationToken cancellationToken = default) + where TAggregateRoot : IAggregateRoot + where TId : IEquatable + { + var idArray = ids as TId[] ?? ids.ToArray(); + var resolved = new TAggregateRoot[idArray.Length]; + using IExceptionBuilder builder = NotFoundException.UseBuilder(); + for (var i = 0; i < idArray.Length; i++) + { + resolved[i] = await repository.FindAsync(idArray[i], cancellationToken).ConfigureAwait(false); + builder.AddNotFoundWhenNull(idArray[i], resolved[i]); + } + + return resolved; + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Persistence/InMem/AggregateDictionaries.cs b/src/abstractions/Backend.Fx/Features/Persistence/InMem/AggregateDictionaries.cs new file mode 100644 index 00000000..e0f54438 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Persistence/InMem/AggregateDictionaries.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using Backend.Fx.Domain; + +namespace Backend.Fx.Features.Persistence.InMem +{ + public interface IAggregateDictionaries + { + IQueryable GetQueryable() + where TAggregateRoot : IAggregateRoot; + } + + public interface IAggregateDictionaries : IAggregateDictionaries + where TId : IEquatable + { + AggregateDictionary For() + where TAggregateRoot : IAggregateRoot; + } + + public class AggregateDictionaries : IAggregateDictionaries where TId : IEquatable + { + private readonly ConcurrentDictionary _aggregateDictionaries = new(); + + public AggregateDictionary For() + where TAggregateRoot : IAggregateRoot + { + var store = (AggregateDictionary)_aggregateDictionaries.GetOrAdd( + typeof(TAggregateRoot), + _ => new AggregateDictionary()); + return store; + } + + public IQueryable GetQueryable() where TAggregateRoot : IAggregateRoot + { + dynamic store = _aggregateDictionaries.GetOrAdd( + typeof(TAggregateRoot), + aggType => + { + PropertyInfo idPropertyInfo = aggType.GetProperty("Id") ?? aggType.GetProperty("ID"); + Debug.Assert(idPropertyInfo != null); + Type idType = idPropertyInfo.PropertyType; + Type aggregateDictionaryType = typeof(AggregateDictionary<,>).MakeGenericType(aggType, idType); + return Activator.CreateInstance(aggregateDictionaryType); + }); + return Queryable.AsQueryable(store.Values); + + + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Persistence/InMem/AggregateDictionary.cs b/src/abstractions/Backend.Fx/Features/Persistence/InMem/AggregateDictionary.cs new file mode 100644 index 00000000..6bb0dcd2 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Persistence/InMem/AggregateDictionary.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Backend.Fx.Domain; +using JetBrains.Annotations; + +namespace Backend.Fx.Features.Persistence.InMem +{ + public interface IAggregateDictionary : IDictionary where TAggregateRoot : IAggregateRoot + where TId : IEquatable + { } + + [UsedImplicitly] + public class AggregateDictionary : IAggregateDictionary + where TAggregateRoot : IAggregateRoot + where TId : IEquatable + { + private readonly IDictionary _dictionary = new Dictionary(); + + public IEnumerator> GetEnumerator() + { + return _dictionary.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable) _dictionary).GetEnumerator(); + } + + public void Add(KeyValuePair item) + { + _dictionary.Add(item); + } + + public void Clear() + { + _dictionary.Clear(); + } + + public bool Contains(KeyValuePair item) + { + return _dictionary.Contains(item); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + _dictionary.CopyTo(array, arrayIndex); + } + + public bool Remove(KeyValuePair item) + { + return _dictionary.Remove(item); + } + + public int Count => _dictionary.Count; + + public bool IsReadOnly => _dictionary.IsReadOnly; + + public void Add(TId key, TAggregateRoot value) + { + _dictionary.Add(key, value); + } + + public bool ContainsKey(TId key) + { + return _dictionary.ContainsKey(key); + } + + public bool Remove(TId key) + { + return _dictionary.Remove(key); + } + + public bool TryGetValue(TId key, out TAggregateRoot value) + { + return _dictionary.TryGetValue(key, out value); + } + + public TAggregateRoot this[TId key] + { + get => _dictionary[key]; + set => _dictionary[key] = value; + } + + public ICollection Keys => _dictionary.Keys; + + public ICollection Values => _dictionary.Values; + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Persistence/InMem/InMemoryDatabase.cs b/src/abstractions/Backend.Fx/Features/Persistence/InMem/InMemoryDatabase.cs new file mode 100644 index 00000000..7d697168 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Persistence/InMem/InMemoryDatabase.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Concurrent; +using Backend.Fx.Features.MultiTenancy; + +namespace Backend.Fx.Features.Persistence.InMem +{ + public class InMemoryDatabase where TId : IEquatable + { + private readonly ConcurrentDictionary> _inMemoryStores = new(); + + public IAggregateDictionaries GetInMemoryStores() + { + return _inMemoryStores.GetOrAdd(new TenantId(null), _ => new AggregateDictionaries()); + } + + public IAggregateDictionaries GetInMemoryStoresOfTenant(TenantId tenantId) + { + return _inMemoryStores.GetOrAdd(tenantId, _ => new AggregateDictionaries()); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Persistence/InMem/InMemoryDatabaseAccessor.cs b/src/abstractions/Backend.Fx/Features/Persistence/InMem/InMemoryDatabaseAccessor.cs new file mode 100644 index 00000000..be485469 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Persistence/InMem/InMemoryDatabaseAccessor.cs @@ -0,0 +1,44 @@ +using System; +using Backend.Fx.Features.MultiTenancy; +using Backend.Fx.Util; + +namespace Backend.Fx.Features.Persistence.InMem +{ + public interface IInMemoryDatabaseAccessor where TId : IEquatable + { + IAggregateDictionaries GetAggregateDictionaries(); + } + + public class InMemoryDatabaseAccessor : IInMemoryDatabaseAccessor where TId : IEquatable + { + private readonly InMemoryDatabase _inMemoryDatabase; + + public InMemoryDatabaseAccessor(InMemoryDatabase inMemoryDatabase) + { + _inMemoryDatabase = inMemoryDatabase; + } + + public IAggregateDictionaries GetAggregateDictionaries() + { + return _inMemoryDatabase.GetInMemoryStores(); + } + } + + public class MultiTenancyInMemoryDatabaseAccessor : IInMemoryDatabaseAccessor where TId : IEquatable + { + private readonly InMemoryDatabase _inMemoryDatabase; + private readonly ICurrentTHolder _tenantIdHolder; + + // ReSharper disable once UnusedParameter.Local + public MultiTenancyInMemoryDatabaseAccessor(InMemoryDatabase inMemoryDatabase, ICurrentTHolder tenantIdHolder, IInMemoryDatabaseAccessor unused) + { + _inMemoryDatabase = inMemoryDatabase; + _tenantIdHolder = tenantIdHolder; + } + + public IAggregateDictionaries GetAggregateDictionaries() + { + return _inMemoryDatabase.GetInMemoryStoresOfTenant(_tenantIdHolder.Current); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Persistence/InMem/InMemoryPersistenceModule.cs b/src/abstractions/Backend.Fx/Features/Persistence/InMem/InMemoryPersistenceModule.cs new file mode 100644 index 00000000..8093a8d4 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Persistence/InMem/InMemoryPersistenceModule.cs @@ -0,0 +1,47 @@ +using System; +using System.Linq; +using Backend.Fx.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.Features.Persistence.InMem +{ + public class InMemoryPersistenceModule : PersistenceModule where TId : IEquatable + { + public override void Register(ICompositionRoot compositionRoot) + { + base.Register(compositionRoot); + + // a singleton database + compositionRoot.Register(ServiceDescriptor.Singleton, InMemoryDatabase>()); + + // a scoped accessor to get the aggregate dictionaries + compositionRoot.Register(ServiceDescriptor.Scoped, InMemoryDatabaseAccessor>()); + + // we use the scoped accessor to get the aggregate dictionaries. This will be decorated in case + // of Multi Tenancy to switch the tenant store (we provide it as generic and non generic version, too) + compositionRoot.Register(ServiceDescriptor.Scoped(sp => + sp.GetRequiredService>().GetAggregateDictionaries())); + compositionRoot.Register(ServiceDescriptor.Scoped(sp => + sp.GetRequiredService>().GetAggregateDictionaries())); + + compositionRoot.Register( + ServiceDescriptor.Scoped(typeof(IQueryable<>),typeof(InMemoryQueryable<>))); + + compositionRoot.Register( + ServiceDescriptor.Scoped(typeof(IRepository<,>), typeof(InMemoryRepository<,>))); + } + + public override IModule MultiTenancyModule => new InMemoryMultiTenancyPersistenceModule(); + } + + public class InMemoryMultiTenancyPersistenceModule : IModule where TId : IEquatable + { + public void Register(ICompositionRoot compositionRoot) + { + // the MultiTenancyInMemoryDatabaseAccessor used the ICurrentTHolder to determine the respective + // in memory collection specific to this tenant id + compositionRoot.RegisterDecorator(ServiceDescriptor + .Scoped, MultiTenancyInMemoryDatabaseAccessor>()); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Persistence/InMem/InMemoryQueryable.cs b/src/abstractions/Backend.Fx/Features/Persistence/InMem/InMemoryQueryable.cs new file mode 100644 index 00000000..3bb5aae0 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Persistence/InMem/InMemoryQueryable.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Backend.Fx.Domain; + +namespace Backend.Fx.Features.Persistence.InMem +{ + public class InMemoryQueryable : IQueryable + where TAggregateRoot : IAggregateRoot + { + private readonly IQueryable _aggregateQueryable; + + public InMemoryQueryable(IAggregateDictionaries aggregateDictionaries) + { + _aggregateQueryable = aggregateDictionaries.GetQueryable(); + } + + public Type ElementType => _aggregateQueryable.ElementType; + + public Expression Expression => _aggregateQueryable.Expression; + + IQueryProvider IQueryable.Provider => _aggregateQueryable.Provider; + + public IEnumerator GetEnumerator() + { + return _aggregateQueryable.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_aggregateQueryable).GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Persistence/InMem/InMemoryRepository.cs b/src/abstractions/Backend.Fx/Features/Persistence/InMem/InMemoryRepository.cs new file mode 100644 index 00000000..c12122f8 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Persistence/InMem/InMemoryRepository.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Domain; +using Backend.Fx.Exceptions; +using JetBrains.Annotations; + +namespace Backend.Fx.Features.Persistence.InMem +{ + [PublicAPI] + public class InMemoryRepository : IRepository + where TAggregateRoot : class, IAggregateRoot + where TId : IEquatable + { + private readonly IQueryable _aggregateQueryable; + + public InMemoryRepository( + IAggregateDictionaries aggregateDictionaries, + IQueryable aggregateQueryable) + { + // we could get the queryable directly from the store, but that would prevent the DI from decorating + // the queryable, as it is done e.g. in case of Authorization + _aggregateQueryable = aggregateQueryable; + Store = aggregateDictionaries.For(); + } + + public virtual IAggregateDictionary Store { get; } + + public void Clear() + { + Store.Clear(); + } + + public Task GetAsync(TId id, CancellationToken cancellationToken = default) + { + return Task.FromResult(_aggregateQueryable.FirstOrDefault(agg => Equals(agg.Id, id)) + ?? throw new NotFoundException(id)); + } + + public Task FindAsync(TId id, CancellationToken cancellationToken = default) + { + return Task.FromResult(_aggregateQueryable.FirstOrDefault(agg => Equals(agg.Id, id))); + } + + public Task GetAllAsync(CancellationToken cancellationToken = default) + { + // the "useless" where condition makes sure we do not return the underlying array, but evaluate the + // expression, that might have been extended with additional filters (authorization, multi tenancy) + return Task.FromResult(_aggregateQueryable.Where(agg => true).ToArray()); + } + + public Task AnyAsync(CancellationToken cancellationToken = default) + { + return Task.FromResult(_aggregateQueryable.Any()); + } + + public Task ResolveAsync(IEnumerable ids, CancellationToken cancellationToken = default) + { + return RepositoryEx.ResolveAsync(this, ids, cancellationToken); + } + + public Task DeleteAsync(TAggregateRoot aggregateRoot, CancellationToken cancellationToken = default) + { + if (Store.ContainsKey(aggregateRoot.Id)) + { + Store.Remove(aggregateRoot.Id); + } + + return Task.CompletedTask; + } + + public Task AddAsync(TAggregateRoot aggregateRoot, CancellationToken cancellationToken = default) + { + if (Store.ContainsKey(aggregateRoot.Id)) + { + throw new ConflictedException( + $"There is already an {aggregateRoot.GetType().Name} with id {aggregateRoot.Id} present"); + } + + Store[aggregateRoot.Id] = aggregateRoot; + return Task.CompletedTask; + } + + public async Task AddRangeAsync(TAggregateRoot[] aggregateRoots, + CancellationToken cancellationToken = default) + { + foreach (TAggregateRoot aggregateRoot in aggregateRoots) + { + await AddAsync(aggregateRoot, cancellationToken).ConfigureAwait(false); + } + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Persistence/PersistenceFeature.cs b/src/abstractions/Backend.Fx/Features/Persistence/PersistenceFeature.cs new file mode 100644 index 00000000..844eb3bf --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Persistence/PersistenceFeature.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Backend.Fx.Features.Persistence +{ + [PublicAPI] + public sealed class PersistenceFeature : Feature, IBootableFeature, IMultiTenancyFeature + { + private readonly PersistenceModule _persistenceModule; + private readonly IDatabaseAvailabilityAwaiter _databaseAvailabilityAwaiter; + private readonly IDatabaseBootstrapper _databaseBootstrapper; + + + public PersistenceFeature( + PersistenceModule persistenceModule, + IDatabaseAvailabilityAwaiter databaseAvailabilityAwaiter = null, + IDatabaseBootstrapper databaseBootstrapper = null) + { + _persistenceModule = persistenceModule; + _databaseAvailabilityAwaiter = databaseAvailabilityAwaiter; + _databaseBootstrapper = databaseBootstrapper; + } + + public override void Enable(IBackendFxApplication application) + { + application.CompositionRoot.RegisterModules(_persistenceModule); + } + + public void EnableMultiTenancyServices(IBackendFxApplication application) + { + if (_persistenceModule.MultiTenancyModule == null) + { + throw new InvalidOperationException($"No multi tenancy module provided by {_persistenceModule.GetType().Name}"); + } + + application.CompositionRoot.RegisterModules(_persistenceModule.MultiTenancyModule); + } + + public async Task BootAsync(IBackendFxApplication application, CancellationToken cancellationToken = default) + { + if (_databaseAvailabilityAwaiter != null) + { + await _databaseAvailabilityAwaiter.WaitForDatabase(cancellationToken).ConfigureAwait(false); + } + + if (_databaseBootstrapper != null) + { + await _databaseBootstrapper.EnsureDatabaseExistenceAsync(cancellationToken).ConfigureAwait(false); + } + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Features/Persistence/PersistenceModule.cs b/src/abstractions/Backend.Fx/Features/Persistence/PersistenceModule.cs new file mode 100644 index 00000000..ad1143a0 --- /dev/null +++ b/src/abstractions/Backend.Fx/Features/Persistence/PersistenceModule.cs @@ -0,0 +1,23 @@ +using Backend.Fx.DependencyInjection; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Features.DomainEvents; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.Features.Persistence +{ + public abstract class PersistenceModule : IModule + { + public virtual void Register(ICompositionRoot compositionRoot) + { + // register a default flush scoped instance, so that it can be decorated later + compositionRoot.Register(ServiceDescriptor.Scoped()); + compositionRoot.RegisterDecorator(ServiceDescriptor.Scoped()); + + // make sure we flush pending changes before raising pending domain events + compositionRoot.RegisterDecorator( + ServiceDescriptor.Scoped()); + } + + public virtual IModule MultiTenancyModule => null; + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Hacking/AdjustableClock.cs b/src/abstractions/Backend.Fx/Hacking/AdjustableClock.cs new file mode 100644 index 00000000..dbe4f236 --- /dev/null +++ b/src/abstractions/Backend.Fx/Hacking/AdjustableClock.cs @@ -0,0 +1,37 @@ +using Backend.Fx.Logging; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using NodaTime; + +namespace Backend.Fx.Hacking +{ + [PublicAPI] + public class AdjustableClock : IClock + { + private readonly ILogger _logger = Log.Create(); + + private readonly IClock _clockImplementation; + private Instant? _overriddenUtcNow; + + public AdjustableClock(IClock clockImplementation) + { + _clockImplementation = clockImplementation; + } + + public Instant GetCurrentInstant() => _overriddenUtcNow ?? _clockImplementation.GetCurrentInstant(); + + public void OverrideUtcNow(Instant instant) + { + _logger.LogTrace("Adjusting clock to {Instant}", instant); + _overriddenUtcNow = instant; + } + + public Instant Advance(Duration duration) + { + _overriddenUtcNow ??= _clockImplementation.GetCurrentInstant(); + _logger.LogTrace("Advancing clock by {TimeSpan}", duration); + _overriddenUtcNow = _overriddenUtcNow.Value.Plus(duration); + return _overriddenUtcNow.Value; + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Hacking/InitProperty.cs b/src/abstractions/Backend.Fx/Hacking/InitProperty.cs new file mode 100644 index 00000000..e122763b --- /dev/null +++ b/src/abstractions/Backend.Fx/Hacking/InitProperty.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; + +// this class is a workaround for a version incompatibility +// https://developercommunity.visualstudio.com/t/error-cs0518-predefined-type-systemruntimecompiler/1244809 + + +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices +{ + [UsedImplicitly] + internal static class IsExternalInit {} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Hacking/PrivateSetterCaller.cs b/src/abstractions/Backend.Fx/Hacking/PrivateSetterCaller.cs index db95c07b..65aa043a 100644 --- a/src/abstractions/Backend.Fx/Hacking/PrivateSetterCaller.cs +++ b/src/abstractions/Backend.Fx/Hacking/PrivateSetterCaller.cs @@ -2,9 +2,11 @@ using System.Diagnostics; using System.Linq.Expressions; using System.Reflection; +using JetBrains.Annotations; namespace Backend.Fx.Hacking { + [PublicAPI] public static class PrivateSetterCaller { public static void SetPrivate(this T instance, Expression> propertyExpression, TValue value) @@ -14,10 +16,10 @@ public static void SetPrivate(this T instance, Expression(Expression> exp) { - if (!(exp.Body is MemberExpression body)) + if (exp.Body is not MemberExpression body) { - var ubody = (UnaryExpression) exp.Body; - body = ubody.Operand as MemberExpression; + var unaryExpression = (UnaryExpression) exp.Body; + body = unaryExpression.Operand as MemberExpression; } Debug.Assert(body != null, "body != null"); diff --git a/src/abstractions/Backend.Fx/Hacking/PrivateUtil.cs b/src/abstractions/Backend.Fx/Hacking/PrivateUtil.cs index a2cb535c..dba42007 100644 --- a/src/abstractions/Backend.Fx/Hacking/PrivateUtil.cs +++ b/src/abstractions/Backend.Fx/Hacking/PrivateUtil.cs @@ -1,9 +1,11 @@ using System; using System.Linq; using System.Reflection; +using JetBrains.Annotations; namespace Backend.Fx.Hacking { + [PublicAPI] public static class PrivateUtil { public static T CreateInstanceFromPrivateDefaultConstructor() diff --git a/src/abstractions/Backend.Fx/Logging/DebugExceptionLogger.cs b/src/abstractions/Backend.Fx/Logging/DebugExceptionLogger.cs index 8db9eee5..1f5f5719 100644 --- a/src/abstractions/Backend.Fx/Logging/DebugExceptionLogger.cs +++ b/src/abstractions/Backend.Fx/Logging/DebugExceptionLogger.cs @@ -1,16 +1,18 @@ using System; using System.Diagnostics; using Backend.Fx.Exceptions; +using JetBrains.Annotations; namespace Backend.Fx.Logging { + [PublicAPI] public class DebugExceptionLogger : IExceptionLogger { public void LogException(Exception exception) { if (exception is ClientException cex) { - Debug.WriteLine(cex + System.Environment.NewLine + cex.Errors); + Debug.WriteLine(cex + Environment.NewLine + cex.Errors); } else { diff --git a/src/abstractions/Backend.Fx/Logging/DurationLogger.cs b/src/abstractions/Backend.Fx/Logging/DurationLogger.cs index 0fdf2be0..2160f3d4 100644 --- a/src/abstractions/Backend.Fx/Logging/DurationLogger.cs +++ b/src/abstractions/Backend.Fx/Logging/DurationLogger.cs @@ -1,16 +1,18 @@ using System; using System.Diagnostics; +using JetBrains.Annotations; using Microsoft.Extensions.Logging; namespace Backend.Fx.Logging { + [PublicAPI] [DebuggerStepThrough] public class DurationLogger : IDisposable { private readonly string _endMessage; private readonly Action _logAction; - private readonly Stopwatch _stopwatch = new Stopwatch(); + private readonly Stopwatch _stopwatch = new(); public DurationLogger(Action logAction, string activity) : this(logAction, activity, activity) @@ -47,34 +49,35 @@ private static string FormatDuration(string activity, TimeSpan duration) } } + [PublicAPI] public static class DurationLoggerEx { - public static IDisposable LogInformationDuration(this Microsoft.Extensions.Logging.ILogger logger, string activity) + public static IDisposable LogInformationDuration(this ILogger logger, string activity) { return new DurationLogger(s => logger.LogInformation(s), activity); } - public static IDisposable LogInformationDuration(this Microsoft.Extensions.Logging.ILogger logger, string beginMessage, string endMessage) + public static IDisposable LogInformationDuration(this ILogger logger, string beginMessage, string endMessage) { return new DurationLogger(s => logger.LogInformation(s), beginMessage, endMessage); } - public static IDisposable LogDebugDuration(this Microsoft.Extensions.Logging.ILogger logger, string activity) + public static IDisposable LogDebugDuration(this ILogger logger, string activity) { return new DurationLogger(s => logger.LogDebug(s), activity); } - public static IDisposable LogDebugDuration(this Microsoft.Extensions.Logging.ILogger logger, string beginMessage, string endMessage) + public static IDisposable LogDebugDuration(this ILogger logger, string beginMessage, string endMessage) { return new DurationLogger(s => logger.LogDebug(s), beginMessage, endMessage); } - public static IDisposable LogTraceDuration(this Microsoft.Extensions.Logging.ILogger logger, string activity) + public static IDisposable LogTraceDuration(this ILogger logger, string activity) { return new DurationLogger(s => logger.LogTrace(s), activity); } - public static IDisposable LogTraceDuration(this Microsoft.Extensions.Logging.ILogger logger, string beginMessage, string endMessage) + public static IDisposable LogTraceDuration(this ILogger logger, string beginMessage, string endMessage) { return new DurationLogger(s => logger.LogTrace(s), beginMessage, endMessage); } diff --git a/src/abstractions/Backend.Fx/Logging/ExceptionExtensions.cs b/src/abstractions/Backend.Fx/Logging/ExceptionExtensions.cs index 2d4d1859..a8df1bec 100644 --- a/src/abstractions/Backend.Fx/Logging/ExceptionExtensions.cs +++ b/src/abstractions/Backend.Fx/Logging/ExceptionExtensions.cs @@ -2,9 +2,11 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using JetBrains.Annotations; namespace Backend.Fx.Logging { + [PublicAPI] [DebuggerStepThrough] public static class ExceptionExtensions { @@ -23,7 +25,7 @@ public static IEnumerable FromHierarchy( Func nextItem, Func canContinue) where TSource : Exception { - for (var current = source; canContinue(current); current = nextItem(current)) + for (TSource current = source; canContinue(current); current = nextItem(current)) { yield return current; } diff --git a/src/abstractions/Backend.Fx/Logging/ExceptionLoggers.cs b/src/abstractions/Backend.Fx/Logging/ExceptionLoggers.cs index 18584133..fa56c4c3 100644 --- a/src/abstractions/Backend.Fx/Logging/ExceptionLoggers.cs +++ b/src/abstractions/Backend.Fx/Logging/ExceptionLoggers.cs @@ -1,13 +1,15 @@ using System; using System.Collections; using System.Collections.Generic; +using JetBrains.Annotations; using Microsoft.Extensions.Logging; namespace Backend.Fx.Logging { + [PublicAPI] public class ExceptionLoggers : ICollection, IExceptionLogger { - private static readonly Microsoft.Extensions.Logging.ILogger Logger = Log.Create(); + private readonly ILogger _logger = Log.Create(); private readonly ICollection _collectionImplementation = new List(); public ExceptionLoggers() @@ -32,7 +34,7 @@ public void LogException(Exception ex) } catch (Exception ex2) { - Logger.LogError(ex, + _logger.LogError(ex, "{ExceptionLoggerTypeName} failed to log the {ExceptionTypeName} with message {ExceptionMessage}", exceptionLogger.GetType().Name, ex2.GetType(), diff --git a/src/abstractions/Backend.Fx/Logging/IExceptionLogger.cs b/src/abstractions/Backend.Fx/Logging/IExceptionLogger.cs index 891eb7a5..f967fb46 100644 --- a/src/abstractions/Backend.Fx/Logging/IExceptionLogger.cs +++ b/src/abstractions/Backend.Fx/Logging/IExceptionLogger.cs @@ -11,9 +11,9 @@ public interface IExceptionLogger public class ExceptionLogger : IExceptionLogger { - private readonly Microsoft.Extensions.Logging.ILogger _logger; + private readonly ILogger _logger; - public ExceptionLogger(Microsoft.Extensions.Logging.ILogger logger) + public ExceptionLogger(ILogger logger) { _logger = logger; } diff --git a/src/abstractions/Backend.Fx/Logging/Log.cs b/src/abstractions/Backend.Fx/Logging/Log.cs index 17a2435b..351e4f37 100644 --- a/src/abstractions/Backend.Fx/Logging/Log.cs +++ b/src/abstractions/Backend.Fx/Logging/Log.cs @@ -1,6 +1,7 @@ using System; using System.Threading; -using Backend.Fx.Extensions; +using Backend.Fx.Util; +using JetBrains.Annotations; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -9,14 +10,14 @@ namespace Backend.Fx.Logging /// /// static class to keep an ILoggerFactory instance to use Microsoft.Extension.Logging without dependency injection /// + [PublicAPI] public static class Log { - private static Microsoft.Extensions.Logging.ILoggerFactory _loggerFactory = new NullLoggerFactory(); + private static ILoggerFactory _loggerFactory = new NullLoggerFactory(); - private static readonly AsyncLocal AsyncLocalLoggerFactory = - new AsyncLocal(); + private static readonly AsyncLocal AsyncLocalLoggerFactory = new(); - public static void Initialize(Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) + public static void Initialize(ILoggerFactory loggerFactory) { _loggerFactory = loggerFactory; } @@ -25,44 +26,43 @@ public static void Initialize(Microsoft.Extensions.Logging.ILoggerFactory logger /// Override the global, static ILoggerFactory in this async local scope. This can be done per web request or per test run /// /// - public static IDisposable InitAsyncLocal(Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) + public static IDisposable InitAsyncLocal(ILoggerFactory loggerFactory) { AsyncLocalLoggerFactory.Value = loggerFactory; return new DelegateDisposable(() => AsyncLocalLoggerFactory.Value = null); } - public static Microsoft.Extensions.Logging.ILogger Create() + public static ILogger Create() { return LoggerFactory.CreateLogger(typeof(T).FullName); } - public static Microsoft.Extensions.Logging.ILogger Create(Type t) + public static ILogger Create(Type t) { return LoggerFactory.CreateLogger(t.FullName); } - public static Microsoft.Extensions.Logging.ILogger Create(string category) + public static ILogger Create(string category) { return LoggerFactory.CreateLogger(category); } - public static Microsoft.Extensions.Logging.ILoggerFactory LoggerFactory { get; } - = new MaybeAsyncLocalLoggerFactory(); + public static ILoggerFactory LoggerFactory { get; } = new MaybeAsyncLocalLoggerFactory(); - private class MaybeAsyncLocalLoggerFactory : Microsoft.Extensions.Logging.ILoggerFactory + private class MaybeAsyncLocalLoggerFactory : ILoggerFactory { public void Dispose() { } - public Microsoft.Extensions.Logging.ILogger CreateLogger(string categoryName) + public ILogger CreateLogger(string categoryName) { - return (AsyncLocalLoggerFactory.Value ?? _loggerFactory) - .CreateLogger(categoryName); + return (AsyncLocalLoggerFactory.Value ?? _loggerFactory).CreateLogger(categoryName); } public void AddProvider(ILoggerProvider provider) { + (AsyncLocalLoggerFactory.Value ?? _loggerFactory).AddProvider(provider); } } } diff --git a/src/abstractions/Backend.Fx/Logging/Obsolete/BackendFxToMicrosoftLoggingLogger.cs b/src/abstractions/Backend.Fx/Logging/Obsolete/BackendFxToMicrosoftLoggingLogger.cs deleted file mode 100644 index c4733bcf..00000000 --- a/src/abstractions/Backend.Fx/Logging/Obsolete/BackendFxToMicrosoftLoggingLogger.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System; -using JetBrains.Annotations; -using Microsoft.Extensions.Logging; - -namespace Backend.Fx.Logging -{ - [Obsolete] - public class BackendFxToMicrosoftLoggingLogger : Backend.Fx.Logging.ILogger - { - private readonly Microsoft.Extensions.Logging.ILogger _logger; - - public BackendFxToMicrosoftLoggingLogger(Microsoft.Extensions.Logging.ILogger logger) - { - _logger = logger; - } - - public Exception Fatal(Exception exception) - { - _logger.LogCritical(exception, "Exception: {Message}", exception.Message); - return exception; - } - - public void Fatal([StructuredMessageTemplate] string format, params object[] args) - { - _logger.LogCritical(format, args); - } - - public Exception Fatal(Exception exception, [StructuredMessageTemplate] string format, params object[] args) - { - _logger.LogCritical(exception, format, args); - return exception; - } - - - - public Exception Error(Exception exception) - { - _logger.LogError(exception, "Exception: {Message}", exception.Message); - return exception; - } - - public void Error([StructuredMessageTemplate] string format, params object[] args) - { - _logger.LogError(format, args); - } - - public Exception Error(Exception exception, [StructuredMessageTemplate] string format, params object[] args) - { - _logger.LogError(exception, format, args); - return exception; - } - - - public Exception Warn(Exception exception) - { - _logger.LogWarning(exception, "Exception: {Message}", exception.Message); - return exception; - } - - public void Warn([StructuredMessageTemplate] string format, params object[] args) - { - _logger.LogWarning(format, args); - } - - public Exception Warn(Exception exception, [StructuredMessageTemplate] string format, params object[] args) - { - _logger.LogWarning(exception, format, args); - return exception; - } - - - public Exception Info(Exception exception) - { - _logger.LogInformation(exception, "Exception: {Message}", exception.Message); - return exception; - } - - public IDisposable InfoDuration(string activity) - { - return new DurationLogger(s => _logger.LogInformation(s), activity); - } - - public IDisposable InfoDuration(string beginMessage, string endMessage) - { - return new DurationLogger(s => _logger.LogInformation(s), beginMessage, endMessage); - } - - public void Info([StructuredMessageTemplate] string format, params object[] args) - { - _logger.LogInformation(format, args); - } - - public Exception Info(Exception exception, [StructuredMessageTemplate] string format, params object[] args) - { - _logger.LogInformation(exception, format, args); - return exception; - } - - public bool IsDebugEnabled() - { - return _logger.IsEnabled(LogLevel.Debug); - } - - - public Exception Debug(Exception exception) - { - _logger.LogDebug(exception, "Exception: {Message}", exception.Message); - return exception; - } - - public IDisposable DebugDuration(string activity) - { - return new DurationLogger(s => _logger.LogDebug(s), activity); - } - - public IDisposable DebugDuration(string beginMessage, string endMessage) - { - return new DurationLogger(s => _logger.LogDebug(s), beginMessage, endMessage); - } - - public void Debug([StructuredMessageTemplate] string format, params object[] args) - { - _logger.LogDebug(format, args); - } - - public Exception Debug(Exception exception, [StructuredMessageTemplate] string format, params object[] args) - { - _logger.LogDebug(exception, format, args); - return exception; - } - - public bool IsTraceEnabled() - { - return _logger.IsEnabled(LogLevel.Trace); - } - - - public Exception Trace(Exception exception) - { - _logger.LogTrace(exception, "Exception: {Message}", exception.Message); - return exception; - } - - public IDisposable TraceDuration(string activity) - { - return new DurationLogger(s => _logger.LogTrace(s), activity); - } - - public IDisposable TraceDuration(string beginMessage, string endMessage) - { - return new DurationLogger(s => _logger.LogTrace(s), beginMessage, endMessage); - } - - public void Trace([StructuredMessageTemplate] string format, params object[] args) - { - _logger.LogTrace(format, args); - } - - public Exception Trace(Exception exception, [StructuredMessageTemplate] string format, params object[] args) - { - _logger.LogTrace(exception, format, args); - return exception; - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Logging/Obsolete/BackendFxToMicrosoftLoggingLoggerFactory.cs b/src/abstractions/Backend.Fx/Logging/Obsolete/BackendFxToMicrosoftLoggingLoggerFactory.cs deleted file mode 100644 index c42397cd..00000000 --- a/src/abstractions/Backend.Fx/Logging/Obsolete/BackendFxToMicrosoftLoggingLoggerFactory.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; - -namespace Backend.Fx.Logging -{ - [Obsolete] - public class BackendFxToMicrosoftLoggingLoggerFactory : ILoggerFactory - { - private readonly Microsoft.Extensions.Logging.ILoggerFactory _loggerFactory; - - public BackendFxToMicrosoftLoggingLoggerFactory(Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) - { - _loggerFactory = loggerFactory; - } - - public ILogger Create(string s) - { - return new BackendFxToMicrosoftLoggingLogger(_loggerFactory.CreateLogger(s)); - } - - public ILogger Create(Type t) - { - return new BackendFxToMicrosoftLoggingLogger(_loggerFactory.CreateLogger(t.FullName)); - } - - public ILogger Create() - { - return new BackendFxToMicrosoftLoggingLogger(_loggerFactory.CreateLogger(typeof(T).FullName)); - } - - public void BeginActivity(int activityIndex) - { - - } - - public void Shutdown() - { - - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Logging/Obsolete/DebugLogger.cs b/src/abstractions/Backend.Fx/Logging/Obsolete/DebugLogger.cs deleted file mode 100644 index 754213fa..00000000 --- a/src/abstractions/Backend.Fx/Logging/Obsolete/DebugLogger.cs +++ /dev/null @@ -1,174 +0,0 @@ -using System; -using System.Diagnostics; - -namespace Backend.Fx.Logging -{ - [Obsolete] - [DebuggerStepThrough] - public class DebugLogger : ILogger - { - private readonly string _type; - - public DebugLogger(string type) - { - _type = type; - } - - public Exception Fatal(Exception exception) - { - PrintToDebug(exception); - return exception; - } - - public void Fatal(string format, params object[] args) - { - PrintToDebug(format, args); - } - - public void Error(string format, params object[] args) - { - PrintToDebug(format, args); - } - - public void Warn(string format, params object[] args) - { - PrintToDebug(format, args); - } - - public IDisposable InfoDuration(string activity) - { - return new DurationLogger(s => System.Diagnostics.Debug.WriteLine(s), activity); - } - - public IDisposable InfoDuration(string beginMessage, string endMessage) - { - return new DurationLogger(s => System.Diagnostics.Debug.WriteLine(s), beginMessage, endMessage); - } - - public void Info(string format, params object[] args) - { - PrintToDebug(format, args); - } - - public bool IsDebugEnabled() - { - return true; - } - - public IDisposable DebugDuration(string activity) - { - return new DurationLogger(s => System.Diagnostics.Debug.WriteLine(s), activity); - } - - public IDisposable DebugDuration(string beginMessage, string endMessage) - { - return new DurationLogger(s => System.Diagnostics.Debug.WriteLine(s), beginMessage, endMessage); - } - - public void Debug(string format, params object[] args) - { - PrintToDebug(format, args); - } - - public bool IsTraceEnabled() - { - return true; - } - - public IDisposable TraceDuration(string activity) - { - return new DurationLogger(s => System.Diagnostics.Debug.WriteLine(s), activity); - } - - public IDisposable TraceDuration(string beginMessage, string endMessage) - { - return new DurationLogger(s => System.Diagnostics.Debug.WriteLine(s), beginMessage, endMessage); - } - - public void Trace(string format, params object[] args) - { - PrintToDebug(format, args); - } - - public Exception Trace(Exception exception, string format, params object[] args) - { - PrintToDebug(exception); - PrintToDebug(format, args); - return exception; - } - - public Exception Trace(Exception exception) - { - PrintToDebug(exception); - return exception; - } - - public Exception Debug(Exception exception, string format, params object[] args) - { - PrintToDebug(format, args); - return exception; - } - - public Exception Debug(Exception exception) - { - PrintToDebug(exception); - return exception; - } - - public Exception Info(Exception exception, string format, params object[] args) - { - PrintToDebug(exception); - PrintToDebug(format, args); - return exception; - } - - public Exception Info(Exception exception) - { - PrintToDebug(exception); - return exception; - } - - public Exception Warn(Exception exception, string format, params object[] args) - { - PrintToDebug(exception); - PrintToDebug(format, args); - return exception; - } - - public Exception Warn(Exception exception) - { - PrintToDebug(exception); - return exception; - } - - public Exception Error(Exception exception, string format, params object[] args) - { - PrintToDebug(exception); - PrintToDebug(format, args); - return exception; - } - - public Exception Error(Exception exception) - { - PrintToDebug(exception); - return exception; - } - - public Exception Fatal(Exception exception, string format, params object[] args) - { - PrintToDebug(exception); - PrintToDebug(format, args); - return exception; - } - - private void PrintToDebug(Exception ex) - { - System.Diagnostics.Debug.WriteLine($"{_type} {ex}"); - } - - private void PrintToDebug(string format, params object[] args) - { - System.Diagnostics.Debug.WriteLine(_type + format, args); - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Logging/Obsolete/DebugLoggerFactory.cs b/src/abstractions/Backend.Fx/Logging/Obsolete/DebugLoggerFactory.cs deleted file mode 100644 index a53d24be..00000000 --- a/src/abstractions/Backend.Fx/Logging/Obsolete/DebugLoggerFactory.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Diagnostics; - -namespace Backend.Fx.Logging -{ - [DebuggerStepThrough] - [Obsolete] - public class DebugLoggerFactory : ILoggerFactory - { - public ILogger Create(string s) - { - return new DebugLogger(s); - } - - public ILogger Create(Type t) - { - string s = t.FullName; - var indexOf = s?.IndexOf('[') ?? 0; - if (indexOf > 0) - { - s = s?.Substring(0, indexOf); - } - - return Create(s); - } - - public ILogger Create() - { - return Create(typeof(T)); - } - - public void BeginActivity(int activityIndex) - { - Debug.WriteLine($"Beginning activity {activityIndex}"); - } - - public void Shutdown() - { - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Logging/Obsolete/ILogger.cs b/src/abstractions/Backend.Fx/Logging/Obsolete/ILogger.cs deleted file mode 100644 index 489076e8..00000000 --- a/src/abstractions/Backend.Fx/Logging/Obsolete/ILogger.cs +++ /dev/null @@ -1,98 +0,0 @@ -// ReSharper disable UnusedMethodReturnValue.Global -using System; -using JetBrains.Annotations; - -namespace Backend.Fx.Logging -{ - [Obsolete] - public interface ILogger - { - #region fatal - - Exception Fatal(Exception exception); - - [StringFormatMethod("format")] - void Fatal(string format, params object[] args); - - [StringFormatMethod("format")] - Exception Fatal(Exception exception, string format, params object[] args); - - #endregion - - #region error - - Exception Error(Exception exception); - - [StringFormatMethod("format")] - void Error(string format, params object[] args); - - [StringFormatMethod("format")] - Exception Error(Exception exception, string format, params object[] args); - - #endregion - - #region warn - - Exception Warn(Exception exception); - - [StringFormatMethod("format")] - void Warn(string format, params object[] args); - - [StringFormatMethod("format")] - Exception Warn(Exception exception, string format, params object[] args); - - #endregion - - #region info - - Exception Info(Exception exception); - - IDisposable InfoDuration(string activity); - - IDisposable InfoDuration(string beginMessage, string endMessage); - - [StringFormatMethod("format")] - void Info(string format, params object[] args); - - [StringFormatMethod("format")] - Exception Info(Exception exception, string format, params object[] args); - - #endregion - - #region debug - - bool IsDebugEnabled(); - - Exception Debug(Exception exception); - - IDisposable DebugDuration(string activity); - - IDisposable DebugDuration(string beginMessage, string endMessage); - - [StringFormatMethod("format")] - void Debug(string format, params object[] args); - - [StringFormatMethod("format")] - Exception Debug(Exception exception, string format, params object[] args); - - #endregion - - #region Trace - - bool IsTraceEnabled(); - - Exception Trace(Exception exception); - - IDisposable TraceDuration(string activity); - - IDisposable TraceDuration(string beginMessage, string endMessage); - - [StringFormatMethod("format")] - void Trace(string format, params object[] args); - - [StringFormatMethod("format")] - Exception Trace(Exception exception, string format, params object[] args); - - #endregion - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Logging/Obsolete/ILoggerFactory.cs b/src/abstractions/Backend.Fx/Logging/Obsolete/ILoggerFactory.cs deleted file mode 100644 index 18179a4f..00000000 --- a/src/abstractions/Backend.Fx/Logging/Obsolete/ILoggerFactory.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Backend.Fx.Logging -{ - [Obsolete] - public interface ILoggerFactory - { - ILogger Create(string s); - ILogger Create(Type t); - ILogger Create(); - - void BeginActivity(int activityIndex); - void Shutdown(); - } -} diff --git a/src/abstractions/Backend.Fx/Logging/Obsolete/LegacyExceptionLogger.cs b/src/abstractions/Backend.Fx/Logging/Obsolete/LegacyExceptionLogger.cs deleted file mode 100644 index 964dff0a..00000000 --- a/src/abstractions/Backend.Fx/Logging/Obsolete/LegacyExceptionLogger.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using Backend.Fx.Exceptions; - -namespace Backend.Fx.Logging -{ - [Obsolete] - public class LegacyExceptionLogger : IExceptionLogger - { - private readonly ILogger _logger; - - public LegacyExceptionLogger(ILogger logger) - { - _logger = logger; - } - - public void LogException(Exception exception) - { - if (exception is ClientException cex) - { - _logger.Warn(cex, "Client Exception"); - } - else - { - _logger.Error(exception, "Server Exception"); - } - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Logging/Obsolete/LogManager.cs b/src/abstractions/Backend.Fx/Logging/Obsolete/LogManager.cs deleted file mode 100644 index 22426643..00000000 --- a/src/abstractions/Backend.Fx/Logging/Obsolete/LogManager.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Threading; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Backend.Fx.Logging -{ - [Obsolete] - public static class LogManager - { - private static ILoggerFactory _loggerFactory = new BackendFxToMicrosoftLoggingLoggerFactory(new NullLoggerFactory()); - private static int _activityIndex; - - public static void Initialize(ILoggerFactory loggerFactory) - { - _loggerFactory = loggerFactory; - } - - public static ILogger Create() - { - return _loggerFactory.Create(typeof(T).FullName); - } - - public static ILogger Create(Type t) - { - return _loggerFactory.Create(t.FullName); - } - - public static ILogger Create(string category) - { - return _loggerFactory.Create(category); - } - - public static void BeginActivity() - { - Interlocked.Increment(ref _activityIndex); - _loggerFactory.BeginActivity(_activityIndex); - } - - public static void Shutdown() - { - _loggerFactory.Shutdown(); - } - } -} diff --git a/src/abstractions/Backend.Fx/Logging/Obsolete/LoggerExtensions.cs b/src/abstractions/Backend.Fx/Logging/Obsolete/LoggerExtensions.cs deleted file mode 100644 index 131c3235..00000000 --- a/src/abstractions/Backend.Fx/Logging/Obsolete/LoggerExtensions.cs +++ /dev/null @@ -1,158 +0,0 @@ -using System; -using JetBrains.Annotations; -using Microsoft.Extensions.Logging; - -namespace Backend.Fx.Logging -{ - [Obsolete] - public static class LoggerExtensions - { - public static Exception Fatal(this Microsoft.Extensions.Logging.ILogger logger, Exception exception) - { - logger.LogCritical(exception, "Exception: {Message}", exception.Message); - return exception; - } - - public static void Fatal(this Microsoft.Extensions.Logging.ILogger logger, [StructuredMessageTemplate] string format, params object[] args) - { - logger.LogCritical(format, args); - } - - public static Exception Fatal(this Microsoft.Extensions.Logging.ILogger logger, Exception exception, [StructuredMessageTemplate] string format, params object[] args) - { - logger.LogCritical(exception, format, args); - return exception; - } - - - - public static Exception Error(this Microsoft.Extensions.Logging.ILogger logger, Exception exception) - { - logger.LogError(exception, "Exception: {Message}", exception.Message); - return exception; - } - - public static void Error(this Microsoft.Extensions.Logging.ILogger logger, [StructuredMessageTemplate] string format, params object[] args) - { - logger.LogError(format, args); - } - - public static Exception Error(this Microsoft.Extensions.Logging.ILogger logger, Exception exception, [StructuredMessageTemplate] string format, params object[] args) - { - logger.LogError(exception, format, args); - return exception; - } - - - public static Exception Warn(this Microsoft.Extensions.Logging.ILogger logger, Exception exception) - { - logger.LogWarning(exception, "Exception: {Message}", exception.Message); - return exception; - } - - public static void Warn(this Microsoft.Extensions.Logging.ILogger logger, [StructuredMessageTemplate] string format, params object[] args) - { - logger.LogWarning(format, args); - } - - public static Exception Warn(this Microsoft.Extensions.Logging.ILogger logger, Exception exception, [StructuredMessageTemplate] string format, params object[] args) - { - logger.LogWarning(exception, format, args); - return exception; - } - - - public static Exception Info(this Microsoft.Extensions.Logging.ILogger logger, Exception exception) - { - logger.LogInformation(exception, "Exception: {Message}", exception.Message); - return exception; - } - - public static IDisposable InfoDuration(this Microsoft.Extensions.Logging.ILogger logger, string activity) - { - return new DurationLogger(s => logger.LogInformation(s), activity); - } - - public static IDisposable InfoDuration(this Microsoft.Extensions.Logging.ILogger logger, string beginMessage, string endMessage) - { - return new DurationLogger(s => logger.LogInformation(s), beginMessage, endMessage); - } - - public static void Info(this Microsoft.Extensions.Logging.ILogger logger, [StructuredMessageTemplate] string format, params object[] args) - { - logger.LogInformation(format, args); - } - - public static Exception Info(this Microsoft.Extensions.Logging.ILogger logger, Exception exception, [StructuredMessageTemplate] string format, params object[] args) - { - logger.LogInformation(exception, format, args); - return exception; - } - - public static bool IsDebugEnabled(this Microsoft.Extensions.Logging.ILogger logger) - { - return logger.IsEnabled(LogLevel.Debug); - } - - - public static Exception Debug(this Microsoft.Extensions.Logging.ILogger logger, Exception exception) - { - logger.LogDebug(exception, "Exception: {Message}", exception.Message); - return exception; - } - - public static IDisposable DebugDuration(this Microsoft.Extensions.Logging.ILogger logger, string activity) - { - return new DurationLogger(s => logger.LogDebug(s), activity); - } - - public static IDisposable DebugDuration(this Microsoft.Extensions.Logging.ILogger logger, string beginMessage, string endMessage) - { - return new DurationLogger(s => logger.LogDebug(s), beginMessage, endMessage); - } - - public static void Debug(this Microsoft.Extensions.Logging.ILogger logger, [StructuredMessageTemplate] string format, params object[] args) - { - logger.LogDebug(format, args); - } - - public static Exception Debug(this Microsoft.Extensions.Logging.ILogger logger, Exception exception, [StructuredMessageTemplate] string format, params object[] args) - { - logger.LogDebug(exception, format, args); - return exception; - } - - public static bool IsTraceEnabled(this Microsoft.Extensions.Logging.ILogger logger) - { - return logger.IsEnabled(LogLevel.Trace); - } - - - public static Exception Trace(this Microsoft.Extensions.Logging.ILogger logger, Exception exception) - { - logger.LogTrace(exception, "Exception: {Message}", exception.Message); - return exception; - } - - public static IDisposable TraceDuration(this Microsoft.Extensions.Logging.ILogger logger, string activity) - { - return new DurationLogger(s => logger.LogTrace(s), activity); - } - - public static IDisposable TraceDuration(this Microsoft.Extensions.Logging.ILogger logger, string beginMessage, string endMessage) - { - return new DurationLogger(s => logger.LogTrace(s), beginMessage, endMessage); - } - - public static void Trace(this Microsoft.Extensions.Logging.ILogger logger, [StructuredMessageTemplate] string format, params object[] args) - { - logger.LogTrace(format, args); - } - - public static Exception Trace(this Microsoft.Extensions.Logging.ILogger logger, Exception exception, [StructuredMessageTemplate] string format, params object[] args) - { - logger.LogTrace(exception, format, args); - return exception; - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/MultiTenancyBackendFxApplication.cs b/src/abstractions/Backend.Fx/MultiTenancyBackendFxApplication.cs new file mode 100644 index 00000000..665bfaeb --- /dev/null +++ b/src/abstractions/Backend.Fx/MultiTenancyBackendFxApplication.cs @@ -0,0 +1,46 @@ +using System.Reflection; +using Backend.Fx.DependencyInjection; +using Backend.Fx.Features; +using Backend.Fx.Features.MultiTenancy; +using Backend.Fx.Features.MultiTenancy.InProc; +using Backend.Fx.Logging; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx +{ + [PublicAPI] + public class MultiTenancyBackendFxApplication : BackendFxApplication where TCurrentTenantIdSelector : class, ICurrentTenantIdSelector + { + private readonly ILogger _logger = Log.Create>(); + + public MultiTenancyBackendFxApplication( + ICompositionRoot compositionRoot, + IExceptionLogger exceptionLogger, + ITenantEnumerator tenantEnumerator, + params Assembly[] assemblies) + : this(compositionRoot, exceptionLogger, tenantEnumerator, new InProcTenantWideMutexManager(), assemblies) + { + } + + public MultiTenancyBackendFxApplication( + ICompositionRoot compositionRoot, + IExceptionLogger exceptionLogger, + ITenantEnumerator tenantEnumerator, + ITenantWideMutexManager tenantWideMutexManager, + params Assembly[] assemblies) + : base(compositionRoot, exceptionLogger, assemblies) + { + CompositionRoot.RegisterModules(new MultiTenancyModule(tenantEnumerator, tenantWideMutexManager)); + } + + public override void EnableFeature(Feature feature) + { + base.EnableFeature(feature); + if (feature is IMultiTenancyFeature multiTenancyFeature) + { + multiTenancyFeature.EnableMultiTenancyServices(this); + } + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/Authorization/AggregateAuthorization.cs b/src/abstractions/Backend.Fx/Patterns/Authorization/AggregateAuthorization.cs deleted file mode 100644 index a5045f44..00000000 --- a/src/abstractions/Backend.Fx/Patterns/Authorization/AggregateAuthorization.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Linq; -using System.Linq.Expressions; -using Backend.Fx.BuildingBlocks; -using Backend.Fx.Logging; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.Patterns.Authorization -{ - public abstract class AggregateAuthorization : IAggregateAuthorization where TAggregateRoot : AggregateRoot - { - private static readonly ILogger Logger = Log.Create>(); - - /// > - public abstract Expression> HasAccessExpression { get; } - - /// > - public virtual IQueryable Filter(IQueryable queryable) - { - return queryable.Where(HasAccessExpression); - } - - /// > - public abstract bool CanCreate(TAggregateRoot t); - - /// - /// Implement a guard that might disallow modifying an existing aggregate. - /// This overload is called directly before saving modification of an instance, so that you can use the instance's state for deciding. - /// This default implementation forwards to - /// - public virtual bool CanModify(TAggregateRoot t) - { - var canCreate = CanCreate(t); - Logger.LogTrace("CanCreate({AggregateRootTypeName}): {CanCreate}", t.DebuggerDisplay, canCreate); - return canCreate; - } - - /// > - public virtual bool CanDelete(TAggregateRoot t) - { - var canModify = CanModify(t); - Logger.LogTrace("CanModify({AggregateRootTypeName}): {CanCreate}", t.DebuggerDisplay, canModify); - return canModify; - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DataGeneration/DataGenerated.cs b/src/abstractions/Backend.Fx/Patterns/DataGeneration/DataGenerated.cs deleted file mode 100644 index bc778c67..00000000 --- a/src/abstractions/Backend.Fx/Patterns/DataGeneration/DataGenerated.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Backend.Fx.Patterns.EventAggregation.Integration; - -namespace Backend.Fx.Patterns.DataGeneration -{ - /// - /// Will appear on the message bus when the data generation process has been completed - /// - public class DataGenerated : IntegrationEvent - { - public DataGenerated(int tenantId) : base() - { - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DataGeneration/DataGeneratingApplication.cs b/src/abstractions/Backend.Fx/Patterns/DataGeneration/DataGeneratingApplication.cs deleted file mode 100644 index 41fef787..00000000 --- a/src/abstractions/Backend.Fx/Patterns/DataGeneration/DataGeneratingApplication.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.Patterns.EventAggregation.Integration; -using JetBrains.Annotations; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.Patterns.DataGeneration -{ - /// - /// Enriches the by calling all data generators for all tenants - /// on application start and when a tenant gets activated - /// - public class DataGeneratingApplication : IBackendFxApplication - { - private static readonly ILogger Logger = Log.Create(); - - private readonly ITenantIdProvider _tenantIdProvider; - private readonly IBackendFxApplication _application; - private readonly IModule _dataGenerationModule; - - public IDataGenerationContext DataGenerationContext { get; [UsedImplicitly] private set; } - - /// To be able to query all active demo/production tenants - /// To register the collection of IDataGenerator with the composition root. Internally, IInstanceProvider.GetInstances<IDataGenerator>() is being used - /// to make sure data generation will never run in parallel for the same tenant - /// the decorated instance - public DataGeneratingApplication( - ITenantIdProvider tenantIdProvider, - IModule dataGenerationModule, - ITenantWideMutexManager tenantWideMutexManager, - IBackendFxApplication application) - { - _tenantIdProvider = tenantIdProvider; - _application = application; - _dataGenerationModule = dataGenerationModule; - DataGenerationContext = new DataGenerationContext( - _application.CompositionRoot, - _application.Invoker, - tenantWideMutexManager); - } - - public void Dispose() - { - _application.Dispose(); - } - - public IBackendFxApplicationAsyncInvoker AsyncInvoker => _application.AsyncInvoker; - public ICompositionRoot CompositionRoot => _application.CompositionRoot; - public IBackendFxApplicationInvoker Invoker => _application.Invoker; - - public IMessageBus MessageBus => _application.MessageBus; - - public bool WaitForBoot(int timeoutMilliSeconds = Int32.MaxValue, CancellationToken cancellationToken = default) - { - return _application.WaitForBoot(timeoutMilliSeconds, cancellationToken); - } - - public Task Boot(CancellationToken cancellationToken = default) => BootAsync(cancellationToken); - - public async Task BootAsync(CancellationToken cancellationToken = default) - { - _application.CompositionRoot.RegisterModules(_dataGenerationModule); - EnableDataGenerationForNewTenants(); - - await _application.BootAsync(cancellationToken); - - SeedDataForAllActiveTenants(); - } - - private void SeedDataForAllActiveTenants() - { - using (Logger.LogInformationDuration("Seeding data")) - { - var prodTenantIds = _tenantIdProvider.GetActiveProductionTenantIds(); - foreach (TenantId prodTenantId in prodTenantIds) - { - DataGenerationContext.SeedDataForTenant(prodTenantId, false); - _application.MessageBus.Publish(new DataGenerated(prodTenantId.Value)); - } - - var demoTenantIds = _tenantIdProvider.GetActiveDemonstrationTenantIds(); - foreach (TenantId demoTenantId in demoTenantIds) - { - DataGenerationContext.SeedDataForTenant(demoTenantId, true); - _application.MessageBus.Publish(new DataGenerated(demoTenantId.Value)); - } - } - } - - private void EnableDataGenerationForNewTenants() - { - _application.MessageBus.Subscribe(new DelegateIntegrationMessageHandler(tenantCreated => - { - Logger.LogInformation( - "Seeding data for recently activated tenant (with demo data: {IsDemoTenant}) {TenantId}", - tenantCreated.IsDemoTenant, - tenantCreated.TenantId); - try - { - DataGenerationContext.SeedDataForTenant(new TenantId(tenantCreated.TenantId), - tenantCreated.IsDemoTenant); - } - catch (Exception ex) - { - Logger.LogError(ex, - "Seeding data for recently activated tenant (with demo data: {IsDemoTenant}) {TenantId} failed", - tenantCreated.IsDemoTenant, - tenantCreated.TenantId); - } - })); - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DataGeneration/DataGenerationContext.cs b/src/abstractions/Backend.Fx/Patterns/DataGeneration/DataGenerationContext.cs deleted file mode 100644 index 0d5035ae..00000000 --- a/src/abstractions/Backend.Fx/Patterns/DataGeneration/DataGenerationContext.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Linq; -using Backend.Fx.Environment.Authentication; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.Patterns.DataGeneration -{ - public interface IDataGenerationContext - { - void SeedDataForTenant(TenantId tenantId, bool isDemoTenant); - } - - public class DataGenerationContext : IDataGenerationContext - { - private static readonly ILogger Logger = Log.Create(); - - private readonly ICompositionRoot _compositionRoot; - private readonly IBackendFxApplicationInvoker _invoker; - private readonly ITenantWideMutexManager _mutexManager; - - public DataGenerationContext( - ICompositionRoot compositionRoot, - IBackendFxApplicationInvoker invoker, - ITenantWideMutexManager mutexManager) - { - _compositionRoot = compositionRoot; - _invoker = invoker; - _mutexManager = mutexManager; - } - - public void SeedDataForTenant(TenantId tenantId, bool isDemoTenant) - { - if (!_mutexManager.TryAcquire(tenantId, GetType().Name, out var mutex)) return; - - using (mutex) - { - using (Logger.LogInformationDuration($"Seeding data for tenant {tenantId.Value}")) - { - Type[] dataGeneratorTypesToRun = GetDataGeneratorTypes(_compositionRoot, isDemoTenant); - foreach (Type dataGeneratorTypeToRun in dataGeneratorTypesToRun) - { - _invoker.Invoke(instanceProvider => - { - IDataGenerator dataGenerator = instanceProvider - .GetInstances() - .Single(dg => dg.GetType() == dataGeneratorTypeToRun); - dataGenerator.Generate(); - }, new SystemIdentity(), tenantId); - } - } - } - } - - private static Type[] GetDataGeneratorTypes(ICompositionRoot compositionRoot, bool includeDemoDataGenerators) - { - using (IInjectionScope scope = compositionRoot.BeginScope()) - { - var dataGenerators = scope - .InstanceProvider - .GetInstances() - .OrderBy(dg => dg.Priority) - .Select(dg => dg.GetType()); - - if (!includeDemoDataGenerators) - { - dataGenerators = dataGenerators.Where(dg => !typeof(IDemoDataGenerator).IsAssignableFrom(dg)); - } - - return dataGenerators.ToArray(); - } - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/BackendFxApplication.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/BackendFxApplication.cs deleted file mode 100644 index 22a2de4c..00000000 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/BackendFxApplication.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System; -using System.Security.Principal; -using System.Threading; -using System.Threading.Tasks; -using Backend.Fx.Environment.Authentication; -using Backend.Fx.Environment.DateAndTime; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.EventAggregation.Domain; -using Backend.Fx.Patterns.EventAggregation.Integration; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.Patterns.DependencyInjection -{ - /// - /// The root object of the whole backend fx application framework - /// - public interface IBackendFxApplication : IDisposable - { - /// - /// The async invoker runs a given action asynchronously in an application scope with injection facilities - /// - IBackendFxApplicationAsyncInvoker AsyncInvoker { get; } - - /// - /// The composition root of the dependency injection framework - /// - ICompositionRoot CompositionRoot { get; } - - /// - /// The invoker runs a given action in an application scope with injection facilities - /// - IBackendFxApplicationInvoker Invoker { get; } - - /// - /// The message bus to send and receive event messages - /// - IMessageBus MessageBus { get; } - - /// - /// allows synchronously awaiting application startup - /// - bool WaitForBoot(int timeoutMilliSeconds = int.MaxValue, CancellationToken cancellationToken = default); - - [Obsolete("Use BootAsync()")] - Task Boot(CancellationToken cancellationToken = default); - - /// - /// Initializes and starts the application (async) - /// - /// - Task BootAsync(CancellationToken cancellationToken = default); - } - - - public class BackendFxApplication : IBackendFxApplication - { - private static readonly ILogger Logger = Log.Create(); - private readonly ManualResetEventSlim _isBooted = new ManualResetEventSlim(false); - - /// - /// Initializes the application's runtime instance - /// - /// The composition root of the dependency injection framework - /// The message bus implementation used by this application instance - /// - public BackendFxApplication(ICompositionRoot compositionRoot, IMessageBus messageBus, IExceptionLogger exceptionLogger) - { - var invoker = new BackendFxApplicationInvoker(compositionRoot); - AsyncInvoker = new ExceptionLoggingAsyncInvoker(exceptionLogger, invoker); - Invoker = new ExceptionLoggingInvoker(exceptionLogger, invoker); - MessageBus = messageBus; - MessageBus.ProvideInvoker(new SequentializingBackendFxApplicationInvoker( - new WaitForBootInvoker(this, - new ExceptionLoggingAndHandlingInvoker(exceptionLogger, Invoker)))); - CompositionRoot = compositionRoot; - CompositionRoot.InfrastructureModule.RegisterScoped(); - CompositionRoot.InfrastructureModule.RegisterScoped, CurrentCorrelationHolder>(); - CompositionRoot.InfrastructureModule.RegisterScoped, CurrentIdentityHolder>(); - CompositionRoot.InfrastructureModule.RegisterScoped, CurrentTenantIdHolder>(); - CompositionRoot.InfrastructureModule.RegisterScoped(); - CompositionRoot.InfrastructureModule.RegisterScoped(() => new DomainEventAggregator(compositionRoot)); - CompositionRoot.InfrastructureModule.RegisterScoped( - () => new MessageBusScope( - MessageBus, - compositionRoot.InstanceProvider.GetInstance>(), - compositionRoot.InstanceProvider.GetInstance>())); - } - - public IBackendFxApplicationAsyncInvoker AsyncInvoker { get; } - - public ICompositionRoot CompositionRoot { get; } - - public IBackendFxApplicationInvoker Invoker { get; } - - public IMessageBus MessageBus { get; } - - public Task Boot(CancellationToken cancellationToken = default) => BootAsync(cancellationToken); - - public Task BootAsync(CancellationToken cancellationToken = default) - { - Logger.LogInformation("Booting application"); - CompositionRoot.Verify(); - MessageBus.Connect(); - _isBooted.Set(); - return Task.CompletedTask; - } - - public bool WaitForBoot(int timeoutMilliSeconds = int.MaxValue, CancellationToken cancellationToken = default) - { - return _isBooted.Wait(timeoutMilliSeconds, cancellationToken); - } - - protected void Dispose(bool disposing) - { - if (disposing) - { - Logger.LogInformation("Application shut down initialized"); - CompositionRoot?.Dispose(); - } - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/BackendFxApplicationInvoker.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/BackendFxApplicationInvoker.cs deleted file mode 100644 index bdb09522..00000000 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/BackendFxApplicationInvoker.cs +++ /dev/null @@ -1,128 +0,0 @@ -using System; -using System.Security.Principal; -using System.Threading.Tasks; -using Backend.Fx.Environment.Authentication; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Extensions; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.EventAggregation.Domain; -using Backend.Fx.Patterns.EventAggregation.Integration; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.Patterns.DependencyInjection -{ - public interface IBackendFxApplicationAsyncInvoker - { - /// The async action to be invoked by the application - /// The acting identity - /// The targeted tenant id - /// The correlation id, when it was continued - Task InvokeAsync(Func awaitableAsyncAction, IIdentity identity, TenantId tenantId, Guid? correlationId = null); - } - - - public interface IBackendFxApplicationInvoker - { - /// The action to be invoked by the application - /// The acting identity - /// The targeted tenant id - /// The correlation id, when it was continued - void Invoke(Action action, IIdentity identity, TenantId tenantId, Guid? correlationId = null); - } - - - public class BackendFxApplicationInvoker : IBackendFxApplicationInvoker, IBackendFxApplicationAsyncInvoker - { - private readonly ICompositionRoot _compositionRoot; - private static readonly ILogger Logger = Log.Create(); - - public BackendFxApplicationInvoker(ICompositionRoot compositionRoot) - { - _compositionRoot = compositionRoot; - } - - - public void Invoke(Action action, IIdentity identity, TenantId tenantId, Guid? correlationId = null) - { - Logger.LogInformation("Invoking synchronous action as {Identity} in {TenantId}", identity, tenantId); - using (IInjectionScope injectionScope = BeginScope(identity, tenantId, correlationId)) - { - using (UseDurationLogger(injectionScope)) - { - var operation = injectionScope.InstanceProvider.GetInstance(); - try - { - operation.Begin(); - action.Invoke(injectionScope.InstanceProvider); - injectionScope.InstanceProvider.GetInstance().RaiseEvents(); - operation.Complete(); - } - catch - { - operation.Cancel(); - throw; - } - - var messageBusScope = injectionScope.InstanceProvider.GetInstance(); - AsyncHelper.RunSync(() => messageBusScope.RaiseEvents()); - } - } - } - - public async Task InvokeAsync(Func awaitableAsyncAction, IIdentity identity, TenantId tenantId, Guid? correlationId = null) - { - Logger.LogInformation("Invoking asynchronous action as {Identity} in {TenantId}", identity, tenantId); - using (IInjectionScope injectionScope = BeginScope(identity, tenantId, correlationId)) - { - using (UseDurationLogger(injectionScope)) - { - var operation = injectionScope.InstanceProvider.GetInstance(); - try - { - operation.Begin(); - await awaitableAsyncAction.Invoke(injectionScope.InstanceProvider); - injectionScope.InstanceProvider.GetInstance().RaiseEvents(); - operation.Complete(); - } - catch - { - operation.Cancel(); - throw; - } - - await injectionScope.InstanceProvider.GetInstance().RaiseEvents(); - } - } - } - - - private IInjectionScope BeginScope(IIdentity identity, TenantId tenantId, Guid? correlationId) - { - IInjectionScope injectionScope = _compositionRoot.BeginScope(); - tenantId = tenantId ?? new TenantId(null); - injectionScope.InstanceProvider.GetInstance>().ReplaceCurrent(tenantId); - - identity = identity ?? new AnonymousIdentity(); - injectionScope.InstanceProvider.GetInstance>().ReplaceCurrent(identity); - - if (correlationId.HasValue) - { - injectionScope.InstanceProvider.GetInstance>().Current.Resume(correlationId.Value); - } - - return injectionScope; - } - - - private static IDisposable UseDurationLogger(IInjectionScope injectionScope) - { - IIdentity identity = injectionScope.InstanceProvider.GetInstance>().Current; - TenantId tenantId = injectionScope.InstanceProvider.GetInstance>().Current; - Correlation correlation = injectionScope.InstanceProvider.GetInstance>().Current; - return Logger.LogInformationDuration( - $"Starting scope {injectionScope.SequenceNumber} (correlation [{correlation.Id}]) for {identity.Name} in tenant {(tenantId.HasValue ? tenantId.Value.ToString() : "null")}", - $"Ended scope {injectionScope.SequenceNumber} (correlation [{correlation.Id}]) for {identity.Name} in tenant {(tenantId.HasValue ? tenantId.Value.ToString() : "null")}"); - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ExceptionLoggingAsyncInvoker.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ExceptionLoggingAsyncInvoker.cs deleted file mode 100644 index f87ede34..00000000 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ExceptionLoggingAsyncInvoker.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Security.Principal; -using System.Threading.Tasks; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Logging; - -namespace Backend.Fx.Patterns.DependencyInjection -{ - public class ExceptionLoggingAsyncInvoker : IBackendFxApplicationAsyncInvoker - { - private readonly IExceptionLogger _exceptionLogger; - private readonly IBackendFxApplicationAsyncInvoker _invoker; - - public ExceptionLoggingAsyncInvoker(IExceptionLogger exceptionLogger, IBackendFxApplicationAsyncInvoker invoker) - { - _exceptionLogger = exceptionLogger; - _invoker = invoker; - } - - public async Task InvokeAsync(Func awaitableAsyncAction, IIdentity identity, TenantId tenantId, Guid? correlationId = null) - { - try - { - await _invoker.InvokeAsync(awaitableAsyncAction, identity, tenantId, correlationId); - } - catch (Exception ex) - { - _exceptionLogger.LogException(ex); - throw; - } - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ExceptionLoggingInvoker.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ExceptionLoggingInvoker.cs deleted file mode 100644 index 04097ea4..00000000 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ExceptionLoggingInvoker.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Security.Principal; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Logging; - -namespace Backend.Fx.Patterns.DependencyInjection -{ - public class ExceptionLoggingInvoker : IBackendFxApplicationInvoker - { - private readonly IExceptionLogger _exceptionLogger; - private readonly IBackendFxApplicationInvoker _invoker; - - public ExceptionLoggingInvoker(IExceptionLogger exceptionLogger, IBackendFxApplicationInvoker invoker) - { - _exceptionLogger = exceptionLogger; - _invoker = invoker; - } - - public void Invoke(Action action, IIdentity identity, TenantId tenantId, Guid? correlationId = null) - { - try - { - _invoker.Invoke(action, identity, tenantId, correlationId); - } - catch (Exception ex) - { - _exceptionLogger.LogException(ex); - throw; - } - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ICompositionRoot.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ICompositionRoot.cs deleted file mode 100644 index 2dcfa214..00000000 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ICompositionRoot.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using Backend.Fx.Patterns.EventAggregation.Domain; - -namespace Backend.Fx.Patterns.DependencyInjection -{ - /// - /// Encapsulates the injection framework of choice. The implementation follows the Register/Resolve/Release pattern. - /// Usage of this interface is only allowed for framework integration (or tests). NEVER (!) access the injector from - /// the domain or application logic, this would result in the Service Locator anti pattern, described here: - /// http://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/ - /// - public interface ICompositionRoot : IDisposable, IDomainEventHandlerProvider - { - void Verify(); - - void RegisterModules(params IModule[] modules); - - IInjectionScope BeginScope(); - - /// - /// Access to the container's resolution functionality - /// - IInstanceProvider InstanceProvider { get; } - - /// - /// Access to the container's configuration functionality - /// - IInfrastructureModule InfrastructureModule { get; } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/IInstanceProvider.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/IInstanceProvider.cs deleted file mode 100644 index f5af5265..00000000 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/IInstanceProvider.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; - -namespace Backend.Fx.Patterns.DependencyInjection -{ - public interface IInstanceProvider - { - /// - /// Gets a service instance valid for the scope by providing its type - /// - /// - /// - object GetInstance(Type serviceType); - - /// - /// Gets all service instances valid for the scope by providing their type - /// - /// - /// - IEnumerable GetInstances(Type serviceType); - - /// - /// Gets a service instance valid for the scope by providing its type via generic type parameter - /// - /// - /// - T GetInstance() where T : class; - - /// - /// Gets all service instances valid for the scope by providing their type via generic type parameter - /// - /// - /// - IEnumerable GetInstances() where T : class; - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/InfrastructureModule.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/InfrastructureModule.cs deleted file mode 100644 index 8dc57a87..00000000 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/InfrastructureModule.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Reflection; - -namespace Backend.Fx.Patterns.DependencyInjection -{ - public interface IInfrastructureModule - { - void RegisterScoped() - where TImpl : class, TService - where TService : class; - - void RegisterScoped(Func factory) - where TService : class; - - void RegisterScoped(Type serviceType, Type implementationType); - void RegisterScoped(Type serviceType, Assembly[] assembliesToScan); - - void RegisterDecorator() where TService : class where TImpl : class, TService; - - void RegisterSingleton() where TService : class where TImpl : class, TService; - void RegisterInstance(TService instance) where TService : class; - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/InjectionScope.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/InjectionScope.cs deleted file mode 100644 index 0cf52b1c..00000000 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/InjectionScope.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; - -namespace Backend.Fx.Patterns.DependencyInjection -{ - /// - /// During a scope, services by default are singletons. Scopes may exist in parallel, providing totally separate singleton - /// instances for every scope. - /// - public interface IInjectionScope : IDisposable - { - int SequenceNumber { get; } - - IInstanceProvider InstanceProvider { get; } - } - - - public abstract class InjectionScope : IInjectionScope - { - protected InjectionScope(int sequenceNumber) - { - SequenceNumber = sequenceNumber; - } - - public int SequenceNumber { get; } - - public abstract IInstanceProvider InstanceProvider { get; } - - public abstract void Dispose(); - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/Operation.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/Operation.cs deleted file mode 100644 index a1a5b39f..00000000 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/Operation.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using Backend.Fx.Logging; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.Patterns.DependencyInjection -{ - /// - /// The basic interface of an operation invoked by the (or its async counterpart). - /// Decorate this interface to provide operation specific infrastructure services (like a database connection, a database transaction - /// an entry-exit logging etc.) - /// - public interface IOperation - { - void Begin(); - - void Complete(); - - void Cancel(); - } - - public class Operation : IOperation - { - private static readonly ILogger Logger = Log.Create(); - private static int _index; - private readonly int _instanceId = _index++; - private bool? _isActive; - private IDisposable _lifetimeLogger; - - public virtual void Begin() - { - if (_isActive != null) - { - throw new InvalidOperationException($"Cannot begin an operation that is {(_isActive.Value ? "active" : "terminated")}"); - } - - _lifetimeLogger = Logger.LogDebugDuration($"Beginning operation #{_instanceId}", $"Terminating operation #{_instanceId}"); - _isActive = true; - } - - public virtual void Complete() - { - Logger.LogInformation("Completing operation #{OperationId}", _instanceId); - if (_isActive != true) - { - throw new InvalidOperationException($"Cannot begin an operation that is {(_isActive == false ? "terminated" : "not active")}"); - } - - _isActive = false; - _lifetimeLogger?.Dispose(); - _lifetimeLogger = null; - } - - public void Cancel() - { - Logger.LogInformation("Canceling operation #{OperationId}", _instanceId); - if (_isActive != true) - { - throw new InvalidOperationException($"Cannot cancel an operation that is {(_isActive == false ? "terminated" : "not active")}"); - } - _isActive = false; - _lifetimeLogger?.Dispose(); - _lifetimeLogger = null; - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/SequentializingBackendFxApplicationInvoker.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/SequentializingBackendFxApplicationInvoker.cs deleted file mode 100644 index 893aac1b..00000000 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/SequentializingBackendFxApplicationInvoker.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Security.Principal; -using Backend.Fx.Environment.MultiTenancy; - -namespace Backend.Fx.Patterns.DependencyInjection -{ - /// - /// Decorates the to prevent parallel invocation. - /// - public class SequentializingBackendFxApplicationInvoker : IBackendFxApplicationInvoker - { - private readonly object _syncLock = new object(); - private readonly IBackendFxApplicationInvoker _backendFxApplicationInvokerImplementation; - - public SequentializingBackendFxApplicationInvoker(IBackendFxApplicationInvoker backendFxApplicationInvokerImplementation) - { - _backendFxApplicationInvokerImplementation = backendFxApplicationInvokerImplementation; - } - - - public void Invoke(Action action, IIdentity identity, TenantId tenantId, Guid? correlationId = null) - { - lock (_syncLock) - { - _backendFxApplicationInvokerImplementation.Invoke(action, identity, tenantId, correlationId); - } - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/WaitForBootInvoker.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/WaitForBootInvoker.cs deleted file mode 100644 index 3dcecb9f..00000000 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/WaitForBootInvoker.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Security.Principal; -using Backend.Fx.Environment.MultiTenancy; - -namespace Backend.Fx.Patterns.DependencyInjection -{ - public class WaitForBootInvoker : IBackendFxApplicationInvoker - { - private readonly IBackendFxApplication _application; - private readonly IBackendFxApplicationInvoker _invoker; - - public WaitForBootInvoker(IBackendFxApplication application, IBackendFxApplicationInvoker invoker) - { - _application = application; - _invoker = invoker; - } - - public void Invoke(Action action, IIdentity identity, TenantId tenantId, Guid? correlationId = null) - { - _application.WaitForBoot(); - _invoker.Invoke(action, identity, tenantId, correlationId); - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Domain/DomainEventAggregator.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Domain/DomainEventAggregator.cs deleted file mode 100644 index b6b7521b..00000000 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Domain/DomainEventAggregator.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections.Concurrent; -using Backend.Fx.Logging; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.Patterns.EventAggregation.Domain -{ - public class DomainEventAggregator : IDomainEventAggregator - { - private class HandleAction - { - public HandleAction(string domainEventName, string handlerTypeName, Action action) - { - DomainEventName = domainEventName; - HandlerTypeName = handlerTypeName; - Action = action; - } - - public string DomainEventName { get; } - public string HandlerTypeName { get; } - public Action Action { get; } - } - - private static readonly ILogger Logger = Log.Create(); - private readonly IDomainEventHandlerProvider _domainEventHandlerProvider; - private readonly ConcurrentQueue _handleActions = new ConcurrentQueue(); - - public DomainEventAggregator(IDomainEventHandlerProvider domainEventHandlerProvider) - { - _domainEventHandlerProvider = domainEventHandlerProvider; - } - - /// - /// Publish a domain event that is handled by all handlers synchronously in the same scope/transaction. - /// Possible exceptions are not caught, so that your action might fail due to a failing event handler. - /// - /// - /// - public void PublishDomainEvent(TDomainEvent domainEvent) where TDomainEvent : IDomainEvent - { - foreach (var injectedHandler in _domainEventHandlerProvider.GetAllEventHandlers()) - { - var handleAction = new HandleAction( - typeof(TDomainEvent).Name, - injectedHandler.GetType().Name, - () => injectedHandler.Handle(domainEvent)); - - _handleActions.Enqueue(handleAction); - Logger.LogDebug( - "Invocation of {HandlerTypeName} for domain event {DomainEvent} registered. It will be executed on completion of operation", - injectedHandler.GetType().Name, - domainEvent); - } - } - - public void RaiseEvents() - { - while (_handleActions.TryDequeue(out HandleAction handleAction)) - { - try - { - handleAction.Action.Invoke(); - } - catch (Exception ex) - { - Logger.LogError(ex, - "Handling of {DomainEvent} by {HandlerTypeName} failed", - handleAction.DomainEventName, - handleAction.HandlerTypeName); - throw; - } - } - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Domain/IDomainEventHandler.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Domain/IDomainEventHandler.cs deleted file mode 100644 index eb5b4a24..00000000 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Domain/IDomainEventHandler.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Backend.Fx.Patterns.EventAggregation.Domain -{ - public interface IDomainEventHandler where TDomainEvent : IDomainEvent - { - void Handle(TDomainEvent domainEvent); - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Domain/IDomainEventHandlerProvider.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Domain/IDomainEventHandlerProvider.cs deleted file mode 100644 index c500524e..00000000 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Domain/IDomainEventHandlerProvider.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Backend.Fx.Patterns.EventAggregation.Domain -{ - using System.Collections.Generic; - - public interface IDomainEventHandlerProvider - { - /// - /// get all domain event handlers that want to handle a specific domain event - /// - /// - /// - IEnumerable> GetAllEventHandlers() where TDomainEvent : IDomainEvent; - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/DelegateIntegrationMessageHandler.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/DelegateIntegrationMessageHandler.cs deleted file mode 100644 index 043bec4e..00000000 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/DelegateIntegrationMessageHandler.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace Backend.Fx.Patterns.EventAggregation.Integration -{ - public class DelegateIntegrationMessageHandler - : IIntegrationMessageHandler where TIntegrationEvent : IIntegrationEvent - { - private readonly Action _handleAction; - - public DelegateIntegrationMessageHandler(Action handleAction) - { - _handleAction = handleAction; - } - - public void Handle(TIntegrationEvent eventData) - { - _handleAction.Invoke(eventData); - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/DynamicSubscription.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/DynamicSubscription.cs deleted file mode 100644 index c108f4b8..00000000 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/DynamicSubscription.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using Backend.Fx.Extensions; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.Patterns.EventAggregation.Integration -{ - public class DynamicSubscription : ISubscription - { - private static readonly ILogger Logger = Log.Create(); - private readonly Type _handlerType; - - public DynamicSubscription(Type handlerType) - { - _handlerType = handlerType; - } - - public void Process(IInstanceProvider instanceProvider, EventProcessingContext context) - { - Logger.LogInformation("Getting subscribed handler instance of type {HandlerTypeName}", _handlerType.Name); - object handlerInstance = instanceProvider.GetInstance(_handlerType); - using (Logger.LogInformationDuration($"Invoking subscribed handler {_handlerType.GetDetailedTypeName()}")) - { - ((IIntegrationMessageHandler) handlerInstance).Handle(context.DynamicEvent); - } - } - - public bool Matches(object handler) - { - return (Type) handler == _handlerType; - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/EventProcessingContext.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/EventProcessingContext.cs deleted file mode 100644 index eb913e8f..00000000 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/EventProcessingContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using Backend.Fx.Environment.MultiTenancy; - -namespace Backend.Fx.Patterns.EventAggregation.Integration -{ - public abstract class EventProcessingContext - { - public abstract TenantId TenantId { get; } - public abstract dynamic DynamicEvent { get; } - public abstract Guid CorrelationId { get; } - - public abstract IIntegrationEvent GetTypedEvent(Type eventType); - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IIntegrationMessageHandler.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IIntegrationMessageHandler.cs deleted file mode 100644 index 2c2a0e1c..00000000 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IIntegrationMessageHandler.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Backend.Fx.Patterns.EventAggregation.Integration -{ - public interface IIntegrationMessageHandler - { - void Handle(dynamic eventData); - } - - public interface IIntegrationMessageHandler where TEvent : IIntegrationEvent - { - void Handle(TEvent eventData); - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IMessageBus.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IMessageBus.cs deleted file mode 100644 index 0e4be87b..00000000 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IMessageBus.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Threading.Tasks; -using Backend.Fx.Patterns.DependencyInjection; - -namespace Backend.Fx.Patterns.EventAggregation.Integration -{ - public interface IMessageBus : IDisposable - { - /// - /// This instance is used to determine a message name from an integration event. The default implementation just - /// returns event.GetType().Name - /// - IMessageNameProvider MessageNameProvider { get; } - - void Connect(); - - /// - /// Directly publishes an event on the message bus without delay. - /// In most cases you want to publish an event when the cause is considered as safely done, e.g. when the - /// wrapping transaction is committed. Use to let the framework raise all events - /// after completing the operation. - /// - /// - /// - Task Publish(IIntegrationEvent integrationEvent); - - /// - /// Subscribes to an integration event with a dynamic event handler - /// - /// The handler type - /// The event name to subscribe to. - void Subscribe(string eventName) - where THandler : IIntegrationMessageHandler; - - /// - /// Subscribes to an integration event with a generically typed event handler - /// - /// The handler type - /// The event type to subscribe to - void Subscribe() - where THandler : IIntegrationMessageHandler - where TEvent : IIntegrationEvent; - - /// - /// Subscribes to an integration event with a singleton instance event handler - /// - /// The event type to subscribe to - void Subscribe(IIntegrationMessageHandler handler) - where TEvent : IIntegrationEvent; - - void Unsubscribe(string eventName) - where THandler : IIntegrationMessageHandler; - - void Unsubscribe() - where THandler : IIntegrationMessageHandler - where TEvent : IIntegrationEvent; - - void Unsubscribe(IIntegrationMessageHandler handler) - where TEvent : IIntegrationEvent; - - void ProvideInvoker(IBackendFxApplicationInvoker invoker); - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IMessageNameProvider.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IMessageNameProvider.cs deleted file mode 100644 index cd7883a0..00000000 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IMessageNameProvider.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using JetBrains.Annotations; - -namespace Backend.Fx.Patterns.EventAggregation.Integration -{ - public interface IMessageNameProvider - { - [NotNull] - string GetMessageName(); - [NotNull] - string GetMessageName(Type t); - [NotNull] - string GetMessageName(IIntegrationEvent integrationEvent); - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/ISubscription.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/ISubscription.cs deleted file mode 100644 index 91ca9a39..00000000 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/ISubscription.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Backend.Fx.Patterns.DependencyInjection; - -namespace Backend.Fx.Patterns.EventAggregation.Integration -{ - public interface ISubscription - { - void Process(IInstanceProvider instanceProvider, EventProcessingContext context); - bool Matches(object handler); - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/InMemoryMessageBus.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/InMemoryMessageBus.cs deleted file mode 100644 index 10b1ea3c..00000000 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/InMemoryMessageBus.cs +++ /dev/null @@ -1,76 +0,0 @@ -namespace Backend.Fx.Patterns.EventAggregation.Integration -{ - using System; - using System.Threading.Tasks; - using Environment.MultiTenancy; - - public class InMemoryMessageBus : MessageBus - { - private readonly InMemoryMessageBusChannel _channel; - - public InMemoryMessageBus() - { - _channel = new InMemoryMessageBusChannel(); - } - - public InMemoryMessageBus(InMemoryMessageBusChannel channel) - { - _channel = channel; - } - - public override void Connect() - { - _channel.MessageReceived += ChannelOnMessageReceived; - } - - protected override void Dispose(bool disposing) - { - _channel.MessageReceived -= ChannelOnMessageReceived; - } - - protected override Task PublishOnMessageBus(IIntegrationEvent integrationEvent) - { - _channel.Publish(integrationEvent); - - // the returning Task is about publishing the event, not processing! - return Task.CompletedTask; - } - - protected override void Subscribe(string eventName) - { - } - - protected override void Unsubscribe(string eventName) - { - } - - private void ChannelOnMessageReceived( - object sender, - InMemoryMessageBusChannel.MessageReceivedEventArgs eventArgs) - { - Process( - MessageNameProvider.GetMessageName(eventArgs.IntegrationEvent), - new InMemoryProcessingContext(eventArgs.IntegrationEvent)); - } - - private class InMemoryProcessingContext : EventProcessingContext - { - private readonly IIntegrationEvent _integrationEvent; - - public InMemoryProcessingContext(IIntegrationEvent integrationEvent) - { - _integrationEvent = integrationEvent; - } - - public override TenantId TenantId => new TenantId(_integrationEvent.TenantId); - - public override dynamic DynamicEvent => _integrationEvent; - public override Guid CorrelationId => _integrationEvent.CorrelationId; - - public override IIntegrationEvent GetTypedEvent(Type eventType) - { - return _integrationEvent; - } - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/InMemoryMessageBusChannel.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/InMemoryMessageBusChannel.cs deleted file mode 100644 index 30d17f64..00000000 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/InMemoryMessageBusChannel.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Concurrent; - -namespace Backend.Fx.Patterns.EventAggregation.Integration -{ - using System; - using System.Threading.Tasks; - - public class InMemoryMessageBusChannel - { - private readonly ConcurrentBag _messageHandlingTasks = new ConcurrentBag(); - - internal event EventHandler MessageReceived; - - internal void Publish(IIntegrationEvent integrationEvent) - { - var eventArgs = new MessageReceivedEventArgs { IntegrationEvent = integrationEvent }; - _messageHandlingTasks.Add(Task.Run(() => MessageReceived?.Invoke(this, eventArgs))); - } - - public async Task FinishHandlingAllMessagesAsync() - { - while (_messageHandlingTasks.TryTake(out var messageHandlingTask)) - { - await messageHandlingTask; - } - } - - internal class MessageReceivedEventArgs - { - public IIntegrationEvent IntegrationEvent { get; set; } - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IntegrationEvent.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IntegrationEvent.cs deleted file mode 100644 index 6ea7cb8a..00000000 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IntegrationEvent.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using Backend.Fx.Environment.MultiTenancy; - -namespace Backend.Fx.Patterns.EventAggregation.Integration -{ - public interface IIntegrationEvent - { - Guid Id { get; } - DateTime CreationDate { get; } - int TenantId { get; } - Guid CorrelationId { get; } - } - - /// - /// Events that should be handled in a separate context. Might be persisted as well using an external message bus. - /// See https://blogs.msdn.microsoft.com/cesardelatorre/2017/02/07/domain-events-vs-integration-events-in-domain-driven-design-and-microservices-architectures/ - /// - public abstract class IntegrationEvent : IIntegrationEvent - { - public Guid Id { get; } = Guid.NewGuid(); - - public DateTime CreationDate { get; } = DateTime.UtcNow; - - public int TenantId { get; private set; } - - public Guid CorrelationId { get; private set; } - - protected IntegrationEvent() - { - } - - [Obsolete("TenantId is maintained by the framework now")] - protected IntegrationEvent(int tenantId) - { - TenantId = tenantId; - } - - internal void SetCorrelationId(Guid correlationId) - { - CorrelationId = correlationId; - } - - public void SetTenantId(TenantId tenantId) - { - TenantId = (int) tenantId; - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/MessageBus.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/MessageBus.cs deleted file mode 100644 index 1698f4ca..00000000 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/MessageBus.cs +++ /dev/null @@ -1,201 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Threading.Tasks; -using Backend.Fx.Environment.Authentication; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.Patterns.EventAggregation.Integration -{ - public abstract class MessageBus : IMessageBus - { - private static readonly ILogger Logger = Log.Create(); - - /// - /// Holds the registered handlers. - /// Each event type name (key) matches to various subscriptions - /// - private readonly ConcurrentDictionary> _subscriptions = new ConcurrentDictionary>(); - - private IBackendFxApplicationInvoker _invoker; - - public IMessageNameProvider MessageNameProvider { get; } = new DefaultMessageNameProvider(); - public abstract void Connect(); - - public void ProvideInvoker(IBackendFxApplicationInvoker invoker) - { - if (_invoker != null && !Equals(_invoker, invoker)) - { - throw new InvalidOperationException("This message bus instance has been linked to an application instance invoker before. " + - "You cannot share the same message bus instance between multiple applications."); - } - _invoker = invoker; - } - - public Task Publish(IIntegrationEvent integrationEvent) - { - return PublishOnMessageBus(integrationEvent); - } - - protected abstract Task PublishOnMessageBus(IIntegrationEvent integrationEvent); - - - /// - public void Subscribe(string eventName) where THandler : IIntegrationMessageHandler - { - Logger.LogInformation("Subscribing to {EventName}", eventName); - EnsureInvoker(); - var subscription = new DynamicSubscription(typeof(THandler)); - _subscriptions.AddOrUpdate(eventName, - s => new List {subscription}, - (s, list) => - { - list.Add(subscription); - return list; - }); - Subscribe(eventName); - } - - /// - public void Subscribe() where THandler : IIntegrationMessageHandler where TEvent : IIntegrationEvent - { - string eventName = MessageNameProvider.GetMessageName(); - Logger.LogInformation("Subscribing to {EventName}", eventName); - EnsureInvoker(); - var subscription = new TypedSubscription(typeof(THandler), typeof(TEvent)); - _subscriptions.AddOrUpdate(eventName, - s => new List {subscription}, - (s, list) => - { - list.Add(subscription); - return list; - }); - Subscribe(eventName); - } - - public void Subscribe(IIntegrationMessageHandler handler) - where TEvent : IIntegrationEvent - { - string eventName = MessageNameProvider.GetMessageName(); - Logger.LogInformation("Subscribing to {EventName}", eventName); - EnsureInvoker(); - var subscription = new SingletonSubscription(handler); - _subscriptions.AddOrUpdate(eventName, - s => new List {subscription}, - (s, list) => - { - list.Add(subscription); - return list; - }); - Subscribe(eventName); - } - - public void Unsubscribe(string eventName) where THandler : IIntegrationMessageHandler - { - Logger.LogInformation("Unsubscribing from {EventName}", eventName); - if (_subscriptions.TryGetValue(eventName, out var handlers)) - { - handlers.RemoveAll(t => t.Matches(typeof(THandler))); - } - - Unsubscribe(eventName); - } - - public void Unsubscribe() where THandler : IIntegrationMessageHandler where TEvent : IIntegrationEvent - { - string eventName = MessageNameProvider.GetMessageName(); - Logger.LogInformation("Unsubscribing from {EventName}", eventName); - if (_subscriptions.TryGetValue(eventName, out var handlers)) - { - handlers.RemoveAll(t => t.Matches(typeof(THandler))); - } - - Unsubscribe(eventName); - } - - public void Unsubscribe(IIntegrationMessageHandler handler) where TEvent : IIntegrationEvent - { - string eventName = MessageNameProvider.GetMessageName(); - Logger.LogInformation("Unsubscribing from {EventName}", eventName); - if (_subscriptions.TryGetValue(eventName, out var handlers)) - { - handlers.RemoveAll(t => t.Matches(handler)); - } - - Unsubscribe(eventName); - } - - protected abstract void Subscribe(string eventName); - protected abstract void Unsubscribe(string eventName); - - protected void Process(string eventName, EventProcessingContext context) - { - Logger.LogInformation("Processing a {EventName} message", eventName); - EnsureInvoker(); - - if (_subscriptions.TryGetValue(eventName, out List subscriptions)) - { - foreach (ISubscription subscription in subscriptions) - { - try - { - _invoker.Invoke( - instanceProvider => subscription.Process(instanceProvider, context), - new SystemIdentity(), - context.TenantId, - context.CorrelationId); - } - catch (Exception ex) - { - Logger.LogWarning(ex, "Processing a {EventName} message failed", eventName); - throw; - } - } - } - else - { - Logger.LogInformation("No handler registered. Ignoring {EventName} event", eventName); - } - } - - private void EnsureInvoker() - { - if (_invoker == null) - { - throw new InvalidOperationException("Before using the message bus you have to provide the application invoker by calling ProvideInvoker()"); - } - } - - protected virtual void Dispose(bool disposing) - { - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private class DefaultMessageNameProvider : IMessageNameProvider - { - public string GetMessageName() - { - return GetMessageName(typeof(T)); - } - - public string GetMessageName(Type t) - { - var messageName = t.Name; - return messageName; - } - - public string GetMessageName(IIntegrationEvent integrationEvent) - { - return GetMessageName(integrationEvent.GetType()); - } - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/SingletonSubscription.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/SingletonSubscription.cs deleted file mode 100644 index 62966b36..00000000 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/SingletonSubscription.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.Patterns.EventAggregation.Integration -{ - public class SingletonSubscription : ISubscription where TEvent : IIntegrationEvent - { - private static readonly ILogger Logger = Log.Create>(); - private readonly IIntegrationMessageHandler _handler; - - public SingletonSubscription(IIntegrationMessageHandler handler) - { - _handler = handler; - } - - public void Process(IInstanceProvider instanceProvider, EventProcessingContext context) - { - using (Logger.LogInformationDuration($"Invoking subscribed handler {_handler.GetType().Name}")) - { - _handler.Handle((TEvent) context.GetTypedEvent(typeof(TEvent))); - } - } - - public bool Matches(object handler) - { - return _handler == handler; - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/TypedSubscription.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/TypedSubscription.cs deleted file mode 100644 index 0aa27c8f..00000000 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/TypedSubscription.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Diagnostics; -using System.Reflection; -using Backend.Fx.Extensions; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.Patterns.EventAggregation.Integration -{ - public class TypedSubscription : ISubscription - { - private static readonly ILogger Logger = Log.Create(); - private readonly Type _handlerType; - private readonly Type _eventType; - - public TypedSubscription(Type handlerType, Type eventType) - { - _handlerType = handlerType; - _eventType = eventType; - } - - public void Process(IInstanceProvider instanceProvider, EventProcessingContext context) - { - IIntegrationEvent integrationEvent = context.GetTypedEvent(_eventType); - MethodInfo handleMethod = _handlerType.GetRuntimeMethod("Handle", new[] {_eventType}); - Debug.Assert(handleMethod != null, $"No method with signature `Handle({_eventType.Name} event)` found on {_handlerType.Name}"); - - Logger.LogInformation("Getting subscribed handler instance of type {HandlerTypeName}", _handlerType.Name); - object handlerInstance = instanceProvider.GetInstance(_handlerType); - - using (Logger.LogInformationDuration($"Invoking subscribed handler {_handlerType.GetDetailedTypeName()}")) - { - handleMethod.Invoke(handlerInstance, new object[] {integrationEvent}); - } - } - - public bool Matches(object handler) - { - return (Type) handler == _handlerType; - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/IdGeneration/HiLoIdGenerator.cs b/src/abstractions/Backend.Fx/Patterns/IdGeneration/HiLoIdGenerator.cs deleted file mode 100644 index 3f61085c..00000000 --- a/src/abstractions/Backend.Fx/Patterns/IdGeneration/HiLoIdGenerator.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Threading; -using Backend.Fx.Logging; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.Patterns.IdGeneration -{ - public abstract class HiLoIdGenerator : IIdGenerator - { - private static readonly ILogger Logger = Log.Create(); - private int _highId = -1; - private int _lowId = -1; - private static readonly object Mutex = new object(); - private readonly bool _isTraceEnabled; - - protected HiLoIdGenerator() - { - _isTraceEnabled = Logger.IsEnabled(LogLevel.Trace); - } - - public int NextId() - { - lock (Mutex) - { - EnsureValidLowAndHiId(); - var nextId = _lowId; - Interlocked.Increment(ref _lowId); - if (_isTraceEnabled) Logger.LogTrace("Providing id {NextId}", nextId); - return nextId; - } - } - - private void EnsureValidLowAndHiId() - { - if (_lowId == -1 || _lowId > _highId) - { - // first fetch from sequence in life time - _lowId = GetNextBlockStart(); - _highId = _lowId + BlockSize- 1; - } - } - - protected abstract int GetNextBlockStart(); - - protected abstract int BlockSize { get; } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/IdGeneration/IEntityIdGenerator.cs b/src/abstractions/Backend.Fx/Patterns/IdGeneration/IEntityIdGenerator.cs deleted file mode 100644 index cca401b2..00000000 --- a/src/abstractions/Backend.Fx/Patterns/IdGeneration/IEntityIdGenerator.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Backend.Fx.Patterns.IdGeneration -{ - public interface IEntityIdGenerator - { - int NextId(); - } -} diff --git a/src/abstractions/Backend.Fx/Patterns/IdGeneration/IIdGenerator.cs b/src/abstractions/Backend.Fx/Patterns/IdGeneration/IIdGenerator.cs deleted file mode 100644 index 5c59c6d2..00000000 --- a/src/abstractions/Backend.Fx/Patterns/IdGeneration/IIdGenerator.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Backend.Fx.Patterns.IdGeneration -{ - public interface IIdGenerator - { - int NextId(); - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/IdGeneration/ISequence.cs b/src/abstractions/Backend.Fx/Patterns/IdGeneration/ISequence.cs deleted file mode 100644 index 02a4516b..00000000 --- a/src/abstractions/Backend.Fx/Patterns/IdGeneration/ISequence.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Backend.Fx.Patterns.IdGeneration -{ - public interface ISequence - { - void EnsureSequence(); - int GetNextValue(); - int Increment { get; } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/IdGeneration/SequenceHiLoIdGenerator.cs b/src/abstractions/Backend.Fx/Patterns/IdGeneration/SequenceHiLoIdGenerator.cs deleted file mode 100644 index ec4b7619..00000000 --- a/src/abstractions/Backend.Fx/Patterns/IdGeneration/SequenceHiLoIdGenerator.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Backend.Fx.Patterns.IdGeneration -{ - public abstract class SequenceHiLoIdGenerator : HiLoIdGenerator - { - private readonly ISequence _sequence; - - protected SequenceHiLoIdGenerator(ISequence sequence) - { - _sequence = sequence; - } - - protected override int GetNextBlockStart() - { - return _sequence.GetNextValue(); - } - - protected override int BlockSize => _sequence.Increment; - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/IdGeneration/SequenceIdGenerator.cs b/src/abstractions/Backend.Fx/Patterns/IdGeneration/SequenceIdGenerator.cs deleted file mode 100644 index 9d696ff3..00000000 --- a/src/abstractions/Backend.Fx/Patterns/IdGeneration/SequenceIdGenerator.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Backend.Fx.Patterns.IdGeneration -{ - public abstract class SequenceIdGenerator : IIdGenerator - { - private readonly ISequence _sequence; - - protected SequenceIdGenerator(ISequence sequence) - { - _sequence = sequence; - } - - public int NextId() - { - return _sequence.GetNextValue(); - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/Jobs/IJob.cs b/src/abstractions/Backend.Fx/Patterns/Jobs/IJob.cs deleted file mode 100644 index 552e21eb..00000000 --- a/src/abstractions/Backend.Fx/Patterns/Jobs/IJob.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Backend.Fx.Patterns.Jobs -{ - /// - /// This interface describes a job that can be executed directly or by a scheduler. - /// - public interface IJob - { - void Run(); - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/Jobs/WithTenantWideMutex.cs b/src/abstractions/Backend.Fx/Patterns/Jobs/WithTenantWideMutex.cs deleted file mode 100644 index 43bb1fcb..00000000 --- a/src/abstractions/Backend.Fx/Patterns/Jobs/WithTenantWideMutex.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.Patterns.Jobs -{ - public class WithTenantWideMutex : IJob where TJob : IJob - { - private static readonly ILogger Logger = Log.Create>(); - private readonly TJob _job; - private readonly ICurrentTHolder _tenantIdHolder; - private readonly ITenantWideMutexManager _tenantWideMutexManager; - - public WithTenantWideMutex( - ITenantWideMutexManager tenantWideMutexManager, - TJob job, - ICurrentTHolder tenantIdHolder) - { - _tenantWideMutexManager = tenantWideMutexManager; - _job = job; - _tenantIdHolder = tenantIdHolder; - } - - public void Run() - { - if (_tenantWideMutexManager.TryAcquire(_tenantIdHolder.Current, typeof(TJob).Name, out var mutex)) - { - try - { - _job.Run(); - } - finally - { - mutex.Dispose(); - } - } - else - { - var tenantIdString - = _tenantIdHolder.Current.HasValue ? _tenantIdHolder.Current.Value.ToString() : "null"; - Logger.LogInformation("{Job} is already running in tenant {TenantId}", typeof(TJob).Name, tenantIdString); - } - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/Generator.cs b/src/abstractions/Backend.Fx/RandomData/Generator.cs deleted file mode 100644 index 77595ae6..00000000 --- a/src/abstractions/Backend.Fx/RandomData/Generator.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; - -namespace Backend.Fx.RandomData -{ - public abstract class Generator : IEnumerable - { - private readonly HashSet _identicalPreventionMemory = new HashSet(); - - public IEnumerator GetEnumerator() - { - const int maxRetries = 1000; - int retries = 0; - while (true) - { - T next; - int hashCode; - do - { - next = Next(); - hashCode = next.GetHashCode(); - - if (retries++ > maxRetries) - { - throw new Exception($"Tried {maxRetries} times to generate a unique {typeof(T).Name} but did not succeed, aborting now."); - } - } while (_identicalPreventionMemory.Contains(hashCode)); - - _identicalPreventionMemory.Add(hashCode); - yield return next; - } - // ReSharper disable once IteratorNeverReturns - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - protected abstract T Next(); - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/LandLineGenerator.cs b/src/abstractions/Backend.Fx/RandomData/LandLineGenerator.cs deleted file mode 100644 index 996f4f02..00000000 --- a/src/abstractions/Backend.Fx/RandomData/LandLineGenerator.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Linq; - -namespace Backend.Fx.RandomData -{ - public class LandLineGenerator : Generator - { - public static string Generate() - { - return new LandLineGenerator().Take(1).Single(); - } - - protected override string Next() - { - var generated = Numbers.LandLineNetworks.Random(); - while (generated.Length < TestRandom.Instance.Next(8, 11)) generated += Numbers.Ciphers.Random(); - return generated; - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/Letters.cs b/src/abstractions/Backend.Fx/RandomData/Letters.cs deleted file mode 100644 index ee079429..00000000 --- a/src/abstractions/Backend.Fx/RandomData/Letters.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace Backend.Fx.RandomData -{ - public static class Letters - { - public static string UpperCase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - public static string LowerCase = "abcdefghijklmnopqrstuvwxyz"; - - public static string RandomUpperCase(int length) - { - var random = string.Empty; - for (var i = 0; i < length; i++) random += UpperCase.Random(); - return random; - } - - public static string RandomLowerCase(int length) - { - var random = string.Empty; - for (var i = 0; i < length; i++) random += LowerCase.Random(); - return random; - } - - public static string RandomNormalCase(int length) - { - var random = string.Empty; - for (var i = 0; i < length; i++) random += i == 0 ? UpperCase.Random() : LowerCase.Random(); - return random; - } - - public static string RandomPassword(int length = 10, int numberCount = 2, int specialCharCount = 2) - { - const string letters = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz"; - const string numbers = "23456789"; - const string specials = "§$%&#+*-<>"; - var password = new char[length]; - - for (var i = 0; i < password.Length; i++) - { - password[i] = letters.Random(); - } - - for (int i = 0; i < numberCount; i++) - { - password[TestRandom.Next(length)] = numbers.Random(); - } - - for (int i = 0; i < specialCharCount; i++) - { - password[TestRandom.Next(length)] = specials.Random(); - } - - return new string(password); - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/LinqExtensions.cs b/src/abstractions/Backend.Fx/RandomData/LinqExtensions.cs deleted file mode 100644 index 8fcce937..00000000 --- a/src/abstractions/Backend.Fx/RandomData/LinqExtensions.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Dynamic.Core; -using System.Reflection; -using Backend.Fx.BuildingBlocks; - -namespace Backend.Fx.RandomData -{ - public static class LinqExtensions - { - /// - /// https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle - /// - /// - /// - /// - public static IEnumerable Shuffle(this IEnumerable source) - { - if (source == null) throw new ArgumentNullException(nameof(source)); - - var sourceAsArray = source as T[] ?? source.ToArray(); - - var n = sourceAsArray.Length; - while (n > 1) - { - var k = TestRandom.Instance.Next(n--); - T temp = sourceAsArray[n]; - sourceAsArray[n] = sourceAsArray[k]; - sourceAsArray[k] = temp; - } - - return sourceAsArray; - } - - public static T Random(this IEnumerable source) - { - if (source == null) throw new ArgumentNullException(nameof(source)); - - // ReSharper disable once PossibleMultipleEnumeration - if (TryAsQueryable(source, out var sourceQueryable, out var count)) - { - if (count == 0) - throw new ArgumentException( - $"The enumerable of {typeof(T).Name} does not contain any items, therefore no random item can be returned.", nameof(source)); - - return sourceQueryable.Skip(TestRandom.Next(count - 1)).First(); - } - - // ReSharper disable once PossibleMultipleEnumeration - var sourceArray = source as T[] ?? source.ToArray(); - if (sourceArray.Length == 0) - throw new ArgumentException( - $"The enumerable of {typeof(T).Name} does not contain any items, therefore no random item can be returned.", nameof(source)); - return sourceArray.ElementAt(TestRandom.Next(sourceArray.Length)); - } - - public static T RandomOrDefault(this IEnumerable source) - { - if (source == null) throw new ArgumentNullException(nameof(source)); - - // ReSharper disable once PossibleMultipleEnumeration - if (TryAsQueryable(source, out var sourceQueryable, out var count)) - { - if (count == 0) return default; - - return sourceQueryable.Skip(TestRandom.Next(count - 1)).FirstOrDefault(); - } - - // ReSharper disable once PossibleMultipleEnumeration - var sourceArray = source as T[] ?? source.ToArray(); - if (sourceArray.Length == 0) return default; - return sourceArray.ElementAt(TestRandom.Next(sourceArray.Length)); - } - - private static bool TryAsQueryable(this IEnumerable source, out IQueryable outQueryable, out int count) - { - outQueryable = null; - count = 0; - - if (source is IQueryable sourceQueryable) - { - count = sourceQueryable.Count(); - if (count == 0) - { - outQueryable = new T[0].AsQueryable(); - return true; - } - - PropertyInfo idProperty = typeof(T).GetProperty(nameof(Identified.Id), BindingFlags.Instance | BindingFlags.Public); - if (idProperty != null) sourceQueryable = sourceQueryable.OrderBy(nameof(Identified.Id)); - - outQueryable = sourceQueryable; - return true; - } - - return false; - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/LoremIpsum.cs b/src/abstractions/Backend.Fx/RandomData/LoremIpsum.cs deleted file mode 100644 index f4546be8..00000000 --- a/src/abstractions/Backend.Fx/RandomData/LoremIpsum.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Linq; - -namespace Backend.Fx.RandomData -{ - public class LoremIpsumGenerator : Generator - { - private static string[] _words = new[] - { - "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit", "proin", "eget", "iaculis", "quam", "pellentesque", "elementum", "gravida", "nulla", - "at", "tincidunt", "donec", "vulputate", "velit", "sapien", "a", "auctor", "justo", "id", "nunc", "et", "consequat", "magna", "in", "blandit", "ut", "eros", - "tempus", "condimentum", "sem", "ac", "feugiat", "tellus", "curabitur", "aliquet", "ultrices", "arcu", "eu", "lacinia", "aliquam", "integer", "non", "venenatis", - "sed", "accumsan", "massa", "nibh", "vestibulum", "nec", "porta", "libero", "vel", "ex", "molestie", "pretium", "dignissim", "ligula", "maximus", "placerat", - "nisl", "felis", "fringilla", "efficitur", "mi", "nam", "vitae", "orci", "suscipit", "porttitor", "leo", "posuere", "sollicitudin", "dictum", "tristique", "dui", - "urna", "quis", "quisque", "semper", "diam", "pulvinar", "erat", "ornare", "maecenas", "euismod", "odio", "tortor", "cursus", "convallis", "enim", "sodales", - "facilisis", "faucibus", "fusce", "scelerisque", "purus", "praesent", "interdum", "turpis", "mauris", "duis", "finibus", "augue", "nullam", "mollis", "lacus", - "egestas", "metus", "mattis", "morbi", "laoreet", "bibendum", "phasellus", "risus", "neque", "volutpat", "lobortis", "malesuada", "sagittis", "rhoncus", "est", - "imperdiet", "aenean", "fermentum", "varius", "vivamus", "suspendisse", "commodo", "luctus", "dapibus", "ullamcorper", "viverra", "congue", "hendrerit", "pharetra", - "tempor", "eleifend", "lectus", "te" - }; - - protected override string Next() - { - return _words[TestRandom.Instance.Next(_words.Length)]; - } - - public static string Generate(int minWords, int maxWords, bool asSentence) - { - int wordCount = TestRandom.Next(minWords, maxWords); - string loremIpsumText = string.Join(" ", new LoremIpsumGenerator().Take(wordCount)); - if (asSentence) - { - loremIpsumText += "."; - } - - return loremIpsumText; - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/MobileLineGenerator.cs b/src/abstractions/Backend.Fx/RandomData/MobileLineGenerator.cs deleted file mode 100644 index 094e7a25..00000000 --- a/src/abstractions/Backend.Fx/RandomData/MobileLineGenerator.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Linq; - -namespace Backend.Fx.RandomData -{ - public class MobileLineGenerator : Generator - { - public static string Generate() - { - return new MobileLineGenerator().First(); - } - - - protected override string Next() - { - var generated = Numbers.MobileNetworks.Random(); - while (generated.Length < TestRandom.Instance.Next(11)) generated += Numbers.Ciphers.Random(); - - return generated; - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/Names.cs b/src/abstractions/Backend.Fx/RandomData/Names.cs deleted file mode 100644 index 31e5bdd4..00000000 --- a/src/abstractions/Backend.Fx/RandomData/Names.cs +++ /dev/null @@ -1,5758 +0,0 @@ -namespace Backend.Fx.RandomData -{ - public static class Names - { - public static readonly string[] LegalEntityTypes = - { - "GmbH", - "AG", - "KG", - "OHG", - "Ltd.", - "SA", - "GmbH & Co KG", - "" - }; - - public static readonly string[] CompanyTypes = - { - "Schuldnerberatung", - "Autohaus", - "Apotheke", - "IT Dienstleistungen", - "Tischlerei", - "Schreinerei", - "Hausmeister", - "Bauunternehmen" - }; - - public static readonly string[] Streets = - { - "Abboweg", - "Abteistraße", - "Achatweg", - "Achenbach Straße", - "Achterstraße", - "Ackerstraße", - "Ackerweg", - "Adalbertstraße", - "Adalmundstraße", - "Adamsweg", - "Adelenstraße", - "Adelhartweg", - "Adickesstraße", - "Adlerstraße", - "Adlerweg", - "Admiralstraße", - "Agavenstraße", - "Ahornweg", - "Ährenweg", - "Akazienstraße", - "Akenschockweg", - "Albert-Schweitzer-Weg", - "Albertstraße", - "Albingerstraße", - "Albrecht-Dürer-Straße", - "Albrechtstraße", - "Albroweg", - "Aldinghofer Straße", - "Alekestraße", - "Alemannenstraße", - "Alexanderstraße", - "Alfred-Berndsen-Weg", - "Alfred-Lange-Straße", - "Alfred-Nobel-Straße", - "Alfred-Reinoldsmann-Weg", - "Alfred-Trappen-Straße", - "Allensteiner Straße", - "Allerstraße", - "Alpenheide", - "Alpenrosenstraße", - "Alsenstraße", - "Alte Benninghofer Straße", - "Alte Dortmunder Straße", - "Alte Ellinghauser Straße", - "Alte Märsch", - "Alte Straße", - "Alte Teichstraße", - "Altenderner Straße", - "Altenhennestraße", - "Altenrathstraße", - "Alter Burgwall", - "Alter Erlenweg", - "Alter Heideweg", - "Alter Hellweg", - "Alter Mühlenweg", - "Alter Postweg", - "Altfriedstraße", - "Althoffstraße", - "Althüserstraße", - "Altmengeder Straße", - "Altonaer Straße", - "Altumstraße", - "Altwickeder Hellweg", - "Am Alten Forsthaus", - "Am Alten Garten", - "Am Alten Sportplatz", - "Am Amtshaus", - "Am Apelstück", - "Am Bahnhof Somborn", - "Am Bahnhof Tierpark", - "Am Ballroth", - "Am Beilstück", - "Am Beisenkamp", - "Am Beisenufer", - "Am Bentenskamp", - "Am Bergfeld", - "Am Bertholdshof", - "Am Birkenbaum", - "Am Bönner", - "Am Brandhof", - "Am Brennbusch", - "Am Bruchheck", - "Am Burhag", - "Am Büter", - "Am Dieckhof", - "Am Dimberg", - "Am Dingelkamp", - "Am Dorfplatz", - "Am Dreisch", - "Am Düppersholl", - "Am Eiserfeld", - "Am Ellberg", - "Am Erlenbruch", - "Am Feldbrand", - "Am Flachsteich", - "Am Flinsbach", - "Am Franzosensiepen", - "Am Freck", - "Am Funkturm", - "Am Gardenkamp", - "Am Geenseel", - "Am Gemeindehaus", - "Am Gerrenbach", - "Am Gottesacker", - "Am Grenzgraben", - "Am Grimmelsiepen", - "Am Großen Stück", - "Am Grünen Ufer", - "Am Gulloh", - "Am Hagedorn", - "Am Hahnenholz", - "Am Hang", - "Am Hartweg", - "Am Haselhof", - "Am Hasenberg", - "Am Häugter", - "Am Hausacker", - "Am Hedreisch", - "Am Heedbrink", - "Am Heidenpost", - "Am Heiderand", - "Am Heiligen Busch", - "Am Heisterbach", - "Am Heller", - "Am Hemesod", - "Am Heuningsfeld", - "Am Hilgenbaum", - "Am Hittenauwer", - "Am Hofstück", - "Am Hohen Teich", - "Am Höhweg", - "Am Hölzgen", - "Am Holzgraben", - "Am Hombruchsfeld", - "Am Hombruchskamp", - "Am Hülsenberg", - "Am Jungbrunnen", - "Am Kaiserhain", - "Am Kämpen", - "Am Kapellenufer", - "Am Kattenbrauck", - "Am Kirchenfeld", - "Am Klusenberg", - "Am Knapp", - "Am Knappenberg", - "Am Knie", - "Am Kohlrücken", - "Am Kraftwerk", - "Am Krähenort", - "Am Kramberg", - "Am Krankenhaus", - "Am Kreuzloh", - "Am Kucksberg", - "Am Kuhlenweg", - "Am Lindeneck", - "Am Lohbach", - "Am Lohbachhang", - "Am Marksbach", - "Am Mergelbruch", - "Am Mühlenberg", - "Am Mühlenwinkel", - "Am Münzenkamp", - "Am Nocken", - "Am Oelpfad", - "Am Oespeler Dorney", - "Am Oestricher Bruch", - "Am Oldendieck", - "Am Ossenbrink", - "Am Ostbrink", - "Am Osterbruch", - "Am Ostheck", - "Am Ostpark", - "Am Paß", - "Am Pastorenwäldchen", - "Am Petersheck", - "Am Pfauenufer", - "Am Quartus", - "Am Quellweg", - "Am Rabensmorgen", - "Am Rebstock", - "Am Remberg", - "Am Rhader Holz", - "Am Richterbusch", - "Am Rode", - "Am Roggenfeld", - "Am Rombergpark", - "Am Rondell", - "Am Rosenplätzchen", - "Am Roten Haus", - "Am Rüggen", - "Am Rundbogen", - "Am Schallacker", - "Am Schleppweg", - "Am Schlosspark", - "Am Schmandsack", - "Am Schmechtingsbach", - "Am Schoopställer", - "Am Schultenhof", - "Am Segen", - "Am Siepenhohl", - "Am Sodkamp", - "Am Sommerberg", - "Am Sonnenblick", - "Am Spörkel", - "Am Stift", - "Am Stövenhof", - "Am Stucken", - "Am Stuckenrodt", - "Am Sturmwald", - "Am Südwestfriedhof", - "Am Sümpelmannhof", - "Am Sumpf", - "Am Surck", - "Am Talenberg", - "Am Täufling", - "Am Thurmacker", - "Am Timmerbed", - "Am Tremoniapark", - "Am Trienensiepen", - "Am Trissel", - "Am Truxhof", - "Am Uhlenhorst", - "Am Voerstenhof", - "Am Volksgarten", - "Am Vosshohl", - "Am Walde", - "Am Wasserfall", - "Am Wemphof", - "Am Westfalenstadion", - "Am Westheck", - "Am Wiebusch", - "Am Wildgatter", - "Am Winkelsweg", - "Am Winterberg", - "Am Wittfeld", - "Am Zechenhof", - "Am Zehnthof", - "Am Zippen", - "Am Zitter", - "Amalienstraße", - "Ammerbaumweg", - "Ammerstraße", - "Amselstraße", - "Amselweg", - "Amsterdamer Weg", - "Amtsberg", - "Amtsstraße", - "An den Birken", - "An den Bräukenhöfen", - "An den Gräften", - "An den Rühlen", - "An den Stahlhäusern", - "An den Teichen", - "An der Buschmühle", - "An der Deipenbeck", - "An der Eiche", - "An der Fillkuhle", - "An der Gosekuhle", - "An der Goymark", - "An der Halde", - "An der Herz-Jesu-Kirche", - "An der Hordelwiese", - "An der Hühnerhecke", - "An der Hundewiese", - "An der Kemna", - "An der Margarethenkapelle", - "An der Palmweide", - "An der Panne", - "An der Scheune", - "An der schlanken Mathilde", - "An der Stipskuhle", - "An der Teithe", - "An der Wasserburg", - "An der Weustmühle", - "An der Windhake", - "An der Witwe", - "An Steffens Wiesche", - "Andreasstraße", - "Anemonenstraße", - "Anna-Siemsen-Straße", - "Annenstraße", - "Anneweihstraße", - "Apelank", - "Apelbachstraße", - "Apfelbaumweg", - "Apfeldweg", - "Aplerbecker Bahnhofstraße", - "Aplerbecker Schulstraße", - "Aplerbecker Straße", - "Aplerbecker Wald", - "Aplerbecker-Mark-Straße", - "Apolloweg", - "Apotheker-Eick-Straße", - "Appelbecke", - "Archenbecke", - "Arcostraße", - "Ardeystraße", - "Arenbergstraße", - "Arent-Rupe-Straße", - "Arminiusstraße", - "Arndtstraße", - "Arneckestraße", - "Arnold-Böcklin-Straße", - "Arnoldstraße", - "Arthur-Beringer-Straße", - "Artusweg", - "Aschebrockstraße", - "Aschenputtelweg", - "Ascloonweg", - "Aspeystraße", - "Asselburgstraße", - "Asselner Hellweg", - "Asselner Straße", - "Asternstraße", - "Astrid-Lindgren-Ring", - "Athener Weg", - "Attinghausenstraße", - "Auf dem Berge", - "Auf dem Bleck", - "Auf dem Blick", - "Auf dem Brand", - "Auf dem Brauck", - "Auf dem Brink", - "Auf dem Brümmer", - "Auf dem Feldgraben", - "Auf dem Gummel", - "Auf dem Heiken", - "Auf dem Hohwart", - "Auf dem Hövellande", - "Auf dem Kellerkamp", - "Auf dem Klai", - "Auf dem Kranz", - "Auf dem Mühlenhof", - "Auf dem Mühlenhofe", - "Auf dem Rott", - "Auf dem Schloßacker", - "Auf dem Schnee", - "Auf dem Sonneborn", - "Auf dem Springstück", - "Auf dem Steine", - "Auf dem Toren", - "Auf dem Wodeacker", - "Auf der Bicke", - "Auf der Bokkenbredde", - "Auf der Bredde", - "Auf der Burg", - "Auf der Goldbreite", - "Auf der hohen Fuhr", - "Auf der Horte", - "Auf der Kiste", - "Auf der Kluse", - "Auf der Knappule", - "Auf der Kuhweide", - "Auf der Linnert", - "Auf der Mühle", - "Auf der Wenge", - "Auf der Wieck", - "Auf der Wittbräucke", - "Aufenangerstraße", - "Auf'm Brautschatz", - "Auf'm Lehmbrink", - "Auf'm Plätzchen", - "Augustastraße", - "Auguste-Prigge-Straße", - "August-Schmidt-Straße", - "Aussigring", - "Autobahn Downhill", - "Autobahn Downhill Bittermark", - "Avermannstraße", - "Azaleenweg", - "B 1", - "B 236", - "B 54", - "Baackweg", - "Baaderweg", - "Baakskamp", - "Bachstraße", - "Backenköhlerweg", - "Bäckerstraße", - "Badische Straße", - "Badweg", - "Baedekerstraße", - "Baerweg", - "Baeumerstraße", - "Bahnebredde", - "Bahnhangstraße", - "Bahnhofstraße", - "Balkenstraße", - "Balsterstraße", - "Baltischer Weg", - "Banatstraße", - "Bandelstraße", - "Bannweg", - "Baptistaweg", - "Barbarastraße", - "Barbergestraße", - "Bärenbruch", - "Bärenkamp", - "Barichstraße", - "Baroper Bahnhofstraße", - "Baroper Bergstraße", - "Baroper Heidestraße", - "Baroper Kirchweg", - "Baroper Landwehr", - "Baroper Marktplatz", - "Baroper Schulstraße", - "Baroper Straße", - "Barsinghausenstraße", - "Bartelsstraße", - "Bartgeierweg", - "Barthstraße", - "Baseler Weg", - "Bassestraße", - "Baststraße", - "Batenburgstraße", - "Batheyweg", - "Bauerholz", - "Bauernkamp", - "Bauerstraße", - "Baukamp", - "Bauksheide", - "Baumertweg", - "Baumhaselweg", - "Baumhofweg", - "Baumwirtsweg", - "Baurat-Marx-Allee", - "Bayrische Straße", - "Bebelstraße", - "Beckleystraße", - "Beckstedtweg", - "Bedarfszufahrt DELTA", - "Bedastraße", - "Beerenweg", - "Beethovenstraße", - "Begonienstraße", - "Beguinenstraße", - "Behringstraße", - "Beisemannskamp", - "Beisenherzstraße", - "Beisterweg", - "Beitterstraße", - "Bekassinenweg", - "Belgischer Weg", - "Belle-Alliance-Straße", - "Benediktinerstraße", - "Benninghofer Feld", - "Benninghofer Heide", - "Benninghofer Mark", - "Benninghofer Straße", - "Benno-Jacob-Straße", - "Benno-Niggemeyer-Weg", - "Bennostraße", - "Bensbergweg", - "Benthausweg", - "Beratgerstraße", - "Berberisweg", - "Berchumweg", - "Berenbredde", - "Bergelchen-Ort", - "Bergerhofweg", - "Berghofer Schulstraße", - "Berghofer Straße", - "Bergiusstraße", - "Bergmannsknapp", - "Bergmannstraße", - "Bergmeisterstraße", - "Bergparte", - "Bergstraße", - "Berliner Straße", - "Bermesdickerstraße", - "Berner Weg", - "Bernhard-Letterhaus-Straße", - "Bernsteinweg", - "Bertastraße", - "Bertha-von-Suttner-Straße", - "Bertholdstraße", - "Betenstraße", - "Beukenbergstraße", - "Beurhausstraße", - "Beuthstraße", - "Beverstraße", - "Beylingstraße", - "Biberweg", - "Bielefelder Straße", - "Bienenstraße", - "Bierhoffstraße", - "Bierkamp", - "Bifangweg", - "Biggestieg", - "Binnerstraße", - "Binsengarten", - "Birkenufer", - "Birkenweg", - "Birnbaumweg", - "Bischofsgasse", - "Bismarckstraße", - "Bissenkamp", - "Bitterfeldstraße", - "Bittermarkstraße", - "Bladenhorstweg", - "Blankensteiner Straße", - "Blaumenacker", - "Blaurakenweg", - "Blausielweg", - "Bleichmärsch", - "Bleichstraße", - "Blenkerweg", - "Blickstraße", - "Blitzstraße", - "Blücherstraße", - "Blumenkamp", - "Blumenstraße", - "Blütenweg", - "Bobelohweg", - "Boberstraße", - "Bockenfelder Straße", - "Bockensiepen", - "Böcklerstraße", - "Böckmannstraße", - "Bockumweg", - "Bodeckstraße", - "Bodelschwingher Berg", - "Bodelschwingher Straße", - "Bodenweg", - "Boeselagerstraße", - "Bogenstraße", - "Boickholtstraße", - "Bojerstraße", - "Bollwerkstraße", - "Bolmkeblick", - "Bolmkeweg", - "Bomacker", - "Bömckestraße", - "Bömelburgstraße", - "Bomheuerweg", - "Bonhoefferstraße", - "Bonifatiusstraße", - "Bönschstraße", - "Bookenburgweg", - "Börgerhoffweg", - "Borgmannsweg", - "Borkenstraße", - "Bornstraße", - "Borsigplatz", - "Borsigstraße", - "Borussiastraße", - "Boschstraße", - "Botdingweg", - "Bothestraße", - "Boverfeld", - "Bovermannstraße", - "Bövinghauser Dorfstraße", - "Bövinghauser Straße", - "Bozener Straße", - "Brabänderweg", - "Brabeckweg", - "Brache", - "Brackeler Hellweg", - "Brackeler Linde", - "Brackeler Straße", - "Brahmsstraße", - "Brambauerstraße", - "Bramey", - "Bramkampsweg", - "Brandbruchstraße", - "Brandenburger Straße", - "Brandeniusstraße", - "Brandheide", - "Brandisstraße", - "Brandschachtstraße", - "Brassertstraße", - "Brauhausstraße", - "Braunsbergweg", - "Braunschweiger Straße", - "Braunstraße", - "Brechtener Heide", - "Brechtener Straße", - "Brechtstraße", - "Breddeweg", - "Bredenbeckstraße", - "Bredowstraße", - "Brehtonweg", - "Breierspfad", - "Breisenbachstraße", - "Breitscheidstraße", - "Brembuschweg", - "Bremer Straße", - "Bremerkamp", - "Bremmenstraße", - "Bremsstraße", - "Brennaborstraße", - "Brennerstraße", - "Brentweg", - "Breslaustraße", - "Briefsweg", - "Brietenstraße", - "Brinkhoffstraße", - "Brinkmannstraße", - "Brinksitzerweg", - "Britischer Weg", - "Brixener Straße", - "Brockenscheidter Weg", - "Brockhausweg", - "Brögerstraße", - "Bromberger Straße", - "Bronckhorststraße", - "Bronnerstraße", - "Brücherhofstraße", - "Brüchtenweg", - "Brückstraße", - "Brüderweg", - "Brüggenfeld", - "Brügmannstraße", - "Brühlweg", - "Bruktererstraße", - "Brunebeckweg", - "Brunnenstraße", - "Brünninghauser Straße", - "Brunostraße", - "Brunshollweg", - "Brütingsweg", - "Buchenacker", - "Buchenlandstraße", - "Buchenstraße", - "Buchsbaumweg", - "Büchtersweg", - "Buddenacker", - "Buddenhofweg", - "Buddinkstraße", - "Büderichweg", - "Buggestraße", - "Büllestraße", - "Bülowstraße", - "Bummelberg", - "Büngerstraße", - "Bünnerhelfstraße", - "Bunsen-Kirchhoff-Straße", - "Buntspechtweg", - "Bunzlaustraße", - "Bürenstraße", - "Bürgerstraße", - "Burggrafenstraße", - "Burgheisterkamp", - "Burgholzstraße", - "Burgring", - "Burgtor", - "Burgunderstraße", - "Burgwall", - "Burgweg", - "Burrichterweg", - "Buschei", - "Büscher Straße", - "Buschgarten", - "Buschstraße", - "Busenbergstraße", - "Bussardstraße", - "Bussardweg", - "Butenandtstraße", - "Büttelweg", - "Büttnerstraße", - "Buttweg", - "Butzstraße", - "Caesariusstraße", - "Calvinstraße", - "Canarisstraße", - "Carl-Duisberg-Straße", - "Carl-Holtschneider-Straße", - "Carl-von-Ossietzky-Straße", - "Castellestraße", - "Castroper Straße", - "Chattenstraße", - "Chaussee", - "Chemnitzer Straße", - "Cheruskerstraße", - "Christianstraße", - "Cimbernstraße", - "Clarenberg", - "Clarissenstraße", - "Clausthaler Straße", - "Clematisweg", - "Clemens-Veltum-Straße", - "Cobbenheimweg", - "Cobbingstraße", - "Colonatenweg", - "Conradusstraße", - "Corvarastraße", - "Cottastraße", - "Crachtstraße", - "Crispinstraße", - "Crudewagenweg", - "Cunigundeweg", - "Dachstraße", - "Dachsweg", - "Dahlienstraße", - "Dahmsfeldstraße", - "Damaschkestraße", - "Dammstraße", - "Danckwardtstraße", - "Danewerkstraße", - "Dänischer Weg", - "Danzerweg", - "Danziger Straße", - "Darbovenstraße", - "Dasselstraße", - "Daßloh", - "Däumlingsweg", - "Davidisstraße", - "Dechenstraße", - "Deggingstraße", - "Deilmannstraße", - "Deininghauser Straße", - "Deintelleweg", - "Deipenbeckstraße", - "Deitertstraße", - "Dellwiger Feld", - "Dellwiger Straße", - "Denickestraße", - "Denkmalstraße", - "Dennewitzstraße", - "Derner Bahnstraße", - "Derner Kippshof", - "Derner Straße", - "Dessauerstraße", - "Dethmar-Mülher-Straße", - "Detmarstraße", - "Detmolder Straße", - "Deusener Straße", - "Deuser Wiesen", - "Deutsche Straße", - "Deutsch-Luxemburger-Straße", - "Deutzer Weg", - "Diakon-Koch-Weg", - "Dickebankstraße", - "Dickhofskamp", - "Dickrath", - "Dieckmannweg", - "Diedenhofener Straße", - "Diedrichstraße", - "Diekmüllerbaum", - "Dielstraße", - "Diemelstraße", - "Diepenbrockstraße", - "Dietrich-Schröder-Straße", - "Dingweg", - "Dinnendahlweg", - "Dionysiusstraße", - "Dirschauer Weg", - "Disselhoffstraße", - "Distelkamp", - "Distelweg", - "Ditzschweg", - "Döbbengasse", - "Döbelner Straße", - "Doertweg", - "Dohlenstraße", - "Dollersweg", - "Domänenstraße", - "Dompfaffweg", - "Donarstraße", - "Donnerstraße", - "Dönnstraße", - "Dopheidestraße", - "Dorfgrenze", - "Dorfstraße", - "Dörhoffstraße", - "Dorhofweg", - "Dornackerstraße", - "Dornbruchstraße", - "Dorneburgweg", - "Dörnenstraße", - "Dorneystraße", - "Dornröschenweg", - "Dornstraße", - "Dorotheenstraße", - "Dorstelmannstraße", - "Dorstfelder Allee", - "Dorstfelder Hellweg", - "Dortmunder Feld", - "Dortmunder Straße", - "Dortustraße", - "Dörwerstraße", - "Dransfeldstraße", - "Drechslerweg", - "Drehbrückenstraße", - "Dreherstraße", - "Dreihüttenstraße", - "Dresdener Straße", - "Driverweg", - "Droote", - "Drosselweg", - "Droste-Hülshoff-Straße", - "Droste-zu-Vischering-Siedlung", - "Drüwelhof", - "Dubliner Weg", - "Dückerstraße", - "Dudenstraße", - "Dudweilerstraße", - "Dümpelstraße", - "Dünnebecke", - "Durchstraße", - "Dürener Dorfstraße", - "Dürener Straße", - "Durlachstraße", - "Düsseldorfer Straße", - "Düsterstraße", - "Düttelstraße", - "Düwelssiepen", - "Dyckhoffsweg", - "Ebbendorfstraße", - "Ebbinghausstraße", - "Eberstraße", - "Echeloh", - "Echtermannsweg", - "Eckardtstraße", - "Eckei", - "Eckenerstraße", - "Eddaweg", - "Edelrautenweg", - "Edelrosenstraße", - "Edelweißstraße", - "Ederstraße", - "Edingkweg", - "Eduard-Kleine-Straße", - "Egerstraße", - "Egge Wiede", - "Egilmarstraße", - "Ehmsenstraße", - "Ehrlichstraße", - "Eibenweg", - "Eichelhäherweg", - "Eichendorffstraße", - "Eichenmarkweg", - "Eichenstraße", - "Eichhoffstraße", - "Eichholzstraße", - "Eichhörnchenweg", - "Eichkamp", - "Eichlinghofer Straße", - "Eichsfeld", - "Eichwaldstraße", - "Eickensiepen", - "Eiderstraße", - "Eierkampstraße", - "Eifelweg", - "Eigenheimweg", - "Einigkeit", - "Einsteinstraße", - "Eintrachtstraße", - "Eisenacher Straße", - "Eisenhüttenweg", - "Eisenmarkt", - "Eisenstraße", - "Eisvogelweg", - "Elberskamp", - "Elbestraße", - "Elbinger Straße", - "Elchweg", - "Eleonorestraße", - "Elf Kreuzen", - "Elfenweg", - "Elisabethstraße", - "Elisabeth-Wilms-Weg", - "Ellinghauser Straße", - "Elsa-Brändstöm-Straße", - "Elsa-Brändström-Straße", - "Elsbeerenweg", - "Elsborn", - "Elverdinckweg", - "Elverfeldstraße", - "Emil-Figge-Straße", - "Emkraft", - "Emscherallee", - "Emscherbrücke", - "Emscherdelle", - "Emschergasse", - "Emscherpfad", - "Emscherpromenade", - "Emscherstraße", - "Emschertalstraße", - "Emsighofstraße", - "Emsinghofstraße", - "Enekingstraße", - "Engelbertstraße", - "Enid-Blyton-Weg", - "Enkweg", - "Ennepeweg", - "Enscheder Straße", - "Enstestraße", - "Entenpoth", - "Enzianweg", - "Erbenstraße", - "Erbstollen", - "Erdelhofstraße", - "Erdestraße", - "Erdmannstraße", - "Erenbertstraße", - "Erfurter Straße", - "Erich-Kästner-Ring", - "Erikastraße", - "Erikaweg", - "Erlemannsweg", - "Erlenbachstraße", - "Erlenkamp", - "Ermbrachtstraße", - "Ermlinghofer Straße", - "Ernestineweg", - "Ernst-Mehlich-Straße", - "Ernst-Wiechert-Straße", - "Ernteweg", - "Erpinghofstraße", - "Ertmarweg", - "Erwinstraße", - "Erzbergerstraße", - "Eschenstraße", - "Eschenwaldstraße", - "Espenstraße", - "Esselstieg", - "Essener Straße", - "Essingloh", - "Eugen-Richter-Straße", - "Eulenstraße", - "Europaplatz", - "Evastraße", - "Evertstraße", - "Evinger Berg", - "Evinger Parkweg", - "Evinger Platz", - "Evinger Straße", - "Ewald-Görshop-Straße", - "Ewald-Sprave-Weg", - "Ewaldstraße", - "Externberg", - "Exzellenzstraße", - "Ezzestraße", - "Faberstraße", - "Fächerstraße", - "Falkenstraße", - "Falläckerweg", - "Fallgatter", - "Falterweg", - "Färberstraße", - "Fasanenweg", - "Faßstraße", - "Feilerweg", - "Feineisenstraße", - "Feldahornweg", - "Feldbachacker", - "Feldbank", - "Feldblumenweg", - "Feldbrandweg", - "Feldchenstraße", - "Feldgarten", - "Feldhauskamp", - "Feldherrnstraße", - "Feldhofstraße", - "Feldmark", - "Feldstraße", - "Felheuerstraße", - "Felicitasstraße", - "Felkestraße", - "Fellhammerstraße", - "Feltmannweg", - "Ferdinandstraße", - "Fernstraße", - "Feuerbachweg", - "Feuervogelweg", - "Feuerwehr Trail", - "Fichtestraße", - "Fildeweg", - "Fine Frau", - "Finkenweg", - "Finnenbahn", - "Fischbecke", - "Flachsweg", - "Flamingoweg", - "Flaspoete", - "Flautweg", - "Flavusstraße", - "Fleckweg", - "Flegelstraße", - "Flemerskamp", - "Flensburger Straße", - "Fliederstraße", - "Flimmweg", - "Floraweg", - "Florenzer Weg", - "Florianstraße", - "Flotowstraße", - "Flözweg", - "Flügelstraße", - "Flughafenstraße", - "Flugplatz", - "Flurpeute", - "Flurstraße", - "Föbusweg", - "Fohlenkampstraße", - "Föhrenstraße", - "Försterstraße", - "Forsthausstraße", - "Frankenstraße", - "Frankfurter Straße", - "Fränkischer Friedhof", - "Franz-Hitze-Straße", - "Franziskanerstraße", - "Franziusstraße", - "Franz-Liszt-Straße", - "Französischer Weg", - "Franz-Schlüter-Straße", - "Freiastraße", - "Freiberger Weg", - "Freibergstraße", - "Freie Scholle", - "Freie-Vogel-Straße", - "Freigrafenweg", - "Freiherr-vom-Stein-Platz", - "Freihofstraße", - "Freiligrathstraße", - "Freistuhl", - "Freizeitstraße", - "Frendsdorfstraße", - "Frenkingstraße", - "Fresienstraße", - "Fridtjof-Nansen-Straße", - "Friedensplatz", - "Friedenstraße", - "Friedhof", - "Friedhofsweg", - "Friedlandweg", - "Friedrich-Ebert-Platz", - "Friedrich-Ebert-Straße", - "Friedrich-Engels-Straße", - "Friedrich-Hausemann-Allee", - "Friedrich-Henkel-Weg", - "Friedrich-Hölscher-Straße", - "Friedrich-Kohn-Straße", - "Friedrich-Loose-Straße", - "Friedrich-Menze-Weg", - "Friedrich-Naumann-Straße", - "Friedrichruher Straße", - "Friedrich-Schröder-Straße", - "Friedrichshagen", - "Friedrichstraße", - "Friedrich-Uhde-Straße", - "Friedrich-Wilhelm-Straße", - "Friedrich-Wöhler-Weg", - "Friegstraße", - "Frielinghausweg", - "Friesendorfstraße", - "Friesenstraße", - "Frischaufweg", - "Frische Luft", - "Fritz-Funke-Straße", - "Fritz-Kahl-Straße", - "Fritz-Menze-Straße", - "Fritz-Reuter-Straße", - "Fritz-Romann-Weg", - "Fröbelweg", - "Frohenort", - "Frohlinder Straße", - "Frohnauer Weg", - "Fronbotenweg", - "Fröndenbergstraße", - "Froschlake", - "Froschloch", - "Fruchtweg", - "Fuchshöhle", - "Fuchsweg", - "Fühlestraße", - "Fuhmannstraße", - "Fuhrmannstraße", - "Fuldastraße", - "Füllort", - "Fündlingsweg", - "Fürstenbergweg", - "Fürst-Hardenberg-Allee", - "Füssmannstraße", - "Gabelinckstraße", - "Gabelsbergerstraße", - "Gabelstraße", - "Gablonzstraße", - "Gahmener Straße", - "Galenstraße", - "Galmeiweg", - "Galoppstraße", - "Gänsemarkt", - "Gansmannshof", - "Gantenhals", - "Garbenweg", - "Gartenstraße", - "Gartenweg", - "Gasenbergstraße", - "Gastkamp", - "Gaudingstraße", - "Gäuseland", - "Gaußstraße", - "Gebrüder-Grimm-Straße", - "Gecks Heide", - "Gedingeweg", - "Gehrenstraße", - "Geismerg", - "Geitlingsweg", - "Geleitstraße", - "Gemeinschaftsweg", - "Genter Weg", - "Gentzweg", - "Gerader Weg", - "Geranienstraße", - "Gerberstraße", - "Gerdesweg", - "Gerhard-Hohendahl-Straße", - "Gerhart-Hauptmann-Straße", - "Gerichtsstraße", - "Gerlachweg", - "Gerlindstraße", - "Germaniastraße", - "Gernotstraße", - "Gerockstraße", - "Gersdorffstraße", - "Gerstenstraße", - "Gertrudstraße", - "Gerwinweg", - "Geschwister-Scholl-Straße", - "Gesenhofstraße", - "Geßlerstraße", - "Gevelsbergstraße", - "Gewerbeparkstraße", - "Gewerbeweg", - "Gibbenhey", - "Giesenberg", - "Gießereistraße", - "Giesweg", - "Gildenstraße", - "Ginsterstraße", - "Ginsterweg", - "Girondelle", - "Giselherstraße", - "Gitschiner Straße", - "Gladiolenstraße", - "Gleichheitsstraße", - "Gleiwitzsstraße", - "Gleiwitzstraße", - "Glennestieg", - "Glimmerstraße", - "Glockengießerweg", - "Glockenwiese", - "Glörstraße", - "Glückaufsegenstraße", - "Glückaufstraße", - "Gnadenort", - "Gneisenaustraße", - "Gnesener Weg", - "Godefriedstraße", - "Godekinstraße", - "Goebenstraße", - "Goerdelerstraße", - "Goethestraße", - "Goldammerweg", - "Goldschmiedingweg", - "Golfplatz Wurzel Trail", - "Göllenkamp", - "Görresstraße", - "Gosestraße", - "Goslarstraße", - "Gössingstraße", - "Goswinweg", - "Gotenstraße", - "Gottesbergstraße", - "Gotthelfstraße", - "Gottlieb-Levermann-Straße", - "Gottscheestraße", - "Goyenfeld", - "Goystraße", - "Grabbeplatz", - "Grabbestraße", - "Gradigenweg", - "Graebnerstraße", - "Grafenhof", - "Graffweg", - "Graf-Haeseler-Straße", - "Graf-Konrad-Straße", - "Grasenkamp", - "Grasmückenweg", - "Gratzstraße", - "Graudenzer Straße", - "Grävingholzstraße", - "Gregorstraße", - "Grenzweg", - "Gretelweg", - "Greveler Straße", - "Grevendicks Feld", - "Grevenhecke", - "Griebweg", - "Griechischer Weg", - "Grimbergsweg", - "Grimmeweg", - "Grisarstraße", - "Grollmannsweg", - "Gronaustraße", - "Gröningweg", - "Groppenbrucher Straße", - "Gröpperheide", - "Großbeerenstraße", - "Große Heimstraße", - "Große Riedbruchstraße", - "Großholthauser Straße", - "Grotenbachstraße", - "Grotenkamp", - "Grotestraße", - "Grothusweg", - "Grubenweg", - "Gruelsiepenstraße", - "Grüggelsort", - "Grummetweg", - "Grünbergstraße", - "Grundstraße", - "Grüne Straße", - "Grüner Bogen", - "Grüner Weg", - "Grunewald", - "Grüningsweg", - "Grüntalstraße", - "Gruwellstraße", - "Gudrunstraße", - "Gugelweg", - "Güldene Eiche", - "Gundlachstraße", - "Güntherstraße", - "Gurlittstraße", - "Gürtlerstraße", - "Gustav-Korthen-Allee", - "Gustav-Schade-Weg", - "Gustavstraße", - "Gutenbergstraße", - "Gut-Heil-Straße", - "Haardtstraße", - "Haarstrang", - "Haberkamps Vöhde", - "Haberlandstraße", - "Habichtstraße", - "Habichtweg", - "Hacheneyer Kirchweg", - "Hacheneyer Straße", - "Hackländerplatz", - "Hadubrandstraße", - "Haenischstraße", - "Haferfeldstraße", - "Hafer-Vöhde", - "Hafnerstraße", - "Hagebuttenweg", - "Hagenauweg", - "Hagener Straße", - "Hahnenmühlenweg", - "Hainallee", - "Hainbuchenweg", - "Hakenstraße", - "Haldenstraße", - "Halfmannstraße", - "Hallerey", - "Hallermannstraße", - "Hallesche Straße", - "Halmweg", - "Hamburger Straße", - "Hamelmannstraße", - "Hamey", - "Hammer Straße", - "Hampittelknapp", - "Hamsterweg", - "Händelstraße", - "Handweiserstraße", - "Hanebeckstraße", - "Hänflingweg", - "Hangeneystraße", - "Hangstraße", - "Hannöversche Straße", - "Hansaplatz", - "Hansastraße", - "Hansbergstraße", - "Hänselweg", - "Hansemannstraße", - "Hans-Grüning-Weg", - "Hans-Holbein-Straße", - "Hans-Litten-Straße", - "Hansmannstraße", - "Hans-Peters-Straße", - "Hardenackerweg", - "Hardenbergstraße", - "Harkortshof", - "Harkortstraße", - "Harkortweg", - "Harnackstraße", - "Harpener Hellweg", - "Hartwinkel", - "Harzweg", - "Haselhoffstraße", - "Hasenberg", - "Hasenkamp", - "Haslindestraße", - "Hatzfeldstraße", - "Haubachstraße", - "Hauerstraße", - "Hauert", - "Haumannstraße", - "Hauptfeld", - "Hausdorfstraße", - "Häuskenweg", - "Hausmannstraße", - "Havelandsheck", - "Haydnstraße", - "Hebelenhof", - "Hebelerweg", - "Heckelbeckstraße", - "Heckelingweg", - "Heckenstraße", - "Hedingsmorgen", - "Hedwigstraße", - "Hegemanns Heide", - "Heideblick", - "Heidekopf", - "Heidekrugweg", - "Heideweg", - "Heidstrang", - "Heiduferweg", - "Heilbronner Straße", - "Heiligegartenstraße", - "Heiliger Weg", - "Heilsberger Weg", - "Heimannstrße", - "Heimbaustraße", - "Heimbrügge", - "Heimsenstraße", - "Heimstättenweg", - "Heino-Brauckhoff-Weg", - "Heinrich-August-Schulte-Straße", - "Heinrich-Hertz-Straße", - "Heinrich-Mann-Straße", - "Heinrich-Munsbeck-Straße", - "Heinrich-Pieper-Straße", - "Heinrich-Staubach-Straße", - "Heinrich-Stephan-Straße", - "Heinrichstraße", - "Heinrich-Sträter-Straße", - "Heinstück", - "Heisenbergstraße", - "Heisterkamp", - "Heisterstraße", - "Heiterkeitsweg", - "Heitkampstraße", - "Helene-Meiser-Weg", - "Helenenbergweg", - "Helenenstraße", - "Helene-Wessel-Straße", - "Helgaweg", - "Helgoland", - "Heliosweg", - "Helle", - "Hellenbank", - "Hellerstraße", - "Helmutstraße", - "Helsinkistraße", - "Hengsener Straße", - "Hengstenbergweg", - "Hengsteystraße", - "Hennetwiete", - "Henningsweg", - "Henriettenweg", - "Heraweg", - "Herbersknapp", - "Herbert-Frommberger-Weg", - "Herderstraße", - "Herdstraße", - "Heribertstraße", - "Heringenstraße", - "Herkulesstraße", - "Hermann-Löns-Straße", - "Hermannstraße", - "Hermelinweg", - "Hermelskamp", - "Hermerichweg", - "Herner Straße", - "Heroldstraße", - "Herpersbusch", - "Herrekestraße", - "Herrenstraße", - "Herrenwiesenstraße", - "Hertastraße", - "Herwingweg", - "Hesselkamp", - "Hessenbank", - "Hesseweg", - "Hessische Straße", - "Heßlingsweg", - "Hesternweg", - "Heuerlingsweg", - "Heunerstraße", - "Heuweg", - "Hevesteige", - "Heydbrekenstraße", - "Heyden-Rynsch-Straße", - "Heyerstraße", - "Heyneckenweg", - "Hiddingstraße", - "Hildastraße", - "Hildebrandstraße", - "Hildegundweg", - "Hildesheimer Straße", - "Hilgenloh", - "Hilgenstockstraße", - "Hillermannweg", - "Hiltropwall", - "Himpendahlweg", - "Hinnenberg", - "Hinter dem Garten", - "Hinter der Wiese", - "Hinter Holtein", - "Hintere Schildstraße", - "Hinterer Remberg", - "Hirschweg", - "Hirtenstraße", - "Hobestadt", - "Hochfelder Straße", - "Hochofenstraße", - "Höchstener Straße", - "Hochstraße", - "Hochwaldstraße", - "Hoddenfeld", - "Hoeteweg", - "Hofeswiese", - "Hofgerichtsweg", - "Höfkerstraße", - "Hofstadtweg", - "Hofstraße", - "Hohbrinkstraße", - "Hohe Braukstraße", - "Hohe Luft", - "Hohe Straße", - "Hohenfriedberger Straße", - "Hohensyburgstraße", - "Höhenweg", - "Hohenzollernstraße", - "Hoher Wall", - "Höhfuhr", - "Hohle Eiche", - "Hollandstraße", - "Hollestraße", - "Hollmannstraße", - "Holsteiner Straße", - "Holtbeuteweg", - "Holtbrügge", - "Holter Weg", - "Holtestraße", - "Holthauser Straße", - "Holtingsweg", - "Holtkottenweg", - "Holtzplatz", - "Holunderweg", - "Holzener Straße", - "Holzener Weg", - "Holzerweg", - "Holzheck", - "Holzrichterweg", - "Holzwickeder Straße", - "Hombrucher Straße", - "Hönnestieg", - "Hopfenstraße", - "Hopmanns Mühlenweg", - "Hordemannshof", - "Hörder Bahnhofstraße", - "Hörder Bruch", - "Hörder Kämpchen", - "Hörder Kampweg", - "Hörder Neumarkt", - "Hörder Rathausstraße", - "Hörder Semerteichstraße", - "Hörder Straße", - "Hörigstraße", - "Horstmarer Straße", - "Hortensienstraße", - "Hosbachstraße", - "Hospitalstraße", - "Hostedder Heide", - "Hostedder Straße", - "Hövelstraße", - "Hövischestraße", - "Hubertusstraße", - "Hubertusweg", - "Huckarder Allee", - "Huckarder Bruch", - "Huckarder Heide", - "Huckarder Hölzchen", - "Huckarder Straße", - "Hückerstraße", - "Hueckstraße", - "Huestraße", - "Hufelandstraße", - "Hügelstraße", - "Hugo-Heimsath-Straße", - "Hugo-Pork-Straße", - "Hugo-Sickmann-Straße", - "Hugostraße", - "Hüllbergstraße", - "Hülsenbuschstraße", - "Hülshof", - "Hultschiner Straße", - "Humboldtstraße", - "Hummelbank", - "Humperdinckweg", - "Hünefeldstraße", - "Hunnentränke", - "Hünninghausstraße", - "Hunoltstraße", - "Husarenstraße", - "Husemannstraße", - "Husener Eichwaldstraße", - "Husener Straße", - "Hüsingheide", - "Hutererweg", - "Hüttemannstraße", - "Hüttenbruchweg", - "Hüttenhospitalstraße", - "Hüttenstraße", - "Hüttnerstraße", - "Hyazinthenstraße", - "I. Bickestraße", - "Ibbebbürenstraße", - "Ibbenbürenstraße", - "Idastraße", - "Igelweg", - "Iggelhorst", - "Ihlanden", - "II. Bickestraße", - "Iltisweg", - "Im Apen", - "Im Aufferoth", - "Im Bromkamp", - "Im Bruch", - "Im Buschholz", - "Im Dahl", - "Im Defdahl", - "Im Dorfe", - "Im Dorloh", - "Im Dreieck", - "Im Eck", - "Im Grubenfeld", - "Im Heidegrund", - "Im Heidewinkel", - "Im Hilger", - "Im Honigstal", - "Im Horst", - "Im Hundeswinkel", - "Im Kallenrott", - "Im Karrenberg", - "Im Löken", - "Im Odemsloh", - "Im Orde", - "Im Ostfeld", - "Im Papenkamp", - "Im Rabenloh", - "Im Rauhen Holz", - "Im Rübel", - "Im Schellenkai", - "Im Schlingen", - "Im Siepen", - "Im Siesack", - "Im Spähenfelde", - "Im Sperrfeld", - "Im Streitfeld", - "Im Telgei", - "Im Velm", - "Im Weidkamp", - "Im Weißen Feld", - "Im Wiengarten", - "Im Wiesengrund", - "Im Wiesenkamp", - "Imbuschweg", - "Imckebank", - "Imigstraße", - "Immanuel-Kant-Straße", - "Immermannstraße", - "In den Böcken", - "In den Börten", - "In den Breen", - "In den Erlen", - "In den Hüchten", - "In den Kämpen", - "In den Ostgärten", - "In den Stämmen", - "In den Weidbüschen", - "In den Westenkempen", - "In der Esche", - "In der Fühle", - "In der Großen Heide", - "In der Heide", - "In der Liethe", - "In der Lohwiese", - "In der Meile", - "In der Mulde", - "In der Oeverscheidt", - "In der Rünsterte", - "In der Schmechting", - "In der Teufe", - "In der Wollmei", - "Innsbruckstraße", - "Inselstraße", - "Insterburger Straße", - "Intückenweg", - "Irischer Weg", - "Irisstraße", - "Irmgardstraße", - "Irminsulstraße", - "Italienischer Weg", - "Jadeweg", - "Jagdhausstraße", - "Jägerstraße", - "Jägerweg", - "Jasminstraße", - "Joachim-Neander-Straße", - "Joachimstraße", - "Johanna-Melzer-Straße", - "Johanna-Spyri-Weg", - "Johannes-Gronowski-Straße", - "Johannes-Menne-Weg", - "Johannesstraße", - "Johann-Heckeroth-Straße", - "Johannisbergstraße", - "Johannisborn", - "Jonathanstraße", - "Joseph-Cremer-Straße", - "Joseph-Scherer-Straße", - "Josephstraße", - "Joseph-von-Fraunhofer-Straße", - "Jubachweg", - "Juchostraße", - "Jülicher Straße", - "Juliusstraße", - "Julius-Vogel-Straße", - "Jungferntalstraße", - "Junggesellenstraße", - "Jung-Stilling-Weg", - "Junoweg", - "Jupiterstraße", - "Jürgensstraße", - "Justusweg", - "K 3", - "Kafkastraße", - "Kahle Hege", - "Kahlsiepe", - "Kaiseradlerweg", - "Kaiserstraße", - "Kaldehofweg", - "Kaldernstraße", - "Kalmeichweg", - "Kamener Straße", - "Kameradschaftsweg", - "Kamergstraße", - "Kammerstück", - "Kämpchenstraße", - "Kamphecke", - "Kampmannsweg", - "Kampstraße", - "Kanalstraße", - "Kannengießerweg", - "Kanzlerstraße", - "Kapellenstraße", - "Kapellenweg", - "Kapitelwiese", - "Kappenberger Straße", - "Karbonweg", - "Karinstraße", - "Karl-Brühne-Weg", - "Karl-Ernst-Straße", - "Karl-Funke-Straße", - "Karl-Harr-Straße", - "Karl-Klose-Weg", - "Karl-Liebknecht-Straße", - "Karl-Marx-Straße", - "Karl-Prümer-Straße", - "Karl-Rübel-Straße", - "Karlsbader Straße", - "Karlsbank", - "Karl-Schwartz-Straße", - "Karlsglückstraße", - "Karlsruhestraße", - "Karl-Wenk-Straße", - "Karl-Zahn-Straße", - "Kärntenstraße", - "Karolinenstraße", - "Karrenpad", - "Kaspar-Schulte-Straße", - "Kasseler Straße", - "Kastanienallee", - "Kastanienplatz", - "Kastanienstraße", - "Katharinenstraße", - "Käthe-Kollwitz-Straße", - "Käthe-Schaub-Weg", - "Kattenkuhle", - "Kattenstert", - "Kattenstraße", - "Kattowitzstraße", - "Katzbachstraße", - "Kaubomstraße", - "Kaubstraße", - "Kautskystraße", - "Kavernenweg", - "Kebbestraße", - "Kehrbrock", - "Keilhausstraße", - "Keinstraße", - "Keldermannweg", - "Kellerstraße", - "Keltenstraße", - "Kemminghauser Straße", - "Kemnaderweg", - "Keplerstraße", - "KercKeringswiese", - "Kerschensteinerstraße", - "Kesselborn", - "Kesselstraße", - "Kettelerweg", - "Kettlerskamp", - "Kiebitzweg", - "Kieferstraße", - "Kiefholz", - "Kielstraße", - "Kiewitzweg", - "Kinkelstraße", - "Kippstraße", - "Kipsburg", - "Kirchbruchstraße", - "Kirchderner Straße", - "Kirchenkamp", - "Kirchenstraße", - "Kirchhörder Berg", - "Kirchhörder Kopf", - "Kirchhörder Straße", - "Kirchlinder Feld", - "Kirchlinder Straße", - "Kirchnerstraße", - "Kirschbaumweg", - "Klarastraße", - "Kleiberweg", - "Kleine Beurhausstraße", - "Kleine Brücke", - "Kleine Burgholzstraße", - "Kleine Gildenstraße", - "Kleine Grisarstraße", - "Kleine Heide", - "Kleine Herderstraße", - "Kleine Kielstraße", - "Kleine Kleiststraße", - "Kleine Riedbruchstraße", - "Kleine Rückertstraße", - "Kleine Schwerter Straße", - "Kleine Uhlandstraße", - "Kleine Wannestraße", - "Kleiner Floraweg", - "Kleiner Pfad", - "Kleiner Schewenort", - "Kleiner Waldhausweg", - "Kleiststraße", - "Klemptweg", - "Kleppingstraße", - "Klever Straße", - "Kleveskamp", - "Kleybergstraße", - "Kleybredde", - "Kleyer Dorfstraße", - "Kleyer Feld", - "Kleyer Weg", - "Kleymannsweg", - "Klink", - "Klobesstraße", - "Klöcknerstraße", - "Klönnestraße", - "Klöppelweg", - "Klosterbredde", - "Klosterstraße", - "Klüsenerskamp", - "Klusenweg", - "Klütingweg", - "Knappenstraße", - "Knappmannweg", - "Knappstraße", - "Knauerweg", - "Kneebuschstraße", - "Knospenweg", - "Knyphausenstraße", - "Kobbendelle", - "Kocklinckeweg", - "Koerstarße", - "Kohlenbankweg", - "Kohlensiepenstraße", - "Kohlgartenstraße", - "Kohlweißlingsweg", - "Kokshohlweg", - "Kölbestraße", - "Kolbstraße", - "Kolibriweg", - "Kolingestraße", - "Kolmarer Straße", - "Köln-Berliner-Straße", - "Kolpingstraße", - "Kometenstraße", - "König Grätzer Straße", - "Königsbergstraße", - "Königshalt", - "Königsheide", - "Königshüttestraße", - "Königs-Sunden", - "Königstraße", - "Königswall", - "Konrad-Adenauer-Allee", - "Konrad-Glocker-Straße", - "Konradstraße", - "Könzgenstraße", - "Kopernikusstraße", - "Koppelweg", - "Koppweg", - "Korallenweg", - "Körfken", - "Kornacker", - "Kornblumenstraße", - "Körnebachstraße", - "Korneliusstraße", - "Körner Grund", - "Körner Hellweg", - "Körnerstraße", - "Kornweg", - "Korte Geitke", - "Kortenstraße", - "Kortental", - "Körtingsweg", - "Kortschstraße", - "Kortumweg", - "Kosselstraße", - "Kösterstraße", - "Kötterkamp", - "Kötterweg", - "Kraepelinweg", - "Krähenbruch", - "Krählweg", - "Krämerbank", - "Krampelo", - "Kräutergarten", - "Kreftenscher", - "Kreigershofstraße", - "Kreisstraße", - "Kressenweg", - "Kreuzheide", - "Kreuzstraße", - "Kreyenbachweg", - "Kriemhildstraße", - "Krimstraße", - "Krinkelbach", - "Krinkelweg", - "Krokusweg", - "Kronenburgallee", - "Kronenstraße", - "Kronprinzenstraße", - "Kruckeler Straße", - "Kruckelhoek", - "Krückenweg", - "Kuckelbusch", - "Kuckelke", - "Kückshauser Straße", - "Kuckucksweg", - "Kühlingstraße", - "Kühlkamp", - "Kuhlmannstraße", - "Kühnstraße", - "Kuhstraße", - "Kuithanstraße", - "Kükenhöhlerweg", - "Kullenberg", - "Kullrichstraße", - "Kümper Heide", - "Kümperheide", - "Kumpstraße", - "Kunibertstraße", - "Kuntzestraße", - "Küpenweg", - "Küpferstraße", - "Kuppenweg", - "Kurfürstenstraße", - "Kurler Straße", - "Kurze Hecke", - "Kurze Heed", - "Kurze Reihe", - "Kurze Straße", - "Kurzer Morgen", - "Kußnachtstraße", - "Küsterkamp", - "Kymnastraße", - "Labandstraße", - "Lachsweg", - "Lachterweg", - "Lagerhausstraße", - "Lambachstraße", - "Lambergstraße", - "Landgrafenstraße", - "Landmannweg", - "Landoisweg", - "Landskroner Straße", - "Landwehrstraße", - "Lange Fuhr", - "Lange Hecke", - "Lange Heed", - "Lange Reihe", - "Lange Straße", - "Lange Wiese", - "Langeloh", - "Langenacker", - "Langerohstraße", - "Langobardenstraße", - "Langschedestraße", - "Lanstroper Straße", - "Lantfridweg", - "Lappenbergsbank", - "Lappenkreutz", - "Laubsängerweg", - "Lauenburger Straße", - "Lauestraße", - "Lautastraße", - "Lavendelweg", - "Leberstraße", - "Lechlohweg", - "Lehmkuhle", - "Lehmufer", - "Lehnemannsweg", - "Lehnertweg", - "Leibnizstraße", - "Leideckerweg", - "Leierweg", - "Leiloh", - "Leinbergerstraße", - "Leineweberstraße", - "Leineweg", - "Leipziger Straße", - "Leisse", - "Leitmeritzstraße", - "Lemberger Feld", - "Lenderichstraße", - "Leneckeweg", - "Lenhoffweg", - "Lenigheck", - "Leni-Rommel-Straße", - "Lenneweg", - "Lenninghausstraße", - "Lensingstraße", - "Lenteninsel", - "Leonhard-Euler-Straße", - "Leopold-Schütte-Straße", - "Leopoldstraße", - "Leostraße", - "Leppinghof", - "Lerchenweg", - "Lessenstraße", - "Lessingstraße", - "Leßnerweg", - "Lesumstraße", - "Leukelwiese", - "Leunenschloßstraße", - "Leuschnerstraße", - "Leuthardstraße", - "Leveringstraße", - "Leythestraße", - "Liboristraße", - "Lichtendorfer Straße", - "Liebermannstraße", - "Liebfrauenstraße", - "Liebigstraße", - "Liedweg", - "Liegendanfahrt", - "Liesemorgen", - "Liethschulteweg", - "Ligusterweg", - "Lilienthalstraße", - "Limbecker Postweg", - "Limbecker Straße", - "Limburger Postweg", - "Lina-Schäfer-Straße", - "Lindberghstraße", - "Lindbreiteweg", - "Lindemannstraße", - "Lindenhorster Straße", - "Lindenstraße", - "Lindentalweg", - "Lindstraße", - "Linienstraße", - "Linkestraße", - "Linneweberstraße", - "Linnigmannstraße", - "Lippestraße", - "Lippmannstraße", - "Lippstädter Straße", - "Lissaboner Allee", - "Listertwiete", - "Littgenloh", - "Littweg", - "Lodemannsweg", - "Lohacker", - "Lohheide", - "Lohkampweg", - "Lohoffstraße", - "Londoner Bogen", - "Lorenweg", - "Lorzingstraße", - "Lotenkamp", - "Lotharstraße", - "Lotosweg", - "Lots Siepen", - "Löttringhauser Straße", - "Löwenstraße", - "Lowenthal", - "Lübbertweg", - "Lübbringweg", - "Lübckerhofstraße", - "Lübecker Straße", - "Lubigweg", - "Lübkestraße", - "Ludeckeweg", - "Lüdinghauser Straße", - "Ludolfweg", - "Ludwig-Lohner-Straße", - "Ludwigstraße", - "Lueckestraße", - "Luegstraße", - "Luerwaldstraße", - "Luftschacht", - "Lugaustraße", - "Lugierstraße", - "Lührmannstraße", - "Luisenglück", - "Luisenhoffnung", - "Luisenplatz", - "Luisenschachtstraße", - "Luisenstraße", - "Luise-Rinser-Weg", - "Lüneburger Straße", - "Lünener Straße", - "Lunestraße", - "Luninkhofstraße", - "Lupinenweg", - "Lüserbachstraße", - "Lütge Brückstraße", - "Lütge Heidestraße", - "Lütge Vöhde", - "Lütgendortmunder Hellweg", - "Lütgendortmunder Straße", - "Lütgenholthauser Straße", - "Lütgenholz", - "Lüttenwiese", - "Lützowstraße", - "Machariusstraße", - "Mackenrothweg", - "Magdalenenstraße", - "Magdeburger Straße", - "Mahlscheidt", - "Maienweg", - "Maiglöckchenweg", - "Mailoh", - "Mallinckrodtstraße", - "Malmessiepen", - "Malritzstraße", - "Malterweg", - "Malvenweg", - "Malzstraße", - "Mannheimplatz", - "Mansfeldstraße", - "Manteuffelstraße", - "Märchenweg", - "Marderweg", - "Margaretenstraße", - "Maria-Block-Sraße", - "Maria-Goeppert-Mayer-Straße", - "Mariannenstraße", - "Marie-Juchacz-Straße", - "Marienbader Straße", - "Marienborn", - "Marienburger Weg", - "Marienstraße", - "Markbauernstraße", - "Markenhudeweg", - "Markenwaldweg", - "Märker Feld", - "Märker Grund", - "Märker Höhe", - "Markgrafenstraße", - "Markhege", - "Märkische Straße", - "Markscheiderstraße", - "Marksweg", - "Markt", - "Marktplatz", - "Markusstraße", - "Marsbruchstraße", - "Marschallstraße", - "Marsstraße", - "Martener Brücke", - "Martener Hellweg", - "Martener Straße", - "Martha-Gillessen-Straße", - "Martha-Neumann-Straße", - "Marthastraße", - "Martin-Schmeißer-Platz", - "Martin-Schmeißer-Weg", - "Martinstraße", - "Märtmannstraße", - "Massener Hellweg", - "Massener Straße", - "Massener Weg", - "Massenezstraße", - "Materna Parkplatz", - "Mathildenstraße", - "Matilda-Wrede-Straße", - "Matthias-Grünewald-Straße", - "Mattlacke", - "Maulwurfsweg", - "Maurice-Vast-Straße", - "Max-Brandes-Straße", - "Max-Brod-Straße", - "Max-Eyth-Straße", - "Maximiliam-Kolbe-Straße", - "Max-Ophüls-Platz", - "Mc Drive", - "Mechtildstraße", - "Mehlbeerenweg", - "Meinbergstraße", - "Meinertstraße", - "Meinhardstraße", - "Meininghausstraße", - "Meisenweg", - "Meißener Straße", - "Meitnerweg", - "Melanchthonstraße", - "Melchtalstraße", - "Melscheder Weg", - "Memeler Straße", - "Mendestraße", - "Mengeder Markt", - "Mengeder Schulstraße", - "Mengeder Straße", - "Menglinghauser Straße", - "Mentlerstraße", - "Merckenbuschweg", - "Mergelkopfweg", - "Mergelkuhle", - "Mergelteichstraße", - "Merklinder Straße", - "Merkurstraße", - "Messelinckstraße", - "Mettestraße", - "Metzer Straße", - "Meuselwitzstraße", - "Meylantstraße", - "Michael-Ende-Straße", - "Michael-Holzach-Weg", - "Michaelstraße", - "Milanweg", - "Milchgasse", - "Millkottenweg", - "Mimosenweg", - "Mindener Straße", - "Minister-Stein-Allee", - "Minoritenstraße", - "Missundestraße", - "Mittelstraße", - "Mittelweg", - "Möckernstraße", - "Mödershof", - "Möhneweg", - "Mohnweg", - "Molenarkweg", - "Molkereistraße", - "Mollenacker", - "Möllenbeckstraße", - "Möllenhoffstraße", - "Möllerstraße", - "Mollwitzer Straße", - "Molnerweg", - "Moltkestraße", - "Mommsenweg", - "Mönchengang", - "Mönchenwordt", - "Mondstraße", - "Mönninghofstraße", - "Mooskamp", - "Morgenstraße", - "Moritzgasse", - "Morterstraße", - "Mortmannshof", - "Moskauer Straße", - "Mosselde", - "Mountainbike-Hügel", - "Mozartstraße", - "Muddepenningweg", - "Mühlackerstraße", - "Mühlenstraße", - "Müllerstraße", - "Mulmannweg", - "Münsterstraße", - "Müserstraße", - "Museumsgasse", - "Muspelheimstraße", - "Myrtenweg", - "Nachoder Straße", - "Nachtigallenweg", - "Nackhofstraße", - "Nagelpötchen", - "Nagelschmiedgasse", - "Narzissenstraße", - "Nasses Holz", - "Nathebachstraße", - "Natherweg", - "Nathmerichstraße", - "Natorpweg", - "Naturfreundehaus Downhill", - "Nauenstraße", - "Neben dem Brand", - "Nebenbruch", - "Nederhoffstraße", - "Neißestraße", - "Nelkenstraße", - "Nelly-Sachs-Straße", - "Neptunstraße", - "Nertusstraße", - "Nervierstraße", - "Nerzweg", - "Nettelbeckstraße", - "Neu-Crengeldanz-Straße", - "Neue Dorfstraße", - "Neue Ringstraße", - "Neue Sendstraße", - "Neue Tremoniastraße", - "Neuer Graben", - "Neuflöz", - "Neuhammerweg", - "Neuhoffstraße", - "Neu-Iserlohn-Straße", - "Neulandplatz", - "Neulandstraße", - "Neumarkstraße", - "Neunkirchenstraße", - "Neurodestraße", - "Niederadener Straße", - "Niederhofener Straße", - "Niederhofer Holz", - "Niederhofer Kohlenweg", - "Niedernetter Straße", - "Niedersachsenweg", - "Niederste Feldweg", - "Niergartenstraße", - "Nierhausstraße", - "Nierstefeldstraße", - "Nießstraße", - "Nixenweg", - "Nollendorfplatz", - "Nollendorfstraße", - "Nordblick", - "Nordbruch", - "Nordmarkt", - "Nordstraße", - "Nortkirchenstraße", - "Noskestraße", - "Nöthenort", - "Nottebaumweg", - "Notweg", - "NS IX", - "Nußbaumweg", - "Ob der Kolmke", - "Oberadener Straße", - "Oberbank", - "Oberbecker Straße", - "Oberdelle", - "Oberdorfstraße", - "Obere Brinkstraße", - "Obere Egge", - "Obere Gartenstraße", - "Obere Hangstraße", - "Obere Pekingstraße", - "Oberevinger Straße", - "Oberfeldstraße", - "Oberhausstraße", - "Oberholte", - "Obermarkstraße", - "Obernetter Straße", - "Obernkirchenstraße", - "Oberschlesierstraße", - "Oberste Kamp", - "Oberste-Wilms-Straße", - "Oderstraße", - "Oelmühlenweg", - "Oerfeld", - "Oespeler Dorfstraße", - "Oespeler Kirchweg", - "Oesterholzstraße", - "Oestermärsch", - "Oesterstraße", - "Oestricher Straße", - "Oetringhauser Straße", - "Oleanderweg", - "Oleariusstraße", - "Olgastraße", - "Olof-Palme-Straße", - "Olpe", - "Olpketalstraße", - "Olufsweg", - "Orchideenweg", - "Ordalweg", - "Orensteinstraße", - "Örlinghauser Weg", - "Örlingweg", - "Ortdieck", - "Ortelsburger Weg", - "Ortfeld", - "Ortli", - "Ortsmühle", - "Ortwinkel", - "Oskar-Wachtel-Weg", - "Oskarweg", - "Oslostraße", - "Osningstraße", - "Ostberger Feldweg", - "Ostberger Straße", - "Ostenbergstraße", - "Ostenhellweg", - "Ostenschleifweg", - "Osterfeldstraße", - "Osterkuhle", - "Osterlandwehr", - "Ostermannstraße", - "Osterrothweg", - "Osterschleppweg", - "Osterymweg", - "Ostfalenstraße", - "Osthoffstraße", - "Ostholzstraße", - "Ostkirchstraße", - "Ostwall", - "Oswaldstraße", - "Otto-Brenner-Weg", - "Otto-Hahn-Straße", - "Ottostraße", - "Ottweilerstraße", - "Ötztaler Straße", - "Ovelackerstraße", - "Overbeck", - "Overbeckstraße", - "Overgünne", - "Overhoffstraße", - "Pacellistraße", - "Paderborner Straße", - "Pallandtweg", - "Papenacker", - "Papengasse", - "Papenhofskamp", - "Pappelstraße", - "Paradiesstraße", - "Parkplatzstraße", - "Parkstraße", - "Parsevalstraße", - "Paschknappstraße", - "Päßchen", - "Passmannweg", - "Passweg", - "Pastoratsweg", - "Patroklusweg", - "Paul-Gerhardt-Straße", - "Paulinenstraße", - "Paul-Winzen-Straße", - "Peddenbrink", - "Penningskamp", - "Pentelingstraße", - "Pepperstraße", - "Persebecker Straße", - "Peschweg", - "Pestalozzistraße", - "Peter-Florenz-Weddingen-Straße", - "Petergasse", - "Peter-Hille-Straße", - "Petermannsweg", - "Peter-Paul-Rubens-Straße", - "Petershagenstraße", - "Petrikirchhof", - "Petrystraße", - "Pfahlstück", - "Pfarrer-Barheine-Weg", - "Pfarrer-Beule-Weg", - "Pfarrer-Klinzing-Weg", - "Pfarrer-Kneipp-Straße", - "Pfarrstraße", - "Pfauenaugenweg", - "Phönixstraße", - "Piepenbrink", - "Piepenstockplatz", - "Piepenstockstraße", - "Pilgermannsweg", - "Pinienweg", - "Pirolweg", - "Planckstraße", - "Planetenfeldstraße", - "Plaßstraße", - "Platanenweg", - "Platz der Alten Synagoge", - "Platz von Hiroshima", - "Platz von Leeds", - "Platz von Rostow am Don", - "Plauener Straße", - "Pleckenbrinck", - "Pleckenbrink", - "Plettenbergstraße", - "Plümers Ort", - "Plutoweg", - "Pohlmannweg", - "Pöllerstraße", - "Polliusweg", - "Poppelsdorfer Straße", - "Portmannsweg", - "Portugiesischer Weg", - "Posener Straße", - "Posthornweg", - "Postkutschenstraße", - "Poststraße", - "Postweg", - "Potacker", - "Potgasse", - "Pothecke", - "Pothmorgenweg", - "Pottenkamp", - "Potthöferei", - "Prager Weg", - "Preinstraße", - "Prellerstraße", - "Preußische Straße", - "Priesterwiese", - "Primelstraße", - "Prinzenstraße", - "Prinz-Friedrich-Karl-Straße", - "Priorstraße", - "Probstheidastraße", - "Pröbstingkamp", - "Propsteihof", - "Provinzialstraße", - "Prüferweg", - "Pruzzenweg", - "Pulverstraße", - "Putmanstraße", - "Püttbeckenstraße", - "Püttlingenstraße", - "Püttweg", - "Pyrmonter Straße", - "Quadbeckstraße", - "Quakmannsweg", - "Quartlenbeckstraße", - "Quarzweg", - "Querstraße", - "Rabboltstraße", - "Rabenstraße", - "Rahestraße", - "Rählwiese", - "Rahmer Straße", - "Rahmkesweg", - "Rahmsloher Weg", - "Raiderweg", - "Ramhofstraße", - "Rankenweg", - "Rappäusweg", - "Rapunzelweg", - "Rastenburger Straße", - "Rathenaustraße", - "Rathoffsweg", - "Raudestraße", - "Rauher Dorn", - "Rauher Kamp", - "Raulfskamp", - "Rauschenbuschstraße", - "Raveike", - "Ravensberger Straße", - "Ravensweg", - "Rebhuhnweg", - "Reckerdingstraße", - "Recklinghauser Straße", - "Redtenbacherstraße", - "Regenpfeiferweg", - "Reichenberger Straße", - "Reichmarkstraße", - "Reichshofstraße", - "Reichsmarkstraße", - "Reichswehrstraße", - "Reiherhorst", - "Reiner-Daelen-Straße", - "Reinickendorfer Weg", - "Reinoldistraße", - "Reinwardtstraße", - "Reiserstraße", - "Reitweg", - "Remigiusstraße", - "Remigustrße", - "Rennweg", - "Resedastraße", - "Revierstraße", - "Rhader Weg", - "Rhedeweg", - "Rheinische Straße", - "Rheinischer Esel", - "Rheinlanddamm", - "Rhönweg", - "Richardstraße", - "Richard-Wagner-Straße", - "Richtersdorfstraße", - "Richterstraße", - "Richtsteig", - "Riesestraße", - "Riewepläßken", - "Rigwinstraße", - "Rindenstraße", - "Ringeloh", - "Ringelohstraße", - "Ringofenstraße", - "Ringstraße", - "Rinscheweg", - "Rispenstraße", - "Ristweg", - "Ritsartweg", - "Rittershausstraße", - "Rittershofer Straße", - "Ritterstraße", - "Robert-Koch-Straße", - "Robertstraße", - "Robinienweg", - "Röddingsbaumweg", - "Rodenbergstraße", - "Roggenkamp", - "Rohdesdiek", - "Roholte", - "Röhrsteige", - "Rohwedderstraße", - "Rolandstraße", - "Rolevinckstraße", - "Rombergstraße", - "Römermorgen", - "Römerstraße", - "Römerweg", - "Roningweg", - "Röntgenstraße", - "Roonheide", - "Roonstraße", - "Rosa-Luxemburg-Straße", - "Roseggerstraße", - "Rosemeyerstraße", - "Rosenowstraße", - "Rosenstraße", - "Rosental", - "Rosmarinweg", - "Roßbachstraße", - "Rösselmannstraße", - "Rotariusstraße", - "Rotbuchenweg", - "Rotdornallee", - "Rotdornweg", - "Rote Fuhr", - "Rote-Becker-Straße", - "Roter Morgen", - "Roter Weg", - "Rotgerweg", - "Rothöfstraße", - "Rotkäppchenweg", - "Rotkehlchenweg", - "Röttgersbank", - "RTW/KTW", - "Rubbertweg", - "Rübenkamp", - "Rübenstraße", - "Rübezahlweg", - "Rubinstraße", - "Ruckebierstraße", - "Rückertstraße", - "Rüdigerstraße", - "Rüdinghauser Straße", - "Rügecke", - "Ruhfusstraße", - "Ruhrallee", - "Ruhrschnellweg", - "Ruhrwaldstraße", - "Ruinenstraße", - "Rumpstraße", - "Rundstraße", - "Rupinghofstraße", - "Rüschebrinkstraße", - "Rüschenstraße", - "Rüsterweg", - "Ruthgerusstraße", - "Ruthstraße", - "Rütlistraße", - "Rybnikstraße", - "Saarbrücker Straße", - "Saarlandstraße", - "Sachsenwaldstraße", - "Sadelhof", - "Salamanderweg", - "Salweyweg", - "Salzbrunnstraße", - "Salzburger Straße", - "Salzgasse", - "Salzwedeler Straße", - "Sämannweg", - "Sandbirkenweg", - "Sanderoth", - "Sanitätsrat-Hallermann-Straße", - "Sankt-Georg-Straße", - "Sartoristraße", - "Sattelweg", - "Saturnstraße", - "Sauerländer Straße", - "Schaarstraße", - "Schachtstraße", - "Schachtweg", - "Schafackerweg", - "Schäferkampstraße", - "Schäferstraße", - "Schäfflerweg", - "Schafstallstraße", - "Schalkenbergsiepen", - "Schanzenweg", - "Schaperstraße", - "Schaphusstraße", - "Schärenhof", - "Scharnhorststraße", - "Scheffelstraße", - "Schelerweg", - "Schemmersfeld", - "Schenkebierweg", - "Scheuseweg", - "Schewenort", - "Schichtweg", - "Schieferbank", - "Schiffhorst", - "Schiffstraße", - "Schildplatz", - "Schildstraße", - "Schillerknapp", - "Schillerstraße", - "Schillingstraße", - "Schillstraße", - "Schimmelreiterweg", - "Schimmelstraße", - "Schirrmannweg", - "Schlagbaumstraße", - "Schlangenweg", - "Schleefstraße", - "Schlehenweg", - "Schleifenstraße", - "Schleipweg", - "Schleppbahnstraße", - "Schleswiger Straße", - "Schlickenkamp", - "Schliepstraße", - "Schlosserstraße", - "Schloßstraße", - "Schloß-Westhusener-Straße", - "Schlotweg", - "Schlüsselweg", - "Schmaler Weg", - "Schmelzgerweg", - "Schmemannsweg", - "Schmerkottenstraße", - "Schmetterlingsweg", - "Schmettowstraße", - "Schmiedestraße", - "Schmiedingstraße", - "Schmölterweg", - "Schmuckstraße", - "Schneewittchenweg", - "Schneiderstraße", - "Schnepfenweg", - "Schnettkerweg", - "Schnitterweg", - "Schöffenweg", - "Schoffsweg", - "Schölerpatt", - "Schönaichstraße", - "Schönaustraße", - "Schondellestraße", - "Schöneichensiepen", - "Schöner Pfad", - "Schönhalsweg", - "Schönhauser Straße", - "Schönstraße", - "Schönwaldstraße", - "Schoppenbergweg", - "Schorlemmerskamp", - "Schotteweg", - "Schragmüllerstraße", - "Schramweg", - "Schröderstraße", - "Schübbestraße", - "Schubertstraße", - "Schüchtermannstraße", - "Schuhhof", - "Schulenburgstraße", - "Schulstraße", - "Schulte-Heuthaus-Straße", - "Schultenhude", - "Schultenstraße", - "Schulte-Sodingen-Straße", - "Schulzstraße", - "Schumannstraße", - "Schumpeterweg", - "Schürbankstraße", - "Schürener Straße", - "Schürener Vorstadt", - "Schurfweg", - "Schürhoffstraße", - "Schüruferstraße", - "Schüttersort", - "Schützenstraße", - "Schwäbische Straße", - "Schwalbenbrink", - "Schwanenstraße", - "Schwanenwall", - "Schwärmerweg", - "Schwarzdornweg", - "Schwarzdrosselweg", - "Schwarze-Becker-Straße", - "Schwarze-Brüder-Weg", - "Schwarze-Ewald-Straße", - "Schwarzenbergstraße", - "Schwarzer Weg", - "Schwarzerlenweg", - "Schweizer Allee", - "Schwerter Kirchweg", - "Schwerter Straße", - "Schwieringhauser Straße", - "Schwimmweg", - "Sckellstraße", - "Sebrathweg", - "Seekante", - "Seepenweg", - "Seerosenweg", - "Seibertzweg", - "Seidenspinnerweg", - "Seilbahnweg", - "Seilerstraße", - "Selbachstraße", - "Selbsthilfstraße", - "Selma-Lagerlöf-Straße", - "Selzerstraße", - "Semerteichstraße", - "Sendstraße", - "Senftenbergstraße", - "Sengsbank", - "Sennestraße", - "Severingstraße", - "Seydlitzstraße", - "Sichelstraße", - "Sichterweg", - "Siebenbürgenstraße", - "Siebensternweg", - "Siedlung Salingen", - "Siedlungsweg", - "Siegburgstraße", - "Siegelbaumweg", - "Siegelstraße", - "Siegenstraße", - "Siegfried-Drupp-Straße", - "Siegfriedstraße", - "Siemensstraße", - "Siepmannstraße", - "Sigurdweg", - "Silberhecke", - "Silberknapp", - "Silberstraße", - "Silberweidenweg", - "Simmelweg", - "Sindernweg", - "Sindfeld", - "Singerhoffstraße", - "Soester Straße", - "Solbergweg", - "Sölder Bruch", - "Sölder Kirchweg", - "Sölder Straße", - "Sölder Waldstraße", - "Solinggut", - "Solmstraße", - "Sombartweg", - "Somborner Feldweg", - "Somborner Höh", - "Somborner Straße", - "Sommerberger Kirchweg", - "Sommerbergweg", - "Sommerlindenweg", - "Sommerseite", - "Sonnenplatz", - "Sonnenscheineck", - "Sonnenstraße", - "Sonnenwendstraße", - "Sophie-Thiemann-Straße", - "Sorbenweg", - "Sorpeliet", - "Spanbreite", - "Spanischer Weg", - "Spannerweg", - "Spannstraße", - "Spechtstraße", - "Speckacker", - "Speckestraße", - "Speestraße", - "Speierlingsweg", - "Spenhofweg", - "Sperberstraße", - "Sperberweg", - "Sperkelweg", - "Sperlingstraße", - "Speyerstraße", - "Spicherner Straße", - "Spickufer", - "Spiegelstraße", - "Spinnheide", - "Spissenagelstraße", - "Spitzwegstraße", - "Splintstraße", - "Spohrstraße", - "Spornerweg", - "Spraveweg", - "Spreestraße", - "Sprengelweg", - "Sprickmannweg", - "Springmorgen", - "Springorumstraße", - "Springweg", - "Staatsbusch", - "Stabelpfad", - "Stadtgärtnerei", - "Stadtrat-Cremer-Allee", - "Stahlhöfer Weg", - "Stahlwerkstraße", - "Stallbaumstraße", - "Stangefolstraße", - "Stapelweg", - "Stargarder Weg", - "Starweg", - "Stattskamp", - "Staudenweg", - "Staufenstraße", - "Stauffacherstraße", - "Stauffenbergstraße", - "Steckestraße", - "Stefan-Albringer-Straße", - "Stefanstraße", - "Steglitzeck", - "Stehmannstraße", - "Stehrstraße", - "Steiermarkstraße", - "Steigerstraße", - "Steile Straße", - "Steinäckerstraße", - "Steinauweg", - "Steinbreite", - "Steinbrinkstraße", - "Steinbruchstraße", - "Steinerne Kirche", - "Steinfurtweg", - "Steinhammerstraße", - "Steinhauser Weg", - "Steinhofstraße", - "Steinkauzweg", - "Steinkühlerweg", - "Steinmetzstraße", - "Steinstraße", - "Steinsweg", - "Stemmering", - "Stemmkeweg", - "Stempelweg", - "Stengelweg", - "Sterie", - "Sternstraße", - "Sterntalerweg", - "Sterzinger Straße", - "Stettiner Straße", - "Steubenstraße", - "Steyler Straße", - "Stiegenweg", - "Stieglitzweg", - "Stiftsgehölz", - "Stiftskamp", - "Stiftstraße", - "Stille Gasse", - "Stockholmer Allee", - "Stockumer Bruch", - "Stockumer Straße", - "Stofferstraße", - "Stollenstraße", - "Stolzestraße", - "Stoppelheck", - "Stoppelmannsweg", - "Storch Straße", - "Stortsweg", - "Straßburger Straße", - "Straußstraße", - "Strebweg", - "Streckenstraße", - "Stresemannstraße", - "Strickerstraße", - "Strobelallee", - "Strohnstraße", - "Strümpenbusch", - "Strüningweg", - "Strünkedestraße", - "Stübbenstraße", - "Stubengasse", - "Stuchteystraße", - "Stuckmannshof", - "Studtstraße", - "Stufenweg", - "Stürzelbreite", - "Stuttgartstraße", - "Stypelmanweg", - "Südbecke", - "Südblick", - "Sudermannstraße", - "Südfeld", - "Südflügelweg", - "Südrandweg", - "Südwall", - "Suebenstraße", - "Sugambrerstraße", - "Süggelberg", - "Süggelrandweg", - "Süggelweg", - "Suitbertstraße", - "Sülbeckstraße", - "Suledestraße", - "Sulpkestraße", - "Sulzbacher Straße", - "Sumbecks Holz", - "Sümpelmannstraße", - "Sundagskamp", - "Sunderweg", - "Sunthoffstraße", - "Swedestraße", - "Syburger Dorfstraße", - "Syburger Kirchstraße", - "Syburger Straße", - "Sydowstraße", - "Talbrücke Enderbach", - "Talbrücke Grotenbach", - "Talbrücke Isensteinsiepen", - "Talbrücke Rombergholz", - "Talstraße", - "Tannenkamp", - "Tannenstraße", - "Tassiloweg", - "Taubenweg", - "Tauentzienstraße", - "Taunusweg", - "Tecklenborn", - "Teimannweg", - "Teinerstraße", - "Teislerweg", - "Tellstraße", - "Tengelmannweg", - "Tennenweg", - "Ter-Nedden-Straße", - "Terwestenstraße", - "Tetschener Straße", - "Tettenbachstraße", - "Teutoburger Straße", - "Teutonenstraße", - "Tewaagstraße", - "Tewagastraße", - "Thälmannstraße", - "Theißstraße", - "Thelenort", - "Theodor-Freywald-Weg", - "Theodor-Hürth-Straße", - "Theresenstraße", - "Thieheuerstraße", - "Thielenstraße", - "Thierschweg", - "Thomasgasse", - "Thomas-Mann-Straße", - "Thoniesstraße", - "Thorner Straße", - "Thranestraße", - "Thüringer Straße", - "Thusneldastraße", - "Tidbaldweg", - "Tidemannweg", - "Tiefe Mark", - "Tiefe Straße", - "Tiefenbachtal", - "Tiefenweg", - "Tielkenweg", - "Tierschweg", - "Tilmonweg", - "Tiranaweg", - "Tiroler Straße", - "Toblacher Straße", - "Toepkenweg", - "Tölckestraße", - "Töllenkamp", - "Töllnerstraße", - "Tönnishof", - "Tönnisweg", - "Topasstraße", - "Toppstraße", - "Torckstraße", - "Torgauer Straße", - "Tospelliweg", - "Traddeweg", - "Trakehnerweg", - "Trapmannweg", - "Trapphofstraße", - "Traubenweg", - "Trauermantelweg", - "Traugottweg", - "Trautenauer Straße", - "Treckmannsweg", - "Treibstraße", - "Tremoniabogen", - "Tremoniastraße", - "Trippestraße", - "Tronjestraße", - "Tropauer Straße", - "Trumweg", - "Tschechischer Weg", - "Tucholskystraße", - "Tullstraße", - "Tulpenstraße", - "Tunnel Berghofen", - "Tunnelweg", - "Türkisweg", - "Turmalinweg", - "Turmfalkenstraße", - "Tüselmannweg", - "Tutenweg", - "Twerskuhle", - "Tybbinkstraße", - "Tye", - "Tymannstraße", - "U-Bahnsteig Dortmund-Hafen", - "Übelgönne", - "Überwasserstraße", - "Ubinckstraße", - "Uferstraße", - "Uhlandstraße", - "Uhlmann-Bixterheide-Weg", - "Uhustraße", - "Ulmenstraße", - "Ulmenweg", - "Ulrich-Bonse-Weg", - "Umbreitstraße", - "Unionstraße", - "Universitätsstraße", - "Unnaer Straße", - "Unten im Felde", - "Unter den Linden", - "Unterbank", - "Unterdelle", - "Untere Brinkstraße", - "Untere Egge", - "Untere Gartenstraße", - "Untere Hangstraße", - "Untere Pekingstraße", - "Unterer Sendweg", - "Unterer Weg", - "Unterfeldstraße", - "Untermarkstraße", - "Unterste-Wilms-Straße", - "Unterwaldener Straße", - "Unverhofftstraße", - "Uranusstraße", - "Urbanusstraße", - "Ursulastraße", - "Vahleweg", - "Vahrenort", - "Valmeweg", - "Varstbruch", - "Varusstraße", - "Varziner Straße", - "Veilchenstraße", - "Veitstraße", - "Vellinghauser Straße", - "Velthusstraße", - "Venusstraße", - "Veraweg", - "Verbindungsweg", - "Vereinsstraße", - "Verlorenes Holz", - "Verseweg", - "Vestingweg", - "Veteranenstraße", - "Vethackeweg", - "Vierkandtweg", - "Viermärker Weg", - "Vieselerhofstraße", - "Vikar-Kleffmann-Weg", - "Viktoriastraße", - "Viktor-Toyka-Straße", - "Vinckeplatz", - "Vinckestraße", - "Vinklöther Mark", - "Virchowstraße", - "Voerste-Dieckhof-Straße", - "Vogelinckweg", - "Vogelpothsweg", - "Vogelsangskamp", - "Vogtsstück", - "Vöhdekamp", - "Vöhdeweg", - "Völklinger Straße", - "Volksbundstraße", - "Volksgartenstraße", - "Völksmannweg", - "Volmarsteiner Straße", - "Volmehang", - "Voltaweg", - "Von-der-Berken-Straße", - "Von-der-Goltz-Straße", - "Von-der-Mark-Straße", - "Von-der-Recke-Straße", - "Von-der-Tann-Straße", - "Vor der Brügge", - "Vorhölterstraße", - "Vorläuferweg", - "Vormbrockweg", - "Vorsteherstraße", - "Vorstenstraße", - "Vorwärtsstraße", - "Voßkuhle", - "Vossloh", - "Vrydagweg", - "Vulkanstraße", - "Waarbaum", - "Wacholderstraße", - "Wachteloh", - "Wachtelweg", - "Wagenfeldstraße", - "Wahne Uhle", - "Wahrbuschstraße", - "Waidmannslust", - "Walbertstraße", - "Waldblickweg", - "Waldecker Straße", - "Waldenburgstraße", - "Waldental", - "Walderseestraße", - "Waldhausweg", - "Waldpförtnerweg", - "Waldrodeweg", - "Waldsängerweg", - "Waldstraße", - "Walkmühlenweg", - "Wallachstraße", - "Wallbaumstraße", - "Wallnussweg", - "Wallrabenhof", - "Wallrabestraße", - "Wallstraße", - "Walpkestieg", - "Walstattstraße", - "Walter-Berg-Weg", - "Walter-Dirks-Straße", - "Walter-Schücking-Straße", - "Walter-Welp-Straße", - "Walther-Kohlmann-Straße", - "Waltroper Straße", - "Wambeler Heide", - "Wambeler Hellweg", - "Wambeler Holz", - "Wambeler Straße", - "Wandweg", - "Waneckerweg", - "Wannebachstraße", - "Wannestraße", - "Warburger Straße", - "Wasserbank", - "Wasserfuhr", - "Wasserkunst", - "Wassermannweg", - "Wasserscheide", - "Wasserstraße", - "Waterloostraße", - "Watermannberg", - "Wattenscheidskamp", - "Webershohl", - "Weberstraße", - "Weckherlinweg", - "Weckweg", - "Weddepoth", - "Wedelstraße", - "Weidenbohrerweg", - "Weidenhope", - "Weidenstraße", - "Weilberg", - "Weilkeweg", - "Weingartenstraße", - "Weisbachstraße", - "Weischedestraße", - "Weißdornweg", - "Weiße Hecke", - "Weiße Taube", - "Weiße-Ewald-Straße", - "Weißenburger Straße", - "Weißsteinweg", - "Weitacker", - "Welkenerstraße", - "Wellinghofer Amtsstraße", - "Wellinghofer Hecke", - "Wellinghofer Straße", - "Wembersweg", - "Wendenweg", - "Wenemarstraße", - "Wengestraße", - "Wenkerstraße", - "Wennestieg", - "Wenzelstraße", - "Werdauer Weg", - "Werderstraße", - "Werimboldstraße", - "Werkloh", - "Werkmeisterstraße", - "Werner Hellweg", - "Werner Straße", - "Werner-Petermann-Weg", - "Werrastraße", - "Werswand", - "Werzenkamp", - "Weserstraße", - "Wesselingweg", - "Westbrink", - "Westendorfstraße", - "Westenhellweg", - "Westentor", - "Westerbleichstraße", - "Westererbenstraße, Pottgießerstraße", - "Westerfilder Straße", - "Westerholzstraße", - "Westermannstraße", - "Westerwaldweg", - "Westerwikstraße", - "Westfalendamm", - "Westfaliastraße", - "Westfälische Straße", - "Westhang", - "Westheide", - "Westhofener Straße", - "Westhoffstraße", - "Westholz", - "Westhusener Straße", - "Westicker Straße", - "Westkamp", - "Westricher Dorfstraße", - "Westricher Straße", - "Wetterschachtweg", - "Wetterstraße", - "Wichburgstraße", - "Wichlinghofer Bergstraße", - "Wichlinghofer Markstraße", - "Wickeder Hellweg", - "Wickeder Straße", - "Wicker Heck", - "Widumer Platz", - "Widumer Straße", - "Wieckesweg", - "Wiedbusch", - "Wiedeloh", - "Wielandstraße", - "Wiemerstraße", - "Wiendahlsbank", - "Wienstraße", - "Wieselweg", - "Wiesengrund", - "Wiesenkamp", - "Wiesenstraße", - "Wiesenweg", - "Wiesnerstraße", - "Wiethagenweg", - "Wiggerstraße", - "Wilberstraße", - "Wildbannweg", - "Wildermannstraße", - "Wildrosenstraße", - "Wilhelm-Brand-Straße", - "Wilhelm-Crüwell-Straße", - "Wilhelm-Dilthey-Straße", - "Wilhelm-Dresing-Straße", - "Wilhelm-Kaiser-Weg", - "Wilhelmplatz", - "Wilhelm-Schmidt-Straße", - "Wilhelmshöh", - "Wilhelmstraße", - "Willem-van-Vloten-Straße", - "Williburgstraße", - "Willstätterstraße", - "Willy-Brandt-Platz", - "Willy-Reinke-Straße", - "Wilmsmannstraße", - "Wilsingweg", - "Winandweg", - "Windausstraße", - "Windflügelweg", - "Windhorststraße", - "Windmühlenweg", - "Winkelriedweg", - "Winkelstraße", - "Winkshohlweg", - "Winterfeldtstraße", - "Winterkamp", - "Winterkampweg", - "Winterlindenweg", - "Winzerweg", - "Wipfelweg", - "Wipperkamp", - "Wiscelusweg", - "Wischlinger Weg", - "Wiskottstraße", - "Wißstraße", - "Wittbräucker Straße", - "Wittekindstraße", - "Wittelsbacherstraße", - "Wittener Straße", - "Witthausstraße", - "Wittichstraße", - "Witzlebenstraße", - "Wodanstraße", - "Woerderfeld", - "Woldenmey", - "Wormannsweg", - "Wormsstraße", - "Wörthstraße", - "Wrangelstraße", - "Wulebringweg", - "Wülferichstraße", - "Wulffsweg", - "Wulfgraben", - "Wulfshofstraße", - "Wulfskamp", - "Wunnebergstraße", - "Wupperstraße", - "Württemberger Straße", - "Xaveriweg", - "Yorckstraße", - "Zaunkönigweg", - "Zeche Oespel", - "Zeche-Freiberg-Straße", - "Zeche-Kaiser-Friedrich-Straße", - "Zeche-Margarete-Straße", - "Zeche-Norm-Straße", - "Zechenstraße", - "Zedernweg", - "Zeppelinstraße", - "Zeusweg", - "Zickenbrink", - "Ziegelbrandstraße", - "Ziegelhüttenstraße", - "Ziegelofenweg", - "Ziegelwiese", - "Ziethenstraße", - "Zillestraße", - "Zimmerstraße", - "Zinsweg", - "Zipsstraße", - "Zittauer Straße", - "Zobelweg", - "Zollernstraße", - "Zollvereinstraße", - "Zugstraße", - "Zum Buchenhain", - "Zum Burgkamp", - "Zum Erdbeerfeld", - "Zum Hallenbad", - "Zum Hövelteich", - "Zum Ihnedieck", - "Zum Knapp", - "Zum Lonnenhohl", - "Zum Mühlenheck", - "Zum Nubbental", - "Zum Stadtwald", - "Zum Steigeturm", - "Zum Uhlenbrauck", - "Zum Wäldchen", - "Zünslerweg", - "Zur Hockeneicke", - "Zur Hunnebboke", - "Zweigstraße", - "Zweiwiedenstraße", - "Zwergenweg", - "Zwergweg", - "Zwickauer Straße", - "Zwitschergasse" - }; - - public static readonly string[] Female = - { - "Andrea", - "Angelika", - "Anja", - "Anke", - "Anna", - "Anne", - "Annett", - "Antje", - "Barbara", - "Birgit", - "Brigitte", - "Christin", - "Kristin", - "Christina", - "Claudia", - "Daniela", - "Diana", - "Doreen", - "Franziska", - "Gabriele", - "Heike", - "Ines", - "Jana", - "Janina", - "Jennifer", - "Jessica", - "Jessika", - "Julia", - "Juliane", - "Karin", - "Karolin", - "Katharina", - "Kathrin", - "Katrin", - "Katja", - "Kerstin", - "Laura", - "Lea", - "Lena", - "Lisa", - "Mandy", - "Manuela", - "Maria", - "Marie", - "Marina", - "Martina", - "Melanie", - "Monika", - "Nadine", - "Nicole", - "Petra", - "Sabine", - "Sabrina", - "Sandra", - "Sara", - "Sarah", - "Silke", - "Simone", - "Sophia", - "Sophie", - "Stefanie", - "Stephanie", - "Susanne", - "Tanja", - "Ulrike", - "Ursula", - "Uta", - "Ute", - "Vanessa", - "Yvonne" - }; - - public static readonly string[] Family = - { - "Müller", - "Schmidt", - "Schneider", - "Fischer", - "Weber", - "Meyer", - "Wagner", - "Becker", - "Schulz", - "Hoffmann", - "Schäfer", - "Koch", - "Bauer", - "Richter", - "Klein", - "Wolf", - "Schröder", - "Neumann", - "Schwarz", - "Zimmermann", - "Braun", - "Krüger", - "Hofmann", - "Hartmann", - "Lange", - "Schmitt", - "Werner", - "Schmitz", - "Krause", - "Meier", - "Lehmann", - "Schmid", - "Schulze", - "Maier", - "Köhler", - "Herrmann", - "König", - "Walter", - "Mayer", - "Huber", - "Kaiser", - "Fuchs", - "Peters", - "Lang", - "Scholz", - "Möller", - "Weiß", - "Jung", - "Hahn", - "Schubert", - "Vogel", - "Friedrich", - "Keller", - "Günther", - "Frank", - "Berger", - "Winkler", - "Roth", - "Beck", - "Lorenz", - "Baumann", - "Franke", - "Albrecht", - "Schuster", - "Simon", - "Ludwig", - "Böhm", - "Winter", - "Kraus", - "Martin", - "Schumacher", - "Krämer", - "Vogt", - "Stein", - "Jäger", - "Otto", - "Sommer", - "Groß", - "Seidel", - "Heinrich", - "Brandt", - "Haas", - "Schreiber", - "Graf", - "Schulte", - "Dietrich", - "Ziegler", - "Kuhn", - "Kühn", - "Pohl", - "Engel", - "Horn", - "Busch", - "Bergmann", - "Thomas", - "Voigt", - "Sauer", - "Arnold", - "Wolff", - "Pfeiffer" - }; - - public static readonly string[] Male = - { - "Alexander", - "Andreas", - "Benjamin", - "Bernd", - "Christian", - "Daniel", - "David", - "Dennis", - "Dieter", - "Dirk", - "Dominik", - "Eric", - "Erik", - "Felix", - "Florian", - "Frank", - "Jan", - "Jens", - "Jonas", - "Jörg", - "Jürgen", - "Kevin", - "Klaus", - "Claus", - "Leon", - "Lukas", - "Lucas", - "Marcel", - "Marco", - "Marko", - "Mario", - "Markus", - "Martin", - "Mathias", - "Matthias", - "Max", - "Maximilian", - "Michael", - "Mike", - "Maik", - "Niklas", - "Patrick", - "Paul", - "Peter", - "Philipp", - "Phillipp", - "Ralf", - "Ralph", - "René", - "Robert", - "Sebastian", - "Stefan", - "Stephan", - "Steffen", - "Sven", - "Swen", - "Thomas", - "Thorsten", - "Tim", - "Tobias", - "Tom", - "Torsten", - "Ulrich", - "Uwe", - "Wolfgang" - }; - - public static readonly string[] Departments = - { - "Vertrieb", - "Einkauf", - "IT", - "Kundenbetreuung", - "Geschäftsleitung" - }; - - public static readonly string[] Cities = - { - "Aach (Hegau)", - "Aach", - "Aachen", - "Aalen", - "Abenberg", - "Abensberg", - "Achern", - "Achim (Landkreis Verden)", - "Adelsheim", - "Adenau", - "Adorf/Vogtl.", - "Ahaus", - "Ahlen", - "Ahrensburg", - "Aichach", - "Aichtal", - "Aken (Elbe)", - "Albstadt", - "Alfeld (Leine)", - "Allendorf (Lumda)", - "Allstedt", - "Alpirsbach", - "Alsfeld", - "Alsdorf", - "Alsleben (Saale)", - "Altdorf bei Nürnberg", - "Altena", - "Altenau", - "Altenberg (Erzgebirge)", - "Altenburg", - "Altenkirchen (Westerwald)", - "Altensteig", - "Altentreptow", - "Altlandsberg", - "Altötting", - "Alzenau", - "Alzey", - "Amberg", - "Amöneburg", - "Amorbach", - "Andernach", - "Angermünde", - "Anklam", - "Annaberg-Buchholz", - "Annaburg", - "Annweiler am Trifels", - "Ansbach", - "Apolda", - "Arendsee (Altmark)", - "Arneburg", - "Arnis", - "Arnsberg", - "Arnstadt", - "Arnstein (Unterfranken)", - "Arnstein (Sachsen-Anhalt)", - "Artern/Unstrut", - "Arzberg (Oberfranken)", - "Aschaffenburg", - "Aschersleben", - "Asperg", - "Aßlar", - "Attendorn", - "Aub", - "Aue (Sachsen)", - "Auerbach in der Oberpfalz", - "Auerbach/Vogtl.", - "Augsburg", - "Augustusburg", - "Aulendorf", - "Auma-Weidatal", - "Aurich", - "Babenhausen (Hessen)", - "Bacharach", - "Backnang", - "Bad Aibling", - "Bad Arolsen", - "Bad Belzig", - "Bad Bentheim", - "Bad Bergzabern", - "Bad Berka", - "Bad Berleburg", - "Bad Berneck im Fichtelgebirge", - "Bad Bevensen", - "Bad Bibra", - "Bad Blankenburg", - "Bad Bramstedt", - "Bad Breisig", - "Bad Brückenau", - "Bad Buchau", - "Bad Camberg", - "Bad Colberg-Heldburg", - "Bad Doberan", - "Bad Driburg", - "Bad Düben", - "Bad Dürkheim", - "Bad Dürrenberg", - "Bad Dürrheim", - "Bad Elster", - "Bad Ems", - "Baden-Baden", - "Bad Fallingbostel", - "Bad Frankenhausen/Kyffhäuser", - "Bad Freienwalde (Oder)", - "Bad Friedrichshall", - "Bad Gandersheim", - "Bad Gottleuba-Berggießhübel", - "Bad Griesbach im Rottal", - "Bad Harzburg", - "Bad Herrenalb", - "Bad Hersfeld", - "Bad Homburg vor der Höhe", - "Bad Honnef", - "Bad Hönningen", - "Bad Iburg", - "Bad Karlshafen", - "Bad Kissingen", - "Bad König", - "Bad Königshofen im Grabfeld", - "Bad Köstritz", - "Bad Kötzting", - "Bad Kreuznach", - "Bad Krozingen", - "Bad Laasphe", - "Bad Langensalza", - "Bad Lauchstädt", - "Bad Lausick", - "Bad Lauterberg im Harz", - "Bad Liebenstein (Gemeinde)", - "Bad Liebenwerda", - "Bad Liebenzell", - "Bad Lippspringe", - "Bad Lobenstein", - "Bad Marienberg (Westerwald)", - "Bad Mergentheim", - "Bad Münder am Deister", - "Bad Münster am Stein-Ebernburg", - "Bad Münstereifel", - "Bad Muskau", - "Bad Nauheim", - "Bad Nenndorf", - "Bad Neuenahr-Ahrweiler", - "Bad Neustadt an der Saale", - "Bad Oeynhausen", - "Bad Oldesloe", - "Bad Orb", - "Bad Pyrmont", - "Bad Rappenau", - "Bad Reichenhall", - "Bad Rodach", - "Bad Sachsa", - "Bad Säckingen", - "Bad Salzdetfurth", - "Bad Salzuflen", - "Bad Salzungen", - "Bad Saulgau", - "Bad Schandau", - "Bad Schmiedeberg", - "Bad Schussenried", - "Bad Schwalbach", - "Bad Schwartau", - "Bad Segeberg", - "Bad Sobernheim", - "Bad Soden am Taunus", - "Bad Soden-Salmünster", - "Bad Sooden-Allendorf", - "Bad Staffelstein", - "Bad Sulza", - "Bad Sülze", - "Bad Teinach-Zavelstein", - "Bad Tennstedt", - "Bad Tölz", - "Bad Urach", - "Bad Vilbel", - "Bad Waldsee", - "Bad Wildbad", - "Bad Wildungen", - "Bad Wilsnack", - "Bad Wimpfen", - "Bad Windsheim", - "Bad Wörishofen", - "Bad Wünnenberg", - "Bad Wurzach", - "Baesweiler", - "Baiersdorf", - "Balingen", - "Ballenstedt", - "Balve", - "Bamberg", - "Barby", - "Bargteheide", - "Barmstedt", - "Bärnau", - "Barntrup", - "Barsinghausen", - "Barth", - "Baruth/Mark", - "Bassum", - "Battenberg (Eder)", - "Baumholder", - "Baunach", - "Baunatal", - "Bautzen", - "Bayreuth", - "Bebra", - "Beckum", - "Bedburg", - "Beelitz", - "Beerfelden", - "Beeskow", - "Beilngries", - "Beilstein (Württemberg)", - "Belgern-Schildau", - "Bendorf", - "Bensheim", - "Berching", - "Berga/Elster", - "Bergen (Landkreis Celle)", - "Bergen auf Rügen", - "Bergheim", - "Bergisch Gladbach", - "Bergkamen", - "Bergneustadt", - "Berka/Werra", - "Berlin", - "Bernau bei Berlin", - "Bernburg (Saale)", - "Bernkastel-Kues", - "Bernsdorf (Oberlausitz)", - "Bernstadt a. d. Eigen", - "Bersenbrück", - "Besigheim", - "Betzdorf", - "Betzenstein", - "Beverungen", - "Bexbach", - "Biberach an der Riß", - "Biedenkopf", - "Bielefeld", - "Biesenthal", - "Bietigheim-Bissingen", - "Billerbeck", - "Bingen am Rhein", - "Birkenfeld (Nahe)", - "Bischofsheim an der Rhön", - "Bischofswerda", - "Bismark (Altmark)", - "Bitburg", - "Bitterfeld-Wolfen", - "Blankenburg (Harz)", - "Blankenhain", - "Blaubeuren", - "Bleckede", - "Bleicherode", - "Blieskastel", - "Blomberg", - "Blumberg", - "Bobingen", - "Böblingen", - "Bocholt", - "Bochum", - "Bockenem", - "Bodenwerder", - "Bogen (Stadt)", - "Böhlen (Sachsen)", - "Boizenburg/Elbe", - "Bonn", - "Bonndorf im Schwarzwald", - "Bönnigheim", - "Bopfingen", - "Boppard", - "Borgentreich", - "Borgholzhausen", - "Borken", - "Borken (Hessen)", - "Borkum", - "Borna", - "Bornheim (Rheinland)", - "Bottrop", - "Boxberg (Baden)", - "Brackenheim", - "Brake (Unterweser)", - "Brakel", - "Bramsche", - "Brandenburg an der Havel", - "Brand-Erbisdorf", - "Brandis", - "Braubach", - "Braunfels", - "Braunlage", - "Bräunlingen", - "Braunsbedra", - "Braunschweig", - "Breckerfeld", - "Bredstedt", - "Breisach am Rhein", - "Bremen", - "Bremerhaven", - "Bremervörde", - "Bretten", - "Breuberg", - "Brilon", - "Brotterode-Trusetal", - "Bruchköbel", - "Bruchsal", - "Brück", - "Brüel", - "Brühl (Rheinland)", - "Brunsbüttel", - "Brüssow", - "Buchen (Odenwald)", - "Buchholz in der Nordheide", - "Buchloe", - "Bückeburg", - "Buckow (Märkische Schweiz)", - "Büdelsdorf", - "Büdingen", - "Bühl (Baden)", - "Bünde", - "Büren (Westfalen)", - "Burg (bei Magdeburg)", - "Burgau", - "Burgbernheim", - "Burgdorf (Region Hannover)", - "Bürgel (Thüringen)", - "Burghausen", - "Burgkunstadt", - "Burglengenfeld", - "Burgstädt", - "Burg Stargard", - "Burgwedel", - "Burladingen", - "Burscheid", - "Bürstadt", - "Buttelstedt", - "Buttstädt", - "Butzbach", - "Bützow", - "Buxtehude", - "Calau", - "Calbe (Saale)", - "Calw", - "Castrop-Rauxel", - "Celle", - "Cham (Oberpfalz)", - "Chemnitz", - "Clausthal-Zellerfeld", - "Clingen", - "Cloppenburg", - "Coburg", - "Cochem", - "Coesfeld", - "Colditz", - "Coswig (Sachsen)", - "Coswig (Anhalt)", - "Cottbus", - "Crailsheim", - "Creglingen", - "Creußen", - "Creuzburg", - "Crimmitschau", - "Crivitz", - "Cuxhaven", - "Dachau", - "Dahlen (Sachsen)", - "Dahme/Mark", - "Dahn", - "Damme (Dümmer)", - "Dannenberg (Elbe)", - "Dargun", - "Darmstadt", - "Dassel", - "Dassow", - "Datteln", - "Daun", - "Deggendorf", - "Deidesheim", - "Delbrück", - "Delitzsch", - "Delmenhorst", - "Demmin", - "Dessau-Roßlau", - "Detmold", - "Dettelbach", - "Dieburg", - "Diemelstadt", - "Diepholz", - "Dierdorf", - "Dietenheim", - "Dietfurt an der Altmühl", - "Dietzenbach", - "Diez", - "Dillenburg", - "Dillingen an der Donau", - "Dillingen/Saar", - "Dingelstädt", - "Dingolfing", - "Dinkelsbühl", - "Dinklage", - "Dinslaken", - "Dippoldiswalde", - "Dissen am Teutoburger Wald", - "Ditzingen", - "Döbeln", - "Doberlug-Kirchhain", - "Döbern", - "Dohna", - "Dömitz", - "Dommitzsch", - "Donaueschingen", - "Donauwörth", - "Donzdorf", - "Dorfen", - "Dormagen", - "Dornburg-Camburg", - "Dornhan", - "Dornstetten", - "Dorsten", - "Dortmund", - "Dransfeld", - "Drebkau", - "Dreieich", - "Drensteinfurt", - "Dresden", - "Drolshagen", - "Duderstadt", - "Duisburg", - "Dülmen", - "Düren", - "Düsseldorf", - "Ebeleben", - "Eberbach", - "Ebermannstadt", - "Ebern", - "Ebersbach an der Fils", - "Ebersbach-Neugersdorf", - "Ebersberg", - "Eberswalde", - "Eckartsberga", - "Eckernförde", - "Edenkoben", - "Egeln", - "Eggenfelden", - "Eggesin", - "Ehingen (Donau)", - "Ehrenfriedersdorf", - "Eibelstadt", - "Eibenstock", - "Eichstätt", - "Eilenburg", - "Einbeck", - "Eisenach", - "Eisenberg (Thüringen)", - "Eisenberg (Pfalz)", - "Eisenhüttenstadt", - "Eisfeld", - "Lutherstadt Eisleben", - "Eislingen/Fils", - "Ellingen", - "Ellrich", - "Ellwangen (Jagst)", - "Elmshorn", - "Elsdorf (Rheinland)", - "Elsfleth", - "Elsterberg", - "Elsterwerda", - "Elstra", - "Elterlein", - "Eltmann", - "Eltville am Rhein", - "Elzach", - "Elze", - "Emden", - "Emmelshausen", - "Emmendingen", - "Emmerich am Rhein", - "Emsdetten", - "Endingen am Kaiserstuhl", - "Engen", - "Enger", - "Ennepetal", - "Ennigerloh", - "Eppelheim", - "Eppingen", - "Eppstein", - "Erbach (Donau)", - "Erbach (Odenwald)", - "Erbendorf", - "Erding", - "Erftstadt", - "Erfurt", - "Erkelenz", - "Erkner", - "Erkrath", - "Erlangen", - "Erlenbach am Main", - "Erlensee", - "Erwitte", - "Eschborn", - "Eschenbach in der Oberpfalz", - "Eschershausen", - "Eschwege", - "Eschweiler", - "Esens", - "Espelkamp", - "Essen", - "Esslingen am Neckar", - "Ettenheim", - "Ettlingen", - "Euskirchen", - "Eutin", - "Falkenberg/Elster", - "Falkensee", - "Falkenstein/Harz", - "Falkenstein/Vogtl.", - "Fehmarn", - "Fellbach", - "Felsberg (Hessen)", - "Feuchtwangen", - "Filderstadt", - "Finsterwalde", - "Fladungen", - "Flensburg", - "Flöha", - "Flörsheim am Main", - "Florstadt", - "Forchheim", - "Forchtenberg", - "Forst (Lausitz)", - "Frankenau", - "Frankenberg (Eder)", - "Frankenberg/Sa.", - "Frankenthal (Pfalz)", - "Frankfurt am Main", - "Frankfurt (Oder)", - "Franzburg", - "Frauenstein (Erzgebirge)", - "Frechen", - "Freiberg am Neckar", - "Freiberg", - "Freiburg im Breisgau", - "Freilassing", - "Freinsheim", - "Freising", - "Freital", - "Freren", - "Freudenberg (Baden)", - "Freudenberg (Siegerland)", - "Freudenstadt", - "Freyburg (Unstrut)", - "Freystadt", - "Freyung", - "Fridingen an der Donau", - "Friedberg (Bayern)", - "Friedberg (Hessen)", - "Friedland (Mecklenburg)", - "Friedland (Niederlausitz)", - "Friedrichroda", - "Friedrichsdorf", - "Friedrichshafen", - "Friedrichstadt", - "Friedrichsthal (Saar)", - "Friesack", - "Friesoythe", - "Fritzlar", - "Frohburg", - "Fröndenberg/Ruhr", - "Fulda", - "Fürstenau", - "Fürstenberg/Havel", - "Fürstenfeldbruck", - "Fürstenwalde/Spree", - "Fürth", - "Furth im Wald", - "Furtwangen im Schwarzwald", - "Füssen", - "Gadebusch", - "Gaggenau", - "Gaildorf", - "Gammertingen", - "Garbsen", - "Garching bei München", - "Gardelegen", - "Garding", - "Gartz (Oder)", - "Garz/Rügen", - "Gau-Algesheim", - "Gebesee", - "Gedern", - "Geesthacht", - "Gefell", - "Gefrees", - "Gehrden", - "Gehren", - "Geilenkirchen", - "Geisa", - "Geiselhöring", - "Geisenfeld", - "Geisenheim", - "Geisingen", - "Geislingen (Zollernalbkreis)", - "Geislingen an der Steige", - "Geithain", - "Geldern", - "Gelnhausen", - "Gelsenkirchen", - "Gemünden am Main", - "Gemünden (Wohra)", - "Gengenbach", - "Genthin", - "Georgsmarienhütte", - "Gera", - "Gerabronn", - "Gerbstedt", - "Geretsried", - "Geringswalde", - "Gerlingen", - "Germering", - "Germersheim", - "Gernsbach", - "Gernrode (Harz)", - "Gernsheim", - "Gerolstein", - "Gerolzhofen", - "Gersfeld (Rhön)", - "Gersthofen", - "Gescher", - "Geseke", - "Gevelsberg", - "Geyer", - "Giengen an der Brenz", - "Gießen", - "Gifhorn", - "Ginsheim-Gustavsburg", - "Gladbeck", - "Gladenbach", - "Glashütte (Sachsen)", - "Glauchau", - "Glinde", - "Glücksburg (Ostsee)", - "Glückstadt", - "Gnoien", - "Goch", - "Goldberg (Mecklenburg)", - "Goldkronach", - "Golßen", - "Gommern", - "Göppingen", - "Görlitz", - "Goslar", - "Gößnitz (Thüringen)", - "Gotha", - "Göttingen", - "Grabow (Elde)", - "Grafenau (Niederbayern)", - "Gräfenberg", - "Gräfenhainichen", - "Gräfenthal", - "Grafenwöhr", - "Grafing bei München", - "Gransee", - "Grebenau", - "Grebenstein", - "Greding", - "Greifswald", - "Greiz", - "Greußen", - "Greven", - "Grevenbroich", - "Grevesmühlen", - "Griesheim", - "Grimma", - "Grimmen", - "Gröditz", - "Groitzsch", - "Gronau (Leine)", - "Gronau (Westf.)", - "Gröningen", - "Großalmerode", - "Groß-Bieberau", - "Großbottwar", - "Großbreitenbach", - "Großenehrich", - "Großenhain", - "Groß-Gerau", - "Großräschen", - "Großröhrsdorf", - "Großschirma", - "Groß-Umstadt", - "Grünberg (Hessen)", - "Grünhain-Beierfeld", - "Grünsfeld", - "Grünstadt", - "Guben", - "Gudensberg", - "Güglingen", - "Gummersbach", - "Gundelfingen an der Donau", - "Gundelsheim (Württemberg)", - "Günzburg", - "Gunzenhausen", - "Güsten", - "Güstrow", - "Gütersloh", - "Gützkow", - "Haan", - "Hachenburg", - "Hadamar", - "Hagen", - "Hagenbach", - "Hagenow", - "Haiger", - "Haigerloch", - "Hainichen", - "Haiterbach", - "Halberstadt", - "Haldensleben", - "Halle (Saale)", - "Halle (Westf.)", - "Hallenberg", - "Hallstadt", - "Haltern am See", - "Halver", - "Hamburg", - "Hameln", - "Hamm", - "Hammelburg", - "Hamminkeln", - "Hanau", - "Hannover", - "Hann. Münden", - "Harburg (Schwaben)", - "Hardegsen", - "Haren (Ems)", - "Harsewinkel", - "Hartenstein (Sachsen)", - "Hartha", - "Harzgerode", - "Haselünne", - "Haslach im Kinzigtal", - "Haßfurt", - "Hattersheim am Main", - "Hattingen", - "Hatzfeld (Eder)", - "Hausach", - "Hauzenberg", - "Havelberg", - "Havelsee", - "Hayingen", - "Hechingen", - "Hecklingen", - "Heide (Holstein)", - "Heideck", - "Heidelberg", - "Heidenau (Sachsen)", - "Heidenheim an der Brenz", - "Heilbad Heiligenstadt", - "Heilbronn", - "Heiligenhafen", - "Heiligenhaus", - "Heilsbronn", - "Heimbach (Eifel)", - "Heimsheim", - "Heinsberg", - "Heitersheim", - "Heldrungen", - "Helmbrechts", - "Helmstedt", - "Hemau", - "Hemer", - "Hemmingen (Niedersachsen)", - "Hemmoor", - "Hemsbach", - "Hennef (Sieg)", - "Hennigsdorf", - "Heppenheim (Bergstraße)", - "Herbolzheim", - "Herborn", - "Herbrechtingen", - "Herbstein", - "Herdecke", - "Herdorf", - "Herford", - "Heringen/Helme", - "Heringen (Werra)", - "Hermeskeil", - "Hermsdorf (Thüringen)", - "Herne", - "Herrenberg", - "Herrieden", - "Herrnhut", - "Hersbruck", - "Herten", - "Herzberg am Harz", - "Herzberg (Elster)", - "Herzogenaurach", - "Herzogenrath", - "Hessisch Lichtenau", - "Hessisch Oldendorf", - "Hettingen", - "Hettstedt", - "Heubach", - "Heusenstamm", - "Hilchenbach", - "Hildburghausen", - "Hilden", - "Hildesheim", - "Hillesheim (Eifel)", - "Hilpoltstein", - "Hirschau", - "Hirschberg (Saale)", - "Hirschhorn (Neckar)", - "Hitzacker (Elbe)", - "Hochheim am Main", - "Höchstadt an der Aisch", - "Höchstädt an der Donau", - "Hockenheim", - "Hof (Saale)", - "Hofgeismar", - "Hofheim am Taunus", - "Hofheim in Unterfranken", - "Hohenberg an der Eger", - "Hohenleuben", - "Hohenmölsen", - "Hohen Neuendorf", - "Hohenstein-Ernstthal", - "Hohnstein (Sächsische Schweiz)", - "Höhr-Grenzhausen", - "Hollfeld", - "Holzgerlingen", - "Holzminden", - "Homberg (Efze)", - "Homberg (Ohm)", - "Homburg", - "Horb am Neckar", - "Hornbach", - "Horn-Bad Meinberg", - "Hornberg", - "Hornburg", - "Hörstel", - "Horstmar", - "Höxter", - "Hoya", - "Hoyerswerda", - "Hückelhoven", - "Hückeswagen", - "Hüfingen", - "Hünfeld", - "Hungen", - "Hürth", - "Husum", - "Ibbenbüren", - "Ichenhausen", - "Idar-Oberstein", - "Idstein", - "Illertissen", - "Ilmenau", - "Ilsenburg (Harz)", - "Ilshofen", - "Immenhausen", - "Immenstadt im Allgäu", - "Ingelfingen", - "Ingelheim am Rhein", - "Ingolstadt", - "Iphofen", - "Iserlohn", - "Isny im Allgäu", - "Isselburg", - "Itzehoe", - "Jarmen", - "Jena", - "Jerichow", - "Jessen (Elster)", - "Jever", - "Joachimsthal (Barnim)", - "Johanngeorgenstadt", - "Jöhstadt", - "Jülich", - "Jüterbog", - "Kaarst", - "Kahla", - "Kaisersesch", - "Kaiserslautern", - "Kalbe (Milde)", - "Kalkar", - "Kaltenkirchen", - "Kaltennordheim", - "Kamen", - "Kamenz", - "Kamp-Lintfort", - "Kandel (Pfalz)", - "Kandern", - "Kappeln", - "Karben", - "Karlsruhe", - "Karlstadt", - "Kassel", - "Kastellaun", - "Katzenelnbogen", - "Kaub", - "Kaufbeuren", - "Kehl", - "Kelbra (Kyffhäuser)", - "Kelheim", - "Kelkheim (Taunus)", - "Kellinghusen", - "Kelsterbach", - "Kemberg", - "Kemnath", - "Kempen", - "Kempten (Allgäu)", - "Kenzingen", - "Kerpen", - "Ketzin/Havel", - "Kevelaer", - "Kiel", - "Kierspe", - "Kindelbrück", - "Kirchberg (Sachsen)", - "Kirchberg an der Jagst", - "Kirchberg (Hunsrück)", - "Kirchen (Sieg)", - "Kirchenlamitz", - "Kirchhain", - "Kirchheimbolanden", - "Kirchheim unter Teck", - "Kirn", - "Kirtorf", - "Kitzingen", - "Kitzscher", - "Kleve", - "Klingenberg am Main", - "Klingenthal", - "Klötze", - "Klütz", - "Knittlingen", - "Koblenz", - "Kohren-Sahlis", - "Kolbermoor", - "Kölleda", - "Köln", - "Königsberg in Bayern", - "Königsbrück", - "Königsbrunn", - "Königsee-Rottenbach", - "Königslutter am Elm", - "Königstein im Taunus", - "Königstein (Sächsische Schweiz)", - "Königswinter", - "Königs Wusterhausen", - "Könnern", - "Konstanz", - "Konz", - "Korbach", - "Korntal-Münchingen", - "Kornwestheim", - "Korschenbroich", - "Köthen (Anhalt)", - "Kraichtal", - "Krakow am See", - "Kranichfeld", - "Krautheim (Jagst)", - "Krefeld", - "Kremmen", - "Krempe (Steinburg)", - "Kreuztal", - "Kronach", - "Kronberg im Taunus", - "Kröpelin", - "Kroppenstedt", - "Krumbach (Schwaben)", - "Kühlungsborn", - "Kulmbach", - "Külsheim", - "Künzelsau", - "Kupferberg", - "Kuppenheim", - "Kusel", - "Kyllburg", - "Kyritz", - "Laage", - "Laatzen", - "Ladenburg", - "Lage (Lippe)", - "Lahnstein", - "Lahr/Schwarzwald", - "Laichingen", - "Lambrecht (Pfalz)", - "Lampertheim", - "Landau an der Isar", - "Landau in der Pfalz", - "Landsberg am Lech", - "Landsberg (Saalekreis)", - "Landshut", - "Landstuhl", - "Langelsheim", - "Langen (bei Bremerhaven)", - "Langen (Hessen)", - "Langenau", - "Langenburg", - "Langenfeld (Rheinland)", - "Langenhagen", - "Langenselbold", - "Langenzenn", - "Langewiesen", - "Lassan", - "Laubach", - "Laucha an der Unstrut", - "Lauchhammer", - "Lauchheim", - "Lauda-Königshofen", - "Lauenburg/Elbe", - "Lauf an der Pegnitz", - "Laufen (Salzach)", - "Laufenburg (Baden)", - "Lauffen am Neckar", - "Lauingen (Donau)", - "Laupheim", - "Lauscha", - "Lauta", - "Lauter-Bernsbach", - "Lauterbach (Hessen)", - "Lauterecken", - "Lauterstein", - "Lebach", - "Lebus", - "Leer (Ostfriesland)", - "Lehesten (Thüringer Wald)", - "Lehrte", - "Leichlingen (Rheinland)", - "Leimen (Baden)", - "Leinefelde-Worbis", - "Leinfelden-Echterdingen", - "Leipheim", - "Leipzig", - "Leisnig", - "Lemgo", - "Lengefeld", - "Lengenfeld (Vogtland)", - "Lengerich (Westfalen)", - "Lennestadt", - "Lenzen (Elbe)", - "Leonberg", - "Leun", - "Leuna", - "Leutenberg", - "Leutershausen", - "Leutkirch im Allgäu", - "Leverkusen", - "Lich", - "Lichtenau (Baden)", - "Lichtenau (Westfalen)", - "Lichtenberg (Oberfranken)", - "Lichtenfels (Oberfranken)", - "Lichtenfels (Hessen)", - "Lichtenstein/Sa.", - "Liebenau (Hessen)", - "Liebenwalde", - "Lieberose", - "Liebstadt", - "Limbach-Oberfrohna", - "Limburg an der Lahn", - "Lindau (Bodensee)", - "Linden (Hessen)", - "Lindenberg im Allgäu", - "Lindenfels", - "Lindow (Mark)", - "Lingen (Ems)", - "Linnich", - "Linz am Rhein", - "Lippstadt", - "Löbau", - "Löffingen", - "Lohmar", - "Lohne (Oldenburg)", - "Löhne", - "Lohr am Main", - "Loitz", - "Lollar", - "Lommatzsch", - "Löningen", - "Lorch (Württemberg)", - "Lorch (Rheingau)", - "Lörrach", - "Lorsch", - "Lößnitz (Erzgebirge)", - "Löwenstein", - "Lübbecke", - "Lübben (Spreewald)", - "Lübbenau/Spreewald", - "Lübeck", - "Lübtheen", - "Lübz", - "Lüchow (Wendland)", - "Lucka", - "Luckau", - "Luckenwalde", - "Lüdenscheid", - "Lüdinghausen", - "Ludwigsburg", - "Ludwigsfelde", - "Ludwigshafen am Rhein", - "Ludwigslust", - "Ludwigsstadt", - "Lugau/Erzgeb.", - "Lügde", - "Lüneburg", - "Lünen", - "Lunzenau", - "Lütjenburg", - "Lützen", - "Lychen", - "Magdala", - "Magdeburg", - "Mahlberg", - "Mainbernheim", - "Mainburg", - "Maintal", - "Mainz", - "Malchin", - "Malchow (Mecklenburg)", - "Mannheim", - "Manderscheid", - "Mansfeld", - "Marbach am Neckar", - "Marburg", - "Marienberg", - "Marienmünster", - "Markdorf", - "Markgröningen", - "Märkisch Buchholz", - "Markkleeberg", - "Markneukirchen", - "Markranstädt", - "Marktbreit", - "Marktheidenfeld", - "Marktleuthen", - "Marktoberdorf", - "Marktredwitz", - "Marktsteft", - "Marl", - "Marlow", - "Marne (Holstein)", - "Marsberg", - "Maulbronn", - "Maxhütte-Haidhof", - "Mayen", - "Mechernich", - "Meckenheim (Rheinland)", - "Medebach", - "Meerane", - "Meerbusch", - "Meersburg", - "Meinerzhagen", - "Meiningen", - "Meisenheim", - "Meißen", - "Meldorf", - "Melle", - "Mellrichstadt", - "Melsungen", - "Memmingen", - "Menden (Sauerland)", - "Mendig", - "Mengen", - "Meppen", - "Merkendorf (Mittelfranken)", - "Merseburg", - "Merzig", - "Meschede", - "Meßkirch", - "Meßstetten", - "Mettmann", - "Metzingen", - "Meuselwitz", - "Meyenburg", - "Michelstadt", - "Miesbach", - "Miltenberg", - "Mindelheim", - "Minden", - "Mirow", - "Mittenwalde", - "Mitterteich", - "Mittweida", - "Möckern", - "Möckmühl", - "Moers", - "Mölln", - "Mönchengladbach", - "Monheim am Rhein", - "Monheim (Schwaben)", - "Monschau", - "Montabaur", - "Moosburg an der Isar", - "Mörfelden-Walldorf", - "Moringen", - "Mosbach", - "Mössingen", - "Mücheln (Geiseltal)", - "Mügeln", - "Mühlacker", - "Mühlberg/Elbe", - "Mühldorf am Inn", - "Mühlhausen/Thüringen", - "Mühlheim am Main", - "Mühlheim an der Donau", - "Mülheim an der Ruhr", - "Mülheim-Kärlich", - "Müllheim (Baden)", - "Müllrose", - "Münchberg", - "Müncheberg", - "München", - "Münchenbernsdorf", - "Munderkingen", - "Münnerstadt", - "Münsingen (Württemberg)", - "Munster (Örtze)", - "Münster (Westfalen)", - "Münstermaifeld", - "Münzenberg", - "Murrhardt", - "Mylau", - "Nabburg", - "Nagold (Stadt)", - "Naila", - "Nassau (Lahn)", - "Nastätten", - "Nauen", - "Naumburg (Hessen)", - "Naumburg (Saale)", - "Naunhof", - "Nebra (Unstrut)", - "Neckarbischofsheim", - "Neckargemünd", - "Neckarsteinach", - "Neckarsulm", - "Neresheim", - "Netphen", - "Nettetal", - "Netzschkau", - "Neu-Anspach", - "Neubrandenburg", - "Neubukow", - "Neubulach", - "Neuburg an der Donau", - "Neudenau", - "Neuenbürg", - "Neuenburg am Rhein", - "Neuenhaus", - "Neuenrade", - "Neuenstadt am Kocher", - "Neuenstein (Hohenlohe)", - "Neuerburg", - "Neuffen", - "Neuhaus am Rennweg", - "Neu-Isenburg", - "Neukalen", - "Neukirchen (Knüll)", - "Neukirchen-Vluyn", - "Neukloster", - "Neumark (bei Weimar)", - "Neumarkt in der Oberpfalz", - "Neumarkt-Sankt Veit", - "Neumünster", - "Neunburg vorm Wald", - "Neunkirchen (Saar)", - "Neuötting", - "Neuruppin", - "Neusalza-Spremberg", - "Neusäß", - "Neuss", - "Neustadt an der Aisch", - "Neustadt an der Donau", - "Neustadt an der Waldnaab", - "Neustadt am Kulm", - "Neustadt am Rübenberge", - "Neustadt an der Orla", - "Neustadt an der Weinstraße", - "Neustadt bei Coburg", - "Neustadt (Dosse)", - "Neustadt-Glewe", - "Neustadt (Hessen)", - "Neustadt in Holstein", - "Neustadt in Sachsen", - "Neustrelitz", - "Neutraubling", - "Neu-Ulm", - "Neuwied", - "Nidda", - "Niddatal", - "Nidderau", - "Nideggen", - "Niebüll", - "Niedenstein", - "Niederkassel", - "Niedernhall", - "Nieder-Olm", - "Niederstetten", - "Niederstotzingen", - "Nieheim", - "Niemegk", - "Nienburg (Saale)", - "Nienburg/Weser", - "Nierstein", - "Niesky", - "Nittenau", - "Norden (Ostfriesland)", - "Nordenham", - "Norderney", - "Norderstedt", - "Nordhausen", - "Nordhorn", - "Nördlingen", - "Northeim", - "Nortorf", - "Nossen", - "Nürnberg", - "Nürtingen", - "Oberasbach", - "Oberharz am Brocken", - "Oberhausen", - "Oberhof", - "Oberkirch (Baden)", - "Oberkochen", - "Oberlungwitz", - "Obermoschel", - "Obernburg am Main", - "Oberndorf am Neckar", - "Obernkirchen", - "Ober-Ramstadt", - "Oberriexingen", - "Obertshausen", - "Oberursel (Taunus)", - "Oberviechtach", - "Oberweißbach/Thüringer Wald", - "Oberwesel", - "Oberwiesenthal", - "Ochsenfurt", - "Ochsenhausen", - "Ochtrup", - "Oderberg", - "Oebisfelde-Weferlingen", - "Oederan", - "Oelde", - "Oelsnitz/Erzgeb.", - "Oelsnitz/Vogtl.", - "Oer-Erkenschwick", - "Oerlinghausen", - "Oestrich-Winkel", - "Oettingen in Bayern", - "Offenbach am Main", - "Offenburg", - "Ohrdruf", - "Öhringen", - "Olbernhau", - "Olching", - "Oldenburg (Oldenburg)", - "Oldenburg in Holstein", - "Olfen", - "Olpe", - "Olsberg", - "Oppenau", - "Oppenheim", - "Oranienbaum-Wörlitz", - "Oranienburg", - "Orlamünde", - "Ornbau", - "Ortenberg (Hessen)", - "Ortrand", - "Oschatz", - "Oschersleben (Bode)", - "Osnabrück", - "Osterburg (Altmark)", - "Osterburken", - "Osterfeld (bei Naumburg)", - "Osterhofen", - "Osterholz-Scharmbeck", - "Osterode am Harz", - "Osterwieck", - "Ostfildern", - "Ostheim vor der Rhön", - "Osthofen", - "Östringen", - "Ostritz", - "Otterberg", - "Otterndorf", - "Ottweiler", - "Overath", - "Owen", - "Paderborn", - "Papenburg", - "Pappenheim", - "Parchim", - "Parsberg", - "Pasewalk", - "Passau", - "Pattensen", - "Pausa-Mühltroff", - "Pegau", - "Pegnitz (Stadt)", - "Peine", - "Peitz", - "Penig", - "Penkun", - "Penzberg", - "Penzlin", - "Perleberg", - "Petershagen", - "Pfaffenhofen an der Ilm", - "Pfarrkirchen", - "Pforzheim", - "Pfreimd", - "Pfullendorf", - "Pfullingen", - "Pfungstadt", - "Philippsburg", - "Pinneberg", - "Pirmasens", - "Pirna", - "Plattling", - "Plau am See", - "Plaue", - "Plauen", - "Plettenberg", - "Pleystein", - "Plochingen", - "Plön", - "Pocking", - "Pohlheim", - "Polch", - "Porta Westfalica (Stadt)", - "Pößneck", - "Potsdam", - "Pottenstein (Oberfranken)", - "Preetz", - "Premnitz", - "Prenzlau", - "Pressath", - "Preußisch Oldendorf", - "Prichsenstadt", - "Pritzwalk", - "Prüm", - "Puchheim", - "Pulheim", - "Pulsnitz", - "Putbus", - "Putlitz", - "Püttlingen", - "Quakenbrück", - "Quedlinburg", - "Querfurt", - "Quickborn", - "Rabenau (Sachsen)", - "Radeberg", - "Radebeul", - "Radeburg", - "Radevormwald", - "Radolfzell am Bodensee", - "Raguhn-Jeßnitz", - "Rahden", - "Rain (Lech)", - "Ramstein-Miesenbach", - "Ranis", - "Ransbach-Baumbach", - "Rastatt", - "Rastenberg", - "Rathenow", - "Ratingen", - "Ratzeburg", - "Rauenberg", - "Raunheim", - "Rauschenberg", - "Ravensburg", - "Ravenstein", - "Recklinghausen", - "Rees", - "Regen (Stadt)", - "Regensburg", - "Regis-Breitingen", - "Rehau", - "Rehburg-Loccum", - "Rehna", - "Reichelsheim (Wetterau)", - "Reichenbach im Vogtland", - "Reichenbach/O.L.", - "Reinbek", - "Reinfeld (Holstein)", - "Reinheim", - "Remagen", - "Remda-Teichel", - "Remscheid", - "Remseck am Neckar", - "Renchen", - "Rendsburg", - "Rennerod", - "Renningen", - "Rerik", - "Rethem (Aller)", - "Reutlingen", - "Rheda-Wiedenbrück", - "Rhede", - "Rheinau (Baden)", - "Rheinbach", - "Rheinberg", - "Rheinböllen", - "Rheine", - "Rheinfelden (Baden)", - "Rheinsberg", - "Rheinstetten", - "Rhens", - "Rhinow", - "Ribnitz-Damgarten", - "Richtenberg", - "Riedenburg", - "Riedlingen", - "Riedstadt", - "Rieneck", - "Riesa", - "Rietberg", - "Rinteln", - "Röbel/Müritz", - "Rochlitz", - "Rockenhausen", - "Rodalben", - "Rodenberg", - "Rödental", - "Rödermark", - "Rodewisch", - "Rodgau", - "Roding", - "Römhild", - "Romrod", - "Ronneburg (Thüringen)", - "Ronnenberg", - "Rosbach vor der Höhe", - "Rosenfeld", - "Rosenheim", - "Rosenthal (Hessen)", - "Rösrath", - "Roßleben", - "Roßwein", - "Rostock", - "Rotenburg an der Fulda", - "Rotenburg (Wümme)", - "Roth", - "Rötha", - "Röthenbach an der Pegnitz", - "Rothenburg/Oberlausitz", - "Rothenburg ob der Tauber", - "Rothenfels", - "Rottenburg am Neckar", - "Rottenburg an der Laaber", - "Röttingen", - "Rottweil", - "Rötz", - "Rüdesheim am Rhein", - "Rudolstadt", - "Ruhla", - "Ruhland", - "Runkel", - "Rüsselsheim", - "Rutesheim", - "Rüthen", - "Saalburg-Ebersdorf", - "Saalfeld/Saale", - "Saarbrücken", - "Saarburg", - "Saarlouis", - "Sachsenhagen", - "Sachsenheim", - "Salzgitter", - "Salzkotten", - "Salzwedel", - "Sandau (Elbe)", - "Sandersdorf-Brehna", - "Sangerhausen", - "Sankt Augustin", - "Sankt Goar", - "Sankt Goarshausen", - "Sarstedt", - "Sassenberg", - "Sassnitz", - "Sayda", - "Schalkau", - "Schauenstein", - "Scheer", - "Scheibenberg", - "Scheinfeld", - "Schelklingen", - "Schenefeld (Kreis Pinneberg)", - "Scheßlitz", - "Schieder-Schwalenberg", - "Schifferstadt", - "Schillingsfürst", - "Schiltach", - "Schirgiswalde-Kirschau", - "Schkeuditz", - "Schkölen", - "Schleiden", - "Schleiz", - "Schleswig", - "Schlettau", - "Schleusingen", - "Schlieben", - "Schlitz (Vogelsbergkreis)", - "Schloß Holte-Stukenbrock", - "Schlotheim", - "Schlüchtern", - "Schlüsselfeld", - "Schmalkalden", - "Schmallenberg", - "Schmölln", - "Schnackenburg", - "Schnaittenbach", - "Schneeberg (Erzgebirge)", - "Schneverdingen", - "Schömberg (Zollernalbkreis)", - "Schönau (Odenwald)", - "Schönau im Schwarzwald", - "Schönberg (Mecklenburg)", - "Schönebeck (Elbe)", - "Schöneck/Vogtl.", - "Schönewalde", - "Schongau", - "Schöningen", - "Schönsee", - "Schönwald (Bayern)", - "Schopfheim", - "Schöppenstedt", - "Schorndorf", - "Schortens", - "Schotten (Stadt)", - "Schramberg", - "Schraplau", - "Schriesheim", - "Schrobenhausen", - "Schrozberg", - "Schüttorf", - "Schwaan", - "Schwabach", - "Schwäbisch Gmünd", - "Schwäbisch Hall", - "Schwabmünchen", - "Schwaigern", - "Schwalbach am Taunus", - "Schwalmstadt", - "Schwandorf", - "Schwanebeck", - "Schwarzenbach am Wald", - "Schwarzenbach an der Saale", - "Schwarzenbek", - "Schwarzenberg/Erzgeb.", - "Schwarzenborn (Knüll)", - "Schwarzheide", - "Schwedt/Oder", - "Schweich", - "Schweinfurt", - "Schwelm", - "Schwentinental", - "Schwerin", - "Schwerte", - "Schwetzingen", - "Sebnitz", - "Seehausen (Altmark)", - "Seeland (Sachsen-Anhalt)", - "Seelow", - "Seelze", - "Seesen", - "Sehnde", - "Seifhennersdorf", - "Selb", - "Selbitz (Oberfranken)", - "Seligenstadt", - "Selm", - "Selters (Westerwald)", - "Senden (Bayern)", - "Sendenhorst", - "Senftenberg", - "Seßlach", - "Siegburg", - "Siegen", - "Sigmaringen", - "Simbach am Inn", - "Simmern/Hunsrück", - "Sindelfingen", - "Singen (Hohentwiel)", - "Sinsheim", - "Sinzig", - "Soest", - "Solingen", - "Solms", - "Soltau", - "Sömmerda", - "Sondershausen", - "Sonneberg", - "Sonnewalde", - "Sonthofen", - "Sontra", - "Spaichingen", - "Spalt", - "Spangenberg", - "Speicher (Eifel)", - "Spenge", - "Speyer", - "Spremberg", - "Springe", - "Sprockhövel", - "Stade", - "Stadtallendorf", - "Stadtbergen", - "Stadthagen", - "Stadtilm", - "Stadtlengsfeld", - "Stadtlohn", - "Stadtoldendorf", - "Stadtprozelten", - "Stadtroda", - "Stadtsteinach", - "Stadt Wehlen", - "Starnberg", - "Staßfurt", - "Staufen im Breisgau", - "Staufenberg (Hessen)", - "Stavenhagen", - "St. Blasien", - "Stein (Mittelfranken)", - "Steinach (Thüringen)", - "Steinau an der Straße", - "Steinbach-Hallenberg", - "Steinbach (Taunus)", - "Steinfurt", - "Steinheim an der Murr", - "Steinheim (Westfalen)", - "Stendal", - "Sternberg", - "St. Ingbert", - "St. Georgen im Schwarzwald", - "Stockach", - "Stolberg (Rhld.)", - "Stollberg/Erzgeb.", - "Stolpen", - "Storkow (Mark)", - "Stößen", - "Straelen", - "Stralsund", - "Strasburg (Uckermark)", - "Straubing", - "Strausberg", - "Strehla", - "Stromberg (Hunsrück)", - "Stühlingen", - "Stutensee", - "Stuttgart", - "St. Wendel", - "Suhl", - "Sulingen", - "Sulz am Neckar", - "Sulzbach/Saar", - "Sulzbach-Rosenberg", - "Sulzburg", - "Sundern (Sauerland)", - "Südliches Anhalt", - "Süßen", - "Syke", - "Tambach-Dietharz", - "Tangerhütte", - "Tangermünde", - "Tann (Rhön)", - "Tanna", - "Tauberbischofsheim", - "Taucha", - "Taunusstein", - "Tecklenburg", - "Tegernsee (Stadt)", - "Telgte", - "Teltow", - "Templin", - "Tengen", - "Tessin (bei Rostock)", - "Teterow", - "Tettnang", - "Teublitz", - "Teuchern", - "Teupitz", - "Teuschnitz", - "Thale", - "Thalheim/Erzgeb.", - "Thannhausen (Schwaben)", - "Tharandt", - "Themar", - "Thum", - "Tirschenreuth", - "Titisee-Neustadt", - "Tittmoning", - "Todtnau", - "Töging am Inn", - "Tönisvorst", - "Tönning", - "Torgau", - "Torgelow", - "Tornesch", - "Traben-Trarbach", - "Traunreut", - "Traunstein", - "Trebbin", - "Trebsen/Mulde", - "Treffurt", - "Trendelburg", - "Treuchtlingen", - "Treuen", - "Treuenbrietzen", - "Triberg im Schwarzwald", - "Tribsees", - "Trier", - "Triptis", - "Trochtelfingen", - "Troisdorf", - "Trossingen", - "Trostberg", - "Tübingen", - "Tuttlingen", - "Twistringen", - "Übach-Palenberg", - "Überlingen", - "Uebigau-Wahrenbrück", - "Ueckermünde", - "Uelzen", - "Uetersen", - "Uffenheim", - "Uhingen", - "Ulm", - "Ulmen (Eifel)", - "Ulrichstein", - "Ummerstadt", - "Unkel", - "Unna", - "Unterschleißheim", - "Usedom (Stadt)", - "Usingen", - "Uslar", - "Vacha", - "Vaihingen an der Enz", - "Vallendar", - "Varel", - "Vechta", - "Velbert", - "Velburg", - "Velden (Pegnitz)", - "Vellberg", - "Velen", - "Vellmar", - "Velten", - "Verden (Aller)", - "Veringenstadt", - "Verl", - "Versmold", - "Vetschau/Spreewald", - "Viechtach", - "Vienenburg", - "Viernheim", - "Viersen", - "Villingen-Schwenningen", - "Vilsbiburg", - "Vilseck", - "Vilshofen an der Donau", - "Visselhövede", - "Vlotho", - "Voerde (Niederrhein)", - "Vogtsburg im Kaiserstuhl", - "Vohburg an der Donau", - "Vohenstrauß", - "Vöhrenbach", - "Vöhringen (Iller)", - "Volkach", - "Völklingen", - "Volkmarsen", - "Vreden", - "Wachenheim an der Weinstraße", - "Wächtersbach", - "Wadern", - "Waghäusel", - "Wahlstedt", - "Waiblingen", - "Waibstadt", - "Waischenfeld", - "Waldbröl", - "Waldeck (Stadt)", - "Waldenbuch", - "Waldenburg (Sachsen)", - "Waldenburg (Württemberg)", - "Waldershof", - "Waldheim", - "Waldkappel", - "Waldkirch", - "Waldkirchen", - "Waldkraiburg", - "Waldmünchen", - "Waldsassen", - "Waldshut-Tiengen", - "Walldorf (Baden)", - "Walldürn", - "Wallenfels", - "Walsrode", - "Waltershausen", - "Waltrop", - "Wanfried", - "Wangen im Allgäu", - "Wanzleben-Börde", - "Warburg", - "Waren (Müritz)", - "Warendorf", - "Warin", - "Warstein", - "Wassenberg", - "Wasserburg am Inn", - "Wassertrüdingen", - "Wasungen", - "Wedel", - "Weener", - "Wegberg", - "Wegeleben", - "Wehr (Baden)", - "Weida", - "Weiden in der Oberpfalz", - "Weikersheim", - "Weil am Rhein", - "Weilburg", - "Weil der Stadt", - "Weilheim an der Teck", - "Weilheim in Oberbayern", - "Weimar", - "Weingarten (Württemberg)", - "Weinheim", - "Weinsberg", - "Weinstadt", - "Weismain", - "Weißenberg", - "Weißenburg in Bayern", - "Weißenfels", - "Weißenhorn", - "Weißensee (Thüringen)", - "Weißenstadt", - "Weißenthurm", - "Weißwasser/Oberlausitz", - "Weiterstadt", - "Welzheim", - "Welzow", - "Wemding", - "Wendlingen am Neckar", - "Werben (Elbe)", - "Werdau", - "Werder (Havel)", - "Werdohl", - "Werl", - "Wermelskirchen", - "Wernau (Neckar)", - "Werne", - "Werneuchen", - "Wernigerode", - "Wertheim", - "Werther (Westf.)", - "Wertingen", - "Wesel", - "Wesenberg (Mecklenburg)", - "Wesselburen", - "Wesseling", - "Westerburg", - "Westerstede", - "Wetter (Ruhr)", - "Wetter (Hessen)", - "Wettin-Löbejün", - "Wetzlar", - "Widdern", - "Wiehe", - "Wiehl", - "Wiesbaden", - "Wiesmoor", - "Wiesensteig", - "Wiesloch", - "Wildau", - "Wildberg (Schwarzwald)", - "Wildemann", - "Wildenfels", - "Wildeshausen", - "Wilhelmshaven", - "Wilkau-Haßlau", - "Willebadessen", - "Willich", - "Wilsdruff", - "Wilster", - "Wilthen", - "Windischeschenbach", - "Windsbach", - "Winnenden", - "Winsen (Luhe)", - "Winterberg", - "Wipperfürth", - "Wirges", - "Wismar", - "Wissen (Stadt)", - "Witten", - "Lutherstadt Wittenberg", - "Wittenberge", - "Wittenburg", - "Wittichenau", - "Wittlich", - "Wittingen", - "Wittmund", - "Wittstock/Dosse", - "Witzenhausen", - "Woldegk", - "Wolfach", - "Wolfenbüttel", - "Wolfhagen", - "Wolframs-Eschenbach", - "Wolfratshausen", - "Wolfsburg", - "Wolfstein", - "Wolgast", - "Wolkenstein (Erzgebirge)", - "Wolmirstedt", - "Worms", - "Wörrstadt", - "Wörth am Rhein", - "Wörth an der Donau", - "Wörth am Main", - "Wriezen", - "Wülfrath", - "Wunsiedel", - "Wunstorf", - "Wuppertal", - "Würselen", - "Wurzbach", - "Würzburg", - "Wurzen", - "Wustrow (Wendland)", - "Wyk auf Föhr", - "Xanten", - "Zahna-Elster", - "Zarrentin am Schaalsee", - "Zehdenick", - "Zeil am Main", - "Zeitz", - "Zell am Harmersbach", - "Zell im Wiesental", - "Zell (Mosel)", - "Zella-Mehlis", - "Zerbst/Anhalt", - "Zeulenroda-Triebes", - "Zeven", - "Ziegenrück", - "Zierenberg", - "Ziesar", - "Zirndorf", - "Zittau", - "Zörbig", - "Zossen", - "Zschopau", - "Zülpich", - "Zweibrücken", - "Zwenkau", - "Zwickau", - "Zwiesel", - "Zwingenberg (Bergstraße)", - "Zwönitz" - }; - - public static readonly string[] Countries = - { - "Austria", - "Germany", - "Switzerland" - }; - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/Numbers.cs b/src/abstractions/Backend.Fx/RandomData/Numbers.cs deleted file mode 100644 index 182cd7c8..00000000 --- a/src/abstractions/Backend.Fx/RandomData/Numbers.cs +++ /dev/null @@ -1,5298 +0,0 @@ -using System.Globalization; -using System.Linq; - -namespace Backend.Fx.RandomData -{ - public static class Numbers - { - public static readonly string[] Ciphers = Enumerable.Range(0, 10).Select(i => i.ToString()).ToArray(); - public static readonly string[] PostalCodes = Enumerable.Range(9000, 90999).Select(i => i.ToString("00000")).ToArray(); - - public static readonly string[] LandLineNetworks = - { - "0201", - "0202", - "0203", - "02041", - "02043", - "02045", - "02051", - "02052", - "02053", - "02054", - "02056", - "02058", - "02064", - "02065", - "02066", - "0208", - "0209", - "02102", - "02103", - "02104", - "0211", - "0212", - "02129", - "02131", - "02132", - "02133", - "02137", - "0214", - "02150", - "02151", - "02152", - "02153", - "02154", - "02156", - "02157", - "02158", - "02159", - "02161", - "02162", - "02163", - "02164", - "02165", - "02166", - "02171", - "02173", - "02174", - "02175", - "02181", - "02182", - "02183", - "02191", - "02192", - "02193", - "02195", - "02196", - "02202", - "02203", - "02204", - "02205", - "02206", - "02207", - "02208", - "0221", - "02222", - "02223", - "02224", - "02225", - "02226", - "02227", - "02228", - "02232", - "02233", - "02234", - "02235", - "02236", - "02237", - "02238", - "02241", - "02242", - "02243", - "02244", - "02245", - "02246", - "02247", - "02248", - "02251", - "02252", - "02253", - "02254", - "02255", - "02256", - "02257", - "02261", - "02262", - "02263", - "02264", - "02265", - "02266", - "02267", - "02268", - "02269", - "02271", - "02272", - "02273", - "02274", - "02275", - "0228", - "02291", - "02292", - "02293", - "02294", - "02295", - "02296", - "02297", - "02301", - "02302", - "02303", - "02304", - "02305", - "02306", - "02307", - "02308", - "02309", - "0231", - "02323", - "02324", - "02325", - "02327", - "02330", - "02331", - "02332", - "02333", - "02334", - "02335", - "02336", - "02337", - "02338", - "02339", - "0234", - "02351", - "02352", - "02353", - "02354", - "02355", - "02357", - "02358", - "02359", - "02360", - "02361", - "02362", - "02363", - "02364", - "02365", - "02366", - "02367", - "02368", - "02369", - "02371", - "02372", - "02373", - "02374", - "02375", - "02377", - "02378", - "02379", - "02381", - "02382", - "02383", - "02384", - "02385", - "02387", - "02388", - "02389", - "02391", - "02392", - "02393", - "02394", - "02395", - "02401", - "02402", - "02403", - "02404", - "02405", - "02406", - "02407", - "02408", - "02409", - "0241", - "02421", - "02422", - "02423", - "02424", - "02425", - "02426", - "02427", - "02428", - "02429", - "02431", - "02432", - "02433", - "02434", - "02435", - "02436", - "02440", - "02441", - "02443", - "02444", - "02445", - "02446", - "02447", - "02448", - "02449", - "02451", - "02452", - "02453", - "02454", - "02455", - "02456", - "02461", - "02462", - "02463", - "02464", - "02465", - "02471", - "02472", - "02473", - "02474", - "02482", - "02484", - "02485", - "02486", - "02501", - "02502", - "02504", - "02505", - "02506", - "02507", - "02508", - "02509", - "0251", - "02520", - "02521", - "02522", - "02523", - "02524", - "02525", - "02526", - "02527", - "02528", - "02529", - "02532", - "02533", - "02534", - "02535", - "02536", - "02538", - "02541", - "02542", - "02543", - "02545", - "02546", - "02547", - "02548", - "02551", - "02552", - "02553", - "02554", - "02555", - "02556", - "02557", - "02558", - "02561", - "02562", - "02563", - "02564", - "02565", - "02566", - "02567", - "02568", - "02571", - "02572", - "02573", - "02574", - "02575", - "02581", - "02582", - "02583", - "02584", - "02585", - "02586", - "02587", - "02588", - "02590", - "02591", - "02592", - "02593", - "02594", - "02595", - "02596", - "02597", - "02598", - "02599", - "02601", - "02602", - "02603", - "02604", - "02605", - "02606", - "02607", - "02608", - "0261", - "02620", - "02621", - "02622", - "02623", - "02624", - "02625", - "02626", - "02627", - "02628", - "02630", - "02631", - "02632", - "02633", - "02634", - "02635", - "02636", - "02637", - "02638", - "02639", - "02641", - "02642", - "02643", - "02644", - "02645", - "02646", - "02647", - "02651", - "02652", - "02653", - "02654", - "02655", - "02656", - "02657", - "02661", - "02662", - "02663", - "02664", - "02666", - "02667", - "02671", - "02672", - "02673", - "02674", - "02675", - "02676", - "02677", - "02678", - "02680", - "02681", - "02682", - "02683", - "02684", - "02685", - "02686", - "02687", - "02688", - "02689", - "02691", - "02692", - "02693", - "02694", - "02695", - "02696", - "02697", - "0271", - "02721", - "02722", - "02723", - "02724", - "02725", - "02732", - "02733", - "02734", - "02735", - "02736", - "02737", - "02738", - "02739", - "02741", - "02742", - "02743", - "02744", - "02745", - "02747", - "02750", - "02751", - "02752", - "02753", - "02754", - "02755", - "02758", - "02759", - "02761", - "02762", - "02763", - "02764", - "02770", - "02771", - "02772", - "02773", - "02774", - "02775", - "02776", - "02777", - "02778", - "02779", - "02801", - "02802", - "02803", - "02804", - "0281", - "02821", - "02822", - "02823", - "02824", - "02825", - "02826", - "02827", - "02828", - "02831", - "02832", - "02833", - "02834", - "02835", - "02836", - "02837", - "02838", - "02839", - "02841", - "02842", - "02843", - "02844", - "02845", - "02850", - "02851", - "02852", - "02853", - "02855", - "02856", - "02857", - "02858", - "02859", - "02861", - "02862", - "02863", - "02864", - "02865", - "02866", - "02867", - "02871", - "02872", - "02873", - "02874", - "02902", - "02903", - "02904", - "02905", - "0291", - "02921", - "02922", - "02923", - "02924", - "02925", - "02927", - "02928", - "02931", - "02932", - "02933", - "02934", - "02935", - "02937", - "02938", - "02941", - "02942", - "02943", - "02944", - "02945", - "02947", - "02948", - "02951", - "02952", - "02953", - "02954", - "02955", - "02957", - "02958", - "02961", - "02962", - "02963", - "02964", - "02971", - "02972", - "02973", - "02974", - "02975", - "02977", - "02981", - "02982", - "02983", - "02984", - "02985", - "02991", - "02992", - "02993", - "02994", - "030", - "03301", - "03302", - "03303", - "03304", - "033051", - "033053", - "033054", - "033055", - "033056", - "03306", - "03307", - "033080", - "033082", - "033083", - "033084", - "033085", - "033086", - "033087", - "033088", - "033089", - "033093", - "033094", - "0331", - "033200", - "033201", - "033202", - "033203", - "033204", - "033205", - "033206", - "033207", - "033208", - "033209", - "03321", - "03322", - "033230", - "033231", - "033232", - "033233", - "033234", - "033235", - "033237", - "033238", - "033239", - "03327", - "03328", - "03329", - "03331", - "03332", - "033331", - "033332", - "033333", - "033334", - "033335", - "033336", - "033337", - "033338", - "03334", - "03335", - "033361", - "033362", - "033363", - "033364", - "033365", - "033366", - "033367", - "033368", - "033369", - "03337", - "03338", - "033393", - "033394", - "033395", - "033396", - "033397", - "033398", - "03341", - "03342", - "033432", - "033433", - "033434", - "033435", - "033436", - "033437", - "033438", - "033439", - "03344", - "033451", - "033452", - "033454", - "033456", - "033457", - "033458", - "03346", - "033470", - "033472", - "033473", - "033474", - "033475", - "033476", - "033477", - "033478", - "033479", - "0335", - "033601", - "033602", - "033603", - "033604", - "033605", - "033606", - "033607", - "033608", - "033609", - "03361", - "03362", - "033631", - "033632", - "033633", - "033634", - "033635", - "033636", - "033637", - "033638", - "03364", - "033652", - "033653", - "033654", - "033655", - "033656", - "033657", - "03366", - "033671", - "033672", - "033673", - "033674", - "033675", - "033676", - "033677", - "033678", - "033679", - "033701", - "033702", - "033703", - "033704", - "033708", - "03371", - "03372", - "033731", - "033732", - "033733", - "033734", - "033741", - "033742", - "033743", - "033744", - "033745", - "033746", - "033747", - "033748", - "03375", - "033760", - "033762", - "033763", - "033764", - "033765", - "033766", - "033767", - "033768", - "033769", - "03377", - "03378", - "03379", - "03381", - "03382", - "033830", - "033831", - "033832", - "033833", - "033834", - "033835", - "033836", - "033837", - "033838", - "033839", - "033841", - "033843", - "033844", - "033845", - "033846", - "033847", - "033848", - "033849", - "03385", - "03386", - "033870", - "033872", - "033873", - "033874", - "033875", - "033876", - "033877", - "033878", - "03391", - "033920", - "033921", - "033922", - "033923", - "033924", - "033925", - "033926", - "033928", - "033929", - "033931", - "033932", - "033933", - "03394", - "03395", - "033962", - "033963", - "033964", - "033965", - "033966", - "033967", - "033968", - "033969", - "033970", - "033971", - "033972", - "033973", - "033974", - "033975", - "033976", - "033977", - "033978", - "033979", - "033981", - "033982", - "033983", - "033984", - "033986", - "033989", - "0340", - "0341", - "034202", - "034203", - "034204", - "034205", - "034206", - "034207", - "034208", - "03421", - "034221", - "034222", - "034223", - "034224", - "03423", - "034241", - "034242", - "034243", - "034244", - "03425", - "034261", - "034262", - "034263", - "034291", - "034292", - "034293", - "034294", - "034295", - "034296", - "034297", - "034298", - "034299", - "03431", - "034321", - "034322", - "034324", - "034325", - "034327", - "034328", - "03433", - "034341", - "034342", - "034343", - "034344", - "034345", - "034346", - "034347", - "034348", - "03435", - "034361", - "034362", - "034363", - "034364", - "03437", - "034381", - "034382", - "034383", - "034384", - "034385", - "034386", - "03441", - "034422", - "034423", - "034424", - "034425", - "034426", - "03443", - "034441", - "034443", - "034444", - "034445", - "034446", - "03445", - "034461", - "034462", - "034463", - "034464", - "034465", - "034466", - "034467", - "03447", - "03448", - "034491", - "034492", - "034493", - "034494", - "034495", - "034496", - "034497", - "034498", - "0345", - "034600", - "034601", - "034602", - "034603", - "034604", - "034605", - "034606", - "034607", - "034609", - "03461", - "03462", - "034632", - "034633", - "034635", - "034636", - "034637", - "034638", - "034639", - "03464", - "034651", - "034652", - "034653", - "034654", - "034656", - "034658", - "034659", - "03466", - "034671", - "034672", - "034673", - "034691", - "034692", - "03471", - "034721", - "034722", - "03473", - "034741", - "034742", - "034743", - "034745", - "034746", - "03475", - "03476", - "034771", - "034772", - "034773", - "034774", - "034775", - "034776", - "034779", - "034781", - "034782", - "034783", - "034785", - "034901", - "034903", - "034904", - "034905", - "034906", - "034907", - "034909", - "03491", - "034920", - "034921", - "034922", - "034923", - "034924", - "034925", - "034926", - "034927", - "034928", - "034929", - "03493", - "03494", - "034953", - "034954", - "034955", - "034956", - "03496", - "034973", - "034975", - "034976", - "034977", - "034978", - "034979", - "03501", - "035020", - "035021", - "035022", - "035023", - "035024", - "035025", - "035026", - "035027", - "035028", - "035032", - "035033", - "03504", - "035052", - "035053", - "035054", - "035055", - "035056", - "035057", - "035058", - "0351", - "035200", - "035201", - "035202", - "035203", - "035204", - "035205", - "035206", - "035207", - "035208", - "035209", - "03521", - "03522", - "03523", - "035240", - "035241", - "035242", - "035243", - "035244", - "035245", - "035246", - "035247", - "035248", - "035249", - "03525", - "035263", - "035264", - "035265", - "035266", - "035267", - "035268", - "03528", - "03529", - "03531", - "035322", - "035323", - "035324", - "035325", - "035326", - "035327", - "035329", - "03533", - "035341", - "035342", - "035343", - "03535", - "035361", - "035362", - "035363", - "035364", - "035365", - "03537", - "035383", - "035384", - "035385", - "035386", - "035387", - "035388", - "035389", - "03541", - "03542", - "035433", - "035434", - "035435", - "035436", - "035439", - "03544", - "035451", - "035452", - "035453", - "035454", - "035455", - "035456", - "03546", - "035471", - "035472", - "035473", - "035474", - "035475", - "035476", - "035477", - "035478", - "0355", - "035600", - "035601", - "035602", - "035603", - "035604", - "035605", - "035606", - "035607", - "035608", - "035609", - "03561", - "03562", - "03563", - "03564", - "035691", - "035692", - "035693", - "035694", - "035695", - "035696", - "035697", - "035698", - "03571", - "035722", - "035723", - "035724", - "035725", - "035726", - "035727", - "035728", - "03573", - "03574", - "035751", - "035752", - "035753", - "035754", - "035755", - "035756", - "03576", - "035771", - "035772", - "035773", - "035774", - "035775", - "03578", - "035792", - "035793", - "035795", - "035796", - "035797", - "03581", - "035820", - "035822", - "035823", - "035825", - "035826", - "035827", - "035828", - "035829", - "03583", - "035841", - "035842", - "035843", - "035844", - "03585", - "03586", - "035872", - "035873", - "035874", - "035875", - "035876", - "035877", - "03588", - "035891", - "035892", - "035893", - "035894", - "035895", - "03591", - "03592", - "035930", - "035931", - "035932", - "035933", - "035934", - "035935", - "035936", - "035937", - "035938", - "035939", - "03594", - "035951", - "035952", - "035953", - "035954", - "035955", - "03596", - "035971", - "035973", - "035974", - "035975", - "03601", - "036020", - "036021", - "036022", - "036023", - "036024", - "036025", - "036026", - "036027", - "036028", - "036029", - "03603", - "036041", - "036042", - "036043", - "03605", - "03606", - "036071", - "036072", - "036074", - "036075", - "036076", - "036077", - "036081", - "036082", - "036083", - "036084", - "036085", - "036087", - "0361", - "036200", - "036201", - "036202", - "036203", - "036204", - "036205", - "036206", - "036207", - "036208", - "036209", - "03621", - "03622", - "03623", - "03624", - "036252", - "036253", - "036254", - "036255", - "036256", - "036257", - "036258", - "036259", - "03628", - "03629", - "03631", - "03632", - "036330", - "036331", - "036332", - "036333", - "036334", - "036335", - "036336", - "036337", - "036338", - "03634", - "03635", - "03636", - "036370", - "036371", - "036372", - "036373", - "036374", - "036375", - "036376", - "036377", - "036378", - "036379", - "03641", - "036421", - "036422", - "036423", - "036424", - "036425", - "036426", - "036427", - "036428", - "03643", - "03644", - "036450", - "036451", - "036452", - "036453", - "036454", - "036458", - "036459", - "036461", - "036462", - "036463", - "036464", - "036465", - "03647", - "036481", - "036482", - "036483", - "036484", - "0365", - "036601", - "036602", - "036603", - "036604", - "036605", - "036606", - "036607", - "036608", - "03661", - "036621", - "036622", - "036623", - "036624", - "036625", - "036626", - "036628", - "03663", - "036640", - "036642", - "036643", - "036644", - "036645", - "036646", - "036647", - "036648", - "036649", - "036651", - "036652", - "036653", - "036691", - "036692", - "036693", - "036694", - "036695", - "036701", - "036702", - "036703", - "036704", - "036705", - "03671", - "03672", - "036730", - "036731", - "036732", - "036733", - "036734", - "036735", - "036736", - "036737", - "036738", - "036739", - "036741", - "036742", - "036743", - "036744", - "03675", - "036761", - "036762", - "036764", - "036766", - "03677", - "036781", - "036782", - "036783", - "036784", - "036785", - "03679.", - "03681", - "03682", - "03683", - "036840", - "036841", - "036842", - "036843", - "036844", - "036845", - "036846", - "036847", - "036848", - "036849", - "03685", - "03686", - "036870", - "036871", - "036873", - "036874", - "036875", - "036878", - "03691", - "036920", - "036921", - "036922", - "036923", - "036924", - "036925", - "036926", - "036927", - "036928", - "036929", - "03693", - "036940", - "036941", - "036943", - "036944", - "036945", - "036946", - "036947", - "036948", - "036949", - "03695", - "036961", - "036962", - "036963", - "036964", - "036965", - "036966", - "036967", - "036968", - "036969", - "0371", - "037200", - "037202", - "037203", - "037204", - "037206", - "037207", - "037208", - "037209", - "03721", - "03722", - "03723", - "03724", - "03725", - "03726", - "03727", - "037291", - "037292", - "037293", - "037294", - "037295", - "037296", - "037297", - "037298", - "03731", - "037320", - "037321", - "037322", - "037323", - "037324", - "037325", - "037326", - "037327", - "037328", - "037329", - "03733", - "037341", - "037342", - "037343", - "037344", - "037346", - "037347", - "037348", - "037349", - "03735", - "037360", - "037361", - "037362", - "037363", - "037364", - "037365", - "037366", - "037367", - "037368", - "037369", - "03737", - "037381", - "037382", - "037383", - "037384", - "03741", - "037421", - "037422", - "037423", - "037430", - "037431", - "037432", - "037433", - "037434", - "037435", - "037436", - "037437", - "037438", - "037439", - "03744", - "03745", - "037462", - "037463", - "037464", - "037465", - "037467", - "037468", - "0375", - "037600", - "037601", - "037602", - "037603", - "037604", - "037605", - "037606", - "037607", - "037608", - "037609", - "03761", - "03762", - "03763", - "03764", - "03765", - "03771", - "03772", - "03773", - "03774", - "037752", - "037754", - "037755", - "037756", - "037757", - "0381", - "038201", - "038202", - "038203", - "038204", - "038205", - "038206", - "038207", - "038208", - "038209", - "03821", - "038220", - "038221", - "038222", - "038223", - "038224", - "038225", - "038226", - "038227", - "038228", - "038229", - "038231", - "038232", - "038233", - "038234", - "038292", - "038293", - "038294", - "038295", - "038296", - "038297", - "038300", - "038301", - "038302", - "038303", - "038304", - "038305", - "038306", - "038307", - "038308", - "038309", - "03831", - "038320", - "038321", - "038322", - "038323", - "038324", - "038325", - "038326", - "038327", - "038328", - "038331", - "038332", - "038333", - "038334", - "03834", - "038351", - "038352", - "038353", - "038354", - "038355", - "038356", - "03836", - "038370", - "038371", - "038372", - "038373", - "038374", - "038375", - "038376", - "038377", - "038378,", - "038379", - "03838", - "038391", - "038392", - "038393", - "03841", - "038422", - "038423", - "038424", - "038425", - "038426", - "038427", - "038428", - "038429", - "03843", - "03844", - "038450", - "038451", - "038452", - "038453", - "038454", - "038455", - "038456", - "038457", - "038458", - "038459", - "038461", - "038462", - "038464", - "038466", - "03847", - "038481", - "038482", - "038483", - "038484", - "038485", - "038486", - "038488", - "0385", - "03860", - "03861", - "03863", - "03865", - "03866", - "03867", - "03868", - "03869", - "03871", - "038720", - "038721", - "038722", - "038723", - "038724", - "038725", - "038726", - "038727", - "038728", - "038729", - "038731", - "038732", - "038733", - "038735", - "038736", - "038737", - "038738", - "03874", - "038750", - "038751", - "038752", - "038753", - "038754", - "038755", - "038756", - "038757", - "038758", - "038759", - "03876", - "03877", - "038780", - "038781", - "038782", - "038783", - "038784", - "038785", - "038787", - "038788", - "038789", - "038791", - "038792", - "038793", - "038794", - "038796", - "038797", - "03881", - "038821", - "038822.", - "038823", - "038824", - "038825", - "038826", - "038827", - "038828", - "03883", - "038841", - "038842", - "038843", - "038844", - "038845", - "038847", - "038848", - "038850", - "038851", - "038852", - "038853", - "038854", - "038855", - "038856", - "038858", - "038859", - "03886", - "038871", - "038872", - "038873", - "038874", - "038875", - "038876", - "039000", - "039001", - "039002", - "039003", - "039004", - "039005", - "039006", - "039007", - "039008", - "039009", - "03901", - "03902", - "039030", - "039031", - "039032", - "039033", - "039034", - "039035", - "039036", - "039037", - "039038", - "039039", - "03904", - "039050", - "039051", - "039052", - "039053", - "039054", - "039055", - "039056", - "039057", - "039058", - "039059", - "039061", - "039062", - "03907", - "039080", - "039081", - "039082", - "039083", - "039084", - "039085", - "039086", - "039087", - "039088", - "039089", - "03909", - "0391", - "039200", - "039201", - "039202", - "039203", - "039204", - "039205", - "039206", - "039207", - "039208", - "039209", - "03921", - "039221", - "039222", - "039223", - "039224", - "039225", - "039226", - "03923", - "039241", - "039242", - "039243", - "039244", - "039245", - "039246", - "039247", - "039248", - "03925", - "039262", - "039263", - "039264", - "039265", - "039266", - "039267", - "039268", - "03928", - "039291", - "039292", - "039293", - "039294", - "039295", - "039296", - "039297", - "039298", - "03931", - "039320", - "039321", - "039322", - "039323", - "039324", - "039325", - "039327", - "039328", - "039329", - "03933", - "039341", - "039342", - "039343", - "039344", - "039345", - "039346", - "039347", - "039348", - "039349", - "03935", - "039361", - "039362", - "039363", - "039364", - "039365", - "039366", - "03937", - "039382", - "039383", - "039384", - "039386", - "039387", - "039388", - "039389", - "039390", - "039391", - "039392", - "039393", - "039394", - "039395", - "039396", - "039397", - "039398", - "039399", - "039400", - "039401", - "039402", - "039403", - "039404", - "039405", - "039406", - "039407", - "039408", - "039409", - "03941", - "039421", - "039422", - "039423", - "039424", - "039425", - "039426", - "039427", - "039428", - "03943", - "03944", - "039451", - "039452", - "039453", - "039454", - "039455", - "039456", - "039457", - "039458", - "039459", - "03946", - "03947", - "039481", - "039482", - "039483", - "039484", - "039485", - "039487", - "039488", - "039489", - "03949", - "0395", - "039600", - "039601", - "039602", - "039603", - "039604", - "039605", - "039606", - "039607", - "039608", - "03961", - "03962", - "03963", - "03964", - "03965", - "03966", - "03967", - "03968", - "03969", - "03971", - "039721", - "039722", - "039723", - "039724", - "039726", - "039727", - "039728", - "03973", - "039740", - "039741", - "039742", - "039743", - "039744", - "039745", - "039746", - "039747", - "039748", - "039749", - "039751", - "039752", - "039753", - "039754", - "03976", - "039771", - "039772", - "039773", - "039774", - "039775", - "039776", - "039777", - "039778", - "039779", - "03981", - "039820", - "039821", - "039822", - "039823", - "039824", - "039825", - "039826", - "039827", - "039828", - "039829", - "039831", - "039832", - "039833", - "03984", - "039851", - "039852", - "039853", - "039854", - "039855", - "039856", - "039857", - "039858", - "039859", - "039861", - "039862", - "039863", - "03987", - "039881", - "039882", - "039883", - "039884", - "039885", - "039886", - "039887", - "039888", - "039889", - "03991", - "039921", - "039922", - "039923", - "039924", - "039925", - "039926", - "039927", - "039928", - "039929", - "039931", - "039932", - "039933", - "039934", - "03994", - "039951", - "039952", - "039953", - "039954", - "039955", - "039956", - "039957", - "039959", - "03996", - "039971", - "039972", - "039973", - "039975", - "039976", - "039977", - "039978", - "03998", - "039991", - "039992", - "039993", - "039994", - "039995", - "039996", - "039997", - "039998", - "039999", - "040", - "040", - "040", - "040", - "040", - "04101", - "04102", - "04103", - "04104", - "04105", - "04106", - "04107", - "04108", - "04109", - "04120", - "04121", - "04122", - "04123", - "04124", - "04125", - "04126", - "04127", - "04128", - "04129", - "04131", - "04132", - "04133", - "04134", - "04135", - "04136", - "04137", - "04138", - "04139", - "04140", - "04141", - "04142", - "04143", - "04144", - "04146", - "04148", - "04149", - "04151", - "04152", - "04153", - "04154", - "04155", - "04156", - "04158", - "04159", - "04161", - "04162", - "04163", - "04164", - "04165", - "04166", - "04167", - "04168", - "04169", - "04171", - "04172", - "04173", - "04174", - "04175", - "04176", - "04177", - "04178", - "04179", - "04180", - "04181", - "04182", - "04183", - "04184", - "04185", - "04186", - "04187", - "04188", - "04189", - "04191", - "04192", - "04193", - "04194", - "04195", - "04202", - "04203", - "04204", - "04205", - "04206", - "04207", - "04208", - "04209", - "0421", - "04221", - "04222", - "04223", - "04224", - "04230", - "04231", - "04232", - "04233", - "04234", - "04235", - "04236", - "04237", - "04238", - "04239", - "04240", - "04241", - "04242", - "04243", - "04244", - "04245", - "04246", - "04247", - "04248", - "04249", - "04251", - "04252", - "04253", - "04254", - "04255", - "04256", - "04257", - "04258", - "04260", - "04261", - "04262", - "04263", - "04264", - "04265", - "04266", - "04267", - "04268", - "04269", - "04271", - "04272", - "04273", - "04274", - "04275", - "04276", - "04277", - "04281", - "04282", - "04283", - "04284", - "04285", - "04286", - "04287", - "04288", - "04289", - "04292", - "04293", - "04294", - "04295", - "04296", - "04297", - "04298", - "04302", - "04303", - "04305", - "04307", - "04308", - "0431", - "04320", - "04321", - "04322", - "04323", - "04324", - "04326", - "04327", - "04328", - "04329", - "04330", - "04331", - "04332", - "04333", - "04334", - "04335", - "04336", - "04337", - "04338", - "04339", - "04340", - "04342", - "04343", - "04344", - "04346", - "04347", - "04348", - "04349", - "04351", - "04352", - "04353", - "04354", - "04355", - "04356", - "04357", - "04358", - "04361", - "04362", - "04363", - "04364", - "04365", - "04366", - "04367", - "04371", - "04372", - "04381", - "04382", - "04383", - "04384", - "04385", - "04392", - "04393", - "04394", - "04401", - "04402", - "04403", - "04404", - "04405", - "04406", - "04407", - "04408", - "04409", - "0441", - "04421", - "04422", - "04423", - "04425", - "04426", - "04431", - "04432", - "04433", - "04434", - "04435", - "04441", - "04442", - "04443", - "04444", - "04445", - "04446", - "04447", - "04451", - "04452", - "04453", - "04454", - "04455", - "04456", - "04458", - "04461", - "04462", - "04463", - "04464", - "04465", - "04466", - "04467", - "04468", - "04469", - "04471", - "04472", - "04473", - "04474", - "04475", - "04477", - "04478", - "04479", - "04480", - "04481", - "04482", - "04483", - "04484", - "04485", - "04486", - "04487", - "04488", - "04489", - "04491", - "04492", - "04493", - "04494", - "04495", - "04496", - "04497", - "04498", - "04499", - "04501", - "04502", - "04503", - "04504", - "04505", - "04506", - "04508", - "04509", - "0451", - "04521", - "04522", - "04523", - "04524", - "04525", - "04526", - "04527", - "04528", - "04529", - "04531", - "04532", - "04533", - "04534", - "04535", - "04536", - "04537", - "04539", - "04541", - "04542", - "04543", - "04544", - "04545", - "04546", - "04547", - "04550", - "04551", - "04552", - "04553", - "04554", - "04555", - "04556", - "04557", - "04558", - "04559", - "04561", - "04562", - "04563", - "04564", - "04602", - "04603", - "04604", - "04605", - "04606", - "04607", - "04608", - "04609", - "0461", - "04621", - "04622", - "04623", - "04624", - "04625", - "04626", - "04627", - "04630", - "04631", - "04632", - "04633", - "04634", - "04635", - "04636", - "04637", - "04638", - "04639", - "04641", - "04642", - "04643", - "04644", - "04646", - "04651", - "04661", - "04662", - "04663", - "04664", - "04665", - "04666", - "04667", - "04668", - "04671", - "04672", - "04673", - "04674", - "04681", - "04682", - "04683", - "04684", - "04702", - "04703", - "04704", - "04705", - "04706", - "04707", - "04708", - "0471", - "04721", - "04722", - "04723", - "04724", - "04725", - "04731", - "04732", - "04733", - "04734", - "04735", - "04736", - "04737", - "04740", - "04741", - "04742", - "04743", - "04744", - "04745", - "04746", - "04747", - "04748", - "04749", - "04751", - "04752", - "04753", - "04754", - "04755", - "04756", - "04757", - "04758", - "04761", - "04762", - "04763", - "04764", - "04765", - "04766", - "04767", - "04768", - "04769", - "04770", - "04771", - "04772", - "04773", - "04774", - "04775", - "04776", - "04777", - "04778", - "04779", - "04791", - "04792", - "04793", - "04794", - "04795", - "04796", - "04802", - "04803", - "04804", - "04805", - "04806", - "0481", - "04821", - "04822", - "04823", - "04824", - "04825", - "04826", - "04827", - "04828", - "04829", - "04830", - "04832", - "04833", - "04834", - "04835", - "04836", - "04837", - "04838", - "04839", - "04841", - "04842", - "04843", - "04844", - "04845", - "04846", - "04847", - "04848", - "04849", - "04851", - "04852", - "04853", - "04854", - "04855", - "04856", - "04857", - "04858", - "04859", - "04861", - "04862", - "04863", - "04864", - "04865", - "04871", - "04872", - "04873", - "04874", - "04875", - "04876", - "04877", - "04881", - "04882", - "04883", - "04884", - "04885", - "04892", - "04893", - "04902", - "04903", - "0491", - "04920", - "04921", - "04922", - "04923", - "04924", - "04925", - "04926", - "04927", - "04928", - "04929", - "04931", - "04932", - "04933", - "04934", - "04935", - "04936", - "04938", - "04939", - "04941", - "04942", - "04943", - "04944", - "04945", - "04946", - "04947", - "04948", - "04950", - "04951", - "04952", - "04953", - "04954", - "04955", - "04956", - "04957", - "04958", - "04959", - "04961", - "04962", - "04963", - "04964", - "04965", - "04966", - "04967", - "04968", - "04971", - "04972", - "04973", - "04974", - "04975", - "04976", - "04977", - "05021", - "05022", - "05023", - "05024", - "05025", - "05026", - "05027", - "05028", - "05031", - "05032", - "05033", - "05034", - "05035", - "05036", - "05037", - "05041", - "05042", - "05043", - "05044", - "05045", - "05051", - "05052", - "05053", - "05054", - "05055", - "05056", - "05060", - "05062", - "05063", - "05064", - "05065", - "05066", - "05067", - "05068", - "05069", - "05071", - "05072", - "05073", - "05074", - "05082", - "05083", - "05084", - "05085", - "05086", - "05101", - "05102", - "05103", - "05105", - "05108", - "05109", - "0511", - "05121", - "05123", - "05126", - "05127", - "05128", - "05129", - "05130", - "05131", - "05132", - "05135", - "05136", - "05137", - "05138", - "05139", - "05141", - "05142", - "05143", - "05144", - "05145", - "05146", - "05147", - "05148", - "05149", - "05151", - "05152", - "05153", - "05154", - "05155", - "05156", - "05157", - "05158", - "05159", - "05161", - "05162", - "05163", - "05164", - "05165", - "05166", - "05167", - "05168", - "05171", - "05172", - "05173", - "05174", - "05175", - "05176", - "05177", - "05181", - "05182", - "05183", - "05184", - "05185", - "05186", - "05187", - "05190", - "05191", - "05192", - "05193", - "05194", - "05195", - "05196", - "05197", - "05198", - "05199", - "05201", - "05202", - "05203", - "05204", - "05205", - "05206", - "05207", - "05208", - "05209", - "0521", - "05221", - "05222", - "05223", - "05224", - "05225", - "05226", - "05228", - "05231", - "05232", - "05233", - "05234", - "05235", - "05236", - "05237", - "05238", - "05241", - "05242", - "05244", - "05245", - "05246", - "05247", - "05248", - "05250", - "05251", - "05252", - "05253", - "05254", - "05255", - "05257", - "05258", - "05259", - "05261", - "05262", - "05263", - "05264", - "05265", - "05266", - "05271", - "05272", - "05273", - "05274", - "05275", - "05276", - "05277", - "05278", - "05281", - "05282", - "05283", - "05284", - "05285", - "05286", - "05292", - "05293", - "05294", - "05295", - "05300", - "05301", - "05302", - "05303", - "05304", - "05305", - "05306", - "05307", - "05308", - "05309", - "0531", - "05320", - "05321", - "05322", - "05323", - "05324", - "05325", - "05326", - "05327", - "05328", - "05329", - "05331", - "05332", - "05333", - "05334", - "05335", - "05336", - "05337", - "05339", - "05341", - "05344", - "05345", - "05346", - "05347", - "05351", - "05352", - "05353", - "05354", - "05355", - "05356", - "05357", - "05358", - "05361", - "05362", - "05363", - "05364", - "05365", - "05366", - "05367", - "05368", - "05371", - "05372", - "05373", - "05374", - "05375", - "05376", - "05377", - "05378", - "05379", - "05381", - "05382", - "05383", - "05384", - "05401", - "05402", - "05403", - "05404", - "05405", - "05406", - "05407", - "05409", - "0541", - "05421", - "05422", - "05423", - "05424", - "05425", - "05426", - "05427", - "05428", - "05429", - "05431", - "05432", - "05433", - "05434", - "05435", - "05436", - "05437", - "05438", - "05439", - "05441", - "05442", - "05443", - "05444", - "05445", - "05446", - "05447", - "05448", - "05451", - "05452", - "05453", - "05454", - "05455", - "05456", - "05457", - "05458", - "05459", - "05461", - "05462", - "05464", - "05465", - "05466", - "05467", - "05468", - "05471", - "05472", - "05473", - "05474", - "05475", - "05476", - "05481", - "05482", - "05483", - "05484", - "05485", - "05491", - "05492", - "05493", - "05494", - "05495", - "05502", - "05503", - "05504", - "05505", - "05506", - "05507", - "05508", - "05509", - "0551", - "05520", - "05521", - "05522", - "05523", - "05524", - "05525", - "05527", - "05528", - "05529", - "05531", - "05532", - "05533", - "05534", - "05535", - "05536", - "05541", - "05542", - "05543", - "05544", - "05545", - "05546", - "05551", - "05552", - "05553", - "05554", - "05555", - "05556", - "05561", - "05562", - "05563", - "05564", - "05565", - "05571", - "05572", - "05573", - "05574", - "05582", - "05583", - "05584", - "05585", - "05586", - "05592", - "05593", - "05594", - "05601", - "05602", - "05603", - "05604", - "05605", - "05606", - "05607", - "05608", - "05609", - "0561", - "05621", - "05622", - "05623", - "05624", - "05625", - "05626", - "05631", - "05632", - "05633", - "05634", - "05635", - "05636", - "05641", - "05642", - "05643", - "05644", - "05645", - "05646", - "05647", - "05648", - "05650", - "05651", - "05652", - "05653", - "05654", - "05655", - "05656", - "05657", - "05658", - "05659", - "05661", - "05662", - "05663", - "05664", - "05665", - "05671", - "05672", - "05673", - "05674", - "05675", - "05676", - "05677", - "05681", - "05682", - "05683", - "05684", - "05685", - "05686", - "05691", - "05692", - "05693", - "05694", - "05695", - "05696", - "05702", - "05703", - "05704", - "05705", - "05706", - "05707", - "0571", - "05721", - "05722", - "05723", - "05724", - "05725", - "05726", - "05731", - "05732", - "05733", - "05734", - "05741", - "05742", - "05743", - "05744", - "05745", - "05746", - "05751", - "05752", - "05753", - "05754", - "05755", - "05761", - "05763", - "05764", - "05765", - "05766", - "05767", - "05768", - "05769", - "05771", - "05772", - "05773", - "05774", - "05775", - "05776", - "05777", - "05802", - "05803", - "05804", - "05805", - "05806", - "05807", - "05808", - "0581", - "05820", - "05821", - "05822", - "05823", - "05824", - "05825", - "05826", - "05827", - "05828", - "05829", - "05831", - "05832", - "05833", - "05834", - "05835", - "05836", - "05837", - "05838", - "05839", - "05840", - "05841", - "05842", - "05843", - "05844", - "05845", - "05846", - "05848", - "05849", - "05850", - "05851", - "05852", - "05853", - "05854", - "05855", - "05857", - "05858", - "05859", - "05861", - "05862", - "05863", - "05864", - "05865", - "05872", - "05873", - "05874", - "05875", - "05882", - "05883", - "05901", - "05902", - "05903", - "05904", - "05905", - "05906", - "05907", - "05908", - "05909", - "0591", - "05921", - "05922", - "05923", - "05924", - "05925", - "05926", - "05931", - "05932", - "05933", - "05934", - "05935", - "05936", - "05937", - "05939", - "05941", - "05942", - "05943", - "05944", - "05945", - "05946", - "05947", - "05948", - "05951", - "05952", - "05953", - "05954", - "05955", - "05956", - "05957", - "05961", - "05962", - "05963", - "05964", - "05965", - "05966", - "05971", - "05973", - "05975", - "05976", - "05977", - "05978", - "06002", - "06003", - "06004", - "06007", - "06008", - "06020", - "06021", - "06022", - "06023", - "06024", - "06026", - "06027", - "06028", - "06029", - "06031", - "06032", - "06033", - "06034", - "06035", - "06036", - "06039", - "06041", - "06042", - "06043", - "06044", - "06045", - "06046", - "06047", - "06048", - "06049", - "06050", - "06051", - "06052", - "06053", - "06054", - "06055", - "06056", - "06057", - "06058", - "06059", - "06061", - "06062", - "06063", - "06066", - "06068", - "06071", - "06073", - "06074", - "06078", - "06081", - "06082", - "06083", - "06084", - "06085", - "06086", - "06087", - "06092", - "06093", - "06094", - "06095", - "06096", - "06101", - "06102", - "06103", - "06104", - "06105", - "06106", - "06107", - "06108", - "06109", - "0611", - "06120", - "06122", - "06123", - "06124", - "06126", - "06127", - "06128", - "06129", - "06130", - "06131", - "06132", - "06133", - "06134", - "06135", - "06136", - "06138", - "06139", - "06142", - "06144", - "06145", - "06146", - "06147", - "06150", - "06151", - "06152", - "06154", - "06155", - "06157", - "06158", - "06159", - "06161", - "06162", - "06163", - "06164", - "06165", - "06166", - "06167", - "06171", - "06172", - "06173", - "06174", - "06175", - "06181", - "06182", - "06183", - "06184", - "06185", - "06186", - "06187", - "06188", - "06190", - "06192", - "06195", - "06196", - "06198", - "06201", - "06202", - "06203", - "06204", - "06205", - "06206", - "06207", - "06209", - "0621", - "0621", - "06220", - "06221", - "06222", - "06223", - "06224", - "06226", - "06227", - "06228", - "06229", - "06231", - "06232", - "06233", - "06234", - "06235", - "06236", - "06237", - "06238", - "06239", - "06241", - "06242", - "06243", - "06244", - "06245", - "06246", - "06247", - "06249", - "06251", - "06252", - "06253", - "06254", - "06255", - "06256", - "06257", - "06258", - "06261", - "06262", - "06263", - "06264", - "06265", - "06266", - "06267", - "06268", - "06269", - "06271", - "06272", - "06274", - "06275", - "06276", - "06281", - "06282", - "06283", - "06284", - "06285", - "06286", - "06287", - "06291", - "06292", - "06293", - "06294", - "06295", - "06296", - "06297", - "06298", - "06301", - "06302", - "06303", - "06304", - "06305", - "06306", - "06307", - "06308", - "0631", - "06321", - "06322", - "06323", - "06324", - "06325", - "06326", - "06327", - "06328", - "06329", - "06331", - "06332", - "06333", - "06334", - "06335", - "06336", - "06337", - "06338", - "06339", - "06340", - "06341", - "06342", - "06343", - "06344", - "06345", - "06346", - "06347", - "06348", - "06349", - "06351", - "06352", - "06353", - "06355", - "06356", - "06357", - "06358", - "06359", - "06361", - "06362", - "06363", - "06364", - "06371", - "06372", - "06373", - "06374", - "06375", - "06381", - "06382", - "06383", - "06384", - "06385", - "06386", - "06387", - "06391", - "06392", - "06393", - "06394", - "06395", - "06396", - "06397", - "06398", - "06400", - "06401", - "06402", - "06403", - "06404", - "06405", - "06406", - "06407", - "06408", - "06409", - "0641", - "06420", - "06421", - "06422", - "06423", - "06424", - "06425", - "06426", - "06427", - "06428", - "06429", - "06430", - "06431", - "06432", - "06433", - "06434", - "06435", - "06436", - "06438", - "06439", - "06440", - "06441", - "06442", - "06443", - "06444", - "06445", - "06446", - "06447", - "06449", - "06451", - "06452", - "06453", - "06454", - "06455", - "06456", - "06457", - "06458", - "06461", - "06462", - "06464", - "06465", - "06466", - "06467", - "06468", - "06471", - "06472", - "06473", - "06474", - "06475", - "06476", - "06477", - "06478", - "06479", - "06482", - "06483", - "06484", - "06485", - "06486", - "06500", - "06501", - "06502", - "06503", - "06504", - "06505", - "06506", - "06507", - "06508", - "06509", - "0651", - "06522", - "06523", - "06524", - "06525", - "06526", - "06527", - "06531", - "06532", - "06533", - "06534", - "06535", - "06536", - "06541", - "06542", - "06543", - "06544", - "06545", - "06550", - "06551", - "06552", - "06553", - "06554", - "06555", - "06556", - "06557", - "06558", - "06559", - "06561", - "06562", - "06563", - "06564", - "06565", - "06566", - "06567", - "06568", - "06569", - "06571", - "06572", - "06573", - "06574", - "06575", - "06578", - "06580", - "06581", - "06582", - "06583", - "06584", - "06585", - "06586", - "06587", - "06588", - "06589", - "06591", - "06592", - "06593", - "06594", - "06595", - "06596", - "06597", - "06599", - "0661", - "06620", - "06621", - "06622", - "06623", - "06624", - "06625", - "06626", - "06627", - "06628", - "06629", - "06630", - "06631", - "06633", - "06634", - "06635", - "06636", - "06637", - "06638", - "06639", - "06641", - "06642", - "06643", - "06644", - "06645", - "06646", - "06647", - "06648", - "06650", - "06651", - "06652", - "06653", - "06654", - "06655", - "06656", - "06657", - "06658", - "06659", - "06660", - "06661", - "06663", - "06664", - "06665", - "06666", - "06667", - "06668", - "06669", - "06670", - "06672", - "06673", - "06674", - "06675", - "06676", - "06677", - "06678", - "06681", - "06682", - "06683", - "06684", - "06691", - "06692", - "06693", - "06694", - "06695", - "06696", - "06697", - "06698", - "06701", - "06703", - "06704", - "06706", - "06707", - "06708", - "06709", - "0671", - "06721", - "06722", - "06723", - "06724", - "06725", - "06726", - "06727", - "06728", - "06731", - "06732", - "06733", - "06734", - "06735", - "06736", - "06737", - "06741", - "06742", - "06743", - "06744", - "06745", - "06746", - "06747", - "06751", - "06752", - "06753", - "06754", - "06755", - "06756", - "06757", - "06758", - "06761", - "06762", - "06763", - "06764", - "06765", - "06766", - "06771", - "06772", - "06773", - "06774", - "06775", - "06776", - "06781", - "06782", - "06783", - "06784", - "06785", - "06786", - "06787", - "06788", - "06789", - "06802", - "06803", - "06804", - "06805", - "06806", - "06809", - "0681", - "06821", - "06824", - "06825", - "06826", - "06827", - "06831", - "06832", - "06833", - "06834", - "06835", - "06836", - "06837", - "06838", - "06841", - "06842", - "06843", - "06844", - "06848", - "06849", - "06851", - "06852", - "06853", - "06854", - "06855", - "06856", - "06857", - "06858", - "06861", - "06864", - "06865", - "06866", - "06867", - "06868", - "06869", - "06871", - "06872", - "06873", - "06874", - "06875", - "06876", - "06881", - "06887", - "06888", - "06893", - "06894", - "06897", - "06898", - "069", - "069", - "07021", - "07022", - "07023", - "07024", - "07025", - "07026", - "07031", - "07032", - "07033", - "07034", - "07041", - "07042", - "07043", - "07044", - "07045", - "07046", - "07051", - "07052", - "07053", - "07054", - "07055", - "07056", - "07062", - "07063", - "07066", - "07071", - "07072", - "07073", - "07081", - "07082", - "07083", - "07084", - "07085", - "0711", - "0711", - "07121", - "07122", - "07123", - "07124", - "07125", - "07126", - "07127", - "07128", - "07129", - "07130", - "07131", - "07132", - "07133", - "07134", - "07135", - "07136", - "07138", - "07139", - "07141", - "07142", - "07143", - "07144", - "07145", - "07146", - "07147", - "07148", - "07150", - "07151", - "07152", - "07153", - "07153", - "07154", - "07156", - "07157", - "07158", - "07159", - "07161", - "07162", - "07163", - "07164", - "07165", - "07166", - "07171", - "07172", - "07173", - "07174", - "07175", - "07176", - "07181", - "07182", - "07183", - "07184", - "07191", - "07192", - "07193", - "07194", - "07195", - "07202", - "07203", - "07204", - "0721", - "07220", - "07221", - "07222", - "07223", - "07224", - "07225", - "07226", - "07227", - "07228", - "07229", - "07231", - "07232", - "07233", - "07234", - "07235", - "07236", - "07237", - "07240", - "07242", - "07243", - "07244", - "07245", - "07246", - "07247", - "07248", - "07249", - "07250", - "07251", - "07252", - "07253", - "07254", - "07255", - "07256", - "07257", - "07258", - "07259", - "07260", - "07261", - "07262", - "07263", - "07264", - "07265", - "07266", - "07267", - "07268", - "07269", - "07271", - "07272", - "07273", - "07274", - "07275", - "07276", - "07277", - "07300", - "07302", - "07303", - "07304", - "07305", - "07306", - "07307", - "07308", - "07309", - "0731", - "07321", - "07322", - "07323", - "07324", - "07325", - "07326", - "07327", - "07328", - "07329", - "07331", - "07332", - "07333", - "07334", - "07335", - "07336", - "07337", - "07340", - "07343", - "07344", - "07345", - "07346", - "07347", - "07348", - "07351", - "07352", - "07353", - "07354", - "07355", - "07356", - "07357", - "07358", - "07361", - "07362", - "07363", - "07364", - "07365", - "07366", - "07367", - "07371", - "07373", - "07374", - "07375", - "07376", - "07381", - "07382", - "07383", - "07384", - "07385", - "07386", - "07387", - "07388", - "07389", - "07391", - "07392", - "07393", - "07394", - "07395", - "07402", - "07403", - "07404", - "0741", - "07420", - "07422", - "07423", - "07424", - "07425", - "07426", - "07427", - "07428", - "07429", - "07431", - "07432", - "07433", - "07434", - "07435", - "07436", - "07440", - "07441", - "07442", - "07443", - "07444", - "07445", - "07446", - "07447", - "07448", - "07449", - "07451", - "07452", - "07453,", - "07454", - "07455", - "07456", - "07457", - "07458", - "07459", - "07461", - "07462", - "07463", - "07464", - "07465", - "07466", - "07467", - "07471", - "07472", - "07473", - "07474", - "07475", - "07476", - "07477", - "07478", - "07482", - "07483", - "07484", - "07485", - "07486", - "07502", - "07503", - "07504", - "07505", - "07506", - "0751", - "07520", - "07522", - "07524", - "07525", - "07527", - "07528", - "07529", - "07531", - "07532", - "07533", - "07534", - "07541", - "07542", - "07543", - "07544", - "07545", - "07546", - "07551", - "07552", - "07553", - "07554", - "07555", - "07556", - "07557", - "07558", - "07561", - "07562", - "07563", - "07564", - "07565", - "07566", - "07567", - "07568", - "07569", - "07570", - "07571", - "07572", - "07573", - "07574", - "07575", - "07576", - "07577", - "07578", - "07579", - "07581", - "07582", - "07583", - "07584", - "07585", - "07586", - "07587", - "07602", - "0761", - "07620", - "07621", - "07622", - "07623", - "07624", - "07625", - "07626", - "07627", - "07628", - "07629", - "07631", - "07632", - "07633", - "07634", - "07635", - "07636", - "07641", - "07642", - "07643", - "07644", - "07645", - "07646", - "07651", - "07652", - "07653", - "07654", - "07655", - "07656", - "07657", - "07660", - "07661", - "07662", - "07663", - "07664", - "07665", - "07666", - "07667", - "07668", - "07669", - "07671", - "07672", - "07673", - "07674", - "07675", - "07676", - "07681", - "07682", - "07683", - "07684", - "07685", - "07702", - "07703", - "07704", - "07705", - "07706", - "07707", - "07708", - "07709", - "0771", - "07720", - "07721", - "07722", - "07723", - "07724", - "07725", - "07726", - "07727", - "07728", - "07729", - "07731", - "07732", - "07733", - "07734", - "07735", - "07736", - "07738", - "07739", - "07741", - "07742", - "07743", - "07744", - "07745", - "07746", - "07747", - "07748", - "07751", - "07753", - "07754", - "07755", - "07761", - "07762", - "07763", - "07764", - "07765", - "07771", - "07773", - "07774", - "07775", - "07777", - "07802", - "07803", - "07804", - "07805", - "07806", - "07807", - "07808", - "0781", - "07821", - "07822", - "07823", - "07824", - "07825", - "07826", - "07831", - "07832", - "07833", - "07834", - "07835", - "07836", - "07837", - "07838", - "07839", - "07841", - "07842", - "07843", - "07844", - "07851", - "07852", - "07853", - "07854", - "07903", - "07904", - "07905", - "07906", - "07907", - "0791", - "07930", - "07931", - "07932", - "07933", - "07934", - "07935", - "07936", - "07937", - "07938", - "07939", - "07940", - "07941", - "07942", - "07943", - "07944", - "07945", - "07946", - "07947", - "07948", - "07949", - "07950", - "07951", - "07952", - "07953", - "07954", - "07955", - "07957", - "07958", - "07959", - "07961", - "07962", - "07963", - "07964", - "07965", - "07966", - "07967", - "07971", - "07972", - "07973", - "07974", - "07975", - "07976", - "07977", - "08020", - "08021", - "08022", - "08023", - "08024", - "08025", - "08026", - "08027", - "08028", - "08029", - "08031", - "08032", - "08033", - "08034", - "08035", - "08036", - "08038", - "08039", - "08041", - "08042", - "08043", - "08045", - "08046", - "08051", - "08052", - "08053", - "08054", - "08055", - "08056", - "08057", - "08061", - "08062", - "08063", - "08064", - "08065", - "08066", - "08067", - "08071", - "08072", - "08073", - "08074", - "08075", - "08076", - "08081", - "08082", - "08083", - "08084", - "08085", - "08086", - "08091", - "08092", - "08093", - "08094", - "08095", - "08102", - "08104", - "08105", - "08106", - "0811", - "08121", - "08122", - "08123", - "08124", - "08131", - "08133", - "08134", - "08135", - "08136", - "08137", - "08138", - "08139", - "08141", - "08142", - "08143", - "08144", - "08145", - "08146", - "08151", - "08152", - "08153", - "08157", - "08158", - "08161", - "08165", - "08166", - "08167", - "08168", - "08170", - "08171", - "08176", - "08177", - "08178", - "08179", - "08191", - "08192", - "08193", - "08194", - "08195", - "08196", - "08202", - "08203", - "08204", - "08205", - "08206", - "08207", - "08208", - "0821", - "08221", - "08222", - "08223", - "08224", - "08225", - "08226", - "08230", - "08231", - "08232", - "08233", - "08234", - "08236", - "08237", - "08238", - "08239", - "08241", - "08243", - "08245", - "08246", - "08247", - "08248", - "08249", - "08250", - "08251", - "08252", - "08253", - "08254", - "08257", - "08258", - "08259", - "08261", - "08262", - "08263", - "08265", - "08266", - "08267", - "08268", - "08269", - "08271", - "08272", - "08273", - "08274", - "08276", - "08281", - "08282", - "08283", - "08284", - "08285", - "08291", - "08292", - "08293", - "08294", - "08295", - "08296", - "08302", - "08303", - "08304", - "08306", - "0831", - "08320", - "08321", - "08322", - "08323", - "08324", - "08325", - "08326", - "08327", - "08328", - "08329", - "08330", - "08331", - "08332", - "08333", - "08334", - "08335", - "08336", - "08337", - "08338", - "08340", - "08341", - "08342", - "08343", - "08344", - "08345", - "08346", - "08347", - "08348", - "08349", - "08361", - "08362", - "08363", - "08364", - "08365", - "08366", - "08367", - "08368", - "08369", - "08370", - "08372", - "08373", - "08374", - "08375", - "08376", - "08377", - "08378", - "08379", - "08380", - "08381", - "08382", - "08383", - "08384", - "08385", - "08386", - "08387", - "08388", - "08389", - "08392", - "08393", - "08394", - "08395", - "08402", - "08403", - "08404", - "08405", - "08406", - "08407", - "0841", - "08421", - "08422", - "08423", - "08424", - "08426", - "08427", - "08431", - "08432", - "08433", - "08434", - "08435", - "08441", - "08442", - "08443", - "08444", - "08445", - "08446", - "08450", - "08452", - "08453", - "08454", - "08456", - "08457", - "08458", - "08459", - "08460", - "08461", - "08462", - "08463", - "08464", - "08465", - "08466", - "08467", - "08468", - "08469", - "08501", - "08502", - "08503", - "08504", - "08505", - "08506", - "08507", - "08509", - "0851", - "08531", - "08532", - "08533", - "08534", - "08535", - "08536", - "08537", - "08538", - "08541", - "08542", - "08543", - "08544", - "08545", - "08546", - "08547", - "08548", - "08549", - "08550", - "08551", - "08552", - "08553", - "08554", - "08555", - "08556", - "08557", - "08558", - "08561", - "08562", - "08563", - "08564", - "08565", - "08571", - "08572", - "08573", - "08574", - "08581", - "08582", - "08583", - "08584", - "08585", - "08586", - "08591", - "08592", - "08593", - "0861", - "08621", - "08622", - "08623", - "08624", - "08628", - "08629", - "08630", - "08631", - "08633", - "08634", - "08635", - "08636", - "08637", - "08638", - "08639", - "08640", - "08641", - "08642", - "08649", - "08650", - "08651", - "08652", - "08654", - "08656", - "08657", - "08661", - "08662", - "08663", - "08664", - "08665", - "08666", - "08667", - "08669", - "08670", - "08671", - "08677", - "08678", - "08679", - "08681", - "08682", - "08683", - "08684", - "08685", - "08686", - "08687", - "08702", - "08703", - "08704", - "08705", - "08706", - "08707", - "08708", - "08709", - "0871", - "08721", - "08722", - "08723", - "08724", - "08725", - "08726", - "08727", - "08728", - "08731", - "08732", - "08733", - "08734", - "08735", - "08741", - "08742", - "08743", - "08744", - "08745", - "08751", - "08752", - "08753", - "08754", - "08756", - "08761", - "08762", - "08764", - "08765", - "08766", - "08771", - "08772", - "08773", - "08774", - "08781", - "08782", - "08783", - "08784", - "08785", - "08801", - "08802", - "08803", - "08805", - "08806", - "08807", - "08808", - "08809", - "0881", - "08821", - "08822", - "08823", - "08824", - "08825", - "08841", - "08845", - "08846", - "08847", - "08851", - "08856", - "08857", - "08858", - "08860", - "08861", - "08862", - "08867", - "08868", - "08869", - "089", - "0906", - "09070", - "09071", - "09072", - "09073", - "09074", - "09075", - "09076", - "09077", - "09078", - "09080", - "09081", - "09082", - "09083", - "09084", - "09085", - "09086", - "09087", - "09088", - "09089", - "09090", - "09091", - "09092", - "09093", - "09094", - "09097", - "09099", - "09101", - "09102", - "09103", - "09104", - "09105", - "09106", - "09107", - "0911", - "0911", - "09120", - "09122", - "09123", - "09126", - "09127", - "09128", - "09129", - "09131", - "09132", - "09133", - "09134", - "09135", - "09141", - "09142", - "09143", - "09144", - "09145", - "09146", - "09147", - "09148", - "09149", - "09151", - "09152", - "09153", - "09154", - "09155", - "09156", - "09157", - "09158", - "09161", - "09162", - "09163", - "09164", - "09165", - "09166", - "09167", - "09170", - "09171", - "09172", - "09173", - "09174", - "09175", - "09176", - "09177", - "09178", - "09179", - "09180", - "09181", - "09182", - "09183", - "09184", - "09185", - "09186", - "09187", - "09188", - "09189", - "09190", - "09191", - "09192", - "09193", - "09194", - "09195", - "09196", - "09197", - "09198", - "09199", - "09201", - "09202", - "09203", - "09204", - "09205", - "09206", - "09207", - "09208", - "09209", - "0921", - "09220", - "09221", - "09222", - "09223", - "09225", - "09227", - "09228", - "09229", - "09231", - "09232", - "09233", - "09234", - "09235", - "09236", - "09238", - "09241", - "09242", - "09243", - "09244", - "09245", - "09246", - "09251", - "09252", - "09253", - "09254", - "09255", - "09256", - "09257", - "09260", - "09261", - "09262", - "09263", - "09264", - "09265", - "09266", - "09267", - "09268", - "09269", - "09270", - "09271", - "09272", - "09273", - "09274", - "09275", - "09276", - "09277", - "09278", - "09279", - "09280", - "09281", - "09282", - "09283", - "09284", - "09285", - "09286", - "09287", - "09288", - "09289", - "09292", - "09293", - "09294", - "09295", - "09302", - "09303", - "09305", - "09306", - "09307", - "0931", - "09321", - "09323", - "09324", - "09325", - "09326", - "09331", - "09332", - "09333", - "09334", - "09335", - "09336", - "09337", - "09338", - "09339", - "09340", - "09341", - "09342", - "09343", - "09344", - "09345", - "09346", - "09347", - "09348", - "09349", - "09350", - "09351", - "09352", - "09353", - "09354", - "09355", - "09356", - "09357", - "09358", - "09359", - "09360", - "09363", - "09364", - "09365", - "09366", - "09367", - "09369", - "09371", - "09372", - "09373", - "09374", - "09375", - "09376", - "09377", - "09378", - "09381", - "09382", - "09383", - "09384", - "09385", - "09386", - "09391", - "09392", - "09393", - "09394", - "09395", - "09396", - "09397", - "09398", - "09401", - "09402", - "09403", - "09404", - "09405", - "09406", - "09407", - "09408", - "09409", - "0941", - "09420", - "09421", - "09422", - "09423", - "09424", - "09426", - "09427", - "09428", - "09429", - "09431", - "09433", - "09434", - "09435", - "09436", - "09438", - "09439", - "09441", - "09442", - "09443", - "09444", - "09445", - "09446", - "09447", - "09448", - "09451", - "09452", - "09453", - "09454", - "09461", - "09462", - "09463", - "09464", - "09465", - "09466", - "09467", - "09468", - "09469", - "09471", - "09472", - "09473", - "09474", - "09480", - "09481", - "09482", - "09484", - "09491", - "09492", - "09493", - "09495", - "09497", - "09498", - "09499", - "09502", - "09503", - "09504", - "09505", - "0951", - "09521", - "09522", - "09523", - "09524", - "09525", - "09526", - "09527", - "09528", - "09529", - "09531", - "09532", - "09533", - "09534", - "09535", - "09536", - "09542", - "09543", - "09544", - "09545", - "09546", - "09547", - "09548", - "09549", - "09551", - "09552", - "09553", - "09554", - "09555", - "09556", - "09560", - "09561", - "09562", - "09563", - "09564", - "09565", - "09566", - "09567", - "09568", - "09569", - "09571", - "09572", - "09573", - "09574", - "09575", - "09576", - "09602", - "09603", - "09604", - "09605", - "09606", - "09607", - "09608", - "0961", - "09621", - "09622", - "09624", - "09625", - "09626", - "09627", - "09628", - "09631", - "09632", - "09633", - "09634", - "09635", - "09636", - "09637", - "09638", - "09639", - "09641", - "09642", - "09643", - "09644", - "09645", - "09646", - "09647", - "09648", - "09651", - "09652", - "09653", - "09654", - "09655", - "09656", - "09657", - "09658", - "09659", - "09661", - "09662", - "09663", - "09664", - "09665", - "09666", - "09671", - "09672", - "09673", - "09674", - "09675", - "09676", - "09677", - "09681", - "09682", - "09683", - "09701", - "09704", - "09708", - "0971", - "09720", - "09721", - "09722", - "09723", - "09724", - "09725", - "09726", - "09727", - "09728", - "09729", - "09732", - "09733", - "09734", - "09735", - "09736", - "09737", - "09738", - "09741", - "09742", - "09744", - "09745", - "09746", - "09747", - "09748", - "09749", - "09761", - "09762", - "09763", - "09764", - "09765", - "09766", - "09771", - "09772", - "09773", - "09774", - "09775", - "09776", - "09777", - "09778", - "09779", - "09802", - "09803", - "09804", - "09805", - "0981", - "09820", - "09822", - "09823", - "09824", - "09825", - "09826", - "09827", - "09828", - "09829", - "09831", - "09832", - "09833", - "09834", - "09835", - "09836", - "09837", - "09841", - "09842", - "09843", - "09844", - "09845", - "09846", - "09847", - "09848", - "09851", - "09852", - "09853", - "09854", - "09855", - "09856", - "09857", - "09861", - "09865", - "09867", - "09868", - "09869", - "09871", - "09872", - "09873", - "09874", - "09875", - "09876", - "09901", - "09903", - "09904", - "09905", - "09906", - "09907", - "09908", - "0991", - "09920", - "09921", - "09922", - "09923", - "09924", - "09925", - "09926", - "09927", - "09928", - "09929", - "09931", - "09932", - "09933", - "09935", - "09936", - "09937", - "09938", - "09941", - "09942", - "09943", - "09944", - "09945", - "09946", - "09947", - "09948", - "09951", - "09952", - "09953", - "09954", - "09955", - "09956", - "09961", - "09962", - "09963", - "09964", - "09965", - "09966", - "09971", - "09972", - "09973", - "09974", - "09975", - "09976", - "09977", - "09978" - }; - - public static readonly string[] MobileNetworks = - { - "01510", - "01511", - "01512", - "01513", - "01514", - "01515", - "01516", - "01517", - "01518", - "01519", - "0160", - "0170", - "0171", - "0175", - "01520", - "01521", - "01522", - "01523", - "01524", - "01525", - "01526", - "01527", - "01528", - "01529", - "0162", - "0172", - "0173", - "0174", - "01570", - "01571", - "01572", - "01573", - "01574", - "01575", - "01576", - "01577", - "01578", - "01579", - "0163", - "0177", - "0178", - "01590", - "01591", - "01592", - "01593", - "01594", - "01595", - "01596", - "01597", - "01598", - "01599", - "0176", - "0179" - }; - - public static string RandomPostalCode() - { - return PostalCodes.Random(); - } - - public static string RandomHouseNumber() - { - var next = TestRandom.Next(100); - var nr = TestRandom.Next(100); - if (next < 10) return $"{nr} - {nr + TestRandom.Next(1, 5)}"; - - if (next < 30) return nr.ToString(CultureInfo.InvariantCulture) + "abcd".Random(); - - return nr.ToString(CultureInfo.InvariantCulture); - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/TestAddress.cs b/src/abstractions/Backend.Fx/RandomData/TestAddress.cs deleted file mode 100644 index 735e3f9e..00000000 --- a/src/abstractions/Backend.Fx/RandomData/TestAddress.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Collections.Generic; -using Backend.Fx.BuildingBlocks; - -namespace Backend.Fx.RandomData -{ - public class TestAddress : ValueObject - { - public TestAddress(string street, string number, string postalCode, string city, string country) - { - Street = street; - Number = number; - PostalCode = postalCode; - City = city; - Country = country; - } - - public string Street { get; } - - public string Number { get; } - - public string PostalCode { get; } - - public string City { get; } - - public string Country { get; } - - protected override IEnumerable GetEqualityComponents() - { - yield return Street; - yield return Number; - yield return PostalCode; - yield return City; - yield return Country; - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/TestAddressGenerator.cs b/src/abstractions/Backend.Fx/RandomData/TestAddressGenerator.cs deleted file mode 100644 index 588d99f2..00000000 --- a/src/abstractions/Backend.Fx/RandomData/TestAddressGenerator.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Linq; - -namespace Backend.Fx.RandomData -{ - public class TestAddressGenerator : Generator - { - public static TestAddress Generate() - { - return new TestAddressGenerator().First(); - } - - - protected override TestAddress Next() - { - return new TestAddress( - Names.Streets.Random(), - Numbers.RandomHouseNumber(), - Numbers.RandomPostalCode(), - Names.Cities.Random(), - Names.Countries.Random()); - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/TestChemical.cs b/src/abstractions/Backend.Fx/RandomData/TestChemical.cs deleted file mode 100644 index af09d77e..00000000 --- a/src/abstractions/Backend.Fx/RandomData/TestChemical.cs +++ /dev/null @@ -1,200 +0,0 @@ -using System; -using System.Linq; - -namespace Backend.Fx.RandomData -{ - public class TestChemical - { - public TestChemical(string name, string description, string alternativeNames, string formula, decimal molecularWeight, string casRegistryNumber, string molFile) - { - Name = name; - Description = description; - AlternativeNames = alternativeNames; - Formula = formula; - MolecularWeight = molecularWeight; - CasRegistryNumber = casRegistryNumber; - MolFile = molFile; - } - - public string Name { get; } - public string Description { get; } - public string AlternativeNames { get; } - public string Formula { get; } - public decimal MolecularWeight { get; } - public string CasRegistryNumber { get; } - public string MolFile { get; } - - public string[] AlternativeNamesArray - { - get { return AlternativeNames?.Split(new[] {';'}, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray(); } - } - - // data taken from http://webbook.nist.gov/chemistry/name-ser.html - public static TestChemical[] All { get; } = - { - //name, description, altName, formula, mol weight, casNo, mol file - new TestChemical("1,2-Dichloroethan", - "1,2-Dichlorethan (Ethylendichlorid, EDC) ist eine farblose, brennbare und giftige Flüssigkeit mit chloroformartigem Geruch. Diese chemische Verbindung gehört zu den Chlorkohlenwasserstoffen.", - "α,β-Dichloroethane; s-Dichloroethane; Brocide; Dutch liquid; Ethylene chloride; Ethylene dichloride; Freon 150; Glycol dichloride; 1,2-Bichloroethane; 1,2-Dichlorethane; 1,2-Dichloroethane; CH2ClCH2Cl; sym-Dichloroethane; Aethylenchlorid; Bichlorure D'ethylene; Borer sol; Chlorure D'ethylene; Cloruro di ethene; 1,2-DCE; Destruxol borer-sol; 1,2-Dichloorethaan; 1,2-Dichlor-aethan; Dichloremulsion; Di-chlor-mulsion; Dichloro-1,2-ethane; 1,2-Dicloroetano; Dutch oil; EDC; ENT 1,656; Ethane dichloride; Ethyleendichloride; 1,2-Ethylene dichloride; NCI-C00511; Rcra waste number U077; UN 1184; DCE; EDC (halocarbon); HCC 150; 1,2-dichloroethane (ethylene dichloride)", - "C2H4Cl2", 98.959m, "107-06-2", - "\n\n\n 8 7 0 0 0 1 V2000\n 2.3785 0.7551 0.8326 C 0 0 0 0 0 0 0 0 0\n 1.5134 1.8542 1.3880 C 0 0 0 0 0 0 0 0 0\n 0.0000 1.1383 1.9817 Cl 0 0 0 0 0 0 0 0 0\n 3.8896 1.4714 0.2337 Cl 0 0 0 0 0 0 0 0 0\n 1.8822 0.2254 0.0000 H 0 0 0 0 0 0 0 0 0\n 2.6233 0.0000 1.6007 H 0 0 0 0 0 0 0 0 0\n 1.2715 2.6111 0.6209 H 0 0 0 0 0 0 0 0 0\n 2.0083 2.3814 2.2230 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 5 1 0 0 0\n 1 6 1 0 0 0\n 2 3 1 0 0 0\n 2 7 1 0 0 0\n 2 8 1 0 0 0\nM END\n"), - new TestChemical("1-Buten", - "Butene (auch Butylene) sind eine Gruppe von vier isomeren Kohlenwasserstoffen mit der allgemeinen Summenformel C4H8, die über eine C–C-Doppelbindung verfügen. Sie zählen damit zu den Alkenen. Zwei der Isomere unterscheiden sich durch cis-trans-Isomerie. Butene sind unter Standardbedingungen farblose, brennbare Gase mit einer größeren Dichte als Luft. Unter Druck lassen sich die Isomere verflüssigen. Sie wirken in höheren Konzentrationen narkotisierend und erstickend. Mit Luft bilden sie explosive Gemische.", - "α-Butene; α-Butylene; But-1-ene; Butene-1; Ethylethylene; 1-Butylene; 1-C4H8", "C4H8", 56.1063m, "106-98-9", - "\n\n\n 12 11 0 0 0 1 V2000\n 1.3702 2.1763 1.6455 C 0 0 0 0 0 0 0 0 0\n 0.4451 2.8431 0.9657 C 0 0 0 0 0 0 0 0 0\n 1.9783 0.9053 1.1655 C 0 0 0 0 0 0 0 0 0\n 3.4886 0.9497 1.2530 C 0 0 0 0 0 0 0 0 0\n 1.7309 2.5402 2.6153 H 0 0 0 0 0 0 0 0 0\n 0.0000 3.7633 1.3313 H 0 0 0 0 0 0 0 0 0\n 0.0658 2.5199 0.0000 H 0 0 0 0 0 0 0 0 0\n 1.5835 0.0745 1.7845 H 0 0 0 0 0 0 0 0 0\n 1.6643 0.6803 0.1265 H 0 0 0 0 0 0 0 0 0\n 3.9327 0.0000 0.9290 H 0 0 0 0 0 0 0 0 0\n 3.9065 1.7417 0.6183 H 0 0 0 0 0 0 0 0 0\n 3.8311 1.1398 2.2789 H 0 0 0 0 0 0 0 0 0\n 1 2 2 0 0 0\n 1 3 1 0 0 0\n 1 5 1 0 0 0\n 2 6 1 0 0 0\n 2 7 1 0 0 0\n 3 4 1 0 0 0\n 3 8 1 0 0 0\n 3 9 1 0 0 0\n 4 10 1 0 0 0\n 4 11 1 0 0 0\n 4 12 1 0 0 0\nM END\n"), - new TestChemical("1-Hexen", - "Hexene (Betonung auf der zweiten Silbe) sind chemische Verbindungen aus der Gruppe der Alkene mit der Summenformel C6H12. Der Wortstamm Hex weist auf die sechs Kohlenstoffatome, die Endung en auf die Doppelbindung zwischen zwei der Kohlenstoffatome hin. Es existieren verschiedene Isomere, die sich in der Position der Doppelbindung und dem Vorhandensein bzw. der Lage einer Verzweigung der Kohlenstoffkette unterscheiden. Die in der Industrie am häufigsten eingesetzte Verbindung ist 1-Hexen, welche z. B. als Comonomer bei der Produktion von Polyethen eingesetzt wird.", - "Hexene-1; 1-n-Hexene; 1-C6H12; Butylethylene; Hexene; Hex-1-ene; UN 2370; Hexylene; Neodene 6 XHP; NSC 74121; Dialene 6", "C6H12", 84.1595m, - "592-41-6", - "\n\n\n 18 17 0 0 0 0 0 0 0 0 V2000\n -2.3439 -0.9761 0.0000 H 0000000000000000000\n -2.5829 -2.8063 0.0000 H 0000000000000000000\n -1.8943 -1.9654 0.0000 C 0000000000000000000\n -0.1940 -3.1757 0.0000 H 0000000000000000000\n -0.5736 -2.1523 0.0000 C 0000000000000000000\n 1.1457 -1.2481 0.8727 H 0000000000000000000\n 1.1457 -1.2481 -0.8727 H 0000000000000000000\n 0.4932 -1.0856 0.0000 C 0000000000000000000\n -0.6369 0.5346 -0.8794 H 0000000000000000000\n -0.6369 0.5346 0.8794 H 0000000000000000000\n 0.0000 0.3648 0.0000 C 0000000000000000000\n 1.7839 1.2154 0.8785 H 0000000000000000000\n 1.7839 1.2154 -0.8785 H 0000000000000000000\n 1.1455 1.3862 0.0000 C 0000000000000000000\n 0.0421 3.0486 -0.8848 H 0000000000000000000\n 0.0421 3.0486 0.8848 H 0000000000000000000\n 1.4941 3.5435 0.0000 H 0000000000000000000\n 0.6554 2.8379 0.0000 C 0000000000000000000\n 1 3 1 0 0 0\n 2 3 1 0 0 0\n 3 5 2 0 0 0\n 4 5 1 0 0 0\n 5 8 1 0 0 0\n 6 8 1 0 0 0\n 7 8 1 0 0 0\n 8 11 1 0 0 0\n 9 11 1 0 0 0\n 10 11 1 0 0 0\n 11 14 1 0 0 0\n 12 14 1 0 0 0\n 13 14 1 0 0 0\n 14 18 1 0 0 0\n 15 18 1 0 0 0\n 16 18 1 0 0 0\n 17 18 1 0 0 0\nM END\n"), - new TestChemical("2,3-Dimethylpentan", - "3-Methylhexan und 2,3-Dimethylpentan sind chirale Verbindungen, von denen zwei Enantiomere existieren; Chiralitätszentrum ist das C3, das als Substituenten Wasserstoff und je einen Methyl-, Ethyl- und Propyl- bzw. Isopropylrest trägt. Sie sind die einfachsten chiralen Alkane.", - "2,3-Dimethylpentane; 3,4-Dimethylpentane", "C7H16", 100.2019m, "565-59-3", - "\n\n\n 23 22 0 0 0 1 V2000\n 4.3751 2.6128 2.7529 C 0 0 0 0 0 0 0 0 0\n 3.2736 1.9976 1.8651 C 0 0 0 0 0 0 0 0 0\n 1.9284 2.6959 2.0896 C 0 0 0 0 0 0 0 0 0\n 3.1364 0.5066 2.1417 C 0 0 0 0 0 0 0 0 0\n 4.5445 4.1009 2.4849 C 0 0 0 0 0 0 0 0 0\n 5.7094 1.9144 2.5314 C 0 0 0 0 0 0 0 0 0\n 0.9524 2.4445 0.9624 C 0 0 0 0 0 0 0 0 0\n 4.0701 2.4735 3.8192 H 0 0 0 0 0 0 0 0 0\n 3.5772 2.1391 0.7982 H 0 0 0 0 0 0 0 0 0\n 1.4902 2.3691 3.0539 H 0 0 0 0 0 0 0 0 0\n 2.0935 3.7916 2.1977 H 0 0 0 0 0 0 0 0 0\n 2.7404 0.3139 3.1481 H 0 0 0 0 0 0 0 0 0\n 2.4601 0.0256 1.4233 H 0 0 0 0 0 0 0 0 0\n 4.1132 0.0000 2.0752 H 0 0 0 0 0 0 0 0 0\n 5.2661 4.5496 3.1789 H 0 0 0 0 0 0 0 0 0\n 4.9040 4.2910 1.4645 H 0 0 0 0 0 0 0 0 0\n 3.5891 4.6380 2.5995 H 0 0 0 0 0 0 0 0 0\n 6.1048 2.1059 1.5247 H 0 0 0 0 0 0 0 0 0\n 6.4620 2.2562 3.2527 H 0 0 0 0 0 0 0 0 0\n 5.6092 0.8222 2.6385 H 0 0 0 0 0 0 0 0 0\n 0.7285 1.3752 0.8458 H 0 0 0 0 0 0 0 0 0\n 0.0000 2.9597 1.1415 H 0 0 0 0 0 0 0 0 0\n 1.3427 2.8017 0.0000 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 5 1 0 0 0\n 1 6 1 0 0 0\n 1 8 1 0 0 0\n 2 3 1 0 0 0\n 2 4 1 0 0 0\n 2 9 1 0 0 0\n 3 7 1 0 0 0\n 3 10 1 0 0 0\n 3 11 1 0 0 0\n 4 12 1 0 0 0\n 4 13 1 0 0 0\n 4 14 1 0 0 0\n 5 15 1 0 0 0\n 5 16 1 0 0 0\n 5 17 1 0 0 0\n 6 18 1 0 0 0\n 6 19 1 0 0 0\n 6 20 1 0 0 0\n 7 21 1 0 0 0\n 7 22 1 0 0 0\n 7 23 1 0 0 0\nM END\n"), - new TestChemical("Essigsäure", - "Essigsäure (systematisch Ethansäure, lateinisch acidum aceticum) ist eine farblose, flüssige, ätzende und typisch riechende Carbonsäure der Zusammensetzung C2H4O2 (Halbstrukturformel CH3COOH). Als Lebensmittelzusatzstoff trägt sie die E-Nummer E 260. Wässrige Lösungen der Essigsäure werden trivial nur Essig und reine Essigsäure Eisessig genannt. Die Salze und Ester der Essigsäure heißen Acetate oder (systematisch) Ethanoate", - "Ethanoic acid; Ethylic acid; Glacial acetic acid; Methanecarboxylic acid; Vinegar acid; CH3COOH; Acetasol; Acide acetique; Acido acetico; Azijnzuur; Essigsaeure; Octowy kwas; Acetic acid, glacial; Kyselina octova; UN 2789; Aci-jel; Shotgun; Ethanoic acid monomer; NSC 132953", - "C2H4O2", 60.052m, "64-19-7", - "\n\n\n 8 7 0 0 0 0 0 0 0 0999 V2000\n 0.7724 0.9670 1.0069 C 0 0 0 0 0\n 2.0576 1.7541 0.9321 C 0 0 0 0 0\n 3.0833 0.9883 0.4751 O 0 0 0 0 0\n 2.1974 2.9214 1.2200 O 0 0 0 0 0\n 0.0114 1.5612 1.5141 H 0 0 0 0 0\n 0.4326 0.7231 -0.0064 H 0 0 0 0 0\n 0.9331 0.0221 1.5356 H 0 0 0 0 0\n 3.8606 1.5761 0.4447 H 0 0 0 0 0\n 6 1 1 0 0 0\n 2 1 1 0 0 0\n 1 5 1 0 0 0\n 1 7 1 0 0 0\n 3 2 1 0 0 0\n 2 4 2 0 0 0\n 8 3 1 0 0 0\nM END\n"), - new TestChemical( - "Aceton", - "Aceton [at͡səˈtoːn] (auch: Azeton) ist der Trivialname für die organisch-chemische Verbindung Propanon bzw. Dimethylketon. Aceton ist eine farblose Flüssigkeit und findet Verwendung als polares, aprotisches Lösungsmittel und als Ausgangsstoff für viele Synthesen der organischen Chemie. Es ist mit seinem Strukturmerkmal der Carbonylgruppe (>C=O), die zwei Methylgruppen trägt, das einfachste Keton.", - "2-Propanone; β-Ketopropane; Dimethyl ketone; Dimethylformaldehyde; Methyl ketone; Propanone; Pyroacetic ether; (CH3)2CO; Dimethylketal; Ketone propane; Ketone, dimethyl-; Chevron acetone; Rcra waste number U002; UN 1090; Sasetone; Propan-2-one; NSC 135802", - "C3H6O", 58.0791m, "67-64-1", - "\n\n\n 10 9 0 0 0 0 0 0 0 0 V2000\n 0.0000 0.0000 0.1857 C 0000000000000000000\n 0.0000 0.0000 1.4013 O 0000000000000000000\n 0.0000 1.2929 -0.6150 C 0000000000000000000\n 0.0000 -1.2929 -0.6150 C 0000000000000000000\n 0.0000 2.1487 0.0628 H 0000000000000000000\n 0.0000 -2.1487 0.0628 H 0000000000000000000\n 0.8810 1.3407 -1.2674 H 0000000000000000000\n -0.8810 1.3407 -1.2674 H 0000000000000000000\n -0.8810 -1.3407 -1.2674 H 0000000000000000000\n 0.8810 -1.3407 -1.2674 H 0000000000000000000\n 1 2 2 0 0 0\n 1 3 1 0 0 0\n 1 4 1 0 0 0\n 3 5 1 0 0 0\n 3 7 1 0 0 0\n 3 8 1 0 0 0\n 4 6 1 0 0 0\n 4 9 1 0 0 0\n 4 10 1 0 0 0\nM END\n"), - new TestChemical("Acetonitril", "Acetonitril ist ein organisches Lösungsmittel und gehört zur Stoffgruppe der Nitrile.", - "Cyanomethane; Ethanenitrile; Ethyl nitrile; Methane, cyano-; Methanecarbonitrile; Methyl cyanide; CH3CN; Acetonitril; Cyanure de methyl; USAF EK-488; Methylkyanid; NA 1648; NCI-C60822; Rcra waste number U003; UN 1648; Ethanonitrile", - "C2H3N", 41.0519m, "75-05-8", - "\n\n\n 6 5 0 0 0 1 V2000\n 0.4219 0.8958 0.5193 C 0 0 0 0 0 0 0 0 0\n 1.8612 0.9422 0.4897 C 0 0 0 0 0 0 0 0 0\n 3.0198 0.9796 0.4659 N 0 0 0 0 0 0 0 0 0\n 0.0000 1.7811 0.0257 H 0 0 0 0 0 0 0 0 0\n 0.0568 0.0000 0.0000 H 0 0 0 0 0 0 0 0 0\n 0.0607 0.8694 1.5558 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 5 1 0 0 0\n 1 6 1 0 0 0\n 2 3 3 0 0 0\nM END\n"), - new TestChemical( - "Benzen", - "Benzol (nach der IUPAC Benzen) ist eine flüssige organische Verbindung mit einem charakteristischen aromatischen Geruch. Die Verbindung mit der Summenformel C6H6 ist ein aromatischer Kohlenwasserstoff und das einfachste und zugleich klassische Beispiel für die Aromatizität bestimmter Verbindungen. Benzol ist mischbar mit fast allen organischen Solventien, jedoch kaum mit Wasser. Als Lösungsmittel hat Benzol seine Bedeutung verloren, da es krebserregend ist. Als mutagenes Klastogen wirken Benzol bzw. dessen Metabolite als Gift, welches Chromosomenaberrationen hervorrufen kann.", - "[6]Annulene; Benzol; Benzole; Coal naphtha; Cyclohexatriene; Phenyl hydride; Pyrobenzol; Pyrobenzole; Benzolene; Bicarburet of hydrogen; Carbon oil; Mineral naphtha; Motor benzol; Benzeen; Benzen; Benzin; Benzine; Benzolo; Fenzen; NCI-C55276; Phene; Rcra waste number U019; UN 1114; NSC 67315; 1,3,5-Cyclohexatriene", - "C6H6", 78.1118m, "71-43-2", - "\n\n\n 12 12 0 0 0 1 V2000\n 3.2883 3.3891 0.2345 C 0 0 0 0 0 0 0 0 0\n 1.9047 3.5333 0.2237 C 0 0 0 0 0 0 0 0 0\n 3.8560 2.1213 0.1612 C 0 0 0 0 0 0 0 0 0\n 1.0888 2.4099 0.1396 C 0 0 0 0 0 0 0 0 0\n 3.0401 0.9977 0.0771 C 0 0 0 0 0 0 0 0 0\n 1.6565 1.1421 0.0663 C 0 0 0 0 0 0 0 0 0\n 3.9303 4.2734 0.3007 H 0 0 0 0 0 0 0 0 0\n 1.4582 4.5312 0.2815 H 0 0 0 0 0 0 0 0 0\n 4.9448 2.0077 0.1699 H 0 0 0 0 0 0 0 0 0\n 0.0000 2.5234 0.1311 H 0 0 0 0 0 0 0 0 0\n 3.4870 0.0000 0.0197 H 0 0 0 0 0 0 0 0 0\n 1.0145 0.2578 0.0000 H 0 0 0 0 0 0 0 0 0\n 2 1 2 0 0 0\n 1 3 1 0 0 0\n 1 7 1 0 0 0\n 4 2 1 0 0 0\n 2 8 1 0 0 0\n 3 5 2 0 0 0\n 3 9 1 0 0 0\n 6 4 2 0 0 0\n 4 10 1 0 0 0\n 5 6 1 0 0 0\n 5 11 1 0 0 0\n 6 12 1 0 0 0\nM END\n"), - new TestChemical("Kohlentetrachlorid", - "Kohlenstoffgruppe oder Kohlenstoff-Silicium-Gruppe bezeichnet die 4. Hauptgruppe („Tetrele“) (nach neuer Nummerierung der IUPAC Gruppe 14) des Periodensystems. Sie umfasst die Elemente Kohlenstoff (C), Silicium (Si), Germanium (Ge), Zinn (Sn) und Blei (Pb). Auch ein radioaktives Element, das Flerovium (Fl), ist vertreten.", - "Methane, tetrachloro-; Benzinoform; Carbon chloride (CCl4); Carbona; Fasciolin; Flukoids; Freon 10; Necatorina; Perchloromethane; Tetrachlorocarbon; Tetrachloromethane; Tetrafinol; Tetraform; Tetrasol; Univerm; Vermoestricid; CCl4; Benzenoform; Carbon tet; Methane tetrachloride; Czterochlorek wegla; ENT 4,705; Halon 1040; Necatorine; R 10; Tetrachloorkoolstof; Tetrachloormetaan; Tetrachlorkohlenstoff, tetra; Tetrachlormethan; Tetrachlorure de carbone; Tetraclorometano; Tetracloruro di carbonio; Chlorid uhlicity; ENT 27164; Rcra waste number U211; UN 1846; Katharin; Seretin; Thawpit; NSC 97063; R 10 (Refrigerant)", - "CCl4", 153.823m, "56-23-5", - "\n\n\n 5 4 0 0 0 0 0 0 0 0 V2000\n 0.0000 0.0000 0.0000 C 0000000000000000000\n 1.0350 1.0350 1.0350 Cl 0000000000000000000\n -1.0350 -1.0350 1.0350 Cl 0000000000000000000\n -1.0350 1.0350 -1.0350 Cl 0000000000000000000\n 1.0350 -1.0350 -1.0350 Cl 0000000000000000000\n 1 2 1 0 0 0\n 1 3 1 0 0 0\n 1 4 1 0 0 0\n 1 5 1 0 0 0\nM END\n"), - new TestChemical("Chloroform", "Chloroform (systematische Bezeichnung Trichlormethan) ist ein chlorierter Kohlenwasserstoff mit der Summenformel CHCl3.", - "Chloroform; Freon 20; Methane, trichloro-; R 20; Trichloroform; CHCl3; Formyl trichloride; Methane trichloride; Methenyl trichloride; Methyl trichloride; Chloroforme; Cloroformio; NCI-C02686; R 20 (refrigerant); Trichloormethaan; Trichlormethan; Triclorometano; Rcra waste number U044; UN 1888; NSC 77361; F 20", - "CHCl3", 119.378m, "67-66-3", - "\n\n\n 5 4 0 0 0 0 0 0 0 0 V2000\n 0.0000 0.0000 0.4548 C 0000000000000000000\n 0.0000 0.0000 1.5402 H 0000000000000000000\n 0.0000 1.7050 -0.0837 Cl 0000000000000000000\n 1.4766 -0.8525 -0.0837 Cl 0000000000000000000\n -1.4766 -0.8525 -0.0837 Cl 0000000000000000000\n 1 2 1 0 0 0\n 1 3 1 0 0 0\n 1 4 1 0 0 0\n 1 5 1 0 0 0\nM END\n"), - new TestChemical("Cyclohexan", - "Cyclohexan (auch Hexahydrobenzol, Hexamethylen, Naphthen) ist eine farblose Flüssigkeit. Es ist ein Cycloalkan mit der Summenformel C6H12, das im Erdöl vorkommt und als Lösungsmittel und Grundstoff in der Synthese genutzt wird.", - "Benzene, hexahydro-; Hexahydrobenzene; Hexamethylene; Hexanaphthene; Cicloesano; Cykloheksan; Rcra waste number U056; UN 1145; NSC 406835", "C6H12", - 84.1595m, "110-82-7", - "\n\n\n 18 18 0 0 0 0 0 0 0 0 V2000\n 0.0000 -1.4672 0.2293 C 0000000000000000000\n -1.2706 0.7336 0.2293 C 0000000000000000000\n 1.2706 0.7336 0.2293 C 0000000000000000000\n 0.0000 1.4672 -0.2293 C 0000000000000000000\n -1.2706 -0.7336 -0.2293 C 0000000000000000000\n 1.2706 -0.7336 -0.2293 C 0000000000000000000\n 0.0000 -1.5355 1.3276 H 0000000000000000000\n -1.3298 0.7677 1.3276 H 0000000000000000000\n 1.3298 0.7677 1.3276 H 0000000000000000000\n 0.0000 1.5355 -1.3276 H 0000000000000000000\n -1.3298 -0.7677 -1.3276 H 0000000000000000000\n 1.3298 -0.7677 -1.3276 H 0000000000000000000\n 0.0000 -2.4985 -0.1466 H 0000000000000000000\n -2.1638 1.2493 -0.1466 H 0000000000000000000\n 2.1638 1.2493 -0.1466 H 0000000000000000000\n 0.0000 2.4985 0.1466 H 0000000000000000000\n -2.1638 -1.2493 0.1466 H 0000000000000000000\n 2.1638 -1.2493 0.1466 H 0000000000000000000\n 1 5 1 0 0 0\n 1 6 1 0 0 0\n 1 7 1 0 0 0\n 1 13 1 0 0 0\n 2 4 1 0 0 0\n 2 5 1 0 0 0\n 2 8 1 0 0 0\n 2 14 1 0 0 0\n 3 4 1 0 0 0\n 3 6 1 0 0 0\n 3 9 1 0 0 0\n 3 15 1 0 0 0\n 4 10 1 0 0 0\n 4 16 1 0 0 0\n 5 11 1 0 0 0\n 5 17 1 0 0 0\n 6 12 1 0 0 0\n 6 18 1 0 0 0\nM END\n"), - new TestChemical("Cyclopenten", - "Cyclopenten ist eine organische Verbindung mit der Summenformel C5H8. Sie besteht aus einem fünfgliedrigen, ungesättigten Ring, welcher eine Doppelbindung aufweist. In der homologen Reihe der Cycloalkene steht Cyclopenten zwischen Cyclobuten und Cyclohexen. Formal handelt es sich um ein einfach hydriertes Cyclopentadien beziehungsweise ein einfach dehydriertes Cyclopentan. Cyclopenten besitzt nur wenige Anwendungen.", - "", "C5H8", 68.1170m, "142-29-0", - "\n\n\n 13 13 0 0 0 1 V2000\n 0.7026 2.1487 1.2803 C 0 0 0 0 0 0 0 0 0\n 1.9143 3.0520 1.0028 C 0 0 0 0 0 0 0 0 0\n 3.1024 2.1512 1.1173 C 0 0 0 0 0 0 0 0 0\n 2.7505 0.8940 1.4098 C 0 0 0 0 0 0 0 0 0\n 1.2685 0.7440 1.5398 C 0 0 0 0 0 0 0 0 0\n 0.1217 2.5129 2.1461 H 0 0 0 0 0 0 0 0 0\n 0.0000 2.1470 0.4281 H 0 0 0 0 0 0 0 0 0\n 1.8684 3.5149 0.0000 H 0 0 0 0 0 0 0 0 0\n 1.9848 3.8877 1.7230 H 0 0 0 0 0 0 0 0 0\n 4.1143 2.5269 0.9699 H 0 0 0 0 0 0 0 0 0\n 3.4198 0.0455 1.5471 H 0 0 0 0 0 0 0 0 0\n 0.8848 0.0000 0.8178 H 0 0 0 0 0 0 0 0 0\n 0.9989 0.3651 2.5427 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 5 1 1 0 0 0\n 1 6 1 0 0 0\n 1 7 1 0 0 0\n 2 3 1 0 0 0\n 2 8 1 0 0 0\n 2 9 1 0 0 0\n 3 4 2 0 0 0\n 3 10 1 0 0 0\n 4 5 1 0 0 0\n 4 11 1 0 0 0\n 5 12 1 0 0 0\n 5 13 1 0 0 0\nM END\n"), - new TestChemical("Diethylether", - "Diethylether ist der wichtigste Vertreter der organisch-chemischen Verbindungsklasse der Ether und wird deshalb häufig auch einfach als Ether (standardsprachlich und in der älteren wissenschaftlichen Literatur Äther) bezeichnet. Aufgrund der Herstellung aus Ethanol und Schwefelsäure war die historische Bezeichnung Schwefeläther, obwohl Diethylether keinen Schwefel enthält.", - "Ethane, 1,1'-oxybis-; Anaesthetic ether; Anesthesia ether; Anesthetic ether; Diethyl ether; Diethyl oxide; Ethoxyethane; Pronarcol; Solvent ether; 1,1'-Oxybisethane; (C2H5)2O; Aether; Diaethylaether; Dwuetylowy eter; Etere etilico; Ether ethylique; Ether, ethyl; Ethyl ether, tech.; Ethyl oxide; Oxyde d'ethyle; Rcra waste number U117; UN 1155; 3-Oxapentane; Ether; Ethyl ether anhydrous A.C.S.; Sulfuric ether; NSC 100036", - "C4H10O", 74.1216m, "60-29-7", - "\n\n\n 15 14 0 0 0 1 V2000\n 0.9744 2.7710 2.3574 C 0 0 0 0 0 0 0 0 0\n 0.9176 4.1589 1.7420 C 0 0 0 0 0 0 0 0 0\n 1.8106 1.8731 1.6596 O 0 0 0 0 0 0 0 0 0\n 3.2030 2.0811 1.8614 C 0 0 0 0 0 0 0 0 0\n 3.9340 1.0107 1.0734 C 0 0 0 0 0 0 0 0 0\n 0.0000 2.2572 2.2827 H 0 0 0 0 0 0 0 0 0\n 1.2519 2.8230 3.4284 H 0 0 0 0 0 0 0 0 0\n 0.1388 4.7597 2.2281 H 0 0 0 0 0 0 0 0 0\n 0.6868 4.1218 0.6694 H 0 0 0 0 0 0 0 0 0\n 1.8666 4.7007 1.8520 H 0 0 0 0 0 0 0 0 0\n 3.4420 2.0182 2.9412 H 0 0 0 0 0 0 0 0 0\n 3.4890 3.0953 1.5205 H 0 0 0 0 0 0 0 0 0\n 5.0187 1.1245 1.1931 H 0 0 0 0 0 0 0 0 0\n 3.7104 1.0671 0.0000 H 0 0 0 0 0 0 0 0 0\n 3.6667 0.0000 1.4086 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 3 1 0 0 0\n 1 6 1 0 0 0\n 1 7 1 0 0 0\n 2 8 1 0 0 0\n 2 9 1 0 0 0\n 2 10 1 0 0 0\n 3 4 1 0 0 0\n 4 5 1 0 0 0\n 4 11 1 0 0 0\n 4 12 1 0 0 0\n 5 13 1 0 0 0\n 5 14 1 0 0 0\n 5 15 1 0 0 0\nM END\n"), - new TestChemical("Ethanol", - "Ethanol (häufige Trivialnamen: Äthanol, Ethylalkohol, Alkohol) ist ein aliphatischer, einwertiger Alkohol mit der Summenformel C2H6O. Die reine Substanz ist eine bei Raumtemperatur farblose, leicht entzündliche Flüssigkeit mit einem brennenden Geschmack und einem charakteristischen, würzigen (süßlichen) Geruch. Die als Lebergift eingestufte Droge wird bei der Herstellung von Genussmitteln und alkoholischen Getränken wie Wein, Bier und Spirituosen aus kohlehydrathaltigem Material durch eine von Hefen ausgelöste Gärung in relativ großen Mengen produziert.", - "Ethyl alcohol; Alcohol; Alcohol anhydrous; Algrain; Anhydrol; Denatured ethanol; Ethyl hydrate; Ethyl hydroxide; Jaysol; Jaysol S; Methylcarbinol; SD Alchol 23-hydrogen; Tecsol; C2H5OH; Absolute ethanol; Cologne spirit; Fermentation alcohol; Grain alcohol; Molasses alcohol; Potato alcohol; Aethanol; Aethylalkohol; Alcohol, dehydrated; Alcool ethylique; Alcool etilico; Alkohol; Cologne spirits; Denatured alcohol CD-10; Denatured alcohol CD-5; Denatured alcohol CD-5a; Denatured alcohol SD-1; Denatured alcohol SD-13a; Denatured alcohol SD-17; Denatured alcohol SD-23a; Denatured alcohol SD-28; Denatured alcohol SD-3a; Denatured alcohol SD-30; Denatured alcohol SD-39b; Denatured alcohol SD-39c; Denatured alcohol SD-40m; Etanolo; Ethanol 200 proof; Ethyl alc; Etylowy alkohol; EtOH; NCI-C03134; Spirits of wine; Spirt; Alkoholu etylowego; Ethyl alcohol anhydrous; SD alcohol 23-hydrogen; UN 1170; Tecsol C; Alcare Hand Degermer; Absolute alcohol; Denatured alcohol; Ethanol, silent spirit; Ethylol; Punctilious ethyl alcohol; SD 3A", - "C2H6O", 46.0684m, "64-17-5", - "\n\n\n 9 8 0 0 0 0 0 0 0 0999 V2000\n 1.0195 0.8856 0.9752 C 0 0 0 0 0\n 1.8780 1.9882 1.5739 C 0 0 0 0 0\n 3.1989 1.4758 1.7291 O 0 0 0 0 0\n -0.0045 1.2392 0.8098 H 0 0 0 0 0\n 0.9875 0.0188 1.6438 H 0 0 0 0 0\n 1.4360 0.5612 0.0153 H 0 0 0 0 0\n 1.8717 2.8699 0.9114 H 0 0 0 0 0\n 1.4594 2.3055 2.5439 H 0 0 0 0 0\n 3.7472 2.1776 2.1115 H 0 0 0 0 0\n 6 1 1 0 0 0\n 4 1 1 0 0 0\n 1 2 1 0 0 0\n 1 5 1 0 0 0\n 7 2 1 0 0 0\n 2 3 1 0 0 0\n 2 8 1 0 0 0\n 3 9 1 0 0 0\nM END\n"), - new TestChemical("Essigsäureethylester", - "Essigsäureethylester, auch Ethylacetat oder Essigester, ist eine chemische Verbindung aus der Gruppe der Carbonsäureester. Es ist der Ester von Essigsäure und Ethanol. Die farblose Flüssigkeit ist ein charakteristisch nach Klebstoff riechendes Lösungsmittel, das in der chemischen Industrie und in Laboratorien oft verwendet wird.", - "Acetic acid, ethyl ester; Acetic ether; Acetidin; Acetoxyethane; Ethyl acetic ester; Ethyl ethanoate; Vinegar naphtha; CH3COOC2H5; Aethylacetat; Essigester; Ethyle (acetate d'); Etile (acetato di); Ethylacetaat; Ethylester kyseliny octove; Rcra waste number U112; UN 1173; Ethyl ester of acetic acid; 1-Acetoxyethane; NSC 70930; ac. acetic ethyl ester", - "C4H8O2", 88.1051m, "141-78-6", - "\n\n\n 14 13 0 0 0 0 0 0 0 0999 V2000\n 3.4731 2.2227 2.4832 O 0 0 0 0 0\n 2.1749 2.8339 2.4803 C 0 0 0 0 0\n 2.2477 4.1592 1.7469 C 0 0 0 0 0\n 3.7187 1.3350 1.4827 C 0 0 0 0 0\n 5.1346 0.8483 1.5545 C 0 0 0 0 0\n 2.9285 1.0057 0.6092 O 0 0 0 0 0\n 1.4145 2.1783 2.0421 H 0 0 0 0 0\n 1.8932 3.0019 3.5247 H 0 0 0 0 0\n 1.2769 4.6623 1.7528 H 0 0 0 0 0\n 2.9878 4.8162 2.2162 H 0 0 0 0 0\n 2.5643 4.0145 0.7089 H 0 0 0 0 0\n 5.1634 -0.2191 1.3199 H 0 0 0 0 0\n 5.7451 1.4089 0.8424 H 0 0 0 0 0\n 5.5400 0.9802 2.5620 H 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 2 3 1 0 0 0\n 2 7 1 0 0 0\n 2 8 1 0 0 0\n 3 9 1 0 0 0\n 3 10 1 0 0 0\n 3 11 1 0 0 0\n 4 5 1 0 0 0\n 4 6 2 0 0 0\n 5 12 1 0 0 0\n 5 13 1 0 0 0\n 5 14 1 0 0 0\nM END\n"), - new TestChemical("Ethylpropylether", "Ethylpropylether", - "Ether, ethyl propyl; Ethyl n-propyl ether; Ethyl propyl ether; Propyl ethyl ether; 1-Ethoxypropane; n-C3H7OC2H5; UN 2615", "C5H12O", 88.1482m, - "628-32-0", - "\n\n\n 18 17 0 0 0 1 V2000\n 3.1079 1.7452 1.0645 O 0 0 0 0 0 0 0 0 0\n 4.4441 1.2694 1.1114 C 0 0 0 0 0 0 0 0 0\n 5.3490 2.4893 1.2632 C 0 0 0 0 0 0 0 0 0\n 2.1504 0.7063 0.9251 C 0 0 0 0 0 0 0 0 0\n 6.8002 2.0698 1.3208 C 0 0 0 0 0 0 0 0 0\n 0.7827 1.3612 0.8832 C 0 0 0 0 0 0 0 0 0\n 4.6830 0.7077 0.1862 H 0 0 0 0 0 0 0 0 0\n 4.5718 0.5698 1.9616 H 0 0 0 0 0 0 0 0 0\n 5.0789 3.0544 2.1773 H 0 0 0 0 0 0 0 0 0\n 5.1881 3.1923 0.4219 H 0 0 0 0 0 0 0 0 0\n 2.2307 0.0000 1.7749 H 0 0 0 0 0 0 0 0 0\n 2.3473 0.1294 0.0000 H 0 0 0 0 0 0 0 0 0\n 7.4601 2.9403 1.4280 H 0 0 0 0 0 0 0 0 0\n 7.1058 1.5371 0.4105 H 0 0 0 0 0 0 0 0 0\n 6.9969 1.4029 2.1707 H 0 0 0 0 0 0 0 0 0\n 0.0000 0.5997 0.7766 H 0 0 0 0 0 0 0 0 0\n 0.6863 2.0571 0.0395 H 0 0 0 0 0 0 0 0 0\n 0.5712 1.9289 1.7988 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 2 3 1 0 0 0\n 2 7 1 0 0 0\n 2 8 1 0 0 0\n 3 5 1 0 0 0\n 3 9 1 0 0 0\n 3 10 1 0 0 0\n 4 6 1 0 0 0\n 4 11 1 0 0 0\n 4 12 1 0 0 0\n 5 13 1 0 0 0\n 5 14 1 0 0 0\n 5 15 1 0 0 0\n 6 16 1 0 0 0\n 6 17 1 0 0 0\n 6 18 1 0 0 0\nM END\n"), - new TestChemical("Essigsäureisopropylester", - "Essigsäureisopropylester ist eine chemische Verbindung aus der Gruppe der Carbonsäureester. Es ist eine farblose, leichtentzündliche und flüchtige Flüssigkeit.", - "Acetic acid, 1-methylethyl ester; Acetic acid, isopropyl ester; 2-Acetoxypropane; 2-Propyl acetate; CH3COOCH(CH3)2; Acetate d'isopropyle; Isopropile(acetato di); Isopropyl ethanoate; Isopropyl (acetate d'); Isopropylacetaat; Isopropylacetat; Isopropylester kyseliny octove; UN 1220; Isopropyl ester of acetic acid; sec-Propyl acetate; Acetic acid, 2-propyl ester; 1-Methylethyl acetate; NSC 9295", - "C5H10O2", 102.1317m, "108-21-4", - "\n\n\n 17 16 0 0 0 1 V2000\n 3.1938 1.0106 1.8174 O 0 0 0 0 0 0 0 0 0\n 2.0014 1.7632 2.1082 C 0 0 0 0 0 0 0 0 0\n 4.4164 1.5950 1.9641 C 0 0 0 0 0 0 0 0 0\n 0.9499 0.7230 2.4807 C 0 0 0 0 0 0 0 0 0\n 1.5706 2.5640 0.8846 C 0 0 0 0 0 0 0 0 0\n 5.5443 0.6574 1.6359 C 0 0 0 0 0 0 0 0 0\n 4.5054 2.7501 2.3330 O 0 0 0 0 0 0 0 0 0\n 2.1755 2.4482 2.9734 H 0 0 0 0 0 0 0 0 0\n 1.2515 0.1324 3.3552 H 0 0 0 0 0 0 0 0 0\n 0.7564 0.0192 1.6598 H 0 0 0 0 0 0 0 0 0\n 0.0000 1.2159 2.7240 H 0 0 0 0 0 0 0 0 0\n 1.4415 1.9261 0.0000 H 0 0 0 0 0 0 0 0 0\n 0.6116 3.0626 1.0738 H 0 0 0 0 0 0 0 0 0\n 2.3036 3.3399 0.6292 H 0 0 0 0 0 0 0 0 0\n 5.3046 0.0000 0.7896 H 0 0 0 0 0 0 0 0 0\n 5.7670 0.0184 2.5009 H 0 0 0 0 0 0 0 0 0\n 6.4576 1.2122 1.3851 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 3 1 0 0 0\n 2 4 1 0 0 0\n 2 5 1 0 0 0\n 2 8 1 0 0 0\n 3 6 1 0 0 0\n 3 7 2 0 0 0\n 4 9 1 0 0 0\n 4 10 1 0 0 0\n 4 11 1 0 0 0\n 5 12 1 0 0 0\n 5 13 1 0 0 0\n 5 14 1 0 0 0\n 6 15 1 0 0 0\n 6 16 1 0 0 0\n 6 17 1 0 0 0\nM END\n"), - new TestChemical( - "Methan", - "Methan ist eine chemische Verbindung aus der Gruppe der Alkane mit der Summenformel CH4. Das farb- und geruchlose, brennbare Gas kommt in der Natur vor und ist ein Hauptbestandteil von Erdgas. Es dient als Heizgas und ist in der chemischen Industrie als Ausgangsprodukt für technische Synthesen von großer Bedeutung.", - "Marsh gas; Methyl hydride; CH4; Fire Damp; R 50; Biogas; R 50 (refrigerant)", "CH4", 16.0425m, "74-82-8", - "\n\n\n 5 4 0 0 0 1 V2000\n 1.0582 0.9353 0.8103 C 0 0 0 0 0 0 0 0 0\n 1.4145 1.5662 0.0000 H 0 0 0 0 0 0 0 0 0\n 1.2065 1.4452 1.7588 H 0 0 0 0 0 0 0 0 0\n 0.0000 0.7294 0.6710 H 0 0 0 0 0 0 0 0 0\n 1.6121 0.0000 0.8114 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 3 1 0 0 0\n 1 4 1 0 0 0\n 1 5 1 0 0 0\nM END\n"), - new TestChemical("Methylalkohol", - "Methanol, auch Methylalkohol, ist eine organische chemische Verbindung mit der Summenformel CH4O (Halbstrukturformel: CH3OH) und der einfachste Vertreter aus der Stoffgruppe der Alkohole. Unter Normalbedingungen ist Methanol eine klare, farblose, entzündliche und leicht flüchtige Flüssigkeit mit alkoholischem Geruch.", - "Methanol; Carbinol; Methyl hydroxide; Methylol; Monohydroxymethane; Wood alcohol; CH3OH; Colonial spirit; Columbian spirit; Hydroxymethane; Wood naphtha; Alcool methylique; Alcool metilico; Columbian spirits; Metanolo; Methylalkohol; Metylowy alkohol; Pyroxylic spirit; Wood spirit; Rcra waste number U154; UN 1230; Pyro alcohol; Spirit of wood; Bieleski's solution; NSC 85232", - "CH4O", 32.0419m, "67-56-1", - "\n\n\n 6 5 0 0 0 1 V2000\n 0.2453 0.8386 1.6056 H 0 0 0 0 0 0 0 0 0\n 0.6776 0.9803 0.6074 C 0 0 0 0 0 0 0 0 0\n 0.0000 1.5869 0.0000 H 0 0 0 0 0 0 0 0 0\n 0.8133 0.0000 0.1338 H 0 0 0 0 0 0 0 0 0\n 1.8631 1.7142 0.6464 O 0 0 0 0 0 0 0 0 0\n 2.4856 1.2216 1.1660 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 2 3 1 0 0 0\n 2 4 1 0 0 0\n 2 5 1 0 0 0\n 5 6 1 0 0 0\nM END\n"), - new TestChemical("2-Butanone", - "Butanon ist neben Aceton eines der wichtigsten industriell genutzten Ketone. Es ist eine farblose, leicht bewegliche Flüssigkeit mit einem typischen Geruch und wird allgemein als Methylethylketon (MEK) bezeichnet.", - "Butan-2-one; Butanone; Ethyl methyl ketone; Ketone, methyl ethyl; Methyl ethyl ketone; MEK; C2H5COCH3; Acetone, methyl-; Aethylmethylketon; 3-Butanone; Butanone 2; Ethyl methyl cetone; Ethylmethylketon; Ketone, ethyl methyl; Meetco; Methyl acetone; Metiletilchetone; Metyloetyloketon; Rcra waste number U159; UN 1193; 2-Oxobutane; 2-Butanal; 2-butanone (MEK; methyl ethyl ketone); 2-butanone (MEK)", - "C4H8O", 72.1057m, "78-93-3", - "\n\n\n 13 12 0 0 0 1 V2000\n 1.7893 1.4200 2.8269 C 0 0 0 0 0 0 0 0 0\n 0.8451 2.5827 2.6164 C 0 0 0 0 0 0 0 0 0\n 2.3705 0.9490 1.5071 C 0 0 0 0 0 0 0 0 0\n 3.5557 1.7224 0.9969 C 0 0 0 0 0 0 0 0 0\n 1.9075 0.0000 0.9038 O 0 0 0 0 0 0 0 0 0\n 2.6155 1.7113 3.5049 H 0 0 0 0 0 0 0 0 0\n 1.2589 0.5906 3.3355 H 0 0 0 0 0 0 0 0 0\n 0.0000 2.3072 1.9722 H 0 0 0 0 0 0 0 0 0\n 0.4300 2.9289 3.5716 H 0 0 0 0 0 0 0 0 0\n 1.3499 3.4390 2.1488 H 0 0 0 0 0 0 0 0 0\n 3.8802 1.3974 0.0000 H 0 0 0 0 0 0 0 0 0\n 3.3200 2.7937 0.9410 H 0 0 0 0 0 0 0 0 0\n 4.4074 1.6050 1.6799 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 3 1 0 0 0\n 1 6 1 0 0 0\n 1 7 1 0 0 0\n 2 8 1 0 0 0\n 2 9 1 0 0 0\n 2 10 1 0 0 0\n 3 4 1 0 0 0\n 3 5 2 0 0 0\n 4 11 1 0 0 0\n 4 12 1 0 0 0\n 4 13 1 0 0 0\nM END\n"), - new TestChemical( - "Butan", - "Die Butane sind eine Stoffgruppe innerhalb der Alkane, die die Summenformel C4H10 aufweisen. Sie besteht aus den beiden Vertretern n-Butan und iso-Butan, die zueinander isomer sind. Beide Butane sind farblose, brennbare, leicht zu verflüssigende Gase („Flüssiggase“), die sich kaum in Wasser, aber gut in Ethanol und Ether lösen.", - "n-Butane; Diethyl; Freon 600; Liquefied petroleum gas; LPG; n-C4H10; Butanen; Butani; Methylethylmethane; UN 1011; A 21; HC 600; HC 600 (hydrocarbon); R 600; R 600 (alkane)", - "C4H10", 58.1222m, "106-97-8", - "\n\n\n 14 13 0 0 0 1 V2000\n 3.5864 1.1360 0.9321 C 0 0 0 0 0 0 0 0 0\n 2.5594 0.8276 1.9979 C 0 0 0 0 0 0 0 0 0\n 1.7180 2.0488 2.3336 C 0 0 0 0 0 0 0 0 0\n 0.6912 1.7404 3.3995 C 0 0 0 0 0 0 0 0 0\n 3.1136 1.4730 0.0000 H 0 0 0 0 0 0 0 0 0\n 4.1901 0.2515 0.6922 H 0 0 0 0 0 0 0 0 0\n 4.2769 1.9280 1.2514 H 0 0 0 0 0 0 0 0 0\n 3.0625 0.4552 2.9124 H 0 0 0 0 0 0 0 0 0\n 1.9030 0.0000 1.6631 H 0 0 0 0 0 0 0 0 0\n 1.2148 2.4207 1.4190 H 0 0 0 0 0 0 0 0 0\n 2.3747 2.8766 2.6678 H 0 0 0 0 0 0 0 0 0\n 0.0000 0.9495 3.0791 H 0 0 0 0 0 0 0 0 0\n 0.0880 2.6250 3.6404 H 0 0 0 0 0 0 0 0 0\n 1.1635 1.4017 4.3313 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 5 1 0 0 0\n 1 6 1 0 0 0\n 1 7 1 0 0 0\n 2 3 1 0 0 0\n 2 8 1 0 0 0\n 2 9 1 0 0 0\n 3 4 1 0 0 0\n 3 10 1 0 0 0\n 3 11 1 0 0 0\n 4 12 1 0 0 0\n 4 13 1 0 0 0\n 4 14 1 0 0 0\nM END\n"), - new TestChemical("Butanol", - "1-Butanol (auch n-Butanol oder nach IUPAC Butan-1-ol) ist eine chemische Verbindung aus der Gruppe der Alkanole. Der primäre Alkohol leitet sich vom aliphatischen Kohlenwasserstoff n-Butan ab.", - "Butyl alcohol; n-Butan-1-ol; n-Butanol; n-Butyl alcohol; Butyl hydroxide; CCS 203; Hemostyp; Methylolpropane; Propylcarbinol; n-C4H9OH; Butanol; Butan-1-ol; 1-Hydroxybutane; Alcool butylique; Butanolo; Butylowy alkohol; Butyric alcohol; Propylmethanol; Butanolen; 1-Butyl alcohol; Rcra waste number U031; Butanol-1; NSC 62782", - "C4H10O", 74.1216m, "71-36-3", - "\n\n\n 15 14 0 0 0 1 V2000\n 2.9651 2.0464 2.4042 C 0 0 0 0 0 0 0 0 0\n 2.3281 2.7103 1.1934 C 0 0 0 0 0 0 0 0 0\n 0.8652 2.3158 0.9860 C 0 0 0 0 0 0 0 0 0\n 3.3109 0.5952 2.1559 C 0 0 0 0 0 0 0 0 0\n 0.0000 2.7697 1.9922 O 0 0 0 0 0 0 0 0 0\n 3.8836 2.6000 2.6826 H 0 0 0 0 0 0 0 0 0\n 2.2886 2.1307 3.2779 H 0 0 0 0 0 0 0 0 0\n 2.8987 2.4514 0.2796 H 0 0 0 0 0 0 0 0 0\n 2.4074 3.8120 1.2923 H 0 0 0 0 0 0 0 0 0\n 0.5089 2.6739 0.0000 H 0 0 0 0 0 0 0 0 0\n 0.7244 1.2206 1.0177 H 0 0 0 0 0 0 0 0 0\n 2.4210 0.0000 1.9086 H 0 0 0 0 0 0 0 0 0\n 3.7712 0.1403 3.0423 H 0 0 0 0 0 0 0 0 0\n 4.0193 0.4827 1.3240 H 0 0 0 0 0 0 0 0 0\n 0.1229 3.7075 2.0732 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 6 1 0 0 0\n 1 7 1 0 0 0\n 2 3 1 0 0 0\n 2 8 1 0 0 0\n 2 9 1 0 0 0\n 3 5 1 0 0 0\n 3 10 1 0 0 0\n 3 11 1 0 0 0\n 4 12 1 0 0 0\n 4 13 1 0 0 0\n 4 14 1 0 0 0\n 5 15 1 0 0 0\nM END\n"), - new TestChemical("Heptan", "Heptane sind zu den Alkanen zählende Kohlenwasserstoffe mit der Summenformel C7H16. Es existieren neun Konstitutionsisomere", - "n-Heptane; Dipropylmethane; Heptyl hydride; Skellysolve C; n-C7H16; Eptani; Heptan; Heptanen; Gettysolve-C; NSC 62784", "C7H16", 100.2019m, - "142-82-5", - "\n\n\n 23 22 0 0 0 1 V2000\n 4.9119 1.1117 1.6160 C 0 0 0 0 0 0 0 0 0\n 3.6978 1.8855 1.1285 C 0 0 0 0 0 0 0 0 0\n 2.4040 1.2047 1.5468 C 0 0 0 0 0 0 0 0 0\n 6.2065 1.7895 1.1965 C 0 0 0 0 0 0 0 0 0\n 1.1708 1.9589 1.0725 C 0 0 0 0 0 0 0 0 0\n 7.4159 1.0214 1.6802 C 0 0 0 0 0 0 0 0 0\n 0.9041 3.2087 1.8814 C 0 0 0 0 0 0 0 0 0\n 4.8843 0.0753 1.2230 H 0 0 0 0 0 0 0 0 0\n 4.8794 1.0110 2.7196 H 0 0 0 0 0 0 0 0 0\n 3.7266 2.9218 1.5233 H 0 0 0 0 0 0 0 0 0\n 3.7322 1.9883 0.0250 H 0 0 0 0 0 0 0 0 0\n 2.3839 0.1736 1.1402 H 0 0 0 0 0 0 0 0 0\n 2.3759 1.0904 2.6492 H 0 0 0 0 0 0 0 0 0\n 6.2340 2.8252 1.5899 H 0 0 0 0 0 0 0 0 0\n 6.2388 1.8907 0.0934 H 0 0 0 0 0 0 0 0 0\n 1.2758 2.2178 0.0000 H 0 0 0 0 0 0 0 0 0\n 0.2900 1.2893 1.1316 H 0 0 0 0 0 0 0 0 0\n 7.4323 0.9394 2.7752 H 0 0 0 0 0 0 0 0 0\n 8.3484 1.5122 1.3742 H 0 0 0 0 0 0 0 0 0\n 7.4348 0.0000 1.2773 H 0 0 0 0 0 0 0 0 0\n 1.7349 3.9244 1.8126 H 0 0 0 0 0 0 0 0 0\n 0.7607 2.9796 2.9459 H 0 0 0 0 0 0 0 0 0\n 0.0000 3.7234 1.5317 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 8 1 0 0 0\n 1 9 1 0 0 0\n 2 3 1 0 0 0\n 2 10 1 0 0 0\n 2 11 1 0 0 0\n 3 5 1 0 0 0\n 3 12 1 0 0 0\n 3 13 1 0 0 0\n 4 6 1 0 0 0\n 4 14 1 0 0 0\n 4 15 1 0 0 0\n 5 7 1 0 0 0\n 5 16 1 0 0 0\n 5 17 1 0 0 0\n 6 18 1 0 0 0\n 6 19 1 0 0 0\n 6 20 1 0 0 0\n 7 21 1 0 0 0\n 7 22 1 0 0 0\n 7 23 1 0 0 0\nM END\n"), - new TestChemical( - "Hexan", - "n-Hexan ist eine den Alkanen (gesättigte Kohlenwasserstoffe) zugehörige chemische Verbindung. Es ist eine farblose Flüssigkeit mit der Summenformel C6H14. Es ist das unverzweigte Isomer der fünf Hexanisomeren.", - "n-Hexane; Skellysolve B; n-C6H14; Esani; Heksan; Hexanen; Hexyl hydride; Gettysolve-B; NCI-C60571; NSC 68472", "C6H14", 86.1754m, "110-54-3", - "\n\n\n 20 19 0 0 0 1 V2000\n 3.0831 1.0831 1.9494 C 0 0 0 0 0 0 0 0 0\n 2.9735 1.4738 3.4178 C 0 0 0 0 0 0 0 0 0\n 3.7734 2.7246 3.7510 C 0 0 0 0 0 0 0 0 0\n 2.1289 1.8691 1.0623 C 0 0 0 0 0 0 0 0 0\n 5.2567 2.5130 3.5423 C 0 0 0 0 0 0 0 0 0\n 0.6830 1.5862 1.4060 C 0 0 0 0 0 0 0 0 0\n 2.8829 0.0000 1.8355 H 0 0 0 0 0 0 0 0 0\n 4.1330 1.2389 1.6114 H 0 0 0 0 0 0 0 0 0\n 1.9001 1.6293 3.6713 H 0 0 0 0 0 0 0 0 0\n 3.3194 0.6360 4.0538 H 0 0 0 0 0 0 0 0 0\n 3.4222 3.5715 3.1275 H 0 0 0 0 0 0 0 0 0\n 3.5761 3.0236 4.7988 H 0 0 0 0 0 0 0 0 0\n 2.3340 2.9542 1.1601 H 0 0 0 0 0 0 0 0 0\n 2.3218 1.6234 0.0000 H 0 0 0 0 0 0 0 0 0\n 5.8188 3.4487 3.6460 H 0 0 0 0 0 0 0 0 0\n 5.6708 1.7956 4.2625 H 0 0 0 0 0 0 0 0 0\n 5.4489 2.1109 2.5328 H 0 0 0 0 0 0 0 0 0\n 0.5108 1.7246 2.4871 H 0 0 0 0 0 0 0 0 0\n 0.0000 2.2530 0.8669 H 0 0 0 0 0 0 0 0 0\n 0.4010 0.5540 1.1613 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 7 1 0 0 0\n 1 8 1 0 0 0\n 2 3 1 0 0 0\n 2 9 1 0 0 0\n 2 10 1 0 0 0\n 3 5 1 0 0 0\n 3 11 1 0 0 0\n 3 12 1 0 0 0\n 4 6 1 0 0 0\n 4 13 1 0 0 0\n 4 14 1 0 0 0\n 5 15 1 0 0 0\n 5 16 1 0 0 0\n 5 17 1 0 0 0\n 6 18 1 0 0 0\n 6 19 1 0 0 0\n 6 20 1 0 0 0\nM END\n"), - new TestChemical( - "Octan", - "n-Octan ist eine farblose Flüssigkeit, die zu den Alkanen zählt. In der Chemie wird es entsprechend den aktuellen Nomenklaturregeln als n-Octan geschrieben, in Deutschland wird jedoch oft – gerade im Zusammenhang mit der Oktanzahl – die veraltete Schreibweise Oktan bevorzugt. Es handelt sich um den unverzweigten Vertreter der 18 Isomere der Octane.", - "n-Octane; n-C8H18; Oktan; Oktanen; Ottani; UN 1262", "C8H18", 114.2285m, "111-65-9", - "\n\n\n 26 25 0 0 0 1 V2000\n 3.8144 2.3469 1.6808 C 0 0 0 0 0 0 0 0 0\n 2.8536 2.9952 2.6656 C 0 0 0 0 0 0 0 0 0\n 1.4042 2.8035 2.2422 C 0 0 0 0 0 0 0 0 0\n 5.2606 2.6768 2.0091 C 0 0 0 0 0 0 0 0 0\n 1.0820 3.5021 0.9282 C 0 0 0 0 0 0 0 0 0\n 6.2381 2.0228 1.0442 C 0 0 0 0 0 0 0 0 0\n 0.8478 4.9859 1.0983 C 0 0 0 0 0 0 0 0 0\n 6.4020 0.5402 1.2955 C 0 0 0 0 0 0 0 0 0\n 3.5588 2.6881 0.6508 H 0 0 0 0 0 0 0 0 0\n 3.6652 1.2480 1.6733 H 0 0 0 0 0 0 0 0 0\n 3.0793 4.0774 2.7579 H 0 0 0 0 0 0 0 0 0\n 3.0069 2.5713 3.6775 H 0 0 0 0 0 0 0 0 0\n 0.7310 3.1711 3.0415 H 0 0 0 0 0 0 0 0 0\n 1.1916 1.7202 2.1456 H 0 0 0 0 0 0 0 0 0\n 5.4911 2.3686 3.0487 H 0 0 0 0 0 0 0 0 0\n 5.4015 3.7762 1.9864 H 0 0 0 0 0 0 0 0 0\n 0.1878 3.0372 0.4697 H 0 0 0 0 0 0 0 0 0\n 1.9182 3.3248 0.2134 H 0 0 0 0 0 0 0 0 0\n 7.2242 2.5203 1.1318 H 0 0 0 0 0 0 0 0 0\n 5.9096 2.1958 0.0000 H 0 0 0 0 0 0 0 0 0\n 0.6299 5.4668 0.1362 H 0 0 0 0 0 0 0 0 0\n 0.0000 5.1885 1.7663 H 0 0 0 0 0 0 0 0 0\n 1.7255 5.4904 1.5249 H 0 0 0 0 0 0 0 0 0\n 5.4519 0.0000 1.1826 H 0 0 0 0 0 0 0 0 0\n 7.1172 0.0943 0.5924 H 0 0 0 0 0 0 0 0 0\n 6.7707 0.3401 2.3104 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 9 1 0 0 0\n 1 10 1 0 0 0\n 2 3 1 0 0 0\n 2 11 1 0 0 0\n 2 12 1 0 0 0\n 3 5 1 0 0 0\n 3 13 1 0 0 0\n 3 14 1 0 0 0\n 4 6 1 0 0 0\n 4 15 1 0 0 0\n 4 16 1 0 0 0\n 5 7 1 0 0 0\n 5 17 1 0 0 0\n 5 18 1 0 0 0\n 6 8 1 0 0 0\n 6 19 1 0 0 0\n 6 20 1 0 0 0\n 7 21 1 0 0 0\n 7 22 1 0 0 0\n 7 23 1 0 0 0\n 8 24 1 0 0 0\n 8 25 1 0 0 0\n 8 26 1 0 0 0\nM END\n"), - new TestChemical( - "Pentan", - "Pentane sind Kohlenwasserstoffe mit der Summenformel C5H12 und zählen zu den Alkanen. Es existieren drei Konstitutionsisomere: n-Pentan, Isopentan und Neopentan.", - "n-Pentane; Skellysolve A; n-C5H12; Pentan; Pentanen; Pentani; Amyl hydride; NSC 72415", "C5H12", 72.1488m, "109-66-0", - "\n\n\n 17 16 0 0 0 1 V2000\n 3.7280 2.4135 2.8751 C 0 0 0 0 0 0 0 0 0\n 2.5997 1.4666 2.4988 C 0 0 0 0 0 0 0 0 0\n 1.8538 1.9510 1.2659 C 0 0 0 0 0 0 0 0 0\n 4.4698 1.9317 4.1013 C 0 0 0 0 0 0 0 0 0\n 0.7349 1.0066 0.8889 C 0 0 0 0 0 0 0 0 0\n 3.3234 3.4299 3.0526 H 0 0 0 0 0 0 0 0 0\n 4.4308 2.5184 2.0246 H 0 0 0 0 0 0 0 0 0\n 1.8961 1.3604 3.3490 H 0 0 0 0 0 0 0 0 0\n 3.0039 0.4497 2.3206 H 0 0 0 0 0 0 0 0 0\n 2.5589 2.0599 0.4178 H 0 0 0 0 0 0 0 0 0\n 1.4479 2.9664 1.4462 H 0 0 0 0 0 0 0 0 0\n 5.2821 2.6183 4.3713 H 0 0 0 0 0 0 0 0 0\n 3.8049 1.8514 4.9716 H 0 0 0 0 0 0 0 0 0\n 4.9169 0.9415 3.9412 H 0 0 0 0 0 0 0 0 0\n 0.0000 0.9059 1.6988 H 0 0 0 0 0 0 0 0 0\n 1.1125 0.0000 0.6654 H 0 0 0 0 0 0 0 0 0\n 0.1970 1.3600 0.0000 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 6 1 0 0 0\n 1 7 1 0 0 0\n 2 3 1 0 0 0\n 2 8 1 0 0 0\n 2 9 1 0 0 0\n 3 5 1 0 0 0\n 3 10 1 0 0 0\n 3 11 1 0 0 0\n 4 12 1 0 0 0\n 4 13 1 0 0 0\n 4 14 1 0 0 0\n 5 15 1 0 0 0\n 5 16 1 0 0 0\n 5 17 1 0 0 0\nM END\n"), - new TestChemical("1-Pentanol", - "1-Pentanol (veraltet: Amylalkohol) ist eine organische chemische Verbindung und gehört zu den Alkoholen. 1-Pentanol ist Bestandteil der Fuselöle.", - "Pentyl alcohol; n-Amyl alcohol; n-Butylcarbinol; n-Pentan-1-ol; n-Pentanol; n-Pentyl alcohol; Amyl alcohol; Amylol; Pentanol; 1-Pentyl alcohol; n-C5H11OH; Pentan-1-ol; Pentanol-1; Pentasol; n-Amylalkohol; Alcool amylique; Amyl alcohol, n-; Amyl alcohol, normal; Primary amyl alcohol; UN 1105; 1-Pentol; Primary-N-amyl alcohol; Butyl carbinol; NSC 5707", - "C5H12O", 88.1482m, "71-41-0", - "\n\n\n 18 17 0 0 0 1 V2000\n 2.0264 2.2581 1.9056 C 0 0 0 0 0 0 0 0 0\n 3.2462 1.6488 1.2359 C 0 0 0 0 0 0 0 0 0\n 4.4464 1.6425 2.1693 C 0 0 0 0 0 0 0 0 0\n 0.8116 2.2939 0.9786 C 0 0 0 0 0 0 0 0 0\n 5.6613 1.0380 1.5026 C 0 0 0 0 0 0 0 0 0\n 0.3196 1.0290 0.6256 O 0 0 0 0 0 0 0 0 0\n 2.2542 3.2923 2.2314 H 0 0 0 0 0 0 0 0 0\n 1.7856 1.7003 2.8338 H 0 0 0 0 0 0 0 0 0\n 3.0191 0.6161 0.9030 H 0 0 0 0 0 0 0 0 0\n 3.4892 2.2109 0.3117 H 0 0 0 0 0 0 0 0 0\n 4.6718 2.6757 2.5011 H 0 0 0 0 0 0 0 0 0\n 4.2030 1.0807 3.0931 H 0 0 0 0 0 0 0 0 0\n 1.0538 2.7449 0.0000 H 0 0 0 0 0 0 0 0 0\n 0.0000 2.8939 1.4351 H 0 0 0 0 0 0 0 0 0\n 5.4791 0.0000 1.1934 H 0 0 0 0 0 0 0 0 0\n 5.9510 1.5990 0.6041 H 0 0 0 0 0 0 0 0 0\n 6.5258 1.0323 2.1788 H 0 0 0 0 0 0 0 0 0\n 0.1634 0.5491 1.4297 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 7 1 0 0 0\n 1 8 1 0 0 0\n 2 3 1 0 0 0\n 2 9 1 0 0 0\n 2 10 1 0 0 0\n 3 5 1 0 0 0\n 3 11 1 0 0 0\n 3 12 1 0 0 0\n 4 6 1 0 0 0\n 4 13 1 0 0 0\n 4 14 1 0 0 0\n 5 15 1 0 0 0\n 5 16 1 0 0 0\n 5 17 1 0 0 0\n 6 18 1 0 0 0\nM END\n"), - new TestChemical("1-Propanol", - "Propanole sind Alkohole mit drei Kohlenstoffatomen und einer Hydroxygruppe (–OH). Sie haben die allgemeine Summenformel C3H8O und eine molare Masse von 60,10 g/mol. Es gibt nur zwei Isomere.", - "Propyl alcohol; n-Propan-1-ol; n-Propanol; n-Propyl alcohol; Ethylcarbinol; Optal; Osmosol extra; Propanol; Propylic alcohol; 1-Propyl alcohol; n-C3H7OH; 1-Hydroxypropane; Propanol-1; Propan-1-ol; n-Propyl alkohol; Alcool propilico; Alcool propylique; Propanole; Propanolen; Propanoli; Propylowy alkohol; UN 1274; Propylan-propyl alcohol; NSC 30300; Alcohol, propyl", - "C3H8O", 60.0950m, "71-23-8", - "\n\n\n 12 11 0 0 0 1 V2000\n 0.7713 1.5705 1.3838 C 0 0 0 0 0 0 0 0 0\n 2.1696 1.0226 1.0958 C 0 0 0 0 0 0 0 0 0\n 3.2631 1.9141 1.6398 C 0 0 0 0 0 0 0 0 0\n 0.3563 1.3950 2.7118 O 0 0 0 0 0 0 0 0 0\n 0.7013 2.6399 1.1017 H 0 0 0 0 0 0 0 0 0\n 0.0000 1.0213 0.8161 H 0 0 0 0 0 0 0 0 0\n 2.2872 0.9154 0.0000 H 0 0 0 0 0 0 0 0 0\n 2.2716 0.0000 1.5099 H 0 0 0 0 0 0 0 0 0\n 3.2248 1.9921 2.7352 H 0 0 0 0 0 0 0 0 0\n 3.1932 2.9340 1.2381 H 0 0 0 0 0 0 0 0 0\n 4.2564 1.5270 1.3793 H 0 0 0 0 0 0 0 0 0\n 1.0138 1.7939 3.2691 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 5 1 0 0 0\n 1 6 1 0 0 0\n 2 3 1 0 0 0\n 2 7 1 0 0 0\n 2 8 1 0 0 0\n 3 9 1 0 0 0\n 3 10 1 0 0 0\n 3 11 1 0 0 0\n 4 12 1 0 0 0\nM END\n"), - new TestChemical( - "Phenol", - "Phenol (nach IUPAC: Benzenol, veraltet: Karbolsäure oder kurz Karbol) ist eine aromatische, organische Verbindung und besteht aus einer Phenylgruppe (–C6H5), an die eine Hydroxygruppe (–OH) gebunden ist. Der farblose, kristalline Feststoff ist eine wichtige Industriechemikalie und dient als Zwischenprodukt besonders zur Herstellung diverser Kunststoffe.", - "Carbolic acid; Baker's P and S Liquid and Ointment; Benzenol; Hydroxybenzene; Izal; Monohydroxybenzene; Monophenol; Oxybenzene; Phenic acid; Phenyl alcohol; Phenyl hydrate; Phenyl hydroxide; Phenylic acid; Phenylic alcohol; PhOH; Benzene, hydroxy-; Acide carbolique; Baker's P & S liquid & Ointment; Fenol; Fenolo; NCI-C50124; Paoscle; Phenole; Carbolsaure; NA 2821; Phenol alcohol; Phenol, molten; Rcra waste number U188; UN 1671; UN 2312; UN 2821; Phenic alcohol; NSC 36808", - "C6H6O", 94.1112m, "108-95-2", - "\n\n\n 13 13 0 0 0 1 V2000\n 0.3792 2.3991 0.0767 O 0 0 0 0 0 0 0 0 0\n 1.7410 2.2635 0.0604 C 0 0 0 0 0 0 0 0 0\n 2.4822 3.4537 0.0746 C 0 0 0 0 0 0 0 0 0\n 2.3800 1.0169 0.0335 C 0 0 0 0 0 0 0 0 0\n 3.8685 3.3780 0.0622 C 0 0 0 0 0 0 0 0 0\n 3.7689 0.9695 0.0214 C 0 0 0 0 0 0 0 0 0\n 4.5128 2.1443 0.0358 C 0 0 0 0 0 0 0 0 0\n 0.0000 1.5292 0.0598 H 0 0 0 0 0 0 0 0 0\n 1.9740 4.4239 0.0950 H 0 0 0 0 0 0 0 0 0\n 1.8001 0.0870 0.0221 H 0 0 0 0 0 0 0 0 0\n 4.4597 4.2998 0.0734 H 0 0 0 0 0 0 0 0 0\n 4.2770 0.0000 0.0000 H 0 0 0 0 0 0 0 0 0\n 5.6063 2.0990 0.0278 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 8 1 0 0 0\n 2 3 2 0 0 0\n 4 2 1 0 0 0\n 3 5 1 0 0 0\n 3 9 1 0 0 0\n 6 4 2 0 0 0\n 4 10 1 0 0 0\n 5 7 2 0 0 0\n 5 11 1 0 0 0\n 7 6 1 0 0 0\n 6 12 1 0 0 0\n 7 13 1 0 0 0\nM END\n"), - new TestChemical( - "Propan", "Propan ist ein farbloses brennbares Gas und gehört zu den Kohlenwasserstoffen. Es steht in der homologen Reihe der Alkane an dritter Stelle.", - "n-Propane; Dimethylmethane; Freon 290; Liquefied petroleum gas; LPG; Propyl hydride; R 290; C3H8; UN 1978; A-108; Hydrocarbon propellant A-108; HC 290", "C3H8", - 44.0956m, "74-98-6", - "\n\n\n 11 10 0 0 0 1 V2000\n 3.3461 1.6436 1.3326 C 0 0 0 0 0 0 0 0 0\n 2.0042 1.0740 0.9307 C 0 0 0 0 0 0 0 0 0\n 0.9734 1.2486 2.0232 C 0 0 0 0 0 0 0 0 0\n 3.7393 1.1540 2.2333 H 0 0 0 0 0 0 0 0 0\n 3.2805 2.7182 1.5491 H 0 0 0 0 0 0 0 0 0\n 4.0907 1.5138 0.5370 H 0 0 0 0 0 0 0 0 0\n 1.6524 1.5612 0.0000 H 0 0 0 0 0 0 0 0 0\n 2.1104 0.0000 0.6811 H 0 0 0 0 0 0 0 0 0\n 0.0000 0.8391 1.7246 H 0 0 0 0 0 0 0 0 0\n 0.8224 2.3078 2.2707 H 0 0 0 0 0 0 0 0 0\n 1.2746 0.7385 2.9479 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 5 1 0 0 0\n 1 6 1 0 0 0\n 2 3 1 0 0 0\n 2 7 1 0 0 0\n 2 8 1 0 0 0\n 3 9 1 0 0 0\n 3 10 1 0 0 0\n 3 11 1 0 0 0\nM END\n"), - new TestChemical("p-Xylen", "", - "Benzene, 1,4-dimethyl-; p-Dimethylbenzene; p-Xylol; 1,4-Dimethylbenzene; 1,4-Xylene; p-Methyltoluene; para-Xylene; Chromar; Scintillar; 4-Methyltoluene; NSC 72419; 1,4-dimethyl-benzene ( p-xylene)", - "C8H10", 106.1650m, "106-42-3", - "\n\n\n 18 18 0 0 0 1 V2000\n 1.0336 0.8636 0.7240 C 0 0 0 0 0 0 0 0 0\n 1.8222 1.2612 1.9184 C 0 0 0 0 0 0 0 0 0\n 3.2175 1.2383 1.8736 C 0 0 0 0 0 0 0 0 0\n 1.1814 1.6695 3.0884 C 0 0 0 0 0 0 0 0 0\n 3.9602 1.6220 2.9826 C 0 0 0 0 0 0 0 0 0\n 1.9245 2.0535 4.1976 C 0 0 0 0 0 0 0 0 0\n 3.3192 2.0345 4.1521 C 0 0 0 0 0 0 0 0 0\n 4.1123 2.4606 5.3336 C 0 0 0 0 0 0 0 0 0\n 0.9917 1.6889 0.0000 H 0 0 0 0 0 0 0 0 0\n 0.0000 0.5972 0.9816 H 0 0 0 0 0 0 0 0 0\n 1.4818 0.0000 0.2149 H 0 0 0 0 0 0 0 0 0\n 3.7265 0.9144 0.9590 H 0 0 0 0 0 0 0 0 0\n 0.0866 1.6867 3.1337 H 0 0 0 0 0 0 0 0 0\n 5.0549 1.5997 2.9402 H 0 0 0 0 0 0 0 0 0\n 1.4139 2.3716 5.1134 H 0 0 0 0 0 0 0 0 0\n 5.0993 1.9796 5.3595 H 0 0 0 0 0 0 0 0 0\n 3.6010 2.2196 6.2750 H 0 0 0 0 0 0 0 0 0\n 4.2743 3.5470 5.3142 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 9 1 0 0 0\n 1 10 1 0 0 0\n 1 11 1 0 0 0\n 3 2 2 0 0 0\n 2 4 1 0 0 0\n 5 3 1 0 0 0\n 3 12 1 0 0 0\n 4 6 2 0 0 0\n 4 13 1 0 0 0\n 7 5 2 0 0 0\n 5 14 1 0 0 0\n 6 7 1 0 0 0\n 6 15 1 0 0 0\n 7 8 1 0 0 0\n 8 16 1 0 0 0\n 8 17 1 0 0 0\n 8 18 1 0 0 0\nM END\n"), - new TestChemical( - "Toluol", - "Toluol, Trivialname nach IUPAC auch Toluen, Methylbenzol, Phenylmethan, nach IUPAC-Nomenklatur Methylbenzen genannt, ist eine farblose, charakteristisch riechende, flüchtige Flüssigkeit, die in vielen ihrer Eigenschaften dem Benzol ähnelt. Toluol ist ein aromatischer Kohlenwasserstoff, häufig ersetzt es als Lösungsmittel das giftige Benzol. Es ist unter anderem auch im Benzin enthalten.", - "Benzene, methyl; Methacide; Methylbenzene; Methylbenzol; Phenylmethane; Antisal 1a; Toluol; Methane, phenyl-; NCI-C07272; Tolueen; Toluen; Toluolo; Rcra waste number U220; Tolu-sol; UN 1294; Dracyl; Monomethyl benzene; CP 25; NSC 406333; methylbenzene (toluene)", - "C7H8", 92.1384m, "108-88-3", - "\n\n\n 7 7 0 0 0 1 V2000\n 1.4722 1.7260 0.0000 C 0 0 0 0 0 0 0 0 0\n 0.9645 0.8630 0.0000 C 0 0 0 0 0 0 0 0 0\n 2.4874 1.7260 0.0000 C 0 0 0 0 0 0 0 0 0\n 2.9951 0.8630 0.0000 C 0 0 0 0 0 0 0 0 0\n 2.4874 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0\n 1.4722 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0\n 0.0000 0.8630 0.0000 C 0 0 0 0 0 0 0 0 0\n 1 2 2 0 0 0\n 3 1 1 0 0 0\n 2 6 1 0 0 0\n 2 7 1 0 0 0\n 4 3 2 0 0 0\n 5 4 1 0 0 0\n 6 5 2 0 0 0\nM END\n"), - new TestChemical( - "Wasser", - "Wasser (H2O) ist eine chemische Verbindung aus den Elementen Sauerstoff (O) und Wasserstoff (H). Wasser ist als Flüssigkeit durchsichtig, weitgehend farb-, geruch- und geschmacklos. Wasser ist die einzige chemische Verbindung auf der Erde, die in der Natur als Flüssigkeit, als Festkörper und als Gas vorkommt. Die Bezeichnung Wasser wird dabei für den flüssigen Aggregatzustand verwendet. Im festen Zustand spricht man von Eis, im gasförmigen Zustand von Wasserdampf. Wasser ist Grundlage des Lebens auf der Erde.", - "Water vapor; Distilled water; Ice; H2O; Dihydrogen oxide; steam; Tritiotope", "H2O", 18.0153m, "7732-18-5", - "\n\n\n 3 2 0 0 0 0 0 0 0 0999 V2000\n -0.2308 -0.3260 0.0000 O 0 0 0 0 0\n 0.7373 -0.2766 0.0000 H 0 0 0 0 0\n -0.5064 0.6026 0.0000 H 0 0 0 0 0\n 1 2 1 0 0 0\n 1 3 1 0 0 0\nM END\n") - }; - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/TestPerson.cs b/src/abstractions/Backend.Fx/RandomData/TestPerson.cs deleted file mode 100644 index 94886676..00000000 --- a/src/abstractions/Backend.Fx/RandomData/TestPerson.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Backend.Fx.BuildingBlocks; - -namespace Backend.Fx.RandomData -{ - public class TestPerson : ValueObject - { - public enum Genders - { - Male, - Female - } - - private static readonly Dictionary InvalidCharacterReplacements = new Dictionary - { - {"À", "A"}, - {"Á", "A"}, - {"Â", "A"}, - {"Ã", "A"}, - {"Ä", "A"}, - {"Å", "A"}, - {"Æ", "A"}, - {"Ç", "C"}, - {"È", "E"}, - {"É", "E"}, - {"Ê", "E"}, - {"Ë", "E"}, - {"Ì", "I"}, - {"Í", "I"}, - {"Î", "I"}, - {"Ï", "I"}, - {"Ð", "D"}, - {"Ñ", "N"}, - {"Ò", "O"}, - {"Ó", "O"}, - {"Ô", "O"}, - {"Õ", "O"}, - {"Ö", "O"}, - {"×", "x"}, - {"Ø", "O"}, - {"Ù", "U"}, - {"Ú", "U"}, - {"Û", "U"}, - {"Ü", "U"}, - {"Ý", "Y"}, - {"Þ", "p"}, - {"ß", "ss"}, - {"à", "a"}, - {"á", "a"}, - {"â", "a"}, - {"ã", "a"}, - {"ä", "a"}, - {"å", "a"}, - {"æ", "a"}, - {"ç", "c"}, - {"è", "e"}, - {"é", "e"}, - {"ê", "e"}, - {"ë", "e"}, - {"ì", "i"}, - {"í", "i"}, - {"î", "i"}, - {"ï", "i"}, - {"ð", "o"}, - {"ñ", "n"}, - {"ò", "o"}, - {"ó", "o"}, - {"ô", "o"}, - {"õ", "o"}, - {"ö", "o"}, - {"÷", ""}, - {"ø", "o"}, - {"ù", ""}, - {"ú", "u"}, - {"û", "u"}, - {"ü", "u"}, - {"ý", "y"}, - {"þ", "p"}, - {"ÿ", "y"} - }; - - private readonly string _email; - - public TestPerson(string firstName, string middleName, string lastName, string title, Genders gender, DateTime dateOfBirth, string email = null) - { - _email = email; - FirstName = firstName; - MiddleName = middleName; - LastName = lastName; - Title = title; - Gender = gender; - DateOfBirth = dateOfBirth; - } - - public DateTime DateOfBirth { get; } - - public string FirstName { get; } - - public Genders Gender { get; } - - public string LastName { get; } - - public int Age - { - get - { - DateTime today = DateTime.Today; - var age = today.Year - DateOfBirth.Year; - if (DateOfBirth > today.AddYears(-age)) age--; - - return age; - } - } - - public string MiddleName { get; } - - public string Title { get; } - - public string Email => _email ?? $"{UserName}@no-email.not"; - - public string UserName => SanitizeForUserName(FirstName) + "." + SanitizeForUserName(LastName); - - private static string SanitizeForUserName(string s) - { - s = new string(s.Where(char.IsLetterOrDigit).ToArray()); - foreach (var invalidCharacterReplacement in InvalidCharacterReplacements) s = s.Replace(invalidCharacterReplacement.Key, invalidCharacterReplacement.Value); - return s; - } - - protected override IEnumerable GetEqualityComponents() - { - yield return DateOfBirth; - yield return FirstName; - yield return Gender; - yield return LastName; - yield return MiddleName; - yield return Title; - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/TestPersonGenerator.cs b/src/abstractions/Backend.Fx/RandomData/TestPersonGenerator.cs deleted file mode 100644 index 2094a8dd..00000000 --- a/src/abstractions/Backend.Fx/RandomData/TestPersonGenerator.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Backend.Fx.RandomData -{ - public class TestPersonGenerator : Generator - { - private const int Year = 365; - private readonly Random _random = TestRandom.Instance; - private readonly HashSet _uniqueNames = new HashSet(); - - public bool EnforceUniqueNames { get; set; } = false; - public int FemalePercentage { get; set; } = 55; - public int MaximumAgeInDays { get; set; } = 80 * Year; - public int MinimumAgeInDays { get; set; } = 18 * Year; - - public static TestPerson Generate() - { - return new TestPersonGenerator().First(); - } - - protected override TestPerson Next() - { - var isFemale = _random.Next(1, 100) < FemalePercentage; - TestPerson generated; - do - { - generated = new TestPerson( - isFemale ? Names.Female.Random() : Names.Male.Random(), - _random.Next(100) < 30 - ? isFemale ? Names.Female.Random() : Names.Male.Random() - : "", - Names.Family.Random(), - _random.Next(100) < 20 ? "Dr." : "", - isFemale ? TestPerson.Genders.Female : TestPerson.Genders.Male, - DateTime.Now.AddDays(-_random.Next(MinimumAgeInDays, MaximumAgeInDays)).Date); - } while (EnforceUniqueNames && _uniqueNames.Contains($"{generated.FirstName}{generated.LastName}")); - - if (EnforceUniqueNames) - { - _uniqueNames.Add($"{generated.FirstName}{generated.LastName}"); - } - - return generated; - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/TestRandom.cs b/src/abstractions/Backend.Fx/RandomData/TestRandom.cs deleted file mode 100644 index a26a29d9..00000000 --- a/src/abstractions/Backend.Fx/RandomData/TestRandom.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Backend.Fx.RandomData -{ - public static class TestRandom - { - public static Random Instance { get; set; } = new Random(429756); - - public static IEnumerable Next(int amount, Func generate) - { - for (var i = 0; i < amount; i++) yield return generate(); - } - - public static int Next() - { - return Instance.Next(); - } - - public static int Next(int max) - { - return Instance.Next(max); - } - - public static int Next(int min, int max) - { - return Instance.Next(min, max); - } - - public static bool NextBool() - { - return Instance.Next(2) == 1; - } - - public static double NextDouble() - { - return Instance.NextDouble(); - } - - public static DateTime RandomDateTime(int rangeDays) - { - return rangeDays < 0 - ? DateTime.Now.AddDays(-Next(-rangeDays)).AddSeconds(-Next(100000)) - : DateTime.Now.AddDays(Next(rangeDays)).AddSeconds(-Next(100000)); - } - - public static decimal RandomDecimal(int min = 0, int max = 999999) - { - var abs = Next(min, max); - return abs + Math.Round(Next(100) / 100m, 2); - } - - [Obsolete("See Letters.RandomPassword()")] - public static string NextPassword(int length = 10) - { - const string letters = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz"; - const string numbers = "23456789"; - const string specials = "§$%&#+*-<>"; - var password = new char[length]; - - for (var i = 0; i < password.Length; i++) - { - var rnd = Next(100); - password[i] = rnd < 60 - ? letters.Random() - : rnd < 90 - ? numbers.Random() - : specials.Random(); - } - - return new string(password); - } - - public static decimal NextDecimal(decimal minimum, decimal maximum) - { - return (decimal) Instance.NextDouble() * (maximum - minimum) + minimum; - } - - public static bool NextProbability(int p) - { - return Instance.Next(0, 100) < p; - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Extensions/AsyncHelper.cs b/src/abstractions/Backend.Fx/Util/AsyncHelper.cs similarity index 86% rename from src/abstractions/Backend.Fx/Extensions/AsyncHelper.cs rename to src/abstractions/Backend.Fx/Util/AsyncHelper.cs index 46bf8915..3e73918c 100644 --- a/src/abstractions/Backend.Fx/Extensions/AsyncHelper.cs +++ b/src/abstractions/Backend.Fx/Util/AsyncHelper.cs @@ -2,9 +2,11 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; -namespace Backend.Fx.Extensions +namespace Backend.Fx.Util { + [PublicAPI] public static class AsyncHelper { /// @@ -20,25 +22,26 @@ public static void RunSync(Func task) SynchronizationContext oldContext = SynchronizationContext.Current; try { - var synch = new ExclusiveSynchronizationContext(); - SynchronizationContext.SetSynchronizationContext(synch); - synch.Post(async _ => + var exclusiveSynchronizationContext = new ExclusiveSynchronizationContext(); + SynchronizationContext.SetSynchronizationContext(exclusiveSynchronizationContext); + // ReSharper disable once AsyncVoidLambda + exclusiveSynchronizationContext.Post(async _ => { try { - await task(); + await task().ConfigureAwait(false); } catch (Exception e) { - synch.InnerException = e; + exclusiveSynchronizationContext.InnerException = e; throw; } finally { - synch.EndMessageLoop(); + exclusiveSynchronizationContext.EndMessageLoop(); } }, null); - synch.BeginMessageLoop(); + exclusiveSynchronizationContext.BeginMessageLoop(); } finally { @@ -60,11 +63,12 @@ public static T RunSync(Func> task) { var exclusiveSynchronizationContext = new ExclusiveSynchronizationContext(); SynchronizationContext.SetSynchronizationContext(exclusiveSynchronizationContext); + // ReSharper disable once AsyncVoidLambda exclusiveSynchronizationContext.Post(async _ => { try { - ret = await task(); + ret = await task().ConfigureAwait(false); } catch (Exception e) { @@ -90,8 +94,8 @@ private class ExclusiveSynchronizationContext : SynchronizationContext { private bool _done; public Exception InnerException { private get; set; } - private readonly AutoResetEvent _workItemsWaiting = new AutoResetEvent(false); - private readonly Queue> _items = new Queue>(); + private readonly AutoResetEvent _workItemsWaiting = new(false); + private readonly Queue> _items = new(); public override void Send(SendOrPostCallback d, object state) { diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/CurrentTHolder.cs b/src/abstractions/Backend.Fx/Util/CurrentTHolder.cs similarity index 72% rename from src/abstractions/Backend.Fx/Patterns/DependencyInjection/CurrentTHolder.cs rename to src/abstractions/Backend.Fx/Util/CurrentTHolder.cs index 5b9abc40..3c404c09 100644 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/CurrentTHolder.cs +++ b/src/abstractions/Backend.Fx/Util/CurrentTHolder.cs @@ -1,14 +1,15 @@ using Backend.Fx.Logging; +using JetBrains.Annotations; using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; -namespace Backend.Fx.Patterns.DependencyInjection +namespace Backend.Fx.Util { /// /// Holds a current instance of T that might be replaced during the scope /// /// - public interface ICurrentTHolder where T : class + [PublicAPI] + public interface ICurrentTHolder { T Current { get; } @@ -17,9 +18,10 @@ public interface ICurrentTHolder where T : class T ProvideInstance(); } - public abstract class CurrentTHolder : ICurrentTHolder where T : class + [PublicAPI] + public abstract class CurrentTHolder : ICurrentTHolder { - private static readonly ILogger Logger = Log.Create>(); + private readonly ILogger _logger = Log.Create>(); private T _current; protected CurrentTHolder() @@ -36,9 +38,9 @@ public T Current { if (_current == null) { - Logger.LogDebug("Providing initial {HeldTypeName} instance", typeof(T).Name); + _logger.LogDebug("Providing initial {HeldTypeName} instance", typeof(T).Name); _current = ProvideInstance(); - Logger.LogDebug("Initial instance of {HeldTypeName} is: {HeldInstanceDescription}", typeof(T).Name, Describe(_current)); + _logger.LogDebug("Initial instance of {HeldTypeName} is: {HeldInstanceDescription}", typeof(T).Name, Describe(_current)); } return _current; @@ -49,7 +51,7 @@ public void ReplaceCurrent(T newCurrentInstance) { if (Equals(_current, newCurrentInstance)) return; - Logger.LogDebug( + _logger.LogDebug( "Replacing current instance of {HeldTypename} ({HeldInstanceDescription}) with another instance ({NewInstanceDescription})", typeof(T).Name, Describe(Current), diff --git a/src/abstractions/Backend.Fx/Extensions/DelegateDisposable.cs b/src/abstractions/Backend.Fx/Util/DelegateDisposable.cs similarity index 92% rename from src/abstractions/Backend.Fx/Extensions/DelegateDisposable.cs rename to src/abstractions/Backend.Fx/Util/DelegateDisposable.cs index 562b56da..83d10c1c 100644 --- a/src/abstractions/Backend.Fx/Extensions/DelegateDisposable.cs +++ b/src/abstractions/Backend.Fx/Util/DelegateDisposable.cs @@ -1,7 +1,7 @@ using System; using JetBrains.Annotations; -namespace Backend.Fx.Extensions +namespace Backend.Fx.Util { public class DelegateDisposable : IDisposable { diff --git a/src/abstractions/Backend.Fx/Extensions/EnumerableEx.cs b/src/abstractions/Backend.Fx/Util/EnumerableEx.cs similarity index 68% rename from src/abstractions/Backend.Fx/Extensions/EnumerableEx.cs rename to src/abstractions/Backend.Fx/Util/EnumerableEx.cs index 97dfb7c6..bb4e9502 100644 --- a/src/abstractions/Backend.Fx/Extensions/EnumerableEx.cs +++ b/src/abstractions/Backend.Fx/Util/EnumerableEx.cs @@ -1,8 +1,10 @@ -namespace Backend.Fx.Extensions -{ - using System; - using System.Collections.Generic; +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +namespace Backend.Fx.Util +{ + [PublicAPI] public static class EnumerableEx { public static void ForAll(this IEnumerable enumerable, Action action) diff --git a/src/abstractions/Backend.Fx/Extensions/MultipleDisposable.cs b/src/abstractions/Backend.Fx/Util/MultipleDisposable.cs similarity index 86% rename from src/abstractions/Backend.Fx/Extensions/MultipleDisposable.cs rename to src/abstractions/Backend.Fx/Util/MultipleDisposable.cs index df5b80d5..493b9d55 100644 --- a/src/abstractions/Backend.Fx/Extensions/MultipleDisposable.cs +++ b/src/abstractions/Backend.Fx/Util/MultipleDisposable.cs @@ -1,7 +1,9 @@ using System; +using JetBrains.Annotations; -namespace Backend.Fx.Extensions +namespace Backend.Fx.Util { + [PublicAPI] public class MultipleDisposable : IDisposable { private readonly IDisposable[] _disposables; diff --git a/src/abstractions/Backend.Fx/Extensions/ReaderWriterLockSlimExtensions.cs b/src/abstractions/Backend.Fx/Util/ReaderWriterLockSlimExtensions.cs similarity index 95% rename from src/abstractions/Backend.Fx/Extensions/ReaderWriterLockSlimExtensions.cs rename to src/abstractions/Backend.Fx/Util/ReaderWriterLockSlimExtensions.cs index 7ad0645d..b1a5ce33 100644 --- a/src/abstractions/Backend.Fx/Extensions/ReaderWriterLockSlimExtensions.cs +++ b/src/abstractions/Backend.Fx/Util/ReaderWriterLockSlimExtensions.cs @@ -1,8 +1,10 @@ using System; using System.Threading; +using JetBrains.Annotations; -namespace Backend.Fx.Extensions +namespace Backend.Fx.Util { + [PublicAPI] public static class ReaderWriterLockSlimExtensions { private sealed class ReadLockToken : IDisposable diff --git a/src/abstractions/Backend.Fx/Util/ReflectionEx.cs b/src/abstractions/Backend.Fx/Util/ReflectionEx.cs new file mode 100644 index 00000000..c6e268f8 --- /dev/null +++ b/src/abstractions/Backend.Fx/Util/ReflectionEx.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using JetBrains.Annotations; + +namespace Backend.Fx.Util +{ + [PublicAPI] + public static class ReflectionEx + { + public static IEnumerable GetImplementingTypes(this IEnumerable assemblies) + { + return assemblies.GetImplementingTypes(typeof(TService)); + } + + public static IEnumerable GetImplementingTypes(this IEnumerable assemblies, Type serviceType) + { + return assemblies + .Distinct() + .Where(assembly => !assembly.IsDynamic) + .SelectMany(assembly => assembly.GetTypes()) + .Where(t => t.IsClass && !t.IsAbstract) + .Where(serviceType.IsAssignableFrom); + } + + public static IEnumerable GetImplementingTypes(this Assembly assembly, Type serviceType) + { + return assembly.GetTypes() + .Where(t => t.IsClass && !t.IsAbstract) + .Where(serviceType.IsAssignableFrom); + } + + public static bool IsImplementationOfOpenGenericInterface(this Type t, Type openGenericInterface) + { + return t.GetInterfaces().Any(x => x.GetTypeInfo().IsGenericType && x.GetGenericTypeDefinition() == openGenericInterface); + } + + public static string GetDetailedTypeName(this Type t) + { + string detailedTypeName = t.Name; + if (t.GetTypeInfo().IsGenericType) + { + var genericNameWithoutArgCount = t.Name.Substring(0, t.Name.IndexOf('`')); + var typeArgNames = t.GenericTypeArguments.Select(a => a.Name); + detailedTypeName = $"{genericNameWithoutArgCount}<{string.Join(",", typeArgNames)}>"; + } + + return detailedTypeName; + } + + public static bool IsOpenGeneric(this Type t) + { + if (t == null) return false; + if (t.IsGenericParameter) return true; + if (t.IsGenericType && t.GetGenericArguments().Any(arg => arg.IsOpenGeneric())) return true; + + return false; + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Extensions/StringEnumUtil.cs b/src/abstractions/Backend.Fx/Util/StringEnumUtil.cs similarity index 77% rename from src/abstractions/Backend.Fx/Extensions/StringEnumUtil.cs rename to src/abstractions/Backend.Fx/Util/StringEnumUtil.cs index de594405..37c09c01 100644 --- a/src/abstractions/Backend.Fx/Extensions/StringEnumUtil.cs +++ b/src/abstractions/Backend.Fx/Util/StringEnumUtil.cs @@ -1,8 +1,10 @@ using System; using System.Linq; +using JetBrains.Annotations; -namespace Backend.Fx.Extensions +namespace Backend.Fx.Util { + [PublicAPI] public static class StringEnumUtil { public static TEnum Parse(this string value) where TEnum : struct @@ -13,7 +15,7 @@ public static TEnum Parse(this string value) where TEnum : struct } var validValues = Enum.GetValues(typeof(TEnum)).Cast(); - string validValuesString = string.Join("], [", validValues.Select(en => en.ToString())); + var validValuesString = string.Join("], [", validValues.Select(en => en.ToString())); throw new ArgumentException($"The string [{value}] is not a valid value for the enum type {typeof(TEnum).Name}. Valid string values are: [{validValuesString}]"); } } diff --git a/src/abstractions/Backend.Fx/Extensions/StringEx.cs b/src/abstractions/Backend.Fx/Util/StringEx.cs similarity index 90% rename from src/abstractions/Backend.Fx/Extensions/StringEx.cs rename to src/abstractions/Backend.Fx/Util/StringEx.cs index 54091267..52517ba0 100644 --- a/src/abstractions/Backend.Fx/Extensions/StringEx.cs +++ b/src/abstractions/Backend.Fx/Util/StringEx.cs @@ -1,7 +1,9 @@ using System.Text.RegularExpressions; +using JetBrains.Annotations; -namespace Backend.Fx.Extensions +namespace Backend.Fx.Util { + [PublicAPI] public static class StringEx { public static string Cut(this string s, int length) diff --git a/src/abstractions/Backend.Fx/Extensions/TolerantDateTimeComparer.cs b/src/abstractions/Backend.Fx/Util/TolerantDateTimeComparer.cs similarity index 64% rename from src/abstractions/Backend.Fx/Extensions/TolerantDateTimeComparer.cs rename to src/abstractions/Backend.Fx/Util/TolerantDateTimeComparer.cs index cb401099..e0085d46 100644 --- a/src/abstractions/Backend.Fx/Extensions/TolerantDateTimeComparer.cs +++ b/src/abstractions/Backend.Fx/Util/TolerantDateTimeComparer.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; +using NodaTime; -namespace Backend.Fx.Extensions +namespace Backend.Fx.Util { + [PublicAPI] public class TolerantDateTimeOffsetComparer : IEqualityComparer { private readonly TimeSpan _epsilon; @@ -27,6 +30,7 @@ public int GetHashCode(DateTimeOffset? obj) } } + [PublicAPI] public class TolerantDateTimeComparer : IEqualityComparer { private readonly TimeSpan _epsilon; @@ -50,4 +54,29 @@ public int GetHashCode(DateTime? obj) return obj?.GetHashCode() ?? 0; } } + + [PublicAPI] + public class TolerantInstantComparer : IEqualityComparer + { + private readonly Duration _epsilon; + + public TolerantInstantComparer(Duration epsilon) + { + _epsilon = epsilon; + } + + public bool Equals(Instant? x, Instant? y) + { + if (x == null && y == null) return true; + + if (x == null || y == null) return false; + + return Math.Abs((x.Value - y.Value).TotalMilliseconds) < _epsilon.TotalMilliseconds; + } + + public int GetHashCode(Instant? obj) + { + return obj?.GetHashCode() ?? 0; + } + } } \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Backend.Fx.AspNetCore.csproj b/src/environments/Backend.Fx.AspNetCore/Backend.Fx.AspNetCore.csproj index 2f836e89..deec7937 100644 --- a/src/environments/Backend.Fx.AspNetCore/Backend.Fx.AspNetCore.csproj +++ b/src/environments/Backend.Fx.AspNetCore/Backend.Fx.AspNetCore.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net6.0 Library true snupkg @@ -29,9 +29,8 @@ - - - + + diff --git a/src/environments/Backend.Fx.AspNetCore/BackendFxApplicationStartup.cs b/src/environments/Backend.Fx.AspNetCore/BackendFxApplicationStartup.cs deleted file mode 100644 index da15bc23..00000000 --- a/src/environments/Backend.Fx.AspNetCore/BackendFxApplicationStartup.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Security.Principal; -using Backend.Fx.AspNetCore.MultiTenancy; -using Backend.Fx.AspNetCore.Mvc; -using Backend.Fx.AspNetCore.Mvc.Activators; -using Backend.Fx.Patterns.DependencyInjection; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace Backend.Fx.AspNetCore -{ - public static class BackendFxApplicationStartup - { - public static void AddBackendFxApplication(this IServiceCollection services) - where THostedService : class, IBackendFxApplicationHostedService - { - services.AddSingleton(); - services.AddSingleton(provider => provider.GetRequiredService()); - services.AddSingleton(provider => provider.GetRequiredService().Application); - services.AddSingleton(); - } - - public static void UseBackendFxApplication(this IApplicationBuilder app) - where THostedService : class, IBackendFxApplicationHostedService - { - app.UseMiddleware(); - - app.Use(async (context, requestDelegate) => - { - IBackendFxApplication application = app.ApplicationServices.GetRequiredService().Application; - application.WaitForBoot(); - - // set the instance provider for the controller activator - context.SetCurrentInstanceProvider(application.CompositionRoot.InstanceProvider); - - // the ambient tenant id has been set before by a TenantMiddleware - var tenantId = context.GetTenantId(); - - // the invoking identity has been set before by an AuthenticationMiddleware - IIdentity actingIdentity = context.User.Identity; - - await application.AsyncInvoker.InvokeAsync(_ => requestDelegate.Invoke(), actingIdentity, tenantId); - }); - } - } -} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Bootstrapping/WaitForBootMiddleware.cs b/src/environments/Backend.Fx.AspNetCore/Bootstrapping/WaitForBootMiddleware.cs deleted file mode 100644 index fdc4659f..00000000 --- a/src/environments/Backend.Fx.AspNetCore/Bootstrapping/WaitForBootMiddleware.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Threading.Tasks; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.AspNetCore.Bootstrapping -{ - /// - /// Queues all requests until the application finished booting. - /// - public class WaitForBootMiddleware - { - private static readonly ILogger Logger = Log.Create(); - private readonly RequestDelegate _next; - private readonly IBackendFxApplication _application; - - [UsedImplicitly] - public WaitForBootMiddleware(RequestDelegate next, IBackendFxApplication application) - { - _next = next; - _application = application; - } - - /// - /// This method is being called by the previous middleware in the HTTP pipeline - /// - [UsedImplicitly] - public async Task Invoke(HttpContext context) - { - while (!_application.WaitForBoot(3000)) - { - Logger.LogInformation("Queuing Request while application is booting..."); - } - - await _next.Invoke(context); - } - } -} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Configuration/ConfigurationEx.cs b/src/environments/Backend.Fx.AspNetCore/Configuration/ConfigurationEx.cs deleted file mode 100644 index d01f9edf..00000000 --- a/src/environments/Backend.Fx.AspNetCore/Configuration/ConfigurationEx.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Options; - -namespace Backend.Fx.AspNetCore.Configuration -{ - public static class ConfigurationEx - { - public static TOptions Load(this IConfiguration configuration) where TOptions : class, new() - { - IConfigurationSection configurationSection = configuration.GetSection(typeof(TOptions).Name); - var configurationOptions = new NamedConfigureFromConfigurationOptions( - typeof(TOptions).Name, - configurationSection); - var options = new TOptions(); - configurationOptions.Action(options); - return options; - } - } -} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/ErrorHandling/ErrorHandlingMiddleware.cs b/src/environments/Backend.Fx.AspNetCore/ErrorHandling/ErrorHandlingMiddleware.cs index c8bfdf9e..0f60fca6 100644 --- a/src/environments/Backend.Fx.AspNetCore/ErrorHandling/ErrorHandlingMiddleware.cs +++ b/src/environments/Backend.Fx.AspNetCore/ErrorHandling/ErrorHandlingMiddleware.cs @@ -6,81 +6,80 @@ using JetBrains.Annotations; using Microsoft.AspNetCore.Http; -namespace Backend.Fx.AspNetCore.ErrorHandling +namespace Backend.Fx.AspNetCore.ErrorHandling; + +public abstract class ErrorHandlingMiddleware { - public abstract class ErrorHandlingMiddleware - { - private readonly RequestDelegate _next; + private readonly RequestDelegate _next; - /// - /// This constructor is being called by the framework DI container - /// - [UsedImplicitly] - protected ErrorHandlingMiddleware(RequestDelegate next) - { - _next = next; - } + /// + /// This constructor is being called by the framework DI container + /// + [UsedImplicitly] + protected ErrorHandlingMiddleware(RequestDelegate next) + { + _next = next; + } - /// - /// This method is being called by the previous middleware in the HTTP pipeline - /// - [UsedImplicitly] - public async Task Invoke(HttpContext context) + /// + /// This method is being called by the previous middleware in the HTTP pipeline + /// + [UsedImplicitly] + public async Task Invoke(HttpContext context) + { + if (await ShouldHandle(context)) { - if (await ShouldHandle(context)) + try { - try + await _next.Invoke(context); + } + catch (TooManyRequestsException tooManyRequestsException) + { + if (tooManyRequestsException.RetryAfter > 0) { - await _next.Invoke(context); + context.Response.Headers.Add("Retry-After", tooManyRequestsException.RetryAfter.ToString(CultureInfo.InvariantCulture)); } - catch (TooManyRequestsException tmrex) - { - if (tmrex.RetryAfter > 0) - { - context.Response.Headers.Add("Retry-After", tmrex.RetryAfter.ToString(CultureInfo.InvariantCulture)); - } - await HandleClientError(context, 429, "TooManyRequests", tmrex); - } - catch (UnprocessableException uex) - { - await HandleClientError(context, 422, "Unprocessable", uex); - } - catch (NotFoundException nfex) - { - await HandleClientError(context, (int) HttpStatusCode.NotFound, HttpStatusCode.NotFound.ToString(), nfex); - } - catch (ConflictedException confex) - { - await HandleClientError(context, (int) HttpStatusCode.Conflict, HttpStatusCode.Conflict.ToString(), confex); - } - catch (ForbiddenException uex) - { - await HandleClientError(context, (int) HttpStatusCode.Forbidden, HttpStatusCode.Forbidden.ToString(), uex); - } - catch (UnauthorizedException uex) - { - await HandleClientError(context, (int) HttpStatusCode.Unauthorized, HttpStatusCode.Unauthorized.ToString(), uex); - } - catch (ClientException cex) - { - await HandleClientError(context, (int) HttpStatusCode.BadRequest, HttpStatusCode.BadRequest.ToString(), cex); - } - catch (Exception ex) - { - await HandleServerError(context, ex); - } + await HandleClientError(context, 429, "TooManyRequests", tooManyRequestsException); } - else + catch (UnprocessableException uex) { - await _next.Invoke(context); + await HandleClientError(context, 422, "Unprocessable", uex); + } + catch (NotFoundException notFoundException) + { + await HandleClientError(context, (int) HttpStatusCode.NotFound, HttpStatusCode.NotFound.ToString(), notFoundException); + } + catch (ConflictedException conflictedException) + { + await HandleClientError(context, (int) HttpStatusCode.Conflict, HttpStatusCode.Conflict.ToString(), conflictedException); + } + catch (ForbiddenException forbiddenException) + { + await HandleClientError(context, (int) HttpStatusCode.Forbidden, HttpStatusCode.Forbidden.ToString(), forbiddenException); } + catch (UnauthorizedException unauthorizedException) + { + await HandleClientError(context, (int) HttpStatusCode.Unauthorized, HttpStatusCode.Unauthorized.ToString(), unauthorizedException); + } + catch (ClientException clientException) + { + await HandleClientError(context, (int) HttpStatusCode.BadRequest, HttpStatusCode.BadRequest.ToString(), clientException); + } + catch (Exception exception) + { + await HandleServerError(context, exception); + } + } + else + { + await _next.Invoke(context); } + } - protected abstract Task ShouldHandle(HttpContext context); + protected abstract Task ShouldHandle(HttpContext context); - protected abstract Task HandleClientError(HttpContext context, int httpStatusCode, string message, ClientException exception); + protected abstract Task HandleClientError(HttpContext context, int httpStatusCode, string message, ClientException exception); - protected abstract Task HandleServerError(HttpContext context, Exception exception); - } + protected abstract Task HandleServerError(HttpContext context, Exception exception); } \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/ErrorHandling/ErrorLoggingMiddleware.cs b/src/environments/Backend.Fx.AspNetCore/ErrorHandling/ErrorLoggingMiddleware.cs deleted file mode 100644 index b384b1e1..00000000 --- a/src/environments/Backend.Fx.AspNetCore/ErrorHandling/ErrorLoggingMiddleware.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Threading.Tasks; -using JetBrains.Annotations; -using Backend.Fx.Logging; -using Microsoft.AspNetCore.Http; - -namespace Backend.Fx.AspNetCore.ErrorHandling -{ - public class ErrorLoggingMiddleware - { - private readonly RequestDelegate _next; - private readonly IExceptionLogger _exceptionLogger; - - [UsedImplicitly] - public ErrorLoggingMiddleware(RequestDelegate next, IExceptionLogger exceptionLogger) - { - _next = next; - _exceptionLogger = exceptionLogger; - } - - [UsedImplicitly] - public async Task Invoke(HttpContext context) - { - try - { - await _next.Invoke(context); - } - catch (Exception exception) - { - if (!context.Items.ContainsKey("ExceptionLogged")) - { - _exceptionLogger.LogException(exception); - context.Items["ExceptionLogged"] = true; - } - - throw; - } - } - } -} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/ErrorHandling/ErrorResponse.cs b/src/environments/Backend.Fx.AspNetCore/ErrorHandling/ErrorResponse.cs new file mode 100644 index 00000000..6b7d993d --- /dev/null +++ b/src/environments/Backend.Fx.AspNetCore/ErrorHandling/ErrorResponse.cs @@ -0,0 +1,38 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using Backend.Fx.Exceptions; +using JetBrains.Annotations; + +namespace Backend.Fx.AspNetCore.ErrorHandling +{ + [PublicAPI] + public class ErrorResponse + { + public ErrorResponse([NotNull] Errors errors) + { + if (errors == null) throw new ArgumentNullException(nameof(errors)); + + GenericError = errors + .Where(kvp => kvp.Key == Backend.Fx.Exceptions.Errors.GenericErrorKey) + .Select(kvp => string.Join(Environment.NewLine, kvp.Value)).FirstOrDefault(); + Errors = errors + .Where(kvp => kvp.Key != Backend.Fx.Exceptions.Errors.GenericErrorKey) + .Select(kvp => new SerializableError { Key = kvp.Key, Errors = kvp.Value }) + .ToArray(); + } + + [JsonPropertyName("_error")] + public string GenericError { get; } + + [JsonPropertyName("errors")] + public SerializableError[] Errors { get; } + + public string ToJsonString(JsonSerializerOptions options = null) + { + options ??= new JsonSerializerOptions { WriteIndented = true }; + return JsonSerializer.Serialize(this, options); + } + } +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/ErrorHandling/JsonErrorHandlingMiddleware.cs b/src/environments/Backend.Fx.AspNetCore/ErrorHandling/JsonErrorHandlingMiddleware.cs index 1267abb1..643d7a1a 100644 --- a/src/environments/Backend.Fx.AspNetCore/ErrorHandling/JsonErrorHandlingMiddleware.cs +++ b/src/environments/Backend.Fx.AspNetCore/ErrorHandling/JsonErrorHandlingMiddleware.cs @@ -2,103 +2,75 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Text.Json; using System.Threading.Tasks; using Backend.Fx.Exceptions; using Backend.Fx.Logging; +using JetBrains.Annotations; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; -using ILogger = Microsoft.Extensions.Logging.ILogger; +using NodaTime; +using NodaTime.Serialization.SystemTextJson; -namespace Backend.Fx.AspNetCore.ErrorHandling -{ - public class JsonErrorHandlingMiddleware : ErrorHandlingMiddleware - { - private readonly bool _showInternalServerErrorDetails; - private static readonly ILogger Logger = Log.Create(); - - protected JsonSerializerSettings JsonSerializerSettings { get; } = new JsonSerializerSettings - { - ContractResolver = new DefaultContractResolver - { - NamingStrategy = new CamelCaseNamingStrategy {ProcessDictionaryKeys = true} - }, - }; +namespace Backend.Fx.AspNetCore.ErrorHandling; - public JsonErrorHandlingMiddleware(RequestDelegate next, bool showInternalServerErrorDetails) - : base(next) - { - _showInternalServerErrorDetails = showInternalServerErrorDetails; - } +[PublicAPI] +public class JsonErrorHandlingMiddleware : ErrorHandlingMiddleware +{ + private readonly bool _showInternalServerErrorDetails; + private readonly ILogger _logger = Log.Create(); - protected override Task ShouldHandle(HttpContext context) - { - // this middleware only handles requests that accept json as response - IList accept = context.Request.GetTypedHeaders().Accept; - return Task.FromResult(accept?.Any(mth => mth.Type == "application" && mth.SubType == "json") == true); - } + private static readonly JsonSerializerOptions SerializerOptions = + new JsonSerializerOptions { WriteIndented = true }.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); - protected override async Task HandleClientError(HttpContext context, int httpStatusCode, string message, ClientException exception) - { - if (context.Response.HasStarted) - { - Logger.LogWarning("exception cannot be handled correctly, because the response has already started"); - return; - } - // convention: only the errors array will be transmitted to the client, allowing technical (possibly - // revealing) information in the exception message. - Errors errors = exception.HasErrors() - ? exception.Errors - : new Errors().Add($"HTTP{httpStatusCode}: {message}"); + public JsonErrorHandlingMiddleware(RequestDelegate next, bool showInternalServerErrorDetails) + : base(next) + { + _showInternalServerErrorDetails = showInternalServerErrorDetails; + } - context.Response.StatusCode = httpStatusCode; - string serializedErrors = SerializeErrors(errors); - context.Response.ContentType = "application/json; charset=utf-8"; - await context.Response.WriteAsync(serializedErrors); - } + protected override Task ShouldHandle(HttpContext context) + { + // this middleware only handles requests that accept json as response + IList accept = context.Request.GetTypedHeaders().Accept; + return Task.FromResult(accept.Any(mth => mth.Type == "application" && mth.SubType == "json")); + } - protected override async Task HandleServerError(HttpContext context, Exception exception) + protected override async Task HandleClientError(HttpContext context, int httpStatusCode, string message, + ClientException exception) + { + if (context.Response.HasStarted) { - if (context.Response.HasStarted) - { - Logger.LogWarning("exception cannot be handled correctly, because the response has already started"); - return; - } - - context.Response.StatusCode = (int) HttpStatusCode.InternalServerError; - var responseContent = _showInternalServerErrorDetails - ? JsonConvert.SerializeObject(new {message = exception.Message, stackTrace = exception.StackTrace}, JsonSerializerSettings) - : JsonConvert.SerializeObject(new {message = "An internal error occured"}, JsonSerializerSettings); - context.Response.ContentType = "application/json; charset=utf-8"; - await context.Response.WriteAsync(responseContent); + _logger.LogWarning("exception cannot be handled correctly, because the response has already started"); + return; } - protected virtual string SerializeErrors(Errors errors) - { - var errorsDictionaryForJson = errors.ToDictionary(kvp => kvp.Key == "" ? "_error" : kvp.Key, kvp => kvp.Value); - return JsonConvert.SerializeObject(errorsDictionaryForJson, JsonSerializerSettings); - } + // convention: only the errors array will be transmitted to the client, allowing technical (possibly + // revealing) information in the exception message. + Errors errors = exception.HasErrors() + ? exception.Errors + : new Errors($"HTTP{httpStatusCode}: {message}"); + + context.Response.StatusCode = httpStatusCode; + context.Response.ContentType = "application/json; charset=utf-8"; + await context.Response.WriteAsync(new ErrorResponse(errors).ToJsonString()); } - - public class ErrorShape - { - public Dictionary Errors { get; set; } - public string[] GenericError + protected override async Task HandleServerError(HttpContext context, Exception exception) + { + if (context.Response.HasStarted) { - get - { - Errors.TryGetValue("_error", out var genericError); - return genericError; - } + _logger.LogWarning("exception cannot be handled correctly, because the response has already started"); + return; } - public bool HasOnlyGenericError() - { - return Errors.Count == 1 && Errors.Keys.Single() == "_error"; - } + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + var responseContent = _showInternalServerErrorDetails + ? JsonSerializer.Serialize(new { message = exception.Message, stackTrace = exception.StackTrace }, SerializerOptions) + : JsonSerializer.Serialize(new { message = "An internal error occured" }, SerializerOptions); + context.Response.ContentType = "application/json; charset=utf-8"; + await context.Response.WriteAsync(responseContent); } } \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/ErrorHandling/SerializableError.cs b/src/environments/Backend.Fx.AspNetCore/ErrorHandling/SerializableError.cs new file mode 100644 index 00000000..23967cec --- /dev/null +++ b/src/environments/Backend.Fx.AspNetCore/ErrorHandling/SerializableError.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; +using JetBrains.Annotations; + +namespace Backend.Fx.AspNetCore.ErrorHandling +{ + [PublicAPI] + public class SerializableError + { + [JsonPropertyName("key")] + public string Key { get; set; } + + [JsonPropertyName("errors")] + public string[] Errors { get; set; } + } +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/BackendFxApplicationHostedService.cs b/src/environments/Backend.Fx.AspNetCore/Hosting/BackendFxApplicationHostedService.cs similarity index 51% rename from src/environments/Backend.Fx.AspNetCore/BackendFxApplicationHostedService.cs rename to src/environments/Backend.Fx.AspNetCore/Hosting/BackendFxApplicationHostedService.cs index 9a93bd61..ac0aedde 100644 --- a/src/environments/Backend.Fx.AspNetCore/BackendFxApplicationHostedService.cs +++ b/src/environments/Backend.Fx.AspNetCore/Hosting/BackendFxApplicationHostedService.cs @@ -2,35 +2,42 @@ using System.Threading; using System.Threading.Tasks; using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; +using JetBrains.Annotations; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using ILogger = Microsoft.Extensions.Logging.ILogger; -namespace Backend.Fx.AspNetCore +namespace Backend.Fx.AspNetCore.Hosting { + [PublicAPI] public interface IBackendFxApplicationHostedService : IHostedService { IBackendFxApplication Application { get; } } - - public abstract class BackendFxApplicationHostedService : IBackendFxApplicationHostedService + + [PublicAPI] + public class BackendFxApplicationHostedService : IBackendFxApplicationHostedService { - private static readonly ILogger Logger = Log.Create(); + private readonly ILogger _logger = Log.Create(); - public abstract IBackendFxApplication Application { get; } + public BackendFxApplicationHostedService(IBackendFxApplication application) + { + Application = application; + } + + public IBackendFxApplication Application { get; } public virtual async Task StartAsync(CancellationToken ct) { - using (Logger.LogInformationDuration("Application starting...")) + using (_logger.LogInformationDuration("Application starting...")) { try { - await Application.BootAsync(ct); + await Application.BootAsync(ct).ConfigureAwait(false); } catch (Exception ex) { - Logger.LogCritical(ex, "Application could not be started"); + _logger.LogCritical(ex, "Application could not be started"); throw; } } @@ -38,11 +45,12 @@ public virtual async Task StartAsync(CancellationToken ct) public virtual Task StopAsync(CancellationToken cancellationToken) { - using (Logger.LogInformationDuration("Application stopping...")) + using (_logger.LogInformationDuration("Application stopping...")) { Application.Dispose(); return Task.CompletedTask; } } } + } \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Hosting/StartupEx.cs b/src/environments/Backend.Fx.AspNetCore/Hosting/StartupEx.cs new file mode 100644 index 00000000..2e4ffb5b --- /dev/null +++ b/src/environments/Backend.Fx.AspNetCore/Hosting/StartupEx.cs @@ -0,0 +1,17 @@ +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Backend.Fx.AspNetCore.Hosting; + +[PublicAPI] +public static class StartupEx +{ + public static void AddBackendFxApplication(this IServiceCollection services, IBackendFxApplication application) + { + // by wrapping the application in a hosted service and adding it to the service collection we + // ensure that the application gets booted on Asp.Net Core application start + IHostedService hostedService = new BackendFxApplicationHostedService(application); + services.AddSingleton(hostedService); + } +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/HttpRequestEx.cs b/src/environments/Backend.Fx.AspNetCore/HttpRequestEx.cs deleted file mode 100644 index d1c337dd..00000000 --- a/src/environments/Backend.Fx.AspNetCore/HttpRequestEx.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace Backend.Fx.AspNetCore -{ - public static class HttpRequestEx - { - /// - /// Is the request method considered as safe in sense of a RESTful API? - /// See https://restcookbook.com/HTTP%20Methods/idempotency/ - /// - /// - /// - public static bool IsRestfulSafe(this HttpRequest request) - { - return request.IsGet() || request.IsOptions() || request.IsHead(); - } - - /// - /// Is the request method considered as idempotent in sense of a RESTful API? - /// See https://restcookbook.com/HTTP%20Methods/idempotency/ - /// - /// - /// - public static bool IsRestfulIdempotent(this HttpRequest request) - { - return request.IsGet() || request.IsOptions() || request.IsHead() || request.IsDelete() || request.IsPut(); - } - - public static bool IsGet(this HttpRequest request) - { - return HttpMethods.IsGet(request.Method); - } - - public static bool IsConnect(this HttpRequest request) - { - return HttpMethods.IsConnect(request.Method); - } - - public static bool IsDelete(this HttpRequest request) - { - return HttpMethods.IsDelete(request.Method); - } - - public static bool IsHead(this HttpRequest request) - { - return HttpMethods.IsHead(request.Method); - } - - public static bool IsOptions(this HttpRequest request) - { - return HttpMethods.IsOptions(request.Method); - } - - public static bool IsPatch(this HttpRequest request) - { - return HttpMethods.IsPatch(request.Method); - } - - public static bool IsPost(this HttpRequest request) - { - return HttpMethods.IsPost(request.Method); - } - - public static bool IsPut(this HttpRequest request) - { - return HttpMethods.IsPut(request.Method); - } - - public static bool IsTrace(this HttpRequest request) - { - return HttpMethods.IsTrace(request.Method); - } - } -} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/HttpResponseEx.cs b/src/environments/Backend.Fx.AspNetCore/HttpResponseEx.cs deleted file mode 100644 index 01355ab0..00000000 --- a/src/environments/Backend.Fx.AspNetCore/HttpResponseEx.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; - -namespace Backend.Fx.AspNetCore -{ - public static class HttpResponseEx - { - public static async Task WriteJsonAsync(this HttpResponse response, object o, JsonSerializerOptions options = null, string contentType = null) - { - options ??= new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - AllowTrailingCommas = true, - DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, - }; - - await response.WriteJsonAsync(JsonSerializer.Serialize(o, options), contentType); - } - - public static async Task WriteJsonAsync(this HttpResponse response, string json, string contentType = null) - { - response.ContentType = (contentType ?? "application/json; charset=UTF-8"); - await response.WriteAsync(json); - await response.Body.FlushAsync(); - } - } -} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/MultiTenancy/CreateTenantParams.cs b/src/environments/Backend.Fx.AspNetCore/MultiTenancy/CreateTenantParams.cs deleted file mode 100644 index 52957e3c..00000000 --- a/src/environments/Backend.Fx.AspNetCore/MultiTenancy/CreateTenantParams.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Newtonsoft.Json; - -namespace Backend.Fx.AspNetCore.MultiTenancy -{ - public class CreateTenantParams - { - [JsonProperty(PropertyName = "isDemo")] - public bool IsDemo { get; set; } - - [JsonProperty(PropertyName = "name")] - public string Name { get; set; } - - [JsonProperty(PropertyName = "description")] - public string Description { get; set; } - - [JsonProperty(PropertyName = "administratorEmail")] - public string AdministratorEmail { get; set; } - - [JsonProperty(PropertyName = "administratorPassword")] - public string AdministratorPassword { get; set; } - - [JsonProperty(PropertyName = "configuration")] - public string Configuration { get; set; } - } -} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/MultiTenancy/HttpContextEx.cs b/src/environments/Backend.Fx.AspNetCore/MultiTenancy/HttpContextEx.cs deleted file mode 100644 index 09876ccb..00000000 --- a/src/environments/Backend.Fx.AspNetCore/MultiTenancy/HttpContextEx.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using Backend.Fx.Environment.MultiTenancy; -using Microsoft.AspNetCore.Http; - -namespace Backend.Fx.AspNetCore.MultiTenancy -{ - public static class HttpContextEx - { - private const string TenantId = nameof(TenantId); - - public static void SetCurrentTenantId(this HttpContext httpContext, TenantId tenantId) - { - if (httpContext.Items.TryGetValue(TenantId, out object untyped)) - { - throw new InvalidOperationException($"TenantId has been set already in this HttpContext. Value: {(untyped ?? "null")}"); - } - - httpContext.Items[TenantId] = tenantId; - } - - public static TenantId GetTenantId(this HttpContext httpContext) - { - if (httpContext.Items.TryGetValue(TenantId, out object untyped)) - { - return (TenantId) untyped; - } - - throw new InvalidOperationException("No TenantId present in this HttpContext"); - } - } -} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/MultiTenancy/MultiTenantMiddlewareBase.cs b/src/environments/Backend.Fx.AspNetCore/MultiTenancy/MultiTenantMiddlewareBase.cs deleted file mode 100644 index 60c01c15..00000000 --- a/src/environments/Backend.Fx.AspNetCore/MultiTenancy/MultiTenantMiddlewareBase.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Threading.Tasks; -using Backend.Fx.Environment.MultiTenancy; -using Microsoft.AspNetCore.Http; - -namespace Backend.Fx.AspNetCore.MultiTenancy -{ - public abstract class MultiTenantMiddlewareBase - { - private readonly RequestDelegate _next; - - protected MultiTenantMiddlewareBase(RequestDelegate next) - { - _next = next; - } - - public async Task Invoke(HttpContext context) - { - context.SetCurrentTenantId(FindMatchingTenantId(context)); - await _next.Invoke(context); - } - - /// - /// Detects the for this request from the current HttpContext. Possible implementations might rely on - /// a dedicated header value, the (sub-) domain name, a query string parameter etc. This method is called for each request. If - /// the database is required for determination, some kind of caching is advised. - /// - /// - /// The TenantId for this request - protected abstract TenantId FindMatchingTenantId(HttpContext context); - } -} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/MultiTenancy/SingleTenantMiddleware.cs b/src/environments/Backend.Fx.AspNetCore/MultiTenancy/SingleTenantMiddleware.cs deleted file mode 100644 index 92a18301..00000000 --- a/src/environments/Backend.Fx.AspNetCore/MultiTenancy/SingleTenantMiddleware.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Threading.Tasks; -using Backend.Fx.Environment.MultiTenancy; -using Microsoft.AspNetCore.Http; - -namespace Backend.Fx.AspNetCore.MultiTenancy -{ - /// - /// Always assumes TenantId: 1 for all requests. - /// - public class SingleTenantMiddleware - { - private readonly RequestDelegate _next; - - public SingleTenantMiddleware(RequestDelegate next) - { - _next = next; - } - - public virtual async Task Invoke(HttpContext context) - { - context.SetCurrentTenantId(new TenantId(1)); - await _next.Invoke(context); - } - } -} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/MultiTenancy/TenantAdminMiddlewareBase.cs b/src/environments/Backend.Fx.AspNetCore/MultiTenancy/TenantAdminMiddlewareBase.cs deleted file mode 100644 index 55a771d4..00000000 --- a/src/environments/Backend.Fx.AspNetCore/MultiTenancy/TenantAdminMiddlewareBase.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Exceptions; -using Backend.Fx.Logging; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.AspNetCore.MultiTenancy -{ - public abstract class TenantAdminMiddlewareBase - { - private static readonly ILogger Logger = Log.Create(); - private readonly RequestDelegate _next; - protected virtual string TenantsApiBaseUrl { get; } = "/api/tenants"; - protected ITenantService TenantService { get; } - - protected TenantAdminMiddlewareBase(RequestDelegate next, ITenantService tenantService) - { - _next = next; - TenantService = tenantService; - } - - public async Task Invoke(HttpContext context) - { - if (context.Request.Path.StartsWithSegments(TenantsApiBaseUrl)) - { - if (!IsTenantsAdmin(context)) - { - Logger.LogWarning("Unauthorized attempt to access tenant endpoints"); - context.Response.StatusCode = (int) HttpStatusCode.Forbidden; - return; - } - - if (context.Request.Method.ToLower() == "post") - { - Logger.LogInformation("Creating Tenant"); - - try - { - using (var inputStream = new StreamReader(context.Request.Body)) - { - string inputStreamContent = await inputStream.ReadToEndAsync(); - - var createTenantParams = JsonConvert.DeserializeObject(inputStreamContent); - if (createTenantParams == null) throw new ClientException(); - - Tenant tenant = await CreateTenant(createTenantParams); - Logger.LogInformation("Created Tenant {@Tenant}", tenant); - - await context.Response.WriteJsonAsync(tenant); - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Tenant Creation failed"); - context.Response.StatusCode = (int) HttpStatusCode.BadRequest; - await context.Response.WriteAsync(ex.Message); - return; - } - - return; - } - - if (HttpMethods.IsGet(context.Request.Method)) - { - var tenantIdStr = context.Request.Path.Value.Split('/').Last(); - if (int.TryParse(tenantIdStr, out int tenantId)) - { - Logger.LogInformation("Getting Tenant[{TenantId}]", tenantId); - - var tenant = TenantService.GetTenant(new TenantId(tenantId)); - await context.Response.WriteJsonAsync(tenant); - } - else - { - Logger.LogInformation("Getting Tenants"); - - var tenants = TenantService.GetTenants(); - await context.Response.WriteJsonAsync(tenants); - } - - return; - } - } - - await _next.Invoke(context); - } - - private async Task CreateTenant(CreateTenantParams createTenantParams) - { - var tenantId = TenantService.CreateTenant( - createTenantParams.Name, - createTenantParams.Description, - createTenantParams.IsDemo, - GetTenantConfiguration(createTenantParams)); - await AfterTenantCreation(createTenantParams, tenantId); - var tenant = TenantService.GetTenant(tenantId); - return tenant; - } - - protected abstract string GetTenantConfiguration(CreateTenantParams createTenantParams); - - protected virtual Task AfterTenantCreation(CreateTenantParams createTenantParams, TenantId tenantId) - { - return Task.CompletedTask; - } - - protected abstract bool IsTenantsAdmin(HttpContext context); - } -} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Mvc/Activators/BackendFxApplicationControllerActivator.cs b/src/environments/Backend.Fx.AspNetCore/Mvc/Activators/BackendFxApplicationControllerActivator.cs index c6f4d4a4..b617bcff 100644 --- a/src/environments/Backend.Fx.AspNetCore/Mvc/Activators/BackendFxApplicationControllerActivator.cs +++ b/src/environments/Backend.Fx.AspNetCore/Mvc/Activators/BackendFxApplicationControllerActivator.cs @@ -1,51 +1,37 @@ using System; using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; +using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; -namespace Backend.Fx.AspNetCore.Mvc.Activators -{ - /// - /// This controller activator relies on an set before in the - /// http context items dictionary. If non is to be found, the controller is activated - /// using the default (without providing any ctor arguments). - /// - public class BackendFxApplicationControllerActivator : IControllerActivator - { - private static readonly ILogger Logger = Log.Create(); - - public virtual object Create(ControllerContext c) - { - var requestedControllerType = c.ActionDescriptor.ControllerTypeInfo.AsType(); - - return c.HttpContext.TryGetInstanceProvider(out var ip) - ? CreateInstanceUsingInstanceProvider(ip, requestedControllerType) - : CreateInstanceUsingSystemActivator(requestedControllerType); - } +namespace Backend.Fx.AspNetCore.Mvc.Activators; - private static object CreateInstanceUsingInstanceProvider(object ip, Type requestedControllerType) - { - Logger.LogDebug("Providing {ControllerTypeName} using {InstanceProvider}", requestedControllerType.Name, ip.GetType().Name); - return ((IInstanceProvider)ip).GetInstance(requestedControllerType); - } +[PublicAPI] +public class BackendFxApplicationControllerActivator : IControllerActivator +{ + private readonly IBackendFxApplication _application; + private readonly ILogger _logger = Log.Create(); - private static object CreateInstanceUsingSystemActivator(Type requestedControllerType) - { - Logger.LogDebug("Providing {ControllerTypeName} using {Activator}", requestedControllerType.Name, nameof(Activator)); - return Activator.CreateInstance(requestedControllerType); - } + public BackendFxApplicationControllerActivator(IBackendFxApplication application) + { + _application = application; + } + + public virtual object Create(ControllerContext c) + { + Type requestedControllerType = c.ActionDescriptor.ControllerTypeInfo.AsType(); + return _application.CompositionRoot.ServiceProvider.GetRequiredService(requestedControllerType); + } - public virtual void Release(ControllerContext c, object controller) + public virtual void Release(ControllerContext c, object controller) + { + _logger.LogTrace("Releasing {ControllerTypeName}", controller.GetType().Name); + if (controller is IDisposable disposable) { - Logger.LogTrace("Releasing {ControllerTypeName}", controller.GetType().Name); - if (controller is IDisposable disposable) - { - Logger.LogDebug("Disposing {ControllerTypeName}", controller.GetType().Name); - disposable.Dispose(); - } + _logger.LogDebug("Disposing {ControllerTypeName}", controller.GetType().Name); + disposable.Dispose(); } } } \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Mvc/Activators/BackendFxApplicationHubActivator.cs b/src/environments/Backend.Fx.AspNetCore/Mvc/Activators/BackendFxApplicationHubActivator.cs deleted file mode 100644 index e34fb0b6..00000000 --- a/src/environments/Backend.Fx.AspNetCore/Mvc/Activators/BackendFxApplicationHubActivator.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; -using Microsoft.AspNetCore.SignalR; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.AspNetCore.Mvc.Activators -{ - public class BackendFxApplicationHubActivator : IHubActivator where T : Hub - { - private readonly IBackendFxApplication _backendFxApplication; - private static readonly ILogger Logger = Log.Create>(); - - - public BackendFxApplicationHubActivator(IBackendFxApplication backendFxApplication) - { - _backendFxApplication = backendFxApplication; - } - - - public T Create() - { - var ip = _backendFxApplication.CompositionRoot.InstanceProvider; - Logger.LogDebug("Providing {HubTypeName} using {InstanceProvider}", typeof(T).Name, ip.GetType().Name); - return ip.GetInstance(); - } - - public void Release(T hub) - { - Logger.LogTrace("Releasing {HubTypeName}", hub.GetType().Name); - if (hub is IDisposable disposable) - { - Logger.LogDebug("Disposing {HubTypeName}", hub.GetType().Name); - disposable.Dispose(); - } - } - } -} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Mvc/Activators/BackendFxApplicationViewComponentActivator.cs b/src/environments/Backend.Fx.AspNetCore/Mvc/Activators/BackendFxApplicationViewComponentActivator.cs index d9eb8efb..cfcf03d2 100644 --- a/src/environments/Backend.Fx.AspNetCore/Mvc/Activators/BackendFxApplicationViewComponentActivator.cs +++ b/src/environments/Backend.Fx.AspNetCore/Mvc/Activators/BackendFxApplicationViewComponentActivator.cs @@ -1,39 +1,30 @@ using System; using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; +using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc.ViewComponents; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; -namespace Backend.Fx.AspNetCore.Mvc.Activators +namespace Backend.Fx.AspNetCore.Mvc.Activators; + +[PublicAPI] +public class BackendFxApplicationViewComponentActivator : IViewComponentActivator { - public class BackendFxApplicationViewComponentActivator : IViewComponentActivator + private readonly ILogger _logger = Log.Create(); + private readonly IBackendFxApplication _application; + + public BackendFxApplicationViewComponentActivator(IBackendFxApplication application) { - private static readonly ILogger Logger = Log.Create(); + _application = application; + } - public object Create(ViewComponentContext context) - { - var requestedViewComponentType = context.ViewComponentDescriptor.TypeInfo.AsType(); - - return context.ViewContext.HttpContext.TryGetInstanceProvider(out var ip) - ? CreateInstanceUsingInstanceProvider(ip, requestedViewComponentType) - : CreateInstanceUsingSystemActivator(requestedViewComponentType); - } - - private static object CreateInstanceUsingInstanceProvider(object ip, Type requestedViewComponentType) - { - Logger.LogDebug("Providing {ViewComponentName} using {InstanceProvider}", requestedViewComponentType.Name, ip.GetType().Name); - return ((IInstanceProvider)ip).GetInstance(requestedViewComponentType); - } - - private static object CreateInstanceUsingSystemActivator(Type requestedViewComponentType) - { - Logger.LogDebug("Providing {ViewComponentName} using {InstanceProvider}", requestedViewComponentType.Name, nameof(Activator)); - return Activator.CreateInstance(requestedViewComponentType); - } + public object Create(ViewComponentContext context) + { + Type requestedViewComponentType = context.ViewComponentDescriptor.TypeInfo.AsType(); + return _application.CompositionRoot.ServiceProvider.GetRequiredService(requestedViewComponentType); + } - public void Release(ViewComponentContext context, object viewComponent) - { - } + public void Release(ViewComponentContext context, object viewComponent) + { } } \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Mvc/AspNetMvcFeature.cs b/src/environments/Backend.Fx.AspNetCore/Mvc/AspNetMvcFeature.cs new file mode 100644 index 00000000..2f3618f4 --- /dev/null +++ b/src/environments/Backend.Fx.AspNetCore/Mvc/AspNetMvcFeature.cs @@ -0,0 +1,32 @@ +using Backend.Fx.AspNetCore.Mvc.Activators; +using Backend.Fx.Features; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.ViewComponents; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.AspNetCore.Mvc; + +[PublicAPI] +public class AspNetMvcFeature : Feature +{ + private readonly IServiceCollection _frameworkServices; + + public AspNetMvcFeature(IServiceCollection frameworkServices) + { + _frameworkServices = frameworkServices; + } + + public override void Enable(IBackendFxApplication application) + { + // tell ASP.Net Core to use BackendFx to create controller instances + _frameworkServices.AddSingleton( + new BackendFxApplicationControllerActivator(application)); + + // tell ASP.Net Core to use BackendFx to create view component instances + _frameworkServices.AddSingleton( + new BackendFxApplicationViewComponentActivator(application)); + + application.CompositionRoot.RegisterModules(new AspNetMvcModule(application.Assemblies)); + } +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Mvc/AspNetMvcModule.cs b/src/environments/Backend.Fx.AspNetCore/Mvc/AspNetMvcModule.cs new file mode 100644 index 00000000..4c06ae24 --- /dev/null +++ b/src/environments/Backend.Fx.AspNetCore/Mvc/AspNetMvcModule.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Reflection; +using Backend.Fx.DependencyInjection; +using Backend.Fx.Util; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.AspNetCore.Mvc; + +public class AspNetMvcModule : IModule +{ + private readonly IEnumerable _assemblies; + + public AspNetMvcModule(IEnumerable assemblies) + { + _assemblies = assemblies; + } + public void Register(ICompositionRoot compositionRoot) + { + RegisterAllImplementationsOf(compositionRoot); + RegisterAllImplementationsOf(compositionRoot); + } + + private void RegisterAllImplementationsOf(ICompositionRoot compositionRoot) + { + foreach (var implementationType in _assemblies.GetImplementingTypes()) + { + compositionRoot.Register( + new ServiceDescriptor( + implementationType, + implementationType, + ServiceLifetime.Scoped)); + } + } +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Mvc/Execution/FlushFilter.cs b/src/environments/Backend.Fx.AspNetCore/Mvc/Execution/FlushFilter.cs deleted file mode 100644 index 2691e7a2..00000000 --- a/src/environments/Backend.Fx.AspNetCore/Mvc/Execution/FlushFilter.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Backend.Fx.Environment.Persistence; -using Microsoft.AspNetCore.Mvc.Filters; - -namespace Backend.Fx.AspNetCore.Mvc.Execution -{ - /// - /// Makes sure that possible dirty objects are flushed to the persistence layer when the MVC action was executed. This will reveal - /// persistence related problems early and makes them easier to diagnose. - /// - public class FlushFilter : IActionFilter - { - public void OnActionExecuting(ActionExecutingContext context) - { } - - public void OnActionExecuted(ActionExecutedContext context) - { - context.HttpContext.GetInstanceProvider().GetInstance().Flush(); - } - } -} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Mvc/HttpContextEx.cs b/src/environments/Backend.Fx.AspNetCore/Mvc/HttpContextEx.cs deleted file mode 100644 index e8185a3e..00000000 --- a/src/environments/Backend.Fx.AspNetCore/Mvc/HttpContextEx.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using Backend.Fx.Patterns.DependencyInjection; -using Microsoft.AspNetCore.Http; - -namespace Backend.Fx.AspNetCore.Mvc -{ - public static class HttpContextEx - { - private const string InstanceProvider = nameof(InstanceProvider); - - public static void SetCurrentInstanceProvider(this HttpContext httpContext, IInstanceProvider tenantId) - { - if (httpContext.Items.TryGetValue(InstanceProvider, out object untyped)) - { - throw new InvalidOperationException("IInstanceProvider has been set already in this HttpContext"); - } - - httpContext.Items[InstanceProvider] = tenantId; - } - - public static IInstanceProvider GetInstanceProvider(this HttpContext httpContext) - { - if (httpContext.Items.TryGetValue(InstanceProvider, out object untyped)) - { - return (IInstanceProvider) untyped; - } - - throw new InvalidOperationException("No IInstanceProvider present in this HttpContext"); - } - - public static bool TryGetInstanceProvider(this HttpContext httpContext, out IInstanceProvider instanceProvider) - { - if (httpContext.Items.TryGetValue(InstanceProvider, out object untyped)) - { - instanceProvider = (IInstanceProvider) untyped; - return true; - } - - instanceProvider = null; - return false; - } - } -} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Mvc/StartupEx.cs b/src/environments/Backend.Fx.AspNetCore/Mvc/StartupEx.cs new file mode 100644 index 00000000..5a1f09e5 --- /dev/null +++ b/src/environments/Backend.Fx.AspNetCore/Mvc/StartupEx.cs @@ -0,0 +1,29 @@ +using Backend.Fx.ExecutionPipeline; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.AspNetCore.Mvc; + +[PublicAPI] +public static class StartupEx +{ + public static void AddBackendFxMvcApplication(this IServiceCollection services, IBackendFxApplication application) + { + application.EnableFeature(new AspNetMvcFeature(services)); + } + + public static void UseBackendFxMvcApplication(this IApplicationBuilder app, IBackendFxApplication application) + { + app.Use(async (context, requestDelegate) => + { + // make sure it finished the boot process + await application.WaitForBootAsync().ConfigureAwait(false); + + await application.Invoker.InvokeAsync( + (_, _) => requestDelegate.Invoke(), + context.User.Identity ?? new AnonymousIdentity(), + context.RequestAborted); + }); + } +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Mvc/Throttling/ExceptionThrottlingAttribute.cs b/src/environments/Backend.Fx.AspNetCore/Mvc/Throttling/ExceptionThrottlingAttribute.cs index 4ed56721..e41b393f 100644 --- a/src/environments/Backend.Fx.AspNetCore/Mvc/Throttling/ExceptionThrottlingAttribute.cs +++ b/src/environments/Backend.Fx.AspNetCore/Mvc/Throttling/ExceptionThrottlingAttribute.cs @@ -1,41 +1,42 @@ using System; using Backend.Fx.Exceptions; +using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; -namespace Backend.Fx.AspNetCore.Mvc.Throttling +namespace Backend.Fx.AspNetCore.Mvc.Throttling; + +/// +/// returns HTTP 429 "Too many requests" when the attributed action gets called from the same IP address in less than +/// the configured interval and an exception was thrown. Useful to prevent brute force attacks.. +/// +[PublicAPI] +public class ExceptionThrottlingAttribute : ThrottlingBaseAttribute { - /// - /// returns HTTP 429 "Too many requests" when the attributed action get's called from the same IP address in less than - /// the configured interval and an exception was thrown. Useful to prevent brute force attacks.. - /// - public class ExceptionThrottlingAttribute : ThrottlingBaseAttribute + public override void OnActionExecuted(ActionExecutedContext actionContext) { - public override void OnActionExecuted(ActionExecutedContext actionContext) - { - var cache = actionContext.HttpContext.RequestServices.GetRequiredService(); - var key = string.Concat(Name, "-", actionContext.HttpContext.Connection.RemoteIpAddress); - - if (actionContext.Exception == null) - { - cache.Remove(key); - return; - } + var cache = actionContext.HttpContext.RequestServices.GetRequiredService(); + var key = string.Concat(Name, "-", actionContext.HttpContext.Connection.RemoteIpAddress); - if (cache.TryGetValue(key, out int repetition)) - { - var retryAfter = Math.Max(1, CalculateRepeatedTimeoutFactor(repetition)) * Seconds; - cache.Set(key, ++repetition, TimeSpan.FromSeconds(retryAfter)); - throw new TooManyRequestsException(retryAfter).AddError(string.Format(Message, retryAfter)); - } - - cache.Set(key, 1, TimeSpan.FromSeconds(Seconds)); + if (actionContext.Exception == null) + { + cache.Remove(key); + return; } - protected override int CalculateRepeatedTimeoutFactor(int repetition) + if (cache.TryGetValue(key, out int repetition)) { - return repetition * repetition; + var retryAfter = Math.Max(1, CalculateRepeatedTimeoutFactor(repetition)) * Seconds; + cache.Set(key, ++repetition, TimeSpan.FromSeconds(retryAfter)); + throw new TooManyRequestsException(retryAfter).AddError(string.Format(Message, retryAfter)); } + + cache.Set(key, 1, TimeSpan.FromSeconds(Seconds)); + } + + protected override int CalculateRepeatedTimeoutFactor(int repetition) + { + return repetition * repetition; } } \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Mvc/Throttling/ThrottlingAttribute.cs b/src/environments/Backend.Fx.AspNetCore/Mvc/Throttling/ThrottlingAttribute.cs index 031a53f8..a193e03e 100644 --- a/src/environments/Backend.Fx.AspNetCore/Mvc/Throttling/ThrottlingAttribute.cs +++ b/src/environments/Backend.Fx.AspNetCore/Mvc/Throttling/ThrottlingAttribute.cs @@ -1,31 +1,32 @@ using System; using Backend.Fx.Exceptions; +using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; -namespace Backend.Fx.AspNetCore.Mvc.Throttling +namespace Backend.Fx.AspNetCore.Mvc.Throttling; + +/// +/// returns HTTP 429 "Too many requests" when the attributed action gets called from the same IP address in less than +/// the configured interval. Useful to prevent denial of service attacks. +/// +[PublicAPI] +public class ThrottlingAttribute : ThrottlingBaseAttribute { - /// - /// returns HTTP 429 "Too many requests" when the attributed action get's called from the same IP address in less than - /// the configured interval. Useful to prevent denial of service attacks. - /// - public class ThrottlingAttribute : ThrottlingBaseAttribute + public override void OnActionExecuting(ActionExecutingContext actionContext) { - public override void OnActionExecuting(ActionExecutingContext actionContext) - { - var cache = actionContext.HttpContext.RequestServices.GetRequiredService(); - var key = string.Concat(Name, "-", actionContext.HttpContext.Connection.RemoteIpAddress); - - if (cache.TryGetValue(key, out int repetition)) - { - repetition++; - var retryAfter = Math.Max(1, CalculateRepeatedTimeoutFactor(repetition)) * Seconds; - cache.Set(key, repetition, TimeSpan.FromSeconds(retryAfter)); - throw new TooManyRequestsException(retryAfter).AddError(string.Format(Message, retryAfter)); - } + var cache = actionContext.HttpContext.RequestServices.GetRequiredService(); + var key = string.Concat(Name, "-", actionContext.HttpContext.Connection.RemoteIpAddress); - cache.Set(key, 1, TimeSpan.FromSeconds(Seconds)); + if (cache.TryGetValue(key, out int repetition)) + { + repetition++; + var retryAfter = Math.Max(1, CalculateRepeatedTimeoutFactor(repetition)) * Seconds; + cache.Set(key, repetition, TimeSpan.FromSeconds(retryAfter)); + throw new TooManyRequestsException(retryAfter).AddError(string.Format(Message, retryAfter)); } + + cache.Set(key, 1, TimeSpan.FromSeconds(Seconds)); } } \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Mvc/Throttling/ThrottlingBaseAttribute.cs b/src/environments/Backend.Fx.AspNetCore/Mvc/Throttling/ThrottlingBaseAttribute.cs index b10e2ee2..d859e1b4 100644 --- a/src/environments/Backend.Fx.AspNetCore/Mvc/Throttling/ThrottlingBaseAttribute.cs +++ b/src/environments/Backend.Fx.AspNetCore/Mvc/Throttling/ThrottlingBaseAttribute.cs @@ -1,35 +1,34 @@ using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc.Filters; -namespace Backend.Fx.AspNetCore.Mvc.Throttling +namespace Backend.Fx.AspNetCore.Mvc.Throttling; + +public abstract class ThrottlingBaseAttribute : ActionFilterAttribute { - public abstract class ThrottlingBaseAttribute : ActionFilterAttribute - { - /// - /// A unique name for this Throttle. - /// - /// - /// We'll be inserting a Cache record based on this name and client IP, e.g. "Name-192.168.0.1" - /// - [UsedImplicitly] - public string Name { get; set; } + /// + /// A unique name for this Throttle. + /// + /// + /// We'll be inserting a Cache record based on this name and client IP, e.g. "Name-192.168.0.1" + /// + [UsedImplicitly] + public string Name { get; set; } - /// - /// The number of seconds clients must wait before executing this decorated route again. - /// - [UsedImplicitly] - public int Seconds { get; set; } + /// + /// The number of seconds clients must wait before executing this decorated route again. + /// + [UsedImplicitly] + public int Seconds { get; set; } - /// - /// A text message that will be sent to the client upon throttling. You can include the token {0} to - /// show this.Seconds in the message, e.g. "Wait {0} seconds before trying again". - /// - [UsedImplicitly] - public string Message { get; set; } = "Wait {0} seconds before trying again"; + /// + /// A text message that will be sent to the client upon throttling. You can include the token {0} to + /// show this.Seconds in the message, e.g. "Wait {0} seconds before trying again". + /// + [UsedImplicitly] + public string Message { get; set; } = "Wait {0} seconds before trying again"; - protected virtual int CalculateRepeatedTimeoutFactor(int repetition) - { - return 1; - } + protected virtual int CalculateRepeatedTimeoutFactor(int repetition) + { + return 1; } } \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Mvc/Validation/ModelStateEx.cs b/src/environments/Backend.Fx.AspNetCore/Mvc/Validation/ModelStateEx.cs index 35d53113..1e2d1634 100644 --- a/src/environments/Backend.Fx.AspNetCore/Mvc/Validation/ModelStateEx.cs +++ b/src/environments/Backend.Fx.AspNetCore/Mvc/Validation/ModelStateEx.cs @@ -1,40 +1,32 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using Backend.Fx.Exceptions; using Microsoft.AspNetCore.Mvc.ModelBinding; -namespace Backend.Fx.AspNetCore.Mvc.Validation +namespace Backend.Fx.AspNetCore.Mvc.Validation; + +public static class ModelStateEx { - public static class ModelStateEx + public static Errors ToErrorsDictionary(this ModelStateDictionary modelState) { - public static string ToDebugString(this ModelStateDictionary modelState) + var dictionary = new Dictionary(); + + foreach (var keyValuePair in modelState) { - var modelErrorMessages = modelState - .Where(kvp => kvp.Value.ValidationState == ModelValidationState.Invalid) - .Select(kvp => $"{kvp.Key}: {string.Join(", ", kvp.Value.Errors.Select(err => err.ErrorMessage))}"); - - return string.Join(System.Environment.NewLine, modelErrorMessages); + dictionary.Add(keyValuePair.Key, keyValuePair.Value.Errors.Select(err => err.ErrorMessage).ToArray()); } - public static Errors ToErrorsDictionary(this ModelStateDictionary modelState) - { - var errors = new Errors(); - foreach (var keyValuePair in modelState) - { - errors.Add(keyValuePair.Key, keyValuePair.Value.Errors.Select(err => err.ErrorMessage)); - } - - return errors; - } + return new Errors(dictionary); + } - public static void Add(this ModelStateDictionary modelState, Errors errors) + public static void Add(this ModelStateDictionary modelState, Errors errors) + { + foreach (var (key, value) in errors) { - foreach (var (key, value) in errors) + foreach (var errorMessage in value) { - foreach (var errorMessage in value) - { - modelState.AddModelError(key, errorMessage); - } + modelState.AddModelError(key, errorMessage); } } } diff --git a/src/environments/Backend.Fx.AspNetCore/Mvc/Validation/ModelValidationFilter.cs b/src/environments/Backend.Fx.AspNetCore/Mvc/Validation/ModelValidationFilter.cs index 247238d3..b1718fba 100644 --- a/src/environments/Backend.Fx.AspNetCore/Mvc/Validation/ModelValidationFilter.cs +++ b/src/environments/Backend.Fx.AspNetCore/Mvc/Validation/ModelValidationFilter.cs @@ -10,48 +10,47 @@ using Microsoft.Net.Http.Headers; using ILogger = Microsoft.Extensions.Logging.ILogger; -namespace Backend.Fx.AspNetCore.Mvc.Validation +namespace Backend.Fx.AspNetCore.Mvc.Validation; + +public abstract class ModelValidationFilter : IActionFilter { - public abstract class ModelValidationFilter : IActionFilter + public abstract void OnActionExecuting(ActionExecutingContext context); + public abstract void OnActionExecuted(ActionExecutedContext context); + + protected static void LogErrors(FilterContext context, string controllerName, Errors errors) { - public abstract void OnActionExecuting(ActionExecutingContext context); - public abstract void OnActionExecuted(ActionExecutedContext context); + ILogger logger = TryGetControllerType(controllerName, out Type controllerType) + ? Log.Create(controllerType) + : Log.Create(); + logger.LogWarning("Model validation failed during {Method} {RequestUrl}: {@Errors}", + context.HttpContext.Request.Method, + context.HttpContext.Request.GetDisplayUrl(), + errors); + } - protected void LogErrors(FilterContext context, string controllerName, Errors errors) - { - ILogger logger = TryGetControllerType(controllerName, out Type controllerType) - ? Log.Create(controllerType) - : Log.Create(); - logger.LogWarning("Model validation failed during {Method} {RequestUrl}: {@Errors}", - context.HttpContext.Request.Method, - context.HttpContext.Request.GetDisplayUrl(), - errors); - } + protected static bool AcceptsJson(FilterContext context) + { + IList accept = context.HttpContext.Request.GetTypedHeaders().Accept; + return accept.Any(mth => mth.Type == "application" && mth.SubType == "json") == true; + } - protected bool AcceptsJson(FilterContext context) - { - IList accept = context.HttpContext.Request.GetTypedHeaders().Accept; - return accept?.Any(mth => mth.Type == "application" && mth.SubType == "json") == true; - } + protected static bool AcceptsHtml(FilterContext context) + { + IList accept = context.HttpContext.Request.GetTypedHeaders().Accept; + return accept.Any(mth => mth.Type == "text" && mth.SubType == "html") == true; + } - protected bool AcceptsHtml(FilterContext context) + private static bool TryGetControllerType(string controllerName, out Type type) + { + try { - IList accept = context.HttpContext.Request.GetTypedHeaders().Accept; - return accept?.Any(mth => mth.Type == "text" && mth.SubType == "html") == true; + type = Type.GetType(controllerName); } - - private static bool TryGetControllerType(string controllerName, out Type type) + catch { - try - { - type = Type.GetType(controllerName); - } - catch - { - type = null; - } - - return type != null; + type = null; } + + return type != null; } } \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Mvc/Validation/RedirectBackToGetActionModelValidationFilter.cs b/src/environments/Backend.Fx.AspNetCore/Mvc/Validation/RedirectBackToGetActionModelValidationFilter.cs index 851e0b22..7979a2f0 100644 --- a/src/environments/Backend.Fx.AspNetCore/Mvc/Validation/RedirectBackToGetActionModelValidationFilter.cs +++ b/src/environments/Backend.Fx.AspNetCore/Mvc/Validation/RedirectBackToGetActionModelValidationFilter.cs @@ -1,59 +1,60 @@ using Backend.Fx.Exceptions; +using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ViewFeatures; -namespace Backend.Fx.AspNetCore.Mvc.Validation +namespace Backend.Fx.AspNetCore.Mvc.Validation; + +[PublicAPI] +public class RedirectBackToGetActionModelValidationFilter : ModelValidationFilter { - public class RedirectBackToGetActionModelValidationFilter : ModelValidationFilter + private readonly IModelMetadataProvider _modelMetadataProvider; + + public RedirectBackToGetActionModelValidationFilter(IModelMetadataProvider modelMetadataProvider) { - private readonly IModelMetadataProvider _modelMetadataProvider; + _modelMetadataProvider = modelMetadataProvider; + } - public RedirectBackToGetActionModelValidationFilter(IModelMetadataProvider modelMetadataProvider) + public override void OnActionExecuting(ActionExecutingContext context) + { + if (!context.ModelState.IsValid && AcceptsHtml(context)) { - _modelMetadataProvider = modelMetadataProvider; - } + Errors errors = context.ModelState.ToErrorsDictionary(); + LogErrors(context, context.Controller.ToString(), errors); - public override void OnActionExecuting(ActionExecutingContext context) - { - if (!context.ModelState.IsValid && AcceptsHtml(context)) + // return the same view, using the posted model again + var viewData = new ViewDataDictionary(_modelMetadataProvider, context.ModelState); + BeforeRedirect(viewData); + context.Result = new ViewResult { - Errors errors = context.ModelState.ToErrorsDictionary(); - LogErrors(context, context.Controller.ToString(), errors); - - // return the same view, using the posted model again - var viewData = new ViewDataDictionary(_modelMetadataProvider, context.ModelState); - BeforeRedirect(viewData); - context.Result = new ViewResult - { - ViewName = context.RouteData.Values["action"].ToString(), - ViewData = viewData, - }; - } + ViewName = context.RouteData.Values["action"].ToString(), + ViewData = viewData, + }; } + } - public override void OnActionExecuted(ActionExecutedContext context) + public override void OnActionExecuted(ActionExecutedContext context) + { + if (context.Exception is ClientException cex && AcceptsHtml(context)) { - if (context.Exception is ClientException cex && AcceptsHtml(context)) + LogErrors(context, context.Controller.ToString(), cex.Errors); + context.ModelState.Add(cex.Errors); + + // return the same view, using the posted model again + var viewData = new ViewDataDictionary(_modelMetadataProvider, context.ModelState); + BeforeRedirect(viewData); + context.Result = new ViewResult { - LogErrors(context, context.Controller.ToString(), cex.Errors); - context.ModelState.Add(cex.Errors); - - // return the same view, using the posted model again - var viewData = new ViewDataDictionary(_modelMetadataProvider, context.ModelState); - BeforeRedirect(viewData); - context.Result = new ViewResult - { - ViewName = context.RouteData.Values["action"].ToString(), - ViewData = viewData, - }; - context.ExceptionHandled = true; - } + ViewName = context.RouteData.Values["action"].ToString(), + ViewData = viewData, + }; + context.ExceptionHandled = true; } + } - protected virtual void BeforeRedirect(ViewDataDictionary viewData) - { - } + protected virtual void BeforeRedirect(ViewDataDictionary viewData) + { } } \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Mvc/Validation/ReturnModelStateAsJsonModelValidationFilter.cs b/src/environments/Backend.Fx.AspNetCore/Mvc/Validation/ReturnModelStateAsJsonModelValidationFilter.cs index 43909a58..41f0089c 100644 --- a/src/environments/Backend.Fx.AspNetCore/Mvc/Validation/ReturnModelStateAsJsonModelValidationFilter.cs +++ b/src/environments/Backend.Fx.AspNetCore/Mvc/Validation/ReturnModelStateAsJsonModelValidationFilter.cs @@ -1,32 +1,34 @@ -using Backend.Fx.Exceptions; +using Backend.Fx.AspNetCore.ErrorHandling; +using Backend.Fx.Exceptions; +using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -namespace Backend.Fx.AspNetCore.Mvc.Validation +namespace Backend.Fx.AspNetCore.Mvc.Validation; + +/// +/// Returns HTTP 400 "Bad Request" when model validation failed. In addition, the bad model state is converted +/// into an s that is returned as JSON body, if the request +/// stated that it accepts JSON as response type content. +/// +[PublicAPI] +public class ReturnModelStateAsJsonModelValidationFilter : ModelValidationFilter { - /// - /// Returns HTTP 400 "Bad Request" when model validation failed. In addition, the bad model state is converted into an instance of - /// gets serialized to the body as JSON. - /// - public class ReturnModelStateAsJsonModelValidationFilter : ModelValidationFilter + public override void OnActionExecuting(ActionExecutingContext context) { - public override void OnActionExecuting(ActionExecutingContext context) - { - if (!context.ModelState.IsValid && AcceptsJson(context)) - { - Errors errors = context.ModelState.ToErrorsDictionary(); - LogErrors(context, context.Controller.ToString(), errors); - context.Result = CreateResult(errors); - } - } + if (context.ModelState.IsValid || !AcceptsJson(context)) return; + + Errors errors = context.ModelState.ToErrorsDictionary(); + LogErrors(context, context.Controller.ToString(), errors); + context.Result = CreateResult(errors); + } - protected virtual IActionResult CreateResult(Errors errors) - { - return new BadRequestObjectResult(errors); - } + protected virtual IActionResult CreateResult(Errors errors) + { + return new JsonResult(new ErrorResponse(errors)); + } - public override void OnActionExecuted(ActionExecutedContext context) - { - } + public override void OnActionExecuted(ActionExecutedContext context) + { } } \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Security/ContentSecurityPolicyOptions.cs b/src/environments/Backend.Fx.AspNetCore/Security/ContentSecurityPolicyOptions.cs deleted file mode 100644 index cb04455d..00000000 --- a/src/environments/Backend.Fx.AspNetCore/Security/ContentSecurityPolicyOptions.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Backend.Fx.AspNetCore.Security -{ - public class ContentSecurityPolicyOptions - { - public string ContentSecurityPolicy { get; set; } - public bool ReportOnly { get; set; } - public string ReportUrl { get; set; } - } -} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Security/SecurityHeadersMiddleware.cs b/src/environments/Backend.Fx.AspNetCore/Security/SecurityHeadersMiddleware.cs deleted file mode 100644 index 595bc347..00000000 --- a/src/environments/Backend.Fx.AspNetCore/Security/SecurityHeadersMiddleware.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Threading.Tasks; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; - -namespace Backend.Fx.AspNetCore.Security -{ - public class SecurityHeadersMiddleware - { - private readonly RequestDelegate _next; - private readonly IOptions _securityOptionsAccessor; - - - [UsedImplicitly] - public SecurityHeadersMiddleware(RequestDelegate next, IOptions securityOptionsAccessor) - { - _next = next ?? throw new ArgumentNullException(nameof(next)); - _securityOptionsAccessor = securityOptionsAccessor; - } - - [UsedImplicitly] - public async Task Invoke(HttpContext context) - { - ContentSecurityPolicyOptions csp = _securityOptionsAccessor.Value.ContentSecurityPolicy; - if (csp?.ContentSecurityPolicy != null && csp.ContentSecurityPolicy.Length > 0) - { - string cspHeaderKey = csp.ReportOnly ? "Content-Security-Policy-Report-Only" : "Content-Security-Policy"; - - string completeCsp = csp.ContentSecurityPolicy; - - if (!string.IsNullOrEmpty(csp.ReportUrl) && ShouldAppendReportUri(context)) - { - completeCsp += "; report-uri " + csp.ReportUrl; - } - - context.Response.Headers.Add(cspHeaderKey, new StringValues(completeCsp)); - } - - if (_securityOptionsAccessor.Value.HstsExpiration > 0) - { - context.Response.Headers.Add("Strict-Transport-Security", new StringValues($"max-age={_securityOptionsAccessor.Value.HstsExpiration}")); - } - - await _next.Invoke(context); - } - - /// - /// Override this if you want the user to be able to permit/forbid reporting - /// - /// - /// - protected virtual bool ShouldAppendReportUri(HttpContext context) - { - return true; - } - } -} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Security/SecurityHeadersOptions.cs b/src/environments/Backend.Fx.AspNetCore/Security/SecurityHeadersOptions.cs deleted file mode 100644 index e4a8a0b6..00000000 --- a/src/environments/Backend.Fx.AspNetCore/Security/SecurityHeadersOptions.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Backend.Fx.AspNetCore.Security -{ - public class SecurityHeadersOptions - { - public int HstsExpiration { get; set; } - public ContentSecurityPolicyOptions ContentSecurityPolicy { get; set; } - } -} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/SignalR/AspNetSignalRFeature.cs b/src/environments/Backend.Fx.AspNetCore/SignalR/AspNetSignalRFeature.cs new file mode 100644 index 00000000..ef714568 --- /dev/null +++ b/src/environments/Backend.Fx.AspNetCore/SignalR/AspNetSignalRFeature.cs @@ -0,0 +1,21 @@ +using Backend.Fx.Features; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.AspNetCore.SignalR; + +[PublicAPI] +public class AspNetSignalRFeature : Feature +{ + private readonly IServiceCollection _frameworkServices; + + public AspNetSignalRFeature(IServiceCollection frameworkServices) + { + _frameworkServices = frameworkServices; + } + + public override void Enable(IBackendFxApplication application) + { + application.CompositionRoot.RegisterModules(new AspNetSignalRModule(_frameworkServices, application.Assemblies)); + } +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/SignalR/AspNetSignalRModule.cs b/src/environments/Backend.Fx.AspNetCore/SignalR/AspNetSignalRModule.cs new file mode 100644 index 00000000..65736a08 --- /dev/null +++ b/src/environments/Backend.Fx.AspNetCore/SignalR/AspNetSignalRModule.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using Backend.Fx.AspNetCore.Mvc.Activators; +using Backend.Fx.DependencyInjection; +using Backend.Fx.Util; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.AspNetCore.SignalR; + +public class AspNetSignalRModule : IModule +{ + private readonly IServiceCollection _frameworkServices; + private readonly IEnumerable _assemblies; + + public AspNetSignalRModule(IServiceCollection frameworkServices, IEnumerable assemblies) + { + _frameworkServices = frameworkServices; + _assemblies = assemblies; + } + public void Register(ICompositionRoot compositionRoot) + { + foreach (var hubType in _assemblies.GetImplementingTypes()) + { + // register the singleton hub instance + compositionRoot.Register(new ServiceDescriptor(hubType, hubType, ServiceLifetime.Singleton)); + + // register a respective hub factory in the framework service collection + Type hubActivatorServiceType = typeof(IHubActivator<>).MakeGenericType(hubType); + Type hubActivatorImplementationType = typeof(BackendFxApplicationHubActivator<>).MakeGenericType(hubType); + object hubActivatorImplementation = Activator.CreateInstance(hubActivatorImplementationType, compositionRoot); + Debug.Assert(hubActivatorImplementation != null, nameof(hubActivatorImplementation) + " != null"); + _frameworkServices.AddSingleton(hubActivatorServiceType, hubActivatorImplementation); + } + } +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/SignalR/BackendFxApplicationHubActivator.cs b/src/environments/Backend.Fx.AspNetCore/SignalR/BackendFxApplicationHubActivator.cs new file mode 100644 index 00000000..aff21ca5 --- /dev/null +++ b/src/environments/Backend.Fx.AspNetCore/SignalR/BackendFxApplicationHubActivator.cs @@ -0,0 +1,38 @@ +using System; +using Backend.Fx.DependencyInjection; +using Backend.Fx.Logging; +using JetBrains.Annotations; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using ILogger = Microsoft.Extensions.Logging.ILogger; + +namespace Backend.Fx.AspNetCore.SignalR; + +[PublicAPI] +public class BackendFxApplicationHubActivator : IHubActivator where T : Hub +{ + private readonly ICompositionRoot _compositionRoot; + private readonly ILogger _logger = Log.Create>(); + + + public BackendFxApplicationHubActivator(ICompositionRoot compositionRoot) + { + _compositionRoot = compositionRoot; + } + + public T Create() + { + return _compositionRoot.ServiceProvider.GetRequiredService(); + } + + public void Release(T hub) + { + _logger.LogTrace("Releasing {HubTypeName}", hub.GetType().Name); + if (hub is IDisposable disposable) + { + _logger.LogDebug("Disposing {HubTypeName}", hub.GetType().Name); + disposable.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/SignalR/StartupEx.cs b/src/environments/Backend.Fx.AspNetCore/SignalR/StartupEx.cs new file mode 100644 index 00000000..af4a9518 --- /dev/null +++ b/src/environments/Backend.Fx.AspNetCore/SignalR/StartupEx.cs @@ -0,0 +1,13 @@ +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.AspNetCore.SignalR; + +[PublicAPI] +public static class StartupEx +{ + public static void AddBackendFxSignalRApplication(this IServiceCollection services, IBackendFxApplication application) + { + application.EnableFeature(new AspNetSignalRFeature(services)); + } +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Util/ConfigurationEx.cs b/src/environments/Backend.Fx.AspNetCore/Util/ConfigurationEx.cs new file mode 100644 index 00000000..7e757a29 --- /dev/null +++ b/src/environments/Backend.Fx.AspNetCore/Util/ConfigurationEx.cs @@ -0,0 +1,22 @@ +using System; +using JetBrains.Annotations; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace Backend.Fx.AspNetCore.Util; + +[PublicAPI] +[Obsolete("Use configuration.GetSection(nameof(MyOptions)).Bind(myOptionsInstance)")] +public static class ConfigurationEx +{ + public static TOptions Load(this IConfiguration configuration) where TOptions : class, new() + { + IConfigurationSection configurationSection = configuration.GetSection(typeof(TOptions).Name); + var configurationOptions = new NamedConfigureFromConfigurationOptions( + typeof(TOptions).Name, + configurationSection); + var options = new TOptions(); + configurationOptions.Action(options); + return options; + } +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Util/HttpRequestEx.cs b/src/environments/Backend.Fx.AspNetCore/Util/HttpRequestEx.cs new file mode 100644 index 00000000..66cbd295 --- /dev/null +++ b/src/environments/Backend.Fx.AspNetCore/Util/HttpRequestEx.cs @@ -0,0 +1,75 @@ +using JetBrains.Annotations; +using Microsoft.AspNetCore.Http; + +namespace Backend.Fx.AspNetCore.Util; + +[PublicAPI] +public static class HttpRequestEx +{ + /// + /// Is the request method considered as safe in sense of a RESTful API? + /// See https://restcookbook.com/HTTP%20Methods/idempotency/ + /// + /// + /// + public static bool IsRestfulSafe(this HttpRequest request) + { + return request.IsGet() || request.IsOptions() || request.IsHead(); + } + + /// + /// Is the request method considered as idempotent in sense of a RESTful API? + /// See https://restcookbook.com/HTTP%20Methods/idempotency/ + /// + /// + /// + public static bool IsRestfulIdempotent(this HttpRequest request) + { + return request.IsGet() || request.IsOptions() || request.IsHead() || request.IsDelete() || request.IsPut(); + } + + public static bool IsGet(this HttpRequest request) + { + return HttpMethods.IsGet(request.Method); + } + + public static bool IsConnect(this HttpRequest request) + { + return HttpMethods.IsConnect(request.Method); + } + + public static bool IsDelete(this HttpRequest request) + { + return HttpMethods.IsDelete(request.Method); + } + + public static bool IsHead(this HttpRequest request) + { + return HttpMethods.IsHead(request.Method); + } + + public static bool IsOptions(this HttpRequest request) + { + return HttpMethods.IsOptions(request.Method); + } + + public static bool IsPatch(this HttpRequest request) + { + return HttpMethods.IsPatch(request.Method); + } + + public static bool IsPost(this HttpRequest request) + { + return HttpMethods.IsPost(request.Method); + } + + public static bool IsPut(this HttpRequest request) + { + return HttpMethods.IsPut(request.Method); + } + + public static bool IsTrace(this HttpRequest request) + { + return HttpMethods.IsTrace(request.Method); + } +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Util/HttpResponseEx.cs b/src/environments/Backend.Fx.AspNetCore/Util/HttpResponseEx.cs new file mode 100644 index 00000000..5c3ae73f --- /dev/null +++ b/src/environments/Backend.Fx.AspNetCore/Util/HttpResponseEx.cs @@ -0,0 +1,29 @@ +using System.Text.Json; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Http; + +namespace Backend.Fx.AspNetCore.Util; + +[PublicAPI] +public static class HttpResponseEx +{ + public static async Task WriteJsonAsync(this HttpResponse response, object o, JsonSerializerOptions options = null, string contentType = null) + { + options ??= new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + AllowTrailingCommas = true, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + }; + + await response.WriteJsonAsync(JsonSerializer.Serialize(o, options), contentType); + } + + public static async Task WriteJsonAsync(this HttpResponse response, string json, string contentType = null) + { + response.ContentType = contentType ?? "application/json; charset=UTF-8"; + await response.WriteAsync(json); + await response.Body.FlushAsync(); + } +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Versioning/VersionHeaderMiddleware.cs b/src/environments/Backend.Fx.AspNetCore/Versioning/VersionHeaderMiddleware.cs index ea4908e8..84e9968a 100644 --- a/src/environments/Backend.Fx.AspNetCore/Versioning/VersionHeaderMiddleware.cs +++ b/src/environments/Backend.Fx.AspNetCore/Versioning/VersionHeaderMiddleware.cs @@ -5,38 +5,37 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; -namespace Backend.Fx.AspNetCore.Versioning +namespace Backend.Fx.AspNetCore.Versioning; + +[PublicAPI] +public class VersionHeaderMiddleware { - [UsedImplicitly] - public class VersionHeaderMiddleware - { - private readonly RequestDelegate _next; - private readonly string _assemblyName; - private readonly string _version; + private readonly RequestDelegate _next; + private readonly string _assemblyName; + private readonly string _version; - public VersionHeaderMiddleware(RequestDelegate next) + public VersionHeaderMiddleware(RequestDelegate next) + { + _next = next; + var entryAssembly = Assembly.GetEntryAssembly(); + if (entryAssembly == null) { - _next = next; - var entryAssembly = Assembly.GetEntryAssembly(); - if (entryAssembly == null) - { - throw new InvalidOperationException("Unable to determine the entry assembly. The Version Header Middleware cannot be used in this environment"); - } - - AssemblyName entryAssemblyName = entryAssembly.GetName(); - if (entryAssemblyName.Version == null) - { - throw new InvalidOperationException("Unable to determine the version of the entry assembly. The Version Header Middleware cannot be used in this environment"); - } - - _assemblyName = entryAssemblyName.Name; - _version = entryAssemblyName.Version.ToString(3); + throw new InvalidOperationException("Unable to determine the entry assembly. The Version Header Middleware cannot be used in this environment"); } - public async Task InvokeAsync(HttpContext context) + AssemblyName entryAssemblyName = entryAssembly.GetName(); + if (entryAssemblyName.Version == null) { - context.Response.Headers.Add(_assemblyName, new StringValues(_version)); - await _next.Invoke(context); + throw new InvalidOperationException("Unable to determine the version of the entry assembly. The Version Header Middleware cannot be used in this environment"); } + + _assemblyName = entryAssemblyName.Name; + _version = entryAssemblyName.Version.ToString(3); + } + + public async Task InvokeAsync(HttpContext context) + { + context.Response.Headers.Add(_assemblyName, new StringValues(_version)); + await _next.Invoke(context); } } \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCore6Persistence/AggregateMapping.cs b/src/implementations/Backend.Fx.EfCore6Persistence/AggregateMapping.cs deleted file mode 100644 index a7ce812b..00000000 --- a/src/implementations/Backend.Fx.EfCore6Persistence/AggregateMapping.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using Backend.Fx.BuildingBlocks; -using Microsoft.EntityFrameworkCore; - -namespace Backend.Fx.EfCorePersistence -{ - public abstract class AggregateMapping : IAggregateMapping where T : AggregateRoot - { - public abstract IEnumerable>> IncludeDefinitions { get; } - - public abstract void ApplyEfMapping(ModelBuilder modelBuilder); - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCore6Persistence/Backend.Fx.EfCore6Persistence.csproj b/src/implementations/Backend.Fx.EfCore6Persistence/Backend.Fx.EfCore6Persistence.csproj index f8dc3d71..32bada34 100644 --- a/src/implementations/Backend.Fx.EfCore6Persistence/Backend.Fx.EfCore6Persistence.csproj +++ b/src/implementations/Backend.Fx.EfCore6Persistence/Backend.Fx.EfCore6Persistence.csproj @@ -14,7 +14,7 @@ Marc Wittke anic GmbH All rights reserved. Distributed under the terms of the MIT License. - Persistence implementation for Backend.Fx using Entity Framework Core 6 + Persistence implementation for Backend.Fx using Entity Framework Core 2 False MIT https://github.com/marcwittke/Backend.Fx @@ -26,7 +26,7 @@ - + diff --git a/src/implementations/Backend.Fx.EfCore6Persistence/Backend.Fx.EfCore6Persistence.csproj.DotSettings b/src/implementations/Backend.Fx.EfCore6Persistence/Backend.Fx.EfCore6Persistence.csproj.DotSettings new file mode 100644 index 00000000..c2f4bbdb --- /dev/null +++ b/src/implementations/Backend.Fx.EfCore6Persistence/Backend.Fx.EfCore6Persistence.csproj.DotSettings @@ -0,0 +1,8 @@ + + Library + \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCore6Persistence/Bootstrapping/DbContextTransactionOperationDecorator.cs b/src/implementations/Backend.Fx.EfCore6Persistence/Bootstrapping/DbContextTransactionOperationDecorator.cs deleted file mode 100644 index 6dedd0be..00000000 --- a/src/implementations/Backend.Fx.EfCore6Persistence/Bootstrapping/DbContextTransactionOperationDecorator.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Data; -using System.Data.Common; -using Backend.Fx.Environment.Persistence; -using Backend.Fx.Patterns.DependencyInjection; -using Microsoft.EntityFrameworkCore; - -namespace Backend.Fx.EfCorePersistence.Bootstrapping -{ - public class DbContextTransactionOperationDecorator : DbTransactionOperationDecorator - { - private readonly DbContext _dbContext; - - public DbContextTransactionOperationDecorator(DbContext dbContext, IDbConnection dbConnection, IOperation operation) - : base(dbConnection, operation) - { - _dbContext = dbContext; - } - - public override void Begin() - { - base.Begin(); - _dbContext.Database.UseTransaction((DbTransaction) CurrentTransaction); - } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCore6Persistence/Bootstrapping/EfCorePersistenceModule.cs b/src/implementations/Backend.Fx.EfCore6Persistence/Bootstrapping/EfCorePersistenceModule.cs deleted file mode 100644 index c8db9817..00000000 --- a/src/implementations/Backend.Fx.EfCore6Persistence/Bootstrapping/EfCorePersistenceModule.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Data; -using System.Linq; -using System.Reflection; -using Backend.Fx.BuildingBlocks; -using Backend.Fx.Environment.Persistence; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.Patterns.EventAggregation.Domain; -using Backend.Fx.Patterns.IdGeneration; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace Backend.Fx.EfCorePersistence.Bootstrapping -{ - public class EfCorePersistenceModule : IModule - where TDbContext : DbContext - { - private readonly ILoggerFactory _loggerFactory; - private readonly Action, IDbConnection> _configure; - private readonly IDbConnectionFactory _dbConnectionFactory; - private readonly IEntityIdGenerator _entityIdGenerator; - private readonly Assembly[] _assemblies; - - public EfCorePersistenceModule(IDbConnectionFactory dbConnectionFactory, IEntityIdGenerator entityIdGenerator, - ILoggerFactory loggerFactory, Action, IDbConnection> configure, params Assembly[] assemblies) - { - _dbConnectionFactory = dbConnectionFactory; - _entityIdGenerator = entityIdGenerator; - _loggerFactory = loggerFactory; - _configure = configure; - _assemblies = assemblies; - } - - public void Register(ICompositionRoot compositionRoot) - { - // by letting the container create the connection we can be sure, that only one connection per scope is used, and disposing is done accordingly - compositionRoot.InfrastructureModule.RegisterScoped(() => _dbConnectionFactory.Create()); - - // singleton id generator - compositionRoot.InfrastructureModule.RegisterInstance(_entityIdGenerator); - - // EF core requires us to flush frequently, because of a missing identity map - compositionRoot.InfrastructureModule.RegisterScoped(); - - // EF Repositories - compositionRoot.InfrastructureModule.RegisterScoped(typeof(IRepository<>), typeof(EfRepository<>)); - - // IQueryable is supported, but should be use with caution, since it bypasses authorization - compositionRoot.InfrastructureModule.RegisterScoped(typeof(IQueryable<>), typeof(EntityQueryable<>)); - - // DbContext is injected into repositories - compositionRoot.InfrastructureModule.RegisterScoped(() => CreateDbContextOptions(compositionRoot.InstanceProvider.GetInstance())); - compositionRoot.InfrastructureModule.RegisterScoped(); - - // wrapping the operation: connection.open - transaction.begin - operation - (flush) - transaction.commit - connection.close - compositionRoot.InfrastructureModule.RegisterDecorator(); - compositionRoot.InfrastructureModule.RegisterDecorator(); - compositionRoot.InfrastructureModule.RegisterDecorator(); - - // ensure everything dirty is flushed to the db before handling domain events - compositionRoot.InfrastructureModule.RegisterDecorator(); - - compositionRoot.InfrastructureModule.RegisterScoped(typeof(IAggregateMapping<>), _assemblies); - } - - protected virtual DbContextOptions CreateDbContextOptions(IDbConnection connection) - { - var dbContextOptionsBuilder = new DbContextOptionsBuilder(); - _configure.Invoke(dbContextOptionsBuilder, connection); - return dbContextOptionsBuilder.UseLoggerFactory(_loggerFactory).Options; - } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCore6Persistence/Bootstrapping/IDbConnectionFactory.cs b/src/implementations/Backend.Fx.EfCore6Persistence/Bootstrapping/IDbConnectionFactory.cs deleted file mode 100644 index df953115..00000000 --- a/src/implementations/Backend.Fx.EfCore6Persistence/Bootstrapping/IDbConnectionFactory.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Data; - -namespace Backend.Fx.EfCorePersistence.Bootstrapping -{ - public interface IDbConnectionFactory - { - IDbConnection Create(); - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCore6Persistence/DbContextExtensions.cs b/src/implementations/Backend.Fx.EfCore6Persistence/DbContextExtensions.cs index 35668e25..46550132 100644 --- a/src/implementations/Backend.Fx.EfCore6Persistence/DbContextExtensions.cs +++ b/src/implementations/Backend.Fx.EfCore6Persistence/DbContextExtensions.cs @@ -1,87 +1,6 @@ -using System; -using System.Globalization; -using System.Linq; -using System.Reflection; -using Backend.Fx.BuildingBlocks; -using Backend.Fx.Extensions; -using Backend.Fx.Logging; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.ChangeTracking; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; +namespace Backend.Fx.EfCore6Persistence; -namespace Backend.Fx.EfCorePersistence +public static class DbContextExtensions { - public static class DbContextExtensions - { - private static readonly ILogger Logger = Log.Create(typeof(DbContextExtensions)); - - public static void DisableChangeTracking(this DbContext dbContext) - { - Logger.LogDebug("Disabling change tracking on {DbContextTypeName} instance", dbContext.GetType().Name); - dbContext.ChangeTracker.AutoDetectChangesEnabled = false; - dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - } - - public static void RegisterRowVersionProperty(this ModelBuilder modelBuilder) - { - modelBuilder.Model - .GetEntityTypes() - .Where(mt => typeof(Entity).GetTypeInfo().IsAssignableFrom(mt.ClrType.GetTypeInfo())) - .ForAll(mt => modelBuilder.Entity(mt.ClrType).Property("RowVersion").IsRowVersion()); - } - - public static void RegisterEntityIdAsNeverGenerated(this ModelBuilder modelBuilder) - { - modelBuilder.Model - .GetEntityTypes() - .Where(mt => typeof(Entity).GetTypeInfo().IsAssignableFrom(mt.ClrType.GetTypeInfo())) - .ForAll(mt => modelBuilder.Entity(mt.ClrType).Property(nameof(Entity.Id)).ValueGeneratedNever()); - } - - public static void ApplyAggregateMappings(this DbContext dbContext, ModelBuilder modelBuilder) - { - //CAVE: IAggregateMapping implementations must reside in the same assembly as the Applications DbContext-type - var aggregateDefinitionTypeInfos = dbContext - .GetType() - .GetTypeInfo() - .Assembly - .ExportedTypes - .Select(t => t.GetTypeInfo()) - .Where(t => t.IsClass && !t.IsAbstract && !t.IsGenericType && typeof(IAggregateMapping).GetTypeInfo().IsAssignableFrom(t)); - foreach (TypeInfo typeInfo in aggregateDefinitionTypeInfos) - { - var aggregateMapping = (IAggregateMapping) Activator.CreateInstance(typeInfo.AsType()); - aggregateMapping.ApplyEfMapping(modelBuilder); - } - } - - - - public static void TraceChangeTrackerState(this DbContext dbContext) - { - if (Logger.IsEnabled(LogLevel.Trace)) - try - { - var changeTrackerState = new - { - added = dbContext.ChangeTracker.Entries().Where(entry => entry.State == EntityState.Added).ToArray(), - modified = dbContext.ChangeTracker.Entries().Where(entry => entry.State == EntityState.Modified).ToArray(), - deleted = dbContext.ChangeTracker.Entries().Where(entry => entry.State == EntityState.Deleted).ToArray(), - unchanged = dbContext.ChangeTracker.Entries().Where(entry => entry.State == EntityState.Unchanged).ToArray() - }; - - Logger.LogTrace("Change tracker state: {@ChangeTrackerState}", changeTrackerState); - } - catch (Exception ex) - { - Logger.LogWarning(ex, "Change tracker state could not be dumped"); - } - } - - private static string GetPrimaryKeyValue(EntityEntry entry) - { - return (entry.Entity as Entity)?.Id.ToString(CultureInfo.InvariantCulture) ?? "?"; - } - } + } \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCore6Persistence/DbContextTransactionOperationDecorator.cs b/src/implementations/Backend.Fx.EfCore6Persistence/DbContextTransactionOperationDecorator.cs new file mode 100644 index 00000000..7370abae --- /dev/null +++ b/src/implementations/Backend.Fx.EfCore6Persistence/DbContextTransactionOperationDecorator.cs @@ -0,0 +1,43 @@ +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Util; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.EfCore6Persistence; + +public class DbContextTransactionOperationDecorator : IOperation +{ + private readonly DbContext _dbContext; + private readonly ICurrentTHolder _dbTransactionHolder; + private readonly IOperation _operation; + + public DbContextTransactionOperationDecorator( + DbContext dbContext, + ICurrentTHolder dbTransactionHolder, + IOperation operation) + { + _dbContext = dbContext; + _dbTransactionHolder = dbTransactionHolder; + _operation = operation; + } + + public async Task BeginAsync(IServiceScope scope, CancellationToken cancellationToken = default) + { + await _operation.BeginAsync(scope).ConfigureAwait(false); + await _dbContext.Database.UseTransactionAsync((DbTransaction)_dbTransactionHolder.Current).ConfigureAwait(false); + } + + public Task CompleteAsync(CancellationToken cancellationToken = default) + { + return _operation.CompleteAsync(cancellationToken); + } + + public Task CancelAsync(CancellationToken cancellationToken = default) + { + return _operation.CancelAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCore6Persistence/EfCorePersistenceModule.cs b/src/implementations/Backend.Fx.EfCore6Persistence/EfCorePersistenceModule.cs new file mode 100644 index 00000000..461a9fe5 --- /dev/null +++ b/src/implementations/Backend.Fx.EfCore6Persistence/EfCorePersistenceModule.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Reflection; +using Backend.Fx.DependencyInjection; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Features.Persistence; +using Backend.Fx.Features.Persistence.AdoNet; +using Backend.Fx.Util; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.EfCore6Persistence; + +[PublicAPI] +public class EfCorePersistenceModule : AdoNetPersistenceModule + where TDbContext : DbContext +{ + private readonly IDbContextOptionsFactory _dbContextOptionsFactory; + private readonly IEnumerable _assemblies; + + public EfCorePersistenceModule( + IDbConnectionFactory dbConnectionFactory, + IDbContextOptionsFactory dbContextOptionsFactory, + params Assembly[] assemblies) + : base(dbConnectionFactory) + { + _dbContextOptionsFactory = dbContextOptionsFactory; + _assemblies = assemblies; + } + + public override void Register(ICompositionRoot compositionRoot) + { + base.Register(compositionRoot); + + compositionRoot.Register( + ServiceDescriptor.Scoped(sp => + _dbContextOptionsFactory.GetDbContextOptions(sp.GetRequiredService()))); + + compositionRoot.Register( + ServiceDescriptor.Scoped()); + + compositionRoot.Register( + ServiceDescriptor.Scoped(typeof(IQueryable<>), typeof(EfCoreQueryable<>))); + + compositionRoot.RegisterDecorator( + ServiceDescriptor.Scoped()); + + compositionRoot.RegisterDecorator( + ServiceDescriptor.Scoped()); + + RegisterRepositories(compositionRoot, _assemblies); + } + + protected virtual void RegisterRepositories(ICompositionRoot compositionRoot, IEnumerable assemblies) + { + compositionRoot.Register( + ServiceDescriptor.Scoped(typeof(IRepository<,>), typeof(EfCoreRepository<,>))); + } +} + +/// +/// Use this module, when your application should support full transparent multi tenancy. The DbContext will add a +/// shadow "TenantId" property on each aggregate root and a respective global query filter to all DbContext instances. +/// The will be asked for the current tenant id. +/// +/// +public class EfCoreMultiTenancyPersistenceModule : EfCorePersistenceModule + where TDbContext : MultiTenancyDbContext +{ + public EfCoreMultiTenancyPersistenceModule( + IDbConnectionFactory dbConnectionFactory, + IDbContextOptionsFactory dbContextOptionsFactory, + params Assembly[] assemblies) + : base(dbConnectionFactory, dbContextOptionsFactory, assemblies) + { + } + + /// + /// there is no module to be added. This multi tenancy module is just to ensure the more specific DbContext type + /// when enabling the feature with multi tenancy + /// + public override IModule MultiTenancyModule => new NullModule(); + + private class NullModule : IModule + { + public void Register(ICompositionRoot compositionRoot) + { + } + } +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCore6Persistence/EfCoreQueryable.cs b/src/implementations/Backend.Fx.EfCore6Persistence/EfCoreQueryable.cs new file mode 100644 index 00000000..71234d1f --- /dev/null +++ b/src/implementations/Backend.Fx.EfCore6Persistence/EfCoreQueryable.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Backend.Fx.Domain; +using Backend.Fx.Features.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace Backend.Fx.EfCore6Persistence; + +public class EfCoreQueryable : IQueryable + where TAggregateRoot : class, IAggregateRoot +{ + private readonly IQueryable _dbSet; + + + public EfCoreQueryable(DbContext dbContext) + { + _dbSet = dbContext.Set(); + } + + public Type ElementType => typeof(TAggregateRoot); + + public Expression Expression => _dbSet.AsQueryable().Expression; + + IQueryProvider IQueryable.Provider => _dbSet.Provider; + + public IEnumerator GetEnumerator() + { + return _dbSet.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_dbSet).GetEnumerator(); + } +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCore6Persistence/EfCoreRepository.cs b/src/implementations/Backend.Fx.EfCore6Persistence/EfCoreRepository.cs new file mode 100644 index 00000000..0ecb7b09 --- /dev/null +++ b/src/implementations/Backend.Fx.EfCore6Persistence/EfCoreRepository.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Domain; +using Backend.Fx.Exceptions; +using Backend.Fx.Features.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace Backend.Fx.EfCore6Persistence; + +public class EfCoreRepository : IRepository + where TAggregateRoot : class, IAggregateRoot + where TId : IEquatable + +{ + private readonly DbContext _dbContext; + private readonly IQueryable _queryable; + + public EfCoreRepository(DbContext dbContext, IQueryable queryable) + { + _dbContext = dbContext; + _queryable = queryable; + } + + public async Task GetAsync(TId id, CancellationToken cancellationToken = default) + { + return (await _queryable + .FirstOrDefaultAsync(agg => Equals(agg.Id, id), cancellationToken: cancellationToken) + .ConfigureAwait(false)) + ?? throw new NotFoundException(id); + } + + public async Task FindAsync(TId id, CancellationToken cancellationToken = default) + { + return await _queryable + .FirstOrDefaultAsync(agg => Equals(agg.Id, id), cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + + public async Task GetAllAsync(CancellationToken cancellationToken = default) + { + return await _queryable + .ToArrayAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + + public async Task AnyAsync(CancellationToken cancellationToken = default) + { + return await _queryable + .AnyAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + + public Task ResolveAsync(IEnumerable ids, CancellationToken cancellationToken = default) + { + return RepositoryEx.ResolveAsync(this, ids, cancellationToken); + } + + public Task DeleteAsync(TAggregateRoot aggregateRoot, CancellationToken cancellationToken = default) + { + _dbContext.Set().Remove(aggregateRoot); + return Task.CompletedTask; + } + + public async Task AddAsync(TAggregateRoot aggregateRoot, CancellationToken cancellationToken = default) + { + await _dbContext.Set().AddAsync(aggregateRoot, cancellationToken).ConfigureAwait(false); + } + + public async Task AddRangeAsync( + TAggregateRoot[] aggregateRoots, + CancellationToken cancellationToken = default) + { + await _dbContext.Set().AddRangeAsync(aggregateRoots, cancellationToken).ConfigureAwait(false); + } +} diff --git a/src/implementations/Backend.Fx.EfCore6Persistence/EfFlush.cs b/src/implementations/Backend.Fx.EfCore6Persistence/EfFlush.cs index c9b25368..58c1aef3 100644 --- a/src/implementations/Backend.Fx.EfCore6Persistence/EfFlush.cs +++ b/src/implementations/Backend.Fx.EfCore6Persistence/EfFlush.cs @@ -1,87 +1,45 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using System.Security.Principal; -using Backend.Fx.BuildingBlocks; -using Backend.Fx.Environment.DateAndTime; -using Backend.Fx.Environment.Persistence; using Backend.Fx.Exceptions; -using Backend.Fx.Extensions; +using Backend.Fx.Features.Persistence; using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.ChangeTracking; -using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; -namespace Backend.Fx.EfCorePersistence +namespace Backend.Fx.EfCore6Persistence { public class EfFlush : ICanFlush { - private static readonly ILogger Logger = Log.Create(); + private readonly ICanFlush _canFlush; + private readonly ILogger _logger = Log.Create(); + public DbContext DbContext { get; } - public ICurrentTHolder IdentityHolder { get; } - public IClock Clock { get; } - public EfFlush(DbContext dbContext, ICurrentTHolder identityHolder, IClock clock) + public EfFlush(DbContext dbContext, ICanFlush canFlush) { + _canFlush = canFlush; DbContext = dbContext; - Logger.LogInformation("Disabling auto detect changes on this DbContext. Changes will be detected explicitly when flushing"); + _logger.LogInformation("Disabling change tracking on {DbContextTypeName} instance", dbContext.GetType().Name); DbContext.ChangeTracker.AutoDetectChangesEnabled = false; - IdentityHolder = identityHolder; - Clock = clock; } public void Flush() { DetectChanges(); - UpdateTrackingProperties(); - DbContext.TraceChangeTrackerState(); - CheckForMissingTenantIds(); SaveChanges(); + _canFlush.Flush(); } private void DetectChanges() { - using (Logger.LogDebugDuration("Detecting changes")) + using (_logger.LogDebugDuration("Detecting changes")) { DbContext.ChangeTracker.DetectChanges(); } } - private void UpdateTrackingProperties() - { - using (Logger.LogDebugDuration("Updating tracking properties of created and modified entities")) - { - UpdateTrackingProperties(IdentityHolder.Current.Name, Clock.UtcNow); - } - } - - private void CheckForMissingTenantIds() - { - using (Logger.LogDebugDuration("Checking for missing tenant ids")) - { - AggregateRoot[] aggregatesWithoutTenantId = DbContext - .ChangeTracker - .Entries() - .Where(e => e.State == EntityState.Added) - .Select(e => e.Entity) - .OfType() - .Where(ent => ent.TenantId == 0) - .ToArray(); - if (aggregatesWithoutTenantId.Length > 0) - { - throw new InvalidOperationException( - $"Attempt to save aggregate root entities without tenant id: {string.Join(",", aggregatesWithoutTenantId.Select(agg => agg.DebuggerDisplay))}"); - } - } - } private void SaveChanges() { - using (Logger.LogDebugDuration("Saving changes")) + using (_logger.LogDebugDuration("Saving changes")) { try { @@ -94,101 +52,5 @@ private void SaveChanges() } } - private void UpdateTrackingProperties(string identity, DateTime utcNow) - { - identity ??= "anonymous"; - var isTraceEnabled = Logger.IsEnabled(LogLevel.Trace); - var count = 0; - - // Modifying an entity (also removing an entity from an aggregate) should leave the aggregate root as modified - DbContext.ChangeTracker - .Entries() - .Where(entry => entry.State == EntityState.Added || entry.State == EntityState.Modified || entry.State == EntityState.Deleted) - .Where(entry => !(entry.Entity is AggregateRoot)) - .ToArray() - .ForAll(entry => - { - EntityEntry aggregateRootEntry = GetAggregateRootEntry(DbContext.ChangeTracker, entry); - if (aggregateRootEntry.State == EntityState.Unchanged) aggregateRootEntry.State = EntityState.Modified; - }); - - DbContext.ChangeTracker - .Entries() - .Where(entry => entry.State == EntityState.Added || entry.State == EntityState.Modified) - .ForAll(entry => - { - try - { - count++; - Entity entity = entry.Entity; - - if (entry.State == EntityState.Added) - { - if (isTraceEnabled) Logger.LogTrace("tracking that {EntityTypeName}[{Id}] was created by {Identity} at {UtcNow}", entity.GetType().Name, entity.Id, identity, utcNow); - entity.SetCreatedProperties(identity, utcNow); - } - else if (entry.State == EntityState.Modified) - { - if (isTraceEnabled) Logger.LogTrace("tracking that {EntityTypeName}[{Id}] was modified by {Identity} at {UtcNow}", entity.GetType().Name, entity.Id, identity, utcNow); - entity.SetModifiedProperties(identity, utcNow); - - // this line causes the recent changes of tracking properties to be detected before flushing - entry.State = EntityState.Modified; - } - } - catch (Exception ex) - { - Logger.LogWarning(ex, "Updating tracking properties failed"); - throw; - } - }); - if (count > 0) Logger.LogDebug("Tracked {EntityCount} entities as created/changed on {UtcNow} by {Identity}", count, utcNow, identity); - } - - /// - /// This method finds the EntityEntry<AggregateRoot> of an EntityEntry<Entity> - /// assuming it has been loaded and is being tracked by the change tracker. - /// - [return: NotNull] - private static EntityEntry GetAggregateRootEntry(ChangeTracker changeTracker, EntityEntry entry) - { - Logger.LogDebug("Searching aggregate root of {EntityTypeName}[{Id}]", entry.Entity.GetType().Name, (entry.Entity as Identified)?.Id); - foreach (NavigationEntry navigation in entry.Navigations) - { - TypeInfo navTargetTypeInfo = navigation.Metadata.TargetEntityType.ClrType.GetTypeInfo(); - int navigationTargetForeignKeyValue; - - if (navigation.CurrentValue == null) - { - var navigationMetadata = ((INavigation)navigation.Metadata); - // orphaned entity, original value contains the foreign key value - if (navigationMetadata.ForeignKey.Properties.Count > 1) throw new InvalidOperationException("Foreign Keys with multiple properties are not supported."); - - IProperty property = navigationMetadata.ForeignKey.Properties[0]; - navigationTargetForeignKeyValue = (int) entry.OriginalValues[property]; - } - else - { - // added or modified entity, current value contains the foreign key value - navigationTargetForeignKeyValue = ((Entity) navigation.CurrentValue).Id; - } - - // assumption: an entity cannot be loaded on its own. Everything on the navigation path starting from the - // aggregate root must have been loaded before, therefore we can find it using the change tracker - var navigationTargetEntry = changeTracker - .Entries() - .Single(ent => Equals(ent.Entity.GetType().GetTypeInfo(), navTargetTypeInfo) - && ent.Property(nameof(Entity.Id)).CurrentValue.Equals(navigationTargetForeignKeyValue)); - - // if the target is AggregateRoot, no (further) recursion is needed - if (typeof(AggregateRoot).GetTypeInfo().IsAssignableFrom(navTargetTypeInfo)) return navigationTargetEntry; - - // recurse in case of "Entity -> Entity -> AggregateRoot" - Logger.LogDebug("Recursing..."); - return GetAggregateRootEntry(changeTracker, navigationTargetEntry); - } - - throw new InvalidOperationException($"Could not find aggregate root of {entry.Entity.GetType().Name}[{(entry.Entity as Identified)?.Id}]"); - } } } \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCore6Persistence/EfRepository.cs b/src/implementations/Backend.Fx.EfCore6Persistence/EfRepository.cs deleted file mode 100644 index 03258217..00000000 --- a/src/implementations/Backend.Fx.EfCore6Persistence/EfRepository.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Backend.Fx.BuildingBlocks; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Exceptions; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.Authorization; -using Backend.Fx.Patterns.DependencyInjection; -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.EfCorePersistence -{ - public class EfRepository : Repository, IAsyncRepository where TAggregateRoot : AggregateRoot - { - private static readonly ILogger Logger = Log.Create>(); - private readonly IAggregateAuthorization _aggregateAuthorization; - private readonly IAggregateMapping _aggregateMapping; - private DbContext _dbContext; - - [SuppressMessage("ReSharper", "EF1001")] - public EfRepository([CanBeNull] DbContext dbContext, IAggregateMapping aggregateMapping, - ICurrentTHolder currentTenantIdHolder, IAggregateAuthorization aggregateAuthorization) - : base(currentTenantIdHolder, aggregateAuthorization) - { - _dbContext = dbContext; - _aggregateMapping = aggregateMapping; - _aggregateAuthorization = aggregateAuthorization; - - // somewhat a hack: using the internal EF Core services against advice - var localViewListener = dbContext?.GetService(); - localViewListener?.RegisterView(AuthorizeChanges); - } - - [SuppressMessage("ReSharper", "EF1001")] - public DbContext DbContext - { - get => _dbContext ?? throw new InvalidOperationException( - "This EfRepository does not have a DbContext yet. You might either make sure a proper DbContext gets injected or the DbContext is initialized later using a derived class") - ; - protected set - { - if (_dbContext != null) throw new InvalidOperationException("This EfRepository has already a DbContext assigned. It is not allowed to change it later."); - _dbContext = value; - var localViewListener = _dbContext?.GetService(); - localViewListener?.RegisterView(AuthorizeChanges); - } - } - - public async Task SingleAsync(int id, CancellationToken cancellationToken = default) - { - return await AggregateQueryable.SingleAsync(agg => agg.Id == id, cancellationToken); - } - - public async Task SingleOrDefaultAsync(int id, CancellationToken cancellationToken = default) - { - return await AggregateQueryable.SingleOrDefaultAsync(agg => agg.Id == id, cancellationToken); - } - - public async Task GetAllAsync(CancellationToken cancellationToken = default) - { - return await AggregateQueryable.ToArrayAsync(cancellationToken); - } - - public async Task AnyAsync(CancellationToken cancellationToken = default) - { - return await AggregateQueryable.AnyAsync(cancellationToken); - } - - public async Task ResolveAsync(IEnumerable ids, CancellationToken cancellationToken = default) - { - if (ids == null) - { - return Array.Empty(); - } - - int[] idsToResolve = ids as int[] ?? ids.ToArray(); - TAggregateRoot[] resolved = await AggregateQueryable.Where(agg => idsToResolve.Contains(agg.Id)).ToArrayAsync(cancellationToken); - if (resolved.Length != idsToResolve.Length) - { - throw new ArgumentException($"The following {AggregateTypeName} ids could not be resolved: {string.Join(", ", idsToResolve.Except(resolved.Select(agg => agg.Id)))}"); - } - return resolved; - } - - protected override IQueryable RawAggregateQueryable - { - get - { - IQueryable queryable = DbContext.Set(); - if (_aggregateMapping.IncludeDefinitions != null) - foreach (var include in _aggregateMapping.IncludeDefinitions) - queryable = queryable.Include(include); - return queryable; - } - } - - /// - /// Due to the fact, that a real lifecycle hook API is not yet available (see issue https://github.com/aspnet/EntityFrameworkCore/issues/626) - /// we are using an internal service to achieve the same goal: When a state change occurs from unchanged to modified, and this repository is - /// handling this type of aggregate, we're calling IAggregateAuthorization.CanModify to enforce user privilege checking. - /// We're accepting the possible instability of EF Core internals due to the fact that there is a full API feature in the pipeline that will - /// make this workaround obsolete. Also, not much of an effort was done to write this code, so if we have to deal with this issue in the future - /// again, we do not loose a lot. - /// - /// - /// - [SuppressMessage("ReSharper", "EF1001")] - private void AuthorizeChanges(InternalEntityEntry entry, EntityState previousState) - { - if (previousState == EntityState.Unchanged && entry.EntityState == EntityState.Modified && entry.EntityType.ClrType == typeof(TAggregateRoot)) - { - var aggregateRoot = (TAggregateRoot) entry.Entity; - if (!_aggregateAuthorization.CanModify(aggregateRoot)) throw new ForbiddenException("Unauthorized attempt to modify {AggregateTypeName}[{aggregateRoot.Id}]") - .AddError($"You are not allowed to modify {AggregateTypeName}[{aggregateRoot.Id}]"); - } - } - - protected override void AddPersistent(TAggregateRoot aggregateRoot) - { - Logger.LogDebug("Persistently adding new {AggregateTypeName}", AggregateTypeName); - DbContext.Set().Add(aggregateRoot); - } - - protected override void AddRangePersistent(TAggregateRoot[] aggregateRoots) - { - Logger.LogDebug("Persistently adding {Count} item(s) of type {AggregateTypeName}", aggregateRoots.Length, AggregateTypeName); - DbContext.Set().AddRange(aggregateRoots); - } - - protected override void DeletePersistent(TAggregateRoot aggregateRoot) - { - Logger.LogDebug("Persistently removing {AggregateTypeName}[{Id}]", AggregateTypeName, aggregateRoot.Id); - DbContext.Set().Remove(aggregateRoot); - } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCore6Persistence/EntityQueryable.cs b/src/implementations/Backend.Fx.EfCore6Persistence/EntityQueryable.cs deleted file mode 100644 index 48d4a959..00000000 --- a/src/implementations/Backend.Fx.EfCore6Persistence/EntityQueryable.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using Backend.Fx.BuildingBlocks; -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; - -namespace Backend.Fx.EfCorePersistence -{ - public class EntityQueryable : IQueryable where TEntity : Entity - { - [CanBeNull] private DbContext _dbContext; - - public EntityQueryable(DbContext dbContext) - { - _dbContext = dbContext; - } - - public DbContext DbContext - { - get => _dbContext ?? throw new InvalidOperationException( - "This EntityQueryable does not have a DbContext yet. You might either make sure a proper DbContext gets injected or the DbContext is initialized later using a derived class") - ; - protected set - { - if (_dbContext != null) throw new InvalidOperationException("This EntityQueryable has already a DbContext assigned. It is not allowed to change it later."); - _dbContext = value; - } - } - - private IQueryable InnerQueryable => DbContext.Set(); - - public IEnumerator GetEnumerator() - { - return InnerQueryable.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return ((IEnumerable) InnerQueryable).GetEnumerator(); - } - - public Type ElementType => InnerQueryable.ElementType; - - public Expression Expression => InnerQueryable.Expression; - - public IQueryProvider Provider => InnerQueryable.Provider; - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCore6Persistence/IAggregateMapping.cs b/src/implementations/Backend.Fx.EfCore6Persistence/IAggregateMapping.cs deleted file mode 100644 index 76dd9fd5..00000000 --- a/src/implementations/Backend.Fx.EfCore6Persistence/IAggregateMapping.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using Backend.Fx.BuildingBlocks; -using Microsoft.EntityFrameworkCore; - -namespace Backend.Fx.EfCorePersistence -{ - public interface IAggregateMapping - { - void ApplyEfMapping(ModelBuilder modelBuilder); - } - - public interface IAggregateMapping : IAggregateMapping where T : AggregateRoot - { - IEnumerable>> IncludeDefinitions { get; } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCore6Persistence/IDbContextOptionsFactory.cs b/src/implementations/Backend.Fx.EfCore6Persistence/IDbContextOptionsFactory.cs new file mode 100644 index 00000000..72857f35 --- /dev/null +++ b/src/implementations/Backend.Fx.EfCore6Persistence/IDbContextOptionsFactory.cs @@ -0,0 +1,9 @@ +using System.Data; +using Microsoft.EntityFrameworkCore; + +namespace Backend.Fx.EfCore6Persistence; + +public interface IDbContextOptionsFactory where TDbContext : DbContext +{ + DbContextOptions GetDbContextOptions(IDbConnection dbConnection); +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCore6Persistence/Mssql/MsSqlSequence.cs b/src/implementations/Backend.Fx.EfCore6Persistence/Mssql/MsSqlSequence.cs deleted file mode 100644 index 078d172d..00000000 --- a/src/implementations/Backend.Fx.EfCore6Persistence/Mssql/MsSqlSequence.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Data; -using Backend.Fx.EfCorePersistence.Bootstrapping; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.IdGeneration; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.EfCorePersistence.Mssql -{ - public abstract class MsSqlSequence : ISequence - { - private static readonly ILogger Logger = Log.Create(); - private readonly IDbConnectionFactory _dbConnectionFactory; - - protected MsSqlSequence(IDbConnectionFactory dbConnectionFactory) - { - _dbConnectionFactory = dbConnectionFactory; - } - - protected abstract string SequenceName { get; } - protected virtual string SchemaName { get; } = "dbo"; - - public void EnsureSequence() - { - Logger.LogInformation("Ensuring existence of mssql sequence {SchemaName}.{SequenceName}", SchemaName, SequenceName); - using (IDbConnection dbConnection = _dbConnectionFactory.Create()) - { - dbConnection.Open(); - bool sequenceExists; - using (IDbCommand cmd = dbConnection.CreateCommand()) - { - cmd.CommandText = $"SELECT count(*) FROM sys.sequences seq join sys.schemas s on s.schema_id = seq.schema_id WHERE seq.name = '{SequenceName}' and s.name = '{SchemaName}'"; - sequenceExists = (int) cmd.ExecuteScalar() == 1; - } - - if (sequenceExists) - { - Logger.LogInformation("Sequence {SchemaName}.{SequenceName} exists", SchemaName, SequenceName); - } - else - { - Logger.LogInformation("Sequence {SchemaName}.{SequenceName} does not exist yet and will be created now", SchemaName, SequenceName); - using (IDbCommand cmd = dbConnection.CreateCommand()) - { - cmd.CommandText = $"CREATE SEQUENCE [{SchemaName}].[{SequenceName}] START WITH 1 INCREMENT BY {Increment}"; - cmd.ExecuteNonQuery(); - Logger.LogInformation("Sequence {SchemaName}.{SequenceName} created", SchemaName, SequenceName); - } - } - } - } - - public int GetNextValue() - { - using (IDbConnection dbConnection = _dbConnectionFactory.Create()) - { - dbConnection.Open(); - int nextValue; - using (IDbCommand selectNextValCommand = dbConnection.CreateCommand()) - { - selectNextValCommand.CommandText = $"SELECT next value FOR {SchemaName}.{SequenceName}"; - nextValue = Convert.ToInt32(selectNextValCommand.ExecuteScalar()); - Logger.LogDebug("{SchemaName}.{SequenceName} served {NextValue} as next value", SchemaName, SequenceName, nextValue); - } - - return nextValue; - } - } - - public abstract int Increment { get; } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCore6Persistence/MultiTenancyDbContext.cs b/src/implementations/Backend.Fx.EfCore6Persistence/MultiTenancyDbContext.cs new file mode 100644 index 00000000..b2dc5f7e --- /dev/null +++ b/src/implementations/Backend.Fx.EfCore6Persistence/MultiTenancyDbContext.cs @@ -0,0 +1,109 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Domain; +using Backend.Fx.Features.MultiTenancy; +using Backend.Fx.Util; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace Backend.Fx.EfCore6Persistence; + +public abstract class MultiTenancyDbContext : DbContext +{ + private readonly ICurrentTHolder _tenantIdHolder; + + private int? TenantIdForGlobalQueryFilter => + _tenantIdHolder.Current.HasValue ? _tenantIdHolder.Current.Value : null; + + protected MultiTenancyDbContext(ICurrentTHolder tenantIdHolder, DbContextOptions options) + : base(options) + { + _tenantIdHolder = tenantIdHolder; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + IMutableEntityType[] aggregateRootEntityTypes = modelBuilder + .Model + .GetEntityTypes() + .Where(met => + met.ClrType.IsClass + && !met.ClrType.IsAbstract + && typeof(IAggregateRoot).IsAssignableFrom(met.ClrType)) + .ToArray(); + + foreach (IMutableEntityType entityType in aggregateRootEntityTypes) + { + // add a shadow property for the tenant id + modelBuilder.Entity(entityType.ClrType).Property("TenantId"); + + // add a global query filter expression that is equivalent to: + // e => e.TenantId == this.TenantIdForGlobalQueryFilter + ParameterExpression parameter = Expression.Parameter(entityType.ClrType, "e"); + BinaryExpression body = Expression.Equal( + Expression.Call( + typeof(EF), + nameof(EF.Property), + new[] { typeof(int?) }, + parameter, + Expression.Constant("TenantId")), + Expression.Property( + Expression.Constant(this), + typeof(MultiTenancyDbContext).GetProperty( + nameof(TenantIdForGlobalQueryFilter), + BindingFlags.Instance | BindingFlags.NonPublic)!)); + + LambdaExpression queryFilterExpression = Expression.Lambda(body, parameter); + + modelBuilder.Entity(entityType.ClrType).HasQueryFilter(queryFilterExpression); + } + } + + public override int SaveChanges() + { + ApplyTenantIdToAddedEntities(); + return base.SaveChanges(); + } + + public override int SaveChanges(bool acceptAllChangesOnSuccess) + { + ApplyTenantIdToAddedEntities(); + return base.SaveChanges(acceptAllChangesOnSuccess); + } + + public override Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + ApplyTenantIdToAddedEntities(); + return base.SaveChangesAsync(cancellationToken); + } + + public override Task SaveChangesAsync( + bool acceptAllChangesOnSuccess, + CancellationToken cancellationToken = default) + { + ApplyTenantIdToAddedEntities(); + return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); + } + + private void ApplyTenantIdToAddedEntities() + { + if (!_tenantIdHolder.Current.HasValue) + { + throw new InvalidOperationException( + "Attempt to call SaveChanges() with added entities but with no tenant id available"); + } + + ChangeTracker + .Entries() + .Where(entry => entry.Entity is IAggregateRoot) + .Where(entry => entry.State == EntityState.Added) + .AsParallel() + .ForAll(entry => entry.Property("TenantId").CurrentValue = _tenantIdHolder.Current.Value); + } +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCore6Persistence/Oracle/OracleSequence.cs b/src/implementations/Backend.Fx.EfCore6Persistence/Oracle/OracleSequence.cs deleted file mode 100644 index 7aa7e248..00000000 --- a/src/implementations/Backend.Fx.EfCore6Persistence/Oracle/OracleSequence.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Data; -using Backend.Fx.EfCorePersistence.Bootstrapping; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.IdGeneration; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.EfCorePersistence.Oracle -{ - public abstract class OracleSequence : ISequence - { - private static readonly ILogger Logger = Log.Create(); - private readonly IDbConnectionFactory _dbConnectionFactory; - - protected OracleSequence(IDbConnectionFactory dbConnectionFactory) - { - _dbConnectionFactory = dbConnectionFactory; - } - - protected abstract string SequenceName { get; } - protected abstract string SchemaName { get; } - - private string SchemaPrefix - { - get - { - if (string.IsNullOrEmpty(SchemaName)) return string.Empty; - - return SchemaName + "."; - } - } - - public void EnsureSequence() - { - Logger.LogInformation("Ensuring existence of oracle sequence {SchemaPrefix}.{SequenceName}", SchemaPrefix, SequenceName); - - using (IDbConnection dbConnection = _dbConnectionFactory.Create()) - { - dbConnection.Open(); - bool sequenceExists; - using (IDbCommand command = dbConnection.CreateCommand()) - { - command.CommandText = $"SELECT count(*) FROM user_sequences WHERE sequence_name = '{SequenceName}'"; - sequenceExists = (decimal)command.ExecuteScalar() == 1; - } - - if (sequenceExists) - { - Logger.LogInformation("Oracle sequence {SchemaPrefix}.{SequenceName} exists", SchemaPrefix, SequenceName); - } - else - { - Logger.LogInformation("Oracle sequence {SchemaPrefix}.{SequenceName} does not exist yet and will be created now", - SchemaPrefix, - SequenceName); - using (IDbCommand cmd = dbConnection.CreateCommand()) - { - cmd.CommandText = $"CREATE SEQUENCE {SchemaPrefix}{SequenceName} START WITH 1 INCREMENT BY {Increment}"; - cmd.ExecuteNonQuery(); - Logger.LogInformation("Oracle sequence {SchemaPrefix}.{SequenceName} created", SchemaPrefix, SequenceName); - } - } - } - } - - public int GetNextValue() - { - using (IDbConnection dbConnection = _dbConnectionFactory.Create()) - { - dbConnection.Open(); - - int nextValue; - using (IDbCommand command = dbConnection.CreateCommand()) - { - command.CommandText = $"SELECT {SchemaPrefix}{SequenceName}.NEXTVAL FROM dual"; - nextValue = Convert.ToInt32(command.ExecuteScalar()); - Logger.LogDebug("Oracle sequence {SchemaPrefix}.{SequenceName} served {NextValue} as next value", - SchemaPrefix, - SequenceName, - nextValue); - } - - return nextValue; - } - } - - public abstract int Increment { get; } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCore6Persistence/PlainAggregateMapping.cs b/src/implementations/Backend.Fx.EfCore6Persistence/PlainAggregateMapping.cs deleted file mode 100644 index 2bf4efec..00000000 --- a/src/implementations/Backend.Fx.EfCore6Persistence/PlainAggregateMapping.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using Backend.Fx.BuildingBlocks; -using Microsoft.EntityFrameworkCore; - -namespace Backend.Fx.EfCorePersistence -{ - public class PlainAggregateMapping : AggregateMapping - where TAggregateRoot : AggregateRoot - { - public override IEnumerable>> IncludeDefinitions => new Expression>[0]; - - public override void ApplyEfMapping(ModelBuilder modelBuilder) - { - } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCore6Persistence/Postgres/PostgresSequence.cs b/src/implementations/Backend.Fx.EfCore6Persistence/Postgres/PostgresSequence.cs deleted file mode 100644 index 83e54535..00000000 --- a/src/implementations/Backend.Fx.EfCore6Persistence/Postgres/PostgresSequence.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Data; -using Backend.Fx.EfCorePersistence.Bootstrapping; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.IdGeneration; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.EfCorePersistence.Postgres -{ - public abstract class PostgresSequence : ISequence - { - private static readonly ILogger Logger = Log.Create(); - private readonly IDbConnectionFactory _dbConnectionFactory; - - protected PostgresSequence(IDbConnectionFactory dbConnectionFactory) - { - _dbConnectionFactory = dbConnectionFactory; - } - - protected abstract string SequenceName { get; } - protected abstract string SchemaName { get; } - - public void EnsureSequence() - { - Logger.LogInformation("Ensuring existence of postgres sequence {SchemaName}.{SequenceName}", SchemaName, SequenceName); - - using (IDbConnection dbConnection = _dbConnectionFactory.Create()) - { - dbConnection.Open(); - bool sequenceExists; - using (IDbCommand command = dbConnection.CreateCommand()) - { - command.CommandText = $"SELECT count(*) FROM information_schema.sequences WHERE sequence_name = '{SequenceName}' AND sequence_schema = '{SchemaName}'"; - sequenceExists = (long) command.ExecuteScalar() == 1L; - } - - if (sequenceExists) - { - Logger.LogInformation("Sequence {SchemaName}.{SequenceName} exists", SchemaName, SequenceName); - } - else - { - Logger.LogInformation( - "Sequence {SchemaName}.{SequenceName} does not exist yet and will be created now", - SchemaName, - SequenceName); - using (IDbCommand cmd = dbConnection.CreateCommand()) - { - cmd.CommandText = $"CREATE SEQUENCE {SchemaName}.{SequenceName} START WITH 1 INCREMENT BY {Increment}"; - cmd.ExecuteNonQuery(); - Logger.LogInformation("Sequence {SchemaName}.{SequenceName} created", SchemaName, SequenceName); - } - } - } - } - - public int GetNextValue() - { - using (IDbConnection dbConnection = _dbConnectionFactory.Create()) - { - dbConnection.Open(); - - int nextValue; - using (IDbCommand command = dbConnection.CreateCommand()) - { - command.CommandText = $"SELECT nextval('{SchemaName}.{SequenceName}');"; - nextValue = Convert.ToInt32(command.ExecuteScalar()); - Logger.LogDebug("{SchemaName}.{SequenceName} served {2} as next value", SchemaName, SequenceName, nextValue); - } - - return nextValue; - } - } - - public abstract int Increment { get; } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCore6Persistence/Properties/AssemblyInfo.cs b/src/implementations/Backend.Fx.EfCore6Persistence/Properties/AssemblyInfo.cs deleted file mode 100644 index 5f565d7d..00000000 --- a/src/implementations/Backend.Fx.EfCore6Persistence/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System.Reflection; - -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyVersion("0.0.0.0")] -[assembly: AssemblyFileVersion("0.0.0.0")] -[assembly: AssemblyInformationalVersion("0.0.0.0")] \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/AggregateMapping.cs b/src/implementations/Backend.Fx.EfCorePersistence/AggregateMapping.cs deleted file mode 100644 index a7ce812b..00000000 --- a/src/implementations/Backend.Fx.EfCorePersistence/AggregateMapping.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using Backend.Fx.BuildingBlocks; -using Microsoft.EntityFrameworkCore; - -namespace Backend.Fx.EfCorePersistence -{ - public abstract class AggregateMapping : IAggregateMapping where T : AggregateRoot - { - public abstract IEnumerable>> IncludeDefinitions { get; } - - public abstract void ApplyEfMapping(ModelBuilder modelBuilder); - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/Backend.Fx.EfCorePersistence.csproj b/src/implementations/Backend.Fx.EfCorePersistence/Backend.Fx.EfCorePersistence.csproj deleted file mode 100644 index 4e63596d..00000000 --- a/src/implementations/Backend.Fx.EfCorePersistence/Backend.Fx.EfCorePersistence.csproj +++ /dev/null @@ -1,36 +0,0 @@ - - - - netstandard2.1 - true - snupkg - false - false - false - false - - - - Marc Wittke - anic GmbH - All rights reserved. Distributed under the terms of the MIT License. - Persistence implementation for Backend.Fx using Entity Framework Core 2 - False - MIT - https://github.com/marcwittke/Backend.Fx - Backend.Fx - Git - https://github.com/marcwittke/Backend.Fx.git - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/Bootstrapping/DbContextTransactionOperationDecorator.cs b/src/implementations/Backend.Fx.EfCorePersistence/Bootstrapping/DbContextTransactionOperationDecorator.cs deleted file mode 100644 index 6dedd0be..00000000 --- a/src/implementations/Backend.Fx.EfCorePersistence/Bootstrapping/DbContextTransactionOperationDecorator.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Data; -using System.Data.Common; -using Backend.Fx.Environment.Persistence; -using Backend.Fx.Patterns.DependencyInjection; -using Microsoft.EntityFrameworkCore; - -namespace Backend.Fx.EfCorePersistence.Bootstrapping -{ - public class DbContextTransactionOperationDecorator : DbTransactionOperationDecorator - { - private readonly DbContext _dbContext; - - public DbContextTransactionOperationDecorator(DbContext dbContext, IDbConnection dbConnection, IOperation operation) - : base(dbConnection, operation) - { - _dbContext = dbContext; - } - - public override void Begin() - { - base.Begin(); - _dbContext.Database.UseTransaction((DbTransaction) CurrentTransaction); - } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/Bootstrapping/EfCorePersistenceModule.cs b/src/implementations/Backend.Fx.EfCorePersistence/Bootstrapping/EfCorePersistenceModule.cs deleted file mode 100644 index c8db9817..00000000 --- a/src/implementations/Backend.Fx.EfCorePersistence/Bootstrapping/EfCorePersistenceModule.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Data; -using System.Linq; -using System.Reflection; -using Backend.Fx.BuildingBlocks; -using Backend.Fx.Environment.Persistence; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.Patterns.EventAggregation.Domain; -using Backend.Fx.Patterns.IdGeneration; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace Backend.Fx.EfCorePersistence.Bootstrapping -{ - public class EfCorePersistenceModule : IModule - where TDbContext : DbContext - { - private readonly ILoggerFactory _loggerFactory; - private readonly Action, IDbConnection> _configure; - private readonly IDbConnectionFactory _dbConnectionFactory; - private readonly IEntityIdGenerator _entityIdGenerator; - private readonly Assembly[] _assemblies; - - public EfCorePersistenceModule(IDbConnectionFactory dbConnectionFactory, IEntityIdGenerator entityIdGenerator, - ILoggerFactory loggerFactory, Action, IDbConnection> configure, params Assembly[] assemblies) - { - _dbConnectionFactory = dbConnectionFactory; - _entityIdGenerator = entityIdGenerator; - _loggerFactory = loggerFactory; - _configure = configure; - _assemblies = assemblies; - } - - public void Register(ICompositionRoot compositionRoot) - { - // by letting the container create the connection we can be sure, that only one connection per scope is used, and disposing is done accordingly - compositionRoot.InfrastructureModule.RegisterScoped(() => _dbConnectionFactory.Create()); - - // singleton id generator - compositionRoot.InfrastructureModule.RegisterInstance(_entityIdGenerator); - - // EF core requires us to flush frequently, because of a missing identity map - compositionRoot.InfrastructureModule.RegisterScoped(); - - // EF Repositories - compositionRoot.InfrastructureModule.RegisterScoped(typeof(IRepository<>), typeof(EfRepository<>)); - - // IQueryable is supported, but should be use with caution, since it bypasses authorization - compositionRoot.InfrastructureModule.RegisterScoped(typeof(IQueryable<>), typeof(EntityQueryable<>)); - - // DbContext is injected into repositories - compositionRoot.InfrastructureModule.RegisterScoped(() => CreateDbContextOptions(compositionRoot.InstanceProvider.GetInstance())); - compositionRoot.InfrastructureModule.RegisterScoped(); - - // wrapping the operation: connection.open - transaction.begin - operation - (flush) - transaction.commit - connection.close - compositionRoot.InfrastructureModule.RegisterDecorator(); - compositionRoot.InfrastructureModule.RegisterDecorator(); - compositionRoot.InfrastructureModule.RegisterDecorator(); - - // ensure everything dirty is flushed to the db before handling domain events - compositionRoot.InfrastructureModule.RegisterDecorator(); - - compositionRoot.InfrastructureModule.RegisterScoped(typeof(IAggregateMapping<>), _assemblies); - } - - protected virtual DbContextOptions CreateDbContextOptions(IDbConnection connection) - { - var dbContextOptionsBuilder = new DbContextOptionsBuilder(); - _configure.Invoke(dbContextOptionsBuilder, connection); - return dbContextOptionsBuilder.UseLoggerFactory(_loggerFactory).Options; - } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/DbContextExtensions.cs b/src/implementations/Backend.Fx.EfCorePersistence/DbContextExtensions.cs deleted file mode 100644 index 35668e25..00000000 --- a/src/implementations/Backend.Fx.EfCorePersistence/DbContextExtensions.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Globalization; -using System.Linq; -using System.Reflection; -using Backend.Fx.BuildingBlocks; -using Backend.Fx.Extensions; -using Backend.Fx.Logging; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.ChangeTracking; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.EfCorePersistence -{ - public static class DbContextExtensions - { - private static readonly ILogger Logger = Log.Create(typeof(DbContextExtensions)); - - public static void DisableChangeTracking(this DbContext dbContext) - { - Logger.LogDebug("Disabling change tracking on {DbContextTypeName} instance", dbContext.GetType().Name); - dbContext.ChangeTracker.AutoDetectChangesEnabled = false; - dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - } - - public static void RegisterRowVersionProperty(this ModelBuilder modelBuilder) - { - modelBuilder.Model - .GetEntityTypes() - .Where(mt => typeof(Entity).GetTypeInfo().IsAssignableFrom(mt.ClrType.GetTypeInfo())) - .ForAll(mt => modelBuilder.Entity(mt.ClrType).Property("RowVersion").IsRowVersion()); - } - - public static void RegisterEntityIdAsNeverGenerated(this ModelBuilder modelBuilder) - { - modelBuilder.Model - .GetEntityTypes() - .Where(mt => typeof(Entity).GetTypeInfo().IsAssignableFrom(mt.ClrType.GetTypeInfo())) - .ForAll(mt => modelBuilder.Entity(mt.ClrType).Property(nameof(Entity.Id)).ValueGeneratedNever()); - } - - public static void ApplyAggregateMappings(this DbContext dbContext, ModelBuilder modelBuilder) - { - //CAVE: IAggregateMapping implementations must reside in the same assembly as the Applications DbContext-type - var aggregateDefinitionTypeInfos = dbContext - .GetType() - .GetTypeInfo() - .Assembly - .ExportedTypes - .Select(t => t.GetTypeInfo()) - .Where(t => t.IsClass && !t.IsAbstract && !t.IsGenericType && typeof(IAggregateMapping).GetTypeInfo().IsAssignableFrom(t)); - foreach (TypeInfo typeInfo in aggregateDefinitionTypeInfos) - { - var aggregateMapping = (IAggregateMapping) Activator.CreateInstance(typeInfo.AsType()); - aggregateMapping.ApplyEfMapping(modelBuilder); - } - } - - - - public static void TraceChangeTrackerState(this DbContext dbContext) - { - if (Logger.IsEnabled(LogLevel.Trace)) - try - { - var changeTrackerState = new - { - added = dbContext.ChangeTracker.Entries().Where(entry => entry.State == EntityState.Added).ToArray(), - modified = dbContext.ChangeTracker.Entries().Where(entry => entry.State == EntityState.Modified).ToArray(), - deleted = dbContext.ChangeTracker.Entries().Where(entry => entry.State == EntityState.Deleted).ToArray(), - unchanged = dbContext.ChangeTracker.Entries().Where(entry => entry.State == EntityState.Unchanged).ToArray() - }; - - Logger.LogTrace("Change tracker state: {@ChangeTrackerState}", changeTrackerState); - } - catch (Exception ex) - { - Logger.LogWarning(ex, "Change tracker state could not be dumped"); - } - } - - private static string GetPrimaryKeyValue(EntityEntry entry) - { - return (entry.Entity as Entity)?.Id.ToString(CultureInfo.InvariantCulture) ?? "?"; - } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/EfFlush.cs b/src/implementations/Backend.Fx.EfCorePersistence/EfFlush.cs deleted file mode 100644 index c9b25368..00000000 --- a/src/implementations/Backend.Fx.EfCorePersistence/EfFlush.cs +++ /dev/null @@ -1,194 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using System.Security.Principal; -using Backend.Fx.BuildingBlocks; -using Backend.Fx.Environment.DateAndTime; -using Backend.Fx.Environment.Persistence; -using Backend.Fx.Exceptions; -using Backend.Fx.Extensions; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.ChangeTracking; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.EfCorePersistence -{ - public class EfFlush : ICanFlush - { - private static readonly ILogger Logger = Log.Create(); - public DbContext DbContext { get; } - public ICurrentTHolder IdentityHolder { get; } - public IClock Clock { get; } - - public EfFlush(DbContext dbContext, ICurrentTHolder identityHolder, IClock clock) - { - DbContext = dbContext; - Logger.LogInformation("Disabling auto detect changes on this DbContext. Changes will be detected explicitly when flushing"); - DbContext.ChangeTracker.AutoDetectChangesEnabled = false; - IdentityHolder = identityHolder; - Clock = clock; - } - - public void Flush() - { - DetectChanges(); - UpdateTrackingProperties(); - DbContext.TraceChangeTrackerState(); - CheckForMissingTenantIds(); - SaveChanges(); - } - - private void DetectChanges() - { - using (Logger.LogDebugDuration("Detecting changes")) - { - DbContext.ChangeTracker.DetectChanges(); - } - } - - private void UpdateTrackingProperties() - { - using (Logger.LogDebugDuration("Updating tracking properties of created and modified entities")) - { - UpdateTrackingProperties(IdentityHolder.Current.Name, Clock.UtcNow); - } - } - - private void CheckForMissingTenantIds() - { - using (Logger.LogDebugDuration("Checking for missing tenant ids")) - { - AggregateRoot[] aggregatesWithoutTenantId = DbContext - .ChangeTracker - .Entries() - .Where(e => e.State == EntityState.Added) - .Select(e => e.Entity) - .OfType() - .Where(ent => ent.TenantId == 0) - .ToArray(); - if (aggregatesWithoutTenantId.Length > 0) - { - throw new InvalidOperationException( - $"Attempt to save aggregate root entities without tenant id: {string.Join(",", aggregatesWithoutTenantId.Select(agg => agg.DebuggerDisplay))}"); - } - } - } - - private void SaveChanges() - { - using (Logger.LogDebugDuration("Saving changes")) - { - try - { - DbContext.SaveChanges(); - } - catch (DbUpdateConcurrencyException concurrencyException) - { - throw new ConflictedException("Saving changes failed due to optimistic concurrency violation", concurrencyException); - } - } - } - - private void UpdateTrackingProperties(string identity, DateTime utcNow) - { - identity ??= "anonymous"; - var isTraceEnabled = Logger.IsEnabled(LogLevel.Trace); - var count = 0; - - // Modifying an entity (also removing an entity from an aggregate) should leave the aggregate root as modified - DbContext.ChangeTracker - .Entries() - .Where(entry => entry.State == EntityState.Added || entry.State == EntityState.Modified || entry.State == EntityState.Deleted) - .Where(entry => !(entry.Entity is AggregateRoot)) - .ToArray() - .ForAll(entry => - { - EntityEntry aggregateRootEntry = GetAggregateRootEntry(DbContext.ChangeTracker, entry); - if (aggregateRootEntry.State == EntityState.Unchanged) aggregateRootEntry.State = EntityState.Modified; - }); - - DbContext.ChangeTracker - .Entries() - .Where(entry => entry.State == EntityState.Added || entry.State == EntityState.Modified) - .ForAll(entry => - { - try - { - count++; - Entity entity = entry.Entity; - - if (entry.State == EntityState.Added) - { - if (isTraceEnabled) Logger.LogTrace("tracking that {EntityTypeName}[{Id}] was created by {Identity} at {UtcNow}", entity.GetType().Name, entity.Id, identity, utcNow); - entity.SetCreatedProperties(identity, utcNow); - } - else if (entry.State == EntityState.Modified) - { - if (isTraceEnabled) Logger.LogTrace("tracking that {EntityTypeName}[{Id}] was modified by {Identity} at {UtcNow}", entity.GetType().Name, entity.Id, identity, utcNow); - entity.SetModifiedProperties(identity, utcNow); - - // this line causes the recent changes of tracking properties to be detected before flushing - entry.State = EntityState.Modified; - } - } - catch (Exception ex) - { - Logger.LogWarning(ex, "Updating tracking properties failed"); - throw; - } - }); - if (count > 0) Logger.LogDebug("Tracked {EntityCount} entities as created/changed on {UtcNow} by {Identity}", count, utcNow, identity); - } - - /// - /// This method finds the EntityEntry<AggregateRoot> of an EntityEntry<Entity> - /// assuming it has been loaded and is being tracked by the change tracker. - /// - [return: NotNull] - private static EntityEntry GetAggregateRootEntry(ChangeTracker changeTracker, EntityEntry entry) - { - Logger.LogDebug("Searching aggregate root of {EntityTypeName}[{Id}]", entry.Entity.GetType().Name, (entry.Entity as Identified)?.Id); - foreach (NavigationEntry navigation in entry.Navigations) - { - TypeInfo navTargetTypeInfo = navigation.Metadata.TargetEntityType.ClrType.GetTypeInfo(); - int navigationTargetForeignKeyValue; - - if (navigation.CurrentValue == null) - { - var navigationMetadata = ((INavigation)navigation.Metadata); - // orphaned entity, original value contains the foreign key value - if (navigationMetadata.ForeignKey.Properties.Count > 1) throw new InvalidOperationException("Foreign Keys with multiple properties are not supported."); - - IProperty property = navigationMetadata.ForeignKey.Properties[0]; - navigationTargetForeignKeyValue = (int) entry.OriginalValues[property]; - } - else - { - // added or modified entity, current value contains the foreign key value - navigationTargetForeignKeyValue = ((Entity) navigation.CurrentValue).Id; - } - - // assumption: an entity cannot be loaded on its own. Everything on the navigation path starting from the - // aggregate root must have been loaded before, therefore we can find it using the change tracker - var navigationTargetEntry = changeTracker - .Entries() - .Single(ent => Equals(ent.Entity.GetType().GetTypeInfo(), navTargetTypeInfo) - && ent.Property(nameof(Entity.Id)).CurrentValue.Equals(navigationTargetForeignKeyValue)); - - // if the target is AggregateRoot, no (further) recursion is needed - if (typeof(AggregateRoot).GetTypeInfo().IsAssignableFrom(navTargetTypeInfo)) return navigationTargetEntry; - - // recurse in case of "Entity -> Entity -> AggregateRoot" - Logger.LogDebug("Recursing..."); - return GetAggregateRootEntry(changeTracker, navigationTargetEntry); - } - - throw new InvalidOperationException($"Could not find aggregate root of {entry.Entity.GetType().Name}[{(entry.Entity as Identified)?.Id}]"); - } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/EfRepository.cs b/src/implementations/Backend.Fx.EfCorePersistence/EfRepository.cs deleted file mode 100644 index 03258217..00000000 --- a/src/implementations/Backend.Fx.EfCorePersistence/EfRepository.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Backend.Fx.BuildingBlocks; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Exceptions; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.Authorization; -using Backend.Fx.Patterns.DependencyInjection; -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.EfCorePersistence -{ - public class EfRepository : Repository, IAsyncRepository where TAggregateRoot : AggregateRoot - { - private static readonly ILogger Logger = Log.Create>(); - private readonly IAggregateAuthorization _aggregateAuthorization; - private readonly IAggregateMapping _aggregateMapping; - private DbContext _dbContext; - - [SuppressMessage("ReSharper", "EF1001")] - public EfRepository([CanBeNull] DbContext dbContext, IAggregateMapping aggregateMapping, - ICurrentTHolder currentTenantIdHolder, IAggregateAuthorization aggregateAuthorization) - : base(currentTenantIdHolder, aggregateAuthorization) - { - _dbContext = dbContext; - _aggregateMapping = aggregateMapping; - _aggregateAuthorization = aggregateAuthorization; - - // somewhat a hack: using the internal EF Core services against advice - var localViewListener = dbContext?.GetService(); - localViewListener?.RegisterView(AuthorizeChanges); - } - - [SuppressMessage("ReSharper", "EF1001")] - public DbContext DbContext - { - get => _dbContext ?? throw new InvalidOperationException( - "This EfRepository does not have a DbContext yet. You might either make sure a proper DbContext gets injected or the DbContext is initialized later using a derived class") - ; - protected set - { - if (_dbContext != null) throw new InvalidOperationException("This EfRepository has already a DbContext assigned. It is not allowed to change it later."); - _dbContext = value; - var localViewListener = _dbContext?.GetService(); - localViewListener?.RegisterView(AuthorizeChanges); - } - } - - public async Task SingleAsync(int id, CancellationToken cancellationToken = default) - { - return await AggregateQueryable.SingleAsync(agg => agg.Id == id, cancellationToken); - } - - public async Task SingleOrDefaultAsync(int id, CancellationToken cancellationToken = default) - { - return await AggregateQueryable.SingleOrDefaultAsync(agg => agg.Id == id, cancellationToken); - } - - public async Task GetAllAsync(CancellationToken cancellationToken = default) - { - return await AggregateQueryable.ToArrayAsync(cancellationToken); - } - - public async Task AnyAsync(CancellationToken cancellationToken = default) - { - return await AggregateQueryable.AnyAsync(cancellationToken); - } - - public async Task ResolveAsync(IEnumerable ids, CancellationToken cancellationToken = default) - { - if (ids == null) - { - return Array.Empty(); - } - - int[] idsToResolve = ids as int[] ?? ids.ToArray(); - TAggregateRoot[] resolved = await AggregateQueryable.Where(agg => idsToResolve.Contains(agg.Id)).ToArrayAsync(cancellationToken); - if (resolved.Length != idsToResolve.Length) - { - throw new ArgumentException($"The following {AggregateTypeName} ids could not be resolved: {string.Join(", ", idsToResolve.Except(resolved.Select(agg => agg.Id)))}"); - } - return resolved; - } - - protected override IQueryable RawAggregateQueryable - { - get - { - IQueryable queryable = DbContext.Set(); - if (_aggregateMapping.IncludeDefinitions != null) - foreach (var include in _aggregateMapping.IncludeDefinitions) - queryable = queryable.Include(include); - return queryable; - } - } - - /// - /// Due to the fact, that a real lifecycle hook API is not yet available (see issue https://github.com/aspnet/EntityFrameworkCore/issues/626) - /// we are using an internal service to achieve the same goal: When a state change occurs from unchanged to modified, and this repository is - /// handling this type of aggregate, we're calling IAggregateAuthorization.CanModify to enforce user privilege checking. - /// We're accepting the possible instability of EF Core internals due to the fact that there is a full API feature in the pipeline that will - /// make this workaround obsolete. Also, not much of an effort was done to write this code, so if we have to deal with this issue in the future - /// again, we do not loose a lot. - /// - /// - /// - [SuppressMessage("ReSharper", "EF1001")] - private void AuthorizeChanges(InternalEntityEntry entry, EntityState previousState) - { - if (previousState == EntityState.Unchanged && entry.EntityState == EntityState.Modified && entry.EntityType.ClrType == typeof(TAggregateRoot)) - { - var aggregateRoot = (TAggregateRoot) entry.Entity; - if (!_aggregateAuthorization.CanModify(aggregateRoot)) throw new ForbiddenException("Unauthorized attempt to modify {AggregateTypeName}[{aggregateRoot.Id}]") - .AddError($"You are not allowed to modify {AggregateTypeName}[{aggregateRoot.Id}]"); - } - } - - protected override void AddPersistent(TAggregateRoot aggregateRoot) - { - Logger.LogDebug("Persistently adding new {AggregateTypeName}", AggregateTypeName); - DbContext.Set().Add(aggregateRoot); - } - - protected override void AddRangePersistent(TAggregateRoot[] aggregateRoots) - { - Logger.LogDebug("Persistently adding {Count} item(s) of type {AggregateTypeName}", aggregateRoots.Length, AggregateTypeName); - DbContext.Set().AddRange(aggregateRoots); - } - - protected override void DeletePersistent(TAggregateRoot aggregateRoot) - { - Logger.LogDebug("Persistently removing {AggregateTypeName}[{Id}]", AggregateTypeName, aggregateRoot.Id); - DbContext.Set().Remove(aggregateRoot); - } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/EntityQueryable.cs b/src/implementations/Backend.Fx.EfCorePersistence/EntityQueryable.cs deleted file mode 100644 index 48d4a959..00000000 --- a/src/implementations/Backend.Fx.EfCorePersistence/EntityQueryable.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using Backend.Fx.BuildingBlocks; -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; - -namespace Backend.Fx.EfCorePersistence -{ - public class EntityQueryable : IQueryable where TEntity : Entity - { - [CanBeNull] private DbContext _dbContext; - - public EntityQueryable(DbContext dbContext) - { - _dbContext = dbContext; - } - - public DbContext DbContext - { - get => _dbContext ?? throw new InvalidOperationException( - "This EntityQueryable does not have a DbContext yet. You might either make sure a proper DbContext gets injected or the DbContext is initialized later using a derived class") - ; - protected set - { - if (_dbContext != null) throw new InvalidOperationException("This EntityQueryable has already a DbContext assigned. It is not allowed to change it later."); - _dbContext = value; - } - } - - private IQueryable InnerQueryable => DbContext.Set(); - - public IEnumerator GetEnumerator() - { - return InnerQueryable.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return ((IEnumerable) InnerQueryable).GetEnumerator(); - } - - public Type ElementType => InnerQueryable.ElementType; - - public Expression Expression => InnerQueryable.Expression; - - public IQueryProvider Provider => InnerQueryable.Provider; - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/IAggregateMapping.cs b/src/implementations/Backend.Fx.EfCorePersistence/IAggregateMapping.cs deleted file mode 100644 index 76dd9fd5..00000000 --- a/src/implementations/Backend.Fx.EfCorePersistence/IAggregateMapping.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using Backend.Fx.BuildingBlocks; -using Microsoft.EntityFrameworkCore; - -namespace Backend.Fx.EfCorePersistence -{ - public interface IAggregateMapping - { - void ApplyEfMapping(ModelBuilder modelBuilder); - } - - public interface IAggregateMapping : IAggregateMapping where T : AggregateRoot - { - IEnumerable>> IncludeDefinitions { get; } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/Mssql/MsSqlSequence.cs b/src/implementations/Backend.Fx.EfCorePersistence/Mssql/MsSqlSequence.cs deleted file mode 100644 index 9131007e..00000000 --- a/src/implementations/Backend.Fx.EfCorePersistence/Mssql/MsSqlSequence.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Data; -using Backend.Fx.EfCorePersistence.Bootstrapping; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.IdGeneration; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.EfCorePersistence.Mssql -{ - public abstract class MsSqlSequence : ISequence - { - private static readonly ILogger Logger = Log.Create(); - private readonly IDbConnectionFactory _dbConnectionFactory; - private readonly int _startWith; - - protected MsSqlSequence(IDbConnectionFactory dbConnectionFactory, int startWith = 1) - { - _dbConnectionFactory = dbConnectionFactory; - _startWith = startWith; - } - - protected abstract string SequenceName { get; } - protected virtual string SchemaName { get; } = "dbo"; - - public void EnsureSequence() - { - Logger.LogInformation("Ensuring existence of mssql sequence {SchemaName}.{SequenceName}", SchemaName, SequenceName); - using (IDbConnection dbConnection = _dbConnectionFactory.Create()) - { - dbConnection.Open(); - bool sequenceExists; - using (IDbCommand cmd = dbConnection.CreateCommand()) - { - cmd.CommandText = $"SELECT count(*) FROM sys.sequences seq join sys.schemas s on s.schema_id = seq.schema_id WHERE seq.name = '{SequenceName}' and s.name = '{SchemaName}'"; - sequenceExists = (int) cmd.ExecuteScalar() == 1; - } - - if (sequenceExists) - { - Logger.LogInformation("Sequence {SchemaName}.{SequenceName} exists", SchemaName, SequenceName); - } - else - { - Logger.LogInformation("Sequence {SchemaName}.{SequenceName} does not exist yet and will be created now", SchemaName, SequenceName); - using (IDbCommand cmd = dbConnection.CreateCommand()) - { - cmd.CommandText = $"CREATE SEQUENCE [{SchemaName}].[{SequenceName}] START WITH {_startWith} INCREMENT BY {Increment}"; - cmd.ExecuteNonQuery(); - Logger.LogInformation("Sequence {SchemaName}.{SequenceName} created", SchemaName, SequenceName); - } - } - } - } - - public int GetNextValue() - { - using (IDbConnection dbConnection = _dbConnectionFactory.Create()) - { - dbConnection.Open(); - int nextValue; - using (IDbCommand selectNextValCommand = dbConnection.CreateCommand()) - { - selectNextValCommand.CommandText = $"SELECT next value FOR {SchemaName}.{SequenceName}"; - nextValue = Convert.ToInt32(selectNextValCommand.ExecuteScalar()); - Logger.LogDebug("{SchemaName}.{SequenceName} served {NextValue} as next value", SchemaName, SequenceName, nextValue); - } - - return nextValue; - } - } - - public abstract int Increment { get; } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/Oracle/OracleSequence.cs b/src/implementations/Backend.Fx.EfCorePersistence/Oracle/OracleSequence.cs deleted file mode 100644 index aea02cef..00000000 --- a/src/implementations/Backend.Fx.EfCorePersistence/Oracle/OracleSequence.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Data; -using Backend.Fx.EfCorePersistence.Bootstrapping; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.IdGeneration; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.EfCorePersistence.Oracle -{ - public abstract class OracleSequence : ISequence - { - private static readonly ILogger Logger = Log.Create(); - private readonly IDbConnectionFactory _dbConnectionFactory; - private readonly int _startWith; - - protected OracleSequence(IDbConnectionFactory dbConnectionFactory, int startWith = 1) - { - _dbConnectionFactory = dbConnectionFactory; - _startWith = startWith; - } - - protected abstract string SequenceName { get; } - protected abstract string SchemaName { get; } - - private string SchemaPrefix - { - get - { - if (string.IsNullOrEmpty(SchemaName)) return string.Empty; - - return SchemaName + "."; - } - } - - public void EnsureSequence() - { - Logger.LogInformation("Ensuring existence of oracle sequence {SchemaPrefix}.{SequenceName}", SchemaPrefix, SequenceName); - - using (IDbConnection dbConnection = _dbConnectionFactory.Create()) - { - dbConnection.Open(); - bool sequenceExists; - using (IDbCommand command = dbConnection.CreateCommand()) - { - command.CommandText = $"SELECT count(*) FROM user_sequences WHERE sequence_name = '{SequenceName}'"; - sequenceExists = (decimal)command.ExecuteScalar() == 1; - } - - if (sequenceExists) - { - Logger.LogInformation("Oracle sequence {SchemaPrefix}.{SequenceName} exists", SchemaPrefix, SequenceName); - } - else - { - Logger.LogInformation("Oracle sequence {SchemaPrefix}.{SequenceName} does not exist yet and will be created now", - SchemaPrefix, - SequenceName); - using (IDbCommand cmd = dbConnection.CreateCommand()) - { - cmd.CommandText = $"CREATE SEQUENCE {SchemaPrefix}{SequenceName} START WITH {_startWith} INCREMENT BY {Increment}"; - cmd.ExecuteNonQuery(); - Logger.LogInformation("Oracle sequence {SchemaPrefix}.{SequenceName} created", SchemaPrefix, SequenceName); - } - } - } - } - - public int GetNextValue() - { - using (IDbConnection dbConnection = _dbConnectionFactory.Create()) - { - dbConnection.Open(); - - int nextValue; - using (IDbCommand command = dbConnection.CreateCommand()) - { - command.CommandText = $"SELECT {SchemaPrefix}{SequenceName}.NEXTVAL FROM dual"; - nextValue = Convert.ToInt32(command.ExecuteScalar()); - Logger.LogDebug("Oracle sequence {SchemaPrefix}.{SequenceName} served {NextValue} as next value", - SchemaPrefix, - SequenceName, - nextValue); - } - - return nextValue; - } - } - - public abstract int Increment { get; } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/PlainAggregateMapping.cs b/src/implementations/Backend.Fx.EfCorePersistence/PlainAggregateMapping.cs deleted file mode 100644 index 2bf4efec..00000000 --- a/src/implementations/Backend.Fx.EfCorePersistence/PlainAggregateMapping.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using Backend.Fx.BuildingBlocks; -using Microsoft.EntityFrameworkCore; - -namespace Backend.Fx.EfCorePersistence -{ - public class PlainAggregateMapping : AggregateMapping - where TAggregateRoot : AggregateRoot - { - public override IEnumerable>> IncludeDefinitions => new Expression>[0]; - - public override void ApplyEfMapping(ModelBuilder modelBuilder) - { - } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/Postgres/PostgresSequence.cs b/src/implementations/Backend.Fx.EfCorePersistence/Postgres/PostgresSequence.cs deleted file mode 100644 index 7d51835b..00000000 --- a/src/implementations/Backend.Fx.EfCorePersistence/Postgres/PostgresSequence.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Data; -using Backend.Fx.EfCorePersistence.Bootstrapping; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.IdGeneration; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.EfCorePersistence.Postgres -{ - public abstract class PostgresSequence : ISequence - { - private static readonly ILogger Logger = Log.Create(); - private readonly IDbConnectionFactory _dbConnectionFactory; - private readonly int _startWith; - - protected PostgresSequence(IDbConnectionFactory dbConnectionFactory, int startWith = 1) - { - _dbConnectionFactory = dbConnectionFactory; - _startWith = startWith; - } - - protected abstract string SequenceName { get; } - protected abstract string SchemaName { get; } - - public void EnsureSequence() - { - Logger.LogInformation("Ensuring existence of postgres sequence {SchemaName}.{SequenceName}", SchemaName, SequenceName); - - using (IDbConnection dbConnection = _dbConnectionFactory.Create()) - { - dbConnection.Open(); - bool sequenceExists; - using (IDbCommand command = dbConnection.CreateCommand()) - { - command.CommandText = $"SELECT count(*) FROM information_schema.sequences WHERE sequence_name = '{SequenceName}' AND sequence_schema = '{SchemaName}'"; - sequenceExists = (long) command.ExecuteScalar() == 1L; - } - - if (sequenceExists) - { - Logger.LogInformation("Sequence {SchemaName}.{SequenceName} exists", SchemaName, SequenceName); - } - else - { - Logger.LogInformation( - "Sequence {SchemaName}.{SequenceName} does not exist yet and will be created now", - SchemaName, - SequenceName); - using (IDbCommand cmd = dbConnection.CreateCommand()) - { - cmd.CommandText = $"CREATE SEQUENCE {SchemaName}.{SequenceName} START WITH {_startWith} INCREMENT BY {Increment}"; - cmd.ExecuteNonQuery(); - Logger.LogInformation("Sequence {SchemaName}.{SequenceName} created", SchemaName, SequenceName); - } - } - } - } - - public int GetNextValue() - { - using (IDbConnection dbConnection = _dbConnectionFactory.Create()) - { - dbConnection.Open(); - - int nextValue; - using (IDbCommand command = dbConnection.CreateCommand()) - { - command.CommandText = $"SELECT nextval('{SchemaName}.{SequenceName}');"; - nextValue = Convert.ToInt32(command.ExecuteScalar()); - Logger.LogDebug("{SchemaName}.{SequenceName} served {2} as next value", SchemaName, SequenceName, nextValue); - } - - return nextValue; - } - } - - public abstract int Increment { get; } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/Properties/AssemblyInfo.cs b/src/implementations/Backend.Fx.EfCorePersistence/Properties/AssemblyInfo.cs deleted file mode 100644 index 5f565d7d..00000000 --- a/src/implementations/Backend.Fx.EfCorePersistence/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System.Reflection; - -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyVersion("0.0.0.0")] -[assembly: AssemblyFileVersion("0.0.0.0")] -[assembly: AssemblyInformationalVersion("0.0.0.0")] \ No newline at end of file diff --git a/src/implementations/Backend.Fx.InMemoryPersistence/Backend.Fx.InMemoryPersistence.csproj b/src/implementations/Backend.Fx.InMemoryPersistence/Backend.Fx.InMemoryPersistence.csproj deleted file mode 100644 index 993c7626..00000000 --- a/src/implementations/Backend.Fx.InMemoryPersistence/Backend.Fx.InMemoryPersistence.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - - netstandard2.0 - true - snupkg - true - false - false - false - false - - - - Marc Wittke - anic GmbH - All rights reserved. Distributed under the terms of the MIT License. - In memory dictionary based implementation of Backend.Fx persistence abstraction - False - MIT - https://github.com/marcwittke/Backend.Fx - Backend.Fx - Git - https://github.com/marcwittke/Backend.Fx.git - - - - - - - - diff --git a/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryEntityIdGenerator.cs b/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryEntityIdGenerator.cs deleted file mode 100644 index 1f9f54fd..00000000 --- a/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryEntityIdGenerator.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Backend.Fx.Patterns.IdGeneration; - -namespace Backend.Fx.InMemoryPersistence -{ - public class InMemoryEntityIdGenerator : IEntityIdGenerator - { - private int _nextId = 1; - - public int NextId() - { - return _nextId++; - } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryQueryable.cs b/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryQueryable.cs deleted file mode 100644 index 9797c793..00000000 --- a/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryQueryable.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using Backend.Fx.BuildingBlocks; - -namespace Backend.Fx.InMemoryPersistence -{ - public class InMemoryQueryable : IQueryable where TAggregateRoot : AggregateRoot - { - private readonly IQueryable _queryableImplementation; - - public InMemoryQueryable(IInMemoryStore store) - { - _queryableImplementation = store.Values.AsQueryable(); - } - - - public IEnumerator GetEnumerator() - { - return _queryableImplementation.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return ((IEnumerable) _queryableImplementation).GetEnumerator(); - } - - public Type ElementType => _queryableImplementation.ElementType; - - public Expression Expression => _queryableImplementation.Expression; - - public IQueryProvider Provider => _queryableImplementation.Provider; - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryRepository.cs b/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryRepository.cs deleted file mode 100644 index 07708f7a..00000000 --- a/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryRepository.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Linq; -using Backend.Fx.BuildingBlocks; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Extensions; -using Backend.Fx.Patterns.Authorization; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.RandomData; - -namespace Backend.Fx.InMemoryPersistence -{ - public class InMemoryRepository : Repository where T : AggregateRoot - { - public InMemoryRepository(IInMemoryStore store, ICurrentTHolder currentTenantIdHolder, IAggregateAuthorization aggregateAuthorization) - : base(currentTenantIdHolder, aggregateAuthorization) - { - Store = store; - } - - public virtual IInMemoryStore Store { get; } - - protected override IQueryable RawAggregateQueryable => Store.Values.AsQueryable(); - - public void Clear() - { - Store.Clear(); - } - - public T Random() - { - return Store.Values.Random(); - } - - protected override void AddPersistent(T aggregateRoot) - { - Store.Add(aggregateRoot.Id, aggregateRoot); - } - - protected override void AddRangePersistent(T[] aggregateRoots) - { - aggregateRoots.ForAll(AddPersistent); - } - - protected override void DeletePersistent(T aggregateRoot) - { - Store.Remove(aggregateRoot.Id); - } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryStore.cs b/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryStore.cs deleted file mode 100644 index 67a5d975..00000000 --- a/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryStore.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using Backend.Fx.BuildingBlocks; - -namespace Backend.Fx.InMemoryPersistence -{ - public interface IInMemoryStore : IDictionary where T : AggregateRoot - { - } - - public class InMemoryStore : IInMemoryStore where T : AggregateRoot - { - private readonly IDictionary _dictionaryImplementation = new Dictionary(); - - public IEnumerator> GetEnumerator() - { - return _dictionaryImplementation.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return ((IEnumerable) _dictionaryImplementation).GetEnumerator(); - } - - public void Add(KeyValuePair item) - { - _dictionaryImplementation.Add(item); - } - - public void Clear() - { - _dictionaryImplementation.Clear(); - } - - public bool Contains(KeyValuePair item) - { - return _dictionaryImplementation.Contains(item); - } - - public void CopyTo(KeyValuePair[] array, int arrayIndex) - { - _dictionaryImplementation.CopyTo(array, arrayIndex); - } - - public bool Remove(KeyValuePair item) - { - return _dictionaryImplementation.Remove(item); - } - - public int Count => _dictionaryImplementation.Count; - - public bool IsReadOnly => _dictionaryImplementation.IsReadOnly; - - public void Add(int key, T value) - { - _dictionaryImplementation.Add(key, value); - } - - public bool ContainsKey(int key) - { - return _dictionaryImplementation.ContainsKey(key); - } - - public bool Remove(int key) - { - return _dictionaryImplementation.Remove(key); - } - - public bool TryGetValue(int key, out T value) - { - return _dictionaryImplementation.TryGetValue(key, out value); - } - - public T this[int key] - { - get => _dictionaryImplementation[key]; - set => _dictionaryImplementation[key] = value; - } - - public ICollection Keys => _dictionaryImplementation.Keys; - - public ICollection Values => _dictionaryImplementation.Values; - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryTenantRepository.cs b/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryTenantRepository.cs deleted file mode 100644 index b9f2f2ad..00000000 --- a/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryTenantRepository.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Backend.Fx.Environment.MultiTenancy; - -namespace Backend.Fx.InMemoryPersistence -{ - public class InMemoryTenantRepository : ITenantRepository - { - private readonly Dictionary _store = new Dictionary(); - - public Tenant[] GetTenants() - { - return _store.Values.ToArray(); - } - - public Tenant GetTenant(TenantId tenantId) - { - return _store[tenantId.Value]; - } - - public void DeleteTenant(TenantId tenantId) - { - _store.Remove(tenantId.Value); - } - - public void SaveTenant(Tenant tenant) - { - if (tenant.Id == 0) - { - tenant.Id = _store.Any() ? _store.Keys.Max() + 1 : 1; - _store[tenant.Id] = tenant; - } - else - { - _store[tenant.Id].Description = tenant.Description; - _store[tenant.Id].Configuration = tenant.Configuration; - _store[tenant.Id].IsDemoTenant = tenant.IsDemoTenant; - _store[tenant.Id].Name = tenant.Name; - _store[tenant.Id].State = tenant.State; - } - } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryView.cs b/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryView.cs deleted file mode 100644 index cd4ff074..00000000 --- a/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryView.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using Backend.Fx.BuildingBlocks; - -namespace Backend.Fx.InMemoryPersistence -{ - public class InMemoryView : IView - { - private readonly IList _list; - - public InMemoryView(IList list) - { - _list = list; - } - - public IEnumerator GetEnumerator() - { - return _list.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return ((IEnumerable) _list).GetEnumerator(); - } - - public Type ElementType => typeof(T); - public Expression Expression => _list.AsQueryable().Expression; - public IQueryProvider Provider => _list.AsQueryable().Provider; - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.MicrosoftDependencyInjection/Backend.Fx.MicrosoftDependencyInjection.csproj b/src/implementations/Backend.Fx.MicrosoftDependencyInjection/Backend.Fx.MicrosoftDependencyInjection.csproj new file mode 100644 index 00000000..ea478ef4 --- /dev/null +++ b/src/implementations/Backend.Fx.MicrosoftDependencyInjection/Backend.Fx.MicrosoftDependencyInjection.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0 + + + + + + + + + + + + diff --git a/src/implementations/Backend.Fx.MicrosoftDependencyInjection/MicrosoftCompositionRoot.cs b/src/implementations/Backend.Fx.MicrosoftDependencyInjection/MicrosoftCompositionRoot.cs new file mode 100644 index 00000000..7b2ec8b1 --- /dev/null +++ b/src/implementations/Backend.Fx.MicrosoftDependencyInjection/MicrosoftCompositionRoot.cs @@ -0,0 +1,37 @@ +using System; +using Backend.Fx.Logging; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.MicrosoftDependencyInjection +{ + [PublicAPI] + public class MicrosoftCompositionRoot : MicrosoftCompositionRootBase + { + private readonly ILogger _logger = Log.Create(); + private readonly Lazy _serviceProvider; + + public MicrosoftCompositionRoot() + { + _serviceProvider = new Lazy(() => + { + _logger.LogInformation("Building Microsoft ServiceProvider"); + return ServiceCollection.BuildServiceProvider( + new ServiceProviderOptions + { + ValidateScopes = true, + ValidateOnBuild = true + }); + }); + } + + public override IServiceProvider ServiceProvider => _serviceProvider.Value; + + public override void Verify() + { + // ensure creation of lazy service provider, this will trigger the validation + var unused = _serviceProvider.Value; + } + } +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.MicrosoftDependencyInjection/MicrosoftCompositionRootBase.cs b/src/implementations/Backend.Fx.MicrosoftDependencyInjection/MicrosoftCompositionRootBase.cs new file mode 100644 index 00000000..8dd2851f --- /dev/null +++ b/src/implementations/Backend.Fx.MicrosoftDependencyInjection/MicrosoftCompositionRootBase.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Backend.Fx.DependencyInjection; +using Backend.Fx.Logging; +using Backend.Fx.Util; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.MicrosoftDependencyInjection +{ + public abstract class MicrosoftCompositionRootBase : CompositionRoot + { + private readonly ILogger _logger = Log.Create(); + + protected MicrosoftCompositionRootBase(IServiceCollection serviceCollection = null) + { + ServiceCollection = serviceCollection ?? new ServiceCollection(); + } + + protected IServiceCollection ServiceCollection { get; } + + + public override void Register(ServiceDescriptor serviceDescriptor) + { + var existingRegistration = ServiceCollection + .SingleOrDefault(sd => sd.ServiceType == serviceDescriptor.ServiceType); + + if (existingRegistration == null) + { + ServiceCollection.Add(serviceDescriptor); + } + else + { + _logger.LogDebug("{Verb} {Lifetime} {RegistrationType} for {ServiceType}: {ImplementationType}", + "Replacing", + serviceDescriptor.Lifetime.ToString().ToLowerInvariant(), + "registration", + serviceDescriptor.ServiceType.GetDetailedTypeName(), + serviceDescriptor.GetImplementationTypeDescription()); + ServiceCollection.Replace(serviceDescriptor); + } + } + + public override void RegisterDecorator(ServiceDescriptor serviceDescriptor) + { + if (serviceDescriptor.ServiceType.IsOpenGeneric()) + { + throw new NotSupportedException("Microsoft's DI does not support decoration of open generic types. " + + "See https://github.com/khellang/Scrutor/issues/39 for more info"); + } + + if (ServiceCollection.Any(sd => sd.ServiceType == serviceDescriptor.ServiceType)) + { + ServiceCollection.Decorate( + serviceDescriptor.ServiceType, + serviceDescriptor.ImplementationType + ?? throw new ArgumentException("You must provide an implementationType when registering a decorator", nameof(serviceDescriptor))); + } + else + { + _logger.LogWarning("Skipping registration of decorator {DecoratorTypeName} because the service type to decorate ({ServiceType}) is not registered", + serviceDescriptor.GetImplementationTypeDescription(), + serviceDescriptor.ServiceType.Name); + } + } + + public override void RegisterCollection(IEnumerable serviceDescriptors) + { + var serviceDescriptorArray = serviceDescriptors as ServiceDescriptor[] ?? serviceDescriptors.ToArray(); + + if (serviceDescriptorArray.Length == 0) + { + _logger.LogWarning("Skipping registration of empty collection"); + return; + } + + foreach (var serviceDescriptor in serviceDescriptorArray) + { + ServiceCollection.Add(serviceDescriptor); + } + } + + public override IServiceScope BeginScope() + { + return ServiceProvider.CreateScope(); + } + + protected override void Dispose(bool disposing) + { + } + } +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.MicrosoftDependencyInjection/SharedMicrosoftCompositionRoot.cs b/src/implementations/Backend.Fx.MicrosoftDependencyInjection/SharedMicrosoftCompositionRoot.cs new file mode 100644 index 00000000..1142bb4c --- /dev/null +++ b/src/implementations/Backend.Fx.MicrosoftDependencyInjection/SharedMicrosoftCompositionRoot.cs @@ -0,0 +1,28 @@ +using System; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.MicrosoftDependencyInjection +{ + [PublicAPI] + public class SharedMicrosoftCompositionRoot : MicrosoftCompositionRootBase + { + private IServiceProvider _serviceProvider; + + public SharedMicrosoftCompositionRoot(IServiceCollection serviceCollection) : base(serviceCollection) + { } + + public override IServiceProvider ServiceProvider => + _serviceProvider ?? throw new InvalidOperationException("ServiceProvider not in use. Call UseServiceProvider(app.ServiceProvider) in Startup.Configure"); + + public override void Verify() + { + // out of our control + } + + public void UseServiceProvider(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + } +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.RabbitMq/Backend.Fx.RabbitMq.csproj b/src/implementations/Backend.Fx.RabbitMq/Backend.Fx.RabbitMq.csproj index 5c563d70..2c9990cf 100644 --- a/src/implementations/Backend.Fx.RabbitMq/Backend.Fx.RabbitMq.csproj +++ b/src/implementations/Backend.Fx.RabbitMq/Backend.Fx.RabbitMq.csproj @@ -24,10 +24,9 @@ - - + diff --git a/src/implementations/Backend.Fx.RabbitMq/RabbitMQChannel.cs b/src/implementations/Backend.Fx.RabbitMq/RabbitMqChannel.cs similarity index 59% rename from src/implementations/Backend.Fx.RabbitMq/RabbitMQChannel.cs rename to src/implementations/Backend.Fx.RabbitMq/RabbitMqChannel.cs index 9010cc7c..a4dcb5bb 100644 --- a/src/implementations/Backend.Fx.RabbitMq/RabbitMQChannel.cs +++ b/src/implementations/Backend.Fx.RabbitMq/RabbitMqChannel.cs @@ -1,27 +1,25 @@ using System; using System.Collections.Generic; using System.Net.Sockets; -using System.Text; +using System.Threading.Tasks; +using Backend.Fx.Features.MessageBus; using Backend.Fx.Logging; -using Backend.Fx.Patterns.EventAggregation.Integration; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; using Polly; using RabbitMQ.Client; using RabbitMQ.Client.Events; using RabbitMQ.Client.Exceptions; -using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Backend.Fx.RabbitMq { public class RabbitMqChannel : IDisposable { - private static readonly ILogger Logger = Log.Create(); + private readonly ILogger _logger = Log.Create(); private readonly string _exchangeName; - private readonly IMessageNameProvider _messageNameProvider; private readonly IConnectionFactory _connectionFactory; private readonly string _queueName; private readonly int _retryCount; + private readonly Func _handleMessage; private readonly HashSet _subscribedEventNames = new HashSet(); private readonly object _syncRoot = new object(); @@ -30,17 +28,18 @@ public class RabbitMqChannel : IDisposable private bool _isDisposed; private IModel _channel; - public RabbitMqChannel(IMessageNameProvider messageNameProvider, - IConnectionFactory connectionFactory, - string exchangeName, - string queueName, - int retryCount) + public RabbitMqChannel( + IConnectionFactory connectionFactory, + string exchangeName, + string queueName, + int retryCount, + Func handleMessage) { - _messageNameProvider = messageNameProvider; _connectionFactory = connectionFactory; _exchangeName = exchangeName; _queueName = queueName; _retryCount = retryCount; + _handleMessage = handleMessage; } public void Dispose() @@ -55,17 +54,13 @@ public void Dispose() } catch (Exception ex) { - Logger.LogError(ex, "Closing RabbitMQ channel failed"); + _logger.LogError(ex, "Closing RabbitMQ channel failed"); } } - public void EnsureClosed() + private void EnsureClosed() { - if (_consumer != null) - { - _consumer.Received -= OnMessageReceived; - _consumer = null; - } + _consumer = null; if (_channel != null) { @@ -98,22 +93,20 @@ public bool EnsureOpen() return Open(); } - public event EventHandler MessageReceived; - private bool Open() { lock (_syncRoot) { EnsureClosed(); - Logger.LogInformation("RabbitMQ Client is trying to connect"); + _logger.LogInformation("RabbitMQ Client is trying to connect"); Policy.Handle() - .Or() - .WaitAndRetry(_retryCount, - retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), - (ex, time) => { Logger.LogWarning(ex, "Connection not ready"); } - ) - .Execute(() => { _connection = _connectionFactory.CreateConnection(); }); + .Or() + .WaitAndRetry(_retryCount, + retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + (ex, time) => { _logger.LogWarning(ex, "Connection not ready"); } + ) + .Execute(() => { _connection = _connectionFactory.CreateConnection(); }); if (_connection?.IsOpen == true) { @@ -121,20 +114,39 @@ private bool Open() _connection.CallbackException += OnCallbackException; _connection.ConnectionBlocked += OnConnectionBlocked; - Logger.LogInformation("Acquired a connection to RabbitMQ host {HostName} and is subscribed to failure events", + _logger.LogInformation( + "Acquired a connection to RabbitMQ host {HostName} and is subscribed to failure events", _connection.Endpoint.HostName); _channel = _connection.CreateModel(); _channel.ExchangeDeclare(_exchangeName, "direct"); _channel.QueueDeclare(_queueName, true, false, false, null); _consumer = new EventingBasicConsumer(_channel); - _consumer.Received += OnMessageReceived; + _consumer.Received += (sender, args) => + { + _logger.LogDebug("RabbitMQ message with routing key {RoutingKey} received", args.RoutingKey); + if (_subscribedEventNames.Contains(args.RoutingKey)) + { + try + { + + Task.Run(() => _handleMessage(new SerializedMessage(args.RoutingKey, args.Body))); + Acknowledge(args.DeliveryTag); + + } + catch + { + NAcknowledge(args.DeliveryTag); + throw; + } + } + }; _channel.BasicConsume(_queueName, false, _consumer); _channel.CallbackException += OnCallbackException; foreach (var subscribedEventName in _subscribedEventNames) { - Logger.LogInformation( + _logger.LogInformation( "Binding messages on exchange {ExchangeName} with routing key {RoutingKey} to queue {QueueName}", _exchangeName, subscribedEventName, @@ -145,43 +157,38 @@ private bool Open() return true; } - Logger.LogError("RabbitMQ connection could not be created and opened"); + _logger.LogError("RabbitMQ connection could not be created and opened"); return false; } } - public void PublishEvent(IIntegrationEvent integrationEvent) + public void PublishMessage(SerializedMessage serializedMessage) { - var messageName = _messageNameProvider.GetMessageName(integrationEvent); - var message = JsonConvert.SerializeObject(integrationEvent); - var body = Encoding.UTF8.GetBytes(message); - - DoResilient(() => _channel.BasicPublish(_exchangeName, messageName, null, body)); + DoResilient( + () => _channel.BasicPublish( + _exchangeName, + serializedMessage.EventTypeName, + null, + serializedMessage.MessagePayload)); } public void Subscribe(string messageName) { + _logger.LogDebug("Subscribing to {MessageName}", messageName); EnsureOpen(); _channel.QueueBind(_queueName, _exchangeName, messageName); _subscribedEventNames.Add(messageName); } - public void Unsubscribe(string eventName) - { - EnsureOpen(); - _channel.QueueUnbind(_queueName, _exchangeName, eventName); - _subscribedEventNames.Remove(eventName); - } - public void Acknowledge(ulong deliveryTag) { - Logger.LogDebug("Acknowledging {DeliveryTag}", deliveryTag); + _logger.LogDebug("Acknowledging {DeliveryTag}", deliveryTag); DoResilient(() => _channel.BasicAck(deliveryTag, false)); } public void NAcknowledge(ulong deliveryTag) { - Logger.LogDebug("NAcknowledging {DeliveryTag}", deliveryTag); + _logger.LogDebug("NAcknowledging {DeliveryTag}", deliveryTag); DoResilient(() => _channel.BasicNack(deliveryTag, false, false)); } @@ -189,7 +196,7 @@ private void OnCallbackException(object sender, CallbackExceptionEventArgs e) { if (_isDisposed) return; - Logger.LogWarning(e.Exception, "A RabbitMQ connection threw an exception"); + _logger.LogWarning(e.Exception, "A RabbitMQ connection threw an exception"); Open(); } @@ -197,7 +204,7 @@ private void OnConnectionBlocked(object sender, ConnectionBlockedEventArgs e) { if (_isDisposed) return; - Logger.LogWarning("A RabbitMQ connection is blocked with reason {Reason}", e.Reason); + _logger.LogWarning("A RabbitMQ connection is blocked with reason {Reason}", e.Reason); Open(); } @@ -205,23 +212,18 @@ private void OnConnectionShutdown(object sender, ShutdownEventArgs reason) { if (_isDisposed) return; - Logger.LogWarning("A RabbitMQ connection is shut down with reason {@Reason}", reason); + _logger.LogWarning("A RabbitMQ connection is shut down with reason {@Reason}", reason); Open(); } - private void OnMessageReceived(object sender, BasicDeliverEventArgs basicDeliverEventArgs) - { - MessageReceived?.Invoke(this, basicDeliverEventArgs); - } - private void DoResilient(Action action) { Policy.Handle() - .Or() - .WaitAndRetry(_retryCount, - retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), - (ex, time) => { Logger.LogWarning(ex, "Connection not ready"); }) - .Execute(action); + .Or() + .WaitAndRetry(_retryCount, + retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + (ex, time) => { _logger.LogWarning(ex, "Connection not ready"); }) + .Execute(action); } } } \ No newline at end of file diff --git a/src/implementations/Backend.Fx.RabbitMq/RabbitMqMessageBus.cs b/src/implementations/Backend.Fx.RabbitMq/RabbitMqMessageBus.cs index 25b08ac6..d05a48a8 100644 --- a/src/implementations/Backend.Fx.RabbitMq/RabbitMqMessageBus.cs +++ b/src/implementations/Backend.Fx.RabbitMq/RabbitMqMessageBus.cs @@ -1,112 +1,64 @@ -using System; -using System.Text; -using System.Threading.Tasks; -using Backend.Fx.Environment.MultiTenancy; +using System.Threading.Tasks; +using Backend.Fx.Features.MessageBus; using Backend.Fx.Logging; -using Backend.Fx.Patterns.EventAggregation.Integration; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using RabbitMQ.Client; -using RabbitMQ.Client.Events; -using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Backend.Fx.RabbitMq { public class RabbitMqMessageBus : MessageBus { - private static readonly ILogger Logger = Log.Create(); + private readonly ILogger _logger = Log.Create(); private readonly RabbitMqChannel _channel; - public RabbitMqMessageBus(IConnectionFactory connectionFactory, int retryCount, string exchangeName, string receiveQueueName) + public RabbitMqMessageBus(RabbitMqOptions options) { - _channel = new RabbitMqChannel(MessageNameProvider, connectionFactory, exchangeName, receiveQueueName, retryCount); + var connectionFactory = new ConnectionFactory + { + HostName = options.Hostname, + Port = options.Port, + UserName = options.Username, + Password = options.Password, + UseBackgroundThreadsForIO = true + }; + _channel = new RabbitMqChannel(connectionFactory, options.ExchangeName, options.ReceiveQueueName, options.RetryCount, ProcessAsync); } public override void Connect() { - Logger.LogInformation("Opening a channel to RabbitMQ..."); + _logger.LogInformation("Opening a channel to RabbitMQ..."); if (_channel.EnsureOpen()) { - _channel.MessageReceived += ChannelOnMessageReceived; - Logger.LogInformation("Channel to RabbitMQ opened"); + _logger.LogInformation("Channel to RabbitMQ opened"); } } - - private void ChannelOnMessageReceived(object sender, BasicDeliverEventArgs args) + + protected override void SubscribeToEventMessage(string eventTypeName) { - Logger.LogDebug("RabbitMQ message with routing key {RoutingKey} received", args.RoutingKey); - try - { - Process(args.RoutingKey, new RabbitMqEventProcessingContext(args.Body)); - _channel.Acknowledge(args.DeliveryTag); - } - catch - { - _channel.NAcknowledge(args.DeliveryTag); - throw; - } - } - - protected override Task PublishOnMessageBus(IIntegrationEvent integrationEvent) - { - Logger.LogInformation("Publishing {MessageName}", MessageNameProvider.GetMessageName(integrationEvent)); + _logger.LogInformation("Subscribing to messages of {EventTypeName}", eventTypeName); _channel.EnsureOpen(); - _channel.PublishEvent(integrationEvent); - return Task.CompletedTask; + _channel.Subscribe(eventTypeName); } - protected override void Subscribe(string messageName) + protected override Task PublishMessageAsync(SerializedMessage serializedMessage) { - Logger.LogInformation("Subscribing to {MessageName}", messageName); _channel.EnsureOpen(); - _channel.Subscribe(messageName); + _channel.PublishMessage(serializedMessage); + return Task.CompletedTask; } - protected override void Unsubscribe(string messageName) - { - Logger.LogInformation("Unsubscribing from {MessageName}", messageName); - _channel.Unsubscribe(messageName); - } protected override void Dispose(bool disposing) { if (disposing) if (_channel != null) { - Logger.LogInformation("Closing RabbitMQ channel..."); - _channel.MessageReceived -= ChannelOnMessageReceived; + _logger.LogInformation("Closing RabbitMQ channel..."); _channel.Dispose(); - Logger.LogInformation("RabbitMQ channel closed"); + _logger.LogInformation("RabbitMQ channel closed"); } base.Dispose(disposing); } - - private class RabbitMqEventProcessingContext : EventProcessingContext - { - private readonly string _jsonString; - - public RabbitMqEventProcessingContext(object rawReceivedMessage) - { - Logger.LogTrace("Deserializing a message of type {MessageType}", rawReceivedMessage?.GetType().Name ?? "???"); - if (!(rawReceivedMessage is byte[] rawEventPayloadBytes)) throw new InvalidOperationException("Raw event payload is not a binary JSON string"); - - _jsonString = Encoding.UTF8.GetString(rawEventPayloadBytes); - var eventStub = JsonConvert.DeserializeAnonymousType(_jsonString, new {tenantId = 0, correlationId = Guid.Empty}); - TenantId = new TenantId(eventStub.tenantId); - CorrelationId = eventStub.correlationId; - } - - public override TenantId TenantId { get; } - - public override dynamic DynamicEvent => JObject.Parse(_jsonString); - public override Guid CorrelationId { get; } - - public override IIntegrationEvent GetTypedEvent(Type eventType) - { - return (IIntegrationEvent) JsonConvert.DeserializeObject(_jsonString, eventType); - } - } } } \ No newline at end of file diff --git a/src/implementations/Backend.Fx.RabbitMq/RabbitMqOptions.cs b/src/implementations/Backend.Fx.RabbitMq/RabbitMqOptions.cs new file mode 100644 index 00000000..283c9bb2 --- /dev/null +++ b/src/implementations/Backend.Fx.RabbitMq/RabbitMqOptions.cs @@ -0,0 +1,16 @@ +using JetBrains.Annotations; + +namespace Backend.Fx.RabbitMq +{ + [PublicAPI] + public class RabbitMqOptions + { + public string Hostname { get; set; } + public int Port { get; set; } = 5672; + public string Username { get; set; } + public string Password { get; set; } + public int RetryCount { get; set; } = 1; + public string ExchangeName { get; set; } + public string ReceiveQueueName { get; set; } + } +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Backend.Fx.SimpleInjectorDependencyInjection.csproj b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Backend.Fx.SimpleInjectorDependencyInjection.csproj index 133cea5a..69a5ffd5 100644 --- a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Backend.Fx.SimpleInjectorDependencyInjection.csproj +++ b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Backend.Fx.SimpleInjectorDependencyInjection.csproj @@ -25,7 +25,7 @@ - + diff --git a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorDataGenerationModule.cs b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorDataGenerationModule.cs deleted file mode 100644 index f7faa48b..00000000 --- a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorDataGenerationModule.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Reflection; -using Backend.Fx.Patterns.DataGeneration; -using Backend.Fx.Patterns.DependencyInjection; -using SimpleInjector; - -namespace Backend.Fx.SimpleInjectorDependencyInjection.Modules -{ - public class SimpleInjectorDataGenerationModule : IModule - { - private readonly Assembly[] _domainAssemblies; - - public SimpleInjectorDataGenerationModule(params Assembly[] domainAssemblies) - { - _domainAssemblies = domainAssemblies; - } - - public void Register(ICompositionRoot compositionRoot) - { - Container container = ((SimpleInjectorCompositionRoot) compositionRoot).Container; - container.Collection.Register(container.GetTypesToRegister(typeof(IDataGenerator), _domainAssemblies)); - } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorDomainModule.cs b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorDomainModule.cs deleted file mode 100644 index b0e3edd6..00000000 --- a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorDomainModule.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using Backend.Fx.BuildingBlocks; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.Authorization; -using Backend.Fx.Patterns.DataGeneration; -using Backend.Fx.Patterns.EventAggregation.Domain; -using Backend.Fx.Patterns.EventAggregation.Integration; -using Backend.Fx.Patterns.Jobs; -using Microsoft.Extensions.Logging; -using SimpleInjector; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.SimpleInjectorDependencyInjection.Modules -{ - /// - /// Wires all public domain services to be injected as scoped instances provided by the array of domain assemblies: - /// - s - /// - s - /// - s - /// - s - /// - s - /// - the collections of s - /// - 's - /// - public class SimpleInjectorDomainModule : SimpleInjectorModule - { - private static readonly ILogger Logger = Log.Create(); - private readonly Assembly[] _domainAssemblies; - private readonly string _domainAssembliesForLogging; - - public SimpleInjectorDomainModule(params Assembly[] domainAssemblies) - { - _domainAssemblies = domainAssemblies; - _domainAssembliesForLogging = string.Join(",", _domainAssemblies.Select(ass => ass.GetName().Name)); - } - - protected override void Register(Container container, ScopedLifestyle scopedLifestyle) - { - RegisterDomainAndApplicationServices(container); - - RegisterAuthorization(container); - - // all jobs are dynamically registered - foreach (Type jobType in container.GetTypesToRegister(typeof(IJob), _domainAssemblies)) - { - Logger.LogDebug("Registering {JobType}", jobType.Name); - container.Register(jobType); - } - - // domain event handlers - foreach (Type domainEventHandlerType in container.GetTypesToRegister(typeof(IDomainEventHandler<>), _domainAssemblies)) - { - Logger.LogDebug("Appending {DomainEventHandlerType} to list of IDomainEventHandler", domainEventHandlerType.Name); - container.Collection.Append(typeof(IDomainEventHandler<>), domainEventHandlerType); - } - - // integration message handlers - foreach (Type integrationMessageHandlerType in container.GetTypesToRegister(typeof(IIntegrationMessageHandler<>), _domainAssemblies)) - { - Logger.LogDebug("Registering {IntegrationEventHandler}", integrationMessageHandlerType.Name); - container.Register(integrationMessageHandlerType); - } - } - - private void RegisterDomainAndApplicationServices(Container container) - { - Logger.LogDebug("Registering domain and application services from {DomainAssemblies}", - string.Join(",", _domainAssemblies.Select(ass => ass.GetName().Name))); - var serviceRegistrations = container - .GetTypesToRegister(typeof(IDomainService), _domainAssemblies) - .Concat(container.GetTypesToRegister(typeof(IApplicationService), _domainAssemblies)) - .SelectMany(type => - type.GetTypeInfo() - .ImplementedInterfaces - .Where(i => typeof(IDomainService) != i - && typeof(IApplicationService) != i - && (i.Namespace != null && i.Namespace.StartsWith("Backend") - || _domainAssemblies.Contains(i.GetTypeInfo().Assembly))) - .Select(service => new - { - Service = service, - Implementation = type - }) - ); - foreach (var reg in serviceRegistrations) - { - Logger.LogDebug("Registering scoped service {ServiceType} with implementation {ImplementationType}", - reg.Service.Name, - reg.Implementation.Name); - container.Register(reg.Service, reg.Implementation); - } - } - - /// - /// Auto registering all aggregate authorization classes - /// - private void RegisterAuthorization(Container container) - { - Logger.LogDebug("Registering authorization services from {DomainAssemblies}", _domainAssembliesForLogging); - var aggregateRootAuthorizationTypes = container.GetTypesToRegister(typeof(IAggregateAuthorization<>), _domainAssemblies).ToArray(); - foreach (Type aggregateAuthorizationType in aggregateRootAuthorizationTypes) - { - var serviceTypes = aggregateAuthorizationType - .GetTypeInfo() - .ImplementedInterfaces - .Where(impif => impif.GetTypeInfo().IsGenericType - && impif.GenericTypeArguments.Length == 1 - && typeof(AggregateRoot).GetTypeInfo().IsAssignableFrom(impif.GenericTypeArguments[0].GetTypeInfo())); - - foreach (Type serviceType in serviceTypes) - { - Logger.LogDebug("Registering scoped authorization service {ServiceType} with implementation {ImplementationType}", - serviceType.Name, - aggregateAuthorizationType.Name); - container.Register(serviceType, aggregateAuthorizationType); - } - } - } - } -} diff --git a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorInfrastructureModule.cs b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorInfrastructureModule.cs deleted file mode 100644 index e9331a84..00000000 --- a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorInfrastructureModule.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Reflection; -using Backend.Fx.Patterns.DependencyInjection; -using SimpleInjector; - -namespace Backend.Fx.SimpleInjectorDependencyInjection.Modules -{ - public class SimpleInjectorInfrastructureModule : IInfrastructureModule - { - private readonly Container _container; - - public SimpleInjectorInfrastructureModule(Container container) - { - _container = container; - } - - public void RegisterScoped() where TService : class where TImpl : class, TService - { - _container.Register(); - } - - public void RegisterScoped(Func factory) where TService : class - { - _container.Register(factory); - } - - public void RegisterScoped(Type serviceType, Assembly[] assembliesToScan) - { - _container.Register(serviceType, assembliesToScan); - } - - public void RegisterDecorator() where TService : class where TImpl : class, TService - { - _container.RegisterDecorator(); - } - - public void RegisterScoped(Type serviceType, Type implementationType) - { - _container.Register(serviceType, implementationType); - } - - public void RegisterSingleton() where TService : class where TImpl : class, TService - { - _container.RegisterSingleton(); - } - - public void RegisterInstance(TService instance) where TService : class - { - _container.RegisterInstance(instance); - } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorModule.cs b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorModule.cs deleted file mode 100644 index 151fc3a4..00000000 --- a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorModule.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; -using Microsoft.Extensions.Logging; -using SimpleInjector; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace Backend.Fx.SimpleInjectorDependencyInjection.Modules -{ - public abstract class SimpleInjectorModule : IModule - { - private static readonly ILogger Logger = Log.Create(); - - protected abstract void Register(Container container, ScopedLifestyle scopedLifestyle); - - public virtual void Register(ICompositionRoot compositionRoot) - { - Logger.LogDebug("Registering {ServiceType}", GetType().Name); - var simpleInjectorCompositionRoot = (SimpleInjectorCompositionRoot) compositionRoot; - Register(simpleInjectorCompositionRoot.Container, simpleInjectorCompositionRoot.ScopedLifestyle); - } - } -} diff --git a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorCompositionRoot.cs b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorCompositionRoot.cs index 3da55be7..57a701ed 100644 --- a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorCompositionRoot.cs +++ b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorCompositionRoot.cs @@ -1,11 +1,10 @@ using System; -using System.Collections; using System.Collections.Generic; -using System.Threading; +using System.Linq; +using Backend.Fx.DependencyInjection; using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.Patterns.EventAggregation.Domain; -using Backend.Fx.SimpleInjectorDependencyInjection.Modules; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using SimpleInjector; using SimpleInjector.Advanced; @@ -17,122 +16,183 @@ namespace Backend.Fx.SimpleInjectorDependencyInjection /// /// Provides a reusable composition root assuming Simple Injector as container /// - public class SimpleInjectorCompositionRoot : ICompositionRoot + [PublicAPI] + public class SimpleInjectorCompositionRoot : CompositionRoot { - private static readonly ILogger Logger = Log.Create(); + private readonly ILogger _logger = Log.Create(); + private readonly IList _services = new List(); + private readonly IList _decorators = new List(); + private readonly IList _serviceCollections = new List(); - private int _scopeSequenceNumber = 1; /// /// This constructor creates a composition root that prefers scoped lifestyle /// - public SimpleInjectorCompositionRoot() + public SimpleInjectorCompositionRoot() : this(new ScopedLifestyleBehavior(), new AsyncScopedLifestyle()) - {} + { + } - public SimpleInjectorCompositionRoot(ILifestyleSelectionBehavior lifestyleBehavior, ScopedLifestyle scopedLifestyle) + public SimpleInjectorCompositionRoot( + ILifestyleSelectionBehavior lifestyleBehavior, + ScopedLifestyle scopedLifestyle) { - Logger.LogInformation("Initializing SimpleInjector"); + _logger.LogInformation("Initializing SimpleInjector"); ScopedLifestyle = scopedLifestyle; + Container.Options.LifestyleSelectionBehavior = lifestyleBehavior; - Container.Options.DefaultScopedLifestyle = ScopedLifestyle; - InfrastructureModule = new SimpleInjectorInfrastructureModule(Container); - InstanceProvider = new SimpleInjectorInstanceProvider(Container); - - // SimpleInjector 5 needs this to resolve controllers - Container.Options.ResolveUnregisteredConcreteTypes = true; + Container.Options.DefaultScopedLifestyle = scopedLifestyle; + + // required to support extension method IServiceProvider.CreateScope() + Container.RegisterInstance(new SimpleInjectorServiceScopeFactory(Container)); } + public ScopedLifestyle ScopedLifestyle { get; } + public Container Container { get; } = new Container(); - internal ScopedLifestyle ScopedLifestyle { get; } + #region ICompositionRoot implementation - public void RegisterModules(params IModule[] modules) + public override void Register(ServiceDescriptor serviceDescriptor) { - foreach (var module in modules) + if (Container.IsLocked) { - Logger.LogInformation("Registering {Module}", module.GetType().Name); - module.Register(this); + throw new InvalidOperationException("Container has been built and cannot be changed any more."); } - } - #region ICompositionRoot implementation + foreach (var descriptor in _services.Where(sd => sd.ServiceType == serviceDescriptor.ServiceType).ToArray()) + { + _services.Remove(descriptor); + } + + _services.Add(serviceDescriptor); + } - public void Verify() + public override void RegisterDecorator(ServiceDescriptor serviceDescriptor) { - Logger.LogInformation("container is being verified"); - try + if (Container.IsLocked) { - Container.Verify(VerificationOption.VerifyAndDiagnose); + throw new InvalidOperationException("Container has been built and cannot be changed any more."); } - catch (Exception ex) + + _decorators.Add(serviceDescriptor); + } + + public override void RegisterCollection(IEnumerable serviceDescriptors) + { + if (Container.IsLocked) { - Logger.LogWarning(ex, "container configuration invalid"); - throw; + throw new InvalidOperationException("Container has been built and cannot be changed any more."); } - } + var serviceDescriptorArray = serviceDescriptors as ServiceDescriptor[] ?? serviceDescriptors.ToArray(); - public object GetInstance(Type serviceType) - { - return Container.GetInstance(serviceType); - } + if (serviceDescriptorArray.Select(sd => sd.ServiceType).Distinct().Count() > 1) + { + throw new InvalidOperationException( + "To register a collection of services they must implement the same service type"); + } - public IEnumerable GetInstances(Type serviceType) - { - return Container.GetAllInstances(serviceType); + _serviceCollections.Add(serviceDescriptorArray); } - public T GetInstance() where T : class + public override void Verify() { - return Container.GetInstance(); - } + FillContainer(); - public IEnumerable GetInstances() where T : class - { - return Container.GetAllInstances(); + _logger.LogInformation("Verifying container"); + Container.Verify(VerificationOption.VerifyAndDiagnose); } - + /// - public IInjectionScope BeginScope() + public override IServiceScope BeginScope() { - return new SimpleInjectorInjectionScope(Interlocked.Increment(ref _scopeSequenceNumber), AsyncScopedLifestyle.BeginScope(Container)); + return Container.CreateScope(); } - public IInstanceProvider InstanceProvider { get; } - - public IInfrastructureModule InfrastructureModule { get; } + public override IServiceProvider ServiceProvider => Container; - public Scope GetCurrentScope() + /// + /// A behavior that defaults to scoped life style for injected instances + /// + private class ScopedLifestyleBehavior : ILifestyleSelectionBehavior { - return ScopedLifestyle.GetCurrentScope(Container); + public Lifestyle SelectLifestyle(Type implementationType) + { + return Lifestyle.Scoped; + } } - #endregion - - #region IEventHandlerProvider implementation - /// - public IEnumerable> GetAllEventHandlers() where TDomainEvent : IDomainEvent + private void FillContainer() { - return Container.GetAllInstances>(); + _logger.LogInformation("Registering services with container"); + foreach (var serviceDescriptor in _services) + { + if (serviceDescriptor.ImplementationType != null) + { + Container.Register( + serviceDescriptor.ServiceType, + serviceDescriptor.ImplementationType, + serviceDescriptor.Lifetime.MapLifestyle()); + } + else if (serviceDescriptor.ImplementationFactory != null) + { + Container.Register( + serviceDescriptor.ServiceType, + () => serviceDescriptor.ImplementationFactory(Container), + serviceDescriptor.Lifetime.MapLifestyle()); + } + else if (serviceDescriptor.ImplementationInstance != null && + serviceDescriptor.Lifetime == ServiceLifetime.Singleton) + { + Container.RegisterInstance(serviceDescriptor.ServiceType, + serviceDescriptor.ImplementationInstance); + } + else + { + throw new InvalidOperationException("Bad service descriptor"); + } + } + + foreach (var serviceDescriptor in _decorators) + { + if (serviceDescriptor.ImplementationType != null) + { + Container.RegisterDecorator( + serviceDescriptor.ServiceType, + serviceDescriptor.ImplementationType, + serviceDescriptor.Lifetime.MapLifestyle()); + } + else + { + throw new InvalidOperationException("Can only register decorators by type"); + } + } + + foreach (var serviceDescriptors in _serviceCollections) + { + foreach (var serviceDescriptor in serviceDescriptors) + { + Container.Collection.Append( + serviceDescriptor.ServiceType, + serviceDescriptor.ImplementationType ?? + throw new ArgumentException("You must provide an implementationType when registering a collection", nameof(serviceDescriptor)), + serviceDescriptor.Lifetime.MapLifestyle()); + } + } } + #endregion #region IDisposable implementation - public void Dispose() - { - Container?.Dispose(); - } - #endregion - /// - /// A behavior that defaults to scoped life style for injected instances - /// - private class ScopedLifestyleBehavior : ILifestyleSelectionBehavior + protected override void Dispose(bool disposing) { - public Lifestyle SelectLifestyle(Type implementationType) + if (disposing) { - return Lifestyle.Scoped; + Container.Dispose(); } } + + #endregion } } \ No newline at end of file diff --git a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorInjectionScope.cs b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorInjectionScope.cs deleted file mode 100644 index c22eedc3..00000000 --- a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorInjectionScope.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Backend.Fx.Patterns.DependencyInjection; -using SimpleInjector; - -namespace Backend.Fx.SimpleInjectorDependencyInjection -{ - public class SimpleInjectorInjectionScope : InjectionScope - { - private readonly Scope _scope; - - public SimpleInjectorInjectionScope(int sequenceNumber, Scope scope) : base(sequenceNumber) - { - _scope = scope; - InstanceProvider = new SimpleInjectorInstanceProvider(_scope.Container); - } - - public override IInstanceProvider InstanceProvider { get; } - - - public override void Dispose() - { - _scope.Dispose(); - } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorInstanceProvider.cs b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorInstanceProvider.cs deleted file mode 100644 index 610dd71b..00000000 --- a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorInstanceProvider.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using Backend.Fx.Patterns.DependencyInjection; -using SimpleInjector; - -namespace Backend.Fx.SimpleInjectorDependencyInjection -{ - public class SimpleInjectorInstanceProvider : IInstanceProvider - { - private readonly Container _container; - - public SimpleInjectorInstanceProvider(Container container) - { - _container = container; - } - - public object GetInstance(Type serviceType) - { - return _container.GetInstance(serviceType); - } - - public IEnumerable GetInstances(Type serviceType) - { - return (IEnumerable) _container.GetInstance(typeof(IEnumerable<>).MakeGenericType(serviceType)); - } - - public T GetInstance() where T : class - { - return _container.GetInstance(); - } - - public IEnumerable GetInstances() where T : class - { - return _container.GetInstance>(); - } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorMappings.cs b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorMappings.cs new file mode 100644 index 00000000..926efdd8 --- /dev/null +++ b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorMappings.cs @@ -0,0 +1,20 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using SimpleInjector; + +namespace Backend.Fx.SimpleInjectorDependencyInjection +{ + public static class SimpleInjectorMappings + { + public static Lifestyle MapLifestyle(this ServiceLifetime serviceLifetime) + { + switch (serviceLifetime) + { + case ServiceLifetime.Scoped: return Lifestyle.Scoped; + case ServiceLifetime.Singleton: return Lifestyle.Singleton; + case ServiceLifetime.Transient: return Lifestyle.Transient; + default: throw new ArgumentException($"Unknown ServiceLifetime {serviceLifetime}"); + } + } + } +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorServiceScope.cs b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorServiceScope.cs new file mode 100644 index 00000000..6f65de98 --- /dev/null +++ b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorServiceScope.cs @@ -0,0 +1,24 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using SimpleInjector; + +namespace Backend.Fx.SimpleInjectorDependencyInjection +{ + public class SimpleInjectorServiceScope : IServiceScope + { + private readonly Scope _scope; + + public SimpleInjectorServiceScope(Scope scope) + { + _scope = scope; + } + + public IServiceProvider ServiceProvider => _scope.Container; + + + public void Dispose() + { + _scope.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorServiceScopeFactory.cs b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorServiceScopeFactory.cs new file mode 100644 index 00000000..8bfa8f86 --- /dev/null +++ b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorServiceScopeFactory.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using SimpleInjector; +using SimpleInjector.Lifestyles; + +namespace Backend.Fx.SimpleInjectorDependencyInjection +{ + public class SimpleInjectorServiceScopeFactory : IServiceScopeFactory + { + private readonly Container _container; + + public SimpleInjectorServiceScopeFactory(Container container) + { + _container = container; + } + public IServiceScope CreateScope() + { + return new SimpleInjectorServiceScope(AsyncScopedLifestyle.BeginScope(_container)); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.AspNetCore.Tests/Backend.Fx.AspNetCore.Tests.csproj b/tests/Backend.Fx.AspNetCore.Tests/Backend.Fx.AspNetCore.Tests.csproj index 3068b0ae..ac43c0d1 100644 --- a/tests/Backend.Fx.AspNetCore.Tests/Backend.Fx.AspNetCore.Tests.csproj +++ b/tests/Backend.Fx.AspNetCore.Tests/Backend.Fx.AspNetCore.Tests.csproj @@ -1,27 +1,27 @@ - net5.0 + net6.0 false - - - - - + + + + + - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -33,6 +33,11 @@ + + + + + diff --git a/tests/Backend.Fx.AspNetCore.Tests/SampleApp/Domain/ICalculationService.cs b/tests/Backend.Fx.AspNetCore.Tests/SampleApp/Domain/ICalculationService.cs index 3e833eb7..d9bc6db7 100644 --- a/tests/Backend.Fx.AspNetCore.Tests/SampleApp/Domain/ICalculationService.cs +++ b/tests/Backend.Fx.AspNetCore.Tests/SampleApp/Domain/ICalculationService.cs @@ -1,10 +1,10 @@ using System; using System.Security.Principal; -using Backend.Fx.BuildingBlocks; -using Backend.Fx.Environment.DateAndTime; using Backend.Fx.Environment.MultiTenancy; using Backend.Fx.Exceptions; -using Backend.Fx.Patterns.DependencyInjection; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Util; +using NodaTime; namespace Backend.Fx.AspNetCore.Tests.SampleApp.Domain { @@ -17,7 +17,7 @@ public interface ICalculationService public class CalculationResult { - public CalculationResult(DateTime timestamp, string executor, double result, int tenantId) + public CalculationResult(Instant timestamp, string executor, double result, int tenantId) { Timestamp = timestamp; Executor = executor; @@ -25,7 +25,7 @@ public CalculationResult(DateTime timestamp, string executor, double result, int TenantId = tenantId; } - public DateTime Timestamp { get; } + public Instant Timestamp { get; } public string Executor { get; } public double Result { get; } public int TenantId { get; } @@ -47,17 +47,17 @@ public CalculationService(IClock clock, ICurrentTHolder identityHolde public ICalculationService.CalculationResult Add(double arg1, double arg2) { - return new(_clock.UtcNow, _identityHolder.Current.Name, arg1 + arg2, _tenantIdHolder.Current.Value); + return new(_clock.GetCurrentInstant(), _identityHolder.Current.Name, arg1 + arg2, _tenantIdHolder.Current.Value); } public ICalculationService.CalculationResult Subtract(double arg1, double arg2) { - return new(_clock.UtcNow, _identityHolder.Current.Name, arg1 - arg2, _tenantIdHolder.Current.Value); + return new(_clock.GetCurrentInstant(), _identityHolder.Current.Name, arg1 - arg2, _tenantIdHolder.Current.Value); } public ICalculationService.CalculationResult Multiply(double arg1, double arg2) { - return new(_clock.UtcNow, _identityHolder.Current.Name, arg1 * arg2, _tenantIdHolder.Current.Value); + return new(_clock.GetCurrentInstant(), _identityHolder.Current.Name, arg1 * arg2, _tenantIdHolder.Current.Value); } public ICalculationService.CalculationResult Divide(double arg1, double arg2) @@ -69,7 +69,7 @@ public ICalculationService.CalculationResult Divide(double arg1, double arg2) } - return new(_clock.UtcNow, _identityHolder.Current.Name, arg1 / arg2, _tenantIdHolder.Current.Value); + return new(_clock.GetCurrentInstant(), _identityHolder.Current.Name, arg1 / arg2, _tenantIdHolder.Current.Value); } } } \ No newline at end of file diff --git a/tests/Backend.Fx.AspNetCore.Tests/SampleApp/Domain/JwtService.cs b/tests/Backend.Fx.AspNetCore.Tests/SampleApp/Domain/JwtService.cs index a56c80c8..24959265 100644 --- a/tests/Backend.Fx.AspNetCore.Tests/SampleApp/Domain/JwtService.cs +++ b/tests/Backend.Fx.AspNetCore.Tests/SampleApp/Domain/JwtService.cs @@ -30,7 +30,7 @@ public static TokenValidationParameters TokenValidationParameters() { return new TokenValidationParameters { - NameClaimTypeRetriever = (token, s) => ClaimTypes.Name, + NameClaimTypeRetriever = (_, _) => ClaimTypes.Name, ValidateIssuerSigningKey = true, IssuerSigningKey = SigningCredentials.Key, ValidateIssuer = true, diff --git a/tests/Backend.Fx.AspNetCore.Tests/SampleApp/Runtime/SampleApplication.cs b/tests/Backend.Fx.AspNetCore.Tests/SampleApp/Runtime/SampleApplication.cs deleted file mode 100644 index 6d78bef5..00000000 --- a/tests/Backend.Fx.AspNetCore.Tests/SampleApp/Runtime/SampleApplication.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DataGeneration; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.Patterns.EventAggregation.Integration; -using Backend.Fx.SimpleInjectorDependencyInjection; -using Backend.Fx.SimpleInjectorDependencyInjection.Modules; - -namespace Backend.Fx.AspNetCore.Tests.SampleApp.Runtime -{ - public class SampleApplication : IBackendFxApplication - { - private readonly IBackendFxApplication _application; - - public SampleApplication(ITenantIdProvider tenantIdProvider, IExceptionLogger exceptionLogger) - { - ITenantWideMutexManager tenantWideMutexManager = new InMemoryTenantWideMutexManager(); - Assembly domainAssembly = GetType().Assembly; - - _application = new BackendFxApplication( - new SimpleInjectorCompositionRoot(), - new InMemoryMessageBus(), - exceptionLogger); - - _application = new DataGeneratingApplication( - tenantIdProvider, - new SimpleInjectorDataGenerationModule(domainAssembly), - tenantWideMutexManager, _application); - - _application.CompositionRoot.RegisterModules( - new SimpleInjectorDomainModule(domainAssembly)); - } - - public void Dispose() - { - _application.Dispose(); - } - - public IBackendFxApplicationAsyncInvoker AsyncInvoker => _application.AsyncInvoker; - - public ICompositionRoot CompositionRoot => _application.CompositionRoot; - - public IBackendFxApplicationInvoker Invoker => _application.Invoker; - - public IMessageBus MessageBus => _application.MessageBus; - - public bool WaitForBoot(int timeoutMilliSeconds = 2147483647, CancellationToken cancellationToken = default) - { - return _application.WaitForBoot(timeoutMilliSeconds, cancellationToken); - } - - public Task Boot(CancellationToken cancellationToken = default) => BootAsync(cancellationToken); - public Task BootAsync(CancellationToken cancellationToken = default) - { - return _application.BootAsync(cancellationToken); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.AspNetCore.Tests/SampleApp/SampleApplicationHostedService.cs b/tests/Backend.Fx.AspNetCore.Tests/SampleApp/SampleApplicationHostedService.cs index 5a20a3ca..74e434cb 100644 --- a/tests/Backend.Fx.AspNetCore.Tests/SampleApp/SampleApplicationHostedService.cs +++ b/tests/Backend.Fx.AspNetCore.Tests/SampleApp/SampleApplicationHostedService.cs @@ -1,22 +1,27 @@ -using Backend.Fx.AspNetCore.Tests.SampleApp.Runtime; +using Backend.Fx.AspNetCore.Bootstrapping; using Backend.Fx.Environment.MultiTenancy; using Backend.Fx.InMemoryPersistence; using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.Patterns.EventAggregation.Integration; +using Backend.Fx.SimpleInjectorDependencyInjection; namespace Backend.Fx.AspNetCore.Tests.SampleApp { public class SampleApplicationHostedService : BackendFxApplicationHostedService { - public ITenantService TenantService { get; } - public override IBackendFxApplication Application { get; } - public SampleApplicationHostedService(IExceptionLogger exceptionLogger) { - IMessageBus messageBus = new InMemoryMessageBus(); - TenantService = new TenantService(messageBus, new InMemoryTenantRepository()); - Application = new SampleApplication(TenantService.TenantIdProvider, exceptionLogger); + IBackendFxApplication application = new BackendFxApplication( + new SimpleInjectorCompositionRoot(), + exceptionLogger, + typeof(SampleApplicationHostedService).Assembly); + + application = new AspNetCoreApplication(application); + + Application = application; } + + public ITenantService TenantService { get; } = new TenantService(new InMemoryTenantRepository()); + + public override IBackendFxApplication Application { get; } } } \ No newline at end of file diff --git a/tests/Backend.Fx.AspNetCore.Tests/TheBackendFxMvcApplication.cs b/tests/Backend.Fx.AspNetCore.Tests/TheBackendFxMvcApplication.cs index 5107f7d5..4073e9dd 100644 --- a/tests/Backend.Fx.AspNetCore.Tests/TheBackendFxMvcApplication.cs +++ b/tests/Backend.Fx.AspNetCore.Tests/TheBackendFxMvcApplication.cs @@ -2,7 +2,7 @@ using System.Net.Http; using System.Threading.Tasks; using Backend.Fx.AspNetCore.Tests.SampleApp.Domain; -using Backend.Fx.Tests; +using Backend.Fx.TestUtil; using Newtonsoft.Json; using Xunit; using Xunit.Abstractions; @@ -16,7 +16,7 @@ public class TheBackendFxMvcApplication : TestWithLogging public TheBackendFxMvcApplication(ITestOutputHelper output) : base(output) { - _factory = new SampleAppWebApplicationFactory(base.Logger); + _factory = new SampleAppWebApplicationFactory(Logger); } [Fact] diff --git a/tests/Backend.Fx.AspNetCore.Tests/TheMultiTenantApplication.cs b/tests/Backend.Fx.AspNetCore.Tests/TheMultiTenantApplication.cs index 1a52f798..185cccbf 100644 --- a/tests/Backend.Fx.AspNetCore.Tests/TheMultiTenantApplication.cs +++ b/tests/Backend.Fx.AspNetCore.Tests/TheMultiTenantApplication.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using Backend.Fx.AspNetCore.MultiTenancy; using Backend.Fx.RandomData; -using Backend.Fx.Tests; +using Backend.Fx.TestUtil; using Newtonsoft.Json; using Xunit; using Xunit.Abstractions; @@ -15,7 +15,7 @@ public class TheMultiTenantApplication: TestWithLogging public TheMultiTenantApplication(ITestOutputHelper output) : base(output) { - _factory = new SampleAppWebApplicationFactory(base.Logger); + _factory = new SampleAppWebApplicationFactory(Logger); } [Fact] diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/Backend.Fx.EfCore6Persistence.Tests.csproj b/tests/Backend.Fx.EfCore6Persistence.Tests/Backend.Fx.EfCore6Persistence.Tests.csproj index 0187d99b..3429a12b 100644 --- a/tests/Backend.Fx.EfCore6Persistence.Tests/Backend.Fx.EfCore6Persistence.Tests.csproj +++ b/tests/Backend.Fx.EfCore6Persistence.Tests/Backend.Fx.EfCore6Persistence.Tests.csproj @@ -5,33 +5,42 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - + + + + diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/DbConnectionEx.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/DbConnectionEx.cs deleted file mode 100644 index 499d8a51..00000000 --- a/tests/Backend.Fx.EfCore6Persistence.Tests/DbConnectionEx.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using JetBrains.Annotations; - -namespace Backend.Fx.EfCorePersistence.Tests -{ - public static class DbConnectionEx - { - public static void ExecuteNonQuery(this IDbConnection openConnection, string cmd) - { - using (IDbCommand command = openConnection.CreateCommand()) - { - command.CommandText = cmd; - command.ExecuteNonQuery(); - } - } - - public static T ExecuteScalar(this IDbConnection openConnection, string cmd) - { - using (IDbCommand command = openConnection.CreateCommand()) - { - command.CommandText = cmd; - object scalarResult = command.ExecuteScalar(); - if (typeof(T) == typeof(int)) return (T) (object) Convert.ToInt32(scalarResult); - return (T) scalarResult; - } - } - - [UsedImplicitly] - public static IEnumerable ExecuteReader(this IDbConnection openConnection, string cmd, Func forEachResultFunc) - { - using (IDbCommand command = openConnection.CreateCommand()) - { - command.CommandText = cmd; - IDataReader reader = command.ExecuteReader(); - while (reader.NextResult()) yield return forEachResultFunc(reader); - } - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyAggregates/Article.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyAggregates/Article.cs new file mode 100644 index 00000000..bb307464 --- /dev/null +++ b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyAggregates/Article.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using Backend.Fx.Domain; +using Backend.Fx.Features.IdGeneration; +using JetBrains.Annotations; + +namespace Backend.Fx.EfCore6Persistence.Tests.DummyAggregates; + +/// +/// An aggregate with nested collection +/// +public sealed class Article : Identified, IAggregateRoot +{ + private readonly ISet _variants = new SortedSet(Comparer.Create((left, right) => left.Id.CompareTo(right.Id))); + + [UsedImplicitly] + private Article() + { } + + public Article(IIdGenerator idGen, string sku, string name, IEnumerable variants) + : base(idGen.NextId()) + { + Sku = sku; + Name = name; + foreach (ArticleVariant variant in variants) + { + variant.Id = idGen.NextId(); + _variants.Add(variant); + } + } + + public string Sku { get; [UsedImplicitly] private set; } + + public string Name { get; [UsedImplicitly] private set; } + + public IEnumerable Variants => _variants; + + public static Article CreateNewArticle(IEntityIdGenerator entityIdGenerator) + { + var random = new Random(); + var article = new Article( + entityIdGenerator, + $"SKU-{random.Next(10000, 99999)}", + "Article {random.Next(10000,99999)}", + new[] + { + new ArticleVariant("Green", "XL"), + new ArticleVariant("Blue", "S"), + new ArticleVariant("Black", "M"), + }); + + return article; + } +} + +public class ArticleVariant +{ + public ArticleVariant(string color, string size) + { + Color = color; + Size = size; + } + + public int Id { get; set; } + + public string Color { get; private set; } + public string Size { get; private set; } + +} diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyAggregates/Article2.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyAggregates/Article2.cs new file mode 100644 index 00000000..8745d7a5 --- /dev/null +++ b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyAggregates/Article2.cs @@ -0,0 +1,113 @@ + +using System; +using System.Collections.Generic; +using Backend.Fx.Domain; +using Backend.Fx.Features.IdGeneration; +using JetBrains.Annotations; + +namespace Backend.Fx.EfCore6Persistence.Tests.DummyAggregates; + + +/// +/// An aggregate with nested collection that uses value type +/// +public sealed class Article2 : Identified, IAggregateRoot +{ + private readonly ISet _variants = new SortedSet(Comparer.Create((left, right) => left.Id.CompareTo(right.Id))); + + [UsedImplicitly] + private Article2() + { } + + public Article2(IEntityIdGenerator idGen, string sku, string name, IEnumerable variants) + : base(idGen.NextId()) + { + Sku = sku; + Name = name; + foreach (ArticleVariant2 variant in variants) + { + variant.Id = idGen.NextId(); + _variants.Add(variant); + } + } + + public string Sku { get; [UsedImplicitly] private set; } + + public string Name { get; [UsedImplicitly] private set; } + + public IEnumerable Variants => _variants; + + public static Article2 CreateNewArticle(IEntityIdGenerator entityIdGenerator) + { + var random = new Random(); + var article = new Article2( + entityIdGenerator, + $"SKU-{random.Next(10000, 99999)}", + "Article {random.Next(10000,99999)}", + new[] + { + new ArticleVariant2("Green", "XL"), + new ArticleVariant2("Blue", "S"), + new ArticleVariant2("Black", "M"), + }); + + return article; + } + + public void ReplaceVariants(IEntityIdGenerator idGen, ArticleVariant2[] variants) + { + _variants.Clear(); + AddVariants(idGen, variants); + + } + + public void AddVariants(IEntityIdGenerator idGen, ArticleVariant2[] variants) + { + foreach (ArticleVariant2 variant in variants) + { + variant.Id = idGen.NextId(); + _variants.Add(variant); + } + } + + public void RemoveVariant(ArticleVariant2 variant) + { + _variants.Remove(variant); + } +} + +public class ArticleVariant2 +{ + [UsedImplicitly] + private ArticleVariant2() + { } + + public ArticleVariant2(string color, string size) + { + ColorAndSize = new ColorAndSize(color, size); + } + + public int Id { get; set; } + + public ColorAndSize ColorAndSize { get; private set; } + +} + +public class ColorAndSize : ValueObject +{ + public ColorAndSize(string color, string size) + { + Color = color; + Size = size; + } + + public string Color { get; private set; } + + public string Size { get; private set; } + + protected override IEnumerable GetEqualityComponents() + { + yield return Color; + yield return Size; + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyAggregates/Supplier.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyAggregates/Supplier.cs new file mode 100644 index 00000000..d9837bdd --- /dev/null +++ b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyAggregates/Supplier.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using Backend.Fx.Domain; +using Backend.Fx.Features.IdGeneration; +using Bogus; +using JetBrains.Annotations; + +namespace Backend.Fx.EfCore6Persistence.Tests.DummyAggregates; + +/// +/// An aggregate that contains a value type: Address +/// +public sealed class Supplier : Identified, IAggregateRoot +{ + [UsedImplicitly] + private Supplier() { } + + public Supplier(int id, string name, Address postalAddress) : base(id) + { + Id = id; + Name = name; + PostalAddress = postalAddress; + } + + public string Name { get; [UsedImplicitly] init; } + + public Address PostalAddress { get; [UsedImplicitly] init; } + + public static Supplier CreateNewSupplier(IEntityIdGenerator entityIdGenerator) + { + var faker = new Faker(); + + var supplier = new Supplier(entityIdGenerator.NextId(), faker.Company.CompanyName(), new Address( + faker.Address.StreetAddress(), faker.Address.SecondaryAddress(), faker.Address.City(), + faker.Address.ZipCode(), faker.Address.State(), faker.Address.Country())); + + return supplier; + } +} + +public class Address : ValueObject +{ + [UsedImplicitly] + private Address() + { } + + public Address(string line1, string line2, string city, string postalCode, string state, string country) + { + Line1 = line1; + Line2 = line2; + City = city; + PostalCode = postalCode; + State = state; + Country = country; + } + + public string Line1 { get; init; } + public string Line2 { get; init; } + public string City { get; init; } + public string PostalCode { get; init; } + public string State { get; init; } + public string Country { get; init; } + + protected override IEnumerable GetEqualityComponents() + { + yield return Line1; + yield return Line2; + yield return City; + yield return PostalCode; + yield return State; + yield return Country; + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyImpl/Domain/Blog.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyImpl/Domain/Blog.cs deleted file mode 100644 index 27c8be41..00000000 --- a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyImpl/Domain/Blog.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using Backend.Fx.BuildingBlocks; -using Backend.Fx.Patterns.IdGeneration; -using JetBrains.Annotations; - -namespace Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain -{ - public class Blog : AggregateRoot - { - [UsedImplicitly] - private Blog() - { - } - - public Blog(int id, string name) : base(id) - { - Name = name; - } - - public string Name { get; private set; } - - public ISet Posts { get; } = new HashSet(); - - public Post AddPost(IEntityIdGenerator idGenerator, string name, bool isPublic = false) - { - var post = new Post(idGenerator.NextId(), this, name, isPublic); - Posts.Add(post); - return post; - } - - public void Modify(string modified) - { - Name = modified; - foreach (Post post in Posts) post.SetName(modified); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyImpl/Domain/BlogAuthorization.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyImpl/Domain/BlogAuthorization.cs deleted file mode 100644 index 958e27a8..00000000 --- a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyImpl/Domain/BlogAuthorization.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Backend.Fx.Patterns.Authorization; -using JetBrains.Annotations; - -namespace Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain -{ - [UsedImplicitly] - public class BlogAuthorization : AllowAll - { - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyImpl/Domain/Blogger.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyImpl/Domain/Blogger.cs deleted file mode 100644 index a21db5ab..00000000 --- a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyImpl/Domain/Blogger.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Backend.Fx.BuildingBlocks; -using JetBrains.Annotations; - -namespace Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain -{ - public class Blogger : AggregateRoot - { - [UsedImplicitly] - private Blogger() - { - } - - public Blogger(int id, string lastName, string firstName) : base(id) - { - LastName = lastName; - FirstName = firstName; - } - - public string LastName { get; set; } - public string FirstName { get; set; } - public string Bio { get; set; } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyImpl/Domain/Post.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyImpl/Domain/Post.cs deleted file mode 100644 index e71875c0..00000000 --- a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyImpl/Domain/Post.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using Backend.Fx.BuildingBlocks; -using JetBrains.Annotations; - -namespace Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain -{ - public class Post : Entity - { - [UsedImplicitly] - private Post() - { - } - - public Post(int id, Blog blog, string name, bool isPublic = false) : base(id) - { - Blog = blog; - BlogId = blog.Id; - Name = name; - TargetAudience = new TargetAudience {IsPublic = isPublic, Culture = "fr-FR"}; - } - - [UsedImplicitly] public int BlogId { get; private set; } - - [UsedImplicitly] public Blog Blog { get; private set; } - - [UsedImplicitly] public string Name { get; private set; } - - [UsedImplicitly] public TargetAudience TargetAudience { get; set; } - - public void SetName(string name) - { - Name = name; - } - } - - public class TargetAudience : ValueObject - { - public string Culture { get; set; } - - public bool IsPublic { get; set; } - - protected override IEnumerable GetEqualityComponents() - { - yield return Culture; - yield return IsPublic; - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyImpl/Persistence/BlogMapping.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyImpl/Persistence/BlogMapping.cs deleted file mode 100644 index 4f21dddb..00000000 --- a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyImpl/Persistence/BlogMapping.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain; -using Microsoft.EntityFrameworkCore; - -namespace Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence -{ - public class BlogMapping : AggregateMapping - { - public override IEnumerable>> IncludeDefinitions - { - get - { - return new Expression>[] - { - blog => blog.Posts - }; - } - } - - public override void ApplyEfMapping(ModelBuilder modelBuilder) - { - modelBuilder.Entity().OwnsOne(p => p.TargetAudience); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyImpl/Persistence/BloggerMapping.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyImpl/Persistence/BloggerMapping.cs deleted file mode 100644 index 2659bcd9..00000000 --- a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyImpl/Persistence/BloggerMapping.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain; - -namespace Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence -{ - public class BloggerMapping : PlainAggregateMapping - { - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyImpl/Persistence/TestDbContext.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyImpl/Persistence/TestDbContext.cs deleted file mode 100644 index bd40f2f5..00000000 --- a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyImpl/Persistence/TestDbContext.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain; -using Backend.Fx.Environment.MultiTenancy; -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; - -namespace Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence -{ - public sealed class TestDbContext : DbContext - { - public TestDbContext([NotNull] DbContextOptions options) : base(options) - { - Database.AutoTransactionsEnabled = false; - } - - public DbSet Bloggers { get; [UsedImplicitly] set; } - - public DbSet Blogs { get; [UsedImplicitly] set; } - public DbSet Posts { get; [UsedImplicitly] set; } - public DbSet Tenants { get; [UsedImplicitly] set; } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - this.ApplyAggregateMappings(modelBuilder); - modelBuilder.RegisterRowVersionProperty(); - modelBuilder.RegisterEntityIdAsNeverGenerated(); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyImpl/Persistence/TestDbContextFactory.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyImpl/Persistence/TestDbContextFactory.cs deleted file mode 100644 index cefd3713..00000000 --- a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyImpl/Persistence/TestDbContextFactory.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Design; - -namespace Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence -{ - [UsedImplicitly] - [Obsolete("Only for migration support at design time")] - public class TestDbContextFactory : IDesignTimeDbContextFactory - { - public TestDbContext CreateDbContext(string[] args) - { - return new TestDbContext(new DbContextOptionsBuilder().UseSqlite("DataSource=:memory:").Options); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyPersistence/Article2EntityTypeConfiguration.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyPersistence/Article2EntityTypeConfiguration.cs new file mode 100644 index 00000000..46452ba1 --- /dev/null +++ b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyPersistence/Article2EntityTypeConfiguration.cs @@ -0,0 +1,17 @@ +using Backend.Fx.EfCore6Persistence.Tests.DummyAggregates; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Backend.Fx.EfCore6Persistence.Tests.DummyPersistence; + +public class Article2EntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.OwnsMany(art => art.Variants, bld => + { + bld.OwnsOne(v => v.ColorAndSize); + bld.WithOwner(); + }); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyPersistence/ArticleEntityTypeConfiguration.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyPersistence/ArticleEntityTypeConfiguration.cs new file mode 100644 index 00000000..3c53472e --- /dev/null +++ b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyPersistence/ArticleEntityTypeConfiguration.cs @@ -0,0 +1,13 @@ +using Backend.Fx.EfCore6Persistence.Tests.DummyAggregates; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Backend.Fx.EfCore6Persistence.Tests.DummyPersistence; + +public class ArticleEntityTypeConfiguration : IEntityTypeConfiguration
+{ + public void Configure(EntityTypeBuilder
builder) + { + builder.OwnsMany(art => art.Variants).WithOwner(); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyPersistence/DummyDatabaseBootstrapper.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyPersistence/DummyDatabaseBootstrapper.cs new file mode 100644 index 00000000..77f558f0 --- /dev/null +++ b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyPersistence/DummyDatabaseBootstrapper.cs @@ -0,0 +1,33 @@ +using System.Data; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Features.MultiTenancy; +using Backend.Fx.Features.Persistence; +using Backend.Fx.Features.Persistence.AdoNet; + +namespace Backend.Fx.EfCore6Persistence.Tests.DummyPersistence; + +public class DummyDatabaseBootstrapper : IDatabaseBootstrapper +{ + private readonly IDbConnectionFactory _dbConnectionFactory; + private readonly IDbContextOptionsFactory _dbContextOptionsFactory; + + public DummyDatabaseBootstrapper(IDbConnectionFactory dbConnectionFactory, IDbContextOptionsFactory dbContextOptionsFactory) + { + _dbConnectionFactory = dbConnectionFactory; + _dbContextOptionsFactory = dbContextOptionsFactory; + } + + public void Dispose() + { + } + + public async Task EnsureDatabaseExistenceAsync(CancellationToken cancellationToken) + { + using IDbConnection dbConnection = _dbConnectionFactory.Create(); + await using var dbContext = new DummyDbContext( + new CurrentTenantIdHolder(), + _dbContextOptionsFactory.GetDbContextOptions(dbConnection)); + await dbContext.Database.EnsureCreatedAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyPersistence/DummyDbContext.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyPersistence/DummyDbContext.cs new file mode 100644 index 00000000..1447e102 --- /dev/null +++ b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyPersistence/DummyDbContext.cs @@ -0,0 +1,25 @@ +using Backend.Fx.EfCore6Persistence.Tests.DummyAggregates; +using Backend.Fx.Features.MultiTenancy; +using Backend.Fx.Util; +using Microsoft.EntityFrameworkCore; + +namespace Backend.Fx.EfCore6Persistence.Tests.DummyPersistence; + +public class DummyDbContext : MultiTenancyDbContext +{ + public DummyDbContext( ICurrentTHolder tenantIdHolder, DbContextOptions options) + : base(tenantIdHolder, options) + { + } + + public DbSet Suppliers { get; set; } + + public DbSet
Articles { get; set; } + public DbSet Articles2 { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfigurationsFromAssembly(typeof(DummyDbContext).Assembly); + base.OnModelCreating(modelBuilder); + } +} diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyPersistence/DummyDbContextOptionsFactory.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyPersistence/DummyDbContextOptionsFactory.cs new file mode 100644 index 00000000..b3b66f0c --- /dev/null +++ b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyPersistence/DummyDbContextOptionsFactory.cs @@ -0,0 +1,15 @@ +using System.Data; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +namespace Backend.Fx.EfCore6Persistence.Tests.DummyPersistence; + +public class DummyDbContextOptionsFactory : IDbContextOptionsFactory +{ + public DbContextOptions GetDbContextOptions(IDbConnection dbConnection) + { + return new DbContextOptionsBuilder() + .UseSqlite((SqliteConnection)dbConnection, opt => opt.UseNodaTime()) + .Options; + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/DummyPersistence/SupplierEntityTypeConfiguration.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyPersistence/SupplierEntityTypeConfiguration.cs new file mode 100644 index 00000000..22e2a72d --- /dev/null +++ b/tests/Backend.Fx.EfCore6Persistence.Tests/DummyPersistence/SupplierEntityTypeConfiguration.cs @@ -0,0 +1,13 @@ +using Backend.Fx.EfCore6Persistence.Tests.DummyAggregates; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Backend.Fx.EfCore6Persistence.Tests.DummyPersistence; + +public class SupplierEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.OwnsOne(s => s.PostalAddress); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/Fixtures/DatabaseFixture.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/Fixtures/DatabaseFixture.cs deleted file mode 100644 index 59578f8c..00000000 --- a/tests/Backend.Fx.EfCore6Persistence.Tests/Fixtures/DatabaseFixture.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Data; -using System.Security.Principal; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence; -using Backend.Fx.Environment.Authentication; -using Backend.Fx.Environment.DateAndTime; -using Backend.Fx.Environment.Persistence; -using Microsoft.EntityFrameworkCore; - -namespace Backend.Fx.EfCorePersistence.Tests.Fixtures -{ - public abstract class DatabaseFixture - { - public void CreateDatabase() - { - using (var dbContext = new TestDbContext(GetDbContextOptionsForDbCreation())) - { - dbContext.Database.EnsureCreated(); - } - } - - protected abstract DbContextOptions GetDbContextOptionsForDbCreation(); - - public abstract DbContextOptionsBuilder GetDbContextOptionsBuilder(IDbConnection connection); - - public abstract DbConnectionOperationDecorator UseOperation(); - - public TestDbSession CreateTestDbSession(DbConnectionOperationDecorator operation = null, IIdentity asIdentity = null, IClock clock = null) - { - CurrentIdentityHolder CreateAsIdentity() - { - var cih = new CurrentIdentityHolder(); - cih.ReplaceCurrent(asIdentity); - return cih; - } - - clock ??= new WallClock(); - operation ??= UseOperation(); - - operation.Begin(); - - var identityHolder = asIdentity == null - ? CurrentIdentityHolder.CreateSystem() - : CreateAsIdentity(); - return new TestDbSession(this, operation, identityHolder, clock); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/Fixtures/SqlServerDatabaseFixture.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/Fixtures/SqlServerDatabaseFixture.cs deleted file mode 100644 index 5f57d5b2..00000000 --- a/tests/Backend.Fx.EfCore6Persistence.Tests/Fixtures/SqlServerDatabaseFixture.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Data; -using System.Data.SqlClient; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence; -using Backend.Fx.Environment.Persistence; -using Backend.Fx.Patterns.DependencyInjection; -using Microsoft.EntityFrameworkCore; - -namespace Backend.Fx.EfCorePersistence.Tests.Fixtures -{ - [Obsolete("Not supported on build agents")] - public class SqlServerDatabaseFixture : DatabaseFixture - { - private static int _testindex = 1; - private readonly string _connectionString; - - public SqlServerDatabaseFixture() - { - var dbName = $"TestFixture_{_testindex++:000}"; - var sqlConnectionStringBuilder = new SqlConnectionStringBuilder("Server=.\\SQLExpress;Trusted_Connection=True;"); - using (IDbConnection connection = new SqlConnection(sqlConnectionStringBuilder.ConnectionString)) - { - connection.Open(); - - using (IDbCommand dropCommand = connection.CreateCommand()) - { - dropCommand.CommandText = $"IF EXISTS(SELECT * FROM sys.Databases WHERE Name='{dbName}') ALTER DATABASE [{dbName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE"; - dropCommand.ExecuteNonQuery(); - } - - using (IDbCommand dropCommand = connection.CreateCommand()) - { - dropCommand.CommandText = $"IF EXISTS(SELECT * FROM sys.Databases WHERE Name='{dbName}') DROP DATABASE [{dbName}]"; - dropCommand.ExecuteNonQuery(); - } - - using (IDbCommand createCommand = connection.CreateCommand()) - { - createCommand.CommandText = $"CREATE DATABASE [{dbName}]"; - createCommand.ExecuteNonQuery(); - } - - connection.Close(); - } - - sqlConnectionStringBuilder.InitialCatalog = dbName; - _connectionString = sqlConnectionStringBuilder.ConnectionString; - } - - protected override DbContextOptions GetDbContextOptionsForDbCreation() - { - return new DbContextOptionsBuilder().UseSqlServer(_connectionString).Options; - } - - - public override DbContextOptionsBuilder GetDbContextOptionsBuilder(IDbConnection connection) - { - return new DbContextOptionsBuilder().UseSqlServer((SqlConnection) connection); - } - - public override DbConnectionOperationDecorator UseOperation() - { - var sqliteConnection = new SqlConnection(_connectionString); - IOperation operation = new Operation(); - operation = new DbTransactionOperationDecorator(sqliteConnection, operation); - return new DbConnectionOperationDecorator(sqliteConnection, operation); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/Fixtures/SqliteDatabaseFixture.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/Fixtures/SqliteDatabaseFixture.cs deleted file mode 100644 index 18440c2c..00000000 --- a/tests/Backend.Fx.EfCore6Persistence.Tests/Fixtures/SqliteDatabaseFixture.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Data; -using System.IO; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence; -using Backend.Fx.Environment.Persistence; -using Backend.Fx.Patterns.DependencyInjection; -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; - -namespace Backend.Fx.EfCorePersistence.Tests.Fixtures -{ - public class SqliteDatabaseFixture : DatabaseFixture - { - private readonly string _connectionString = "Data Source=" + Path.GetTempFileName(); - - protected override DbContextOptions GetDbContextOptionsForDbCreation() - { - return new DbContextOptionsBuilder().UseSqlite(_connectionString).Options; - } - - public override DbContextOptionsBuilder GetDbContextOptionsBuilder(IDbConnection connection) - { - return new DbContextOptionsBuilder().UseSqlite((SqliteConnection) connection); - } - - public override DbConnectionOperationDecorator UseOperation() - { - var sqliteConnection = new SqliteConnection(_connectionString); - IOperation operation = new Operation(); - operation = new DbTransactionOperationDecorator(sqliteConnection, operation); - return new DbConnectionOperationDecorator(sqliteConnection, operation); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/Fixtures/SqliteTestDatabase.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/Fixtures/SqliteTestDatabase.cs new file mode 100644 index 00000000..ae1460ed --- /dev/null +++ b/tests/Backend.Fx.EfCore6Persistence.Tests/Fixtures/SqliteTestDatabase.cs @@ -0,0 +1,26 @@ +using System.Data; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Data.Sqlite; + +namespace Backend.Fx.EfCore6Persistence.Tests.Fixtures; + +public class SqliteTestDatabase : TestDatabase +{ + private readonly string _fileName = Path.GetTempFileName(); + + public override string ConnectionString => $"Data Source={_fileName};"; + + public override IDbConnection Create() + { + return new SqliteConnection(ConnectionString); + } + + public override async Task EnsureDatabaseExistenceAsync(CancellationToken cancellationToken) + { + await using var dbConnection = new SqliteConnection(ConnectionString); + await dbConnection.OpenAsync(cancellationToken); + await dbConnection.CloseAsync(); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/Fixtures/TestDatabase.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/Fixtures/TestDatabase.cs new file mode 100644 index 00000000..58080876 --- /dev/null +++ b/tests/Backend.Fx.EfCore6Persistence.Tests/Fixtures/TestDatabase.cs @@ -0,0 +1,30 @@ +using System; +using System.Data; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Features.Persistence; +using Backend.Fx.Features.Persistence.AdoNet; + +namespace Backend.Fx.EfCore6Persistence.Tests.Fixtures; + +public abstract class TestDatabase : IDatabaseBootstrapper, IDbConnectionFactory +{ + public abstract string ConnectionString { get; } + + public abstract IDbConnection Create(); + + public abstract Task EnsureDatabaseExistenceAsync(CancellationToken cancellationToken); + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/Fixtures/TestDbSession.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/Fixtures/TestDbSession.cs deleted file mode 100644 index 8fe47f7f..00000000 --- a/tests/Backend.Fx.EfCore6Persistence.Tests/Fixtures/TestDbSession.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Data; -using System.Security.Principal; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence; -using Backend.Fx.Environment.DateAndTime; -using Backend.Fx.Environment.Persistence; -using Backend.Fx.Patterns.DependencyInjection; - -namespace Backend.Fx.EfCorePersistence.Tests.Fixtures -{ - public class TestDbSession : ICanFlush, IDisposable - { - private readonly DbConnectionOperationDecorator _operation; - private readonly EfFlush _efFlush; - - public TestDbSession(DatabaseFixture fixture, DbConnectionOperationDecorator operation, ICurrentTHolder identityHolder, IClock clock) - { - _operation = operation; - DbContext = new TestDbContext(fixture.GetDbContextOptionsBuilder(operation.DbConnection).Options); - _efFlush = new EfFlush(DbContext, identityHolder, clock); - DbConnection = operation.DbConnection; - } - - - public TestDbContext DbContext { get; } - public IDbConnection DbConnection { get; } - - public void Flush() - { - _efFlush.Flush(); - } - - public void Dispose() - { - _efFlush.Flush(); - DbContext.Dispose(); - _operation.Complete(); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/Migrations/20190624150947_Initial.Designer.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/Migrations/20190624150947_Initial.Designer.cs deleted file mode 100644 index 1c1cdd8b..00000000 --- a/tests/Backend.Fx.EfCore6Persistence.Tests/Migrations/20190624150947_Initial.Designer.cs +++ /dev/null @@ -1,155 +0,0 @@ -// -using System; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace Backend.Fx.EfCorePersistence.Tests.Migrations -{ - [DbContext(typeof(TestDbContext))] - [Migration("20190624150947_Initial")] - partial class Initial - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.1.1-rtm-30846"); - - modelBuilder.Entity("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.Blog", b => - { - b.Property("Id"); - - b.Property("ChangedBy") - .HasMaxLength(100); - - b.Property("ChangedOn"); - - b.Property("CreatedBy") - .HasMaxLength(100); - - b.Property("CreatedOn"); - - b.Property("Name"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate(); - - b.Property("TenantId"); - - b.HasKey("Id"); - - b.ToTable("Blogs"); - }); - - modelBuilder.Entity("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.Blogger", b => - { - b.Property("Id"); - - b.Property("Bio"); - - b.Property("ChangedBy") - .HasMaxLength(100); - - b.Property("ChangedOn"); - - b.Property("CreatedBy") - .HasMaxLength(100); - - b.Property("CreatedOn"); - - b.Property("FirstName"); - - b.Property("LastName"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate(); - - b.Property("TenantId"); - - b.HasKey("Id"); - - b.ToTable("Bloggers"); - }); - - modelBuilder.Entity("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.Post", b => - { - b.Property("Id"); - - b.Property("BlogId"); - - b.Property("ChangedBy") - .HasMaxLength(100); - - b.Property("ChangedOn"); - - b.Property("CreatedBy") - .HasMaxLength(100); - - b.Property("CreatedOn"); - - b.Property("Name"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate(); - - b.HasKey("Id"); - - b.HasIndex("BlogId"); - - b.ToTable("Posts"); - }); - - modelBuilder.Entity("Backend.Fx.Environment.MultiTenancy.Tenant", b => - { - b.Property("Id") - .ValueGeneratedOnAdd(); - - b.Property("DefaultCultureName"); - - b.Property("Description"); - - b.Property("IsDemoTenant"); - - b.Property("Name") - .IsRequired(); - - b.Property("State"); - - b.HasKey("Id"); - - b.ToTable("Tenants"); - }); - - modelBuilder.Entity("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.Post", b => - { - b.HasOne("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.Blog", "Blog") - .WithMany("Posts") - .HasForeignKey("BlogId") - .OnDelete(DeleteBehavior.Cascade); - - b.OwnsOne("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.TargetAudience", "TargetAudience", b1 => - { - b1.Property("PostId"); - - b1.Property("Culture"); - - b1.Property("IsPublic"); - - b1.ToTable("Posts"); - - b1.HasOne("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.Post") - .WithOne("TargetAudience") - .HasForeignKey("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.TargetAudience", "PostId") - .OnDelete(DeleteBehavior.Cascade); - }); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/Migrations/20190624150947_Initial.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/Migrations/20190624150947_Initial.cs deleted file mode 100644 index 841db0cf..00000000 --- a/tests/Backend.Fx.EfCore6Persistence.Tests/Migrations/20190624150947_Initial.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -// ReSharper disable RedundantArgumentDefaultValue - -namespace Backend.Fx.EfCorePersistence.Tests.Migrations -{ - public partial class Initial : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - "Bloggers", - table => new - { - Id = table.Column(nullable: false), - CreatedOn = table.Column(nullable: false), - CreatedBy = table.Column(maxLength: 100, nullable: true), - ChangedOn = table.Column(nullable: true), - ChangedBy = table.Column(maxLength: 100, nullable: true), - TenantId = table.Column(nullable: false), - LastName = table.Column(nullable: true), - FirstName = table.Column(nullable: true), - Bio = table.Column(nullable: true), - RowVersion = table.Column(rowVersion: true, nullable: true) - }, - constraints: table => { table.PrimaryKey("PK_Bloggers", x => x.Id); }); - - migrationBuilder.CreateTable( - "Blogs", - table => new - { - Id = table.Column(nullable: false), - CreatedOn = table.Column(nullable: false), - CreatedBy = table.Column(maxLength: 100, nullable: true), - ChangedOn = table.Column(nullable: true), - ChangedBy = table.Column(maxLength: 100, nullable: true), - TenantId = table.Column(nullable: false), - Name = table.Column(nullable: true), - RowVersion = table.Column(rowVersion: true, nullable: true) - }, - constraints: table => { table.PrimaryKey("PK_Blogs", x => x.Id); }); - - migrationBuilder.CreateTable( - "Tenants", - table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(nullable: false), - Description = table.Column(nullable: true), - IsDemoTenant = table.Column(nullable: false), - State = table.Column(nullable: false), - DefaultCultureName = table.Column(nullable: true) - }, - constraints: table => { table.PrimaryKey("PK_Tenants", x => x.Id); }); - - migrationBuilder.CreateTable( - "Posts", - table => new - { - Id = table.Column(nullable: false), - CreatedOn = table.Column(nullable: false), - CreatedBy = table.Column(maxLength: 100, nullable: true), - ChangedOn = table.Column(nullable: true), - ChangedBy = table.Column(maxLength: 100, nullable: true), - BlogId = table.Column(nullable: false), - Name = table.Column(nullable: true), - TargetAudience_Culture = table.Column(nullable: true), - TargetAudience_IsPublic = table.Column(nullable: false), - RowVersion = table.Column(rowVersion: true, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Posts", x => x.Id); - table.ForeignKey( - "FK_Posts_Blogs_BlogId", - x => x.BlogId, - "Blogs", - "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - "IX_Posts_BlogId", - "Posts", - "BlogId"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - "Bloggers"); - - migrationBuilder.DropTable( - "Posts"); - - migrationBuilder.DropTable( - "Tenants"); - - migrationBuilder.DropTable( - "Blogs"); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/Migrations/TestDbContextModelSnapshot.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/Migrations/TestDbContextModelSnapshot.cs deleted file mode 100644 index e76dd8b8..00000000 --- a/tests/Backend.Fx.EfCore6Persistence.Tests/Migrations/TestDbContextModelSnapshot.cs +++ /dev/null @@ -1,152 +0,0 @@ -// -using System; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; - -namespace Backend.Fx.EfCorePersistence.Tests.Migrations -{ - [DbContext(typeof(TestDbContext))] - partial class TestDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.1.1-rtm-30846"); - - modelBuilder.Entity("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.Blog", b => - { - b.Property("Id"); - - b.Property("ChangedBy") - .HasMaxLength(100); - - b.Property("ChangedOn"); - - b.Property("CreatedBy") - .HasMaxLength(100); - - b.Property("CreatedOn"); - - b.Property("Name"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate(); - - b.Property("TenantId"); - - b.HasKey("Id"); - - b.ToTable("Blogs"); - }); - - modelBuilder.Entity("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.Blogger", b => - { - b.Property("Id"); - - b.Property("Bio"); - - b.Property("ChangedBy") - .HasMaxLength(100); - - b.Property("ChangedOn"); - - b.Property("CreatedBy") - .HasMaxLength(100); - - b.Property("CreatedOn"); - - b.Property("FirstName"); - - b.Property("LastName"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate(); - - b.Property("TenantId"); - - b.HasKey("Id"); - - b.ToTable("Bloggers"); - }); - - modelBuilder.Entity("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.Post", b => - { - b.Property("Id"); - - b.Property("BlogId"); - - b.Property("ChangedBy") - .HasMaxLength(100); - - b.Property("ChangedOn"); - - b.Property("CreatedBy") - .HasMaxLength(100); - - b.Property("CreatedOn"); - - b.Property("Name"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate(); - - b.HasKey("Id"); - - b.HasIndex("BlogId"); - - b.ToTable("Posts"); - }); - - modelBuilder.Entity("Backend.Fx.Environment.MultiTenancy.Tenant", b => - { - b.Property("Id") - .ValueGeneratedOnAdd(); - - b.Property("DefaultCultureName"); - - b.Property("Description"); - - b.Property("IsDemoTenant"); - - b.Property("Name") - .IsRequired(); - - b.Property("State"); - - b.HasKey("Id"); - - b.ToTable("Tenants"); - }); - - modelBuilder.Entity("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.Post", b => - { - b.HasOne("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.Blog", "Blog") - .WithMany("Posts") - .HasForeignKey("BlogId") - .OnDelete(DeleteBehavior.Cascade); - - b.OwnsOne("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.TargetAudience", "TargetAudience", b1 => - { - b1.Property("PostId"); - - b1.Property("Culture"); - - b1.Property("IsPublic"); - - b1.ToTable("Posts"); - - b1.HasOne("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.Post") - .WithOne("TargetAudience") - .HasForeignKey("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.TargetAudience", "PostId") - .OnDelete(DeleteBehavior.Cascade); - }); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/TestConfig.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/TestConfig.cs deleted file mode 100644 index 6a3fbb4c..00000000 --- a/tests/Backend.Fx.EfCore6Persistence.Tests/TestConfig.cs +++ /dev/null @@ -1,17 +0,0 @@ -// using Backend.Fx.EfCorePersistence.Tests; -// using Backend.Fx.NLogLogging; -// using MarcWittke.Xunit.AssemblyFixture; -// using Xunit; -// -// [assembly: TestFramework("MarcWittke.Xunit.AssemblyFixture.XunitTestFrameworkWithAssemblyFixture", "MarcWittke.Xunit.AssemblyFixture")] -// [assembly: AssemblyFixture(typeof(TestLoggingFixture))] -// -// namespace Backend.Fx.EfCorePersistence.Tests -// { -// public class TestLoggingFixture : LoggingFixture -// { -// public TestLoggingFixture() : base("Backend.Fx") -// { -// } -// } -// } \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/TheDbContext.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/TheDbContext.cs deleted file mode 100644 index 2374a566..00000000 --- a/tests/Backend.Fx.EfCore6Persistence.Tests/TheDbContext.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Linq; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain; -using Backend.Fx.EfCorePersistence.Tests.Fixtures; -using Backend.Fx.Tests; -using Microsoft.EntityFrameworkCore; -using Serilog.Formatting.Display; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.EfCorePersistence.Tests -{ - public class TheDbContext: TestWithLogging - { - public TheDbContext(ITestOutputHelper output) : base(output) - { - _fixture = new SqliteDatabaseFixture(); - _fixture.CreateDatabase(); - } - - private readonly DatabaseFixture _fixture; - private static int _nextTenantId = 2675; - private readonly int _tenantId = _nextTenantId++; - - [Fact] - public void CanClearAndReplaceDependentEntities() - { - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var blog = new Blog(1, "original blog") {TenantId = _tenantId}; - blog.Posts.Add(new Post(1, blog, "new name 1")); - blog.Posts.Add(new Post(2, blog, "new name 2")); - blog.Posts.Add(new Post(3, blog, "new name 3")); - blog.Posts.Add(new Post(4, blog, "new name 4")); - blog.Posts.Add(new Post(5, blog, "new name 5")); - dbSession.DbContext.Add(blog); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - Blog blog = dbSession.DbContext.Blogs.Include(b => b.Posts).Single(b => b.Id == 1); - blog.Posts.Clear(); - blog.Posts.Add(new Post(6, blog, "new name 6")); - blog.Posts.Add(new Post(7, blog, "new name 7")); - blog.Posts.Add(new Post(8, blog, "new name 8")); - blog.Posts.Add(new Post(9, blog, "new name 9")); - blog.Posts.Add(new Post(10, blog, "new name 10")); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - Blog blog = dbSession.DbContext.Blogs.Include(b => b.Posts).Single(b => b.Id == 1); - - Assert.Equal(5, blog.Posts.Count); - - for (var i = 1; i <= 5; i++) Assert.DoesNotContain(blog.Posts, p => p.Id == i); - - for (var i = 6; i <= 10; i++) Assert.Contains(blog.Posts, p => p.Id == i); - } - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/TheEfCore6PersistenceApplication.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/TheEfCore6PersistenceApplication.cs new file mode 100644 index 00000000..e8590a87 --- /dev/null +++ b/tests/Backend.Fx.EfCore6Persistence.Tests/TheEfCore6PersistenceApplication.cs @@ -0,0 +1,326 @@ +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Backend.Fx.EfCore6Persistence.Tests.DummyAggregates; +using Backend.Fx.EfCore6Persistence.Tests.DummyPersistence; +using Backend.Fx.EfCore6Persistence.Tests.Fixtures; +using Backend.Fx.Features.IdGeneration; +using Backend.Fx.Features.IdGeneration.InMem; +using Backend.Fx.Features.MultiTenancy; +using Backend.Fx.Features.MultiTenancyAdmin; +using Backend.Fx.Features.MultiTenancyAdmin.InMem; +using Backend.Fx.Features.Persistence; +using Backend.Fx.Logging; +using Backend.Fx.SimpleInjectorDependencyInjection; +using Backend.Fx.TestUtil; +using FakeItEasy; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.EfCore6Persistence.Tests; + +public class TheEfCore6PersistenceApplication : TestWithLogging +{ + private readonly InMemoryTenantRepository _tenantRepository = new(); + private readonly TestDatabase _fixture = new SqliteTestDatabase(); + private readonly IBackendFxApplication _sut; + private readonly IExceptionLogger _exceptionLogger = A.Fake(); + + private readonly IEntityIdGenerator _entityIdGenerator = + new EntityIdGenerator(new SequenceHiLoIntIdGenerator(new InMemorySequence(1000))); + + private readonly DummyDbContextOptionsFactory _dummyDbContextOptionsFactory; + + public TheEfCore6PersistenceApplication(ITestOutputHelper output) : base(output) + { + _tenantRepository.SaveTenant(new Tenant(1, "t1", "tenant 1", false)); + _tenantRepository.SaveTenant(new Tenant(2, "t2", "tenant 2", true)); + + _sut = new MultiTenancyBackendFxApplication( + new SimpleInjectorCompositionRoot(), + _exceptionLogger, + new DirectTenantEnumerator(_tenantRepository), + GetType().Assembly); + _sut.EnableFeature(new IdGenerationFeature(_entityIdGenerator)); + + _dummyDbContextOptionsFactory = new DummyDbContextOptionsFactory(); + _sut.EnableFeature(new PersistenceFeature( + new EfCoreMultiTenancyPersistenceModule(_fixture, _dummyDbContextOptionsFactory), + databaseBootstrapper: new DummyDatabaseBootstrapper(_fixture, _dummyDbContextOptionsFactory))); + } + + [Fact] + public async Task CreatesDatabaseOnBoot() + { + await _sut.BootAsync(); + + await using var dbContext = new DummyDbContext( + new CurrentTenantIdHolder(), + _dummyDbContextOptionsFactory.GetDbContextOptions(_fixture.Create())); + + var unused = dbContext.Suppliers.ToArray(); + } + + [Fact] + public async Task InjectsScopedDbContext() + { + await _sut.BootAsync(); + + int hashCode = 0; + await _sut.Invoker.InvokeAsync(sp => + { + var dbContext1 = sp.GetRequiredService(); + var dbContext2 = sp.GetRequiredService(); + + Assert.NotNull(dbContext1); + Assert.StrictEqual(dbContext1, dbContext2); + + hashCode = dbContext1.GetHashCode(); + + return Task.CompletedTask; + }); + + await _sut.Invoker.InvokeAsync(sp => + { + var dbContext3 = sp.GetRequiredService(); + + Assert.NotEqual(hashCode, dbContext3.GetHashCode()); + + return Task.CompletedTask; + }); + } + + [Fact] + public async Task InjectsScopedRepositories() + { + await _sut.BootAsync(); + + int hashCode = 0; + await _sut.Invoker.InvokeAsync(sp => + { + var repo1 = sp.GetRequiredService>(); + var repo2 = sp.GetRequiredService>(); + + Assert.NotNull(repo1); + Assert.StrictEqual(repo1, repo2); + + hashCode = repo1.GetHashCode(); + + return Task.CompletedTask; + }); + + + await _sut.Invoker.InvokeAsync(sp => + { + var repo3 = sp.GetRequiredService>(); + + Assert.NotEqual(hashCode, repo3.GetHashCode()); + + return Task.CompletedTask; + }); + } + + [Fact] + public async Task SavesAggregateWithOwnedValueType() + { + await _sut.BootAsync(); + + Supplier supplier = Supplier.CreateNewSupplier(_entityIdGenerator); + + await _sut.Invoker.InvokeAsync(async sp => + { + var repo = sp.GetRequiredService>(); + await repo.AddAsync(supplier); + }); + + Supplier loadedSupplier = null; + await _sut.Invoker.InvokeAsync(async sp => + { + var dbContext = sp.GetRequiredService(); + loadedSupplier = await dbContext.Set().SingleAsync(o => o.Id == supplier.Id); + }); + + Assert.NotNull(loadedSupplier); + Assert.Equal(supplier, loadedSupplier); + Assert.Equal(supplier.Name, loadedSupplier.Name); + Assert.Equal(supplier.PostalAddress, loadedSupplier.PostalAddress); + } + + [Fact] + public async Task SavesAggregateWithNestedCollection() + { + await _sut.BootAsync(); + + Article article = Article.CreateNewArticle(_entityIdGenerator); + + await _sut.Invoker.InvokeAsync(async sp => + { + var repo = sp.GetRequiredService>(); + await repo.AddAsync(article); + }); + + Article loadedArticle = null; + await _sut.Invoker.InvokeAsync(async sp => + { + var dbContext = sp.GetRequiredService(); + loadedArticle = await dbContext.Set
().SingleAsync(o => o.Id == article.Id); + }); + + Assert.NotNull(loadedArticle); + AssertEqual(article, loadedArticle); + } + + [Fact] + public async Task SavesAggregateWithNestedCollectionThatUsesOwnedType() + { + await _sut.BootAsync(); + + Article2 article = Article2.CreateNewArticle(_entityIdGenerator); + + await _sut.Invoker.InvokeAsync(async sp => + { + var repo = sp.GetRequiredService>(); + await repo.AddAsync(article); + }); + + Article2 loadedArticle = null; + await _sut.Invoker.InvokeAsync(async sp => + { + var dbContext = sp.GetRequiredService(); + loadedArticle = await dbContext.Set().SingleAsync(o => o.Id == article.Id); + }); + + Assert.NotNull(loadedArticle); + AssertEqual(article, loadedArticle); + } + + [Fact] + public async Task CanReplaceNestedCollectionCompletely() + { + await _sut.BootAsync(); + + Article2 article = Article2.CreateNewArticle(_entityIdGenerator); + + await _sut.Invoker.InvokeAsync(async sp => + { + var repo = sp.GetRequiredService>(); + await repo.AddAsync(article); + }); + + Article2 updatedArticle = null; + await _sut.Invoker.InvokeAsync(async sp => + { + var dbContext = sp.GetRequiredService(); + updatedArticle = await dbContext.Set().SingleAsync(o => o.Id == article.Id); + updatedArticle.ReplaceVariants( + _entityIdGenerator, + new[] + { + new ArticleVariant2("yellow", "XXL"), + new ArticleVariant2("orange", "XXL") + }); + }); + + Article2 loadedArticle = null; + await _sut.Invoker.InvokeAsync(async sp => + { + var dbContext = sp.GetRequiredService(); + loadedArticle = await dbContext.Set().SingleAsync(o => o.Id == article.Id); + }); + + AssertEqual(updatedArticle, loadedArticle); + } + + [Fact] + public async Task CanAddToNestedCollection() + { + await _sut.BootAsync(); + + Article2 article = Article2.CreateNewArticle(_entityIdGenerator); + await _sut.Invoker.InvokeAsync(async sp => + { + var repo = sp.GetRequiredService>(); + await repo.AddAsync(article); + }); + + Article2 updatedArticle = null; + await _sut.Invoker.InvokeAsync(async sp => + { + var dbContext = sp.GetRequiredService(); + updatedArticle = await dbContext.Set().SingleAsync(o => o.Id == article.Id); + + updatedArticle.AddVariants(_entityIdGenerator, new[] + { + new ArticleVariant2("yellow", "XXL"), + new ArticleVariant2("orange", "XXL") + }); + }); + + Article2 loadedArticle = null; + await _sut.Invoker.InvokeAsync(async sp => + { + var dbContext = sp.GetRequiredService(); + loadedArticle = await dbContext.Set().SingleAsync(o => o.Id == article.Id); + }); + + AssertEqual(updatedArticle, loadedArticle); + } + + [Fact] + public async Task CanRemoveFromNestedCollection() + { + await _sut.BootAsync(); + + Article2 article = Article2.CreateNewArticle(_entityIdGenerator); + + await _sut.Invoker.InvokeAsync(async sp => + { + var repo = sp.GetRequiredService>(); + await repo.AddAsync(article); + }); + + Article2 updatedArticle = null; + await _sut.Invoker.InvokeAsync(async sp => + { + var dbContext = sp.GetRequiredService(); + updatedArticle = await dbContext.Set().SingleAsync(o => o.Id == article.Id); + updatedArticle.RemoveVariant(updatedArticle.Variants.First()); + }); + + Article2 loadedArticle = null; + await _sut.Invoker.InvokeAsync(async sp => + { + var dbContext = sp.GetRequiredService(); + loadedArticle = await dbContext.Set().SingleAsync(o => o.Id == article.Id); + }); + + AssertEqual(updatedArticle, loadedArticle); + } + + private static void AssertEqual(Article left, Article right) + { + var leftSerialized = JsonSerializer.Serialize(left); + var rightSerialized = JsonSerializer.Serialize(right); + Assert.Equal(leftSerialized, rightSerialized); + } + + private static void AssertEqual(Article2 left, Article2 right) + { + var leftSerialized = JsonSerializer.Serialize(left); + var rightSerialized = JsonSerializer.Serialize(right); + Assert.Equal(leftSerialized, rightSerialized); + } + + [UsedImplicitly] + private class StaticTenantIdSelector : ICurrentTenantIdSelector + { + public TenantId GetCurrentTenantId() + { + return new TenantId(42); + } + } +} + diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/TheRepositoryOfComposedAggregate.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/TheRepositoryOfComposedAggregate.cs deleted file mode 100644 index a8ebb6a0..00000000 --- a/tests/Backend.Fx.EfCore6Persistence.Tests/TheRepositoryOfComposedAggregate.cs +++ /dev/null @@ -1,353 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence; -using Backend.Fx.EfCorePersistence.Tests.Fixtures; -using Backend.Fx.Environment.DateAndTime; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Extensions; -using Backend.Fx.Patterns.Authorization; -using Backend.Fx.Patterns.IdGeneration; -using Backend.Fx.Tests; -using FakeItEasy; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.EfCorePersistence.Tests -{ - public class TheRepositoryOfComposedAggregate : TestWithLogging - { - public TheRepositoryOfComposedAggregate(ITestOutputHelper testOutputHelper) : base(testOutputHelper) - { - A.CallTo(() => _idGenerator.NextId()).ReturnsLazily(() => _nextId++); - //_fixture = new SqlServerDatabaseFixture(); - _fixture = new SqliteDatabaseFixture(); - _fixture.CreateDatabase(); - } - - private static int _nextTenantId = 57839; - private static int _nextId = 1; - private readonly int _tenantId = _nextTenantId++; - - private readonly IEqualityComparer _tolerantDateTimeComparer = - new TolerantDateTimeComparer(TimeSpan.FromMilliseconds(5000)); - - private readonly IEntityIdGenerator _idGenerator = A.Fake(); - private readonly DatabaseFixture _fixture; - - private int CreateBlogWithPost(IDbConnection dbConnection, int postCount = 1) - { - { - var blogId = _nextId++; - dbConnection.ExecuteNonQuery( - $"INSERT INTO Blogs (Id, TenantId, Name, CreatedOn, CreatedBy) VALUES ({blogId}, {CurrentTenantIdHolder.Create(_tenantId).Current.Value}, 'my blog', CURRENT_TIMESTAMP, 'persistence test')"); - var count = dbConnection.ExecuteScalar("SELECT count(*) FROM Blogs"); - Assert.Equal(1, count); - - for (var i = 0; i < postCount; i++) - dbConnection.ExecuteNonQuery( - $"INSERT INTO Posts (Id, BlogId, Name, TargetAudience_IsPublic, TargetAudience_Culture, CreatedOn, CreatedBy) VALUES ({_nextId++}, {blogId}, 'my post {i:00}', '1', 'de-DE', CURRENT_TIMESTAMP, 'persistence test')"); - - return blogId; - } - } - - //FAILING!!!! - // this shows, that ValueObjects treated as OwnedTypes are not supported very well - //[Fact] - //public void CanUpdateDependantValueObject() - //{ - // using (DbSession dbs = _fixture.UseDbSession()) - // { - // int id = CreateBlogWithPost(dbSession.DbConnection, 10); - // Post post; - - // using (var uow = dbs.UseUnitOfWork(_clock)) - // { - // var sut = new EfRepository(uow.DbContext, new BlogMapping(), CurrentTenantIdHolder.Create(_tenantId), - // new AllowAll()); - // var blog = sut.Single(id); - // post = blog.Posts.First(); - // post.TargetAudience = new TargetAudience{Culture = "es-AR", IsPublic = false}; - // uow.Complete(); - // } - - // - // { - // string culture = dbSession.DbConnection.ExecuteScalar($"SELECT TargetAudience_Culture ame FROM Posts where id = {post.Id}"); - // Assert.Equal("es-AR", culture); - - // string strChangedOn = dbSession.DbConnection.ExecuteScalar($"SELECT ChangedOn FROM Posts where id = {post.Id}"); - // DateTime changedOn = DateTime.Parse(strChangedOn); - // Assert.Equal(_clock.UtcNow, changedOn, new TolerantDateTimeComparer(TimeSpan.FromMilliseconds(500))); - // } - // } - //} - - [Fact] - public void CanAddDependent() - { - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var id = CreateBlogWithPost(dbSession.DbConnection, 10); - var sut = new EfRepository(dbSession.DbContext, - new BlogMapping(), - CurrentTenantIdHolder.Create(_tenantId), - new AllowAll()); - Blog blog = sut.Single(id); - blog.Posts.Add(new Post(_idGenerator.NextId(), blog, "added")); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Posts"); - Assert.Equal(11, count); - } - } - - [Fact] - public void CanCreate() - { - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Blogs"); - Assert.Equal(0, count); - - count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Posts"); - Assert.Equal(0, count); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var sut = new EfRepository(dbSession.DbContext, - new BlogMapping(), - CurrentTenantIdHolder.Create(_tenantId), - new AllowAll()); - var blog = new Blog(_idGenerator.NextId(), "my blog"); - blog.AddPost(_idGenerator, "my post"); - sut.Add(blog); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Blogs"); - Assert.Equal(1, count); - - count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Posts"); - Assert.Equal(1, count); - } - } - - [Fact] - public void CanDelete() - { - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var id = CreateBlogWithPost(dbSession.DbConnection); - - var sut = new EfRepository(dbSession.DbContext, - new BlogMapping(), - CurrentTenantIdHolder.Create(_tenantId), - new AllowAll()); - Blog blog = sut.Single(id); - sut.Delete(blog); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Blogs"); - Assert.Equal(0, count); - - count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Posts"); - Assert.Equal(0, count); - } - } - - [Fact] - public void CanDeleteDependent() - { - int id; - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - id = CreateBlogWithPost(dbSession.DbConnection, 10); - var count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Posts"); - Assert.Equal(10, count); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var sut = new EfRepository(dbSession.DbContext, - new BlogMapping(), - CurrentTenantIdHolder.Create(_tenantId), - new AllowAll()); - Blog blog = sut.Single(id); - Post firstPost = blog.Posts.First(); - firstPost.SetName("sadfasfsadf"); - blog.Posts.Remove(firstPost); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Posts"); - Assert.Equal(9, count); - } - } - - - [Fact] - public void CanRead() - { - int id; - Blog blog; - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - id = CreateBlogWithPost(dbSession.DbConnection); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var sut = new EfRepository(dbSession.DbContext, - new BlogMapping(), - CurrentTenantIdHolder.Create(_tenantId), - new AllowAll()); - blog = sut.Single(id); - } - - Assert.NotNull(blog); - Assert.Equal(id, blog.Id); - Assert.Equal("my blog", blog.Name); - Assert.NotEmpty(blog.Posts); - } - - - [Fact] - public void CanReplaceDependentCollection() - { - int id; - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - id = CreateBlogWithPost(dbSession.DbConnection, 10); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var sut = new EfRepository(dbSession.DbContext, - new BlogMapping(), - CurrentTenantIdHolder.Create(_tenantId), - new AllowAll()); - Blog blog = sut.Single(id); - blog.Posts.Clear(); - blog.Posts.Add(new Post(_idGenerator.NextId(), blog, "new name 1")); - blog.Posts.Add(new Post(_idGenerator.NextId(), blog, "new name 2")); - blog.Posts.Add(new Post(_idGenerator.NextId(), blog, "new name 3")); - blog.Posts.Add(new Post(_idGenerator.NextId(), blog, "new name 4")); - blog.Posts.Add(new Post(_idGenerator.NextId(), blog, "new name 5")); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Posts"); - Assert.Equal(5, count); - } - } - - [Fact] - public void CanUpdate() - { - int id; - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - id = CreateBlogWithPost(dbSession.DbConnection); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var sut = new EfRepository(dbSession.DbContext, - new BlogMapping(), - CurrentTenantIdHolder.Create(_tenantId), - new AllowAll()); - Blog blog = sut.Single(id); - blog.Modify("modified"); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - Assert.Equal(1, dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Blogs")); - Assert.Equal(id, dbSession.DbConnection.ExecuteScalar("SELECT Id FROM Blogs LIMIT 1")); - Assert.Equal("modified", dbSession.DbConnection.ExecuteScalar("SELECT Name FROM Blogs LIMIT 1")); - Assert.Equal("modified", dbSession.DbConnection.ExecuteScalar("SELECT Name FROM Posts LIMIT 1")); - } - } - - [Fact] - public void CanUpdateDependant() - { - var clock = new AdjustableClock(new WallClock()); - clock.OverrideUtcNow(new DateTime(2020, 01, 20, 20, 30, 40)); - - int id; - using (TestDbSession dbSession = _fixture.CreateTestDbSession(clock: clock)) - { - id = CreateBlogWithPost(dbSession.DbConnection, 10); - } - - Post post; - - using (TestDbSession dbSession = _fixture.CreateTestDbSession(clock: clock)) - { - var sut = new EfRepository(dbSession.DbContext, - new BlogMapping(), - CurrentTenantIdHolder.Create(_tenantId), - new AllowAll()); - Blog blog = sut.Single(id); - post = blog.Posts.First(); - post.SetName("modified"); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession(clock: clock)) - { - var name = dbSession.DbConnection.ExecuteScalar($"SELECT name FROM Posts where id = {post.Id}"); - Assert.Equal("modified", name); - - var strChangedOn = dbSession.DbConnection.ExecuteScalar($"SELECT changedon FROM Posts where id = {post.Id}"); - DateTime changedOn = DateTime.Parse(strChangedOn); - Assert.Equal(clock.UtcNow, changedOn, new TolerantDateTimeComparer(TimeSpan.FromMilliseconds(500))); - } - } - - [Fact] - public void UpdatesAggregateTrackingPropertiesOnDeleteOfDependant() - { - var clock = new AdjustableClock(new WallClock()); - clock.OverrideUtcNow(new DateTime(2020, 01, 20, 20, 30, 40)); - - int id; - using (TestDbSession dbSession = _fixture.CreateTestDbSession(clock: clock)) - { - id = CreateBlogWithPost(dbSession.DbConnection, 10); - } - - DateTime expectedModifiedOn = clock.Advance(TimeSpan.FromHours(1)); - - using (TestDbSession dbSession = _fixture.CreateTestDbSession(clock: clock)) - { - var sut = new EfRepository(dbSession.DbContext, - new BlogMapping(), - CurrentTenantIdHolder.Create(_tenantId), - new AllowAll()); - Blog b = sut.Single(id); - b.Posts.Remove(b.Posts.First()); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession(clock: clock)) - { - Blog blog = dbSession.DbContext.Set().Find(id); - Assert.NotNull(blog.ChangedOn); - Assert.Equal(expectedModifiedOn, blog.ChangedOn.Value, _tolerantDateTimeComparer); - } - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCore6Persistence.Tests/TheRepositoryOfPlainAggregate.cs b/tests/Backend.Fx.EfCore6Persistence.Tests/TheRepositoryOfPlainAggregate.cs deleted file mode 100644 index a77f4b57..00000000 --- a/tests/Backend.Fx.EfCore6Persistence.Tests/TheRepositoryOfPlainAggregate.cs +++ /dev/null @@ -1,191 +0,0 @@ -using System; -using System.Threading.Tasks; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence; -using Backend.Fx.EfCorePersistence.Tests.Fixtures; -using Backend.Fx.Environment.Authentication; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Extensions; -using Backend.Fx.Patterns.Authorization; -using Backend.Fx.Tests; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.EfCorePersistence.Tests -{ - public class TheRepositoryOfPlainAggregate: TestWithLogging - { - public TheRepositoryOfPlainAggregate(ITestOutputHelper testOutputHelper) : base(testOutputHelper) - { - //_fixture = new SqlServerDatabaseFixture(); - _fixture = new SqliteDatabaseFixture(); - _fixture.CreateDatabase(); - } - - private static int _nextTenantId = 12312; - private readonly int _tenantId = _nextTenantId++; - private readonly DatabaseFixture _fixture; - - [Fact] - public void CanCreate() - { - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var repo = new EfRepository(dbSession.DbContext, new BloggerMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - repo.Add(new Blogger(345, "Metulsky", "Bratislav")); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var count = dbSession.DbConnection.ExecuteScalar("SELECT Count(*) FROM Bloggers"); - Assert.Equal(1, count); - - count = dbSession.DbConnection.ExecuteScalar( - $"SELECT Count(*) FROM Bloggers WHERE Id=345"); - Assert.Equal(1, count); - } - } - - - [Fact] - public void CanDelete() - { - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - dbSession.DbConnection.ExecuteNonQuery( - "INSERT INTO Bloggers (Id, TenantId, CreatedOn, CreatedBy, FirstName, LastName, Bio) " + - $"VALUES (555, {_tenantId}, '2012-05-12 23:12:09', 'the test', 'Bratislav', 'Metulsky', 'whatever')"); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var repo = new EfRepository(dbSession.DbContext, new BloggerMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - Blogger bratislavMetulsky = repo.Single(555); - repo.Delete(bratislavMetulsky); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var count = dbSession.DbConnection.ExecuteScalar("SELECT Count(*) FROM Bloggers WHERE Id = 555"); - Assert.Equal(0, count); - } - } - - - - [Fact] - public void CanRead() - { - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - dbSession.DbConnection.ExecuteNonQuery( - "INSERT INTO Bloggers (Id, TenantId, CreatedOn, CreatedBy, FirstName, LastName, Bio) " + - $"VALUES (444, {_tenantId}, '2012-05-12 23:12:09', 'the test', 'Bratislav', 'Metulsky', 'whatever')"); - - { - var repo = new EfRepository(dbSession.DbContext, new BloggerMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - - bool any = repo.Any(); - Assert.True(any); - - Blogger[] all = repo.GetAll(); - Assert.NotEmpty(all); - - Blogger bratislavMetulsky = repo.Single(444); - Assert.Equal(_tenantId, bratislavMetulsky.TenantId); - Assert.Equal("the test", bratislavMetulsky.CreatedBy); - Assert.Equal(new DateTime(2012, 05, 12, 23, 12, 09), bratislavMetulsky.CreatedOn); - Assert.Equal("Bratislav", bratislavMetulsky.FirstName); - Assert.Equal("Metulsky", bratislavMetulsky.LastName); - Assert.Equal("whatever", bratislavMetulsky.Bio); - - bratislavMetulsky = repo.SingleOrDefault(444); - Assert.NotNull(bratislavMetulsky); - Assert.Equal(_tenantId, bratislavMetulsky.TenantId); - Assert.Equal("the test", bratislavMetulsky.CreatedBy); - Assert.Equal(new DateTime(2012, 05, 12, 23, 12, 09), bratislavMetulsky.CreatedOn); - Assert.Equal("Bratislav", bratislavMetulsky.FirstName); - Assert.Equal("Metulsky", bratislavMetulsky.LastName); - Assert.Equal("whatever", bratislavMetulsky.Bio); - } - } - } - - - [Fact] - public async Task CanReadAsync() - { - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - dbSession.DbConnection.ExecuteNonQuery( - "INSERT INTO Bloggers (Id, TenantId, CreatedOn, CreatedBy, FirstName, LastName, Bio) " + - $"VALUES (555, {_tenantId}, '2012-05-12 23:12:09', 'the test', 'Bratislav', 'Metulsky', 'whatever')"); - - { - var repo = new EfRepository(dbSession.DbContext, new BloggerMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - - bool any = await repo.AnyAsync(); - Assert.True(any); - - Blogger[] all = await repo.GetAllAsync(); - Assert.NotEmpty(all); - - Blogger bratislavMetulsky = await repo.SingleAsync(555); - Assert.Equal(_tenantId, bratislavMetulsky.TenantId); - Assert.Equal("the test", bratislavMetulsky.CreatedBy); - Assert.Equal(new DateTime(2012, 05, 12, 23, 12, 09), bratislavMetulsky.CreatedOn); - Assert.Equal("Bratislav", bratislavMetulsky.FirstName); - Assert.Equal("Metulsky", bratislavMetulsky.LastName); - Assert.Equal("whatever", bratislavMetulsky.Bio); - - bratislavMetulsky = await repo.SingleOrDefaultAsync(555); - Assert.NotNull(bratislavMetulsky); - Assert.Equal(_tenantId, bratislavMetulsky.TenantId); - Assert.Equal("the test", bratislavMetulsky.CreatedBy); - Assert.Equal(new DateTime(2012, 05, 12, 23, 12, 09), bratislavMetulsky.CreatedOn); - Assert.Equal("Bratislav", bratislavMetulsky.FirstName); - Assert.Equal("Metulsky", bratislavMetulsky.LastName); - Assert.Equal("whatever", bratislavMetulsky.Bio); - } - } - } - - [Fact] - public void CanUpdate() - { - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - dbSession.DbConnection.ExecuteNonQuery( - "INSERT INTO Bloggers (Id, TenantId, CreatedOn, CreatedBy, FirstName, LastName, Bio) " + - $"VALUES (456, {_tenantId}, '2012-05-12 23:12:09', 'the test', 'Bratislav', 'Metulsky', 'whatever')"); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var repo = new EfRepository(dbSession.DbContext, new BloggerMapping(), - CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - Blogger bratislavMetulsky = repo.Single(456); - bratislavMetulsky.FirstName = "Johnny"; - bratislavMetulsky.LastName = "Flash"; - bratislavMetulsky.Bio = "Der lustige Clown"; - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var count = dbSession.DbConnection.ExecuteScalar( - $"SELECT Count(*) FROM Bloggers WHERE FirstName = 'Johnny' AND LastName = 'Flash' AND TenantId = '{_tenantId}'"); - Assert.Equal(1, count); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var repo = new EfRepository(dbSession.DbContext, new BloggerMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - Blogger johnnyFlash = repo.Single(456); - Assert.Equal(DateTime.UtcNow, johnnyFlash.ChangedOn, new TolerantDateTimeComparer(TimeSpan.FromMilliseconds(5000))); - Assert.Equal(new SystemIdentity().Name, johnnyFlash.ChangedBy); - Assert.Equal("Johnny", johnnyFlash.FirstName); - Assert.Equal("Flash", johnnyFlash.LastName); - } - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/Backend.Fx.EfCorePersistence.Tests.csproj b/tests/Backend.Fx.EfCorePersistence.Tests/Backend.Fx.EfCorePersistence.Tests.csproj deleted file mode 100644 index 2d7f4b3c..00000000 --- a/tests/Backend.Fx.EfCorePersistence.Tests/Backend.Fx.EfCorePersistence.Tests.csproj +++ /dev/null @@ -1,37 +0,0 @@ - - - - netcoreapp3.1 - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/DbConnectionEx.cs b/tests/Backend.Fx.EfCorePersistence.Tests/DbConnectionEx.cs deleted file mode 100644 index 499d8a51..00000000 --- a/tests/Backend.Fx.EfCorePersistence.Tests/DbConnectionEx.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using JetBrains.Annotations; - -namespace Backend.Fx.EfCorePersistence.Tests -{ - public static class DbConnectionEx - { - public static void ExecuteNonQuery(this IDbConnection openConnection, string cmd) - { - using (IDbCommand command = openConnection.CreateCommand()) - { - command.CommandText = cmd; - command.ExecuteNonQuery(); - } - } - - public static T ExecuteScalar(this IDbConnection openConnection, string cmd) - { - using (IDbCommand command = openConnection.CreateCommand()) - { - command.CommandText = cmd; - object scalarResult = command.ExecuteScalar(); - if (typeof(T) == typeof(int)) return (T) (object) Convert.ToInt32(scalarResult); - return (T) scalarResult; - } - } - - [UsedImplicitly] - public static IEnumerable ExecuteReader(this IDbConnection openConnection, string cmd, Func forEachResultFunc) - { - using (IDbCommand command = openConnection.CreateCommand()) - { - command.CommandText = cmd; - IDataReader reader = command.ExecuteReader(); - while (reader.NextResult()) yield return forEachResultFunc(reader); - } - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/Blog.cs b/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/Blog.cs deleted file mode 100644 index 27c8be41..00000000 --- a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/Blog.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using Backend.Fx.BuildingBlocks; -using Backend.Fx.Patterns.IdGeneration; -using JetBrains.Annotations; - -namespace Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain -{ - public class Blog : AggregateRoot - { - [UsedImplicitly] - private Blog() - { - } - - public Blog(int id, string name) : base(id) - { - Name = name; - } - - public string Name { get; private set; } - - public ISet Posts { get; } = new HashSet(); - - public Post AddPost(IEntityIdGenerator idGenerator, string name, bool isPublic = false) - { - var post = new Post(idGenerator.NextId(), this, name, isPublic); - Posts.Add(post); - return post; - } - - public void Modify(string modified) - { - Name = modified; - foreach (Post post in Posts) post.SetName(modified); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/BlogAuthorization.cs b/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/BlogAuthorization.cs deleted file mode 100644 index 958e27a8..00000000 --- a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/BlogAuthorization.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Backend.Fx.Patterns.Authorization; -using JetBrains.Annotations; - -namespace Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain -{ - [UsedImplicitly] - public class BlogAuthorization : AllowAll - { - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/Blogger.cs b/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/Blogger.cs deleted file mode 100644 index a21db5ab..00000000 --- a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/Blogger.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Backend.Fx.BuildingBlocks; -using JetBrains.Annotations; - -namespace Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain -{ - public class Blogger : AggregateRoot - { - [UsedImplicitly] - private Blogger() - { - } - - public Blogger(int id, string lastName, string firstName) : base(id) - { - LastName = lastName; - FirstName = firstName; - } - - public string LastName { get; set; } - public string FirstName { get; set; } - public string Bio { get; set; } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/Post.cs b/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/Post.cs deleted file mode 100644 index e71875c0..00000000 --- a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/Post.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using Backend.Fx.BuildingBlocks; -using JetBrains.Annotations; - -namespace Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain -{ - public class Post : Entity - { - [UsedImplicitly] - private Post() - { - } - - public Post(int id, Blog blog, string name, bool isPublic = false) : base(id) - { - Blog = blog; - BlogId = blog.Id; - Name = name; - TargetAudience = new TargetAudience {IsPublic = isPublic, Culture = "fr-FR"}; - } - - [UsedImplicitly] public int BlogId { get; private set; } - - [UsedImplicitly] public Blog Blog { get; private set; } - - [UsedImplicitly] public string Name { get; private set; } - - [UsedImplicitly] public TargetAudience TargetAudience { get; set; } - - public void SetName(string name) - { - Name = name; - } - } - - public class TargetAudience : ValueObject - { - public string Culture { get; set; } - - public bool IsPublic { get; set; } - - protected override IEnumerable GetEqualityComponents() - { - yield return Culture; - yield return IsPublic; - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/BlogMapping.cs b/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/BlogMapping.cs deleted file mode 100644 index 4f21dddb..00000000 --- a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/BlogMapping.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain; -using Microsoft.EntityFrameworkCore; - -namespace Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence -{ - public class BlogMapping : AggregateMapping - { - public override IEnumerable>> IncludeDefinitions - { - get - { - return new Expression>[] - { - blog => blog.Posts - }; - } - } - - public override void ApplyEfMapping(ModelBuilder modelBuilder) - { - modelBuilder.Entity().OwnsOne(p => p.TargetAudience); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/BloggerMapping.cs b/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/BloggerMapping.cs deleted file mode 100644 index 2659bcd9..00000000 --- a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/BloggerMapping.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain; - -namespace Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence -{ - public class BloggerMapping : PlainAggregateMapping - { - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/TestDbContext.cs b/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/TestDbContext.cs deleted file mode 100644 index bd40f2f5..00000000 --- a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/TestDbContext.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain; -using Backend.Fx.Environment.MultiTenancy; -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; - -namespace Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence -{ - public sealed class TestDbContext : DbContext - { - public TestDbContext([NotNull] DbContextOptions options) : base(options) - { - Database.AutoTransactionsEnabled = false; - } - - public DbSet Bloggers { get; [UsedImplicitly] set; } - - public DbSet Blogs { get; [UsedImplicitly] set; } - public DbSet Posts { get; [UsedImplicitly] set; } - public DbSet Tenants { get; [UsedImplicitly] set; } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - this.ApplyAggregateMappings(modelBuilder); - modelBuilder.RegisterRowVersionProperty(); - modelBuilder.RegisterEntityIdAsNeverGenerated(); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/TestDbContextFactory.cs b/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/TestDbContextFactory.cs deleted file mode 100644 index cefd3713..00000000 --- a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/TestDbContextFactory.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Design; - -namespace Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence -{ - [UsedImplicitly] - [Obsolete("Only for migration support at design time")] - public class TestDbContextFactory : IDesignTimeDbContextFactory - { - public TestDbContext CreateDbContext(string[] args) - { - return new TestDbContext(new DbContextOptionsBuilder().UseSqlite("DataSource=:memory:").Options); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/DatabaseFixture.cs b/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/DatabaseFixture.cs deleted file mode 100644 index 59578f8c..00000000 --- a/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/DatabaseFixture.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Data; -using System.Security.Principal; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence; -using Backend.Fx.Environment.Authentication; -using Backend.Fx.Environment.DateAndTime; -using Backend.Fx.Environment.Persistence; -using Microsoft.EntityFrameworkCore; - -namespace Backend.Fx.EfCorePersistence.Tests.Fixtures -{ - public abstract class DatabaseFixture - { - public void CreateDatabase() - { - using (var dbContext = new TestDbContext(GetDbContextOptionsForDbCreation())) - { - dbContext.Database.EnsureCreated(); - } - } - - protected abstract DbContextOptions GetDbContextOptionsForDbCreation(); - - public abstract DbContextOptionsBuilder GetDbContextOptionsBuilder(IDbConnection connection); - - public abstract DbConnectionOperationDecorator UseOperation(); - - public TestDbSession CreateTestDbSession(DbConnectionOperationDecorator operation = null, IIdentity asIdentity = null, IClock clock = null) - { - CurrentIdentityHolder CreateAsIdentity() - { - var cih = new CurrentIdentityHolder(); - cih.ReplaceCurrent(asIdentity); - return cih; - } - - clock ??= new WallClock(); - operation ??= UseOperation(); - - operation.Begin(); - - var identityHolder = asIdentity == null - ? CurrentIdentityHolder.CreateSystem() - : CreateAsIdentity(); - return new TestDbSession(this, operation, identityHolder, clock); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/SqlServerDatabaseFixture.cs b/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/SqlServerDatabaseFixture.cs deleted file mode 100644 index 5f57d5b2..00000000 --- a/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/SqlServerDatabaseFixture.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Data; -using System.Data.SqlClient; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence; -using Backend.Fx.Environment.Persistence; -using Backend.Fx.Patterns.DependencyInjection; -using Microsoft.EntityFrameworkCore; - -namespace Backend.Fx.EfCorePersistence.Tests.Fixtures -{ - [Obsolete("Not supported on build agents")] - public class SqlServerDatabaseFixture : DatabaseFixture - { - private static int _testindex = 1; - private readonly string _connectionString; - - public SqlServerDatabaseFixture() - { - var dbName = $"TestFixture_{_testindex++:000}"; - var sqlConnectionStringBuilder = new SqlConnectionStringBuilder("Server=.\\SQLExpress;Trusted_Connection=True;"); - using (IDbConnection connection = new SqlConnection(sqlConnectionStringBuilder.ConnectionString)) - { - connection.Open(); - - using (IDbCommand dropCommand = connection.CreateCommand()) - { - dropCommand.CommandText = $"IF EXISTS(SELECT * FROM sys.Databases WHERE Name='{dbName}') ALTER DATABASE [{dbName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE"; - dropCommand.ExecuteNonQuery(); - } - - using (IDbCommand dropCommand = connection.CreateCommand()) - { - dropCommand.CommandText = $"IF EXISTS(SELECT * FROM sys.Databases WHERE Name='{dbName}') DROP DATABASE [{dbName}]"; - dropCommand.ExecuteNonQuery(); - } - - using (IDbCommand createCommand = connection.CreateCommand()) - { - createCommand.CommandText = $"CREATE DATABASE [{dbName}]"; - createCommand.ExecuteNonQuery(); - } - - connection.Close(); - } - - sqlConnectionStringBuilder.InitialCatalog = dbName; - _connectionString = sqlConnectionStringBuilder.ConnectionString; - } - - protected override DbContextOptions GetDbContextOptionsForDbCreation() - { - return new DbContextOptionsBuilder().UseSqlServer(_connectionString).Options; - } - - - public override DbContextOptionsBuilder GetDbContextOptionsBuilder(IDbConnection connection) - { - return new DbContextOptionsBuilder().UseSqlServer((SqlConnection) connection); - } - - public override DbConnectionOperationDecorator UseOperation() - { - var sqliteConnection = new SqlConnection(_connectionString); - IOperation operation = new Operation(); - operation = new DbTransactionOperationDecorator(sqliteConnection, operation); - return new DbConnectionOperationDecorator(sqliteConnection, operation); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/SqliteDatabaseFixture.cs b/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/SqliteDatabaseFixture.cs deleted file mode 100644 index 18440c2c..00000000 --- a/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/SqliteDatabaseFixture.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Data; -using System.IO; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence; -using Backend.Fx.Environment.Persistence; -using Backend.Fx.Patterns.DependencyInjection; -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; - -namespace Backend.Fx.EfCorePersistence.Tests.Fixtures -{ - public class SqliteDatabaseFixture : DatabaseFixture - { - private readonly string _connectionString = "Data Source=" + Path.GetTempFileName(); - - protected override DbContextOptions GetDbContextOptionsForDbCreation() - { - return new DbContextOptionsBuilder().UseSqlite(_connectionString).Options; - } - - public override DbContextOptionsBuilder GetDbContextOptionsBuilder(IDbConnection connection) - { - return new DbContextOptionsBuilder().UseSqlite((SqliteConnection) connection); - } - - public override DbConnectionOperationDecorator UseOperation() - { - var sqliteConnection = new SqliteConnection(_connectionString); - IOperation operation = new Operation(); - operation = new DbTransactionOperationDecorator(sqliteConnection, operation); - return new DbConnectionOperationDecorator(sqliteConnection, operation); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/TestDbSession.cs b/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/TestDbSession.cs deleted file mode 100644 index 8fe47f7f..00000000 --- a/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/TestDbSession.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Data; -using System.Security.Principal; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence; -using Backend.Fx.Environment.DateAndTime; -using Backend.Fx.Environment.Persistence; -using Backend.Fx.Patterns.DependencyInjection; - -namespace Backend.Fx.EfCorePersistence.Tests.Fixtures -{ - public class TestDbSession : ICanFlush, IDisposable - { - private readonly DbConnectionOperationDecorator _operation; - private readonly EfFlush _efFlush; - - public TestDbSession(DatabaseFixture fixture, DbConnectionOperationDecorator operation, ICurrentTHolder identityHolder, IClock clock) - { - _operation = operation; - DbContext = new TestDbContext(fixture.GetDbContextOptionsBuilder(operation.DbConnection).Options); - _efFlush = new EfFlush(DbContext, identityHolder, clock); - DbConnection = operation.DbConnection; - } - - - public TestDbContext DbContext { get; } - public IDbConnection DbConnection { get; } - - public void Flush() - { - _efFlush.Flush(); - } - - public void Dispose() - { - _efFlush.Flush(); - DbContext.Dispose(); - _operation.Complete(); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/Migrations/20190624150947_Initial.Designer.cs b/tests/Backend.Fx.EfCorePersistence.Tests/Migrations/20190624150947_Initial.Designer.cs deleted file mode 100644 index 1c1cdd8b..00000000 --- a/tests/Backend.Fx.EfCorePersistence.Tests/Migrations/20190624150947_Initial.Designer.cs +++ /dev/null @@ -1,155 +0,0 @@ -// -using System; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace Backend.Fx.EfCorePersistence.Tests.Migrations -{ - [DbContext(typeof(TestDbContext))] - [Migration("20190624150947_Initial")] - partial class Initial - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.1.1-rtm-30846"); - - modelBuilder.Entity("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.Blog", b => - { - b.Property("Id"); - - b.Property("ChangedBy") - .HasMaxLength(100); - - b.Property("ChangedOn"); - - b.Property("CreatedBy") - .HasMaxLength(100); - - b.Property("CreatedOn"); - - b.Property("Name"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate(); - - b.Property("TenantId"); - - b.HasKey("Id"); - - b.ToTable("Blogs"); - }); - - modelBuilder.Entity("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.Blogger", b => - { - b.Property("Id"); - - b.Property("Bio"); - - b.Property("ChangedBy") - .HasMaxLength(100); - - b.Property("ChangedOn"); - - b.Property("CreatedBy") - .HasMaxLength(100); - - b.Property("CreatedOn"); - - b.Property("FirstName"); - - b.Property("LastName"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate(); - - b.Property("TenantId"); - - b.HasKey("Id"); - - b.ToTable("Bloggers"); - }); - - modelBuilder.Entity("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.Post", b => - { - b.Property("Id"); - - b.Property("BlogId"); - - b.Property("ChangedBy") - .HasMaxLength(100); - - b.Property("ChangedOn"); - - b.Property("CreatedBy") - .HasMaxLength(100); - - b.Property("CreatedOn"); - - b.Property("Name"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate(); - - b.HasKey("Id"); - - b.HasIndex("BlogId"); - - b.ToTable("Posts"); - }); - - modelBuilder.Entity("Backend.Fx.Environment.MultiTenancy.Tenant", b => - { - b.Property("Id") - .ValueGeneratedOnAdd(); - - b.Property("DefaultCultureName"); - - b.Property("Description"); - - b.Property("IsDemoTenant"); - - b.Property("Name") - .IsRequired(); - - b.Property("State"); - - b.HasKey("Id"); - - b.ToTable("Tenants"); - }); - - modelBuilder.Entity("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.Post", b => - { - b.HasOne("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.Blog", "Blog") - .WithMany("Posts") - .HasForeignKey("BlogId") - .OnDelete(DeleteBehavior.Cascade); - - b.OwnsOne("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.TargetAudience", "TargetAudience", b1 => - { - b1.Property("PostId"); - - b1.Property("Culture"); - - b1.Property("IsPublic"); - - b1.ToTable("Posts"); - - b1.HasOne("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.Post") - .WithOne("TargetAudience") - .HasForeignKey("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.TargetAudience", "PostId") - .OnDelete(DeleteBehavior.Cascade); - }); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/Migrations/20190624150947_Initial.cs b/tests/Backend.Fx.EfCorePersistence.Tests/Migrations/20190624150947_Initial.cs deleted file mode 100644 index 841db0cf..00000000 --- a/tests/Backend.Fx.EfCorePersistence.Tests/Migrations/20190624150947_Initial.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -// ReSharper disable RedundantArgumentDefaultValue - -namespace Backend.Fx.EfCorePersistence.Tests.Migrations -{ - public partial class Initial : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - "Bloggers", - table => new - { - Id = table.Column(nullable: false), - CreatedOn = table.Column(nullable: false), - CreatedBy = table.Column(maxLength: 100, nullable: true), - ChangedOn = table.Column(nullable: true), - ChangedBy = table.Column(maxLength: 100, nullable: true), - TenantId = table.Column(nullable: false), - LastName = table.Column(nullable: true), - FirstName = table.Column(nullable: true), - Bio = table.Column(nullable: true), - RowVersion = table.Column(rowVersion: true, nullable: true) - }, - constraints: table => { table.PrimaryKey("PK_Bloggers", x => x.Id); }); - - migrationBuilder.CreateTable( - "Blogs", - table => new - { - Id = table.Column(nullable: false), - CreatedOn = table.Column(nullable: false), - CreatedBy = table.Column(maxLength: 100, nullable: true), - ChangedOn = table.Column(nullable: true), - ChangedBy = table.Column(maxLength: 100, nullable: true), - TenantId = table.Column(nullable: false), - Name = table.Column(nullable: true), - RowVersion = table.Column(rowVersion: true, nullable: true) - }, - constraints: table => { table.PrimaryKey("PK_Blogs", x => x.Id); }); - - migrationBuilder.CreateTable( - "Tenants", - table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(nullable: false), - Description = table.Column(nullable: true), - IsDemoTenant = table.Column(nullable: false), - State = table.Column(nullable: false), - DefaultCultureName = table.Column(nullable: true) - }, - constraints: table => { table.PrimaryKey("PK_Tenants", x => x.Id); }); - - migrationBuilder.CreateTable( - "Posts", - table => new - { - Id = table.Column(nullable: false), - CreatedOn = table.Column(nullable: false), - CreatedBy = table.Column(maxLength: 100, nullable: true), - ChangedOn = table.Column(nullable: true), - ChangedBy = table.Column(maxLength: 100, nullable: true), - BlogId = table.Column(nullable: false), - Name = table.Column(nullable: true), - TargetAudience_Culture = table.Column(nullable: true), - TargetAudience_IsPublic = table.Column(nullable: false), - RowVersion = table.Column(rowVersion: true, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Posts", x => x.Id); - table.ForeignKey( - "FK_Posts_Blogs_BlogId", - x => x.BlogId, - "Blogs", - "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - "IX_Posts_BlogId", - "Posts", - "BlogId"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - "Bloggers"); - - migrationBuilder.DropTable( - "Posts"); - - migrationBuilder.DropTable( - "Tenants"); - - migrationBuilder.DropTable( - "Blogs"); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/Migrations/TestDbContextModelSnapshot.cs b/tests/Backend.Fx.EfCorePersistence.Tests/Migrations/TestDbContextModelSnapshot.cs deleted file mode 100644 index e76dd8b8..00000000 --- a/tests/Backend.Fx.EfCorePersistence.Tests/Migrations/TestDbContextModelSnapshot.cs +++ /dev/null @@ -1,152 +0,0 @@ -// -using System; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; - -namespace Backend.Fx.EfCorePersistence.Tests.Migrations -{ - [DbContext(typeof(TestDbContext))] - partial class TestDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.1.1-rtm-30846"); - - modelBuilder.Entity("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.Blog", b => - { - b.Property("Id"); - - b.Property("ChangedBy") - .HasMaxLength(100); - - b.Property("ChangedOn"); - - b.Property("CreatedBy") - .HasMaxLength(100); - - b.Property("CreatedOn"); - - b.Property("Name"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate(); - - b.Property("TenantId"); - - b.HasKey("Id"); - - b.ToTable("Blogs"); - }); - - modelBuilder.Entity("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.Blogger", b => - { - b.Property("Id"); - - b.Property("Bio"); - - b.Property("ChangedBy") - .HasMaxLength(100); - - b.Property("ChangedOn"); - - b.Property("CreatedBy") - .HasMaxLength(100); - - b.Property("CreatedOn"); - - b.Property("FirstName"); - - b.Property("LastName"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate(); - - b.Property("TenantId"); - - b.HasKey("Id"); - - b.ToTable("Bloggers"); - }); - - modelBuilder.Entity("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.Post", b => - { - b.Property("Id"); - - b.Property("BlogId"); - - b.Property("ChangedBy") - .HasMaxLength(100); - - b.Property("ChangedOn"); - - b.Property("CreatedBy") - .HasMaxLength(100); - - b.Property("CreatedOn"); - - b.Property("Name"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate(); - - b.HasKey("Id"); - - b.HasIndex("BlogId"); - - b.ToTable("Posts"); - }); - - modelBuilder.Entity("Backend.Fx.Environment.MultiTenancy.Tenant", b => - { - b.Property("Id") - .ValueGeneratedOnAdd(); - - b.Property("DefaultCultureName"); - - b.Property("Description"); - - b.Property("IsDemoTenant"); - - b.Property("Name") - .IsRequired(); - - b.Property("State"); - - b.HasKey("Id"); - - b.ToTable("Tenants"); - }); - - modelBuilder.Entity("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.Post", b => - { - b.HasOne("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.Blog", "Blog") - .WithMany("Posts") - .HasForeignKey("BlogId") - .OnDelete(DeleteBehavior.Cascade); - - b.OwnsOne("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.TargetAudience", "TargetAudience", b1 => - { - b1.Property("PostId"); - - b1.Property("Culture"); - - b1.Property("IsPublic"); - - b1.ToTable("Posts"); - - b1.HasOne("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.Post") - .WithOne("TargetAudience") - .HasForeignKey("Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain.TargetAudience", "PostId") - .OnDelete(DeleteBehavior.Cascade); - }); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/TestConfig.cs b/tests/Backend.Fx.EfCorePersistence.Tests/TestConfig.cs deleted file mode 100644 index 6a3fbb4c..00000000 --- a/tests/Backend.Fx.EfCorePersistence.Tests/TestConfig.cs +++ /dev/null @@ -1,17 +0,0 @@ -// using Backend.Fx.EfCorePersistence.Tests; -// using Backend.Fx.NLogLogging; -// using MarcWittke.Xunit.AssemblyFixture; -// using Xunit; -// -// [assembly: TestFramework("MarcWittke.Xunit.AssemblyFixture.XunitTestFrameworkWithAssemblyFixture", "MarcWittke.Xunit.AssemblyFixture")] -// [assembly: AssemblyFixture(typeof(TestLoggingFixture))] -// -// namespace Backend.Fx.EfCorePersistence.Tests -// { -// public class TestLoggingFixture : LoggingFixture -// { -// public TestLoggingFixture() : base("Backend.Fx") -// { -// } -// } -// } \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/TheDbContext.cs b/tests/Backend.Fx.EfCorePersistence.Tests/TheDbContext.cs deleted file mode 100644 index 2374a566..00000000 --- a/tests/Backend.Fx.EfCorePersistence.Tests/TheDbContext.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Linq; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain; -using Backend.Fx.EfCorePersistence.Tests.Fixtures; -using Backend.Fx.Tests; -using Microsoft.EntityFrameworkCore; -using Serilog.Formatting.Display; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.EfCorePersistence.Tests -{ - public class TheDbContext: TestWithLogging - { - public TheDbContext(ITestOutputHelper output) : base(output) - { - _fixture = new SqliteDatabaseFixture(); - _fixture.CreateDatabase(); - } - - private readonly DatabaseFixture _fixture; - private static int _nextTenantId = 2675; - private readonly int _tenantId = _nextTenantId++; - - [Fact] - public void CanClearAndReplaceDependentEntities() - { - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var blog = new Blog(1, "original blog") {TenantId = _tenantId}; - blog.Posts.Add(new Post(1, blog, "new name 1")); - blog.Posts.Add(new Post(2, blog, "new name 2")); - blog.Posts.Add(new Post(3, blog, "new name 3")); - blog.Posts.Add(new Post(4, blog, "new name 4")); - blog.Posts.Add(new Post(5, blog, "new name 5")); - dbSession.DbContext.Add(blog); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - Blog blog = dbSession.DbContext.Blogs.Include(b => b.Posts).Single(b => b.Id == 1); - blog.Posts.Clear(); - blog.Posts.Add(new Post(6, blog, "new name 6")); - blog.Posts.Add(new Post(7, blog, "new name 7")); - blog.Posts.Add(new Post(8, blog, "new name 8")); - blog.Posts.Add(new Post(9, blog, "new name 9")); - blog.Posts.Add(new Post(10, blog, "new name 10")); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - Blog blog = dbSession.DbContext.Blogs.Include(b => b.Posts).Single(b => b.Id == 1); - - Assert.Equal(5, blog.Posts.Count); - - for (var i = 1; i <= 5; i++) Assert.DoesNotContain(blog.Posts, p => p.Id == i); - - for (var i = 6; i <= 10; i++) Assert.Contains(blog.Posts, p => p.Id == i); - } - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/TheRepositoryOfComposedAggregate.cs b/tests/Backend.Fx.EfCorePersistence.Tests/TheRepositoryOfComposedAggregate.cs deleted file mode 100644 index a8ebb6a0..00000000 --- a/tests/Backend.Fx.EfCorePersistence.Tests/TheRepositoryOfComposedAggregate.cs +++ /dev/null @@ -1,353 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence; -using Backend.Fx.EfCorePersistence.Tests.Fixtures; -using Backend.Fx.Environment.DateAndTime; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Extensions; -using Backend.Fx.Patterns.Authorization; -using Backend.Fx.Patterns.IdGeneration; -using Backend.Fx.Tests; -using FakeItEasy; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.EfCorePersistence.Tests -{ - public class TheRepositoryOfComposedAggregate : TestWithLogging - { - public TheRepositoryOfComposedAggregate(ITestOutputHelper testOutputHelper) : base(testOutputHelper) - { - A.CallTo(() => _idGenerator.NextId()).ReturnsLazily(() => _nextId++); - //_fixture = new SqlServerDatabaseFixture(); - _fixture = new SqliteDatabaseFixture(); - _fixture.CreateDatabase(); - } - - private static int _nextTenantId = 57839; - private static int _nextId = 1; - private readonly int _tenantId = _nextTenantId++; - - private readonly IEqualityComparer _tolerantDateTimeComparer = - new TolerantDateTimeComparer(TimeSpan.FromMilliseconds(5000)); - - private readonly IEntityIdGenerator _idGenerator = A.Fake(); - private readonly DatabaseFixture _fixture; - - private int CreateBlogWithPost(IDbConnection dbConnection, int postCount = 1) - { - { - var blogId = _nextId++; - dbConnection.ExecuteNonQuery( - $"INSERT INTO Blogs (Id, TenantId, Name, CreatedOn, CreatedBy) VALUES ({blogId}, {CurrentTenantIdHolder.Create(_tenantId).Current.Value}, 'my blog', CURRENT_TIMESTAMP, 'persistence test')"); - var count = dbConnection.ExecuteScalar("SELECT count(*) FROM Blogs"); - Assert.Equal(1, count); - - for (var i = 0; i < postCount; i++) - dbConnection.ExecuteNonQuery( - $"INSERT INTO Posts (Id, BlogId, Name, TargetAudience_IsPublic, TargetAudience_Culture, CreatedOn, CreatedBy) VALUES ({_nextId++}, {blogId}, 'my post {i:00}', '1', 'de-DE', CURRENT_TIMESTAMP, 'persistence test')"); - - return blogId; - } - } - - //FAILING!!!! - // this shows, that ValueObjects treated as OwnedTypes are not supported very well - //[Fact] - //public void CanUpdateDependantValueObject() - //{ - // using (DbSession dbs = _fixture.UseDbSession()) - // { - // int id = CreateBlogWithPost(dbSession.DbConnection, 10); - // Post post; - - // using (var uow = dbs.UseUnitOfWork(_clock)) - // { - // var sut = new EfRepository(uow.DbContext, new BlogMapping(), CurrentTenantIdHolder.Create(_tenantId), - // new AllowAll()); - // var blog = sut.Single(id); - // post = blog.Posts.First(); - // post.TargetAudience = new TargetAudience{Culture = "es-AR", IsPublic = false}; - // uow.Complete(); - // } - - // - // { - // string culture = dbSession.DbConnection.ExecuteScalar($"SELECT TargetAudience_Culture ame FROM Posts where id = {post.Id}"); - // Assert.Equal("es-AR", culture); - - // string strChangedOn = dbSession.DbConnection.ExecuteScalar($"SELECT ChangedOn FROM Posts where id = {post.Id}"); - // DateTime changedOn = DateTime.Parse(strChangedOn); - // Assert.Equal(_clock.UtcNow, changedOn, new TolerantDateTimeComparer(TimeSpan.FromMilliseconds(500))); - // } - // } - //} - - [Fact] - public void CanAddDependent() - { - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var id = CreateBlogWithPost(dbSession.DbConnection, 10); - var sut = new EfRepository(dbSession.DbContext, - new BlogMapping(), - CurrentTenantIdHolder.Create(_tenantId), - new AllowAll()); - Blog blog = sut.Single(id); - blog.Posts.Add(new Post(_idGenerator.NextId(), blog, "added")); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Posts"); - Assert.Equal(11, count); - } - } - - [Fact] - public void CanCreate() - { - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Blogs"); - Assert.Equal(0, count); - - count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Posts"); - Assert.Equal(0, count); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var sut = new EfRepository(dbSession.DbContext, - new BlogMapping(), - CurrentTenantIdHolder.Create(_tenantId), - new AllowAll()); - var blog = new Blog(_idGenerator.NextId(), "my blog"); - blog.AddPost(_idGenerator, "my post"); - sut.Add(blog); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Blogs"); - Assert.Equal(1, count); - - count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Posts"); - Assert.Equal(1, count); - } - } - - [Fact] - public void CanDelete() - { - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var id = CreateBlogWithPost(dbSession.DbConnection); - - var sut = new EfRepository(dbSession.DbContext, - new BlogMapping(), - CurrentTenantIdHolder.Create(_tenantId), - new AllowAll()); - Blog blog = sut.Single(id); - sut.Delete(blog); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Blogs"); - Assert.Equal(0, count); - - count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Posts"); - Assert.Equal(0, count); - } - } - - [Fact] - public void CanDeleteDependent() - { - int id; - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - id = CreateBlogWithPost(dbSession.DbConnection, 10); - var count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Posts"); - Assert.Equal(10, count); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var sut = new EfRepository(dbSession.DbContext, - new BlogMapping(), - CurrentTenantIdHolder.Create(_tenantId), - new AllowAll()); - Blog blog = sut.Single(id); - Post firstPost = blog.Posts.First(); - firstPost.SetName("sadfasfsadf"); - blog.Posts.Remove(firstPost); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Posts"); - Assert.Equal(9, count); - } - } - - - [Fact] - public void CanRead() - { - int id; - Blog blog; - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - id = CreateBlogWithPost(dbSession.DbConnection); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var sut = new EfRepository(dbSession.DbContext, - new BlogMapping(), - CurrentTenantIdHolder.Create(_tenantId), - new AllowAll()); - blog = sut.Single(id); - } - - Assert.NotNull(blog); - Assert.Equal(id, blog.Id); - Assert.Equal("my blog", blog.Name); - Assert.NotEmpty(blog.Posts); - } - - - [Fact] - public void CanReplaceDependentCollection() - { - int id; - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - id = CreateBlogWithPost(dbSession.DbConnection, 10); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var sut = new EfRepository(dbSession.DbContext, - new BlogMapping(), - CurrentTenantIdHolder.Create(_tenantId), - new AllowAll()); - Blog blog = sut.Single(id); - blog.Posts.Clear(); - blog.Posts.Add(new Post(_idGenerator.NextId(), blog, "new name 1")); - blog.Posts.Add(new Post(_idGenerator.NextId(), blog, "new name 2")); - blog.Posts.Add(new Post(_idGenerator.NextId(), blog, "new name 3")); - blog.Posts.Add(new Post(_idGenerator.NextId(), blog, "new name 4")); - blog.Posts.Add(new Post(_idGenerator.NextId(), blog, "new name 5")); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Posts"); - Assert.Equal(5, count); - } - } - - [Fact] - public void CanUpdate() - { - int id; - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - id = CreateBlogWithPost(dbSession.DbConnection); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var sut = new EfRepository(dbSession.DbContext, - new BlogMapping(), - CurrentTenantIdHolder.Create(_tenantId), - new AllowAll()); - Blog blog = sut.Single(id); - blog.Modify("modified"); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - Assert.Equal(1, dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Blogs")); - Assert.Equal(id, dbSession.DbConnection.ExecuteScalar("SELECT Id FROM Blogs LIMIT 1")); - Assert.Equal("modified", dbSession.DbConnection.ExecuteScalar("SELECT Name FROM Blogs LIMIT 1")); - Assert.Equal("modified", dbSession.DbConnection.ExecuteScalar("SELECT Name FROM Posts LIMIT 1")); - } - } - - [Fact] - public void CanUpdateDependant() - { - var clock = new AdjustableClock(new WallClock()); - clock.OverrideUtcNow(new DateTime(2020, 01, 20, 20, 30, 40)); - - int id; - using (TestDbSession dbSession = _fixture.CreateTestDbSession(clock: clock)) - { - id = CreateBlogWithPost(dbSession.DbConnection, 10); - } - - Post post; - - using (TestDbSession dbSession = _fixture.CreateTestDbSession(clock: clock)) - { - var sut = new EfRepository(dbSession.DbContext, - new BlogMapping(), - CurrentTenantIdHolder.Create(_tenantId), - new AllowAll()); - Blog blog = sut.Single(id); - post = blog.Posts.First(); - post.SetName("modified"); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession(clock: clock)) - { - var name = dbSession.DbConnection.ExecuteScalar($"SELECT name FROM Posts where id = {post.Id}"); - Assert.Equal("modified", name); - - var strChangedOn = dbSession.DbConnection.ExecuteScalar($"SELECT changedon FROM Posts where id = {post.Id}"); - DateTime changedOn = DateTime.Parse(strChangedOn); - Assert.Equal(clock.UtcNow, changedOn, new TolerantDateTimeComparer(TimeSpan.FromMilliseconds(500))); - } - } - - [Fact] - public void UpdatesAggregateTrackingPropertiesOnDeleteOfDependant() - { - var clock = new AdjustableClock(new WallClock()); - clock.OverrideUtcNow(new DateTime(2020, 01, 20, 20, 30, 40)); - - int id; - using (TestDbSession dbSession = _fixture.CreateTestDbSession(clock: clock)) - { - id = CreateBlogWithPost(dbSession.DbConnection, 10); - } - - DateTime expectedModifiedOn = clock.Advance(TimeSpan.FromHours(1)); - - using (TestDbSession dbSession = _fixture.CreateTestDbSession(clock: clock)) - { - var sut = new EfRepository(dbSession.DbContext, - new BlogMapping(), - CurrentTenantIdHolder.Create(_tenantId), - new AllowAll()); - Blog b = sut.Single(id); - b.Posts.Remove(b.Posts.First()); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession(clock: clock)) - { - Blog blog = dbSession.DbContext.Set().Find(id); - Assert.NotNull(blog.ChangedOn); - Assert.Equal(expectedModifiedOn, blog.ChangedOn.Value, _tolerantDateTimeComparer); - } - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/TheRepositoryOfPlainAggregate.cs b/tests/Backend.Fx.EfCorePersistence.Tests/TheRepositoryOfPlainAggregate.cs deleted file mode 100644 index a77f4b57..00000000 --- a/tests/Backend.Fx.EfCorePersistence.Tests/TheRepositoryOfPlainAggregate.cs +++ /dev/null @@ -1,191 +0,0 @@ -using System; -using System.Threading.Tasks; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence; -using Backend.Fx.EfCorePersistence.Tests.Fixtures; -using Backend.Fx.Environment.Authentication; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Extensions; -using Backend.Fx.Patterns.Authorization; -using Backend.Fx.Tests; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.EfCorePersistence.Tests -{ - public class TheRepositoryOfPlainAggregate: TestWithLogging - { - public TheRepositoryOfPlainAggregate(ITestOutputHelper testOutputHelper) : base(testOutputHelper) - { - //_fixture = new SqlServerDatabaseFixture(); - _fixture = new SqliteDatabaseFixture(); - _fixture.CreateDatabase(); - } - - private static int _nextTenantId = 12312; - private readonly int _tenantId = _nextTenantId++; - private readonly DatabaseFixture _fixture; - - [Fact] - public void CanCreate() - { - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var repo = new EfRepository(dbSession.DbContext, new BloggerMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - repo.Add(new Blogger(345, "Metulsky", "Bratislav")); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var count = dbSession.DbConnection.ExecuteScalar("SELECT Count(*) FROM Bloggers"); - Assert.Equal(1, count); - - count = dbSession.DbConnection.ExecuteScalar( - $"SELECT Count(*) FROM Bloggers WHERE Id=345"); - Assert.Equal(1, count); - } - } - - - [Fact] - public void CanDelete() - { - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - dbSession.DbConnection.ExecuteNonQuery( - "INSERT INTO Bloggers (Id, TenantId, CreatedOn, CreatedBy, FirstName, LastName, Bio) " + - $"VALUES (555, {_tenantId}, '2012-05-12 23:12:09', 'the test', 'Bratislav', 'Metulsky', 'whatever')"); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var repo = new EfRepository(dbSession.DbContext, new BloggerMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - Blogger bratislavMetulsky = repo.Single(555); - repo.Delete(bratislavMetulsky); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var count = dbSession.DbConnection.ExecuteScalar("SELECT Count(*) FROM Bloggers WHERE Id = 555"); - Assert.Equal(0, count); - } - } - - - - [Fact] - public void CanRead() - { - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - dbSession.DbConnection.ExecuteNonQuery( - "INSERT INTO Bloggers (Id, TenantId, CreatedOn, CreatedBy, FirstName, LastName, Bio) " + - $"VALUES (444, {_tenantId}, '2012-05-12 23:12:09', 'the test', 'Bratislav', 'Metulsky', 'whatever')"); - - { - var repo = new EfRepository(dbSession.DbContext, new BloggerMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - - bool any = repo.Any(); - Assert.True(any); - - Blogger[] all = repo.GetAll(); - Assert.NotEmpty(all); - - Blogger bratislavMetulsky = repo.Single(444); - Assert.Equal(_tenantId, bratislavMetulsky.TenantId); - Assert.Equal("the test", bratislavMetulsky.CreatedBy); - Assert.Equal(new DateTime(2012, 05, 12, 23, 12, 09), bratislavMetulsky.CreatedOn); - Assert.Equal("Bratislav", bratislavMetulsky.FirstName); - Assert.Equal("Metulsky", bratislavMetulsky.LastName); - Assert.Equal("whatever", bratislavMetulsky.Bio); - - bratislavMetulsky = repo.SingleOrDefault(444); - Assert.NotNull(bratislavMetulsky); - Assert.Equal(_tenantId, bratislavMetulsky.TenantId); - Assert.Equal("the test", bratislavMetulsky.CreatedBy); - Assert.Equal(new DateTime(2012, 05, 12, 23, 12, 09), bratislavMetulsky.CreatedOn); - Assert.Equal("Bratislav", bratislavMetulsky.FirstName); - Assert.Equal("Metulsky", bratislavMetulsky.LastName); - Assert.Equal("whatever", bratislavMetulsky.Bio); - } - } - } - - - [Fact] - public async Task CanReadAsync() - { - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - dbSession.DbConnection.ExecuteNonQuery( - "INSERT INTO Bloggers (Id, TenantId, CreatedOn, CreatedBy, FirstName, LastName, Bio) " + - $"VALUES (555, {_tenantId}, '2012-05-12 23:12:09', 'the test', 'Bratislav', 'Metulsky', 'whatever')"); - - { - var repo = new EfRepository(dbSession.DbContext, new BloggerMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - - bool any = await repo.AnyAsync(); - Assert.True(any); - - Blogger[] all = await repo.GetAllAsync(); - Assert.NotEmpty(all); - - Blogger bratislavMetulsky = await repo.SingleAsync(555); - Assert.Equal(_tenantId, bratislavMetulsky.TenantId); - Assert.Equal("the test", bratislavMetulsky.CreatedBy); - Assert.Equal(new DateTime(2012, 05, 12, 23, 12, 09), bratislavMetulsky.CreatedOn); - Assert.Equal("Bratislav", bratislavMetulsky.FirstName); - Assert.Equal("Metulsky", bratislavMetulsky.LastName); - Assert.Equal("whatever", bratislavMetulsky.Bio); - - bratislavMetulsky = await repo.SingleOrDefaultAsync(555); - Assert.NotNull(bratislavMetulsky); - Assert.Equal(_tenantId, bratislavMetulsky.TenantId); - Assert.Equal("the test", bratislavMetulsky.CreatedBy); - Assert.Equal(new DateTime(2012, 05, 12, 23, 12, 09), bratislavMetulsky.CreatedOn); - Assert.Equal("Bratislav", bratislavMetulsky.FirstName); - Assert.Equal("Metulsky", bratislavMetulsky.LastName); - Assert.Equal("whatever", bratislavMetulsky.Bio); - } - } - } - - [Fact] - public void CanUpdate() - { - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - dbSession.DbConnection.ExecuteNonQuery( - "INSERT INTO Bloggers (Id, TenantId, CreatedOn, CreatedBy, FirstName, LastName, Bio) " + - $"VALUES (456, {_tenantId}, '2012-05-12 23:12:09', 'the test', 'Bratislav', 'Metulsky', 'whatever')"); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var repo = new EfRepository(dbSession.DbContext, new BloggerMapping(), - CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - Blogger bratislavMetulsky = repo.Single(456); - bratislavMetulsky.FirstName = "Johnny"; - bratislavMetulsky.LastName = "Flash"; - bratislavMetulsky.Bio = "Der lustige Clown"; - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var count = dbSession.DbConnection.ExecuteScalar( - $"SELECT Count(*) FROM Bloggers WHERE FirstName = 'Johnny' AND LastName = 'Flash' AND TenantId = '{_tenantId}'"); - Assert.Equal(1, count); - } - - using (TestDbSession dbSession = _fixture.CreateTestDbSession()) - { - var repo = new EfRepository(dbSession.DbContext, new BloggerMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - Blogger johnnyFlash = repo.Single(456); - Assert.Equal(DateTime.UtcNow, johnnyFlash.ChangedOn, new TolerantDateTimeComparer(TimeSpan.FromMilliseconds(5000))); - Assert.Equal(new SystemIdentity().Name, johnnyFlash.ChangedBy); - Assert.Equal("Johnny", johnnyFlash.FirstName); - Assert.Equal("Flash", johnnyFlash.LastName); - } - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.RabbitMq.Tests/Backend.Fx.RabbitMq.Tests.csproj b/tests/Backend.Fx.RabbitMq.Tests/Backend.Fx.RabbitMq.Tests.csproj index 3072ebc7..567519f7 100644 --- a/tests/Backend.Fx.RabbitMq.Tests/Backend.Fx.RabbitMq.Tests.csproj +++ b/tests/Backend.Fx.RabbitMq.Tests/Backend.Fx.RabbitMq.Tests.csproj @@ -1,18 +1,21 @@ - netcoreapp3.1 + net6.0 false - - - + + + - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -20,7 +23,7 @@ - + diff --git a/tests/Backend.Fx.RabbitMq.Tests/DummyIntegrationEvent.cs b/tests/Backend.Fx.RabbitMq.Tests/DummyIntegrationEvent.cs new file mode 100644 index 00000000..f9865535 --- /dev/null +++ b/tests/Backend.Fx.RabbitMq.Tests/DummyIntegrationEvent.cs @@ -0,0 +1,7 @@ +using Backend.Fx.Features.MessageBus; + +namespace Backend.Fx.RabbitMq.Tests; + +public class DummyIntegrationEvent : IntegrationEvent +{ +} \ No newline at end of file diff --git a/tests/Backend.Fx.RabbitMq.Tests/DummyIntegrationEventHandler.cs b/tests/Backend.Fx.RabbitMq.Tests/DummyIntegrationEventHandler.cs new file mode 100644 index 00000000..6156bf61 --- /dev/null +++ b/tests/Backend.Fx.RabbitMq.Tests/DummyIntegrationEventHandler.cs @@ -0,0 +1,16 @@ +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Features.MessageBus; + +namespace Backend.Fx.RabbitMq.Tests; + +public class DummyIntegrationEventHandler : IIntegrationEventHandler +{ + public static ManualResetEvent Called = new ManualResetEvent(false); + + public Task HandleAsync(DummyIntegrationEvent integrationEvent, CancellationToken cancellationToken = default) + { + Called.Set(); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.RabbitMq.Tests/TheRabbitMqChannel.cs b/tests/Backend.Fx.RabbitMq.Tests/TheRabbitMqChannel.cs index 768aeee7..c0dcf161 100644 --- a/tests/Backend.Fx.RabbitMq.Tests/TheRabbitMqChannel.cs +++ b/tests/Backend.Fx.RabbitMq.Tests/TheRabbitMqChannel.cs @@ -1,7 +1,7 @@ using System.Linq; using System.Text; using System.Threading; -using Backend.Fx.Tests; +using Backend.Fx.TestUtil; using RabbitMQ.Client; using RabbitMQ.Client.Events; using Xunit; @@ -96,7 +96,7 @@ public void Connect() _testOutputHelper.WriteLine(" [*] Waiting for messages."); var consumer = new EventingBasicConsumer(_channel); - consumer.Received += (model, ea) => + consumer.Received += (_, ea) => { var body = ea.Body.ToArray(); var message = Encoding.UTF8.GetString(body); diff --git a/tests/Backend.Fx.RabbitMq.Tests/TheRabbitMqMessageBus.cs b/tests/Backend.Fx.RabbitMq.Tests/TheRabbitMqMessageBus.cs index b26b7506..861b965e 100644 --- a/tests/Backend.Fx.RabbitMq.Tests/TheRabbitMqMessageBus.cs +++ b/tests/Backend.Fx.RabbitMq.Tests/TheRabbitMqMessageBus.cs @@ -1,88 +1,96 @@ -using System; +using System.Collections.Generic; using System.Diagnostics; -using System.Threading; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.Patterns.EventAggregation.Integration; -using Backend.Fx.Tests; +using System.Linq; +using System.Threading.Tasks; +using Backend.Fx.Features.MessageBus; +using Backend.Fx.Logging; +using Backend.Fx.SimpleInjectorDependencyInjection; +using Backend.Fx.TestUtil; using FakeItEasy; -using RabbitMQ.Client; +using Microsoft.Extensions.DependencyInjection; using Xunit; using Xunit.Abstractions; -namespace Backend.Fx.RabbitMq.Tests -{ - public class TheRabbitMqMessageBus : TestWithLogging - { - private readonly ManualResetEvent _received = new ManualResetEvent(false); - private readonly BackendFxApplicationInvoker _senderInvoker; - private readonly BackendFxApplicationInvoker _receiverInvoker; +namespace Backend.Fx.RabbitMq.Tests; - public TheRabbitMqMessageBus(ITestOutputHelper testOutputHelper) : base(testOutputHelper) - { - var fakeSenderApplication = A.Fake(); - _senderInvoker = new BackendFxApplicationInvoker(fakeSenderApplication.CompositionRoot); +public class TheRabbitMqMessageBus : TestWithLogging +{ + private readonly IBackendFxApplication _sender; + private readonly IBackendFxApplication _recipient; + private readonly IExceptionLogger _exceptionLogger = A.Fake(); + // requires rabbitmq running on localhost: + // docker run -p 5672:5672 -e RABBITMQ_DEFAULT_USER=test -e RABBITMQ_DEFAULT_PASS=password rabbitmq + private readonly RabbitMqOptions _options = new() + { + Hostname = "localhost", + Username = "test", + Password = "password", + ExchangeName = "test-exchange", + ReceiveQueueName = "test-queue", + RetryCount = 3, + }; - var fakeReceiverApplication = A.Fake(); - _receiverInvoker = new BackendFxApplicationInvoker(fakeReceiverApplication.CompositionRoot); - var fakeScope = A.Fake(); - var fakeInstanceProvider = A.Fake(); - A.CallTo(() => fakeReceiverApplication.CompositionRoot.BeginScope()).Returns(fakeScope); - A.CallTo(() => fakeScope.InstanceProvider).Returns(fakeInstanceProvider); - A.CallTo(() => fakeInstanceProvider.GetInstance(A.That.IsEqualTo(typeof(TestIntegrationEventHandler)))) - .Returns(new TestIntegrationEventHandler(_received)); - } + public TheRabbitMqMessageBus(ITestOutputHelper output) : base(output) + { + _sender = new BackendFxApplication(new SimpleInjectorCompositionRoot(), _exceptionLogger); + _sender.EnableFeature(new MessageBusFeature(new RabbitMqMessageBus(_options))); + + _recipient = new BackendFxApplication(new SimpleInjectorCompositionRoot(), _exceptionLogger, GetType().Assembly); + _recipient.EnableFeature(new MessageBusFeature(new RabbitMqMessageBus(_options))); + } - //[Fact] - public void CanBeUsedWithBackendFxApplication() + [Fact] + public async Task InjectsMessageBusScope() + { + await _sender.BootAsync(); + + await _sender.Invoker.InvokeAsync(sp => { - IMessageBus sender = new RabbitMqMessageBus(new ConnectionFactory - { - HostName = "localhost", - UserName = "anicors", - Password = "R4bb!tMQ" - }, 5, "unittest", "testSender"); - sender.ProvideInvoker(_senderInvoker); - sender.Connect(); - - var receiver = new RabbitMqMessageBus(new ConnectionFactory - { - HostName = "localhost", - UserName = "anicors", - Password = "R4bb!tMQ" - }, 5, "unittest", "testReceiver"); - - receiver.ProvideInvoker(_receiverInvoker); - receiver.Connect(); - receiver.Subscribe(); - - sender.Publish(new TestIntegrationEvent(1)); - Assert.True(_received.WaitOne(Debugger.IsAttached ? int.MaxValue : 5000)); - } - - public class TestIntegrationEventHandler : IIntegrationMessageHandler + var messageBusScope = sp.GetRequiredService(); + Assert.NotNull(messageBusScope); + return Task.CompletedTask; + }); + } + + [Fact] + public async Task InjectsHandlersAsCollections() + { + await _recipient.BootAsync(); + await _recipient.Invoker.InvokeAsync(sp => { - private readonly ManualResetEvent _received; + var handlers = sp.GetRequiredService>>().ToArray(); + Assert.NotNull(handlers); + Assert.IsType(Assert.Single(handlers)); + return Task.CompletedTask; + }); + } - public TestIntegrationEventHandler(ManualResetEvent received) - { - _received = received; - } + //todo fixme [Fact] + public async Task PublishesAndHandlesEvents() + { + // we have two applications + await _sender.BootAsync(); + await _recipient.BootAsync(); + + // we send an integration event on the first app + var dummyIntegrationEvent = new DummyIntegrationEvent(); + await _sender.Invoker.InvokeAsync(sp => + { + var messageBusScope = sp.GetRequiredService(); + messageBusScope.Publish(dummyIntegrationEvent); + return Task.CompletedTask; + }); - public void Handle(TestIntegrationEvent eventData) - { - _received.Set(); - } - } + Assert.True(DummyIntegrationEventHandler.Called.WaitOne(Debugger.IsAttached ? int.MaxValue : 3000)); + } - public class TestIntegrationEvent : IntegrationEvent + protected override void Dispose(bool disposing) + { + if (disposing) { - public TestIntegrationEvent(int sequencenumber) : base() - { - Sequencenumber = sequencenumber; - } - - public int Sequencenumber { get; } + _sender.Dispose(); + _recipient.Dispose(); } } } \ No newline at end of file diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests.csproj b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests.csproj deleted file mode 100644 index d47530f5..00000000 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - netcoreapp3.1 - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADemoAggregateGenerator.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADemoAggregateGenerator.cs deleted file mode 100644 index a43d9900..00000000 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADemoAggregateGenerator.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Backend.Fx.BuildingBlocks; -using Backend.Fx.Patterns.DataGeneration; - -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain -{ - public class ADemoAggregateGenerator : DataGenerator, IDemoDataGenerator - { - private static int _id = 457567; - public static string Name = "Demo record"; - - private readonly IRepository _repository; - public override int Priority => 1; - - public ADemoAggregateGenerator(IRepository repository) - { - _repository = repository; - } - - protected override void GenerateCore() - { - _repository.Add(new AnAggregate(_id++, Name)); - } - - protected override void Initialize() - { } - - protected override bool ShouldRun() - { - return true; - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainEvent.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainEvent.cs deleted file mode 100644 index 80e47e33..00000000 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainEvent.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Backend.Fx.Patterns.EventAggregation.Domain; - -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain -{ - public class ADomainEvent : IDomainEvent - { - } -} diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainEventHandler1.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainEventHandler1.cs deleted file mode 100644 index 6f1f7214..00000000 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainEventHandler1.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Backend.Fx.Patterns.EventAggregation.Domain; - -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain -{ - public class ADomainEventHandler1 : IDomainEventHandler - { - public void Handle(ADomainEvent domainEvent) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainEventHandler2.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainEventHandler2.cs deleted file mode 100644 index e52b0fe5..00000000 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainEventHandler2.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Backend.Fx.Patterns.EventAggregation.Domain; - -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain -{ - public class ADomainEventHandler2 : IDomainEventHandler - { - public void Handle(ADomainEvent domainEvent) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainEventHandler3.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainEventHandler3.cs deleted file mode 100644 index 72bb7c2b..00000000 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainEventHandler3.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Backend.Fx.Patterns.EventAggregation.Domain; - -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain -{ - public class ADomainEventHandler3 : IDomainEventHandler - { - public void Handle(ADomainEvent domainEvent) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainModule.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainModule.cs deleted file mode 100644 index 6dbb4c02..00000000 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainModule.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Reflection; -using Backend.Fx.SimpleInjectorDependencyInjection.Modules; -using SimpleInjector; - -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain -{ - public class ADomainModule : SimpleInjectorDomainModule - { - public ADomainModule(params Assembly[] domainAssemblies) - : base(domainAssemblies) - { - } - - protected override void Register(Container container, ScopedLifestyle scopedLifestyle) - { - base.Register(container, scopedLifestyle); - container.RegisterSingleton(); - container.Register(scopedLifestyle); - } - } -} diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainService.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainService.cs deleted file mode 100644 index d2feebf0..00000000 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainService.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Backend.Fx.BuildingBlocks; - -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain -{ - public interface ITestDomainService : IDomainService - { } - - public interface IAnotherTestDomainService : IDomainService - { } - - public class ADomainService : ITestDomainService, IAnotherTestDomainService - { } -} diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AJob.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AJob.cs deleted file mode 100644 index fa1ac6d0..00000000 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AJob.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Backend.Fx.Patterns.Jobs; -using JetBrains.Annotations; - -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain -{ - [UsedImplicitly] - public class AJob : IJob - { - public void Run() - {} - } -} diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AProdAggregateGenerator.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AProdAggregateGenerator.cs deleted file mode 100644 index d8d1e2cd..00000000 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AProdAggregateGenerator.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Backend.Fx.BuildingBlocks; -using Backend.Fx.Patterns.DataGeneration; - -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain -{ - public class AProdAggregateGenerator : DataGenerator, IProductiveDataGenerator - { - private static int _id = 2341234; - public static string Name = "Productive record"; - - private readonly IRepository _repository; - public override int Priority => 1; - - public AProdAggregateGenerator(IRepository repository) - { - _repository = repository; - } - - protected override void GenerateCore() - { - _repository.Add(new AnAggregate(_id++, Name)); - } - - protected override void Initialize() - { } - - protected override bool ShouldRun() - { - return true; - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ASingletonService.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ASingletonService.cs deleted file mode 100644 index 2ca6e09d..00000000 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ASingletonService.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain -{ - public interface ISingletonService {} - - public class ASingletonService : ISingletonService - { - - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AnAggregate.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AnAggregate.cs deleted file mode 100644 index 592f21ba..00000000 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AnAggregate.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Backend.Fx.BuildingBlocks; - -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain -{ - public class AnAggregate : AggregateRoot - { - public AnAggregate(int id, string name) : base(id) - { - Name = name; - } - - public string Name { get; set; } - } -} diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AnAggregateAuthorization.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AnAggregateAuthorization.cs deleted file mode 100644 index 7b43b787..00000000 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AnAggregateAuthorization.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Backend.Fx.Patterns.Authorization; - -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain -{ - public class AnAggregateAuthorization : AllowAll { } -} diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AnApplicationService.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AnApplicationService.cs deleted file mode 100644 index ee4fa719..00000000 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AnApplicationService.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Backend.Fx.BuildingBlocks; - -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain -{ - public interface ITestApplicationService : IApplicationService - { } - - public class AnApplicationService : ITestApplicationService - { } -} diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AnIntegrationEvent.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AnIntegrationEvent.cs deleted file mode 100644 index 04e76118..00000000 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AnIntegrationEvent.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Backend.Fx.Patterns.EventAggregation.Integration; -using JetBrains.Annotations; - -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain -{ - [UsedImplicitly] - public class AnIntegrationEvent : IntegrationEvent - { - public AnIntegrationEvent(int tenantId, int whatever) : base() - { - Whatever = whatever; - } - - public int Whatever { [UsedImplicitly] get; } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/SomeState.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/SomeState.cs deleted file mode 100644 index a76d2934..00000000 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/SomeState.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain -{ - public class SomeState - { - public string Value { get; set; } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/TestConfig.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/TestConfig.cs deleted file mode 100644 index 6e64b497..00000000 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/TestConfig.cs +++ /dev/null @@ -1,15 +0,0 @@ -// using Backend.Fx.NLogLogging; -// using Backend.Fx.SimpleInjectorDependencyInjection.Tests; -// using MarcWittke.Xunit.AssemblyFixture; -// -// [assembly: Xunit.TestFramework("MarcWittke.Xunit.AssemblyFixture.XunitTestFrameworkWithAssemblyFixture", "MarcWittke.Xunit.AssemblyFixture")] -// [assembly: AssemblyFixture(typeof(TestLoggingFixture))] -// -// namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests -// { -// public class TestLoggingFixture : LoggingFixture -// { -// public TestLoggingFixture() : base("Backend.Fx") -// { } -// } -// } \ No newline at end of file diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/TheMisconfiguredSimpleInjectorCompositionRoot.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/TheMisconfiguredSimpleInjectorCompositionRoot.cs deleted file mode 100644 index fd4f355a..00000000 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/TheMisconfiguredSimpleInjectorCompositionRoot.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using Backend.Fx.BuildingBlocks; -using Backend.Fx.SimpleInjectorDependencyInjection.Modules; -using Backend.Fx.Tests; -using SimpleInjector; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests -{ - public class TheMisconfiguredSimpleInjectorCompositionRoot : TestWithLogging - { - [Fact] - public void ThrowsOnValidation() - { - var sut = new SimpleInjectorCompositionRoot(); - sut.RegisterModules(new BadModule()); - Assert.Throws(() => sut.Verify()); - } - - public class UnresolvableService - { - public UnresolvableService(Entity e) - { - throw new Exception( - $"This constructor should never be called, since the Entity {e?.GetType().Name} cannot be resolved by the container"); - } - } - - public class BadModule : SimpleInjectorModule - { - protected override void Register(Container container, ScopedLifestyle scopedLifestyle) - { - // this registration should be recognized as unresolvable during validation - container.Register(); - } - } - - public TheMisconfiguredSimpleInjectorCompositionRoot(ITestOutputHelper output) : base(output) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/TheSimpleInjectorCompositionRoot.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/TheSimpleInjectorCompositionRoot.cs deleted file mode 100644 index 1c2c4406..00000000 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/TheSimpleInjectorCompositionRoot.cs +++ /dev/null @@ -1,170 +0,0 @@ -using System.Linq; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain; -using Backend.Fx.Tests; -using SimpleInjector; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests -{ - public class TheSimpleInjectorCompositionRoot : TestWithLogging - { - private readonly SimpleInjectorCompositionRoot _sut; - - public TheSimpleInjectorCompositionRoot(ITestOutputHelper output) : base(output) - { - _sut = new SimpleInjectorCompositionRoot(); - Assembly domainAssembly = typeof(AnAggregate).GetTypeInfo().Assembly; - _sut.RegisterModules(new ADomainModule(domainAssembly)); - _sut.Verify(); - } - - [Fact] - public void ProvidesAutoRegisteredDomainServices() - { - using (_sut.BeginScope()) - { - var testDomainService = _sut.GetInstance(); - Assert.IsType(testDomainService); - } - } - - [Fact] - public void ProvidesAutoRegisteredDomainServicesThatImplementTwoInterfaces() - { - using (_sut.BeginScope()) - { - var testDomainService = _sut.GetInstance(); - Assert.IsType(testDomainService); - - var anotherTestDomainService = _sut.GetInstance(); - Assert.IsType(anotherTestDomainService); - - Assert.True(Equals(testDomainService, anotherTestDomainService)); - } - } - - [Fact] - public void ProvidesAutoRegisteredApplicationServices() - { - using (_sut.BeginScope()) - { - Assert.IsType(_sut.GetInstance()); - } - } - - - [Fact] - public void ProvidesScopedInstancesWhenScopeHasBeenStarted() - { - ITestDomainService scope1Instance; - ITestDomainService scope2Instance; - - using (_sut.BeginScope()) - { - scope1Instance = _sut.GetInstance(); - Assert.NotNull(scope1Instance); - } - - using (_sut.BeginScope()) - { - scope2Instance = _sut.GetInstance(); - Assert.NotNull(scope2Instance); - } - - Assert.NotEqual(scope1Instance, scope2Instance); - } - - [Fact] - public void ProvidesSingletonAndScopedInstancesAccordingly() - { - const int parallelScopeCount = 1000; - object[] scopedInstances = new object[parallelScopeCount]; - object[] singletonInstances = new object[parallelScopeCount]; - Task[] tasks = new Task[parallelScopeCount]; - - var waiter = new ManualResetEvent(false); - - // resolving a singleton service and a scoped service in a massive parallel scenario - for (int index = 0; index < parallelScopeCount; index++) - { - var indexClosure = index; - tasks[index] = Task.Factory.StartNew(() => - { - // using the reset event to enforce a maximum grade of parallelism - waiter.WaitOne(); - using (_sut.BeginScope()) - { - scopedInstances[indexClosure] = _sut.GetInstance(); - singletonInstances[indexClosure] = _sut.GetInstance(); - } - }); - } - - // let the show begin... - waiter.Set(); - Task.WaitAll(tasks); - - // asserting for equality: singleton instances must be equal, scoped instances must be unique - for (int index = 0; index < parallelScopeCount; index++) - { - Assert.NotNull(scopedInstances[index]); - Assert.NotNull(singletonInstances[index]); - - for (int indexComp = 0; indexComp < parallelScopeCount; indexComp++) - { - if (index != indexComp) - { - Assert.NotEqual(scopedInstances[index], scopedInstances[indexComp]); - } - - Assert.Equal(singletonInstances[index], singletonInstances[indexComp]); - } - } - } - - [Fact] - public void ThrowsWhenScopedInstanceIsRequestedOutsideScope() - { - Assert.Throws(() => _sut.GetInstance()); - Assert.Throws(() => _sut.GetInstance(typeof(ITestDomainService))); - Assert.Null(_sut.GetCurrentScope()); - - using (_sut.BeginScope()) - { - var sutInstance = _sut.GetInstance(); - var scopeInstance = _sut.GetInstance(); - Assert.NotNull(sutInstance); - Assert.NotNull(scopeInstance); - Assert.Equal(sutInstance, scopeInstance); - } - - Assert.Null(_sut.GetCurrentScope()); - Assert.Throws(() => _sut.GetInstance()); - Assert.Throws(() => _sut.GetInstance(typeof(ITestDomainService))); - } - - [Fact] - public void CanProvideEventHandlers() - { - using (_sut.BeginScope()) - { - var handlers = _sut.GetAllEventHandlers().ToArray(); - - // these three handlers should have been auto registered during boot by scanning the assembly - Assert.True(handlers.OfType().Any()); - Assert.True(handlers.OfType().Any()); - Assert.True(handlers.OfType().Any()); - } - } - - protected override void Dispose(bool disposing) - { - _sut.Dispose(); - base.Dispose(disposing); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.TestUtil/Backend.Fx.TestUtil.csproj b/tests/Backend.Fx.TestUtil/Backend.Fx.TestUtil.csproj new file mode 100644 index 00000000..769bdb63 --- /dev/null +++ b/tests/Backend.Fx.TestUtil/Backend.Fx.TestUtil.csproj @@ -0,0 +1,18 @@ + + + + netstandard2.1 + + + + + + + + + + + + + + diff --git a/tests/Backend.Fx.TestUtil/CompositionRootType.cs b/tests/Backend.Fx.TestUtil/CompositionRootType.cs new file mode 100644 index 00000000..ec345850 --- /dev/null +++ b/tests/Backend.Fx.TestUtil/CompositionRootType.cs @@ -0,0 +1,26 @@ +using System; +using Backend.Fx.DependencyInjection; +using Backend.Fx.MicrosoftDependencyInjection; +using Backend.Fx.SimpleInjectorDependencyInjection; + +namespace Backend.Fx.TestUtil +{ + public enum CompositionRootType + { + Microsoft, + SimpleInjector + } + + public static class CompositionRootTypeEx + { + public static ICompositionRoot Create(this CompositionRootType compositionRootType) + { + return compositionRootType switch + { + CompositionRootType.Microsoft => new MicrosoftCompositionRoot(), + CompositionRootType.SimpleInjector => new SimpleInjectorCompositionRoot(), + _ => throw new ArgumentException(nameof(compositionRootType)) + }; + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/TestWithLogging.cs b/tests/Backend.Fx.TestUtil/TestWithLogging.cs similarity index 89% rename from tests/Backend.Fx.Tests/TestWithLogging.cs rename to tests/Backend.Fx.TestUtil/TestWithLogging.cs index ad386a42..c77f9621 100644 --- a/tests/Backend.Fx.Tests/TestWithLogging.cs +++ b/tests/Backend.Fx.TestUtil/TestWithLogging.cs @@ -1,11 +1,10 @@ using System; -using Backend.Fx.Logging; using Serilog; using Serilog.Extensions.Logging; using Xunit.Abstractions; using ILogger = Serilog.ILogger; -namespace Backend.Fx.Tests +namespace Backend.Fx.TestUtil { public abstract class TestWithLogging : IDisposable { @@ -13,7 +12,7 @@ public abstract class TestWithLogging : IDisposable protected TestWithLogging(ITestOutputHelper output) { - var loggerConfiguration = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.TestOutput(output); + var loggerConfiguration = new LoggerConfiguration().MinimumLevel.Verbose().WriteTo.TestOutput(output); Logger = loggerConfiguration.CreateLogger(); _disposableLogger = Logging.Log.InitAsyncLocal(new SerilogLoggerFactory(Logger)); } diff --git a/tests/Backend.Fx.Tests/Backend.Fx.Tests.csproj b/tests/Backend.Fx.Tests/Backend.Fx.Tests.csproj index c9499d22..35653d47 100644 --- a/tests/Backend.Fx.Tests/Backend.Fx.Tests.csproj +++ b/tests/Backend.Fx.Tests/Backend.Fx.Tests.csproj @@ -1,38 +1,35 @@  - netcoreapp3.1 + net6.0 + default + enable - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + + - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - - - - - - - - - PreserveNewest - + + + diff --git a/tests/Backend.Fx.Tests/Backup.bak b/tests/Backend.Fx.Tests/Backup.bak deleted file mode 100644 index 2783379b..00000000 Binary files a/tests/Backend.Fx.Tests/Backup.bak and /dev/null differ diff --git a/tests/Backend.Fx.Tests/BuildingBlocks/TheAggregateRoot.cs b/tests/Backend.Fx.Tests/BuildingBlocks/TheAggregateRoot.cs deleted file mode 100644 index e3080865..00000000 --- a/tests/Backend.Fx.Tests/BuildingBlocks/TheAggregateRoot.cs +++ /dev/null @@ -1,139 +0,0 @@ -using System; -using System.Collections.Generic; -using Backend.Fx.BuildingBlocks; -using Backend.Fx.RandomData; -using JetBrains.Annotations; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.BuildingBlocks -{ - public class TheAggregateRoot : TestWithLogging - { - private static int _nextId; - - public class TestAggregateRoot : AggregateRoot - { - public TestAggregateRoot(int id, string name) : base(id) - { - Name = name; - Children.Add(new TestEntity("Child 1", this)); - Children.Add(new TestEntity("Child 2", this)); - Children.Add(new TestEntity("Child 3", this)); - } - - [UsedImplicitly] public string Name { get; private set; } - - public ISet Children { get; } = new HashSet(); - } - - public class TestEntity : Entity - { - public TestEntity(string name, TestAggregateRoot parent) - { - Name = name; - Parent = parent; - } - - [UsedImplicitly] public string Name { get; set; } - - [UsedImplicitly] public TestAggregateRoot Parent { get; set; } - } - - [Fact] - public void ChangedByPropertyIsChoppedAt100Chars() - { - DateTime now = DateTime.Now; - var sut = new TestAggregateRoot(_nextId++, "gaga"); - var moreThanHundred = Letters.RandomLowerCase(110); - sut.SetModifiedProperties(moreThanHundred, now); - Assert.Equal(moreThanHundred.Substring(0, 99) + "…", sut.ChangedBy); - } - - [Fact] - public void ChangedByPropertyIsStoredCorrectly() - { - DateTime now = DateTime.Now; - var sut = new TestAggregateRoot(_nextId++, "gaga"); - sut.SetModifiedProperties("me", now); - Assert.Equal("me", sut.ChangedBy); - Assert.Null(sut.CreatedBy); - } - - [Fact] - public void ChangedOnPropertyIsStoredCorrectly() - { - DateTime now = DateTime.Now; - var sut = new TestAggregateRoot(_nextId++, "gaga"); - sut.SetModifiedProperties("me", now); - Assert.Equal(now, sut.ChangedOn); - Assert.Equal(default, sut.CreatedOn); - } - - [Fact] - public void CreatedByPropertyIsChoppedAt100Chars() - { - DateTime now = DateTime.Now; - var sut = new TestAggregateRoot(_nextId++, "gaga"); - var moreThanHundred = Letters.RandomLowerCase(110); - sut.SetCreatedProperties(moreThanHundred, now); - Assert.Equal(moreThanHundred.Substring(0, 99) + "…", sut.CreatedBy); - } - - [Fact] - public void CreatedByPropertyIsStoredCorrectly() - { - DateTime now = DateTime.Now; - var sut = new TestAggregateRoot(_nextId++, "gaga"); - sut.SetCreatedProperties("me", now); - Assert.Equal("me", sut.CreatedBy); - Assert.Null(sut.ChangedBy); - } - - [Fact] - public void CreatedOnPropertyIsStoredCorrectly() - { - DateTime now = DateTime.Now; - var sut = new TestAggregateRoot(_nextId++, "gaga"); - sut.SetCreatedProperties("me", now); - Assert.Equal(now, sut.CreatedOn); - Assert.Null(sut.ChangedOn); - } - - [Fact] - public void ThrowsGivenEmptyChangedBy() - { - var sut = new TestAggregateRoot(_nextId++, "gaga"); - // ReSharper disable once AssignNullToNotNullAttribute - Assert.Throws(() => sut.SetModifiedProperties("", DateTime.Now)); - } - - [Fact] - public void ThrowsGivenEmptyCreatedBy() - { - var sut = new TestAggregateRoot(_nextId++, "gaga"); - // ReSharper disable once AssignNullToNotNullAttribute - Assert.Throws(() => sut.SetCreatedProperties("", DateTime.Now)); - } - - [Fact] - public void ThrowsGivenNullChangedBy() - { - var sut = new TestAggregateRoot(_nextId++, "gaga"); - // ReSharper disable once AssignNullToNotNullAttribute - Assert.Throws(() => sut.SetModifiedProperties(null, DateTime.Now)); - } - - [Fact] - public void ThrowsGivenNullCreatedBy() - { - var sut = new TestAggregateRoot(_nextId++, "gaga"); - // ReSharper disable once AssignNullToNotNullAttribute - Assert.Throws(() => sut.SetCreatedProperties(null, DateTime.Now)); - } - - public TheAggregateRoot(ITestOutputHelper output) : base(output) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/BuildingBlocks/TheRepository.cs b/tests/Backend.Fx.Tests/BuildingBlocks/TheRepository.cs deleted file mode 100644 index 939ef2ba..00000000 --- a/tests/Backend.Fx.Tests/BuildingBlocks/TheRepository.cs +++ /dev/null @@ -1,496 +0,0 @@ -using System; -using System.Linq; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Exceptions; -using Backend.Fx.InMemoryPersistence; -using Backend.Fx.Patterns.Authorization; -using FakeItEasy; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.BuildingBlocks -{ - public class TheRepository : TestWithLogging - { - public TheRepository(ITestOutputHelper output) : base(output) - { - } - - [Fact] - public void AcceptsNullArrayToResolve() - { - var authorization = A.Fake>(); - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.Filter(A>._)) - .ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(true); - - var sut = new InMemoryRepository(new InMemoryStore(), - CurrentTenantIdHolder.Create(234), - authorization); - Assert.Empty(sut.Resolve(null)); - } - - [Fact] - public void CanResolveListOfIds() - { - var authorization = A.Fake>(); - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.Filter(A>._)) - .ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(true); - - var sut = new InMemoryRepository(new InMemoryStore(), - CurrentTenantIdHolder.Create(234), - authorization); - var agg1 = new TheAggregateRoot.TestAggregateRoot(23, "whatever") { TenantId = 234 }; - var agg2 = new TheAggregateRoot.TestAggregateRoot(24, "whatever") { TenantId = 234 }; - var agg3 = new TheAggregateRoot.TestAggregateRoot(25, "whatever") { TenantId = 234 }; - var agg4 = new TheAggregateRoot.TestAggregateRoot(26, "whatever") { TenantId = 234 }; - - sut.Store.Add(agg1.Id, agg1); - sut.Store.Add(agg2.Id, agg2); - sut.Store.Add(agg3.Id, agg3); - sut.Store.Add(agg4.Id, agg4); - - var resolved = sut.Resolve(new[] { 23, 24, 25, 26 }); - Assert.Equal(4, resolved.Length); - Assert.Contains(agg1, resolved); - Assert.Contains(agg2, resolved); - Assert.Contains(agg3, resolved); - Assert.Contains(agg4, resolved); - } - - [Fact] - public void ThrowsOnAttemptToAddNull() - { - var sut = new InMemoryRepository(new InMemoryStore(), - CurrentTenantIdHolder.Create(234), - new AllowAll()); - Assert.Throws(() => sut.AddRange(null!)); - Assert.Throws(() => sut.Add(null!)); - } - - [Fact] - public void ThrowsOnAttemptToDeleteNull() - { - var sut = new InMemoryRepository(new InMemoryStore(), - CurrentTenantIdHolder.Create(234), - new AllowAll()); - Assert.Throws(() => sut.Delete(null!)); - } - - [Fact] - public void DeletesItemFromMyTenant() - { - var authorization = A.Fake>(); - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.Filter(A>._)) - .ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(true); - A.CallTo(() => authorization.CanDelete(A._)).Returns(true); - - var sut = new InMemoryRepository(new InMemoryStore(), - CurrentTenantIdHolder.Create(234), - authorization); - - var agg1 = new TheAggregateRoot.TestAggregateRoot(12123123, "whatever") { TenantId = 234 }; - sut.Store.Add(agg1.Id, agg1); - - sut.Delete(agg1); - - Assert.Empty(sut.Store); - } - - [Fact] - public void DoesNotReturnItemsFromOtherTenants() - { - var authorization = A.Fake>(); - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.Filter(A>._)) - .ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(true); - - var store = new InMemoryStore(); - var sut = new InMemoryRepository(store, CurrentTenantIdHolder.Create(234), authorization); - - sut.Add(new TheAggregateRoot.TestAggregateRoot(22, "1")); - sut.Add(new TheAggregateRoot.TestAggregateRoot(23, "2")); - sut.Add(new TheAggregateRoot.TestAggregateRoot(24, "3")); - - // now I am in another tenant - sut = new InMemoryRepository(store, CurrentTenantIdHolder.Create(233), authorization); - Assert.Empty(sut.AggregateQueryable); - } - - [Fact] - public void MaintainsTenantIdOnAdd() - { - var authorization = A.Fake>(); - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.Filter(A>._)) - .ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(true); - - var sut = new InMemoryRepository(new InMemoryStore(), - CurrentTenantIdHolder.Create(234), - authorization); - - var agg1 = new TheAggregateRoot.TestAggregateRoot(22, "1"); - sut.Add(agg1); - Assert.Equal(234, agg1.TenantId); - } - - [Fact] - public void ProvidesCorrectAny() - { - var authorization = A.Fake>(); - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.Filter(A>._)) - .ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(false); - - var sut = new InMemoryRepository(new InMemoryStore(), - CurrentTenantIdHolder.Create(234), - authorization); - Assert.False(sut.Any()); - - var agg1 = new TheAggregateRoot.TestAggregateRoot(23, "whatever") { TenantId = 234 }; - var agg2 = new TheAggregateRoot.TestAggregateRoot(24, "whatever") { TenantId = 234 }; - var agg3 = new TheAggregateRoot.TestAggregateRoot(25, "whatever") { TenantId = 234 }; - var agg4 = new TheAggregateRoot.TestAggregateRoot(26, "whatever") { TenantId = 234 }; - - sut.Store.Add(agg1.Id, agg1); - sut.Store.Add(agg2.Id, agg2); - sut.Store.Add(agg3.Id, agg3); - sut.Store.Add(agg4.Id, agg4); - - Assert.True(sut.Any()); - } - - [Fact] - public void ReturnsAll() - { - var authorization = A.Fake>(); - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.Filter(A>._)) - .ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(true); - - var sut = new InMemoryRepository(new InMemoryStore(), - CurrentTenantIdHolder.Create(234), - authorization); - var agg1 = new TheAggregateRoot.TestAggregateRoot(12123123, "whatever") { TenantId = 234 }; - var agg2 = new TheAggregateRoot.TestAggregateRoot(12123124, "whatever") { TenantId = 234 }; - var agg3 = new TheAggregateRoot.TestAggregateRoot(12123125, "whatever") { TenantId = 234 }; - var agg4 = new TheAggregateRoot.TestAggregateRoot(12123126, "whatever") { TenantId = 234 }; - - sut.AddRange(new[] { agg1, agg2, agg3, agg4 }); - - Assert.Equal(4, sut.GetAll().Length); - Assert.Contains(agg1, sut.GetAll()); - Assert.Contains(agg2, sut.GetAll()); - Assert.Contains(agg3, sut.GetAll()); - Assert.Contains(agg4, sut.GetAll()); - } - - [Fact] - public void ReturnsByIdOnSingle() - { - var authorization = A.Fake>(); - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.Filter(A>._)) - .ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(false); - - var sut = new InMemoryRepository(new InMemoryStore(), - CurrentTenantIdHolder.Create(234), - authorization); - var agg1 = new TheAggregateRoot.TestAggregateRoot(23, "whatever") { TenantId = 234 }; - var agg2 = new TheAggregateRoot.TestAggregateRoot(24, "whatever") { TenantId = 234 }; - var agg3 = new TheAggregateRoot.TestAggregateRoot(25, "whatever") { TenantId = 234 }; - var agg4 = new TheAggregateRoot.TestAggregateRoot(26, "whatever") { TenantId = 234 }; - - sut.Store.Add(agg1.Id, agg1); - sut.Store.Add(agg2.Id, agg2); - sut.Store.Add(agg3.Id, agg3); - sut.Store.Add(agg4.Id, agg4); - - Assert.Equal(agg1, sut.Single(agg1.Id)); - Assert.Equal(agg2, sut.Single(agg2.Id)); - Assert.Equal(agg3, sut.Single(agg3.Id)); - Assert.Equal(agg4, sut.Single(agg4.Id)); - Assert.Throws>(() => sut.Single(235421354)); - } - - [Fact] - public void ReturnsByIdOnSingleOrDefault() - { - var authorization = A.Fake>(); - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.Filter(A>._)) - .ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(false); - - var sut = new InMemoryRepository(new InMemoryStore(), - CurrentTenantIdHolder.Create(234), - authorization); - var agg1 = new TheAggregateRoot.TestAggregateRoot(23, "whatever") { TenantId = 234 }; - var agg2 = new TheAggregateRoot.TestAggregateRoot(24, "whatever") { TenantId = 234 }; - var agg3 = new TheAggregateRoot.TestAggregateRoot(25, "whatever") { TenantId = 234 }; - var agg4 = new TheAggregateRoot.TestAggregateRoot(26, "whatever") { TenantId = 234 }; - - sut.Store.Add(agg1.Id, agg1); - sut.Store.Add(agg2.Id, agg2); - sut.Store.Add(agg3.Id, agg3); - sut.Store.Add(agg4.Id, agg4); - - Assert.Equal(agg1, sut.SingleOrDefault(agg1.Id)); - Assert.Equal(agg2, sut.SingleOrDefault(agg2.Id)); - Assert.Equal(agg3, sut.SingleOrDefault(agg3.Id)); - Assert.Equal(agg4, sut.SingleOrDefault(agg4.Id)); - Assert.Null(sut.SingleOrDefault(235421354)); - } - - [Fact] - public void ReturnsEmptyWhenTenantIdHolderIsEmpty() - { - var authorization = A.Fake>(); - var sut = new InMemoryRepository(new InMemoryStore(), - CurrentTenantIdHolder.Create(null), - authorization); - - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.Filter(A>._)) - .ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(true); - Assert.Empty(sut.AggregateQueryable); - } - - [Fact] - public void ReturnsOnlyAuthorizedRecords() - { - var authorization = A.Fake>(); - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.Filter(A>._)) - .ReturnsLazily((IQueryable q) => q.Where(agg => agg.Id == 25 || agg.Id == 26)); - A.CallTo(() => authorization.CanCreate(A._)).Returns(false); - - var sut = new InMemoryRepository(new InMemoryStore(), - CurrentTenantIdHolder.Create(234), - authorization); - var agg1 = new TheAggregateRoot.TestAggregateRoot(23, "whatever") { TenantId = 234 }; - var agg2 = new TheAggregateRoot.TestAggregateRoot(24, "whatever") { TenantId = 234 }; - var agg3 = new TheAggregateRoot.TestAggregateRoot(25, "whatever") { TenantId = 234 }; - var agg4 = new TheAggregateRoot.TestAggregateRoot(26, "whatever") { TenantId = 234 }; - - sut.Store.Add(agg1.Id, agg1); - sut.Store.Add(agg2.Id, agg2); - sut.Store.Add(agg3.Id, agg3); - sut.Store.Add(agg4.Id, agg4); - - var all = sut.GetAll(); - Assert.Equal(2, all.Length); - Assert.DoesNotContain(agg1, all); - Assert.DoesNotContain(agg2, all); - Assert.Contains(agg3, all); - Assert.Contains(agg4, all); - } - - [Fact] - public void ReturnsOnlyItemsFromMyTenant() - { - var authorization = A.Fake>(); - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.Filter(A>._)) - .ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(true); - - var agg1 = new TheAggregateRoot.TestAggregateRoot(11, "1"); - var agg2 = new TheAggregateRoot.TestAggregateRoot(12, "2"); - var agg3 = new TheAggregateRoot.TestAggregateRoot(13, "3"); - var agg4 = new TheAggregateRoot.TestAggregateRoot(14, "4"); - var agg5 = new TheAggregateRoot.TestAggregateRoot(15, "5"); - - var store = new InMemoryStore(); - var sut = new InMemoryRepository(store, CurrentTenantIdHolder.Create(234), authorization); - sut.Add(agg1); - sut.Add(agg2); - sut.Add(agg3); - - sut = new InMemoryRepository(store, CurrentTenantIdHolder.Create(567), authorization); - sut.Add(agg4); - sut.Add(agg5); - - sut = new InMemoryRepository(store, CurrentTenantIdHolder.Create(234), authorization); - Assert.Equal(3, sut.AggregateQueryable.Count()); - Assert.Contains(agg1, sut.AggregateQueryable); - Assert.Contains(agg2, sut.AggregateQueryable); - Assert.Contains(agg3, sut.AggregateQueryable); - - sut = new InMemoryRepository(store, CurrentTenantIdHolder.Create(567), authorization); - Assert.Equal(2, sut.AggregateQueryable.Count()); - Assert.Contains(agg4, sut.AggregateQueryable); - Assert.Contains(agg5, sut.AggregateQueryable); - } - - [Fact] - public void ThrowsOnAddWhenTenantIdIsEmpty() - { - var authorization = A.Fake>(); - var sut = new InMemoryRepository(new InMemoryStore(), - CurrentTenantIdHolder.Create(null), - authorization); - - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.CanCreate(A._)).Returns(true); - A.CallTo(() => authorization.Filter(A>._)) - .ReturnsLazily((IQueryable q) => q); - Assert.Throws(() => sut.Add(new TheAggregateRoot.TestAggregateRoot(77, "whatever"))); - - // even when I don't have permissions - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => false); - A.CallTo(() => authorization.CanCreate(A._)).Returns(false); - Assert.Throws(() => sut.Add(new TheAggregateRoot.TestAggregateRoot(78, "whatever"))); - } - - [Fact] - public void ThrowsOnAddRangeWhenTenantIdIsEmpty() - { - var authorization = A.Fake>(); - var sut = new InMemoryRepository(new InMemoryStore(), - CurrentTenantIdHolder.Create(null), - authorization); - - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.CanCreate(A._)).Returns(true); - A.CallTo(() => authorization.Filter(A>._)) - .ReturnsLazily((IQueryable q) => q); - Assert.Throws(() => sut.Add(new TheAggregateRoot.TestAggregateRoot(77, "whatever"))); - - // even when I don't have permissions - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => false); - A.CallTo(() => authorization.CanCreate(A._)).Returns(false); - Assert.Throws(() => sut.AddRange(new[] { new TheAggregateRoot.TestAggregateRoot(78, "whatever") })); - } - - [Fact] - public void ThrowsOnAddWhenUnauthorized() - { - var authorization = A.Fake>(); - var sut = new InMemoryRepository(new InMemoryStore(), - CurrentTenantIdHolder.Create(234), - authorization); - - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.Filter(A>._)) - .ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(false); - Assert.Throws(() => sut.Add(new TheAggregateRoot.TestAggregateRoot(44, "whatever"))); - } - - [Fact] - public void ThrowsOnAddRangeWhenUnauthorized() - { - var authorization = A.Fake>(); - var sut = new InMemoryRepository(new InMemoryStore(), - CurrentTenantIdHolder.Create(234), - authorization); - - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.Filter(A>._)) - .ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(false); - Assert.Throws(() => sut.AddRange(new[] { new TheAggregateRoot.TestAggregateRoot(44, "whatever") })); - } - - [Fact] - public void ThrowsOnDeleteWhenTenantDoesNotMatch() - { - var authorization = A.Fake>(); - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.Filter(A>._)) - .ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(true); - - var sut = new InMemoryRepository(new InMemoryStore(), - CurrentTenantIdHolder.Create(234), - authorization); - var agg1 = new TheAggregateRoot.TestAggregateRoot(23, "whatever") { TenantId = 234 }; - var agg2 = new TheAggregateRoot.TestAggregateRoot(24, "whatever") { TenantId = 234 }; - var agg3 = new TheAggregateRoot.TestAggregateRoot(25, "whatever") { TenantId = 234 }; - var agg4 = new TheAggregateRoot.TestAggregateRoot(26, "whatever") { TenantId = 999 }; - - sut.Store.Add(agg1.Id, agg1); - sut.Store.Add(agg2.Id, agg2); - sut.Store.Add(agg3.Id, agg3); - sut.Store.Add(agg4.Id, agg4); - - Assert.Throws(() => sut.Delete(agg4)); - } - - [Fact] - public void ThrowsOnDeleteWhenTenantIdHolderIsEmpty() - { - var authorization = A.Fake>(); - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.Filter(A>._)) - .ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(true); - - var sut = new InMemoryRepository(new InMemoryStore(), - CurrentTenantIdHolder.Create(null), - authorization); - - var agg1 = new TheAggregateRoot.TestAggregateRoot(12123123, "whatever") { TenantId = 234 }; - sut.Store.Add(agg1.Id, agg1); - - Assert.Throws(() => sut.Delete(agg1)); - } - - [Fact] - public void ThrowsOnDeleteWhenUnauthorized() - { - var authorization = A.Fake>(); - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.Filter(A>._)) - .ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(true); - A.CallTo(() => authorization.CanDelete(A._)).Returns(false); - - var sut = new InMemoryRepository(new InMemoryStore(), - CurrentTenantIdHolder.Create(234), - authorization); - - var agg1 = new TheAggregateRoot.TestAggregateRoot(12123123, "whatever") { TenantId = 234 }; - sut.Store.Add(agg1.Id, agg1); - - Assert.Throws(() => sut.Delete(agg1)); - } - - [Fact] - public void ThrowsOnResolveWhenTenantDoesNotMatch() - { - var authorization = A.Fake>(); - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.Filter(A>._)) - .ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(true); - - var sut = new InMemoryRepository(new InMemoryStore(), - CurrentTenantIdHolder.Create(234), - authorization); - var agg1 = new TheAggregateRoot.TestAggregateRoot(23, "whatever") { TenantId = 234 }; - var agg2 = new TheAggregateRoot.TestAggregateRoot(24, "whatever") { TenantId = 234 }; - var agg3 = new TheAggregateRoot.TestAggregateRoot(25, "whatever") { TenantId = 234 }; - var agg4 = new TheAggregateRoot.TestAggregateRoot(26, "whatever") { TenantId = 999 }; - - sut.Store.Add(agg1.Id, agg1); - sut.Store.Add(agg2.Id, agg2); - sut.Store.Add(agg3.Id, agg3); - sut.Store.Add(agg4.Id, agg4); - - Assert.Throws(() => sut.Resolve(new[] { 23, 24, 25, 26 })); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/ConfigurationSettings/TheSetting.cs b/tests/Backend.Fx.Tests/ConfigurationSettings/TheSetting.cs deleted file mode 100644 index 435baad4..00000000 --- a/tests/Backend.Fx.Tests/ConfigurationSettings/TheSetting.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using Backend.Fx.BuildingBlocks; -using Backend.Fx.ConfigurationSettings; -using Backend.Fx.Patterns.IdGeneration; -using JetBrains.Annotations; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.ConfigurationSettings -{ - public class TheSetting : TestWithLogging - { - [UsedImplicitly] - public class TestSettingsService : SettingsService - { - public TestSettingsService(IEntityIdGenerator idGenerator, IRepository settingRepository) - : base("Test", idGenerator, settingRepository, new SettingSerializerFactory()) - { - } - } - - [Fact] - public void CanStoreBoolean() - { - const bool booleanValue = true; - var sut = new Setting(3, "key"); - sut.SetValue(new BooleanSerializer(), booleanValue); - Assert.Equal("True", sut.SerializedValue); - var booleanValueRead = sut.GetValue(new BooleanSerializer()); - Assert.Equal(booleanValue, booleanValueRead); - } - - [Fact] - public void CanStoreDateTime() - { - var dateTimeValue = new DateTime(1987, 4, 22, 23, 12, 11); - var sut = new Setting(9, "key"); - sut.SetValue(new DateTimeSerializer(), dateTimeValue); - Assert.Equal("1987-04-22T23:12:11.0000000", sut.SerializedValue); - var dateTimeValueRead = sut.GetValue(new DateTimeSerializer()); - Assert.Equal(dateTimeValue, dateTimeValueRead); - } - - [Fact] - public void CanStoreDouble() - { - const double doubleValue = 2354.2341234d; - var sut = new Setting(5, "key"); - sut.SetValue(new DoubleSerializer(), doubleValue); - Assert.Equal("2354.2341234", sut.SerializedValue); - var doubleValueRead = sut.GetValue(new DoubleSerializer()); - Assert.Equal(doubleValue, doubleValueRead); - } - - [Fact] - public void CanStoreInt() - { - const int intValue = 235234; - var sut = new Setting(7, "key"); - sut.SetValue(new IntegerSerializer(), intValue); - Assert.Equal("235234", sut.SerializedValue); - var intValueRead = sut.GetValue(new IntegerSerializer()); - Assert.Equal(intValue, intValueRead); - } - - [Fact] - public void CanStoreNullBoolean() - { - var sut = new Setting(4, "key"); - sut.SetValue(new BooleanSerializer(), null); - Assert.Null(sut.SerializedValue); - var booleanValueRead = sut.GetValue(new BooleanSerializer()); - Assert.Null(booleanValueRead); - } - - [Fact] - public void CanStoreNullDateTime() - { - var sut = new Setting(10, "key"); - sut.SetValue(new DateTimeSerializer(), null); - Assert.Null(sut.SerializedValue); - var dateTimeValueRead = sut.GetValue(new DateTimeSerializer()); - Assert.Null(dateTimeValueRead); - } - - [Fact] - public void CanStoreNullDouble() - { - var sut = new Setting(6, "key"); - sut.SetValue(new DoubleSerializer(), null); - Assert.Null(sut.SerializedValue); - var doubleValueRead = sut.GetValue(new DoubleSerializer()); - Assert.Null(doubleValueRead); - } - - [Fact] - public void CanStoreNullInt() - { - var sut = new Setting(8, "key"); - sut.SetValue(new IntegerSerializer(), null); - Assert.Null(sut.SerializedValue); - var intValueRead = sut.GetValue(new IntegerSerializer()); - Assert.Null(intValueRead); - } - - [Fact] - public void CanStoreNullString() - { - const string stringValue = null; - var sut = new Setting(2, "key"); - sut.SetValue(new StringSerializer(), stringValue); - Assert.Equal(stringValue, sut.SerializedValue); - var stringValueRead = sut.GetValue(new StringSerializer()); - Assert.Equal(stringValue, stringValueRead); - } - - [Fact] - public void CanStoreString() - { - const string stringValue = "sdufhpsdfb ^ ÄÜÖÄÜ psdj"; - var sut = new Setting(1, "key"); - sut.SetValue(new StringSerializer(), stringValue); - Assert.Equal(stringValue, sut.SerializedValue); - var stringValueRead = sut.GetValue(new StringSerializer()); - Assert.Equal(stringValue, stringValueRead); - } - - public TheSetting(ITestOutputHelper output) : base(output) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/ConfigurationSettings/TheSettingSerializerFactory.cs b/tests/Backend.Fx.Tests/ConfigurationSettings/TheSettingSerializerFactory.cs deleted file mode 100644 index f24749cf..00000000 --- a/tests/Backend.Fx.Tests/ConfigurationSettings/TheSettingSerializerFactory.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using Backend.Fx.ConfigurationSettings; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.ConfigurationSettings -{ - public class TheSettingSerializerFactory : TestWithLogging - { - private readonly SettingSerializerFactory _sut = new SettingSerializerFactory(); - - [Fact] - public void ProvidesBooleanSerializerForNullableBool() - { - var serializer = _sut.GetSerializer(); - Assert.IsType(serializer); - } - - [Fact] - public void ProvidesBooleanSerializerForNullableDateTime() - { - var serializer = _sut.GetSerializer(); - Assert.IsType(serializer); - } - - [Fact] - public void ProvidesBooleanSerializerForNullableDouble() - { - var serializer = _sut.GetSerializer(); - Assert.IsType(serializer); - } - - [Fact] - public void ProvidesBooleanSerializerForNullableInt() - { - var serializer = _sut.GetSerializer(); - Assert.IsType(serializer); - } - - [Fact] - public void ProvidesBooleanSerializerForString() - { - var serializer = _sut.GetSerializer(); - Assert.IsType(serializer); - } - - [Fact] - public void ProvidesNoSerializerForBool() - { - Assert.Throws(() => _sut.GetSerializer()); - } - - [Fact] - public void ProvidesNoSerializerForDateTime() - { - Assert.Throws(() => _sut.GetSerializer()); - } - - [Fact] - public void ProvidesNoSerializerForDouble() - { - Assert.Throws(() => _sut.GetSerializer()); - } - - [Fact] - public void ProvidesNoSerializerForInt() - { - Assert.Throws(() => _sut.GetSerializer()); - } - - public TheSettingSerializerFactory(ITestOutputHelper output) : base(output) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/ConfigurationSettings/TheSettingsService.cs b/tests/Backend.Fx.Tests/ConfigurationSettings/TheSettingsService.cs deleted file mode 100644 index 5a33811e..00000000 --- a/tests/Backend.Fx.Tests/ConfigurationSettings/TheSettingsService.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System.Linq; -using Backend.Fx.BuildingBlocks; -using Backend.Fx.ConfigurationSettings; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Hacking; -using Backend.Fx.InMemoryPersistence; -using Backend.Fx.Patterns.Authorization; -using Backend.Fx.Patterns.IdGeneration; -using FakeItEasy; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.ConfigurationSettings -{ - public class TheSettingsService : TestWithLogging - { - public TheSettingsService(ITestOutputHelper output): base(output) - { - var settingAuthorization = A.Fake>(); - A.CallTo(() => settingAuthorization.HasAccessExpression).Returns(setting => true); - A.CallTo(() => settingAuthorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); - A.CallTo(() => settingAuthorization.CanCreate(A._)).Returns(true); - - _idGenerator = A.Fake(); - var nextId = 1; - A.CallTo(() => _idGenerator.NextId()).ReturnsLazily(() => nextId++); - _settingRepository = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(999), settingAuthorization); - } - - public class MySettingsService : SettingsService - { - public MySettingsService(IEntityIdGenerator idGenerator, IRepository repo) - : base("My", idGenerator, repo, new SettingSerializerFactory()) - { - } - - public int SmtpPort - { - get => ReadSetting(nameof(SmtpPort)) ?? 25; - set => WriteSetting(nameof(SmtpPort), value); - } - - public string SmtpHost - { - get => ReadSetting(nameof(SmtpHost)); - set => WriteSetting(nameof(SmtpHost), value); - } - } - - private readonly InMemoryRepository _settingRepository; - private readonly IEntityIdGenerator _idGenerator; - - [Fact] - public void ReadsNonExistingSettingAsDefaultFromRepository() - { - var sut = new MySettingsService(_idGenerator, _settingRepository); - Assert.Null(sut.SmtpHost); - } - - [Fact] - public void ReadsNullSettingFromRepository() - { - var setting = new Setting(3, "My.SmtpHost"); - setting.SetPrivate(set => set.SerializedValue, null); - - _settingRepository.Add(setting); - - var sut = new MySettingsService(_idGenerator, _settingRepository); - Assert.Null(sut.SmtpHost); - } - - [Fact] - public void ReadsSettingFromRepository() - { - var setting = new Setting(1, "My.SmtpPort"); - setting.SetPrivate(set => set.SerializedValue, "333"); - - _settingRepository.Add(setting); - - var sut = new MySettingsService(_idGenerator, _settingRepository); - Assert.Equal(333, sut.SmtpPort); - } - - [Fact] - public void StoresSettingsInRepository() - { - var sut = new MySettingsService(_idGenerator, _settingRepository) {SmtpPort = 333}; - Assert.Equal(333, sut.SmtpPort); - - var settings = _settingRepository.GetAll(); - Assert.Single(settings); - Assert.Equal("333", settings[0].SerializedValue); - Assert.Equal("My.SmtpPort", settings[0].Key); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/DependencyInjection/TheCompositionRoot.cs b/tests/Backend.Fx.Tests/DependencyInjection/TheCompositionRoot.cs new file mode 100644 index 00000000..38f04a33 --- /dev/null +++ b/tests/Backend.Fx.Tests/DependencyInjection/TheCompositionRoot.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using Backend.Fx.DependencyInjection; +using Backend.Fx.TestUtil; +using FakeItEasy; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.DependencyInjection; + +public class TheCompositionRoot : TestWithLogging +{ + public TheCompositionRoot(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void RegistersModules() + { + var fakeModule = A.Fake(); + using var sut = new DummyCompositionRoot(); + sut.RegisterModules(fakeModule); + A.CallTo(() => fakeModule.Register(A.That.IsSameAs(sut))) + .MustHaveHappenedOnceExactly(); + } + + private class DummyCompositionRoot : CompositionRoot + { + private ICompositionRoot FakeCompositionRoot { get; } = A.Fake(); + + public override IServiceScope BeginScope() + { + return FakeCompositionRoot.BeginScope(); + } + + protected override void Dispose(bool disposing) + { + } + + public override IServiceProvider ServiceProvider => FakeCompositionRoot.ServiceProvider; + + public override void Verify() + { + FakeCompositionRoot.Verify(); + } + + public override void Register(ServiceDescriptor serviceDescriptor) + { + FakeCompositionRoot.Register(serviceDescriptor); + } + + public override void RegisterDecorator(ServiceDescriptor serviceDescriptor) + { + FakeCompositionRoot.RegisterDecorator(serviceDescriptor); + } + + public override void RegisterCollection(IEnumerable serviceDescriptors) + { + FakeCompositionRoot.RegisterCollection(serviceDescriptors); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Domain/TheIdentified.cs b/tests/Backend.Fx.Tests/Domain/TheIdentified.cs new file mode 100644 index 00000000..603f053a --- /dev/null +++ b/tests/Backend.Fx.Tests/Domain/TheIdentified.cs @@ -0,0 +1,119 @@ +#pragma warning disable CS1718, CS0252, CS0253, CS8618, CS8625 + +using Backend.Fx.Domain; +using JetBrains.Annotations; +using Xunit; + +namespace Backend.Fx.Tests.Domain; + +public class TheIdentified +{ + [Fact] + public void HasDebuggerDisplay() + { + var someEntity = new SomeEntity(new EntityId(1), "Entity 1"); + Assert.Equal("SomeEntity[1]", someEntity.DebuggerDisplay); + } + + [Fact] + public void ConsidersSameObjectToBeEqual() + { + var someEntity = new SomeEntity(new EntityId(1), "Entity 1"); + + Assert.True(someEntity.Equals(someEntity)); + // ReSharper disable once EqualExpressionComparison + Assert.True(someEntity == someEntity); + // ReSharper disable once EqualExpressionComparison + Assert.False(someEntity != someEntity); + // ReSharper disable once EqualExpressionComparison + Assert.True(Equals(someEntity, someEntity)); + Assert.StrictEqual(someEntity, someEntity); + Assert.Equal(someEntity, someEntity); + } + + [Fact] + public void ConsidersDifferentObjectsWithSameIdToBeEqual() + { + var someEntity = new SomeEntity(new EntityId(1), "Entity 1"); + var someEntityToo = new SomeEntity(new EntityId(1), "Entity 1"); + + Assert.True(someEntity.Equals(someEntityToo)); + Assert.True(someEntity == someEntityToo); + Assert.False(someEntity != someEntityToo); + Assert.True(Equals(someEntity, someEntityToo)); + Assert.StrictEqual(someEntity, someEntityToo); + Assert.Equal(someEntity, someEntityToo); + } + + [Fact] + public void ConsidersDifferentInstancesNotToBeEqual() + { + var someEntity1 = new SomeEntity(new EntityId(1), "Entity 1"); + var someEntity2 = new SomeEntity(new EntityId(2), "Entity 2"); + + Assert.False(someEntity1.Equals(someEntity2)); + Assert.False(someEntity1 == someEntity2); + Assert.True(someEntity1 != someEntity2); + Assert.False(Equals(someEntity1, someEntity2)); + Assert.NotStrictEqual(someEntity1, someEntity2); + Assert.NotEqual(someEntity1, someEntity2); + } + + [Fact] + public void ConsidersDifferentObjectsNotToBeEqual() + { + var someEntity1 = new SomeEntity(new EntityId(1), "Entity 1"); + var somethingCompletelyDifferent = new object(); + + Assert.False(someEntity1.Equals(somethingCompletelyDifferent)); + Assert.False(someEntity1 == somethingCompletelyDifferent); + Assert.True(someEntity1 != somethingCompletelyDifferent); + Assert.False(Equals(someEntity1, somethingCompletelyDifferent)); + Assert.NotStrictEqual(someEntity1, somethingCompletelyDifferent); + Assert.NotEqual(someEntity1, somethingCompletelyDifferent); + } + + [Fact] + public void EqualInstancesHaveEqualHashCodes() + { + var someEntity1 = new SomeEntity(new EntityId(1), "Entity 1"); + var someEntity2 = new SomeEntity(new EntityId(1), "Entity 1"); + Assert.Equal(someEntity1.GetHashCode(), someEntity2.GetHashCode()); + } + + [Fact] + public void NotEqualInstancesHaveEqualHashCodes() + { + var someEntity1 = new SomeEntity(new EntityId(1), "Entity 1"); + var someEntity2 = new SomeEntity(new EntityId(2), "Entity 2"); + Assert.NotEqual(someEntity1.GetHashCode(), someEntity2.GetHashCode()); + } + + private readonly struct EntityId + { + public EntityId(int value) + { + Value = value; + } + + [UsedImplicitly] + public int Value { get; } + + public override string ToString() + { + return Value.ToString(); + } + } + + private class SomeEntity : Identified + { + public SomeEntity(EntityId id, string name) : base(id) + { + Name = name; + } + + [UsedImplicitly] + public string Name { get; init; } + } +} +#pragma warning restore CS1718, CS0252, CS0253, CS8618, CS8625 diff --git a/tests/Backend.Fx.Tests/BuildingBlocks/TheValueObject.cs b/tests/Backend.Fx.Tests/Domain/TheValueObject.cs similarity index 84% rename from tests/Backend.Fx.Tests/BuildingBlocks/TheValueObject.cs rename to tests/Backend.Fx.Tests/Domain/TheValueObject.cs index 161ba558..0d32e668 100644 --- a/tests/Backend.Fx.Tests/BuildingBlocks/TheValueObject.cs +++ b/tests/Backend.Fx.Tests/Domain/TheValueObject.cs @@ -1,10 +1,12 @@ +#pragma warning disable CS1718, CS0252, CS0253, CS8618, CS8625 using System; using System.Collections.Generic; -using Backend.Fx.BuildingBlocks; +using Backend.Fx.Domain; +using Backend.Fx.TestUtil; using Xunit; using Xunit.Abstractions; -namespace Backend.Fx.Tests.BuildingBlocks +namespace Backend.Fx.Tests.Domain { public class TheValueObject : TestWithLogging { @@ -86,6 +88,22 @@ public void CanCompareToNull() var myValueObject1 = new MyValueObject(333, "gnarf"); Assert.Equal(1,myValueObject1.CompareTo(null)); } + + [Fact] + public void EqualInstancesHaveEqualHashCodes() + { + var myValueObject1 = new MyValueObject(333, "gnarf"); + var myValueObject2 = new MyValueObject(333, "gnarf"); + Assert.Equal(myValueObject1.GetHashCode(), myValueObject2.GetHashCode()); + } + + [Fact] + public void NotEqualInstancesHaveEqualHashCodes() + { + var myValueObject1 = new MyValueObject(333, "gnarf2"); + var myValueObject2 = new MyValueObject(333, "gnarf1"); + Assert.NotEqual(myValueObject1.GetHashCode(), myValueObject2.GetHashCode()); + } private class MyValueObject : ComparableValueObject { @@ -115,4 +133,5 @@ public TheValueObject(ITestOutputHelper output) : base(output) { } } -} \ No newline at end of file +} +#pragma warning restore CS1718, CS0252, CS0253, CS8618, CS8625 \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/DummyServices/DummyAggregate.cs b/tests/Backend.Fx.Tests/DummyServices/DummyAggregate.cs new file mode 100644 index 00000000..aef805b0 --- /dev/null +++ b/tests/Backend.Fx.Tests/DummyServices/DummyAggregate.cs @@ -0,0 +1,17 @@ +using Backend.Fx.Domain; + +namespace Backend.Fx.Tests.DummyServices; + +public class DummyAggregate : IAggregateRoot +{ + + public DummyAggregate(int id, string name) + { + Id = id; + Name = name; + } + + public int Id { get; } + + public string Name { get; } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/DummyServices/DummyApplicationService.cs b/tests/Backend.Fx.Tests/DummyServices/DummyApplicationService.cs new file mode 100644 index 00000000..22394d41 --- /dev/null +++ b/tests/Backend.Fx.Tests/DummyServices/DummyApplicationService.cs @@ -0,0 +1,21 @@ +using Backend.Fx.Features.DomainServices; +using JetBrains.Annotations; + +namespace Backend.Fx.Tests.DummyServices; + + +public interface IDummyApplicationService +{ + string SayHelloToApplication(); +} + +[UsedImplicitly] +public class DummyApplicationService : IDummyApplicationService, IDomainService +{ + public const string Message = "Hello Application!"; + + public string SayHelloToApplication() + { + return Message; + } +} diff --git a/tests/Backend.Fx.Tests/DummyServices/DummyAuthorizationPolicy.cs b/tests/Backend.Fx.Tests/DummyServices/DummyAuthorizationPolicy.cs new file mode 100644 index 00000000..eb510b9b --- /dev/null +++ b/tests/Backend.Fx.Tests/DummyServices/DummyAuthorizationPolicy.cs @@ -0,0 +1,38 @@ +using System; +using System.Linq.Expressions; +using Backend.Fx.Features.Authorization; +using JetBrains.Annotations; + +namespace Backend.Fx.Tests.DummyServices; + +[UsedImplicitly] +public class DummyAuthorizationPolicy : IAuthorizationPolicy +{ + private readonly IDummyAuthorizationPolicySpy _spy; + + public DummyAuthorizationPolicy(IDummyAuthorizationPolicySpy spy) + { + _spy = spy; + } + + + public Expression> HasAccessExpression => _spy.HasAccessExpression; + + public bool CanCreate(DummyAggregate t) + { + return _spy.CanCreate(t); + } + + public bool CanModify(DummyAggregate t) + { + return _spy.CanModify(t); + } + + public bool CanDelete(DummyAggregate t) + { + return _spy.CanDelete(t); + } +} + +public interface IDummyAuthorizationPolicySpy : IAuthorizationPolicy +{ } \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/DummyServices/DummyDemoDataGenerator.cs b/tests/Backend.Fx.Tests/DummyServices/DummyDemoDataGenerator.cs new file mode 100644 index 00000000..d1957e1d --- /dev/null +++ b/tests/Backend.Fx.Tests/DummyServices/DummyDemoDataGenerator.cs @@ -0,0 +1,49 @@ +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Features.DataGeneration; +using Backend.Fx.Logging; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.Tests.DummyServices; + +[UsedImplicitly] +public class DummyDemoDataGenerator : DataGenerator, IDemoDataGenerator +{ + private readonly DummyDemoDataGeneratorSpy _spy; + private readonly ILogger _logger = Log.Create(); + private static readonly AsyncLocal Spy = new(); + + public DummyDemoDataGenerator(DummyDemoDataGeneratorSpy spy) + { + _spy = spy; + } + + public override int Priority => 1; + + protected override Task GenerateCoreAsync(CancellationToken cancellationToken) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Invocation {Invocations} of DummyDemoDataGenerator happens", + Interlocked.Increment(ref _spy.InvocationCount)); + } + else + { + Interlocked.Increment(ref _spy.InvocationCount); + } + + return Task.CompletedTask; + } + + protected override void Initialize() + { } + + protected override bool ShouldRun() => _spy.ShouldRun; +} + +public class DummyDemoDataGeneratorSpy +{ + public int InvocationCount; + public bool ShouldRun = true; +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/DummyServices/DummyDomainEvent.cs b/tests/Backend.Fx.Tests/DummyServices/DummyDomainEvent.cs new file mode 100644 index 00000000..3625fa9b --- /dev/null +++ b/tests/Backend.Fx.Tests/DummyServices/DummyDomainEvent.cs @@ -0,0 +1,5 @@ +using Backend.Fx.Features.DomainEvents; + +namespace Backend.Fx.Tests.DummyServices; + +public class DummyDomainEvent : IDomainEvent {} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/DummyServices/DummyDomainEventHandler.cs b/tests/Backend.Fx.Tests/DummyServices/DummyDomainEventHandler.cs new file mode 100644 index 00000000..df0f9343 --- /dev/null +++ b/tests/Backend.Fx.Tests/DummyServices/DummyDomainEventHandler.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Features.DomainEvents; + +namespace Backend.Fx.Tests.DummyServices; + +public class DummyDomainEventHandler : IDomainEventHandler +{ + private readonly IDummyDomainEventHandlerSpy _spy; + + public DummyDomainEventHandler(IDummyDomainEventHandlerSpy spy) + { + _spy = spy; + } + + public Task HandleAsync(DummyDomainEvent domainEvent, CancellationToken cancellationToken = default) + { + _spy.Handle(domainEvent); + return Task.CompletedTask; + } +} + +public interface IDummyDomainEventHandlerSpy +{ + void Handle(DummyDomainEvent dummyDomainEvent); +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/DummyServices/DummyDomainService.cs b/tests/Backend.Fx.Tests/DummyServices/DummyDomainService.cs new file mode 100644 index 00000000..01b2bced --- /dev/null +++ b/tests/Backend.Fx.Tests/DummyServices/DummyDomainService.cs @@ -0,0 +1,21 @@ +using Backend.Fx.Features.DomainServices; +using JetBrains.Annotations; + +namespace Backend.Fx.Tests.DummyServices; + + +public interface IDummyDomainService +{ + string SayHelloToDomain(); +} + +[UsedImplicitly] +public class DummyDomainService : IDummyDomainService, IDomainService +{ + public const string Message = "Hello Domain!"; + + public string SayHelloToDomain() + { + return Message; + } +} diff --git a/tests/Backend.Fx.Tests/DummyServices/DummyIntegrationEvent.cs b/tests/Backend.Fx.Tests/DummyServices/DummyIntegrationEvent.cs new file mode 100644 index 00000000..2a2f20ad --- /dev/null +++ b/tests/Backend.Fx.Tests/DummyServices/DummyIntegrationEvent.cs @@ -0,0 +1,7 @@ +using Backend.Fx.Features.MessageBus; + +namespace Backend.Fx.Tests.DummyServices; + +public class DummyIntegrationEvent : IntegrationEvent +{ +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/DummyServices/DummyIntegrationEventHandler.cs b/tests/Backend.Fx.Tests/DummyServices/DummyIntegrationEventHandler.cs new file mode 100644 index 00000000..f4d75ea8 --- /dev/null +++ b/tests/Backend.Fx.Tests/DummyServices/DummyIntegrationEventHandler.cs @@ -0,0 +1,27 @@ +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Features.MessageBus; + +namespace Backend.Fx.Tests.DummyServices; + +public interface IDummyIntegrationEventHandlerSpy +{ + public Task HandleAsync(DummyIntegrationEvent integrationEvent); +} + +public class DummyIntegrationEventHandler : IIntegrationEventHandler +{ + private readonly IDummyIntegrationEventHandlerSpy _spy; + + + public DummyIntegrationEventHandler(IDummyIntegrationEventHandlerSpy spy) + { + _spy = spy; + } + + + public Task HandleAsync(DummyIntegrationEvent integrationEvent, CancellationToken cancellationToken = default) + { + return _spy.HandleAsync(integrationEvent); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/DummyServices/DummyJob.cs b/tests/Backend.Fx.Tests/DummyServices/DummyJob.cs new file mode 100644 index 00000000..81855797 --- /dev/null +++ b/tests/Backend.Fx.Tests/DummyServices/DummyJob.cs @@ -0,0 +1,27 @@ +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Features.Jobs; +using FakeItEasy; +using JetBrains.Annotations; + +namespace Backend.Fx.Tests.DummyServices; + +[UsedImplicitly] +public class DummyJob : IJob +{ + private readonly IDummyJobSpy _spy; + + public DummyJob(IDummyJobSpy spy) + { + _spy = spy; + } + + public async Task RunAsync(CancellationToken cancellationToken = default) + { + + await _spy.RunAsync(cancellationToken); + } +} + +public interface IDummyJobSpy : IJob +{ } \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/DummyServices/DummyProductiveDataGenerator.cs b/tests/Backend.Fx.Tests/DummyServices/DummyProductiveDataGenerator.cs new file mode 100644 index 00000000..dcfc3e10 --- /dev/null +++ b/tests/Backend.Fx.Tests/DummyServices/DummyProductiveDataGenerator.cs @@ -0,0 +1,49 @@ +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Features.DataGeneration; +using Backend.Fx.Logging; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.Tests.DummyServices; + +[UsedImplicitly] +public class DummyProductiveDataGenerator : DataGenerator, IProductiveDataGenerator +{ + private readonly DummyProductiveDataGeneratorSpy _spy; + private readonly ILogger _logger = Log.Create(); + + public DummyProductiveDataGenerator(DummyProductiveDataGeneratorSpy spy) + { + _spy = spy; + } + + public override int Priority => 1; + + protected override Task GenerateCoreAsync(CancellationToken cancellationToken) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Invocation {Invocations} of DummyProductiveDataGenerator happens", + Interlocked.Increment(ref _spy.InvocationCount)); + } + else + { + Interlocked.Increment(ref _spy.InvocationCount); + } + + return Task.CompletedTask; + } + + protected override void Initialize() + { } + + protected override bool ShouldRun() => _spy.ShouldRun; +} + + +public class DummyProductiveDataGeneratorSpy +{ + public int InvocationCount; + public bool ShouldRun = true; +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/DummyServices/DummyServicesModule.cs b/tests/Backend.Fx.Tests/DummyServices/DummyServicesModule.cs new file mode 100644 index 00000000..3c3373c8 --- /dev/null +++ b/tests/Backend.Fx.Tests/DummyServices/DummyServicesModule.cs @@ -0,0 +1,66 @@ +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.DependencyInjection; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Features; +using Backend.Fx.Features.IdGeneration; +using Backend.Fx.Tests.ExecutionPipeline; +using FakeItEasy; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.Tests.DummyServices; + +public class DummyServicesFeature : Feature, IBootableFeature +{ + private readonly DummyServicesModule _module = new(); + + public DummyServicesModule Spies => _module; + + public override void Enable(IBackendFxApplication application) + { + application.CompositionRoot.RegisterModules(_module); + } + + public Task BootAsync(IBackendFxApplication application, CancellationToken cancellationToken = default) + { + _module.ResetSpies(); + return Task.CompletedTask; + } +} + +public class DummyServicesModule : IModule +{ + public DummyDemoDataGeneratorSpy DummyDemoDataGeneratorSpy { get; } = new(); + public DummyProductiveDataGeneratorSpy DummyProductiveDataGeneratorSpy { get; } = new(); + + public IDummyAuthorizationPolicySpy DummyAuthorizationPolicySpy { get; } = A.Fake(); + public IDummyDomainEventHandlerSpy DummyDomainEventHandlerSpy { get; } = A.Fake(); + public IDummyJobSpy DummyJobSpy { get; } = A.Fake(); + public IOperationSpy OperationSpy { get; } = A.Fake(); + + public IDummyIntegrationEventHandlerSpy DummyIntegrationEventHandlerSpy { get; } = + A.Fake(); + public IEntityIdGenerator EntityIdGenerator { get; } = A.Fake>(); + + + public void Register(ICompositionRoot compositionRoot) + { + compositionRoot.Register(ServiceDescriptor.Singleton(DummyDemoDataGeneratorSpy)); + compositionRoot.Register(ServiceDescriptor.Singleton(DummyProductiveDataGeneratorSpy)); + compositionRoot.Register(ServiceDescriptor.Singleton(DummyDomainEventHandlerSpy)); + compositionRoot.Register(ServiceDescriptor.Singleton(DummyJobSpy)); + compositionRoot.Register(ServiceDescriptor.Singleton(DummyAuthorizationPolicySpy)); + compositionRoot.Register(ServiceDescriptor.Singleton(OperationSpy)); + compositionRoot.Register(ServiceDescriptor.Singleton(DummyIntegrationEventHandlerSpy)); + compositionRoot.RegisterDecorator(ServiceDescriptor.Scoped()); + } + + public void ResetSpies() + { + Fake.ClearRecordedCalls(DummyAuthorizationPolicySpy); + Fake.ClearRecordedCalls(DummyDomainEventHandlerSpy); + Fake.ClearRecordedCalls(DummyJobSpy); + Fake.ClearRecordedCalls(OperationSpy); + Fake.ClearRecordedCalls(DummyIntegrationEventHandlerSpy); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Environment/Authentication/TheAnonymousIdentity.cs b/tests/Backend.Fx.Tests/Environment/Authentication/TheAnonymousIdentity.cs deleted file mode 100644 index 3c78ac09..00000000 --- a/tests/Backend.Fx.Tests/Environment/Authentication/TheAnonymousIdentity.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Backend.Fx.Environment.Authentication; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Environment.Authentication -{ - public class TheAnonymousIdentity : TestWithLogging - { - [Fact] - public void HasNameAnonymous() - { - Assert.Equal("ANONYMOUS", new AnonymousIdentity().Name); - } - - [Fact] - public void HasNoAuthenticationType() - { - Assert.Equal(string.Empty, new AnonymousIdentity().AuthenticationType); - } - - [Fact] - public void IsNotAuthenticated() - { - Assert.False(new AnonymousIdentity().IsAuthenticated); - } - - public TheAnonymousIdentity(ITestOutputHelper output) : base(output) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Environment/Authentication/TheCurrentIdentityHolder.cs b/tests/Backend.Fx.Tests/Environment/Authentication/TheCurrentIdentityHolder.cs deleted file mode 100644 index 6e0fac60..00000000 --- a/tests/Backend.Fx.Tests/Environment/Authentication/TheCurrentIdentityHolder.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Backend.Fx.Environment.Authentication; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Environment.Authentication -{ - public class TheCurrentIdentityHolder : TestWithLogging - { - [Fact] - public void FallsBackToInitialValueWhenReplacingWithNull() - { - var currentIdentityHolder = new CurrentIdentityHolder(); - currentIdentityHolder.ReplaceCurrent(null); - Assert.Equal("ANONYMOUS", currentIdentityHolder.Current.Name); - } - - [Fact] - public void InitializesWithAnonymousIdentity() - { - var currentIdentityHolder = new CurrentIdentityHolder(); - Assert.Equal("ANONYMOUS", currentIdentityHolder.Current.Name); - } - - [Fact] - public void ReplacesCurrentIdentity() - { - var currentIdentityHolder = new CurrentIdentityHolder(); - currentIdentityHolder.ReplaceCurrent(new SystemIdentity()); - Assert.Equal("SYSTEM", currentIdentityHolder.Current.Name); - } - - public TheCurrentIdentityHolder(ITestOutputHelper output) : base(output) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Environment/Authentication/TheSystemIdentity.cs b/tests/Backend.Fx.Tests/Environment/Authentication/TheSystemIdentity.cs deleted file mode 100644 index 94cb83b6..00000000 --- a/tests/Backend.Fx.Tests/Environment/Authentication/TheSystemIdentity.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Backend.Fx.Environment.Authentication; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Environment.Authentication -{ - public class TheSystemIdentity : TestWithLogging - { - [Fact] - public void HasAuthenticationTypeSystemInternal() - { - Assert.Equal("system internal", new SystemIdentity().AuthenticationType); - } - - [Fact] - public void HasNameSystem() - { - Assert.Equal("SYSTEM", new SystemIdentity().Name); - } - - [Fact] - public void IsAuthenticated() - { - Assert.True(new SystemIdentity().IsAuthenticated); - } - - public TheSystemIdentity(ITestOutputHelper output) : base(output) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Environment/DateAndTime/TheAdjustableClock.cs b/tests/Backend.Fx.Tests/Environment/DateAndTime/TheAdjustableClock.cs deleted file mode 100644 index 9e0cf2a0..00000000 --- a/tests/Backend.Fx.Tests/Environment/DateAndTime/TheAdjustableClock.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Threading; -using Backend.Fx.Environment.DateAndTime; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Environment.DateAndTime -{ - public - class TheAdjustableClock : TestWithLogging - { - [Fact] - public void AllowsOverridingOfUtcNow() - { - var overriddenUtcNow = new DateTime(2000, 1, 1, 12, 0, 0); - var sut = new AdjustableClock(new WallClock()); - sut.OverrideUtcNow(overriddenUtcNow); - Assert.Equal(overriddenUtcNow, sut.UtcNow); - Thread.Sleep(100); - Assert.Equal(overriddenUtcNow, sut.UtcNow); - } - - public TheAdjustableClock(ITestOutputHelper output) : base(output) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Environment/DateAndTime/TheFrozenClock.cs b/tests/Backend.Fx.Tests/Environment/DateAndTime/TheFrozenClock.cs deleted file mode 100644 index a4be9bb8..00000000 --- a/tests/Backend.Fx.Tests/Environment/DateAndTime/TheFrozenClock.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Threading; -using Backend.Fx.Environment.DateAndTime; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Environment.DateAndTime -{ - public class TheFrozenClock : TestWithLogging - { - [Fact] - public void IsFrozen() - { - - IClock sut = new FrozenClock(new WallClock()); - DateTime systemUtcNow = sut.UtcNow; - Thread.Sleep(100); - Assert.Equal(systemUtcNow, sut.UtcNow); - Assert.NotEqual(DateTime.UtcNow, sut.UtcNow); - } - - public TheFrozenClock(ITestOutputHelper output) : base(output) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Environment/DateAndTime/TheWallClock.cs b/tests/Backend.Fx.Tests/Environment/DateAndTime/TheWallClock.cs deleted file mode 100644 index efe0b818..00000000 --- a/tests/Backend.Fx.Tests/Environment/DateAndTime/TheWallClock.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using Backend.Fx.Environment.DateAndTime; -using Backend.Fx.Extensions; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Environment.DateAndTime -{ - public class TheWallClock : TestWithLogging - { - private readonly IEqualityComparer _tolerantDateTimeComparer = new TolerantDateTimeComparer(TimeSpan.FromMilliseconds(10)); - - [Fact] - public void IsTheSystemClock() - { - IClock sut = new WallClock(); - - Assert.Equal(DateTime.UtcNow, sut.UtcNow, _tolerantDateTimeComparer); - - Thread.Sleep(100); - - Assert.Equal(DateTime.UtcNow, sut.UtcNow, _tolerantDateTimeComparer); - } - - public TheWallClock(ITestOutputHelper output) : base(output) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheAllTenantBackendFxApplicationInvoker.cs b/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheAllTenantBackendFxApplicationInvoker.cs deleted file mode 100644 index af13ae27..00000000 --- a/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheAllTenantBackendFxApplicationInvoker.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Linq; -using System.Security.Principal; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Patterns.DependencyInjection; -using FakeItEasy; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Environment.MultiTenancy -{ - public class TheAllTenantBackendFxApplicationInvoker : TestWithLogging - { - public TheAllTenantBackendFxApplicationInvoker(ITestOutputHelper output): base(output) - { - _sut = new AllTenantBackendFxApplicationInvoker(_tenantService, _invoker); - } - - private readonly AllTenantBackendFxApplicationInvoker _sut; - private readonly ITenantIdProvider _tenantService = A.Fake(); - private readonly IBackendFxApplicationInvoker _invoker = A.Fake(); - - [Fact] - public void InvokesActionForAllTenants() - { - var demoTenantIds = Enumerable.Range(0, 10).Select(i => new TenantId(i)).ToArray(); - var prodTenantIds = Enumerable.Range(10, 10).Select(i => new TenantId(i)).ToArray(); - A.CallTo(() => _tenantService.GetActiveDemonstrationTenantIds()).Returns(demoTenantIds); - A.CallTo(() => _tenantService.GetActiveProductionTenantIds()).Returns(prodTenantIds); - - _sut.Invoke(_ => { }); - - foreach (TenantId tenantId in demoTenantIds) - { - A.CallTo(() => _invoker.Invoke(A>._, A._, A.That.IsSameAs(tenantId), A._)) - .MustHaveHappenedOnceExactly(); - } - - foreach (TenantId tenantId in prodTenantIds) - { - A.CallTo(() => _invoker.Invoke(A>._, A._, A.That.IsSameAs(tenantId), A._)) - .MustHaveHappenedOnceExactly(); - } - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheCurrentTenantIdHolder.cs b/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheCurrentTenantIdHolder.cs deleted file mode 100644 index 27c81ba0..00000000 --- a/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheCurrentTenantIdHolder.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Backend.Fx.Environment.MultiTenancy; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Environment.MultiTenancy -{ - public class TheCurrentTenantIdHolder : TestWithLogging - { - [Fact] - public void InitializesWithNullTenantIdIdentity() - { - var currentTenantIdHolder = new CurrentTenantIdHolder(); - Assert.False(currentTenantIdHolder.Current.HasValue); - } - - [Fact] - public void ReplacesCurrentTenantId() - { - var currentTenantIdHolder = new CurrentTenantIdHolder(); - currentTenantIdHolder.ReplaceCurrent(new TenantId(345)); - Assert.Equal(345, currentTenantIdHolder.Current.Value); - } - - public TheCurrentTenantIdHolder(ITestOutputHelper output) : base(output) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheSingleTenantApplication.cs b/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheSingleTenantApplication.cs deleted file mode 100644 index 6e181b4a..00000000 --- a/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheSingleTenantApplication.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Threading.Tasks; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.InMemoryPersistence; -using Backend.Fx.Patterns.EventAggregation.Integration; -using FakeItEasy; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Environment.MultiTenancy -{ - public class TheSingleTenantApplication : TestWithLogging - { - private readonly SingleTenantApplication _sut; - private readonly ITenantRepository _tenantRepository = new InMemoryTenantRepository(); - private readonly IMessageBus _messageBus = A.Fake(); - - public TheSingleTenantApplication(ITestOutputHelper output) : base(output) - { - _sut = new SingleTenantApplication(_messageBus, _tenantRepository, false); - } - - [Fact] - public void CreatesTenantOnBootWhenNotExistent() - { - var tenants = _tenantRepository.GetTenants(); - Assert.Empty(tenants); - - _sut.Boot(); - - tenants = _tenantRepository.GetTenants(); - Assert.Single(tenants); - } - - [Fact] - public void CreatesNoTenantOnBootWhenExistent() - { - var tenant = new Tenant("single tenant", "", false); - _tenantRepository.SaveTenant(tenant); - - _sut.Boot(); - - var tenants = _tenantRepository.GetTenants(); - Assert.Single(tenants); - - Assert.Equal(tenant.Id, tenants[0].Id); - } - - [Fact] - public void CreatesOnlyOneTenantEvenWhenBootedMultipleTimes() - { - var tenants = _tenantRepository.GetTenants(); - Assert.Empty(tenants); - - _sut.Boot(); - _sut.Boot(); - _sut.Boot(); - _sut.Boot(); - - tenants = _tenantRepository.GetTenants(); - Assert.Single(tenants); - } - - [Fact] - public void CreatesOnlyOneTenantEvenWhenBootedMultipleTimesInParallel() - { - var tenants = _tenantRepository.GetTenants(); - Assert.Empty(tenants); - - Task.WaitAll( - Task.Run(() => _sut.Boot()), - Task.Run(() => _sut.Boot()), - Task.Run(() => _sut.Boot()), - Task.Run(() => _sut.Boot()) - ); - - - tenants = _tenantRepository.GetTenants(); - Assert.Single(tenants); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheTenant.cs b/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheTenant.cs deleted file mode 100644 index 1f115c82..00000000 --- a/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheTenant.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using Backend.Fx.Environment.MultiTenancy; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Environment.MultiTenancy -{ - public class TheTenant : TestWithLogging - { - [Fact] - public void CannotBeInitializedWithoutName() - { - Assert.Throws(() => new Tenant("", "", false)); - // ReSharper disable once AssignNullToNotNullAttribute - testing null case exception - Assert.Throws(() => new Tenant(null, "", false)); - Assert.Throws(() => new Tenant(" ", "", false)); - } - - [Fact] - public void InitializesCorrectly() - { - var tenant = new Tenant("name", "description", true); - Assert.Equal("name", tenant.Name); - Assert.Equal("description", tenant.Description); - Assert.True(tenant.IsDemoTenant); - } - - public TheTenant(ITestOutputHelper output) : base(output) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheTenantId.cs b/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheTenantId.cs deleted file mode 100644 index 2b72a093..00000000 --- a/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheTenantId.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using Backend.Fx.Environment.MultiTenancy; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Environment.MultiTenancy -{ - public class TheTenantId : TestWithLogging - { - [Fact] - public void HasNoValueWhenInitializedWithNull() - { - var sut = new TenantId(null); - Assert.False(sut.HasValue); - } - - [Fact] - public void ThrowsOnAccessingTheValueWhenInitializedWithNull() - { - var sut = new TenantId(null); - Assert.Throws(() => sut.Value); - } - - public TheTenantId(ITestOutputHelper output) : base(output) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheTenantService.cs b/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheTenantService.cs deleted file mode 100644 index a168ae4c..00000000 --- a/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheTenantService.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.InMemoryPersistence; -using Backend.Fx.Patterns.EventAggregation.Integration; -using FakeItEasy; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Environment.MultiTenancy -{ - public class TheTenantService : TestWithLogging - { - public TheTenantService(ITestOutputHelper output): base(output) - { - _sut = new TenantService(_messageBus, _tenantRepository); - } - - private readonly ITenantService _sut; - private readonly IMessageBus _messageBus = A.Fake(); - private readonly InMemoryTenantRepository _tenantRepository = new InMemoryTenantRepository(); - - [Fact] - public void CannotCreateTenantWithoutName() - { - Assert.Throws(() => _sut.CreateTenant("", "d", true)); - Assert.Throws(() => _sut.CreateTenant(null, "d", true)); - } - - [Fact] - public void CannotCreateTenantWithSameName() - { - _sut.CreateTenant("n", "d", true); - Assert.Throws(() => _sut.CreateTenant("n", "d", true)); - Assert.Throws(() => _sut.CreateTenant("n", "d", false)); - Assert.Throws(() => _sut.CreateTenant("N", "d", true)); - } - - [Fact] - public void GetsProductiveTenantIds() - { - var tenants = Enumerable.Range(1, 7) - .Select(i => new Tenant("n" + i, "d" + i, i % 2 == 0)) - .ToArray(); - - foreach (Tenant tenant in tenants) - { - tenant.State = TenantState.Active; - _tenantRepository.SaveTenant(tenant); - } - - var tenantIds = tenants.Select(t => new TenantId(t.Id)).ToArray(); - var demoTenantIds = tenants.Where(t => t.IsDemoTenant).Select(t => new TenantId(t.Id)).ToArray(); - var prodTenantIds = tenants.Where(t => !t.IsDemoTenant).Select(t => new TenantId(t.Id)).ToArray(); - - Assert.Equal(tenantIds, _sut.TenantIdProvider.GetActiveTenantIds()); - Assert.Equal(prodTenantIds, _sut.TenantIdProvider.GetActiveProductionTenantIds()); - Assert.Equal(demoTenantIds, _sut.TenantIdProvider.GetActiveDemonstrationTenantIds()); - } - - [Fact] - public void RaisesTenantActivatedEvent() - { - var ev = new ManualResetEvent(false); - A.CallTo(() => _messageBus.Publish(A._)).Invokes(() => ev.Set()); - Task.Run(() => _sut.CreateTenant("prod", "unit test created", false)); - Assert.True(ev.WaitOne(Debugger.IsAttached ? int.MaxValue : 10000)); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Exceptions/TheClientException.cs b/tests/Backend.Fx.Tests/Exceptions/TheClientException.cs new file mode 100644 index 00000000..15d5a08b --- /dev/null +++ b/tests/Backend.Fx.Tests/Exceptions/TheClientException.cs @@ -0,0 +1,93 @@ +using System; +using Backend.Fx.Exceptions; +using Backend.Fx.TestUtil; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.Exceptions; + +public class TheClientException : TestWithLogging +{ + private readonly ITestOutputHelper _output; + + public TheClientException(ITestOutputHelper output) : base(output) + { + _output = output; + } + + [Fact] + public void CanBeInstantiated() + { + var exception1 = new ClientException(); + var exception2 = new ClientException("With a message"); + var exception3 = new ClientException("With a message and an inner", new Exception()); + } + + [Fact] + public void AllowsAddingError() + { + var exception = new ClientException() + .AddError("The error message"); + + Assert.Contains(exception.Errors, err => err.Key == string.Empty && err.Value[0] == "The error message"); + Assert.True(exception.HasErrors()); + Assert.NotEmpty(exception.Errors[string.Empty]); + Assert.Contains(string.Empty, exception.Errors.Keys); + Assert.Contains(exception.Errors.Values, val => val[0] == "The error message"); + } + + [Fact] + public void AllowsAddingErrors() + { + var exception = new ClientException() + .AddErrors(new [] {"The first error message", "The second error message"}); + + Assert.Contains( + exception.Errors, + err => + err.Key == string.Empty + && err.Value[0] == "The first error message" + && err.Value[1] == "The second error message"); + Assert.True(exception.HasErrors()); + } + + [Fact] + public void AllowsAddingKeyedError() + { + var exception = new ClientException() + .AddError("key", "The error message"); + + Assert.Contains(exception.Errors, err => err.Key == "key" && err.Value[0] == "The error message"); + Assert.True(exception.HasErrors()); + Assert.True(exception.Errors.ContainsKey("key")); + } + + [Fact] + public void AllowsAddingKeyedErrors() + { + var exception = new ClientException() + .AddErrors("key", new [] {"The first error message", "The second error message"}); + + Assert.Contains( + exception.Errors, + err => + err.Key == "key" + && err.Value[0] == "The first error message" + && err.Value[1] == "The second error message"); + Assert.True(exception.HasErrors()); + Assert.True(exception.Errors.ContainsKey("key")); + } + + [Fact] + public void ContainsErrorsInToString() + { + var exception = new ClientException() + .AddErrors("key", new [] {"The first error message", "The second error message"}); + + string exToString = exception.ToString(); + Assert.Contains("Errors: 1", exToString); + Assert.Contains("key", exToString); + Assert.Contains("[0] The first error message", exToString); + Assert.Contains("[1] The second error message", exToString); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Exceptions/TheConflictedException.cs b/tests/Backend.Fx.Tests/Exceptions/TheConflictedException.cs new file mode 100644 index 00000000..ac369761 --- /dev/null +++ b/tests/Backend.Fx.Tests/Exceptions/TheConflictedException.cs @@ -0,0 +1,16 @@ +using System; +using Backend.Fx.Exceptions; +using Xunit; + +namespace Backend.Fx.Tests.Exceptions; + +public class TheConflictedException +{ + [Fact] + public void CanBeInstantiated() + { + var exception1 = new ConflictedException(); + var exception2 = new ConflictedException("With a message"); + var exception3 = new ConflictedException("With a message and an inner", new Exception()); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Exceptions/TheForbiddenException.cs b/tests/Backend.Fx.Tests/Exceptions/TheForbiddenException.cs new file mode 100644 index 00000000..e84423a2 --- /dev/null +++ b/tests/Backend.Fx.Tests/Exceptions/TheForbiddenException.cs @@ -0,0 +1,16 @@ +using System; +using Backend.Fx.Exceptions; +using Xunit; + +namespace Backend.Fx.Tests.Exceptions; + +public class TheForbiddenException +{ + [Fact] + public void CanBeInstantiated() + { + var exception1 = new ForbiddenException(); + var exception2 = new ForbiddenException("With a message"); + var exception3 = new ForbiddenException("With a message and an inner", new Exception()); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Exceptions/TheNotFoundException.cs b/tests/Backend.Fx.Tests/Exceptions/TheNotFoundException.cs index 17205c5a..abc62d12 100644 --- a/tests/Backend.Fx.Tests/Exceptions/TheNotFoundException.cs +++ b/tests/Backend.Fx.Tests/Exceptions/TheNotFoundException.cs @@ -1,5 +1,7 @@ -using Backend.Fx.Exceptions; -using Backend.Fx.Tests.BuildingBlocks; +using Backend.Fx.Domain; +using Backend.Fx.Exceptions; +using Backend.Fx.TestUtil; +using JetBrains.Annotations; using Xunit; using Xunit.Abstractions; @@ -7,16 +9,29 @@ namespace Backend.Fx.Tests.Exceptions { public class TheNotFoundException : TestWithLogging { + public TheNotFoundException(ITestOutputHelper output) : base(output) + { } + + [Fact] + public void CanBeThrownWithoutAnyParameters() + { + var exception = new NotFoundException(); + Assert.Null(exception.EntityName); + } + [Fact] public void FillsNameAndIdProperties() { - var exception = new NotFoundException(4711); - Assert.Equal("TestAggregateRoot", exception.EntityName); + var exception = new NotFoundException(4711); + Assert.Equal("SomeEntity", exception.EntityName); Assert.Equal(4711, exception.Id); } - public TheNotFoundException(ITestOutputHelper output) : base(output) + + [UsedImplicitly] + private class SomeEntity : IAggregateRoot { + public int Id { get; } } } } \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Exceptions/TheTooManyRequestsException.cs b/tests/Backend.Fx.Tests/Exceptions/TheTooManyRequestsException.cs new file mode 100644 index 00000000..4707b43b --- /dev/null +++ b/tests/Backend.Fx.Tests/Exceptions/TheTooManyRequestsException.cs @@ -0,0 +1,23 @@ +using System; +using Backend.Fx.Exceptions; +using Xunit; + +namespace Backend.Fx.Tests.Exceptions; + +public class TheTooManyRequestsException +{ + [Fact] + public void CanBeInstantiated() + { + var exception1 = new TooManyRequestsException(5); + var exception2 = new TooManyRequestsException(5, "With a message"); + var exception3 = new TooManyRequestsException(5, "With a message and an inner", new Exception()); + } + + [Fact] + public void KeepsRetryAfter() + { + var exception1 = new TooManyRequestsException(5); + Assert.Equal(5, exception1.RetryAfter); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Exceptions/TheUnauthorizedException.cs b/tests/Backend.Fx.Tests/Exceptions/TheUnauthorizedException.cs new file mode 100644 index 00000000..7a8f9375 --- /dev/null +++ b/tests/Backend.Fx.Tests/Exceptions/TheUnauthorizedException.cs @@ -0,0 +1,16 @@ +using System; +using Backend.Fx.Exceptions; +using Xunit; + +namespace Backend.Fx.Tests.Exceptions; + +public class TheUnauthorizedException +{ + [Fact] + public void CanBeInstantiated() + { + var exception1 = new UnauthorizedException(); + var exception2 = new UnauthorizedException("With a message"); + var exception3 = new UnauthorizedException("With a message and an inner", new Exception()); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Exceptions/TheUnprocessableException.cs b/tests/Backend.Fx.Tests/Exceptions/TheUnprocessableException.cs new file mode 100644 index 00000000..ffc3af77 --- /dev/null +++ b/tests/Backend.Fx.Tests/Exceptions/TheUnprocessableException.cs @@ -0,0 +1,16 @@ +using System; +using Backend.Fx.Exceptions; +using Xunit; + +namespace Backend.Fx.Tests.Exceptions; + +public class TheUnprocessableException +{ + [Fact] + public void CanBeInstantiated() + { + var exception1 = new UnprocessableException(); + var exception2 = new UnprocessableException("With a message"); + var exception3 = new UnprocessableException("With a message and an inner", new Exception()); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Exceptions/TheUnprocessableExceptionBuilder.cs b/tests/Backend.Fx.Tests/Exceptions/TheUnprocessableExceptionBuilder.cs index 7dfeb17d..c70c2aa3 100644 --- a/tests/Backend.Fx.Tests/Exceptions/TheUnprocessableExceptionBuilder.cs +++ b/tests/Backend.Fx.Tests/Exceptions/TheUnprocessableExceptionBuilder.cs @@ -1,5 +1,7 @@ -using Backend.Fx.Exceptions; -using Backend.Fx.Tests.BuildingBlocks; +using Backend.Fx.Domain; +using Backend.Fx.Exceptions; +using Backend.Fx.TestUtil; +using JetBrains.Annotations; using Xunit; using Xunit.Abstractions; @@ -11,7 +13,7 @@ public class TheUnprocessableExceptionBuilder : TestWithLogging public void AddsExceptionWhenAggregateIsNull() { IExceptionBuilder sut = UnprocessableException.UseBuilder(); - sut.AddNotFoundWhenNull(1111, null); + sut.AddNotFoundWhenNull(1111, null!); Assert.Throws(() => sut.Dispose()); } @@ -19,7 +21,7 @@ public void AddsExceptionWhenAggregateIsNull() public void AddsNoExceptionWhenAggregateIsNotNull() { IExceptionBuilder sut = UnprocessableException.UseBuilder(); - sut.AddNotFoundWhenNull(1111, new TheAggregateRoot.TestAggregateRoot(12345, "gaga")); + sut.AddNotFoundWhenNull(1111, new SomeEntity()); sut.Dispose(); } @@ -38,6 +40,14 @@ public void ThrowsExceptionWhenAddingConditionalError() sut.AddIf(true, "something is broken"); Assert.Throws(() => sut.Dispose()); } + + [Fact] + public void ThrowsExceptionWhenAddingConditionalKeyedError() + { + IExceptionBuilder sut = UnprocessableException.UseBuilder(); + sut.AddIf("the key", true, "something is broken"); + Assert.Throws(() => sut.Dispose()); + } [Fact] public void ThrowsExceptionWhenAddingError() @@ -46,9 +56,24 @@ public void ThrowsExceptionWhenAddingError() sut.Add("something is broken"); Assert.Throws(() => sut.Dispose()); } + + [Fact] + public void ThrowsExceptionWhenAddingKeyedError() + { + IExceptionBuilder sut = UnprocessableException.UseBuilder(); + sut.Add("theKey", "something is broken"); + Assert.Throws(() => sut.Dispose()); + } public TheUnprocessableExceptionBuilder(ITestOutputHelper output) : base(output) { } + + [UsedImplicitly] + private class SomeEntity : IAggregateRoot + { + [UsedImplicitly] + public int Id { get; } + } } } \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/ExecutionPipeline/OperationSpy.cs b/tests/Backend.Fx.Tests/ExecutionPipeline/OperationSpy.cs new file mode 100644 index 00000000..d89f7bd5 --- /dev/null +++ b/tests/Backend.Fx.Tests/ExecutionPipeline/OperationSpy.cs @@ -0,0 +1,40 @@ +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.ExecutionPipeline; +using Microsoft.Extensions.DependencyInjection; + +namespace Backend.Fx.Tests.ExecutionPipeline; + +public interface IOperationSpy : IOperation +{ +} + +public class OperationSpy : IOperation +{ + private readonly IOperationSpy _operationSpy; + private readonly IOperation _operation; + + public OperationSpy(IOperationSpy operationSpy, IOperation operation) + { + _operationSpy = operationSpy; + _operation = operation; + } + + public async Task BeginAsync(IServiceScope serviceScope, CancellationToken cancellationToken = default) + { + await _operationSpy.BeginAsync(serviceScope); + await _operation.BeginAsync(serviceScope); + } + + public async Task CompleteAsync(CancellationToken cancellationToken = default) + { + await _operationSpy.CompleteAsync(cancellationToken); + await _operation.CompleteAsync(cancellationToken); + } + + public async Task CancelAsync(CancellationToken cancellationToken = default) + { + await _operationSpy.CancelAsync(cancellationToken); + await _operation.CancelAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/ExecutionPipeline/TheAnonymousIdentity.cs b/tests/Backend.Fx.Tests/ExecutionPipeline/TheAnonymousIdentity.cs new file mode 100644 index 00000000..6c3ea821 --- /dev/null +++ b/tests/Backend.Fx.Tests/ExecutionPipeline/TheAnonymousIdentity.cs @@ -0,0 +1,63 @@ +using System.Security.Principal; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.TestUtil; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.ExecutionPipeline; + +public class TheAnonymousIdentity : TestWithLogging +{ + public TheAnonymousIdentity(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void HasCorrectName() + { + var sut = new AnonymousIdentity(); + Assert.Equal("ANONYMOUS", sut.Name); + } + + [Fact] + public void IsNotAuthenticated() + { + var sut = new AnonymousIdentity(); + Assert.False(sut.IsAuthenticated); + } + + [Fact] + public void IsDetectedAsAnonymousIdentity() + { + IIdentity sut = new AnonymousIdentity(); + Assert.True(sut.IsAnonymous()); + Assert.False(sut.IsSystem()); + } + + [Fact] + public void HasNoAuthenticationType() + { + var sut = new AnonymousIdentity(); + Assert.Null(sut.AuthenticationType); + } + + [Fact] + public void EqualsOtherAnonymousIdentity() + { + var sut = new AnonymousIdentity(); + var other = new AnonymousIdentity(); + Assert.True(sut.Equals(other)); + Assert.True(Equals(sut,other)); + Assert.Equal(sut.GetHashCode(), other.GetHashCode()); + } + + [Fact] + public void DoesNotEqualOtherIdentity() + { + var sut = new AnonymousIdentity(); + var other = new SystemIdentity(); + Assert.False(sut.Equals(other)); + Assert.False(Equals(sut,other)); + Assert.NotEqual(sut.GetHashCode(), other.GetHashCode()); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/ExecutionPipeline/TheCorrelation.cs b/tests/Backend.Fx.Tests/ExecutionPipeline/TheCorrelation.cs new file mode 100644 index 00000000..d6c701be --- /dev/null +++ b/tests/Backend.Fx.Tests/ExecutionPipeline/TheCorrelation.cs @@ -0,0 +1,30 @@ +using System; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.TestUtil; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.ExecutionPipeline; + +public class TheCorrelation : TestWithLogging +{ + public TheCorrelation(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void InitializesWithRandomGuid() + { + var sut = new Correlation(); + Assert.NotEqual(Guid.Empty, sut.Id); + } + + [Fact] + public void CanResume() + { + Guid correlationIdToResume = Guid.NewGuid(); + var sut = new Correlation(); + sut.Resume(correlationIdToResume); + Assert.Equal(correlationIdToResume, sut.Id); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/ExecutionPipeline/TheCurrentIdentityHolder.cs b/tests/Backend.Fx.Tests/ExecutionPipeline/TheCurrentIdentityHolder.cs new file mode 100644 index 00000000..4e565ec2 --- /dev/null +++ b/tests/Backend.Fx.Tests/ExecutionPipeline/TheCurrentIdentityHolder.cs @@ -0,0 +1,36 @@ +using System.Security.Claims; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.TestUtil; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.ExecutionPipeline; + +public class TheCurrentIdentityHolder : TestWithLogging +{ + public TheCurrentIdentityHolder(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void CanBeCreatedWithSystemIdentity() + { + var sut = CurrentIdentityHolder.CreateSystem(); + Assert.Equal(sut.Current, new SystemIdentity()); + } + + [Fact] + public void CanBeCreatedWithArbitraryIdentity() + { + var identity = new ClaimsIdentity(); + var sut = CurrentIdentityHolder.Create(identity); + Assert.Equal(sut.Current, identity); + } + + [Fact] + public void DefaultsToAnonymousIdentityWhenCreatedWithNull() + { + var sut = CurrentIdentityHolder.Create(null); + Assert.Equal(sut.Current, new AnonymousIdentity()); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/ExecutionPipeline/TheExceptionLoggingAndHandlingInvoker.cs b/tests/Backend.Fx.Tests/ExecutionPipeline/TheExceptionLoggingAndHandlingInvoker.cs new file mode 100644 index 00000000..27cd3627 --- /dev/null +++ b/tests/Backend.Fx.Tests/ExecutionPipeline/TheExceptionLoggingAndHandlingInvoker.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading.Tasks; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Logging; +using Backend.Fx.SimpleInjectorDependencyInjection; +using Backend.Fx.TestUtil; +using FakeItEasy; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.ExecutionPipeline; + +public class TheExceptionLoggingAndHandlingInvoker : TestWithLogging +{ + private readonly IBackendFxApplicationInvoker _sut; + private readonly IExceptionLogger _exceptionLogger = A.Fake(); + + + public TheExceptionLoggingAndHandlingInvoker(ITestOutputHelper output) : base(output) + { + var application = new BackendFxApplication( + new SimpleInjectorCompositionRoot(), + _exceptionLogger, + GetType().Assembly); + _sut = new ExceptionLoggingAndHandlingInvoker(_exceptionLogger, application.Invoker); + } + + + [Fact] + public void SwallowsExceptions() + { + _sut.InvokeAsync(_ => Task.CompletedTask); + _sut.InvokeAsync(_ => throw new DivideByZeroException()); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/ExecutionPipeline/TheFrozenClock.cs b/tests/Backend.Fx.Tests/ExecutionPipeline/TheFrozenClock.cs new file mode 100644 index 00000000..a4fc1fb7 --- /dev/null +++ b/tests/Backend.Fx.Tests/ExecutionPipeline/TheFrozenClock.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.TestUtil; +using NodaTime; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.ExecutionPipeline; + +public class TheFrozenClock : TestWithLogging +{ + public TheFrozenClock(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task IsFrozen() + { + var sut = new FrozenClock(SystemClock.Instance); + await Task.Delay(10); + Assert.True( + sut.GetCurrentInstant() <= SystemClock.Instance.GetCurrentInstant().Plus(-Duration.FromMilliseconds(9))); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/ExecutionPipeline/TheSystemIdentity.cs b/tests/Backend.Fx.Tests/ExecutionPipeline/TheSystemIdentity.cs new file mode 100644 index 00000000..96e180a0 --- /dev/null +++ b/tests/Backend.Fx.Tests/ExecutionPipeline/TheSystemIdentity.cs @@ -0,0 +1,63 @@ +using System.Security.Principal; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.TestUtil; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.ExecutionPipeline; + +public class TheSystemIdentity : TestWithLogging +{ + public TheSystemIdentity(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void HasCorrectName() + { + var sut = new SystemIdentity(); + Assert.Equal("SYSTEM", sut.Name); + } + + [Fact] + public void IsAuthenticated() + { + var sut = new SystemIdentity(); + Assert.True(sut.IsAuthenticated); + } + + [Fact] + public void IsDetectedAsSystemIdentity() + { + IIdentity sut = new SystemIdentity(); + Assert.True(sut.IsSystem()); + Assert.False(sut.IsAnonymous()); + } + + [Fact] + public void HasInternalAuthenticationType() + { + var sut = new SystemIdentity(); + Assert.Equal("Internal", sut.AuthenticationType); + } + + [Fact] + public void EqualsOtherSystemIdentity() + { + var sut = new SystemIdentity(); + var other = new SystemIdentity(); + Assert.True(sut.Equals(other)); + Assert.True(Equals(sut,other)); + Assert.Equal(sut.GetHashCode(), other.GetHashCode()); + } + + [Fact] + public void DoesNotEqualOtherIdentity() + { + var sut = new SystemIdentity(); + var other = new AnonymousIdentity(); + Assert.False(sut.Equals(other)); + Assert.False(Equals(sut,other)); + Assert.NotEqual(sut.GetHashCode(), other.GetHashCode()); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Extensions/TheDateTimeEx.cs b/tests/Backend.Fx.Tests/Extensions/TheDateTimeEx.cs deleted file mode 100644 index a8227ce6..00000000 --- a/tests/Backend.Fx.Tests/Extensions/TheDateTimeEx.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using Backend.Fx.Extensions; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Extensions -{ - public class TheDateTimeEx : TestWithLogging - { - [Fact] - public void CanConvertToUnixEpochDate() - { - Assert.Equal(0L, new DateTime(1970, 1, 1).ToUnixEpochDate()); - Assert.Equal(1495675672L, new DateTime(2017, 5, 25, 1, 27, 52).ToUnixEpochDate()); - Assert.Equal(int.MaxValue, new DateTime(2038, 1, 19, 3, 14, 7).ToUnixEpochDate()); - Assert.Equal((long)int.MaxValue + 1, new DateTime(2038, 1, 19, 3, 14, 8).ToUnixEpochDate()); - } - - [Fact] - public void CanGetStartOfWeek() - { - var dt = new DateTime(2017, 05, 24, 1, 2, 3); - Assert.Equal(new DateTime(2017, 05, 22), dt.StartOfWeek()); - Assert.Equal(new DateTime(2017, 05, 21), dt.StartOfWeek(DayOfWeek.Sunday)); - } - - [Fact] - public void CanGetWeekDay() - { - var dt = new DateTime(2017, 05, 24, 1, 2, 3); - Assert.Equal(new DateTime(2017, 05, 22), dt.GetWeekDay(DayOfWeek.Monday)); - Assert.Equal(new DateTime(2017, 05, 23), dt.GetWeekDay(DayOfWeek.Tuesday)); - Assert.Equal(new DateTime(2017, 05, 24), dt.GetWeekDay(DayOfWeek.Wednesday)); - Assert.Equal(new DateTime(2017, 05, 25), dt.GetWeekDay(DayOfWeek.Thursday)); - Assert.Equal(new DateTime(2017, 05, 26), dt.GetWeekDay(DayOfWeek.Friday)); - Assert.Equal(new DateTime(2017, 05, 27), dt.GetWeekDay(DayOfWeek.Saturday)); - Assert.Equal(new DateTime(2017, 05, 28), dt.GetWeekDay(DayOfWeek.Sunday)); - } - - public TheDateTimeEx(ITestOutputHelper output) : base(output) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Extensions/TheEnumerableEx.cs b/tests/Backend.Fx.Tests/Extensions/TheEnumerableEx.cs deleted file mode 100644 index e083f847..00000000 --- a/tests/Backend.Fx.Tests/Extensions/TheEnumerableEx.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Backend.Fx.Extensions; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Extensions -{ - public class TheEnumerableEx : TestWithLogging - { - private class Item - { - public bool Touched { get; set; } - } - - [Fact] - public void ExecutesActionForAll() - { - IEnumerable enumerable = Enumerable.Range(0, 100).Select(i => new Item()).ToArray(); - enumerable.ForAll(itm => itm.Touched = true); - - Assert.All(enumerable, itm => Assert.True(itm.Touched)); - } - - public TheEnumerableEx(ITestOutputHelper output) : base(output) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Extensions/TheStringEnumUtil.cs b/tests/Backend.Fx.Tests/Extensions/TheStringEnumUtil.cs deleted file mode 100644 index 457c8b50..00000000 --- a/tests/Backend.Fx.Tests/Extensions/TheStringEnumUtil.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using Backend.Fx.Extensions; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Extensions -{ - public enum AnEnum - { - One, - Two, - Three - } - - public class TheStringEnumUtil : TestWithLogging - { - [Theory] - [InlineData("One")] - [InlineData("Two")] - [InlineData("Three")] - public void ParsesStringsToEnums(string s) - { - Assert.IsType(s.Parse()); - } - - [Fact] - public void ParsesCaseInsensitive() - { - Assert.Equal(AnEnum.One, "One".Parse()); - Assert.Equal(AnEnum.Two, "two".Parse()); - Assert.Equal(AnEnum.Three, "THREE".Parse()); - } - - [Fact] - public void ThrowsOnInvalidString() - { - Assert.Throws(() => "whatever".Parse()); - } - - public TheStringEnumUtil(ITestOutputHelper output) : base(output) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/Authorization/TheAllowAllPolicy.cs b/tests/Backend.Fx.Tests/Features/Authorization/TheAllowAllPolicy.cs new file mode 100644 index 00000000..8a69fd3d --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/Authorization/TheAllowAllPolicy.cs @@ -0,0 +1,53 @@ +using System.Linq; +using Backend.Fx.Domain; +using Backend.Fx.Features.Authorization; +using Xunit; + +namespace Backend.Fx.Tests.Features.Authorization; + +public class TheAllowAllPolicy +{ + private class Agg : IAggregateRoot + { + public Agg(int id) + { + Id = id; + } + + public int Id { get; } + } + + private class AllowAllSut : AllowAll {} + + [Fact] + public void AllowsRead() + { + var sut = new AllowAllSut(); + + var aggregates = Enumerable.Range(1, 10).Select(i => new Agg(i)).ToArray(); + + var count = aggregates.Count(agg => sut.HasAccessExpression.Compile().Invoke(agg)); + Assert.Equal(10, count); + } + + [Fact] + public void AllowsCreate() + { + var sut = new AllowAllSut(); + Assert.True(sut.CanCreate(new Agg(1))); + } + + [Fact] + public void AllowsModify() + { + var sut = new AllowAllSut(); + Assert.True(sut.CanModify(new Agg(1))); + } + + [Fact] + public void AllowsDelete() + { + var sut = new AllowAllSut(); + Assert.True(sut.CanDelete(new Agg(1))); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/Authorization/TheAuthorizationFeature.cs b/tests/Backend.Fx.Tests/Features/Authorization/TheAuthorizationFeature.cs new file mode 100644 index 00000000..f0b3836c --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/Authorization/TheAuthorizationFeature.cs @@ -0,0 +1,254 @@ +using System; +using System.Threading.Tasks; +using Backend.Fx.DependencyInjection; +using Backend.Fx.Exceptions; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Features.Authorization; +using Backend.Fx.Features.Persistence; +using Backend.Fx.Features.Persistence.InMem; +using Backend.Fx.Logging; +using Backend.Fx.SimpleInjectorDependencyInjection; +using Backend.Fx.Tests.DummyServices; +using Backend.Fx.TestUtil; +using FakeItEasy; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.Features.Authorization; + +[UsedImplicitly] +public class TheAuthorizationFeatureWithSimpleInjector : TheAuthorizationFeature +{ + public TheAuthorizationFeatureWithSimpleInjector(ITestOutputHelper output) + : base(new SimpleInjectorCompositionRoot(), output) + { + } +} + +// failing: Microsoft's DI does not support decoration of open generic types. See https://github.com/khellang/Scrutor/issues/39 for more info +// [UsedImplicitly] +// public class TheAuthorizationFeatureWithMicrosoftDI : TheAuthorizationFeature +// { +// public TheAuthorizationFeatureWithMicrosoftDI(ITestOutputHelper output) +// : base(new MicrosoftCompositionRoot(), output) +// { +// } +// } + +public abstract class TheAuthorizationFeature : TestWithLogging +{ + private readonly IBackendFxApplication _sut; + private readonly IExceptionLogger _exceptionLogger = A.Fake(); + private readonly DummyServicesFeature _dummyServicesFeature = new (); + + protected TheAuthorizationFeature(ICompositionRoot compositionRoot, ITestOutputHelper output) : base(output) + { + _sut = new BackendFxApplication(compositionRoot, _exceptionLogger, GetType().Assembly); + _sut.EnableFeature(_dummyServicesFeature); + } + + [Fact] + public void CanNotEnableFeatureWithoutPersistence() + { + Assert.Throws(() => _sut.EnableFeature(new AuthorizationFeature())); + } + + [Fact] + public async Task CanAllowRead() + { + await ConfigureAndBootApplication(); + + FillStore(); + + A.CallTo(() => _dummyServicesFeature.Spies.DummyAuthorizationPolicySpy.HasAccessExpression) + .Returns(agg => true); + + await _sut.Invoker.InvokeAsync(async sp=> + { + var repository = sp.GetRequiredService>(); + var aggregates = await repository.GetAllAsync(); + Assert.NotEmpty(aggregates); + Assert.Equal(4, aggregates.Length); + + var repoHasAny = await repository.AnyAsync(); + Assert.True(repoHasAny); + + for (var i = 1; i <= 4; i++) + { + var aggregate = await repository.FindAsync(i); + Assert.NotNull(aggregate); + + aggregate = await repository.GetAsync(i); + Assert.NotNull(aggregate); + } + + aggregates = await repository.ResolveAsync(new[] { 1, 2, 3, 4 }); + Assert.NotEmpty(aggregates); + Assert.Equal(4, aggregates.Length); + }, new AnonymousIdentity()); + } + + [Fact] + public async Task CanDenyRead() + { + await ConfigureAndBootApplication(); + + FillStore(); + + A.CallTo(() => _dummyServicesFeature.Spies.DummyAuthorizationPolicySpy.HasAccessExpression) + .Returns(agg => false); + + await _sut.Invoker.InvokeAsync(async sp=> + { + var repository = sp.GetRequiredService>(); + var aggregates = await repository.GetAllAsync(); + Assert.Empty(aggregates); + + var repoHasAny = await repository.AnyAsync(); + Assert.False(repoHasAny); + + for (var i = 1; i <= 4; i++) + { + var aggregate = await repository.FindAsync(i); + Assert.Null(aggregate); + + await Assert.ThrowsAsync>(async () => + await repository.GetAsync(i)); + } + + await Assert.ThrowsAsync(async () => + await repository.ResolveAsync(new[] { 1, 2, 3, 4 })); + }, new AnonymousIdentity()); + } + + [Fact] + public async Task CanAllowCreate() + { + await ConfigureAndBootApplication(); + + FillStore(); + + A.CallTo(() => _dummyServicesFeature.Spies.DummyAuthorizationPolicySpy.HasAccessExpression).Returns(agg => true); + A.CallTo(() => _dummyServicesFeature.Spies.DummyAuthorizationPolicySpy.CanCreate(A._)).Returns(true); + + await _sut.Invoker.InvokeAsync(async sp=> + { + var repository = sp.GetRequiredService>(); + await repository.AddAsync(new DummyAggregate(1000, "thousand")); + await repository.AddRangeAsync(new[] + { + new DummyAggregate(1001, "thousand and one"), + new DummyAggregate(1002, "thousand and two") + }); + }, new AnonymousIdentity()); + + await _sut.Invoker.InvokeAsync(async sp=> + { + var repository = sp.GetRequiredService>(); + var unused1 = await repository.GetAsync(1000); + var unused2 = await repository.GetAsync(1001); + var unused3 = await repository.GetAsync(1002); + }, new AnonymousIdentity()); + } + + [Fact] + public async Task CanAllowDelete() + { + await ConfigureAndBootApplication(); + + FillStore(); + + A.CallTo(() => _dummyServicesFeature.Spies.DummyAuthorizationPolicySpy.HasAccessExpression).Returns(agg => true); + A.CallTo(() => _dummyServicesFeature.Spies.DummyAuthorizationPolicySpy.CanCreate(A._)).Returns(true); + A.CallTo(() => _dummyServicesFeature.Spies.DummyAuthorizationPolicySpy.CanDelete(A._)).Returns(true); + + await _sut.Invoker.InvokeAsync(async sp=> + { + var repository = sp.GetRequiredService>(); + await repository.AddAsync(new DummyAggregate(1000, "thousand")); + }, new AnonymousIdentity()); + + await _sut.Invoker.InvokeAsync(async sp => + { + var repository = sp.GetRequiredService>(); + var aggregate = await repository.GetAsync(1000); + await repository.DeleteAsync(aggregate); + }, new AnonymousIdentity()); + + await _sut.Invoker.InvokeAsync(async sp => + { + var repository = sp.GetRequiredService>(); + var aggregate = await repository.FindAsync(1000); + Assert.Null(aggregate); + }, new AnonymousIdentity()); + } + + [Fact] + public async Task CanDenyCreate() + { + await ConfigureAndBootApplication(); + + FillStore(); + + A.CallTo(() => _dummyServicesFeature.Spies.DummyAuthorizationPolicySpy.HasAccessExpression).Returns(agg => true); + A.CallTo(() => _dummyServicesFeature.Spies.DummyAuthorizationPolicySpy.CanCreate(A._)).Returns(false); + + await _sut.Invoker.InvokeAsync(async sp => + { + var repository = sp.GetRequiredService>(); + await Assert.ThrowsAsync(async () => + await repository.AddAsync(new DummyAggregate(1000, "thousand"))); + await Assert.ThrowsAsync(async () => await repository.AddRangeAsync( + new[] + { + new DummyAggregate(1001, "thousand and one"), + new DummyAggregate(1002, "thousand and two") + } + )); + }, new AnonymousIdentity()); + } + + [Fact] + public async Task CanDenyDelete() + { + await ConfigureAndBootApplication(); + + FillStore(); + + A.CallTo(() => _dummyServicesFeature.Spies.DummyAuthorizationPolicySpy.HasAccessExpression).Returns(agg => true); + A.CallTo(() => _dummyServicesFeature.Spies.DummyAuthorizationPolicySpy.CanCreate(A._)).Returns(true); + A.CallTo(() => _dummyServicesFeature.Spies.DummyAuthorizationPolicySpy.CanDelete(A._)).Returns(false); + + await _sut.Invoker.InvokeAsync(async sp => + { + var repository = sp.GetRequiredService>(); + await repository.AddAsync(new DummyAggregate(1000, "thousand")); + }, new AnonymousIdentity()); + + await _sut.Invoker.InvokeAsync(async sp => + { + var repository = sp.GetRequiredService>(); + var aggregate = await repository.GetAsync(1000); + await Assert.ThrowsAsync(async () => await repository.DeleteAsync(aggregate)); + }, new AnonymousIdentity()); + } + + private async Task ConfigureAndBootApplication() + { + _sut.EnableFeature(new PersistenceFeature(new InMemoryPersistenceModule())); + _sut.EnableFeature(new AuthorizationFeature()); + await _sut.BootAsync(); + } + + private void FillStore() + { + var inMemoryDatabase = _sut.CompositionRoot.ServiceProvider.GetRequiredService>(); + var dummyAggregateStore = inMemoryDatabase.GetInMemoryStores().For(); + dummyAggregateStore.Add(1, new DummyAggregate(1, "one")); + dummyAggregateStore.Add(2, new DummyAggregate(2, "two")); + dummyAggregateStore.Add(3, new DummyAggregate(3, "three")); + dummyAggregateStore.Add(4, new DummyAggregate(4, "four")); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/Authorization/TheDenyAllPolicy.cs b/tests/Backend.Fx.Tests/Features/Authorization/TheDenyAllPolicy.cs new file mode 100644 index 00000000..f7948520 --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/Authorization/TheDenyAllPolicy.cs @@ -0,0 +1,53 @@ +using System.Linq; +using Backend.Fx.Domain; +using Backend.Fx.Features.Authorization; +using Xunit; + +namespace Backend.Fx.Tests.Features.Authorization; + +public class TheDenyAllPolicy +{ + private class Agg : IAggregateRoot + { + public Agg(int id) + { + Id = id; + } + + public int Id { get; } + } + + private class DenyAllSut : DenyAll {} + + [Fact] + public void AllowsRead() + { + var sut = new DenyAllSut(); + + var aggregates = Enumerable.Range(1, 10).Select(i => new Agg(i)).ToArray(); + + var count = aggregates.Count(agg => sut.HasAccessExpression.Compile().Invoke(agg)); + Assert.Equal(0, count); + } + + [Fact] + public void DeniesCreate() + { + var sut = new DenyAllSut(); + Assert.False(sut.CanCreate(new Agg(1))); + } + + [Fact] + public void DeniesModify() + { + var sut = new DenyAllSut(); + Assert.False(sut.CanModify(new Agg(1))); + } + + [Fact] + public void DeniesDelete() + { + var sut = new DenyAllSut(); + Assert.False(sut.CanDelete(new Agg(1))); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/ConfigurationSettings/TheConfigurationSettingsFeature.cs b/tests/Backend.Fx.Tests/Features/ConfigurationSettings/TheConfigurationSettingsFeature.cs new file mode 100644 index 00000000..fe770e76 --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/ConfigurationSettings/TheConfigurationSettingsFeature.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Backend.Fx.DependencyInjection; +using Backend.Fx.Features.ConfigurationSettings; +using Backend.Fx.Features.ConfigurationSettings.InMem; +using Backend.Fx.Logging; +using Backend.Fx.MicrosoftDependencyInjection; +using Backend.Fx.SimpleInjectorDependencyInjection; +using Backend.Fx.TestUtil; +using FakeItEasy; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using NodaTime; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.Features.ConfigurationSettings; + +[UsedImplicitly] +public class TheConfigurationSettingsFeatureWithSimpleInjector : TheConfigurationSettingsFeature +{ + public TheConfigurationSettingsFeatureWithSimpleInjector(ITestOutputHelper output) + : base(new SimpleInjectorCompositionRoot(), output) + { + } +} + +[UsedImplicitly] +public class TheConfigurationSettingsFeatureWithMicrosoftDI : TheConfigurationSettingsFeature +{ + public TheConfigurationSettingsFeatureWithMicrosoftDI(ITestOutputHelper output) + : base(new MicrosoftCompositionRoot(), output) + { + } +} + +public abstract class TheConfigurationSettingsFeature : TestWithLogging +{ + private readonly IBackendFxApplication _sut; + private readonly IExceptionLogger _exceptionLogger = A.Fake(); + + protected TheConfigurationSettingsFeature(ICompositionRoot compositionRoot, ITestOutputHelper output) : base(output) + { + _sut = new BackendFxApplication(compositionRoot, _exceptionLogger, GetType().Assembly); + _sut.EnableFeature(new ConfigurationSettingsFeature()); + } + + [Fact] + public async Task HasInjectedSettingsSerializerFactory() + { + await _sut.BootAsync(); + var settingSerializerFactory = + _sut.CompositionRoot.ServiceProvider.GetRequiredService(); + Assert.IsType(settingSerializerFactory); + } + + [Fact] + public async Task CanWriteAndReadSettings() + { + await _sut.BootAsync(); + await _sut.Invoker.InvokeAsync(sp => + { + var category = sp.GetRequiredService(); + category.MyIntegerSetting = 42; + category.MyDoubleSetting = 42.42d; + category.MyBooleanSetting = true; + category.MyStringSetting = "Forty two"; + category.MyLocalDateTimeSetting = new LocalDateTime(2000, 1, 2, 3, 4, 5); + category.MyDateTimeSetting = new DateTime(2001, 1, 2, 3, 4, 5); + return Task.CompletedTask; + }, new ClaimsIdentity()); + + Assert.Equal("42",TestSettingsRepository.DummyStore["my"]["MyIntegerSetting"]); + Assert.Equal("42.42",TestSettingsRepository.DummyStore["my"]["MyDoubleSetting"]); + Assert.Equal("True",TestSettingsRepository.DummyStore["my"]["MyBooleanSetting"]); + Assert.Equal("Forty two",TestSettingsRepository.DummyStore["my"]["MyStringSetting"]); + Assert.Equal("2000-01-02T03:04:05.0000000",TestSettingsRepository.DummyStore["my"]["MyLocalDateTimeSetting"]); + Assert.Equal("2001-01-02T03:04:05.0000000",TestSettingsRepository.DummyStore["my"]["MyDateTimeSetting"]); + + await _sut.Invoker.InvokeAsync(sp => + { + var category = sp.GetRequiredService(); + Assert.Equal(42, category.MyIntegerSetting); + Assert.Equal(42.42d, category.MyDoubleSetting); + Assert.True(category.MyBooleanSetting); + Assert.Equal("Forty two", category.MyStringSetting); + Assert.Equal(new LocalDateTime(2000, 1, 2, 3, 4, 5), category.MyLocalDateTimeSetting); + Assert.Equal(new DateTime(2001, 1, 2, 3, 4, 5), category.MyDateTimeSetting); + return Task.CompletedTask; + }, new ClaimsIdentity()); + } + + + [UsedImplicitly] + private class TestSettingsRepository : InMemorySettingRepository + { + public static readonly ConcurrentDictionary> DummyStore = new(); + + protected override ConcurrentDictionary> SettingsStore => DummyStore; + } + + [UsedImplicitly] + public class TestSettingsCategory : SettingsCategory + { + public TestSettingsCategory(ISettingRepository settingRepository, ISettingSerializerFactory settingSerializerFactory) + : base("my", settingRepository, settingSerializerFactory) + { + } + + public int MyIntegerSetting + { + get => ReadSetting(nameof(MyIntegerSetting)) ?? 0; + set => WriteSetting(nameof(MyIntegerSetting), value); + } + + public bool MyBooleanSetting + { + get => ReadSetting(nameof(MyBooleanSetting)) ?? false; + set => WriteSetting(nameof(MyBooleanSetting), value); + } + + public double MyDoubleSetting + { + get => ReadSetting(nameof(MyDoubleSetting)) ?? 0d; + set => WriteSetting(nameof(MyDoubleSetting), value); + } + + public LocalDateTime? MyLocalDateTimeSetting + { + get => ReadSetting(nameof(MyLocalDateTimeSetting)) ?? null; + set => WriteSetting(nameof(MyLocalDateTimeSetting), value); + } + + public DateTime? MyDateTimeSetting + { + get => ReadSetting(nameof(MyDateTimeSetting)) ?? null; + set => WriteSetting(nameof(MyDateTimeSetting), value); + } + + public string MyStringSetting + { + get => ReadSetting(nameof(MyStringSetting)); + set => WriteSetting(nameof(MyStringSetting), value); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/ConfigurationSettings/TheSerializers.cs b/tests/Backend.Fx.Tests/Features/ConfigurationSettings/TheSerializers.cs new file mode 100644 index 00000000..b4f84df5 --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/ConfigurationSettings/TheSerializers.cs @@ -0,0 +1,219 @@ +using System; +using System.Globalization; +using Backend.Fx.Features.ConfigurationSettings.Serializers; +using NodaTime; +using Xunit; + +namespace Backend.Fx.Tests.Features.ConfigurationSettings; + +public class TheSerializers +{ + [Fact] + public void CanSerializeAnnualDate() + { + var data = new AnnualDate(10,20); + var sut = new AnnualDateSerializer(); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de"); + var serialized = sut.Serialize(data); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en"); + Assert.Equal(data, sut.Deserialize(serialized)); + } + + [Fact] + public void CanSerializeBoolean() + { + var data = true; + var sut = new BooleanSerializer(); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de"); + var serialized = sut.Serialize(data); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en"); + Assert.Equal(data, sut.Deserialize(serialized)); + } + + [Fact] + public void CanSerializeDateTimeOffset() + { + var data = DateTimeOffset.UtcNow; + var sut = new DateTimeOffsetSerializer(); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de"); + var serialized = sut.Serialize(data); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en"); + Assert.Equal(data, sut.Deserialize(serialized)); + } + + [Fact] + public void CanSerializeDateTime() + { + var data = DateTime.UtcNow; + var sut = new DateTimeSerializer(); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de"); + var serialized = sut.Serialize(data); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en"); + Assert.Equal(data, sut.Deserialize(serialized)); + } + + [Fact] + public void CanSerializeDecimal() + { + var data = 123.456m; + var sut = new DecimalSerializer(); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de"); + var serialized = sut.Serialize(data); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en"); + Assert.Equal(data, sut.Deserialize(serialized)); + } + + [Fact] + public void CanSerializeDouble() + { + double data = 345.678; + var sut = new DoubleSerializer(); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de"); + var serialized = sut.Serialize(data); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en"); + Assert.Equal(data, sut.Deserialize(serialized)); + } + + [Fact] + public void CanSerializeDuration() + { + var data = Duration.FromMinutes(123456); + var sut = new DurationSerializer(); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de"); + var serialized = sut.Serialize(data); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en"); + Assert.Equal(data, sut.Deserialize(serialized)); + } + + [Fact] + public void CanSerializeFloat() + { + var data = 234.567f; + var sut = new FloatSerializer(); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de"); + var serialized = sut.Serialize(data); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en"); + Assert.Equal(data, sut.Deserialize(serialized)); + } + + [Fact] + public void CanSerializeInstant() + { + var data = SystemClock.Instance.GetCurrentInstant(); + var sut = new InstantSerializer(); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de"); + var serialized = sut.Serialize(data); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en"); + Assert.Equal(data, sut.Deserialize(serialized)); + } + + [Fact] + public void CanSerializeInteger() + { + var data = 456787; + var sut = new IntegerSerializer(); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de"); + var serialized = sut.Serialize(data); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en"); + Assert.Equal(data, sut.Deserialize(serialized)); + } + + [Fact] + public void CanSerializeLocalDate() + { + var data = new LocalDate(2022,10,20); + var sut = new LocalDateSerializer(); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de"); + var serialized = sut.Serialize(data); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en"); + Assert.Equal(data, sut.Deserialize(serialized)); + } + + [Fact] + public void CanSerializeLocalDateTime() + { + var data = new LocalDateTime(2001,12,22,12,34,55,456); + var sut = new LocalDateTimeSerializer(); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de"); + var serialized = sut.Serialize(data); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en"); + Assert.Equal(data, sut.Deserialize(serialized)); + } + + [Fact] + public void CanSerializeLocalTime() + { + var data = new LocalTime(10,20, 33); + var sut = new LocalTimeSerializer(); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de"); + var serialized = sut.Serialize(data); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en"); + Assert.Equal(data, sut.Deserialize(serialized)); + } + + [Fact] + public void CanSerializeLong() + { + var data = 325234562364345L; + var sut = new LongSerializer(); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de"); + var serialized = sut.Serialize(data); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en"); + Assert.Equal(data, sut.Deserialize(serialized)); + } + + [Fact] + public void CanSerializeOffsetDate() + { + var data = new OffsetDate(new LocalDate(2000,3,4), Offset.FromHours(-3)); + var sut = new OffsetDateSerializer(); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de"); + var serialized = sut.Serialize(data); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en"); + Assert.Equal(data, sut.Deserialize(serialized)); + } + + [Fact] + public void CanSerializeOffset() + { + var data = Offset.FromHoursAndMinutes(1,30); + var sut = new OffsetSerializer(); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de"); + var serialized = sut.Serialize(data); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en"); + Assert.Equal(data, sut.Deserialize(serialized)); + } + + [Fact] + public void CanSerializeOffsetTime() + { + var data = new OffsetTime(new LocalTime(10,33), Offset.FromHours(3)); + var sut = new OffsetTimeSerializer(); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de"); + var serialized = sut.Serialize(data); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en"); + Assert.Equal(data, sut.Deserialize(serialized)); + } + + [Fact] + public void CanSerializeShort() + { + short data = 333; + var sut = new ShortSerializer(); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de"); + var serialized = sut.Serialize(data); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en"); + Assert.Equal(data, sut.Deserialize(serialized)); + } + + [Fact] + public void CanSerializeTimeSpan() + { + var data = TimeSpan.FromMilliseconds(123235); + var sut = new TimeSpanSerializer(); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de"); + var serialized = sut.Serialize(data); + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en"); + Assert.Equal(data, sut.Deserialize(serialized)); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/DataGeneration/TheDataGenerationFeature.cs b/tests/Backend.Fx.Tests/Features/DataGeneration/TheDataGenerationFeature.cs new file mode 100644 index 00000000..816ede97 --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/DataGeneration/TheDataGenerationFeature.cs @@ -0,0 +1,109 @@ +using System.Linq; +using System.Threading.Tasks; +using Backend.Fx.DependencyInjection; +using Backend.Fx.Features.DataGeneration; +using Backend.Fx.Logging; +using Backend.Fx.MicrosoftDependencyInjection; +using Backend.Fx.SimpleInjectorDependencyInjection; +using Backend.Fx.Tests.DummyServices; +using Backend.Fx.TestUtil; +using FakeItEasy; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.Features.DataGeneration; + +public class TheDataGenerationFeatureWithSimpleInjector : TheDataGenerationFeature +{ + public TheDataGenerationFeatureWithSimpleInjector(ITestOutputHelper output) + : base(new SimpleInjectorCompositionRoot(), output) + { + } +} + +public class TheDataGenerationFeatureWithMicrosoftDI : TheDataGenerationFeature +{ + public TheDataGenerationFeatureWithMicrosoftDI(ITestOutputHelper output) + : base(new MicrosoftCompositionRoot(), output) + { + } +} + +public abstract class TheDataGenerationFeature : TestWithLogging +{ + private readonly IBackendFxApplication _sut; + private readonly IExceptionLogger _exceptionLogger = A.Fake(); + private readonly DummyServicesFeature _dummyServicesFeature = new(); + + protected TheDataGenerationFeature(ICompositionRoot compositionRoot, ITestOutputHelper output) : base(output) + { + _sut = new BackendFxApplication(compositionRoot, _exceptionLogger, GetType().Assembly); + } + + [Fact] + public async Task HasInjectedDataGenerationContextAndGenerators() + { + _sut.EnableFeature(new DataGenerationFeature()); + _sut.EnableFeature(_dummyServicesFeature); + + await _sut.BootAsync(); + var dataGenerationContext = _sut.CompositionRoot.ServiceProvider.GetRequiredService(); + Assert.IsType(dataGenerationContext); + + var dataGeneratorTypes = await dataGenerationContext.GetDataGeneratorTypesAsync(_sut.Invoker); + Assert.Contains(dataGeneratorTypes, dgt => typeof(IProductiveDataGenerator).IsAssignableFrom(dgt)); + Assert.Contains(dataGeneratorTypes, dgt => typeof(IDemoDataGenerator).IsAssignableFrom(dgt)); + } + + [Fact] + public async Task OnlyInjectsProductiveDataGenerationTypesWhenNoDemoDataGenerationIsAllowed() + { + _sut.EnableFeature(new DataGenerationFeature(allowDemoDataGeneration: false)); + _sut.EnableFeature(_dummyServicesFeature); + + await _sut.BootAsync(); + var dataGenerationContext = _sut.CompositionRoot.ServiceProvider.GetRequiredService(); + var dataGeneratorTypes = await dataGenerationContext.GetDataGeneratorTypesAsync(_sut.Invoker); + Assert.True(dataGeneratorTypes.All(dgt => typeof(IProductiveDataGenerator).IsAssignableFrom(dgt))); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task GeneratesDataOnBoot(bool allowDemoDataGeneration) + { + _sut.EnableFeature(new DataGenerationFeature(allowDemoDataGeneration)); + _sut.EnableFeature(_dummyServicesFeature); + + await _sut.BootAsync(); + + Assert.Equal(1, _dummyServicesFeature.Spies.DummyProductiveDataGeneratorSpy.InvocationCount); + Assert.Equal(allowDemoDataGeneration ? 1 : 0, + _dummyServicesFeature.Spies.DummyDemoDataGeneratorSpy.InvocationCount); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task DataGeneratorsDoNotRunWhenShouldRunReturnsFalse(bool allowDemoDataGeneration) + { + _dummyServicesFeature.Spies.DummyDemoDataGeneratorSpy.ShouldRun = false; + _dummyServicesFeature.Spies.DummyProductiveDataGeneratorSpy.ShouldRun = false; + + _sut.EnableFeature(new DataGenerationFeature(allowDemoDataGeneration)); + _sut.EnableFeature(_dummyServicesFeature); + await _sut.BootAsync(); + + Assert.Equal(0, _dummyServicesFeature.Spies.DummyDemoDataGeneratorSpy.InvocationCount); + Assert.Equal(0, _dummyServicesFeature.Spies.DummyProductiveDataGeneratorSpy.InvocationCount); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _sut.Dispose(); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/DataGeneration/TheDataGenerationFeatureWithMultiTenancy.cs b/tests/Backend.Fx.Tests/Features/DataGeneration/TheDataGenerationFeatureWithMultiTenancy.cs new file mode 100644 index 00000000..2dfa6c8c --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/DataGeneration/TheDataGenerationFeatureWithMultiTenancy.cs @@ -0,0 +1,129 @@ +using System.Linq; +using System.Threading.Tasks; +using Backend.Fx.DependencyInjection; +using Backend.Fx.Features.DataGeneration; +using Backend.Fx.Features.MultiTenancy; +using Backend.Fx.Features.MultiTenancy.InProc; +using Backend.Fx.Features.MultiTenancyAdmin; +using Backend.Fx.Features.MultiTenancyAdmin.InMem; +using Backend.Fx.Logging; +using Backend.Fx.MicrosoftDependencyInjection; +using Backend.Fx.SimpleInjectorDependencyInjection; +using Backend.Fx.Tests.DummyServices; +using Backend.Fx.TestUtil; +using FakeItEasy; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.Features.DataGeneration; + +[UsedImplicitly] +public class TheDataGenerationFeatureWithMultiTenancyWithSimpleInjector : TheDataGenerationFeatureWithMultiTenancy +{ + public TheDataGenerationFeatureWithMultiTenancyWithSimpleInjector(ITestOutputHelper output) + : base(new SimpleInjectorCompositionRoot(), output) + { + } +} + +[UsedImplicitly] +public class TheDataGenerationFeatureWithMultiTenancyWithMicrosoftDI : TheDataGenerationFeatureWithMultiTenancy +{ + public TheDataGenerationFeatureWithMultiTenancyWithMicrosoftDI(ITestOutputHelper output) + : base(new MicrosoftCompositionRoot(), output) + { + } +} + +public abstract class TheDataGenerationFeatureWithMultiTenancy : TestWithLogging +{ + private readonly InMemoryTenantRepository _tenantRepository = new(); + private readonly IBackendFxApplication _sut; + private readonly IExceptionLogger _exceptionLogger = A.Fake(); + + protected TheDataGenerationFeatureWithMultiTenancy(ICompositionRoot compositionRoot, ITestOutputHelper output) : + base(output) + { + _tenantRepository.SaveTenant(new Tenant(1, "t1", "tenant 1", false)); + _tenantRepository.SaveTenant(new Tenant(2, "t2", "tenant 2", true)); + + _sut = new MultiTenancyBackendFxApplication( + compositionRoot, + _exceptionLogger, + new DirectTenantEnumerator(_tenantRepository), + new InProcTenantWideMutexManager(), + GetType().Assembly); + } + + [Fact] + public async Task HasInjectedDataGenerationContextAndGenerators() + { + _sut.EnableFeature(new DataGenerationFeature()); + var demoSpy = new DummyDemoDataGeneratorSpy(); + var prodSpy = new DummyProductiveDataGeneratorSpy(); + _sut.CompositionRoot.Register(ServiceDescriptor.Singleton(demoSpy)); + _sut.CompositionRoot.Register(ServiceDescriptor.Singleton(prodSpy)); + await _sut.BootAsync(); + + var dataGenerationContext = _sut.CompositionRoot.ServiceProvider.GetRequiredService(); + Assert.IsType(dataGenerationContext); + + var dataGeneratorTypes = await dataGenerationContext.GetDataGeneratorTypesAsync(_sut.Invoker); + Assert.Contains(dataGeneratorTypes, dgt => typeof(IProductiveDataGenerator).IsAssignableFrom(dgt)); + Assert.Contains(dataGeneratorTypes, dgt => typeof(IDemoDataGenerator).IsAssignableFrom(dgt)); + } + + [Fact] + public async Task OnlyInjectsProductiveDataGenerationTypesWhenNoDemoDataGenerationIsAllowed() + { + _sut.EnableFeature(new DataGenerationFeature(allowDemoDataGeneration: false)); + var demoSpy = new DummyDemoDataGeneratorSpy(); + var prodSpy = new DummyProductiveDataGeneratorSpy(); + _sut.CompositionRoot.Register(ServiceDescriptor.Singleton(demoSpy)); + _sut.CompositionRoot.Register(ServiceDescriptor.Singleton(prodSpy)); + await _sut.BootAsync(); + var dataGenerationContext = _sut.CompositionRoot.ServiceProvider.GetRequiredService(); + var dataGeneratorTypes = await dataGenerationContext.GetDataGeneratorTypesAsync(_sut.Invoker); + Assert.True(dataGeneratorTypes.All(dgt => typeof(IProductiveDataGenerator).IsAssignableFrom(dgt))); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task GeneratesDataOnBoot(bool allowDemoDataGeneration) + { + _sut.EnableFeature(new DataGenerationFeature(allowDemoDataGeneration)); + var demoSpy = new DummyDemoDataGeneratorSpy(); + var prodSpy = new DummyProductiveDataGeneratorSpy(); + + demoSpy.ShouldRun = false; + demoSpy.ShouldRun = true; + + _sut.CompositionRoot.Register(ServiceDescriptor.Singleton(demoSpy)); + _sut.CompositionRoot.Register(ServiceDescriptor.Singleton(prodSpy)); + await _sut.BootAsync(); + + Assert.Equal(2, prodSpy.InvocationCount); + Assert.Equal(allowDemoDataGeneration ? 1 : 0, demoSpy.InvocationCount); + } + + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _sut.Dispose(); + } + } + + [UsedImplicitly] + private class DummyTenantIdSelector : ICurrentTenantIdSelector + { + public TenantId GetCurrentTenantId() + { + return new TenantId(1000); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/DomainEvents/TheDomainEventsFeature.cs b/tests/Backend.Fx.Tests/Features/DomainEvents/TheDomainEventsFeature.cs new file mode 100644 index 00000000..d1a669ce --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/DomainEvents/TheDomainEventsFeature.cs @@ -0,0 +1,72 @@ +using System.Threading.Tasks; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Features.DomainEvents; +using Backend.Fx.Logging; +using Backend.Fx.MicrosoftDependencyInjection; +using Backend.Fx.Tests.DummyServices; +using Backend.Fx.TestUtil; +using FakeItEasy; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.Features.DomainEvents; + +public class TheDomainEventsFeature : TestWithLogging +{ + private readonly IBackendFxApplication _sut; + private readonly IExceptionLogger _exceptionLogger = A.Fake(); + private readonly IDummyDomainEventHandlerSpy _spy = A.Fake(); + + public TheDomainEventsFeature(ITestOutputHelper output) : base(output) + { + _sut = new BackendFxApplication(new MicrosoftCompositionRoot(), _exceptionLogger, GetType().Assembly); + _sut.EnableFeature(new DomainEventsFeature()); + _sut.CompositionRoot.Register(ServiceDescriptor.Singleton(_spy)); + } + + [Fact] + public async Task InjectsDomainEventAggregator() + { + await _sut.BootAsync(); + await _sut.Invoker.InvokeAsync(sp => + { + var domainEventAggregator = sp.GetRequiredService(); + Assert.IsType(domainEventAggregator); + return Task.CompletedTask; + }, new AnonymousIdentity()); + } + + [Fact] + public async Task InjectsDomainEventHandlers() + { + await _sut.BootAsync(); + + var dummyDomainEvent = new DummyDomainEvent(); + + await _sut.Invoker.InvokeAsync(sp => + { + var domainEventHandler = sp.GetRequiredService>(); + domainEventHandler.HandleAsync(dummyDomainEvent); + return Task.CompletedTask; + }, new AnonymousIdentity()); + + A.CallTo(() => _spy.Handle(A.That.IsSameAs(dummyDomainEvent))).MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task RaisesAndHandlesDomainEvents() + { + await _sut.BootAsync(); + + var dummyDomainEvent = new DummyDomainEvent(); + await _sut.Invoker.InvokeAsync(sp => + { + var domainEventAggregator = sp.GetRequiredService(); + domainEventAggregator.PublishDomainEvent(dummyDomainEvent); + return Task.CompletedTask; + }, new AnonymousIdentity()); + + A.CallTo(() => _spy.Handle(A.That.IsSameAs(dummyDomainEvent))).MustHaveHappenedOnceExactly(); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/DomainServices/TheDomainServiceFeature.cs b/tests/Backend.Fx.Tests/Features/DomainServices/TheDomainServiceFeature.cs new file mode 100644 index 00000000..70b3b3de --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/DomainServices/TheDomainServiceFeature.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Features.DomainServices; +using Backend.Fx.Logging; +using Backend.Fx.MicrosoftDependencyInjection; +using Backend.Fx.Tests.DummyServices; +using Backend.Fx.TestUtil; +using FakeItEasy; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.Features.DomainServices; + +public class TheDomainServiceFeature : TestWithLogging +{ + private readonly IBackendFxApplication _sut; + private readonly IExceptionLogger _exceptionLogger = A.Fake(); + + public TheDomainServiceFeature(ITestOutputHelper output) : base(output) + { + _sut = new BackendFxApplication(new MicrosoftCompositionRoot(), _exceptionLogger, GetType().Assembly); + _sut.EnableFeature(new DomainServicesFeature()); + } + + [Fact] + public async Task InjectsDomainServices() + { + await _sut.BootAsync(); + await _sut.Invoker.InvokeAsync(sp => + { + var domainService = sp.GetRequiredService(); + Assert.IsType(domainService); + Assert.Equal(DummyDomainService.Message, domainService.SayHelloToDomain()); + return Task.CompletedTask; + }, new AnonymousIdentity()); + } + + [Fact] + public async Task InjectsApplicationServices() + { + await _sut.BootAsync(); + await _sut.Invoker.InvokeAsync(sp => + { + var applicationService = sp.GetRequiredService(); + Assert.IsType(applicationService); + Assert.Equal(DummyApplicationService.Message, applicationService.SayHelloToApplication()); + return Task.CompletedTask; + }, new AnonymousIdentity()); + } + + [Fact] + public async Task DoesNotProvideSingletonServices() + { + await _sut.BootAsync(); + Assert.Throws(() => + _sut.CompositionRoot.ServiceProvider.GetRequiredService()); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/IdGeneration/TheHiLoIdGenerator.cs b/tests/Backend.Fx.Tests/Features/IdGeneration/TheHiLoIdGenerator.cs new file mode 100644 index 00000000..9fb93417 --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/IdGeneration/TheHiLoIdGenerator.cs @@ -0,0 +1,53 @@ +using System; +using System.Linq; +using Backend.Fx.Features.IdGeneration; +using Backend.Fx.Features.IdGeneration.InMem; +using Backend.Fx.TestUtil; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.Features.IdGeneration +{ + public class TheHiLoIdGenerator : TestWithLogging + { + private readonly HiLoIdGenerator _sut = new SequenceHiLoIntIdGenerator(new InMemorySequence(100)); + + private class IdConsumer + { + public int[] Ids { get; private set; } = Array.Empty(); + + public void GetIds(int count, IIdGenerator idGenerator) + { + Ids = new int[count]; + for (var i = 0; i < count; i++) Ids[i] = idGenerator.NextId(); + } + } + + [Fact] + public void AllowsMultipleThreadsToGetIds() + { + const int consumerCount = 50; + const int idCountPerConsumer = 1000; + var idConsumers = new IdConsumer[consumerCount]; + + for (var i = 0; i < consumerCount; i++) idConsumers[i] = new IdConsumer(); + + idConsumers.AsParallel().ForAll(idConsumer => { idConsumer.GetIds(idCountPerConsumer, _sut); }); + + var allIds = idConsumers.SelectMany(idConsumer => idConsumer.Ids).ToArray(); + Assert.Equal(consumerCount * idCountPerConsumer, allIds.Length); + Assert.Equal(consumerCount * idCountPerConsumer, allIds.Distinct().Count()); + Assert.Equal(allIds.Max() + 1, _sut.NextId()); + } + + [Fact] + public void StartsWithInitialValueAndCountsUp() + { + for (var i = 1; i < 1000; i++) Assert.Equal(i, _sut.NextId()); + } + + public TheHiLoIdGenerator(ITestOutputHelper output) : base(output) + { + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/IdGeneration/TheIdGenerationFeature.cs b/tests/Backend.Fx.Tests/Features/IdGeneration/TheIdGenerationFeature.cs new file mode 100644 index 00000000..d5005e23 --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/IdGeneration/TheIdGenerationFeature.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Features.IdGeneration; +using Backend.Fx.Logging; +using Backend.Fx.SimpleInjectorDependencyInjection; +using Backend.Fx.Tests.DummyServices; +using Backend.Fx.TestUtil; +using FakeItEasy; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.Features.IdGeneration; + +public class TheIdGenerationFeature : TestWithLogging +{ + private readonly DummyServicesFeature _dummyServicesFeature = new(); + private readonly IBackendFxApplication _sut; + private readonly IExceptionLogger _exceptionLogger = A.Fake(); + + public TheIdGenerationFeature(ITestOutputHelper output) : base(output) + { + _sut = new BackendFxApplication(new SimpleInjectorCompositionRoot(), _exceptionLogger, GetType().Assembly); + _sut.EnableFeature(_dummyServicesFeature); + _sut.EnableFeature(new IdGenerationFeature(_dummyServicesFeature.Spies.EntityIdGenerator)); + } + + [Fact] + public async Task InjectsTheSingletonEntityIdGenerator() + { + await _sut.BootAsync(); + var entityIdGenerator = _sut.CompositionRoot.ServiceProvider.GetRequiredService>(); + Assert.StrictEqual(_dummyServicesFeature.Spies.EntityIdGenerator, entityIdGenerator); + + await _sut.Invoker.InvokeAsync(sp => + { + entityIdGenerator = sp.GetRequiredService>(); + Assert.StrictEqual(_dummyServicesFeature.Spies.EntityIdGenerator, entityIdGenerator); + return Task.CompletedTask; + }, new AnonymousIdentity()); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/IdGeneration/TheSequenceHiLoIdGenerator.cs b/tests/Backend.Fx.Tests/Features/IdGeneration/TheSequenceHiLoIdGenerator.cs similarity index 65% rename from tests/Backend.Fx.Tests/Patterns/IdGeneration/TheSequenceHiLoIdGenerator.cs rename to tests/Backend.Fx.Tests/Features/IdGeneration/TheSequenceHiLoIdGenerator.cs index 49a4f55f..1b2c9533 100644 --- a/tests/Backend.Fx.Tests/Patterns/IdGeneration/TheSequenceHiLoIdGenerator.cs +++ b/tests/Backend.Fx.Tests/Features/IdGeneration/TheSequenceHiLoIdGenerator.cs @@ -1,9 +1,10 @@ -using Backend.Fx.Patterns.IdGeneration; +using Backend.Fx.Features.IdGeneration; +using Backend.Fx.TestUtil; using FakeItEasy; using Xunit; using Xunit.Abstractions; -namespace Backend.Fx.Tests.Patterns.IdGeneration +namespace Backend.Fx.Tests.Features.IdGeneration { public class TheSequenceHiLoIdGenerator : TestWithLogging { @@ -13,12 +14,12 @@ public TheSequenceHiLoIdGenerator(ITestOutputHelper output): base(output) _sut = new TestIdGenerator(_sequence); } - private readonly ISequence _sequence = A.Fake(); - private readonly SequenceHiLoIdGenerator _sut; + private readonly ISequence _sequence = A.Fake>(); + private readonly SequenceHiLoIntIdGenerator _sut; - private class TestIdGenerator : SequenceHiLoIdGenerator + private class TestIdGenerator : SequenceHiLoIntIdGenerator { - public TestIdGenerator(ISequence sequence) : base(sequence) + public TestIdGenerator(ISequence sequence) : base(sequence) { } } diff --git a/tests/Backend.Fx.Tests/Features/IdGeneration/TheSequenceIdGenerator.cs b/tests/Backend.Fx.Tests/Features/IdGeneration/TheSequenceIdGenerator.cs new file mode 100644 index 00000000..444bdf5b --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/IdGeneration/TheSequenceIdGenerator.cs @@ -0,0 +1,53 @@ +using System; +using System.Linq; +using Backend.Fx.Features.IdGeneration; +using Backend.Fx.Features.IdGeneration.InMem; +using Backend.Fx.TestUtil; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.Features.IdGeneration +{ + public class TheSequenceIdGenerator : TestWithLogging + { + private readonly IIdGenerator _sut = new SequenceIdGenerator(new InMemorySequence()); + + public TheSequenceIdGenerator(ITestOutputHelper output) : base(output) + { + } + + private class IdConsumer + { + public int[] Ids { get; private set; } = Array.Empty(); + + public void GetIds(int count, IIdGenerator idGenerator) + { + Ids = new int[count]; + for (var i = 0; i < count; i++) Ids[i] = idGenerator.NextId(); + } + } + + [Fact] + public void AllowsMultipleThreadsToGetIds() + { + const int consumerCount = 50; + const int idCountPerConsumer = 1000; + var idConsumers = new IdConsumer[consumerCount]; + + for (var i = 0; i < consumerCount; i++) idConsumers[i] = new IdConsumer(); + + idConsumers.AsParallel().ForAll(idConsumer => { idConsumer.GetIds(idCountPerConsumer, _sut); }); + + var allIds = idConsumers.SelectMany(idConsumer => idConsumer.Ids).ToArray(); + Assert.Equal(consumerCount * idCountPerConsumer, allIds.Length); + Assert.Equal(consumerCount * idCountPerConsumer, allIds.Distinct().Count()); + Assert.Equal(allIds.Max() + 1, _sut.NextId()); + } + + [Fact] + public void StartsWithInitialValueAndCountsUp() + { + for (var i = 1; i < 1000; i++) Assert.Equal(i, _sut.NextId()); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/IdGeneration/UuidGenerator.cs b/tests/Backend.Fx.Tests/Features/IdGeneration/UuidGenerator.cs new file mode 100644 index 00000000..5cc4cb1a --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/IdGeneration/UuidGenerator.cs @@ -0,0 +1,12 @@ +using System; +using Backend.Fx.Features.IdGeneration; + +namespace Backend.Fx.Tests.Features.IdGeneration; + +public class UuidGenerator : IIdGenerator +{ + public Guid NextId() + { + return Guid.NewGuid(); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/Jobs/TheJobsFeature.cs b/tests/Backend.Fx.Tests/Features/Jobs/TheJobsFeature.cs new file mode 100644 index 00000000..df15f44c --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/Jobs/TheJobsFeature.cs @@ -0,0 +1,44 @@ +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Features.Jobs; +using Backend.Fx.Logging; +using Backend.Fx.MicrosoftDependencyInjection; +using Backend.Fx.Tests.DummyServices; +using Backend.Fx.TestUtil; +using FakeItEasy; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.Features.Jobs; + +public class TheJobsFeature : TestWithLogging +{ + private readonly IBackendFxApplication _sut; + private readonly IExceptionLogger _exceptionLogger = A.Fake(); + private readonly DummyServicesFeature _dummyServicesFeature = new (); + + public TheJobsFeature(ITestOutputHelper output) : base(output) + { + _sut = new BackendFxApplication(new MicrosoftCompositionRoot(), _exceptionLogger, GetType().Assembly); + _sut.EnableFeature(_dummyServicesFeature); + } + + [Fact] + public async Task HasInjectedJobExecutor() + { + _sut.EnableFeature(new JobsFeature()); + await _sut.BootAsync(); + var jobExecutor = _sut.CompositionRoot.ServiceProvider.GetRequiredService(); + Assert.IsType(jobExecutor); + } + + [Fact] + public async Task CanExecuteJob() + { + _sut.EnableFeature(new JobsFeature()); + await _sut.BootAsync(); + await _sut.ExecuteJob(); + A.CallTo(() => _dummyServicesFeature.Spies.DummyJobSpy.RunAsync(A._)).MustHaveHappenedOnceExactly(); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/Jobs/TheJobsFeatureWithMultiTenancy.cs b/tests/Backend.Fx.Tests/Features/Jobs/TheJobsFeatureWithMultiTenancy.cs new file mode 100644 index 00000000..45089b3f --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/Jobs/TheJobsFeatureWithMultiTenancy.cs @@ -0,0 +1,66 @@ +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Features.Jobs; +using Backend.Fx.Features.MultiTenancy; +using Backend.Fx.Features.MultiTenancy.InProc; +using Backend.Fx.Features.MultiTenancyAdmin; +using Backend.Fx.Features.MultiTenancyAdmin.InMem; +using Backend.Fx.Logging; +using Backend.Fx.MicrosoftDependencyInjection; +using Backend.Fx.Tests.DummyServices; +using Backend.Fx.TestUtil; +using FakeItEasy; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.Features.Jobs; + +public class TheJobsFeatureWithMultiTenancy : TestWithLogging +{ + private readonly InMemoryTenantRepository _tenantRepository = new(); + + private readonly IBackendFxApplication _sut; + private readonly IExceptionLogger _exceptionLogger = A.Fake(); + private readonly DummyServicesFeature _dummyServicesFeature = new (); + + public TheJobsFeatureWithMultiTenancy(ITestOutputHelper output) : base(output) + { + _tenantRepository.SaveTenant(new Tenant(1, "t1", "tenant 1", false)); + _tenantRepository.SaveTenant(new Tenant(2, "t2", "tenant 2", false)); + + _sut = new MultiTenancyBackendFxApplication(new MicrosoftCompositionRoot(), + _exceptionLogger, + new DirectTenantEnumerator(_tenantRepository), new InProcTenantWideMutexManager(), GetType().Assembly); + + _sut.EnableFeature(_dummyServicesFeature); + } + + [Fact] + public async Task HasInjectedJobExecutor() + { + _sut.EnableFeature(new JobsFeature()); + await _sut.BootAsync(); + var jobExecutor = _sut.CompositionRoot.ServiceProvider.GetRequiredService(); + Assert.IsType(jobExecutor); + } + + [Fact] + public async Task CanExecuteJobForEachTenant() + { + _sut.EnableFeature(new JobsFeature()); + await _sut.BootAsync(); + await _sut.ExecuteJob(); + A.CallTo(() => _dummyServicesFeature.Spies.DummyJobSpy.RunAsync(A._)).MustHaveHappened(2, Times.Exactly); + } + + [UsedImplicitly] + private class DummyTenantIdSelector : ICurrentTenantIdSelector + { + public TenantId GetCurrentTenantId() + { + return new TenantId(1000); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/MessageBus/InProc/TheInProcMessageBusChannel.cs b/tests/Backend.Fx.Tests/Features/MessageBus/InProc/TheInProcMessageBusChannel.cs new file mode 100644 index 00000000..fcdeaf36 --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/MessageBus/InProc/TheInProcMessageBusChannel.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading.Tasks; +using Backend.Fx.Features.MessageBus; +using Backend.Fx.Features.MessageBus.InProc; +using Backend.Fx.Logging; +using Backend.Fx.SimpleInjectorDependencyInjection; +using Backend.Fx.Tests.DummyServices; +using Backend.Fx.TestUtil; +using FakeItEasy; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.Features.MessageBus.InProc; + +public class TheInProcMessageBusChannel :TestWithLogging +{ + private readonly IExceptionLogger _exceptionLogger = A.Fake(); + + public TheInProcMessageBusChannel(ITestOutputHelper output) : base(output) + { } + + [Fact] + public async Task CanBeUsedToCommunicateBetweenTwoApplications() + { + var sut = new InProcMessageBusChannel(); + + // note that app1 does not scan any assemblies for handlers + var app1 = new BackendFxApplication(new SimpleInjectorCompositionRoot(), _exceptionLogger); + var bus1 = new InProcMessageBus(sut); + app1.EnableFeature(new MessageBusFeature(bus1)); + await app1.BootAsync(); + + // app2 will find the DummyIntegrationEventHandler type and makes a subscription + var app2 = new BackendFxApplication(new SimpleInjectorCompositionRoot(), _exceptionLogger, GetType().Assembly); + var spyInApp2 = A.Fake(); + app2.CompositionRoot.Register(ServiceDescriptor.Singleton(spyInApp2)); + var bus2 = new InProcMessageBus(sut); + app2.EnableFeature(new MessageBusFeature(bus2)); + await app2.BootAsync(); + + await bus1.PublishAsync(new DummyIntegrationEvent()); + + // the PublishAsync task awaits for completion of the publish operation, not the handling of the event. + // instead we have to await the channel to finish its work: + await sut.FinishHandlingAllMessagesAsync(); + + A.CallTo(() => _exceptionLogger.LogException(A._)).MustNotHaveHappened(); + A.CallTo(() => spyInApp2.HandleAsync(A._)).MustHaveHappenedOnceExactly(); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/MessageBus/TheIntegrationEventMessageMessageSerializer.cs b/tests/Backend.Fx.Tests/Features/MessageBus/TheIntegrationEventMessageMessageSerializer.cs new file mode 100644 index 00000000..b7fdb426 --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/MessageBus/TheIntegrationEventMessageMessageSerializer.cs @@ -0,0 +1,36 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Backend.Fx.Features.MessageBus; +using Backend.Fx.Tests.DummyServices; +using Backend.Fx.TestUtil; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.Features.MessageBus; + +public class TheIntegrationEventMessageMessageSerializer : TestWithLogging +{ + private readonly IIntegrationEventMessageSerializer _sut = new IntegrationEventMessageMessageSerializer(new []{ typeof(DummyIntegrationEvent)}); + + public TheIntegrationEventMessageMessageSerializer(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task CanRoundtrip() + { + var data = new DummyIntegrationEvent(); + var somePropValue = Guid.NewGuid().ToString(); + data.Properties["SomeProp"] = somePropValue; + SerializedMessage serialized = await _sut.SerializeAsync(data); + + await File.WriteAllBytesAsync("message.json", serialized.MessagePayload); + + IIntegrationEvent deserialized = await _sut.DeserializeAsync(serialized); + Assert.NotNull(deserialized); + Assert.Equal(data.Id, deserialized.Id); + Assert.True(data.Properties.ContainsKey("SomeProp")); + Assert.Equal(somePropValue, data.Properties["SomeProp"]); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/MessageBus/TheMessageBusFeature.cs b/tests/Backend.Fx.Tests/Features/MessageBus/TheMessageBusFeature.cs new file mode 100644 index 00000000..0a06144a --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/MessageBus/TheMessageBusFeature.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Features.MessageBus; +using Backend.Fx.Features.MessageBus.InProc; +using Backend.Fx.Logging; +using Backend.Fx.SimpleInjectorDependencyInjection; +using Backend.Fx.Tests.DummyServices; +using Backend.Fx.TestUtil; +using Backend.Fx.Util; +using FakeItEasy; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.Features.MessageBus; + +public class TheMessageBusFeature : TestWithLogging +{ + private readonly IBackendFxApplication _sender; + private readonly IBackendFxApplication _recipient; + private readonly IExceptionLogger _exceptionLogger = A.Fake(); + private readonly DummyServicesFeature _dummyServicesFeature = new(); + private readonly InProcMessageBusChannel _inProcMessageBusChannel = new(); + + public TheMessageBusFeature(ITestOutputHelper output) : base(output) + { + _sender = new BackendFxApplication(new SimpleInjectorCompositionRoot(), _exceptionLogger); + _sender.EnableFeature(new MessageBusFeature(new InProcMessageBus(_inProcMessageBusChannel))); + _sender.EnableFeature(new DummyServicesFeature()); + + _recipient = + new BackendFxApplication(new SimpleInjectorCompositionRoot(), _exceptionLogger, GetType().Assembly); + _recipient.EnableFeature(new MessageBusFeature(new InProcMessageBus(_inProcMessageBusChannel))); + _recipient.EnableFeature(_dummyServicesFeature); + } + + [Fact] + public async Task InjectsMessageBusScope() + { + await _sender.BootAsync(); + + await _sender.Invoker.InvokeAsync(sp => + { + var messageBusScope = sp.GetRequiredService(); + Assert.NotNull(messageBusScope); + return Task.CompletedTask; + }); + } + + [Fact] + public async Task InjectsHandlersAsCollections() + { + await _recipient.BootAsync(); + await _recipient.Invoker.InvokeAsync(sp => + { + var handlers = sp.GetRequiredService>>().ToArray(); + Assert.NotNull(handlers); + Assert.IsType(Assert.Single(handlers)); + return Task.CompletedTask; + }); + } + + [Fact] + public async Task PublishesAndHandlesEvents() + { + // we have two applications + await _sender.BootAsync(); + await _recipient.BootAsync(); + + // we send an integration event on the first app + var dummyIntegrationEvent = new DummyIntegrationEvent(); + var sendingCorrelationId = Guid.Empty; + await _sender.Invoker.InvokeAsync(sp => + { + sendingCorrelationId = sp.GetRequiredService>().Current.Id; + + var messageBusScope = sp.GetRequiredService(); + messageBusScope.Publish(dummyIntegrationEvent); + return Task.CompletedTask; + }); + + // wait that the async processing finishes + await _inProcMessageBusChannel.FinishHandlingAllMessagesAsync(); + + // assert that the second application received the message and handled the event + A.CallTo(() => + _dummyServicesFeature.Spies.DummyIntegrationEventHandlerSpy.HandleAsync( + A.That.Matches(ev => ev.Id == dummyIntegrationEvent.Id && sendingCorrelationId == dummyIntegrationEvent.CorrelationId))) + .MustHaveHappenedOnceExactly(); + + Assert.NotEqual(Guid.Empty, sendingCorrelationId); + Assert.NotEqual(Guid.Empty, dummyIntegrationEvent.Id); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _sender.Dispose(); + _recipient.Dispose(); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/MessageBus/TheMessageBusFeatureWithMultiTenancy.cs b/tests/Backend.Fx.Tests/Features/MessageBus/TheMessageBusFeatureWithMultiTenancy.cs new file mode 100644 index 00000000..61f8d118 --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/MessageBus/TheMessageBusFeatureWithMultiTenancy.cs @@ -0,0 +1,132 @@ +using System; +using System.Threading.Tasks; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Features.MessageBus; +using Backend.Fx.Features.MessageBus.InProc; +using Backend.Fx.Features.MultiTenancy; +using Backend.Fx.Features.MultiTenancy.InProc; +using Backend.Fx.Features.MultiTenancyAdmin; +using Backend.Fx.Features.MultiTenancyAdmin.InMem; +using Backend.Fx.Logging; +using Backend.Fx.SimpleInjectorDependencyInjection; +using Backend.Fx.Tests.DummyServices; +using Backend.Fx.TestUtil; +using Backend.Fx.Util; +using FakeItEasy; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.Features.MessageBus; + +public class TheMessageBusFeatureWithMultiTenancy : TestWithLogging +{ + private readonly InMemoryTenantRepository _tenantRepository = new(); + private readonly IBackendFxApplication _sender; + private readonly IBackendFxApplication _recipient; + private readonly IExceptionLogger _exceptionLogger = A.Fake(); + private readonly DummyServicesFeature _dummyServicesFeature = new(); + private readonly InProcMessageBusChannel _inProcMessageBusChannel = new(); + + public TheMessageBusFeatureWithMultiTenancy(ITestOutputHelper output) : base(output) + { + _tenantRepository.SaveTenant(new Tenant(1, "t1", "tenant 1", false)); + _tenantRepository.SaveTenant(new Tenant(2, "t2", "tenant 2", true)); + + _sender = new MultiTenancyBackendFxApplication( + new SimpleInjectorCompositionRoot(), + _exceptionLogger, + new DirectTenantEnumerator(_tenantRepository), + new InProcTenantWideMutexManager()); + _sender.EnableFeature(new MessageBusFeature(new InProcMessageBus(_inProcMessageBusChannel))); + _sender.EnableFeature(new DummyServicesFeature()); + + // for this test we do not provide any tenantId, so that we can check whether the message handling detects the correct tenant id + _recipient = new MultiTenancyBackendFxApplication( + new SimpleInjectorCompositionRoot(), + _exceptionLogger, + new DirectTenantEnumerator(_tenantRepository), + new InProcTenantWideMutexManager(), + GetType().Assembly); + _recipient.EnableFeature(new MessageBusFeature(new InProcMessageBus(_inProcMessageBusChannel))); + _recipient.EnableFeature(_dummyServicesFeature); + + } + + [Fact] + public async Task PublishesAndHandlesEvents() + { + // we have two applications + await _sender.BootAsync(); + await _recipient.BootAsync(); + + // we send an integration event on the first app + var dummyIntegrationEvent = new DummyIntegrationEvent(); + var sendingCorrelationId = Guid.Empty; + int? sendingTenantId = null; + + + await _sender.Invoker.InvokeAsync(sp => + { + sendingTenantId = sp.GetRequiredService>().Current.Value; + sendingCorrelationId = sp.GetRequiredService>().Current.Id; + var messageBusScope = sp.GetRequiredService(); + messageBusScope.Publish(dummyIntegrationEvent); + return Task.CompletedTask; + }); + + Assert.True(dummyIntegrationEvent.Properties.ContainsKey("TenantId")); + Assert.NotNull(sendingTenantId); + Assert.Equal(sendingTenantId!.Value.ToString(), dummyIntegrationEvent.Properties["TenantId"]); + + // wait that the async processing finishes + await _inProcMessageBusChannel.FinishHandlingAllMessagesAsync(); + + A.CallTo(()=>_exceptionLogger.LogException(A._)).MustNotHaveHappened(); + + // assert that the second application received the message and handled the event + A.CallTo(() => + _dummyServicesFeature.Spies.DummyIntegrationEventHandlerSpy.HandleAsync( + A.That.Matches(ev => ev.Id == dummyIntegrationEvent.Id + && sendingCorrelationId == dummyIntegrationEvent.CorrelationId + && sendingTenantId.Value.ToString() == dummyIntegrationEvent.Properties["TenantId"] + ))) + .MustHaveHappenedOnceExactly(); + + Assert.NotEqual(Guid.Empty, sendingCorrelationId); + Assert.NotEqual(Guid.Empty, dummyIntegrationEvent.Id); + } + + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _sender.Dispose(); + _recipient.Dispose(); + } + } + + [UsedImplicitly] + private class DummyTenantIdSelector : ICurrentTenantIdSelector + { + public static int? TenantId = 1000; + + public TenantId GetCurrentTenantId() + { + return new TenantId(TenantId); + } + } + + [UsedImplicitly] + private class NullTenantIdSelector : ICurrentTenantIdSelector + { + public static int? TenantId = null; + + public TenantId GetCurrentTenantId() + { + return new TenantId(TenantId); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/MultiTenancy/TheCurrentTenantIdHolder.cs b/tests/Backend.Fx.Tests/Features/MultiTenancy/TheCurrentTenantIdHolder.cs new file mode 100644 index 00000000..aa858f66 --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/MultiTenancy/TheCurrentTenantIdHolder.cs @@ -0,0 +1,34 @@ +using Backend.Fx.Features.MultiTenancy; +using Backend.Fx.TestUtil; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.Features.MultiTenancy; + +public class TheCurrentTenantIdHolder : TestWithLogging +{ + public TheCurrentTenantIdHolder(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void CanBeCreatedEmpty() + { + var sut = new CurrentTenantIdHolder(); + Assert.False(sut.Current.HasValue); + } + + [Fact] + public void CanBeCreatedFromTenantId() + { + var sut = CurrentTenantIdHolder.Create(new TenantId(234)); + Assert.Equal(234, sut.Current.Value); + } + + [Fact] + public void CanBeCreatedFromInteger() + { + var sut = CurrentTenantIdHolder.Create(234); + Assert.Equal(234, sut.Current.Value); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/MultiTenancy/TheMultiTenancyApplication.cs b/tests/Backend.Fx.Tests/Features/MultiTenancy/TheMultiTenancyApplication.cs new file mode 100644 index 00000000..25f50995 --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/MultiTenancy/TheMultiTenancyApplication.cs @@ -0,0 +1,55 @@ +using System.Threading.Tasks; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Features.MultiTenancy; +using Backend.Fx.Features.MultiTenancy.InProc; +using Backend.Fx.Features.MultiTenancyAdmin; +using Backend.Fx.Features.MultiTenancyAdmin.InMem; +using Backend.Fx.Logging; +using Backend.Fx.MicrosoftDependencyInjection; +using Backend.Fx.TestUtil; +using Backend.Fx.Util; +using FakeItEasy; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.Features.MultiTenancy; + +public class TheMultiTenancyApplication : TestWithLogging +{ + private readonly InMemoryTenantRepository _tenantRepository = new(); + private readonly IBackendFxApplication _sut; + private readonly IExceptionLogger _exceptionLogger = A.Fake(); + + public TheMultiTenancyApplication(ITestOutputHelper output) : base(output) + { + _tenantRepository.SaveTenant(new Tenant(1, "t1", "tenant 1", false)); + _tenantRepository.SaveTenant(new Tenant(2, "t2", "tenant 2", true)); + + _sut = new MultiTenancyBackendFxApplication(new MicrosoftCompositionRoot(), + _exceptionLogger, + new DirectTenantEnumerator(_tenantRepository), new InProcTenantWideMutexManager(), GetType().Assembly); + } + + [Fact] + public async Task InjectsTheTenantIdIntoTheOperationScope() + { + await _sut.Invoker.InvokeAsync(sp => + { + var tenantIdHolder = sp.GetRequiredService>(); + Assert.Equal(new TenantId(1234), tenantIdHolder.Current); + return Task.CompletedTask; + }, new AnonymousIdentity()); + } + + + [UsedImplicitly] + private class DummyTenantIdSelector : ICurrentTenantIdSelector + { + public TenantId GetCurrentTenantId() + { + return new TenantId(1234); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/MultiTenancy/TheTenantId.cs b/tests/Backend.Fx.Tests/Features/MultiTenancy/TheTenantId.cs new file mode 100644 index 00000000..32824c44 --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/MultiTenancy/TheTenantId.cs @@ -0,0 +1,56 @@ +using System; +using Backend.Fx.Features.MultiTenancy; +using Backend.Fx.TestUtil; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.Features.MultiTenancy; + +public class TheTenantId : TestWithLogging +{ + public TheTenantId(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void CanBeCreatedWithoutTenantId() + { + var sut = new TenantId(null); + Assert.False(sut.HasValue); + Assert.Throws(() => sut.Value); + Assert.Throws(() => (int)sut); + Assert.Equal("TenantId: NULL", sut.DebuggerDisplay); + Assert.Equal("NULL", sut.ToString()); + } + + [Fact] + public void CanBeCreatedWithTenantId() + { + var sut = new TenantId(333); + Assert.True(sut.HasValue); + Assert.Equal(333, sut.Value); + Assert.Equal("TenantId: 333", sut.DebuggerDisplay); + Assert.Equal("333", sut.ToString()); + Assert.Equal(333, (int)sut); + } + + [Fact] + public void ConsidersEqualTenantIdsAsEqualObjects() + { + var sut1 = new TenantId(333); + var sut2 = (TenantId)333; + Assert.Equal(sut1, sut2); + Assert.True(Equals(sut1, sut2)); + Assert.True(sut1 == sut2); + Assert.False(sut1 != sut2); + Assert.Equal(sut1.GetHashCode(), sut2.GetHashCode()); + + sut1 = new TenantId(null); + sut2 = new TenantId(null); + Assert.Equal(sut1, sut2); + Assert.True(Equals(sut1, sut2)); + Assert.True(sut1 == sut2); + Assert.False(sut1 != sut2); + Assert.Equal(sut1.GetHashCode(), sut2.GetHashCode()); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/MultiTenancyAdmin/TheMultiTenancyAdminFeature.cs b/tests/Backend.Fx.Tests/Features/MultiTenancyAdmin/TheMultiTenancyAdminFeature.cs new file mode 100644 index 00000000..160a3c18 --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/MultiTenancyAdmin/TheMultiTenancyAdminFeature.cs @@ -0,0 +1,56 @@ +using System.Threading.Tasks; +using Backend.Fx.Features.MultiTenancy; +using Backend.Fx.Features.MultiTenancyAdmin; +using Backend.Fx.Features.MultiTenancyAdmin.InMem; +using Backend.Fx.Logging; +using Backend.Fx.MicrosoftDependencyInjection; +using Backend.Fx.TestUtil; +using JetBrains.Annotations; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.Features.MultiTenancyAdmin; + +public class TheMultiTenancyAdminFeature : TestWithLogging +{ + private readonly InMemoryTenantRepository _tenantRepository = new(); + private readonly IBackendFxApplication _sut; + + public TheMultiTenancyAdminFeature(ITestOutputHelper output) : base(output) + { + _sut = new MultiTenancyBackendFxApplication( + new MicrosoftCompositionRoot(), + new ExceptionLogger(Log.Create()), + new DirectTenantEnumerator(_tenantRepository)); + } + + [Fact] + public async Task DoesNotCreateTenantsOnBoot() + { + _sut.EnableFeature(new MultiTenancyAdminFeature(_tenantRepository, ensureDemoTenantOnBoot: false)); + await _sut.BootAsync(); + + var tenants = _tenantRepository.GetTenants(); + Assert.Empty(tenants); + } + + [Fact] + public async Task CreatesDemoTenantOnBootWhenRequested() + { + _sut.EnableFeature(new MultiTenancyAdminFeature(_tenantRepository, ensureDemoTenantOnBoot: true)); + await _sut.BootAsync(); + + var tenants = _tenantRepository.GetTenants(); + Assert.Single(tenants); + Assert.True(tenants[0].IsDemoTenant); + } + + [UsedImplicitly] + private class DummyTenantIdSelector : ICurrentTenantIdSelector + { + public TenantId GetCurrentTenantId() + { + return new TenantId(1000); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/MultiTenancyAdmin/TheTenantService.cs b/tests/Backend.Fx.Tests/Features/MultiTenancyAdmin/TheTenantService.cs new file mode 100644 index 00000000..24880bb1 --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/MultiTenancyAdmin/TheTenantService.cs @@ -0,0 +1,109 @@ +using Backend.Fx.Exceptions; +using Backend.Fx.Features.MultiTenancyAdmin; +using Backend.Fx.Features.MultiTenancyAdmin.InMem; +using Backend.Fx.TestUtil; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.Features.MultiTenancyAdmin; + +public class TheTenantService : TestWithLogging +{ + public TheTenantService(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void CreatesTenantsActive() + { + var repo = new InMemoryTenantRepository(); + var sut = new TenantService(repo); + + Tenant? tenant = sut.CreateTenant("T1", "Tenant 1", false); + Assert.True(tenant.IsActive); + } + + [Fact] + public void ProvidesTenants() + { + var repo = new InMemoryTenantRepository(); + repo.SaveTenant(new Tenant(1, "t1", "tenant 1", false)); + repo.SaveTenant(new Tenant(2, "t2", "tenant 2", true)); + + var sut = new TenantService(repo); + + var tenant = sut.GetTenant(1); + Assert.Equal(1, tenant.Id); + + Assert.Throws>(()=>sut.GetTenant(99)); + + var tenants = sut.GetActiveTenants(); + Assert.Equal(2, tenants.Length); + + tenants = sut.GetActiveDemonstrationTenants(); + Assert.Single(tenants); + Assert.Equal(2, tenants[0].Id); + + tenants = sut.GetActiveProductionTenants(); + Assert.Single(tenants); + Assert.Equal(1, tenants[0].Id); + + sut.DeactivateTenant(1); + + tenants = sut.GetActiveTenants(); + Assert.Single(tenants); + Assert.Equal(2, tenants[0].Id); + + sut.DeactivateTenant(2); + + tenants = sut.GetActiveTenants(); + Assert.Empty(tenants); + + tenants = sut.GetActiveDemonstrationTenants(); + Assert.Empty(tenants); + + tenants = sut.GetActiveProductionTenants(); + Assert.Empty(tenants); + + + } + + [Fact] + public void CannotDeleteActiveTenant() + { + var repo = new InMemoryTenantRepository(); + var sut = new TenantService(repo); + + Tenant? tenant = sut.CreateTenant("T1", "Tenant 1", false); + Assert.True(tenant.IsActive); + + Assert.Throws(() => sut.DeleteTenant(tenant.Id)); + } + + [Fact] + public void CanDeleteInactiveTenant() + { + var repo = new InMemoryTenantRepository(); + var sut = new TenantService(repo); + + Tenant? tenant = sut.CreateTenant("T1", "Tenant 1", false); + Assert.True(tenant.IsActive); + sut.DeactivateTenant(tenant.Id); + sut.DeleteTenant(tenant.Id); + Assert.Empty(repo.GetTenants()); + } + + [Fact] + public void CanDeactivateAndActivateTenants() + { + var repo = new InMemoryTenantRepository(); + var sut = new TenantService(repo); + + Tenant? tenant = sut.CreateTenant("T1", "Tenant 1", false); + + sut.DeactivateTenant(tenant.Id); + Assert.False(tenant.IsActive); + sut.ActivateTenant(tenant.Id); + Assert.True(tenant.IsActive); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/Persistence/CanFlushSpy.cs b/tests/Backend.Fx.Tests/Features/Persistence/CanFlushSpy.cs new file mode 100644 index 00000000..5afc4bb5 --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/Persistence/CanFlushSpy.cs @@ -0,0 +1,25 @@ +using Backend.Fx.Features.Persistence; + +namespace Backend.Fx.Tests.Features.Persistence; + +public interface ICanFlushSpy : ICanFlush +{ +} + +public class CanFlushSpy : ICanFlush +{ + private readonly ICanFlushSpy _canFlushSpy; + private readonly ICanFlush _canFlush; + + public CanFlushSpy(ICanFlushSpy canFlushSpy, ICanFlush canFlush) + { + _canFlushSpy = canFlushSpy; + _canFlush = canFlush; + } + + public void Flush() + { + _canFlushSpy.Flush(); + _canFlush.Flush(); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/Persistence/ThePersistenceFeature.cs b/tests/Backend.Fx.Tests/Features/Persistence/ThePersistenceFeature.cs new file mode 100644 index 00000000..d458851d --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/Persistence/ThePersistenceFeature.cs @@ -0,0 +1,121 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Features.DomainEvents; +using Backend.Fx.Features.Persistence; +using Backend.Fx.Features.Persistence.InMem; +using Backend.Fx.Logging; +using Backend.Fx.MicrosoftDependencyInjection; +using Backend.Fx.Tests.DummyServices; +using Backend.Fx.TestUtil; +using FakeItEasy; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.Features.Persistence; + +public class ThePersistenceFeature : TestWithLogging +{ + private readonly IBackendFxApplication _sut; + private readonly IExceptionLogger _exceptionLogger = A.Fake(); + private readonly DummyServicesFeature _dummyServicesFeature = new(); + + public ThePersistenceFeature(ITestOutputHelper output) : base(output) + { + _sut = new BackendFxApplication(new MicrosoftCompositionRoot(), _exceptionLogger, GetType().Assembly); + } + + [Fact] + public async Task WaitsForDatabaseAvailabilityOnBoot() + { + var fake = A.Fake(); + _sut.EnableFeature(new PersistenceFeature(new InMemoryPersistenceModule(), databaseAvailabilityAwaiter: fake)); + await _sut.BootAsync(); + + A.CallTo(() => fake.WaitForDatabase(A._)).MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task BootstrapsDatabaseOnBoot() + { + var fake = A.Fake(); + _sut.EnableFeature(new PersistenceFeature(new InMemoryPersistenceModule(), databaseBootstrapper: fake)); + await _sut.BootAsync(); + + A.CallTo(() => fake.EnsureDatabaseExistenceAsync(A._)).MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task FlushesOnOperationCompletion() + { + _sut.EnableFeature(new PersistenceFeature(new InMemoryPersistenceModule())); + _sut.EnableFeature(_dummyServicesFeature); + + ICanFlushSpy canFlushSpy = A.Fake(); + _sut.CompositionRoot.RegisterDecorator(ServiceDescriptor.Scoped()); + _sut.CompositionRoot.Register(ServiceDescriptor.Singleton(canFlushSpy)); + await _sut.BootAsync(); + + await _sut.Invoker.InvokeAsync(_ => Task.CompletedTask); + + A.CallTo(() => canFlushSpy.Flush()).MustHaveHappenedOnceExactly(); + } + + + [Fact] + public async Task FlushesOnRaisingDomainEvents() + { + _sut.EnableFeature(new DomainEventsFeature()); + _sut.EnableFeature(new PersistenceFeature(new InMemoryPersistenceModule())); + _sut.EnableFeature(_dummyServicesFeature); + + var canFlushSpy = A.Fake(); + _sut.CompositionRoot.RegisterDecorator(ServiceDescriptor.Scoped()); + _sut.CompositionRoot.Register(ServiceDescriptor.Singleton(canFlushSpy)); + await _sut.BootAsync(); + + await _sut.Invoker.InvokeAsync(_ => Task.CompletedTask); + + // we expect two calls to Flush(): before raising domain events and when completing the operation + A.CallTo(() => canFlushSpy.Flush()).MustHaveHappenedTwiceExactly(); + } + + [Fact] + public async Task DoesNotFlushOnInvocationError() + { + _sut.EnableFeature(new PersistenceFeature(new InMemoryPersistenceModule())); + _sut.EnableFeature(_dummyServicesFeature); + + ICanFlushSpy canFlushSpy = A.Fake(); + _sut.CompositionRoot.RegisterDecorator(ServiceDescriptor.Scoped()); + _sut.CompositionRoot.Register(ServiceDescriptor.Singleton(canFlushSpy)); + await _sut.BootAsync(); + + await Assert.ThrowsAsync(async () => + await _sut.Invoker.InvokeAsync(_ => throw new DivideByZeroException())); + + A.CallTo(() => canFlushSpy.Flush()).MustNotHaveHappened(); + } + + + [Fact] + public async Task ProvidesRepositories() + { + _sut.EnableFeature(new PersistenceFeature(new InMemoryPersistenceModule())); + await _sut.BootAsync(); + + await _sut.Invoker.InvokeAsync(async sp => + { + var repository = sp.GetRequiredService>(); + await repository.AddAsync(new DummyAggregate(1, "one")); + }); + + await _sut.Invoker.InvokeAsync(async sp => + { + var repository = sp.GetRequiredService>(); + DummyAggregate aggregate = await repository.GetAsync(1); + Assert.NotNull(aggregate); + }); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Features/Persistence/ThePersistenceFeatureWithMultiTenancy.cs b/tests/Backend.Fx.Tests/Features/Persistence/ThePersistenceFeatureWithMultiTenancy.cs new file mode 100644 index 00000000..71c8a817 --- /dev/null +++ b/tests/Backend.Fx.Tests/Features/Persistence/ThePersistenceFeatureWithMultiTenancy.cs @@ -0,0 +1,88 @@ +using System.Threading.Tasks; +using Backend.Fx.Features.MultiTenancy; +using Backend.Fx.Features.MultiTenancyAdmin; +using Backend.Fx.Features.MultiTenancyAdmin.InMem; +using Backend.Fx.Features.Persistence; +using Backend.Fx.Features.Persistence.InMem; +using Backend.Fx.Logging; +using Backend.Fx.SimpleInjectorDependencyInjection; +using Backend.Fx.Tests.DummyServices; +using Backend.Fx.TestUtil; +using FakeItEasy; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests.Features.Persistence; + +public class ThePersistenceFeatureWithMultiTenancy : TestWithLogging +{ + private readonly InMemoryTenantRepository _tenantRepository = new(); + + private readonly IBackendFxApplication _sut; + private readonly IExceptionLogger _exceptionLogger = A.Fake(); + private readonly DummyServicesFeature _dummyServicesFeature = new (); + + public ThePersistenceFeatureWithMultiTenancy(ITestOutputHelper output) : base(output) + { + _tenantRepository.SaveTenant(new Tenant(1, "t1", "tenant 1", false)); + _tenantRepository.SaveTenant(new Tenant(2, "t2", "tenant 2", false)); + + _sut = new MultiTenancyBackendFxApplication( + new SimpleInjectorCompositionRoot(), + _exceptionLogger, + new DirectTenantEnumerator(_tenantRepository), + GetType().Assembly); + + _sut.EnableFeature(_dummyServicesFeature); + } + + [Fact] + public async Task IsolatesTenantsDataFromEachOther() + { + _sut.EnableFeature(new PersistenceFeature(new InMemoryPersistenceModule())); + await _sut.BootAsync(); + + DummyTenantIdSelector.TenantId = 100; + var inTenant100 = new DummyAggregate(1, "one"); + await _sut.Invoker.InvokeAsync(async sp => + { + var repository = sp.GetRequiredService>(); + await repository.AddAsync(inTenant100); + }); + + await _sut.Invoker.InvokeAsync(async sp => + { + var repository = sp.GetRequiredService>(); + DummyAggregate aggregate = await repository.GetAsync(1); + Assert.Equal(inTenant100, aggregate); + }); + + DummyTenantIdSelector.TenantId = 200; + var inTenant200 = new DummyAggregate(1, "one"); + await _sut.Invoker.InvokeAsync(async sp => + { + var repository = sp.GetRequiredService>(); + await repository.AddAsync(inTenant200); + }); + + await _sut.Invoker.InvokeAsync(async sp => + { + var repository = sp.GetRequiredService>(); + DummyAggregate aggregate = await repository.GetAsync(1); + Assert.Equal(inTenant200, aggregate); + }); + } + + [UsedImplicitly] + private class DummyTenantIdSelector : ICurrentTenantIdSelector + { + public static int TenantId { get; set; } + + public TenantId GetCurrentTenantId() + { + return new TenantId(TenantId); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/Authorization/TheAllowAllImplementation.cs b/tests/Backend.Fx.Tests/Patterns/Authorization/TheAllowAllImplementation.cs deleted file mode 100644 index e00e3d5b..00000000 --- a/tests/Backend.Fx.Tests/Patterns/Authorization/TheAllowAllImplementation.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Linq; -using Backend.Fx.Patterns.Authorization; -using Backend.Fx.Tests.BuildingBlocks; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Patterns.Authorization -{ - public class TheAllowAllImplementation : TestWithLogging - { - private readonly IAggregateAuthorization _sut = new AllowAll(); - private readonly TheAggregateRoot.TestAggregateRoot _testAggregateRoot = new TheAggregateRoot.TestAggregateRoot(1,"e"); - - [Fact] - public void AllowsAccess() - { - Assert.True(_sut.HasAccessExpression.Compile().Invoke(_testAggregateRoot)); - Assert.Contains(_testAggregateRoot, _sut.Filter(new[] {_testAggregateRoot}.AsQueryable())); - } - - [Fact] - public void AllowsCreation() - { - Assert.True(_sut.CanCreate(_testAggregateRoot)); - } - - [Fact] - public void AllowsModification() - { - Assert.True(_sut.CanModify(_testAggregateRoot)); - } - - [Fact] - public void AllowsDeletion() - { - Assert.True(_sut.CanDelete(_testAggregateRoot)); - } - - public TheAllowAllImplementation(ITestOutputHelper output) : base(output) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/Authorization/TheDenyAllImplementation.cs b/tests/Backend.Fx.Tests/Patterns/Authorization/TheDenyAllImplementation.cs deleted file mode 100644 index 8151b29b..00000000 --- a/tests/Backend.Fx.Tests/Patterns/Authorization/TheDenyAllImplementation.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Linq; -using Backend.Fx.Patterns.Authorization; -using Backend.Fx.Tests.BuildingBlocks; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Patterns.Authorization -{ - public class TheDenyAllImplementation : TestWithLogging - { - private readonly IAggregateAuthorization _sut = new DenyAll(); - private readonly TheAggregateRoot.TestAggregateRoot _testAggregateRoot = new TheAggregateRoot.TestAggregateRoot(1,"e"); - - - [Fact] - public void DeniesAccess() - { - Assert.False(_sut.HasAccessExpression.Compile().Invoke(_testAggregateRoot)); - Assert.Empty(_sut.Filter(new[] {_testAggregateRoot}.AsQueryable())); - } - - [Fact] - public void DeniesCreation() - { - Assert.False(_sut.CanCreate(_testAggregateRoot)); - } - - [Fact] - public void DeniesModification() - { - Assert.False(_sut.CanModify(_testAggregateRoot)); - } - - [Fact] - public void DeniesDeletion() - { - Assert.False(_sut.CanDelete(_testAggregateRoot)); - } - - public TheDenyAllImplementation(ITestOutputHelper output) : base(output) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DataGeneration/TheDataGenerationContext.cs b/tests/Backend.Fx.Tests/Patterns/DataGeneration/TheDataGenerationContext.cs deleted file mode 100644 index acc37c8f..00000000 --- a/tests/Backend.Fx.Tests/Patterns/DataGeneration/TheDataGenerationContext.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System.Linq; -using System.Threading; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Patterns.DataGeneration; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.Patterns.EventAggregation.Integration; -using Backend.Fx.Tests.Patterns.DependencyInjection; -using FakeItEasy; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Patterns.DataGeneration -{ - public class TheDataGenerationContext : TestWithLogging - { - private readonly ITenantWideMutexManager _tenantWideMutexManager = A.Fake(); - - public TheDataGenerationContext(ITestOutputHelper output) : base(output) - { - var fakes = new DiTestFakes(); - A.CallTo(() => fakes.InstanceProvider.GetInstances()) - .Returns(_demoDataGenerators.Concat(_prodDataGenerators.Cast()).ToArray()); - - var application = A.Fake(); - A.CallTo(() => application.Invoker).Returns(fakes.Invoker); - A.CallTo(() => application.WaitForBoot(A._, A._)).Returns(true); - - var messageBus = new InMemoryMessageBus(); - messageBus.ProvideInvoker(application.Invoker); - - var tenantIdProvider = A.Fake(); - A.CallTo(() => tenantIdProvider.GetActiveDemonstrationTenantIds()).Returns(_demoTenants); - A.CallTo(() => tenantIdProvider.GetActiveProductionTenantIds()).Returns(_prodTenants); - - _sut = new DataGenerationContext(fakes.CompositionRoot, - fakes.Invoker, - _tenantWideMutexManager); - } - - private readonly DataGenerationContext _sut; - - private readonly IDemoDataGenerator[] _demoDataGenerators = - {new DemoDataGenerator1(), new DemoDataGenerator2()}; - - private readonly IProductiveDataGenerator[] _prodDataGenerators = {new ProdDataGenerator1()}; - private readonly TenantId[] _demoTenants = {new TenantId(1), new TenantId(2)}; - private readonly TenantId[] _prodTenants = {new TenantId(11), new TenantId(12)}; - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void CallsDataGeneratorWhenSeedingForSpecificTenant(bool isDemoTenant) - { - ITenantWideMutex disposable = A.Fake(); - ITenantWideMutex m; - var tryAcquireCall = A.CallTo(() => - _tenantWideMutexManager.TryAcquire( - A.That.Matches( - t => t.Value == 123), - A.That.Matches(s => s == "DataGenerationContext"), - out m)); - tryAcquireCall - .Returns(true) - .AssignsOutAndRefParameters(disposable); - - _sut.SeedDataForTenant(new TenantId(123), isDemoTenant); - - foreach (IProductiveDataGenerator dataGenerator in _prodDataGenerators) - A.CallTo(() => ((ProdDataGenerator) dataGenerator).Impl.Generate()).MustHaveHappenedOnceExactly(); - - foreach (IDemoDataGenerator dataGenerator in _demoDataGenerators) - if (isDemoTenant) - A.CallTo(() => ((DemoDataGenerator) dataGenerator).Impl.Generate()).MustHaveHappenedOnceExactly(); - else - A.CallTo(() => ((DemoDataGenerator) dataGenerator).Impl.Generate()).MustNotHaveHappened(); - - tryAcquireCall.MustHaveHappenedOnceExactly(); - A.CallTo(() => disposable.Dispose()).MustHaveHappenedOnceExactly(); - } - - [Fact] - public void DoesNothingWhenCannotAcquireTenantWideMutex() - { - ITenantWideMutex m; - var tryAcquireCall = A.CallTo(() => - _tenantWideMutexManager.TryAcquire( - A.That.Matches( - t => t.Value == 123), - A.That.Matches(s => s == "DataGenerationContext"), - out m)); - tryAcquireCall.Returns(false); - - _sut.SeedDataForTenant(new TenantId(123), false); - - foreach (IProductiveDataGenerator dataGenerator in _prodDataGenerators) - { - A.CallTo(() => ((ProdDataGenerator) dataGenerator).Impl.Generate()).MustNotHaveHappened(); - } - - foreach (IDemoDataGenerator dataGenerator in _demoDataGenerators) - { - A.CallTo(() => ((DemoDataGenerator) dataGenerator).Impl.Generate()).MustNotHaveHappened(); - } - - tryAcquireCall.MustHaveHappenedOnceExactly(); - } - - private abstract class DemoDataGenerator : IDemoDataGenerator - { - public readonly IDemoDataGenerator Impl; - - protected DemoDataGenerator() - { - Impl = A.Fake(o => o.Named(GetType().Name)); - } - - public int Priority => Impl.Priority; - - public void Generate() - { - Impl.Generate(); - } - } - - private class DemoDataGenerator1 : DemoDataGenerator - { - } - - private class DemoDataGenerator2 : DemoDataGenerator - { - } - - private abstract class ProdDataGenerator : IProductiveDataGenerator - { - public readonly IProductiveDataGenerator Impl; - - protected ProdDataGenerator() - { - Impl = A.Fake(o => o.Named(GetType().Name)); - } - - public int Priority => Impl.Priority; - - public void Generate() - { - Impl.Generate(); - } - } - - private class ProdDataGenerator1 : ProdDataGenerator - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DataGeneration/TheGenerateDataOnBootDecorator.cs b/tests/Backend.Fx.Tests/Patterns/DataGeneration/TheGenerateDataOnBootDecorator.cs deleted file mode 100644 index dfcf4bbe..00000000 --- a/tests/Backend.Fx.Tests/Patterns/DataGeneration/TheGenerateDataOnBootDecorator.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Threading.Tasks; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Hacking; -using Backend.Fx.Patterns.DataGeneration; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.Patterns.EventAggregation.Integration; -using FakeItEasy; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Patterns.DataGeneration -{ - public class TheGenerateDataOnBootDecorator : TestWithLogging - { - public TheGenerateDataOnBootDecorator(ITestOutputHelper output) : base(output) - { - ITenantWideMutexManager tenantWideMutexManager = new InMemoryTenantWideMutexManager(); - _compositionRoot = A.Fake(); - _dataGenerationModule = A.Fake(); - _dataGenerationContext = A.Fake(); - _tenantIdProvider = A.Fake(); - - var backendFxApplication = A.Fake(); - A.CallTo(() => backendFxApplication.CompositionRoot).Returns(_compositionRoot); - _sut = new DataGeneratingApplication( - _tenantIdProvider, - _dataGenerationModule, - tenantWideMutexManager, backendFxApplication); - - _sut.SetPrivate(f => f.DataGenerationContext, _dataGenerationContext); - } - - private readonly DataGeneratingApplication _sut; - private readonly IModule _dataGenerationModule; - private readonly IDataGenerationContext _dataGenerationContext; - private readonly ICompositionRoot _compositionRoot; - private readonly ITenantIdProvider _tenantIdProvider; - - [Fact] - public void DelegatesAllOtherCalls() - { - var app = A.Fake(); - IBackendFxApplication sut = new DataGeneratingApplication(A.Fake(), A.Fake(), new InMemoryTenantWideMutexManager(), app); - - - // ReSharper disable UnusedVariable - IBackendFxApplicationAsyncInvoker ai = sut.AsyncInvoker; - A.CallTo(() => app.AsyncInvoker).MustHaveHappenedOnceExactly(); - - ICompositionRoot cr = sut.CompositionRoot; - A.CallTo(() => app.CompositionRoot).MustHaveHappenedOnceOrMore(); - - sut.Dispose(); - A.CallTo(() => app.Dispose()).MustHaveHappenedOnceExactly(); - - IBackendFxApplicationInvoker i = sut.Invoker; - A.CallTo(() => app.Invoker).MustHaveHappenedOnceOrMore(); - - IMessageBus mb = sut.MessageBus; - A.CallTo(() => app.MessageBus).MustHaveHappenedOnceExactly(); - - // ReSharper restore UnusedVariable - } - - [Fact] - public async Task RegistersDataGenerationModuleOnBoot() - { - await _sut.BootAsync(); - A.CallTo(() => _compositionRoot.RegisterModules(A.That.Contains(_dataGenerationModule))) - .MustHaveHappenedOnceExactly(); - } - - [Fact] - public async Task RunsDataGeneratorsOnBoot() - { - var tenantId1 = new TenantId(1); - var tenantId2 = new TenantId(2); - var tenantId3 = new TenantId(3); - var tenantId4 = new TenantId(4); - - A.CallTo(() => _tenantIdProvider.GetActiveDemonstrationTenantIds()).Returns(new[] {tenantId1, tenantId2}); - A.CallTo(() => _tenantIdProvider.GetActiveProductionTenantIds()).Returns(new[] {tenantId3, tenantId4}); - await _sut.BootAsync(); - A.CallTo(() => - _dataGenerationContext.SeedDataForTenant( - A.That.IsEqualTo(tenantId1), - A.That.IsEqualTo(true))).MustHaveHappenedOnceExactly(); - A.CallTo(() => - _dataGenerationContext.SeedDataForTenant( - A.That.IsEqualTo(tenantId2), - A.That.IsEqualTo(true))).MustHaveHappenedOnceExactly(); - A.CallTo(() => - _dataGenerationContext.SeedDataForTenant( - A.That.IsEqualTo(tenantId3), - A.That.IsEqualTo(false))).MustHaveHappenedOnceExactly(); - A.CallTo(() => - _dataGenerationContext.SeedDataForTenant( - A.That.IsEqualTo(tenantId4), - A.That.IsEqualTo(false))).MustHaveHappenedOnceExactly(); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DataGeneration/TheInitialDataGenerator.cs b/tests/Backend.Fx.Tests/Patterns/DataGeneration/TheInitialDataGenerator.cs deleted file mode 100644 index 263a9ddc..00000000 --- a/tests/Backend.Fx.Tests/Patterns/DataGeneration/TheInitialDataGenerator.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Backend.Fx.Patterns.DataGeneration; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Patterns.DataGeneration -{ - public class ADataGenerator : DataGenerator - { - public bool ShouldRunOverride { get; set; } - public int GenerateCoreCalled { get; private set; } - public int ShouldRunCalled { get; private set; } - public int InitializeCalled { get; private set; } - - public override int Priority => 12; - - protected override void GenerateCore() - { - GenerateCoreCalled++; - } - - protected override void Initialize() - { - InitializeCalled++; - } - - protected override bool ShouldRun() - { - ShouldRunCalled++; - return ShouldRunOverride; - } - } - - public class TheInitialDataGenerator : TestWithLogging - { - private readonly ADataGenerator _sut = new ADataGenerator(); - - [Fact] - public void RespectsNegativeShouldRunMethodResult() - { - _sut.Generate(); - Assert.Equal(1, _sut.ShouldRunCalled); - Assert.Equal(0, _sut.GenerateCoreCalled); - Assert.Equal(0, _sut.InitializeCalled); - } - - [Fact] - public void RespectsPositiveShouldRunMethodResult() - { - _sut.ShouldRunOverride = true; - _sut.Generate(); - Assert.Equal(1, _sut.ShouldRunCalled); - Assert.Equal(1, _sut.GenerateCoreCalled); - Assert.Equal(1, _sut.InitializeCalled); - } - - public TheInitialDataGenerator(ITestOutputHelper output) : base(output) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/DITestFakes.cs b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/DITestFakes.cs deleted file mode 100644 index 6198d8c1..00000000 --- a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/DITestFakes.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Security.Principal; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.Patterns.EventAggregation.Integration; -using Backend.Fx.RandomData; -using FakeItEasy; - -namespace Backend.Fx.Tests.Patterns.DependencyInjection -{ - public class DiTestFakes - { - private readonly int _sequenceNumber = TestRandom.Next(100000); - - public DiTestFakes() - { - A.CallTo(() => InstanceProvider.GetInstance>()).Returns(CurrentCorrelationHolder); - A.CallTo(() => InstanceProvider.GetInstance>()).Returns(TenantIdHolder); - A.CallTo(() => InstanceProvider.GetInstance>()).Returns(IdentityHolder); - A.CallTo(() => InstanceProvider.GetInstance()).Returns(Operation); - - A.CallTo(() => InjectionScope.SequenceNumber).Returns(_sequenceNumber++); - A.CallTo(() => InjectionScope.InstanceProvider).Returns(InstanceProvider); - - A.CallTo(() => CompositionRoot.BeginScope()).Returns(InjectionScope); - A.CallTo(() => CompositionRoot.InfrastructureModule).Returns(InfrastructureModule); - - A.CallTo(() => Invoker.Invoke(A>._, A._, A._, A._)) - .Invokes((Action a, IIdentity i, TenantId t, Guid? g) => a.Invoke(InstanceProvider)); - } - - public ICurrentTHolder TenantIdHolder { get; } = A.Fake>(); - public ICurrentTHolder IdentityHolder { get; } = A.Fake>(); - public IOperation Operation { get; } = A.Fake(); - public ICompositionRoot CompositionRoot { get; } = A.Fake(); - public CurrentCorrelationHolder CurrentCorrelationHolder { get; } = new CurrentCorrelationHolder(); - public IInjectionScope InjectionScope { get; } = A.Fake(); - public IExceptionLogger ExceptionLogger { get; } = A.Fake(); - public IInstanceProvider InstanceProvider { get; } = A.Fake(); - public IMessageBus MessageBus { get; } = A.Fake(); - public IInfrastructureModule InfrastructureModule { get; } = A.Fake(); - public IBackendFxApplicationInvoker Invoker { get; } = A.Fake(); - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/SimulatedException.cs b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/SimulatedException.cs deleted file mode 100644 index b6949ea3..00000000 --- a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/SimulatedException.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Backend.Fx.Tests.Patterns.DependencyInjection -{ - public class SimulatedException : Exception - { - public SimulatedException() : base("This exception was intentionally thrown by the unit test. If you see this message unexpectedly, probably the exception handling is broken") - { - - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheBackendFxApplication.cs b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheBackendFxApplication.cs deleted file mode 100644 index c8f27976..00000000 --- a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheBackendFxApplication.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System; -using System.Diagnostics; -using System.Security.Principal; -using System.Threading; -using System.Threading.Tasks; -using Backend.Fx.Environment.Authentication; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.Patterns.EventAggregation.Domain; -using Backend.Fx.Patterns.EventAggregation.Integration; -using FakeItEasy; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Patterns.DependencyInjection -{ - public class TheBackendFxApplication : TestWithLogging - { - public TheBackendFxApplication(ITestOutputHelper output): base(output) - { - _fakes = new DiTestFakes(); - - Func domainEventAggregatorFactory = () => null; - A.CallTo(() => _fakes.InfrastructureModule.RegisterScoped(A>._)) - .Invokes((Func f) => domainEventAggregatorFactory = f); - A.CallTo(() => _fakes.InstanceProvider.GetInstance()).ReturnsLazily(() => domainEventAggregatorFactory.Invoke()); - - Func messageBusScopeFactory = () => null; - A.CallTo(() => _fakes.InfrastructureModule.RegisterScoped(A>._)) - .Invokes((Func f) => messageBusScopeFactory = f); - A.CallTo(() => _fakes.InstanceProvider.GetInstance()).ReturnsLazily(() => messageBusScopeFactory.Invoke()); - - - _sut = new BackendFxApplication(_fakes.CompositionRoot, _fakes.MessageBus, A.Fake()); - } - - private readonly IBackendFxApplication _sut; - private readonly DiTestFakes _fakes; - - - [Fact] - public void CanWaitForBoot() - { - int bootTime = 200; - A.CallTo(() => _fakes.CompositionRoot.Verify()).Invokes(() => Thread.Sleep(bootTime)); - var sw = new Stopwatch(); - - Task.Factory.StartNew(() => _sut.BootAsync()); - sw.Start(); - Assert.True(_sut.WaitForBoot()); - Assert.True(sw.ElapsedMilliseconds >= bootTime); - } - - [Fact] - public void DisposesCompositionRootOnDispose() - { - _sut.BootAsync(); - _sut.Dispose(); - A.CallTo(() => _fakes.CompositionRoot.Dispose()).MustHaveHappenedOnceExactly(); - } - - [Fact] - public void ProvidesExceptionLoggingAsyncInvoker() - { - Assert.IsType(_sut.AsyncInvoker); - } - - [Fact] - public void ProvidesDomainEventAggregator() - { - using (IInjectionScope scope = _sut.CompositionRoot.BeginScope()) - { - var domainEventAggregator = scope.InstanceProvider.GetInstance(); - Assert.NotNull(domainEventAggregator); - } - - A.CallTo(() => _fakes.InstanceProvider.GetInstance()).MustHaveHappenedOnceExactly(); - } - - [Fact] - public void ProvidesExceptionLoggingInvoker() - { - Assert.IsType(_sut.Invoker); - } - - [Fact] - public void IntegratesWithMessageBus() - { - A.CallTo(() => _fakes.MessageBus.ProvideInvoker(A._)).MustHaveHappenedOnceExactly(); - } - - [Fact] - public void ProvidesMessageBusScope() - { - using (IInjectionScope scope = _sut.CompositionRoot.BeginScope()) - { - var messageBusScope = scope.InstanceProvider.GetInstance(); - Assert.NotNull(messageBusScope); - } - - A.CallTo(() => _fakes.InstanceProvider.GetInstance()).MustHaveHappenedOnceExactly(); - } - - [Fact] - public void RegistersInfrastructureModule() - { - A.CallTo(() => _fakes.InfrastructureModule.RegisterScoped, CurrentCorrelationHolder>()).MustHaveHappenedOnceExactly(); - A.CallTo(() => _fakes.InfrastructureModule.RegisterScoped(A>._)).MustHaveHappenedOnceExactly(); - A.CallTo(() => _fakes.InfrastructureModule.RegisterScoped,CurrentIdentityHolder>()).MustHaveHappenedOnceExactly(); - A.CallTo(() => _fakes.InfrastructureModule.RegisterScoped(A>._)).MustHaveHappenedOnceExactly(); - A.CallTo(() => _fakes.InfrastructureModule.RegisterScoped,CurrentTenantIdHolder>()).MustHaveHappenedOnceExactly(); - } - - [Fact] - public void VerifiesCompositionRootOnBoot() - { - _sut.BootAsync(); - A.CallTo(() => _fakes.CompositionRoot.Verify()).MustHaveHappenedOnceExactly(); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheBackendFxApplicationAsyncInvoker.cs b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheBackendFxApplicationAsyncInvoker.cs deleted file mode 100644 index 0f58d88e..00000000 --- a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheBackendFxApplicationAsyncInvoker.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using System.Security.Principal; -using System.Threading.Tasks; -using Backend.Fx.Environment.Authentication; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Patterns.DependencyInjection; -using FakeItEasy; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Patterns.DependencyInjection -{ - public class TheBackendFxApplicationAsyncInvoker : TestWithLogging - { - public TheBackendFxApplicationAsyncInvoker(ITestOutputHelper output): base(output) - { - _fakes = new DiTestFakes(); - _sut = new BackendFxApplicationInvoker(_fakes.CompositionRoot); - } - - private readonly IBackendFxApplicationAsyncInvoker _sut; - private readonly DiTestFakes _fakes; - - [Fact] - public async Task BeginsNewScopeForEveryInvocation() - { - await _sut.InvokeAsync(ip => Task.CompletedTask, new AnonymousIdentity(), new TenantId(111)); - A.CallTo(() => _fakes.CompositionRoot.BeginScope()).MustHaveHappenedOnceExactly(); - A.CallTo(() => _fakes.InjectionScope.Dispose()).MustHaveHappenedOnceExactly(); - - await _sut.InvokeAsync(ip => Task.CompletedTask, new AnonymousIdentity(), new TenantId(111)); - A.CallTo(() => _fakes.CompositionRoot.BeginScope()).MustHaveHappenedTwiceExactly(); - A.CallTo(() => _fakes.InjectionScope.Dispose()).MustHaveHappenedTwiceExactly(); - } - - [Fact] - public async Task BeginsNewOperationForEveryInvocation() - { - await _sut.InvokeAsync(ip => Task.CompletedTask, new AnonymousIdentity(), new TenantId(111)); - A.CallTo(() => _fakes.Operation.Begin()).MustHaveHappenedOnceExactly(); - A.CallTo(() => _fakes.Operation.Complete()).MustHaveHappenedOnceExactly(); - - await _sut.InvokeAsync(ip => Task.CompletedTask, new AnonymousIdentity(), new TenantId(111)); - A.CallTo(() => _fakes.Operation.Begin()).MustHaveHappenedTwiceExactly(); - A.CallTo(() => _fakes.Operation.Complete()).MustHaveHappenedTwiceExactly(); - } - - [Fact] - public async Task DoesNotCatchFrameworkExceptions() - { - A.CallTo(() => _fakes.CompositionRoot.BeginScope()).Throws(); - await Assert.ThrowsAsync(async () => await _sut.InvokeAsync(ip => Task.CompletedTask, new AnonymousIdentity(), new TenantId(111))); - A.CallTo(() => _fakes.Operation.Begin()).MustNotHaveHappened(); - A.CallTo(() => _fakes.Operation.Complete()).MustNotHaveHappened(); - } - - [Fact] - public async Task DoesNotCatchOperationExceptions() - { - await Assert.ThrowsAsync(async () => await _sut.InvokeAsync(ip => throw new SimulatedException(), new AnonymousIdentity(), new TenantId(111))); - A.CallTo(() => _fakes.Operation.Begin()).MustHaveHappenedOnceExactly(); - A.CallTo(() => _fakes.Operation.Complete()).MustNotHaveHappened(); - } - - [Fact] - public async Task MaintainsCorrelationIdOnInvocation() - { - var correlationId = Guid.NewGuid(); - await _sut.InvokeAsync(ip => Task.CompletedTask, new AnonymousIdentity(), new TenantId(123), correlationId); - Assert.Equal(correlationId, _fakes.CurrentCorrelationHolder.Current.Id); - } - - [Fact] - public async Task MaintainsIdentityOnInvocation() - { - var identity = new GenericIdentity("me"); - await _sut.InvokeAsync(ip => Task.CompletedTask, identity, new TenantId(123)); - A.CallTo(() => _fakes.IdentityHolder.ReplaceCurrent(A.That.IsEqualTo(identity))).MustHaveHappenedOnceExactly(); - } - - [Fact] - public async Task MaintainsTenantIdOnInvocation() - { - var tenantId = new TenantId(123); - await _sut.InvokeAsync(ip => Task.CompletedTask, new AnonymousIdentity(), tenantId); - A.CallTo(() => _fakes.TenantIdHolder.ReplaceCurrent(A.That.IsEqualTo(tenantId))).MustHaveHappenedOnceExactly(); - } - - [Fact] - public async Task ProvidesInstanceProviderForInvocation() - { - IInstanceProvider provided = null; - await _sut.InvokeAsync(ip => - { - provided = ip; - return Task.CompletedTask; - }, new AnonymousIdentity(), new TenantId(111)); - Assert.StrictEqual(_fakes.InstanceProvider, provided); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheBackendFxApplicationInvoker.cs b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheBackendFxApplicationInvoker.cs deleted file mode 100644 index 88f4c218..00000000 --- a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheBackendFxApplicationInvoker.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.Security.Principal; -using Backend.Fx.Environment.Authentication; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Patterns.DependencyInjection; -using FakeItEasy; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Patterns.DependencyInjection -{ - public class TheBackendFxApplicationInvoker : TestWithLogging - { - public TheBackendFxApplicationInvoker(ITestOutputHelper output): base(output) - { - _fakes = new DiTestFakes(); - _sut = new BackendFxApplicationInvoker(_fakes.CompositionRoot); - } - - private readonly IBackendFxApplicationInvoker _sut; - private readonly DiTestFakes _fakes; - - [Fact] - public void BeginsNewScopeForEveryInvocation() - { - _sut.Invoke(ip => { }, new AnonymousIdentity(), new TenantId(111)); - A.CallTo(() => _fakes.CompositionRoot.BeginScope()).MustHaveHappenedOnceExactly(); - A.CallTo(() => _fakes.InjectionScope.Dispose()).MustHaveHappenedOnceExactly(); - - _sut.Invoke(ip => { }, new AnonymousIdentity(), new TenantId(111)); - A.CallTo(() => _fakes.CompositionRoot.BeginScope()).MustHaveHappenedTwiceExactly(); - A.CallTo(() => _fakes.InjectionScope.Dispose()).MustHaveHappenedTwiceExactly(); - } - - [Fact] - public void BeginsNewOperationForEveryInvocation() - { - _sut.Invoke(ip => { }, new AnonymousIdentity(), new TenantId(111)); - A.CallTo(() => _fakes.Operation.Begin()).MustHaveHappenedOnceExactly(); - A.CallTo(() => _fakes.Operation.Complete()).MustHaveHappenedOnceExactly(); - - _sut.Invoke(ip => { }, new AnonymousIdentity(), new TenantId(111)); - A.CallTo(() => _fakes.Operation.Begin()).MustHaveHappenedTwiceExactly(); - A.CallTo(() => _fakes.Operation.Complete()).MustHaveHappenedTwiceExactly(); - } - - [Fact] - public void DoesNotCatchFrameworkExceptions() - { - A.CallTo(() => _fakes.CompositionRoot.BeginScope()).Throws(); - Assert.Throws(() => _sut.Invoke(ip => { }, new AnonymousIdentity(), new TenantId(111))); - A.CallTo(() => _fakes.Operation.Begin()).MustNotHaveHappened(); - A.CallTo(() => _fakes.Operation.Complete()).MustNotHaveHappened(); - } - - [Fact] - public void DoesNotCatchOperationExceptions() - { - Assert.Throws(() => _sut.Invoke(ip => throw new SimulatedException(), new AnonymousIdentity(), new TenantId(111))); - A.CallTo(() => _fakes.Operation.Begin()).MustHaveHappenedOnceExactly(); - A.CallTo(() => _fakes.Operation.Complete()).MustNotHaveHappened(); - } - - [Fact] - public void MaintainsCorrelationIdOnInvocation() - { - var correlationId = Guid.NewGuid(); - _sut.Invoke(ip => { }, new AnonymousIdentity(), new TenantId(123), correlationId); - Assert.Equal(correlationId, _fakes.CurrentCorrelationHolder.Current.Id); - } - - [Fact] - public void MaintainsIdentityOnInvocation() - { - var identity = new GenericIdentity("me"); - _sut.Invoke(ip => { }, identity, new TenantId(123)); - A.CallTo(() => _fakes.IdentityHolder.ReplaceCurrent(A.That.IsEqualTo(identity))).MustHaveHappenedOnceExactly(); - } - - [Fact] - public void MaintainsTenantIdOnInvocation() - { - var tenantId = new TenantId(123); - _sut.Invoke(ip => { }, new AnonymousIdentity(), tenantId); - A.CallTo(() => _fakes.TenantIdHolder.ReplaceCurrent(A.That.IsEqualTo(tenantId))).MustHaveHappenedOnceExactly(); - } - - - [Fact] - public void ProvidesInstanceProviderForInvocation() - { - IInstanceProvider provided = null; - _sut.Invoke(ip => provided = ip, new AnonymousIdentity(), new TenantId(111)); - Assert.StrictEqual(_fakes.InstanceProvider, provided); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheBackendFxDbApplication.cs b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheBackendFxDbApplication.cs deleted file mode 100644 index 055f9027..00000000 --- a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheBackendFxDbApplication.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Threading; -using Backend.Fx.Environment.Persistence; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.Patterns.EventAggregation.Integration; -using FakeItEasy; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Patterns.DependencyInjection -{ - public class TheBackendFxDbApplication : TestWithLogging - { - public TheBackendFxDbApplication(ITestOutputHelper output): base(output) - { - IBackendFxApplication application = new BackendFxApplication(_fakes.CompositionRoot, _fakes.MessageBus, A.Fake()); - _sut = new BackendFxDbApplication(_databaseBootstrapper, _databaseAvailabilityAwaiter, application); - } - - private readonly DiTestFakes _fakes = new DiTestFakes(); - private readonly IBackendFxApplication _sut; - private readonly IDatabaseBootstrapper _databaseBootstrapper = A.Fake(); - private readonly IDatabaseAvailabilityAwaiter _databaseAvailabilityAwaiter = A.Fake(); - - [Fact] - public void CallsDatabaseBootExtensionPointsOnBoot() - { - A.CallTo(() => _databaseAvailabilityAwaiter.WaitForDatabase(A._)).MustNotHaveHappened(); - A.CallTo(() => _databaseBootstrapper.EnsureDatabaseExistence()).MustNotHaveHappened(); - _sut.BootAsync(); - A.CallTo(() => _databaseAvailabilityAwaiter.WaitForDatabase(A._)).MustHaveHappenedOnceExactly(); - A.CallTo(() => _databaseBootstrapper.EnsureDatabaseExistence()).MustHaveHappenedOnceExactly(); - } - - [Fact] - public void DelegatesAllCalls() - { - var application =A.Fake(); - var sut = new BackendFxDbApplication(A.Fake(), - A.Fake(), - application); - - // ReSharper disable once UnusedVariable - IBackendFxApplicationAsyncInvoker ai = sut.AsyncInvoker; - A.CallTo(() => application.AsyncInvoker).MustHaveHappenedOnceExactly(); - - // ReSharper disable once UnusedVariable - ICompositionRoot cr = sut.CompositionRoot; - A.CallTo(() => application.CompositionRoot).MustHaveHappenedOnceExactly(); - - // ReSharper disable once UnusedVariable - IBackendFxApplicationInvoker i = sut.Invoker; - A.CallTo(() => application.Invoker).MustHaveHappenedOnceExactly(); - - // ReSharper disable once UnusedVariable - IMessageBus mb = sut.MessageBus; - A.CallTo(() => application.MessageBus).MustHaveHappenedOnceExactly(); - - sut.BootAsync(); - A.CallTo(() => application.BootAsync(A._)).MustHaveHappenedOnceExactly(); - - sut.Dispose(); - A.CallTo(() => application.Dispose()).MustHaveHappenedOnceExactly(); - - sut.WaitForBoot(); - A.CallTo(() => application.WaitForBoot(A._, A._)).MustHaveHappenedOnceExactly(); - } - - - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheCorrelation.cs b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheCorrelation.cs deleted file mode 100644 index cd104f90..00000000 --- a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheCorrelation.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using Backend.Fx.Patterns.DependencyInjection; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Patterns.DependencyInjection -{ - public class TheCorrelation : TestWithLogging - { - private readonly Correlation _sut = new Correlation(); - - [Fact] - public void CanResume() - { - var resumedCorrelationId = Guid.NewGuid(); - _sut.Resume(resumedCorrelationId); - Assert.Equal(resumedCorrelationId, _sut.Id); - } - - [Fact] - public void InitializesWithId() - { - Assert.NotEqual(Guid.Empty, _sut.Id); - } - - public TheCorrelation(ITestOutputHelper output) : base(output) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheCurrentTHolder.cs b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheCurrentTHolder.cs deleted file mode 100644 index a72ca0bd..00000000 --- a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheCurrentTHolder.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Patterns.DependencyInjection; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Patterns.DependencyInjection -{ - public class TheCurrentTHolder : TestWithLogging - { - private readonly ICurrentTHolder _sut = new CurrentTenantIdHolder(); - private readonly TenantId _instance2 = new TenantId(2); - - [Fact] - public void CanReplaceCurrent() - { - _sut.ReplaceCurrent(_instance2); - Assert.StrictEqual(_instance2, _sut.Current); - } - - [Fact] - public void HoldsCurrent() - { - TenantId current = _sut.Current; - Assert.False(current.HasValue); - } - - public TheCurrentTHolder(ITestOutputHelper output) : base(output) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheInjectionScope.cs b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheInjectionScope.cs deleted file mode 100644 index 1b4419ac..00000000 --- a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheInjectionScope.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using Backend.Fx.Patterns.DependencyInjection; -using FakeItEasy; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Patterns.DependencyInjection -{ - public class TheInjectionScope : TestWithLogging - { - private readonly IInstanceProvider _instanceProvider = A.Fake(); - - private class TestInjectionScope : InjectionScope - { - public TestInjectionScope(int sequenceNumber, IInstanceProvider instanceProvider) : base(sequenceNumber) - { - InstanceProvider = instanceProvider; - } - - public override IInstanceProvider InstanceProvider { get; } - - public override void Dispose() - { - throw new NotImplementedException(); - } - } - - [Fact] - public void InitializesWithSequenceNumber() - { - var injectionScope = new TestInjectionScope(42, _instanceProvider); - Assert.Equal(42, injectionScope.SequenceNumber); - } - - [Fact] - public void KeepsInstanceProvider() - { - var injectionScope = new TestInjectionScope(42, _instanceProvider); - Assert.Equal(_instanceProvider, injectionScope.InstanceProvider); - } - - public TheInjectionScope(ITestOutputHelper output) : base(output) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheSequentializingBackendFxApplicationInvoker.cs b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheSequentializingBackendFxApplicationInvoker.cs deleted file mode 100644 index 97e901cd..00000000 --- a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheSequentializingBackendFxApplicationInvoker.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Backend.Fx.Environment.Authentication; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Patterns.DependencyInjection; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Patterns.DependencyInjection -{ - public class TheSequentializingBackendFxApplicationInvoker : TestWithLogging - { - private readonly ITestOutputHelper _output; - - public TheSequentializingBackendFxApplicationInvoker(ITestOutputHelper output): base(output) - { - _output = output; - var fakes = new DiTestFakes(); - _invoker = new BackendFxApplicationInvoker(fakes.CompositionRoot); - _decoratedInvoker = new SequentializingBackendFxApplicationInvoker(_invoker); - } - - private readonly int _actionDuration = 100; - private readonly IBackendFxApplicationInvoker _invoker; - private readonly IBackendFxApplicationInvoker _decoratedInvoker; - - private async Task InvokeSomeActions(int count, IBackendFxApplicationInvoker invoker) - { - var tasks = Enumerable - .Range(0, count) - .Select(i => Task.Run(() => invoker.Invoke(DoTheAction, new AnonymousIdentity(), new TenantId(1)))) - .ToArray(); - - await Task.WhenAll(tasks); - } - - private void DoTheAction(IInstanceProvider _) - { - _output.WriteLine("start"); - Thread.Sleep(_actionDuration); - _output.WriteLine("end"); - } - - [Fact] - public async Task IsReallyNeeded() - { - if (System.Environment.ProcessorCount > 2) - { - // negative test: without sequentialization all tasks run in parallel - var count = 10; - var sw = new Stopwatch(); - sw.Start(); - await InvokeSomeActions(count, _invoker); - long actualDuration = sw.ElapsedMilliseconds; - var expectedDuration = _actionDuration * count; - Assert.True(actualDuration < expectedDuration, - $"Actual duration of {actualDuration}ms is greater than maximum expected duration of {expectedDuration}ms"); - } - else - { - // fails on CI Pipeline due to CPU count - } - } - - [Fact] - public async Task SequentializesInvocations() - { - var count = 10; - var sw = new Stopwatch(); - sw.Start(); - await InvokeSomeActions(count, _decoratedInvoker); - Assert.True(sw.ElapsedMilliseconds >= _actionDuration * count); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/FailingDomainEventHandler.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/FailingDomainEventHandler.cs deleted file mode 100644 index f4c2f8d0..00000000 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/FailingDomainEventHandler.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using Backend.Fx.Patterns.EventAggregation.Domain; - -namespace Backend.Fx.Tests.Patterns.EventAggregation.Domain -{ - public class FailingDomainEventHandler : IDomainEventHandler - { - public void Handle(TestDomainEvent testDomainEvent) - { - throw new NotSupportedException(); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TestDomainEvent.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TestDomainEvent.cs deleted file mode 100644 index c9fb4e59..00000000 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TestDomainEvent.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Backend.Fx.Patterns.EventAggregation.Domain; - -namespace Backend.Fx.Tests.Patterns.EventAggregation.Domain -{ - public class TestDomainEvent : IDomainEvent - { - public TestDomainEvent(int id) - { - Id = id; - } - - public int Id { get; } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TestDomainEventHandler.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TestDomainEventHandler.cs deleted file mode 100644 index faa65c19..00000000 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TestDomainEventHandler.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; -using Backend.Fx.Patterns.EventAggregation.Domain; - -namespace Backend.Fx.Tests.Patterns.EventAggregation.Domain -{ - public class TestDomainEventHandler : IDomainEventHandler - { - public List Events { get; } = new List(); - - public void Handle(TestDomainEvent testDomainEvent) - { - Events.Add(testDomainEvent); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TestIntegrationEvent.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TestIntegrationEvent.cs deleted file mode 100644 index 0e121fcb..00000000 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TestIntegrationEvent.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Backend.Fx.Patterns.EventAggregation.Integration; -using JetBrains.Annotations; - -namespace Backend.Fx.Tests.Patterns.EventAggregation.Domain -{ - [UsedImplicitly] - public class TestIntegrationEvent : IntegrationEvent - { - public TestIntegrationEvent(int whatever) - { - Whatever = whatever; - } - - [UsedImplicitly] public int Whatever { get; } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TheEventAggregator.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TheEventAggregator.cs deleted file mode 100644 index 2c13c1fd..00000000 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TheEventAggregator.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using Backend.Fx.Patterns.EventAggregation.Domain; -using FakeItEasy; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Patterns.EventAggregation.Domain -{ - public class TheEventAggregator : TestWithLogging - { - [Fact] - public void CallsAllDomainEventHandlers() - { - var handler1 = new TestDomainEventHandler(); - var handler2 = new TestDomainEventHandler(); - var fakeDomainEventHandlerProvider = A.Fake(); - A.CallTo(() => fakeDomainEventHandlerProvider.GetAllEventHandlers()).Returns(new[] {handler1, handler2}); - - IDomainEventAggregator sut = new DomainEventAggregator(fakeDomainEventHandlerProvider); - - sut.PublishDomainEvent(new TestDomainEvent(4711)); - sut.RaiseEvents(); - - A.CallTo(() => fakeDomainEventHandlerProvider.GetAllEventHandlers()).MustHaveHappenedOnceExactly(); - - Assert.Single(handler1.Events); - Assert.Equal(4711, handler1.Events[0].Id); - - Assert.Single(handler2.Events); - Assert.Equal(4711, handler2.Events[0].Id); - - Assert.Equal(handler1.Events[0], handler2.Events[0]); - } - - [Fact] - public void DoesNotSwallowExceptionOnDomainEventHandling() - { - IDomainEventHandler handler = new FailingDomainEventHandler(); - var fakeDomainEventHandlerProvider = A.Fake(); - A.CallTo(() => fakeDomainEventHandlerProvider.GetAllEventHandlers()).Returns(new[] {handler}); - - IDomainEventAggregator sut = new DomainEventAggregator(fakeDomainEventHandlerProvider); - sut.PublishDomainEvent(new TestDomainEvent(444)); - Assert.Throws(() => sut.RaiseEvents()); - } - - public TheEventAggregator(ITestOutputHelper output) : base(output) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/BlockingMessageHandler.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/BlockingMessageHandler.cs deleted file mode 100644 index 52fa0fa5..00000000 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/BlockingMessageHandler.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Diagnostics; -using System.Threading; -using Backend.Fx.Patterns.EventAggregation.Integration; -using Xunit; - -namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration -{ - public class BlockingMessageHandler : IIntegrationMessageHandler - { - private readonly ManualResetEvent _manualResetEvent; - - public BlockingMessageHandler(ManualResetEvent manualResetEvent) - { - _manualResetEvent = manualResetEvent; - } - - public void Handle(TestIntegrationEvent eventData) - { - Assert.True( - _manualResetEvent.WaitOne(Debugger.IsAttached ? int.MaxValue : 1000), - "The BlockingMessageHandler was not reset."); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/DynamicMessageHandler.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/DynamicMessageHandler.cs deleted file mode 100644 index e0cafc60..00000000 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/DynamicMessageHandler.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Backend.Fx.Patterns.EventAggregation.Integration; - -namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration -{ - public class DynamicMessageHandler : IIntegrationMessageHandler - { - private readonly IIntegrationMessageHandler _integrationMessageHandlerImplementation; - - public DynamicMessageHandler(IIntegrationMessageHandler integrationMessageHandlerImplementation) - { - _integrationMessageHandlerImplementation = integrationMessageHandlerImplementation; - } - - public void Handle(dynamic eventData) - { - _integrationMessageHandlerImplementation.Handle(eventData); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/LongRunningMessageHandler.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/LongRunningMessageHandler.cs deleted file mode 100644 index 2cf3567c..00000000 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/LongRunningMessageHandler.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Threading; -using Backend.Fx.Patterns.EventAggregation.Integration; - -namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration -{ - public class LongRunningMessageHandler : IIntegrationMessageHandler - { - private readonly IIntegrationMessageHandler _handler; - - public LongRunningMessageHandler(IIntegrationMessageHandler handler) - { - _handler = handler; - } - - public void Handle(TestIntegrationEvent eventData) - { - Thread.Sleep(1000); - _handler.Handle(eventData); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/SerializingMessageBus.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/SerializingMessageBus.cs deleted file mode 100644 index 77af8d4d..00000000 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/SerializingMessageBus.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Threading.Tasks; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Patterns.EventAggregation.Integration; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration -{ - public class SerializingMessageBus : MessageBus - { - public override void Connect() - { - } - - protected override Task PublishOnMessageBus(IIntegrationEvent integrationEvent) - { - var jsonString = JsonConvert.SerializeObject(integrationEvent); - return Task.Run(() => Process(MessageNameProvider.GetMessageName(), new SerializingProcessingContext(jsonString))); - } - - protected override void Subscribe(string eventName) - { - } - - protected override void Unsubscribe(string eventName) - { - } - - private class SerializingProcessingContext : EventProcessingContext - { - private readonly string _jsonString; - - public SerializingProcessingContext(string jsonString) - { - _jsonString = jsonString; - var eventStub = JsonConvert.DeserializeAnonymousType(jsonString, new {tenantId = 0, correlationId = Guid.Empty}); - TenantId = new TenantId(eventStub.tenantId); - CorrelationId = eventStub.correlationId; - } - - public override TenantId TenantId { get; } - - public override dynamic DynamicEvent => JObject.Parse(_jsonString); - public override Guid CorrelationId { get; } - - public override IIntegrationEvent GetTypedEvent(Type eventType) - { - return (IIntegrationEvent) JsonConvert.DeserializeObject(_jsonString, eventType); - } - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TestIntegrationEvent.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TestIntegrationEvent.cs deleted file mode 100644 index 1393463c..00000000 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TestIntegrationEvent.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Threading; -using Backend.Fx.Patterns.EventAggregation.Integration; - -namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration -{ - public class TestIntegrationEvent : IntegrationEvent - { - public TestIntegrationEvent(int intParam, string stringParam) : base() - { - IntParam = intParam; - StringParam = stringParam; - } - - public int IntParam { get; } - - public string StringParam { get; } - - public ManualResetEventSlim TypedProcessed { get; } = new ManualResetEventSlim(false); - public ManualResetEventSlim DynamicProcessed { get; } = new ManualResetEventSlim(false); - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TheInMemoryMessageBusChannel.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TheInMemoryMessageBusChannel.cs deleted file mode 100644 index 022d346e..00000000 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TheInMemoryMessageBusChannel.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Backend.Fx.Patterns.EventAggregation.Integration; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration -{ - public class TheInMemoryMessageBusChannel : TestWithLogging - { - [Fact] - public async Task HandlesEventsAsynchronously() - { - var channel = new InMemoryMessageBusChannel(); - var messageBus = new InMemoryMessageBus(channel); - messageBus.Connect(); - messageBus.ProvideInvoker(new TheMessageBus.TestInvoker()); - - var handled = new ManualResetEvent(false); - messageBus.Subscribe(new BlockingMessageHandler(handled)); - - await messageBus.Publish(new TestIntegrationEvent(0, string.Empty)); - - var finishHandleTask = channel.FinishHandlingAllMessagesAsync(); - Assert.Contains(finishHandleTask.Status, new[] {TaskStatus.WaitingForActivation, TaskStatus.Running}); - handled.Set(); - - await finishHandleTask; - } - - [Fact] - public async Task InvokesAllApplicationHandlers() - { - var channel = new InMemoryMessageBusChannel(); - - var messageBus = new InMemoryMessageBus(channel); - var eventHandled = false; - messageBus.Connect(); - messageBus.ProvideInvoker(new TheMessageBus.TestInvoker()); - messageBus.Subscribe(new DelegateIntegrationMessageHandler(ev => eventHandled = true)); - - var anotherMessageBus = new InMemoryMessageBus(channel); - var anotherEventHandled = false; - anotherMessageBus.Connect(); - anotherMessageBus.ProvideInvoker(new TheMessageBus.TestInvoker()); - messageBus.Subscribe(new DelegateIntegrationMessageHandler(ev => anotherEventHandled = true)); - - await messageBus.Publish(new TestIntegrationEvent(0, string.Empty)); - await channel.FinishHandlingAllMessagesAsync(); - - Assert.True(eventHandled); - Assert.True(anotherEventHandled); - - eventHandled = false; - anotherEventHandled = false; - - await anotherMessageBus.Publish(new TestIntegrationEvent(0, string.Empty)); - await channel.FinishHandlingAllMessagesAsync(); - - Assert.True(eventHandled); - Assert.True(anotherEventHandled); - } - - [Fact] - public async Task DoesAwaitAllPendingMessages() - { - var channel = new InMemoryMessageBusChannel(); - var messageBus = new InMemoryMessageBus(channel); - messageBus.Connect(); - messageBus.ProvideInvoker(new TheMessageBus.TestInvoker()); - - var allMessagesAreHandled = false; - - messageBus.Subscribe(new DelegateIntegrationMessageHandler(x => - { - if (x.StringParam == "first message") - { - messageBus.Publish(new TestIntegrationEvent(0, "second message")); - } - else if (x.StringParam == "second message") - { - messageBus.Publish(new TestIntegrationEvent(0, "third message")); - } - else if (x.StringParam == "third message") - { - allMessagesAreHandled = true; - } - })); - - // Publish the first message and await the result. - // This should block until all three messages are processed not only the first one was. - await messageBus.Publish(new TestIntegrationEvent(0, "first message")); - await channel.FinishHandlingAllMessagesAsync(); - - Assert.True(allMessagesAreHandled); - } - - public TheInMemoryMessageBusChannel(ITestOutputHelper output) : base(output) - { - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TheMessageBus.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TheMessageBus.cs deleted file mode 100644 index 17c804d8..00000000 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TheMessageBus.cs +++ /dev/null @@ -1,190 +0,0 @@ -using System; -using System.Diagnostics; -using System.Security.Principal; -using System.Threading; -using System.Threading.Tasks; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.Patterns.EventAggregation.Integration; -using FakeItEasy; -using JetBrains.Annotations; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration -{ - public sealed class TheInMemoryMessageBus : TheMessageBus - { - public TheInMemoryMessageBus(ITestOutputHelper output): base(output) - { - Sut.ProvideInvoker(FakeApplication.Invoker); - Sut.Connect(); - } - - [Fact] - public async Task HandlesEventsAsynchronously() - { - Sut.Subscribe(); - var sw = new Stopwatch(); - sw.Start(); - await Sut.Publish(Invoker.IntegrationEvent); - Assert.True(sw.ElapsedMilliseconds < 900); - Assert.True(Invoker.IntegrationEvent.TypedProcessed.Wait(Debugger.IsAttached ? int.MaxValue : 10000)); - Assert.True(sw.ElapsedMilliseconds > 1000); - } - - protected override IMessageBus Sut { get; } = new InMemoryMessageBus(); - } - - [UsedImplicitly] - public sealed class TheSerializingMessageBus : TheMessageBus - { - public TheSerializingMessageBus(ITestOutputHelper output): base(output) - { - Sut.ProvideInvoker(FakeApplication.Invoker); - } - - protected override IMessageBus Sut { get; } = new SerializingMessageBus(); - } - - public abstract class TheMessageBus : TestWithLogging - { - protected IBackendFxApplication FakeApplication { get; } = A.Fake(); - - protected TheMessageBus(ITestOutputHelper output): base(output) - { - A.CallTo(() => FakeApplication.Invoker).Returns(Invoker); - A.CallTo(() => FakeApplication.WaitForBoot(A._, A._)).Returns(true); - } - - protected TestInvoker Invoker { get; } = new TestInvoker(); - protected abstract IMessageBus Sut { get; } - - - public class TestInvoker : IBackendFxApplicationInvoker - { - public TestIntegrationEvent IntegrationEvent = new TestIntegrationEvent(34, "gaga"); - - public TestInvoker() - { - A.CallTo(() => TypedHandler.Handle(A._)).Invokes((TestIntegrationEvent e) => IntegrationEvent.TypedProcessed.Set()); - A.CallTo(() => DynamicHandler.Handle(new object())).WithAnyArguments().Invokes((object e) => IntegrationEvent.DynamicProcessed.Set()); - - A.CallTo(() => FakeInstanceProvider.GetInstance(A.That.IsEqualTo(typeof(TypedMessageHandler)))) - .Returns(new TypedMessageHandler(TypedHandler)); - - A.CallTo(() => FakeInstanceProvider.GetInstance(A.That.IsEqualTo(typeof(LongRunningMessageHandler)))) - .Returns(new LongRunningMessageHandler(TypedHandler)); - - A.CallTo(() => FakeInstanceProvider.GetInstance(A.That.IsEqualTo(typeof(ThrowingTypedMessageHandler)))) - .Returns(new ThrowingTypedMessageHandler(TypedHandler)); - - A.CallTo(() => FakeInstanceProvider.GetInstance(A.That.IsEqualTo(typeof(DynamicMessageHandler)))) - .Returns(new DynamicMessageHandler(DynamicHandler)); - - A.CallTo(() => FakeInstanceProvider.GetInstance(A.That.IsEqualTo(typeof(ThrowingDynamicMessageHandler)))) - .Returns(new ThrowingDynamicMessageHandler(DynamicHandler)); - } - - public IIntegrationMessageHandler TypedHandler { get; } = A.Fake>(); - public IIntegrationMessageHandler DynamicHandler { get; } = A.Fake(); - public IInstanceProvider FakeInstanceProvider { get; } = A.Fake(); - - public void Invoke(Action action, IIdentity identity, TenantId tenantId, Guid? correlationId = null) - { - action(FakeInstanceProvider); - } - } - - [Fact] - public async void CallsDynamicEventHandler() - { - Sut.Subscribe(Sut.MessageNameProvider.GetMessageName()); - await Sut.Publish(Invoker.IntegrationEvent); - Assert.True(Invoker.IntegrationEvent.DynamicProcessed.Wait(Debugger.IsAttached ? int.MaxValue : 10000)); - - A.CallTo(() => Invoker.TypedHandler.Handle(A._)).MustNotHaveHappened(); - A.CallTo(() => Invoker.DynamicHandler.Handle(A._)).MustHaveHappenedOnceExactly(); - } - - [Fact] - public async void CallsMixedEventHandlers() - { - Sut.Subscribe(Sut.MessageNameProvider.GetMessageName()); - Sut.Subscribe(); - - await Sut.Publish(Invoker.IntegrationEvent); - Assert.True(Invoker.IntegrationEvent.TypedProcessed.Wait(Debugger.IsAttached ? int.MaxValue : 10000)); - Assert.True(Invoker.IntegrationEvent.DynamicProcessed.Wait(Debugger.IsAttached ? int.MaxValue : 10000)); - - A.CallTo(() => Invoker.TypedHandler.Handle(A - .That - .Matches(evt => evt.IntParam == 34 && evt.StringParam == "gaga"))) - .MustHaveHappenedOnceExactly(); - - A.CallTo(() => Invoker.DynamicHandler.Handle(A._)).MustHaveHappenedOnceExactly(); - } - - [Fact] - public async Task CallsTypedEventHandler() - { - Sut.Subscribe(); - await Sut.Publish(Invoker.IntegrationEvent); - Assert.True(Invoker.IntegrationEvent.TypedProcessed.Wait(Debugger.IsAttached ? int.MaxValue : 10000)); - A.CallTo(() => Invoker.TypedHandler.Handle(A - .That - .Matches(evt => evt.IntParam == 34 && evt.StringParam == "gaga"))) - .MustHaveHappenedOnceExactly(); - - A.CallTo(() => Invoker.DynamicHandler.Handle(A._)).MustNotHaveHappened(); - } - - [Fact] - public async Task DoesNotCallUnsubscribedTypedEventHandler() - { - Sut.Subscribe(); - Sut.Unsubscribe(); - await Sut.Publish(Invoker.IntegrationEvent); - A.CallTo(() => Invoker.TypedHandler.Handle(A - .That - .Matches(evt => evt.IntParam == 34 && evt.StringParam == "gaga"))) - .MustNotHaveHappened(); - } - - [Fact] - public async void DoesNotCallUnsubscribedDynamicEventHandler() - { - Sut.Subscribe(Sut.MessageNameProvider.GetMessageName()); - Sut.Unsubscribe(Sut.MessageNameProvider.GetMessageName()); - await Sut.Publish(Invoker.IntegrationEvent); - A.CallTo(() => Invoker.DynamicHandler.Handle(A._)).MustNotHaveHappened(); - } - - [Fact] - public async void DoesNotCallUnsubscribedDelegateEventHandler() - { - var handled = new ManualResetEvent(false); - var handler = new DelegateIntegrationMessageHandler(ev => handled.Set()); - Sut.Subscribe(handler); - Sut.Unsubscribe(handler); - await Sut.Publish(Invoker.IntegrationEvent); - Assert.False(handled.WaitOne(Debugger.IsAttached ? int.MaxValue : 1000)); - } - - [Fact] - public void CannCallConnectButItDoesNothing() - { - Sut.Connect(); - } - - [Fact] - public async void DelegateIntegrationMessageHandler() - { - var handled = new ManualResetEvent(false); - var handler = new DelegateIntegrationMessageHandler(ev => handled.Set()); - Sut.Subscribe(handler); - await Sut.Publish(Invoker.IntegrationEvent); - Assert.True(handled.WaitOne(Debugger.IsAttached ? int.MaxValue : 10000)); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TheMessageBusScope.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TheMessageBusScope.cs deleted file mode 100644 index eb5109a9..00000000 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TheMessageBusScope.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.Patterns.EventAggregation.Integration; -using FakeItEasy; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration -{ - public class TheMessageBusScope : TestWithLogging - { - private readonly IMessageBus _messageBus = A.Fake(); - private readonly ICurrentTHolder _currentCorrelationHolder = new CurrentCorrelationHolder(); - private readonly ICurrentTHolder _currentTenantIdHolder = new CurrentTenantIdHolder(); - private readonly IMessageBusScope _sut; - - public TheMessageBusScope(ITestOutputHelper output): base(output) - { - _currentTenantIdHolder.ReplaceCurrent(new TenantId(789)); - _sut = new MessageBusScope(_messageBus, _currentCorrelationHolder, _currentTenantIdHolder); - } - - [Fact] - public void MaintainsCorrelationIdOnPublish() - { - var testIntegrationEvent = new Domain.TestIntegrationEvent(44); - _sut.Publish(testIntegrationEvent); - Assert.Equal(_currentCorrelationHolder.Current.Id, testIntegrationEvent.CorrelationId); - } - - [Fact] - public void MaintainsTenantIdOnPublish() - { - var testIntegrationEvent = new Domain.TestIntegrationEvent(44); - _sut.Publish(testIntegrationEvent); - Assert.Equal(_currentTenantIdHolder.Current.Value, testIntegrationEvent.TenantId); - } - - [Fact] - public void DoesNotPublishOnBusWhenPublishing() - { - var testIntegrationEvent = new Domain.TestIntegrationEvent(44); - _sut.Publish(testIntegrationEvent); - A.CallTo(()=>_messageBus.Publish(A._)).MustNotHaveHappened(); - } - - [Fact] - public void PublishesAllEventsOnRaise() - { - var ev1 = new Domain.TestIntegrationEvent(44); - var ev2 = new Domain.TestIntegrationEvent(45); - var ev3 = new Domain.TestIntegrationEvent(46); - var ev4 = new Domain.TestIntegrationEvent(47); - _sut.Publish(ev1); - _sut.Publish(ev2); - _sut.Publish(ev3); - _sut.Publish(ev4); - _sut.RaiseEvents(); - A.CallTo(()=>_messageBus.Publish(A.That.IsEqualTo(ev1))).MustHaveHappenedOnceExactly(); - A.CallTo(()=>_messageBus.Publish(A.That.IsEqualTo(ev2))).MustHaveHappenedOnceExactly(); - A.CallTo(()=>_messageBus.Publish(A.That.IsEqualTo(ev3))).MustHaveHappenedOnceExactly(); - A.CallTo(()=>_messageBus.Publish(A.That.IsEqualTo(ev4))).MustHaveHappenedOnceExactly(); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/ThrowingDynamicEventHandler.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/ThrowingDynamicEventHandler.cs deleted file mode 100644 index a5dd498e..00000000 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/ThrowingDynamicEventHandler.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using Backend.Fx.Patterns.EventAggregation.Integration; - -namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration -{ - public class ThrowingDynamicMessageHandler : IIntegrationMessageHandler - { - public const string ExceptionMessage = "From ThrowingDynamicEventHandler"; - private readonly IIntegrationMessageHandler _handler; - - public ThrowingDynamicMessageHandler(IIntegrationMessageHandler handler) - { - _handler = handler; - } - - public void Handle(dynamic eventData) - { - _handler.Handle(eventData); - throw new InvalidOperationException(ExceptionMessage); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/ThrowingTypedMessageHandler.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/ThrowingTypedMessageHandler.cs deleted file mode 100644 index 3bdd71d9..00000000 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/ThrowingTypedMessageHandler.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using Backend.Fx.Patterns.EventAggregation.Integration; - -namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration -{ - public class ThrowingTypedMessageHandler : IIntegrationMessageHandler - { - public const string ExceptionMessage = "From ThrowingTypedEventHandler"; - private readonly IIntegrationMessageHandler _handler; - - public ThrowingTypedMessageHandler(IIntegrationMessageHandler handler) - { - _handler = handler; - } - - public void Handle(TestIntegrationEvent eventData) - { - _handler.Handle(eventData); - throw new InvalidOperationException(ExceptionMessage); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TypedMessageHandler.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TypedMessageHandler.cs deleted file mode 100644 index e76416b8..00000000 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TypedMessageHandler.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Backend.Fx.Patterns.EventAggregation.Integration; - -namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration -{ - public class TypedMessageHandler : IIntegrationMessageHandler - { - private readonly IIntegrationMessageHandler _integrationMessageHandlerImplementation; - - public TypedMessageHandler(IIntegrationMessageHandler integrationMessageHandlerImplementation) - { - _integrationMessageHandlerImplementation = integrationMessageHandlerImplementation; - } - - public void Handle(TestIntegrationEvent eventData) - { - _integrationMessageHandlerImplementation.Handle(eventData); - eventData.TypedProcessed.Set(); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/IdGeneration/TheHiLoIdGenerator.cs b/tests/Backend.Fx.Tests/Patterns/IdGeneration/TheHiLoIdGenerator.cs deleted file mode 100644 index 7f029538..00000000 --- a/tests/Backend.Fx.Tests/Patterns/IdGeneration/TheHiLoIdGenerator.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Linq; -using Backend.Fx.Patterns.IdGeneration; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Patterns.IdGeneration -{ - public class TheHiLoIdGenerator : TestWithLogging - { - private readonly HiLoIdGenerator _sut = new InMemoryHiLoIdGenerator(1, 100); - - private class IdConsument - { - public int[] Ids { get; private set; } - - public void GetIds(int count, IIdGenerator idGenerator) - { - Ids = new int[count]; - for (var i = 0; i < count; i++) Ids[i] = idGenerator.NextId(); - } - } - - [Fact] - public void AllowsMultipleThreadsToGetIds() - { - const int consumentCount = 50; - const int idCountPerConsument = 1000; - var idConsuments = new IdConsument[consumentCount]; - - for (var i = 0; i < consumentCount; i++) idConsuments[i] = new IdConsument(); - - idConsuments.AsParallel().ForAll(idConsument => { idConsument.GetIds(idCountPerConsument, _sut); }); - - var allIds = idConsuments.SelectMany(idConsument => idConsument.Ids).ToArray(); - Assert.Equal(consumentCount * idCountPerConsument, allIds.Length); - Assert.Equal(consumentCount * idCountPerConsument, allIds.Distinct().Count()); - Assert.Equal(consumentCount * idCountPerConsument + 1, _sut.NextId()); - } - - [Fact] - public void StartsWithInitialValueAndCountsUp() - { - for (var i = 1; i < 1000; i++) Assert.Equal(i, _sut.NextId()); - } - - public TheHiLoIdGenerator(ITestOutputHelper output) : base(output) - { - } - } - - public class InMemoryHiLoIdGenerator : HiLoIdGenerator - { - private readonly object _synclock = new object(); - private int _nextBlockStart; - - public InMemoryHiLoIdGenerator(int start, int increment) - { - _nextBlockStart = start; - BlockSize = increment; - } - - protected override int BlockSize { get; } - - protected override int GetNextBlockStart() - { - lock (_synclock) - { - // this simulates the behavior of a SQL sequence for example - var result = _nextBlockStart; - _nextBlockStart += BlockSize; - return result; - } - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/Transactions/TheDbTransactionOperationDecorator.cs b/tests/Backend.Fx.Tests/Patterns/Transactions/TheDbTransactionOperationDecorator.cs deleted file mode 100644 index 1f1998f9..00000000 --- a/tests/Backend.Fx.Tests/Patterns/Transactions/TheDbTransactionOperationDecorator.cs +++ /dev/null @@ -1,203 +0,0 @@ -using System; -using System.Data; -using Backend.Fx.Environment.Persistence; -using Backend.Fx.Patterns.DependencyInjection; -using FakeItEasy; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Patterns.Transactions -{ - public class TheDbTransactionOperationDecorator : TestWithLogging - { - public TheDbTransactionOperationDecorator(ITestOutputHelper output): base(output) - { - _sut = new DbTransactionOperationDecorator(_dbConnection, _operation); - A.CallTo(() => _dbConnection.BeginTransaction(A._)).Returns(_dbTransaction); - } - - private readonly IDbConnection _dbConnection = A.Fake(); - private readonly IDbTransaction _dbTransaction = A.Fake(); - private readonly IOperation _operation = new Operation(); - private readonly DbTransactionOperationDecorator _sut; - - [Theory] - [InlineData(IsolationLevel.ReadCommitted)] - [InlineData(IsolationLevel.ReadUncommitted)] - [InlineData(IsolationLevel.Serializable)] - public void BeginsTransactionInSpecificIsolationLevel(IsolationLevel isolationLevel) - { - _sut.SetIsolationLevel(isolationLevel); - A.CallTo(() => _dbConnection.State).Returns(ConnectionState.Open); - _sut.Begin(); - A.CallTo(() => _dbConnection.BeginTransaction(A.That.IsEqualTo(isolationLevel))).MustHaveHappenedOnceExactly(); - Assert.Equal(_sut.CurrentTransaction, _dbTransaction); - } - - [Fact] - public void BeginsTransactionInUnspecifiedIsolationLevel() - { - A.CallTo(() => _dbConnection.State).Returns(ConnectionState.Open); - _sut.Begin(); - A.CallTo(() => _dbConnection.BeginTransaction(A.That.IsEqualTo(IsolationLevel.Unspecified))).MustHaveHappenedOnceExactly(); - Assert.Equal(_sut.CurrentTransaction, _dbTransaction); - } - - [Theory] - [InlineData(ConnectionState.Broken)] - [InlineData(ConnectionState.Connecting)] - [InlineData(ConnectionState.Executing)] - [InlineData(ConnectionState.Fetching)] - public void CannotBeginWithConnectionIsInWrongState(ConnectionState wrongState) - { - A.CallTo(() => _dbConnection.State).Returns(wrongState); - var sut = new DbTransactionOperationDecorator(_dbConnection, _operation); - Assert.Throws(() => sut.Begin()); - } - - [Fact] - public void ClosesAndDisposesOnCancel() - { - A.CallTo(() => _dbConnection.State).Returns(ConnectionState.Closed); - _sut.Begin(); - A.CallTo(() => _dbConnection.State).Returns(ConnectionState.Open); - _sut.Cancel(); - A.CallTo(() => _dbConnection.Close()).MustHaveHappenedOnceExactly(); - A.CallTo(() => _dbTransaction.Dispose()).MustHaveHappenedOnceExactly(); - A.CallTo(() => _dbTransaction.Commit()).MustNotHaveHappened(); - } - - [Fact] - public void DisposesOnCancel() - { - A.CallTo(() => _dbConnection.State).Returns(ConnectionState.Open); - _sut.Begin(); - _sut.Cancel(); - A.CallTo(() => _dbConnection.Close()).MustNotHaveHappened(); - A.CallTo(() => _dbTransaction.Dispose()).MustHaveHappenedOnceExactly(); - } - - [Fact] - public void DoesNotAllowToChangeIsolationLevenWhenBegun() - { - _sut.SetIsolationLevel(IsolationLevel.ReadCommitted); - A.CallTo(() => _dbConnection.State).Returns(ConnectionState.Open); - _sut.Begin(); - Assert.Throws(() => _sut.SetIsolationLevel(IsolationLevel.Chaos)); - } - - [Fact] - public void DoesNotCommitbutRollbackOnCancel() - { - A.CallTo(() => _dbConnection.State).Returns(ConnectionState.Open); - _sut.Begin(); - A.CallTo(() => _dbConnection.Open()).MustNotHaveHappened(); - _sut.Cancel(); - A.CallTo(() => _dbTransaction.Commit()).MustNotHaveHappened(); - A.CallTo(() => _dbTransaction.Rollback()).MustHaveHappenedOnceExactly(); - } - - [Fact] - public void DoesNotMaintainConnectionStateOnCompleteWhenProvidingOpenConnection() - { - A.CallTo(() => _dbConnection.State).Returns(ConnectionState.Open); - - _sut.Begin(); - A.CallTo(() => _dbConnection.Open()).MustNotHaveHappened(); - _sut.Complete(); - A.CallTo(() => _dbConnection.Close()).MustNotHaveHappened(); - } - - [Fact] - public void DoesNotMaintainConnectionStateOnCancelWhenProvidingOpenConnection() - { - A.CallTo(() => _dbConnection.State).Returns(ConnectionState.Open); - - _sut.Begin(); - A.CallTo(() => _dbConnection.Open()).MustNotHaveHappened(); - _sut.Cancel(); - A.CallTo(() => _dbConnection.Close()).MustNotHaveHappened(); - } - - [Fact] - public void DoesNotRollbackButCommitOnComplete() - { - A.CallTo(() => _dbConnection.State).Returns(ConnectionState.Open); - _sut.Begin(); - A.CallTo(() => _dbConnection.Open()).MustNotHaveHappened(); - _sut.Complete(); - A.CallTo(() => _dbTransaction.Rollback()).MustNotHaveHappened(); - A.CallTo(() => _dbTransaction.Commit()).MustHaveHappenedOnceExactly(); - } - - [Fact] - public void InitializesWithoutCurrentTransaction() - { - Assert.Null(_sut.CurrentTransaction); - } - - [Fact] - public void MaintainsConnectionStateOnCompleteWhenProvidingClosedConnection() - { - A.CallTo(() => _dbConnection.State).Returns(ConnectionState.Closed); - _sut.Begin(); - A.CallTo(() => _dbConnection.Open()).MustHaveHappenedOnceExactly(); - _sut.Complete(); - A.CallTo(() => _dbConnection.Close()).MustHaveHappenedOnceExactly(); - - } - - [Fact] - public void MaintainsConnectionStateOnCancelWhenProvidingClosedConnection() - { - A.CallTo(() => _dbConnection.State).Returns(ConnectionState.Closed); - - _sut.Begin(); - A.CallTo(() => _dbConnection.Open()).MustHaveHappenedOnceExactly(); - _sut.Cancel(); - A.CallTo(() => _dbConnection.Close()).MustHaveHappenedOnceExactly(); - } - - [Fact] - public void UsesGivenConnection() - { - _sut.Begin(); - A.CallTo(() => _dbConnection.BeginTransaction(A._)).MustHaveHappenedOnceExactly(); - } - - [Fact] - public void DoesNotAllowBeginWhenActive() - { - _sut.Begin(); - Assert.Throws(() => _sut.Begin()); - } - - [Fact] - public void DoesNotAllowCompleteWhenNotStarted() - { - Assert.Throws(() => _sut.Complete()); - } - - [Fact] - public void DoesNotAllowCancelWhenNotStarted() - { - Assert.Throws(() => _sut.Cancel()); - } - - [Fact] - public void DoesNotAllowCancelWhenCompleted() - { - _sut.Begin(); - _sut.Complete(); - Assert.Throws(() => _sut.Cancel()); - } - - [Fact] - public void DoesNotAllowCompleteWhenRolledBack() - { - _sut.Begin(); - _sut.Cancel(); - Assert.Throws(() => _sut.Complete()); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/Transactions/TheReadonlyDecorator.cs b/tests/Backend.Fx.Tests/Patterns/Transactions/TheReadonlyDecorator.cs deleted file mode 100644 index c8a399c9..00000000 --- a/tests/Backend.Fx.Tests/Patterns/Transactions/TheReadonlyDecorator.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Backend.Fx.Environment.Persistence; -using Backend.Fx.Patterns.DependencyInjection; -using FakeItEasy; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.Patterns.Transactions -{ - public class TheReadonlyDecorator : TestWithLogging - { - private readonly IOperation _operation; - private readonly IOperation _sut; - - public TheReadonlyDecorator(ITestOutputHelper output): base(output) - { - _operation = A.Fake(); - _sut = new ReadonlyDbTransactionOperationDecorator(_operation); - } - - [Fact] - public void DelegatesOtherCalls() - { - _sut.Begin(); - A.CallTo(() => _operation.Begin()).MustHaveHappenedOnceExactly(); - - _sut.Cancel(); - A.CallTo(() => _operation.Cancel()).MustHaveHappenedOnceExactly(); - } - - [Fact] - public void CancelsOperationOnComplete() - { - _sut.Begin(); - _sut.Complete(); - A.CallTo(() => _operation.Complete()).MustNotHaveHappened(); - A.CallTo(() => _operation.Cancel()).MustHaveHappenedOnceExactly(); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/RandomData/TheGenerators.cs b/tests/Backend.Fx.Tests/RandomData/TheGenerators.cs deleted file mode 100644 index abd38469..00000000 --- a/tests/Backend.Fx.Tests/RandomData/TheGenerators.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Linq; -using Backend.Fx.RandomData; -using Xunit; -using Xunit.Abstractions; - -namespace Backend.Fx.Tests.RandomData -{ - public class TheLandLineGenerator : TheGenerator - { - public TheLandLineGenerator(ITestOutputHelper output) : base(output) - { - } - } - public class TheMobileLineGenerator : TheGenerator - { - public TheMobileLineGenerator(ITestOutputHelper output) : base(output) - { - } - } - public class TheTestAddressGenerator : TheGenerator - { - public TheTestAddressGenerator(ITestOutputHelper output) : base(output) - { - } - } - public class TheTestPersonGenerator : TheGenerator - { - public TheTestPersonGenerator(ITestOutputHelper output) : base(output) - { - } - } - - public class TheLoremIpsumGenerator : TheGenerator - { - [Fact] - public void GeneratesAsExpected() - { - var sentence = LoremIpsumGenerator.Generate(10, 10, true); - Assert.Equal(10, sentence.Split(" ").Length); - Assert.True(sentence.EndsWith('.')); - - sentence = LoremIpsumGenerator.Generate(10, 10, false); - Assert.Equal(10, sentence.Split(" ").Length); - Assert.False(sentence.EndsWith('.')); - } - - public TheLoremIpsumGenerator(ITestOutputHelper output) : base(output) - { - } - } - - public abstract class TheGenerator : TestWithLogging where TGen : Generator, new() - { - private readonly TGen _sut; - - protected TheGenerator(ITestOutputHelper output): base(output) - { - _sut = new TGen(); - } - - [Fact] - public void CanGenerateMany() - { - T[] generated = _sut.Take(100).ToArray(); - Assert.Equal(100, generated.Length); - Assert.Equal(100, generated.Distinct().Count()); - } - - [Fact] - public void GeneratesNotEqual() - { - T[] generated = _sut.Take(100).ToArray(); - Assert.Equal(100, generated.Distinct().Count()); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/TheBackendFxApplication.cs b/tests/Backend.Fx.Tests/TheBackendFxApplication.cs new file mode 100644 index 00000000..8bca4bf9 --- /dev/null +++ b/tests/Backend.Fx.Tests/TheBackendFxApplication.cs @@ -0,0 +1,280 @@ +using System; +using System.Diagnostics; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.DependencyInjection; +using Backend.Fx.ExecutionPipeline; +using Backend.Fx.Features; +using Backend.Fx.Features.DataGeneration; +using Backend.Fx.Features.DomainEvents; +using Backend.Fx.Features.DomainServices; +using Backend.Fx.Features.IdGeneration; +using Backend.Fx.Features.Jobs; +using Backend.Fx.Logging; +using Backend.Fx.MicrosoftDependencyInjection; +using Backend.Fx.SimpleInjectorDependencyInjection; +using Backend.Fx.Tests.DummyServices; +using Backend.Fx.TestUtil; +using Backend.Fx.Util; +using FakeItEasy; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using NodaTime; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.Tests; + +public abstract class TheBackendFxApplication : TestWithLogging +{ + private readonly IBackendFxApplication _sut; + private readonly IExceptionLogger _exceptionLogger = A.Fake(); + private readonly IEntityIdGenerator _entityIdGenerator = A.Fake>(); + private readonly DummyServicesFeature _dummyServicesFeature = new(); + + protected TheBackendFxApplication(ICompositionRoot compositionRoot, ITestOutputHelper output) : base(output) + { + _sut = new BackendFxApplication(compositionRoot, _exceptionLogger, GetType().Assembly); + _sut.EnableFeature(new JobsFeature()); + _sut.EnableFeature(new DataGenerationFeature()); + _sut.EnableFeature(new DomainEventsFeature()); + _sut.EnableFeature(new DomainServicesFeature()); + _sut.EnableFeature(new IdGenerationFeature(_entityIdGenerator)); + _sut.EnableFeature(_dummyServicesFeature); + } + + [Fact] + public async Task DoesNotAllowEnablingFeaturesWhenBooted() + { + await _sut.BootAsync(); + Assert.Throws(() => _sut.EnableFeature(new DomainEventsFeature())); + } + + [Fact] + public async Task DoesNotSwallowExceptions() + { + await _sut.BootAsync(); + await Assert.ThrowsAsync( + async () => await _sut.Invoker.InvokeAsync( + _ => throw new DivideByZeroException(), new AnonymousIdentity())); + } + + [Fact] + public async Task LogsExceptions() + { + await _sut.BootAsync(); + try + { + await _sut.Invoker.InvokeAsync(_ => throw new DivideByZeroException(), new AnonymousIdentity()); + } + catch (DivideByZeroException) + { + // expected + } + + A.CallTo(() => _sut.ExceptionLogger.LogException(A._)).MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task DoesNotWaitForBootWhenBooted() + { + await _sut.BootAsync(); + + await _sut.WaitForBootAsync(); + } + + [Fact] + public async Task CanWaitForBoot() + { + const int delayMillisecondsOnBoot = 300; + _sut.EnableFeature(new SlowBootingFeature(delayMillisecondsOnBoot)); + var booting = _sut.BootAsync(); + Assert.False(booting.IsCompleted); + var sw = new Stopwatch(); + sw.Start(); + await _sut.WaitForBootAsync(); + Assert.True(booting.IsCompleted); + Assert.True(sw.ElapsedMilliseconds >= delayMillisecondsOnBoot * 0.95); + } + + [Fact] + public async Task HasInjectedClock() + { + await _sut.BootAsync(); + await _sut.Invoker.InvokeAsync(sp => + { + Assert.IsType(sp.GetRequiredService()); + return Task.CompletedTask; + }, new SystemIdentity()); + } + + [Fact] + public async Task HasInjectedIdentityHolder() + { + await _sut.BootAsync(); + await _sut.Invoker.InvokeAsync(sp => + { + var identityHolder = sp.GetRequiredService>(); + Assert.IsType(identityHolder); + Assert.Equal(new SystemIdentity(), identityHolder.Current); + return Task.CompletedTask; + }, new SystemIdentity()); + } + + [Fact] + public async Task HasInjectedCorrelationHolder() + { + await _sut.BootAsync(); + var firstCorrelationId = Guid.Empty; + await _sut.Invoker.InvokeAsync(sp => + { + var correlationHolder = sp.GetRequiredService>(); + Assert.IsType(correlationHolder); + firstCorrelationId = correlationHolder.Current.Id; + return Task.CompletedTask; + }, new SystemIdentity()); + + await _sut.Invoker.InvokeAsync(sp => + { + var correlationHolder = sp.GetRequiredService>(); + Assert.IsType(correlationHolder); + Assert.NotEqual(firstCorrelationId, correlationHolder.Current.Id); + return Task.CompletedTask; + }, new SystemIdentity()); + } + + [Fact] + public async Task DisposesTheCompositionRoot() + { + var compositionRoot = A.Fake(); + + using (var sut = new BackendFxApplication(compositionRoot, _exceptionLogger, GetType().Assembly)) + { + sut.CompositionRoot.RegisterModules(new DummyServicesModule()); + await sut.BootAsync(); + } + + A.CallTo(() => compositionRoot.Dispose()).MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task WrapsInvocationsWithOperation() + { + await _sut.BootAsync(); + + await _sut.Invoker.InvokeAsync(_ => + { + A.CallTo(() => + _dummyServicesFeature.Spies.OperationSpy.BeginAsync(A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _dummyServicesFeature.Spies.OperationSpy.CompleteAsync(A._)) + .MustNotHaveHappened(); + A.CallTo(() => _dummyServicesFeature.Spies.OperationSpy.CancelAsync(A._)) + .MustNotHaveHappened(); + + return Task.CompletedTask; + }); + + A.CallTo(() => _dummyServicesFeature.Spies.OperationSpy.BeginAsync(A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _dummyServicesFeature.Spies.OperationSpy.CompleteAsync(A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _dummyServicesFeature.Spies.OperationSpy.CancelAsync(A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task DoesNotAllowToBeginOperationTwice() + { + await _sut.BootAsync(); + + await Assert.ThrowsAsync(async () => await _sut.Invoker.InvokeAsync(async sp => + { + await sp.GetRequiredService().BeginAsync(sp.CreateScope()); + })); + } + + [Fact] + public async Task DoesNotAllowToCompleteOperationTwice() + { + await _sut.BootAsync(); + + await Assert.ThrowsAsync(async () => await _sut.Invoker.InvokeAsync(sp => + { + sp.GetRequiredService().CompleteAsync(); + return Task.CompletedTask; + })); + } + + [Fact] + public async Task CancelsOperationWhenInvocationFails() + { + await _sut.BootAsync(); + + await Assert.ThrowsAsync(async () => await _sut.Invoker.InvokeAsync(_ => + { + A.CallTo(() => + _dummyServicesFeature.Spies.OperationSpy.BeginAsync(A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _dummyServicesFeature.Spies.OperationSpy.CompleteAsync(A._)) + .MustNotHaveHappened(); + A.CallTo(() => _dummyServicesFeature.Spies.OperationSpy.CancelAsync(A._)) + .MustNotHaveHappened(); + + throw new DivideByZeroException(); + })); + + A.CallTo(() => _dummyServicesFeature.Spies.OperationSpy.BeginAsync(A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _dummyServicesFeature.Spies.OperationSpy.CompleteAsync(A._)) + .MustNotHaveHappened(); + A.CallTo(() => _dummyServicesFeature.Spies.OperationSpy.CancelAsync(A._)) + .MustHaveHappenedOnceExactly(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _sut.Dispose(); + } + } + + private class SlowBootingFeature : Feature, IBootableFeature + { + private readonly int _delayMillisecondsOnBoot; + + public SlowBootingFeature(int delayMillisecondsOnBoot) + { + _delayMillisecondsOnBoot = delayMillisecondsOnBoot; + } + + public override void Enable(IBackendFxApplication application) + { + } + + public async Task BootAsync(IBackendFxApplication application, CancellationToken cancellationToken = default) + { + await Task.Delay(_delayMillisecondsOnBoot, cancellationToken); + } + } + + [UsedImplicitly] + public class TheBackendFxApplicationWithMicrosoftDI : TheBackendFxApplication + { + public TheBackendFxApplicationWithMicrosoftDI(ITestOutputHelper output) + : base(new MicrosoftCompositionRoot(), output) + { + } + } + + [UsedImplicitly] + public class TheBackendFxApplicationWithSimpleInjector : TheBackendFxApplication + { + public TheBackendFxApplicationWithSimpleInjector(ITestOutputHelper output) + : base(new SimpleInjectorCompositionRoot(), output) + { + } + } +} \ No newline at end of file