diff --git a/Build/CommonAssemblyInfo.cs b/Build/CommonAssemblyInfo.cs index a600459..0a46fdb 100644 --- a/Build/CommonAssemblyInfo.cs +++ b/Build/CommonAssemblyInfo.cs @@ -1,5 +1,4 @@ -using System; -using System.Reflection; +using System.Reflection; using System.Resources; using System.Runtime.InteropServices; @@ -8,6 +7,5 @@ [assembly: AssemblyVersion("1.0.0.0")] // Used by the runtime to bind to strongly named assemblies, format : MAJOR.0.0.0 [assembly: AssemblyFileVersion("1.0.0.0")] // Used to uniquely identify a build of the individual assembly, format : MAJOR.MINOR.BUILD.REVISION [assembly: AssemblyInformationalVersion("1.0.0")] // Used to represent the product version, format : MAJOR.MINOR.PATCH -[assembly: CLSCompliant(true)] [assembly: ComVisible(false)] [assembly: NeutralResourcesLanguage("en-US")] \ No newline at end of file diff --git a/DDD.sln b/DDD.sln index cc90354..a637b7b 100644 --- a/DDD.sln +++ b/DDD.sln @@ -1,61 +1,86 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29519.181 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33213.308 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Src", "Src", "{7080D95A-39E8-418A-BA03-99ED89D4020E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.Abstractions", "Src\DDD.Core.Abstractions\DDD.Core.Abstractions.csproj", "{596A8700-3D18-4A62-B200-1F78A9EA4617}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.Abstractions", "Src\DDD.Core.Abstractions\DDD.Core.Abstractions.csproj", "{596A8700-3D18-4A62-B200-1F78A9EA4617}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{1DF607A2-5750-4BF4-AB2F-E21EC51ECCA0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.UnitTests", "Test\DDD.Core.UnitTests\DDD.Core.UnitTests.csproj", "{63505329-BD97-4C75-B7D7-77FB42670E79}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.UnitTests", "Test\DDD.Core.UnitTests\DDD.Core.UnitTests.csproj", "{63505329-BD97-4C75-B7D7-77FB42670E79}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.Xunit", "Src\DDD.Core.Xunit\DDD.Core.Xunit.csproj", "{62B652DE-4CC9-4B85-8AE9-D66CEEA358DD}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.Xunit", "Src\DDD.Core.Xunit\DDD.Core.Xunit.csproj", "{62B652DE-4CC9-4B85-8AE9-D66CEEA358DD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.Dapper", "Src\DDD.Core.Dapper\DDD.Core.Dapper.csproj", "{701DA58B-AE36-429F-8621-64109B8D29D7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.Dapper", "Src\DDD.Core.Dapper\DDD.Core.Dapper.csproj", "{701DA58B-AE36-429F-8621-64109B8D29D7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.Abstractions.UnitTests", "Test\DDD.Core.Abstractions.UnitTests\DDD.Core.Abstractions.UnitTests.csproj", "{9CC062C7-73EA-49B4-B694-31A5F48FD09D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.Abstractions.UnitTests", "Test\DDD.Core.Abstractions.UnitTests\DDD.Core.Abstractions.UnitTests.csproj", "{9CC062C7-73EA-49B4-B694-31A5F48FD09D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core", "Src\DDD.Core\DDD.Core.csproj", "{C6C3E419-B9AA-44AD-9DBF-789294687AE6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core", "Src\DDD.Core\DDD.Core.csproj", "{C6C3E419-B9AA-44AD-9DBF-789294687AE6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.HealthcareDelivery", "Src\DDD.HealthcareDelivery\DDD.HealthcareDelivery.csproj", "{5B8FFFD3-9A1C-4620-9DB3-CD76CD9E79BF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.HealthcareDelivery", "Src\DDD.HealthcareDelivery\DDD.HealthcareDelivery.csproj", "{5B8FFFD3-9A1C-4620-9DB3-CD76CD9E79BF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Common", "Src\DDD.Common\DDD.Common.csproj", "{0B70B4FD-F5A0-4A6C-A3FD-90031E08C1C2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Common", "Src\DDD.Common\DDD.Common.csproj", "{0B70B4FD-F5A0-4A6C-A3FD-90031E08C1C2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Common.UnitTests", "Test\DDD.Common.UnitTests\DDD.Common.UnitTests.csproj", "{25F6B88A-4EFA-4516-BB7A-34ED68548636}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Common.UnitTests", "Test\DDD.Common.UnitTests\DDD.Common.UnitTests.csproj", "{25F6B88A-4EFA-4516-BB7A-34ED68548636}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.EF", "Src\DDD.Core.EF\DDD.Core.EF.csproj", "{6D227AA7-FF90-48CA-B13D-ED23C1FFFBA5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.HealthcareDelivery.IntegrationTests", "Test\DDD.HealthcareDelivery.IntegrationTests\DDD.HealthcareDelivery.IntegrationTests.csproj", "{B53007C7-B314-40DF-B6E7-6C6576A5611C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.HealthcareDelivery.IntegrationTests", "Test\DDD.HealthcareDelivery.IntegrationTests\DDD.HealthcareDelivery.IntegrationTests.csproj", "{B53007C7-B314-40DF-B6E7-6C6576A5611C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.FluentValidation", "Src\DDD.Core.FluentValidation\DDD.Core.FluentValidation.csproj", "{5E3745FC-CA80-4D0F-8A25-20EE0F9CF163}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.FluentValidation", "Src\DDD.Core.FluentValidation\DDD.Core.FluentValidation.csproj", "{5E3745FC-CA80-4D0F-8A25-20EE0F9CF163}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.HealthcareDelivery.UnitTests", "Test\DDD.HealthcareDelivery.UnitTests\DDD.HealthcareDelivery.UnitTests.csproj", "{CA376D7C-2A71-4518-B297-4C4A08DBF19D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.HealthcareDelivery.UnitTests", "Test\DDD.HealthcareDelivery.UnitTests\DDD.HealthcareDelivery.UnitTests.csproj", "{CA376D7C-2A71-4518-B297-4C4A08DBF19D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Doc", "Doc", "{862B0E9C-B685-4C40-ACFB-83A193C82254}" ProjectSection(SolutionItems) = preProject Doc\CommandComponents.png = Doc\CommandComponents.png + Doc\DeploymentEvolution.png = Doc\DeploymentEvolution.png Doc\EntityFrameworkMapping.png = Doc\EntityFrameworkMapping.png Doc\NHibernateMapping.png = Doc\NHibernateMapping.png Doc\PrescriptionLifecycle.png = Doc\PrescriptionLifecycle.png Doc\QueryComponents.png = Doc\QueryComponents.png EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.Newtonsoft", "Src\DDD.Core.Newtonsoft\DDD.Core.Newtonsoft.csproj", "{8BF8E0BD-B92C-4FC1-BE17-7C10036A2FE2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.Newtonsoft", "Src\DDD.Core.Newtonsoft\DDD.Core.Newtonsoft.csproj", "{8BF8E0BD-B92C-4FC1-BE17-7C10036A2FE2}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{C4C73BC8-47A5-4D1C-AFA6-10B16DC8BBEE}" ProjectSection(SolutionItems) = preProject Build\CommonAssemblyInfo.cs = Build\CommonAssemblyInfo.cs EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.Messages", "Src\DDD.Core.Messages\DDD.Core.Messages.csproj", "{2438B31A-3A39-4878-81FA-BE5AE715EAE5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.Messages", "Src\DDD.Core.Messages\DDD.Core.Messages.csproj", "{2438B31A-3A39-4878-81FA-BE5AE715EAE5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.HealthcareDelivery.Messages", "Src\DDD.HealthcareDelivery.Messages\DDD.HealthcareDelivery.Messages.csproj", "{B8BB212C-8AFC-4258-A023-EB1F6937F53D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Common.Messages", "Src\DDD.Common.Messages\DDD.Common.Messages.csproj", "{40A849C5-C8D7-4F76-856A-138AED73A6C3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.NHibernate", "Src\DDD.Core.NHibernate\DDD.Core.NHibernate.csproj", "{D4FC7E1B-CAE8-40F9-9FAF-0029D91CC6C4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Common.NHibernate", "Src\DDD.Common.NHibernate\DDD.Common.NHibernate.csproj", "{7466B831-1642-4B2B-9EA3-1C9299596C2A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.SimpleInjector", "Src\DDD.Core.SimpleInjector\DDD.Core.SimpleInjector.csproj", "{3EFAACD8-CF5E-4E31-884B-6B9F87F1E495}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.Polly", "Src\DDD.Core.Polly\DDD.Core.Polly.csproj", "{4878A4FB-2C1E-4C06-BB42-AFE1ACE2A882}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.Newtonsoft.UnitTests", "Test\DDD.Core.Newtonsoft.UnitTests\DDD.Core.Newtonsoft.UnitTests.csproj", "{501F9D19-0C3A-4E46-BF8E-7B87C22CFF56}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.Cronos", "Src\DDD.Core.Cronos\DDD.Core.Cronos.csproj", "{3FB0CF18-2BF6-4C1C-AF42-90B22E683BAF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.Dapper.Oracle", "Src\DDD.Core.Dapper.Oracle\DDD.Core.Dapper.Oracle.csproj", "{C3A2523B-9620-45B9-98A4-8B5311C3731F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.Dapper.IntegrationTests", "Test\DDD.Core.Dapper.IntegrationTests\DDD.Core.Dapper.IntegrationTests.csproj", "{E0A3A275-1AB4-4E7A-8A0F-15DA7644A6E6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.NSubstitute", "Src\DDD.Core.NSubstitute\DDD.Core.NSubstitute.csproj", "{48A75D67-FA07-49B0-AAC7-A78CC04441C7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.SimpleInjector.FluentValidation", "Src\DDD.Core.SimpleInjector.FluentValidation\DDD.Core.SimpleInjector.FluentValidation.csproj", "{FE8E2C2B-27B7-40ED-BFAA-10072825F397}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.SimpleInjector.FluentValidation.UnitTests", "Test\DDD.Core.SimpleInjector.FluentValidation.UnitTests\DDD.Core.SimpleInjector.FluentValidation.UnitTests.csproj", "{0544424F-7648-429E-963A-EE443815AB2E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.HealthcareDelivery.Messages", "Src\DDD.HealthcareDelivery.Messages\DDD.HealthcareDelivery.Messages.csproj", "{B8BB212C-8AFC-4258-A023-EB1F6937F53D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.SimpleInjector.UnitTests", "Test\DDD.Core.SimpleInjector.UnitTests\DDD.Core.SimpleInjector.UnitTests.csproj", "{75CA9FEB-E304-4A80-95B9-1D3E2118C091}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Common.Messages", "Src\DDD.Common.Messages\DDD.Common.Messages.csproj", "{40A849C5-C8D7-4F76-856A-138AED73A6C3}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.SimpleInjector.NHibernate", "Src\DDD.Core.SimpleInjector.NHibernate\DDD.Core.SimpleInjector.NHibernate.csproj", "{6B9284D6-DFFF-4902-8165-C05685299C10}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.NServiceBus", "Src\DDD.Core.NServiceBus\DDD.Core.NServiceBus.csproj", "{436D869F-7566-4436-90A8-B655145C5BCA}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.SimpleInjector.NHibernate.UnitTests", "Test\DDD.Core.SimpleInjector.NHibernate.UnitTests\DDD.Core.SimpleInjector.NHibernate.UnitTests.csproj", "{A51FFF86-9483-4380-B334-673D79B4DC6D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -99,10 +124,6 @@ Global {25F6B88A-4EFA-4516-BB7A-34ED68548636}.Debug|Any CPU.Build.0 = Debug|Any CPU {25F6B88A-4EFA-4516-BB7A-34ED68548636}.Release|Any CPU.ActiveCfg = Release|Any CPU {25F6B88A-4EFA-4516-BB7A-34ED68548636}.Release|Any CPU.Build.0 = Release|Any CPU - {6D227AA7-FF90-48CA-B13D-ED23C1FFFBA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6D227AA7-FF90-48CA-B13D-ED23C1FFFBA5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6D227AA7-FF90-48CA-B13D-ED23C1FFFBA5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6D227AA7-FF90-48CA-B13D-ED23C1FFFBA5}.Release|Any CPU.Build.0 = Release|Any CPU {B53007C7-B314-40DF-B6E7-6C6576A5611C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B53007C7-B314-40DF-B6E7-6C6576A5611C}.Debug|Any CPU.Build.0 = Debug|Any CPU {B53007C7-B314-40DF-B6E7-6C6576A5611C}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -131,10 +152,62 @@ Global {40A849C5-C8D7-4F76-856A-138AED73A6C3}.Debug|Any CPU.Build.0 = Debug|Any CPU {40A849C5-C8D7-4F76-856A-138AED73A6C3}.Release|Any CPU.ActiveCfg = Release|Any CPU {40A849C5-C8D7-4F76-856A-138AED73A6C3}.Release|Any CPU.Build.0 = Release|Any CPU - {436D869F-7566-4436-90A8-B655145C5BCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {436D869F-7566-4436-90A8-B655145C5BCA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {436D869F-7566-4436-90A8-B655145C5BCA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {436D869F-7566-4436-90A8-B655145C5BCA}.Release|Any CPU.Build.0 = Release|Any CPU + {D4FC7E1B-CAE8-40F9-9FAF-0029D91CC6C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4FC7E1B-CAE8-40F9-9FAF-0029D91CC6C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4FC7E1B-CAE8-40F9-9FAF-0029D91CC6C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4FC7E1B-CAE8-40F9-9FAF-0029D91CC6C4}.Release|Any CPU.Build.0 = Release|Any CPU + {7466B831-1642-4B2B-9EA3-1C9299596C2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7466B831-1642-4B2B-9EA3-1C9299596C2A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7466B831-1642-4B2B-9EA3-1C9299596C2A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7466B831-1642-4B2B-9EA3-1C9299596C2A}.Release|Any CPU.Build.0 = Release|Any CPU + {3EFAACD8-CF5E-4E31-884B-6B9F87F1E495}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3EFAACD8-CF5E-4E31-884B-6B9F87F1E495}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3EFAACD8-CF5E-4E31-884B-6B9F87F1E495}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3EFAACD8-CF5E-4E31-884B-6B9F87F1E495}.Release|Any CPU.Build.0 = Release|Any CPU + {4878A4FB-2C1E-4C06-BB42-AFE1ACE2A882}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4878A4FB-2C1E-4C06-BB42-AFE1ACE2A882}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4878A4FB-2C1E-4C06-BB42-AFE1ACE2A882}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4878A4FB-2C1E-4C06-BB42-AFE1ACE2A882}.Release|Any CPU.Build.0 = Release|Any CPU + {501F9D19-0C3A-4E46-BF8E-7B87C22CFF56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {501F9D19-0C3A-4E46-BF8E-7B87C22CFF56}.Debug|Any CPU.Build.0 = Debug|Any CPU + {501F9D19-0C3A-4E46-BF8E-7B87C22CFF56}.Release|Any CPU.ActiveCfg = Release|Any CPU + {501F9D19-0C3A-4E46-BF8E-7B87C22CFF56}.Release|Any CPU.Build.0 = Release|Any CPU + {3FB0CF18-2BF6-4C1C-AF42-90B22E683BAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3FB0CF18-2BF6-4C1C-AF42-90B22E683BAF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3FB0CF18-2BF6-4C1C-AF42-90B22E683BAF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3FB0CF18-2BF6-4C1C-AF42-90B22E683BAF}.Release|Any CPU.Build.0 = Release|Any CPU + {C3A2523B-9620-45B9-98A4-8B5311C3731F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3A2523B-9620-45B9-98A4-8B5311C3731F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3A2523B-9620-45B9-98A4-8B5311C3731F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3A2523B-9620-45B9-98A4-8B5311C3731F}.Release|Any CPU.Build.0 = Release|Any CPU + {E0A3A275-1AB4-4E7A-8A0F-15DA7644A6E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E0A3A275-1AB4-4E7A-8A0F-15DA7644A6E6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E0A3A275-1AB4-4E7A-8A0F-15DA7644A6E6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E0A3A275-1AB4-4E7A-8A0F-15DA7644A6E6}.Release|Any CPU.Build.0 = Release|Any CPU + {48A75D67-FA07-49B0-AAC7-A78CC04441C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {48A75D67-FA07-49B0-AAC7-A78CC04441C7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48A75D67-FA07-49B0-AAC7-A78CC04441C7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {48A75D67-FA07-49B0-AAC7-A78CC04441C7}.Release|Any CPU.Build.0 = Release|Any CPU + {FE8E2C2B-27B7-40ED-BFAA-10072825F397}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FE8E2C2B-27B7-40ED-BFAA-10072825F397}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE8E2C2B-27B7-40ED-BFAA-10072825F397}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FE8E2C2B-27B7-40ED-BFAA-10072825F397}.Release|Any CPU.Build.0 = Release|Any CPU + {0544424F-7648-429E-963A-EE443815AB2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0544424F-7648-429E-963A-EE443815AB2E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0544424F-7648-429E-963A-EE443815AB2E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0544424F-7648-429E-963A-EE443815AB2E}.Release|Any CPU.Build.0 = Release|Any CPU + {75CA9FEB-E304-4A80-95B9-1D3E2118C091}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {75CA9FEB-E304-4A80-95B9-1D3E2118C091}.Debug|Any CPU.Build.0 = Debug|Any CPU + {75CA9FEB-E304-4A80-95B9-1D3E2118C091}.Release|Any CPU.ActiveCfg = Release|Any CPU + {75CA9FEB-E304-4A80-95B9-1D3E2118C091}.Release|Any CPU.Build.0 = Release|Any CPU + {6B9284D6-DFFF-4902-8165-C05685299C10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B9284D6-DFFF-4902-8165-C05685299C10}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6B9284D6-DFFF-4902-8165-C05685299C10}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6B9284D6-DFFF-4902-8165-C05685299C10}.Release|Any CPU.Build.0 = Release|Any CPU + {A51FFF86-9483-4380-B334-673D79B4DC6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A51FFF86-9483-4380-B334-673D79B4DC6D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A51FFF86-9483-4380-B334-673D79B4DC6D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A51FFF86-9483-4380-B334-673D79B4DC6D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -149,7 +222,6 @@ Global {5B8FFFD3-9A1C-4620-9DB3-CD76CD9E79BF} = {7080D95A-39E8-418A-BA03-99ED89D4020E} {0B70B4FD-F5A0-4A6C-A3FD-90031E08C1C2} = {7080D95A-39E8-418A-BA03-99ED89D4020E} {25F6B88A-4EFA-4516-BB7A-34ED68548636} = {1DF607A2-5750-4BF4-AB2F-E21EC51ECCA0} - {6D227AA7-FF90-48CA-B13D-ED23C1FFFBA5} = {7080D95A-39E8-418A-BA03-99ED89D4020E} {B53007C7-B314-40DF-B6E7-6C6576A5611C} = {1DF607A2-5750-4BF4-AB2F-E21EC51ECCA0} {5E3745FC-CA80-4D0F-8A25-20EE0F9CF163} = {7080D95A-39E8-418A-BA03-99ED89D4020E} {CA376D7C-2A71-4518-B297-4C4A08DBF19D} = {1DF607A2-5750-4BF4-AB2F-E21EC51ECCA0} @@ -157,7 +229,20 @@ Global {2438B31A-3A39-4878-81FA-BE5AE715EAE5} = {7080D95A-39E8-418A-BA03-99ED89D4020E} {B8BB212C-8AFC-4258-A023-EB1F6937F53D} = {7080D95A-39E8-418A-BA03-99ED89D4020E} {40A849C5-C8D7-4F76-856A-138AED73A6C3} = {7080D95A-39E8-418A-BA03-99ED89D4020E} - {436D869F-7566-4436-90A8-B655145C5BCA} = {7080D95A-39E8-418A-BA03-99ED89D4020E} + {D4FC7E1B-CAE8-40F9-9FAF-0029D91CC6C4} = {7080D95A-39E8-418A-BA03-99ED89D4020E} + {7466B831-1642-4B2B-9EA3-1C9299596C2A} = {7080D95A-39E8-418A-BA03-99ED89D4020E} + {3EFAACD8-CF5E-4E31-884B-6B9F87F1E495} = {7080D95A-39E8-418A-BA03-99ED89D4020E} + {4878A4FB-2C1E-4C06-BB42-AFE1ACE2A882} = {7080D95A-39E8-418A-BA03-99ED89D4020E} + {501F9D19-0C3A-4E46-BF8E-7B87C22CFF56} = {1DF607A2-5750-4BF4-AB2F-E21EC51ECCA0} + {3FB0CF18-2BF6-4C1C-AF42-90B22E683BAF} = {7080D95A-39E8-418A-BA03-99ED89D4020E} + {C3A2523B-9620-45B9-98A4-8B5311C3731F} = {7080D95A-39E8-418A-BA03-99ED89D4020E} + {E0A3A275-1AB4-4E7A-8A0F-15DA7644A6E6} = {1DF607A2-5750-4BF4-AB2F-E21EC51ECCA0} + {48A75D67-FA07-49B0-AAC7-A78CC04441C7} = {7080D95A-39E8-418A-BA03-99ED89D4020E} + {FE8E2C2B-27B7-40ED-BFAA-10072825F397} = {7080D95A-39E8-418A-BA03-99ED89D4020E} + {0544424F-7648-429E-963A-EE443815AB2E} = {1DF607A2-5750-4BF4-AB2F-E21EC51ECCA0} + {75CA9FEB-E304-4A80-95B9-1D3E2118C091} = {1DF607A2-5750-4BF4-AB2F-E21EC51ECCA0} + {6B9284D6-DFFF-4902-8165-C05685299C10} = {7080D95A-39E8-418A-BA03-99ED89D4020E} + {A51FFF86-9483-4380-B334-673D79B4DC6D} = {1DF607A2-5750-4BF4-AB2F-E21EC51ECCA0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {983C3AB4-301E-47A3-93FF-2E4BD8F2090F} diff --git a/Doc/CommandComponents.png b/Doc/CommandComponents.png index b41532c..75b774c 100644 Binary files a/Doc/CommandComponents.png and b/Doc/CommandComponents.png differ diff --git a/Doc/CommandSide.png b/Doc/CommandSide.png new file mode 100644 index 0000000..ffc6d53 Binary files /dev/null and b/Doc/CommandSide.png differ diff --git a/Doc/DeploymentEvolution.png b/Doc/DeploymentEvolution.png new file mode 100644 index 0000000..66fa07d Binary files /dev/null and b/Doc/DeploymentEvolution.png differ diff --git a/Doc/EntityFrameworkMapping.png b/Doc/EntityFrameworkMapping.png index 45be9e8..43446ff 100644 Binary files a/Doc/EntityFrameworkMapping.png and b/Doc/EntityFrameworkMapping.png differ diff --git a/Doc/QueryComponents.png b/Doc/QueryComponents.png index 6b7931a..980c6ff 100644 Binary files a/Doc/QueryComponents.png and b/Doc/QueryComponents.png differ diff --git a/Doc/QuerySide.png b/Doc/QuerySide.png new file mode 100644 index 0000000..c87307d Binary files /dev/null and b/Doc/QuerySide.png differ diff --git a/README.md b/README.md index abab441..4339b41 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,127 @@ -### Domain-Driven Design sample +### Domain-Driven Design example -This project is a sample of .NET implementation of a medical prescription model using the approach "Domain-Driven Design" (DDD) and the architectural pattern "Command and Query Responsibility Segregation" (CQRS). +This project is an example of a .NET implementation of a medical prescription model using the "Domain-Driven Design" (DDD) approach and the "Command and Query Responsibility Segregation" (CQRS) architectural pattern. **Points of interest** -The goal of the project is to experiment some .NET implementations of core concepts of DDD (entities, value objects, domain events, ...) and CQRS (commands, queries, command and query handlers, ...) on the basis of a concrete model. +This project has been created to test an implementation in .NET of the core concepts of DDD (entities, value objects, domain events, ...) and CQRS (commands, queries, command and query handlers, ...) on the basis of a concrete model. -Only a lightweight version of CQRS (no data projection, one data store) has been considered. +The final goal of the project is to develop, for personal use, the main components of a message-based architecture that combines the benefits of the Hexagonal Architecture, CQRS and DDD, namely : -See the [Wiki](https://github.com/draphyz/DDD/wiki) for more information. +- reusability of the application code +- isolation of the business and application logic from external dependencies +- reduction of complexity by splitting the system into smaller parts with high cohesion and low coupling +- maintainability, testability and readability of code +- encapsulation of the business logic and rules in domain objects if necessary + +The scalability of the system is important, but is not the main concern. I want to develop a solution that combines the above-mentioned benefits, but that is accessible to small development teams and that can be deployed in different types of environment. For this reason, only a light version of CQRS (no data projection, one data store) has been considered and no messaging framework (like Apache Kafka or NServiceBus) has been used. + +The main components are developed from technologies commonly used by .NET developers : + +- a relational database +- an ORM framework +- an Ioc container + +**Architecture** + +The architecture of the project is based on the Hexagonal Architecture. The main concepts of the Hexagonal Architecture are clearly explained in the article ["Hexagonal Architecture, there are always two sides to every story"](https://medium.com/ssense-tech/hexagonal-architecture-there-are-always-two-sides-to-every-story-bc0780ed7d9c). Applying the architectural pattern CQRS in this architecture offers many benefits : +- the input ports (driving ports) of the application are represented by two generic fine-grained interfaces (ICommandHandler and IQueryHandler) defining the command and query handlers +- on the query side, the architecture is simplified as shown in the diagrams below : the input and output ports are represented by the same interface (IQueryHandler) defining the query handlers, the application layer is reduced to a few objects (queries and results) participating to the definition of this interface +- by separating command and query sides, the output ports (driven ports) used by the application to persist aggregates on the command side can also be represented by a generic fine-grained interface (IRepository) +- the small interfaces defining the input and output ports can be easily decorated to implement cross-cutting concerns like logging or error handling. It is particularly relevant to apply resilient policies (retry strategies, …) around the execution of a command or a query because they represent a whole (holistic abstraction) : you can retry a command or a query, but not a part of it. +- It is easy to establish a mechanism to perform background recurring tasks : recurring commands can be stored in a database and processed on a recurring basis + +![Alt Architecture on command side](https://github.com/draphyz/DDD/blob/entityframework/Doc/CommandSide.png) + +![Alt Architecture on query side](https://github.com/draphyz/DDD/blob/entityframework/Doc/QuerySide.png) + +**Packaging and deployment** + +In recent years, microservices architecture has become very popular in the IT community : this architecture has been popularized by the big tech companies. The main reason for adopting this architecture is organizational : each service can be developed in its own technology by its own team and can be deployed separately. This architecture, presented as modular, is often contrasted with monolithic architecture, considered as non-modular. However, it is possible to modularize a monolithic application by distributing the application code in different libraries and by assembling them into a monolith for deployment. Such an application is called a modular monolith. As explained in the article ["Why should you build a (modular) monolith first?"](https://newsletter.techworld-with-milan.com/p/why-you-should-build-a-modular-monolith), an adequately produced modular monolith can be a good step that can be more or less transformed into a microservice solution tomorrow if needed. This project has been designed to support such type of application. + +![Alt From monolithic application to distributed application](https://github.com/draphyz/DDD/blob/entityframework/Doc/DeploymentEvolution.png) + +**Message handling** + +In a message-based application, 3 types of messages can be handled : + +- a command : a message that carries data about an operation that changes the state of the application +- a query : a message that carries data about an operation that reads the state of the application +- an event : a message that carries data about something that happened in the application + +Commands and queries are usually handled synchronously and events asynchronously. Some events called "domain events" capture an occurrence of something that happened in the domain and that is considered important by business experts. These events generally register that the state of the application (more precisely the state of an aggregate) has changed and they occur during the processing of a command. It is important to transactionally record this change and the associated event(s). A simple way to do this is to record them in the same database. This is the way taken by the project : events and aggregates are saved in the same database. + +Domain events are mainly used to decouple the different parts (bounded contexts) of the application. Each part of the application can be interested in events that happened in the other parts. When you set up a delivery mechanism of events, you must take into account various concerns : + +- ease of deployment and management +- system scalability +- event ordering : in order, out of order +- delivery guarantees : at most once, at least once, exactly once +- error handling strategies : stop on error, dead letter queue, retry, maintain order + +From a consumer's perspective, we all want a scalable and easy-to-use mechanism that guarantees that events will be delivered exactly once and in order, but it is not technically possible : guarantee an ordered delivery of all events makes the solution non-scalable. However, we can divide the whole stream of events (from a bounded context to another) into smaller streams and ensure the order of events within these streams (under normal conditions). We can also ensure that events are processed exactly once in a bounded context : it is possible by storing the current position in the stream in the database associated with the consuming context. + +In this project, each bounded context is responsible for recording its own events in its own database as well as associating them with a stream type (usually an aggregate type) and a stream identifier (usually an aggregate identifier). Each context is also responsible for registering its own subscriptions to event streams, reading the streams to which it has subscribed, handling the associated events and updating the current position within these streams. When an error occurs during event handling and no “immediate” retry strategy has been defined via a decorator (command handlers can be easily decorated), all events with the same stream type and stream ID as the event causing the error are excluded from event handling. This exclusion can be temporary if the error is considered transient and a delayed retry strategy has been defined. + +**Model** + +The model considered is a simplified model reflecting the life cycle of a medical prescription and, in particular, of a pharmaceutical prescription. This life cycle can be summarized by the following diagram. + +![Alt Prescription Lifecycle](https://github.com/draphyz/DDD/blob/entityframework/Doc/PrescriptionLifecycle.png) + +The current model only takes into account the use cases related to the prescriber : the creation and revocation of a prescription. + +_Command Model_ + +On the write side, a rich domain model has been used to encapsulate the business logic and to control the creation of domain events. This model represents the business model. It incorporates both behavior and data, and is composed of entities and value objects. + +The domain modeling has been focused on two important aspects : +- Encapsulation (to ensure that the aggregate is in a consistent state at all times) +- Separation of concerns (also known as persistence ignorance) + +Therefore, the domain model has been implemented as an object model without public setters and by hiding some information (like database identifiers for the value objects). Entity inheritance and value object inheritance has been used and some value types like enumerations has been implemented as value objects. + +Two options has been considered to map the domain objects to the database tables : +- Mapping the domain model directly to the database by using a flexible and mature ORM like NHibernate 5 (branch nhibernate). + +![Alt NHibernate Mapping](https://github.com/draphyz/DDD/blob/entityframework/Doc/NHibernateMapping.png) + +- Mapping the domain model to an intermediate model (state or persistence model) and then mapping the intermediate model to the database by using a less flexible and mature ORM like Entity Framework Core (branch entityframework). + +![Alt Entity Framework Mapping](https://github.com/draphyz/DDD/blob/entityframework/Doc/EntityFrameworkMapping.png) + +By comparing the purity/complexity ratios of the two options, the first option is preferred to map the domain model to the database. In the branch "NHibernate", some minor changes have been made to the domain model, such as adding protected constructors or private setters. + +The interactions between the main components on the write side can be represented as follows : + +![Alt Command Components](https://github.com/draphyz/DDD/blob/entityframework/Doc/CommandComponents.png) + +_Query Model_ + +As mentioned above, the command and query data stores are not differentiated but the architecture on the query side is simplified (as shown on the following diagram). The query side is composed of simple Data Transfer Objects mapped to the database by using the Micro ORM Dapper. + +![Alt Query Components](https://github.com/draphyz/DDD/blob/entityframework/Doc/QueryComponents.png) + +**Organization of code** + +The libraries are distributed by component (bounded context) : + +- The "Core" libraries include the core components (Entity, ValueObject, CommandHandler, ...) necessary to implement the DDD approach according to the CQRS architectural pattern. +- The "Common" libraries include common components (EmailAddress, FullName, ...) that can be reused in multiple bounded contexts (shared kernel). +- The "HealthcareDelivery" libraries include components related to the context of healthcare delivery. + +The application layer can be tested by using the "DDD.HealthcareDelivery.IntegrationTests" project. + +**Cross-cutting concerns** + +The decorator pattern is especially useful in CQRS to handle cross-cutting concerns such as logging or error handling. Command or query handlers (small interfaces) can be easily decorated. You will find some examples of decorators in the "DDD.Core.Polly" and "DDD.Core" projects. + +Exception chaining (or exception wrapping) has been used. Each abstraction has its own set of exceptions : + +- ISerializer throws a SerializationException +- IObjectMapper or IObjectTranslator throws a MappingException +- IRepository throws a RepositoryException +- IQueryHandler throws a QueryException +- ICommandHandler throws a CommandException + +The Domain and Application layers have their own base exception class (respectively DomainException and ApplicationException). These classes defines a property IsTransient indicating whether the exception is transient. diff --git a/Src/DDD.Common.Messages/DDD.Common.Messages.csproj b/Src/DDD.Common.Messages/DDD.Common.Messages.csproj index b2efab9..51a7feb 100644 --- a/Src/DDD.Common.Messages/DDD.Common.Messages.csproj +++ b/Src/DDD.Common.Messages/DDD.Common.Messages.csproj @@ -1,45 +1,16 @@ - - - + - Debug - AnyCPU - {40A849C5-C8D7-4F76-856A-138AED73A6C3} + net48;netstandard2.1 Library - Properties DDD.Common - DDD.Common.Messages - v4.7.2 - 512 - true + false - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - Properties\CommonAssemblyInfo.cs - - - + + + \ No newline at end of file diff --git a/Src/DDD.Common.Messages/Properties/AssemblyInfo.cs b/Src/DDD.Common.Messages/Properties/AssemblyInfo.cs index a9a700f..bec3cac 100644 --- a/Src/DDD.Common.Messages/Properties/AssemblyInfo.cs +++ b/Src/DDD.Common.Messages/Properties/AssemblyInfo.cs @@ -2,6 +2,6 @@ using System.Runtime.InteropServices; [assembly: AssemblyTitle("DDD.Common.Messages")] -[assembly: AssemblyDescription("Messages shared by multiple contexts.")] +[assembly: AssemblyDescription("Common messages shared by multiple contexts.")] [assembly: AssemblyProduct("DDD.Common.Messages")] [assembly: Guid("40a849c5-c8d7-4f76-856a-138aed73a6c3")] \ No newline at end of file diff --git a/Src/DDD.Common.NHibernate/DDD.Common.NHibernate.csproj b/Src/DDD.Common.NHibernate/DDD.Common.NHibernate.csproj new file mode 100644 index 0000000..e9c5510 --- /dev/null +++ b/Src/DDD.Common.NHibernate/DDD.Common.NHibernate.csproj @@ -0,0 +1,19 @@ + + + DDD.Common.Infrastructure.Data + net48;netstandard2.1 + false + bin\$(Configuration)\ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Src/DDD.Common.NHibernate/EnumerationCodeType.cs b/Src/DDD.Common.NHibernate/EnumerationCodeType.cs new file mode 100644 index 0000000..40d986d --- /dev/null +++ b/Src/DDD.Common.NHibernate/EnumerationCodeType.cs @@ -0,0 +1,80 @@ +using NHibernate.Dialect; +using NHibernate.Engine; +using NHibernate.SqlTypes; +using NHibernate.Type; +using System; +using System.Data.Common; +using NHibernate; + +namespace DDD.Common.Infrastructure.Data +{ + using Domain; + + public class EnumerationCodeType : PrimitiveType where T : Enumeration + { + + #region Fields + + private readonly object defaultValue; + private readonly string name; + + #endregion Fields + + #region Constructors + + public EnumerationCodeType() : base(SqlTypeFactory.GetAnsiString(255)) + { + if (Enumeration.TryParseValue(0, out var result)) + this.defaultValue = result.Code; + else + this.defaultValue = "0"; + this.name = this.GetType().AssemblyQualifiedName; + } + + #endregion Constructors + + #region Properties + + public override object DefaultValue => this.defaultValue; + public override string Name => this.name; + public override Type PrimitiveClass => typeof(string); + public override Type ReturnedClass => typeof(T); + + #endregion Properties + + #region Methods + + public override object Get(DbDataReader rs, int index, ISessionImplementor session) + { + var code = rs[index]; + if (code == DBNull.Value || code == null) return null; + try + { + return Enumeration.ParseCode(code.ToString()); + } + catch (ArgumentOutOfRangeException ex) + { + throw new HibernateException($"Can't Parse {code} as {this.ReturnedClass.Name}", ex); + } + } + + public override object Get(DbDataReader rs, string name, ISessionImplementor session) + { + return this.Get(rs, rs.GetOrdinal(name), session); + } + + public override string ObjectToSQLString(object value, Dialect dialect) => value.ToString(); + + public override void Set(DbCommand cmd, object value, int index, ISessionImplementor session) + { + var parameter = cmd.Parameters[index]; + if (value == null) + parameter.Value = DBNull.Value; + else + parameter.Value = ((T)value).Code; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Common.NHibernate/EnumerationNameType.cs b/Src/DDD.Common.NHibernate/EnumerationNameType.cs new file mode 100644 index 0000000..a86e401 --- /dev/null +++ b/Src/DDD.Common.NHibernate/EnumerationNameType.cs @@ -0,0 +1,80 @@ +using NHibernate; +using NHibernate.Dialect; +using NHibernate.Engine; +using NHibernate.SqlTypes; +using NHibernate.Type; +using System; +using System.Data.Common; + +namespace DDD.Common.Infrastructure.Data +{ + using Domain; + + public class EnumerationNameType : PrimitiveType where T : Enumeration + { + + #region Fields + + private readonly object defaultValue; + private readonly string name; + + #endregion Fields + + #region Constructors + + public EnumerationNameType() : base(SqlTypeFactory.GetAnsiString(255)) + { + if (Enumeration.TryParseValue(0, out var result)) + this.defaultValue = result.Name; + else + this.defaultValue = "0"; + this.name = this.GetType().AssemblyQualifiedName; + } + + #endregion Constructors + + #region Properties + + public override object DefaultValue => this.defaultValue; + public override string Name => this.name; + public override Type PrimitiveClass => typeof(string); + public override Type ReturnedClass => typeof(T); + + #endregion Properties + + #region Methods + + public override object Get(DbDataReader rs, int index, ISessionImplementor session) + { + var name = rs[index]; + if (name == DBNull.Value || name == null) return null; + try + { + return Enumeration.ParseName(name.ToString()); + } + catch (ArgumentOutOfRangeException ex) + { + throw new HibernateException($"Can't Parse {name} as {this.ReturnedClass.Name}", ex); + } + } + + public override object Get(DbDataReader rs, string name, ISessionImplementor session) + { + return this.Get(rs, rs.GetOrdinal(name), session); + } + + public override string ObjectToSQLString(object value, Dialect dialect) => value.ToString(); + + public override void Set(DbCommand cmd, object value, int index, ISessionImplementor session) + { + var parameter = cmd.Parameters[index]; + if (value == null) + parameter.Value = DBNull.Value; + else + parameter.Value = ((T)value).Name; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Common.NHibernate/EnumerationValueType.cs b/Src/DDD.Common.NHibernate/EnumerationValueType.cs new file mode 100644 index 0000000..891342b --- /dev/null +++ b/Src/DDD.Common.NHibernate/EnumerationValueType.cs @@ -0,0 +1,71 @@ +using NHibernate.Dialect; +using NHibernate.Engine; +using NHibernate.SqlTypes; +using NHibernate.Type; +using System; +using System.Data.Common; + +namespace DDD.Common.Infrastructure.Data +{ + using Domain; + using NHibernate; + + public class EnumerationValueType : PrimitiveType where T : Enumeration + { + + private readonly string name; + + #region Constructors + + public EnumerationValueType() : base(SqlTypeFactory.Int32) + { + this.name = this.GetType().AssemblyQualifiedName; + } + + #endregion Constructors + + #region Properties + + public override object DefaultValue => 0; + public override string Name => this.name; + public override Type PrimitiveClass => typeof(int); + public override Type ReturnedClass => typeof(T); + + #endregion Properties + + #region Methods + + public override object Get(DbDataReader rs, int index, ISessionImplementor session) + { + var value = rs[index]; + if (value == DBNull.Value || value == null) return null; + try + { + return Enumeration.ParseValue(Convert.ToInt32(value)); + } + catch (ArgumentOutOfRangeException ex) + { + throw new HibernateException($"Can't Parse {value} as {this.ReturnedClass.Name}", ex); + } + } + + public override object Get(DbDataReader rs, string name, ISessionImplementor session) + { + return this.Get(rs, rs.GetOrdinal(name), session); + } + + public override string ObjectToSQLString(object value, Dialect dialect) => value.ToString(); + + public override void Set(DbCommand cmd, object value, int index, ISessionImplementor session) + { + var parameter = cmd.Parameters[index]; + if (value == null) + parameter.Value = DBNull.Value; + else + parameter.Value = ((T)value).Value; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Common.NHibernate/Properties/AssemblyInfo.cs b/Src/DDD.Common.NHibernate/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..23a6ba0 --- /dev/null +++ b/Src/DDD.Common.NHibernate/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("DDD.Common.NHibernate")] +[assembly: AssemblyDescription("Implementation of common infrastucture components based on NHibernate.")] +[assembly: AssemblyProduct("DDD.Common.NHibernate")] +[assembly: Guid("7466b831-1642-4b2b-9ea3-1c9299596c2a")] \ No newline at end of file diff --git a/Src/DDD.Common/DDD.Common.csproj b/Src/DDD.Common/DDD.Common.csproj index da0c442..a3dabf1 100644 --- a/Src/DDD.Common/DDD.Common.csproj +++ b/Src/DDD.Common/DDD.Common.csproj @@ -1,93 +1,28 @@ - - - + - Debug - AnyCPU - {0B70B4FD-F5A0-4A6C-A3FD-90031E08C1C2} + net48;netstandard2.1 Library - Properties - DDD.Common - DDD.Common - v4.7.2 - 512 - + false - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 bin\Debug\DDD.Common.xml 1591 - pdbonly - true - bin\Release\ - TRACE - prompt - 4 bin\Release\DDD.Common.xml 1591 - - - L:\Packages\Conditions.2.1.0\lib\portable-net45+win+wpa81+wp80+MonoAndroid10+MonoTouch10\Conditions.dll - True - - - - - Properties\CommonAssemblyInfo.cs - - - - - - - - - - - - - - - - - - - - - - + + - - {596a8700-3d18-4a62-b200-1f78a9ea4617} - DDD.Core.Abstractions - - - {c6c3e419-b9aa-44ad-9dbf-789294687ae6} - DDD.Core - + + - - \ No newline at end of file diff --git a/Src/DDD.Common/Domain/Alpha2CountryCode.cs b/Src/DDD.Common/Domain/Alpha2CountryCode.cs index ad12dfb..9dc8895 100644 --- a/Src/DDD.Common/Domain/Alpha2CountryCode.cs +++ b/Src/DDD.Common/Domain/Alpha2CountryCode.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; namespace DDD.Common.Domain { @@ -12,11 +12,12 @@ public class Alpha2CountryCode : CountryCode public Alpha2CountryCode(string value) : base(value) { - Condition.Requires(value, nameof(value)) - .HasLength(2) - .Evaluate(c => c.IsAlphabetic()); + Ensure.That(value, nameof(value)).HasLength(2); + Ensure.That(value, nameof(value)).IsAllLetters(); } + protected Alpha2CountryCode() { } + #endregion Constructors #region Methods diff --git a/Src/DDD.Common/Domain/Alpha2LanguageCode.cs b/Src/DDD.Common/Domain/Alpha2LanguageCode.cs index b046664..68882e2 100644 --- a/Src/DDD.Common/Domain/Alpha2LanguageCode.cs +++ b/Src/DDD.Common/Domain/Alpha2LanguageCode.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; namespace DDD.Common.Domain { @@ -12,11 +12,12 @@ public class Alpha2LanguageCode : LanguageCode public Alpha2LanguageCode(string value) : base(value) { - Condition.Requires(value, nameof(value)) - .HasLength(2) - .Evaluate(c => c.IsAlphabetic()); + Ensure.That(value, nameof(value)).HasLength(2); + Ensure.That(value, nameof(value)).IsAllLetters(); } + protected Alpha2LanguageCode() { } + #endregion Constructors } diff --git a/Src/DDD.Common/Domain/ArbitraryIdentifier.cs b/Src/DDD.Common/Domain/ArbitraryIdentifier.cs index e68ac01..26aaf1e 100644 --- a/Src/DDD.Common/Domain/ArbitraryIdentifier.cs +++ b/Src/DDD.Common/Domain/ArbitraryIdentifier.cs @@ -14,6 +14,8 @@ public abstract class ArbitraryIdentifier : ComparableValueObject #region Constructors + protected ArbitraryIdentifier() { } + protected ArbitraryIdentifier(TId value) { this.Value = value; @@ -23,7 +25,7 @@ protected ArbitraryIdentifier(TId value) #region Properties - public TId Value { get; } + public TId Value { get; private set; } #endregion Properties @@ -39,7 +41,7 @@ public override IEnumerable EqualityComponents() yield return this.Value; } - public override string ToString() => $"{this.GetType().Name} [value={this.Value}]"; + public override string ToString() => $"{GetType().Name} [{nameof(Value)}={Value}]"; #endregion Methods diff --git a/Src/DDD.Common/Domain/BelgianSocialSecurityNumber.cs b/Src/DDD.Common/Domain/BelgianSocialSecurityNumber.cs index 461bad4..34d2224 100644 --- a/Src/DDD.Common/Domain/BelgianSocialSecurityNumber.cs +++ b/Src/DDD.Common/Domain/BelgianSocialSecurityNumber.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System; namespace DDD.Common.Domain @@ -13,11 +13,12 @@ public class BelgianSocialSecurityNumber : SocialSecurityNumber public BelgianSocialSecurityNumber(string value) : base(value) { - Condition.Requires(value, nameof(value)) - .HasLength(11) - .Evaluate(c => c.IsNumeric()); + Ensure.That(value, nameof(value)).HasLength(11); + Ensure.That(value, nameof(value)).IsAllDigits(); } + protected BelgianSocialSecurityNumber() { } + #endregion Constructors #region Enums @@ -37,7 +38,7 @@ public enum Sex /// public static int ComputeCheckDigit(string value, bool bornBefore2000 = true) { - Condition.Requires(value, nameof(value)).IsLongerOrEqual(9); + Ensure.That(value, nameof(value)).HasMinLength(9); long identifier; if (bornBefore2000) identifier = long.Parse(value.Substring(0, 9)); diff --git a/Src/DDD.Common/Domain/BinaryContent.cs b/Src/DDD.Common/Domain/BinaryContent.cs index 13f333d..f5dc352 100644 --- a/Src/DDD.Common/Domain/BinaryContent.cs +++ b/Src/DDD.Common/Domain/BinaryContent.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System.Collections.Generic; namespace DDD.Common.Domain @@ -21,15 +21,17 @@ public class BinaryContent : ValueObject public BinaryContent(byte[] data) { - Condition.Requires(data, nameof(data)).IsNotNull(); + Ensure.That(data, nameof(data)).IsNotNull(); this.Data = data; } + protected BinaryContent() { } + #endregion Constructors #region Properties - public byte[] Data { get; } + public byte[] Data { get; private set; } #endregion Properties @@ -51,7 +53,7 @@ public override IEnumerable HashCodeComponents() yield return this.Data[i]; } - public override string ToString() => $"{this.GetType().Name} [data={this.Data}]"; + public override string ToString() => $"{GetType().Name} [{nameof(Data)}={Data}]"; #endregion Methods diff --git a/Src/DDD.Common/Domain/ContactInformation.cs b/Src/DDD.Common/Domain/ContactInformation.cs index 820ce18..d54f95a 100644 --- a/Src/DDD.Common/Domain/ContactInformation.cs +++ b/Src/DDD.Common/Domain/ContactInformation.cs @@ -5,7 +5,7 @@ namespace DDD.Common.Domain { using Core.Domain; - public class ContactInformation : ValueObject, IStateObjectConvertible + public class ContactInformation : ValueObject { #region Constructors @@ -38,43 +38,30 @@ public ContactInformation(PostalAddress postalAddress = null, this.WebSite = webSite; } + protected ContactInformation() { } + #endregion Constructors #region Properties - public string FaxNumber { get; } + public string FaxNumber { get; private set; } - public PostalAddress PostalAddress { get; } + public PostalAddress PostalAddress { get; private set; } - public EmailAddress PrimaryEmailAddress { get; } + public EmailAddress PrimaryEmailAddress { get; private set; } - public string PrimaryTelephoneNumber { get; } + public string PrimaryTelephoneNumber { get; private set; } - public EmailAddress SecondaryEmailAddress { get; } + public EmailAddress SecondaryEmailAddress { get; private set; } - public string SecondaryTelephoneNumber { get; } + public string SecondaryTelephoneNumber { get; private set; } - public Uri WebSite { get; } + public Uri WebSite { get; private set; } #endregion Properties #region Methods - public static ContactInformation FromState(ContactInformationState state) - { - if (state == null) return null; - return CreateIfNotEmpty - ( - PostalAddress.FromState(state.PostalAddress), - state.PrimaryTelephoneNumber, - state.SecondaryTelephoneNumber, - state.FaxNumber, - EmailAddress.CreateIfNotEmpty(state.PrimaryEmailAddress), - EmailAddress.CreateIfNotEmpty(state.SecondaryEmailAddress), - string.IsNullOrWhiteSpace(state.WebSite) ? null : new Uri(state.WebSite) - ); - } - public static ContactInformation CreateIfNotEmpty(PostalAddress postalAddress = null, string primaryTelephoneNumber = null, string secondaryTelephoneNumber = null, @@ -111,23 +98,11 @@ public override IEnumerable EqualityComponents() yield return this.WebSite; } - public ContactInformationState ToState() - { - return new ContactInformationState - { - PostalAddress = this.PostalAddress == null ? new PostalAddressState() : this.PostalAddress.ToState(), - PrimaryTelephoneNumber = this.PrimaryTelephoneNumber, - SecondaryTelephoneNumber = this.SecondaryTelephoneNumber, - FaxNumber = this.FaxNumber, - PrimaryEmailAddress = this.PrimaryEmailAddress?.Value, - SecondaryEmailAddress = this.SecondaryEmailAddress?.Value, - WebSite = this.WebSite?.AbsoluteUri - }; - } public override string ToString() { - var format = "{0} [postalAddress={1}, primaryTelephoneNumber={2}, secondaryTelephoneNumber={3}, faxNumber={4}, primaryEmailAddress={5}, secondaryEmailAddress={6}, webSite={7}]"; - return string.Format(format, this.GetType().Name, this.PostalAddress, this.PrimaryTelephoneNumber, this.SecondaryTelephoneNumber, this.FaxNumber, this.PrimaryEmailAddress, this.SecondaryEmailAddress, this.WebSite); + var s = $"{GetType().Name} [{nameof(PostalAddress)}={PostalAddress}, {nameof(PrimaryTelephoneNumber)}={PrimaryTelephoneNumber}, {nameof(SecondaryTelephoneNumber)}={SecondaryTelephoneNumber}, "; + s += $"{nameof(FaxNumber)}={FaxNumber}, {nameof(PrimaryEmailAddress)}={PrimaryEmailAddress}, {nameof(SecondaryEmailAddress)}={SecondaryEmailAddress}, {nameof(WebSite)}={WebSite}]"; + return s; } private static bool IsEmpty(PostalAddress postalAddress = null, diff --git a/Src/DDD.Common/Domain/ContactInformationState.cs b/Src/DDD.Common/Domain/ContactInformationState.cs deleted file mode 100644 index 94b3e29..0000000 --- a/Src/DDD.Common/Domain/ContactInformationState.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace DDD.Common.Domain -{ - public class ContactInformationState - { - - #region Properties - - public string FaxNumber { get; set; } - - public PostalAddressState PostalAddress { get; set; } - - public string PrimaryEmailAddress { get; set; } - - public string PrimaryTelephoneNumber { get; set; } - - public string SecondaryEmailAddress { get; set; } - - public string SecondaryTelephoneNumber { get; set; } - - public string WebSite { get; set; } - - #endregion Properties - - } -} diff --git a/Src/DDD.Common/Domain/CountryCode.cs b/Src/DDD.Common/Domain/CountryCode.cs index 8a1b8e4..32d2efa 100644 --- a/Src/DDD.Common/Domain/CountryCode.cs +++ b/Src/DDD.Common/Domain/CountryCode.cs @@ -5,6 +5,8 @@ public abstract class CountryCode : IdentificationCode #region Constructors + protected CountryCode() { } + protected CountryCode(string value) : base(value) { } diff --git a/Src/DDD.Common/Domain/EmailAddress.cs b/Src/DDD.Common/Domain/EmailAddress.cs index be2a1d6..950f84e 100644 --- a/Src/DDD.Common/Domain/EmailAddress.cs +++ b/Src/DDD.Common/Domain/EmailAddress.cs @@ -1,8 +1,7 @@ -using Conditions; +using EnsureThat; +using System; using System.Collections.Generic; using System.Linq; -using System.Text.RegularExpressions; -using System; namespace DDD.Common.Domain { @@ -15,17 +14,18 @@ public class EmailAddress : ComparableValueObject public EmailAddress(string value) { - Condition.Requires(value, nameof(value)) - .IsNotNullOrWhiteSpace() - .Evaluate(a => Regex.IsMatch(a, "\\w+([-+.']\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*")); + Ensure.That(value, nameof(value)).IsNotNullOrWhiteSpace(); + Ensure.That(value, nameof(value)).Matches("\\w+([-+.']\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*"); this.Value = value; } + protected EmailAddress() { } + #endregion Constructors #region Properties - public string Value { get; } + public string Value { get; private set; } #endregion Properties @@ -39,8 +39,8 @@ public static EmailAddress CreateIfNotEmpty(string value) public static EmailAddress FromParts(string username, string domain) { - Condition.Requires(username, nameof(username)).IsNotNullOrWhiteSpace(); - Condition.Requires(domain, nameof(domain)).IsNotNullOrWhiteSpace(); + Ensure.That(username, nameof(username)).IsNotNullOrWhiteSpace(); + Ensure.That(domain, nameof(domain)).IsNotNullOrWhiteSpace(); return new EmailAddress($"{username}@{domain}"); } @@ -58,19 +58,19 @@ public override IEnumerable EqualityComponents() public IEnumerable Parts() => this.Value.Split('@'); - public override string ToString() => $"{this.GetType().Name} [value={this.Value}]"; + public override string ToString() => $"{GetType().Name} [{nameof(Value)}={Value}]"; public string Username() => this.Parts().First(); public EmailAddress WithDomain(string domain) { - Condition.Requires(domain, nameof(domain)).IsNotNullOrWhiteSpace(); + Ensure.That(domain, nameof(domain)).IsNotNullOrWhiteSpace(); return new EmailAddress($"{this.Username()}@{domain}"); } public EmailAddress WithUsername(string username) { - Condition.Requires(username, nameof(username)).IsNotNullOrWhiteSpace(); + Ensure.That(username, nameof(username)).IsNotNullOrWhiteSpace(); return new EmailAddress($"{username}@{this.Domain()}"); } diff --git a/Src/DDD.Common/Domain/Enumeration.cs b/Src/DDD.Common/Domain/Enumeration.cs index cfb1e25..6981586 100644 --- a/Src/DDD.Common/Domain/Enumeration.cs +++ b/Src/DDD.Common/Domain/Enumeration.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -14,7 +14,7 @@ public abstract class Enumeration : ComparableValueObject #region Fields - private static readonly ConcurrentDictionary> cache = new ConcurrentDictionary>(); + private static readonly ConcurrentDictionary>> cache = new ConcurrentDictionary>>(); #endregion Fields @@ -26,11 +26,11 @@ protected Enumeration(int value, string name) : this(value, name, name) protected Enumeration(int value, string code, string name) { - Condition.Requires(code, nameof(code)).IsNotNullOrWhiteSpace(); - Condition.Requires(name, nameof(name)).IsNotNullOrWhiteSpace(); + Ensure.That(code, nameof(code)).IsNotNullOrWhiteSpace(); + Ensure.That(name, nameof(name)).IsNotNullOrWhiteSpace(); this.Value = value; - this.Code = code.ToUpper(); - this.Name = name.ToTitleCase(); + this.Code = code; + this.Name = name; } #endregion Constructors @@ -45,15 +45,9 @@ protected Enumeration(int value, string code, string name) #region Methods - public static IEnumerable All() where TEnum : Enumeration - { - var constants = cache.GetOrAdd(typeof(TEnum), t => new Lazy(GetAll)).Value; - return (IEnumerable)constants; - } - public static TEnum ParseCode(string code, bool ignoreCase = false) where TEnum : Enumeration { - Condition.Requires(code, nameof(code)).IsNotNullOrWhiteSpace(); + Ensure.That(code, nameof(code)).IsNotNullOrWhiteSpace(); if (TryParseCode(code, ignoreCase, out var result)) return result; throw new ArgumentOutOfRangeException("code", code, null); @@ -61,7 +55,7 @@ public static TEnum ParseCode(string code, bool ignoreCase = false) where public static TEnum ParseName(string name, bool ignoreCase = false) where TEnum : Enumeration { - Condition.Requires(name, nameof(name)).IsNotNullOrWhiteSpace(); + Ensure.That(name, nameof(name)).IsNotNullOrWhiteSpace(); if (TryParseName(name, ignoreCase, out var result)) return result; throw new ArgumentOutOfRangeException("name", name, null); @@ -78,7 +72,7 @@ public static bool TryParseCode(string code, bool ignoreCase, out TEnum r { if (string.IsNullOrWhiteSpace(code)) { - result = default(TEnum); + result = default; return false; } return TryParse(c => string.Compare(c.Code, code, ignoreCase) == 0, out result); @@ -88,7 +82,7 @@ public static bool TryParseName(string name, bool ignoreCase, out TEnum r { if (string.IsNullOrWhiteSpace(name)) { - result = default(TEnum); + result = default; return false; } return TryParse(c => string.Compare(c.Name, name, ignoreCase) == 0, out result); @@ -99,6 +93,18 @@ public static bool TryParseValue(int value, out TEnum result) where TEnum return TryParse(c => c.Value == value, out result); } + public static IEnumerable AllInstances() where TEnum : Enumeration + { + var instances = cache.GetOrAdd(typeof(TEnum), t => new Lazy>(GetAllInstances)).Value; + return (IEnumerable)instances; + } + + public static IEnumerable AllValues() where TEnum : Enumeration => AllInstances().Select(i => i.Value); + + public static int MinValue() where TEnum : Enumeration => AllInstances().First().Value; + + public static int MaxValue() where TEnum : Enumeration => AllInstances().Last().Value; + public override IEnumerable ComparableComponents() { yield return this.Value; @@ -111,21 +117,21 @@ public override IEnumerable EqualityComponents() public override string ToString() { - return $"{this.GetType().Name} [value={this.Value}, code={this.Code}, name={this.Name}]"; + return $"{GetType().Name} [{nameof(Value)}={Value}, {nameof(Code)}={Code}, {nameof(Name)}={Name}]"; } - private static TEnum[] GetAll() where TEnum : Enumeration + private static IEnumerable GetAllInstances() where TEnum : Enumeration { var enumType = typeof(TEnum); return enumType.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) .Where(f => enumType.IsAssignableFrom(f.FieldType)) .Select(f => (TEnum)f.GetValue(null)) - .ToArray(); + .OrderBy(e => e.Value); } private static bool TryParse(Func predicate, out TEnum result) where TEnum : Enumeration { - result = All().FirstOrDefault(predicate); + result = AllInstances().FirstOrDefault(predicate); return result != null; } diff --git a/Src/DDD.Common/Domain/FlagsEnumeration.cs b/Src/DDD.Common/Domain/FlagsEnumeration.cs new file mode 100644 index 0000000..1c1811d --- /dev/null +++ b/Src/DDD.Common/Domain/FlagsEnumeration.cs @@ -0,0 +1,174 @@ +using EnsureThat; +using System.Reflection; +using System.Text; +using System.Collections.Generic; +using System.Linq; +using System; + +namespace DDD.Common.Domain +{ + public abstract class FlagsEnumeration : Enumeration + { + + #region Fields + + private const string flagSeperator = ", "; + + #endregion Fields + + #region Constructors + + protected FlagsEnumeration(int value, string name) : base(value, name) + { + } + + protected FlagsEnumeration(int value, string code, string name) : base(value, code, name) + { + } + + #endregion Constructors + + #region Methods + + public static IEnumerable AllFlagValues() where TEnum : FlagsEnumeration + { + var complements = AllValues().Select(v => ~v); + for (var i = MinFlagValue(); i <= MaxFlagValue(); i++) + { + var unaccountedBits = i; + foreach (var complement in complements) + { + unaccountedBits &= complement; + if (unaccountedBits == 0) + { + yield return i; + break; + } + } + } + } + + public static int MaxFlagValue() where TEnum : FlagsEnumeration + { + var max = 0; + foreach (var value in AllValues()) + max |= value; + return max; + } + + public static int MinFlagValue() where TEnum : FlagsEnumeration => MinValue(); + + public static new TEnum ParseCode(string code, bool ignoreCase = false) where TEnum : FlagsEnumeration + { + Ensure.That(code, nameof(code)).IsNotNullOrWhiteSpace(); + if (TryParseCode(code, ignoreCase, out var result)) + return result; + throw new ArgumentOutOfRangeException("code", code, null); + } + + public static new TEnum ParseName(string name, bool ignoreCase = false) where TEnum : FlagsEnumeration + { + Ensure.That(name, nameof(name)).IsNotNullOrWhiteSpace(); + if (TryParseName(name, ignoreCase, out var result)) + return result; + throw new ArgumentOutOfRangeException("name", name, null); + } + + public static new TEnum ParseValue(int value) where TEnum : FlagsEnumeration + { + if (TryParseValue(value, out var result)) + return result; + throw new ArgumentOutOfRangeException("value", value, null); + } + + public static new bool TryParseCode(string code, bool ignoreCase, out TEnum result) where TEnum : FlagsEnumeration + { + result = default; + if (string.IsNullOrWhiteSpace(code)) return false; + if (Enumeration.TryParseCode(code, ignoreCase, out result)) return true; + var flagCodes = code.Split(new[] { flagSeperator }, StringSplitOptions.None); + if (!flagCodes.Any()) return false; + var flags = new List(); + foreach (var flagCode in flagCodes) + { + if (Enumeration.TryParseCode(flagCode, ignoreCase, out result)) + flags.Add(result); + else + return false; + } + result = CombineFlags(flags); + return true; + } + + public static new bool TryParseName(string name, bool ignoreCase, out TEnum result) where TEnum : FlagsEnumeration + { + result = default; + if (string.IsNullOrWhiteSpace(name)) return false; + if (Enumeration.TryParseName(name, ignoreCase, out result)) return true; + var flagNames = name.Split(new[] { flagSeperator }, StringSplitOptions.None); + if (!flagNames.Any()) return false; + var flags = new List(); + foreach (var flagName in flagNames) + { + if (Enumeration.TryParseName(flagName, ignoreCase, out result)) + flags.Add(result); + else + return false; + } + result = CombineFlags(flags); + return true; + } + + public static new bool TryParseValue(int value, out TEnum result) where TEnum : FlagsEnumeration + { + result = default; + if (value < MinFlagValue() || value > MaxFlagValue()) + return false; + if (Enumeration.TryParseValue(value, out result)) return true; + var flags = new List(); + foreach(var flag in AllInstances()) + { + if ((value & flag.Value) != 0) + flags.Add(flag); + } + result = CombineFlags(flags); + return true; + } + + public TEnum AddFlag(TEnum flag) where TEnum : FlagsEnumeration => ParseValue(this.Value | flag.Value); + + public bool HasFlag(TEnum flag) where TEnum : FlagsEnumeration => (this.Value & flag.Value) != 0; + + public TEnum RemoveFlag(TEnum flag) where TEnum : FlagsEnumeration => ParseValue(this.Value & ~flag.Value); + + public TEnum SetFlag(TEnum flag, bool enable) where TEnum : FlagsEnumeration => enable ? AddFlag(flag) : RemoveFlag(flag); + + public TEnum ToggleFlag(TEnum flag) where TEnum : FlagsEnumeration => ParseValue(this.Value ^ flag.Value); + + private static TEnum CombineFlags(IEnumerable flags) where TEnum : FlagsEnumeration + { + flags = flags.OrderBy(f => f.Value); + using (var enumerator = flags.GetEnumerator()) + { + enumerator.MoveNext(); + var value = enumerator.Current.Value; + var nameBuilder = new StringBuilder(enumerator.Current.Name); + var codeBuilder = new StringBuilder(enumerator.Current.Code); + while(enumerator.MoveNext()) + { + value |= enumerator.Current.Value; + nameBuilder.Append(flagSeperator).Append(enumerator.Current.Name); + codeBuilder.Append(flagSeperator).Append(enumerator.Current.Code); + } + var name = nameBuilder.ToString(); + var code = codeBuilder.ToString(); + if (code != name) + return (TEnum)Activator.CreateInstance(typeof(TEnum), BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { value, code, name }, null, null); + return (TEnum)Activator.CreateInstance(typeof(TEnum), BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { value, name }, null, null); + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Common/Domain/FullName.cs b/Src/DDD.Common/Domain/FullName.cs index 3f17e28..d265395 100644 --- a/Src/DDD.Common/Domain/FullName.cs +++ b/Src/DDD.Common/Domain/FullName.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System; using System.Collections.Generic; @@ -6,41 +6,33 @@ namespace DDD.Common.Domain { using Core.Domain; - public class FullName : ComparableValueObject, IStateObjectConvertible + public class FullName : ComparableValueObject { #region Constructors public FullName(string lastName, string firstName) { - Condition.Requires(lastName, nameof(lastName)).IsNotNullOrWhiteSpace(); - Condition.Requires(firstName, nameof(firstName)).IsNotNullOrWhiteSpace(); + Ensure.That(lastName, nameof(lastName)).IsNotNullOrWhiteSpace(); + Ensure.That(firstName, nameof(firstName)).IsNotNullOrWhiteSpace(); this.LastName = lastName.ToTitleCase(); this.FirstName = firstName.ToTitleCase(); } - #endregion Constructors + protected FullName() { } + + #endregion Constructors #region Properties - public string FirstName { get; } + public string FirstName { get; private set; } - public string LastName { get; } + public string LastName { get; private set; } #endregion Properties #region Methods - public static FullName FromState(FullNameState state) - { - if (state == null) return null; - return new FullName - ( - state.LastName, - state.FirstName - ); - } - public string AsFormattedName() => $"{this.LastName.ToUpper()} {this.FirstName}"; public override IEnumerable ComparableComponents() @@ -55,32 +47,24 @@ public override IEnumerable EqualityComponents() yield return this.FirstName; } - public FullNameState ToState() - { - return new FullNameState - { - LastName = this.LastName, - FirstName = this.FirstName - }; - } - public override string ToString() { - return $"{this.GetType().Name} [lastName={this.LastName}, firstName={this.FirstName}]"; + return $"{GetType().Name} [{nameof(LastName)}={LastName}, {nameof(FirstName)}={FirstName}]"; } public FullName WithFirstName(string firstName) { - Condition.Requires(firstName, nameof(firstName)).IsNotNullOrWhiteSpace(); + Ensure.That(firstName, nameof(firstName)).IsNotNullOrWhiteSpace(); return new FullName(this.LastName, firstName); } public FullName WithLastName(string lastName) { - Condition.Requires(lastName, nameof(lastName)).IsNotNullOrWhiteSpace(); + Ensure.That(lastName, nameof(lastName)).IsNotNullOrWhiteSpace(); return new FullName(lastName, this.FirstName); } #endregion Methods + } } \ No newline at end of file diff --git a/Src/DDD.Common/Domain/IdentificationCode.cs b/Src/DDD.Common/Domain/IdentificationCode.cs index 8be9606..8c70639 100644 --- a/Src/DDD.Common/Domain/IdentificationCode.cs +++ b/Src/DDD.Common/Domain/IdentificationCode.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System; using System.Collections.Generic; @@ -14,9 +14,11 @@ public abstract class IdentificationCode : ComparableValueObject #region Constructors + protected IdentificationCode() { } + protected IdentificationCode(string value) { - Condition.Requires(value, nameof(value)).IsNotNullOrWhiteSpace(); + Ensure.That(value, nameof(value)).IsNotNullOrWhiteSpace(); this.Value = value.ToUpper(); } @@ -24,7 +26,7 @@ protected IdentificationCode(string value) #region Properties - public string Value { get; } + public string Value { get; private set; } #endregion Properties @@ -40,7 +42,7 @@ public override IEnumerable EqualityComponents() yield return this.Value; } - public override string ToString() => $"{this.GetType().Name} [value={this.Value}]"; + public override string ToString() => $"{GetType().Name} [{nameof(Value)}={Value}]"; #endregion Methods diff --git a/Src/DDD.Common/Domain/IdentificationNumber.cs b/Src/DDD.Common/Domain/IdentificationNumber.cs index 092664a..471e6f1 100644 --- a/Src/DDD.Common/Domain/IdentificationNumber.cs +++ b/Src/DDD.Common/Domain/IdentificationNumber.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System; using System.Collections.Generic; @@ -14,9 +14,11 @@ public abstract class IdentificationNumber : ComparableValueObject #region Constructors + protected IdentificationNumber() { } + protected IdentificationNumber(string value) { - Condition.Requires(value, nameof(value)).IsNotNullOrWhiteSpace(); + Ensure.That(value, nameof(value)).IsNotNullOrWhiteSpace(); this.Value = value.ToUpper(); } @@ -24,7 +26,7 @@ protected IdentificationNumber(string value) #region Properties - public string Value { get; } + public string Value { get; private set; } #endregion Properties @@ -40,7 +42,7 @@ public override IEnumerable EqualityComponents() yield return this.Value; } - public override string ToString() => $"{this.GetType().Name} [value={this.Value}]"; + public override string ToString() => $"{GetType().Name} [{nameof(Value)}={Value}]"; #endregion Methods diff --git a/Src/DDD.Common/Domain/LanguageCode.cs b/Src/DDD.Common/Domain/LanguageCode.cs index b181dad..fc2f6ad 100644 --- a/Src/DDD.Common/Domain/LanguageCode.cs +++ b/Src/DDD.Common/Domain/LanguageCode.cs @@ -5,6 +5,8 @@ public abstract class LanguageCode : IdentificationCode #region Constructors + protected LanguageCode() { } + protected LanguageCode(string value) : base(value) { } diff --git a/Src/DDD.Common/Domain/PostalAddress.cs b/Src/DDD.Common/Domain/PostalAddress.cs index 5faf191..28a1035 100644 --- a/Src/DDD.Common/Domain/PostalAddress.cs +++ b/Src/DDD.Common/Domain/PostalAddress.cs @@ -1,11 +1,11 @@ -using System.Collections.Generic; -using Conditions; +using EnsureThat; +using System.Collections.Generic; namespace DDD.Common.Domain { using Core.Domain; - public class PostalAddress : ValueObject, IStateObjectConvertible + public class PostalAddress : ValueObject { #region Constructors @@ -17,8 +17,8 @@ public PostalAddress(string street, string houseNumber = null, string boxNumber = null) { - Condition.Requires(street, nameof(street)).IsNotNullOrWhiteSpace(); - Condition.Requires(city, nameof(city)).IsNotNullOrWhiteSpace(); + Ensure.That(street, nameof(street)).IsNotNullOrWhiteSpace(); + Ensure.That(city, nameof(city)).IsNotNullOrWhiteSpace(); this.Street = street.ToTitleCase(); this.City = city.ToTitleCase(); if (!string.IsNullOrWhiteSpace(postalCode)) @@ -30,40 +30,28 @@ public PostalAddress(string street, this.BoxNumber = boxNumber.ToUpper(); } + protected PostalAddress() { } + #endregion Constructors #region Properties - public string BoxNumber { get; } + public string BoxNumber { get; private set; } - public string City { get; } + public string City { get; private set; } - public Alpha2CountryCode CountryCode { get; } + public Alpha2CountryCode CountryCode { get; private set; } - public string HouseNumber { get; } + public string HouseNumber { get; private set; } - public string PostalCode { get; } + public string PostalCode { get; private set; } - public string Street { get; } + public string Street { get; private set; } #endregion Properties #region Methods - public static PostalAddress FromState(PostalAddressState state) - { - if (state == null) return null; - return CreateIfNotEmpty - ( - state.Street, - state.City, - state.PostalCode, - Alpha2CountryCode.CreateIfNotEmpty(state.CountryCode), - state.HouseNumber, - state.BoxNumber - ); - } - public static PostalAddress CreateIfNotEmpty(string street, string city, string postalCode = null, @@ -93,24 +81,9 @@ public override IEnumerable EqualityComponents() yield return this.HouseNumber; yield return this.BoxNumber; } - public PostalAddressState ToState() - { - return new PostalAddressState - { - Street = this.Street, - HouseNumber = this.HouseNumber, - BoxNumber = this.BoxNumber, - PostalCode = this.PostalCode, - City = this.City, - CountryCode = this.CountryCode?.Value - }; - } public override string ToString() - { - var format = "{0} [street={1}, houseNumber={2}, boxNumber{3}, postalCode={4}, city={5}, countryCode={6}]"; - return string.Format(format, this.GetType().Name, this.Street, this.HouseNumber, this.BoxNumber, this.PostalCode, this.City, this.CountryCode); - } + => $"{GetType().Name} [{nameof(Street)}={Street}, {nameof(HouseNumber)}={HouseNumber}, {nameof(BoxNumber)}{BoxNumber}, {nameof(PostalCode)}={PostalCode}, {nameof(City)}={City}, {nameof(CountryCode)}={CountryCode}]"; #endregion Methods diff --git a/Src/DDD.Common/Domain/PostalAddressState.cs b/Src/DDD.Common/Domain/PostalAddressState.cs deleted file mode 100644 index 168ed84..0000000 --- a/Src/DDD.Common/Domain/PostalAddressState.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace DDD.Common.Domain -{ - public class PostalAddressState - { - - #region Properties - - public string BoxNumber { get; set; } - - public string City { get; set; } - - public string CountryCode { get; set; } - - public string HouseNumber { get; set; } - - public string PostalCode { get; set; } - - public string Street { get; set; } - - #endregion Properties - - } -} diff --git a/Src/DDD.Common/Domain/SocialSecurityNumber.cs b/Src/DDD.Common/Domain/SocialSecurityNumber.cs index dc871d2..f2e7edb 100644 --- a/Src/DDD.Common/Domain/SocialSecurityNumber.cs +++ b/Src/DDD.Common/Domain/SocialSecurityNumber.cs @@ -2,8 +2,11 @@ { public abstract class SocialSecurityNumber : IdentificationNumber { + #region Constructors + protected SocialSecurityNumber() { } + protected SocialSecurityNumber(string value) : base(value) { } diff --git a/Src/DDD.Common/packages.config b/Src/DDD.Common/packages.config deleted file mode 100644 index a105d3f..0000000 --- a/Src/DDD.Common/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/Src/DDD.Core.Abstractions/AnyArgExtensions.cs b/Src/DDD.Core.Abstractions/AnyArgExtensions.cs new file mode 100644 index 0000000..e1951e9 --- /dev/null +++ b/Src/DDD.Core.Abstractions/AnyArgExtensions.cs @@ -0,0 +1,22 @@ +using EnsureThat; +using EnsureThat.Enforcers; +using System; + +namespace DDD +{ + public static class AnyArgExtensions + { + + /// + /// Ensures that the argument satisfies the specified condition. + /// + public static T Satisfy(this AnyArg _, T value, Func predicate, string paramName = null, OptsFn optsFn = null) + { + if (predicate != null && !predicate(value)) + throw Ensure.ExceptionFactory.ArgumentException("The argument does not satisfy the specified condition.", paramName, optsFn); + + return value; + } + + } +} diff --git a/Src/DDD.Core.Abstractions/Collections/ICollectionExtensions.cs b/Src/DDD.Core.Abstractions/Collections/ICollectionExtensions.cs index aeefd8b..cb9136a 100644 --- a/Src/DDD.Core.Abstractions/Collections/ICollectionExtensions.cs +++ b/Src/DDD.Core.Abstractions/Collections/ICollectionExtensions.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Conditions; +using EnsureThat; namespace DDD.Collections { @@ -13,8 +13,8 @@ public static class ICollectionExtensions public static void AddRange(this ICollection collection, IEnumerable items) { - Condition.Requires(collection, nameof(collection)).IsNotNull(); - Condition.Requires(items, nameof(items)).IsNotNull(); + Ensure.That(collection, nameof(collection)).IsNotNull(); + Ensure.That(items, nameof(items)).IsNotNull(); foreach(var item in items) collection.Add(item); } diff --git a/Src/DDD.Core.Abstractions/Collections/IDictionaryExtensions.cs b/Src/DDD.Core.Abstractions/Collections/IDictionaryExtensions.cs index df30aed..1397fe4 100644 --- a/Src/DDD.Core.Abstractions/Collections/IDictionaryExtensions.cs +++ b/Src/DDD.Core.Abstractions/Collections/IDictionaryExtensions.cs @@ -1,6 +1,7 @@ -using System.Collections.Generic; +using System.Collections; +using System.Collections.Generic; using System.ComponentModel; -using Conditions; +using EnsureThat; namespace DDD.Collections { @@ -11,7 +12,7 @@ public static class IDictionaryExtensions public static void AddObject(this IDictionary dictionary, object obj) { - Condition.Requires(dictionary, nameof(dictionary)).IsNotNull(); + Ensure.That(dictionary, nameof(dictionary)).IsNotNull(); if (obj != null) { var properties = TypeDescriptor.GetProperties(obj); @@ -23,6 +24,30 @@ public static void AddObject(this IDictionary dictionary, object } } + public static bool TryGetValue(this IDictionary dictionary, TKey key, out TValue value) + { + Ensure.That(dictionary, nameof(dictionary)).IsNotNull(); + if (dictionary.ContainsKey(key)) + { + value = (TValue)dictionary[key]; + return true; + } + value = default; + return false; + } + + public static bool TryGetValue(this IDictionary dictionary, TKey key, out TValue value) + { + Ensure.That(dictionary, nameof(dictionary)).IsNotNull(); + if (dictionary.Contains(key)) + { + value = (TValue)dictionary[key]; + return true; + } + value = default; + return false; + } + #endregion Methods } diff --git a/Src/DDD.Core.Abstractions/Collections/IEnumerableExtensions.cs b/Src/DDD.Core.Abstractions/Collections/IEnumerableExtensions.cs index 57e1de5..cb7a2fa 100644 --- a/Src/DDD.Core.Abstractions/Collections/IEnumerableExtensions.cs +++ b/Src/DDD.Core.Abstractions/Collections/IEnumerableExtensions.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; -using Conditions; -using System.Linq; +using EnsureThat; namespace DDD.Collections { @@ -15,7 +14,7 @@ public static class IEnumerableExtensions public static int CombineHashCodes(this IEnumerable collection) { - Condition.Requires(collection, nameof(collection)).IsNotNull(); + Ensure.That(collection, nameof(collection)).IsNotNull(); unchecked { var hash = 17; @@ -27,8 +26,8 @@ public static int CombineHashCodes(this IEnumerable collection) public static IEnumerable ForEach(this IEnumerable collection, Action action) { - Condition.Requires(collection, nameof(collection)).IsNotNull(); - Condition.Requires(action, nameof(action)).IsNotNull(); + Ensure.That(collection, nameof(collection)).IsNotNull(); + Ensure.That(action, nameof(action)).IsNotNull(); foreach (var item in collection) action(item); return collection; @@ -36,8 +35,8 @@ public static IEnumerable ForEach(this IEnumerable collection, Action ForEach(this IEnumerable collection, Action action) { - Condition.Requires(collection, nameof(collection)).IsNotNull(); - Condition.Requires(action, nameof(action)).IsNotNull(); + Ensure.That(collection, nameof(collection)).IsNotNull(); + Ensure.That(action, nameof(action)).IsNotNull(); var i = 0; foreach (var item in collection) { @@ -49,7 +48,7 @@ public static IEnumerable ForEach(this IEnumerable collection, Action ToHashSet(this IEnumerable collection) { - Condition.Requires(collection, nameof(collection)).IsNotNull(); + Ensure.That(collection, nameof(collection)).IsNotNull(); return new HashSet(collection); } diff --git a/Src/DDD.Core.Abstractions/Collections/IStructuralComparableExtensions.cs b/Src/DDD.Core.Abstractions/Collections/IStructuralComparableExtensions.cs new file mode 100644 index 0000000..6de074b --- /dev/null +++ b/Src/DDD.Core.Abstractions/Collections/IStructuralComparableExtensions.cs @@ -0,0 +1,24 @@ +using System.Collections; + +namespace DDD.Collections +{ + /// + /// Adds extension methods to the interface. + /// + public static class IStructuralComparableExtensions + { + + #region Methods + + /// + /// Determines whether the current collection object precedes, occurs in the same position as, or follows another object in the sort order, using the default comparer. + /// + public static int StructuralCompare(this T a, T b) where T : IStructuralComparable + { + return a.CompareTo(b, StructuralComparisons.StructuralComparer); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Abstractions/Collections/IStructuralEquatableExtensions.cs b/Src/DDD.Core.Abstractions/Collections/IStructuralEquatableExtensions.cs new file mode 100644 index 0000000..2f3c8ae --- /dev/null +++ b/Src/DDD.Core.Abstractions/Collections/IStructuralEquatableExtensions.cs @@ -0,0 +1,22 @@ +using System.Collections; + +namespace DDD.Collections +{ + /// + /// Adds extension methods to the interface. + /// + public static class IStructuralEquatableExtensions + { + #region Methods + + /// + /// Determines whether an object is structurally equal to the current instance, using the default equality comparer. + /// + public static bool StructuralEquals(this T a, T b) where T : IStructuralEquatable + { + return a.Equals(b, StructuralComparisons.StructuralEqualityComparer); + } + + #endregion Methods + } +} diff --git a/Src/DDD.Core.Abstractions/Collections/KeyEqualityComparer.cs b/Src/DDD.Core.Abstractions/Collections/KeyEqualityComparer.cs index 1bda346..481efbb 100644 --- a/Src/DDD.Core.Abstractions/Collections/KeyEqualityComparer.cs +++ b/Src/DDD.Core.Abstractions/Collections/KeyEqualityComparer.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Conditions; +using EnsureThat; namespace DDD.Collections { @@ -22,7 +22,7 @@ public class KeyEqualityComparer : IEqualityComparer public KeyEqualityComparer(Func keySelector) { - Condition.Requires(keySelector, nameof(keySelector)).IsNotNull(); + Ensure.That(keySelector, nameof(keySelector)).IsNotNull(); this.keySelector = keySelector; } diff --git a/Src/DDD.Core.Abstractions/DDD.Core.Abstractions.csproj b/Src/DDD.Core.Abstractions/DDD.Core.Abstractions.csproj index dc25e82..3c20567 100644 --- a/Src/DDD.Core.Abstractions/DDD.Core.Abstractions.csproj +++ b/Src/DDD.Core.Abstractions/DDD.Core.Abstractions.csproj @@ -1,97 +1,25 @@ - - - + - Debug - AnyCPU - {596A8700-3D18-4A62-B200-1F78A9EA4617} + net48;netstandard2.1 Library - Properties DDD - DDD.Core.Abstractions - v4.7.2 - 512 - + false - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - false bin\Debug\DDD.Core.Abstractions.xml 1591 - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - false bin\Release\DDD.Core.Abstractions.xml 1591 - - - L:\Packages\Conditions.2.1.0\lib\portable-net45+win+wpa81+wp80+MonoAndroid10+MonoTouch10\Conditions.dll - True - - - - - Properties\CommonAssemblyInfo.cs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - \ No newline at end of file diff --git a/Src/DDD.Core.Abstractions/Data/DbConnectionExtensions.cs b/Src/DDD.Core.Abstractions/Data/DbConnectionExtensions.cs new file mode 100644 index 0000000..fd8c0b1 --- /dev/null +++ b/Src/DDD.Core.Abstractions/Data/DbConnectionExtensions.cs @@ -0,0 +1,34 @@ +using EnsureThat; +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace DDD.Data +{ + using Threading; + + public static class DbConnectionExtensions + { + + #region Methods + + public static void OpenIfClosed(this DbConnection connection) + { + Ensure.That(connection, nameof(connection)).IsNotNull(); + if (connection.State == ConnectionState.Closed) + connection.Open(); + } + + public static async Task OpenIfClosedAsync(this DbConnection connection, CancellationToken cancellationToken = default) + { + Ensure.That(connection, nameof(connection)).IsNotNull(); + await new SynchronizationContextRemover(); + if (connection.State == ConnectionState.Closed) + await connection.OpenAsync(cancellationToken); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Abstractions/Data/DbConnectionHelper.cs b/Src/DDD.Core.Abstractions/Data/DbConnectionHelper.cs new file mode 100644 index 0000000..afd7e83 --- /dev/null +++ b/Src/DDD.Core.Abstractions/Data/DbConnectionHelper.cs @@ -0,0 +1,27 @@ +using EnsureThat; +using System.Data.Common; + +namespace DDD.Data +{ + /// + /// Helper class for DateTime. + /// + public class DbConnectionHelper + { + + #region Methods + + public static DbConnection CreateConnection(string providerName, string connectionString) + { + Ensure.That(providerName, nameof(providerName)).IsNotNullOrWhiteSpace(); + Ensure.That(connectionString, nameof(connectionString)).IsNotNullOrWhiteSpace(); + var providerFactory = DbProviderFactories.GetFactory(providerName); + var connection = providerFactory.CreateConnection(); + connection.ConnectionString = connectionString; + return connection; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Abstractions/DateTimeExtensions.cs b/Src/DDD.Core.Abstractions/DateTimeExtensions.cs index bfa33a8..02c3359 100644 --- a/Src/DDD.Core.Abstractions/DateTimeExtensions.cs +++ b/Src/DDD.Core.Abstractions/DateTimeExtensions.cs @@ -12,19 +12,37 @@ public static class DateTimeExtensions #region Methods /// - /// Converts the value of the current DateTime object to its equivalent short date string representation using the culture-specific format information. + /// Converts the value of the current DateTime object to its equivalent short date string representation using the french culture-specific format information. /// - public static string ToShortDateString(this DateTime instance, IFormatProvider provider) + public static string ToFrenchShortDateString(this DateTime instance) { - return instance.ToString("d", provider); + return instance.ToString("d", new CultureInfo("fr-FR")); } /// - /// Converts the value of the current DateTime object to its equivalent short date string representation using the french culture-specific format information. + /// Converts the value of the current nullable DateTime object to its equivalent not nullable representation. /// - public static string ToFrenchShortDateString(this DateTime instance) + public static DateTime ToNotNullable(this DateTime? instance) { - return instance.ToString("d", new CultureInfo("fr-FR")); + if (instance == null) return default; + return instance.Value; + } + + /// + /// Converts the value of the current DateTime object to its equivalent nullable representation. + /// + public static DateTime? ToNullable(this DateTime instance) + { + if (instance == default) return null; + return instance; + } + + /// + /// Converts the value of the current DateTime object to its equivalent short date string representation using the culture-specific format information. + /// + public static string ToShortDateString(this DateTime instance, IFormatProvider provider) + { + return instance.ToString("d", provider); } #endregion Methods diff --git a/Src/DDD.Core.Abstractions/DelegatingTimestampProvider.cs b/Src/DDD.Core.Abstractions/DelegatingTimestampProvider.cs new file mode 100644 index 0000000..684e5cd --- /dev/null +++ b/Src/DDD.Core.Abstractions/DelegatingTimestampProvider.cs @@ -0,0 +1,35 @@ +using EnsureThat; +using System; + +namespace DDD +{ + /// + /// Adapter that converts a delegate into an object that implements ITimestampProvider. + /// + public class DelegatingTimestampProvider : ITimestampProvider + { + + #region Fields + + private readonly Func timestamp; + + #endregion Fields + + #region Constructors + + public DelegatingTimestampProvider(Func timestamp) + { + Ensure.That(timestamp, nameof(timestamp)).IsNotNull(); + this.timestamp = timestamp; + } + + #endregion Constructors + + #region Methods + + public DateTime GetTimestamp() => this.timestamp(); + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Abstractions/EnumerableArgExtensions.cs b/Src/DDD.Core.Abstractions/EnumerableArgExtensions.cs new file mode 100644 index 0000000..2c88b46 --- /dev/null +++ b/Src/DDD.Core.Abstractions/EnumerableArgExtensions.cs @@ -0,0 +1,28 @@ +using EnsureThat; +using EnsureThat.Enforcers; +using System.Collections.Generic; +using System.Linq; + +namespace DDD +{ + public static class EnumerableArgExtensions + { + + /// + /// Ensures that the sequence does not contain null values. + /// + public static IEnumerable HasNoNull(this EnumerableArg _, IEnumerable value, string paramName = null, OptsFn optsFn = null) + { + Ensure.Any.IsNotNull(value, paramName); + + if (value.Any(i => i == null)) + throw Ensure.ExceptionFactory.ArgumentException( + "Sequence contains null values.", + paramName, + optsFn); + + return value; + } + + } +} diff --git a/Src/DDD.Core.Abstractions/ExceptionExtensions.cs b/Src/DDD.Core.Abstractions/ExceptionExtensions.cs new file mode 100644 index 0000000..f98276b --- /dev/null +++ b/Src/DDD.Core.Abstractions/ExceptionExtensions.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading; +using EnsureThat; + +namespace DDD +{ + public static class ExceptionExtensions + { + + #region Methods + + public static string FullSource(this Exception exception) + { + Ensure.That(exception, nameof(exception)).IsNotNull(); + if (exception.TargetSite == null) return exception.Source; + var sourceType = exception.TargetSite.ReflectedType; + return $"{sourceType.Assembly.GetName().Name}, {sourceType.FullName}, {exception.TargetSite}"; + } + + /// + /// Indicates whether this exception represents a contract error : some preconditions, postconditions or object invariants are not met. + /// + public static bool IsContractError(this Exception exception) + { + Ensure.That(exception, nameof(exception)).IsNotNull(); + switch(exception) + { + case ArgumentException _: + case InvalidOperationException _: + case FormatException _: + case NotSupportedException _: + return true; + default: + return false; + } + } + + /// + /// Indicates whether this exception should be wrapped. + /// + /// The type of the wrapped exception. + public static bool ShouldBeWrappedIn(this Exception exception) where TDestination : Exception + { + Ensure.That(exception, nameof(exception)).IsNotNull(); + switch (exception) + { + case TDestination _: + case OutOfMemoryException _: + case OperationCanceledException _: + case StackOverflowException _: + case ThreadAbortException _: + return false; + default: + return !exception.IsContractError(); + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Abstractions/FluentBuilder.cs b/Src/DDD.Core.Abstractions/FluentBuilder.cs new file mode 100644 index 0000000..9f53ce7 --- /dev/null +++ b/Src/DDD.Core.Abstractions/FluentBuilder.cs @@ -0,0 +1,40 @@ +using System; +using System.ComponentModel; + +namespace DDD +{ + /// + /// Base class for building an object in a fluent way. + /// + public abstract class FluentBuilder : IObjectBuilder + where T : class + { + + #region Methods + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => base.Equals(obj); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => base.GetHashCode(); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override string ToString() => base.ToString(); + + /// Gets the of the current instance. + /// The instance that represents the exact runtime + /// type of the current instance. + [EditorBrowsable(EditorBrowsableState.Never)] + public new Type GetType() => base.GetType(); + + T IObjectBuilder.Build() => this.Build(); + + protected abstract T Build(); + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Abstractions/IObjectBuilder.cs b/Src/DDD.Core.Abstractions/IObjectBuilder.cs new file mode 100644 index 0000000..f6d1827 --- /dev/null +++ b/Src/DDD.Core.Abstractions/IObjectBuilder.cs @@ -0,0 +1,15 @@ +namespace DDD +{ + /// + /// Defines a method that builds an object of a specified type. + /// + public interface IObjectBuilder + where T : class + { + #region Methods + + T Build(); + + #endregion Methods + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Abstractions/IServiceProviderExtensions.cs b/Src/DDD.Core.Abstractions/IServiceProviderExtensions.cs index 5e016a9..cb649ac 100644 --- a/Src/DDD.Core.Abstractions/IServiceProviderExtensions.cs +++ b/Src/DDD.Core.Abstractions/IServiceProviderExtensions.cs @@ -1,5 +1,7 @@ -using Conditions; +using EnsureThat; using System; +using System.Collections.Generic; +using System.Linq; namespace DDD { @@ -19,10 +21,36 @@ public static class IServiceProviderExtensions /// A service object of type or null if there is no such service. public static T GetService(this IServiceProvider provider) { - Condition.Requires(provider, nameof(provider)).IsNotNull(); + Ensure.That(provider, nameof(provider)).IsNotNull(); return (T)provider.GetService(typeof(T)); } + /// + /// Get an enumeration of services of type from the . + /// + /// The type of service object to get. + /// The to retrieve the services from. + /// An enumeration of services of type . + public static IEnumerable GetServices(this IServiceProvider provider) + { + Ensure.That(provider, nameof(provider)).IsNotNull(); + return provider.GetService>() ?? Enumerable.Empty(); + } + + /// + /// Get an enumeration of services of type from the . + /// + /// The to retrieve the services from. + /// An object that specifies the type of service object to get. + /// An enumeration of services of type . + public static IEnumerable GetServices(this IServiceProvider provider, Type serviceType) + { + Ensure.That(provider, nameof(provider)).IsNotNull(); + Ensure.That(serviceType, nameof(serviceType)).IsNotNull(); + var enumerableServiceType = typeof(IEnumerable<>).MakeGenericType(serviceType); + return (IEnumerable)provider.GetService(enumerableServiceType) ?? Enumerable.Empty(); + } + #endregion Methods } diff --git a/Src/DDD.Core.Abstractions/ITimestampProvider.cs b/Src/DDD.Core.Abstractions/ITimestampProvider.cs new file mode 100644 index 0000000..22a4093 --- /dev/null +++ b/Src/DDD.Core.Abstractions/ITimestampProvider.cs @@ -0,0 +1,17 @@ +using System; + +namespace DDD +{ + /// + /// Defines a method that provides timestamps. + /// + public interface ITimestampProvider + { + + #region Methods + + DateTime GetTimestamp(); + + #endregion Methods + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Abstractions/Linq/IEnumerableExtensions.cs b/Src/DDD.Core.Abstractions/Linq/IEnumerableExtensions.cs index 1a4d84f..3e13f2c 100644 --- a/Src/DDD.Core.Abstractions/Linq/IEnumerableExtensions.cs +++ b/Src/DDD.Core.Abstractions/Linq/IEnumerableExtensions.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using System.Collections.Generic; -using Conditions; +using EnsureThat; namespace DDD.Linq { @@ -19,9 +19,9 @@ public static IEnumerable Except(this IEnumerable IEnumerable source2, Func keySelector) { - Condition.Requires(source1, nameof(source1)).IsNotNull(); - Condition.Requires(source2, nameof(source2)).IsNotNull(); - Condition.Requires(keySelector, nameof(keySelector)).IsNotNull(); + Ensure.That(source1, nameof(source1)).IsNotNull(); + Ensure.That(source2, nameof(source2)).IsNotNull(); + Ensure.That(keySelector, nameof(keySelector)).IsNotNull(); return source1.Except(source2, new KeyEqualityComparer(keySelector)); } @@ -29,12 +29,49 @@ public static IEnumerable Union(this IEnumerable< IEnumerable source2, Func keySelector) { - Condition.Requires(source1, nameof(source1)).IsNotNull(); - Condition.Requires(source2, nameof(source2)).IsNotNull(); - Condition.Requires(keySelector, nameof(keySelector)).IsNotNull(); + Ensure.That(source1, nameof(source1)).IsNotNull(); + Ensure.That(source2, nameof(source2)).IsNotNull(); + Ensure.That(keySelector, nameof(keySelector)).IsNotNull(); return source1.Union(source2, new KeyEqualityComparer(keySelector)); } + /// + /// Returns a new enumerable collection that contains the elements from source with the last count elements of the source collection omitted. + /// + /// + /// If count is not a positive number, this method returns an identical copy of the source enumerable collection. + /// + public static IEnumerable SkipLast(this IEnumerable source, int count) + { + Ensure.That(source, nameof(source)).IsNotNull(); + if (count <= 0) + { + foreach (var item in source) + yield return item; + } + else + { + var queue = new Queue(); + using (var e = source.GetEnumerator()) + { + while (e.MoveNext()) + { + if (queue.Count == count) + { + do + { + yield return queue.Dequeue(); + queue.Enqueue(e.Current); + } + while (e.MoveNext()); + } + else + queue.Enqueue(e.Current); + } + } + } + } + #endregion Methods } } \ No newline at end of file diff --git a/Src/DDD.Core.Abstractions/LocalTimestampProvider.cs b/Src/DDD.Core.Abstractions/LocalTimestampProvider.cs new file mode 100644 index 0000000..ead4198 --- /dev/null +++ b/Src/DDD.Core.Abstractions/LocalTimestampProvider.cs @@ -0,0 +1,16 @@ +using System; + +namespace DDD +{ + /// + /// Provides timestamps based on the current date and time on this computer, expressed as the local time. + /// + public class LocalTimestampProvider : ITimestampProvider + { + #region Methods + + public DateTime GetTimestamp() => DateTime.Now; + + #endregion Methods + } +} diff --git a/Src/DDD.Core.Abstractions/Mapping/CompositeTranslator.cs b/Src/DDD.Core.Abstractions/Mapping/CompositeTranslator.cs new file mode 100644 index 0000000..79c09f3 --- /dev/null +++ b/Src/DDD.Core.Abstractions/Mapping/CompositeTranslator.cs @@ -0,0 +1,62 @@ +using EnsureThat; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace DDD.Mapping +{ + /// + /// A translator composed from multiple child translators. + /// + public class CompositeTranslator : ObjectTranslator + where TSource : class + where TDestination : class + { + + #region Fields + + private readonly List translators = new List(); + + #endregion Fields + + #region Methods + + /// + /// Registers a child translator. + /// + /// + /// The order of registrations is important. Register the translators from the most derived source type to the least derived source type. + /// + public void Register(IObjectTranslator translator) + where TDerivedSource : class, TSource + { + Ensure.That(translator, nameof(translator)).IsNotNull(); + this.translators.Add(translator); + } + + /// + /// Registers a child translator from a delegate. + /// + /// + /// The order of registrations is important. Register the translators from the most derived source type to the least derived source type. + /// + public void Register(Func translator) + where TDerivedSource : class, TSource + { + Ensure.That(translator, nameof(translator)).IsNotNull(); + this.translators.Add(new DelegatingTranslator(translator)); + } + + public override TDestination Translate(TSource source, IMappingContext context) + { + if (source == null) return null; + var translator = this.translators.FirstOrDefault(t => t.SourceType.IsAssignableFrom(source.GetType())); + if (translator != null) + return (TDestination)translator.Translate(source, context); + throw new MappingException($"No child translator registered for '{source.GetType()}'."); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Abstractions/Mapping/DelegatingMapper.cs b/Src/DDD.Core.Abstractions/Mapping/DelegatingMapper.cs new file mode 100644 index 0000000..5e5f53a --- /dev/null +++ b/Src/DDD.Core.Abstractions/Mapping/DelegatingMapper.cs @@ -0,0 +1,43 @@ +using System; +using EnsureThat; + +namespace DDD.Mapping +{ + /// + /// Adapter that converts a delegate into an object that implements the interface IObjectMapper. + /// + public class DelegatingMapper : IObjectMapper + where TSource : class + where TDestination : class + { + + #region Fields + + private readonly Action mapper; + + #endregion Fields + + #region Constructors + + public DelegatingMapper(Action mapper) + { + Ensure.That(mapper, nameof(mapper)).IsNotNull(); + this.mapper = mapper; + } + + #endregion Constructors + + #region Methods + + public void Map(TSource source, TDestination destination, IMappingContext context) + { + Ensure.That(source).IsNotNull(); + Ensure.That(destination).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + this.Map(source, destination, context); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Abstractions/Mapping/DelegatingTranslator.cs b/Src/DDD.Core.Abstractions/Mapping/DelegatingTranslator.cs index ee06db2..7fb5851 100644 --- a/Src/DDD.Core.Abstractions/Mapping/DelegatingTranslator.cs +++ b/Src/DDD.Core.Abstractions/Mapping/DelegatingTranslator.cs @@ -1,36 +1,39 @@ using System; -using System.Collections.Generic; -using Conditions; +using EnsureThat; namespace DDD.Mapping { - public class DelegatingTranslator : IObjectTranslator + /// + /// Adapter that converts a delegate into an object that implements the interface IObjectTranslator. + /// + public class DelegatingTranslator : ObjectTranslator where TSource : class where TDestination : class { #region Fields - private readonly Func, TDestination> toDestination; + private readonly Func translator; #endregion Fields #region Constructors - public DelegatingTranslator(Func, TDestination> toDestination) + public DelegatingTranslator(Func translator) { - Condition.Requires(toDestination).IsNotNull(); - this.toDestination = toDestination; + Ensure.That(translator).IsNotNull(); + this.translator = translator; } #endregion Constructors #region Methods - public TDestination Translate(TSource source, IDictionary options = null) + public override TDestination Translate(TSource source, IMappingContext context) { - Condition.Requires(source).IsNotNull(); - return this.toDestination(source, options); + Ensure.That(source).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + return this.translator(source, context); } #endregion Methods diff --git a/Src/DDD.Core.Abstractions/Mapping/IMappingContext.cs b/Src/DDD.Core.Abstractions/Mapping/IMappingContext.cs new file mode 100644 index 0000000..b9a73d1 --- /dev/null +++ b/Src/DDD.Core.Abstractions/Mapping/IMappingContext.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace DDD.Mapping +{ + /// + /// Provides access to the mapping context. + /// + public interface IMappingContext : IDictionary + { + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Abstractions/Mapping/IMappingContextExtensions.cs b/Src/DDD.Core.Abstractions/Mapping/IMappingContextExtensions.cs new file mode 100644 index 0000000..98a8e38 --- /dev/null +++ b/Src/DDD.Core.Abstractions/Mapping/IMappingContextExtensions.cs @@ -0,0 +1,30 @@ +using EnsureThat; +using System; + +namespace DDD.Mapping +{ + using Collections; + + public static class IMappingContextExtensions + { + + #region Methods + + public static Type DestinationType(this IMappingContext context) + { + Ensure.That(context, nameof(context)).IsNotNull(); + context.TryGetValue(MappingContextInfo.DestinationType, out Type destinationType); + return destinationType; + } + + public static void AddDestinationType(this IMappingContext context, Type destinationType) + { + Ensure.That(context, nameof(context)).IsNotNull(); + Ensure.That(destinationType, nameof(destinationType)).IsNotNull(); + context.Add(MappingContextInfo.DestinationType, destinationType); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Abstractions/Mapping/IMappingProcessor.cs b/Src/DDD.Core.Abstractions/Mapping/IMappingProcessor.cs index 76e9632..e9d753d 100644 --- a/Src/DDD.Core.Abstractions/Mapping/IMappingProcessor.cs +++ b/Src/DDD.Core.Abstractions/Mapping/IMappingProcessor.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace DDD.Mapping +namespace DDD.Mapping { /// /// Defines a component that processes object-object mappings. @@ -10,14 +8,14 @@ public interface IMappingProcessor #region Methods - void Map(TSource source, TDestination destination, IDictionary options = null) + void Map(TSource source, TDestination destination, IMappingContext context) where TSource : class where TDestination : class; - TDestination Translate(object source, IDictionary options = null) + TDestination Translate(object source, IMappingContext context) where TDestination : class; - TDestination Translate(TSource source, IDictionary options = null) + TDestination Translate(TSource source, IMappingContext context) where TSource : class where TDestination : class; diff --git a/Src/DDD.Core.Abstractions/Mapping/IMappingProcessorExtensions.cs b/Src/DDD.Core.Abstractions/Mapping/IMappingProcessorExtensions.cs index ca2b9ec..b2349b9 100644 --- a/Src/DDD.Core.Abstractions/Mapping/IMappingProcessorExtensions.cs +++ b/Src/DDD.Core.Abstractions/Mapping/IMappingProcessorExtensions.cs @@ -1,93 +1,126 @@ -using Conditions; +using EnsureThat; using System.Collections.Generic; namespace DDD.Mapping { - using Collections; - public static class IMappingProcessorExtensions { #region Methods + public static void Map(this IMappingProcessor processor, + TSource source, + TDestination destination) + where TSource : class + where TDestination : class + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + processor.Map(source, destination, new MappingContext()); + } + + public static TDestination Translate(this IMappingProcessor processor, + object source) + where TDestination : class + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.Translate(source, new MappingContext()); + } + + public static TDestination Translate(this IMappingProcessor processor, + TSource source) + where TSource : class + where TDestination : class + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.Translate(source, new MappingContext()); + } public static void Map(this IMappingProcessor processor, TSource source, TDestination destination, - object options) + object context) where TSource : class where TDestination : class { - Condition.Requires(processor, nameof(processor)).IsNotNull(); - var dictionary = new Dictionary(); - dictionary.AddObject(options); - processor.Map(source, destination, dictionary); + Ensure.That(processor, nameof(processor)).IsNotNull(); + processor.Map(source, destination, MappingContext.FromObject(context)); } public static TDestination Translate(this IMappingProcessor processor, object source, - object options) + object context) where TDestination : class { - Condition.Requires(processor, nameof(processor)).IsNotNull(); - var dictionary = new Dictionary(); - dictionary.AddObject(options); - return processor.Translate(source, dictionary); + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.Translate(source, MappingContext.FromObject(context)); } public static TDestination Translate(this IMappingProcessor processor, TSource source, - object options) + object context) + where TSource : class + where TDestination : class + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.Translate(source, MappingContext.FromObject(context)); + } + + public static IEnumerable TranslateCollection(this IMappingProcessor processor, + IEnumerable source) where TSource : class where TDestination : class { - Condition.Requires(processor, nameof(processor)).IsNotNull(); - var dictionary = new Dictionary(); - dictionary.AddObject(options); - return processor.Translate(source, dictionary); + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.TranslateCollection(source, new MappingContext()); + } + + public static IEnumerable TranslateCollection(this IMappingProcessor processor, + IEnumerable source) + where TDestination : class + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.TranslateCollection(source, new MappingContext()); } public static IEnumerable TranslateCollection(this IMappingProcessor processor, IEnumerable source, - IDictionary options = null) + IMappingContext context) where TSource : class where TDestination : class { - Condition.Requires(processor, nameof(processor)).IsNotNull(); + Ensure.That(processor, nameof(processor)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); foreach (var item in source) - yield return processor.Translate(item, options); + yield return processor.Translate(item, context); } public static IEnumerable TranslateCollection(this IMappingProcessor processor, IEnumerable source, - IDictionary options = null) + IMappingContext context) where TDestination : class { - Condition.Requires(processor, nameof(processor)).IsNotNull(); + Ensure.That(processor, nameof(processor)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); foreach (var item in source) - yield return processor.Translate(item, options); + yield return processor.Translate(item, context); } public static IEnumerable TranslateCollection(this IMappingProcessor processor, IEnumerable source, - object options) + object context) where TSource : class where TDestination : class { - Condition.Requires(processor, nameof(processor)).IsNotNull(); - var dictionary = new Dictionary(); - dictionary.AddObject(options); - return processor.TranslateCollection(source, dictionary); + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.TranslateCollection(source, MappingContext.FromObject(context)); } public static IEnumerable TranslateCollection(this IMappingProcessor processor, IEnumerable source, - object options) + object context) where TDestination : class { - Condition.Requires(processor, nameof(processor)).IsNotNull(); - var dictionary = new Dictionary(); - dictionary.AddObject(options); - return processor.TranslateCollection(source, dictionary); + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.TranslateCollection(source, MappingContext.FromObject(context)); } #endregion Methods diff --git a/Src/DDD.Core.Abstractions/Mapping/IObjectMapper.cs b/Src/DDD.Core.Abstractions/Mapping/IObjectMapper.cs index 2f6e878..c6c110d 100644 --- a/Src/DDD.Core.Abstractions/Mapping/IObjectMapper.cs +++ b/Src/DDD.Core.Abstractions/Mapping/IObjectMapper.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace DDD.Mapping +namespace DDD.Mapping { /// /// Defines a method that maps an input object of one type to an output object of a different type. @@ -12,7 +10,7 @@ public interface IObjectMapper #region Methods - void Map(TSource source, TDestination destination, IDictionary options = null); + void Map(TSource source, TDestination destination, IMappingContext context); #endregion Methods diff --git a/Src/DDD.Core.Abstractions/Mapping/IObjectMapperExtensions.cs b/Src/DDD.Core.Abstractions/Mapping/IObjectMapperExtensions.cs index edbfb6a..6446fe1 100644 --- a/Src/DDD.Core.Abstractions/Mapping/IObjectMapperExtensions.cs +++ b/Src/DDD.Core.Abstractions/Mapping/IObjectMapperExtensions.cs @@ -1,26 +1,31 @@ -using Conditions; -using System.Collections.Generic; +using EnsureThat; namespace DDD.Mapping { - using Collections; - public static class IObjectMapperExtensions { #region Methods + public static void Map(this IObjectMapper mapper, + TSource source, + TDestination destination) + where TSource : class + where TDestination : class + { + Ensure.That(mapper, nameof(mapper)).IsNotNull(); + mapper.Map(source, destination, new MappingContext()); + } + public static void Map(this IObjectMapper mapper, TSource source, TDestination destination, - object options) + object context) where TSource : class where TDestination : class { - Condition.Requires(mapper, nameof(mapper)).IsNotNull(); - var dictionary = new Dictionary(); - dictionary.AddObject(options); - mapper.Map(source, destination, dictionary); + Ensure.That(mapper, nameof(mapper)).IsNotNull(); + mapper.Map(source, destination, MappingContext.FromObject(context)); } #endregion Methods diff --git a/Src/DDD.Core.Abstractions/Mapping/IObjectTranslator.cs b/Src/DDD.Core.Abstractions/Mapping/IObjectTranslator.cs index 0add4ad..f851524 100644 --- a/Src/DDD.Core.Abstractions/Mapping/IObjectTranslator.cs +++ b/Src/DDD.Core.Abstractions/Mapping/IObjectTranslator.cs @@ -1,18 +1,27 @@ -using System.Collections.Generic; +using System; namespace DDD.Mapping { /// /// Defines a method that translates an input object of one type into an output object of a different type. /// - public interface IObjectTranslator - where TSource : class - where TDestination : class + public interface IObjectTranslator { + + #region Properties + + Type SourceType { get; } + + Type DestinationType { get; } + + + #endregion Properties + #region Methods - TDestination Translate(TSource source, IDictionary options = null); + object Translate(object source, IMappingContext context); #endregion Methods + } } diff --git a/Src/DDD.Core.Abstractions/Mapping/IObjectTranslatorExtensions.cs b/Src/DDD.Core.Abstractions/Mapping/IObjectTranslatorExtensions.cs index 6780c00..83f3c67 100644 --- a/Src/DDD.Core.Abstractions/Mapping/IObjectTranslatorExtensions.cs +++ b/Src/DDD.Core.Abstractions/Mapping/IObjectTranslatorExtensions.cs @@ -1,48 +1,61 @@ -using Conditions; +using EnsureThat; using System.Collections.Generic; namespace DDD.Mapping { - using Collections; - public static class IObjectTranslatorExtensions { #region Methods + public static TDestination Translate(this IObjectTranslator translator, + TSource source) + where TSource : class + where TDestination : class + { + Ensure.That(translator, nameof(translator)).IsNotNull(); + return translator.Translate(source, new MappingContext()); + } + public static TDestination Translate(this IObjectTranslator translator, TSource source, - object options) + object context) + where TSource : class + where TDestination : class + { + Ensure.That(translator, nameof(translator)).IsNotNull(); + return translator.Translate(source, MappingContext.FromObject(context)); + } + + public static IEnumerable TranslateCollection(this IObjectTranslator translator, + IEnumerable source) where TSource : class where TDestination : class { - Condition.Requires(translator, nameof(translator)).IsNotNull(); - var dictionary = new Dictionary(); - dictionary.AddObject(options); - return translator.Translate(source, dictionary); + Ensure.That(translator, nameof(translator)).IsNotNull(); + return translator.TranslateCollection(source, new MappingContext()); } public static IEnumerable TranslateCollection(this IObjectTranslator translator, IEnumerable source, - IDictionary options = null) + IMappingContext context) where TSource : class where TDestination : class { - Condition.Requires(translator, nameof(translator)).IsNotNull(); + Ensure.That(translator, nameof(translator)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); foreach (var item in source) - yield return translator.Translate(item, options); + yield return translator.Translate(item, context); } public static IEnumerable TranslateCollection(this IObjectTranslator translator, IEnumerable source, - object options) + object context) where TSource : class where TDestination : class { - Condition.Requires(translator, nameof(translator)).IsNotNull(); - var dictionary = new Dictionary(); - dictionary.AddObject(options); - return translator.TranslateCollection(source, dictionary); + Ensure.That(translator, nameof(translator)).IsNotNull(); + return translator.TranslateCollection(source, MappingContext.FromObject(context)); } #endregion Methods diff --git a/Src/DDD.Core.Abstractions/Mapping/IObjectTranslator`1.cs b/Src/DDD.Core.Abstractions/Mapping/IObjectTranslator`1.cs new file mode 100644 index 0000000..e813960 --- /dev/null +++ b/Src/DDD.Core.Abstractions/Mapping/IObjectTranslator`1.cs @@ -0,0 +1,16 @@ +namespace DDD.Mapping +{ + /// + /// Defines a method that translates an input object of one type into an output object of a different type. + /// + public interface IObjectTranslator : IObjectTranslator + where TSource : class + where TDestination : class + { + #region Methods + + TDestination Translate(TSource source, IMappingContext context); + + #endregion Methods + } +} diff --git a/Src/DDD.Core.Abstractions/Mapping/MappingContext.cs b/Src/DDD.Core.Abstractions/Mapping/MappingContext.cs new file mode 100644 index 0000000..df52741 --- /dev/null +++ b/Src/DDD.Core.Abstractions/Mapping/MappingContext.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace DDD.Mapping +{ + using Collections; + + public class MappingContext + : Dictionary, IMappingContext + { + + #region Constructors + + public MappingContext() + { + } + public MappingContext(IDictionary dictionary) : base(dictionary) + { + } + + #endregion Constructors + + #region Methods + + public static IMappingContext FromObject(object context) + { + var mappingContext = new MappingContext(); + if (context != null) + mappingContext.AddObject(context); + return mappingContext; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Abstractions/Mapping/MappingContextInfo.cs b/Src/DDD.Core.Abstractions/Mapping/MappingContextInfo.cs new file mode 100644 index 0000000..7c468e2 --- /dev/null +++ b/Src/DDD.Core.Abstractions/Mapping/MappingContextInfo.cs @@ -0,0 +1,16 @@ +namespace DDD.Mapping +{ + /// + /// /// + /// Standard contextual information about mapping. + /// + /// + public class MappingContextInfo + { + #region Fields + + public const string DestinationType = nameof(DestinationType); + + #endregion Fields + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Abstractions/Mapping/MappingException.cs b/Src/DDD.Core.Abstractions/Mapping/MappingException.cs new file mode 100644 index 0000000..9ebe507 --- /dev/null +++ b/Src/DDD.Core.Abstractions/Mapping/MappingException.cs @@ -0,0 +1,68 @@ +using System; + +namespace DDD.Mapping +{ + /// + /// Exception thrown by mappers or translators when a problem occurred while mapping objects. + /// + public class MappingException : Exception + { + + #region Constructors + + public MappingException() + : base("A problem occurred while mapping objects.") + { + } + + public MappingException(string message) : base(message) + { + } + + public MappingException(string message, Exception innerException) : base(message, innerException) + { + } + + public MappingException(string message, Exception innerException, Type sourceType, Type destinationType) : base(message, innerException) + { + this.SourceType = sourceType; + this.DestinationType = destinationType; + } + + public MappingException(Exception innerException, Type sourceType, Type destinationType) + : base($"A problem occurred while mapping one object of type '{sourceType?.Name}' to another object of type '{destinationType?.Name}'.", innerException) + { + this.SourceType = sourceType; + this.DestinationType = destinationType; + } + + #endregion Constructors + + #region Properties + + public Type DestinationType { get; } + + public Type SourceType { get; } + + #endregion Properties + + #region Methods + + public override string ToString() + { + var s = $"{this.GetType()}: {this.Message} "; + if (this.SourceType != null) + s += $"{Environment.NewLine}{nameof(SourceType)}: {this.SourceType}"; + if (this.DestinationType != null) + s += $"{Environment.NewLine}{nameof(DestinationType)}: {this.DestinationType}"; + if (this.InnerException != null) + s += $" ---> {this.InnerException}"; + if (this.StackTrace != null) + s += $"{Environment.NewLine}{this.StackTrace}"; + return s; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Abstractions/Mapping/MappingProcessor.cs b/Src/DDD.Core.Abstractions/Mapping/MappingProcessor.cs index a3b6f2d..e1f01ef 100644 --- a/Src/DDD.Core.Abstractions/Mapping/MappingProcessor.cs +++ b/Src/DDD.Core.Abstractions/Mapping/MappingProcessor.cs @@ -1,6 +1,5 @@ -using Conditions; +using EnsureThat; using System; -using System.Collections.Generic; namespace DDD.Mapping { @@ -13,47 +12,70 @@ public class MappingProcessor : IMappingProcessor #region Fields private readonly IServiceProvider serviceProvider; + private readonly MappingProcessorSettings settings; #endregion Fields #region Constructors - public MappingProcessor(IServiceProvider serviceProvider) + public MappingProcessor(IServiceProvider serviceProvider, MappingProcessorSettings settings) { - Condition.Requires(serviceProvider, nameof(serviceProvider)).IsNotNull(); + Ensure.That(serviceProvider, nameof(serviceProvider)).IsNotNull(); + Ensure.That(settings, nameof(settings)).IsNotNull(); this.serviceProvider = serviceProvider; + this.settings = settings; } #endregion Constructors #region Methods - public void Map(TSource source, TDestination destination, IDictionary options = null) + public void Map(TSource source, TDestination destination, IMappingContext context) where TSource : class where TDestination : class { var mapper = this.serviceProvider.GetService>(); - if (mapper == null) throw new InvalidOperationException($"The mapper for type {typeof(IObjectMapper)} could not be found."); - mapper.Map(source, destination, options); + if (mapper == null) + { + if (this.settings.DefaultMapper == null) + throw new InvalidOperationException($"The mapper for type {typeof(IObjectMapper)} could not be found."); + this.settings.DefaultMapper.Map(source, destination); + } + else + mapper.Map(source, destination, context); } - public TDestination Translate(object source, IDictionary options = null) + public TDestination Translate(object source, IMappingContext context) where TDestination : class { + Ensure.That(context, nameof(context)).IsNotNull(); if (source == null) return null; var translatorType = typeof(IObjectTranslator<,>).MakeGenericType(source.GetType(), typeof(TDestination)); dynamic translator = this.serviceProvider.GetService(translatorType); - if (translator == null) throw new InvalidOperationException($"The translator for type {translatorType} could not be found."); - return translator.Translate((dynamic)source, options); + if (translator == null) + { + if (this.settings.DefaultTranslator == null) + throw new InvalidOperationException($"The translator for type {translatorType} could not be found."); + context.AddDestinationType(typeof(TDestination)); + return (TDestination)this.settings.DefaultTranslator.Translate(source, context); + } + return translator.Translate((dynamic)source, context); } - public TDestination Translate(TSource source, IDictionary options = null) + public TDestination Translate(TSource source, IMappingContext context) where TSource : class where TDestination : class { + Ensure.That(context, nameof(context)).IsNotNull(); var translator = this.serviceProvider.GetService>(); - if (translator == null) throw new InvalidOperationException($"The translator for type {typeof(IObjectTranslator)} could not be found."); - return translator.Translate(source, options); + if (translator == null) + { + if (this.settings.DefaultTranslator == null) + throw new InvalidOperationException($"The translator for type {typeof(IObjectTranslator)} could not be found."); + context.AddDestinationType(typeof(TDestination)); + return (TDestination)this.settings.DefaultTranslator.Translate(source, context); + } + return translator.Translate(source, context); } #endregion Methods diff --git a/Src/DDD.Core.Abstractions/Mapping/MappingProcessorSettings.cs b/Src/DDD.Core.Abstractions/Mapping/MappingProcessorSettings.cs new file mode 100644 index 0000000..372f080 --- /dev/null +++ b/Src/DDD.Core.Abstractions/Mapping/MappingProcessorSettings.cs @@ -0,0 +1,26 @@ +namespace DDD.Mapping +{ + public class MappingProcessorSettings + { + + #region Constructors + + public MappingProcessorSettings(IObjectMapper defaultMapper = null, + IObjectTranslator defaultTranslator = null) + { + this.DefaultMapper = defaultMapper; + this.DefaultTranslator = defaultTranslator; + } + + #endregion Constructors + + #region Properties + + public IObjectMapper DefaultMapper { get; } + + public IObjectTranslator DefaultTranslator { get; } + + #endregion Properties + + } +} diff --git a/Src/DDD.Core.Abstractions/Mapping/ObjectTranslator.cs b/Src/DDD.Core.Abstractions/Mapping/ObjectTranslator.cs new file mode 100644 index 0000000..7da8f1c --- /dev/null +++ b/Src/DDD.Core.Abstractions/Mapping/ObjectTranslator.cs @@ -0,0 +1,28 @@ +using System; + +namespace DDD.Mapping +{ + /// + /// Base class for translating objects. + /// + public abstract class ObjectTranslator : IObjectTranslator + where TSource : class + where TDestination : class + { + #region Properties + + Type IObjectTranslator.SourceType => typeof(TSource); + + Type IObjectTranslator.DestinationType => typeof(TDestination); + + #endregion Properties + + #region Methods + + public abstract TDestination Translate(TSource source, IMappingContext context); + + object IObjectTranslator.Translate(object source, IMappingContext context) => this.Translate((TSource)source, context); + + #endregion Methods + } +} diff --git a/Src/DDD.Core.Abstractions/ParamExtensions.cs b/Src/DDD.Core.Abstractions/ParamExtensions.cs new file mode 100644 index 0000000..29485b9 --- /dev/null +++ b/Src/DDD.Core.Abstractions/ParamExtensions.cs @@ -0,0 +1,20 @@ +using EnsureThat; +using System; + +namespace DDD +{ + public static class ParamExtensions + { + + #region Methods + + /// + /// Ensures that the argument satisfies the specified condition. + /// + public static void Satisfy(this in Param param, Func predicate) + => Ensure.Any.Satisfy(param.Value, predicate, param.Name, param.OptsFn); + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Abstractions/Serialization/ISerializer.cs b/Src/DDD.Core.Abstractions/Serialization/ISerializer.cs index 1fb21be..12079c2 100644 --- a/Src/DDD.Core.Abstractions/Serialization/ISerializer.cs +++ b/Src/DDD.Core.Abstractions/Serialization/ISerializer.cs @@ -1,13 +1,20 @@ -using System.IO; +using System; +using System.IO; namespace DDD.Serialization { public interface ISerializer { + #region Properties + + SerializationFormat Format { get; } + + #endregion Properties + #region Methods - T Deserialize(Stream stream); + object Deserialize(Stream stream, Type type); void Serialize(Stream stream, object obj); diff --git a/Src/DDD.Core.Abstractions/Serialization/ISerializerExtensions.cs b/Src/DDD.Core.Abstractions/Serialization/ISerializerExtensions.cs index 0d22c45..fc38ba7 100644 --- a/Src/DDD.Core.Abstractions/Serialization/ISerializerExtensions.cs +++ b/Src/DDD.Core.Abstractions/Serialization/ISerializerExtensions.cs @@ -1,4 +1,5 @@ -using Conditions; +using EnsureThat; +using System; using System.IO; namespace DDD.Serialization @@ -8,20 +9,35 @@ public static class ISerializerExtensions #region Methods - public static T DeserializeFromFile(this ISerializer serializer, string filePath) + public static T Deserialize(this ISerializer serializer, Stream stream) + { + Ensure.That(serializer, nameof(serializer)).IsNotNull(); + Ensure.That(stream, nameof(stream)).IsNotNull(); + return (T)serializer.Deserialize(stream, typeof(T)); + } + + public static object DeserializeFromFile(this ISerializer serializer, string filePath, Type type) { - Condition.Requires(serializer, nameof(serializer)).IsNotNull(); - Condition.Requires(filePath, nameof(filePath)).IsNotNullOrWhiteSpace(); + Ensure.That(serializer, nameof(serializer)).IsNotNull(); + Ensure.That(filePath, nameof(filePath)).IsNotNullOrWhiteSpace(); + Ensure.That(type, nameof(type)).IsNotNull(); using (var stream = File.OpenRead(filePath)) { - return serializer.Deserialize(stream); + return serializer.Deserialize(stream, type); } } + public static T DeserializeFromFile(this ISerializer serializer, string filePath) + { + Ensure.That(serializer, nameof(serializer)).IsNotNull(); + Ensure.That(filePath, nameof(filePath)).IsNotNullOrWhiteSpace(); + return (T)serializer.DeserializeFromFile(filePath, typeof(T)); + } + public static void SerializeToFile(this ISerializer serializer, string filePath, object obj) { - Condition.Requires(serializer, nameof(serializer)).IsNotNull(); - Condition.Requires(filePath, nameof(filePath)).IsNotNullOrWhiteSpace(); + Ensure.That(serializer, nameof(serializer)).IsNotNull(); + Ensure.That(filePath, nameof(filePath)).IsNotNullOrWhiteSpace(); using (var stream = File.Create(filePath)) { serializer.Serialize(stream, obj); diff --git a/Src/DDD.Core.Abstractions/Serialization/ITextSerializerExtensions.cs b/Src/DDD.Core.Abstractions/Serialization/ITextSerializerExtensions.cs index c845dd5..f87b985 100644 --- a/Src/DDD.Core.Abstractions/Serialization/ITextSerializerExtensions.cs +++ b/Src/DDD.Core.Abstractions/Serialization/ITextSerializerExtensions.cs @@ -1,4 +1,5 @@ -using Conditions; +using EnsureThat; +using System; using System.IO; namespace DDD.Serialization @@ -10,17 +11,24 @@ public static class ITextSerializerExtensions public static T DeserializeFromString(this ITextSerializer serializer, string input) { - Condition.Requires(serializer, nameof(serializer)).IsNotNull(); + Ensure.That(serializer, nameof(serializer)).IsNotNull(); + return (T)serializer.DeserializeFromString(input, typeof(T)); + } + + public static object DeserializeFromString(this ITextSerializer serializer, string input, Type type) + { + Ensure.That(serializer, nameof(serializer)).IsNotNull(); + Ensure.That(type, nameof(type)).IsNotNull(); var bytes = serializer.Encoding.GetBytes(input); using (var stream = new MemoryStream(bytes)) { - return serializer.Deserialize(stream); + return serializer.Deserialize(stream, type); } } public static string SerializeToString(this ITextSerializer serializer, object obj) { - Condition.Requires(serializer, nameof(serializer)).IsNotNull(); + Ensure.That(serializer, nameof(serializer)).IsNotNull(); using (var stream = new MemoryStream()) { serializer.Serialize(stream, obj); diff --git a/Src/DDD.Core.Abstractions/Serialization/JsonSerializationOptions.cs b/Src/DDD.Core.Abstractions/Serialization/JsonSerializationOptions.cs index 5b5e359..0f41a64 100644 --- a/Src/DDD.Core.Abstractions/Serialization/JsonSerializationOptions.cs +++ b/Src/DDD.Core.Abstractions/Serialization/JsonSerializationOptions.cs @@ -7,10 +7,11 @@ public static class JsonSerializationOptions #region Properties - public static Encoding Encoding { get; set; } = Encoding.UTF8; + public static Encoding Encoding { get; set; } = new UTF8Encoding(false); - public static bool Indent { get; set; } = true; + public static bool Indent { get; set; } = false; #endregion Properties + } -} +} \ No newline at end of file diff --git a/Src/DDD.Core.Abstractions/Serialization/SerializationException.cs b/Src/DDD.Core.Abstractions/Serialization/SerializationException.cs new file mode 100644 index 0000000..46ced8d --- /dev/null +++ b/Src/DDD.Core.Abstractions/Serialization/SerializationException.cs @@ -0,0 +1,57 @@ +using System; + +namespace DDD.Serialization +{ + /// + /// Exception thrown when an error occurs during serialization or deserialization. + /// + public class SerializationException : Exception + { + + #region Constructors + + public SerializationException(Type objectType = null, Exception innerException = null) + : base(DefaultMessage(objectType), innerException) + { + this.ObjectType = objectType; + } + + public SerializationException(string message, Type objectType = null, Exception innerException = null) + : base(message, innerException) + { + this.ObjectType = objectType; + } + + #endregion Constructors + + #region Properties + + public Type ObjectType { get; } + + #endregion Properties + + #region Methods + + public static string DefaultMessage(Type objectType = null) + { + if (objectType == null) + return "An error occurred while serializing or deserializing an object."; + return $"An error occurred while serializing or deserializing an object of the type '{objectType.Name}'."; + } + + public override string ToString() + { + var s = $"{this.GetType()}: {this.Message} "; + if (this.ObjectType != null) + s += $"{Environment.NewLine}{nameof(ObjectType)}: {this.ObjectType}"; + if (this.InnerException != null) + s += $" ---> {this.InnerException}"; + if (this.StackTrace != null) + s += $"{Environment.NewLine}{this.StackTrace}"; + return s; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Abstractions/Serialization/SerializationFormat.cs b/Src/DDD.Core.Abstractions/Serialization/SerializationFormat.cs new file mode 100644 index 0000000..f80e02f --- /dev/null +++ b/Src/DDD.Core.Abstractions/Serialization/SerializationFormat.cs @@ -0,0 +1,8 @@ +namespace DDD.Serialization +{ + public enum SerializationFormat + { + Json, + Xml + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Abstractions/Serialization/XmlSerializationOptions.cs b/Src/DDD.Core.Abstractions/Serialization/XmlSerializationOptions.cs index f04f937..a768d36 100644 --- a/Src/DDD.Core.Abstractions/Serialization/XmlSerializationOptions.cs +++ b/Src/DDD.Core.Abstractions/Serialization/XmlSerializationOptions.cs @@ -9,7 +9,7 @@ public static class XmlSerializationOptions public static Encoding Encoding { get; set; } = Encoding.UTF8; - public static bool Indent { get; set; } = true; + public static bool Indent { get; set; } = false; #endregion Properties } diff --git a/Src/DDD.Core.Abstractions/StringArgExtensions.cs b/Src/DDD.Core.Abstractions/StringArgExtensions.cs new file mode 100644 index 0000000..ce48425 --- /dev/null +++ b/Src/DDD.Core.Abstractions/StringArgExtensions.cs @@ -0,0 +1,74 @@ +using EnsureThat; +using EnsureThat.Enforcers; +using System; + +namespace DDD +{ + public static class StringArgExtensions + { + + #region Methods + + /// + /// Ensures that the string has a minimum length. + /// + public static string HasMinLength(this StringArg _, string value, int minLength, string paramName = null, OptsFn optsFn = null) + { + Ensure.Any.IsNotNull(value, paramName, optsFn); + + if (value.Length < minLength) + throw Ensure.ExceptionFactory.ArgumentException( + string.Format("The string is not long enough. Must be longer than '{0}' but was '{1}' characters long.", minLength, value.Length), + paramName, + optsFn); + + return value; + } + + /// + /// Ensures that the string contains only digits. + /// + public static string IsAllDigits(this StringArg _, string value, string paramName = null, OptsFn optsFn = null) + { + Ensure.Any.IsNotNull(value, paramName, optsFn); + + for (var i = 0; i < value.Length; i++) + if (!char.IsDigit(value[i])) + throw Ensure.ExceptionFactory.ArgumentException( + string.Format("Expected '{0} to contain only digits but does not.", value), + paramName, + optsFn); + + return value; + } + + /// + /// Ensures that the string contains only letters. + /// + public static string IsAllLetters(this StringArg _, string value, string paramName = null, OptsFn optsFn = null) + { + Ensure.Any.IsNotNull(value, paramName, optsFn); + + for (var i = 0; i < value.Length; i++) + if (!char.IsLetter(value[i])) + throw Ensure.ExceptionFactory.ArgumentException( + string.Format("Expected '{0} to contain only letters but does not.", value), + paramName, + optsFn); + + return value; + } + + /// + /// Ensures that the string satisfies the specified condition. + /// + public static string Satisfy(this StringArg _, string value, Func predicate, string paramName = null, OptsFn optsFn = null) + { + Ensure.Any.Satisfy(value, predicate, paramName, optsFn); + return value; + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Abstractions/StringExtensions.cs b/Src/DDD.Core.Abstractions/StringExtensions.cs index 359f83f..1bef8d9 100644 --- a/Src/DDD.Core.Abstractions/StringExtensions.cs +++ b/Src/DDD.Core.Abstractions/StringExtensions.cs @@ -1,7 +1,7 @@ using System; using System.Globalization; using System.Text.RegularExpressions; -using Conditions; +using EnsureThat; using System.Linq; using System.Text; @@ -20,7 +20,7 @@ public static class StringExtensions /// public static bool IsAlphabetic(this string instance) { - Condition.Requires(instance, nameof(instance)).IsNotNull(); + Ensure.That(instance, nameof(instance)).IsNotNull(); return instance.ToCharArray().All(char.IsLetter); } @@ -29,7 +29,7 @@ public static bool IsAlphabetic(this string instance) /// public static bool IsAlphanumeric(this string instance) { - Condition.Requires(instance, nameof(instance)).IsNotNull(); + Ensure.That(instance, nameof(instance)).IsNotNull(); return instance.ToCharArray().All(char.IsLetterOrDigit); } @@ -39,7 +39,7 @@ public static bool IsAlphanumeric(this string instance) /// The current instance. public static bool IsFrenchShortDateString(this string instance) { - Condition.Requires(instance, nameof(instance)).IsNotNull(); + Ensure.That(instance, nameof(instance)).IsNotNull(); return IsShortDateString(instance, CultureInfo.CreateSpecificCulture("fr")); } @@ -48,7 +48,7 @@ public static bool IsFrenchShortDateString(this string instance) /// public static bool IsNumeric(this string instance) { - Condition.Requires(instance, nameof(instance)).IsNotNull(); + Ensure.That(instance, nameof(instance)).IsNotNull(); return instance.ToCharArray().All(char.IsDigit); } @@ -59,10 +59,10 @@ public static bool IsNumeric(this string instance) /// An object that supplies culture-specific formatting information. public static bool IsShortDateString(this string instance, IFormatProvider provider) { - Condition.Requires(instance, nameof(instance)).IsNotNull(); - DateTime result; - return DateTime.TryParseExact(instance, "d", provider, DateTimeStyles.None, out result); + Ensure.That(instance, nameof(instance)).IsNotNull(); + return DateTime.TryParseExact(instance, "d", provider, DateTimeStyles.None, out _); } + /// /// Returns a string containing a specified number of characters from the left side of a string. /// @@ -75,13 +75,31 @@ public static bool IsShortDateString(this string instance, IFormatProvider provi /// Throws an exception when the specified length is less than zero. public static string Left(this string instance, int length) { - Condition.Requires(instance, nameof(instance)).IsNotNull(); - Condition.Requires(length, nameof(length)).IsGreaterOrEqual(0); + Ensure.That(instance, nameof(instance)).IsNotNull(); + Ensure.That(length, nameof(length)).IsGte(0); if (length == 0) return string.Empty; if (length >= instance.Length) return instance; return instance.Substring(0, length); } + /// + /// Returns a new string in which all occurrences of specified characters in this instance are removed. + /// + /// The current instance. + /// The characters to be removed. + public static string Remove(this string instance, params char[] oldChars) + { + Ensure.That(instance, nameof(instance)).IsNotNull(); + Ensure.That(oldChars, nameof(oldChars)).IsNotNull(); + var index = instance.IndexOfAny(oldChars); + while (index >= 0) + { + instance = instance.Remove(index, 1); + index = instance.IndexOfAny(oldChars); + } + return instance; + } + /// /// Returns a new string in which all occurrences of a specified string in the current instance are replaced with another specified string, ignoring or honoring their case. /// @@ -92,7 +110,7 @@ public static string Left(this string instance, int length) /// A string that is equivalent to the current string except that all instances of oldValue are replaced with newValue. public static string Replace(this string instance, string oldValue, string newValue, bool ignoreCase) { - Condition.Requires(instance, nameof(instance)).IsNotNull(); + Ensure.That(instance, nameof(instance)).IsNotNull(); if (ignoreCase) return Regex.Replace(instance, Regex.Escape(oldValue), Regex.Escape(newValue), RegexOptions.IgnoreCase); return instance.Replace(oldValue, newValue); } @@ -102,7 +120,7 @@ public static string Replace(this string instance, string oldValue, string newVa /// public static string Reverse(this string instance) { - Condition.Requires(instance, nameof(instance)).IsNotNull(); + Ensure.That(instance, nameof(instance)).IsNotNull(); if (instance.Length <= 1) return instance; var sb = new StringBuilder(instance.Length); for (var i = instance.Length - 1; i >= 0; i--) @@ -122,8 +140,8 @@ public static string Reverse(this string instance) /// Throws an exception when the specified length is less than zero. public static string Right(this string instance, int length) { - Condition.Requires(instance, nameof(instance)).IsNotNull(); - Condition.Requires(length, nameof(length)).IsGreaterOrEqual(0); + Ensure.That(instance, nameof(instance)).IsNotNull(); + Ensure.That(length, nameof(length)).IsGte(0); if (length == 0) return string.Empty; if (length >= instance.Length) return instance; return instance.Substring(instance.Length - length); @@ -139,7 +157,7 @@ public static string Right(this string instance, int length) /// An object of type TEnum whose value is represented by value. public static TEnum ToEnum(this string instance, bool ignoreCase = true) where TEnum : struct { - Condition.Requires(instance, nameof(instance)).IsNotNull(); + Ensure.That(instance, nameof(instance)).IsNotNull(); return (TEnum)Enum.Parse(typeof(TEnum), instance, ignoreCase); } @@ -150,7 +168,7 @@ public static TEnum ToEnum(this string instance, bool ignoreCase = true) /// The specified string converted to snake case. public static string ToSnakeCase(this string instance) { - Condition.Requires(instance, nameof(instance)).IsNotNull(); + Ensure.That(instance, nameof(instance)).IsNotNull(); return Regex.Replace(instance, "((?<=[a-z])(?=[A-Z]))|((?<=[A-Z])(?=[A-Z][a-z]))", "_"); } @@ -162,11 +180,10 @@ public static string ToSnakeCase(this string instance) /// The current implementation of the ToTitleCase method provides an arbitrary casing behavior which is not necessarily linguistically correct. public static string ToTitleCase(this string instance) { - Condition.Requires(instance, nameof(instance)).IsNotNull(); + Ensure.That(instance, nameof(instance)).IsNotNull(); return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(instance.ToLower()); } #endregion Methods - } } \ No newline at end of file diff --git a/Src/DDD.Core.Abstractions/StringParamExtensions.cs b/Src/DDD.Core.Abstractions/StringParamExtensions.cs new file mode 100644 index 0000000..4b86b69 --- /dev/null +++ b/Src/DDD.Core.Abstractions/StringParamExtensions.cs @@ -0,0 +1,38 @@ +using System; +using EnsureThat; + +namespace DDD +{ + public static class StringParamExtensions + { + + #region Methods + + /// + /// Ensures that the string has a minimum length. + /// + public static void HasMinLength(this in StringParam param, int minLength) + => Ensure.String.HasMinLength(param.Value, minLength, param.Name, param.OptsFn); + + /// + /// Ensures that the string contains only digits. + /// + public static void IsAllDigits(this in StringParam param) + => Ensure.String.IsAllDigits(param.Value, param.Name, param.OptsFn); + + /// + /// Ensures that the string contains only letters. + /// + public static void IsAllLetters(this in StringParam param) + => Ensure.String.IsAllLetters(param.Value, param.Name, param.OptsFn); + + /// + /// Ensures that the string satisfies the specified condition. + /// + public static void Satisfy(this in StringParam param, Func predicate) + => Ensure.String.Satisfy(param.Value, predicate, param.Name, param.OptsFn); + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Abstractions/SystemTime.cs b/Src/DDD.Core.Abstractions/SystemTime.cs new file mode 100644 index 0000000..b977ca6 --- /dev/null +++ b/Src/DDD.Core.Abstractions/SystemTime.cs @@ -0,0 +1,84 @@ +using System; + +namespace DDD +{ + /// + /// This class must be used in the production code instead of the standard built-in DateTime to provide local and universal system times. + /// It allows changing and resetting the current time in tests. + /// + public static class SystemTime + { + + #region Fields + + private static ITimestampProvider localProvider; + private static ITimestampProvider universalProvider; + + #endregion Fields + + #region Constructors + + static SystemTime() + { + Reset(); + } + + #endregion Constructors + + #region Methods + + /// + /// Gets the current date and time on this computer, expressed as the local time. + /// + public static DateTime Local() => localProvider.GetTimestamp(); + + /// + /// Resets the default timestamp providers based on the current date and time on this computer (for testing). + /// + public static void Reset() + { + localProvider = new LocalTimestampProvider(); + universalProvider = new UniversalTimestampProvider(); + } + + /// + /// Replaces the default local timestamp provider (for testing). + /// + public static void SetLocalProvider(ITimestampProvider provider) + { + localProvider = provider; + } + + /// + /// Replaces the default local timestamp provider (for testing). + /// + public static void SetLocalProvider(Func timestamp) + { + localProvider = new DelegatingTimestampProvider(timestamp); + } + + /// + /// Replaces the default universal timestamp provider (for testing). + /// + public static void SetUniversalProvider(ITimestampProvider provider) + { + universalProvider = provider; + } + + /// + /// Replaces the default universal timestamp provider (for testing). + /// + public static void SetUniversalProvider(Func timestamp) + { + universalProvider = new DelegatingTimestampProvider(timestamp); + } + + /// + /// Gets the current date and time on this computer, expressed as the Coordinated Universal Time (UTC). + /// + public static DateTime Universal() => universalProvider.GetTimestamp(); + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Abstractions/TimestampedException.cs b/Src/DDD.Core.Abstractions/TimestampedException.cs new file mode 100644 index 0000000..752cd05 --- /dev/null +++ b/Src/DDD.Core.Abstractions/TimestampedException.cs @@ -0,0 +1,61 @@ +using System; +using System.Runtime.Serialization; + +namespace DDD +{ + /// + /// An exception that records the timestamp of when it was thrown and information about its recoverability. + /// + public abstract class TimestampedException : Exception + { + + #region Constructors + + protected TimestampedException(bool isTransient, string message, Exception innerException = null) + : base(message, innerException) + { + this.Timestamp = SystemTime.Local(); + this.IsTransient = isTransient; + } + + #endregion Constructors + + #region Properties + + /// + /// Gets a value indicating whether the exception is transient. + /// + public bool IsTransient { get; } + + /// + /// Gets the time at which the exception occurred. + /// + public DateTime Timestamp { get; } + + #endregion Properties + + #region Methods + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + info.AddValue("Timestamp", this.Timestamp.ToString()); + info.AddValue("IsTransient", this.IsTransient); + } + + public override string ToString() + { + var s = $"{this.GetType()}: {this.Message} "; + s += $"{Environment.NewLine}{nameof(Timestamp)}: {this.Timestamp}"; + s += $"{Environment.NewLine}{nameof(IsTransient)}: {this.IsTransient}"; + if (this.InnerException != null) + s += $" ---> {this.InnerException}"; + if (this.StackTrace != null) + s += $"{Environment.NewLine}{this.StackTrace}"; + return s; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Abstractions/TypeExtensions.cs b/Src/DDD.Core.Abstractions/TypeExtensions.cs new file mode 100644 index 0000000..2f260db --- /dev/null +++ b/Src/DDD.Core.Abstractions/TypeExtensions.cs @@ -0,0 +1,18 @@ +using System; +using EnsureThat; + +namespace DDD +{ + public static class TypeExtensions + { + #region Methods + + public static string ShortAssemblyQualifiedName(this Type type) + { + Ensure.That(type, nameof(type)).IsNotNull(); + return $"{type.FullName}, {type.Assembly.GetName().Name}"; + } + + #endregion Methods + } +} diff --git a/Src/DDD.Core.Abstractions/UniqueTimestampProvider.cs b/Src/DDD.Core.Abstractions/UniqueTimestampProvider.cs new file mode 100644 index 0000000..1c03bac --- /dev/null +++ b/Src/DDD.Core.Abstractions/UniqueTimestampProvider.cs @@ -0,0 +1,60 @@ +using System; +using EnsureThat; + +namespace DDD +{ + /// + /// Provides unique timestamps with a specified resolution. + /// + public class UniqueTimestampProvider : ITimestampProvider + { + + #region Fields + + private readonly object locker = new object(); + private readonly ITimestampProvider provider; + private DateTime lastTimestamp = DateTime.MinValue; + + #endregion Fields + + #region Constructors + + public UniqueTimestampProvider(ITimestampProvider provider, TimeSpan resolution) + { + Ensure.That(provider, nameof(provider)).IsNotNull(); + this.provider = provider; + this.Resolution = resolution; + } + + #endregion Constructors + + #region Properties + + /// + /// Indicates the resolution of the provided timestamps. + /// + public TimeSpan Resolution { get; } + + #endregion Properties + + #region Methods + + /// + /// Provides a unique timestamp. + /// + public DateTime GetTimestamp() + { + var timestamp = this.provider.GetTimestamp(); + lock (locker) + { + if ((timestamp - lastTimestamp) < Resolution) + timestamp = lastTimestamp.Add(Resolution); + lastTimestamp = timestamp; + } + return timestamp; + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Abstractions/UniversalTimestampProvider.cs b/Src/DDD.Core.Abstractions/UniversalTimestampProvider.cs new file mode 100644 index 0000000..a18fcce --- /dev/null +++ b/Src/DDD.Core.Abstractions/UniversalTimestampProvider.cs @@ -0,0 +1,18 @@ +using System; + +namespace DDD +{ + /// + /// Provides timestamps based on the current date and time on this computer, expressed as the Coordinated Universal Time (UTC). + /// + public class UniversalTimestampProvider : ITimestampProvider + { + + #region Methods + + public DateTime GetTimestamp() => DateTime.UtcNow; + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Abstractions/Validation/DelegatingValidator.cs b/Src/DDD.Core.Abstractions/Validation/DelegatingValidator.cs new file mode 100644 index 0000000..1d3be09 --- /dev/null +++ b/Src/DDD.Core.Abstractions/Validation/DelegatingValidator.cs @@ -0,0 +1,59 @@ +using System; +using System.Threading.Tasks; +using EnsureThat; + +namespace DDD.Validation +{ + /// + /// Adapter that converts a delegate into an object that implements the interface IObjectValidator. + /// + public class DelegatingValidator : IObjectValidator + where T : class + { + + #region Fields + + private readonly Func syncValidator; + private readonly Func> asyncValidator; + + #endregion Fields + + #region Constructors + + public DelegatingValidator(Func syncValidator, + Func> asyncValidator) + { + Ensure.That(syncValidator, nameof(syncValidator)).IsNotNull(); + Ensure.That(asyncValidator, nameof(asyncValidator)).IsNotNull(); + this.syncValidator = syncValidator; + this.asyncValidator = asyncValidator; + } + + #endregion Constructors + + #region Methods + + public static IObjectValidator Create(Func syncValidator) + { + Func> asyncValidator = (obj, context) => Task.FromResult(syncValidator(obj, context)); + return new DelegatingValidator(syncValidator, asyncValidator); + } + + public ValidationResult Validate(T obj, IValidationContext context) + { + Ensure.That(obj, nameof(obj)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + return this.syncValidator(obj, context); + } + + public Task ValidateAsync(T obj, IValidationContext context) + { + Ensure.That(obj, nameof(obj)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + return this.asyncValidator(obj, context); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Abstractions/Validation/IAsyncObjectValidator.cs b/Src/DDD.Core.Abstractions/Validation/IAsyncObjectValidator.cs index a7a230a..1d3525e 100644 --- a/Src/DDD.Core.Abstractions/Validation/IAsyncObjectValidator.cs +++ b/Src/DDD.Core.Abstractions/Validation/IAsyncObjectValidator.cs @@ -10,7 +10,10 @@ public interface IAsyncObjectValidator where T :class #region Methods - Task ValidateAsync(T obj, string ruleSet = null); + /// + /// Validates asynchronously an object of a specified type. + /// + Task ValidateAsync(T obj, IValidationContext context); #endregion Methods diff --git a/Src/DDD.Core.Abstractions/Validation/IAsyncObjectValidatorExtensions.cs b/Src/DDD.Core.Abstractions/Validation/IAsyncObjectValidatorExtensions.cs new file mode 100644 index 0000000..65634a5 --- /dev/null +++ b/Src/DDD.Core.Abstractions/Validation/IAsyncObjectValidatorExtensions.cs @@ -0,0 +1,31 @@ +using EnsureThat; +using System.Threading.Tasks; + +namespace DDD.Validation +{ + public static class IAsyncObjectValidatorExtensions + { + + #region Methods + + public static Task ValidateAsync(this IAsyncObjectValidator validator, + T obj) + where T : class + { + Ensure.That(validator, nameof(validator)).IsNotNull(); + return validator.ValidateAsync(obj, new ValidationContext()); + } + + public static Task ValidateAsync(this IAsyncObjectValidator validator, + T obj, + object context) + where T : class + { + Ensure.That(validator, nameof(validator)).IsNotNull(); + return validator.ValidateAsync(obj, ValidationContext.FromObject(context)); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Abstractions/Validation/IObjectValidator.cs b/Src/DDD.Core.Abstractions/Validation/IObjectValidator.cs index ae2ccba..ec0f896 100644 --- a/Src/DDD.Core.Abstractions/Validation/IObjectValidator.cs +++ b/Src/DDD.Core.Abstractions/Validation/IObjectValidator.cs @@ -1,16 +1,10 @@ namespace DDD.Validation { /// - /// Defines a method that validates an object of a specified type. + /// Defines methods that validate synchronously and asynchronously an object of a specified type. /// - public interface IObjectValidator where T :class + public interface IObjectValidator : ISyncObjectValidator, IAsyncObjectValidator + where T : class { - - #region Methods - - ValidationResult Validate(T obj, string ruleSet = null); - - #endregion Methods - } -} +} \ No newline at end of file diff --git a/Src/DDD.Core.Abstractions/Validation/ISyncObjectValidator.cs b/Src/DDD.Core.Abstractions/Validation/ISyncObjectValidator.cs new file mode 100644 index 0000000..b559dc3 --- /dev/null +++ b/Src/DDD.Core.Abstractions/Validation/ISyncObjectValidator.cs @@ -0,0 +1,19 @@ +namespace DDD.Validation +{ + /// + /// Defines a method that validates synchronously an object of a specified type. + /// + public interface ISyncObjectValidator where T :class + { + + #region Methods + + /// + /// Validates synchronously an object of a specified type. + /// + ValidationResult Validate(T obj, IValidationContext context); + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Abstractions/Validation/ISyncObjectValidatorExtensions.cs b/Src/DDD.Core.Abstractions/Validation/ISyncObjectValidatorExtensions.cs new file mode 100644 index 0000000..90d924a --- /dev/null +++ b/Src/DDD.Core.Abstractions/Validation/ISyncObjectValidatorExtensions.cs @@ -0,0 +1,30 @@ +using EnsureThat; + +namespace DDD.Validation +{ + public static class ISyncObjectValidatorExtensions + { + + #region Methods + + public static ValidationResult Validate(this ISyncObjectValidator validator, + T obj) + where T : class + { + Ensure.That(validator, nameof(validator)).IsNotNull(); + return validator.Validate(obj, new ValidationContext()); + } + + public static ValidationResult Validate(this ISyncObjectValidator validator, + T obj, + object context) + where T : class + { + Ensure.That(validator, nameof(validator)).IsNotNull(); + return validator.Validate(obj, ValidationContext.FromObject(context)); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Abstractions/Validation/IValidationContext.cs b/Src/DDD.Core.Abstractions/Validation/IValidationContext.cs new file mode 100644 index 0000000..a1ace3a --- /dev/null +++ b/Src/DDD.Core.Abstractions/Validation/IValidationContext.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace DDD.Validation +{ + /// + /// Provides access to the validation context. + /// + public interface IValidationContext : IDictionary + { + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Abstractions/Validation/IValidationContextExtensions.cs b/Src/DDD.Core.Abstractions/Validation/IValidationContextExtensions.cs new file mode 100644 index 0000000..ab31759 --- /dev/null +++ b/Src/DDD.Core.Abstractions/Validation/IValidationContextExtensions.cs @@ -0,0 +1,44 @@ +using EnsureThat; +using System.Threading; + +namespace DDD.Validation +{ + using Collections; + + public static class IValidationContextExtensions + { + + #region Methods + + public static void AddRuleSets(this IValidationContext context, params string[] ruleSets) + { + Ensure.That(context, nameof(context)).IsNotNull(); + Ensure.That(ruleSets, nameof(ruleSets)).IsNotNull(); + context.Add(ValidationContextInfo.RuleSets, ruleSets); + } + + public static void AddCancellationToken(this IValidationContext context, CancellationToken cancellationToken) + { + Ensure.That(context, nameof(context)).IsNotNull(); + context.Add(ValidationContextInfo.CancellationToken, cancellationToken); + } + + public static CancellationToken CancellationToken(this IValidationContext context) + { + Ensure.That(context, nameof(context)).IsNotNull(); + context.TryGetValue(ValidationContextInfo.CancellationToken, out CancellationToken cancellationToken); + return cancellationToken; + } + + public static string[] RuleSets(this IValidationContext context) + { + Ensure.That(context, nameof(context)).IsNotNull(); + context.TryGetValue(ValidationContextInfo.RuleSets, out string[] ruleSets); + return ruleSets; + } + + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Abstractions/Validation/ValidationContext.cs b/Src/DDD.Core.Abstractions/Validation/ValidationContext.cs new file mode 100644 index 0000000..0506e75 --- /dev/null +++ b/Src/DDD.Core.Abstractions/Validation/ValidationContext.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; + +namespace DDD.Validation +{ + using Collections; + using System.Threading; + + public class ValidationContext : Dictionary, IValidationContext + { + + #region Constructors + + public ValidationContext() + { + } + + public ValidationContext(IDictionary dictionary) : base(dictionary) + { + } + + #endregion Constructors + + #region Methods + + public static IValidationContext CancellableContext(CancellationToken cancellationToken) + { + var context = new ValidationContext(); + context.AddCancellationToken(cancellationToken); + return context; + } + + public static IValidationContext WithRuleSets(params string[] ruleSets) + { + var context = new ValidationContext(); + context.AddRuleSets(ruleSets); + return context; + } + + public static IValidationContext FromObject(object context) + { + var validationContext = new ValidationContext(); + if (context != null) + validationContext.AddObject(context); + return validationContext; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Abstractions/Validation/ValidationContextInfo.cs b/Src/DDD.Core.Abstractions/Validation/ValidationContextInfo.cs new file mode 100644 index 0000000..81e3ecf --- /dev/null +++ b/Src/DDD.Core.Abstractions/Validation/ValidationContextInfo.cs @@ -0,0 +1,15 @@ +namespace DDD.Validation +{ + /// + /// Standard contextual information about the validation. + /// + public class ValidationContextInfo + { + #region Fields + + public const string RuleSets = nameof(RuleSets), + CancellationToken = nameof(CancellationToken); + + #endregion Fields + } +} diff --git a/Src/DDD.Core.Abstractions/Validation/ValidationFailure.cs b/Src/DDD.Core.Abstractions/Validation/ValidationFailure.cs index 8a6207d..5995014 100644 --- a/Src/DDD.Core.Abstractions/Validation/ValidationFailure.cs +++ b/Src/DDD.Core.Abstractions/Validation/ValidationFailure.cs @@ -1,10 +1,14 @@ -using Conditions; +using EnsureThat; +using System.Runtime.Serialization; +using System.Xml.Serialization; namespace DDD.Validation { /// /// Represents a validation failure. /// + [DataContract()] + [XmlType()] public class ValidationFailure { @@ -14,16 +18,19 @@ public ValidationFailure(string message, string code, FailureLevel level = FailureLevel.Warning, string propertyName = null, - object propertyValue = null) + object propertyValue = null, + string category = null) { - Condition.Requires(message, nameof(message)).IsNotNullOrWhiteSpace(); - Condition.Requires(code, nameof(code)).IsNotNullOrWhiteSpace(); + Ensure.That(message, nameof(message)).IsNotNullOrWhiteSpace(); + Ensure.That(code, nameof(code)).IsNotNullOrWhiteSpace(); this.Message = message; this.Code = code; this.Level = level; if (!string.IsNullOrWhiteSpace(propertyName)) this.PropertyName = propertyName; this.PropertyValue = propertyValue; + if (!string.IsNullOrWhiteSpace(category)) + this.Category = category; } /// For serialization @@ -35,24 +42,54 @@ private ValidationFailure() #region Properties + /// + /// The category of the failure. + /// + [DataMember(Name = "category")] + [XmlElement("category")] + public string Category { get; private set; } + + /// + /// A code associated with the failure. + /// + [DataMember(Name = "code")] + [XmlElement("code")] public string Code { get; private set; } + /// + /// The level of the failure. + /// + [DataMember(Name = "level")] + [XmlElement("level")] public FailureLevel Level { get; private set; } + /// + /// The failure message. + /// + [DataMember(Name = "message")] + [XmlElement("message")] public string Message { get; private set; } + /// + /// The name of the property associated with the failure. + /// + [DataMember(Name = "propertyName")] + [XmlElement("propertyName")] public string PropertyName { get; private set; } + /// + /// The property value that caused the failure. + /// + [DataMember(Name = "propertyValue")] + [XmlElement("propertyValue")] public object PropertyValue { get; private set; } #endregion Properties #region Methods - public override string ToString() - { - return $"{this.GetType().Name} [message={this.Message}, code={this.Code}, level={this.Level}, propertyName={this.PropertyName}, propertyValue={this.PropertyValue}]"; - } + public override string ToString() + => $"{GetType().Name} [{nameof(Message)}={Message}, {nameof(Code)}={Code}, {nameof(Level)}={Level}, {nameof(PropertyName)}={PropertyName}, {nameof(PropertyValue)}={PropertyValue}, {nameof(Category)}={Category}]"; #endregion Methods diff --git a/Src/DDD.Core.Abstractions/Validation/ValidationResult.cs b/Src/DDD.Core.Abstractions/Validation/ValidationResult.cs index 05367cd..626169b 100644 --- a/Src/DDD.Core.Abstractions/Validation/ValidationResult.cs +++ b/Src/DDD.Core.Abstractions/Validation/ValidationResult.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using Conditions; +using EnsureThat; +using System.Linq; namespace DDD.Validation { @@ -11,10 +11,12 @@ public class ValidationResult #region Constructors - public ValidationResult(bool isSuccessful, ValidationFailure[] failures) + public ValidationResult(bool isSuccessful, string objectName, ValidationFailure[] failures) { - Condition.Requires(failures, nameof(failures)).IsNotNull(); + Ensure.That(objectName, nameof(objectName)).IsNotNullOrWhiteSpace(); + Ensure.That(failures, nameof(failures)).IsNotNull(); this.IsSuccessful = isSuccessful; + this.ObjectName = objectName; this.Failures = failures; } @@ -31,7 +33,15 @@ private ValidationResult() public bool IsSuccessful { get; private set; } + public string ObjectName { get; private set; } + #endregion Properties + #region Methods + + public bool HasFailures() => this.Failures.Any(); + + #endregion Methods + } } diff --git a/Src/DDD.Core.Abstractions/packages.config b/Src/DDD.Core.Abstractions/packages.config deleted file mode 100644 index ce9864b..0000000 --- a/Src/DDD.Core.Abstractions/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/Src/DDD.Core.Cronos/CronosSchedule.cs b/Src/DDD.Core.Cronos/CronosSchedule.cs new file mode 100644 index 0000000..e6a3a73 --- /dev/null +++ b/Src/DDD.Core.Cronos/CronosSchedule.cs @@ -0,0 +1,46 @@ +using EnsureThat; +using System; +using Cronos; + +namespace DDD.Core.Infrastructure +{ + using Application; + + public class CronosSchedule : IRecurringSchedule + { + + #region Fields + + CronExpression expression; + + #endregion Fields + + #region Constructors + + public CronosSchedule(CronExpression expression) + { + Ensure.That(expression, nameof(expression)).IsNotNull(); + this.expression = expression; + } + + #endregion Constructors + + #region Methods + + public DateTime? GetNextOccurrence(DateTime startTime) + { + switch(startTime.Kind) + { + case DateTimeKind.Utc: + return this.expression.GetNextOccurrence(startTime); + case DateTimeKind.Local: + return this.expression.GetNextOccurrence(new DateTimeOffset(startTime), TimeZoneInfo.Local)?.DateTime; + default: + throw new ArgumentOutOfRangeException($"The kind '{startTime.Kind}' is not expected."); + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Cronos/CronosScheduleFactory.cs b/Src/DDD.Core.Cronos/CronosScheduleFactory.cs new file mode 100644 index 0000000..1c0edb2 --- /dev/null +++ b/Src/DDD.Core.Cronos/CronosScheduleFactory.cs @@ -0,0 +1,36 @@ +using Cronos; +using EnsureThat; +using System; + +namespace DDD.Core.Infrastructure +{ + using Application; + + public class CronosScheduleFactory : IRecurringScheduleFactory + { + #region Properties + + public RecurringExpressionFormat Format { get; } = RecurringExpressionFormat.Cron; + + #endregion Properties + + #region Methods + + public IRecurringSchedule Create(string recurringExpression) + { + Ensure.That(recurringExpression, nameof(recurringExpression)).IsNotNull(); + var parts = recurringExpression.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); + var format = CronFormat.Standard; + if (parts.Length == 6) + format |= CronFormat.IncludeSeconds; + else if (parts.Length != 5) + throw new CronFormatException( + $"Wrong number of parts in the `{recurringExpression}` recurring expression, you can only use 5 or 6 (with seconds) part-based expressions."); + var expression = CronExpression.Parse(recurringExpression, format); + return new CronosSchedule(expression); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Cronos/DDD.Core.Cronos.csproj b/Src/DDD.Core.Cronos/DDD.Core.Cronos.csproj new file mode 100644 index 0000000..e51e33c --- /dev/null +++ b/Src/DDD.Core.Cronos/DDD.Core.Cronos.csproj @@ -0,0 +1,17 @@ + + + net48;netstandard2.1 + Library + DDD.Core.Infrastructure + false + + + + + + + + + + + diff --git a/Src/DDD.Core.Cronos/Properties/AssemblyInfo.cs b/Src/DDD.Core.Cronos/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..2560a58 --- /dev/null +++ b/Src/DDD.Core.Cronos/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("DDD.Core.Cronos")] +[assembly: AssemblyDescription("Core components for scheduling recurring events from Cron expressions with Cronos.")] +[assembly: AssemblyProduct("DDD.Core.Cronos")] +[assembly: Guid("18581bb3-f808-4f54-b45b-fcaa76b060ef")] \ No newline at end of file diff --git a/Src/DDD.Core.Dapper.Oracle/DDD.Core.Dapper.Oracle.csproj b/Src/DDD.Core.Dapper.Oracle/DDD.Core.Dapper.Oracle.csproj new file mode 100644 index 0000000..ef592bc --- /dev/null +++ b/Src/DDD.Core.Dapper.Oracle/DDD.Core.Dapper.Oracle.csproj @@ -0,0 +1,15 @@ + + + net48;netstandard2.1 + Library + DDD.Core.Infrastructure.Data + false + + + + + + + + + diff --git a/Src/DDD.Core.Dapper.Oracle/OracleGuidTypeHandler.cs b/Src/DDD.Core.Dapper.Oracle/OracleGuidTypeHandler.cs new file mode 100644 index 0000000..ef9e879 --- /dev/null +++ b/Src/DDD.Core.Dapper.Oracle/OracleGuidTypeHandler.cs @@ -0,0 +1,32 @@ +using Dapper; +using Oracle.ManagedDataAccess.Client; +using System; +using System.Data; + +namespace DDD.Core.Infrastructure.Data +{ + /// + /// Conversion between and RAW(16) Oracle data type. + /// + public class OracleGuidTypeHandler : SqlMapper.TypeHandler + { + + #region Methods + + public override Guid Parse(object value) + { + return new Guid((byte[])value); + } + + public override void SetValue(IDbDataParameter parameter, Guid value) + { + OracleParameter oracleParameter = (OracleParameter)parameter; + oracleParameter.OracleDbType = OracleDbType.Raw; + oracleParameter.Size = 16; + parameter.Value = value.ToByteArray(); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Dapper.Oracle/Properties/AssemblyInfo.cs b/Src/DDD.Core.Dapper.Oracle/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..dfc4fd0 --- /dev/null +++ b/Src/DDD.Core.Dapper.Oracle/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("DDD.Core.Dapper.Oracle")] +[assembly: AssemblyDescription("Core components for database access with Dapper and Oracle.")] +[assembly: AssemblyProduct("DDD.Core.Dapper.Oracle")] +[assembly: Guid("4444e839-ca89-4cb7-87cc-e91f0a66bd59")] \ No newline at end of file diff --git a/Src/DDD.Core.Dapper/DDD.Core.Dapper.csproj b/Src/DDD.Core.Dapper/DDD.Core.Dapper.csproj index ed33ddc..9206e10 100644 --- a/Src/DDD.Core.Dapper/DDD.Core.Dapper.csproj +++ b/Src/DDD.Core.Dapper/DDD.Core.Dapper.csproj @@ -1,84 +1,55 @@ - - - + - Debug - AnyCPU - {701DA58B-AE36-429F-8621-64109B8D29D7} + net48;netstandard2.1 Library - Properties DDD.Core.Infrastructure.Data - DDD.Core.Dapper - v4.7.2 - 512 - + false - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 bin\Debug\DDD.Core.Dapper.xml 1591 - pdbonly - true - bin\Release\ - TRACE - prompt - 4 bin\Release\DDD.Core.Dapper.xml 1591 - - - L:\Packages\Conditions.2.1.0\lib\portable-net45+win+wpa81+wp80+MonoAndroid10+MonoTouch10\Conditions.dll - True - - - L:\Packages\Dapper.1.60.1\lib\net451\Dapper.dll - - - L:\Packages\Dapper.SqlBuilder.1.60.1\lib\net451\Dapper.SqlBuilder.dll - - - - - - - - - Properties\CommonAssemblyInfo.cs - - - - {596a8700-3d18-4a62-b200-1f78a9ea4617} - DDD.Core.Abstractions - - - {c6c3e419-b9aa-44ad-9dbf-789294687ae6} - DDD.Core - + + + + + + + + + + + + + + True + True + OracleScripts.resx + + + True + True + SqlScripts.resx + - + + ResXFileCodeGenerator + OracleScripts.Designer.cs + + + ResXFileCodeGenerator + SqlScripts.Designer.cs + - - \ No newline at end of file diff --git a/Src/DDD.Core.Dapper/EventStreamPositionUpdater.cs b/Src/DDD.Core.Dapper/EventStreamPositionUpdater.cs new file mode 100644 index 0000000..89ac13f --- /dev/null +++ b/Src/DDD.Core.Dapper/EventStreamPositionUpdater.cs @@ -0,0 +1,119 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using System.Transactions; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + using Mapping; + using Threading; + + public class EventStreamPositionUpdater : ICommandHandler + where TContext : BoundedContext + { + + #region Fields + + private readonly IDbConnectionProvider connectionProvider; + private readonly CompositeTranslator exceptionTranslator; + + #endregion Fields + + #region Constructors + + public EventStreamPositionUpdater(IDbConnectionProvider connectionProvider) + { + Ensure.That(connectionProvider, nameof(connectionProvider)).IsNotNull(); + this.connectionProvider = connectionProvider; + this.exceptionTranslator = new CompositeTranslator(); + this.exceptionTranslator.Register(new DbToCommandExceptionTranslator()); + this.exceptionTranslator.RegisterFallback(); + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.connectionProvider.Context; + + #endregion Properties + + #region Methods + + public void Handle(UpdateEventStreamPosition command, IMessageContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + try + { + using (var scope = new TransactionScope()) + { + var connection = this.connectionProvider.GetOpenConnection(); + var expressions = connection.Expressions(); + connection.Execute + ( + new CommandDefinition + ( + SqlScripts.UpdateEventStreamPosition.Replace("@", expressions.ParameterPrefix()), + ToParameters(command, connection) + ) + ); + scope.Complete(); + } + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Command = command }); + } + } + + public async Task HandleAsync(UpdateEventStreamPosition command, IMessageContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + var cancellationToken = context.CancellationToken(); + var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); + var expressions = connection.Expressions(); + await connection.ExecuteAsync + ( + new CommandDefinition + ( + SqlScripts.UpdateEventStreamPosition.Replace("@", expressions.ParameterPrefix()), + ToParameters(command, connection), + cancellationToken: cancellationToken + ) + ); + scope.Complete(); + } + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Command = command }); + } + } + + /// Workaround for https://github.com/DapperLib/Dapper/issues/303 + private object ToParameters(UpdateEventStreamPosition command, IDbConnection connectionProvider) + { + if (connectionProvider.HasOracleProvider()) + return new + { + Position = command.Position.ToByteArray(), + command.Type, + command.Source + }; + return command; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Dapper/EventStreamReader.cs b/Src/DDD.Core.Dapper/EventStreamReader.cs new file mode 100644 index 0000000..dd7d0fe --- /dev/null +++ b/Src/DDD.Core.Dapper/EventStreamReader.cs @@ -0,0 +1,122 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using System.Linq; +using System.Collections.Generic; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + using Mapping; + using Threading; + + public class EventStreamReader : IQueryHandler, TContext> + where TContext : BoundedContext + { + + #region Fields + + private readonly IDbConnectionProvider connectionProvider; + private readonly CompositeTranslator exceptionTranslator; + + #endregion Fields + + #region Constructors + + public EventStreamReader(IDbConnectionProvider connectionProvider) + { + Ensure.That(connectionProvider, nameof(connectionProvider)).IsNotNull(); + this.connectionProvider = connectionProvider; + this.exceptionTranslator = new CompositeTranslator(); + this.exceptionTranslator.Register(new DbToQueryExceptionTranslator()); + this.exceptionTranslator.RegisterFallback(); + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.connectionProvider.Context; + + #endregion Properties + + #region Methods + + public IEnumerable Handle(ReadEventStream query, IMessageContext context) + { + Ensure.That(query, nameof(query)).IsNotNull(); + try + { + var connection = this.connectionProvider.GetOpenConnection(); + return connection.Query + ( + new CommandDefinition + ( + GetScript(connection), + ToParameters(query, connection) + ) + ); + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Query = query }); + } + } + + public async Task> HandleAsync(ReadEventStream query, IMessageContext context) + { + Ensure.That(query, nameof(query)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + var cancellationToken = context.CancellationToken(); + var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); + return await connection.QueryAsync + ( + new CommandDefinition + ( + GetScript(connection), + ToParameters(query, connection), + cancellationToken: cancellationToken + ) + ); + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Query = query }); + } + } + + private static string GetScript(IDbConnection connection) + { + var expressions = connection.Expressions(); + var script = connection.HasOracleProvider() ? OracleScripts.ReadEventStream + : SqlScripts.ReadEventStream; + return script.Replace("@", expressions.ParameterPrefix()); + } + + /// Workaround for https://github.com/DapperLib/Dapper/issues/303 + private static object ToParameters(ReadEventStream query, IDbConnection connection) + { + if (!query.ExcludedStreamIds.Any()) + query.ExcludedStreamIds = new[] { " " }; // Workaround for https://github.com/DapperLib/Dapper/issues/202 + if (connection.HasOracleProvider()) + return new + { + query.Top, + StreamPosition = query.StreamPosition.ToByteArray(), + query.ExcludedStreamIds, + query.StreamType + }; + return query; + } + + #endregion Methods + + } + +} \ No newline at end of file diff --git a/Src/DDD.Core.Dapper/EventStreamSubcriber.cs b/Src/DDD.Core.Dapper/EventStreamSubcriber.cs new file mode 100644 index 0000000..237d6e6 --- /dev/null +++ b/Src/DDD.Core.Dapper/EventStreamSubcriber.cs @@ -0,0 +1,123 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using System.Transactions; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + using Mapping; + using Threading; + + public class EventStreamSubscriber : ICommandHandler + where TContext : BoundedContext + { + + #region Fields + + private readonly IDbConnectionProvider connectionProvider; + private readonly CompositeTranslator exceptionTranslator; + + #endregion Fields + + #region Constructors + + public EventStreamSubscriber(IDbConnectionProvider connectionProvider) + { + Ensure.That(connectionProvider, nameof(connectionProvider)).IsNotNull(); + this.connectionProvider = connectionProvider; + this.exceptionTranslator = new CompositeTranslator(); + this.exceptionTranslator.Register(new DbToCommandExceptionTranslator()); + this.exceptionTranslator.RegisterFallback(); + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.connectionProvider.Context; + + #endregion Properties + + #region Methods + + public void Handle(SubscribeToEventStream command, IMessageContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + try + { + using (var scope = new TransactionScope()) + { + var connection = this.connectionProvider.GetOpenConnection(); + var expressions = connection.Expressions(); + connection.Execute + ( + new CommandDefinition + ( + SqlScripts.SubscribeToEventStream.Replace("@", expressions.ParameterPrefix()), + ToParameters(command, connection) + ) + ); + scope.Complete(); + } + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Command = command }); + } + } + + public async Task HandleAsync(SubscribeToEventStream command, IMessageContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + var cancellationToken = context.CancellationToken(); + var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); + var expressions = connection.Expressions(); + await connection.ExecuteAsync + ( + new CommandDefinition + ( + SqlScripts.SubscribeToEventStream.Replace("@", expressions.ParameterPrefix()), + ToParameters(command, connection), + cancellationToken: cancellationToken + ) + ); + scope.Complete(); + } + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Command = command }); + } + } + + + /// Workaround for https://github.com/DapperLib/Dapper/issues/303 + private object ToParameters(SubscribeToEventStream command, IDbConnection connection) + { + if (connection.HasOracleProvider()) + return new + { + command.Type, + command.Source, + Position = command.Position.ToByteArray(), + command.RetryMax, + command.RetryDelays, + command.BlockSize + }; + return command; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Dapper/EventStreamsFinder.cs b/Src/DDD.Core.Dapper/EventStreamsFinder.cs new file mode 100644 index 0000000..d1648d3 --- /dev/null +++ b/Src/DDD.Core.Dapper/EventStreamsFinder.cs @@ -0,0 +1,93 @@ +using System; +using System.Threading.Tasks; +using System.Collections.Generic; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + using Mapping; + using Threading; + + public class EventStreamsFinder : IQueryHandler, TContext> + where TContext : BoundedContext + { + + #region Fields + + private readonly IDbConnectionProvider connectionProvider; + private readonly CompositeTranslator exceptionTranslator; + + #endregion Fields + + #region Constructors + + public EventStreamsFinder(IDbConnectionProvider connectionProvider) + { + Ensure.That(connectionProvider, nameof(connectionProvider)).IsNotNull(); + this.connectionProvider = connectionProvider; + this.exceptionTranslator = new CompositeTranslator(); + this.exceptionTranslator.Register(new DbToQueryExceptionTranslator()); + this.exceptionTranslator.RegisterFallback(); + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.connectionProvider.Context; + + #endregion Properties + + #region Methods + + public IEnumerable Handle(FindEventStreams query, IMessageContext context) + { + Ensure.That(query, nameof(query)).IsNotNull(); + try + { + var connection = this.connectionProvider.GetOpenConnection(); + return connection.Query + ( + new CommandDefinition + ( + SqlScripts.FindEventStreams + ) + ); + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Query = query }); + } + } + + public async Task> HandleAsync(FindEventStreams query, IMessageContext context) + { + Ensure.That(query, nameof(query)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + var cancellationToken = context.CancellationToken(); + var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); + return await connection.QueryAsync + ( + new CommandDefinition + ( + SqlScripts.FindEventStreams, + cancellationToken: cancellationToken + ) + ); + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Query = query }); + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Dapper/FailedEventStreamExcluder.cs b/Src/DDD.Core.Dapper/FailedEventStreamExcluder.cs new file mode 100644 index 0000000..2dcbc0e --- /dev/null +++ b/Src/DDD.Core.Dapper/FailedEventStreamExcluder.cs @@ -0,0 +1,133 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using System.Transactions; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + using Mapping; + using Threading; + + public class FailedEventStreamCreator : ICommandHandler + where TContext : BoundedContext + + { + + #region Fields + + private readonly IDbConnectionProvider connectionProvider; + private readonly CompositeTranslator exceptionTranslator; + + #endregion Fields + + #region Constructors + + public FailedEventStreamCreator(IDbConnectionProvider connectionProvider) + { + Ensure.That(connectionProvider, nameof(connectionProvider)).IsNotNull(); + this.connectionProvider = connectionProvider; + this.exceptionTranslator = new CompositeTranslator(); + this.exceptionTranslator.Register(new DbToCommandExceptionTranslator()); + this.exceptionTranslator.RegisterFallback(); + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.connectionProvider.Context; + + #endregion Properties + + #region Methods + + public void Handle(ExcludeFailedEventStream command, IMessageContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + try + { + using (var scope = new TransactionScope()) + { + var connection = this.connectionProvider.GetOpenConnection(); + var expressions = connection.Expressions(); + connection.Execute + ( + new CommandDefinition + ( + SqlScripts.ExcludeFailedEventStream.Replace("@", expressions.ParameterPrefix()), + ToParameters(command, connection) + ) + ); + scope.Complete(); + } + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Command = command }); + } + } + + public async Task HandleAsync(ExcludeFailedEventStream command, IMessageContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + var cancellationToken = context.CancellationToken(); + var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); + var expressions = connection.Expressions(); + await connection.ExecuteAsync + ( + new CommandDefinition + ( + SqlScripts.ExcludeFailedEventStream.Replace("@", expressions.ParameterPrefix()), + ToParameters(command, connection), + cancellationToken: cancellationToken + ) + ); + scope.Complete(); + } + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Command = command }); + } + } + + /// Workaround for https://github.com/DapperLib/Dapper/issues/303 + private object ToParameters(ExcludeFailedEventStream command, IDbConnection connection) + { + if (connection.HasOracleProvider()) + return new + { + command.StreamId, + command.StreamType, + command.StreamSource, + StreamPosition = command.StreamPosition.ToByteArray(), + EventId = command.EventId.ToByteArray(), + command.EventType, + command.ExceptionTime, + command.ExceptionType, + command.ExceptionMessage, + command.ExceptionSource, + command.ExceptionInfo, + command.BaseExceptionType, + command.BaseExceptionMessage, + command.RetryCount, + command.RetryMax, + command.RetryDelays, + command.BlockSize + }; + return command; + } + + #endregion Methods + } +} diff --git a/Src/DDD.Core.Dapper/FailedEventStreamIncluder.cs b/Src/DDD.Core.Dapper/FailedEventStreamIncluder.cs new file mode 100644 index 0000000..5f4229a --- /dev/null +++ b/Src/DDD.Core.Dapper/FailedEventStreamIncluder.cs @@ -0,0 +1,105 @@ +using System; +using System.Threading.Tasks; +using System.Transactions; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + using Mapping; + using Threading; + + public class FailedEventStreamDeleter : ICommandHandler + where TContext : BoundedContext + { + + #region Fields + + private readonly IDbConnectionProvider connectionProvider; + private readonly CompositeTranslator exceptionTranslator; + + #endregion Fields + + #region Constructors + + public FailedEventStreamDeleter(IDbConnectionProvider connectionProvider) + { + Ensure.That(connectionProvider, nameof(connectionProvider)).IsNotNull(); + this.connectionProvider = connectionProvider; + this.exceptionTranslator = new CompositeTranslator(); + this.exceptionTranslator.Register(new DbToCommandExceptionTranslator()); + this.exceptionTranslator.RegisterFallback(); + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.connectionProvider.Context; + + #endregion Properties + + #region Methods + + public void Handle(IncludeFailedEventStream command, IMessageContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + try + { + using (var scope = new TransactionScope()) + { + var connection = this.connectionProvider.GetOpenConnection(); + var expressions = connection.Expressions(); + connection.Execute + ( + new CommandDefinition + ( + SqlScripts.IncludeFailedEventStream.Replace("@", expressions.ParameterPrefix()), + command + ) + ); + scope.Complete(); + } + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Command = command }); + } + } + + public async Task HandleAsync(IncludeFailedEventStream command, IMessageContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + var cancellationToken = context.CancellationToken(); + var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); + var expressions = connection.Expressions(); + await connection.ExecuteAsync + ( + new CommandDefinition + ( + SqlScripts.IncludeFailedEventStream.Replace("@", expressions.ParameterPrefix()), + command, + cancellationToken: cancellationToken + ) + ); + scope.Complete(); + } + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Command = command }); + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Dapper/FailedEventStreamPositionUpdater.cs b/Src/DDD.Core.Dapper/FailedEventStreamPositionUpdater.cs new file mode 100644 index 0000000..3582a24 --- /dev/null +++ b/Src/DDD.Core.Dapper/FailedEventStreamPositionUpdater.cs @@ -0,0 +1,120 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using System.Transactions; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + using Mapping; + using Threading; + + public class FailedEventStreamPositionUpdater : ICommandHandler + where TContext : BoundedContext + { + + #region Fields + + private readonly IDbConnectionProvider connectionProvider; + private readonly CompositeTranslator exceptionTranslator; + + #endregion Fields + + #region Constructors + + public FailedEventStreamPositionUpdater(IDbConnectionProvider connectionProvider) + { + Ensure.That(connectionProvider, nameof(connectionProvider)).IsNotNull(); + this.connectionProvider = connectionProvider; + this.exceptionTranslator = new CompositeTranslator(); + this.exceptionTranslator.Register(new DbToCommandExceptionTranslator()); + this.exceptionTranslator.RegisterFallback(); + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.connectionProvider.Context; + + #endregion Properties + + #region Methods + + public void Handle(UpdateFailedEventStreamPosition command, IMessageContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + try + { + using (var scope = new TransactionScope()) + { + var connection = this.connectionProvider.GetOpenConnection(); + var expressions = connection.Expressions(); + connection.Execute + ( + new CommandDefinition + ( + SqlScripts.UpdateFailedEventStreamPosition.Replace("@", expressions.ParameterPrefix()), + ToParameters(command, connection) + ) + ); + scope.Complete(); + } + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Command = command }); + } + } + + public async Task HandleAsync(UpdateFailedEventStreamPosition command, IMessageContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + var cancellationToken = context.CancellationToken(); + var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); + var expressions = connection.Expressions(); + await connection.ExecuteAsync + ( + new CommandDefinition + ( + SqlScripts.UpdateFailedEventStreamPosition.Replace("@", expressions.ParameterPrefix()), + ToParameters(command, connection), + cancellationToken: cancellationToken + ) + ); + scope.Complete(); + } + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Command = command }); + } + } + + /// Workaround for https://github.com/DapperLib/Dapper/issues/303 + private object ToParameters(UpdateFailedEventStreamPosition command, IDbConnection connectionProvider) + { + if (connectionProvider.HasOracleProvider()) + return new + { + Position = command.Position.ToByteArray(), + command.Id, + command.Type, + command.Source + }; + return command; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Dapper/FailedEventStreamReader.cs b/Src/DDD.Core.Dapper/FailedEventStreamReader.cs new file mode 100644 index 0000000..9990424 --- /dev/null +++ b/Src/DDD.Core.Dapper/FailedEventStreamReader.cs @@ -0,0 +1,120 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using System.Collections.Generic; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + using Mapping; + using Threading; + + public class FailedEventStreamReader : IQueryHandler, TContext> + where TContext : BoundedContext + { + + #region Fields + + private readonly IDbConnectionProvider connectionProvider; + private readonly CompositeTranslator exceptionTranslator; + + #endregion Fields + + #region Constructors + + public FailedEventStreamReader(IDbConnectionProvider connectionProvider) + { + Ensure.That(connectionProvider, nameof(connectionProvider)).IsNotNull(); + this.connectionProvider = connectionProvider; + this.exceptionTranslator = new CompositeTranslator(); + this.exceptionTranslator.Register(new DbToQueryExceptionTranslator()); + this.exceptionTranslator.RegisterFallback(); + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.connectionProvider.Context; + + #endregion Properties + + #region Methods + + public IEnumerable Handle(ReadFailedEventStream query, IMessageContext context) + { + Ensure.That(query, nameof(query)).IsNotNull(); + try + { + var connection = this.connectionProvider.GetOpenConnection(); + return connection.Query + ( + new CommandDefinition + ( + GetScript(connection), + ToParameters(query, connection) + ) + ); + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Query = query }); + } + } + + public async Task> HandleAsync(ReadFailedEventStream query, IMessageContext context) + { + Ensure.That(query, nameof(query)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + var cancellationToken = context.CancellationToken(); + var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); + return await connection.QueryAsync + ( + new CommandDefinition + ( + GetScript(connection), + ToParameters(query, connection), + cancellationToken: cancellationToken + ) + ); + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Query = query }); + } + } + + private static string GetScript(IDbConnection connection) + { + var expressions = connection.Expressions(); + var script = connection.HasOracleProvider() ? OracleScripts.ReadFailedEventStream + : SqlScripts.ReadFailedEventStream; + return script.Replace("@", expressions.ParameterPrefix()); + } + + /// Workaround for https://github.com/DapperLib/Dapper/issues/303 + private static object ToParameters(ReadFailedEventStream query, IDbConnection connection) + { + if (connection.HasOracleProvider()) + return new + { + query.Top, + EventIdMin = query.EventIdMin.ToByteArray(), + EventIdMax = query.EventIdMax.ToByteArray(), + query.StreamId, + query.StreamType + }; + return query; + } + + #endregion Methods + + } + +} \ No newline at end of file diff --git a/Src/DDD.Core.Dapper/FailedEventStreamUpdater.cs b/Src/DDD.Core.Dapper/FailedEventStreamUpdater.cs new file mode 100644 index 0000000..a259fe5 --- /dev/null +++ b/Src/DDD.Core.Dapper/FailedEventStreamUpdater.cs @@ -0,0 +1,131 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using System.Transactions; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + using Mapping; + using Threading; + + public class FailedEventStreamUpdater : ICommandHandler + where TContext : BoundedContext + { + + #region Fields + + private readonly IDbConnectionProvider connectionProvider; + private readonly CompositeTranslator exceptionTranslator; + + #endregion Fields + + #region Constructors + + public FailedEventStreamUpdater(IDbConnectionProvider connectionProvider) + { + Ensure.That(connectionProvider, nameof(connectionProvider)).IsNotNull(); + this.connectionProvider = connectionProvider; + this.exceptionTranslator = new CompositeTranslator(); + this.exceptionTranslator.Register(new DbToCommandExceptionTranslator()); + this.exceptionTranslator.RegisterFallback(); + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.connectionProvider.Context; + + #endregion Properties + + #region Methods + + public void Handle(UpdateFailedEventStream command, IMessageContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + try + { + using (var scope = new TransactionScope()) + { + var connection = this.connectionProvider.GetOpenConnection(); + var expressions = connection.Expressions(); + connection.Execute + ( + new CommandDefinition + ( + SqlScripts.UpdateFailedEventStream.Replace("@", expressions.ParameterPrefix()), + ToParameters(command, connection) + ) + ); + scope.Complete(); + } + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Command = command }); + } + } + + public async Task HandleAsync(UpdateFailedEventStream command, IMessageContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + var cancellationToken = context.CancellationToken(); + var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); + var expressions = connection.Expressions(); + await connection.ExecuteAsync + ( + new CommandDefinition + ( + SqlScripts.UpdateFailedEventStream.Replace("@", expressions.ParameterPrefix()), + ToParameters(command, connection), + cancellationToken: cancellationToken + ) + ); + scope.Complete(); + } + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Command = command }); + } + } + + /// Workaround for https://github.com/DapperLib/Dapper/issues/303 + private object ToParameters(UpdateFailedEventStream command, IDbConnection connection) + { + if (connection.HasOracleProvider()) + return new + { + command.StreamId, + command.StreamType, + command.StreamSource, + StreamPosition = command.StreamPosition.ToByteArray(), + EventId = command.EventId.ToByteArray(), + command.EventType, + command.ExceptionTime, + command.ExceptionType, + command.ExceptionMessage, + command.ExceptionSource, + command.ExceptionInfo, + command.BaseExceptionType, + command.BaseExceptionMessage, + command.RetryCount, + command.RetryMax, + command.RetryDelays + }; + return command; + } + + #endregion Methods + } +} diff --git a/Src/DDD.Core.Dapper/FailedEventStreamsFinder.cs b/Src/DDD.Core.Dapper/FailedEventStreamsFinder.cs new file mode 100644 index 0000000..a137697 --- /dev/null +++ b/Src/DDD.Core.Dapper/FailedEventStreamsFinder.cs @@ -0,0 +1,95 @@ +using System; +using System.Threading.Tasks; +using System.Collections.Generic; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + using Mapping; + using Threading; + + public class FailedEventStreamsFinder : IQueryHandler, TContext> + where TContext : BoundedContext + { + + #region Fields + + private readonly IDbConnectionProvider connectionProvider; + private readonly CompositeTranslator exceptionTranslator; + + #endregion Fields + + #region Constructors + + public FailedEventStreamsFinder(IDbConnectionProvider connectionProvider) + { + Ensure.That(connectionProvider, nameof(connectionProvider)).IsNotNull(); + this.connectionProvider = connectionProvider; + this.exceptionTranslator = new CompositeTranslator(); + this.exceptionTranslator.Register(new DbToQueryExceptionTranslator()); + this.exceptionTranslator.RegisterFallback(); + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.connectionProvider.Context; + + #endregion Properties + + #region Methods + + public IEnumerable Handle(FindFailedEventStreams query, IMessageContext context) + { + Ensure.That(query, nameof(query)).IsNotNull(); + try + { + var connection = this.connectionProvider.GetOpenConnection(); + var expressions = connection.Expressions(); + return connection.Query + ( + new CommandDefinition + ( + SqlScripts.FindFailedEventStreams + ) + ); + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Query = query }); + } + } + + public async Task> HandleAsync(FindFailedEventStreams query, IMessageContext context) + { + Ensure.That(query, nameof(query)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + var cancellationToken = context.CancellationToken(); + var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); + var expressions = connection.Expressions(); + return await connection.QueryAsync + ( + new CommandDefinition + ( + SqlScripts.FindFailedEventStreams, + cancellationToken: cancellationToken + ) + ); + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Query = query }); + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Dapper/FailedRecurringCommandUpdater.cs b/Src/DDD.Core.Dapper/FailedRecurringCommandUpdater.cs new file mode 100644 index 0000000..572f329 --- /dev/null +++ b/Src/DDD.Core.Dapper/FailedRecurringCommandUpdater.cs @@ -0,0 +1,125 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using System.Transactions; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + using Mapping; + using Threading; + + public class FailedRecurringCommandUpdater : ICommandHandler + where TContext : BoundedContext + { + + #region Fields + + private readonly IDbConnectionProvider connectionProvider; + private readonly CompositeTranslator exceptionTranslator; + + #endregion Fields + + #region Constructors + + public FailedRecurringCommandUpdater(IDbConnectionProvider connectionProvider) + { + Ensure.That(connectionProvider, nameof(connectionProvider)).IsNotNull(); + this.connectionProvider = connectionProvider; + this.exceptionTranslator = new CompositeTranslator(); + this.exceptionTranslator.Register(new DbToCommandExceptionTranslator()); + this.exceptionTranslator.RegisterFallback(); + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.connectionProvider.Context; + + #endregion Properties + + #region Methods + + public void Handle(MarkRecurringCommandAsFailed command, IMessageContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + try + { + using (var scope = new TransactionScope()) + { + var connection = this.connectionProvider.GetOpenConnection(); + var expressions = connection.Expressions(); + connection.Execute + ( + new CommandDefinition + ( + SqlScripts.UpdateRecurringCommandStatus.Replace("@", expressions.ParameterPrefix()), + ToParameters(command, connection) + ) + ); + scope.Complete(); + } + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Command = command }); + } + } + + public async Task HandleAsync(MarkRecurringCommandAsFailed command, IMessageContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + var cancellationToken = context.CancellationToken(); + var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); + var expressions = connection.Expressions(); + await connection.ExecuteAsync + ( + new CommandDefinition + ( + SqlScripts.UpdateRecurringCommandStatus.Replace("@", expressions.ParameterPrefix()), + ToParameters(command, connection), + cancellationToken: cancellationToken + ) + ); + scope.Complete(); + } + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Command = command }); + } + } + + /// Workaround for https://github.com/DapperLib/Dapper/issues/303 + private object ToParameters(MarkRecurringCommandAsFailed command, IDbConnection connection) + { + if (connection.HasOracleProvider()) + return new + { + LastExecutionTime = command.ExecutionTime, + LastExecutionStatus = "F", + LastExceptionInfo = command.ExceptionInfo, + CommandId = command.CommandId.ToByteArray() + }; + return new + { + LastExecutionTime = command.ExecutionTime, + LastExecutionStatus = "F", + LastExceptionInfo = command.ExceptionInfo, + CommandId = command.CommandId + }; + } + + #endregion Methods + } +} diff --git a/Src/DDD.Core.Dapper/IDbConnectionExtensions.cs b/Src/DDD.Core.Dapper/IDbConnectionExtensions.cs index c0ed45e..36bdd73 100644 --- a/Src/DDD.Core.Dapper/IDbConnectionExtensions.cs +++ b/Src/DDD.Core.Dapper/IDbConnectionExtensions.cs @@ -1,6 +1,7 @@ using System.Data; using Dapper; -using Conditions; +using EnsureThat; +using System.Threading; using System.Threading.Tasks; using System.Collections.Generic; using System; @@ -8,6 +9,8 @@ namespace DDD.Core.Infrastructure.Data { + using Threading; + /// /// Adds extension method to the interface. /// @@ -20,16 +23,16 @@ public static int[] ExecuteScript(this IDbConnection connection, string script, IDbTransaction transaction = null, int? commandTimeout = null, - string batchSeparator = "GO", - bool removeComments = true) + string batchSeparator = "GO", + bool removeComments = false) { - Condition.Requires(connection, nameof(connection)).IsNotNull(); - Condition.Requires(script, nameof(script)).IsNotNullOrEmpty(); - Condition.Requires(nameof(batchSeparator), batchSeparator).IsNotNullOrWhiteSpace(); + Ensure.That(connection, nameof(connection)).IsNotNull(); + Ensure.That(script, nameof(script)).IsNotNullOrEmpty(); + Ensure.That(nameof(batchSeparator), batchSeparator).IsNotNullOrWhiteSpace(); var scriptSplitter = new DbScriptSplitter(); var commands = scriptSplitter.Split(script, batchSeparator, removeComments); var result = new List(); - foreach(var command in commands) + foreach (var command in commands) { if (StartsWithUseStatement(command)) connection.ExecuteUseStatement(command); @@ -38,34 +41,86 @@ public static int[] ExecuteScript(this IDbConnection connection, return result.ToArray(); } - private static bool StartsWithUseStatement(string command) + public static async Task ExecuteScriptAsync(this IDbConnection connection, + string script, + IDbTransaction transaction = null, + int? commandTimeout = null, + string batchSeparator = "GO", + bool removeComments = false, + CancellationToken cancellationToken = default) { - return command.StartsWith("USE ", StringComparison.OrdinalIgnoreCase); + Ensure.That(connection, nameof(connection)).IsNotNull(); + Ensure.That(script, nameof(script)).IsNotNullOrEmpty(); + Ensure.That(nameof(batchSeparator), batchSeparator).IsNotNullOrWhiteSpace(); + await new SynchronizationContextRemover(); + var scriptSplitter = new DbScriptSplitter(); + var commands = scriptSplitter.Split(script, batchSeparator, removeComments); + var result = new List(); + foreach (var command in commands) + { + if (StartsWithUseStatement(command)) + connection.ExecuteUseStatement(command); + var commandDefinition = new CommandDefinition(command, null, transaction, commandTimeout, cancellationToken: cancellationToken); + result.Add(await connection.ExecuteAsync(commandDefinition)); + } + return result.ToArray(); } - private static void ExecuteUseStatement(this IDbConnection connection, string command) + public static int NextId(this IDbConnection connection, string table, string primaryKey, int startRangeId = 0) { - var buffer = command.Remove(0, 3).TrimStart(); - var databaseName = Regex.Split(buffer, @"[\s;]+")[0].TrimStart('[').TrimEnd(']'); - connection.ChangeDatabase(databaseName); + Ensure.That(connection, nameof(connection)).IsNotNull(); + Ensure.That(table, nameof(table)).IsNotNullOrEmpty(); + Ensure.That(primaryKey, nameof(primaryKey)).IsNotNullOrEmpty(); + var expressions = connection.Expressions(); + var sql = $"SELECT MAX({primaryKey}) FROM {table} WHERE {primaryKey} >= @StartRangeId".Replace("@", expressions.ParameterPrefix()); + var result = connection.QuerySingleOrDefault(sql, new { StartRangeId = startRangeId }); + if (!result.HasValue) return startRangeId + 1; + return result.Value + 1; + } + + public static async Task NextIdAsync(this IDbConnection connection, string table, string primaryKey, int startRangeId = 0, CancellationToken cancellationToken = default) + { + Ensure.That(connection, nameof(connection)).IsNotNull(); + Ensure.That(table, nameof(table)).IsNotNullOrEmpty(); + Ensure.That(primaryKey, nameof(primaryKey)).IsNotNullOrEmpty(); + await new SynchronizationContextRemover(); + var expressions = connection.Expressions(); + var command = $"SELECT MAX({primaryKey}) FROM {table} WHERE {primaryKey} >= @StartRangeId".Replace("@", expressions.ParameterPrefix()); + var commandDefinition = new CommandDefinition(command, new { StartRangeId = startRangeId }, cancellationToken: cancellationToken); + var result = await connection.QuerySingleOrDefaultAsync(commandDefinition); + if (!result.HasValue) return startRangeId + 1; + return result.Value + 1; } public static TValue NextValue(this IDbConnection connection, string sequence, string schema = null) { - Condition.Requires(connection, nameof(connection)).IsNotNull(); - Condition.Requires(sequence, nameof(sequence)).IsNotNullOrEmpty(); + Ensure.That(connection, nameof(connection)).IsNotNull(); + Ensure.That(sequence, nameof(sequence)).IsNotNullOrEmpty(); var expressions = connection.Expressions(); - var sql = $"SELECT {expressions.NextValue(sequence, schema)} {expressions.FromDummy()}"; - return connection.QuerySingle(sql); + var command = $"SELECT {expressions.NextValue(sequence, schema)} {expressions.FromDummy()}"; + return connection.QuerySingle(command); } - public static Task NextValueAsync(this IDbConnection connection, string sequence, string schema = null) + public static async Task NextValueAsync(this IDbConnection connection, string sequence, string schema = null, CancellationToken cancellationToken = default) { - Condition.Requires(connection, nameof(connection)).IsNotNull(); - Condition.Requires(sequence, nameof(sequence)).IsNotNullOrEmpty(); + Ensure.That(connection, nameof(connection)).IsNotNull(); + Ensure.That(sequence, nameof(sequence)).IsNotNullOrEmpty(); + await new SynchronizationContextRemover(); var expressions = connection.Expressions(); - var sql = $"SELECT {expressions.NextValue(sequence, schema)} {expressions.FromDummy()}"; - return connection.QuerySingleAsync(sql); + var command = $"SELECT {expressions.NextValue(sequence, schema)} {expressions.FromDummy()}"; + var commandDefinition = new CommandDefinition(command, cancellationToken: cancellationToken); + return await connection.QuerySingleAsync(commandDefinition); + } + private static void ExecuteUseStatement(this IDbConnection connection, string command) + { + var buffer = command.Remove(0, 3).TrimStart(); + var databaseName = Regex.Split(buffer, @"[\s;]+")[0].TrimStart('[').TrimEnd(']'); + connection.ChangeDatabase(databaseName); + } + + private static bool StartsWithUseStatement(string command) + { + return command.StartsWith("USE ", StringComparison.OrdinalIgnoreCase); } #endregion Methods diff --git a/Src/DDD.Core.Dapper/IncrementalDelaysTypeMapper.cs b/Src/DDD.Core.Dapper/IncrementalDelaysTypeMapper.cs new file mode 100644 index 0000000..3a1ceb1 --- /dev/null +++ b/Src/DDD.Core.Dapper/IncrementalDelaysTypeMapper.cs @@ -0,0 +1,48 @@ +using Dapper; +using System; +using System.Linq; +using System.Collections.Generic; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using System.Data; + + public class IncrementalDelaysTypeMapper : SqlMapper.TypeHandler> + { + + #region Methods + + public override void SetValue(IDbDataParameter parameter, ICollection value) + { + if (value == null || !value.Any()) + parameter.Value = DBNull.Value; + else + parameter.Value = string.Join(",", value.Select(d => ToString(d))); + } + + public override ICollection Parse(object value) + { + if (value is DBNull || string.IsNullOrWhiteSpace((string)value)) return new List(); + var delays = ((string)value).Replace(" ", "").Split(','); + return delays.Select(d => ToIncrementalDelay(d)).ToList(); + } + + private static string ToString(IncrementalDelay delay) + { + return delay.Increment == 0 ? delay.Delay.ToString() : $"{delay.Delay}/{delay.Increment}"; + } + + private static IncrementalDelay ToIncrementalDelay(string delay) + { + var parts = delay.Split('/'); + var incrementalDelay = new IncrementalDelay { Delay = short.Parse(parts[0]) }; + if (parts.Length == 2) + incrementalDelay.Increment = short.Parse(parts[1]); + return incrementalDelay; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Dapper/OracleScripts.Designer.cs b/Src/DDD.Core.Dapper/OracleScripts.Designer.cs new file mode 100644 index 0000000..142cd70 --- /dev/null +++ b/Src/DDD.Core.Dapper/OracleScripts.Designer.cs @@ -0,0 +1,111 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace DDD.Core.Infrastructure.Data { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class OracleScripts { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal OracleScripts() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DDD.Core.Infrastructure.Data.OracleScripts", typeof(OracleScripts).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to SELECT * + ///FROM + /// (SELECT EventId, + /// EventType, + /// OccurredOn, + /// Body, + /// BodyFormat, + /// StreamId, + /// StreamType, + /// IssuedBy + /// FROM Event + /// WHERE EventId > @StreamPosition + /// AND StreamId NOT IN @ExcludedStreamIds + /// AND StreamType = @StreamType + /// ORDER BY EventId) + ///WHERE ROWNUM <= @Top. + /// + internal static string ReadEventStream { + get { + return ResourceManager.GetString("ReadEventStream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SELECT * + ///FROM + /// (SELECT EventId, + /// EventType, + /// OccurredOn, + /// Body, + /// BodyFormat, + /// StreamId, + /// StreamType, + /// IssuedBy + /// FROM Event + /// WHERE EventId BETWEEN @EventIdMin AND @EventIdMax + /// AND StreamId = @StreamId + /// AND StreamType = @StreamType + /// ORDER BY EventId) + ///WHERE ROWNUM <= @Top. + /// + internal static string ReadFailedEventStream { + get { + return ResourceManager.GetString("ReadFailedEventStream", resourceCulture); + } + } + } +} diff --git a/Src/DDD.Core.Dapper/OracleScripts.resx b/Src/DDD.Core.Dapper/OracleScripts.resx new file mode 100644 index 0000000..1e90fc9 --- /dev/null +++ b/Src/DDD.Core.Dapper/OracleScripts.resx @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + scripts\oracle\readeventstream.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\oracle\readfailedeventstream.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + \ No newline at end of file diff --git a/Src/DDD.Core.Dapper/Properties/AssemblyInfo.cs b/Src/DDD.Core.Dapper/Properties/AssemblyInfo.cs index 8356b07..ee3d74a 100644 --- a/Src/DDD.Core.Dapper/Properties/AssemblyInfo.cs +++ b/Src/DDD.Core.Dapper/Properties/AssemblyInfo.cs @@ -2,6 +2,6 @@ using System.Runtime.InteropServices; [assembly: AssemblyTitle("DDD.Core.Dapper")] -[assembly: AssemblyDescription("Usefull extensions of the Dapper library.")] +[assembly: AssemblyDescription("Core components for database access with Dapper.")] [assembly: AssemblyProduct("DDD.Core.Dapper")] [assembly: Guid("701da58b-ae36-429f-8621-64109b8d29d7")] \ No newline at end of file diff --git a/Src/DDD.Core.Dapper/RecurringCommandIdGenerator.cs b/Src/DDD.Core.Dapper/RecurringCommandIdGenerator.cs new file mode 100644 index 0000000..beba2ff --- /dev/null +++ b/Src/DDD.Core.Dapper/RecurringCommandIdGenerator.cs @@ -0,0 +1,59 @@ +using EnsureThat; +using System; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + using Mapping; + + public class RecurringCommandIdGenerator : ISyncQueryHandler + where TContext : BoundedContext + { + + #region Fields + + private readonly IDbConnectionProvider connectionProvider; + private readonly CompositeTranslator exceptionTranslator; + + #endregion Fields + + #region Constructors + + public RecurringCommandIdGenerator(IDbConnectionProvider connectionProvider) + { + Ensure.That(connectionProvider, nameof(connectionProvider)).IsNotNull(); + this.connectionProvider = connectionProvider; + this.exceptionTranslator = new CompositeTranslator(); + this.exceptionTranslator.Register(new DbToQueryExceptionTranslator()); + this.exceptionTranslator.RegisterFallback(); + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.connectionProvider.Context; + + #endregion Properties + + #region Methods + + public Guid Handle(GenerateRecurringCommandId query, IMessageContext context) + { + Ensure.That(query, nameof(query)).IsNotNull(); + try + { + var connection = this.connectionProvider.GetOpenConnection(); + return connection.SequentialGuidGenerator().Generate(); + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Query = query }); + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Dapper/RecurringCommandRegister.cs b/Src/DDD.Core.Dapper/RecurringCommandRegister.cs new file mode 100644 index 0000000..263cece --- /dev/null +++ b/Src/DDD.Core.Dapper/RecurringCommandRegister.cs @@ -0,0 +1,179 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using System.Transactions; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + using Mapping; + using Threading; + + public class RecurringCommandRegister : ICommandHandler + where TContext : BoundedContext + { + + #region Fields + + private readonly IDbConnectionProvider connectionProvider; + private readonly CompositeTranslator exceptionTranslator; + + #endregion Fields + + #region Constructors + + public RecurringCommandRegister(IDbConnectionProvider connectionProvider) + { + Ensure.That(connectionProvider, nameof(connectionProvider)).IsNotNull(); + this.connectionProvider = connectionProvider; + this.exceptionTranslator = new CompositeTranslator(); + this.exceptionTranslator.Register(new DbToCommandExceptionTranslator()); + this.exceptionTranslator.RegisterFallback(); + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.connectionProvider.Context; + + #endregion Properties + + #region Methods + + public void Handle(RegisterRecurringCommand command, IMessageContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + try + { + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + var connection = this.connectionProvider.GetOpenConnection(); + var parameterPrefix = connection.Expressions().ParameterPrefix(); + var parameters = ToParameters(command, connection); + var recurringCommand = connection.QuerySingleOrDefault + ( + new CommandDefinition + ( + SqlScripts.FindRecurringCommandByType.Replace("@", parameterPrefix), + parameters + ) + ); + if (recurringCommand == null) + { + connection.Execute + ( + new CommandDefinition + ( + SqlScripts.InsertRecurringCommand.Replace("@", parameterPrefix), + parameters + ) + ); + } + else if (HasChanges(command, recurringCommand)) + { + connection.Execute + ( + new CommandDefinition + ( + SqlScripts.UpdateRecurringCommand.Replace("@", parameterPrefix), + parameters + ) + ); + } + scope.Complete(); + } + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Command = command }); + } + } + + public async Task HandleAsync(RegisterRecurringCommand command, IMessageContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + var cancellationToken = context.CancellationToken(); + var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); + var parameterPrefix = connection.Expressions().ParameterPrefix(); + var parameters = ToParameters(command, connection); + var recurringCommand = await connection.QuerySingleOrDefaultAsync + ( + new CommandDefinition + ( + SqlScripts.FindRecurringCommandByType.Replace("@", parameterPrefix), + parameters, + cancellationToken: cancellationToken + ) + ); + if (recurringCommand == null) + { + await connection.ExecuteAsync + ( + new CommandDefinition + ( + SqlScripts.InsertRecurringCommand.Replace("@", parameterPrefix), + parameters, + cancellationToken: cancellationToken + ) + ); + } + else if (HasChanges(command, recurringCommand)) + { + await connection.ExecuteAsync + ( + new CommandDefinition + ( + SqlScripts.UpdateRecurringCommand.Replace("@", parameterPrefix), + parameters, + cancellationToken: cancellationToken + ) + ); + } + scope.Complete(); + } + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Command = command }); + } + } + + private static bool HasChanges(RegisterRecurringCommand command, RecurringCommand recurringCommand) + { + if (command.Body != recurringCommand.Body) return true; + if (command.BodyFormat != recurringCommand.BodyFormat) return true; + if (command.RecurringExpression != recurringCommand.RecurringExpression) return true; + if (command.RecurringExpressionFormat != recurringCommand.RecurringExpressionFormat) return true; + return false; + } + + /// Workaround for https://github.com/DapperLib/Dapper/issues/303 + private object ToParameters(RegisterRecurringCommand command, IDbConnection connection) + { + if (connection.HasOracleProvider()) + return new + { + CommandId = command.CommandId.ToByteArray(), + command.CommandType, + command.Body, + command.BodyFormat, + command.RecurringExpression, + command.RecurringExpressionFormat + }; + return command; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Dapper/RecurringCommandsFinder.cs b/Src/DDD.Core.Dapper/RecurringCommandsFinder.cs new file mode 100644 index 0000000..a3f7be4 --- /dev/null +++ b/Src/DDD.Core.Dapper/RecurringCommandsFinder.cs @@ -0,0 +1,95 @@ +using System; +using System.Threading.Tasks; +using System.Collections.Generic; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + using Mapping; + using Threading; + + public class RecurringCommandsFinder : IQueryHandler, TContext> + where TContext : BoundedContext + { + + #region Fields + + private readonly IDbConnectionProvider connectionProvider; + private readonly CompositeTranslator exceptionTranslator; + + #endregion Fields + + #region Constructors + + public RecurringCommandsFinder(IDbConnectionProvider connectionProvider) + { + Ensure.That(connectionProvider, nameof(connectionProvider)).IsNotNull(); + this.connectionProvider = connectionProvider; + this.exceptionTranslator = new CompositeTranslator(); + this.exceptionTranslator.Register(new DbToQueryExceptionTranslator()); + this.exceptionTranslator.RegisterFallback(); + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.connectionProvider.Context; + + #endregion Properties + + #region Methods + + public IEnumerable Handle(FindRecurringCommands query, IMessageContext context) + { + Ensure.That(query, nameof(query)).IsNotNull(); + try + { + var connection = this.connectionProvider.GetOpenConnection(); + var expressions = connection.Expressions(); + return connection.Query + ( + new CommandDefinition + ( + SqlScripts.FindRecurringCommands + ) + ); + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Query = query }); + } + } + + public async Task> HandleAsync(FindRecurringCommands query, IMessageContext context) + { + Ensure.That(query, nameof(query)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + var cancellationToken = context.CancellationToken(); + var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); + var expressions = connection.Expressions(); + return await connection.QueryAsync + ( + new CommandDefinition + ( + SqlScripts.FindRecurringCommands, + cancellationToken: cancellationToken + ) + ); + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Query = query }); + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Dapper/Scripts/ExcludeFailedEventStream.sql b/Src/DDD.Core.Dapper/Scripts/ExcludeFailedEventStream.sql new file mode 100644 index 0000000..a8e8ef3 Binary files /dev/null and b/Src/DDD.Core.Dapper/Scripts/ExcludeFailedEventStream.sql differ diff --git a/Src/DDD.Core.Dapper/Scripts/FindEventStreams.sql b/Src/DDD.Core.Dapper/Scripts/FindEventStreams.sql new file mode 100644 index 0000000..5020d5e Binary files /dev/null and b/Src/DDD.Core.Dapper/Scripts/FindEventStreams.sql differ diff --git a/Src/DDD.Core.Dapper/Scripts/FindFailedEventStreams.sql b/Src/DDD.Core.Dapper/Scripts/FindFailedEventStreams.sql new file mode 100644 index 0000000..de6b51d Binary files /dev/null and b/Src/DDD.Core.Dapper/Scripts/FindFailedEventStreams.sql differ diff --git a/Src/DDD.Core.Dapper/Scripts/FindRecurringCommandByType.sql b/Src/DDD.Core.Dapper/Scripts/FindRecurringCommandByType.sql new file mode 100644 index 0000000..f37a220 Binary files /dev/null and b/Src/DDD.Core.Dapper/Scripts/FindRecurringCommandByType.sql differ diff --git a/Src/DDD.Core.Dapper/Scripts/FindRecurringCommands.sql b/Src/DDD.Core.Dapper/Scripts/FindRecurringCommands.sql new file mode 100644 index 0000000..0ea50a6 Binary files /dev/null and b/Src/DDD.Core.Dapper/Scripts/FindRecurringCommands.sql differ diff --git a/Src/DDD.Core.Dapper/Scripts/IncludeFailedEventStream.sql b/Src/DDD.Core.Dapper/Scripts/IncludeFailedEventStream.sql new file mode 100644 index 0000000..84ae59e Binary files /dev/null and b/Src/DDD.Core.Dapper/Scripts/IncludeFailedEventStream.sql differ diff --git a/Src/DDD.Core.Dapper/Scripts/InsertRecurringCommand.sql b/Src/DDD.Core.Dapper/Scripts/InsertRecurringCommand.sql new file mode 100644 index 0000000..36629a9 Binary files /dev/null and b/Src/DDD.Core.Dapper/Scripts/InsertRecurringCommand.sql differ diff --git a/Src/DDD.Core.Dapper/Scripts/Oracle/ReadEventStream.sql b/Src/DDD.Core.Dapper/Scripts/Oracle/ReadEventStream.sql new file mode 100644 index 0000000..fc82ee7 Binary files /dev/null and b/Src/DDD.Core.Dapper/Scripts/Oracle/ReadEventStream.sql differ diff --git a/Src/DDD.Core.Dapper/Scripts/Oracle/ReadFailedEventStream.sql b/Src/DDD.Core.Dapper/Scripts/Oracle/ReadFailedEventStream.sql new file mode 100644 index 0000000..6533c1a Binary files /dev/null and b/Src/DDD.Core.Dapper/Scripts/Oracle/ReadFailedEventStream.sql differ diff --git a/Src/DDD.Core.Dapper/Scripts/ReadEventStream.sql b/Src/DDD.Core.Dapper/Scripts/ReadEventStream.sql new file mode 100644 index 0000000..011fb83 Binary files /dev/null and b/Src/DDD.Core.Dapper/Scripts/ReadEventStream.sql differ diff --git a/Src/DDD.Core.Dapper/Scripts/ReadFailedEventStream.sql b/Src/DDD.Core.Dapper/Scripts/ReadFailedEventStream.sql new file mode 100644 index 0000000..360b612 Binary files /dev/null and b/Src/DDD.Core.Dapper/Scripts/ReadFailedEventStream.sql differ diff --git a/Src/DDD.Core.Dapper/Scripts/SubscribeToEventStream.sql b/Src/DDD.Core.Dapper/Scripts/SubscribeToEventStream.sql new file mode 100644 index 0000000..7eece79 Binary files /dev/null and b/Src/DDD.Core.Dapper/Scripts/SubscribeToEventStream.sql differ diff --git a/Src/DDD.Core.Dapper/Scripts/UpdateEventStreamPosition.sql b/Src/DDD.Core.Dapper/Scripts/UpdateEventStreamPosition.sql new file mode 100644 index 0000000..ebf7a93 Binary files /dev/null and b/Src/DDD.Core.Dapper/Scripts/UpdateEventStreamPosition.sql differ diff --git a/Src/DDD.Core.Dapper/Scripts/UpdateFailedEventStream.sql b/Src/DDD.Core.Dapper/Scripts/UpdateFailedEventStream.sql new file mode 100644 index 0000000..ed0c26b Binary files /dev/null and b/Src/DDD.Core.Dapper/Scripts/UpdateFailedEventStream.sql differ diff --git a/Src/DDD.Core.Dapper/Scripts/UpdateFailedEventStreamPosition.sql b/Src/DDD.Core.Dapper/Scripts/UpdateFailedEventStreamPosition.sql new file mode 100644 index 0000000..baff45d Binary files /dev/null and b/Src/DDD.Core.Dapper/Scripts/UpdateFailedEventStreamPosition.sql differ diff --git a/Src/DDD.Core.Dapper/Scripts/UpdateRecurringCommand.sql b/Src/DDD.Core.Dapper/Scripts/UpdateRecurringCommand.sql new file mode 100644 index 0000000..e0461c8 Binary files /dev/null and b/Src/DDD.Core.Dapper/Scripts/UpdateRecurringCommand.sql differ diff --git a/Src/DDD.Core.Dapper/Scripts/UpdateRecurringCommandStatus.sql b/Src/DDD.Core.Dapper/Scripts/UpdateRecurringCommandStatus.sql new file mode 100644 index 0000000..9cfabc0 Binary files /dev/null and b/Src/DDD.Core.Dapper/Scripts/UpdateRecurringCommandStatus.sql differ diff --git a/Src/DDD.Core.Dapper/SqlScripts.Designer.cs b/Src/DDD.Core.Dapper/SqlScripts.Designer.cs new file mode 100644 index 0000000..19c8c1d --- /dev/null +++ b/Src/DDD.Core.Dapper/SqlScripts.Designer.cs @@ -0,0 +1,329 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace DDD.Core.Infrastructure.Data { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class SqlScripts { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal SqlScripts() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DDD.Core.Infrastructure.Data.SqlScripts", typeof(SqlScripts).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to INSERT INTO FailedEventStream + /// (StreamId, + /// StreamType, + /// StreamSource, + /// StreamPosition, + /// EventId, + /// EventType, + /// ExceptionTime, + /// ExceptionType, + /// ExceptionMessage, + /// ExceptionSource, + /// ExceptionInfo, + /// BaseExceptionType, + /// BaseExceptionMessage, + /// RetryCount, + /// RetryMax, + /// RetryDelays, + /// BlockSize) + /// VALUES + /// [rest of string was truncated]";. + /// + internal static string ExcludeFailedEventStream { + get { + return ResourceManager.GetString("ExcludeFailedEventStream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SELECT Type, + /// Source, + /// Position, + /// RetryMax, + /// RetryDelays, + /// BlockSize + ///FROM EventStream + ///ORDER BY Source, Type. + /// + internal static string FindEventStreams { + get { + return ResourceManager.GetString("FindEventStreams", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SELECT StreamId, + /// StreamType, + /// StreamSource, + /// StreamPosition, + /// EventId, + /// ExceptionTime, + /// RetryCount, + /// RetryMax, + /// RetryDelays, + /// BlockSize + ///FROM FailedEventStream + ///ORDER BY StreamSource, StreamType, StreamId. + /// + internal static string FindFailedEventStreams { + get { + return ResourceManager.GetString("FindFailedEventStreams", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SELECT CommandId, + /// CommandType, + /// Body, + /// BodyFormat, + /// RecurringExpression + ///FROM Command + ///WHERE CommandType = @CommandType. + /// + internal static string FindRecurringCommandByType { + get { + return ResourceManager.GetString("FindRecurringCommandByType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SELECT CommandId, + /// CommandType, + /// Body, + /// BodyFormat, + /// RecurringExpression + ///FROM Command + ///ORDER BY CommandId. + /// + internal static string FindRecurringCommands { + get { + return ResourceManager.GetString("FindRecurringCommands", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DELETE + ///FROM FailedEventStream + ///WHERE StreamId = @Id + ///AND StreamType = @Type + ///AND StreamSource = @Source. + /// + internal static string IncludeFailedEventStream { + get { + return ResourceManager.GetString("IncludeFailedEventStream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to INSERT INTO Command + /// (CommandId, + /// CommandType, + /// Body, + /// BodyFormat, + /// RecurringExpression) + /// VALUES + /// (@CommandId, + /// @CommandType, + /// @Body, + /// @BodyFormat, + /// @RecurringExpression). + /// + internal static string InsertRecurringCommand { + get { + return ResourceManager.GetString("InsertRecurringCommand", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SELECT TOP (@Top) + /// EventId, + /// EventType, + /// OccurredOn, + /// Body, + /// BodyFormat, + /// StreamId, + /// StreamType, + /// IssuedBy + ///FROM Event + ///WHERE EventId > @StreamPosition + ///AND StreamId NOT IN @ExcludedStreamIds + ///AND StreamType = @StreamType + ///ORDER BY EventId. + /// + internal static string ReadEventStream { + get { + return ResourceManager.GetString("ReadEventStream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SELECT TOP (@Top) + /// EventId, + /// EventType, + /// OccurredOn, + /// Body, + /// BodyFormat, + /// StreamId, + /// StreamType, + /// IssuedBy + ///FROM Event + ///WHERE EventId BETWEEN @EventIdMin AND @EventIdMax + ///AND StreamId = @StreamId + ///AND StreamType = @StreamType + ///ORDER BY EventId. + /// + internal static string ReadFailedEventStream { + get { + return ResourceManager.GetString("ReadFailedEventStream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to INSERT INTO EventStream + /// (Type, + /// Source, + /// Position, + /// RetryMax, + /// RetryDelays, + /// BlockSize) + /// VALUES + /// (@Type, + /// @Source, + /// @Position, + /// @RetryMax, + /// @RetryDelays, + /// @BlockSize). + /// + internal static string SubscribeToEventStream { + get { + return ResourceManager.GetString("SubscribeToEventStream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to UPDATE EventStream + ///SET Position = @Position + ///WHERE Type = @Type + ///AND Source = @Source. + /// + internal static string UpdateEventStreamPosition { + get { + return ResourceManager.GetString("UpdateEventStreamPosition", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to UPDATE FailedEventStream + ///SET StreamPosition = @StreamPosition, + /// EventId = @EventId, + /// EventType = @EventType, + /// ExceptionTime = @ExceptionTime, + /// ExceptionType = @ExceptionType, + /// ExceptionMessage = @ExceptionMessage, + /// ExceptionSource = @ExceptionSource, + /// ExceptionInfo = @ExceptionInfo, + /// BaseExceptionType = @BaseExceptionType, + /// BaseExceptionMessage = @BaseExceptionMessage, + /// RetryCount = @RetryCount, + /// RetryMax = @RetryMax, + /// R [rest of string was truncated]";. + /// + internal static string UpdateFailedEventStream { + get { + return ResourceManager.GetString("UpdateFailedEventStream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to UPDATE FailedEventStream + ///SET StreamPosition = @Position + ///WHERE StreamId = @Id + ///AND StreamType = @Type + ///AND StreamSource = @Source. + /// + internal static string UpdateFailedEventStreamPosition { + get { + return ResourceManager.GetString("UpdateFailedEventStreamPosition", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to UPDATE Command + ///SET Body = @Body, + /// BodyFormat = @BodyFormat, + /// RecurringExpression = @RecurringExpression + ///WHERE CommandId = @CommandId. + /// + internal static string UpdateRecurringCommand { + get { + return ResourceManager.GetString("UpdateRecurringCommand", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to UPDATE Command + ///SET LastExecutionTime = @LastExecutionTime, + /// LastExecutionStatus = @LastExecutionStatus, + /// LastExceptionInfo = @LastExceptionInfo + ///WHERE CommandId = @CommandId. + /// + internal static string UpdateRecurringCommandStatus { + get { + return ResourceManager.GetString("UpdateRecurringCommandStatus", resourceCulture); + } + } + } +} diff --git a/Src/DDD.Core.Dapper/SqlScripts.resx b/Src/DDD.Core.Dapper/SqlScripts.resx new file mode 100644 index 0000000..6eaad29 --- /dev/null +++ b/Src/DDD.Core.Dapper/SqlScripts.resx @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + scripts\excludefailedeventstream.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\findeventstreams.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\findfailedeventstreams.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\findrecurringcommandbytype.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\findrecurringcommands.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\includefailedeventstream.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\insertrecurringcommand.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\readeventstream.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\readfailedeventstream.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\subscribetoeventstream.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\updateeventstreamposition.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\updatefailedeventstream.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\updatefailedeventstreamposition.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\updaterecurringcommand.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\updaterecurringcommandstatus.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + \ No newline at end of file diff --git a/Src/DDD.Core.Dapper/SuccessfulRecurringCommandUpdater.cs b/Src/DDD.Core.Dapper/SuccessfulRecurringCommandUpdater.cs new file mode 100644 index 0000000..06ada29 --- /dev/null +++ b/Src/DDD.Core.Dapper/SuccessfulRecurringCommandUpdater.cs @@ -0,0 +1,125 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using System.Transactions; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + using Mapping; + using Threading; + + public class SuccessfulRecurringCommandUpdater : ICommandHandler + where TContext : BoundedContext + { + + #region Fields + + private readonly IDbConnectionProvider connectionProvider; + private readonly CompositeTranslator exceptionTranslator; + + #endregion Fields + + #region Constructors + + public SuccessfulRecurringCommandUpdater(IDbConnectionProvider connectionProvider) + { + Ensure.That(connectionProvider, nameof(connectionProvider)).IsNotNull(); + this.connectionProvider = connectionProvider; + this.exceptionTranslator = new CompositeTranslator(); + this.exceptionTranslator.Register(new DbToCommandExceptionTranslator()); + this.exceptionTranslator.RegisterFallback(); + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.connectionProvider.Context; + + #endregion Properties + + #region Methods + + public void Handle(MarkRecurringCommandAsSuccessful command, IMessageContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + try + { + using (var scope = new TransactionScope()) + { + var connection = this.connectionProvider.GetOpenConnection(); + var expressions = connection.Expressions(); + connection.Execute + ( + new CommandDefinition + ( + SqlScripts.UpdateRecurringCommandStatus.Replace("@", expressions.ParameterPrefix()), + ToParameters(command, connection) + ) + ); + scope.Complete(); + } + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Command = command }); + } + } + + public async Task HandleAsync(MarkRecurringCommandAsSuccessful command, IMessageContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + var cancellationToken = context.CancellationToken(); + var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); + var expressions = connection.Expressions(); + await connection.ExecuteAsync + ( + new CommandDefinition + ( + SqlScripts.UpdateRecurringCommandStatus.Replace("@", expressions.ParameterPrefix()), + ToParameters(command, connection), + cancellationToken: cancellationToken + ) + ); + scope.Complete(); + } + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Command = command }); + } + } + + /// Workaround for https://github.com/DapperLib/Dapper/issues/303 + private object ToParameters(MarkRecurringCommandAsSuccessful command, IDbConnection connection) + { + if (connection.HasOracleProvider()) + return new + { + LastExecutionTime = command.ExecutionTime, + LastExecutionStatus = "S", + LastExceptionInfo = (string)null, + CommandId = command.CommandId.ToByteArray() + }; + return new + { + LastExecutionTime = command.ExecutionTime, + LastExecutionStatus = "S", + LastExceptionInfo = (string)null, + CommandId = command.CommandId + }; + } + + #endregion Methods + } +} diff --git a/Src/DDD.Core.Dapper/packages.config b/Src/DDD.Core.Dapper/packages.config deleted file mode 100644 index 20a331e..0000000 --- a/Src/DDD.Core.Dapper/packages.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Src/DDD.Core.EF/App.config b/Src/DDD.Core.EF/App.config deleted file mode 100644 index dae6454..0000000 --- a/Src/DDD.Core.EF/App.config +++ /dev/null @@ -1,13 +0,0 @@ - - - - -
- - - - - - - - diff --git a/Src/DDD.Core.EF/DDD.Core.EF.csproj b/Src/DDD.Core.EF/DDD.Core.EF.csproj deleted file mode 100644 index 55bf08a..0000000 --- a/Src/DDD.Core.EF/DDD.Core.EF.csproj +++ /dev/null @@ -1,101 +0,0 @@ - - - - - Debug - AnyCPU - {6D227AA7-FF90-48CA-B13D-ED23C1FFFBA5} - Library - Properties - DDD.Core.Infrastructure.Data - DDD.Core.EF - v4.7.2 - 512 - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\DDD.Core.EF.xml - 1591 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\DDD.Core.EF.xml - 1591 - - - - L:\Packages\Conditions.2.1.0\lib\portable-net45+win+wpa81+wp80+MonoAndroid10+MonoTouch10\Conditions.dll - True - - - L:\Packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll - True - - - L:\Packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll - True - - - - - - - - - - Properties\CommonAssemblyInfo.cs - - - - - - - - - - - - - - - - - - - - - - - {596a8700-3d18-4a62-b200-1f78a9ea4617} - DDD.Core.Abstractions - - - {2438b31a-3a39-4878-81fa-be5ae715eae5} - DDD.Core.Messages - - - {c6c3e419-b9aa-44ad-9dbf-789294687ae6} - DDD.Core - - - - - \ No newline at end of file diff --git a/Src/DDD.Core.EF/DbContextExtensions.cs b/Src/DDD.Core.EF/DbContextExtensions.cs deleted file mode 100644 index 924d7a8..0000000 --- a/Src/DDD.Core.EF/DbContextExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data.Entity; -using System.Data.Entity.Core.Metadata.Edm; -using System.Data.Entity.Infrastructure; -using System.Linq; - -namespace DDD.Core.Infrastructure.Data -{ - public static class DbContextExtensions - { - #region Methods - - public static IEnumerable GetKeyNames(this DbContext context) - where TEntity : class - { - return context.GetKeyNames(typeof(TEntity)); - } - - public static IEnumerable GetKeyNames(this DbContext context, Type entityType) - { - var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace; - - // Get the mapping between CLR types and metadata OSpace - var objectItemCollection = ((ObjectItemCollection)metadata.GetItemCollection(DataSpace.OSpace)); - - // Get metadata for given CLR type - var entityMetadata = metadata.GetItems(DataSpace.OSpace) - .Single(e => objectItemCollection.GetClrType(e) == entityType); - - return entityMetadata.KeyProperties.Select(p => p.Name); - } - - #endregion Methods - } -} \ No newline at end of file diff --git a/Src/DDD.Core.EF/DbEntityValidationExceptionExtensions.cs b/Src/DDD.Core.EF/DbEntityValidationExceptionExtensions.cs deleted file mode 100644 index 4ed3341..0000000 --- a/Src/DDD.Core.EF/DbEntityValidationExceptionExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Data.Entity.Validation; -using System.Linq; -using Conditions; - -namespace DDD.Core.Infrastructure.Data -{ - using Collections; - - public static class DbEntityValidationExceptionExtensions - { - - #region Methods - - /// - /// Adds the validation errors associated with the entity in the Data property. - /// - /// The current exception. - public static void AddErrorsInData(this DbEntityValidationException exception) - { - Condition.Requires(exception, nameof(exception)).IsNotNull(); - exception.EntityValidationErrors - .SelectMany(e => e.ValidationErrors) - .ForEach((i, e) => exception.AddErrorInData(i, e)); - } - - private static void AddErrorInData(this DbEntityValidationException exception, int index, DbValidationError error) - { - var key = $"EntityValidationError{index}"; - if (!exception.Data.Contains(key)) - exception.Data[key] = $"Property : {error.PropertyName}, Error : {error.ErrorMessage}"; - } - - #endregion Methods - - } -} diff --git a/Src/DDD.Core.EF/DbModelBuilderExtensions.cs b/Src/DDD.Core.EF/DbModelBuilderExtensions.cs deleted file mode 100644 index 0d21e07..0000000 --- a/Src/DDD.Core.EF/DbModelBuilderExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Reflection; -using System.Data.Entity; -using System.Data.Entity.ModelConfiguration.Conventions; - -namespace DDD.Core.Infrastructure.Data -{ - public static class DbModelBuilderExtensions - { - - #region Methods - - /// - /// Add custom upper case conventions to ModelBuilder instance. Default is to use snake case too. - /// - /// Entity Framework DbModelBuilder instance. - /// Use snake case. - /// Returns a value indicating whether the specified property is excluded from the model. - public static void ApplyAllUpperCaseConventions(this DbModelBuilder modelBuilder, bool useSnakeCase = true, Func isIgnoredProperty = null) - { - - IConvention[] conventions = - { - new UpperCaseTableNameConvention(useSnakeCase), - new UpperCaseForeignKeyNameConvention(useSnakeCase), - new UpperCaseColumnNameConvention(useSnakeCase, isIgnoredProperty) - }; - modelBuilder.Conventions.Add(conventions); - } - - #endregion Methods - - } -} diff --git a/Src/DDD.Core.EF/EFRepository.cs b/Src/DDD.Core.EF/EFRepository.cs deleted file mode 100644 index bac6a7a..0000000 --- a/Src/DDD.Core.EF/EFRepository.cs +++ /dev/null @@ -1,160 +0,0 @@ -using Conditions; -using System; -using System.Collections.Generic; -using System.Data.Common; -using System.Data.Entity; -using System.Data.Entity.Infrastructure; -using System.Data.Entity.Validation; -using System.Linq; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; - -namespace DDD.Core.Infrastructure.Data -{ - using Domain; - using Mapping; - using Threading; - - public abstract class EFRepository - : IAsyncRepository - where TDomainEntity : DomainEntity, IStateObjectConvertible - where TStateEntity : class, IStateEntity, new() - where TContext : StateEntitiesContext - { - - #region Fields - - private readonly IAsyncDbContextFactory contextFactory; - - private readonly IObjectTranslator entityTranslator; - - private readonly IObjectTranslator eventTranslator; - - #endregion Fields - - #region Constructors - - protected EFRepository(IObjectTranslator entityTranslator, - IObjectTranslator eventTranslator, - IAsyncDbContextFactory contextFactory) - { - Condition.Requires(entityTranslator, nameof(entityTranslator)).IsNotNull(); - Condition.Requires(eventTranslator, nameof(eventTranslator)).IsNotNull(); - Condition.Requires(contextFactory, nameof(contextFactory)).IsNotNull(); - this.entityTranslator = entityTranslator; - this.eventTranslator = eventTranslator; - this.contextFactory = contextFactory; - } - - #endregion Constructors - - #region Methods - - public async Task FindAsync(ComparableValueObject identity) - { - Condition.Requires(identity, nameof(identity)) - .IsNotNull(); - await new SynchronizationContextRemover(); - var keyValues = identity.PrimitiveEqualityComponents(); - var stateEntity = await this.FindAsync(keyValues); - if (stateEntity == null) return null; - return this.entityTranslator.Translate(stateEntity); - } - - public async Task SaveAsync(TDomainEntity aggregate) - { - Condition.Requires(aggregate, nameof(aggregate)).IsNotNull(); - await new SynchronizationContextRemover(); - var stateEntity = aggregate.ToState(); - var events = ToEventStates(aggregate); - using (var context = await this.CreateContextAsync()) - { - context.Set().Add(stateEntity); - context.Set().AddRange(events); - await SaveChangesAsync(context); - } - } - - protected virtual async Task FindAsync(IEnumerable keyValues) - { - using (var context = await this.CreateContextAsync()) - { - var keyNames = context.GetKeyNames(); - if (keyValues.Count() != keyNames.Count()) - throw new InvalidOperationException($"You must specify {keyNames.Count()} identity components."); - var query = context.Set().AsQueryable(); - foreach (var path in this.RelatedEntitiesPaths()) - query = query.Include(path); - var expression = BuildFindExpression(keyNames, keyValues); - return await query.FirstOrDefaultAsync(expression); - } - } - - protected abstract IEnumerable>> RelatedEntitiesPaths(); - - private static Expression> BuildFindExpression(IEnumerable keyNames, - IEnumerable keyValues) - { - var entity = Expression.Parameter(typeof(TStateEntity), "entity"); - Expression find = null; - for (int i = 0; i < keyNames.Count(); i++) - { - var key = Expression.Property(entity, keyNames.ElementAt(i)); - var keyValue = Expression.Constant(keyValues.ElementAt(i)); - var equals = key.Type.GetMethod("Equals", new[] { key.Type }); - var keyEqualsKeyValue = Expression.Call(key, equals, keyValue); - if (find == null) - find = keyEqualsKeyValue; - else - find = Expression.AndAlso(find, keyEqualsKeyValue); - } - return Expression.Lambda>(find, entity); - } - - private static async Task SaveChangesAsync(TContext context) - { - try - { - await context.SaveChangesAsync(); - } - catch (DbUpdateConcurrencyException ex) - { - throw new RepositoryConcurrencyException(ex, typeof(TDomainEntity)); - } - catch (Exception ex) when (ex is DbUpdateException || ex is DbEntityValidationException) - { - throw new RepositoryException(ex, typeof(TDomainEntity)); - } - } - - private async Task CreateContextAsync() - { - try - { - return await this.contextFactory.CreateContextAsync(); - } - catch (DbException ex) - { - throw new RepositoryException(ex, typeof(TDomainEntity)); - } - } - - private IEnumerable ToEventStates(TDomainEntity aggregate) - { - var commitId = Guid.NewGuid(); - var subject = Thread.CurrentPrincipal?.Identity?.Name; - return aggregate.AllEvents().Select(e => - { - var evt = this.eventTranslator.Translate(e); - evt.StreamId = aggregate.IdentityAsString(); - evt.CommitId = commitId; - evt.Subject = subject; - return evt; - }); - } - - #endregion Methods - - } -} \ No newline at end of file diff --git a/Src/DDD.Core.EF/EventStateConfiguration.cs b/Src/DDD.Core.EF/EventStateConfiguration.cs deleted file mode 100644 index d392447..0000000 --- a/Src/DDD.Core.EF/EventStateConfiguration.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Data.Entity.ModelConfiguration; -using System.ComponentModel.DataAnnotations.Schema; - -namespace DDD.Core.Infrastructure.Data -{ - using Core.Domain; - - public abstract class EventStateConfiguration : EntityTypeConfiguration - { - - #region Fields - - private readonly bool useUpperCase; - - #endregion Fields - - #region Constructors - - protected EventStateConfiguration(bool useUpperCase) - { - this.useUpperCase = useUpperCase; - // Table - this.ToTable(ToCasingConvention("Event")); - // Keys - this.HasKey(e => e.Id); - // Fields - this.Property(e => e.Id) - .HasColumnName(ToCasingConvention("EventId")) - .HasColumnOrder(1) - .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None); - this.Property(e => e.EventType) - .HasColumnOrder(2) - .IsUnicode(false) - .HasMaxLength(50) - .IsRequired(); - this.Property(e => e.StreamId) - .HasColumnOrder(3) - .IsUnicode(false) - .HasMaxLength(50) - .IsRequired(); - this.Property(e => e.CommitId) - .HasColumnOrder(4); - this.Property(e => e.OccurredOn) - .HasColumnOrder(5) - .HasPrecision(2); - this.Property(e => e.Subject) - .HasColumnOrder(6) - .IsUnicode(false) - .HasMaxLength(100); - this.Property(e => e.Body) - .HasColumnOrder(7) - .IsRequired(); - this.Property(e => e.Dispatched) - .HasColumnOrder(8); - } - - #endregion Constructors - - #region Methods - - protected string ToCasingConvention(string name) => this.useUpperCase ? name.ToUpperInvariant() : name; - - #endregion Methods - - } -} \ No newline at end of file diff --git a/Src/DDD.Core.EF/IAsyncDbContextFactory.cs b/Src/DDD.Core.EF/IAsyncDbContextFactory.cs deleted file mode 100644 index 644d353..0000000 --- a/Src/DDD.Core.EF/IAsyncDbContextFactory.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Data.Entity; -using System.Threading.Tasks; - -namespace DDD.Core.Infrastructure.Data -{ - /// - /// Defines a method that creates a of a specified type. - /// - public interface IAsyncDbContextFactory - where TContext : DbContext - { - - #region Methods - - Task CreateContextAsync(); - - #endregion Methods - - } -} diff --git a/Src/DDD.Core.EF/OracleEventStateConfiguration.cs b/Src/DDD.Core.EF/OracleEventStateConfiguration.cs deleted file mode 100644 index a7998db..0000000 --- a/Src/DDD.Core.EF/OracleEventStateConfiguration.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace DDD.Core.Infrastructure.Data -{ - public class OracleEventStateConfiguration : EventStateConfiguration - { - - #region Constructors - - public OracleEventStateConfiguration(bool useUpperCase) : base(useUpperCase) - { - // Fields - this.Property(e => e.OccurredOn) - .HasColumnType("timestamp"); - this.Property(e => e.Body) - .HasColumnType("xmltype"); - } - - #endregion Constructors - - } -} \ No newline at end of file diff --git a/Src/DDD.Core.EF/Properties/AssemblyInfo.cs b/Src/DDD.Core.EF/Properties/AssemblyInfo.cs deleted file mode 100644 index b2ac3c9..0000000 --- a/Src/DDD.Core.EF/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("DDD.Core.EF")] -[assembly: AssemblyDescription("Implementation of infrastucture components based on Entity Framework.")] -[assembly: AssemblyProduct("DDD.Core.EF")] -[assembly: Guid("6d227aa7-ff90-48ca-b13d-ed23c1fffba5")] \ No newline at end of file diff --git a/Src/DDD.Core.EF/SqlServerEventStateConfiguration.cs b/Src/DDD.Core.EF/SqlServerEventStateConfiguration.cs deleted file mode 100644 index ccadf39..0000000 --- a/Src/DDD.Core.EF/SqlServerEventStateConfiguration.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace DDD.Core.Infrastructure.Data -{ - public class SqlServerEventStateConfiguration : EventStateConfiguration - { - - #region Constructors - - public SqlServerEventStateConfiguration(bool useUpperCase) : base(useUpperCase) - { - // Fields - this.Property(e => e.OccurredOn) - .HasColumnType("datetime2"); - this.Property(e => e.Body) - .HasColumnType("xml"); - } - - #endregion Constructors - - } -} \ No newline at end of file diff --git a/Src/DDD.Core.EF/StateEntitiesContext.cs b/Src/DDD.Core.EF/StateEntitiesContext.cs deleted file mode 100644 index 023dc94..0000000 --- a/Src/DDD.Core.EF/StateEntitiesContext.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Data.Entity; -using System.Data.Common; -using System.Threading.Tasks; -using System.Threading; -using System.Data.Entity.Validation; - -namespace DDD.Core.Infrastructure.Data -{ - public abstract class StateEntitiesContext : DbContext - { - - #region Constructors - - static StateEntitiesContext() - { - Database.SetInitializer(null); - } - - protected StateEntitiesContext(DbConnection connection, bool contextOwnsConnection) - : base(connection, contextOwnsConnection) - { - this.Configuration.LazyLoadingEnabled = false; - } - - #endregion Constructors - - #region Methods - - public void FixEntityState() - { - foreach (var entry in ChangeTracker.Entries()) - { - Domain.IStateEntity entity = entry.Entity; - switch (entity.EntityState) - { - case Domain.EntityState.Added: - entry.State = EntityState.Added; - break; - - case Domain.EntityState.Modified: - entry.State = EntityState.Modified; - break; - - case Domain.EntityState.Deleted: - entry.State = EntityState.Deleted; - break; - - default: - entry.State = EntityState.Unchanged; - break; - } - } - } - - public override int SaveChanges() - { - this.FixEntityState(); - this.SetGeneratedValues(); - try - { - return base.SaveChanges(); - } - catch(DbEntityValidationException ex) - { - ex.AddErrorsInData(); - throw; - } - } - - public override Task SaveChangesAsync() - { - this.FixEntityState(); - this.SetGeneratedValues(); - try - { - return base.SaveChangesAsync(); - } - catch (DbEntityValidationException ex) - { - ex.AddErrorsInData(); - throw; - } - } - - public override Task SaveChangesAsync(CancellationToken cancellationToken) - { - this.FixEntityState(); - this.SetGeneratedValues(); - try - { - return base.SaveChangesAsync(cancellationToken); - } - catch (DbEntityValidationException ex) - { - ex.AddErrorsInData(); - throw; - } - } - - protected override void OnModelCreating(DbModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - modelBuilder.Conventions.Add(new StateEntityConvention()); - } - - protected virtual void SetGeneratedValues() - { - } - - #endregion Methods - - } -} \ No newline at end of file diff --git a/Src/DDD.Core.EF/StateEntityConvention.cs b/Src/DDD.Core.EF/StateEntityConvention.cs deleted file mode 100644 index e0de138..0000000 --- a/Src/DDD.Core.EF/StateEntityConvention.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Data.Entity.ModelConfiguration.Conventions; -using System.Reflection; - -namespace DDD.Core.Infrastructure.Data -{ - using Domain; - - public class StateEntityConvention : Convention - { - #region Constructors - - public StateEntityConvention() - { - this.Types().Where(t => Condition(t)).Configure(c => c.Ignore(e => e.EntityState)); - } - - #endregion Constructors - - #region Methods - - private static bool Condition(Type t) - { - return t.GetProperty(nameof(IStateEntity.EntityState), BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance) != null; - } - - #endregion Methods - } -} \ No newline at end of file diff --git a/Src/DDD.Core.EF/UpperCaseColumnNameConvention.cs b/Src/DDD.Core.EF/UpperCaseColumnNameConvention.cs deleted file mode 100644 index 586ef04..0000000 --- a/Src/DDD.Core.EF/UpperCaseColumnNameConvention.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Reflection; -using System.Data.Entity.ModelConfiguration.Configuration; -using System.Data.Entity.ModelConfiguration.Conventions; - -namespace DDD.Core.Infrastructure.Data -{ - /// - /// Make all column names upper case. Default is to use snake case too. - /// - public class UpperCaseColumnNameConvention : Convention - { - - #region Fields - - private readonly bool useSnakeCase = true; - private readonly Func isIgnoredProperty = p => false; - - #endregion Fields - - #region Constructors - - public UpperCaseColumnNameConvention(bool useSnakeCase = true, Func isIgnoredProperty = null) - { - this.useSnakeCase = useSnakeCase; - if (isIgnoredProperty != null) - this.isIgnoredProperty = isIgnoredProperty; - this.Properties() - .Where(p => !this.isIgnoredProperty(p)) - .Configure(c => c.HasColumnName(GetColumnName(c))); - } - - #endregion Constructors - - #region Methods - - private string GetColumnName(ConventionPrimitivePropertyConfiguration type) - { - var columnName = type.ClrPropertyInfo.Name; - if (useSnakeCase) - columnName = columnName.ToSnakeCase(); - return columnName.ToUpperInvariant(); - } - - #endregion Methods - - } -} diff --git a/Src/DDD.Core.EF/UpperCaseForeignKeyNameConvention.cs b/Src/DDD.Core.EF/UpperCaseForeignKeyNameConvention.cs deleted file mode 100644 index aee4a0f..0000000 --- a/Src/DDD.Core.EF/UpperCaseForeignKeyNameConvention.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Collections.Generic; -using System.Data.Entity.Core.Metadata.Edm; -using System.Data.Entity.Infrastructure; -using System.Data.Entity.ModelConfiguration.Conventions; -using Conditions; - -namespace DDD.Core.Infrastructure.Data -{ - /// - /// Make all foreign key names upper case. Default is to use snake case too. - /// - public class UpperCaseForeignKeyNameConvention : IStoreModelConvention - { - #region Fields - - private readonly bool useSnakeCase = true; - - #endregion Fields - - #region Constructors - - public UpperCaseForeignKeyNameConvention(bool useSnakeCase = true) - { - this.useSnakeCase = useSnakeCase; - } - - #endregion Constructors - - #region Methods - - public void Apply(AssociationType association, DbModel model) - { - Condition.Requires(association, nameof(association)).IsNotNull(); - Condition.Requires(model, nameof(model)).IsNotNull(); - if (association.IsForeignKey) - UpperCaseForeignKeyProperties(association.Constraint.ToProperties); - } - - private void UpperCaseForeignKeyProperties(IEnumerable properties) - { - foreach (var property in properties) - { - var foreignKeyName = property.Name; - if (useSnakeCase) - foreignKeyName = foreignKeyName.ToSnakeCase(); - property.Name = foreignKeyName.ToUpperInvariant(); - } - } - - #endregion Methods - } -} diff --git a/Src/DDD.Core.EF/UpperCaseTableNameConvention.cs b/Src/DDD.Core.EF/UpperCaseTableNameConvention.cs deleted file mode 100644 index 89dde3e..0000000 --- a/Src/DDD.Core.EF/UpperCaseTableNameConvention.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Data.Entity.Core.Metadata.Edm; -using System.Data.Entity.ModelConfiguration.Conventions; -using System.Data.Entity.Infrastructure; -using Conditions; - -namespace DDD.Core.Infrastructure.Data -{ - /// - /// Make all table names upper case. Default is to use snake case too. - /// - public class UpperCaseTableNameConvention : IStoreModelConvention - { - - #region Fields - - private readonly bool useSnakeCase = true; - - #endregion Fields - - #region Constructors - - public UpperCaseTableNameConvention(bool useSnakeCase = true) - { - this.useSnakeCase = useSnakeCase; - } - - #endregion Constructors - - #region Methods - - public void Apply(EntitySet item, DbModel model) - { - Condition.Requires(item, nameof(item)).IsNotNull(); - Condition.Requires(model, nameof(model)).IsNotNull(); - var tableName = item.Table; - if (useSnakeCase) - tableName = tableName.ToSnakeCase(); - item.Table = tableName.ToUpperInvariant(); - } - - #endregion Methods - - } -} diff --git a/Src/DDD.Core.EF/packages.config b/Src/DDD.Core.EF/packages.config deleted file mode 100644 index 96a58bb..0000000 --- a/Src/DDD.Core.EF/packages.config +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Src/DDD.Core.FluentValidation/DDD.Core.FluentValidation.csproj b/Src/DDD.Core.FluentValidation/DDD.Core.FluentValidation.csproj index 4e7f872..f25a4af 100644 --- a/Src/DDD.Core.FluentValidation/DDD.Core.FluentValidation.csproj +++ b/Src/DDD.Core.FluentValidation/DDD.Core.FluentValidation.csproj @@ -1,106 +1,35 @@ - - - + - Debug - AnyCPU - {5E3745FC-CA80-4D0F-8A25-20EE0F9CF163} + net48;netstandard2.1 Library - Properties DDD.Core.Infrastructure.Validation - DDD.Core.FluentValidation - v4.7.2 - 512 - + false - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 bin\Debug\DDD.Core.FluentValidation.xml 1591 - pdbonly - true - bin\Release\ - TRACE - prompt - 4 bin\Release\DDD.Core.FluentValidation.xml 1591 - - - L:\Packages\Conditions.2.1.0\lib\portable-net45+win+wpa81+wp80+MonoAndroid10+MonoTouch10\Conditions.dll - True - - - L:\Packages\FluentValidation.8.1.3\lib\net45\FluentValidation.dll - - - - - L:\packages\System.ComponentModel.Primitives.4.3.0\lib\net45\System.ComponentModel.Primitives.dll - True - - - - L:\packages\System.ValueTuple.4.5.0\lib\netstandard1.0\System.ValueTuple.dll - True - - - - - - - - Properties\CommonAssemblyInfo.cs - - - - - - - - - - - - - - + + + - - {596a8700-3d18-4a62-b200-1f78a9ea4617} - DDD.Core.Abstractions - - - {2438B31A-3A39-4878-81FA-BE5AE715EAE5} - DDD.Core.Messages - - - {c6c3e419-b9aa-44ad-9dbf-789294687ae6} - DDD.Core - + + + 11.4.0 + + + + - - \ No newline at end of file diff --git a/Src/DDD.Core.FluentValidation/FluentCommandValidatorAdapter.cs b/Src/DDD.Core.FluentValidation/FluentCommandValidatorAdapter.cs deleted file mode 100644 index ac89be7..0000000 --- a/Src/DDD.Core.FluentValidation/FluentCommandValidatorAdapter.cs +++ /dev/null @@ -1,20 +0,0 @@ -using FluentValidation; - -namespace DDD.Core.Infrastructure.Validation -{ - using Application; - - public class FluentCommandValidatorAdapter : FluentValidatorAdapter, ICommandValidator - where TCommand : class, ICommand - { - - #region Constructors - - public FluentCommandValidatorAdapter(IValidator fluentValidator) : base(fluentValidator) - { - } - - #endregion Constructors - - } -} diff --git a/Src/DDD.Core.FluentValidation/FluentQueryValidatorAdapter.cs b/Src/DDD.Core.FluentValidation/FluentQueryValidatorAdapter.cs deleted file mode 100644 index e3885f8..0000000 --- a/Src/DDD.Core.FluentValidation/FluentQueryValidatorAdapter.cs +++ /dev/null @@ -1,18 +0,0 @@ -using FluentValidation; - -namespace DDD.Core.Infrastructure.Validation -{ - using Application; - - public class FluentQueryValidatorAdapter : FluentValidatorAdapter, IQueryValidator - where TQuery : class, IQuery - { - #region Constructors - - public FluentQueryValidatorAdapter(IValidator fluentValidator) : base(fluentValidator) - { - } - - #endregion Constructors - } -} diff --git a/Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs b/Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs index 0b262e5..bf87f77 100644 --- a/Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs +++ b/Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs @@ -1,55 +1,71 @@ -using FluentValidation; -using FluentValidation.Results; -using Conditions; -using System.Threading.Tasks; +using System.Threading.Tasks; +using EnsureThat; +using FluentValidation; namespace DDD.Core.Infrastructure.Validation { using Mapping; using Threading; + using DDD.Validation; - public class FluentValidatorAdapter - : DDD.Validation.IObjectValidator, DDD.Validation.IAsyncObjectValidator + public class FluentValidatorAdapter : IObjectValidator where T : class { #region Fields - private readonly IObjectTranslator resultTranslator; private readonly IValidator fluentValidator; + private readonly IObjectTranslator resultTranslator; #endregion Fields + #region Constructors + public FluentValidatorAdapter(IValidator fluentValidator) { - Condition.Requires(fluentValidator, nameof(fluentValidator)).IsNotNull(); + Ensure.That(fluentValidator, nameof(fluentValidator)).IsNotNull(); this.fluentValidator = fluentValidator; this.resultTranslator = new ValidationResultTranslator(); } + #endregion Constructors + #region Methods /// - /// Validates the specified object. + /// Validates synchronously the specified object. /// /// The object to validate. - /// The rule set. - public DDD.Validation.ValidationResult Validate(T obj, string ruleSet = null) + /// The validation context. + public ValidationResult Validate(T obj, IValidationContext context) { - var result = this.fluentValidator.Validate(obj, ruleSet: ruleSet); - return this.resultTranslator.Translate(result); + Ensure.That(context, nameof(context)).IsNotNull(); + FluentValidation.Results.ValidationResult result; + var ruleSets = context.RuleSets(); + if (ruleSets == null) + result = this.fluentValidator.Validate(obj); + else + result = this.fluentValidator.Validate(obj, c => c.IncludeRuleSets(ruleSets)); + return this.resultTranslator.Translate(result, new { ObjectName = obj.GetType().Name }); } /// /// Validates asynchronously the specified object. /// /// The object to validate. - /// The rule set. - public async Task ValidateAsync(T obj, string ruleSet = null) + /// The validation context. + public async Task ValidateAsync(T obj, IValidationContext context) { + Ensure.That(context, nameof(context)).IsNotNull(); await new SynchronizationContextRemover(); - var result = await this.fluentValidator.ValidateAsync(obj, ruleSet: ruleSet); - return this.resultTranslator.Translate(result); + FluentValidation.Results.ValidationResult result; + var ruleSets = context.RuleSets(); + var cancellationToken = context.CancellationToken(); + if (ruleSets == null) + result = await this.fluentValidator.ValidateAsync(obj, cancellationToken); + else + result = await this.fluentValidator.ValidateAsync(obj, c => c.IncludeRuleSets(ruleSets), cancellationToken); + return this.resultTranslator.Translate(result, new { ObjectName = obj.GetType().Name }); } #endregion Methods diff --git a/Src/DDD.Core.FluentValidation/FluentValidatorExtensions.cs b/Src/DDD.Core.FluentValidation/FluentValidatorExtensions.cs index 5d5fa79..bfd7b7e 100644 --- a/Src/DDD.Core.FluentValidation/FluentValidatorExtensions.cs +++ b/Src/DDD.Core.FluentValidation/FluentValidatorExtensions.cs @@ -1,4 +1,5 @@ -using FluentValidation; +using EnsureThat; +using FluentValidation; using System.Collections; namespace DDD.Core.Infrastructure.Validation @@ -15,7 +16,7 @@ public static class FluentValidatorExtensions /// public static IRuleBuilderOptions Alphabetic(this IRuleBuilder ruleBuilder) { - return ruleBuilder.SetValidator(new AlphabeticValidator()); + return ruleBuilder.SetValidator(new AlphabeticValidator()); } /// @@ -24,7 +25,7 @@ public static IRuleBuilderOptions Alphabetic(this IRuleBuilder public static IRuleBuilderOptions Alphanumeric(this IRuleBuilder ruleBuilder) { - return ruleBuilder.SetValidator(new AlphanumericValidator()); + return ruleBuilder.SetValidator(new AlphanumericValidator()); } /// @@ -33,7 +34,7 @@ public static IRuleBuilderOptions Alphanumeric(this IRuleBuilder public static IRuleBuilderOptions Count(this IRuleBuilder ruleBuilder, int min, int max) { - return ruleBuilder.SetValidator(new CountValidator(min, max)); + return ruleBuilder.SetValidator(new CountValidator(min, max)); } /// @@ -42,7 +43,7 @@ public static IRuleBuilderOptions Count(this IRuleBuilder public static IRuleBuilderOptions Count(this IRuleBuilder ruleBuilder, int count) { - return ruleBuilder.SetValidator(new ExactCountValidator(count)); + return ruleBuilder.SetValidator(new ExactCountValidator(count)); } /// @@ -51,7 +52,7 @@ public static IRuleBuilderOptions Count(this IRuleBuilder public static IRuleBuilderOptions MaximumCount(this IRuleBuilder ruleBuilder, int max) { - return ruleBuilder.SetValidator(new MaximumCountValidator(max)); + return ruleBuilder.SetValidator(new MaximumCountValidator(max)); } /// @@ -60,7 +61,7 @@ public static IRuleBuilderOptions MaximumCount(this IRuleBuil /// public static IRuleBuilderOptions MinimumCount(this IRuleBuilder ruleBuilder, int min) { - return ruleBuilder.SetValidator(new MinimumCountValidator(min)); + return ruleBuilder.SetValidator(new MinimumCountValidator(min)); } /// @@ -69,10 +70,20 @@ public static IRuleBuilderOptions MinimumCount(this IRuleBuil /// public static IRuleBuilderOptions Numeric(this IRuleBuilder ruleBuilder) { - return ruleBuilder.SetValidator(new NumericValidator()); + return ruleBuilder.SetValidator(new NumericValidator()); + } + + /// + /// Specifies a custom category associated with the validation failure when validation fails for this rule. + /// + public static IRuleBuilderOptions WithCategory(this IRuleBuilderOptions rule, string category) + { + Ensure.That(category, nameof(category)).IsNotNullOrWhiteSpace(); + return rule.WithState(x => $"category={category}"); } #endregion Methods } + } diff --git a/Src/DDD.Core.FluentValidation/Properties/AssemblyInfo.cs b/Src/DDD.Core.FluentValidation/Properties/AssemblyInfo.cs index 4e834c3..1dd606a 100644 --- a/Src/DDD.Core.FluentValidation/Properties/AssemblyInfo.cs +++ b/Src/DDD.Core.FluentValidation/Properties/AssemblyInfo.cs @@ -2,6 +2,6 @@ using System.Runtime.InteropServices; [assembly: AssemblyTitle("DDD.Core.FluentValidation")] -[assembly: AssemblyDescription("Adapters for the FluentValidation library.")] +[assembly: AssemblyDescription("Core components for validation with FluentValidation.")] [assembly: AssemblyProduct("DDD.Core.FluentValidation")] [assembly: Guid("5e3745fc-ca80-4d0f-8a25-20ee0f9cf163")] \ No newline at end of file diff --git a/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs b/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs index b77aff2..a217f16 100644 --- a/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs +++ b/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs @@ -1,42 +1,53 @@ using FluentValidation.Results; using FluentValidation; -using Conditions; +using EnsureThat; using System.Linq; -using System.Collections.Generic; namespace DDD.Core.Infrastructure.Validation { using Mapping; internal class ValidationResultTranslator - : IObjectTranslator + : ObjectTranslator { #region Methods - public DDD.Validation.ValidationResult Translate(ValidationResult result, - IDictionary options) + public override DDD.Validation.ValidationResult Translate(ValidationResult result, IMappingContext context) { - Condition.Requires(result, nameof(result)).IsNotNull(); + Ensure.That(result, nameof(result)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + Ensure.Collection.ContainsKey(context, "ObjectName", nameof(context)); + var objectName = (string)context["ObjectName"]; var isSuccessful = result.Errors.All(f => f.Severity == Severity.Info); var failures = result.Errors.Select(f => ToFailure(f)).ToArray(); - return new DDD.Validation.ValidationResult(isSuccessful, failures); + return new DDD.Validation.ValidationResult(isSuccessful, objectName, failures); + } + + private static string GetCustomStateInfo(object customState, string infoType) + { + if (customState == null) return null; + var state = customState.ToString(); + var parts = state.Split('='); + if (parts.Length == 2 && parts[0] == infoType) + return parts[1]; + return null; } private static DDD.Validation.ValidationFailure ToFailure(ValidationFailure failure) { - Condition.Requires(failure, nameof(failure)).IsNotNull(); + Ensure.That(failure, nameof(failure)).IsNotNull(); return new DDD.Validation.ValidationFailure ( failure.ErrorMessage, failure.ErrorCode, failure.Severity.ToString().ToEnum(), failure.PropertyName, - failure.AttemptedValue + failure.AttemptedValue, + GetCustomStateInfo(failure.CustomState, "category") ); } #endregion Methods - } } diff --git a/Src/DDD.Core.FluentValidation/Validators/AlphaNumericValidator.cs b/Src/DDD.Core.FluentValidation/Validators/AlphaNumericValidator.cs index ff23ea1..4c83195 100644 --- a/Src/DDD.Core.FluentValidation/Validators/AlphaNumericValidator.cs +++ b/Src/DDD.Core.FluentValidation/Validators/AlphaNumericValidator.cs @@ -1,30 +1,27 @@ -using FluentValidation.Validators; +using FluentValidation; +using FluentValidation.Validators; namespace DDD.Core.Infrastructure.Validation.Validators { - using Core; - - internal class AlphanumericValidator : PropertyValidator + internal class AlphanumericValidator : PropertyValidator { - #region Constructors + #region Properties - public AlphanumericValidator() : base("'{PropertyName}' should be alphanumeric.") - { - } + public override string Name => "AlphanumericValidator"; - #endregion Constructors + #endregion Properties #region Methods - protected override bool IsValid(PropertyValidatorContext context) + public override bool IsValid(ValidationContext context, string value) { - if (context.PropertyValue == null) return true; - var value = context.PropertyValue as string; if (value == null) return true; return value.IsAlphanumeric(); } + protected override string GetDefaultMessageTemplate(string errorCode) => "'{PropertyName}' should be alphanumeric."; + #endregion Methods } diff --git a/Src/DDD.Core.FluentValidation/Validators/AlphabeticValidator.cs b/Src/DDD.Core.FluentValidation/Validators/AlphabeticValidator.cs index dbdff0a..ae89c29 100644 --- a/Src/DDD.Core.FluentValidation/Validators/AlphabeticValidator.cs +++ b/Src/DDD.Core.FluentValidation/Validators/AlphabeticValidator.cs @@ -1,30 +1,27 @@ -using FluentValidation.Validators; +using FluentValidation; +using FluentValidation.Validators; namespace DDD.Core.Infrastructure.Validation.Validators { - using Core; - - internal class AlphabeticValidator : PropertyValidator + internal class AlphabeticValidator : PropertyValidator { - #region Constructors + #region Properties - public AlphabeticValidator() : base("'{PropertyName}' should be alphabetic.") - { - } + public override string Name => "AlphabeticValidator"; - #endregion Constructors + #endregion Properties #region Methods - protected override bool IsValid(PropertyValidatorContext context) + public override bool IsValid(ValidationContext context, string value) { - if (context.PropertyValue == null) return true; - var value = context.PropertyValue as string; if (value == null) return true; return value.IsAlphabetic(); } + protected override string GetDefaultMessageTemplate(string errorCode) => "'{PropertyName}' should be alphabetic."; + #endregion Methods } diff --git a/Src/DDD.Core.FluentValidation/Validators/CountValidator.cs b/Src/DDD.Core.FluentValidation/Validators/CountValidator.cs index 1e6793f..b18dc98 100644 --- a/Src/DDD.Core.FluentValidation/Validators/CountValidator.cs +++ b/Src/DDD.Core.FluentValidation/Validators/CountValidator.cs @@ -1,36 +1,41 @@ using System.Linq; using System.Collections; +using FluentValidation; using FluentValidation.Validators; -using Conditions; +using EnsureThat; namespace DDD.Core.Infrastructure.Validation.Validators { - internal class CountValidator : PropertyValidator + internal class CountValidator : PropertyValidator { - public int Min { get; } - public int? Max { get; } + #region Constructors - public CountValidator(int min, int? max, string errorMessage) : base(errorMessage) + public CountValidator(int min, int? max) { - Condition.Requires(min, nameof(min)).IsGreaterOrEqual(0); + Ensure.That(min, nameof(min)).IsGte(0); if (max.HasValue) - Condition.Requires(max, nameof(max)).IsGreaterOrEqual(min); + Ensure.That(max.Value, nameof(max)).IsGte(min); this.Min = min; this.Max = max; } - public CountValidator(int min, int? max) - : this(min, max, "'{PropertyName}' must contain between {Min} and {Max} items. '{PropertyName}' has {Count} items.") - { - } + #endregion Constructors - protected override bool IsValid(PropertyValidatorContext context) + #region Properties + + public int? Max { get; } + public int Min { get; } + public override string Name => "CountValidator"; + + #endregion Properties + + #region Methods + + public override bool IsValid(ValidationContext context, IEnumerable value) { - if (context.PropertyValue == null) return true; - var collection = context.PropertyValue as IEnumerable; - if (collection == null) return true; - var count = collection.Cast().Count(); + if (value == null) return true; + var count = value.Cast().Count(); if (count < this.Min || (this.Max.HasValue && count > this.Max)) { context.MessageFormatter.AppendArgument("Count", count); @@ -41,5 +46,11 @@ protected override bool IsValid(PropertyValidatorContext context) } return true; } + + protected override string GetDefaultMessageTemplate(string errorCode) + => "'{PropertyName}' must contain between {Min} and {Max} items. '{PropertyName}' has {Count} items."; + + #endregion Methods + } } diff --git a/Src/DDD.Core.FluentValidation/Validators/ExactCountValidator.cs b/Src/DDD.Core.FluentValidation/Validators/ExactCountValidator.cs index d37e9d0..f4ac2e0 100644 --- a/Src/DDD.Core.FluentValidation/Validators/ExactCountValidator.cs +++ b/Src/DDD.Core.FluentValidation/Validators/ExactCountValidator.cs @@ -1,10 +1,27 @@ namespace DDD.Core.Infrastructure.Validation.Validators { - internal class ExactCountValidator : CountValidator + internal class ExactCountValidator : CountValidator { - public ExactCountValidator(int count) : - base(count, count, "'{PropertyName}' must contain {Max} item(s). '{PropertyName}' has {Count} item(s).") + + #region Constructors + + public ExactCountValidator(int count) : base(count, count) { } + + #endregion Constructors + + #region Properties + + public override string Name => "ExactCountValidator"; + + #endregion Properties + + #region Methods + + protected override string GetDefaultMessageTemplate(string errorCode) + => "'{PropertyName}' must contain {Max} item(s). '{PropertyName}' has {Count} item(s)."; + + #endregion Methods } } diff --git a/Src/DDD.Core.FluentValidation/Validators/MaximumCountValidator.cs b/Src/DDD.Core.FluentValidation/Validators/MaximumCountValidator.cs index 7cb98ef..6699e32 100644 --- a/Src/DDD.Core.FluentValidation/Validators/MaximumCountValidator.cs +++ b/Src/DDD.Core.FluentValidation/Validators/MaximumCountValidator.cs @@ -1,10 +1,27 @@ namespace DDD.Core.Infrastructure.Validation.Validators { - internal class MaximumCountValidator : CountValidator + internal class MaximumCountValidator : CountValidator { - public MaximumCountValidator(int max) : - base(0, max, "'{PropertyName}' must contain no more than {Max} item(s). '{PropertyName}' has {Count} item(s).") + + #region Constructors + + public MaximumCountValidator(int max) : base(0, max) { } + + #endregion Constructors + + #region Properties + + public override string Name => "MaximumCountValidator"; + + #endregion Properties + + #region Methods + + protected override string GetDefaultMessageTemplate(string errorCode) + => "'{PropertyName}' must contain no more than {Max} item(s). '{PropertyName}' has {Count} item(s)."; + + #endregion Methods } } diff --git a/Src/DDD.Core.FluentValidation/Validators/MinimumCountValidator.cs b/Src/DDD.Core.FluentValidation/Validators/MinimumCountValidator.cs index 4d9df6e..e24be64 100644 --- a/Src/DDD.Core.FluentValidation/Validators/MinimumCountValidator.cs +++ b/Src/DDD.Core.FluentValidation/Validators/MinimumCountValidator.cs @@ -1,10 +1,27 @@ namespace DDD.Core.Infrastructure.Validation.Validators { - internal class MinimumCountValidator : CountValidator + internal class MinimumCountValidator : CountValidator { - public MinimumCountValidator(int min) : - base(min, null, "'{PropertyName}' must contain at least {Min} item(s). '{PropertyName}' has {Count} item(s).") + + #region Constructors + + public MinimumCountValidator(int min) : base(min, null) { } + + #endregion Constructors + + #region Properties + + public override string Name => "MinimumCountValidator"; + + #endregion Properties + + #region Methods + + protected override string GetDefaultMessageTemplate(string errorCode) + => "'{PropertyName}' must contain at least {Min} item(s). '{PropertyName}' has {Count} item(s)."; + + #endregion Methods } } diff --git a/Src/DDD.Core.FluentValidation/Validators/NumericValidator.cs b/Src/DDD.Core.FluentValidation/Validators/NumericValidator.cs index 95e3f95..188b15e 100644 --- a/Src/DDD.Core.FluentValidation/Validators/NumericValidator.cs +++ b/Src/DDD.Core.FluentValidation/Validators/NumericValidator.cs @@ -1,30 +1,28 @@ -using FluentValidation.Validators; +using FluentValidation; +using FluentValidation.Validators; namespace DDD.Core.Infrastructure.Validation.Validators { - using Core; - - internal class NumericValidator : PropertyValidator + internal class NumericValidator : PropertyValidator { - #region Constructors + #region Properties - public NumericValidator() : base("'{PropertyName}' should be numeric.") - { - } + public override string Name => "NumericValidator"; - #endregion Constructors + #endregion Properties #region Methods - protected override bool IsValid(PropertyValidatorContext context) + public override bool IsValid(ValidationContext context, string value) { - if (context.PropertyValue == null) return true; - var value = context.PropertyValue as string; if (value == null) return true; return value.IsNumeric(); } + protected override string GetDefaultMessageTemplate(string errorCode) + => "'{PropertyName}' should be numeric."; + #endregion Methods } diff --git a/Src/DDD.Core.FluentValidation/packages.config b/Src/DDD.Core.FluentValidation/packages.config deleted file mode 100644 index 5b04f10..0000000 --- a/Src/DDD.Core.FluentValidation/packages.config +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/Src/DDD.Core.Messages/Application/CommandExecutionStatus.cs b/Src/DDD.Core.Messages/Application/CommandExecutionStatus.cs new file mode 100644 index 0000000..7132610 --- /dev/null +++ b/Src/DDD.Core.Messages/Application/CommandExecutionStatus.cs @@ -0,0 +1,8 @@ +namespace DDD.Core.Application +{ + public enum CommandExecutionStatus + { + Successful, + Failed + } +} diff --git a/Src/DDD.Core.Messages/Application/Event.cs b/Src/DDD.Core.Messages/Application/Event.cs new file mode 100644 index 0000000..bb83809 --- /dev/null +++ b/Src/DDD.Core.Messages/Application/Event.cs @@ -0,0 +1,36 @@ +using System; + +namespace DDD.Core.Application +{ + public class Event + { + + #region Properties + + public Guid EventId { get; set; } + + public string EventType { get; set; } + + public DateTime OccurredOn { get; set; } + + public string Body { get; set; } + + public string BodyFormat { get; set; } + + public string StreamId { get; set; } + + public string StreamType { get; set; } + + public string IssuedBy { get; set; } + + #endregion Properties + + #region Methods + + public override string ToString() + => $"{this.GetType().Name} [ {nameof(EventId)}={this.EventId}, {nameof(EventType)}={this.EventType}, {nameof(StreamId)}={this.StreamId}, {nameof(StreamType)}={this.StreamType}]"; + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Messages/Application/EventStream.cs b/Src/DDD.Core.Messages/Application/EventStream.cs new file mode 100644 index 0000000..e35c4c5 --- /dev/null +++ b/Src/DDD.Core.Messages/Application/EventStream.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; + +namespace DDD.Core.Application +{ + public class EventStream + { + + #region Properties + + public string Type { get; set; } + + public string Source { get; set; } + + public Guid Position { get; set; } + + public byte RetryMax { get; set; } + + public ICollection RetryDelays { get; set; } = new List(); + + public short BlockSize { get; set; } + + #endregion Properties + + #region Methods + + public override string ToString() + => $"{this.GetType().Name} [{nameof(Type)}={this.Type}, {nameof(Source)}={this.Source}]"; + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Messages/Application/ExcludeFailedEventStream.cs b/Src/DDD.Core.Messages/Application/ExcludeFailedEventStream.cs new file mode 100644 index 0000000..7d15679 --- /dev/null +++ b/Src/DDD.Core.Messages/Application/ExcludeFailedEventStream.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; + +namespace DDD.Core.Application +{ + /// + /// Encapsulates all information needed to exclude a partial event stream in failure from the main event stream. + /// + public class ExcludeFailedEventStream : ICommand + { + + #region Properties + + public string StreamId { get; set; } + + public string StreamType { get; set; } + + public string StreamSource { get; set; } + + public Guid StreamPosition { get; set; } + + public Guid EventId { get; set; } + + public string EventType { get; set; } + + public DateTime ExceptionTime { get; set; } + + public string ExceptionType { get; set; } + + public string ExceptionMessage { get; set; } + + public string ExceptionSource { get; set; } + + public string ExceptionInfo { get; set; } + + public string BaseExceptionType { get; set; } + + public string BaseExceptionMessage { get; set; } + + public byte RetryCount { get; set; } + + public byte RetryMax { get; set; } + + public ICollection RetryDelays { get; set; } = new List(); + + public short BlockSize { get; set; } + + #endregion Properties + + #region Methods + + public override string ToString() + => $"{this.GetType().Name} [{nameof(StreamId)}={this.StreamId}, {nameof(StreamType)}={this.StreamType}, {nameof(StreamSource)}={this.StreamSource}]"; + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Messages/Application/FailedEventStream.cs b/Src/DDD.Core.Messages/Application/FailedEventStream.cs new file mode 100644 index 0000000..02cfe5c --- /dev/null +++ b/Src/DDD.Core.Messages/Application/FailedEventStream.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; + +namespace DDD.Core.Application +{ + public class FailedEventStream + { + + #region Properties + + public string StreamId { get; set; } + + public string StreamType { get; set; } + + public string StreamSource { get; set; } + + public Guid StreamPosition { get; set; } + + public Guid EventId { get; set; } + + public DateTime ExceptionTime { get; set; } + + public byte RetryCount { get; set; } + + public byte RetryMax { get; set; } + + public ICollection RetryDelays { get; set; } = new List(); + + public short BlockSize { get; set; } + + #endregion Properties + + #region Methods + + public override string ToString() + => $"{this.GetType().Name} [{nameof(StreamId)}={this.StreamId}, {nameof(StreamType)}={this.StreamType}, {nameof(StreamSource)}={this.StreamSource}]"; + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Messages/Application/FindEventStreams.cs b/Src/DDD.Core.Messages/Application/FindEventStreams.cs new file mode 100644 index 0000000..d944329 --- /dev/null +++ b/Src/DDD.Core.Messages/Application/FindEventStreams.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace DDD.Core.Application +{ + public class FindEventStreams : IQuery> + { + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Messages/Application/FindFailedEventStreams.cs b/Src/DDD.Core.Messages/Application/FindFailedEventStreams.cs new file mode 100644 index 0000000..cff8381 --- /dev/null +++ b/Src/DDD.Core.Messages/Application/FindFailedEventStreams.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace DDD.Core.Application +{ + public class FindFailedEventStreams : IQuery> + { + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Messages/Application/FindRecurringCommands.cs b/Src/DDD.Core.Messages/Application/FindRecurringCommands.cs new file mode 100644 index 0000000..ac09ca6 --- /dev/null +++ b/Src/DDD.Core.Messages/Application/FindRecurringCommands.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace DDD.Core.Application +{ + public class FindRecurringCommands : IQuery> + { + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Messages/Application/GenerateRecurringCommandId.cs b/Src/DDD.Core.Messages/Application/GenerateRecurringCommandId.cs new file mode 100644 index 0000000..ca54efe --- /dev/null +++ b/Src/DDD.Core.Messages/Application/GenerateRecurringCommandId.cs @@ -0,0 +1,11 @@ +using System; + +namespace DDD.Core.Application +{ + /// + /// Encapsulates all information needed to generate an identifier for a recurring command. + /// + public class GenerateRecurringCommandId : IQuery + { + } +} diff --git a/Src/DDD.Core.Messages/Application/IncludeFailedEventStream.cs b/Src/DDD.Core.Messages/Application/IncludeFailedEventStream.cs new file mode 100644 index 0000000..4b0a68d --- /dev/null +++ b/Src/DDD.Core.Messages/Application/IncludeFailedEventStream.cs @@ -0,0 +1,27 @@ +namespace DDD.Core.Application +{ + /// + /// Encapsulates all information needed to include a partial event stream in failure into the main event stream. + /// + public class IncludeFailedEventStream : ICommand + { + + #region Properties + + public string Id { get; set; } + + public string Type { get; set; } + + public string Source { get; set; } + + #endregion Properties + + #region Methods + + public override string ToString() + => $"{this.GetType().Name} [{nameof(Id)}={this.Id}, {nameof(Type)}={this.Type}, {nameof(Source)}={this.Source}]"; + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Messages/Application/IncrementalDelay.cs b/Src/DDD.Core.Messages/Application/IncrementalDelay.cs new file mode 100644 index 0000000..cd748fe --- /dev/null +++ b/Src/DDD.Core.Messages/Application/IncrementalDelay.cs @@ -0,0 +1,15 @@ +namespace DDD.Core.Application +{ + public class IncrementalDelay + { + + #region Properties + + public short Delay { get; set; } + + public short Increment { get; set; } + + #endregion Properties + + } +} diff --git a/Src/DDD.Core.Messages/Application/MarkRecurringCommandAsFailed.cs b/Src/DDD.Core.Messages/Application/MarkRecurringCommandAsFailed.cs new file mode 100644 index 0000000..505eba7 --- /dev/null +++ b/Src/DDD.Core.Messages/Application/MarkRecurringCommandAsFailed.cs @@ -0,0 +1,24 @@ +using System; + +namespace DDD.Core.Application +{ + public class MarkRecurringCommandAsFailed : ICommand + { + #region Properties + + public Guid CommandId { get; set; } + + public DateTime ExecutionTime { get; set; } + + public string ExceptionInfo { get; set; } + + #endregion Properties + + #region Methods + + public override string ToString() + => $"{this.GetType().Name} [ {nameof(CommandId)}={this.CommandId}, {nameof(ExecutionTime)}={this.ExecutionTime}]"; + + #endregion Methods + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Messages/Application/MarkRecurringCommandAsSuccessful.cs b/Src/DDD.Core.Messages/Application/MarkRecurringCommandAsSuccessful.cs new file mode 100644 index 0000000..33d5612 --- /dev/null +++ b/Src/DDD.Core.Messages/Application/MarkRecurringCommandAsSuccessful.cs @@ -0,0 +1,24 @@ +using System; + +namespace DDD.Core.Application +{ + public class MarkRecurringCommandAsSuccessful : ICommand + { + + #region Properties + + public Guid CommandId { get; set; } + + public DateTime ExecutionTime { get; set; } + + #endregion Properties + + #region Methods + + public override string ToString() + => $"{this.GetType().Name} [ {nameof(CommandId)}={this.CommandId}, {nameof(ExecutionTime)}={this.ExecutionTime}]"; + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Messages/Application/ReadEventStream.cs b/Src/DDD.Core.Messages/Application/ReadEventStream.cs new file mode 100644 index 0000000..acd433a --- /dev/null +++ b/Src/DDD.Core.Messages/Application/ReadEventStream.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace DDD.Core.Application +{ + public class ReadEventStream : IQuery> + { + + #region Properties + + public string[] ExcludedStreamIds { get; set; } = { }; + public Guid StreamPosition { get; set; } + public string StreamType { get; set; } + public short Top { get; set; } = 100; + + #endregion Properties + + #region Methods + + public override string ToString() + => $"{this.GetType().Name} [{nameof(StreamType)}={this.StreamType}, {nameof(StreamPosition)}={this.StreamPosition}]"; + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Messages/Application/ReadFailedEventStream.cs b/Src/DDD.Core.Messages/Application/ReadFailedEventStream.cs new file mode 100644 index 0000000..809ff51 --- /dev/null +++ b/Src/DDD.Core.Messages/Application/ReadFailedEventStream.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace DDD.Core.Application +{ + public class ReadFailedEventStream : IQuery> + { + + #region Properties + + public string StreamId { get; set; } + public Guid EventIdMin { get; set; } + public Guid EventIdMax { get; set; } + public string StreamType { get; set; } + public int Top { get; set; } = 100; + + #endregion Properties + + #region Methods + + public override string ToString() + => $"{this.GetType().Name} [{nameof(StreamId)}={this.StreamId}, {nameof(StreamType)}={this.StreamType}, {nameof(EventIdMin)}={this.EventIdMin}, {nameof(EventIdMax)}={this.EventIdMax}]"; + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Messages/Application/RecurringCommand.cs b/Src/DDD.Core.Messages/Application/RecurringCommand.cs new file mode 100644 index 0000000..5fe510d --- /dev/null +++ b/Src/DDD.Core.Messages/Application/RecurringCommand.cs @@ -0,0 +1,31 @@ +using System; + +namespace DDD.Core.Application +{ + public class RecurringCommand + { + #region Properties + + public Guid CommandId { get; set; } + + public string CommandType { get; set; } + + public string Body { get; set; } + + public string BodyFormat { get; set; } + + public string RecurringExpression { get; set; } + + public string RecurringExpressionFormat { get; set; } + + #endregion Properties + + #region Methods + + public override string ToString() + => $"{this.GetType().Name} [ {nameof(CommandId)}={this.CommandId}, {nameof(CommandType)}={this.CommandType}, {nameof(RecurringExpression)}={this.RecurringExpression}, {nameof(RecurringExpressionFormat)}={this.RecurringExpressionFormat}]"; + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Messages/Application/RecurringCommandDetail.cs b/Src/DDD.Core.Messages/Application/RecurringCommandDetail.cs new file mode 100644 index 0000000..ef2bfa5 --- /dev/null +++ b/Src/DDD.Core.Messages/Application/RecurringCommandDetail.cs @@ -0,0 +1,37 @@ +using System; + +namespace DDD.Core.Application +{ + public class RecurringCommandDetail + { + #region Properties + + public Guid CommandId { get; set; } + + public string CommandType { get; set; } + + public string Body { get; set; } + + public string BodyFormat { get; set; } + + public string RecurringExpression { get; set; } + + public string RecurringExpressionFormat { get; set; } + + public DateTime? LastExecutionTime { get; set; } + + public CommandExecutionStatus? LastExecutionStatus { get; set; } + + public string LastExceptionInfo { get; set; } + + #endregion Properties + + #region Methods + + public override string ToString() + => $"{this.GetType().Name} [ {nameof(CommandId)}={this.CommandId}, {nameof(CommandType)}={this.CommandType}, {nameof(RecurringExpression)}={this.RecurringExpression}, {nameof(RecurringExpressionFormat)}={this.RecurringExpressionFormat}]"; + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Messages/Application/RegisterRecurringCommand.cs b/Src/DDD.Core.Messages/Application/RegisterRecurringCommand.cs new file mode 100644 index 0000000..496514a --- /dev/null +++ b/Src/DDD.Core.Messages/Application/RegisterRecurringCommand.cs @@ -0,0 +1,30 @@ +using System; + +namespace DDD.Core.Application +{ + public class RegisterRecurringCommand : ICommand + { + #region Properties + + public Guid CommandId { get; set; } + + public string CommandType { get; set; } + + public string Body { get; set; } + + public string BodyFormat { get; set; } + + public string RecurringExpression { get; set; } + + public string RecurringExpressionFormat { get; set; } + + #endregion Properties + + #region Methods + + public override string ToString() + => $"{this.GetType().Name} [ {nameof(CommandId)}={this.CommandId}, {nameof(CommandType)}={this.CommandType}, {nameof(RecurringExpression)}={this.RecurringExpression}, {nameof(RecurringExpressionFormat)}={this.RecurringExpressionFormat}]"; + + #endregion Methods + } +} diff --git a/Src/DDD.Core.Messages/Application/SubscribeToEventStream.cs b/Src/DDD.Core.Messages/Application/SubscribeToEventStream.cs new file mode 100644 index 0000000..aeda945 --- /dev/null +++ b/Src/DDD.Core.Messages/Application/SubscribeToEventStream.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; + +namespace DDD.Core.Application +{ + public class SubscribeToEventStream : ICommand + { + + #region Properties + + public string Type { get; set; } + + public string Source { get; set; } + + public Guid Position { get; set; } + + public byte RetryMax { get; set; } + + public ICollection RetryDelays { get; set; } = new List(); + + public short BlockSize { get; set; } + + #endregion Properties + + #region Methods + + public override string ToString() + => $"{this.GetType().Name} [{nameof(Type)}={this.Type}, {nameof(Source)}={this.Source}]"; + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Messages/Application/UpdateEventStreamPosition.cs b/Src/DDD.Core.Messages/Application/UpdateEventStreamPosition.cs new file mode 100644 index 0000000..85b4129 --- /dev/null +++ b/Src/DDD.Core.Messages/Application/UpdateEventStreamPosition.cs @@ -0,0 +1,26 @@ +using System; + +namespace DDD.Core.Application +{ + public class UpdateEventStreamPosition : ICommand + { + + #region Properties + + public string Type { get; set; } + + public string Source { get; set; } + + public Guid Position { get; set; } + + #endregion Properties + + #region Methods + + public override string ToString() + => $"{this.GetType().Name} [{nameof(Type)}={this.Type}, {nameof(Source)}={this.Source}, {nameof(Position)}={this.Position}]"; + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Messages/Application/UpdateFailedEventStream.cs b/Src/DDD.Core.Messages/Application/UpdateFailedEventStream.cs new file mode 100644 index 0000000..e76872e --- /dev/null +++ b/Src/DDD.Core.Messages/Application/UpdateFailedEventStream.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; + +namespace DDD.Core.Application +{ + public class UpdateFailedEventStream : ICommand + { + + #region Properties + + public string StreamId { get; set; } + + public string StreamType { get; set; } + + public string StreamSource { get; set; } + + public Guid StreamPosition { get; set; } + + public Guid EventId { get; set; } + + public string EventType { get; set; } + + public DateTime ExceptionTime { get; set; } + + public string ExceptionType { get; set; } + + public string ExceptionMessage { get; set; } + + public string ExceptionSource { get; set; } + + public string ExceptionInfo { get; set; } + + public string BaseExceptionType { get; set; } + + public string BaseExceptionMessage { get; set; } + + public byte RetryCount { get; set; } + + public byte RetryMax { get; set; } + + public ICollection RetryDelays { get; set; } = new List(); + + #endregion Properties + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Messages/Application/UpdateFailedEventStreamPosition.cs b/Src/DDD.Core.Messages/Application/UpdateFailedEventStreamPosition.cs new file mode 100644 index 0000000..f429d7d --- /dev/null +++ b/Src/DDD.Core.Messages/Application/UpdateFailedEventStreamPosition.cs @@ -0,0 +1,28 @@ +using System; + +namespace DDD.Core.Application +{ + public class UpdateFailedEventStreamPosition : ICommand + { + + #region Properties + + public string Id { get; set; } + + public string Type { get; set; } + + public string Source { get; set; } + + public Guid Position { get; set; } + + #endregion Properties + + #region Methods + + public override string ToString() + => $"{this.GetType().Name} [{nameof(Id)}={this.Id}, {nameof(Type)}={this.Type}, {nameof(Source)}={this.Source}, {nameof(Position)}={this.Position}]"; + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Messages/DDD.Core.Messages.csproj b/Src/DDD.Core.Messages/DDD.Core.Messages.csproj index 681610d..04a02fd 100644 --- a/Src/DDD.Core.Messages/DDD.Core.Messages.csproj +++ b/Src/DDD.Core.Messages/DDD.Core.Messages.csproj @@ -1,51 +1,17 @@ - - - + - Debug - AnyCPU - {2438B31A-3A39-4878-81FA-BE5AE715EAE5} + net48;netstandard2.1 Library - Properties DDD.Core - DDD.Core.Messages - v4.7.2 - 512 - true + false - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - Properties\CommonAssemblyInfo.cs - - - - - - - - - + + + + \ No newline at end of file diff --git a/Src/DDD.Core.Messages/Domain/BoundedContext.cs b/Src/DDD.Core.Messages/Domain/BoundedContext.cs new file mode 100644 index 0000000..f910879 --- /dev/null +++ b/Src/DDD.Core.Messages/Domain/BoundedContext.cs @@ -0,0 +1,79 @@ +using EnsureThat; +using System; +using System.Collections.Generic; + +namespace DDD.Core.Domain +{ + /// + /// Represents a bounded context. + /// + public abstract class BoundedContext : IEquatable + { + + #region Constructors + + protected BoundedContext(string code, string name) + { + Ensure.That(code, nameof(code)).IsNotNullOrWhiteSpace(); + Ensure.That(name, nameof(name)).IsNotNullOrWhiteSpace(); + Code = code; + Name = name; + } + + #endregion Constructors + + #region Properties + + public string Code { get; } + public string Name { get; } + + #endregion Properties + + #region Methods + + public static bool operator !=(BoundedContext left, BoundedContext right) + { + return !(left == right); + } + + public static bool operator ==(BoundedContext left, BoundedContext right) + { + return EqualityComparer.Default.Equals(left, right); + } + + public override bool Equals(object obj) + { + return Equals(obj as BoundedContext); + } + + public bool Equals(BoundedContext other) + { + return !(other is null) && + Code == other.Code && + Name == other.Name; + } + + public override int GetHashCode() + { + return CombineHashCodes(Code, Name); + } + + public override string ToString() + => $"{GetType().Name} [{nameof(Code)}={Code}, {nameof(Name)}={Name}]"; + + private static int CombineHashCodes(params object[] collection) + { + Ensure.That(collection, nameof(collection)).IsNotNull(); + unchecked + { + var hash = 17; + foreach (var obj in collection) + hash = hash * 23 + (obj != null ? obj.GetHashCode() : 0); + return hash; + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Messages/Domain/IEvent.cs b/Src/DDD.Core.Messages/Domain/IEvent.cs index 92cc871..de30086 100644 --- a/Src/DDD.Core.Messages/Domain/IEvent.cs +++ b/Src/DDD.Core.Messages/Domain/IEvent.cs @@ -7,10 +7,12 @@ namespace DDD.Core.Domain /// public interface IEvent : IMessage { + #region Properties DateTime OccurredOn { get; } #endregion Properties + } } \ No newline at end of file diff --git a/Src/DDD.Core.Messages/Properties/AssemblyInfo.cs b/Src/DDD.Core.Messages/Properties/AssemblyInfo.cs index 4fbcbef..8b226a7 100644 --- a/Src/DDD.Core.Messages/Properties/AssemblyInfo.cs +++ b/Src/DDD.Core.Messages/Properties/AssemblyInfo.cs @@ -2,6 +2,6 @@ using System.Runtime.InteropServices; [assembly: AssemblyTitle("DDD.Core.Messages")] -[assembly: AssemblyDescription("Core abstractions for messages.")] +[assembly: AssemblyDescription("Core messages.")] [assembly: AssemblyProduct("DDD.Core.Messages")] [assembly: Guid("2438b31a-3a39-4878-81fa-be5ae715eae5")] \ No newline at end of file diff --git a/Src/DDD.Core.NHibernate/AinsiUriType.cs b/Src/DDD.Core.NHibernate/AinsiUriType.cs new file mode 100644 index 0000000..9f0ca2d --- /dev/null +++ b/Src/DDD.Core.NHibernate/AinsiUriType.cs @@ -0,0 +1,29 @@ +using NHibernate.SqlTypes; +using NHibernate.Type; +using System; + +namespace DDD.Core.Infrastructure.Data +{ + [Serializable] + public class AnsiUriType : UriType + { + + #region Constructors + + public AnsiUriType() : base(new AnsiStringSqlType()) + { + } + + #endregion Constructors + + #region Properties + + public override string Name + { + get { return "AnsiUri"; } + } + + #endregion Properties + + } +} diff --git a/Src/DDD.Core.NHibernate/CompositeUserType.cs b/Src/DDD.Core.NHibernate/CompositeUserType.cs new file mode 100644 index 0000000..e71c531 --- /dev/null +++ b/Src/DDD.Core.NHibernate/CompositeUserType.cs @@ -0,0 +1,570 @@ +using NHibernate; +using NHibernate.Engine; +using NHibernate.Type; +using NHibernate.UserTypes; +using NHibernate.Util; +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace DDD.Core.Infrastructure.Data +{ + using NHibernate.Mapping.ByCode; + + /// + /// This class must be unit tested. + /// + public abstract class CompositeUserType : ICompositeUserType + { + + #region Fields + + public const string NotNullDiscriminatorValue = "not null"; + public const string NullDiscriminatorValue = "null"; + private readonly Type returnedClass; + private readonly Dictionary classes; + private bool isMutable; + private readonly List properties; + private string[] propertyNames; + private IType[] propertyTypes; + + #endregion Fields + + #region Constructors + + protected CompositeUserType() + { + this.properties = new List(); + this.classes = new Dictionary(); + this.isMutable = true; + this.returnedClass = typeof(TComponent); + this.classes[this.returnedClass] = ClassMapping.FromMappedClass(this.returnedClass); + } + + #endregion Constructors + + #region Properties + + /// + /// Are objects of this type mutable? + /// + bool ICompositeUserType.IsMutable => this.isMutable; + + /// + /// Get the "property names" that may be used in a query. + /// + string[] ICompositeUserType.PropertyNames + { + get + { + if (this.propertyNames == null) + { + this.propertyNames = this.properties.Select(p => p.Name).ToArray(); + } + return this.propertyNames; + } + } + + /// + /// Get the corresponding "property types". + /// + IType[] ICompositeUserType.PropertyTypes + { + get + { + if (this.propertyTypes == null) + { + this.propertyTypes = this.properties.Select(p => p.PersistentType).ToArray(); + } + return this.propertyTypes; + } + } + + /// + /// The class returned by NullSafeGet(). + /// + Type ICompositeUserType.ReturnedClass => this.returnedClass; + + int PropertySpan => this.properties.Count; + + #endregion Properties + + #region Methods + + /// + /// Reconstruct an object from the cacheable representation. + /// At the very least this method should perform a deep copy. (optional operation) + /// + object ICompositeUserType.Assemble(object cached, ISessionImplementor session, object owner) + { + if (cached == null) return null; + var values = (object[])cached; + var assembled = new object[values.Length]; + var mappedClass = this.returnedClass; + for (var i = 0; i < values.Length; i++) + { + assembled[i] = this.AsUserType().PropertyTypes[i].Assemble(values[i], session, owner); + if (this.properties[i].IsDriscriminator) + mappedClass = this.GetMappedClass(assembled[i]); + } + var result = this.Instantiate(mappedClass); + this.SetPropertyValues(result, assembled); + return result; + } + + /// + /// Return a deep copy of the persistent state, stopping at entities and at collections. + /// + object ICompositeUserType.DeepCopy(object component) + { + if (component == null) return null; + var values = this.GetPropertyValues(component); + for (var i = 0; i < values.Length; i++) + { + values[i] = this.AsUserType().PropertyTypes[i].DeepCopy(values[i], null); + } + var result = this.Instantiate(component.GetType()); + this.SetPropertyValues(result, values); + return result; + } + + /// + /// Transform the object into its cacheable representation. + /// At the very least this method should perform a deep copy. + /// That may not be enough for some implementations, method should perform a deep copy. That may not be enough for some implementations, however; for example, associations must be cached as identifier values. (optional operation) + /// + object ICompositeUserType.Disassemble(object component, ISessionImplementor session) + { + if (component == null) return null; + var values = this.GetPropertyValues(component); + for (var i = 0; i < values.Length; i++) + { + values[i] = this.AsUserType().PropertyTypes[i].Disassemble(values[i], session, null); + } + return values; + } + + /// + /// Compare two instances of the class mapped by this type for persistence + /// "equality", ie. equality of persistent state. + /// + bool ICompositeUserType.Equals(object x, object y) // TODO : change component + { + var isEqualFast = IsEqualFast(x, y); + if (isEqualFast.HasValue) + return isEqualFast.Value; + + var xvalues = GetPropertyValues(x); + var yvalues = GetPropertyValues(y); + for (var i = 0; i < this.PropertySpan; i++) + { + if (!propertyTypes[i].IsEqual(xvalues[i], yvalues[i])) + { + return false; + } + } + return true; + } + + /// + /// Get a hashcode for the instance, consistent with persistence "equality" + /// + int ICompositeUserType.GetHashCode(object x) + { + if (this.GetClassMapping(x.GetType()).OverridesGetHashCode) + return x.GetHashCode(); + return this.GetHashCode(x); + } + + /// + /// Get the value of a property. + /// + object ICompositeUserType.GetPropertyValue(object component, int property) + { + if (component == null) return null; + var propertyMapping = this.properties[property]; + if (propertyMapping.IsDriscriminator) + return this.GetClassMapping(component.GetType()).DiscriminatorValue; + if (!propertyMapping.MemberInfo.DeclaringType.IsAssignableFrom(component.GetType())) return null; + return propertyMapping.MemberInfo.GetPropertyOrFieldValue(component); + } + + /// + /// Retrieve an instance of the mapped class from a DbDataReader. Implementors + /// should handle possibility of null values. + /// + object ICompositeUserType.NullSafeGet(DbDataReader dr, string[] names, ISessionImplementor session, object owner) + { + var value = this.Hydrate(dr, names, session, owner); + if (value == null) return null; + var values = (object[])value; + var mappedClass = this.returnedClass; + if (this.HasSubComponents()) + { + var discriminatorColumn = names.First(n => n.StartsWith(this.DiscriminatorName(), StringComparison.OrdinalIgnoreCase)); + var discriminatorValue = dr[discriminatorColumn]; + mappedClass = this.GetMappedClass(discriminatorValue); + } + var result = this.Instantiate(mappedClass); + this.SetPropertyValues(result, values); + return result; + } + + /// + /// Write an instance of the mapped class to a prepared statement. + /// Implementors should handle possibility of null values. + /// A multi-column type should be written to parameters starting from index. + /// If a property is not settable, skip it and don't increment the index. + /// + void ICompositeUserType.NullSafeSet(DbCommand cmd, object value, int begin, bool[] settable, ISessionImplementor session) + { + var values = NullSafeGetValues(value); + var loc = 0; + for (var i = 0; i < values.Length; i++) + { + var length = this.AsUserType().PropertyTypes[i].GetColumnSpan(session.Factory); + switch (length) + { + case 0: + //noop + break; + case 1: + if (settable[loc]) + { + this.AsUserType().PropertyTypes[i].NullSafeSet(cmd, values[i], begin, session); + begin++; + } + break; + default: + var subsettable = new bool[length]; + Array.Copy(settable, loc, subsettable, 0, length); + this.AsUserType().PropertyTypes[i].NullSafeSet(cmd, values[i], begin, subsettable, session); + begin += ArrayHelper.CountTrue(subsettable); + break; + } + loc += length; + } + } + + /// + /// During merge, replace the existing (target) value in the entity we are merging to + /// with a new (original) value from the detached entity we are merging. For immutable + /// objects, or null values, it is safe to simply return the first parameter. For + /// mutable objects, it is safe to return a copy of the first parameter. However, since + /// composite user types often define component values, it might make sense to recursively + /// replace component values in the target object. + /// + object ICompositeUserType.Replace(object original, object target, ISessionImplementor session, object owner) + { + if (!this.isMutable || original == null) return original; + var result = target ?? this.Instantiate(original.GetType()); + var values = TypeHelper.Replace(this.GetPropertyValues(original), this.GetPropertyValues(result), this.AsUserType().PropertyTypes, session, owner, null); + this.SetPropertyValues(result, values); + return result; + } + + /// + /// Set the value of a property. + /// + void ICompositeUserType.SetPropertyValue(object component, int property, object value) + { + if (component == null) return; + var propertyMapping = this.properties[property]; + if (propertyMapping.IsDriscriminator) return; + if (!propertyMapping.MemberInfo.DeclaringType.IsAssignableFrom(component.GetType())) return; + propertyMapping.MemberInfo.SetPropertyOrFieldValue(component, value); + } + + protected void Discriminator(string column, IType persistentType = null) + { + var propertyMapping = new PropertyMapping + { + Name = column, + PersistentType = persistentType ?? NHibernateUtil.AnsiString, + IsDriscriminator = true + }; + this.properties.Add(propertyMapping); + } + + protected void DiscriminatorValue(object value) + { + this.classes[this.returnedClass].DiscriminatorValue = value != null ? value.ToString() : NullDiscriminatorValue; + } + + /// + /// Indicates if the component is mutable. + /// + protected void Mutable(bool isMutable) + { + this.isMutable = isMutable; + } + + /// + /// Maps a property of the component. + /// + protected void Property(string notVisiblePropertyOrFieldName, IType persistentType = null) + { + var memberInfo = this.returnedClass.GetPropertyOrFieldMatchingName(notVisiblePropertyOrFieldName); + if (memberInfo == null) + throw new MappingException($"Member not found. The member '{notVisiblePropertyOrFieldName}' does not exists in type {this.returnedClass.FullName}"); + var propertyMapping = PropertyMapping.FromMappedProperty(memberInfo); + if (persistentType != null) + propertyMapping.PersistentType = persistentType; + this.properties.Add(propertyMapping); + } + + /// + /// Maps a property of the component. + /// + protected void Property(Expression> property, IType persistentType = null) + { + var memberInfo = TypeExtensions.DecodeMemberAccessExpressionOf(property); + var propertyMapping = PropertyMapping.FromMappedProperty(memberInfo); + if (persistentType != null) + propertyMapping.PersistentType = persistentType; + this.properties.Add(propertyMapping); + } + + /// + /// Maps a subcomponent. + /// + protected void Subclass(Action> mapping = null) where TSubclass : TComponent + { + var mapper = new SubcomponentMapper(this); + mapping?.Invoke(mapper); + } + + private ICompositeUserType AsUserType() => this; + + private string DiscriminatorName() + { + var propertyMapping = this.properties.FirstOrDefault(p => p.IsDriscriminator); + if (propertyMapping == null) + throw new InvalidOperationException("No discriminator found."); + return propertyMapping.Name; + } + private ClassMapping GetClassMapping(Type mappedClass) + { + if (!this.classes.ContainsKey(mappedClass)) + throw new ArgumentException(nameof(mappedClass), $"The class '{mappedClass.Name}' is not mapped."); + return this.classes[mappedClass]; + } + + private int GetHashCode(object x) + { + var result = 17; + var values = this.GetPropertyValues(x); + unchecked + { + for (var i = 0; i < values.Length; i++) + { + var y = values[i]; + result *= 37; + if (y != null) + result += propertyTypes[i].GetHashCode(y); + } + } + return result; + } + + private Type GetMappedClass(object discriminatorValue) + { + var value = discriminatorValue != null ? discriminatorValue.ToString() : NullDiscriminatorValue; + var mappedClass = this.classes.FirstOrDefault(c => c.Value.DiscriminatorValue == value).Key; + if (mappedClass == null) + { + if (value != NullDiscriminatorValue) + mappedClass = this.classes.FirstOrDefault(c => c.Value.DiscriminatorValue == NotNullDiscriminatorValue).Key; + } + if (mappedClass != null) return mappedClass; + throw new ArgumentOutOfRangeException(nameof(discriminatorValue), $"No mapped class found for discriminator value '{value}'"); + } + + private object[] GetPropertyValues(object component) + { + var values = new object[this.PropertySpan]; + if (component != null) + { + for (var i = 0; i < values.Length; i++) + values[i] = this.AsUserType().GetPropertyValue(component, i); + } + return values; + } + + private bool HasSubComponents() => this.classes.Count() > 1; + + private object Hydrate(DbDataReader rs, string[] names, ISessionImplementor session, object owner) + { + var begin = 0; + var notNull = false; + var values = new object[this.PropertySpan]; + for (var i = 0; i < values.Length; i++) + { + var length = this.AsUserType().PropertyTypes[i].GetColumnSpan(session.Factory); + var range = ArrayHelper.Slice(names, begin, length); //cache this + var value = this.AsUserType().PropertyTypes[i].Hydrate(rs, range, session, owner); + if (value != null) notNull = true; + values[i] = value; + begin += length; + } + if (this.returnedClass.IsValueType) + return values; + else + return notNull ? values : null; + } + + private object Instantiate(Type mappedClass) + { + var mapping = this.classes[mappedClass]; + if (mapping.IsAbstract) + throw new InstantiationException("Cannot instantiate abstract class or interface: ", mappedClass); + var constructor = ReflectHelper.GetDefaultConstructor(mappedClass); + if (constructor == null) + throw new InstantiationException($"No default constructor for component: ", mappedClass); + return constructor.Invoke(null); + } + + private bool? IsEqualFast(object x, object y) + { + if (x == y) return true; + if (x == null || y == null) return false; + var componentType = this.AsUserType().ReturnedClass; + if (!componentType.IsInstanceOfType(x) || !componentType.IsInstanceOfType(y)) return false; + return null; + } + + private object[] NullSafeGetValues(object value) + { + if (value == null) return new object[this.PropertySpan]; + return this.GetPropertyValues(value); + } + + private void SetPropertyValues(object component, object[] values) + { + for (var i = 0; i < values.Length; i++) + this.AsUserType().SetPropertyValue(component, i, values[i]); + } + + #endregion Methods + + #region Classes + + private class ClassMapping + { + + #region Properties + + public string DiscriminatorValue { get; set; } + + public bool IsAbstract { get; set; } + + public bool OverridesGetHashCode { get; set; } + + #endregion Properties + + #region Methods + + public static ClassMapping FromMappedClass(Type mappedClass) + { + return new ClassMapping + { + OverridesGetHashCode = ReflectHelper.OverridesGetHashCode(mappedClass), + IsAbstract = ReflectHelper.IsAbstractClass(mappedClass), + DiscriminatorValue = mappedClass.Name + }; + } + + #endregion Methods + + } + + private class PropertyMapping + { + + #region Properties + + public bool IsDriscriminator { get; set; } + + public MemberInfo MemberInfo { get; set; } + + public string Name { get; set; } + + public IType PersistentType { get; set; } + + #endregion Properties + + #region Methods + + public static PropertyMapping FromMappedProperty(MemberInfo mappedProperty) + { + return new PropertyMapping + { + MemberInfo = mappedProperty, + Name = mappedProperty.Name, + PersistentType = NHibernateUtil.GuessType(mappedProperty.GetPropertyOrFieldType()), + IsDriscriminator = false + }; + } + + #endregion Methods + + } + + private class SubcomponentMapper : ISubcomponentMapper where TSubComponent : TComponent + { + + #region Fields + + private readonly CompositeUserType compositeUserType; + + #endregion Fields + + #region Constructors + + public SubcomponentMapper(CompositeUserType compositeUserType) + { + this.compositeUserType = compositeUserType; + this.compositeUserType.classes[typeof(TSubComponent)] = ClassMapping.FromMappedClass(typeof(TSubComponent)); + } + + #endregion Constructors + + #region Methods + + public void DiscriminatorValue(object value) + { + this.compositeUserType.classes[typeof(TSubComponent)].DiscriminatorValue = value != null ? value.ToString() : NullDiscriminatorValue; + } + + /// + /// Maps a property of the subcomponent. + /// + public void Property(string notVisiblePropertyOrFieldName, IType persistentType = null) + { + this.compositeUserType.Property(notVisiblePropertyOrFieldName, persistentType); + } + + /// + /// Maps a property of the subcomponent. + /// + public void Property(Expression> property, IType persistentType = null) + { + var memberInfo = TypeExtensions.DecodeMemberAccessExpressionOf(property); + var propertyMapping = PropertyMapping.FromMappedProperty(memberInfo); + if (persistentType != null) + propertyMapping.PersistentType = persistentType; + this.compositeUserType.properties.Add(propertyMapping); + } + + #endregion Methods + + } + + #endregion Classes + + } + +} diff --git a/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj b/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj new file mode 100644 index 0000000..5d1ff6d --- /dev/null +++ b/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj @@ -0,0 +1,22 @@ + + + net48;netstandard2.1 + Library + DDD.Core.Infrastructure.Data + false + bin\$(Configuration)\ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Src/DDD.Core.NHibernate/DelegatingSessionFactory.cs b/Src/DDD.Core.NHibernate/DelegatingSessionFactory.cs new file mode 100644 index 0000000..b8e7e4a --- /dev/null +++ b/Src/DDD.Core.NHibernate/DelegatingSessionFactory.cs @@ -0,0 +1,100 @@ +using NHibernate; +using NHibernate.Cfg; +using System; +using System.Threading.Tasks; +using System.Threading; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Domain; + + public class DelegatingSessionFactory : ISessionFactory, IDisposable + where TContext : BoundedContext + { + + #region Fields + + private readonly Func asyncOptions; + private readonly Configuration configuration; + private readonly Action options; + private readonly Lazy lazySessionFactory; + private bool disposed; + + #endregion Fields + + #region Constructors + + public DelegatingSessionFactory(TContext context, + Configuration configuration, + Action options, + Func asyncOptions) + { + Ensure.That(context, nameof(context)).IsNotNull(); + Ensure.That(configuration, nameof(configuration)).IsNotNull(); + Ensure.That(options, nameof(options)).IsNotNull(); + Ensure.That(asyncOptions, nameof(asyncOptions)).IsNotNull(); + this.configuration = configuration; + this.options = options; + this.asyncOptions = asyncOptions; + this.lazySessionFactory = new Lazy(() => this.configuration.BuildSessionFactory()); + this.Context = context; + } + + #endregion Constructors + + #region Properties + + public TContext Context { get; } + + private ISessionFactory SessionFactory => this.lazySessionFactory.Value; + + #endregion Properties + + #region Methods + + public static ISessionFactory Create(TContext context, Configuration configuration, Action options) + { + Ensure.That(configuration, nameof(configuration)).IsNotNull(); + Ensure.That(options, nameof(options)).IsNotNull(); + Func asyncOptions = (b, t) => { options(b); return Task.CompletedTask; }; + return new DelegatingSessionFactory(context, configuration, options, asyncOptions); + } + + public ISession CreateSession() + { + var sessionBuilder = this.SessionFactory.WithOptions(); + this.options(sessionBuilder); + return sessionBuilder.OpenSession(); + } + + public async Task CreateSessionAsync(CancellationToken cancellationToken = default) + { + var sessionBuilder = this.SessionFactory.WithOptions(); + await this.asyncOptions(sessionBuilder, cancellationToken); + return sessionBuilder.OpenSession(); + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + if (disposing) + { + if (this.lazySessionFactory.IsValueCreated) + this.SessionFactory.Dispose(); + } + disposed = true; + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.NHibernate/EventMapping.cs b/Src/DDD.Core.NHibernate/EventMapping.cs new file mode 100644 index 0000000..0fe1282 --- /dev/null +++ b/Src/DDD.Core.NHibernate/EventMapping.cs @@ -0,0 +1,64 @@ +using NHibernate; +using NHibernate.Mapping.ByCode.Conformist; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + + public abstract class EventMapping : ClassMapping + { + + #region Constructors + + protected EventMapping() + { + this.Lazy(false); + // Table + this.Table("Event"); + // Keys + this.Id(e => e.EventId, m1 => + { + m1.Column("EventId"); + }); + // Fields + this.Property(e => e.EventType, m => + { + m.Type(NHibernateUtil.AnsiString); + m.Length(250); + m.NotNullable(true); + }); + this.Property(e => e.OccurredOn, m => m.Precision(3)); // in milliseconds + this.Property(e => e.Body, m => + { + m.Type(NHibernateUtil.AnsiString); + m.NotNullable(true); + }); + this.Property(e => e.BodyFormat, m => + { + m.Type(NHibernateUtil.AnsiString); + m.Length(20); + m.NotNullable(true); + }); + this.Property(e => e.StreamId, m => + { + m.Type(NHibernateUtil.AnsiString); + m.Length(50); + m.NotNullable(true); + }); + this.Property(e => e.StreamType, m => + { + m.Type(NHibernateUtil.AnsiString); + m.Length(50); + m.NotNullable(true); + }); + this.Property(e => e.IssuedBy, m => + { + m.Type(NHibernateUtil.AnsiString); + m.Length(100); + }); + } + + #endregion Constructors + + } +} diff --git a/Src/DDD.Core.NHibernate/ISessionFactory.cs b/Src/DDD.Core.NHibernate/ISessionFactory.cs new file mode 100644 index 0000000..d4f112b --- /dev/null +++ b/Src/DDD.Core.NHibernate/ISessionFactory.cs @@ -0,0 +1,35 @@ +using System.Threading; +using System.Threading.Tasks; +using NHibernate; + +namespace DDD.Core.Infrastructure.Data +{ + using Domain; + + /// + /// Defines a factory for creating instances of Nhibernate sessions associated with a specific bounded context. + /// + public interface ISessionFactory where TContext : BoundedContext + { + + #region Properties + + TContext Context { get; } + + #endregion Properties + + #region Methods + + /// + /// Creates a new instance of NHibernate session. + /// + ISession CreateSession(); + + /// + /// Creates asyncronously a new instance of NHibernate session. + /// + Task CreateSessionAsync(CancellationToken cancellationToken = default); + + #endregion Methods + } +} diff --git a/Src/DDD.Core.NHibernate/ISubcomponentMapper.cs b/Src/DDD.Core.NHibernate/ISubcomponentMapper.cs new file mode 100644 index 0000000..5d4ce3d --- /dev/null +++ b/Src/DDD.Core.NHibernate/ISubcomponentMapper.cs @@ -0,0 +1,20 @@ +using NHibernate.Type; +using System; +using System.Linq.Expressions; + +namespace DDD.Core.Infrastructure.Data +{ + public interface ISubcomponentMapper + { + + #region Methods + + void DiscriminatorValue(object value); + + void Property(string notVisiblePropertyOrFieldName, IType persistentType = null); + + void Property(Expression> property, IType persistentType = null); + + #endregion Methods + } +} \ No newline at end of file diff --git a/Src/DDD.Core.NHibernate/MemberInfoExtensions.cs b/Src/DDD.Core.NHibernate/MemberInfoExtensions.cs new file mode 100644 index 0000000..f7f131c --- /dev/null +++ b/Src/DDD.Core.NHibernate/MemberInfoExtensions.cs @@ -0,0 +1,98 @@ +using System; +using System.Reflection; +using NHibernate; + +namespace DDD.Core.Infrastructure.Data +{ + internal static class MemberInfoExtensions + { + + #region Methods + + public static object GetPropertyOrFieldValue(this MemberInfo propertyOrField, object obj) + { + switch (propertyOrField.MemberType) + { + case MemberTypes.Property: + var property = ((PropertyInfo)propertyOrField); + var getter = property.GetGetMethodRecursively(); + if (getter == null) + throw new PropertyNotFoundException(obj.GetType(), property.Name, "getter"); + return getter.Invoke(obj, new object[] { }); + case MemberTypes.Field: + return ((FieldInfo)propertyOrField).GetValue(obj); + default: + throw new ArgumentOutOfRangeException(nameof(propertyOrField), + $"Expected PropertyInfo or FieldInfo; found :{propertyOrField.MemberType}"); + } + } + + public static void SetPropertyOrFieldValue(this MemberInfo propertyOrField, object obj, object value) + { + switch(propertyOrField.MemberType) + { + case MemberTypes.Property: + var property = ((PropertyInfo)propertyOrField); + var setter = property.GetSetMethodRecursively(); + if (setter == null) + throw new PropertyNotFoundException(obj.GetType(), property.Name, "setter"); + setter.Invoke(obj, new [] { value }); + break; + case MemberTypes.Field: + ((FieldInfo)propertyOrField).SetValue(obj, value); + break; + default: + throw new ArgumentOutOfRangeException(nameof(propertyOrField), + $"Expected PropertyInfo or FieldInfo; found :{propertyOrField.MemberType}"); + } + } + + public static MethodInfo GetSetMethodRecursively(this PropertyInfo property) + { + var setter = property.SetMethod; + if (setter != null) return setter; + var reflectedType = property.ReflectedType; + var declaringType = property.DeclaringType; + var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly; + if (reflectedType != declaringType) + { + setter = declaringType.GetProperty(property.Name, bindingFlags).SetMethod; + if (setter != null) return setter; + } + var interfaces = reflectedType.GetInterfaces(); + foreach(var @interface in interfaces) + { + setter = @interface.GetProperty(property.Name, bindingFlags)?.SetMethod; + if (setter != null) return setter; + } + return null; + } + + public static MethodInfo GetGetMethodRecursively(this PropertyInfo property) + { + var getter = property.GetMethod; + if (getter != null) return getter; + var reflectedType = property.ReflectedType; + var declaringType = property.DeclaringType; + var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly; + if (reflectedType != declaringType) + { + getter = declaringType.GetProperty(property.Name, bindingFlags).GetMethod; + if (getter != null) return getter; + } + var interfaces = reflectedType.GetInterfaces(); + foreach (var @interface in interfaces) + { + getter = @interface.GetProperty(property.Name, bindingFlags)?.GetMethod; + if (getter != null) return getter; + } + return null; + } + + + + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.NHibernate/NHRepository.cs b/Src/DDD.Core.NHibernate/NHRepository.cs new file mode 100644 index 0000000..a6331f7 --- /dev/null +++ b/Src/DDD.Core.NHibernate/NHRepository.cs @@ -0,0 +1,177 @@ +using EnsureThat; +using NHibernate; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace DDD.Core.Infrastructure.Data +{ + using Domain; + using Application; + using Mapping; + using Threading; + using System.Data.Common; + + public abstract class NHRepository : IRepository, IDisposable + where TContext : BoundedContext + where TDomainEntity : DomainEntity + where TIdentity : ComparableValueObject + { + + #region Fields + + private readonly IObjectTranslator eventTranslator; + private readonly IObjectTranslator exceptionTranslator = new NHRepositoryExceptionTranslator(); + private readonly ISessionFactory sessionFactory; + private ISession session; + private bool disposed; + + #endregion Fields + + #region Constructors + + protected NHRepository(ISessionFactory sessionFactory, IObjectTranslator eventTranslator) + { + Ensure.That(sessionFactory, nameof(sessionFactory)).IsNotNull(); + Ensure.That(eventTranslator, nameof(eventTranslator)).IsNotNull(); + this.sessionFactory = sessionFactory; + this.eventTranslator = eventTranslator; + } + + #endregion Constructors + + #region Methods + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + public TDomainEntity Find(TIdentity identity) + { + Ensure.That(identity, nameof(identity)).IsNotNull(); + try + { + var session = this.GetSession(); + return session.Get(identity); + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.TranslateException(ex); + } + } + + public async Task FindAsync(TIdentity identity, CancellationToken cancellationToken = default) + { + Ensure.That(identity, nameof(identity)).IsNotNull(); + await new SynchronizationContextRemover(); + try + { + var session = await this.GetSessionAsync(cancellationToken); + return await session.GetAsync(identity, cancellationToken); + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.TranslateException(ex); + } + } + + public void Save(TDomainEntity aggregate) + { + Ensure.That(aggregate, nameof(aggregate)).IsNotNull(); + try + { + var session = this.GetSession(); + var guidGenerator = session.Connection.SequentialGuidGenerator(); + var events = ToEvents(guidGenerator, aggregate); + session.SaveOrUpdate(aggregate); + foreach (var @event in events) + session.Save(@event); + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.TranslateException(ex); + } + } + + public async Task SaveAsync(TDomainEntity aggregate, CancellationToken cancellationToken = default) + { + Ensure.That(aggregate, nameof(aggregate)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + var session = await this.GetSessionAsync(cancellationToken); + var guidGenerator = session.Connection.SequentialGuidGenerator(); + var events = ToEvents(guidGenerator, aggregate); + await session.SaveOrUpdateAsync(aggregate, cancellationToken); + foreach (var @event in events) + await session.SaveAsync(@event, cancellationToken); + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.TranslateException(ex); + } + } + + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + if (disposing) + this.session?.Dispose(); + disposed = true; + } + } + + protected DbConnection GetConnection() + { + return this.GetSession().Connection; + } + + protected async Task GetConnectionAsync(CancellationToken cancellationToken) + { + return (await this.GetSessionAsync(cancellationToken)).Connection; + } + + protected RepositoryException TranslateException(Exception ex) + { + return this.exceptionTranslator.Translate(ex, new { EntityType = typeof(TDomainEntity) }); + } + + private ISession GetSession() + { + if (this.session == null) + this.session = this.sessionFactory.CreateSession(); + return this.session; + } + + private async Task GetSessionAsync(CancellationToken cancellationToken) + { + if (this.session == null) + this.session = await this.sessionFactory.CreateSessionAsync(cancellationToken); + return this.session; + } + + private IEnumerable ToEvents(IValueGenerator guidGenerator, TDomainEntity aggregate) + { + var username = Thread.CurrentPrincipal?.Identity?.Name; + return aggregate.AllEvents().Select(e => + { + var context = new + { + EventId = guidGenerator.Generate(), + StreamId = aggregate.IdentityAsString(), + StreamType = aggregate.GetType().Name, + IssuedBy = username + }; + return this.eventTranslator.Translate(e, context); + }); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.NHibernate/NHRepositoryExceptionTranslator.cs b/Src/DDD.Core.NHibernate/NHRepositoryExceptionTranslator.cs new file mode 100644 index 0000000..dcf842e --- /dev/null +++ b/Src/DDD.Core.NHibernate/NHRepositoryExceptionTranslator.cs @@ -0,0 +1,49 @@ +using System; +using NHibernate; +using NHibernate.Exceptions; +using System.Collections.Generic; +using System.Data.Common; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Mapping; + using Collections; + using Core.Domain; + + internal class NHRepositoryExceptionTranslator : ObjectTranslator + { + + #region Fields + + private readonly IObjectTranslator dbExceptionTranslator = new DbToRepositoryExceptionTranslator(); + + #endregion Fields + + #region Methods + + public override RepositoryException Translate(Exception exception, IMappingContext context) + { + Ensure.That(exception, nameof(exception)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + context.TryGetValue("EntityType", out Type entityType); + switch (exception) + { + case DbException dbEx: + return dbExceptionTranslator.Translate(dbEx, context); + case ADOException _: + var dbException = ADOExceptionHelper.ExtractDbException(exception); + if (dbException != null) + { + context.Add("OuterException", exception); + return dbExceptionTranslator.Translate(dbException, context); + } + break; + } + return new RepositoryException(isTransient: false, entityType, exception); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.NHibernate/OracleEventMapping.cs b/Src/DDD.Core.NHibernate/OracleEventMapping.cs new file mode 100644 index 0000000..fd5a382 --- /dev/null +++ b/Src/DDD.Core.NHibernate/OracleEventMapping.cs @@ -0,0 +1,15 @@ +namespace DDD.Core.Infrastructure.Data +{ + public class OracleEventMapping : EventMapping + { + #region Constructors + + public OracleEventMapping() + { + // Fields + this.Property(e => e.Body, m => m.Column(m1 => m1.Length(4000))); + } + + #endregion Constructors + } +} diff --git a/Src/DDD.Core.NHibernate/Properties/AssemblyInfo.cs b/Src/DDD.Core.NHibernate/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..1dfeb04 --- /dev/null +++ b/Src/DDD.Core.NHibernate/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("DDD.Core.NHibernate")] +[assembly: AssemblyDescription("Implementation of core infrastucture components based on NHibernate.")] +[assembly: AssemblyProduct("DDD.Core.NHibernate")] +[assembly: Guid("d4fc7e1b-cae8-40f9-9faf-0029d91cc6c4")] \ No newline at end of file diff --git a/Src/DDD.Core.NHibernate/SqlServerEventMapping.cs b/Src/DDD.Core.NHibernate/SqlServerEventMapping.cs new file mode 100644 index 0000000..69d3a2b --- /dev/null +++ b/Src/DDD.Core.NHibernate/SqlServerEventMapping.cs @@ -0,0 +1,15 @@ +namespace DDD.Core.Infrastructure.Data +{ + public class SqlServerEventMapping : EventMapping + { + #region Constructors + + public SqlServerEventMapping() + { + // Fields + this.Property(e => e.Body, m => m.Column(m1 => m1.Length(8000))); + } + + #endregion Constructors + } +} diff --git a/Src/DDD.Core.NHibernate/UpperCaseNamingStrategy.cs b/Src/DDD.Core.NHibernate/UpperCaseNamingStrategy.cs new file mode 100644 index 0000000..a23719f --- /dev/null +++ b/Src/DDD.Core.NHibernate/UpperCaseNamingStrategy.cs @@ -0,0 +1,60 @@ +using NHibernate.Cfg; +using NHibernate.Util; + +namespace DDD.Core.Infrastructure.Data +{ + public class UpperCaseNamingStrategy : INamingStrategy + { + #region Fields + + /// + /// The singleton instance + /// + public static readonly INamingStrategy Instance = new UpperCaseNamingStrategy(); + + #endregion Fields + + #region Constructors + + private UpperCaseNamingStrategy() + { + } + + #endregion Constructors + + #region Methods + + public string ClassToTableName(string className) + { + return StringHelper.Unqualify(className).ToUpperInvariant(); + } + + public string ColumnName(string columnName) + { + return columnName.ToUpperInvariant(); + } + + public string LogicalColumnName(string columnName, string propertyName) + { + return StringHelper.IsNotEmpty(columnName) ? columnName : StringHelper.Unqualify(propertyName); + } + + public string PropertyToColumnName(string propertyName) + { + return StringHelper.Unqualify(propertyName).ToUpperInvariant(); + } + + public string PropertyToTableName(string className, string propertyName) + { + return StringHelper.Unqualify(propertyName).ToUpperInvariant(); + } + + public string TableName(string tableName) + { + return tableName.ToUpperInvariant(); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.NServiceBus/DDD.Core.NServiceBus.csproj b/Src/DDD.Core.NServiceBus/DDD.Core.NServiceBus.csproj deleted file mode 100644 index 611173d..0000000 --- a/Src/DDD.Core.NServiceBus/DDD.Core.NServiceBus.csproj +++ /dev/null @@ -1,79 +0,0 @@ - - - - - Debug - AnyCPU - {436D869F-7566-4436-90A8-B655145C5BCA} - Library - Properties - DDD.Core.Infrastructure.Messaging - DDD.Core.NServiceBus - v4.7.2 - 512 - true - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - 1591 - bin\Debug\DDD.Core.NServiceBus.xml - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\DDD.Core.NServiceBus.xml - 1591 - - - - L:\packages\Conditions.2.1.0\lib\portable-net45+win+wpa81+wp80+MonoAndroid10+MonoTouch10\Conditions.dll - - - L:\Packages\NServiceBus.7.1.6\lib\net452\NServiceBus.Core.dll - - - - - - - - - - Properties\CommonAssemblyInfo.cs - - - - - - - - - - - {596a8700-3d18-4a62-b200-1f78a9ea4617} - DDD.Core.Abstractions - - - {2438B31A-3A39-4878-81FA-BE5AE715EAE5} - DDD.Core.Messages - - - {c6c3e419-b9aa-44ad-9dbf-789294687ae6} - DDD.Core - - - - - - - \ No newline at end of file diff --git a/Src/DDD.Core.NServiceBus/EndpointConfigurationExtensions.cs b/Src/DDD.Core.NServiceBus/EndpointConfigurationExtensions.cs deleted file mode 100644 index a1d9d8c..0000000 --- a/Src/DDD.Core.NServiceBus/EndpointConfigurationExtensions.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Conditions; -using NServiceBus; - -namespace DDD.Core.Infrastructure.Messaging -{ - using Application; - using Domain; - - public static class EndpointConfigurationExtensions - { - - #region Methods - - /// - /// Uses custom marker interfaces to detect messages, commands and events. - /// - public static void UseCustomMarkerInterfaces(this EndpointConfiguration endpointConfiguration) - { - Condition.Requires(endpointConfiguration, nameof(endpointConfiguration)).IsNotNull(); - var conventions = endpointConfiguration.Conventions(); - conventions.DefiningMessagesAs( - type => - { - return typeof(IMessage).IsAssignableFrom(type) && - typeof(IMessage) != type && - typeof(IEvent) != type && - typeof(ICommand) != type; - }); - conventions.DefiningCommandsAs( - type => - { - return typeof(ICommand).IsAssignableFrom(type) && - typeof(ICommand) != type; - }); - conventions.DefiningEventsAs( - type => - { - return typeof(IEvent).IsAssignableFrom(type) && - typeof(IEvent) != type; - }); - } - - #endregion Methods - - } -} diff --git a/Src/DDD.Core.NServiceBus/NServiceBusEventPublisher.cs b/Src/DDD.Core.NServiceBus/NServiceBusEventPublisher.cs deleted file mode 100644 index 257d576..0000000 --- a/Src/DDD.Core.NServiceBus/NServiceBusEventPublisher.cs +++ /dev/null @@ -1,41 +0,0 @@ -using NServiceBus; -using System.Threading.Tasks; -using Conditions; - -namespace DDD.Core.Infrastructure.Messaging -{ - using Domain; - using Threading; - - public class NServiceBusEventPublisher : IAsyncEventPublisher - { - - #region Fields - - private readonly IMessageSession session; - - #endregion Fields - - #region Constructors - - public NServiceBusEventPublisher(IMessageSession session) - { - Condition.Requires(session, nameof(session)).IsNotNull(); - this.session = session; - } - - #endregion Constructors - - #region Methods - - public async Task PublishAsync(IEvent @event) - { - Condition.Requires(@event, nameof(@event)).IsNotNull(); - await new SynchronizationContextRemover(); - await this.session.Publish(@event); - } - - #endregion Methods - - } -} diff --git a/Src/DDD.Core.NServiceBus/Properties/AssemblyInfo.cs b/Src/DDD.Core.NServiceBus/Properties/AssemblyInfo.cs deleted file mode 100644 index e83bfc2..0000000 --- a/Src/DDD.Core.NServiceBus/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("DDD.Core.NServiceBus")] -[assembly: AssemblyDescription("Implementation of infrastucture components based on NServiceBus.")] -[assembly: AssemblyProduct("DDD.Core.NServiceBus")] -[assembly: Guid("436d869f-7566-4436-90a8-b655145c5bca")] \ No newline at end of file diff --git a/Src/DDD.Core.NServiceBus/packages.config b/Src/DDD.Core.NServiceBus/packages.config deleted file mode 100644 index 5d06d6c..0000000 --- a/Src/DDD.Core.NServiceBus/packages.config +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Src/DDD.Core.NSubstitute/DDD.Core.NSubstitute.csproj b/Src/DDD.Core.NSubstitute/DDD.Core.NSubstitute.csproj new file mode 100644 index 0000000..b42ec7b --- /dev/null +++ b/Src/DDD.Core.NSubstitute/DDD.Core.NSubstitute.csproj @@ -0,0 +1,14 @@ + + + net48;netstandard2.1 + Library + DDD.Core.Infrastructure.Testing + false + + + + + + + + diff --git a/Src/DDD.Core.NSubstitute/Properties/AssemblyInfo.cs b/Src/DDD.Core.NSubstitute/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a3e0df7 --- /dev/null +++ b/Src/DDD.Core.NSubstitute/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("DDD.Core.NSubstitute")] +[assembly: AssemblyDescription("Core components for testing with NSubstitute.")] +[assembly: AssemblyProduct("DDD.Core.NSubstitute")] +[assembly: Guid("843b8987-2bdf-4272-9565-139d6e77c39b")] \ No newline at end of file diff --git a/Src/DDD.Core.NSubstitute/SubstituteExtensions.cs b/Src/DDD.Core.NSubstitute/SubstituteExtensions.cs new file mode 100644 index 0000000..ae6d9c8 --- /dev/null +++ b/Src/DDD.Core.NSubstitute/SubstituteExtensions.cs @@ -0,0 +1,40 @@ +using System; +using System.Linq; +using System.Text; +using NSubstitute; +using NSubstitute.Core; +using NSubstitute.Exceptions; + +namespace DDD.Core.Infrastructure.Testing +{ + public static class SubstituteExtensions + { + + #region Methods + + /// + /// Checks this substitute has received a matching call the required number of times. + /// + /// Quick fix for that issue + public static T Received(this T substitute, Predicate predicate, int requiredNumberOfCalls = 1) where T : class + { + if (substitute == null) throw new NullSubstituteReferenceException(); + var numberOfMatchingCalls = substitute.ReceivedCalls().Count(c => predicate(c)); + if (numberOfMatchingCalls != requiredNumberOfCalls) + { + var message = new StringBuilder(); + message.AppendLine(string.Format("Expected to receive {0} {1} matching.", requiredNumberOfCalls, requiredNumberOfCalls == 1 ? "call" : "calls")); + if (numberOfMatchingCalls == 0) + message.AppendLine("Actually received no matching calls."); + else + message.AppendLine(string.Format("Actually received {0} matching {1}.", numberOfMatchingCalls, numberOfMatchingCalls == 1 ? "call" : "calls")); + throw new ReceivedCallsException(message.ToString()); + } + return substitute; + } + + #endregion Methods + + } + +} diff --git a/Src/DDD.Core.Newtonsoft/DDD.Core.Newtonsoft.csproj b/Src/DDD.Core.Newtonsoft/DDD.Core.Newtonsoft.csproj index 2116e45..e204863 100644 --- a/Src/DDD.Core.Newtonsoft/DDD.Core.Newtonsoft.csproj +++ b/Src/DDD.Core.Newtonsoft/DDD.Core.Newtonsoft.csproj @@ -1,67 +1,24 @@ - - - + - Debug - AnyCPU - {8BF8E0BD-B92C-4FC1-BE17-7C10036A2FE2} + net48;netstandard2.1 Library - Properties DDD.Core.Infrastructure.Serialization - DDD.Core.Newtonsoft - v4.7.2 - 512 - true - + false - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - L:\packages\Conditions.2.1.0\lib\portable-net45+win+wpa81+wp80+MonoAndroid10+MonoTouch10\Conditions.dll - - - L:\Packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll - - - - - - - - - - Properties\CommonAssemblyInfo.cs - - - + - - {596a8700-3d18-4a62-b200-1f78a9ea4617} - DDD.Core.Abstractions - + + + + 13.0.2 + + - \ No newline at end of file diff --git a/Src/DDD.Core.Newtonsoft/JsonSerializerWrapper.cs b/Src/DDD.Core.Newtonsoft/JsonSerializerWrapper.cs index 2ade331..29d11d2 100644 --- a/Src/DDD.Core.Newtonsoft/JsonSerializerWrapper.cs +++ b/Src/DDD.Core.Newtonsoft/JsonSerializerWrapper.cs @@ -1,7 +1,8 @@ -using Conditions; +using EnsureThat; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; +using System; using System.IO; using System.Text; @@ -14,31 +15,34 @@ public class JsonSerializerWrapper : IJsonSerializer #region Fields - private readonly JsonSerializer serializer; + private readonly JsonSerializerSettings settings; #endregion Fields #region Constructors public JsonSerializerWrapper() + : this(DefaultSettings(), JsonSerializationOptions.Encoding) { - this.serializer = DefaultSerializer(); } - private JsonSerializerWrapper(JsonSerializer serializer, Encoding encoding) + public JsonSerializerWrapper(JsonSerializerSettings settings, Encoding encoding) { - Condition.Requires(serializer, nameof(serializer)).IsNotNull(); - Condition.Requires(encoding, nameof(encoding)).IsNotNull(); - this.serializer = serializer; + Ensure.That(settings, nameof(settings)).IsNotNull(); + Ensure.That(encoding, nameof(encoding)).IsNotNull(); + this.settings = settings; + this.Encoding = encoding; } #endregion Constructors #region Properties - public Encoding Encoding { get; } = JsonSerializationOptions.Encoding; + public Encoding Encoding { get; } - public bool Indent => this.serializer.Formatting == Formatting.Indented; + public SerializationFormat Format => SerializationFormat.Json; + + public bool Indent => this.settings.Formatting == Formatting.Indented; #endregion Properties @@ -46,43 +50,62 @@ private JsonSerializerWrapper(JsonSerializer serializer, Encoding encoding) public static JsonSerializerWrapper Create(Encoding encoding, bool indent = true) { - Condition.Requires(encoding, nameof(encoding)).IsNotNull(); - var serializer = DefaultSerializer(); - serializer.Formatting = indent ? Formatting.Indented : Formatting.None; - return new JsonSerializerWrapper(serializer, encoding); + Ensure.That(encoding, nameof(encoding)).IsNotNull(); + var settings = DefaultSettings(); + settings.Formatting = indent ? Formatting.Indented : Formatting.None; + return new JsonSerializerWrapper(settings, encoding); } - public T Deserialize(Stream stream) + public static JsonSerializerWrapper Create(bool indent = true) => Create(JsonSerializationOptions.Encoding, indent); + + public object Deserialize(Stream stream, Type type) { - Condition.Requires(stream, nameof(stream)).IsNotNull(); - using (var reader = new StreamReader(stream, this.Encoding)) - using (var jsonReader = new JsonTextReader(reader)) + Ensure.That(stream, nameof(stream)).IsNotNull(); + Ensure.That(type, nameof(type)).IsNotNull(); + using (var streamReader = new StreamReader(stream, this.Encoding, true, 1024, true)) + using (var jsonReader = new JsonTextReader(streamReader)) { - return this.serializer.Deserialize(jsonReader); + var serializer = JsonSerializer.Create(this.settings); + try + { + return serializer.Deserialize(jsonReader, type); + } + catch (JsonException exception) + { + throw new SerializationException(type, exception); + } } } public void Serialize(Stream stream, object obj) { - Condition.Requires(stream, nameof(stream)).IsNotNull(); - using (StreamWriter writer = new StreamWriter(stream, this.Encoding)) - using (JsonTextWriter jsonWriter = new JsonTextWriter(writer)) + Ensure.That(stream, nameof(stream)).IsNotNull(); + using (var streamWriter = new StreamWriter(stream, this.Encoding, 1024, true)) + using (var jsonWriter = new JsonTextWriter(streamWriter)) { - this.serializer.Serialize(jsonWriter, obj); + var serializer = JsonSerializer.Create(this.settings); + try + { + serializer.Serialize(jsonWriter, obj); + } + catch (JsonException exception) + { + throw new SerializationException(obj?.GetType(), exception); + } } } - private static JsonSerializer DefaultSerializer() + private static JsonSerializerSettings DefaultSettings() { - var serializer = new JsonSerializer + var settings = new JsonSerializerSettings { Formatting = JsonSerializationOptions.Indent ? Formatting.Indented : Formatting.None, NullValueHandling = NullValueHandling.Ignore, ContractResolver = new CamelCasePropertyNamesContractResolver() }; - serializer.Converters.Add(new StringEnumConverter()); - return serializer; + settings.Converters.Add(new StringEnumConverter()); + return settings; } #endregion Methods diff --git a/Src/DDD.Core.Newtonsoft/Properties/AssemblyInfo.cs b/Src/DDD.Core.Newtonsoft/Properties/AssemblyInfo.cs index c85973e..8bdde48 100644 --- a/Src/DDD.Core.Newtonsoft/Properties/AssemblyInfo.cs +++ b/Src/DDD.Core.Newtonsoft/Properties/AssemblyInfo.cs @@ -2,6 +2,6 @@ using System.Runtime.InteropServices; [assembly: AssemblyTitle("DDD.Core.Newtonsoft")] -[assembly: AssemblyDescription("Implementation of infrastucture components based on the Newtonsoft library.")] +[assembly: AssemblyDescription("Core components for serialization with Newtonsoft.")] [assembly: AssemblyProduct("DDD.Core.Newtonsoft")] [assembly: Guid("8bf8e0bd-b92c-4fc1-be17-7c10036a2fe2")] \ No newline at end of file diff --git a/Src/DDD.Core.Newtonsoft/packages.config b/Src/DDD.Core.Newtonsoft/packages.config deleted file mode 100644 index dc81d73..0000000 --- a/Src/DDD.Core.Newtonsoft/packages.config +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Src/DDD.Core.Polly/AsyncPollyCommandHandler.cs b/Src/DDD.Core.Polly/AsyncPollyCommandHandler.cs new file mode 100644 index 0000000..37ed459 --- /dev/null +++ b/Src/DDD.Core.Polly/AsyncPollyCommandHandler.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; +using Polly; +using EnsureThat; + +namespace DDD.Core.Infrastructure.ErrorHandling +{ + using Application; + + /// + /// A decorator that applies a resilience policy to the asynchronous execution of a command. + /// + public class AsyncPollyCommandHandler : IAsyncCommandHandler + where TCommand : class, ICommand + { + + #region Fields + + private readonly IAsyncCommandHandler handler; + private readonly IAsyncPolicy policy; + + #endregion Fields + + #region Constructors + + public AsyncPollyCommandHandler(IAsyncCommandHandler handler, IAsyncPolicy policy) + { + Ensure.That(handler, nameof(handler)).IsNotNull(); + Ensure.That(policy, nameof(policy)).IsNotNull(); + this.handler = handler; + this.policy = policy; + } + + #endregion Constructors + + #region Methods + + public async Task HandleAsync(TCommand command, IMessageContext context) + { + await policy.ExecuteAsync(() => this.handler.HandleAsync(command, context)); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Polly/AsyncPollyCommandHandler`1.cs b/Src/DDD.Core.Polly/AsyncPollyCommandHandler`1.cs new file mode 100644 index 0000000..15fc25a --- /dev/null +++ b/Src/DDD.Core.Polly/AsyncPollyCommandHandler`1.cs @@ -0,0 +1,53 @@ +using System.Threading.Tasks; +using Polly; +using EnsureThat; + +namespace DDD.Core.Infrastructure.ErrorHandling +{ + using Application; + using Domain; + + /// + /// A decorator that applies a resilience policy to the asynchronous execution of a command. + /// + public class AsyncPollyCommandHandler : IAsyncCommandHandler + where TCommand : class, ICommand + where TContext : BoundedContext + { + + #region Fields + + private readonly IAsyncCommandHandler handler; + private readonly IAsyncPolicy policy; + + #endregion Fields + + #region Constructors + + public AsyncPollyCommandHandler(IAsyncCommandHandler handler, IAsyncPolicy policy) + { + Ensure.That(handler, nameof(handler)).IsNotNull(); + Ensure.That(policy, nameof(policy)).IsNotNull(); + this.handler = handler; + this.policy = policy; + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.handler.Context; + + #endregion Properties + + #region Methods + + public async Task HandleAsync(TCommand command, IMessageContext context) + { + await policy.ExecuteAsync(() => this.handler.HandleAsync(command, context)); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Polly/AsyncPollyQueryHandler.cs b/Src/DDD.Core.Polly/AsyncPollyQueryHandler.cs new file mode 100644 index 0000000..02f1449 --- /dev/null +++ b/Src/DDD.Core.Polly/AsyncPollyQueryHandler.cs @@ -0,0 +1,45 @@ +using Polly; +using EnsureThat; +using System.Threading.Tasks; + +namespace DDD.Core.Infrastructure.ErrorHandling +{ + using Application; + + /// + /// A decorator that applies a resilience policy to the asynchronous execution of a query. + /// + public class AsyncPollyQueryHandler : IAsyncQueryHandler + where TQuery : class, IQuery + { + + #region Fields + + private readonly IAsyncQueryHandler handler; + private readonly IAsyncPolicy policy; + + #endregion Fields + + #region Constructors + + public AsyncPollyQueryHandler(IAsyncQueryHandler handler, IAsyncPolicy policy) + { + Ensure.That(handler, nameof(handler)).IsNotNull(); + Ensure.That(policy, nameof(policy)).IsNotNull(); + this.handler = handler; + this.policy = policy; + } + + #endregion Constructors + + #region Methods + + public async Task HandleAsync(TQuery query, IMessageContext context) + { + return await policy.ExecuteAsync(() => this.handler.HandleAsync(query, context)); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Polly/AsyncPollyQueryHandler`1.cs b/Src/DDD.Core.Polly/AsyncPollyQueryHandler`1.cs new file mode 100644 index 0000000..ceb32f2 --- /dev/null +++ b/Src/DDD.Core.Polly/AsyncPollyQueryHandler`1.cs @@ -0,0 +1,53 @@ +using Polly; +using EnsureThat; +using System.Threading.Tasks; + +namespace DDD.Core.Infrastructure.ErrorHandling +{ + using Application; + using Domain; + + /// + /// A decorator that applies a resilience policy to the asynchronous execution of a query. + /// + public class AsyncPollyQueryHandler : IAsyncQueryHandler + where TQuery : class, IQuery + where TContext : BoundedContext + { + + #region Fields + + private readonly IAsyncQueryHandler handler; + private readonly IAsyncPolicy policy; + + #endregion Fields + + #region Constructors + + public AsyncPollyQueryHandler(IAsyncQueryHandler handler, IAsyncPolicy policy) + { + Ensure.That(handler, nameof(handler)).IsNotNull(); + Ensure.That(policy, nameof(policy)).IsNotNull(); + this.handler = handler; + this.policy = policy; + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.handler.Context; + + #endregion Properties + + #region Methods + + public async Task HandleAsync(TQuery query, IMessageContext context) + { + return await policy.ExecuteAsync(() => this.handler.HandleAsync(query, context)); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Polly/DDD.Core.Polly.csproj b/Src/DDD.Core.Polly/DDD.Core.Polly.csproj new file mode 100644 index 0000000..3a21cab --- /dev/null +++ b/Src/DDD.Core.Polly/DDD.Core.Polly.csproj @@ -0,0 +1,32 @@ + + + net48;netstandard2.1 + Library + DDD.Core.Infrastructure.ErrorHandling + false + + + bin\Debug\DDD.Core.Polly.xml + 1591 + + + bin\Release\DDD.Core.Polly.xml + 1591 + + + + Properties\CommonAssemblyInfo.cs + + + + + + + + + + + 7.2.3 + + + \ No newline at end of file diff --git a/Src/DDD.Core.Polly/Properties/AssemblyInfo.cs b/Src/DDD.Core.Polly/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..f618d70 --- /dev/null +++ b/Src/DDD.Core.Polly/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("DDD.Core.Polly")] +[assembly: AssemblyDescription("Core components for handling errors with Polly.")] +[assembly: AssemblyProduct("DDD.Core.Polly")] +[assembly: Guid("4878a4fb-2c1e-4c06-bb42-afe1ace2a882")] \ No newline at end of file diff --git a/Src/DDD.Core.Polly/SyncPollyCommandHandler.cs b/Src/DDD.Core.Polly/SyncPollyCommandHandler.cs new file mode 100644 index 0000000..f5ef48c --- /dev/null +++ b/Src/DDD.Core.Polly/SyncPollyCommandHandler.cs @@ -0,0 +1,44 @@ +using Polly; +using EnsureThat; + +namespace DDD.Core.Infrastructure.ErrorHandling +{ + using Application; + + /// + /// A decorator that applies a resilience policy to the synchronous execution of a command. + /// + public class SyncPollyCommandHandler : ISyncCommandHandler + where TCommand : class, ICommand + { + + #region Fields + + private readonly ISyncCommandHandler handler; + private readonly ISyncPolicy policy; + + #endregion Fields + + #region Constructors + + public SyncPollyCommandHandler(ISyncCommandHandler handler, ISyncPolicy policy) + { + Ensure.That(handler, nameof(handler)).IsNotNull(); + Ensure.That(policy, nameof(policy)).IsNotNull(); + this.handler = handler; + this.policy = policy; + } + + #endregion Constructors + + #region Methods + + public void Handle(TCommand command, IMessageContext context) + { + policy.Execute(() => this.handler.Handle(command, context)); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Polly/SyncPollyCommandHandler`1.cs b/Src/DDD.Core.Polly/SyncPollyCommandHandler`1.cs new file mode 100644 index 0000000..3bda959 --- /dev/null +++ b/Src/DDD.Core.Polly/SyncPollyCommandHandler`1.cs @@ -0,0 +1,52 @@ +using Polly; +using EnsureThat; + +namespace DDD.Core.Infrastructure.ErrorHandling +{ + using Application; + using Domain; + + /// + /// A decorator that applies a resilience policy to the synchronous execution of a command. + /// + public class SyncPollyCommandHandler : ISyncCommandHandler + where TCommand : class, ICommand + where TContext : BoundedContext + { + + #region Fields + + private readonly ISyncCommandHandler handler; + private readonly ISyncPolicy policy; + + #endregion Fields + + #region Constructors + + public SyncPollyCommandHandler(ISyncCommandHandler handler, ISyncPolicy policy) + { + Ensure.That(handler, nameof(handler)).IsNotNull(); + Ensure.That(policy, nameof(policy)).IsNotNull(); + this.handler = handler; + this.policy = policy; + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.handler.Context; + + #endregion Properties + + #region Methods + + public void Handle(TCommand command, IMessageContext context) + { + policy.Execute(() => this.handler.Handle(command, context)); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Polly/SyncPollyQueryHandler.cs b/Src/DDD.Core.Polly/SyncPollyQueryHandler.cs new file mode 100644 index 0000000..17ab25f --- /dev/null +++ b/Src/DDD.Core.Polly/SyncPollyQueryHandler.cs @@ -0,0 +1,44 @@ +using Polly; +using EnsureThat; + +namespace DDD.Core.Infrastructure.ErrorHandling +{ + using Application; + + /// + /// A decorator that applies a resilience policy to the synchronous execution of a query. + /// + public class SyncPollyQueryHandler : ISyncQueryHandler + where TQuery : class, IQuery + { + + #region Fields + + private readonly ISyncQueryHandler handler; + private readonly ISyncPolicy policy; + + #endregion Fields + + #region Constructors + + public SyncPollyQueryHandler(ISyncQueryHandler handler, ISyncPolicy policy) + { + Ensure.That(handler, nameof(handler)).IsNotNull(); + Ensure.That(policy, nameof(policy)).IsNotNull(); + this.handler = handler; + this.policy = policy; + } + + #endregion Constructors + + #region Methods + + public TResult Handle(TQuery query, IMessageContext context) + { + return policy.Execute(() => this.handler.Handle(query, context)); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Polly/SyncPollyQueryHandler`1.cs b/Src/DDD.Core.Polly/SyncPollyQueryHandler`1.cs new file mode 100644 index 0000000..d5d2061 --- /dev/null +++ b/Src/DDD.Core.Polly/SyncPollyQueryHandler`1.cs @@ -0,0 +1,52 @@ +using Polly; +using EnsureThat; + +namespace DDD.Core.Infrastructure.ErrorHandling +{ + using Application; + using Domain; + + /// + /// A decorator that applies a resilience policy to the synchronous execution of a query. + /// + public class SyncPollyQueryHandler : ISyncQueryHandler + where TQuery : class, IQuery + where TContext : BoundedContext + { + + #region Fields + + private readonly ISyncQueryHandler handler; + private readonly ISyncPolicy policy; + + #endregion Fields + + #region Constructors + + public SyncPollyQueryHandler(IQueryHandler handler, ISyncPolicy policy) + { + Ensure.That(handler, nameof(handler)).IsNotNull(); + Ensure.That(policy, nameof(policy)).IsNotNull(); + this.handler = handler; + this.policy = policy; + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.handler.Context; + + #endregion Properties + + #region Methods + + public TResult Handle(TQuery query, IMessageContext context) + { + return policy.Execute(() => this.handler.Handle(query, context)); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.SimpleInjector.FluentValidation/AppRegistrationOptionsBuilderExtensions.cs b/Src/DDD.Core.SimpleInjector.FluentValidation/AppRegistrationOptionsBuilderExtensions.cs new file mode 100644 index 0000000..cdb9224 --- /dev/null +++ b/Src/DDD.Core.SimpleInjector.FluentValidation/AppRegistrationOptionsBuilderExtensions.cs @@ -0,0 +1,34 @@ +using EnsureThat; +using FluentValidation; +using SimpleInjector; +using DDD.Validation; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Validation; + + public static class AppRegistrationOptionsBuilderExtensions + { + + #region Methods + + public static AppRegistrationOptions.Builder ConfigureValidation(this AppRegistrationOptions.Builder builder) + { + Ensure.That(builder, nameof(builder)).IsNotNull(); + var extendableBuilder = (IExtendableRegistrationOptionsBuilder)builder; + var appOptions = extendableBuilder.Build(); + extendableBuilder.AddExtension(container => RegisterValidators(container, appOptions)); + return builder; + } + + private static void RegisterValidators(Container container, AppRegistrationOptions options) + { + container.RegisterConditional(typeof(IValidator<>), options.AssembliesToScan, options.TypeFilter); + container.Register(typeof(ISyncObjectValidator<>), typeof(FluentValidatorAdapter<>)); + container.Register(typeof(IAsyncObjectValidator<>), typeof(FluentValidatorAdapter<>)); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.SimpleInjector.FluentValidation/DDD.Core.SimpleInjector.FluentValidation.csproj b/Src/DDD.Core.SimpleInjector.FluentValidation/DDD.Core.SimpleInjector.FluentValidation.csproj new file mode 100644 index 0000000..7198321 --- /dev/null +++ b/Src/DDD.Core.SimpleInjector.FluentValidation/DDD.Core.SimpleInjector.FluentValidation.csproj @@ -0,0 +1,25 @@ + + + net48;netstandard2.1 + Library + DDD.Core.Infrastructure.DependencyInjection + false + + + bin\Debug\DDD.Core.SimpleInjector.FluentValidation.xml + 1591 + + + 1591 + bin\Release\DDD.Core.SimpleInjector.FluentValidation.xml + + + + Properties\CommonAssemblyInfo.cs + + + + + + + \ No newline at end of file diff --git a/Src/DDD.Core.SimpleInjector.FluentValidation/Properties/AssemblyInfo.cs b/Src/DDD.Core.SimpleInjector.FluentValidation/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..61f87b2 --- /dev/null +++ b/Src/DDD.Core.SimpleInjector.FluentValidation/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("DDD.Core.SimpleInjector.FluentValidation")] +[assembly: AssemblyDescription("Extensions for SimpleInjector to register validation components based on FluentValidation.")] +[assembly: AssemblyProduct("DDD.Core.SimpleInjector.FluentValidation")] +[assembly: Guid("8ee573d6-1f32-44a8-b121-82de7f6c66d5")] diff --git a/Src/DDD.Core.SimpleInjector.NHibernate/AppRegistrationOptionsBuilderExtensions.cs b/Src/DDD.Core.SimpleInjector.NHibernate/AppRegistrationOptionsBuilderExtensions.cs new file mode 100644 index 0000000..9e12d22 --- /dev/null +++ b/Src/DDD.Core.SimpleInjector.NHibernate/AppRegistrationOptionsBuilderExtensions.cs @@ -0,0 +1,64 @@ +using EnsureThat; +using NHibernate; +using NHibernate.Cfg; +using SimpleInjector; +using System; +using System.Linq; +using System.Data.Common; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Domain; + using Data; + + public static class AppRegistrationOptionsBuilderExtensions + { + + #region Methods + + public static AppRegistrationOptions.Builder RegisterSessionFactoryFor(this AppRegistrationOptions.Builder builder, + Configuration configuration, + Action options = null) + where TContext : BoundedContext + { + Ensure.That(builder, nameof(builder)).IsNotNull(); + Ensure.That(configuration, nameof(configuration)).IsNotNull(); + options = options ?? (_ => { }); + var extendableBuilder = (IExtendableRegistrationOptionsBuilder)builder; + var appOptions = extendableBuilder.Build(); + extendableBuilder.AddExtension(container => RegisterSessionFactoryFor(container, configuration, options)); + return builder; + } + + private static void RegisterSessionFactoryFor(Container container, Configuration configuration, Action options) + where TContext : BoundedContext + { + container.RegisterSingleton>(() => + { + var context = container.GetInstance(); + return new DelegatingSessionFactory + ( + context, + configuration, + sessionBuilder => + { + var connectionProvider = container.GetInstance>(); + var connection = connectionProvider.GetOpenConnection(); + sessionBuilder.Connection(connection); + options(sessionBuilder); + }, + async (sessionBuilder, cancellationToken) => + { + var connectionProvider = container.GetInstance>(); + var connection = await connectionProvider.GetOpenConnectionAsync(cancellationToken); + sessionBuilder.Connection(connection); + options(sessionBuilder); + } + ); + }); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.SimpleInjector.NHibernate/DDD.Core.SimpleInjector.NHibernate.csproj b/Src/DDD.Core.SimpleInjector.NHibernate/DDD.Core.SimpleInjector.NHibernate.csproj new file mode 100644 index 0000000..c01d0bd --- /dev/null +++ b/Src/DDD.Core.SimpleInjector.NHibernate/DDD.Core.SimpleInjector.NHibernate.csproj @@ -0,0 +1,15 @@ + + + net48;netstandard2.1 + Library + DDD.Core.Infrastructure.DependencyInjection + false + + + + + + + + + \ No newline at end of file diff --git a/Src/DDD.Core.SimpleInjector.NHibernate/Properties/AssemblyInfo.cs b/Src/DDD.Core.SimpleInjector.NHibernate/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..0da677b --- /dev/null +++ b/Src/DDD.Core.SimpleInjector.NHibernate/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("DDD.Core.SimpleInjector.NHibernate")] +[assembly: AssemblyDescription("Extensions for SimpleInjector to register database access components based on NHibernate.")] +[assembly: AssemblyProduct("DDD.Core.SimpleInjector.NHibernate")] +[assembly: Guid("dd36dcb1-29e3-455d-841e-252fd650ebdf")] \ No newline at end of file diff --git a/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs new file mode 100644 index 0000000..7eec107 --- /dev/null +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs @@ -0,0 +1,102 @@ +using SimpleInjector; +using SimpleInjector.Lifestyles; +using EnsureThat; +using System; +using System.Transactions; +using System.Threading.Tasks; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Application; + using Threading; + + /// + /// A decorator that defines a scope around the asynchronous execution of a command. + /// In the context of event handling, this decorator updates the position of event stream in the same transaction as the command. + /// + public class AsyncScopedCommandHandler : IAsyncCommandHandler + where TCommand : class, ICommand + { + + #region Fields + + private readonly Container container; + private readonly Func> handlerProvider; + + #endregion Fields + + #region Constructors + + public AsyncScopedCommandHandler(Func> handlerProvider, + Container container) + { + Ensure.That(handlerProvider, nameof(handlerProvider)).IsNotNull(); + Ensure.That(container, nameof(container)).IsNotNull(); + this.handlerProvider = handlerProvider; + this.container = container; + } + + #endregion Constructors + + #region Methods + + public async Task HandleAsync(TCommand command, IMessageContext context) + { + Ensure.That(context, nameof(context)).IsNotNull(); + await new SynchronizationContextRemover(); + using (AsyncScopedLifestyle.BeginScope(container)) + { + var handler = this.handlerProvider(); + if (context.IsEventHandling()) // Exception to the rule "One transaction per command" to avoid to handle the same event more than once + { + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + await handler.HandleAsync(command, context); + await UpdateEventStreamPositionAsync(context); + scope.Complete(); + } + } + else + await handler.HandleAsync(command, context); + } + } + + private async Task UpdateEventStreamPositionAsync(IMessageContext context) + { + var @event = context.Event(); + var boundedContext = context.BoundedContext(); + var stream = context.Stream(); + if (stream != null) + { + var command = new UpdateEventStreamPosition + { + Position = @event.EventId, + Type = stream.Type, + Source = stream.Source + }; + var handlerType = typeof(IAsyncCommandHandler<,>).MakeGenericType(typeof(UpdateEventStreamPosition), boundedContext.GetType()); + dynamic handler = this.container.GetInstance(handlerType); + await handler.HandleAsync(command, context); + stream.Position = @event.EventId; + } + else + { + var failedStream = context.FailedStream(); + var command = new UpdateFailedEventStreamPosition + { + Position = @event.EventId, + Id = failedStream.StreamId, + Type = failedStream.StreamType, + Source = failedStream.StreamSource + }; + var handlerType = typeof(IAsyncCommandHandler<,>).MakeGenericType(typeof(UpdateFailedEventStreamPosition), boundedContext.GetType()); + dynamic handler = this.container.GetInstance(handlerType); + await handler.HandleAsync(command, context); + failedStream.StreamPosition = @event.EventId; + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler`1.cs b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler`1.cs new file mode 100644 index 0000000..65f08e6 --- /dev/null +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler`1.cs @@ -0,0 +1,61 @@ +using SimpleInjector; +using SimpleInjector.Lifestyles; +using EnsureThat; +using System; +using System.Threading.Tasks; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Application; + using Domain; + using Threading; + + /// + /// A decorator that defines a scope around the asynchronous execution of a command. + /// + public class AsyncScopedCommandHandler : IAsyncCommandHandler + where TCommand : class, ICommand + where TContext : BoundedContext + { + + #region Fields + + private readonly Container container; + private readonly Func> handlerProvider; + + #endregion Fields + + #region Constructors + + public AsyncScopedCommandHandler(Func> handlerProvider, Container container) + { + Ensure.That(handlerProvider, nameof(handlerProvider)).IsNotNull(); + Ensure.That(container, nameof(container)).IsNotNull(); + this.handlerProvider = handlerProvider; + this.container = container; + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.handlerProvider().Context; + + #endregion Properties + + #region Methods + + public async Task HandleAsync(TCommand command, IMessageContext context) + { + await new SynchronizationContextRemover(); + using (AsyncScopedLifestyle.BeginScope(container)) + { + var handler = this.handlerProvider(); + await handler.HandleAsync(command, context); + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs b/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs new file mode 100644 index 0000000..66365c4 --- /dev/null +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs @@ -0,0 +1,53 @@ +using SimpleInjector; +using SimpleInjector.Lifestyles; +using EnsureThat; +using System; +using System.Threading.Tasks; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Application; + using Threading; + + /// + /// A decorator that defines a scope around the asynchronous execution of a query. + /// + public class AsyncScopedQueryHandler : IAsyncQueryHandler + where TQuery : class, IQuery + { + + #region Fields + + private readonly Container container; + private readonly Func> handlerProvider; + + #endregion Fields + + #region Constructors + + public AsyncScopedQueryHandler(Func> handlerProvider, Container container) + { + Ensure.That(handlerProvider, nameof(handlerProvider)).IsNotNull(); + Ensure.That(container, nameof(container)).IsNotNull(); + this.handlerProvider = handlerProvider; + this.container = container; + } + + #endregion Constructors + + #region Methods + + public async Task HandleAsync(TQuery query, IMessageContext context) + { + await new SynchronizationContextRemover(); + using (AsyncScopedLifestyle.BeginScope(container)) + { + var handler = this.handlerProvider(); + return await handler.HandleAsync(query, context); + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler`1.cs b/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler`1.cs new file mode 100644 index 0000000..8044e3e --- /dev/null +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler`1.cs @@ -0,0 +1,61 @@ +using SimpleInjector; +using SimpleInjector.Lifestyles; +using EnsureThat; +using System; +using System.Threading.Tasks; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Application; + using Domain; + using Threading; + + /// + /// A decorator that defines a scope around the asynchronous execution of a query. + /// + public class AsyncScopedQueryHandler : IAsyncQueryHandler + where TQuery : class, IQuery + where TContext : BoundedContext + { + + #region Fields + + private readonly Container container; + private readonly Func> handlerProvider; + + #endregion Fields + + #region Constructors + + public AsyncScopedQueryHandler(Func> handlerProvider, Container container) + { + Ensure.That(handlerProvider, nameof(handlerProvider)).IsNotNull(); + Ensure.That(container, nameof(container)).IsNotNull(); + this.handlerProvider = handlerProvider; + this.container = container; + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.handlerProvider().Context; + + #endregion Properties + + #region Methods + + public async Task HandleAsync(TQuery query, IMessageContext context) + { + await new SynchronizationContextRemover(); + using (AsyncScopedLifestyle.BeginScope(container)) + { + var handler = this.handlerProvider(); + return await handler.HandleAsync(query, context); + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs new file mode 100644 index 0000000..b26ac37 --- /dev/null +++ b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs @@ -0,0 +1,340 @@ +using EnsureThat; +using Microsoft.Extensions.Logging; +using SimpleInjector; +using SimpleInjector.Lifestyles; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Linq; +using DDD.Serialization; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Mapping; + using Application; + using Domain; + using DDD; + using DDD.Core.Infrastructure.Data; + + public static class ContainerExtensions + { + + #region Methods + + /// + /// Gets the decorator chain associated with a specified service. + /// + public static IEnumerable GetDecoratorChainOf(this Container container, bool fromDecorated = true) + where TService : class + { + Ensure.That(container, nameof(container)).IsNotNull(); + var chain = new List(); + if (container.TryGetInstance(out _)) + { + var serviceTypes = new[] + { + typeof(TService), + typeof(Func) + }; + var registration = container.GetCurrentRegistrations().FirstOrDefault(r => serviceTypes.Contains(r.ServiceType))?.Registration; + if (registration != null) + { + if (!serviceTypes.Contains(registration.ImplementationType)) + chain.Add(new RegisteredType(registration.ImplementationType, registration.Lifestyle)); + registration = registration.GetRelationships().FirstOrDefault(r => serviceTypes.Contains(r.Dependency.ServiceType))?.Dependency?.Registration; + while (registration != null) + { + if (!serviceTypes.Contains(registration.ImplementationType)) + chain.Add(new RegisteredType(registration.ImplementationType, registration.Lifestyle)); + registration = registration.GetRelationships().FirstOrDefault(r => serviceTypes.Contains(r.Dependency.ServiceType))?.Dependency?.Registration; + } + } + if (fromDecorated) + chain.Reverse(); + } + return chain; + } + + /// + /// Configures application core components. + /// + public static void ConfigureApp(this Container container, Action> configureOptions) + { + Ensure.That(container, nameof(container)).IsNotNull(); + Ensure.That(configureOptions, nameof(configureOptions)).IsNotNull(); + var builder = new AppRegistrationOptions.Builder(); + var extendableBuilder = (IExtendableRegistrationOptionsBuilder)builder; + configureOptions(builder); + var appOptions = extendableBuilder.Build(); + container.Options.DefaultScopedLifestyle = Lifestyle.CreateHybrid(new ThreadScopedLifestyle(), new AsyncScopedLifestyle()); + container.RegisterInstance(container); + container.RegisterLogger(appOptions); + container.RegisterBoundedContexts(appOptions); + container.RegisterSerializers(appOptions); + foreach (var dbConnectionOptions in appOptions.DbConnectionOptionsCollection) + container.RegisterDbConnectionProvider(dbConnectionOptions); + var commandOptions = appOptions.CommandsRegistrationOptions; + if (commandOptions != null) + { + var settings = new CommandProcessorSettings(commandOptions.DefaultValidator); + container.RegisterInstance(settings); + container.RegisterSingleton(); + container.RegisterCommandHandlers(appOptions); + container.RegisterRepositories(appOptions); + container.RegisterScheduleFactories(commandOptions); + foreach (var managerOptions in commandOptions.ManagerOptionsCollection) + container.RegisterRecurringCommandManager(managerOptions); + } + var queryOptions = appOptions.QueriesRegistrationOptions; + if (queryOptions != null) + { + var settings = new QueryProcessorSettings(queryOptions.DefaultValidator); + container.RegisterInstance(settings); + container.RegisterSingleton(); + container.RegisterQueryHandlers(appOptions); + } + var eventOptions = appOptions.EventsRegistrationOptions; + if (eventOptions != null) + { + container.RegisterSingleton(typeof(IEventPublisher<>), typeof(EventPublisher<>)); + container.RegisterEventHandlers(appOptions); + container.RegisterEventSerializer(eventOptions); + foreach (var consumerOptions in eventOptions.ConsumerOptionsCollection) + container.RegisterEventConsumer(consumerOptions); + } + var mappingOptions = appOptions.MappingRegistrationOptions; + if (mappingOptions != null) + { + var settings = new MappingProcessorSettings(mappingOptions.DefaultMapper, mappingOptions.DefaultTranslator); + container.RegisterInstance(settings); + container.RegisterSingleton(); + container.RegisterMappersAndTranslators(appOptions); + } + extendableBuilder.ApplyExtensions(container); + } + + public static void RegisterConditional(this Container container, + Func instanceCreator, + Lifestyle lifestyle, + Predicate predicate) + where TService : class + { + Ensure.That(container, nameof(container)).IsNotNull(); + var registration = lifestyle.CreateRegistration(instanceCreator, container); + container.RegisterConditional(registration, predicate); + } + + public static void RegisterConditional(this Container container, Func instanceCreator, Predicate predicate) + where TService : class + { + Ensure.That(container, nameof(container)).IsNotNull(); + container.RegisterConditional(instanceCreator, Lifestyle.Transient, predicate); + } + + public static void RegisterConditional(this Container container, + Type serviceType, + IEnumerable assemblies, + Lifestyle lifestyle, + Func predicate, + TypesToRegisterOptions options) + { + Ensure.That(container, nameof(container)).IsNotNull(); + var implementationTypes = container.GetTypesToRegister(serviceType, assemblies, options) + .Where(predicate); + var nonGenericTypeDefinitions = implementationTypes.Where(t => t.IsGenericTypeDefinition == false); + container.Register(serviceType, nonGenericTypeDefinitions, lifestyle); + var genericTypeDefinitions = implementationTypes.Where(t => t.IsGenericTypeDefinition == true); + foreach (var genericTypeDefinition in genericTypeDefinitions) + container.Register(serviceType, genericTypeDefinition, lifestyle); + } + + public static void RegisterConditional(this Container container, + Type serviceType, + IEnumerable assemblies, + Lifestyle lifestyle, + Func predicate) + { + Ensure.That(container, nameof(container)).IsNotNull(); + container.RegisterConditional(serviceType, assemblies, lifestyle, predicate, new TypesToRegisterOptions()); + } + + public static void RegisterConditional(this Container container, + Type serviceType, + IEnumerable assemblies, + Func predicate) + { + Ensure.That(container, nameof(container)).IsNotNull(); + container.RegisterConditional(serviceType, assemblies, Lifestyle.Transient, predicate); + } + + public static void RegisterConditional(this Container container, + Type serviceType, + IEnumerable assemblies, + Func predicate, + TypesToRegisterOptions options) + { + Ensure.That(container, nameof(container)).IsNotNull(); + container.RegisterConditional(serviceType, assemblies, Lifestyle.Transient, predicate, options); + } + + /// + /// Tries to get an instance of the specified service. + /// + public static bool TryGetInstance(this Container container, out TService instance) + where TService : class + { + Ensure.That(container, nameof(container)).IsNotNull(); + IServiceProvider provider = container; + instance = (TService)provider.GetService(typeof(TService)); + return instance != null; + } + + private static void RegisterBoundedContexts(this Container container, AppRegistrationOptions options) + { + var contextTypes = container.GetTypesToRegister(options.AssembliesToScan) + .Where(options.TypeFilter); + foreach (var contextType in contextTypes) + { + container.RegisterSingleton(contextType, contextType); + container.Collection.Append(typeof(BoundedContext), contextType, Lifestyle.Singleton); + } + } + + private static void RegisterCommandHandlers(this Container container, AppRegistrationOptions options) + { + var registerOptions = new TypesToRegisterOptions { IncludeGenericTypeDefinitions = true }; + container.RegisterConditional(typeof(ISyncCommandHandler<,>), options.AssembliesToScan, options.TypeFilter, registerOptions); + container.RegisterDecorator(typeof(ISyncCommandHandler<,>), typeof(SyncCommandHandlerWithLogging<,>)); + container.RegisterDecorator(typeof(ISyncCommandHandler<,>), typeof(ThreadScopedCommandHandler<,>), Lifestyle.Singleton, MustBeDecoratedWithScope); + container.RegisterConditional(typeof(IAsyncCommandHandler<,>), options.AssembliesToScan, options.TypeFilter, registerOptions); + container.RegisterDecorator(typeof(IAsyncCommandHandler<,>), typeof(AsyncCommandHandlerWithLogging<,>)); + container.RegisterDecorator(typeof(IAsyncCommandHandler<,>), typeof(AsyncScopedCommandHandler<,>), Lifestyle.Singleton, MustBeDecoratedWithScope); + container.RegisterConditional(typeof(ISyncCommandHandler<>), options.AssembliesToScan, options.TypeFilter); + container.RegisterDecorator(typeof(ISyncCommandHandler<>), typeof(SyncCommandHandlerWithLogging<>)); + container.RegisterDecorator(typeof(ISyncCommandHandler<>), typeof(ThreadScopedCommandHandler<>), Lifestyle.Singleton); + container.RegisterConditional(typeof(IAsyncCommandHandler<>), options.AssembliesToScan, options.TypeFilter); + container.RegisterDecorator(typeof(IAsyncCommandHandler<>), typeof(AsyncCommandHandlerWithLogging<>)); + container.RegisterDecorator(typeof(IAsyncCommandHandler<>), typeof(AsyncScopedCommandHandler<>), Lifestyle.Singleton); + } + + private static void RegisterEventConsumer(this Container container, EventConsumerOptions options) + { + if (string.IsNullOrWhiteSpace(options.ContextType)) + throw new InvalidOperationException($"Context type not specified for an event consumer."); + var contextType = Type.GetType(options.ContextType); + var settingsType = typeof(EventConsumerSettings<>).MakeGenericType(contextType); + var settings = Activator.CreateInstance(settingsType, TimeSpan.FromSeconds(options.ConsumationDelay), options.ConsumationMax); + var consumerServiceType = typeof(IEventConsumer<>).MakeGenericType(contextType); + var consumerImplementationType = typeof(EventConsumer<>).MakeGenericType(contextType); + container.RegisterInstance(settingsType, settings); + container.RegisterSingleton(consumerServiceType, consumerImplementationType); + container.Collection.Append(typeof(IEventConsumer), consumerImplementationType, Lifestyle.Singleton); + } + + private static void RegisterEventHandlers(this Container container, AppRegistrationOptions options) + { + container.RegisterConditional(typeof(ISyncEventHandler<,>), options.AssembliesToScan, options.TypeFilter); + container.RegisterConditional(typeof(IAsyncEventHandler<,>), options.AssembliesToScan, options.TypeFilter); + container.RegisterDecorator(typeof(ISyncEventHandler<,>), typeof(SyncEventHandlerWithLogging<,>)); + container.RegisterDecorator(typeof(IAsyncEventHandler<,>), typeof(AsyncEventHandlerWithLogging<,>)); + } + + private static void RegisterEventSerializer(this Container container, EventsRegistrationOptions options) + { + container.RegisterConditional(() => container.GetAllInstances().First(s => s.Format == options.CurrentSerializationFormat), + Lifestyle.Singleton, + c => c.HasConsumer ? c.Consumer.ImplementationType == typeof(EventTranslator) : true); + } + + private static void RegisterDbConnectionProvider(this Container container, DbConnectionOptions options) + { + if (string.IsNullOrWhiteSpace(options.ContextType)) + throw new InvalidOperationException($"Context type not specified for a database connection provider."); + var contextType = Type.GetType(options.ContextType); + var settingsType = typeof(DbConnectionSettings<>).MakeGenericType(contextType); + var settings = Activator.CreateInstance(settingsType, options.ProviderName, options.ConnectionString); + var providerServiceType = typeof(IDbConnectionProvider<>).MakeGenericType(contextType); + var providerImplementationType = typeof(LazyDbConnectionProvider<>).MakeGenericType(contextType); + container.RegisterInstance(settingsType, settings); + container.Register(providerServiceType, providerImplementationType, Lifestyle.Scoped); + } + + private static void RegisterLogger(this Container container, AppRegistrationOptions options) + { + container.RegisterSingleton(options.loggerFactoryCreator); + container.RegisterConditional(typeof(ILogger), + c => typeof(Logger<>).MakeGenericType(c.Consumer.ImplementationType), + Lifestyle.Singleton, + _ => true); + container.RegisterSingleton(typeof(ILogger<>), typeof(Logger<>)); + } + + private static void RegisterMappersAndTranslators(this Container container, AppRegistrationOptions options) + { + container.RegisterConditional(typeof(IObjectMapper<,>), options.AssembliesToScan, options.TypeFilter); + container.RegisterConditional(typeof(IObjectTranslator<,>), options.AssembliesToScan, options.TypeFilter); + } + + private static void RegisterQueryHandlers(this Container container, AppRegistrationOptions options) + { + var registerOptions = new TypesToRegisterOptions { IncludeGenericTypeDefinitions = true }; + container.RegisterConditional(typeof(ISyncQueryHandler<,,>), options.AssembliesToScan, options.TypeFilter, registerOptions); + container.RegisterDecorator(typeof(ISyncQueryHandler<,,>), typeof(SyncQueryHandlerWithLogging<,,>)); + container.RegisterDecorator(typeof(ISyncQueryHandler<,,>), typeof(ThreadScopedQueryHandler<,,>), Lifestyle.Singleton); + container.RegisterConditional(typeof(IAsyncQueryHandler<,,>), options.AssembliesToScan, options.TypeFilter, registerOptions); + container.RegisterDecorator(typeof(IAsyncQueryHandler<,,>), typeof(AsyncQueryHandlerWithLogging<,,>)); + container.RegisterDecorator(typeof(IAsyncQueryHandler<,,>), typeof(AsyncScopedQueryHandler<,,>), Lifestyle.Singleton); + container.RegisterConditional(typeof(ISyncQueryHandler<,>), options.AssembliesToScan, options.TypeFilter); + container.RegisterDecorator(typeof(ISyncQueryHandler<,>), typeof(SyncQueryHandlerWithLogging<,>)); + container.RegisterDecorator(typeof(ISyncQueryHandler<,>), typeof(ThreadScopedQueryHandler<,>), Lifestyle.Singleton); + container.RegisterConditional(typeof(IAsyncQueryHandler<,>), options.AssembliesToScan, options.TypeFilter); + container.RegisterDecorator(typeof(IAsyncQueryHandler<,>), typeof(AsyncQueryHandlerWithLogging<,>)); + container.RegisterDecorator(typeof(IAsyncQueryHandler<,>), typeof(AsyncScopedQueryHandler<,>), Lifestyle.Singleton); + } + + private static void RegisterRecurringCommandManager(this Container container, RecurringCommandManagerOptions options) + { + if (string.IsNullOrWhiteSpace(options.ContextType)) + throw new InvalidOperationException($"Context type not specified for a recurring command manager."); + var contextType = Type.GetType(options.ContextType); + var settingsType = typeof(RecurringCommandManagerSettings<>).MakeGenericType(contextType); + var settings = Activator.CreateInstance(settingsType, options.CurrentSerializationFormat, options.CurrentExpressionFormat); + var managerServiceType = typeof(IRecurringCommandManager<>).MakeGenericType(contextType); + var managerImplementationType = typeof(RecurringCommandManager<>).MakeGenericType(contextType); + container.RegisterInstance(settingsType, settings); + container.RegisterSingleton(managerServiceType, managerImplementationType); + container.Collection.Append(typeof(IRecurringCommandManager), managerImplementationType, Lifestyle.Singleton); + } + + private static void RegisterRepositories(this Container container, AppRegistrationOptions options) + { + container.RegisterConditional(typeof(ISyncRepository<,>), options.AssembliesToScan, Lifestyle.Scoped, options.TypeFilter); + container.RegisterConditional(typeof(IAsyncRepository<,>), options.AssembliesToScan, Lifestyle.Scoped, options.TypeFilter); + } + + private static void RegisterSerializers(this Container container, AppRegistrationOptions options) + { + foreach (var serializer in options.Serializers) + container.Collection.Append(serializer, Lifestyle.Singleton); + } + + private static void RegisterScheduleFactories(this Container container, CommandsRegistrationOptions options) + { + foreach (var scheduleFactory in options.SchedulesFactories) + container.Collection.Append(scheduleFactory, Lifestyle.Singleton); + } + + private static bool MustBeDecoratedWithScope(DecoratorPredicateContext context) + { + var commandType = context.ServiceType.GenericTypeArguments[0]; + if (commandType == typeof(UpdateEventStreamPosition)) + return false; + if (commandType == typeof(UpdateFailedEventStreamPosition)) + return false; + return true; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj b/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj new file mode 100644 index 0000000..d69a2be --- /dev/null +++ b/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj @@ -0,0 +1,36 @@ + + + net48;netstandard2.1 + Library + DDD.Core.Infrastructure.DependencyInjection + false + + + bin\Debug\DDD.Core.SimpleInjector.xml + 1591 + + + 1591 + bin\Release\DDD.Core.SimpleInjector.xml + + + + Properties\CommonAssemblyInfo.cs + + + + + + + + + + + + 5.4.1 + + + + + + \ No newline at end of file diff --git a/Src/DDD.Core.SimpleInjector/Properties/AssemblyInfo.cs b/Src/DDD.Core.SimpleInjector/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..043d4e5 --- /dev/null +++ b/Src/DDD.Core.SimpleInjector/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("DDD.Core.SimpleInjector")] +[assembly: AssemblyDescription("Core components for dependency injection with SimpleInjector.")] +[assembly: AssemblyProduct("DDD.Core.SimpleInjector")] +[assembly: Guid("3efaacd8-cf5e-4e31-884b-6b9f87f1e495")] \ No newline at end of file diff --git a/Src/DDD.Core.SimpleInjector/RegisteredType.cs b/Src/DDD.Core.SimpleInjector/RegisteredType.cs new file mode 100644 index 0000000..1d2995a --- /dev/null +++ b/Src/DDD.Core.SimpleInjector/RegisteredType.cs @@ -0,0 +1,31 @@ +using System; +using SimpleInjector; +using EnsureThat; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + public class RegisteredType + { + + #region Constructors + + public RegisteredType(Type type, Lifestyle lifestyle) + { + Ensure.That(type, nameof(type)).IsNotNull(); + Ensure.That(lifestyle, nameof(lifestyle)).IsNotNull(); + this.Type = type; + this.Lifestyle = lifestyle; + } + + #endregion Constructors + + #region Properties + + public Type Type { get; } + + public Lifestyle Lifestyle { get; } + + #endregion Properties + + } +} diff --git a/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs b/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs new file mode 100644 index 0000000..f73215d --- /dev/null +++ b/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs @@ -0,0 +1,99 @@ +using SimpleInjector; +using SimpleInjector.Lifestyles; +using EnsureThat; +using System; +using System.Transactions; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Application; + + /// + /// A decorator that defines a scope around the synchronous execution of a command. + /// In the context of event handling, this decorator updates the position of event stream in the same transaction as the command. + /// + public class ThreadScopedCommandHandler : ISyncCommandHandler + where TCommand : class, ICommand + { + + #region Fields + + private readonly Container container; + private readonly Func> handlerProvider; + + #endregion Fields + + #region Constructors + + public ThreadScopedCommandHandler(Func> handlerProvider, + Container container) + { + Ensure.That(handlerProvider, nameof(handlerProvider)).IsNotNull(); + Ensure.That(container, nameof(container)).IsNotNull(); + this.handlerProvider = handlerProvider; + this.container = container; + } + + #endregion Constructors + + #region Methods< + + public void Handle(TCommand command, IMessageContext context) + { + Ensure.That(context, nameof(context)).IsNotNull(); + using (ThreadScopedLifestyle.BeginScope(container)) + { + var handler = this.handlerProvider(); + if (context.IsEventHandling()) // Exception to the rule "One transaction per command" to avoid to handle the same event more than once + { + using (var scope = new TransactionScope()) + { + handler.Handle(command, context); + UpdateEventStreamPosition(context); + scope.Complete(); + } + } + else + handler.Handle(command, context); + } + } + + private void UpdateEventStreamPosition(IMessageContext context) + { + var @event = context.Event(); + var boundedContext = context.BoundedContext(); + var stream = context.Stream(); + if (stream != null) + { + var command = new UpdateEventStreamPosition + { + Position = @event.EventId, + Type = stream.Type, + Source = stream.Source + }; + var handlerType = typeof(ISyncCommandHandler<,>).MakeGenericType(typeof(UpdateEventStreamPosition), boundedContext.GetType()); + dynamic handler = this.container.GetInstance(handlerType); + handler.Handle(command, context); + stream.Position = @event.EventId; + } + else + { + var failedStream = context.FailedStream(); + var command = new UpdateFailedEventStreamPosition + { + Position = @event.EventId, + Id = failedStream.StreamId, + Type = failedStream.StreamType, + Source = failedStream.StreamSource + }; + var handlerType = typeof(ISyncCommandHandler<,>).MakeGenericType(typeof(UpdateFailedEventStreamPosition), boundedContext.GetType()); + dynamic handler = this.container.GetInstance(handlerType); + handler.Handle(command, context); + failedStream.StreamPosition = @event.EventId; + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler`1.cs b/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler`1.cs new file mode 100644 index 0000000..528e8f7 --- /dev/null +++ b/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler`1.cs @@ -0,0 +1,58 @@ +using SimpleInjector; +using SimpleInjector.Lifestyles; +using EnsureThat; +using System; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Application; + using Domain; + + /// + /// A decorator that defines a scope around the synchronous execution of a command. + /// + public class ThreadScopedCommandHandler : ISyncCommandHandler + where TCommand : class, ICommand + where TContext : BoundedContext + { + + #region Fields + + private readonly Container container; + private readonly Func> handlerProvider; + + #endregion Fields + + #region Constructors + + public ThreadScopedCommandHandler(Func> handlerProvider, Container container) + { + Ensure.That(handlerProvider, nameof(handlerProvider)).IsNotNull(); + Ensure.That(container, nameof(container)).IsNotNull(); + this.handlerProvider = handlerProvider; + this.container = container; + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.handlerProvider().Context; + + #endregion Properties + + #region Methods + + public void Handle(TCommand command, IMessageContext context) + { + using (ThreadScopedLifestyle.BeginScope(container)) + { + var handler = this.handlerProvider(); + handler.Handle(command, context); + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler.cs b/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler.cs new file mode 100644 index 0000000..9aa6f85 --- /dev/null +++ b/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler.cs @@ -0,0 +1,50 @@ +using SimpleInjector; +using SimpleInjector.Lifestyles; +using EnsureThat; +using System; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Application; + + /// + /// A decorator that defines a scope around the synchronous execution of a query. + /// + public class ThreadScopedQueryHandler : ISyncQueryHandler + where TQuery : class, IQuery + { + + #region Fields + + private readonly Container container; + private readonly Func> handlerProvider; + + #endregion Fields + + #region Constructors + + public ThreadScopedQueryHandler(Func> handlerProvider, Container container) + { + Ensure.That(handlerProvider, nameof(handlerProvider)).IsNotNull(); + Ensure.That(container, nameof(container)).IsNotNull(); + this.handlerProvider = handlerProvider; + this.container = container; + } + + #endregion Constructors + + #region Methods + + public TResult Handle(TQuery query, IMessageContext context) + { + using (ThreadScopedLifestyle.BeginScope(container)) + { + var handler = this.handlerProvider(); + return handler.Handle(query, context); + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler`1.cs b/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler`1.cs new file mode 100644 index 0000000..dac4e18 --- /dev/null +++ b/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler`1.cs @@ -0,0 +1,58 @@ +using SimpleInjector; +using SimpleInjector.Lifestyles; +using EnsureThat; +using System; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Application; + using Domain; + + /// + /// A decorator that defines a scope around the synchronous execution of a query. + /// + public class ThreadScopedQueryHandler : ISyncQueryHandler + where TQuery : class, IQuery + where TContext : BoundedContext + { + + #region Fields + + private readonly Container container; + private readonly Func> handlerProvider; + + #endregion Fields + + #region Constructors + + public ThreadScopedQueryHandler(Func> handlerProvider, Container container) + { + Ensure.That(handlerProvider, nameof(handlerProvider)).IsNotNull(); + Ensure.That(container, nameof(container)).IsNotNull(); + this.handlerProvider = handlerProvider; + this.container = container; + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.handlerProvider().Context; + + #endregion Properties + + #region Methods + + public TResult Handle(TQuery query, IMessageContext context) + { + using (ThreadScopedLifestyle.BeginScope(container)) + { + var handler = this.handlerProvider(); + return handler.Handle(query, context); + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Xunit/CustomMemberDataAttribute.cs b/Src/DDD.Core.Xunit/CustomMemberDataAttribute.cs index f67c806..f974e00 100644 --- a/Src/DDD.Core.Xunit/CustomMemberDataAttribute.cs +++ b/Src/DDD.Core.Xunit/CustomMemberDataAttribute.cs @@ -7,7 +7,6 @@ namespace DDD.Core.Infrastructure.Testing { - [CLSCompliant(false)] [DataDiscoverer("Xunit.Sdk.MemberDataDiscoverer", "xunit.core")] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class CustomMemberDataAttribute : MemberDataAttributeBase diff --git a/Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj b/Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj index d374bc5..e009531 100644 --- a/Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj +++ b/Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj @@ -1,94 +1,37 @@ - - - - + - Debug - AnyCPU - {62B652DE-4CC9-4B85-8AE9-D66CEEA358DD} + net48;netstandard2.1 Library - Properties DDD.Core.Infrastructure.Testing - DDD.Core.Xunit - v4.7.2 - 512 - - - + false - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 bin\Debug\DDD.Core.Xunit.xml 1591 - pdbonly - true - bin\Release\ - TRACE - prompt - 4 bin\Release\DDD.Core.Xunit.xml 1591 - - - L:\Packages\Conditions.2.1.0\lib\portable-net45+win+wpa81+wp80+MonoAndroid10+MonoTouch10\Conditions.dll - True - - - - - - - L:\packages\xunit.abstractions.2.0.3\lib\net35\xunit.abstractions.dll - True - - - L:\packages\xunit.extensibility.core.2.4.1\lib\net452\xunit.core.dll - - - L:\packages\xunit.extensibility.execution.2.4.1\lib\net452\xunit.execution.desktop.dll - - Properties\CommonAssemblyInfo.cs - - - - - + - - {c6c3e419-b9aa-44ad-9dbf-789294687ae6} - DDD.Core - + + + + 2.4.2 + + + + + + + - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - \ No newline at end of file diff --git a/Src/DDD.Core.Xunit/DbFixture.cs b/Src/DDD.Core.Xunit/DbFixture.cs index 9b697c9..60a13e3 100644 --- a/Src/DDD.Core.Xunit/DbFixture.cs +++ b/Src/DDD.Core.Xunit/DbFixture.cs @@ -1,66 +1,101 @@ -using Conditions; -using System.Data; +using EnsureThat; +using System; using System.Resources; -using System.Reflection; using System.Linq; +using System.Diagnostics; +using System.Configuration; +using System.Data; +using System.Data.Common; namespace DDD.Core.Infrastructure.Testing { using Data; + using Domain; - public abstract class DbFixture : IDbFixture - where TConnectionFactory : class, IDbConnectionFactory + public abstract class DbFixture : IDbFixture + where TContext : BoundedContext, new() { #region Fields private readonly ResourceManager resourceManager; + private readonly ConnectionStringSettings connectionSettings; #endregion Fields #region Constructors - protected DbFixture(TConnectionFactory connectionFactory, string resourceFile) + protected DbFixture(string resourceFile, ConnectionStringSettings connectionSettings) { - Condition.Requires(connectionFactory, nameof(connectionFactory)).IsNotNull(); - Condition.Requires(resourceFile, nameof(resourceFile)).IsNotNullOrWhiteSpace(); - this.ConnectionFactory = connectionFactory; - var resourceAssembly = Assembly.GetCallingAssembly(); - var resourceType = resourceAssembly.GetTypes().Single(t => t.Name == resourceFile); + Ensure.That(resourceFile, nameof(resourceFile)).IsNotNullOrWhiteSpace(); + Ensure.That(connectionSettings, nameof(connectionSettings)).IsNotNull(); + this.LoadConfiguration(); + var resourceType = GetResourceType(resourceFile); this.resourceManager = new ResourceManager(resourceType); + this.connectionSettings = connectionSettings; this.CreateDatabase(); } #endregion Constructors - #region Properties - - public TConnectionFactory ConnectionFactory { get; } - - #endregion Properties - #region Methods public int[] ExecuteScript(string script) { - Condition.Requires(script, nameof(script)).IsNotNullOrWhiteSpace(); - using (var connection = this.ConnectionFactory.CreateOpenConnection()) + using (var connection = this.CreateConnection()) { + connection.Open(); return this.ExecuteScript(script, connection); } } public int[] ExecuteScriptFromResources(string scriptName) { - Condition.Requires(scriptName, nameof(scriptName)).IsNotNullOrWhiteSpace(); + Ensure.That(scriptName, nameof(scriptName)).IsNotNullOrWhiteSpace(); var script = this.resourceManager.GetString(scriptName); return this.ExecuteScript(script); } - protected abstract void CreateDatabase(); + public IDbConnectionProvider CreateConnectionProvider(bool pooling = true) + { + var connectionString = this.SetPooling(connectionSettings.ConnectionString, pooling); + var settings = new DbConnectionSettings(connectionSettings.ProviderName, connectionString); + return new LazyDbConnectionProvider(new TContext(), settings); + } + + public DbConnection CreateConnection(bool pooling = true) + { + var connectionString = this.SetPooling(connectionSettings.ConnectionString, pooling); + var providerFactory = DbProviderFactories.GetFactory(connectionSettings.ProviderName); + var connection = providerFactory.CreateConnection(); + connection.ConnectionString = connectionString; + return connection; + } + + protected abstract string SetPooling(string connectionString, bool pooling); protected abstract int[] ExecuteScript(string script, IDbConnection connection); + protected abstract void CreateDatabase(); + + protected virtual void LoadConfiguration() + { + } + + private static Type GetResourceType(string resourceFile) + { + var callingAssemblies = new StackTrace().GetFrames() + .Select(x => x.GetMethod().ReflectedType.Assembly) + .Distinct(); + foreach (var assembly in callingAssemblies) + { + var resourceType = assembly.GetTypes().FirstOrDefault(t => t.Name == resourceFile); + if (resourceType != null) + return resourceType; + } + throw new ArgumentException($"Cannot find the resource file '{resourceFile}'", nameof(resourceFile)); + } + #endregion Methods } diff --git a/Src/DDD.Core.Xunit/IDbFixture.cs b/Src/DDD.Core.Xunit/IDbFixture.cs index 47c7855..c01b83f 100644 --- a/Src/DDD.Core.Xunit/IDbFixture.cs +++ b/Src/DDD.Core.Xunit/IDbFixture.cs @@ -1,18 +1,19 @@ -namespace DDD.Core.Infrastructure.Testing +using System.Data.Common; + +namespace DDD.Core.Infrastructure.Testing { using Data; + using Domain; - public interface IDbFixture - where TConnectionFactory : class, IDbConnectionFactory + public interface IDbFixture + where TContext : BoundedContext { - #region Properties - - TConnectionFactory ConnectionFactory { get; } + #region Methods - #endregion Properties + IDbConnectionProvider CreateConnectionProvider(bool pooling = true); - #region Methods + DbConnection CreateConnection(bool pooling = true); int[] ExecuteScript(string script); diff --git a/Src/DDD.Core.Xunit/IDbFixtureExtensions.cs b/Src/DDD.Core.Xunit/IDbFixtureExtensions.cs new file mode 100644 index 0000000..8d52e69 --- /dev/null +++ b/Src/DDD.Core.Xunit/IDbFixtureExtensions.cs @@ -0,0 +1,38 @@ +using EnsureThat; +using System.Data.Common; +using System.Threading.Tasks; +using System.Threading; + +namespace DDD.Core.Infrastructure.Testing +{ + using Domain; + + public static class IDbFixtureExtensions + { + + #region Methods + + public static DbConnection CreateOpenConnection(this IDbFixture fixture, bool pooling = true) + where TContext : BoundedContext + { + Ensure.That(fixture, nameof(fixture)).IsNotNull(); + var connection = fixture.CreateConnection(pooling); + connection.Open(); + return connection; + } + + public static async Task CreateOpenConnectionAsync(this IDbFixture fixture, + CancellationToken cancellationToken = default, + bool pooling = true) + where TContext : BoundedContext + { + Ensure.That(fixture, nameof(fixture)).IsNotNull(); + var connection = fixture.CreateConnection(pooling); + await connection.OpenAsync(cancellationToken).ConfigureAwait(false); + return connection; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Xunit/packages.config b/Src/DDD.Core.Xunit/packages.config deleted file mode 100644 index 3fcf493..0000000 --- a/Src/DDD.Core.Xunit/packages.config +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/Src/DDD.Core/Application/ApplicationException.cs b/Src/DDD.Core/Application/ApplicationException.cs index 4ebda82..96e2c8b 100644 --- a/Src/DDD.Core/Application/ApplicationException.cs +++ b/Src/DDD.Core/Application/ApplicationException.cs @@ -5,25 +5,28 @@ namespace DDD.Core.Application /// /// The base class for all exceptions thrown in the application layer. /// - public abstract class ApplicationException : Exception + public abstract class ApplicationException : TimestampedException { #region Constructors - protected ApplicationException() - : base("An error has occurred in the application layer.") + protected ApplicationException(bool isTransient, Exception innerException = null) + : base(isTransient, DefaultMessage(), innerException) { } - protected ApplicationException(string message) : base(message) - { - } - - protected ApplicationException(string message, Exception innerException) : base(message, innerException) + protected ApplicationException(bool isTransient, string message, Exception innerException = null) + : base(isTransient, message, innerException) { } #endregion Constructors + #region Methods + + public static string DefaultMessage() => "An error occurred in the application layer."; + + #endregion Methods + } } diff --git a/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging.cs b/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging.cs new file mode 100644 index 0000000..51db00e --- /dev/null +++ b/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging.cs @@ -0,0 +1,54 @@ +using EnsureThat; +using Microsoft.Extensions.Logging; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + /// + /// A decorator that logs information about commands. + /// + public class AsyncCommandHandlerWithLogging : IAsyncCommandHandler + where TCommand : class, ICommand + { + + #region Fields + + private readonly IAsyncCommandHandler commandHandler; + private readonly ILogger logger; + + #endregion Fields + + + #region Constructors + + public AsyncCommandHandlerWithLogging(IAsyncCommandHandler commandHandler, ILogger logger) + { + Ensure.That(commandHandler, nameof(commandHandler)).IsNotNull(); + Ensure.That(logger, nameof(logger)).IsNotNull(); + this.commandHandler = commandHandler; + this.logger = logger; + } + + #endregion Constructors + + #region Methods + + public async Task HandleAsync(TCommand command, IMessageContext context) + { + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug("Le traitement de la commande {Command} par {CommandHandler} a commencé.", command, this.commandHandler.GetType().Name); + var stopWatch = Stopwatch.StartNew(); + await this.commandHandler.HandleAsync(command, context); + stopWatch.Stop(); + this.logger.LogDebug("Le traitement de la commande {Command} par {CommandHandler} s'est terminé (temps d'exécution: {CommandExecutionTime} ms).", command, this.commandHandler.GetType().Name, stopWatch.ElapsedMilliseconds); + } + else + await this.commandHandler.HandleAsync(command, context); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging`1.cs b/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging`1.cs new file mode 100644 index 0000000..dedcbf4 --- /dev/null +++ b/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging`1.cs @@ -0,0 +1,62 @@ +using EnsureThat; +using Microsoft.Extensions.Logging; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + using Domain; + + /// + /// A decorator that logs information about commands. + /// + public class AsyncCommandHandlerWithLogging : IAsyncCommandHandler + where TCommand : class, ICommand + where TContext : BoundedContext + { + + #region Fields + + private readonly IAsyncCommandHandler commandHandler; + private readonly ILogger logger; + + #endregion Fields + + #region Constructors + + public AsyncCommandHandlerWithLogging(IAsyncCommandHandler commandHandler, ILogger logger) + { + Ensure.That(commandHandler, nameof(commandHandler)).IsNotNull(); + Ensure.That(logger, nameof(logger)).IsNotNull(); + this.commandHandler = commandHandler; + this.logger = logger; + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.commandHandler.Context; + + #endregion Properties + + #region Methods + + public async Task HandleAsync(TCommand command, IMessageContext context) + { + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug("Le traitement de la commande {Command} par {CommandHandler} a commencé.", command, this.commandHandler.GetType().Name); + var stopWatch = Stopwatch.StartNew(); + await this.commandHandler.HandleAsync(command, context); + stopWatch.Stop(); + this.logger.LogDebug("Le traitement de la commande {Command} par {CommandHandler} s'est terminé (temps d'exécution: {CommandExecutionTime} ms).", command, this.commandHandler.GetType().Name, stopWatch.ElapsedMilliseconds); + } + else + await this.commandHandler.HandleAsync(command, context); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/AsyncDomainCommandHandler.cs b/Src/DDD.Core/Application/AsyncDomainCommandHandler.cs deleted file mode 100644 index 825c219..0000000 --- a/Src/DDD.Core/Application/AsyncDomainCommandHandler.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Conditions; -using System; -using System.Threading.Tasks; - -namespace DDD.Core.Application -{ - using Domain; - using Threading; - - /// - /// Base class for handling asynchronously commands using a domain model. - /// - /// The type of the command. - public abstract class AsyncDomainCommandHandler : IAsyncCommandHandler - where TCommand : class, ICommand - { - - #region Methods - - public async Task HandleAsync(TCommand command) - { - Condition.Requires(command, nameof(command)).IsNotNull(); - await new SynchronizationContextRemover(); - try - { - await this.ExecuteAsync(command); - } - catch (Exception ex) when (ex is DomainServiceException || ex is RepositoryException) - { - throw new CommandException(ex, command); - } - } - - protected abstract Task ExecuteAsync(TCommand command); - - #endregion Methods - - } -} \ No newline at end of file diff --git a/Src/DDD.Core/Application/AsyncEventHandler.cs b/Src/DDD.Core/Application/AsyncEventHandler.cs new file mode 100644 index 0000000..8b62ed4 --- /dev/null +++ b/Src/DDD.Core/Application/AsyncEventHandler.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading.Tasks; +using EnsureThat; + +namespace DDD.Core.Application +{ + using Domain; + + /// + /// Base class for handling asynchronously events. + /// + public abstract class AsyncEventHandler : IAsyncEventHandler + where TEvent : class, IEvent + where TContext : BoundedContext + { + + #region Constructors + + protected AsyncEventHandler(TContext context) + { + Ensure.That(context, nameof(context)).IsNotNull(); + this.Context = context; + } + + #endregion Constructors + + #region Properties + + public TContext Context { get; } + + BoundedContext IAsyncEventHandler.Context => this.Context; + + Type IAsyncEventHandler.EventType => typeof(TEvent); + + #endregion Properties + + #region Methods + + public abstract Task HandleAsync(TEvent @event, IMessageContext context); + + Task IAsyncEventHandler.HandleAsync(IEvent @event, IMessageContext context) => this.HandleAsync((TEvent)@event, context); + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/AsyncEventHandlerWithLogging.cs b/Src/DDD.Core/Application/AsyncEventHandlerWithLogging.cs new file mode 100644 index 0000000..da738f0 --- /dev/null +++ b/Src/DDD.Core/Application/AsyncEventHandlerWithLogging.cs @@ -0,0 +1,69 @@ +using EnsureThat; +using Microsoft.Extensions.Logging; +using System; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + using Domain; + + /// + /// A decorator that logs information about events. + /// + public class AsyncEventHandlerWithLogging : IAsyncEventHandler + where TEvent : class, IEvent + where TContext : BoundedContext + { + + #region Fields + + private readonly IAsyncEventHandler eventHandler; + private readonly ILogger logger; + + #endregion Fields + + #region Constructors + + public AsyncEventHandlerWithLogging(IAsyncEventHandler eventHandler, ILogger logger) + { + Ensure.That(eventHandler, nameof(eventHandler)).IsNotNull(); + Ensure.That(logger, nameof(logger)).IsNotNull(); + this.eventHandler = eventHandler; + this.logger = logger; + } + + #endregion Constructors + + #region Properties + + public TContext Context => eventHandler.Context; + + BoundedContext IAsyncEventHandler.Context => this.Context; + + Type IAsyncEventHandler.EventType => this.eventHandler.EventType; + + #endregion Properties + + #region Methods + + public async Task HandleAsync(TEvent @event, IMessageContext context) + { + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug("Le traitement de l'évènement {Event} par {EventHandler} a commencé.", @event, this.eventHandler.GetType().Name); + var stopWatch = Stopwatch.StartNew(); + await this.eventHandler.HandleAsync(@event, context); + stopWatch.Stop(); + this.logger.LogDebug("Le traitement de l'évènement {Event} par {EventHandler} s'est terminé (temps d'exécution: {EventExecutionTime} ms).", @event, this.eventHandler.GetType().Name, stopWatch.ElapsedMilliseconds); + } + else + await this.eventHandler.HandleAsync(@event, context); + } + + Task IAsyncEventHandler.HandleAsync(IEvent @event, IMessageContext context) => this.HandleAsync((TEvent)@event, context); + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging.cs b/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging.cs new file mode 100644 index 0000000..da199cd --- /dev/null +++ b/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging.cs @@ -0,0 +1,48 @@ +using EnsureThat; +using Microsoft.Extensions.Logging; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + /// + /// A decorator that logs information about queries. + /// + public class AsyncQueryHandlerWithLogging : IAsyncQueryHandler + where TQuery : class, IQuery + { + #region Fields + + private readonly IAsyncQueryHandler queryHandler; + private readonly ILogger logger; + + public AsyncQueryHandlerWithLogging(IAsyncQueryHandler queryHandler, ILogger logger) + { + Ensure.That(queryHandler, nameof(queryHandler)).IsNotNull(); + Ensure.That(logger, nameof(logger)).IsNotNull(); + this.queryHandler = queryHandler; + this.logger = logger; + } + + #endregion Fields + + #region Methods + + public async Task HandleAsync(TQuery query, IMessageContext context) + { + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug("Le traitement de la requête {Query} par {QueryHandler} a commencé.", query, this.queryHandler.GetType().Name); + var stopWatch = Stopwatch.StartNew(); + var result = await this.queryHandler.HandleAsync(query, context); + stopWatch.Stop(); + this.logger.LogDebug("Le traitement de la requête {Query} par {QueryHandler} s'est terminé (temps d'exécution: {QueryExecutionTime} ms).", query, this.queryHandler.GetType().Name, stopWatch.ElapsedMilliseconds); + return result; + } + else + return await this.queryHandler.HandleAsync(query, context); + } + + #endregion Methods + } +} diff --git a/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging`1.cs b/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging`1.cs new file mode 100644 index 0000000..8a158ab --- /dev/null +++ b/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging`1.cs @@ -0,0 +1,63 @@ +using EnsureThat; +using Microsoft.Extensions.Logging; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + using Domain; + + /// + /// A decorator that logs information about queries. + /// + public class AsyncQueryHandlerWithLogging : IAsyncQueryHandler + where TQuery : class, IQuery + where TContext : BoundedContext + { + + #region Fields + + private readonly ILogger logger; + private readonly IAsyncQueryHandler queryHandler; + + #endregion Fields + + #region Constructors + + public AsyncQueryHandlerWithLogging(IAsyncQueryHandler queryHandler, ILogger logger) + { + Ensure.That(queryHandler, nameof(queryHandler)).IsNotNull(); + Ensure.That(logger, nameof(logger)).IsNotNull(); + this.queryHandler = queryHandler; + this.logger = logger; + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.queryHandler.Context; + + #endregion Properties + + #region Methods + + public async Task HandleAsync(TQuery query, IMessageContext context) + { + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug("Le traitement de la requête {Query} par {QueryHandler} a commencé.", query, this.queryHandler.GetType().Name); + var stopWatch = Stopwatch.StartNew(); + var result = await this.queryHandler.HandleAsync(query, context); + stopWatch.Stop(); + this.logger.LogDebug("Le traitement de la requête {Query} par {QueryHandler} s'est terminé (temps d'exécution: {QueryExecutionTime} ms).", query, this.queryHandler.GetType().Name, stopWatch.ElapsedMilliseconds); + return result; + } + else + return await this.queryHandler.HandleAsync(query, context); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/CommandConflictException.cs b/Src/DDD.Core/Application/CommandConflictException.cs new file mode 100644 index 0000000..6b1adb0 --- /dev/null +++ b/Src/DDD.Core/Application/CommandConflictException.cs @@ -0,0 +1,37 @@ +using System; + +namespace DDD.Core.Application +{ + /// + /// Exception thrown when a conflict with other commands has been detected while handling a command. + /// + public class CommandConflictException : CommandException + { + + #region Constructors + + public CommandConflictException(ICommand command = null, Exception innerException = null) + : base(true, DefaultMessage(command), command, innerException) + { + } + + public CommandConflictException(string message, ICommand command = null, Exception innerException = null) + : base(true, message, command, innerException) + { + } + + #endregion Constructors + + #region Methods + + public static new string DefaultMessage(ICommand command = null) + { + if (command == null) + return "A conflict has been detected while handling a command."; + return $"A conflict has been detected while handling the command '{command.GetType().Name}'."; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/CommandException.cs b/Src/DDD.Core/Application/CommandException.cs index 13016df..d820b27 100644 --- a/Src/DDD.Core/Application/CommandException.cs +++ b/Src/DDD.Core/Application/CommandException.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.Serialization; namespace DDD.Core.Application { @@ -10,45 +11,42 @@ public class CommandException : ApplicationException #region Constructors - public CommandException() - : base("The command failed.") - { - } - - public CommandException(string message) : base(message) - { - } - - public CommandException(string message, Exception innerException) : base(message, innerException) - { - } - - public CommandException(string message, Exception innerException, ICommand command) : base(message, innerException) + public CommandException(bool isTransient, ICommand command = null, Exception innerException = null) + : base(isTransient, DefaultMessage(command), innerException) { this.Command = command; } - public CommandException(Exception innerException, ICommand Command) - : base($"The command '{Command.GetType().Name}' failed.", innerException) + public CommandException(bool isTransient, string message, ICommand command = null, Exception innerException = null) + : base(isTransient, message, innerException) { - this.Command = Command; + this.Command = command; } #endregion Constructors #region Properties - public ICommand Command { get; set; } + public ICommand Command { get; } #endregion Properties #region Methods + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + if (this.Command != null) + info.AddValue("Command", this.Command); + } + public override string ToString() { var s = $"{this.GetType()}: {this.Message} "; + s += $"{Environment.NewLine}{nameof(Timestamp)}: {this.Timestamp}"; + s += $"{Environment.NewLine}{nameof(IsTransient)}: {this.IsTransient}"; if (this.Command != null) - s += $"{Environment.NewLine}Command: {this.Command}"; + s += $"{Environment.NewLine}{nameof(Command)}: {this.Command}"; if (this.InnerException != null) s += $" ---> {this.InnerException}"; if (this.StackTrace != null) @@ -56,6 +54,13 @@ public override string ToString() return s; } + public static string DefaultMessage(ICommand command = null) + { + if (command == null) + return "A command failed."; + return $"The command '{command.GetType().Name}' failed."; + } + #endregion Methods } diff --git a/Src/DDD.Core/Application/CommandInvalidException.cs b/Src/DDD.Core/Application/CommandInvalidException.cs new file mode 100644 index 0000000..3dba528 --- /dev/null +++ b/Src/DDD.Core/Application/CommandInvalidException.cs @@ -0,0 +1,80 @@ +using System; +using System.Linq; +using System.Runtime.Serialization; +using DDD.Validation; + +namespace DDD.Core.Application +{ + /// + /// Exception thrown when a command is invalid. + /// + public class CommandInvalidException : CommandException + { + + #region Constructors + + public CommandInvalidException(ICommand command = null, ValidationFailure[] failures = null, Exception innerException = null) + : base(false, DefaultMessage(command), command, innerException) + { + this.Failures = failures; + } + + public CommandInvalidException(string message, ICommand command = null, ValidationFailure[] failures = null, Exception innerException = null) + : base(false, message, command, innerException) + { + this.Failures = failures; + } + + #endregion Constructors + + #region Properties + + public ValidationFailure[] Failures { get; } + + #endregion Properties + + #region Methods + + public static new string DefaultMessage(ICommand command = null) + { + if (command == null) + return "The command is invalid."; + return $"The command '{command.GetType().Name}' is invalid."; + } + + public bool HasFailures() => this.Failures != null && this.Failures.Any(); + + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + if (this.Failures != null) + { + for (var i = 0; i < this.Failures.Length; i++) + info.AddValue($"Failure{i}", this.Failures[i]); + } + } + + public override string ToString() + { + var s = $"{this.GetType()}: {this.Message} "; + s += $"{Environment.NewLine}{nameof(Timestamp)}: {this.Timestamp}"; + s += $"{Environment.NewLine}{nameof(IsTransient)}: {this.IsTransient}"; + if (this.Command != null) + s += $"{Environment.NewLine}{nameof(Command)}: {this.Command}"; + if (this.Failures != null) + { + for (var i = 0; i < this.Failures.Length; i++) + s += $"{Environment.NewLine}Failure{i}: {this.Failures[i]}"; + } + if (this.InnerException != null) + s += $" ---> {this.InnerException}"; + if (this.StackTrace != null) + s += $"{Environment.NewLine}{this.StackTrace}"; + return s; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/CommandProcessor.cs b/Src/DDD.Core/Application/CommandProcessor.cs index c3e3166..b57c1b0 100644 --- a/Src/DDD.Core/Application/CommandProcessor.cs +++ b/Src/DDD.Core/Application/CommandProcessor.cs @@ -1,13 +1,15 @@ -using Conditions; +using EnsureThat; using System; using System.Threading.Tasks; +using System.Collections.Concurrent; namespace DDD.Core.Application { + using Domain; using Validation; /// - /// Finds the correct command handler and validator and invokes them. + /// The default command processor for processing and validating commands of any type. /// public class CommandProcessor : ICommandProcessor { @@ -15,54 +17,131 @@ public class CommandProcessor : ICommandProcessor #region Fields private readonly IServiceProvider serviceProvider; + private readonly CommandProcessorSettings settings; + private static readonly ConcurrentDictionary contextualProcessors + = new ConcurrentDictionary(); #endregion Fields #region Constructors - public CommandProcessor(IServiceProvider serviceProvider) + public CommandProcessor(IServiceProvider serviceProvider, CommandProcessorSettings settings) { - Condition.Requires(serviceProvider, nameof(serviceProvider)).IsNotNull(); + Ensure.That(serviceProvider, nameof(serviceProvider)).IsNotNull(); + Ensure.That(settings, nameof(settings)).IsNotNull(); this.serviceProvider = serviceProvider; + this.settings = settings; } #endregion Constructors #region Methods - public void Process(TCommand command) where TCommand : class, ICommand + public IContextualCommandProcessor InGeneric(TContext context) where TContext : BoundedContext { - Condition.Requires(command, nameof(command)).IsNotNull(); - var handler = this.serviceProvider.GetService>(); - if (handler == null) throw new InvalidOperationException($"The command handler for type {typeof(ICommandHandler)} could not be found."); - handler.Handle(command); + Ensure.That(context, nameof(context)).IsNotNull(); + return (IContextualCommandProcessor)contextualProcessors.GetOrAdd(context, _ => + new ContextualCommandProcessor(this.serviceProvider, context)); } - public Task ProcessAsync(TCommand command) where TCommand : class, ICommand + public IContextualCommandProcessor InSpecific(BoundedContext context) { - Condition.Requires(command, nameof(command)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + return contextualProcessors.GetOrAdd(context, _ => + { + var processorType = typeof(ContextualCommandProcessor<>).MakeGenericType(context.GetType()); + return (IContextualCommandProcessor)Activator.CreateInstance(processorType, this.serviceProvider, context); + }); + } + + public void Process(TCommand command, IMessageContext context) where TCommand : class, ICommand + { + Ensure.That(command, nameof(command)).IsNotNull(); + var handler = this.serviceProvider.GetService>(); + if (handler == null) throw new InvalidOperationException($"The command handler for type {typeof(ISyncCommandHandler)} could not be found."); + handler.Handle(command, context); + } + + public void Process(ICommand command, IMessageContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + var handlerType = typeof(ISyncCommandHandler<>).MakeGenericType(command.GetType()); + dynamic handler = this.serviceProvider.GetService(handlerType); + if (handler == null) throw new InvalidOperationException($"The command handler for type {handlerType} could not be found."); + handler.Handle((dynamic)command, context); + } + + public Task ProcessAsync(TCommand command, IMessageContext context) where TCommand : class, ICommand + { + Ensure.That(command, nameof(command)).IsNotNull(); var handler = this.serviceProvider.GetService>(); - if (handler == null) throw new InvalidOperationException($"The command handler for type {typeof(ICommandHandler)} could not be found."); - return handler.HandleAsync(command); + if (handler == null) throw new InvalidOperationException($"The command handler for type {typeof(IAsyncCommandHandler)} could not be found."); + return handler.HandleAsync(command, context); } - public ValidationResult Validate(TCommand command, string ruleSet = null) where TCommand : class, ICommand + public Task ProcessAsync(ICommand command, IMessageContext context) { - Condition.Requires(command, nameof(command)).IsNotNull(); - var validator = this.serviceProvider.GetService>(); - if (validator == null) throw new InvalidOperationException($"The command validator for type {typeof(ICommandValidator)} could not be found."); - return validator.Validate(command, ruleSet); + Ensure.That(command, nameof(command)).IsNotNull(); + var handlerType = typeof(IAsyncCommandHandler<>).MakeGenericType(command.GetType()); + dynamic handler = this.serviceProvider.GetService(handlerType); + if (handler == null) throw new InvalidOperationException($"The command handler for type {handlerType} could not be found."); + return handler.HandleAsync((dynamic)command, context); } - public Task ValidateAsync(TCommand command, string ruleSet = null) where TCommand : class, ICommand + public ValidationResult Validate(TCommand command, IValidationContext context) where TCommand : class, ICommand { - Condition.Requires(command, nameof(command)).IsNotNull(); - var validator = this.serviceProvider.GetService>(); - if (validator == null) throw new InvalidOperationException($"The command validator for type {typeof(ICommandValidator)} could not be found."); - return validator.ValidateAsync(command, ruleSet); + Ensure.That(command, nameof(command)).IsNotNull(); + var validator = this.serviceProvider.GetService>(); + if (validator == null) + { + if (this.settings.DefaultValidator == null) + throw new InvalidOperationException($"The command validator for type {typeof(ISyncObjectValidator)} could not be found."); + return this.settings.DefaultValidator.Validate(command, context); + } + return validator.Validate(command, context); } - #endregion Methods + public ValidationResult Validate(ICommand command, IValidationContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + var validatorType = typeof(ISyncObjectValidator<>).MakeGenericType(command.GetType()); + dynamic validator = this.serviceProvider.GetService(validatorType); + if (validator == null) + { + if (this.settings.DefaultValidator == null) + throw new InvalidOperationException($"The command validator for type {validatorType} could not be found."); + return this.settings.DefaultValidator.Validate(command, context); + } + return validator.Validate((dynamic)command, context); + } + + public Task ValidateAsync(TCommand command, IValidationContext context) where TCommand : class, ICommand + { + Ensure.That(command, nameof(command)).IsNotNull(); + var validator = this.serviceProvider.GetService>(); + if (validator == null) + { + if (this.settings.DefaultValidator == null) + throw new InvalidOperationException($"The command validator for type {typeof(IAsyncObjectValidator)} could not be found."); + return this.settings.DefaultValidator.ValidateAsync(command, context); + } + return validator.ValidateAsync(command, context); + } + public Task ValidateAsync(ICommand command, IValidationContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + var validatorType = typeof(IAsyncObjectValidator<>).MakeGenericType(command.GetType()); + dynamic validator = this.serviceProvider.GetService(validatorType); + if (validator == null) + { + if (this.settings.DefaultValidator == null) + throw new InvalidOperationException($"The command validator for type {validatorType} could not be found."); + return this.settings.DefaultValidator.ValidateAsync(command, context); + } + return validator.ValidateAsync((dynamic)command, context); + } + + #endregion Methods } } \ No newline at end of file diff --git a/Src/DDD.Core/Application/CommandProcessorSettings.cs b/Src/DDD.Core/Application/CommandProcessorSettings.cs new file mode 100644 index 0000000..6cc0654 --- /dev/null +++ b/Src/DDD.Core/Application/CommandProcessorSettings.cs @@ -0,0 +1,24 @@ +namespace DDD.Core.Application +{ + using Validation; + + public class CommandProcessorSettings + { + + #region Constructors + + public CommandProcessorSettings(IObjectValidator defaultValidator = null) + { + this.DefaultValidator = defaultValidator; + } + + #endregion Constructors + + #region Properties + + public IObjectValidator DefaultValidator { get; } + + #endregion Properties + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/CommandTimeoutException.cs b/Src/DDD.Core/Application/CommandTimeoutException.cs new file mode 100644 index 0000000..0640520 --- /dev/null +++ b/Src/DDD.Core/Application/CommandTimeoutException.cs @@ -0,0 +1,37 @@ +using System; + +namespace DDD.Core.Application +{ + /// + /// Exception thrown when a command has expired. + /// + public class CommandTimeoutException : CommandException + { + + #region Constructors + + public CommandTimeoutException(ICommand command = null, Exception innerException = null) + : base(true, DefaultMessage(command), command, innerException) + { + } + + public CommandTimeoutException(string message, ICommand command = null, Exception innerException = null) + : base(true, message, command, innerException) + { + } + + #endregion Constructors + + #region Methods + + public static new string DefaultMessage(ICommand command = null) + { + if (command == null) + return "The command has expired."; + return $"The command '{command.GetType().Name}' has expired."; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/CommandUnauthorizedException.cs b/Src/DDD.Core/Application/CommandUnauthorizedException.cs new file mode 100644 index 0000000..28e51b1 --- /dev/null +++ b/Src/DDD.Core/Application/CommandUnauthorizedException.cs @@ -0,0 +1,37 @@ +using System; + +namespace DDD.Core.Application +{ + /// + /// Exception thrown when a command was denied. + /// + public class CommandUnauthorizedException : CommandException + { + + #region Constructors + + public CommandUnauthorizedException(ICommand command = null, Exception innerException = null) + : base(false, DefaultMessage(command), command, innerException) + { + } + + public CommandUnauthorizedException(string message, ICommand command = null, Exception innerException = null) + : base(false, message, command, innerException) + { + } + + #endregion Constructors + + #region Methods + + public static new string DefaultMessage(ICommand command = null) + { + if (command == null) + return "The command was denied."; + return $"The command '{command.GetType().Name}' was denied."; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/CommandUnavailableException.cs b/Src/DDD.Core/Application/CommandUnavailableException.cs new file mode 100644 index 0000000..8da6ef9 --- /dev/null +++ b/Src/DDD.Core/Application/CommandUnavailableException.cs @@ -0,0 +1,37 @@ +using System; + +namespace DDD.Core.Application +{ + /// + /// Exception thrown when a command cannot be currently handled. + /// + public class CommandUnavailableException : CommandException + { + + #region Constructors + + public CommandUnavailableException(ICommand command = null, Exception innerException = null) + : base(true, DefaultMessage(command), command, innerException) + { + } + + public CommandUnavailableException(string message, ICommand command = null, Exception innerException = null) + : base(true, message, command, innerException) + { + } + + #endregion Constructors + + #region Methods + + public static new string DefaultMessage(ICommand command = null) + { + if (command == null) + return "The command cannot be currently handled."; + return $"The command '{command.GetType().Name}' cannot be currently handled."; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/CompositeTranslatorExtensions.cs b/Src/DDD.Core/Application/CompositeTranslatorExtensions.cs new file mode 100644 index 0000000..f210fcf --- /dev/null +++ b/Src/DDD.Core/Application/CompositeTranslatorExtensions.cs @@ -0,0 +1,37 @@ +using System; +using EnsureThat; + +namespace DDD.Core.Application +{ + using Mapping; + using Collections; + + public static class CompositeTranslatorExtensions + { + + #region Methods + + public static void RegisterFallback(this CompositeTranslator translator) + { + Ensure.That(translator, nameof(translator)).IsNotNull(); + translator.Register((exception, context) => + { + context.TryGetValue("Command", out ICommand command); + return new CommandException(isTransient: false, command, exception); + }); + } + + public static void RegisterFallback(this CompositeTranslator translator) + { + Ensure.That(translator, nameof(translator)).IsNotNull(); + translator.Register((exception, context) => + { + context.TryGetValue("Query", out IQuery query); + return new QueryException(isTransient: false, query, exception); + }); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/ContextualCommandProcessor.cs b/Src/DDD.Core/Application/ContextualCommandProcessor.cs new file mode 100644 index 0000000..c993514 --- /dev/null +++ b/Src/DDD.Core/Application/ContextualCommandProcessor.cs @@ -0,0 +1,81 @@ +using EnsureThat; +using System; +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + using Domain; + + /// + /// The command processor for processing generic commands in a specific bounded context. + /// + public class ContextualCommandProcessor : IContextualCommandProcessor + where TContext : BoundedContext + { + + #region Fields + + private readonly IServiceProvider serviceProvider; + + #endregion Fields + + #region Constructors + + public ContextualCommandProcessor(IServiceProvider serviceProvider, TContext context) + { + Ensure.That(serviceProvider, nameof(serviceProvider)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + this.serviceProvider = serviceProvider; + this.Context = context; + } + + #endregion Constructors + + #region Properties + + public TContext Context { get; } + + BoundedContext IContextualCommandProcessor.Context => this.Context; + + #endregion Properties + + #region Methods + + public void Process(TCommand command, IMessageContext context) where TCommand : class, ICommand + { + Ensure.That(command, nameof(command)).IsNotNull(); + var handler = this.serviceProvider.GetService>(); + if (handler == null) throw new InvalidOperationException($"The command handler for type {typeof(ISyncCommandHandler)} could not be found."); + handler.Handle(command, context); + } + + public void Process(ICommand command, IMessageContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + var handlerType = typeof(ISyncCommandHandler<,>).MakeGenericType(command.GetType(), this.Context.GetType()); + dynamic handler = this.serviceProvider.GetService(handlerType); + if (handler == null) throw new InvalidOperationException($"The command handler for type {handlerType} could not be found."); + handler.Handle((dynamic)command, context); + } + + public Task ProcessAsync(TCommand command, IMessageContext context) where TCommand : class, ICommand + { + Ensure.That(command, nameof(command)).IsNotNull(); + var handler = this.serviceProvider.GetService>(); + if (handler == null) throw new InvalidOperationException($"The command handler for type {typeof(IAsyncCommandHandler)} could not be found."); + return handler.HandleAsync(command, context); + } + + public Task ProcessAsync(ICommand command, IMessageContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + var handlerType = typeof(IAsyncCommandHandler<,>).MakeGenericType(command.GetType(), this.Context.GetType()); + dynamic handler = this.serviceProvider.GetService(handlerType); + if (handler == null) throw new InvalidOperationException($"The command handler for type {handlerType} could not be found."); + return handler.HandleAsync((dynamic)command, context); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/ContextualQueryProcessor.cs b/Src/DDD.Core/Application/ContextualQueryProcessor.cs new file mode 100644 index 0000000..fa9f29e --- /dev/null +++ b/Src/DDD.Core/Application/ContextualQueryProcessor.cs @@ -0,0 +1,94 @@ +using EnsureThat; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + using Domain; + using Threading; + + /// + /// The query processor for processing and validating generic queries in a specific bounded context. + /// + public class ContextualQueryProcessor : IContextualQueryProcessor + where TContext : BoundedContext + { + + #region Fields + + private readonly IServiceProvider serviceProvider; + + #endregion Fields + + #region Constructors + + public ContextualQueryProcessor(IServiceProvider serviceProvider, TContext context) + { + Ensure.That(serviceProvider, nameof(serviceProvider)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + this.serviceProvider = serviceProvider; + this.Context = context; + } + + #endregion Constructors + + #region Properties + + public TContext Context { get; } + + BoundedContext IContextualQueryProcessor.Context => this.Context; + + #endregion Properties + + #region Methods + + public TResult Process(IQuery query, IMessageContext context) + { + Ensure.That(query, nameof(query)).IsNotNull(); + var handlerType = typeof(ISyncQueryHandler<,,>).MakeGenericType(query.GetType(), typeof(TResult), typeof(TContext)); + dynamic handler = this.serviceProvider.GetService(handlerType); + if (handler == null) throw new InvalidOperationException($"The query handler for type {handlerType} could not be found."); + return handler.Handle((dynamic)query, context); + } + + public object Process(IQuery query, IMessageContext context) + { + Ensure.That(query, nameof(query)).IsNotNull(); + var queryType = query.GetType(); + var queryInterfaceType = queryType.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IQuery<>)); + if (queryInterfaceType == null) throw new ArgumentException($"{queryType.Name} does not implement {typeof(IQuery<>).Name}.", nameof(query)); + var resultType = queryInterfaceType.GetGenericArguments()[0]; + var handlerType = typeof(ISyncQueryHandler<,,>).MakeGenericType(query.GetType(), resultType, typeof(TContext)); + dynamic handler = this.serviceProvider.GetService(handlerType); + if (handler == null) throw new InvalidOperationException($"The query handler for type {handlerType} could not be found."); + return handler.Handle((dynamic)query, context); + } + + public Task ProcessAsync(IQuery query, IMessageContext context) + { + Ensure.That(query, nameof(query)).IsNotNull(); + var handlerType = typeof(IAsyncQueryHandler<,,>).MakeGenericType(query.GetType(), typeof(TResult), typeof(TContext)); + dynamic handler = this.serviceProvider.GetService(handlerType); + if (handler == null) throw new InvalidOperationException($"The query handler for type {handlerType} could not be found."); + return handler.HandleAsync((dynamic)query, context); + } + + public async Task ProcessAsync(IQuery query, IMessageContext context) + { + Ensure.That(query, nameof(query)).IsNotNull(); + await new SynchronizationContextRemover(); + var queryType = query.GetType(); + var queryInterfaceType = queryType.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IQuery<>)); + if (queryInterfaceType == null) throw new ArgumentException($"{queryType.Name} does not implement {typeof(IQuery<>).Name}.", nameof(query)); + var resultType = queryInterfaceType.GetGenericArguments()[0]; + var handlerType = typeof(IAsyncQueryHandler<,,>).MakeGenericType(query.GetType(), resultType, typeof(TContext)); + dynamic handler = this.serviceProvider.GetService(handlerType); + if (handler == null) throw new InvalidOperationException($"The query handler for type {handlerType} could not be found."); + return await handler.HandleAsync((dynamic)query, context); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/DomainCommandHandler.cs b/Src/DDD.Core/Application/DomainCommandHandler.cs deleted file mode 100644 index 54c3df2..0000000 --- a/Src/DDD.Core/Application/DomainCommandHandler.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Conditions; - -namespace DDD.Core.Application -{ - using Domain; - using System; - - /// - /// Base class for handling synchronously commands using a domain model. - /// - /// The type of the command. - public abstract class DomainCommandHandler : ICommandHandler - where TCommand : class, ICommand - { - - #region Methods - - public void Handle(TCommand command) - { - Condition.Requires(command, nameof(command)).IsNotNull(); - try - { - this.Execute(command); - } - catch (DomainException ex) - { - throw new CommandException(ex, command); - } - } - - protected abstract void Execute(TCommand command); - - #endregion Methods - - } - -} \ No newline at end of file diff --git a/Src/DDD.Core/Application/DomainToCommandExceptionTranslator.cs b/Src/DDD.Core/Application/DomainToCommandExceptionTranslator.cs new file mode 100644 index 0000000..df9e28f --- /dev/null +++ b/Src/DDD.Core/Application/DomainToCommandExceptionTranslator.cs @@ -0,0 +1,43 @@ +using EnsureThat; + +namespace DDD.Core.Application +{ + using Mapping; + using Domain; + using Collections; + + public class DomainToCommandExceptionTranslator : ObjectTranslator + { + + #region Methods + + public override CommandException Translate(DomainException exception, IMappingContext context) + { + Ensure.That(exception).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + context.TryGetValue("Command", out ICommand command); + switch (exception) + { + case DomainServiceConflictException _: + case RepositoryConflictException _: + return new CommandConflictException(command, exception); + case DomainServiceTimeoutException _: + case RepositoryTimeoutException _: + return new CommandTimeoutException(command, exception); + case DomainServiceUnauthorizedException _: + case RepositoryUnauthorizedException _: + return new CommandUnauthorizedException(command, exception); + case DomainServiceUnavailableException _: + case RepositoryUnavailableException _: + return new CommandUnavailableException(command, exception); + case DomainServiceInvalidException invalidEx: + return new CommandInvalidException(command, invalidEx.Failures, exception); + default: + return new CommandException(isTransient: false, command, exception); + }; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/EventConsumer.cs b/Src/DDD.Core/Application/EventConsumer.cs new file mode 100644 index 0000000..1e4f349 --- /dev/null +++ b/Src/DDD.Core/Application/EventConsumer.cs @@ -0,0 +1,412 @@ +using EnsureThat; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace DDD.Core.Application +{ + using Serialization; + using Domain; + using Threading; + using DDD; + + public class EventConsumer : IEventConsumer, IDisposable + where TContext : BoundedContext + { + + #region Fields + + private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + private readonly ICommandProcessor commandProcessor; + private readonly IQueryProcessor queryProcessor; + private readonly IEventPublisher eventPublisher; + private readonly IEnumerable boundedContexts; + private readonly IEnumerable eventSerializers; + private readonly ILogger logger; + private readonly EventConsumerSettings settings; + private long consumationCount; + private bool disposed; + private Task consumeEvents; + + #endregion Fields + + #region Constructors + + public EventConsumer(ICommandProcessor commandProcessor, + IQueryProcessor queryProcessor, + IEventPublisher eventPublisher, + IEnumerable boundedContexts, + IEnumerable eventSerializers, + ILogger logger, + EventConsumerSettings settings) + { + Ensure.That(commandProcessor, nameof(commandProcessor)).IsNotNull(); + Ensure.That(queryProcessor, nameof(queryProcessor)).IsNotNull(); + Ensure.That(eventPublisher, nameof(eventPublisher)).IsNotNull(); + Ensure.That(boundedContexts, nameof(boundedContexts)).IsNotNull(); + Ensure.Enumerable.HasItems(boundedContexts, nameof(boundedContexts)); + Ensure.Enumerable.HasAny(boundedContexts, c => c is TContext, nameof(boundedContexts)); + Ensure.That(eventSerializers, nameof(eventSerializers)).IsNotNull(); + Ensure.That(logger, nameof(logger)).IsNotNull(); + Ensure.That(settings, nameof(settings)).IsNotNull(); + this.commandProcessor = commandProcessor; + this.queryProcessor = queryProcessor; + this.eventPublisher = eventPublisher; + this.boundedContexts = boundedContexts; + this.eventSerializers = eventSerializers; + this.logger = logger; + this.settings = settings; + this.Context = (TContext)this.boundedContexts.First(c => c is TContext); + } + + #endregion Constructors + + #region Properties + + public TContext Context { get; } + + public bool IsRunning { get; private set; } + + BoundedContext IEventConsumer.Context => this.Context; + protected CancellationToken CancellationToken => this.cancellationTokenSource.Token; + + #endregion Properties + + #region Methods + + public void Start() + { + if (!this.IsRunning) + { + this.logger.LogInformation("EventConsumer for the context '{Context}' is starting.", this.Context.Name); + this.logger.LogDebug("The consumer settings for the context '{Context}' are as follows : {Settings}", this.Context.Name, this.settings); + this.IsRunning = true; + this.consumeEvents = Task.Run(async () => await ConsumeEventsAsync()); + this.logger.LogInformation("EventConsumer for the context '{Context}' has started.", this.Context.Name); + } + } + + public void Stop() + { + if (this.IsRunning) + { + this.logger.LogInformation("EventConsumer for the context '{Context}' is stopping.", this.Context.Name); + this.cancellationTokenSource.Cancel(); + this.consumeEvents.Wait(); + } + } + + public void Wait(TimeSpan? timeout = null) + { + if (this.IsRunning) + { + if (timeout.HasValue) + this.consumeEvents.Wait(timeout.Value); + else + this.consumeEvents.Wait(); + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + if (disposing) + { + this.Stop(); + this.cancellationTokenSource.Dispose(); + } + disposed = true; + } + } + + private static bool IsTransientException(Exception exception) + { + if (exception is TimestampedException ex) + return ex.IsTransient; + return false; + } + + private async Task ConsumeEventsAsync() + { + try + { + await new SynchronizationContextRemover(); + while (!ConsumationMaxReached()) + { + this.CancellationToken.ThrowIfCancellationRequested(); + this.logger.LogInformation("Consumption of event streams in the context '{Context}' has started.", this.Context.Name); + var streamsInfo = await this.FindAllStreamsAsync(); + this.logger.LogInformation("{StreamsCount} event streams were found for the context '{Context}'.", streamsInfo.Streams.Count(), this.Context.Name); + this.logger.LogInformation("{FailedStreamsCount} failed event streams were found for the context '{Context}'.", streamsInfo.FailedStreams.Count(), this.Context.Name); + await this.ConsumeAllStreamsAsync(streamsInfo); + this.IncrementConsumationCountIfRequired(); + this.logger.LogInformation("Consumption of event streams in the context '{Context}' has finished.", this.Context.Name); + await Task.Delay(this.settings.ConsumationDelay, this.CancellationToken); + } + } + catch(OperationCanceledException) + { + this.logger.LogInformation("EventConsumer for the context '{Context}' has stopped.", this.Context.Name); + } + catch (Exception exception) + { + this.logger.LogCritical(default, exception, "An error occurred while consuming event streams in the context '{Context}'.", this.Context.Name); + } + finally + { + this.IsRunning = false; + } + } + + private async Task FindAllStreamsAsync() + { + var findStreams = this.queryProcessor.InGeneric(Context).ProcessAsync(new FindEventStreams(), MessageContext.CancellableContext(this.CancellationToken)); + var findFailedStreams = this.queryProcessor.InGeneric(Context).ProcessAsync(new FindFailedEventStreams(), MessageContext.CancellableContext(this.CancellationToken)); + await Task.WhenAll(findStreams, findFailedStreams); + return new StreamsInfo(findStreams.Result, findFailedStreams.Result); + } + + private async Task ConsumeAllStreamsAsync(StreamsInfo streamsInfo) + { + var tasks = new List(); + foreach (var stream in streamsInfo.Streams) + { + var excludedStreams = streamsInfo.FailedStreams + .Where(s => s.StreamSource == stream.Source && s.StreamType == stream.Type) + .ToList(); + tasks.Add(ConsumeStreamAsync(stream, excludedStreams)); + } + await Task.WhenAll(tasks); + } + + private async Task ConsumeStreamAsync(EventStream stream, List excludedStreams) + { + await ConsumeExcludedStreamsAsync(stream, excludedStreams); + await ConsumeStreamWithoutExcludedStreamsAsync(stream, excludedStreams); + } + + private async Task ConsumeExcludedStreamsAsync(EventStream stream, List excludedStreams) + { + var activeStreams = excludedStreams.Where(s => s.IsActive()).ToList(); + var tasks = new List(); + foreach (var excludedStream in activeStreams) + tasks.Add(ConsumeExcludedStreamAsync(stream, excludedStream, excludedStreams)); + await Task.WhenAll(tasks); + } + + private async Task ConsumeStreamWithoutExcludedStreamsAsync(EventStream stream, List excludedStreams) + { + this.logger.LogInformation("Consumption of the following event stream in the context '{Context}' has started : {Stream}", this.Context.Name, stream); + var query = new ReadEventStream + { + Top = stream.BlockSize, + StreamPosition = stream.Position, + StreamType = stream.Type, + ExcludedStreamIds = excludedStreams.Select(s => s.StreamId).ToArray() + }; + var sourceContext = this.boundedContexts.Single(c => c.Code == stream.Source); + var notifiedEvents = await this.queryProcessor.InSpecific(sourceContext).ProcessAsync(query, MessageContext.CancellableContext(this.CancellationToken)); + foreach (var notifiedEvent in notifiedEvents) + { + this.CancellationToken.ThrowIfCancellationRequested(); + try + { + this.logger.LogDebug("Handling of event {EventId} in the context '{Context}' has started.", notifiedEvent.EventId, this.Context.Name); + var @event = this.DeserializeEvent(notifiedEvent); + await this.eventPublisher.PublishAsync(@event, CreateContext(notifiedEvent, stream)); + stream.Position = notifiedEvent.EventId; + this.logger.LogDebug("Handling of event {EventId} in the context '{Context}' has finished.", notifiedEvent.EventId, this.Context.Name); + } + catch (Exception exception) when (!(exception is OperationCanceledException)) + { + this.logger.LogError(default, exception, "An error occurred while handling event {EventId} in the context '{Context}'.", notifiedEvent.EventId, this.Context.Name); + var isTransientException = IsTransientException(exception); + var baseException = exception.GetBaseException(); + var command = new ExcludeFailedEventStream + { + StreamPosition = stream.Position, + StreamId = notifiedEvent.StreamId, + StreamType = notifiedEvent.StreamType, + StreamSource = stream.Source, + EventId = notifiedEvent.EventId, + EventType = notifiedEvent.EventType, + ExceptionTime = SystemTime.Local(), + ExceptionType = exception.GetType().ShortAssemblyQualifiedName(), + ExceptionMessage = exception.Message, + ExceptionSource = exception.FullSource(), + ExceptionInfo = exception.ToString(), + BaseExceptionType = baseException.GetType().ShortAssemblyQualifiedName(), + BaseExceptionMessage = baseException.Message, + RetryCount = 0, + RetryMax = this.GetRetryMax(isTransientException, stream), + RetryDelays = this.GetRetryDelays(isTransientException, stream), + BlockSize = stream.BlockSize + }; + await this.commandProcessor.InGeneric(Context).ProcessAsync(command); + break; + } + } + this.logger.LogInformation("Consumption of the following event stream in the context '{Context}' has finished : {Stream}", this.Context.Name, stream); + } + + private async Task ConsumeExcludedStreamAsync(EventStream stream, FailedEventStream excludedStream, List excludedStreams) + { + this.logger.LogInformation("Consumption of the following failed event stream in the context '{Context}' has started : {FailedStream}", this.Context.Name, excludedStream); + var query = new ReadFailedEventStream + { + Top = excludedStream.BlockSize, + EventIdMin = excludedStream.EventId, + EventIdMax = stream.Position, + StreamType = excludedStream.StreamType, + StreamId = excludedStream.StreamId + }; + var sourceContext = this.boundedContexts.Single(c => c.Code == excludedStream.StreamSource); + var notifiedEvents = await this.queryProcessor.InSpecific(sourceContext).ProcessAsync(query, MessageContext.CancellableContext(this.CancellationToken)); + var success = true; + foreach (var notifiedEvent in notifiedEvents) + { + this.CancellationToken.ThrowIfCancellationRequested(); + try + { + this.logger.LogDebug("Handling of event {EventId} in the context '{Context}' has started.", notifiedEvent.EventId, this.Context.Name); + var @event = this.DeserializeEvent(notifiedEvent); + await this.eventPublisher.PublishAsync(@event, CreateContext(notifiedEvent, excludedStream)); + excludedStream.StreamPosition = notifiedEvent.EventId; + this.logger.LogDebug("Handling of event {EventId} in the context '{Context}' has finished.", notifiedEvent.EventId, this.Context.Name); + } + catch (Exception exception) when (!(exception is OperationCanceledException)) + { + success = false; + this.logger.LogError(default, exception, "An error occurred while handling event {EventId} in the context '{Context}'.", notifiedEvent.EventId, this.Context.Name); + var isTransientException = IsTransientException(exception); + var isNewEvent = notifiedEvent.EventId != excludedStream.EventId; + var baseException = exception.GetBaseException(); + var command = new UpdateFailedEventStream + { + StreamPosition = excludedStream.StreamPosition, + StreamId = notifiedEvent.StreamId, + StreamType = notifiedEvent.StreamType, + StreamSource = excludedStream.StreamSource, + EventId = notifiedEvent.EventId, + EventType = notifiedEvent.EventType, + ExceptionTime = SystemTime.Local(), + ExceptionType = exception.GetType().ShortAssemblyQualifiedName(), + ExceptionMessage = exception.Message, + ExceptionSource = exception.FullSource(), + ExceptionInfo = exception.ToString(), + BaseExceptionType = baseException.GetType().ShortAssemblyQualifiedName(), + BaseExceptionMessage = baseException.Message, + RetryCount = isNewEvent ? (byte)0 : ++excludedStream.RetryCount, + RetryMax = isNewEvent ? this.GetRetryMax(isTransientException, stream) : excludedStream.RetryMax, + RetryDelays = isNewEvent ? this.GetRetryDelays(isTransientException, stream) : excludedStream.RetryDelays + }; + await this.commandProcessor.InGeneric(Context).ProcessAsync(command); + break; + } + } + if (success) + { + var command = new IncludeFailedEventStream + { + Id = excludedStream.StreamId, + Type = excludedStream.StreamType, + Source = excludedStream.StreamSource + }; + await this.commandProcessor.InGeneric(Context).ProcessAsync(command); + excludedStreams.Remove(excludedStream); + } + this.logger.LogInformation("Consumption of the following failed event stream in the context '{Context}' has finished : {FailedStream}", this.Context.Name, excludedStream); + } + private bool ConsumationMaxReached() + { + if (!this.settings.ConsumationMax.HasValue) return false; + return this.consumationCount >= this.settings.ConsumationMax; + } + + private IMessageContext CreateContext(Event @event, FailedEventStream stream) + { + var context = MessageContext.CancellableContext(this.CancellationToken); + context.AddEvent(@event); + context.AddFailedStream(stream); + context.AddBoundedContext(this.Context); + return context; + } + + private IMessageContext CreateContext(Event @event, EventStream stream) + { + var context = MessageContext.CancellableContext(this.CancellationToken); + context.AddEvent(@event); + context.AddStream(stream); + context.AddBoundedContext(this.Context); + return context; + } + + private IEvent DeserializeEvent(Event notifiedEvent) + { + var format = (SerializationFormat)Enum.Parse(typeof(SerializationFormat), notifiedEvent.BodyFormat, ignoreCase: true); + var type = Type.GetType(notifiedEvent.EventType); + var serializer = this.eventSerializers.First(s => s.Format == format); + return (IEvent)serializer.DeserializeFromString(notifiedEvent.Body, type); + } + + private ICollection GetRetryDelays(bool isTransientException, EventStream stream) + { + if (isTransientException) + return stream.RetryDelays; + return Array.Empty(); + } + + private byte GetRetryMax(bool isTransientException, EventStream stream) + { + if (isTransientException) + return stream.RetryMax; + return 0; + } + + private void IncrementConsumationCountIfRequired() + { + if (this.settings.ConsumationMax.HasValue) + this.consumationCount++; + } + + #endregion Methods + + #region Classes + + private class StreamsInfo + { + + #region Constructors + + public StreamsInfo(IEnumerable streams, IEnumerable failedStreams) + { + this.Streams = streams; + this.FailedStreams = failedStreams; + } + + #endregion Constructors + + #region Properties + + public IEnumerable Streams { get; } + + public IEnumerable FailedStreams { get; } + + #endregion Properties + + } + + #endregion Classes + + } +} diff --git a/Src/DDD.Core/Application/EventConsumerSettings.cs b/Src/DDD.Core/Application/EventConsumerSettings.cs new file mode 100644 index 0000000..5bd31b7 --- /dev/null +++ b/Src/DDD.Core/Application/EventConsumerSettings.cs @@ -0,0 +1,54 @@ +using EnsureThat; +using System; + +namespace DDD.Core.Application +{ + using Domain; + + public class EventConsumerSettings + where TContext : BoundedContext + { + + #region Constructors + + public EventConsumerSettings(TimeSpan consumationDelay, + long? consumationMax = null) + { + Ensure.That(consumationDelay, nameof(consumationDelay)).IsGte(TimeSpan.Zero); + if (consumationMax != null) + Ensure.That(consumationMax.Value, nameof(consumationMax)).IsGte(0); + this.ContextType = typeof(TContext); + this.ConsumationDelay = consumationDelay; + this.ConsumationMax = consumationMax; + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the type of the associated context. + /// + public Type ContextType { get; } + + /// + /// Gets the delay between two successive consumations. + /// + public TimeSpan ConsumationDelay { get; } + + /// + /// Gets the maximum number of successive consumations. + /// + public long? ConsumationMax { get; } + + #endregion Properties + + #region Methods + + public override string ToString() + => $"{this.GetType().Name} [{nameof(ContextType)}={ContextType}, {nameof(ConsumationDelay)}={ConsumationDelay}, {nameof(ConsumationMax)}={ConsumationMax}]"; + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/EventPublisher.cs b/Src/DDD.Core/Application/EventPublisher.cs new file mode 100644 index 0000000..ea163a1 --- /dev/null +++ b/Src/DDD.Core/Application/EventPublisher.cs @@ -0,0 +1,80 @@ +using EnsureThat; +using System; +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + using Domain; + using Threading; + + public class EventPublisher : IEventPublisher + where TContext : BoundedContext + { + + #region Fields + + private readonly IServiceProvider serviceProvider; + + #endregion Fields + + #region Constructors + + public EventPublisher(TContext context, IServiceProvider serviceProvider) + { + Ensure.That(context, nameof(context)).IsNotNull(); + Ensure.That(serviceProvider, nameof(serviceProvider)).IsNotNull(); + this.Context = context; + this.serviceProvider = serviceProvider; + } + + #endregion Constructors + + #region Properties + + public TContext Context { get; } + + BoundedContext IEventPublisher.Context => this.Context; + + #endregion Properties + + #region Methods + + public async Task PublishAsync(TEvent @event, IMessageContext context) where TEvent : class, IEvent + { + Ensure.That(@event, nameof(@event)).IsNotNull(); + await new SynchronizationContextRemover(); + var asyncHandler = this.GetAsyncEventHandler(@event); + if (asyncHandler != null) + await asyncHandler.HandleAsync(@event, context); + else + { + var syncHandler = this.GetSyncEventHandler(@event); + if (syncHandler != null) + syncHandler.Handle(@event, context); + } + } + + private IAsyncEventHandler GetAsyncEventHandler(TEvent @event) where TEvent : class, IEvent + { + if (typeof(TEvent) == typeof(IEvent)) + { + var handlerType = typeof(IAsyncEventHandler<,>).MakeGenericType(@event.GetType(), typeof(TContext)); + return (IAsyncEventHandler)this.serviceProvider.GetService(handlerType); + } + return this.serviceProvider.GetService>(); + } + + private ISyncEventHandler GetSyncEventHandler(TEvent @event) where TEvent : class, IEvent + { + if (typeof(TEvent) == typeof(IEvent)) + { + var handlerType = typeof(ISyncEventHandler<,>).MakeGenericType(@event.GetType(), typeof(TContext)); + return (ISyncEventHandler)this.serviceProvider.GetService(handlerType); + } + return this.serviceProvider.GetService>(); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/EventTranslator.cs b/Src/DDD.Core/Application/EventTranslator.cs new file mode 100644 index 0000000..c501ea7 --- /dev/null +++ b/Src/DDD.Core/Application/EventTranslator.cs @@ -0,0 +1,57 @@ +using EnsureThat; +using System; +using DDD.Serialization; + +namespace DDD.Core.Application +{ + using Domain; + using Mapping; + using Collections; + + public class EventTranslator : ObjectTranslator + { + + #region Fields + + private readonly ITextSerializer eventSerializer; + + #endregion Fields + + #region Constructors + + public EventTranslator(ITextSerializer eventSerializer) + { + Ensure.That(eventSerializer, nameof(eventSerializer)).IsNotNull(); + this.eventSerializer = eventSerializer; + } + + #endregion Constructors + + #region Methods + + public override Event Translate(IEvent @event, IMappingContext context) + { + Ensure.That(@event, nameof(@event)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + context.TryGetValue("EventId", out Guid eventId); + context.TryGetValue("StreamId", out string streamId); + context.TryGetValue("StreamType", out string streamType); + context.TryGetValue("IssuedBy", out string issuedBy); + var eventType = @event.GetType(); + return new Event() + { + EventId = eventId, + EventType = eventType.ShortAssemblyQualifiedName(), + OccurredOn = @event.OccurredOn, + Body = this.eventSerializer.SerializeToString(@event), + BodyFormat = this.eventSerializer.Format.ToString().ToUpper(), + StreamId = streamId, + StreamType = streamType, + IssuedBy = issuedBy + }; + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/FailedEventStreamSummaryExtensions.cs b/Src/DDD.Core/Application/FailedEventStreamSummaryExtensions.cs new file mode 100644 index 0000000..00349ad --- /dev/null +++ b/Src/DDD.Core/Application/FailedEventStreamSummaryExtensions.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using EnsureThat; + +namespace DDD.Core.Application +{ + internal static class FailedEventStreamSummaryExtensions + { + + #region Methods + + public static bool IsActive(this FailedEventStream stream) + { + Ensure.That(stream, nameof(stream)).IsNotNull(); + return stream.RetryCount < stream.RetryMax && SystemTime.Local() >= stream.RetryTime(); + } + + public static short RetryDelay(this FailedEventStream stream) + { + Ensure.That(stream, nameof(stream)).IsNotNull(); + var retryDelays = stream.AllRetryDelays(); + if (stream.RetryCount >= retryDelays.Count()) + return retryDelays.Last(); + return retryDelays.ElementAt(stream.RetryCount); + } + + public static IEnumerable AllRetryDelays(this FailedEventStream stream) + { + Ensure.That(stream, nameof(stream)).IsNotNull(); + if (stream.RetryDelays == null || stream.RetryDelays.Count == 0 || stream.RetryMax == 0) + return Array.Empty(); + if (stream.RetryDelays.Count >= stream.RetryMax) + return stream.RetryDelays.Take(stream.RetryMax).Select(d => d.Delay); + var delays = new List(stream.RetryDelays.Select(d => d.Delay)); + var last = stream.RetryDelays.Last(); + while (delays.Count < stream.RetryMax) + { + last = last.Next(); + delays.Add(last.Delay); + } + return delays; + } + + public static DateTime RetryTime(this FailedEventStream stream) + { + Ensure.That(stream, nameof(stream)).IsNotNull(); + return stream.ExceptionTime.AddMinutes(stream.RetryDelay()); + } + + private static IncrementalDelay Next(this IncrementalDelay delay) + { + Ensure.That(delay, nameof(delay)).IsNotNull(); + Ensure.That(delay.Increment, $"{nameof(delay)}.{nameof(delay.Increment)}").IsGte((short)0); + return new IncrementalDelay + { + Delay = (short)(delay.Delay + delay.Increment), + Increment = delay.Increment + }; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/IAsyncCommandHandler.cs b/Src/DDD.Core/Application/IAsyncCommandHandler.cs index 39e7a3c..5805bff 100644 --- a/Src/DDD.Core/Application/IAsyncCommandHandler.cs +++ b/Src/DDD.Core/Application/IAsyncCommandHandler.cs @@ -11,7 +11,10 @@ public interface IAsyncCommandHandler #region Methods - Task HandleAsync(TCommand command); + /// + /// Handles asynchronously a command of a specified type. + /// + Task HandleAsync(TCommand command, IMessageContext context); #endregion Methods diff --git a/Src/DDD.Core/Application/IAsyncCommandHandlerExtensions.cs b/Src/DDD.Core/Application/IAsyncCommandHandlerExtensions.cs new file mode 100644 index 0000000..5b8c3ad --- /dev/null +++ b/Src/DDD.Core/Application/IAsyncCommandHandlerExtensions.cs @@ -0,0 +1,33 @@ +using EnsureThat; +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + public static class IAsyncCommandHandlerExtensions + { + + #region Methods + + public static Task HandleAsync(this IAsyncCommandHandler handler, + TCommand command) + where TCommand : class, ICommand + + { + Ensure.That(handler, nameof(handler)).IsNotNull(); + return handler.HandleAsync(command, new MessageContext()); + } + + public static Task HandleAsync(this IAsyncCommandHandler handler, + TCommand command, + object context) + where TCommand : class, ICommand + + { + Ensure.That(handler, nameof(handler)).IsNotNull(); + return handler.HandleAsync(command, MessageContext.FromObject(context)); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/IAsyncCommandHandler`1.cs b/Src/DDD.Core/Application/IAsyncCommandHandler`1.cs new file mode 100644 index 0000000..6e4218a --- /dev/null +++ b/Src/DDD.Core/Application/IAsyncCommandHandler`1.cs @@ -0,0 +1,23 @@ +namespace DDD.Core.Application +{ + using Domain; + + /// + /// Defines a method that handles asynchronously a command of a specified type in a specific bounded context. + /// + public interface IAsyncCommandHandler : IAsyncCommandHandler + where TCommand : class, ICommand + where TContext : BoundedContext + { + + #region Properties + + /// + /// The bounded context in which the command is handled. + /// + TContext Context { get; } + + #endregion Properties + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/IAsyncCommandValidator.cs b/Src/DDD.Core/Application/IAsyncCommandValidator.cs deleted file mode 100644 index 82b4d1b..0000000 --- a/Src/DDD.Core/Application/IAsyncCommandValidator.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace DDD.Core.Application -{ - using Validation; - - /// - /// Defines a method that validates asynchronously a command of a specified type. - /// - public interface IAsyncCommandValidator : IAsyncObjectValidator - where TCommand : class, ICommand - { - } -} \ No newline at end of file diff --git a/Src/DDD.Core/Application/IAsyncEventHandler.cs b/Src/DDD.Core/Application/IAsyncEventHandler.cs new file mode 100644 index 0000000..325badc --- /dev/null +++ b/Src/DDD.Core/Application/IAsyncEventHandler.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + using Domain; + + /// + /// Defines a method that handles asynchronously an event of a specified type in a specific bounded context. + /// + public interface IAsyncEventHandler + { + #region Properties + + /// + /// The bounded context in which the event is handled. + /// + BoundedContext Context { get; } + + /// + /// The event type. + /// + Type EventType { get; } + + #endregion Properties + + #region Methods + + /// + /// Handles asynchronously an event of a specified type in a specific bounded context. + /// + Task HandleAsync(IEvent @event, IMessageContext context); + + #endregion Methods + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/IAsyncEventHandler`1.cs b/Src/DDD.Core/Application/IAsyncEventHandler`1.cs new file mode 100644 index 0000000..b78b985 --- /dev/null +++ b/Src/DDD.Core/Application/IAsyncEventHandler`1.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + using Domain; + + /// + /// Defines a method that handles asynchronously an event of a specified type in a specific bounded context. + /// + public interface IAsyncEventHandler : IAsyncEventHandler + where TEvent : class, IEvent + where TContext : BoundedContext + { + + #region Properties + + /// + /// The bounded context in which the event is handled. + /// + new TContext Context { get; } + + #endregion Properties + + #region Methods + + /// + /// Handles asynchronously an event of a specified type in a specific bounded context. + /// + Task HandleAsync(TEvent @event, IMessageContext context); + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/IAsyncQueryHandler.cs b/Src/DDD.Core/Application/IAsyncQueryHandler.cs index bd144b5..19747ea 100644 --- a/Src/DDD.Core/Application/IAsyncQueryHandler.cs +++ b/Src/DDD.Core/Application/IAsyncQueryHandler.cs @@ -12,7 +12,10 @@ public interface IAsyncQueryHandler { #region Methods - Task HandleAsync(TQuery query); + /// + /// Handles asynchronously a query of a specified type and provides a result of a specified type. + /// + Task HandleAsync(TQuery query, IMessageContext context); #endregion Methods } diff --git a/Src/DDD.Core/Application/IAsyncQueryHandlerExtensions.cs b/Src/DDD.Core/Application/IAsyncQueryHandlerExtensions.cs new file mode 100644 index 0000000..ee5c20d --- /dev/null +++ b/Src/DDD.Core/Application/IAsyncQueryHandlerExtensions.cs @@ -0,0 +1,33 @@ +using EnsureThat; +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + public static class IAsyncQueryHandlerExtensions + { + + #region Methods + + public static Task HandleAsync(this IAsyncQueryHandler handler, + TQuery query) + where TQuery : class, IQuery + + { + Ensure.That(handler, nameof(handler)).IsNotNull(); + return handler.HandleAsync(query, new MessageContext()); + } + + public static Task HandleAsync(this IAsyncQueryHandler handler, + TQuery query, + object context) + where TQuery : class, IQuery + + { + Ensure.That(handler, nameof(handler)).IsNotNull(); + return handler.HandleAsync(query, MessageContext.FromObject(context)); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/IAsyncQueryHandler`1.cs b/Src/DDD.Core/Application/IAsyncQueryHandler`1.cs new file mode 100644 index 0000000..fb2dd22 --- /dev/null +++ b/Src/DDD.Core/Application/IAsyncQueryHandler`1.cs @@ -0,0 +1,26 @@ +namespace DDD.Core.Application +{ + using Domain; + + /// + /// Defines a method that handles asynchronously a query of a specified type and provides a result of a specified type in a specific bounded context. + /// + /// The type of the query. + /// The type of the result. + /// The type of the context. + public interface IAsyncQueryHandler : IAsyncQueryHandler + where TQuery : class, IQuery + where TContext : BoundedContext + { + + #region Properties + + /// + /// The bounded context in which the query is handled. + /// + TContext Context { get; } + + #endregion Properties + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/IAsyncQueryValidator.cs b/Src/DDD.Core/Application/IAsyncQueryValidator.cs deleted file mode 100644 index 670ce66..0000000 --- a/Src/DDD.Core/Application/IAsyncQueryValidator.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace DDD.Core.Application -{ - using Validation; - - /// - /// Defines a method that validates asynchronously a query of a specified type. - /// - public interface IAsyncQueryValidator : IAsyncObjectValidator - where TQuery : class, IQuery - { - } -} \ No newline at end of file diff --git a/Src/DDD.Core/Application/ICommandHandler.cs b/Src/DDD.Core/Application/ICommandHandler.cs index 41dffa1..8de6817 100644 --- a/Src/DDD.Core/Application/ICommandHandler.cs +++ b/Src/DDD.Core/Application/ICommandHandler.cs @@ -1,17 +1,10 @@ namespace DDD.Core.Application { /// - /// Defines a method that handles a command of a specified type. + /// Defines methods that handle synchronously and asynchronously a command of a specified type. /// - public interface ICommandHandler + public interface ICommandHandler : ISyncCommandHandler, IAsyncCommandHandler where TCommand : class, ICommand { - - #region Methods - - void Handle(TCommand command); - - #endregion Methods - } } \ No newline at end of file diff --git a/Src/DDD.Core/Application/ICommandHandler`1.cs b/Src/DDD.Core/Application/ICommandHandler`1.cs new file mode 100644 index 0000000..fe9d650 --- /dev/null +++ b/Src/DDD.Core/Application/ICommandHandler`1.cs @@ -0,0 +1,13 @@ +namespace DDD.Core.Application +{ + using Domain; + + /// + /// Defines methods that handle synchronously and asynchronously a command of a specified type in a specific bounded context. + /// + public interface ICommandHandler : ISyncCommandHandler, IAsyncCommandHandler + where TCommand : class, ICommand + where TContext : BoundedContext + { + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/ICommandProcessor.cs b/Src/DDD.Core/Application/ICommandProcessor.cs index b58bd1b..26f5dc4 100644 --- a/Src/DDD.Core/Application/ICommandProcessor.cs +++ b/Src/DDD.Core/Application/ICommandProcessor.cs @@ -3,6 +3,7 @@ namespace DDD.Core.Application { using Validation; + using Domain; /// /// Defines a component that validates and processes commands of any type. @@ -12,15 +13,56 @@ public interface ICommandProcessor #region Methods - void Process(TCommand command) where TCommand : class, ICommand; + /// + /// Specify the bounded context in which the command must be processed. + /// + IContextualCommandProcessor InGeneric(TContext context) where TContext : BoundedContext; - Task ProcessAsync(TCommand command) where TCommand : class, ICommand; + /// + /// Specify the bounded context in which the command must be processed. + /// + IContextualCommandProcessor InSpecific(BoundedContext context); - ValidationResult Validate(TCommand command, string ruleSet = null) where TCommand : class, ICommand; + /// + /// Processes synchronously a command of a specified type. + /// + void Process(TCommand command, IMessageContext context) where TCommand : class, ICommand; - Task ValidateAsync(TCommand command, string ruleSet = null) where TCommand : class, ICommand; + /// + /// Processes synchronously a command. + /// + void Process(ICommand command, IMessageContext context); - #endregion Methods + /// + /// Processes asynchronously a command of a specified type. + /// + Task ProcessAsync(TCommand command, IMessageContext context) where TCommand : class, ICommand; + + /// + /// Processes asynchronously a command. + /// + Task ProcessAsync(ICommand command, IMessageContext context); + + /// + /// Validates synchronously a command of a specified type. + /// + ValidationResult Validate(TCommand command, IValidationContext context) where TCommand : class, ICommand; + + /// + /// Validates synchronously a command. + /// + ValidationResult Validate(ICommand command, IValidationContext context); + /// + /// Validates asynchronously a command of a specified type. + /// + Task ValidateAsync(TCommand command, IValidationContext context) where TCommand : class, ICommand; + + /// + /// Validates asynchronously a command. + /// + Task ValidateAsync(ICommand command, IValidationContext context); + + #endregion Methods } } diff --git a/Src/DDD.Core/Application/ICommandProcessorExtensions.cs b/Src/DDD.Core/Application/ICommandProcessorExtensions.cs new file mode 100644 index 0000000..3dc18ab --- /dev/null +++ b/Src/DDD.Core/Application/ICommandProcessorExtensions.cs @@ -0,0 +1,209 @@ +using EnsureThat; +using System; +using System.Threading.Tasks; +using DDD.Validation; + +namespace DDD.Core.Application +{ + using Threading; + + public static class ICommandProcessorExtensions + { + + #region Methods + + public static void Process(this ICommandProcessor processor, + TCommand command) + where TCommand : class, ICommand + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + processor.Process(command, new MessageContext()); + } + + public static void Process(this ICommandProcessor processor, + TCommand command, + object context) + where TCommand : class, ICommand + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + processor.Process(command, MessageContext.FromObject(context)); + } + + public static void Process(this ICommandProcessor processor, + ICommand command) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + processor.Process(command, new MessageContext()); + } + + public static void Process(this ICommandProcessor processor, + ICommand command, + object context) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + processor.Process(command, MessageContext.FromObject(context)); + } + + public static Task ProcessAsync(this ICommandProcessor processor, + TCommand command) + where TCommand : class, ICommand + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessAsync(command, new MessageContext()); + } + + public static Task ProcessAsync(this ICommandProcessor processor, + TCommand command, + object context) + where TCommand : class, ICommand + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessAsync(command, MessageContext.FromObject(context)); + } + + public static Task ProcessAsync(this ICommandProcessor processor, + ICommand command) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessAsync(command, new MessageContext()); + } + + public static Task ProcessAsync(this ICommandProcessor processor, + ICommand command, + object context) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessAsync(command, MessageContext.FromObject(context)); + } + + public static Task ProcessWithDelayAsync(this ICommandProcessor processor, + TCommand command, + TimeSpan delay) + where TCommand : class, ICommand + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessWithDelayAsync(command, delay, new MessageContext()); + } + + public static async Task ProcessWithDelayAsync(this ICommandProcessor processor, + TCommand command, + TimeSpan delay, + IMessageContext context) + where TCommand : class, ICommand + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + await new SynchronizationContextRemover(); + var cancellationToken = context.CancellationToken(); + await Task.Delay(delay, cancellationToken); + await processor.ProcessAsync(command, context); + } + + public static Task ProcessWithDelayAsync(this ICommandProcessor processor, + TCommand command, + TimeSpan delay, + object context) + where TCommand : class, ICommand + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessWithDelayAsync(command, delay, MessageContext.FromObject(context)); + } + + public static Task ProcessWithDelayAsync(this ICommandProcessor processor, + ICommand command, + TimeSpan delay) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessWithDelayAsync(command, delay, new MessageContext()); + } + + public static async Task ProcessWithDelayAsync(this ICommandProcessor processor, + ICommand command, + TimeSpan delay, + IMessageContext context) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + await new SynchronizationContextRemover(); + var cancellationToken = context.CancellationToken(); + await Task.Delay(delay, cancellationToken); + await processor.ProcessAsync(command, context); + } + + public static Task ProcessWithDelayAsync(this ICommandProcessor processor, + ICommand command, + TimeSpan delay, + object context) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessWithDelayAsync(command, delay, MessageContext.FromObject(context)); + } + + public static ValidationResult Validate(this ICommandProcessor processor, + TCommand command) + where TCommand : class, ICommand + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.Validate(command, new ValidationContext()); + } + + public static ValidationResult Validate(this ICommandProcessor processor, + TCommand command, + object context) + where TCommand : class, ICommand + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.Validate(command, ValidationContext.FromObject(context)); + } + + public static ValidationResult Validate(this ICommandProcessor processor, + ICommand command) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.Validate(command, new ValidationContext()); + } + + public static ValidationResult Validate(this ICommandProcessor processor, + ICommand command, + object context) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.Validate(command, ValidationContext.FromObject(context)); + } + + public static Task ValidateAsync(this ICommandProcessor processor, + TCommand command) + where TCommand : class, ICommand + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ValidateAsync(command, new ValidationContext()); + } + + public static Task ValidateAsync(this ICommandProcessor processor, + TCommand command, + object context) + where TCommand : class, ICommand + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ValidateAsync(command, ValidationContext.FromObject(context)); + } + + public static Task ValidateAsync(this ICommandProcessor processor, + ICommand command) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ValidateAsync(command, new ValidationContext()); + } + + public static Task ValidateAsync(this ICommandProcessor processor, + ICommand command, + object context) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ValidateAsync(command, ValidationContext.FromObject(context)); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/ICommandValidator.cs b/Src/DDD.Core/Application/ICommandValidator.cs deleted file mode 100644 index 578d346..0000000 --- a/Src/DDD.Core/Application/ICommandValidator.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace DDD.Core.Application -{ - using Validation; - - /// - /// Defines a method that validates a command of a specified type. - /// - public interface ICommandValidator : IObjectValidator - where TCommand : class, ICommand - { - } -} \ No newline at end of file diff --git a/Src/DDD.Core/Application/IContextualCommandProcessor.cs b/Src/DDD.Core/Application/IContextualCommandProcessor.cs new file mode 100644 index 0000000..2bc24fb --- /dev/null +++ b/Src/DDD.Core/Application/IContextualCommandProcessor.cs @@ -0,0 +1,48 @@ +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + using Domain; + + /// + /// Defines a component that processes commands in a specific bounded context. + /// + /// + /// This component is used to implement a generic mechanism to consume events and manage recurring commands in the different bounded contexts. + /// + public interface IContextualCommandProcessor + { + #region Properties + + /// + /// The bounded context in which the command is processed. + /// + BoundedContext Context { get; } + + #endregion Properties + + #region Methods + + /// + /// Processes synchronously a command of a specified type in the specific bounded context. + /// + void Process(TCommand command, IMessageContext context) where TCommand : class, ICommand; + + /// + /// Processes synchronously a command in the specific bounded context. + /// + void Process(ICommand command, IMessageContext context); + + /// + /// Processes asynchronously a command of a specified type in the specific bounded context. + /// + Task ProcessAsync(TCommand command, IMessageContext context) where TCommand : class, ICommand; + + /// + /// Processes asynchronously a command in the specific bounded context. + /// + Task ProcessAsync(ICommand command, IMessageContext context); + + #endregion Methods + } +} diff --git a/Src/DDD.Core/Application/IContextualCommandProcessorExtensions.cs b/Src/DDD.Core/Application/IContextualCommandProcessorExtensions.cs new file mode 100644 index 0000000..c51368c --- /dev/null +++ b/Src/DDD.Core/Application/IContextualCommandProcessorExtensions.cs @@ -0,0 +1,144 @@ +using EnsureThat; +using System; +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + using Threading; + + public static class IContextualCommandProcessorExtensions + { + + #region Methods + + public static void Process(this IContextualCommandProcessor processor, + TCommand command) + where TCommand : class, ICommand + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + processor.Process(command, new MessageContext()); + } + + public static void Process(this IContextualCommandProcessor processor, + TCommand command, + object context) + where TCommand : class, ICommand + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + processor.Process(command, MessageContext.FromObject(context)); + } + + public static void Process(this IContextualCommandProcessor processor, + ICommand command) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + processor.Process(command, new MessageContext()); + } + + public static void Process(this IContextualCommandProcessor processor, + ICommand command, + object context) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + processor.Process(command, MessageContext.FromObject(context)); + } + + public static Task ProcessAsync(this IContextualCommandProcessor processor, + TCommand command) + where TCommand : class, ICommand + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessAsync(command, new MessageContext()); + } + + public static Task ProcessAsync(this IContextualCommandProcessor processor, + TCommand command, + object context) + where TCommand : class, ICommand + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessAsync(command, MessageContext.FromObject(context)); + } + + public static Task ProcessAsync(this IContextualCommandProcessor processor, + ICommand command) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessAsync(command, new MessageContext()); + } + + public static Task ProcessAsync(this IContextualCommandProcessor processor, + ICommand command, + object context) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessAsync(command, MessageContext.FromObject(context)); + } + + public static Task ProcessWithDelayAsync(this IContextualCommandProcessor processor, + TCommand command, + TimeSpan delay) + where TCommand : class, ICommand + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessWithDelayAsync(command, delay, new MessageContext()); + } + + public static async Task ProcessWithDelayAsync(this IContextualCommandProcessor processor, + TCommand command, + TimeSpan delay, + IMessageContext context) + where TCommand : class, ICommand + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + await new SynchronizationContextRemover(); + var cancellationToken = context.CancellationToken(); + await Task.Delay(delay, cancellationToken); + await processor.ProcessAsync(command, context); + } + + public static Task ProcessWithDelayAsync(this IContextualCommandProcessor processor, + TCommand command, + TimeSpan delay, + object context) + where TCommand : class, ICommand + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessWithDelayAsync(command, delay, MessageContext.FromObject(context)); + } + + public static Task ProcessWithDelayAsync(this IContextualCommandProcessor processor, + ICommand command, + TimeSpan delay) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessWithDelayAsync(command, delay, new MessageContext()); + } + + public static async Task ProcessWithDelayAsync(this IContextualCommandProcessor processor, + ICommand command, + TimeSpan delay, + IMessageContext context) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + await new SynchronizationContextRemover(); + var cancellationToken = context.CancellationToken(); + await Task.Delay(delay, cancellationToken); + await processor.ProcessAsync(command, context); + } + + public static Task ProcessWithDelayAsync(this IContextualCommandProcessor processor, + ICommand command, + TimeSpan delay, + object context) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessWithDelayAsync(command, delay, MessageContext.FromObject(context)); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/IContextualCommandProcessor`1.cs b/Src/DDD.Core/Application/IContextualCommandProcessor`1.cs new file mode 100644 index 0000000..120d001 --- /dev/null +++ b/Src/DDD.Core/Application/IContextualCommandProcessor`1.cs @@ -0,0 +1,24 @@ +namespace DDD.Core.Application +{ + using Domain; + + /// + /// Defines a component that processes commands in a specific bounded context. + /// + /// + /// This component is used to implement a generic mechanism to consume events and manage recurring commands in the different bounded contexts. + /// + public interface IContextualCommandProcessor : IContextualCommandProcessor + where TContext : BoundedContext + { + #region Properties + + /// + /// The bounded context in which the command is processed. + /// + new TContext Context { get; } + + #endregion Properties + + } +} diff --git a/Src/DDD.Core/Application/IContextualQueryProcessor.cs b/Src/DDD.Core/Application/IContextualQueryProcessor.cs new file mode 100644 index 0000000..0a5cd13 --- /dev/null +++ b/Src/DDD.Core/Application/IContextualQueryProcessor.cs @@ -0,0 +1,50 @@ +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + using Domain; + + /// + /// Defines a component that processes generic queries in a specific bounded context. + /// + /// + /// This component is used to implement a generic mechanism to consume events and manage recurring commands in the different bounded contexts. + /// + public interface IContextualQueryProcessor + { + + #region Properties + + /// + /// The bounded context in which the query is processed. + /// + BoundedContext Context { get; } + + #endregion Properties + + #region Methods + + /// + /// Processes synchronously a query of a specified type and provides a result of a specified type. + /// + TResult Process(IQuery query, IMessageContext context); + + /// + /// Processes synchronously a query. + /// + object Process(IQuery query, IMessageContext context); + + /// + /// Processes asynchronously a query of a specified type and provides a result of a specified type. + /// + Task ProcessAsync(IQuery query, IMessageContext context); + + /// + /// Processes asynchronously a query. + /// + Task ProcessAsync(IQuery query, IMessageContext context); + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/IContextualQueryProcessorExtensions.cs b/Src/DDD.Core/Application/IContextualQueryProcessorExtensions.cs new file mode 100644 index 0000000..4f83269 --- /dev/null +++ b/Src/DDD.Core/Application/IContextualQueryProcessorExtensions.cs @@ -0,0 +1,74 @@ +using EnsureThat; +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + public static class IContextualQueryProcessorExtensions + { + + #region Methods + + public static TResult Process(this IContextualQueryProcessor processor, + IQuery query) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.Process(query, new MessageContext()); + } + + public static TResult Process(this IContextualQueryProcessor processor, + IQuery query, + object context) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.Process(query, MessageContext.FromObject(context)); + } + + public static object Process(this IContextualQueryProcessor processor, + IQuery query) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.Process(query, new MessageContext()); + } + + public static object Process(this IContextualQueryProcessor processor, + IQuery query, + object context) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.Process(query, MessageContext.FromObject(context)); + } + + public static Task ProcessAsync(this IContextualQueryProcessor processor, + IQuery query) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessAsync(query, new MessageContext()); + } + + public static Task ProcessAsync(this IContextualQueryProcessor processor, + IQuery query, + object context) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessAsync(query, MessageContext.FromObject(context)); + } + + public static Task ProcessAsync(this IContextualQueryProcessor processor, + IQuery query) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessAsync(query, new MessageContext()); + } + + public static Task ProcessAsync(this IContextualQueryProcessor processor, + IQuery query, + object context) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessAsync(query, MessageContext.FromObject(context)); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/IContextualQueryProcessor`1.cs b/Src/DDD.Core/Application/IContextualQueryProcessor`1.cs new file mode 100644 index 0000000..2c95061 --- /dev/null +++ b/Src/DDD.Core/Application/IContextualQueryProcessor`1.cs @@ -0,0 +1,25 @@ +namespace DDD.Core.Application +{ + using Domain; + + /// + /// Defines a component that processes generic queries in a specific bounded context. + /// + /// + /// This component is used to implement a generic mechanism to consume events and manage recurring commands in the different bounded contexts. + /// + public interface IContextualQueryProcessor : IContextualQueryProcessor + where TContext : BoundedContext + { + + #region Properties + + /// + /// The bounded context in which the query is processed. + /// + new TContext Context { get; } + + #endregion Properties + + } +} diff --git a/Src/DDD.Core/Application/IEventConsumer.cs b/Src/DDD.Core/Application/IEventConsumer.cs new file mode 100644 index 0000000..1abf623 --- /dev/null +++ b/Src/DDD.Core/Application/IEventConsumer.cs @@ -0,0 +1,50 @@ +using System; + +namespace DDD.Core.Application +{ + using Domain; + + /// + /// Defines methods for consuming events in a specific bounded context. + /// + /// + /// This component is used to read external event streams and to publish those events in a bounded context. + /// + public interface IEventConsumer + { + + #region Properties + + /// + /// The bounded context in which events are consumed. + /// + BoundedContext Context { get; } + + /// + /// Determines whether the consumer is running. + /// + bool IsRunning { get; } + + #endregion Properties + + #region Methods + + /// + /// Starts consuming events in a specific bounded context. + /// + public void Start(); + + /// + /// Stops consuming events in a specific bounded context. + /// + public void Stop(); + + /// + /// Waits for the event consumation to complete execution within a specified time interval. + /// + public void Wait(TimeSpan? timeout); + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/IEventConsumer`1.cs b/Src/DDD.Core/Application/IEventConsumer`1.cs new file mode 100644 index 0000000..2c39cf4 --- /dev/null +++ b/Src/DDD.Core/Application/IEventConsumer`1.cs @@ -0,0 +1,23 @@ +namespace DDD.Core.Application +{ + using Domain; + + /// + /// Defines methods for consuming events in a specific bounded context. + /// + /// + /// This component is used to read external event streams and to publish those events in a bounded context. + /// + public interface IEventConsumer : IEventConsumer + where TContext : BoundedContext + { + #region Properties + + /// + /// The bounded context in which events are consumed. + /// + new TContext Context { get; } + + #endregion Properties + } +} diff --git a/Src/DDD.Core/Application/IEventPublisher.cs b/Src/DDD.Core/Application/IEventPublisher.cs new file mode 100644 index 0000000..ae2fae2 --- /dev/null +++ b/Src/DDD.Core/Application/IEventPublisher.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + using Domain; + + /// + /// Defines a method that publishes events of any type in a specific bounded context. + /// + public interface IEventPublisher + { + + #region Properties + + /// + /// The bounded context in which events are published. + /// + BoundedContext Context { get; } + + #endregion Properties + + #region Methods + + /// + /// Publishes an event in a specific bounded context. + /// + Task PublishAsync(TEvent @event, IMessageContext context) where TEvent : class, IEvent; + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/IEventPublisherExtensions.cs b/Src/DDD.Core/Application/IEventPublisherExtensions.cs new file mode 100644 index 0000000..653d675 --- /dev/null +++ b/Src/DDD.Core/Application/IEventPublisherExtensions.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using EnsureThat; + +namespace DDD.Core.Application +{ + using Domain; + + public static class IEventPublisherExtensions + { + + #region Methods + + public static Task PublishAsync(this IEventPublisher publisher, + TEvent @event) + where TEvent : class, IEvent + { + Ensure.That(publisher, nameof(publisher)).IsNotNull(); + return publisher.PublishAsync(@event, new MessageContext()); + } + + public static Task PublishAsync(this IEventPublisher publisher, + TEvent @event, + object context) + where TEvent : class, IEvent + { + Ensure.That(publisher, nameof(publisher)).IsNotNull(); + return publisher.PublishAsync(@event, MessageContext.FromObject(context)); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/IEventPublisher`1.cs b/Src/DDD.Core/Application/IEventPublisher`1.cs new file mode 100644 index 0000000..9918dc9 --- /dev/null +++ b/Src/DDD.Core/Application/IEventPublisher`1.cs @@ -0,0 +1,20 @@ +namespace DDD.Core.Application +{ + using Domain; + + /// + /// Defines a method that publishes events of any type in a specific bounded context. + /// + public interface IEventPublisher : IEventPublisher + where TContext: BoundedContext + { + #region Properties + + /// + /// The bounded context in which events are published. + /// + new TContext Context { get; } + + #endregion Properties + } +} diff --git a/Src/DDD.Core/Application/IMessageContext.cs b/Src/DDD.Core/Application/IMessageContext.cs new file mode 100644 index 0000000..25c2c38 --- /dev/null +++ b/Src/DDD.Core/Application/IMessageContext.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace DDD.Core.Application +{ + /// + /// Provides access to the execution context of a message. + /// + public interface IMessageContext : IDictionary + { + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/IMessageContextExtensions.cs b/Src/DDD.Core/Application/IMessageContextExtensions.cs new file mode 100644 index 0000000..580f9ce --- /dev/null +++ b/Src/DDD.Core/Application/IMessageContextExtensions.cs @@ -0,0 +1,91 @@ +using EnsureThat; +using System.Threading; + +namespace DDD.Core.Application +{ + using Collections; + using Domain; + + public static class IMessageContextExtensions + { + + #region Methods + + public static void AddBoundedContext(this IMessageContext context, BoundedContext boundedContext) + { + Ensure.That(context, nameof(context)).IsNotNull(); + Ensure.That(boundedContext, nameof(boundedContext)).IsNotNull(); + context.Add(MessageContextInfo.BoundedContext, boundedContext); + } + + public static void AddCancellationToken(this IMessageContext context, CancellationToken cancellationToken) + { + Ensure.That(context, nameof(context)).IsNotNull(); + context.Add(MessageContextInfo.CancellationToken, cancellationToken); + } + + public static void AddEvent(this IMessageContext context, Event @event) + { + Ensure.That(context, nameof(context)).IsNotNull(); + Ensure.That(@event, nameof(@event)).IsNotNull(); + context.Add(MessageContextInfo.Event, @event); + } + + public static void AddFailedStream(this IMessageContext context, FailedEventStream stream) + { + Ensure.That(context, nameof(context)).IsNotNull(); + Ensure.That(stream, nameof(stream)).IsNotNull(); + context.Add(MessageContextInfo.FailedStream, stream); + } + + public static void AddStream(this IMessageContext context, EventStream stream) + { + Ensure.That(context, nameof(context)).IsNotNull(); + Ensure.That(stream, nameof(stream)).IsNotNull(); + context.Add(MessageContextInfo.Stream, stream); + } + + public static BoundedContext BoundedContext(this IMessageContext context) + { + Ensure.That(context, nameof(context)).IsNotNull(); + context.TryGetValue(MessageContextInfo.BoundedContext, out BoundedContext boundedContext); + return boundedContext; + } + + public static CancellationToken CancellationToken(this IMessageContext context) + { + Ensure.That(context, nameof(context)).IsNotNull(); + context.TryGetValue(MessageContextInfo.CancellationToken, out CancellationToken cancellationToken); + return cancellationToken; + } + + public static Event Event(this IMessageContext context) + { + Ensure.That(context, nameof(context)).IsNotNull(); + context.TryGetValue(MessageContextInfo.Event, out Event @event); + return @event; + } + + public static FailedEventStream FailedStream(this IMessageContext context) + { + Ensure.That(context, nameof(context)).IsNotNull(); + context.TryGetValue(MessageContextInfo.FailedStream, out FailedEventStream stream); + return stream; + } + + public static bool IsEventHandling(this IMessageContext context) + { + Ensure.That(context, nameof(context)).IsNotNull(); + return context.ContainsKey(MessageContextInfo.Event); + } + + public static EventStream Stream(this IMessageContext context) + { + Ensure.That(context, nameof(context)).IsNotNull(); + context.TryGetValue(MessageContextInfo.Stream, out EventStream stream); + return stream; + } + + #endregion Methods + } +} diff --git a/Src/DDD.Core/Application/IQueryHandler.cs b/Src/DDD.Core/Application/IQueryHandler.cs index 8866ae8..bb5d435 100644 --- a/Src/DDD.Core/Application/IQueryHandler.cs +++ b/Src/DDD.Core/Application/IQueryHandler.cs @@ -1,17 +1,12 @@ namespace DDD.Core.Application { /// - /// Defines a method that handles a query of a specified type and provides a result of a specified type. + /// Defines methods that handle synchronously and asynchronously a query of a specified type and provides a result of a specified type. /// /// The type of the query. /// The type of the result. - public interface IQueryHandler + public interface IQueryHandler : ISyncQueryHandler, IAsyncQueryHandler where TQuery : class, IQuery { - #region Methods - - TResult Handle(TQuery query); - - #endregion Methods } -} \ No newline at end of file +} diff --git a/Src/DDD.Core/Application/IQueryHandler`1.cs b/Src/DDD.Core/Application/IQueryHandler`1.cs new file mode 100644 index 0000000..ca18295 --- /dev/null +++ b/Src/DDD.Core/Application/IQueryHandler`1.cs @@ -0,0 +1,16 @@ +namespace DDD.Core.Application +{ + using Domain; + + /// + /// Defines methods that handle synchronously and asynchronously a query of a specified type and provides a result of a specified type in a specific bounded context. + /// + /// The type of the query. + /// The type of the result. + /// The type of the context. + public interface IQueryHandler : ISyncQueryHandler, IAsyncQueryHandler + where TQuery : class, IQuery + where TContext : BoundedContext + { + } +} diff --git a/Src/DDD.Core/Application/IQueryProcessor.cs b/Src/DDD.Core/Application/IQueryProcessor.cs index 75441ff..772f807 100644 --- a/Src/DDD.Core/Application/IQueryProcessor.cs +++ b/Src/DDD.Core/Application/IQueryProcessor.cs @@ -3,6 +3,7 @@ namespace DDD.Core.Application { using Validation; + using Domain; /// /// Defines a component that validates and processes queries of any type. @@ -12,15 +13,56 @@ public interface IQueryProcessor #region Methods - TResult Process(IQuery query); + /// + /// Specify the bounded context in which the query must be processed. + /// + IContextualQueryProcessor InGeneric(TContext context) where TContext : BoundedContext; - Task ProcessAsync(IQuery query); + /// + /// Specify the bounded context in which the query must be processed. + /// + IContextualQueryProcessor InSpecific(BoundedContext context); - ValidationResult Validate(TQuery query, string ruleSet = null) where TQuery : class, IQuery; + /// + /// Processes synchronously a query of a specified type and provides a result of a specified type. + /// + TResult Process(IQuery query, IMessageContext context); - Task ValidateAsync(TQuery query, string ruleSet = null) where TQuery : class, IQuery; + /// + /// Processes synchronously a query. + /// + object Process(IQuery query, IMessageContext context); - #endregion Methods + /// + /// Processes asynchronously a query of a specified type and provides a result of a specified type. + /// + Task ProcessAsync(IQuery query, IMessageContext context); + + /// + /// Processes asynchronously a query. + /// + Task ProcessAsync(IQuery query, IMessageContext context); + + /// + /// Validates synchronously a query of a specified type. + /// + ValidationResult Validate(TQuery query, IValidationContext context) where TQuery : class, IQuery; + + /// + /// Validates synchronously a query. + /// + ValidationResult Validate(IQuery query, IValidationContext context); + /// + /// Validates asynchronously a query of a specified type. + /// + Task ValidateAsync(TQuery query, IValidationContext context) where TQuery : class, IQuery; + + /// + /// Validates asynchronously a query. + /// + Task ValidateAsync(IQuery query, IValidationContext context); + + #endregion Methods } } \ No newline at end of file diff --git a/Src/DDD.Core/Application/IQueryProcessorExtensions.cs b/Src/DDD.Core/Application/IQueryProcessorExtensions.cs new file mode 100644 index 0000000..784d358 --- /dev/null +++ b/Src/DDD.Core/Application/IQueryProcessorExtensions.cs @@ -0,0 +1,140 @@ +using EnsureThat; +using System.Threading.Tasks; +using DDD.Validation; + +namespace DDD.Core.Application +{ + + public static class IQueryProcessorExtensions + { + + #region Methods + + public static TResult Process(this IQueryProcessor processor, + IQuery query) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.Process(query, new MessageContext()); + } + + public static TResult Process(this IQueryProcessor processor, + IQuery query, + object context) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.Process(query, MessageContext.FromObject(context)); + } + + public static object Process(this IQueryProcessor processor, + IQuery query) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.Process(query, new MessageContext()); + } + + public static object Process(this IQueryProcessor processor, + IQuery query, + object context) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.Process(query, MessageContext.FromObject(context)); + } + + public static Task ProcessAsync(this IQueryProcessor processor, + IQuery query) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessAsync(query, new MessageContext()); + } + + public static Task ProcessAsync(this IQueryProcessor processor, + IQuery query, + object context) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessAsync(query, MessageContext.FromObject(context)); + } + + public static Task ProcessAsync(this IQueryProcessor processor, + IQuery query) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessAsync(query, new MessageContext()); + } + + public static Task ProcessAsync(this IQueryProcessor processor, + IQuery query, + object context) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessAsync(query, MessageContext.FromObject(context)); + } + + public static ValidationResult Validate(this IQueryProcessor processor, + TQuery query) + where TQuery : class, IQuery + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.Validate(query, new ValidationContext()); + } + + public static ValidationResult Validate(this IQueryProcessor processor, + TQuery query, + object context) + where TQuery : class, IQuery + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.Validate(query, ValidationContext.FromObject(context)); + } + + public static ValidationResult Validate(this IQueryProcessor processor, + IQuery query) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.Validate(query, new ValidationContext()); + } + + public static ValidationResult Validate(this IQueryProcessor processor, + IQuery query, + object context) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.Validate(query, ValidationContext.FromObject(context)); + } + + public static Task ValidateAsync(this IQueryProcessor processor, + TQuery query) + where TQuery : class, IQuery + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ValidateAsync(query, new ValidationContext()); + } + + public static Task ValidateAsync(this IQueryProcessor processor, + TQuery query, + object context) + where TQuery : class, IQuery + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ValidateAsync(query, ValidationContext.FromObject(context)); + } + + public static Task ValidateAsync(this IQueryProcessor processor, + IQuery query) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ValidateAsync(query, new ValidationContext()); + } + + public static Task ValidateAsync(this IQueryProcessor processor, + IQuery query, + object context) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ValidateAsync(query, ValidationContext.FromObject(context)); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/IQueryValidator.cs b/Src/DDD.Core/Application/IQueryValidator.cs deleted file mode 100644 index 0d4d46c..0000000 --- a/Src/DDD.Core/Application/IQueryValidator.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace DDD.Core.Application -{ - using Validation; - - /// - /// Defines a method that validates a query of a specified type. - /// - public interface IQueryValidator : IObjectValidator - where TQuery : class, IQuery - { - } -} \ No newline at end of file diff --git a/Src/DDD.Core/Application/IRecurringCommandManager.cs b/Src/DDD.Core/Application/IRecurringCommandManager.cs new file mode 100644 index 0000000..be560ef --- /dev/null +++ b/Src/DDD.Core/Application/IRecurringCommandManager.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + /// + /// Represents a component that registers, schedules and processes recurring commands. + /// + public interface IRecurringCommandManager + { + + #region Properties + + /// + /// Determines whether the manager is running. + /// + bool IsRunning { get; } + + #endregion Properties + + #region Methods + + /// + /// Starts managing recurring commands. + /// + public void Start(); + + /// + /// Stops managing recurring commands. + /// + public void Stop(); + + /// + /// Waits for the command management to complete execution within a specified time interval. + /// + public void Wait(TimeSpan? timeout); + + /// + /// Registers a recurring command asynchronously. + /// + /// + Task RegisterAsync(ICommand command, string recurringExpression, CancellationToken cancellationToken = default); + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/IRecurringCommandManager`1.cs b/Src/DDD.Core/Application/IRecurringCommandManager`1.cs new file mode 100644 index 0000000..4691021 --- /dev/null +++ b/Src/DDD.Core/Application/IRecurringCommandManager`1.cs @@ -0,0 +1,17 @@ +namespace DDD.Core.Application +{ + using Domain; + + /// + /// Represents a component that registers, schedules and processes recurring commands in a specific bounded context. + /// + public interface IRecurringCommandManager : IRecurringCommandManager + where TContext : BoundedContext + { + #region Properties + + TContext Context { get; } + + #endregion Properties + } +} diff --git a/Src/DDD.Core/Application/IRecurringSchedule.cs b/Src/DDD.Core/Application/IRecurringSchedule.cs new file mode 100644 index 0000000..da10678 --- /dev/null +++ b/Src/DDD.Core/Application/IRecurringSchedule.cs @@ -0,0 +1,21 @@ +using System; + +namespace DDD.Core.Application +{ + /// + /// Defines a component used to schedule recurring events. + /// + public interface IRecurringSchedule + { + + #region Methods + + /// + /// Gets the next occurrence of this schedule starting with a base time. + /// + DateTime? GetNextOccurrence(DateTime startTime); + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/IRecurringScheduleFactory.cs b/Src/DDD.Core/Application/IRecurringScheduleFactory.cs new file mode 100644 index 0000000..bd0ff38 --- /dev/null +++ b/Src/DDD.Core/Application/IRecurringScheduleFactory.cs @@ -0,0 +1,28 @@ +using System; + +namespace DDD.Core.Application +{ + /// + /// Defines a factory of recurring schedules. + /// + public interface IRecurringScheduleFactory + { + + #region Properties + + RecurringExpressionFormat Format { get; } + + #endregion Properties + + #region Methods + + /// + /// Creates a schedule initialized from a recurring expression. + /// + /// recurringExpression is null + /// recurringExpression is not valid. + IRecurringSchedule Create(string recurringExpression); + + #endregion Methods + } +} diff --git a/Src/DDD.Core/Application/ISyncCommandHandler.cs b/Src/DDD.Core/Application/ISyncCommandHandler.cs new file mode 100644 index 0000000..512b0bc --- /dev/null +++ b/Src/DDD.Core/Application/ISyncCommandHandler.cs @@ -0,0 +1,20 @@ +namespace DDD.Core.Application +{ + /// + /// Defines a method that handles synchronously a command of a specified type. + /// + public interface ISyncCommandHandler + where TCommand : class, ICommand + { + + #region Methods + + /// + /// Handles synchronously a command of a specified type. + /// + void Handle(TCommand command, IMessageContext context); + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/ISyncCommandHandlerExtensions.cs b/Src/DDD.Core/Application/ISyncCommandHandlerExtensions.cs new file mode 100644 index 0000000..adaa00e --- /dev/null +++ b/Src/DDD.Core/Application/ISyncCommandHandlerExtensions.cs @@ -0,0 +1,32 @@ +using EnsureThat; + +namespace DDD.Core.Application +{ + public static class ISyncCommandHandlerExtensions + { + + #region Methods + + public static void Handle(this ISyncCommandHandler handler, + TCommand command) + where TCommand : class, ICommand + + { + Ensure.That(handler, nameof(handler)).IsNotNull(); + handler.Handle(command, new MessageContext()); + } + + public static void Handle(this ISyncCommandHandler handler, + TCommand command, + object context) + where TCommand : class, ICommand + + { + Ensure.That(handler, nameof(handler)).IsNotNull(); + handler.Handle(command, MessageContext.FromObject(context)); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/ISyncCommandHandler`1.cs b/Src/DDD.Core/Application/ISyncCommandHandler`1.cs new file mode 100644 index 0000000..3e58695 --- /dev/null +++ b/Src/DDD.Core/Application/ISyncCommandHandler`1.cs @@ -0,0 +1,23 @@ +namespace DDD.Core.Application +{ + using Domain; + + /// + /// Defines a method that handles synchronously a command of a specified type in a specific bounded context. + /// + public interface ISyncCommandHandler : ISyncCommandHandler + where TCommand : class, ICommand + where TContext : BoundedContext + { + + #region Properties + + /// + /// The bounded context in which the command is handled. + /// + TContext Context { get; } + + #endregion Properties + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/ISyncEventHandler.cs b/Src/DDD.Core/Application/ISyncEventHandler.cs new file mode 100644 index 0000000..50386b2 --- /dev/null +++ b/Src/DDD.Core/Application/ISyncEventHandler.cs @@ -0,0 +1,37 @@ +using System; + +namespace DDD.Core.Application +{ + using Domain; + + /// + /// Defines a method that handles synchronously an event of a specified type in a specific bounded context. + /// + public interface ISyncEventHandler + { + + #region Properties + + /// + /// The bounded context in which the event is handled. + /// + BoundedContext Context { get; } + + /// + /// The event type. + /// + Type EventType { get; } + + #endregion Properties + + #region Methods + + /// + /// Handles synchronously an event of a specified type in a specific bounded context. + /// + void Handle(IEvent @event, IMessageContext context); + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/ISyncEventHandler`1.cs b/Src/DDD.Core/Application/ISyncEventHandler`1.cs new file mode 100644 index 0000000..3829fd2 --- /dev/null +++ b/Src/DDD.Core/Application/ISyncEventHandler`1.cs @@ -0,0 +1,31 @@ +namespace DDD.Core.Application +{ + using Domain; + + /// + /// Defines a method that handles synchronously an event of a specified type in a specific bounded context. + /// + public interface ISyncEventHandler : ISyncEventHandler + where TEvent : class, IEvent + where TContext : BoundedContext + { + #region Properties + + /// + /// The bounded context in which the event is handled. + /// + new TContext Context { get; } + + #endregion Properties + + #region Methods + + /// + /// Handles synchronously an event of a specified type in a specific bounded context. + /// + void Handle(TEvent @event, IMessageContext context); + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/ISyncQueryHandler.cs b/Src/DDD.Core/Application/ISyncQueryHandler.cs new file mode 100644 index 0000000..21f29c8 --- /dev/null +++ b/Src/DDD.Core/Application/ISyncQueryHandler.cs @@ -0,0 +1,20 @@ +namespace DDD.Core.Application +{ + /// + /// Defines a method that handles synchronously a query of a specified type and provides a result of a specified type. + /// + /// The type of the query. + /// The type of the result. + public interface ISyncQueryHandler + where TQuery : class, IQuery + { + #region Methods + + /// + /// Handles synchronously a query of a specified type and provides a result of a specified type. + /// + TResult Handle(TQuery query, IMessageContext context); + + #endregion Methods + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/ISyncQueryHandlerExtensions.cs b/Src/DDD.Core/Application/ISyncQueryHandlerExtensions.cs new file mode 100644 index 0000000..9f702f7 --- /dev/null +++ b/Src/DDD.Core/Application/ISyncQueryHandlerExtensions.cs @@ -0,0 +1,32 @@ +using EnsureThat; + +namespace DDD.Core.Application +{ + public static class ISyncQueryHandlerExtensions + { + + #region Methods + + public static TResult Handle(this ISyncQueryHandler handler, + TQuery query) + where TQuery : class, IQuery + + { + Ensure.That(handler, nameof(handler)).IsNotNull(); + return handler.Handle(query, new MessageContext()); + } + + public static TResult Handle(this ISyncQueryHandler handler, + TQuery query, + object context) + where TQuery : class, IQuery + + { + Ensure.That(handler, nameof(handler)).IsNotNull(); + return handler.Handle(query, MessageContext.FromObject(context)); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/ISyncQueryHandler`1.cs b/Src/DDD.Core/Application/ISyncQueryHandler`1.cs new file mode 100644 index 0000000..b2c9146 --- /dev/null +++ b/Src/DDD.Core/Application/ISyncQueryHandler`1.cs @@ -0,0 +1,26 @@ +namespace DDD.Core.Application +{ + using Domain; + + /// + /// Defines a method that handles synchronously a query of a specified type and provides a result of a specified type in a specific bounded context. + /// + /// The type of the query. + /// The type of the result. + /// The type of the context. + public interface ISyncQueryHandler : ISyncQueryHandler + where TQuery : class, IQuery + where TContext : BoundedContext + { + + #region Properties + + /// + /// The bounded context in which the query is handled. + /// + TContext Context { get; } + + #endregion Properties + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/MessageContext.cs b/Src/DDD.Core/Application/MessageContext.cs new file mode 100644 index 0000000..49f7a27 --- /dev/null +++ b/Src/DDD.Core/Application/MessageContext.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Threading; + +namespace DDD.Core.Application +{ + using Collections; + + /// + /// Provides access to the execution context of a message. + /// + public class MessageContext : Dictionary, IMessageContext + { + + #region Constructors + + public MessageContext() + { + } + + public MessageContext(IDictionary dictionary) : base(dictionary) + { + } + + #endregion Constructors + + #region Methods + + public static IMessageContext CancellableContext(CancellationToken cancellationToken) + { + var context = new MessageContext(); + context.AddCancellationToken(cancellationToken); + return context; + } + + public static IMessageContext FromObject(object context) + { + var messageContext = new MessageContext(); + if (context != null) + messageContext.AddObject(context); + return messageContext; + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/MessageContextInfo.cs b/Src/DDD.Core/Application/MessageContextInfo.cs new file mode 100644 index 0000000..1ce400f --- /dev/null +++ b/Src/DDD.Core/Application/MessageContextInfo.cs @@ -0,0 +1,18 @@ +namespace DDD.Core.Application +{ + /// + /// Standard contextual information about the message. + /// + public class MessageContextInfo + { + #region Fields + + public const string Event = nameof(Event), + CancellationToken = nameof(CancellationToken), + Stream = nameof(Stream), + FailedStream = nameof(FailedStream), + BoundedContext = nameof(BoundedContext); + + #endregion Fields + } +} diff --git a/Src/DDD.Core/Application/QueryConflictException.cs b/Src/DDD.Core/Application/QueryConflictException.cs new file mode 100644 index 0000000..448264b --- /dev/null +++ b/Src/DDD.Core/Application/QueryConflictException.cs @@ -0,0 +1,37 @@ +using System; + +namespace DDD.Core.Application +{ + /// + /// Exception thrown when a conflict with other querys has been detected while handling a query. + /// + public class QueryConflictException : QueryException + { + + #region Constructors + + public QueryConflictException(IQuery query = null, Exception innerException = null) + : base(true, DefaultMessage(query), query, innerException) + { + } + + public QueryConflictException(string message, IQuery query = null, Exception innerException = null) + : base(true, message, query, innerException) + { + } + + #endregion Constructors + + #region Methods + + public static new string DefaultMessage(IQuery query = null) + { + if (query == null) + return "A conflict has been detected while handling a query."; + return $"A conflict has been detected while handling the query '{query.GetType().Name}'."; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/QueryException.cs b/Src/DDD.Core/Application/QueryException.cs index 0529629..89b81a5 100644 --- a/Src/DDD.Core/Application/QueryException.cs +++ b/Src/DDD.Core/Application/QueryException.cs @@ -1,36 +1,24 @@ using System; +using System.Runtime.Serialization; namespace DDD.Core.Application { /// /// Exception thrown when a query failed. /// - [Serializable] public class QueryException : ApplicationException { #region Constructors - public QueryException() - : base("The query failed.") - { - } - - public QueryException(string message) : base(message) - { - } - - public QueryException(string message, Exception innerException) : base(message, innerException) - { - } - - public QueryException(string message, Exception innerException, IQuery query) : base(message, innerException) + public QueryException(bool isTransient, IQuery query = null, Exception innerException = null) + : base(isTransient, DefaultMessage(query), innerException) { this.Query = query; } - public QueryException(Exception innerException, IQuery query) - : base($"The query '{query.GetType().Name}' failed.", innerException) + public QueryException(bool isTransient, string message, IQuery query = null, Exception innerException = null) + : base(isTransient, message, innerException) { this.Query = query; } @@ -45,11 +33,27 @@ public QueryException(Exception innerException, IQuery query) #region Methods + public static string DefaultMessage(IQuery query = null) + { + if (query == null) + return "A query failed."; + return $"The query '{query.GetType().Name}' failed."; + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + if (this.Query != null) + info.AddValue("Query", this.Query); + } + public override string ToString() { var s = $"{this.GetType()}: {this.Message} "; + s += $"{Environment.NewLine}{nameof(Timestamp)}: {this.Timestamp}"; + s += $"{Environment.NewLine}{nameof(IsTransient)}: {this.IsTransient}"; if (this.Query != null) - s += $"{Environment.NewLine}Query: {this.Query}"; + s += $"{Environment.NewLine}{nameof(Query)}: {this.Query}"; if (this.InnerException != null) s += $" ---> {this.InnerException}"; if (this.StackTrace != null) diff --git a/Src/DDD.Core/Application/QueryInvalidException.cs b/Src/DDD.Core/Application/QueryInvalidException.cs new file mode 100644 index 0000000..cdba158 --- /dev/null +++ b/Src/DDD.Core/Application/QueryInvalidException.cs @@ -0,0 +1,80 @@ +using System; +using System.Linq; +using System.Runtime.Serialization; + +namespace DDD.Core.Application +{ + using Validation; + + /// + /// Exception thrown when a query is invalid. + /// + public class QueryInvalidException : QueryException + { + + #region Constructors + + public QueryInvalidException(IQuery query = null, ValidationFailure[] failures = null, Exception innerException = null) + : base(false, DefaultMessage(query), query, innerException) + { + this.Failures = failures; + } + + public QueryInvalidException(string message, IQuery query = null, ValidationFailure[] failures = null, Exception innerException = null) + : base(false, message, query, innerException) + { + this.Failures = failures; + } + + #endregion Constructors + + #region Properties + + public ValidationFailure[] Failures { get; } + + #endregion Properties + + #region Methods + + public static new string DefaultMessage(IQuery query = null) + { + if (query == null) + return "The query is invalid."; + return $"The query '{query.GetType().Name}' is invalid."; + } + + public bool HasFailures() => this.Failures != null && this.Failures.Any(); + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + if (this.Failures != null) + { + for (var i = 0; i < this.Failures.Length; i++) + info.AddValue($"Failure{i}", this.Failures[i]); + } + } + + public override string ToString() + { + var s = $"{this.GetType()}: {this.Message} "; + s += $"{Environment.NewLine}{nameof(Timestamp)}: {this.Timestamp}"; + s += $"{Environment.NewLine}{nameof(IsTransient)}: {this.IsTransient}"; + if (this.Query != null) + s += $"{Environment.NewLine}{nameof(Query)}: {this.Query}"; + if (this.Failures != null) + { + for (var i = 0; i < this.Failures.Length; i++) + s += $"{Environment.NewLine}Failure{i}: {this.Failures[i]}"; + } + if (this.InnerException != null) + s += $" ---> {this.InnerException}"; + if (this.StackTrace != null) + s += $"{Environment.NewLine}{this.StackTrace}"; + return s; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/QueryProcessor.cs b/Src/DDD.Core/Application/QueryProcessor.cs index c3fca73..dd0751d 100644 --- a/Src/DDD.Core/Application/QueryProcessor.cs +++ b/Src/DDD.Core/Application/QueryProcessor.cs @@ -1,67 +1,158 @@ -using Conditions; +using EnsureThat; using System; +using System.Linq; using System.Threading.Tasks; namespace DDD.Core.Application { + using Domain; + using System.Collections.Concurrent; using Validation; + using Threading; /// - /// Finds the correct query handler and validator and invokes them. + /// The default query processor for processing and validating queries of any type. /// public class QueryProcessor : IQueryProcessor { #region Fields + private static readonly ConcurrentDictionary contextualProcessors + = new ConcurrentDictionary(); + private readonly QueryProcessorSettings settings; private readonly IServiceProvider serviceProvider; #endregion Fields #region Constructors - public QueryProcessor(IServiceProvider serviceProvider) + public QueryProcessor(IServiceProvider serviceProvider, QueryProcessorSettings settings) { - Condition.Requires(serviceProvider, nameof(serviceProvider)).IsNotNull(); + Ensure.That(serviceProvider, nameof(serviceProvider)).IsNotNull(); + Ensure.That(settings, nameof(settings)).IsNotNull(); this.serviceProvider = serviceProvider; + this.settings = settings; } #endregion Constructors #region Methods - public TResult Process(IQuery query) + public IContextualQueryProcessor InGeneric(TContext context) where TContext : BoundedContext { - Condition.Requires(query, nameof(query)).IsNotNull(); - var handlerType = typeof(IQueryHandler<,>).MakeGenericType(query.GetType(), typeof(TResult)); + Ensure.That(context, nameof(context)).IsNotNull(); + return (IContextualQueryProcessor)contextualProcessors.GetOrAdd(context, _ => + new ContextualQueryProcessor(this.serviceProvider, context)); + } + + public IContextualQueryProcessor InSpecific(BoundedContext context) + { + Ensure.That(context, nameof(context)).IsNotNull(); + return contextualProcessors.GetOrAdd(context, _ => + { + var processorType = typeof(ContextualQueryProcessor<>).MakeGenericType(context.GetType()); + return (IContextualQueryProcessor)Activator.CreateInstance(processorType, this.serviceProvider, context); + }); + } + + public TResult Process(IQuery query, IMessageContext context) + { + Ensure.That(query, nameof(query)).IsNotNull(); + var handlerType = typeof(ISyncQueryHandler<,>).MakeGenericType(query.GetType(), typeof(TResult)); + dynamic handler = this.serviceProvider.GetService(handlerType); + if (handler == null) throw new InvalidOperationException($"The query handler for type {handlerType} could not be found."); + return handler.Handle((dynamic)query, context); + } + + public object Process(IQuery query, IMessageContext context) + { + Ensure.That(query, nameof(query)).IsNotNull(); + var queryType = query.GetType(); + var queryInterfaceType = queryType.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IQuery<>)); + if (queryInterfaceType == null) throw new ArgumentException($"{queryType.Name} does not implement {typeof(IQuery<>).Name}.", nameof(query)); + var resultType = queryInterfaceType.GetGenericArguments()[0]; + var handlerType = typeof(ISyncQueryHandler<,>).MakeGenericType(queryType, resultType); dynamic handler = this.serviceProvider.GetService(handlerType); if (handler == null) throw new InvalidOperationException($"The query handler for type {handlerType} could not be found."); - return handler.Handle((dynamic)query); + return handler.Handle((dynamic)query, context); } - public Task ProcessAsync(IQuery query) + public Task ProcessAsync(IQuery query, IMessageContext context) { - Condition.Requires(query, nameof(query)).IsNotNull(); + Ensure.That(query, nameof(query)).IsNotNull(); var handlerType = typeof(IAsyncQueryHandler<,>).MakeGenericType(query.GetType(), typeof(TResult)); dynamic handler = this.serviceProvider.GetService(handlerType); if (handler == null) throw new InvalidOperationException($"The query handler for type {handlerType} could not be found."); - return handler.HandleAsync((dynamic)query); + return handler.HandleAsync((dynamic)query, context); + } + + public async Task ProcessAsync(IQuery query, IMessageContext context) + { + Ensure.That(query, nameof(query)).IsNotNull(); + await new SynchronizationContextRemover(); + var queryType = query.GetType(); + var queryInterfaceType = queryType.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IQuery<>)); + if (queryInterfaceType == null) throw new ArgumentException($"{queryType.Name} does not implement {typeof(IQuery<>).Name}.", nameof(query)); + var resultType = queryInterfaceType.GetGenericArguments()[0]; + var handlerType = typeof(IAsyncQueryHandler<,>).MakeGenericType(query.GetType(), resultType); + dynamic handler = this.serviceProvider.GetService(handlerType); + if (handler == null) throw new InvalidOperationException($"The query handler for type {handlerType} could not be found."); + return await handler.HandleAsync((dynamic)query, context); + } + + public ValidationResult Validate(TQuery query, IValidationContext context) where TQuery : class, IQuery + { + Ensure.That(query, nameof(query)).IsNotNull(); + var validator = this.serviceProvider.GetService>(); + if (validator == null) + { + if (this.settings.DefaultValidator == null) + throw new InvalidOperationException($"The query validator for type {typeof(ISyncObjectValidator)} could not be found."); + return this.settings.DefaultValidator.Validate(query, context); + } + return validator.Validate(query, context); + } + + public ValidationResult Validate(IQuery query, IValidationContext context) + { + Ensure.That(query, nameof(query)).IsNotNull(); + var validatorType = typeof(ISyncObjectValidator<>).MakeGenericType(query.GetType()); + dynamic validator = this.serviceProvider.GetService(validatorType); + if (validator == null) + { + if (this.settings.DefaultValidator == null) + throw new InvalidOperationException($"The query validator for type {validatorType} could not be found."); + return this.settings.DefaultValidator.Validate(query, context); + } + return validator.Validate((dynamic)query, context); } - public ValidationResult Validate(TQuery query, string ruleSet = null) where TQuery : class, IQuery + public Task ValidateAsync(TQuery query, IValidationContext context) where TQuery : class, IQuery { - Condition.Requires(query, nameof(query)).IsNotNull(); - var validator = this.serviceProvider.GetService>(); - if (validator == null) throw new InvalidOperationException($"The query validator for type {typeof(IQueryValidator)} could not be found."); - return validator.Validate(query, ruleSet); + Ensure.That(query, nameof(query)).IsNotNull(); + var validator = this.serviceProvider.GetService>(); + if (validator == null) + { + if (this.settings.DefaultValidator == null) + throw new InvalidOperationException($"The query validator for type {typeof(IAsyncObjectValidator)} could not be found."); + return this.settings.DefaultValidator.ValidateAsync(query, context); + } + return validator.ValidateAsync(query, context); } - public Task ValidateAsync(TQuery query, string ruleSet = null) where TQuery : class, IQuery + public Task ValidateAsync(IQuery query, IValidationContext context) { - Condition.Requires(query, nameof(query)).IsNotNull(); - var validator = this.serviceProvider.GetService>(); - if (validator == null) throw new InvalidOperationException($"The query validator for type {typeof(IQueryValidator)} could not be found."); - return validator.ValidateAsync(query, ruleSet); + Ensure.That(query, nameof(query)).IsNotNull(); + var validatorType = typeof(IAsyncObjectValidator<>).MakeGenericType(query.GetType()); + dynamic validator = this.serviceProvider.GetService(validatorType); + if (validator == null) + { + if (this.settings.DefaultValidator == null) + throw new InvalidOperationException($"The query validator for type {validatorType} could not be found."); + return this.settings.DefaultValidator.ValidateAsync(query, context); + } + return validator.ValidateAsync((dynamic)query, context); } #endregion Methods diff --git a/Src/DDD.Core/Application/QueryProcessorSettings.cs b/Src/DDD.Core/Application/QueryProcessorSettings.cs new file mode 100644 index 0000000..3050d22 --- /dev/null +++ b/Src/DDD.Core/Application/QueryProcessorSettings.cs @@ -0,0 +1,24 @@ +namespace DDD.Core.Application +{ + using Validation; + + public class QueryProcessorSettings + { + + #region Constructors + + public QueryProcessorSettings(IObjectValidator defaultValidator = null) + { + this.DefaultValidator = defaultValidator; + } + + #endregion Constructors + + #region Properties + + public IObjectValidator DefaultValidator { get; } + + #endregion Properties + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/QueryTimeoutException.cs b/Src/DDD.Core/Application/QueryTimeoutException.cs new file mode 100644 index 0000000..ac9f10c --- /dev/null +++ b/Src/DDD.Core/Application/QueryTimeoutException.cs @@ -0,0 +1,37 @@ +using System; + +namespace DDD.Core.Application +{ + /// + /// Exception thrown when a query has expired. + /// + public class QueryTimeoutException : QueryException + { + + #region Constructors + + public QueryTimeoutException(IQuery query = null, Exception innerException = null) + : base(true, DefaultMessage(query), query, innerException) + { + } + + public QueryTimeoutException(string message, IQuery query = null, Exception innerException = null) + : base(true, message, query, innerException) + { + } + + #endregion Constructors + + #region Methods + + public static new string DefaultMessage(IQuery query = null) + { + if (query == null) + return "The query has expired."; + return $"The query '{query.GetType().Name}' has expired."; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/QueryUnauthorizedException.cs b/Src/DDD.Core/Application/QueryUnauthorizedException.cs new file mode 100644 index 0000000..c9d8d31 --- /dev/null +++ b/Src/DDD.Core/Application/QueryUnauthorizedException.cs @@ -0,0 +1,37 @@ +using System; + +namespace DDD.Core.Application +{ + /// + /// Exception thrown when a query was denied. + /// + public class QueryUnauthorizedException : QueryException + { + + #region Constructors + + public QueryUnauthorizedException(IQuery query = null, Exception innerException = null) + : base(false, DefaultMessage(query), query, innerException) + { + } + + public QueryUnauthorizedException(string message, IQuery query = null, Exception innerException = null) + : base(false, message, query, innerException) + { + } + + #endregion Constructors + + #region Methods + + public static new string DefaultMessage(IQuery query = null) + { + if (query == null) + return "The query was denied."; + return $"The query '{query.GetType().Name}' was denied."; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/QueryUnavailableException.cs b/Src/DDD.Core/Application/QueryUnavailableException.cs new file mode 100644 index 0000000..5c9eba7 --- /dev/null +++ b/Src/DDD.Core/Application/QueryUnavailableException.cs @@ -0,0 +1,37 @@ +using System; + +namespace DDD.Core.Application +{ + /// + /// Exception thrown when a query cannot be currently handled. + /// + public class QueryUnavailableException : QueryException + { + + #region Constructors + + public QueryUnavailableException(IQuery query = null, Exception innerException = null) + : base(true, DefaultMessage(query), query, innerException) + { + } + + public QueryUnavailableException(string message, IQuery query = null, Exception innerException = null) + : base(true, message, query, innerException) + { + } + + #endregion Constructors + + #region Methods + + public static new string DefaultMessage(IQuery query = null) + { + if (query == null) + return "The query cannot be currently handled."; + return $"The query '{query.GetType().Name}' cannot be currently handled."; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/RecurringCommandManager.cs b/Src/DDD.Core/Application/RecurringCommandManager.cs new file mode 100644 index 0000000..dfa84e4 --- /dev/null +++ b/Src/DDD.Core/Application/RecurringCommandManager.cs @@ -0,0 +1,270 @@ +using EnsureThat; +using System; +using System.Linq; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace DDD.Core.Application +{ + using Domain; + using Serialization; + using Threading; + using DDD; + + public class RecurringCommandManager : IRecurringCommandManager, IDisposable + where TContext : BoundedContext + { + + #region Fields + + private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + private readonly ICommandProcessor commandProcessor; + private readonly IEnumerable commandSerializers; + private readonly ILogger logger; + private readonly IQueryProcessor queryProcessor; + private readonly IEnumerable recurringScheduleFactories; + private readonly RecurringCommandManagerSettings settings; + private bool disposed; + private Task manageCommands; + + #endregion Fields + + #region Constructors + + public RecurringCommandManager(TContext context, + ICommandProcessor commandProcessor, + IQueryProcessor queryProcessor, + IEnumerable commandSerializers, + IEnumerable recurringScheduleFactories, + ILogger logger, + RecurringCommandManagerSettings settings) + { + Ensure.That(context, nameof(context)).IsNotNull(); + Ensure.That(commandProcessor, nameof(commandProcessor)).IsNotNull(); + Ensure.That(queryProcessor, nameof(queryProcessor)).IsNotNull(); + Ensure.That(commandSerializers, nameof(commandSerializers)).IsNotNull(); + Ensure.That(recurringScheduleFactories, nameof(recurringScheduleFactories)).IsNotNull(); + Ensure.That(logger, nameof(logger)).IsNotNull(); + Ensure.That(settings, nameof(settings)).IsNotNull(); + this.Context = context; + this.commandProcessor = commandProcessor; + this.queryProcessor = queryProcessor; + this.commandSerializers = commandSerializers; + this.recurringScheduleFactories = recurringScheduleFactories; + this.logger = logger; + this.settings = settings; + } + + #endregion Constructors + + #region Properties + + public TContext Context { get; } + + public bool IsRunning { get; private set; } + + protected CancellationToken CancellationToken => this.cancellationTokenSource.Token; + + #endregion Properties + + #region Methods + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + public async Task RegisterAsync(ICommand command, string recurringExpression, CancellationToken cancellationToken = default) + { + Ensure.That(command, nameof(command)).IsNotNull(); + Ensure.That(recurringExpression, nameof(recurringExpression)).IsNotNull(); + var recurringScheduleFactory = this.GetScheduleFactory(this.settings.CurrentExpressionFormat); + ValidateRecurringExpression(recurringScheduleFactory, recurringExpression); + await new SynchronizationContextRemover(); + var serializer = this.GetSerializer(this.settings.CurrentSerializationFormat); + var commandId = this.queryProcessor.InGeneric(Context).Process(new GenerateRecurringCommandId(), MessageContext.CancellableContext(cancellationToken)); + var registrationCommand = new RegisterRecurringCommand + { + CommandId = commandId, + CommandType = command.GetType().ShortAssemblyQualifiedName(), + Body = serializer.SerializeToString(command), + BodyFormat = serializer.Format.ToString().ToUpper(), + RecurringExpression = recurringExpression, + RecurringExpressionFormat = recurringScheduleFactory.Format.ToString().ToUpper(), + }; + await this.commandProcessor.InGeneric(Context).ProcessAsync(registrationCommand, MessageContext.CancellableContext(cancellationToken)); + } + + public void Start() + { + if (!this.IsRunning) + { + this.logger.LogInformation("RecurringCommandManager for the context '{Context}' is starting.", this.Context.Name); + this.logger.LogDebug("The manager settings for the context '{Context}' are as follows : {Settings}", this.Context.Name, this.settings); + this.IsRunning = true; + this.manageCommands = Task.Run(async () => await ManageCommandsAsync()); + this.logger.LogInformation("RecurringCommandManager for the context '{Context}' has started.", this.Context.Name); + } + } + + public void Stop() + { + if (this.IsRunning) + { + this.logger.LogInformation("RecurringCommandManager for the context '{Context}' is stopping.", this.Context.Name); + this.cancellationTokenSource.Cancel(); + this.manageCommands.Wait(); + } + } + + public void Wait(TimeSpan? timeout = null) + { + if (this.IsRunning) + { + if (timeout.HasValue) + this.manageCommands.Wait(timeout.Value); + else + this.manageCommands.Wait(); + } + } + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + if (disposing) + { + this.Stop(); + this.cancellationTokenSource.Dispose(); + } + disposed = true; + } + } + + private static void ValidateRecurringExpression(IRecurringScheduleFactory recurringScheduleFactory, string recurringExpression) + { + try + { + recurringScheduleFactory.Create(recurringExpression); + } + catch (Exception ex) + { + throw new ArgumentException( + "The recurring expression is invalid. Please see the inner exception for details.", + nameof(recurringExpression), + ex); + } + } + + private ICommand DeserializeCommand(RecurringCommand recurringCommand) + { + var format = (SerializationFormat)Enum.Parse(typeof(SerializationFormat), recurringCommand.BodyFormat, ignoreCase: true); + var type = Type.GetType(recurringCommand.CommandType); + var serializer = this.GetSerializer(format); + return (ICommand)serializer.DeserializeFromString(recurringCommand.Body, type); + } + + private async Task> FindAllRecurringCommandsAsync() + { + var recurringCommands = await this.queryProcessor.InGeneric(Context).ProcessAsync(new FindRecurringCommands(), MessageContext.CancellableContext(this.CancellationToken)); + var results = new List<(RecurringCommand, IRecurringSchedule)>(); + foreach (var recurringCommand in recurringCommands) + { + var format = (RecurringExpressionFormat)Enum.Parse(typeof(RecurringExpressionFormat), recurringCommand.RecurringExpressionFormat, ignoreCase: true); + var recurringScheduleFactory = this.GetScheduleFactory(format); + results.Add((recurringCommand, recurringScheduleFactory.Create(recurringCommand.RecurringExpression))); + } + return results; + } + + private IRecurringScheduleFactory GetScheduleFactory(RecurringExpressionFormat format) => this.recurringScheduleFactories.First(f => f.Format == format); + + private ITextSerializer GetSerializer(SerializationFormat format) => this.commandSerializers.First(s => s.Format == format); + + private async Task ManageCommandAsync(RecurringCommand recurringCommand, IRecurringSchedule recurringSchedule) + { + try + { + this.logger.LogInformation("Management of recurring command {CommandId} in the context '{Context}' has started.", recurringCommand.CommandId, this.Context.Name); + var command = this.DeserializeCommand(recurringCommand); + var now = SystemTime.Local(); + var nextOccurrence = recurringSchedule.GetNextOccurrence(now); + while (nextOccurrence.HasValue) + { + this.CancellationToken.ThrowIfCancellationRequested(); + this.logger.LogDebug("Next processing of recurring command {CommandId} in the context '{Context}' is scheduled for {CommandExecutionTime}.", recurringCommand.CommandId, this.Context.Name, nextOccurrence); + bool success; + try + { + await this.commandProcessor.ProcessWithDelayAsync(command, nextOccurrence.Value - now, MessageContext.CancellableContext(this.CancellationToken)); + success = true; + } + catch (Exception exception) when (!(exception is OperationCanceledException)) + { + success = false; + this.logger.LogError(default, exception, "An error occurred while processing recurring command {CommandId} in the context '{Context}'.", recurringCommand.CommandId, this.Context.Name); + var failureCommand = new MarkRecurringCommandAsFailed + { + CommandId = recurringCommand.CommandId, + ExecutionTime = nextOccurrence.Value, + ExceptionInfo = exception.ToString() + }; + await this.commandProcessor.InGeneric(Context).ProcessAsync(failureCommand); + } + if (success) + { + this.logger.LogDebug("Processing of recurring command {CommandId} in the context '{Context}' has successfully finished.", recurringCommand.CommandId, this.Context.Name); + var successCommand = new MarkRecurringCommandAsSuccessful + { + CommandId = recurringCommand.CommandId, + ExecutionTime = nextOccurrence.Value + }; + await this.commandProcessor.InGeneric(Context).ProcessAsync(successCommand); + } + now = SystemTime.Local(); + nextOccurrence = recurringSchedule.GetNextOccurrence(now); + } + } + catch (OperationCanceledException) + { + this.logger.LogInformation("Management of recurring command {CommandId} in the context '{Context}' has stopped.", recurringCommand.CommandId, this.Context.Name); + throw; + } + catch (Exception exception) + { + this.logger.LogError(default, exception, "An error occurred while managing recurring command {CommandId} in the context '{Context}'.", recurringCommand.CommandId, this.Context.Name); + } + } + + private async Task ManageCommandsAsync() + { + try + { + this.CancellationToken.ThrowIfCancellationRequested(); + await new SynchronizationContextRemover(); + var commandInfos = await this.FindAllRecurringCommandsAsync(); + var tasks = new List(); + foreach (var commandInfo in commandInfos) + tasks.Add(ManageCommandAsync(commandInfo.RecurringCommand, commandInfo.RecurringSchedule)); + await Task.WhenAll(tasks); + } + catch(OperationCanceledException) + { + this.logger.LogInformation("RecurringCommandManager for the context '{Context}' has stopped.", this.Context.Name); + } + catch (Exception exception) + { + this.logger.LogCritical(default, exception, "An error occurred while managing recurring commands in the context '{Context}'.", this.Context.Name); + } + finally + { + this.IsRunning = false; + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs b/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs new file mode 100644 index 0000000..b57c3ac --- /dev/null +++ b/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs @@ -0,0 +1,50 @@ +using System; + +namespace DDD.Core.Application +{ + using Domain; + using Serialization; + + public class RecurringCommandManagerSettings + where TContext : BoundedContext + { + + #region Constructors + + public RecurringCommandManagerSettings(SerializationFormat currentSerializationFormat, + RecurringExpressionFormat currentExpressionFormat) + { + this.ContextType= typeof(TContext); + this.CurrentSerializationFormat = currentSerializationFormat; + this.CurrentExpressionFormat = currentExpressionFormat; + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the type of the associated context. + /// + public Type ContextType { get; } + + /// + /// Gets the current serialization format of the recurring commands. + /// + public SerializationFormat CurrentSerializationFormat { get; } + + /// + /// Gets the current recurring expression format. + /// + public RecurringExpressionFormat CurrentExpressionFormat { get; } + + #endregion Properties + + #region Methods + + public override string ToString() + => $"{this.GetType().Name} [{nameof(ContextType)}={ContextType}, {nameof(CurrentSerializationFormat)}={CurrentSerializationFormat}, {nameof(CurrentExpressionFormat)}={CurrentExpressionFormat}]"; + + #endregion Methods + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/RecurringExpressionFormat.cs b/Src/DDD.Core/Application/RecurringExpressionFormat.cs new file mode 100644 index 0000000..be2f42c --- /dev/null +++ b/Src/DDD.Core/Application/RecurringExpressionFormat.cs @@ -0,0 +1,7 @@ +namespace DDD.Core.Application +{ + public enum RecurringExpressionFormat + { + Cron + } +} diff --git a/Src/DDD.Core/Application/SyncCommandHandlerWithLogging.cs b/Src/DDD.Core/Application/SyncCommandHandlerWithLogging.cs new file mode 100644 index 0000000..fe76b47 --- /dev/null +++ b/Src/DDD.Core/Application/SyncCommandHandlerWithLogging.cs @@ -0,0 +1,53 @@ +using EnsureThat; +using Microsoft.Extensions.Logging; +using System.Diagnostics; + +namespace DDD.Core.Application +{ + /// + /// A decorator that logs information about commands. + /// + public class SyncCommandHandlerWithLogging : ISyncCommandHandler + where TCommand : class, ICommand + { + + #region Fields + + private readonly ISyncCommandHandler commandHandler; + private readonly ILogger logger; + + #endregion Fields + + + #region Constructors + + public SyncCommandHandlerWithLogging(ISyncCommandHandler commandHandler, ILogger logger) + { + Ensure.That(commandHandler, nameof(commandHandler)).IsNotNull(); + Ensure.That(logger, nameof(logger)).IsNotNull(); + this.commandHandler = commandHandler; + this.logger = logger; + } + + #endregion Constructors + + #region Methods + + public void Handle(TCommand command, IMessageContext context) + { + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug("Le traitement de la commande {Command} par {CommandHandler} a commencé.", command, this.commandHandler.GetType().Name); + var stopWatch = Stopwatch.StartNew(); + this.commandHandler.Handle(command, context); + stopWatch.Stop(); + this.logger.LogDebug("Le traitement de la commande {Command} par {CommandHandler} s'est terminé (temps d'exécution: {CommandExecutionTime} ms).", command, this.commandHandler.GetType().Name, stopWatch.ElapsedMilliseconds); + } + else + this.commandHandler.Handle(command, context); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/SyncCommandHandlerWithLogging`1.cs b/Src/DDD.Core/Application/SyncCommandHandlerWithLogging`1.cs new file mode 100644 index 0000000..0f67796 --- /dev/null +++ b/Src/DDD.Core/Application/SyncCommandHandlerWithLogging`1.cs @@ -0,0 +1,61 @@ +using EnsureThat; +using Microsoft.Extensions.Logging; +using System.Diagnostics; + +namespace DDD.Core.Application +{ + using Domain; + + /// + /// A decorator that logs information about commands. + /// + public class SyncCommandHandlerWithLogging : ISyncCommandHandler + where TCommand : class, ICommand + where TContext : BoundedContext + { + + #region Fields + + private readonly ISyncCommandHandler commandHandler; + private readonly ILogger logger; + + #endregion Fields + + #region Constructors + + public SyncCommandHandlerWithLogging(ISyncCommandHandler commandHandler, ILogger logger) + { + Ensure.That(commandHandler, nameof(commandHandler)).IsNotNull(); + Ensure.That(logger, nameof(logger)).IsNotNull(); + this.commandHandler = commandHandler; + this.logger = logger; + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.commandHandler.Context; + + #endregion Properties + + #region Methods + + public void Handle(TCommand command, IMessageContext context) + { + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug("Le traitement de la commande {Command} par {CommandHandler} a commencé.", command, this.commandHandler.GetType().Name); + var stopWatch = Stopwatch.StartNew(); + this.commandHandler.Handle(command, context); + stopWatch.Stop(); + this.logger.LogDebug("Le traitement de la commande {Command} par {CommandHandler} s'est terminé (temps d'exécution: {CommandExecutionTime} ms).", command, this.commandHandler.GetType().Name, stopWatch.ElapsedMilliseconds); + } + else + this.commandHandler.Handle(command, context); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/SyncEventHandler.cs b/Src/DDD.Core/Application/SyncEventHandler.cs new file mode 100644 index 0000000..d3ce296 --- /dev/null +++ b/Src/DDD.Core/Application/SyncEventHandler.cs @@ -0,0 +1,45 @@ +using System; +using EnsureThat; + +namespace DDD.Core.Application +{ + using Domain; + + /// + /// Base class for handling synchronously events. + /// + public abstract class SyncEventHandler : ISyncEventHandler + where TEvent : class, IEvent + where TContext : BoundedContext + { + + #region Constructors + + protected SyncEventHandler(TContext context) + { + Ensure.That(context, nameof(context)).IsNotNull(); + this.Context = context; + } + + #endregion Constructors + + #region Properties + + public TContext Context { get; } + + BoundedContext ISyncEventHandler.Context => this.Context; + + Type ISyncEventHandler.EventType => typeof(TEvent); + + #endregion Properties + + #region Methods + + public abstract void Handle(TEvent @event, IMessageContext context); + + void ISyncEventHandler.Handle(IEvent @event, IMessageContext context) => this.Handle((TEvent)@event, context); + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/SyncEventHandlerWithLogging.cs b/Src/DDD.Core/Application/SyncEventHandlerWithLogging.cs new file mode 100644 index 0000000..2cb88b0 --- /dev/null +++ b/Src/DDD.Core/Application/SyncEventHandlerWithLogging.cs @@ -0,0 +1,68 @@ +using EnsureThat; +using Microsoft.Extensions.Logging; +using System; +using System.Diagnostics; + +namespace DDD.Core.Application +{ + using Domain; + + /// + /// A decorator that logs information about events. + /// + public class SyncEventHandlerWithLogging : ISyncEventHandler + where TEvent : class, IEvent + where TContext : BoundedContext + { + + #region Fields + + private readonly ISyncEventHandler eventHandler; + private readonly ILogger logger; + + #endregion Fields + + #region Constructors + + public SyncEventHandlerWithLogging(ISyncEventHandler eventHandler, ILogger logger) + { + Ensure.That(eventHandler, nameof(eventHandler)).IsNotNull(); + Ensure.That(logger, nameof(logger)).IsNotNull(); + this.eventHandler = eventHandler; + this.logger = logger; + } + + #endregion Constructors + + #region Properties + + public TContext Context => eventHandler.Context; + + BoundedContext ISyncEventHandler.Context => this.Context; + + Type ISyncEventHandler.EventType => this.eventHandler.EventType; + + #endregion Properties + + #region Methods + + public void Handle(TEvent @event, IMessageContext context) + { + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug("Le traitement de l'évènement {Event} par {EventHandler} a commencé.", @event, this.eventHandler.GetType().Name); + var stopWatch = Stopwatch.StartNew(); + this.eventHandler.Handle(@event, context); + stopWatch.Stop(); + this.logger.LogDebug("Le traitement de l'évènement {Event} par {EventHandler} s'est terminé (temps d'exécution: {EventExecutionTime} ms).", @event, this.eventHandler.GetType().Name, stopWatch.ElapsedMilliseconds); + } + else + this.eventHandler.Handle(@event, context); + } + + void ISyncEventHandler.Handle(IEvent @event, IMessageContext context) => this.Handle((TEvent)@event, context); + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/SyncQueryHandlerWithLogging.cs b/Src/DDD.Core/Application/SyncQueryHandlerWithLogging.cs new file mode 100644 index 0000000..0f0fa85 --- /dev/null +++ b/Src/DDD.Core/Application/SyncQueryHandlerWithLogging.cs @@ -0,0 +1,47 @@ +using EnsureThat; +using Microsoft.Extensions.Logging; +using System.Diagnostics; + +namespace DDD.Core.Application +{ + /// + /// A decorator that logs information about queries. + /// + public class SyncQueryHandlerWithLogging : ISyncQueryHandler + where TQuery : class, IQuery + { + #region Fields + + private readonly ISyncQueryHandler queryHandler; + private readonly ILogger logger; + + public SyncQueryHandlerWithLogging(ISyncQueryHandler queryHandler, ILogger logger) + { + Ensure.That(queryHandler, nameof(queryHandler)).IsNotNull(); + Ensure.That(logger, nameof(logger)).IsNotNull(); + this.queryHandler = queryHandler; + this.logger = logger; + } + + #endregion Fields + + #region Methods + + public TResult Handle(TQuery query, IMessageContext context) + { + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug("Le traitement de la requête {Query} par {QueryHandler} a commencé.", query, this.queryHandler.GetType().Name); + var stopWatch = Stopwatch.StartNew(); + var result = this.queryHandler.Handle(query, context); + stopWatch.Stop(); + this.logger.LogDebug("Le traitement de la requête {Query} par {QueryHandler} s'est terminé (temps d'exécution: {QueryExecutionTime} ms).", query, this.queryHandler.GetType().Name, stopWatch.ElapsedMilliseconds); + return result; + } + else + return this.queryHandler.Handle(query, context); + } + + #endregion Methods + } +} diff --git a/Src/DDD.Core/Application/SyncQueryHandlerWithLogging`1.cs b/Src/DDD.Core/Application/SyncQueryHandlerWithLogging`1.cs new file mode 100644 index 0000000..51496c1 --- /dev/null +++ b/Src/DDD.Core/Application/SyncQueryHandlerWithLogging`1.cs @@ -0,0 +1,62 @@ +using EnsureThat; +using Microsoft.Extensions.Logging; +using System.Diagnostics; + +namespace DDD.Core.Application +{ + using Domain; + + /// + /// A decorator that logs information about queries. + /// + public class SyncQueryHandlerWithLogging : ISyncQueryHandler + where TQuery : class, IQuery + where TContext : BoundedContext + { + + #region Fields + + private readonly ILogger logger; + private readonly ISyncQueryHandler queryHandler; + + #endregion Fields + + #region Constructors + + public SyncQueryHandlerWithLogging(ISyncQueryHandler queryHandler, ILogger logger) + { + Ensure.That(queryHandler, nameof(queryHandler)).IsNotNull(); + Ensure.That(logger, nameof(logger)).IsNotNull(); + this.queryHandler = queryHandler; + this.logger = logger; + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.queryHandler.Context; + + #endregion Properties + + #region Methods + + public TResult Handle(TQuery query, IMessageContext context) + { + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug("Le traitement de la requête {Query} par {QueryHandler} a commencé.", query, this.queryHandler.GetType().Name); + var stopWatch = Stopwatch.StartNew(); + var result = this.queryHandler.Handle(query, context); + stopWatch.Stop(); + this.logger.LogDebug("Le traitement de la requête {Query} par {QueryHandler} s'est terminé (temps d'exécution: {QueryExecutionTime} ms).", query, this.queryHandler.GetType().Name, stopWatch.ElapsedMilliseconds); + return result; + } + else + return this.queryHandler.Handle(query, context); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/DDD.Core.csproj b/Src/DDD.Core/DDD.Core.csproj index abad414..a40bb3a 100644 --- a/Src/DDD.Core/DDD.Core.csproj +++ b/Src/DDD.Core/DDD.Core.csproj @@ -1,128 +1,32 @@ - - - + - Debug - AnyCPU - {C6C3E419-B9AA-44AD-9DBF-789294687AE6} + net48;netstandard2.1 Library - Properties - DDD.Core - DDD.Core - v4.7.2 - 512 - + false + 8.0 - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 bin\Debug\DDD.Core.xml 1591 - pdbonly - true - bin\Release\ - TRACE - prompt - 4 bin\Release\DDD.Core.xml 1591 - - - L:\Packages\Conditions.2.1.0\lib\portable-net45+win+wpa81+wp80+MonoAndroid10+MonoTouch10\Conditions.dll - True - - - - - - - - Properties\CommonAssemblyInfo.cs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - {596a8700-3d18-4a62-b200-1f78a9ea4617} - DDD.Core.Abstractions - - - {2438b31a-3a39-4878-81fa-be5ae715eae5} - DDD.Core.Messages - + + + + 7.0.0 + - - \ No newline at end of file diff --git a/Src/DDD.Core/Domain/CompositeTranslatorExtensions.cs b/Src/DDD.Core/Domain/CompositeTranslatorExtensions.cs new file mode 100644 index 0000000..375c99d --- /dev/null +++ b/Src/DDD.Core/Domain/CompositeTranslatorExtensions.cs @@ -0,0 +1,37 @@ +using EnsureThat; +using System; + +namespace DDD.Core.Domain +{ + using Mapping; + using Collections; + + public static class CompositeTranslatorExtensions + { + + #region Methods + + public static void RegisterFallback(this CompositeTranslator translator) + { + Ensure.That(translator, nameof(translator)).IsNotNull(); + translator.Register((exception, context) => + { + context.TryGetValue("EntityType", out Type entityType); + return new RepositoryException(isTransient: false, entityType, exception); + }); + } + + public static void RegisterFallback(this CompositeTranslator translator) + { + Ensure.That(translator, nameof(translator)).IsNotNull(); + translator.Register((exception, context) => + { + context.TryGetValue("ServiceType", out Type serviceType); + return new DomainServiceException(isTransient: false, serviceType, exception); + }); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Domain/DomainEntity.cs b/Src/DDD.Core/Domain/DomainEntity.cs index 3f44036..579eef8 100644 --- a/Src/DDD.Core/Domain/DomainEntity.cs +++ b/Src/DDD.Core/Domain/DomainEntity.cs @@ -1,12 +1,9 @@ -using Conditions; +using EnsureThat; using System; using System.Collections.Generic; -using System.Linq; namespace DDD.Core.Domain { - using Collections; - /// /// Base class for entities of the Domain Model. /// @@ -20,21 +17,14 @@ public abstract class DomainEntity : IEquatable #region Constructors - protected DomainEntity(EntityState entityState = EntityState.Added, IEnumerable events = null) + protected DomainEntity(IEnumerable events = null) { - this.EntityState = entityState; if (events != null) this.events.AddRange(events); } #endregion Constructors - #region Properties - - protected EntityState EntityState { get; private set; } - - #endregion Properties - #region Methods public static bool operator !=(DomainEntity a, DomainEntity b) @@ -77,16 +67,10 @@ public virtual string IdentityAsString() protected void AddEvent(IDomainEvent @event) { - Condition.Requires(@event, nameof(@event)).IsNotNull(); + Ensure.That(@event, nameof(@event)).IsNotNull(); this.events.Add(@event); } - protected void MarkAsModified() - { - if (this.EntityState != EntityState.Added) - this.EntityState = EntityState.Modified; - } - #endregion Methods } } \ No newline at end of file diff --git a/Src/DDD.Core/Domain/DomainException.cs b/Src/DDD.Core/Domain/DomainException.cs index 5a6d9dd..82969d0 100644 --- a/Src/DDD.Core/Domain/DomainException.cs +++ b/Src/DDD.Core/Domain/DomainException.cs @@ -5,25 +5,27 @@ namespace DDD.Core.Domain /// /// The base class for all exceptions thrown in the domain layer. /// - public abstract class DomainException : Exception + public abstract class DomainException : TimestampedException { #region Constructors - protected DomainException() - : base("An error has occurred in the domain layer.") + protected DomainException(bool isTransient, Exception innerException = null) + : base(isTransient, DefaultMessage(), innerException) { } - protected DomainException(string message) : base(message) - { - } - - protected DomainException(string message, Exception innerException) : base(message, innerException) + protected DomainException(bool isTransient, string message, Exception innerException = null) + : base(isTransient, message, innerException) { } #endregion Constructors + #region Methods + + public static string DefaultMessage() => "An error occurred in the domain layer."; + + #endregion Methods } } diff --git a/Src/DDD.Core/Domain/DomainServiceConflictException.cs b/Src/DDD.Core/Domain/DomainServiceConflictException.cs new file mode 100644 index 0000000..d6e56e2 --- /dev/null +++ b/Src/DDD.Core/Domain/DomainServiceConflictException.cs @@ -0,0 +1,37 @@ +using System; + +namespace DDD.Core.Domain +{ + /// + /// Exception thrown when a conflict with other requests has been detected while calling a domain service. + /// + public class DomainServiceConflictException : DomainServiceException + { + + #region Constructors + + public DomainServiceConflictException(Type serviceType = null, Exception innerException = null) + : base(true, DefaultMessage(serviceType), serviceType, innerException) + { + } + + public DomainServiceConflictException(string message, Type serviceType = null, Exception innerException = null) + : base(true, message, serviceType, innerException) + { + } + + #endregion Constructors + + #region Methods + + public static new string DefaultMessage(Type serviceType = null) + { + if (serviceType == null) + return "A conflict has been detected while calling a domain service."; + return $"A conflict has been detected while calling the domain service '{serviceType.Name}'."; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Domain/DomainServiceException.cs b/Src/DDD.Core/Domain/DomainServiceException.cs index 17eeefb..b1b5bb9 100644 --- a/Src/DDD.Core/Domain/DomainServiceException.cs +++ b/Src/DDD.Core/Domain/DomainServiceException.cs @@ -1,35 +1,24 @@ using System; +using System.Runtime.Serialization; namespace DDD.Core.Domain { /// - /// Exception thrown when a problem occurred while calling a domain service. + /// Exception thrown when an error occurred while calling a domain service. /// public class DomainServiceException : DomainException { #region Constructors - public DomainServiceException() - : base("A problem occurred while calling a domain service.") - { - } - - public DomainServiceException(string message) : base(message) - { - } - - public DomainServiceException(string message, Exception innerException) : base(message, innerException) - { - } - - public DomainServiceException(string message, Exception innerException, Type serviceType) : base(message, innerException) + public DomainServiceException(bool isTransient, Type serviceType = null, Exception innerException = null) + : base(isTransient, DefaultMessage(serviceType), innerException) { this.ServiceType = serviceType; } - public DomainServiceException(Exception innerException, Type serviceType) - : base($"A problem occurred while calling the service '{serviceType.Name}'.", innerException) + public DomainServiceException(bool isTransient, string message, Type serviceType = null, Exception innerException = null) + : base(isTransient, message, innerException) { this.ServiceType = serviceType; } @@ -44,11 +33,27 @@ public DomainServiceException(Exception innerException, Type serviceType) #region Methods + public static string DefaultMessage(Type serviceType = null) + { + if (serviceType == null) + return "An error occurred while calling a domain service."; + return $"An error occurred while calling the service '{serviceType.Name}'."; + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + if (this.ServiceType != null) + info.AddValue("ServiceType", this.ServiceType); + } + public override string ToString() { var s = $"{this.GetType()}: {this.Message} "; + s += $"{Environment.NewLine}{nameof(Timestamp)}: {this.Timestamp}"; + s += $"{Environment.NewLine}{nameof(IsTransient)}: {this.IsTransient}"; if (this.ServiceType != null) - s += $"{Environment.NewLine}ServiceType: {this.ServiceType}"; + s += $"{Environment.NewLine}{nameof(ServiceType)}: {this.ServiceType}"; if (this.InnerException != null) s += $" ---> {this.InnerException}"; if (this.StackTrace != null) @@ -57,6 +62,5 @@ public override string ToString() } #endregion Methods - } } diff --git a/Src/DDD.Core/Domain/DomainServiceInvalidException.cs b/Src/DDD.Core/Domain/DomainServiceInvalidException.cs new file mode 100644 index 0000000..349f2aa --- /dev/null +++ b/Src/DDD.Core/Domain/DomainServiceInvalidException.cs @@ -0,0 +1,80 @@ +using System; +using System.Linq; +using System.Runtime.Serialization; + +namespace DDD.Core.Domain +{ + using Validation; + + /// + /// Exception thrown when a request to an domain service is invalid. + /// + public class DomainServiceInvalidException : DomainServiceException + { + + #region Constructors + + public DomainServiceInvalidException(Type serviceType = null, ValidationFailure[] failures = null, Exception innerException = null) + : base(false, DefaultMessage(serviceType), serviceType, innerException) + { + this.Failures = failures; + } + + public DomainServiceInvalidException(string message, Type serviceType = null, ValidationFailure[] failures = null, Exception innerException = null) + : base(false, message, serviceType, innerException) + { + this.Failures = failures; + } + + #endregion Constructors + + #region Properties + + public ValidationFailure[] Failures { get; } + + #endregion Properties + + #region Methods + + public static new string DefaultMessage(Type serviceType = null) + { + if (serviceType == null) + return "The request to the domain service is invalid."; + return $"The request to the domain service '{serviceType.Name}' is invalid."; + } + + public bool HasFailures() => this.Failures != null && this.Failures.Any(); + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + if (this.Failures != null) + { + for (var i = 0; i < this.Failures.Length; i++) + info.AddValue($"Failure{i}", this.Failures[i]); + } + } + + public override string ToString() + { + var s = $"{this.GetType()}: {this.Message} "; + s += $"{Environment.NewLine}{nameof(Timestamp)}: {this.Timestamp}"; + s += $"{Environment.NewLine}{nameof(IsTransient)}: {this.IsTransient}"; + if (this.ServiceType != null) + s += $"{Environment.NewLine}{nameof(ServiceType)}: {this.ServiceType}"; + if (this.Failures != null) + { + for (var i = 0; i < this.Failures.Length; i++) + s += $"{Environment.NewLine}Failure{i}: {this.Failures[i]}"; + } + if (this.InnerException != null) + s += $" ---> {this.InnerException}"; + if (this.StackTrace != null) + s += $"{Environment.NewLine}{this.StackTrace}"; + return s; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Domain/DomainServiceTimeoutException.cs b/Src/DDD.Core/Domain/DomainServiceTimeoutException.cs new file mode 100644 index 0000000..9fedb4f --- /dev/null +++ b/Src/DDD.Core/Domain/DomainServiceTimeoutException.cs @@ -0,0 +1,37 @@ +using System; + +namespace DDD.Core.Domain +{ + /// + /// Exception thrown when a request to a domain service has expired. + /// + public class DomainServiceTimeoutException : DomainServiceException + { + + #region Constructors + + public DomainServiceTimeoutException(Type serviceType = null, Exception innerException = null) + : base(true, DefaultMessage(serviceType), serviceType, innerException) + { + } + + public DomainServiceTimeoutException(string message, Type serviceType = null, Exception innerException = null) + : base(true, message, serviceType, innerException) + { + } + + #endregion Constructors + + #region Methods + + public static new string DefaultMessage(Type serviceType = null) + { + if (serviceType == null) + return "The request to the domain service has expired."; + return $"The request to the domain service '{serviceType.Name}' has expired."; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Domain/DomainServiceUnauthorizedException.cs b/Src/DDD.Core/Domain/DomainServiceUnauthorizedException.cs new file mode 100644 index 0000000..13399e2 --- /dev/null +++ b/Src/DDD.Core/Domain/DomainServiceUnauthorizedException.cs @@ -0,0 +1,37 @@ +using System; + +namespace DDD.Core.Domain +{ + /// + /// Exception thrown when a request to a domain service was denied. + /// + public class DomainServiceUnauthorizedException : DomainServiceException + { + + #region Constructors + + public DomainServiceUnauthorizedException(Type serviceType = null, Exception innerException = null) + : base(false, DefaultMessage(serviceType), serviceType, innerException) + { + } + + public DomainServiceUnauthorizedException(string message, Type serviceType = null, Exception innerException = null) + : base(false, message, serviceType, innerException) + { + } + + #endregion Constructors + + #region Methods + + public static new string DefaultMessage(Type serviceType = null) + { + if (serviceType == null) + return "The request to the domain service was denied."; + return $"The request to the domain service '{serviceType.Name}' was denied."; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Domain/DomainServiceUnavailableException.cs b/Src/DDD.Core/Domain/DomainServiceUnavailableException.cs new file mode 100644 index 0000000..6b50013 --- /dev/null +++ b/Src/DDD.Core/Domain/DomainServiceUnavailableException.cs @@ -0,0 +1,37 @@ +using System; + +namespace DDD.Core.Domain +{ + /// + /// Exception thrown when a domain service is currently unavailable. + /// + public class DomainServiceUnavailableException : DomainServiceException + { + + #region Constructors + + public DomainServiceUnavailableException(Type serviceType = null, Exception innerException = null) + : base(true, DefaultMessage(serviceType), serviceType, innerException) + { + } + + public DomainServiceUnavailableException(string message, Type serviceType = null, Exception innerException = null) + : base(true, message, serviceType, innerException) + { + } + + #endregion Constructors + + #region Methods + + public static new string DefaultMessage(Type serviceType = null) + { + if (serviceType == null) + return "The domain service is currently unavailable."; + return $"The domain service '{serviceType.Name}' is currently unavailable."; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Domain/EntityState.cs b/Src/DDD.Core/Domain/EntityState.cs deleted file mode 100644 index 1d3d3f8..0000000 --- a/Src/DDD.Core/Domain/EntityState.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace DDD.Core.Domain -{ - public enum EntityState - { - Unchanged = 0, // Default - Added, - Modified, - Deleted - } -} \ No newline at end of file diff --git a/Src/DDD.Core/Domain/EventHandler.cs b/Src/DDD.Core/Domain/EventHandler.cs deleted file mode 100644 index 63dbe06..0000000 --- a/Src/DDD.Core/Domain/EventHandler.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace DDD.Core.Domain -{ - public abstract class EventHandler : IEventHandler - where TEvent : IEvent - { - #region Properties - - Type IEventHandler.EventType => typeof(TEvent); - - #endregion Properties - - #region Methods - - public abstract void Handle(TEvent @event); - - void IEventHandler.Handle(IEvent @event) => this.Handle((TEvent)@event); - - #endregion Methods - } -} \ No newline at end of file diff --git a/Src/DDD.Core/Domain/EventPublisher.cs b/Src/DDD.Core/Domain/EventPublisher.cs deleted file mode 100644 index d3d5728..0000000 --- a/Src/DDD.Core/Domain/EventPublisher.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Conditions; -using System.Collections.Generic; - -namespace DDD.Core.Domain -{ - public class EventPublisher : IEventPublisher - { - #region Fields - - private List subscribers = new List(); - - #endregion Fields - - #region Methods - - public void Publish(IEvent @event) - { - Condition.Requires(@event, nameof(@event)).IsNotNull(); - foreach (var subscriber in this.subscribers) - { - if (subscriber.EventType.IsAssignableFrom(@event.GetType())) - subscriber.Handle(@event); - } - } - - public void Subscribe(IEventHandler subscriber) - { - Condition.Requires(subscriber, nameof(subscriber)).IsNotNull(); - this.subscribers.Add(subscriber); - } - - public void UnSubscribe(IEventHandler subscriber) - { - Condition.Requires(subscriber, nameof(subscriber)).IsNotNull(); - this.subscribers.Remove(subscriber); - } - - #endregion Methods - - } -} \ No newline at end of file diff --git a/Src/DDD.Core/Domain/EventState.cs b/Src/DDD.Core/Domain/EventState.cs deleted file mode 100644 index f921d38..0000000 --- a/Src/DDD.Core/Domain/EventState.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace DDD.Core.Domain -{ - public class EventState - { - - #region Properties - - public string Body { get; set; } - - public Guid CommitId { get; set; } - - public bool Dispatched { get; set; } = false; - - public string EventType { get; set; } - - public long Id { get; set; } - - public DateTime OccurredOn { get; set; } - - public string StreamId { get; set; } - - public string Subject { get; set; } - - #endregion Properties - - } -} \ No newline at end of file diff --git a/Src/DDD.Core/Domain/EventTranslator.cs b/Src/DDD.Core/Domain/EventTranslator.cs deleted file mode 100644 index 5bd7418..0000000 --- a/Src/DDD.Core/Domain/EventTranslator.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Conditions; -using System.Collections.Generic; - -namespace DDD.Core.Domain -{ - using Mapping; - using Serialization; - - public class EventTranslator : IObjectTranslator - { - #region Fields - - private readonly ITextSerializer eventSerializer; - - #endregion Fields - - #region Constructors - - public EventTranslator(ITextSerializer eventSerializer) - { - Condition.Requires(eventSerializer, nameof(eventSerializer)).IsNotNull(); - this.eventSerializer = eventSerializer; - } - - #endregion Constructors - - #region Methods - - public EventState Translate(IEvent @event, IDictionary options = null) - { - Condition.Requires(@event, nameof(@event)).IsNotNull(); - return new EventState() - { - OccurredOn = @event.OccurredOn, - EventType = @event.GetType().Name, - Body = this.eventSerializer.SerializeToString(@event) - }; - } - - #endregion Methods - } -} \ No newline at end of file diff --git a/Src/DDD.Core/Domain/IAsyncEventPublisher.cs b/Src/DDD.Core/Domain/IAsyncEventPublisher.cs deleted file mode 100644 index 9499d38..0000000 --- a/Src/DDD.Core/Domain/IAsyncEventPublisher.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Threading.Tasks; - -namespace DDD.Core.Domain -{ - /// - /// Publish asynchronously events outside the local bounded context (used to decouple bounded contexts). - /// - public interface IAsyncEventPublisher - { - - #region Methods - - Task PublishAsync(IEvent @event); - - #endregion Methods - - } -} \ No newline at end of file diff --git a/Src/DDD.Core/Domain/IAsyncRepository.cs b/Src/DDD.Core/Domain/IAsyncRepository.cs index a1d7d75..a34379c 100644 --- a/Src/DDD.Core/Domain/IAsyncRepository.cs +++ b/Src/DDD.Core/Domain/IAsyncRepository.cs @@ -1,16 +1,21 @@ -using System.Threading.Tasks; +using System.Threading; +using System.Threading.Tasks; namespace DDD.Core.Domain { - public interface IAsyncRepository + /// + /// Defines a component to find and save asynchronously an aggregate. + /// + public interface IAsyncRepository where TDomainEntity : DomainEntity + where TIdentity : ComparableValueObject { #region Methods - Task FindAsync(ComparableValueObject identity); + Task FindAsync(TIdentity identity, CancellationToken cancellationToken = default); - Task SaveAsync(TDomainEntity aggregate); + Task SaveAsync(TDomainEntity aggregate, CancellationToken cancellationToken = default); #endregion Methods } -} +} \ No newline at end of file diff --git a/Src/DDD.Core/Domain/IEventHandler.cs b/Src/DDD.Core/Domain/IEventHandler.cs deleted file mode 100644 index 62407ae..0000000 --- a/Src/DDD.Core/Domain/IEventHandler.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace DDD.Core.Domain -{ - public interface IEventHandler - { - #region Properties - - Type EventType { get; } - - #endregion Properties - - #region Methods - - void Handle(IEvent @event); - - #endregion Methods - } -} \ No newline at end of file diff --git a/Src/DDD.Core/Domain/IEventHandler`1.cs b/Src/DDD.Core/Domain/IEventHandler`1.cs deleted file mode 100644 index b7bbb9c..0000000 --- a/Src/DDD.Core/Domain/IEventHandler`1.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace DDD.Core.Domain -{ - public interface IEventHandler : IEventHandler - where TEvent : IEvent - { - #region Methods - - void Handle(TEvent @event); - - #endregion Methods - } -} \ No newline at end of file diff --git a/Src/DDD.Core/Domain/IEventPublisher.cs b/Src/DDD.Core/Domain/IEventPublisher.cs deleted file mode 100644 index 4e62274..0000000 --- a/Src/DDD.Core/Domain/IEventPublisher.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace DDD.Core.Domain -{ - /// - /// Publish synchronously events inside the local bounded context (used to decouple layers). - /// - public interface IEventPublisher - { - - #region Methods - - void Publish(IEvent @event); - - void Subscribe(IEventHandler subscriber); - - void UnSubscribe(IEventHandler subscriber); - - #endregion Methods - - } -} \ No newline at end of file diff --git a/Src/DDD.Core/Domain/IEventPublisherExtensions.cs b/Src/DDD.Core/Domain/IEventPublisherExtensions.cs deleted file mode 100644 index 4a411f0..0000000 --- a/Src/DDD.Core/Domain/IEventPublisherExtensions.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Conditions; -using System; -using System.Collections.Generic; - -namespace DDD.Core.Domain -{ - public static class IEventPublisherExtensions - { - - #region Methods - - public static void PublishAll(this IEventPublisher publisher, IEnumerable events) - { - Condition.Requires(publisher, nameof(publisher)).IsNotNull(); - Condition.Requires(events, nameof(events)) - .IsNotNull() - .DoesNotContain(null); - foreach (var @event in events) - publisher.Publish(@event); - } - - public static void Subscribe(this IEventPublisher publisher, Action subscriber) where TEvent : IEvent - { - Condition.Requires(publisher, nameof(publisher)).IsNotNull(); - Condition.Requires(subscriber, nameof(subscriber)).IsNotNull(); - publisher.Subscribe(new DelegatingEventHandler(subscriber)); - } - - public static void SubscribeAll(this IEventPublisher publisher, IEnumerable subscribers) - { - Condition.Requires(publisher, nameof(publisher)).IsNotNull(); - Condition.Requires(subscribers, nameof(subscribers)) - .IsNotNull() - .DoesNotContain(null); - foreach (var subscriber in subscribers) - publisher.Subscribe(subscriber); - } - - public static void UnSubscribeAll(this IEventPublisher publisher, IEnumerable subscribers) - { - Condition.Requires(publisher, nameof(publisher)).IsNotNull(); - Condition.Requires(subscribers, nameof(subscribers)) - .IsNotNull() - .DoesNotContain(null); - foreach (var subscriber in subscribers) - publisher.UnSubscribe(subscriber); - } - - #endregion Methods - - #region Classes - - private class DelegatingEventHandler : EventHandler - where TEvent : IEvent - { - - #region Fields - - private readonly Action handle; - - #endregion Fields - - #region Constructors - - public DelegatingEventHandler(Action handle) - { - Condition.Requires(handle, nameof(handle)).IsNotNull(); - this.handle = handle; - } - - #endregion Constructors - - #region Methods - - public override void Handle(TEvent @event) - { - this.handle(@event); - } - - #endregion Methods - - } - - #endregion Classes - - } -} diff --git a/Src/DDD.Core/Domain/IRepository.cs b/Src/DDD.Core/Domain/IRepository.cs index 477656d..89c0712 100644 --- a/Src/DDD.Core/Domain/IRepository.cs +++ b/Src/DDD.Core/Domain/IRepository.cs @@ -1,14 +1,9 @@ namespace DDD.Core.Domain { - public interface IRepository + public interface IRepository + : ISyncRepository, IAsyncRepository where TDomainEntity : DomainEntity + where TIdentity : ComparableValueObject { - #region Methods - - TDomainEntity Find(ComparableValueObject identity); - - void Save(TDomainEntity aggregate); - - #endregion Methods } -} +} \ No newline at end of file diff --git a/Src/DDD.Core/Domain/IStateEntity.cs b/Src/DDD.Core/Domain/IStateEntity.cs deleted file mode 100644 index 303ab16..0000000 --- a/Src/DDD.Core/Domain/IStateEntity.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace DDD.Core.Domain -{ - /// - /// An entity of the State Model. - /// - public interface IStateEntity - { - #region Properties - - EntityState EntityState { get; set; } - - #endregion Properties - } -} \ No newline at end of file diff --git a/Src/DDD.Core/Domain/IStateObjectConvertible.cs b/Src/DDD.Core/Domain/IStateObjectConvertible.cs deleted file mode 100644 index 25f91d6..0000000 --- a/Src/DDD.Core/Domain/IStateObjectConvertible.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace DDD.Core.Domain -{ - public interface IStateObjectConvertible - where TState : class, new() - { - - #region Methods - - TState ToState(); - - #endregion Methods - - } -} \ No newline at end of file diff --git a/Src/DDD.Core/Domain/ISyncRepository.cs b/Src/DDD.Core/Domain/ISyncRepository.cs new file mode 100644 index 0000000..b5f1f1d --- /dev/null +++ b/Src/DDD.Core/Domain/ISyncRepository.cs @@ -0,0 +1,18 @@ +namespace DDD.Core.Domain +{ + /// + /// Defines a component to find and save synchronously an aggregate. + /// + public interface ISyncRepository + where TDomainEntity : DomainEntity + where TIdentity : ComparableValueObject + { + #region Methods + + TDomainEntity Find(TIdentity identity); + + void Save(TDomainEntity aggregate); + + #endregion Methods + } +} diff --git a/Src/DDD.Core/Domain/RepositoryConcurrencyException.cs b/Src/DDD.Core/Domain/RepositoryConcurrencyException.cs deleted file mode 100644 index 66ce778..0000000 --- a/Src/DDD.Core/Domain/RepositoryConcurrencyException.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; - -namespace DDD.Core.Domain -{ - /// - /// Exception thrown by repositories when a concurrency conflict occurred while saving a domain entity. - /// - public class RepositoryConcurrencyException : RepositoryException - { - - #region Constructors - - public RepositoryConcurrencyException() - : base("A concurrency conflict occurred while saving a domain entity.") - { - } - - public RepositoryConcurrencyException(string message) : base(message) - { - } - - public RepositoryConcurrencyException(string message, Exception innerException) : base(message, innerException) - { - } - - public RepositoryConcurrencyException(string message, Exception innerException, Type entityType) - : base(message, innerException, entityType) - { - } - - public RepositoryConcurrencyException(Exception innerException, Type entityType) - : base($"A concurrency conflict occurred while saving a domain entity of type '{entityType.Name}'.", innerException, entityType) - { - } - - #endregion Constructors - - } -} diff --git a/Src/DDD.Core/Domain/RepositoryConflictException.cs b/Src/DDD.Core/Domain/RepositoryConflictException.cs new file mode 100644 index 0000000..53972c0 --- /dev/null +++ b/Src/DDD.Core/Domain/RepositoryConflictException.cs @@ -0,0 +1,37 @@ +using System; + +namespace DDD.Core.Domain +{ + /// + /// Exception thrown when a conflict has been detected while saving a domain entity. + /// + public class RepositoryConflictException : RepositoryException + { + + #region Constructors + + public RepositoryConflictException(Type entityType = null, Exception innerException = null) + : base(true, DefaultMessage(entityType), entityType, innerException) + { + } + + public RepositoryConflictException(string message, Type entityType = null, Exception innerException = null) + : base(true, message, entityType, innerException) + { + } + + #endregion Constructors + + #region Methods + + public static new string DefaultMessage(Type entityType = null) + { + if (entityType == null) + return "A conflict has been detected while saving a domain entity."; + return $"A conflict has been detected while saving a domain entity '{entityType.Name}'."; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Domain/RepositoryException.cs b/Src/DDD.Core/Domain/RepositoryException.cs index 6d1a1a3..62409eb 100644 --- a/Src/DDD.Core/Domain/RepositoryException.cs +++ b/Src/DDD.Core/Domain/RepositoryException.cs @@ -1,35 +1,24 @@ using System; +using System.Runtime.Serialization; namespace DDD.Core.Domain { /// - /// Exception thrown by repositories when a problem occurred while saving or finding domain entities. + /// Exception thrown when an error occurred while calling a repository. /// public class RepositoryException : DomainException { #region Constructors - public RepositoryException() - : base("A problem occurred while saving or finding domain entities.") - { - } - - public RepositoryException(string message) : base(message) - { - } - - public RepositoryException(string message, Exception innerException) : base(message, innerException) - { - } - - public RepositoryException(string message, Exception innerException, Type entityType) : base(message, innerException) + public RepositoryException(bool isTransient, Type entityType = null, Exception innerException = null) + : base(isTransient, DefaultMessage(entityType), innerException) { this.EntityType = entityType; } - public RepositoryException(Exception innerException, Type entityType) - : base($"A problem occurred while saving or finding domain entities of type '{entityType.Name}'.", innerException) + public RepositoryException(bool isTransient, string message, Type entityType = null, Exception innerException = null) + : base(isTransient, message, innerException) { this.EntityType = entityType; } @@ -44,11 +33,27 @@ public RepositoryException(Exception innerException, Type entityType) #region Methods + public static string DefaultMessage(Type entityType = null) + { + if (entityType == null) + return "An error occurred while saving or finding a domain entity."; + return $"An error occurred while saving or finding a domain entity '{entityType.Name}'."; + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + if (this.EntityType != null) + info.AddValue("EntityType", this.EntityType); + } + public override string ToString() { var s = $"{this.GetType()}: {this.Message} "; + s += $"{Environment.NewLine}{nameof(Timestamp)}: {this.Timestamp}"; + s += $"{Environment.NewLine}{nameof(IsTransient)}: {this.IsTransient}"; if (this.EntityType != null) - s += $"{Environment.NewLine}Entity type: {this.EntityType}"; + s += $"{Environment.NewLine}{nameof(EntityType)}: {this.EntityType}"; if (this.InnerException != null) s += $" ---> {this.InnerException}"; if (this.StackTrace != null) @@ -57,6 +62,5 @@ public override string ToString() } #endregion Methods - } } diff --git a/Src/DDD.Core/Domain/RepositoryTimeoutException.cs b/Src/DDD.Core/Domain/RepositoryTimeoutException.cs new file mode 100644 index 0000000..6724842 --- /dev/null +++ b/Src/DDD.Core/Domain/RepositoryTimeoutException.cs @@ -0,0 +1,37 @@ +using System; + +namespace DDD.Core.Domain +{ + /// + /// Exception thrown when a request to a repository has expired. + /// + public class RepositoryTimeoutException : RepositoryException + { + + #region Constructors + + public RepositoryTimeoutException(Type entityType = null, Exception innerException = null) + : base(true, DefaultMessage(entityType), entityType, innerException) + { + } + + public RepositoryTimeoutException(string message, Type entityType = null, Exception innerException = null) + : base(true, message, entityType, innerException) + { + } + + #endregion Constructors + + #region Methods + + public static new string DefaultMessage(Type entityType = null) + { + if (entityType == null) + return "The request to the repository has expired."; + return $"The request to the repository of '{entityType.Name}' has expired."; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Domain/RepositoryUnauthorizedException.cs b/Src/DDD.Core/Domain/RepositoryUnauthorizedException.cs new file mode 100644 index 0000000..c40c6b7 --- /dev/null +++ b/Src/DDD.Core/Domain/RepositoryUnauthorizedException.cs @@ -0,0 +1,37 @@ +using System; + +namespace DDD.Core.Domain +{ + /// + /// Exception thrown when a request to a repository was denied. + /// + public class RepositoryUnauthorizedException : RepositoryException + { + + #region Constructors + + public RepositoryUnauthorizedException(Type entityType = null, Exception innerException = null) + : base(false, DefaultMessage(entityType), entityType, innerException) + { + } + + public RepositoryUnauthorizedException(string message, Type entityType = null, Exception innerException = null) + : base(false, message, entityType, innerException) + { + } + + #endregion Constructors + + #region Methods + + public static new string DefaultMessage(Type entityType = null) + { + if (entityType == null) + return "The request to the repository was denied."; + return $"The request to the repository of '{entityType.Name}' was denied."; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Domain/RepositoryUnavailableException.cs b/Src/DDD.Core/Domain/RepositoryUnavailableException.cs new file mode 100644 index 0000000..3f221ed --- /dev/null +++ b/Src/DDD.Core/Domain/RepositoryUnavailableException.cs @@ -0,0 +1,37 @@ +using System; + +namespace DDD.Core.Domain +{ + /// + /// Exception thrown when a repository is currently unavailable. + /// + public class RepositoryUnavailableException : RepositoryException + { + + #region Constructors + + public RepositoryUnavailableException(Type entityType = null, Exception innerException = null) + : base(true, DefaultMessage(entityType), entityType, innerException) + { + } + + public RepositoryUnavailableException(string message, Type entityType = null, Exception innerException = null) + : base(true, message, entityType, innerException) + { + } + + #endregion Constructors + + #region Methods + + public static new string DefaultMessage(Type entityType = null) + { + if (entityType == null) + return "The repository is currently unavailable."; + return $"The repository of '{entityType.Name}' is currently unavailable."; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Domain/ValueObject.cs b/Src/DDD.Core/Domain/ValueObject.cs index 7f4877b..22381b8 100644 --- a/Src/DDD.Core/Domain/ValueObject.cs +++ b/Src/DDD.Core/Domain/ValueObject.cs @@ -13,13 +13,12 @@ public abstract class ValueObject : IEquatable public static bool operator !=(ValueObject a, ValueObject b) { - if (ReferenceEquals(a, null)) return !ReferenceEquals(b, null); - return !a.Equals(b); + return !(a == b); } public static bool operator ==(ValueObject a, ValueObject b) { - if (ReferenceEquals(a, null)) return ReferenceEquals(b, null); + if (a is null) return b is null; return a.Equals(b); } @@ -27,8 +26,8 @@ public abstract class ValueObject : IEquatable public bool Equals(ValueObject other) { + if (other is null) return false; if (ReferenceEquals(this, other)) return true; - if (ReferenceEquals(other, null)) return false; if (this.GetType() != other.GetType()) return false; return this.EqualityComponents().SequenceEqual(other.EqualityComponents()); } diff --git a/Src/DDD.Core/Infrastructure/Data/DbConnectionFactory.cs b/Src/DDD.Core/Infrastructure/Data/DbConnectionFactory.cs deleted file mode 100644 index 4f4eaed..0000000 --- a/Src/DDD.Core/Infrastructure/Data/DbConnectionFactory.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Data.Common; -using Conditions; - -namespace DDD.Core.Infrastructure.Data -{ - /// - /// Implements a method that creates a based on a specified provider name and a specified connection string. - /// - public abstract class DbConnectionFactory : IDbConnectionFactory - { - - #region Fields - - private readonly string connectionString; - private readonly DbProviderFactory providerFactory; - - #endregion Fields - - #region Constructors - - protected DbConnectionFactory(string providerName, string connectionString) - { - Condition.Requires(providerName, nameof(providerName)).IsNotNullOrWhiteSpace(); - Condition.Requires(connectionString, nameof(connectionString)).IsNotNullOrWhiteSpace(); - this.providerFactory = DbProviderFactories.GetFactory(providerName); - this.connectionString = connectionString; - } - - #endregion Constructors - - #region Methods - - public DbConnection CreateConnection() - { - var connection = this.providerFactory.CreateConnection(); - connection.ConnectionString = this.connectionString; - return connection; - } - - #endregion Methods - - } -} diff --git a/Src/DDD.Core/Infrastructure/Data/DbConnectionSettings.cs b/Src/DDD.Core/Infrastructure/Data/DbConnectionSettings.cs new file mode 100644 index 0000000..e04a23a --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/DbConnectionSettings.cs @@ -0,0 +1,45 @@ +using EnsureThat; +using System; + +namespace DDD.Core.Infrastructure.Data +{ + using Domain; + + public class DbConnectionSettings + where TContext : BoundedContext + { + + #region Constructors + + public DbConnectionSettings(string providerName, string connectionString) + { + Ensure.That(providerName, nameof(providerName)).IsNotNullOrWhiteSpace(); + Ensure.That(connectionString, nameof(connectionString)).IsNotNullOrWhiteSpace(); + this.ContextType = typeof(TContext); + this.ProviderName = providerName; + this.ConnectionString = connectionString; + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the type of the associated context. + /// + public Type ContextType { get; } + + /// + /// Gets the provider name. + /// + public string ProviderName { get; } + + /// + /// Gets the connection string. + /// + public string ConnectionString { get; } + + #endregion Properties + + } +} diff --git a/Src/DDD.Core/Infrastructure/Data/DbQueryHandler.cs b/Src/DDD.Core/Infrastructure/Data/DbQueryHandler.cs deleted file mode 100644 index 31ce33a..0000000 --- a/Src/DDD.Core/Infrastructure/Data/DbQueryHandler.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Conditions; -using System.Data; -using System.Data.Common; -using System.Threading.Tasks; - -namespace DDD.Core.Infrastructure.Data -{ - using Application; - using Threading; - - /// - /// Base class for handling database queries. - /// - /// The type of the query. - /// The type of the result. - /// - public abstract class DbQueryHandler : IAsyncQueryHandler - where TQuery : class, IQuery - { - - #region Constructors - - /// - /// Initializes a new instance of the class. - /// - /// The database connection factory. - protected DbQueryHandler(IDbConnectionFactory connectionFactory) - { - Condition.Requires(connectionFactory, nameof(connectionFactory)).IsNotNull(); - this.ConnectionFactory = connectionFactory; - } - - #endregion Constructors - - #region Properties - - protected IDbConnectionFactory ConnectionFactory { get; } - - #endregion Properties - - #region Methods - - public async Task HandleAsync(TQuery query) - { - Condition.Requires(query, nameof(query)).IsNotNull(); - await new SynchronizationContextRemover(); - try - { - using (var connection = await this.ConnectionFactory.CreateOpenConnectionAsync()) - { - return await this.ExecuteAsync(query, connection); - } - } - catch(DbException ex) - { - throw new QueryException(ex, query); - } - - } - - protected abstract Task ExecuteAsync(TQuery query, IDbConnection connection); - - #endregion Methods - - } -} \ No newline at end of file diff --git a/Src/DDD.Core/Infrastructure/Data/DbScriptSplitter.cs b/Src/DDD.Core/Infrastructure/Data/DbScriptSplitter.cs index 8cfa091..e0aae7a 100644 --- a/Src/DDD.Core/Infrastructure/Data/DbScriptSplitter.cs +++ b/Src/DDD.Core/Infrastructure/Data/DbScriptSplitter.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System; using System.Collections.Generic; using System.Linq; @@ -16,14 +16,14 @@ public class DbScriptSplitter public IEnumerable Split(string script, string batchSeparator = "GO", bool removeComments = true) { - Condition.Requires(nameof(script), script).IsNotNullOrWhiteSpace(); - Condition.Requires(nameof(batchSeparator), batchSeparator).IsNotNullOrWhiteSpace(); + Ensure.That(nameof(script), script).IsNotNullOrWhiteSpace(); + Ensure.That(nameof(batchSeparator), batchSeparator).IsNotNullOrWhiteSpace(); if (removeComments) script = RemoveComments(script); var commands = Regex.Split(script, $@"^\s*({batchSeparator}[ \t]+[0-9]+|{batchSeparator})(?:\s+|$)", RegexOptions.IgnoreCase | RegexOptions.Multiline, - TimeSpan.FromMilliseconds(1000.0)); + TimeSpan.FromMilliseconds(5000.0)); return commands.Where(c => !IsSeparatorOrEmpty(c, batchSeparator)); } @@ -38,7 +38,7 @@ private static string RemoveComments(string script) @"(--[^\r?\n]*)|(/\*[\w\W]*?(?=\*/)\*/)", string.Empty, RegexOptions.Multiline, - TimeSpan.FromMilliseconds(1000.0)); + TimeSpan.FromMilliseconds(5000.0)); } #endregion Methods diff --git a/Src/DDD.Core/Infrastructure/Data/DbStandardExpressions.cs b/Src/DDD.Core/Infrastructure/Data/DbStandardExpressions.cs index c068f6c..193f8c6 100644 --- a/Src/DDD.Core/Infrastructure/Data/DbStandardExpressions.cs +++ b/Src/DDD.Core/Infrastructure/Data/DbStandardExpressions.cs @@ -7,6 +7,14 @@ public abstract class DbStandardExpressions : IDbStandardExpressions { + #region Fields + + public readonly static IDbStandardExpressions Oracle11 = new Oracle11Expressions(); + + public readonly static IDbStandardExpressions SqlServer2012 = new SqlServer2012Expressions(); + + #endregion Fields + #region Methods public virtual string FromDummy() => string.Empty; diff --git a/Src/DDD.Core/Infrastructure/Data/DbToCommandExceptionTranslator.cs b/Src/DDD.Core/Infrastructure/Data/DbToCommandExceptionTranslator.cs new file mode 100644 index 0000000..344cee3 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/DbToCommandExceptionTranslator.cs @@ -0,0 +1,50 @@ +using System.Data.Common; +using System.Collections.Generic; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Mapping; + using Collections; + using Application; + + public class DbToCommandExceptionTranslator : ObjectTranslator + { + + #region Fields + + private readonly Dictionary> translators = new Dictionary>(); + + #endregion Fields + + #region Constructors + + public DbToCommandExceptionTranslator() + { + this.translators.Add("System.Data.SqlClient.SqlException", new SqlServerToCommandExceptionTranslator()); + this.translators.Add("Microsoft.Data.SqlClient.SqlException", new SqlServerToCommandExceptionTranslator()); + this.translators.Add("Oracle.ManagedDataAccess.Client.OracleException", new OracleToCommandExceptionTranslator()); + } + + #endregion Constructors + + #region Methods + + public override CommandException Translate(DbException exception, IMappingContext context) + { + Ensure.That(exception, nameof(exception)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + var exceptionType = exception.GetType().FullName; + if (this.translators.TryGetValue(exceptionType, out var translator)) + return translator.Translate(exception, context); + else + { + context.TryGetValue("Command", out ICommand command); + return new CommandException(isTransient: false, command, exception); + } + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Infrastructure/Data/DbToQueryExceptionTranslator.cs b/Src/DDD.Core/Infrastructure/Data/DbToQueryExceptionTranslator.cs new file mode 100644 index 0000000..fa3509c --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/DbToQueryExceptionTranslator.cs @@ -0,0 +1,50 @@ +using System.Data.Common; +using System.Collections.Generic; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Mapping; + using Collections; + using Application; + + public class DbToQueryExceptionTranslator : ObjectTranslator + { + + #region Fields + + private readonly Dictionary> translators = new Dictionary>(); + + #endregion Fields + + #region Constructors + + public DbToQueryExceptionTranslator() + { + this.translators.Add("System.Data.SqlClient.SqlException", new SqlServerToQueryExceptionTranslator()); + this.translators.Add("Microsoft.Data.SqlClient.SqlException", new SqlServerToQueryExceptionTranslator()); + this.translators.Add("Oracle.ManagedDataAccess.Client.OracleException", new OracleToQueryExceptionTranslator()); + } + + #endregion Constructors + + #region Methods + + public override QueryException Translate(DbException exception, IMappingContext context) + { + Ensure.That(exception, nameof(exception)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + var exceptionType = exception.GetType().FullName; + if (this.translators.TryGetValue(exceptionType, out var translator)) + return translator.Translate(exception, context); + else + { + context.TryGetValue("Query", out IQuery query); + return new QueryException(isTransient: false, query, exception); + } + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Infrastructure/Data/DbToRepositoryExceptionTranslator.cs b/Src/DDD.Core/Infrastructure/Data/DbToRepositoryExceptionTranslator.cs new file mode 100644 index 0000000..4d2883e --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/DbToRepositoryExceptionTranslator.cs @@ -0,0 +1,52 @@ +using System.Data.Common; +using System.Collections.Generic; +using System; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Mapping; + using Collections; + using Domain; + + public class DbToRepositoryExceptionTranslator : ObjectTranslator + { + + #region Fields + + private readonly Dictionary> translators = new Dictionary>(); + + #endregion Fields + + #region Constructors + + public DbToRepositoryExceptionTranslator() + { + this.translators.Add("System.Data.SqlClient.SqlException", new SqlServerToRepositoryExceptionTranslator()); + this.translators.Add("Microsoft.Data.SqlClient.SqlException", new SqlServerToRepositoryExceptionTranslator()); + this.translators.Add("Oracle.ManagedDataAccess.Client.OracleException", new OracleToRepositoryExceptionTranslator()); + } + + #endregion Constructors + + #region Methods + + public override RepositoryException Translate(DbException exception, IMappingContext context) + { + Ensure.That(exception, nameof(exception)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + var exceptionType = exception.GetType().FullName; + if (this.translators.TryGetValue(exceptionType, out var translator)) + return translator.Translate(exception, context); + else + { + context.TryGetValue("EntityType", out Type entityType); + var outerException = context.ContainsKey("OuterException") ? (Exception)context["OuterException"] : exception; + return new RepositoryException(isTransient: false, entityType, outerException); + } + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Infrastructure/Data/GuidGenerator.cs b/Src/DDD.Core/Infrastructure/Data/GuidGenerator.cs new file mode 100644 index 0000000..1453ba2 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/GuidGenerator.cs @@ -0,0 +1,69 @@ +using System; + +namespace DDD.Core.Infrastructure.Data +{ + /// + /// Base class for the generators of Guids. + /// + public abstract class GuidGenerator : IValueGenerator + { + + #region Constructors + + protected GuidGenerator() + { + this.Block1Length = 4; + this.Block2Length = 2; + this.Block3Length = 2; + this.Block4Length = 2; + this.Block5Length = 6; + this.GuidLength = this.Block1Length + this.Block2Length + this.Block3Length + this.Block4Length + this.Block5Length; + } + + #endregion Constructors + + #region Properties + + /// + /// The length of the first block of a UUID (represented as an Int32 in the GUID). + /// + protected int Block1Length { get; } + + /// + /// The length of the second block of a UUID (represented as an Int16 in the GUID). + /// + protected int Block2Length { get; } + + /// + /// The length of the third block of a UUID (represented as an Int16 in the GUID). + /// + protected int Block3Length { get; } + + /// + /// The length of the fourth block of a UUID (represented as bytes in the GUID). + /// + protected int Block4Length { get; } + + /// + /// The length of the fifth block of a UUID (represented as bytes in the GUID). + /// + protected int Block5Length { get; } + + /// + /// The total length of the GUID. + /// + protected int GuidLength { get; } + + #endregion Properties + + #region Methods + + /// + /// Generates Guids. + /// + public abstract Guid Generate(); + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Infrastructure/Data/IDbConnectionExtensions.cs b/Src/DDD.Core/Infrastructure/Data/IDbConnectionExtensions.cs index 7e5f587..8b7441b 100644 --- a/Src/DDD.Core/Infrastructure/Data/IDbConnectionExtensions.cs +++ b/Src/DDD.Core/Infrastructure/Data/IDbConnectionExtensions.cs @@ -1,4 +1,5 @@ -using System; +using EnsureThat; +using System; using System.Data; namespace DDD.Core.Infrastructure.Data @@ -8,23 +9,57 @@ namespace DDD.Core.Infrastructure.Data /// public static class IDbConnectionExtensions { + #region Methods public static IDbStandardExpressions Expressions(this IDbConnection connection) { + Ensure.That(connection, nameof(connection)).IsNotNull(); switch (connection.GetType().ToString()) { case "System.Data.SqlClient.SqlConnection": - return new SqlServer2012Expressions(); - + case "Microsoft.Data.SqlClient.SqlConnection": + return DbStandardExpressions.SqlServer2012; case "Oracle.ManagedDataAccess.Client.OracleConnection": - return new Oracle11Expressions(); + return DbStandardExpressions.Oracle11; default: throw new ArgumentException($"Connection type '{connection.GetType().Name}' not expected.", "connection"); } } + public static bool HasOracleProvider(this IDbConnection connection) + { + Ensure.That(connection, nameof(connection)).IsNotNull(); + switch (connection.GetType().ToString()) + { + case "Oracle.ManagedDataAccess.Client.OracleConnection": + return true; + default: + return false; + } + } + + public static bool HasSqlServerProvider(this IDbConnection connection) + { + Ensure.That(connection, nameof(connection)).IsNotNull(); + switch (connection.GetType().ToString()) + { + case "System.Data.SqlClient.SqlConnection": + case "Microsoft.Data.SqlClient.SqlConnection": + return true; + default: + return false; + } + } + public static IValueGenerator SequentialGuidGenerator(this IDbConnection connection) + { + Ensure.That(connection, nameof(connection)).IsNotNull(); + if (connection.HasSqlServerProvider()) return new SequentialSqlServerGuidGenerator(); + return new SequentialBinaryGuidGenerator(); + } + #endregion Methods + } } \ No newline at end of file diff --git a/Src/DDD.Core/Infrastructure/Data/IDbConnectionFactory.cs b/Src/DDD.Core/Infrastructure/Data/IDbConnectionFactory.cs deleted file mode 100644 index 736c49f..0000000 --- a/Src/DDD.Core/Infrastructure/Data/IDbConnectionFactory.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Data.Common; - -namespace DDD.Core.Infrastructure.Data -{ - /// - /// Defines a method that creates a . - /// - public interface IDbConnectionFactory - { - #region Methods - - DbConnection CreateConnection(); - - #endregion Methods - } -} diff --git a/Src/DDD.Core/Infrastructure/Data/IDbConnectionFactoryExtensions.cs b/Src/DDD.Core/Infrastructure/Data/IDbConnectionFactoryExtensions.cs deleted file mode 100644 index 6104045..0000000 --- a/Src/DDD.Core/Infrastructure/Data/IDbConnectionFactoryExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Data.Common; -using System.Threading.Tasks; -using Conditions; - -namespace DDD.Core.Infrastructure.Data -{ - using Threading; - - public static class IDbConnectionFactoryExtensions - { - - #region Methods - - public static DbConnection CreateOpenConnection(this IDbConnectionFactory factory) - { - Condition.Requires(factory, nameof(factory)).IsNotNull(); - var connection = factory.CreateConnection(); - connection.Open(); - return connection; - } - - public static async Task CreateOpenConnectionAsync(this IDbConnectionFactory factory) - { - Condition.Requires(factory, nameof(factory)).IsNotNull(); - await new SynchronizationContextRemover(); - var connection = factory.CreateConnection(); - await connection.OpenAsync(); - return connection; - } - - #endregion Methods - - } -} diff --git a/Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider.cs b/Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider.cs new file mode 100644 index 0000000..dde13b8 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider.cs @@ -0,0 +1,34 @@ +using System; +using System.Data.Common; + +namespace DDD.Core.Infrastructure.Data +{ + using Domain; + + /// + /// Provides and shares a database connection for a specific bounded context between different components. + /// This component owns the connection and is responsible for disposing the connection. + /// + /// + /// When you use TransactionScope to define the scope of a business transaction, you must use a single connection to avoid escalating local transactions automatically to distributed transaction managed by the Microsoft DTC. + /// This connection must be also opened only once until it is disposed. + /// + public interface IDbConnectionProvider : IDisposable + { + + #region Properties + + /// + /// The bounded context associated with the database connection. + /// + BoundedContext Context { get; } + + /// + /// The shared connection. + /// + DbConnection Connection { get; } + + #endregion Properties + + } +} diff --git a/Src/DDD.Core/Infrastructure/Data/IDbConnectionProviderExtensions.cs b/Src/DDD.Core/Infrastructure/Data/IDbConnectionProviderExtensions.cs new file mode 100644 index 0000000..10929c7 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/IDbConnectionProviderExtensions.cs @@ -0,0 +1,63 @@ +using EnsureThat; +using System.Data.Common; +using DDD.Data; +using System.Threading.Tasks; +using System.Threading; + +namespace DDD.Core.Infrastructure.Data +{ + using Threading; + + public static class IDbConnectionProviderExtensions + { + + #region Methods + + /// + /// Opens the shared connection if closed and returns the connection. + /// + public static DbConnection GetOpenConnection(this IDbConnectionProvider connectionProvider) + { + Ensure.That(connectionProvider, nameof(connectionProvider)).IsNotNull(); + connectionProvider.Connection.OpenIfClosed(); + return connectionProvider.Connection; + } + + /// + /// Opens the shared connection if closed. + /// + public static void OpenConnectionIfClosed(this IDbConnectionProvider connectionProvider) + { + Ensure.That(connectionProvider, nameof(connectionProvider)).IsNotNull(); + connectionProvider.Connection.OpenIfClosed(); + } + + /// + /// Opens asynchronously the shared connection if closed. + /// + public static async Task OpenConnectionIfClosedAsync(this IDbConnectionProvider connectionProvider, + CancellationToken cancellationToken = default) + { + Ensure.That(connectionProvider, nameof(connectionProvider)).IsNotNull(); + await new SynchronizationContextRemover(); + await connectionProvider.Connection.OpenIfClosedAsync(cancellationToken); + } + + /// + /// Opens asynchronously the shared connection if closed and returns the connection. + /// + public async static Task GetOpenConnectionAsync(this IDbConnectionProvider connectionProvider, + CancellationToken cancellationToken = default) + { + Ensure.That(connectionProvider, nameof(connectionProvider)).IsNotNull(); + await new SynchronizationContextRemover(); + await connectionProvider.Connection.OpenIfClosedAsync(cancellationToken); + return connectionProvider.Connection; + } + + #endregion Methods + + } + + +} diff --git a/Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider`1.cs b/Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider`1.cs new file mode 100644 index 0000000..23859b3 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider`1.cs @@ -0,0 +1,27 @@ +namespace DDD.Core.Infrastructure.Data +{ + using Domain; + + /// + /// Provides and shares a database connection for a specific bounded context between different components. + /// This component owns the connection and is responsible for disposing the connection. + /// + /// + /// When you use TransactionScope to define the scope of a business transaction, you must use a single connection to avoid escalating local transactions automatically to distributed transaction managed by the Microsoft DTC. + /// This connection must be also opened only once until it is disposed. + /// + public interface IDbConnectionProvider + : IDbConnectionProvider where TContext : BoundedContext + { + + #region Properties + + /// + /// The bounded context associated with the database connection. + /// + new TContext Context { get; } + + #endregion Properties + + } +} diff --git a/Src/DDD.Core/Infrastructure/Data/IValueGenerator.cs b/Src/DDD.Core/Infrastructure/Data/IValueGenerator.cs new file mode 100644 index 0000000..2735912 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/IValueGenerator.cs @@ -0,0 +1,18 @@ +namespace DDD.Core.Infrastructure.Data +{ + /// + /// Defines a generator of data values of a particular type. + /// + /// The type of data values. + public interface IValueGenerator + { + #region Methods + + /// + /// Generates data values. + /// + TValue Generate(); + + #endregion Methods + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Infrastructure/Data/LazyDbConnectionProvider.cs b/Src/DDD.Core/Infrastructure/Data/LazyDbConnectionProvider.cs new file mode 100644 index 0000000..a0da117 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/LazyDbConnectionProvider.cs @@ -0,0 +1,77 @@ +using EnsureThat; +using System.Data.Common; + +using System; + +namespace DDD.Core.Infrastructure.Data +{ + using Domain; + + public class LazyDbConnectionProvider + : IDbConnectionProvider where TContext : BoundedContext + { + + #region Fields + + private readonly DbConnectionSettings settings; + private readonly Lazy lazyConnection; + private bool isDisposed; + + #endregion Fields + + #region Constructors + + public LazyDbConnectionProvider(TContext context, DbConnectionSettings settings) + { + Ensure.That(context, nameof(context)).IsNotNull(); + Ensure.That(settings, nameof(settings)).IsNotNull(); + this.Context = context; + this.settings = settings; + this.lazyConnection = new Lazy(() => this.CreateConnection()); + } + + #endregion Constructors + + #region Properties + + public DbConnection Connection => this.lazyConnection.Value; + + public TContext Context { get; } + + BoundedContext IDbConnectionProvider.Context => this.Context; + + #endregion Properties + + #region Methods + + public void Dispose() + { + Dispose(isDisposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool isDisposing) + { + if (!isDisposed) + { + if (isDisposing) + { + if (lazyConnection.IsValueCreated) + lazyConnection.Value.Dispose(); + } + isDisposed = true; + } + } + + private DbConnection CreateConnection() + { + var providerFactory = DbProviderFactories.GetFactory(this.settings.ProviderName); + var connection = providerFactory.CreateConnection(); + connection.ConnectionString = this.settings.ConnectionString; + return connection; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Infrastructure/Data/Oracle11Expressions.cs b/Src/DDD.Core/Infrastructure/Data/Oracle11Expressions.cs index db073a1..8c01c3a 100644 --- a/Src/DDD.Core/Infrastructure/Data/Oracle11Expressions.cs +++ b/Src/DDD.Core/Infrastructure/Data/Oracle11Expressions.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; namespace DDD.Core.Infrastructure.Data { @@ -18,7 +18,7 @@ public override string FromDummy() public override string NextValue(string sequence, string schema = null) { - Condition.Requires(sequence, nameof(sequence)).IsNotNullOrWhiteSpace(); + Ensure.That(sequence, nameof(sequence)).IsNotNullOrWhiteSpace(); string expression = string.Empty; if (!string.IsNullOrWhiteSpace(schema)) expression += $"{schema}."; diff --git a/Src/DDD.Core/Infrastructure/Data/OracleErrorHelper.cs b/Src/DDD.Core/Infrastructure/Data/OracleErrorHelper.cs new file mode 100644 index 0000000..15419db --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/OracleErrorHelper.cs @@ -0,0 +1,60 @@ +using System; + +namespace DDD.Core.Infrastructure.Data +{ + /// + /// To Improve. Use dynamic type to avoid to add a dependency on the Oracle library. + /// + internal class OracleErrorHelper + { + #region Methods + + public static bool IsUnavailableError(dynamic error) + { + // Ensure.That(error, nameof(error)).IsNotNull() does not work with dynamic + if (error == null) throw new ArgumentNullException(nameof(error)); + return (dynamic)error.Number switch + { + // Oracle Error Code: 3114 + // not connected to ORACLE + 3114 => true, + _ => false, + }; + } + + public static bool IsUnauthorizedError(dynamic error) + { + if (error == null) throw new ArgumentNullException(nameof(error)); + return (dynamic)error.Number switch + { + // Oracle Error Code: 1017 + // invalid username/password; logon denied + 4060 => true, + _ => false, + }; + } + + public static bool IsTimeoutError(dynamic error) + { + if (error == null) throw new ArgumentNullException(nameof(error)); + return (dynamic)error.Number switch + { + // Oracle Error Code: 1013 + // user requested cancel of current operation + 1013 => true, + _ => false, + }; + } + + public static bool IsConflictError(dynamic error) + { + if (error == null) throw new ArgumentNullException(nameof(error)); + return (dynamic)error.Number switch + { + _ => false, + }; + } + + #endregion Methods + } +} diff --git a/Src/DDD.Core/Infrastructure/Data/OracleToCommandExceptionTranslator.cs b/Src/DDD.Core/Infrastructure/Data/OracleToCommandExceptionTranslator.cs new file mode 100644 index 0000000..79d4ce5 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/OracleToCommandExceptionTranslator.cs @@ -0,0 +1,42 @@ +using System.Data.Common; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Mapping; + using Application; + using Collections; + + /// + /// Use dynamic type to avoid to add a dependency on the Oracle library. + /// + internal class OracleToCommandExceptionTranslator : ObjectTranslator + { + #region Methods + + public override CommandException Translate(DbException exception, IMappingContext context) + { + Ensure.That(exception, nameof(exception)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + context.TryGetValue("Command", out ICommand command); + dynamic oracleException = exception; + foreach (dynamic error in oracleException.Errors) + { + if (OracleErrorHelper.IsUnavailableError(error)) + return new CommandUnavailableException(command, exception); + + if (OracleErrorHelper.IsUnauthorizedError(error)) + return new CommandUnauthorizedException(command, exception); + + if (OracleErrorHelper.IsTimeoutError(error)) + return new CommandTimeoutException(command, exception); + + if (OracleErrorHelper.IsConflictError(error)) + return new CommandConflictException(command, exception); + } + return new CommandException(isTransient: false, command, exception); + } + + #endregion Methods + } +} diff --git a/Src/DDD.Core/Infrastructure/Data/OracleToQueryExceptionTranslator.cs b/Src/DDD.Core/Infrastructure/Data/OracleToQueryExceptionTranslator.cs new file mode 100644 index 0000000..e2e17a9 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/OracleToQueryExceptionTranslator.cs @@ -0,0 +1,42 @@ +using System.Data.Common; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Mapping; + using Collections; + using Application; + + /// + /// Use dynamic type to avoid to add a dependency on the Oracle library. + /// + internal class OracleToQueryExceptionTranslator : ObjectTranslator + { + #region Methods + + public override QueryException Translate(DbException exception, IMappingContext context) + { + Ensure.That(exception, nameof(exception)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + context.TryGetValue("Query", out IQuery query); + dynamic oracleException = exception; + foreach (dynamic error in oracleException.Errors) + { + if (OracleErrorHelper.IsUnavailableError(error)) + return new QueryUnavailableException(query, exception); + + if (OracleErrorHelper.IsUnauthorizedError(error)) + return new QueryUnauthorizedException(query, exception); + + if (OracleErrorHelper.IsTimeoutError(error)) + return new QueryTimeoutException(query, exception); + + if (OracleErrorHelper.IsConflictError(error)) + return new QueryConflictException(query, exception); + } + return new QueryException(isTransient: false, query, exception); + } + + #endregion Methods + } +} diff --git a/Src/DDD.Core/Infrastructure/Data/OracleToRepositoryExceptionTranslator.cs b/Src/DDD.Core/Infrastructure/Data/OracleToRepositoryExceptionTranslator.cs new file mode 100644 index 0000000..2f6ecf2 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/OracleToRepositoryExceptionTranslator.cs @@ -0,0 +1,44 @@ +using System.Data.Common; +using System; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Mapping; + using Collections; + using Domain; + + /// + /// Use dynamic type to avoid to add a dependency on the Oracle library. + /// + internal class OracleToRepositoryExceptionTranslator : ObjectTranslator + { + #region Methods + + public override RepositoryException Translate(DbException exception, IMappingContext context) + { + Ensure.That(exception, nameof(exception)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + context.TryGetValue("EntityType", out Type entityType); + var outerException = context.ContainsKey("OuterException") ? (Exception)context["OuterException"] : exception; + dynamic oracleException = exception; + foreach (dynamic error in oracleException.Errors) + { + if (OracleErrorHelper.IsUnavailableError(error)) + return new RepositoryUnavailableException(entityType, outerException); + + if (OracleErrorHelper.IsUnauthorizedError(error)) + return new RepositoryUnauthorizedException(entityType, outerException); + + if (OracleErrorHelper.IsTimeoutError(error)) + return new RepositoryTimeoutException(entityType, outerException); + + if (OracleErrorHelper.IsConflictError(error)) + return new RepositoryConflictException(entityType, outerException); + } + return new RepositoryException(isTransient: false, entityType, outerException); + } + + #endregion Methods + } +} diff --git a/Src/DDD.Core/Infrastructure/Data/RandomGuidGenerator.cs b/Src/DDD.Core/Infrastructure/Data/RandomGuidGenerator.cs new file mode 100644 index 0000000..ea4b30c --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/RandomGuidGenerator.cs @@ -0,0 +1,15 @@ +using System; + +namespace DDD.Core.Infrastructure.Data +{ + public class RandomGuidGenerator : GuidGenerator + { + + #region Methods + + public override Guid Generate() => Guid.NewGuid(); + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Infrastructure/Data/SequentialBinaryGuidGenerator.cs b/Src/DDD.Core/Infrastructure/Data/SequentialBinaryGuidGenerator.cs new file mode 100644 index 0000000..defacb1 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/SequentialBinaryGuidGenerator.cs @@ -0,0 +1,82 @@ +using System; +using System.Security.Cryptography; + +namespace DDD.Core.Infrastructure.Data +{ + /// + /// Generates sequential Guids for binary data types by combining a sequential time-based part with a random part. + /// + /// + /// Based on GUIDs as fast primary keys under multiple databases + /// + public class SequentialBinaryGuidGenerator : GuidGenerator + { + + #region Constructors + + public SequentialBinaryGuidGenerator() + { + this.SequentialLength = 8; + this.RandomLength = this.GuidLength - this.SequentialLength; + this.TimestampProvider = new UniqueTimestampProvider(new UniversalTimestampProvider(), TimeSpan.FromTicks(1)); + } + + #endregion Constructors + + #region Properties + + protected int RandomLength { get; } + protected int SequentialLength { get; } + protected ITimestampProvider TimestampProvider { get; } + + #endregion Properties + + #region Methods + + public sealed override Guid Generate() + { + var sequentialBytes = this.GenerateSequentialBytes(); + var randomBytes = this.GenerateRandomBytes(); + var guidBytes = this.CombineBytes(sequentialBytes, randomBytes); + return new Guid(guidBytes); + } + + /// + /// Combines the sequential part with the random part of the GUID. + /// + protected virtual byte[] CombineBytes(byte[] sequentialBytes, byte[] randomBytes) + { + var guidBytes = new byte[this.GuidLength]; + sequentialBytes.CopyTo(guidBytes, 0); + randomBytes.CopyTo(guidBytes, this.SequentialLength); + return guidBytes; + } + + /// + /// Generates the random part of the GUID. + /// + protected virtual byte[] GenerateRandomBytes() + { + var randomBytes = new byte[this.RandomLength]; + using (var provider = new RNGCryptoServiceProvider()) + { + provider.GetBytes(randomBytes); + } + return randomBytes; + } + + /// + /// Generates the sequential part of the GUID with the most significant bits first. + /// + protected virtual byte[] GenerateSequentialBytes() + { + var timestamp = this.TimestampProvider.GetTimestamp().Ticks; + var sequentialBytes = BitConverter.GetBytes(timestamp); + if (BitConverter.IsLittleEndian) Array.Reverse(sequentialBytes); + return sequentialBytes; + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Infrastructure/Data/SequentialSqlServerGuidGenerator.cs b/Src/DDD.Core/Infrastructure/Data/SequentialSqlServerGuidGenerator.cs new file mode 100644 index 0000000..91bbaca --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/SequentialSqlServerGuidGenerator.cs @@ -0,0 +1,34 @@ +namespace DDD.Core.Infrastructure.Data +{ + /// + /// Generates sequential Guids for the Microsoft Sql Server uniqueidentifier data type by combining a sequential time-based part with a random part. + /// + /// + /// The comparison is made by looking at byte "groups" right-to-left, and left-to-right within a byte "group". + /// A byte group is what is delimited by the '-' character. + /// More technically, we look at bytes {10 to 15} first, then {8-9}, then {6-7}, then {4-5}, and lastly {0 to 3}. + /// + public class SequentialSqlServerGuidGenerator : SequentialBinaryGuidGenerator + { + #region Methods + + protected override byte[] CombineBytes(byte[] sequentialBytes, byte[] randomBytes) + { + var guidBytes = new byte[this.GuidLength]; + randomBytes.CopyTo(guidBytes, 0); + // Block4 with least significant bytes + guidBytes[08] = sequentialBytes[6]; + guidBytes[09] = sequentialBytes[7]; + // Block5 with most significant bytes + guidBytes[10] = sequentialBytes[0]; + guidBytes[11] = sequentialBytes[1]; + guidBytes[12] = sequentialBytes[2]; + guidBytes[13] = sequentialBytes[3]; + guidBytes[14] = sequentialBytes[4]; + guidBytes[15] = sequentialBytes[5]; + return guidBytes; + } + + #endregion Methods + } +} diff --git a/Src/DDD.Core/Infrastructure/Data/SequentialStringGuidGenerator.cs b/Src/DDD.Core/Infrastructure/Data/SequentialStringGuidGenerator.cs new file mode 100644 index 0000000..03e753e --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/SequentialStringGuidGenerator.cs @@ -0,0 +1,29 @@ +using System; + +namespace DDD.Core.Infrastructure.Data +{ + /// + /// Generates sequential Guids for string data types by combining a sequential time-based part with a random part. + /// + public class SequentialStringGuidGenerator : SequentialBinaryGuidGenerator + { + + #region Methods + + protected override byte[] CombineBytes(byte[] sequentialBytes, byte[] randomBytes) + { + var guidBytes = base.CombineBytes(sequentialBytes, randomBytes); + if (BitConverter.IsLittleEndian) + { + // Reverse the data blocks represented as integers in the GUID + Array.Reverse(guidBytes, 0, this.Block1Length); + Array.Reverse(guidBytes, this.Block1Length, this.Block2Length); + Array.Reverse(guidBytes, this.Block1Length + this.Block2Length, this.Block3Length); + } + return guidBytes; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Infrastructure/Data/SqlServer2012Expressions.cs b/Src/DDD.Core/Infrastructure/Data/SqlServer2012Expressions.cs index 1b387c6..8f89cb8 100644 --- a/Src/DDD.Core/Infrastructure/Data/SqlServer2012Expressions.cs +++ b/Src/DDD.Core/Infrastructure/Data/SqlServer2012Expressions.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; namespace DDD.Core.Infrastructure.Data { @@ -12,7 +12,7 @@ public class SqlServer2012Expressions : DbStandardExpressions public override string NextValue(string sequence, string schema = null) { - Condition.Requires(sequence, nameof(sequence)).IsNotNullOrWhiteSpace(); + Ensure.That(sequence, nameof(sequence)).IsNotNullOrWhiteSpace(); var expression = "NEXT VALUE FOR "; if (!string.IsNullOrWhiteSpace(schema)) expression += $"{schema}."; diff --git a/Src/DDD.Core/Infrastructure/Data/SqlServerErrorHelper.cs b/Src/DDD.Core/Infrastructure/Data/SqlServerErrorHelper.cs new file mode 100644 index 0000000..f652b48 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/SqlServerErrorHelper.cs @@ -0,0 +1,134 @@ +using System; + +namespace DDD.Core.Infrastructure.Data +{ + internal static class SqlServerErrorHelper + { + + #region Methods + + public static bool IsUnavailableError(dynamic error) + { + // Ensure.That(error, nameof(error)).IsNotNull() does not work with dynamic + if (error == null) throw new ArgumentNullException(nameof(error)); + switch (error.Number) + { + // SQL Error Code: 40613 + // Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer + // support, and provide them the session tracing ID of ZZZZZ. + case 40613: + // SQL Error Code: 40540 + // The service has encountered an error processing your request. Please try again. + case 40540: + // SQL Error Code: 40501 + // The service is currently busy. Retry the request after 10 seconds. Code: (reason code to be decoded). + case 40501: + // SQL Error Code: 40197 + // The service has encountered an error processing your request. Please try again. + case 40197: + // SQL Error Code: 40143 + // The service has encountered an error processing your request. Please try again. + case 40143: + // SQL Error Code: 10928 + // Resource ID: %d. The %s limit for the database is %d and has been reached. + case 10928: + // SQL Error Code: 10929 + // Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d and the current usage for the database is %d. + // However, the server is currently too busy to support requests greater than %d for this database. + case 10929: + // SQL Error Code: 10060 + // A network-related or instance-specific error occurred while establishing a connection to SQL Server. + // The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server + // is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed + // because the connected party did not properly respond after a period of time, or established connection failed + // because connected host has failed to respond.)"} + case 10060: + // SQL Error Code: 10053 + // A transport-level error has occurred when receiving results from the server. + // An established connection was aborted by the software in your host machine. + case 10053: + // SQL Error Code: 10054 + // A transport-level error has occurred when sending the request to the server. + // (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) + case 10054: + // SQL Error Code: 233 + // The client was unable to establish a connection because of an error during connection initialization process before login. + // Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy + // to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server. + // (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) + case 233: + // SQL Error Code: 64 + // A connection was successfully established with the server, but then an error occurred during the login process. + // (provider: TCP Provider, error: 0 - The specified network name is no longer available.) + case 64: + // DBNETLIB Error Code: 20 + // The instance of SQL Server you attempted to connect to does not support encryption. + case 20: + return true; + default: + return false; + } + } + + public static bool IsUnauthorizedError(dynamic error) + { + if (error == null) throw new ArgumentNullException(nameof(error)); + switch (error.Number) + { + // SQL Error Code: 40532 + // Cannot open server "%.*ls" requested by the login. The login failed. + case 40532: + // SQL Error Code: 18488 + // Login failed for user '%.*ls'. Reason: The password of the account must be changed.%.*ls + case 18488: + // SQL Error Code: 18487 + // Login failed for user '%.*ls'. Reason: The password of the account has expired.%.*ls + case 18487: + // SQL Error Code: 18486 + // Login failed for user '%.*ls' because the account is currently locked out. The system administrator can unlock it. %.*ls + case 18486: + // SQL Error Code: 18470 + // Login failed for user '%.*ls'. Reason: The account is disabled.%.*ls + case 18470: + // SQL Error Code: 18456 + // Login failed for user '%.*ls'.%.*ls%.* + case 18456: + // SQL Error Code: 18452 + // Login failed. The login is from an untrusted domain and cannot be used with Windows authentication.%.*ls + case 18452: + // SQL Error Code: 4060 + // Cannot open database "%.*ls" requested by the login. The login failed. + case 4060: + return true; + default: + return false; + } + } + + public static bool IsTimeoutError(dynamic error) + { + if (error == null) throw new ArgumentNullException(nameof(error)); + switch (error.Number) + { + // DBNETLIB Error Code: -2 + // Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding. + case -2: + return true; + default: + return false; + } + } + + public static bool IsConflictError(dynamic error) + { + if (error == null) throw new ArgumentNullException(nameof(error)); + return error.Number switch + { + _ => false, + }; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Infrastructure/Data/SqlServerToCommandExceptionTranslator.cs b/Src/DDD.Core/Infrastructure/Data/SqlServerToCommandExceptionTranslator.cs new file mode 100644 index 0000000..2afbdea --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/SqlServerToCommandExceptionTranslator.cs @@ -0,0 +1,39 @@ +using System.Data.Common; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Mapping; + using Application; + using Collections; + + internal class SqlServerToCommandExceptionTranslator : ObjectTranslator + { + #region Methods + + public override CommandException Translate(DbException exception, IMappingContext context) + { + Ensure.That(exception, nameof(exception)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + context.TryGetValue("Command", out ICommand command); + dynamic sqlServerException = exception; + foreach (dynamic error in sqlServerException.Errors) + { + if (SqlServerErrorHelper.IsUnavailableError(error)) + return new CommandUnavailableException(command, exception); + + if (SqlServerErrorHelper.IsUnauthorizedError(error)) + return new CommandUnauthorizedException(command, exception); + + if (SqlServerErrorHelper.IsTimeoutError(error)) + return new CommandTimeoutException(command, exception); + + if (SqlServerErrorHelper.IsConflictError(error)) + return new CommandConflictException(command, exception); + } + return new CommandException(isTransient: false, command, exception); + } + + #endregion Methods + } +} diff --git a/Src/DDD.Core/Infrastructure/Data/SqlServerToQueryExceptionTranslator.cs b/Src/DDD.Core/Infrastructure/Data/SqlServerToQueryExceptionTranslator.cs new file mode 100644 index 0000000..0255e2b --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/SqlServerToQueryExceptionTranslator.cs @@ -0,0 +1,39 @@ +using System.Data.Common; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Mapping; + using Collections; + using Application; + + internal class SqlServerToQueryExceptionTranslator : ObjectTranslator + { + #region Methods + + public override QueryException Translate(DbException exception, IMappingContext context) + { + Ensure.That(exception, nameof(exception)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + context.TryGetValue("Query", out IQuery query); + dynamic sqlServerException = exception; + foreach (dynamic error in sqlServerException.Errors) + { + if (SqlServerErrorHelper.IsUnavailableError(error)) + return new QueryUnavailableException(query, exception); + + if (SqlServerErrorHelper.IsUnauthorizedError(error)) + return new QueryUnauthorizedException(query, exception); + + if (SqlServerErrorHelper.IsTimeoutError(error)) + return new QueryTimeoutException(query, exception); + + if (SqlServerErrorHelper.IsConflictError(error)) + return new QueryConflictException(query, exception); + } + return new QueryException(isTransient: false, query, exception); + } + + #endregion Methods + } +} diff --git a/Src/DDD.Core/Infrastructure/Data/SqlServerToRepositoryExceptionTranslator.cs b/Src/DDD.Core/Infrastructure/Data/SqlServerToRepositoryExceptionTranslator.cs new file mode 100644 index 0000000..d1322c3 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/SqlServerToRepositoryExceptionTranslator.cs @@ -0,0 +1,41 @@ +using System.Data.Common; +using EnsureThat; +using System; + +namespace DDD.Core.Infrastructure.Data +{ + using Mapping; + using Collections; + using Domain; + + internal class SqlServerToRepositoryExceptionTranslator : ObjectTranslator + { + #region Methods + + public override RepositoryException Translate(DbException exception, IMappingContext context) + { + Ensure.That(exception, nameof(exception)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + context.TryGetValue("EntityType", out Type entityType); + var outerException = context.ContainsKey("OuterException") ? (Exception)context["OuterException"] : exception; + dynamic sqlServerException = exception; + foreach (dynamic error in sqlServerException.Errors) + { + if (SqlServerErrorHelper.IsUnavailableError(error)) + return new RepositoryUnavailableException(entityType, outerException); + + if (SqlServerErrorHelper.IsUnauthorizedError(error)) + return new RepositoryUnauthorizedException(entityType, outerException); + + if (SqlServerErrorHelper.IsTimeoutError(error)) + return new RepositoryTimeoutException(entityType, outerException); + + if (SqlServerErrorHelper.IsConflictError(error)) + return new RepositoryConflictException(entityType, outerException); + } + return new RepositoryException(isTransient: false, entityType, outerException); + } + + #endregion Methods + } +} diff --git a/Src/DDD.Core/Infrastructure/DependencyInjection/AppRegistrationOptions.cs b/Src/DDD.Core/Infrastructure/DependencyInjection/AppRegistrationOptions.cs new file mode 100644 index 0000000..9d0b043 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/DependencyInjection/AppRegistrationOptions.cs @@ -0,0 +1,191 @@ +using EnsureThat; +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using DDD.Serialization; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Domain; + + public class AppRegistrationOptions + { + + #region Fields + + private readonly List assembliesToScan; + private readonly IDictionary> serializers; + private readonly List dbConnectionOptionsCollection; + + #endregion Fields + + #region Constructors + + private AppRegistrationOptions() + { + this.assembliesToScan = new List(); + this.serializers = new Dictionary>(); + this.dbConnectionOptionsCollection = new List(); + this.loggerFactoryCreator = () => new NullLoggerFactory(); + this.TypeFilter = _ => true; + } + + #endregion Constructors + + #region Properties + + public IEnumerable AssembliesToScan => this.assembliesToScan; + + public Func TypeFilter { get; private set; } + + public Func loggerFactoryCreator { get; private set; } + + public CommandsRegistrationOptions CommandsRegistrationOptions { get; private set; } + + public QueriesRegistrationOptions QueriesRegistrationOptions { get; private set; } + + public EventsRegistrationOptions EventsRegistrationOptions { get; private set; } + + public MappingRegistrationOptions MappingRegistrationOptions { get; private set; } + + public IEnumerable> Serializers => this.serializers.Values; + + public IEnumerable DbConnectionOptionsCollection => this.dbConnectionOptionsCollection; + + #endregion Properties + + #region Classes + + public class Builder : ExtendableRegistrationOptionsBuilder + where TContainer : class + { + + #region Fields + + private readonly AppRegistrationOptions options = new AppRegistrationOptions(); + + #endregion Fields + + #region Methods + + public Builder RegisterTypesFrom(IEnumerable assemblies) + { + Ensure.That(assemblies, nameof(assemblies)).IsNotNull(); + this.options.assembliesToScan.AddRange(assemblies); + return this; + } + + public Builder RegisterTypesFrom(params Assembly[] assemblies) + { + Ensure.That(assemblies, nameof(assemblies)).IsNotNull(); + this.options.assembliesToScan.AddRange(assemblies); + return this; + } + + public Builder FilterTypesWith(Func filter) + { + Ensure.That(filter, nameof(filter)).IsNotNull(); + this.options.TypeFilter = filter; + return this; + } + + public Builder RegisterSerializer(SerializationFormat format, Func serializerCreator) + { + Ensure.That(serializerCreator, nameof(serializerCreator)).IsNotNull(); + this.options.serializers[format] = serializerCreator; + return this; + } + + public Builder ConfigureDbConnectionFor(Action> configureOptions) + where TContext : BoundedContext + { + Ensure.That(configureOptions, nameof(configureOptions)).IsNotNull(); + var builder = new DbConnectionOptions.Builder(); + configureOptions(builder); + options.dbConnectionOptionsCollection.Add(((IObjectBuilder)builder).Build()); + return this; + } + + public Builder ConfigureDbConnections(IEnumerable optionsCollection) + { + Ensure.That(optionsCollection, nameof(optionsCollection)).IsNotNull(); + options.dbConnectionOptionsCollection.AddRange(optionsCollection); + return this; + } + + public Builder ConfigureDbConnections(params DbConnectionOptions[] optionsCollection) + { + Ensure.That(optionsCollection, nameof(optionsCollection)).IsNotNull(); + options.dbConnectionOptionsCollection.AddRange(optionsCollection); + return this; + } + + public Builder ConfigureCommands() + { + return this.ConfigureCommands(_ => { }); + } + + public Builder ConfigureCommands(Action configureOptions) + { + Ensure.That(configureOptions, nameof(configureOptions)).IsNotNull(); + var builder = new CommandsRegistrationOptions.Builder(); + configureOptions(builder); + options.CommandsRegistrationOptions = ((IObjectBuilder)builder).Build(); + return this; + } + + public Builder ConfigureQueries() + { + return this.ConfigureQueries(_ => { }); + } + + public Builder ConfigureQueries(Action configureOptions) + { + Ensure.That(configureOptions, nameof(configureOptions)).IsNotNull(); + var builder = new QueriesRegistrationOptions.Builder(); + configureOptions(builder); + options.QueriesRegistrationOptions = ((IObjectBuilder)builder).Build(); + return this; + } + + public Builder ConfigureEvents(Action configureOptions) + { + Ensure.That(configureOptions, nameof(configureOptions)).IsNotNull(); + var builder = new EventsRegistrationOptions.Builder(); + configureOptions(builder); + options.EventsRegistrationOptions = ((IObjectBuilder)builder).Build(); + return this; + } + + public Builder ConfigureMapping() + { + return this.ConfigureMapping(_ => { }); + } + + public Builder ConfigureMapping(Action configureOptions) + { + Ensure.That(configureOptions, nameof(configureOptions)).IsNotNull(); + var builder = new MappingRegistrationOptions.Builder(); + configureOptions(builder); + options.MappingRegistrationOptions = ((IObjectBuilder)builder).Build(); + return this; + } + + public Builder ConfigureLogging(Func loggerFactoryCreator) + { + Ensure.That(loggerFactoryCreator, nameof(loggerFactoryCreator)).IsNotNull(); + this.options.loggerFactoryCreator = loggerFactoryCreator; + return this; + } + + protected override AppRegistrationOptions Build() => this.options; + + #endregion Methods + } + + #endregion Classes + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Infrastructure/DependencyInjection/CommandsRegistrationOptions.cs b/Src/DDD.Core/Infrastructure/DependencyInjection/CommandsRegistrationOptions.cs new file mode 100644 index 0000000..673c36b --- /dev/null +++ b/Src/DDD.Core/Infrastructure/DependencyInjection/CommandsRegistrationOptions.cs @@ -0,0 +1,113 @@ +using EnsureThat; +using System; +using System.Collections.Generic; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Application; + using Validation; + using Domain; + + public class CommandsRegistrationOptions + { + + #region Fields + + private readonly List managerOptionsCollection; + private readonly IDictionary> scheduleFactories; + + #endregion Fields + + #region Constructors + + private CommandsRegistrationOptions() + { + this.managerOptionsCollection = new List(); + this.scheduleFactories = new Dictionary>(); + } + + #endregion Constructors + + #region Properties + + public IObjectValidator DefaultValidator { get; private set; } + + public IEnumerable ManagerOptionsCollection => this.managerOptionsCollection; + + public IEnumerable> SchedulesFactories => this.scheduleFactories.Values; + + #endregion Properties + + #region Classes + + public class Builder : FluentBuilder + { + + #region Fields + + private readonly CommandsRegistrationOptions options = new CommandsRegistrationOptions(); + + #endregion Fields + + #region Methods + + public Builder ConfigureManagerFor() + where TContext : BoundedContext + { + return this.ConfigureManagerFor(_ => { }); + } + + public Builder ConfigureManagerFor(Action> configureOptions) + where TContext : BoundedContext + { + Ensure.That(configureOptions, nameof(configureOptions)).IsNotNull(); + var builder = new RecurringCommandManagerOptions.Builder(); + configureOptions(builder); + options.managerOptionsCollection.Add(((IObjectBuilder)builder).Build()); + return this; + } + + public Builder ConfigureManagers(IEnumerable optionsCollection) + { + Ensure.That(optionsCollection, nameof(optionsCollection)).IsNotNull(); + options.managerOptionsCollection.AddRange(optionsCollection); + return this; + } + + public Builder ConfigureManagers(params RecurringCommandManagerOptions[] optionsCollection) + { + Ensure.That(optionsCollection, nameof(optionsCollection)).IsNotNull(); + options.managerOptionsCollection.AddRange(optionsCollection); + return this; + } + + public Builder RegisterScheduleFactory(RecurringExpressionFormat format, Func scheduleFactoryCreator) + { + Ensure.That(scheduleFactoryCreator, nameof(scheduleFactoryCreator)).IsNotNull(); + this.options.scheduleFactories[format] = scheduleFactoryCreator; + return this; + } + + public Builder RegisterDefaultSuccessfullyValidator() + { + Func validator = (command, context) + => new ValidationResult(true, command.GetType().Name, new ValidationFailure[] { }); + return this.RegisterDefaultValidator(validator); + } + + public Builder RegisterDefaultValidator(Func validator) + { + Ensure.That(validator, nameof(validator)).IsNotNull(); + this.options.DefaultValidator = DelegatingValidator.Create(validator); + return this; + } + protected override CommandsRegistrationOptions Build() => this.options; + + #endregion Methods + + } + + #endregion Classes + + } +} diff --git a/Src/DDD.Core/Infrastructure/DependencyInjection/DbConnectionOptions.cs b/Src/DDD.Core/Infrastructure/DependencyInjection/DbConnectionOptions.cs new file mode 100644 index 0000000..b4144da --- /dev/null +++ b/Src/DDD.Core/Infrastructure/DependencyInjection/DbConnectionOptions.cs @@ -0,0 +1,94 @@ +using EnsureThat; +using System.Runtime.Serialization; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Domain; + + [DataContract] + public class DbConnectionOptions + { + + #region Constructors + + private DbConnectionOptions(string contextType) + { + this.ContextType = contextType; + } + + /// + /// For serialization + /// + private DbConnectionOptions() { } + + #endregion Constructors + + #region Properties + + /// + /// Gets the type name of the associated context. + /// + [DataMember(Order = 1)] + public string ContextType { get; private set; } + + /// + /// Gets the provider name. + /// + [DataMember(Order = 2)] + public string ProviderName { get; private set; } + + /// + /// Gets the connection string. + /// + [DataMember(Order = 3)] + public string ConnectionString { get; private set; } + + #endregion Properties + + #region Classes + + public class Builder : FluentBuilder + where TContext : BoundedContext + { + + #region Fields + + private readonly DbConnectionOptions options; + + #endregion Fields + + #region Constructors + + public Builder() + { + this.options = new DbConnectionOptions(typeof(TContext).ShortAssemblyQualifiedName()); + } + + #endregion Constructors + + #region Methods + + public Builder SetProviderName(string providerName) + { + Ensure.That(providerName, nameof(providerName)).IsNotNullOrWhiteSpace(); + options.ProviderName = providerName; + return this; + } + + public Builder SetConnectionString(string connectionString) + { + Ensure.That(connectionString, nameof(connectionString)).IsNotNullOrWhiteSpace(); + options.ConnectionString = connectionString; + return this; + } + + protected override DbConnectionOptions Build() => this.options; + + #endregion Methods + + } + + #endregion Classes + + } +} diff --git a/Src/DDD.Core/Infrastructure/DependencyInjection/EventConsumerOptions.cs b/Src/DDD.Core/Infrastructure/DependencyInjection/EventConsumerOptions.cs new file mode 100644 index 0000000..cbbdb88 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/DependencyInjection/EventConsumerOptions.cs @@ -0,0 +1,98 @@ +using EnsureThat; +using System; +using System.Runtime.Serialization; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Domain; + + [DataContract] + public class EventConsumerOptions + { + + #region Constructors + + private EventConsumerOptions(string contextType) + { + this.ContextType = contextType; + } + + /// + /// For serialization + /// + private EventConsumerOptions() { } + + #endregion Constructors + + #region Properties + + /// + /// Gets the type name of the associated context. + /// + [DataMember(Order = 1)] + public string ContextType { get; private set; } + + /// + /// Gets the delay in seconds between two successive consumations. + /// + [DataMember(Order = 2)] + public double ConsumationDelay { get; private set; } + + /// + /// Gets the maximum number of successive consumations. + /// + [DataMember(Order = 3)] + public long? ConsumationMax { get; private set; } + + #endregion Properties + + #region Classes + + public class Builder : FluentBuilder + where TContext : BoundedContext + { + + #region Fields + + private readonly EventConsumerOptions options; + + #endregion Fields + + public Builder() + { + this.options = new EventConsumerOptions(typeof(TContext).ShortAssemblyQualifiedName()); + } + + #region Methods + + public Builder SetConsumationDelayInSeconds(double consumationDelay) + { + Ensure.That(consumationDelay, nameof(consumationDelay)).IsGte(0); + options.ConsumationDelay = consumationDelay; + return this; + } + + public Builder SetConsumationDelay(TimeSpan consumationDelay) + { + Ensure.That(consumationDelay, nameof(consumationDelay)).IsGte(TimeSpan.Zero); + options.ConsumationDelay = consumationDelay.TotalSeconds; + return this; + } + + public Builder SetConsumationMax(long consumationMax) + { + Ensure.That(consumationMax, nameof(consumationMax)).IsGte(0); + options.ConsumationMax = consumationMax; + return this; + } + + protected override EventConsumerOptions Build() => this.options; + + #endregion Methods + + } + + #endregion Classes + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Infrastructure/DependencyInjection/EventsRegistrationOptions.cs b/Src/DDD.Core/Infrastructure/DependencyInjection/EventsRegistrationOptions.cs new file mode 100644 index 0000000..b835b38 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/DependencyInjection/EventsRegistrationOptions.cs @@ -0,0 +1,94 @@ +using EnsureThat; +using System; +using System.Collections.Generic; +using DDD.Serialization; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Domain; + + public class EventsRegistrationOptions + { + + #region Fields + + private readonly List consumerOptionsCollection = new List(); + + #endregion Fields + + #region Constructors + + private EventsRegistrationOptions() { } + + #endregion Constructors + + #region Properties + + public IEnumerable ConsumerOptionsCollection => this.consumerOptionsCollection; + + /// + /// Gets the current serialization format of events. + /// + public SerializationFormat CurrentSerializationFormat { get; private set; } + + #endregion Properties + + #region Classes + + public class Builder : FluentBuilder + { + + #region Fields + + private readonly EventsRegistrationOptions options = new EventsRegistrationOptions(); + + #endregion Fields + + #region Methods + + public Builder ConfigureConsumerFor() + where TContext : BoundedContext + { + return ConfigureConsumerFor(_ => { }); + } + + public Builder ConfigureConsumerFor(Action> configureOptions) + where TContext : BoundedContext + { + Ensure.That(configureOptions, nameof(configureOptions)).IsNotNull(); + var builder = new EventConsumerOptions.Builder(); + configureOptions(builder); + options.consumerOptionsCollection.Add(((IObjectBuilder)builder).Build()); + return this; + } + + public Builder ConfigureConsumers(IEnumerable optionsCollection) + { + Ensure.That(optionsCollection, nameof(optionsCollection)).IsNotNull(); + options.consumerOptionsCollection.AddRange(optionsCollection); + return this; + } + + public Builder ConfigureConsumers(params EventConsumerOptions[] optionsCollection) + { + Ensure.That(optionsCollection, nameof(optionsCollection)).IsNotNull(); + options.consumerOptionsCollection.AddRange(optionsCollection); + return this; + } + + public Builder SetCurrentSerializationFormat(SerializationFormat format) + { + this.options.CurrentSerializationFormat = format; + return this; + } + + protected override EventsRegistrationOptions Build() => this.options; + + #endregion Methods + + } + + #endregion Classes + + } +} diff --git a/Src/DDD.Core/Infrastructure/DependencyInjection/ExtendableRegistrationOptionsBuilder.cs b/Src/DDD.Core/Infrastructure/DependencyInjection/ExtendableRegistrationOptionsBuilder.cs new file mode 100644 index 0000000..5d5b55c --- /dev/null +++ b/Src/DDD.Core/Infrastructure/DependencyInjection/ExtendableRegistrationOptionsBuilder.cs @@ -0,0 +1,34 @@ +using EnsureThat; +using System; +using System.Collections.Generic; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + public abstract class ExtendableRegistrationOptionsBuilder + : FluentBuilder, IExtendableRegistrationOptionsBuilder + where TOptions : class + where TContainer : class + { + #region Fields + + private readonly List> extensions = new List>(); + + #endregion Fields + + #region Methods + + void IExtendableRegistrationOptionsBuilder.AddExtension(Action extension) + { + Ensure.That(extension, nameof(extension)).IsNotNull(); + this.extensions.Add(extension); + } + + void IExtendableRegistrationOptionsBuilder.ApplyExtensions(TContainer container) + { + foreach (var extension in this.extensions) + extension(container); + } + + #endregion Methods + } +} diff --git a/Src/DDD.Core/Infrastructure/DependencyInjection/IExtendableRegistrationOptionsBuilder.cs b/Src/DDD.Core/Infrastructure/DependencyInjection/IExtendableRegistrationOptionsBuilder.cs new file mode 100644 index 0000000..456eaf9 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/DependencyInjection/IExtendableRegistrationOptionsBuilder.cs @@ -0,0 +1,19 @@ +using System; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + public interface IExtendableRegistrationOptionsBuilder : IObjectBuilder + where TOptions: class + where TContainer : class + { + + #region Methods + + void AddExtension(Action extension); + + void ApplyExtensions(TContainer container); + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Infrastructure/DependencyInjection/MappingRegistrationOptions.cs b/Src/DDD.Core/Infrastructure/DependencyInjection/MappingRegistrationOptions.cs new file mode 100644 index 0000000..31b72a2 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/DependencyInjection/MappingRegistrationOptions.cs @@ -0,0 +1,61 @@ +using EnsureThat; +using System; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Mapping; + + public class MappingRegistrationOptions + { + + #region Constructors + + private MappingRegistrationOptions() { } + + #endregion Constructors + + #region Properties + + public IObjectMapper DefaultMapper { get; private set; } + + public IObjectTranslator DefaultTranslator { get; private set; } + + #endregion Properties + + #region Classes + + public class Builder : FluentBuilder + { + + #region Fields + + private readonly MappingRegistrationOptions options = new MappingRegistrationOptions(); + + #endregion Fields + + #region Methods + + public Builder RegisterDefaultTranslator(Func Translator) + { + Ensure.That(Translator, nameof(Translator)).IsNotNull(); + this.options.DefaultTranslator = new DelegatingTranslator(Translator); + return this; + } + + public Builder RegisterDefaultMapper(Action mapper) + { + Ensure.That(mapper, nameof(mapper)).IsNotNull(); + this.options.DefaultMapper = new DelegatingMapper(mapper); + return this; + } + + protected override MappingRegistrationOptions Build() => this.options; + + #endregion Methods + + } + + #endregion Classes + + } +} diff --git a/Src/DDD.Core/Infrastructure/DependencyInjection/QueriesRegistrationOptions.cs b/Src/DDD.Core/Infrastructure/DependencyInjection/QueriesRegistrationOptions.cs new file mode 100644 index 0000000..0783615 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/DependencyInjection/QueriesRegistrationOptions.cs @@ -0,0 +1,60 @@ +using EnsureThat; +using System; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Application; + using Validation; + + public class QueriesRegistrationOptions + { + + #region Constructors + + private QueriesRegistrationOptions() { } + + #endregion Constructors + + #region Properties + + public IObjectValidator DefaultValidator { get; private set; } + + #endregion Properties + + #region Classes + + public class Builder : FluentBuilder + { + + #region Fields + + private readonly QueriesRegistrationOptions options = new QueriesRegistrationOptions(); + + #endregion Fields + + #region Methods + + public Builder RegisterDefaultValidator(Func validator) + { + Ensure.That(validator, nameof(validator)).IsNotNull(); + this.options.DefaultValidator = DelegatingValidator.Create(validator); + return this; + } + + public Builder RegisterDefaultSuccessfullyValidator() + { + Func validator = (query, context) + => new ValidationResult(true, query.GetType().Name, new ValidationFailure[] { }); + return this.RegisterDefaultValidator(validator); + } + + protected override QueriesRegistrationOptions Build() => this.options; + + #endregion Methods + + } + + #endregion Classes + + } +} diff --git a/Src/DDD.Core/Infrastructure/DependencyInjection/RecurringCommandManagerOptions.cs b/Src/DDD.Core/Infrastructure/DependencyInjection/RecurringCommandManagerOptions.cs new file mode 100644 index 0000000..01fe5da --- /dev/null +++ b/Src/DDD.Core/Infrastructure/DependencyInjection/RecurringCommandManagerOptions.cs @@ -0,0 +1,89 @@ +using System.Runtime.Serialization; +using DDD.Serialization; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Domain; + using Application; + + [DataContract] + public class RecurringCommandManagerOptions + { + + #region Constructors + + private RecurringCommandManagerOptions(string contextType) + { + this.ContextType = contextType; + } + + /// + /// For serialization + /// + private RecurringCommandManagerOptions() { } + + #endregion Constructors + + #region Properties + + /// + /// Gets the type name of the associated context. + /// + [DataMember(Order = 1)] + public string ContextType { get; private set; } + + /// + /// Gets the current serialization format of recurring commands. + /// + [DataMember(Order = 2)] + public SerializationFormat CurrentSerializationFormat { get; private set; } + + /// + /// Gets the current recurring expression format. + /// + [DataMember(Order = 3)] + public RecurringExpressionFormat CurrentExpressionFormat { get; private set; } + + #endregion Properties + + #region Classes + + public class Builder : FluentBuilder + where TContext : BoundedContext + { + + #region Fields + + private readonly RecurringCommandManagerOptions options; + + #endregion Fields + + public Builder() + { + this.options = new RecurringCommandManagerOptions(typeof(TContext).ShortAssemblyQualifiedName()); + } + + #region Methods + + public Builder SetCurrentSerializationFormat(SerializationFormat format) + { + this.options.CurrentSerializationFormat = format; + return this; + } + + public Builder SetCurrentExpressionFormat(RecurringExpressionFormat format) + { + this.options.CurrentExpressionFormat = format; + return this; + } + + protected override RecurringCommandManagerOptions Build() => this.options; + + #endregion Methods + + } + + #endregion Classes + + } +} diff --git a/Src/DDD.Core/Infrastructure/InfrastructureException.cs b/Src/DDD.Core/Infrastructure/InfrastructureException.cs deleted file mode 100644 index dc37612..0000000 --- a/Src/DDD.Core/Infrastructure/InfrastructureException.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace DDD.Core.Infrastructure -{ - /// - /// The base class for all exceptions thrown in the infrastructure layer. - /// - public abstract class InfrastructureException : Exception - { - - #region Constructors - - protected InfrastructureException() - : base("An error has occurred in the infrastructure layer.") - { - } - - protected InfrastructureException(string message) : base(message) - { - } - - protected InfrastructureException(string message, Exception innerException) : base(message, innerException) - { - } - - #endregion Constructors - - } -} diff --git a/Src/DDD.Core/Infrastructure/Serialization/DataContractSerializerWrapper.cs b/Src/DDD.Core/Infrastructure/Serialization/DataContractSerializerWrapper.cs index 494ef30..0763039 100644 --- a/Src/DDD.Core/Infrastructure/Serialization/DataContractSerializerWrapper.cs +++ b/Src/DDD.Core/Infrastructure/Serialization/DataContractSerializerWrapper.cs @@ -1,8 +1,10 @@ -using Conditions; +using EnsureThat; +using System; using System.IO; using System.Runtime.Serialization; using System.Text; using System.Xml; +using RuntimeSerializationException = System.Runtime.Serialization.SerializationException; namespace DDD.Core.Infrastructure.Serialization { @@ -21,20 +23,15 @@ public class DataContractSerializerWrapper : IXmlSerializer #region Constructors public DataContractSerializerWrapper() + : this(DefaultWriterSettings(), DefaultReaderSettings()) { - this.writerSettings = new XmlWriterSettings - { - Encoding = XmlSerializationOptions.Encoding, - Indent = XmlSerializationOptions.Indent - }; - this.readerSettings = new XmlReaderSettings(); } - private DataContractSerializerWrapper(XmlWriterSettings writerSettings, - XmlReaderSettings readerSettings) + public DataContractSerializerWrapper(XmlWriterSettings writerSettings, + XmlReaderSettings readerSettings) { - Condition.Requires(writerSettings, nameof(writerSettings)).IsNotNull(); - Condition.Requires(readerSettings, nameof(readerSettings)).IsNotNull(); + Ensure.That(writerSettings, nameof(writerSettings)).IsNotNull(); + Ensure.That(readerSettings, nameof(readerSettings)).IsNotNull(); this.writerSettings = writerSettings; this.readerSettings = readerSettings; } @@ -45,51 +42,73 @@ private DataContractSerializerWrapper(XmlWriterSettings writerSettings, public Encoding Encoding => this.writerSettings.Encoding; + public SerializationFormat Format => SerializationFormat.Xml; + public bool Indent => this.writerSettings.Indent; #endregion Properties #region Methods - public static DataContractSerializerWrapper Create(XmlWriterSettings writerSettings, - XmlReaderSettings readerSettings) - { - return new DataContractSerializerWrapper(writerSettings, readerSettings); - } - public static DataContractSerializerWrapper Create(Encoding encoding, bool indent = true) { - Condition.Requires(encoding, nameof(encoding)).IsNotNull(); - var writerSettings = new XmlWriterSettings - { - Encoding = encoding, - Indent = indent - }; - var readerSettings = new XmlReaderSettings(); + Ensure.That(encoding, nameof(encoding)).IsNotNull(); + var writerSettings = DefaultWriterSettings(); + writerSettings.Encoding = encoding; + writerSettings.Indent = indent; + var readerSettings = DefaultReaderSettings(); return new DataContractSerializerWrapper(writerSettings, readerSettings); } - public T Deserialize(Stream stream) + public static DataContractSerializerWrapper Create(bool indent = true) => Create(XmlSerializationOptions.Encoding, indent); + + public object Deserialize(Stream stream, Type type) { - Condition.Requires(stream, nameof(stream)).IsNotNull(); + Ensure.That(stream, nameof(stream)).IsNotNull(); + Ensure.That(type, nameof(type)).IsNotNull(); using (var reader = XmlReader.Create(stream, this.readerSettings)) { - var serializer = new DataContractSerializer(typeof(T)); - return (T)serializer.ReadObject(reader); + var serializer = new DataContractSerializer(type); + try + { + return serializer.ReadObject(reader); + } + catch (RuntimeSerializationException exception) + { + throw new SerializationException(type, exception); + } } } public void Serialize(Stream stream, object obj) { - Condition.Requires(stream, nameof(stream)).IsNotNull(); - Condition.Requires(obj, nameof(obj)).IsNotNull(); + Ensure.That(stream, nameof(stream)).IsNotNull(); + Ensure.That(obj, nameof(obj)).IsNotNull(); using (var writer = XmlWriter.Create(stream, this.writerSettings)) { var serializer = new DataContractSerializer(obj.GetType()); - serializer.WriteObject(writer, obj); + try + { + serializer.WriteObject(writer, obj); + } + catch (Exception exception) when (exception is RuntimeSerializationException || exception is InvalidDataContractException) + { + throw new SerializationException(obj.GetType(), exception); + } } } + private static XmlReaderSettings DefaultReaderSettings() => new XmlReaderSettings(); + + private static XmlWriterSettings DefaultWriterSettings() + { + return new XmlWriterSettings + { + Encoding = XmlSerializationOptions.Encoding, + Indent = XmlSerializationOptions.Indent + }; + } + #endregion Methods } diff --git a/Src/DDD.Core/Infrastructure/Serialization/XmlSerializerWrapper.cs b/Src/DDD.Core/Infrastructure/Serialization/XmlSerializerWrapper.cs index ebcd2cd..a022011 100644 --- a/Src/DDD.Core/Infrastructure/Serialization/XmlSerializerWrapper.cs +++ b/Src/DDD.Core/Infrastructure/Serialization/XmlSerializerWrapper.cs @@ -1,4 +1,5 @@ -using Conditions; +using EnsureThat; +using System; using System.IO; using System.Text; using System.Xml; @@ -21,20 +22,15 @@ public class XmlSerializerWrapper : IXmlSerializer #region Constructors public XmlSerializerWrapper() + : this(DefaultWriterSettings(), DefaultReaderSettings()) { - this.writerSettings = new XmlWriterSettings - { - Encoding = XmlSerializationOptions.Encoding, - Indent = XmlSerializationOptions.Indent - }; - this.readerSettings = new XmlReaderSettings(); } - private XmlSerializerWrapper(XmlWriterSettings writerSettings, - XmlReaderSettings readerSettings) + public XmlSerializerWrapper(XmlWriterSettings writerSettings, + XmlReaderSettings readerSettings) { - Condition.Requires(writerSettings, nameof(writerSettings)).IsNotNull(); - Condition.Requires(readerSettings, nameof(readerSettings)).IsNotNull(); + Ensure.That(writerSettings, nameof(writerSettings)).IsNotNull(); + Ensure.That(readerSettings, nameof(readerSettings)).IsNotNull(); this.writerSettings = writerSettings; this.readerSettings = readerSettings; } @@ -45,6 +41,8 @@ private XmlSerializerWrapper(XmlWriterSettings writerSettings, public Encoding Encoding => this.writerSettings.Encoding; + public SerializationFormat Format => SerializationFormat.Xml; + public bool Indent => this.writerSettings.Indent; #endregion Properties @@ -53,43 +51,63 @@ private XmlSerializerWrapper(XmlWriterSettings writerSettings, public static XmlSerializerWrapper Create(Encoding encoding, bool indent = true) { - Condition.Requires(encoding, nameof(encoding)).IsNotNull(); - var writerSettings = new XmlWriterSettings - { - Encoding = encoding, - Indent = indent - }; - var readerSettings = new XmlReaderSettings(); + Ensure.That(encoding, nameof(encoding)).IsNotNull(); + var writerSettings = DefaultWriterSettings(); + writerSettings.Encoding = encoding; + writerSettings.Indent = indent; + var readerSettings = DefaultReaderSettings(); return new XmlSerializerWrapper(writerSettings, readerSettings); } - public static XmlSerializerWrapper Create(XmlWriterSettings writerSettings, - XmlReaderSettings readerSettings) - { - return new XmlSerializerWrapper(writerSettings, readerSettings); - } + public static XmlSerializerWrapper Create(bool indent = true) => Create(XmlSerializationOptions.Encoding, indent); - public T Deserialize(Stream stream) + public object Deserialize(Stream stream, Type type) { - Condition.Requires(stream, nameof(stream)).IsNotNull(); + Ensure.That(stream, nameof(stream)).IsNotNull(); + Ensure.That(type, nameof(type)).IsNotNull(); using (var reader = XmlReader.Create(stream, this.readerSettings)) { - var serializer = new XmlSerializer(typeof(T)); - return (T)serializer.Deserialize(reader); + var serializer = new XmlSerializer(type); + try + { + return serializer.Deserialize(reader); + } + catch (InvalidOperationException exception) + { + throw new SerializationException(type, exception); + } } } public void Serialize(Stream stream, object obj) { - Condition.Requires(stream, nameof(stream)).IsNotNull(); - Condition.Requires(obj, nameof(obj)).IsNotNull(); + Ensure.That(stream, nameof(stream)).IsNotNull(); + Ensure.That(obj, nameof(obj)).IsNotNull(); using (var writer = XmlWriter.Create(stream, this.writerSettings)) { var serializer = new XmlSerializer(obj.GetType()); - serializer.Serialize(writer, obj); + try + { + serializer.Serialize(writer, obj); + } + catch (InvalidOperationException exception) + { + throw new SerializationException(obj.GetType(), exception); + } } } + private static XmlReaderSettings DefaultReaderSettings() => new XmlReaderSettings(); + + private static XmlWriterSettings DefaultWriterSettings() + { + return new XmlWriterSettings + { + Encoding = XmlSerializationOptions.Encoding, + Indent = XmlSerializationOptions.Indent + }; + } + #endregion Methods } diff --git a/Src/DDD.Core/packages.config b/Src/DDD.Core/packages.config deleted file mode 100644 index a105d3f..0000000 --- a/Src/DDD.Core/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery.Messages/Domain/Practitioners/HealthcarePractitionerType.cs b/Src/DDD.HealthcareDelivery.Messages/Application/Practitioners/HealthcarePractitionerType.cs similarity index 55% rename from Src/DDD.HealthcareDelivery.Messages/Domain/Practitioners/HealthcarePractitionerType.cs rename to Src/DDD.HealthcareDelivery.Messages/Application/Practitioners/HealthcarePractitionerType.cs index d69373d..4a772c6 100644 --- a/Src/DDD.HealthcareDelivery.Messages/Domain/Practitioners/HealthcarePractitionerType.cs +++ b/Src/DDD.HealthcareDelivery.Messages/Application/Practitioners/HealthcarePractitionerType.cs @@ -1,4 +1,4 @@ -namespace DDD.HealthcareDelivery.Domain.Practitioners +namespace DDD.HealthcareDelivery.Application.Practitioners { public enum HealthcarePractitionerType { diff --git a/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/CreatePharmaceuticalPrescription.cs b/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/CreatePharmaceuticalPrescription.cs index ce4cb68..e12644d 100644 --- a/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/CreatePharmaceuticalPrescription.cs +++ b/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/CreatePharmaceuticalPrescription.cs @@ -5,8 +5,7 @@ namespace DDD.HealthcareDelivery.Application.Prescriptions { using Common.Application; using Core.Application; - using Domain.Facilities; - using Domain.Practitioners; + using Practitioners; /// /// Encapsulates all information needed to create a pharmaceutical prescription. @@ -16,17 +15,11 @@ public class CreatePharmaceuticalPrescription : ICommand #region Properties - public DateTime CreatedOn { get; set; } = DateTime.Now; + public DateTime CreatedOn { get; set; } - public DateTime? DelivrableAt { get; set; } + public DateTime? DeliverableAt { get; set; } - public int FacilityIdentifier { get; set; } - - public string FacilityLicenseNumber { get; set; } - - public string FacilityName { get; set; } - - public HealthFacilityType FacilityType { get; set; } + public int? EncounterIdentifier { get; set; } public string LanguageCode { get; set; } @@ -91,11 +84,11 @@ public class CreatePharmaceuticalPrescription : ICommand public override string ToString() { - var format = $"{0} [prescriptionIdentifier={1}, prescriberIdentifier={2}, patientIdentifier={3}, facilityIdentifier={4}]"; - return string.Format(format, this.GetType().Name, this.PrescriptionIdentifier, this.PrescriberIdentifier, this.PatientIdentifier, this.FacilityIdentifier); + var format = $"{0} [prescriptionIdentifier={1}, prescriberIdentifier={2}, patientIdentifier={3}]"; + return string.Format(format, this.GetType().Name, this.PrescriptionIdentifier, this.PrescriberIdentifier, this.PatientIdentifier); } #endregion Methods } -} +} \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/PharmaceuticalPrescriptionSummary.cs b/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/PharmaceuticalPrescriptionSummary.cs index c3ab06e..a1bb415 100644 --- a/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/PharmaceuticalPrescriptionSummary.cs +++ b/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/PharmaceuticalPrescriptionSummary.cs @@ -9,7 +9,7 @@ public class PharmaceuticalPrescriptionSummary public DateTime CreatedOn { get; set; } - public DateTime? DelivrableAt { get; set; } + public DateTime? DeliverableAt { get; set; } public int Identifier { get; set; } diff --git a/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/PrescribedMedicationDescriptor.cs b/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/PrescribedMedicationDescriptor.cs index 2716314..955a3c8 100644 --- a/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/PrescribedMedicationDescriptor.cs +++ b/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/PrescribedMedicationDescriptor.cs @@ -1,7 +1,5 @@ namespace DDD.HealthcareDelivery.Application.Prescriptions { - using Domain.Prescriptions; - /// /// Encapsulates all information needed to describe a prescribed medication. /// @@ -18,9 +16,7 @@ public class PrescribedMedicationDescriptor public string Posology { get; set; } - public string Quantity { get; set; } - - public string Duration { get; set; } + public byte? Quantity { get; set; } #endregion Properties diff --git a/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/PrescribedMedicationDetails.cs b/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/PrescribedMedicationDetails.cs index 2d3ee83..41ef9cd 100644 --- a/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/PrescribedMedicationDetails.cs +++ b/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/PrescribedMedicationDetails.cs @@ -6,15 +6,13 @@ public class PrescribedMedicationDetails public string Code { get; set; } - public string Duration { get; set; } - public int Identifier { get; set; } public string NameOrDescription { get; set; } public string Posology { get; set; } - public string Quantity { get; set; } + public byte? Quantity { get; set; } #endregion Properties } diff --git a/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PrescribedMedicationType.cs b/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/PrescribedMedicationType.cs similarity index 64% rename from Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PrescribedMedicationType.cs rename to Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/PrescribedMedicationType.cs index 79ad2c3..3268e22 100644 --- a/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PrescribedMedicationType.cs +++ b/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/PrescribedMedicationType.cs @@ -1,4 +1,4 @@ -namespace DDD.HealthcareDelivery.Domain.Prescriptions +namespace DDD.HealthcareDelivery.Application.Prescriptions { public enum PrescribedMedicationType { diff --git a/Src/DDD.HealthcareDelivery.Messages/DDD.HealthcareDelivery.Messages.csproj b/Src/DDD.HealthcareDelivery.Messages/DDD.HealthcareDelivery.Messages.csproj index 3c11438..638359a 100644 --- a/Src/DDD.HealthcareDelivery.Messages/DDD.HealthcareDelivery.Messages.csproj +++ b/Src/DDD.HealthcareDelivery.Messages/DDD.HealthcareDelivery.Messages.csproj @@ -1,75 +1,21 @@ - - - + - Debug - AnyCPU - {B8BB212C-8AFC-4258-A023-EB1F6937F53D} + net48;netstandard2.1 Library - Properties DDD.HealthcareDelivery - DDD.HealthcareDelivery.Messages - v4.7.2 - 512 - true + false - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - L:\packages\Conditions.2.1.0\lib\portable-net45+win+wpa81+wp80+MonoAndroid10+MonoTouch10\Conditions.dll - - - - - Properties\CommonAssemblyInfo.cs - - - - - - - - - - - - - - - - - {40a849c5-c8d7-4f76-856a-138aed73a6c3} - DDD.Common.Messages - - - {2438b31a-3a39-4878-81fa-be5ae715eae5} - DDD.Core.Messages - + + - + + - \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery.Messages/Domain/Facilities/HealthFacilityType.cs b/Src/DDD.HealthcareDelivery.Messages/Domain/Facilities/HealthFacilityType.cs deleted file mode 100644 index 1dd36ca..0000000 --- a/Src/DDD.HealthcareDelivery.Messages/Domain/Facilities/HealthFacilityType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace DDD.HealthcareDelivery.Domain.Facilities -{ - public enum HealthFacilityType - { - MedicalOffice, - Hospital - } -} diff --git a/Src/DDD.HealthcareDelivery.Messages/Domain/HealthcareDeliveryContext.cs b/Src/DDD.HealthcareDelivery.Messages/Domain/HealthcareDeliveryContext.cs new file mode 100644 index 0000000..a99d4f9 --- /dev/null +++ b/Src/DDD.HealthcareDelivery.Messages/Domain/HealthcareDeliveryContext.cs @@ -0,0 +1,15 @@ +namespace DDD.HealthcareDelivery.Domain +{ + using Core.Domain; + + public class HealthcareDeliveryContext : BoundedContext + { + #region Constructors + + public HealthcareDeliveryContext() : base("DLV", "HealthcareDelivery") + { + } + + #endregion Constructors + } +} diff --git a/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionCreated.cs b/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionCreated.cs index 462d60b..a8fb919 100644 --- a/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionCreated.cs +++ b/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionCreated.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System; using System.Runtime.Serialization; @@ -6,17 +6,17 @@ namespace DDD.HealthcareDelivery.Domain.Prescriptions { using Core.Domain; - [DataContract(Namespace = "DDD.HealthcareDelivery.Domain.Prescriptions")] + [DataContract(Namespace = "http://schemas.ddd.com/healthcare-delivery")] public class PharmaceuticalPrescriptionCreated : IDomainEvent { #region Constructors - public PharmaceuticalPrescriptionCreated(int prescriptionIdentifier, DateTime occuredOn) + public PharmaceuticalPrescriptionCreated(int prescriptionIdentifier, DateTime occurredOn) { - Condition.Requires(prescriptionIdentifier, nameof(prescriptionIdentifier)).IsGreaterThan(0); + Ensure.That(prescriptionIdentifier, nameof(prescriptionIdentifier)).IsGt(0); this.PrescriptionIdentifier = prescriptionIdentifier; - this.OccurredOn = occuredOn; + this.OccurredOn = occurredOn; } /// For serialization diff --git a/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionRevoked.cs b/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionRevoked.cs index 7e691a4..c06558f 100644 --- a/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionRevoked.cs +++ b/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionRevoked.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System; using System.Runtime.Serialization; @@ -6,24 +6,19 @@ namespace DDD.HealthcareDelivery.Domain.Prescriptions { using Core.Domain; - [DataContract(Namespace = "DDD.HealthcareDelivery.Domain.Prescriptions")] + [DataContract(Namespace = "http://schemas.ddd.com/healthcare-delivery")] public class PharmaceuticalPrescriptionRevoked : IDomainEvent { #region Constructors - public PharmaceuticalPrescriptionRevoked(int prescriptionIdentifier, DateTime occuredOn, string reason = null) + public PharmaceuticalPrescriptionRevoked(int prescriptionIdentifier, DateTime occurredOn, string reason = null) { - Condition.Requires(prescriptionIdentifier, nameof(prescriptionIdentifier)).IsGreaterThan(0); - Condition.Requires(reason, nameof(reason)).IsNotNullOrWhiteSpace(); + Ensure.That(prescriptionIdentifier, nameof(prescriptionIdentifier)).IsGt(0); + Ensure.That(reason, nameof(reason)).IsNotNullOrWhiteSpace(); this.PrescriptionIdentifier = prescriptionIdentifier; this.Reason = reason; - this.OccurredOn = occuredOn; - } - - public PharmaceuticalPrescriptionRevoked(int prescriptionIdentifier, string reason = null) - : this(prescriptionIdentifier, DateTime.Now, reason) - { + this.OccurredOn = occurredOn; } /// For serialization diff --git a/Src/DDD.HealthcareDelivery.Messages/packages.config b/Src/DDD.HealthcareDelivery.Messages/packages.config deleted file mode 100644 index d106b18..0000000 --- a/Src/DDD.HealthcareDelivery.Messages/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/App.config b/Src/DDD.HealthcareDelivery/App.config deleted file mode 100644 index a3b6280..0000000 --- a/Src/DDD.HealthcareDelivery/App.config +++ /dev/null @@ -1,40 +0,0 @@ - - - - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs b/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs index 24ce439..747070f 100644 --- a/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs +++ b/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs @@ -1,37 +1,38 @@ -using Conditions; +using EnsureThat; using System; -using System.Collections.Generic; using System.Linq; namespace DDD.HealthcareDelivery.Application.Prescriptions { using Common.Domain; - using Domain.Facilities; + using Practitioners; using Domain.Patients; using Domain.Practitioners; using Domain.Prescriptions; + using Domain.Encounters; using Mapping; public class BelgianPharmaceuticalPrescriptionTranslator - : IObjectTranslator + : ObjectTranslator { #region Methods - public PharmaceuticalPrescription Translate(CreatePharmaceuticalPrescription command, - IDictionary options = null) + public override PharmaceuticalPrescription Translate(CreatePharmaceuticalPrescription command, + IMappingContext context) { - Condition.Requires(command, nameof(command)).IsNotNull(); + Ensure.That(command, nameof(command)).IsNotNull(); + Ensure.That(context, nameof(context)); return PharmaceuticalPrescription.Create ( new PrescriptionIdentifier(command.PrescriptionIdentifier), ToPrescriber(command), ToPatient(command), - ToHealthFacility(command), command.Medications.Select(m => ToPrescribedMedication(m)), command.CreatedOn, new Alpha2LanguageCode(command.LanguageCode), - command.DelivrableAt + EncounterIdentifier.CreateIfNotEmpty(command.EncounterIdentifier), + command.DeliverableAt ); } @@ -41,43 +42,10 @@ private static PrescribedPharmaceuticalCompounding ToCompounding(PrescribedMedic ( medication.NameOrDescription, medication.Posology, - medication.Quantity, - medication.Duration - ); - } - - private static HealthFacility ToHealthFacility(CreatePharmaceuticalPrescription command) - { - switch (command.FacilityType) - { - case HealthFacilityType.Hospital: - return ToHospital(command); - case HealthFacilityType.MedicalOffice: - return ToMedicalOffice(command); - default: - throw new ArgumentException($"Health facility type '{command.FacilityType}' not expected.", nameof(command)); - } - } - - private static Hospital ToHospital(CreatePharmaceuticalPrescription command) - { - return new Hospital - ( - command.FacilityIdentifier, - command.FacilityName, - new BelgianHealthFacilityLicenseNumber(command.FacilityLicenseNumber) + medication.Quantity ); } - private static MedicalOffice ToMedicalOffice(CreatePharmaceuticalPrescription command) - { - return new MedicalOffice - ( - command.FacilityIdentifier, - command.FacilityName, - BelgianHealthFacilityLicenseNumber.CreateIfNotEmpty(command.FacilityLicenseNumber) - ); - } private static Patient ToPatient(CreatePharmaceuticalPrescription command) { return new Patient @@ -164,7 +132,6 @@ private static PrescribedPharmaceuticalProduct ToProduct(PrescribedMedicationDes medication.NameOrDescription, medication.Posology, medication.Quantity, - medication.Duration, BelgianMedicationCode.CreateIfNotEmpty(medication.Code) ); } @@ -175,7 +142,6 @@ private static PrescribedPharmaceuticalSubstance ToSubstance(PrescribedMedicatio medication.NameOrDescription, medication.Posology, medication.Quantity, - medication.Duration, BelgianMedicationCode.CreateIfNotEmpty(medication.Code) ); } @@ -183,4 +149,4 @@ private static PrescribedPharmaceuticalSubstance ToSubstance(PrescribedMedicatio #endregion Methods } -} +} \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionCreator.cs b/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionCreator.cs index c1aac94..0ddcfb7 100644 --- a/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionCreator.cs +++ b/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionCreator.cs @@ -1,4 +1,5 @@ -using Conditions; +using EnsureThat; +using System; using System.Threading.Tasks; using System.Transactions; @@ -8,45 +9,73 @@ namespace DDD.HealthcareDelivery.Application.Prescriptions using Core.Domain; using Domain.Prescriptions; using Mapping; + using Threading; - public class PharmaceuticalPrescriptionCreator - : AsyncDomainCommandHandler + public class PharmaceuticalPrescriptionCreator : ICommandHandler { #region Fields - private readonly IEventPublisher publisher; - private readonly IAsyncRepository repository; - private readonly IObjectTranslator translator; + private readonly IRepository repository; + private readonly IObjectTranslator commandTranslator; + private readonly CompositeTranslator exceptionTranslator; #endregion Fields #region Constructors - public PharmaceuticalPrescriptionCreator(IAsyncRepository repository, - IEventPublisher publisher, - IObjectTranslator translator) + public PharmaceuticalPrescriptionCreator(IRepository repository, + IObjectTranslator commandTranslator) { - Condition.Requires(repository, nameof(repository)).IsNotNull(); - Condition.Requires(publisher, nameof(publisher)).IsNotNull(); - Condition.Requires(translator, nameof(translator)).IsNotNull(); + Ensure.That(repository, nameof(repository)).IsNotNull(); + Ensure.That(commandTranslator, nameof(commandTranslator)).IsNotNull(); this.repository = repository; - this.publisher = publisher; - this.translator = translator; + this.commandTranslator = commandTranslator; + this.exceptionTranslator = new CompositeTranslator(); + this.exceptionTranslator.Register(new DomainToCommandExceptionTranslator()); + this.exceptionTranslator.RegisterFallback(); } #endregion Constructors #region Methods - protected override async Task ExecuteAsync(CreatePharmaceuticalPrescription command) + public void Handle(CreatePharmaceuticalPrescription command, IMessageContext context) { - var prescription = this.translator.Translate(command); - using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + Ensure.That(command, nameof(command)).IsNotNull(); + try { - await this.repository.SaveAsync(prescription); - this.publisher.PublishAll(prescription.AllEvents()); - scope.Complete(); + var prescription = this.commandTranslator.Translate(command); + using (var scope = new TransactionScope()) + { + this.repository.Save(prescription); + scope.Complete(); + } + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Command = command }); + } + } + + public async Task HandleAsync(CreatePharmaceuticalPrescription command, IMessageContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + var cancellationToken = context.CancellationToken(); + var prescription = this.commandTranslator.Translate(command); + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + await this.repository.SaveAsync(prescription, cancellationToken); + scope.Complete(); + } + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Command = command }); } } diff --git a/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionRevoker.cs b/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionRevoker.cs index 51c004e..197a508 100644 --- a/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionRevoker.cs +++ b/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionRevoker.cs @@ -1,4 +1,5 @@ -using Conditions; +using EnsureThat; +using System; using System.Threading.Tasks; using System.Transactions; @@ -7,42 +8,74 @@ namespace DDD.HealthcareDelivery.Application.Prescriptions using Core.Application; using Core.Domain; using Domain.Prescriptions; + using Mapping; + using Threading; + public class PharmaceuticalPrescriptionRevoker - : AsyncDomainCommandHandler + : ICommandHandler { #region Fields - private readonly IEventPublisher publisher; - private readonly IAsyncRepository repository; + private readonly IRepository repository; + private readonly CompositeTranslator exceptionTranslator; #endregion Fields #region Constructors - public PharmaceuticalPrescriptionRevoker(IAsyncRepository repository, - IEventPublisher publisher) + public PharmaceuticalPrescriptionRevoker(IRepository repository) { - Condition.Requires(repository, nameof(repository)).IsNotNull(); - Condition.Requires(publisher, nameof(publisher)).IsNotNull(); + Ensure.That(repository, nameof(repository)).IsNotNull(); this.repository = repository; - this.publisher = publisher; + this.exceptionTranslator = new CompositeTranslator(); + this.exceptionTranslator.Register(new DomainToCommandExceptionTranslator()); + this.exceptionTranslator.RegisterFallback(); } #endregion Constructors #region Methods - protected override async Task ExecuteAsync(RevokePharmaceuticalPrescription command) + public void Handle(RevokePharmaceuticalPrescription command, IMessageContext context) { - using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + Ensure.That(command, nameof(command)).IsNotNull(); + try { - var prescription = await this.repository.FindAsync(new PrescriptionIdentifier(command.PrescriptionIdentifier)); - prescription.Revoke(command.RevocationReason); - await this.repository.SaveAsync(prescription); - this.publisher.PublishAll(prescription.AllEvents()); - scope.Complete(); + using (var scope = new TransactionScope()) + { + var prescription = this.repository.Find(new PrescriptionIdentifier(command.PrescriptionIdentifier)); + prescription.Revoke(command.RevocationReason); + this.repository.Save(prescription); + scope.Complete(); + } + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Command = command }); + } + } + + public async Task HandleAsync(RevokePharmaceuticalPrescription command, IMessageContext context) + { + Ensure.That(command, nameof(command)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + var cancellationToken = context.CancellationToken(); + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + var prescription = await this.repository.FindAsync(new PrescriptionIdentifier(command.PrescriptionIdentifier), cancellationToken); + prescription.Revoke(command.RevocationReason); + await this.repository.SaveAsync(prescription, cancellationToken); + scope.Complete(); + } + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Command = command }); } } diff --git a/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj b/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj index 50e61ea..11c3a97 100644 --- a/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj +++ b/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj @@ -1,191 +1,42 @@ - - - + - Debug - AnyCPU - {5B8FFFD3-9A1C-4620-9DB3-CD76CD9E79BF} - Library - Properties - DDD.HealthcareDelivery - DDD.HealthcareDelivery - v4.7.2 - 512 - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\DDD.HealthcareDelivery.xml - 1591 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\DDD.HealthcareDelivery.xml + net48;netstandard2.1 + false + bin\$(Configuration)\ + bin\$(Configuration)\DDD.HealthcareDelivery.xml 1591 - - L:\Packages\Conditions.2.1.0\lib\portable-net45+win+wpa81+wp80+MonoAndroid10+MonoTouch10\Conditions.dll - True - - - L:\Packages\Dapper.1.60.1\lib\net451\Dapper.dll - - - L:\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll - - - L:\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll - - - L:\Packages\FluentValidation.8.1.3\lib\net45\FluentValidation.dll - - - L:\packages\Oracle.ManagedDataAccess.18.3.0\lib\net40\Oracle.ManagedDataAccess.dll - - - - L:\Packages\System.Collections.Immutable.1.5.0\lib\netstandard2.0\System.Collections.Immutable.dll - - - L:\packages\System.ComponentModel.Annotations.4.5.0\lib\net461\System.ComponentModel.Annotations.dll - - - - L:\packages\System.ComponentModel.Primitives.4.3.0\lib\net45\System.ComponentModel.Primitives.dll - True - True - - - - - - - - L:\packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll - - - - - - - Properties\CommonAssemblyInfo.cs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + SqlScripts.resx True True - - - - - + + + + + + + + + + - - {40A849C5-C8D7-4F76-856A-138AED73A6C3} - DDD.Common.Messages - - - {0b70b4fd-f5a0-4a6c-a3fd-90031e08c1c2} - DDD.Common - - - {701da58b-ae36-429f-8621-64109b8d29d7} - DDD.Core.Dapper - - - {6d227aa7-ff90-48ca-b13d-ed23c1fffba5} - DDD.Core.EF - - - {5e3745fc-ca80-4d0f-8a25-20ee0f9cf163} - DDD.Core.FluentValidation - - - {2438B31A-3A39-4878-81FA-BE5AE715EAE5} - DDD.Core.Messages - - - {c6c3e419-b9aa-44ad-9dbf-789294687ae6} - DDD.Core - - - {596a8700-3d18-4a62-b200-1f78a9ea4617} - DDD.Core.Abstractions - - - {b8bb212c-8afc-4258-a023-eb1f6937f53d} - DDD.HealthcareDelivery.Messages - + + + + + + - - + ResXFileCodeGenerator SqlScripts.Designer.cs Designer @@ -194,12 +45,4 @@ - - \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/Domain/Encounters/EncounterIdentifier.cs b/Src/DDD.HealthcareDelivery/Domain/Encounters/EncounterIdentifier.cs new file mode 100644 index 0000000..f8f9a21 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Domain/Encounters/EncounterIdentifier.cs @@ -0,0 +1,34 @@ +using EnsureThat; + +namespace DDD.HealthcareDelivery.Domain.Encounters +{ + using Common.Domain; + + public class EncounterIdentifier : ArbitraryIdentifier + { + + #region Constructors + + public EncounterIdentifier(int value) : base(value) + { + Ensure.That(value, nameof(value)).IsGt(0); + } + + protected EncounterIdentifier() + { + } + + #endregion Constructors + + #region Methods + + public static EncounterIdentifier CreateIfNotEmpty(int? value) + { + if (!value.HasValue) return null; + return new EncounterIdentifier(value.Value); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/Domain/Facilities/BelgianHealthFacilityLicenseNumber.cs b/Src/DDD.HealthcareDelivery/Domain/Facilities/BelgianHealthFacilityLicenseNumber.cs deleted file mode 100644 index 24d8536..0000000 --- a/Src/DDD.HealthcareDelivery/Domain/Facilities/BelgianHealthFacilityLicenseNumber.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Conditions; - -namespace DDD.HealthcareDelivery.Domain.Facilities -{ - /// - /// Represents a license number attributed to Belgian health facilities by the National Institute for Health and Disability Insurance (INAMI/RIZIV). - /// - public class BelgianHealthFacilityLicenseNumber : HealthFacilityLicenseNumber - { - - #region Constructors - - public BelgianHealthFacilityLicenseNumber(string value) : base(value) - { - Condition.Requires(value, nameof(value)) - .Evaluate(n => n.IsNumeric() && (n.Length == 11 || n.Length == 8)); - } - - #endregion Constructors - - #region Methods - - /// - /// Computes the check digit based on the 6 first digits. - /// - public static int ComputeCheckDigit(string value) - { - Condition.Requires(value, nameof(value)).IsLongerOrEqual(6); - var identifier = int.Parse(value.Substring(0, 6)); - var modulus = 97; - return modulus - (identifier % modulus); - } - - public static BelgianHealthFacilityLicenseNumber CreateIfNotEmpty(string value) - { - if (string.IsNullOrWhiteSpace(value)) return null; - return new BelgianHealthFacilityLicenseNumber(value); - } - - /// - /// Returns the check digit based on the 6 first digits. - /// - public int CheckDigit() => int.Parse(this.Value.Substring(6, 2)); - - - /// - /// Returns the unique identifier of the health facility. - /// - public int FacilityUniqueIdentifier() => int.Parse(this.Value.Substring(0, 6)); - - #endregion Methods - } -} diff --git a/Src/DDD.HealthcareDelivery/Domain/Facilities/BelgianHealthFacilityTranslator.cs b/Src/DDD.HealthcareDelivery/Domain/Facilities/BelgianHealthFacilityTranslator.cs deleted file mode 100644 index f93f3c3..0000000 --- a/Src/DDD.HealthcareDelivery/Domain/Facilities/BelgianHealthFacilityTranslator.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Conditions; -using System; -using System.Collections.Generic; - -namespace DDD.HealthcareDelivery.Domain.Facilities -{ - using Mapping; - - internal class BelgianHealthFacilityTranslator : IObjectTranslator - { - - #region Methods - - public HealthFacility Translate(HealthFacilityState state, - IDictionary options = null) - { - Condition.Requires(state, nameof(state)).IsNotNull(); - switch (state.FacilityType.ToEnum()) - { - case HealthFacilityType.MedicalOffice: - return CreateMedicalOffice(state); - case HealthFacilityType.Hospital: - return CreateHospital(state); - default: - throw new ArgumentException($"Facility type '{state.FacilityType}' not expected.", nameof(state)); - } - } - - private static MedicalOffice CreateMedicalOffice(HealthFacilityState state) - { - return new MedicalOffice(state.Identifier, state.Name, BelgianHealthFacilityLicenseNumber.CreateIfNotEmpty(state.LicenseNumber)); - } - - private static Hospital CreateHospital(HealthFacilityState state) - { - return new Hospital - ( - state.Identifier, - state.Name, - new BelgianHealthFacilityLicenseNumber(state.LicenseNumber) - ); - } - - #endregion Methods - - } -} diff --git a/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacility.cs b/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacility.cs deleted file mode 100644 index 86d25a4..0000000 --- a/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacility.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Collections.Generic; -using Conditions; - -namespace DDD.HealthcareDelivery.Domain.Facilities -{ - using Core.Domain; - - /// - /// Represents any location where healthcare is provided. - /// - public abstract class HealthFacility : ValueObject, IStateObjectConvertible - { - - #region Constructors - - protected HealthFacility(int identifier, - string name, - HealthFacilityLicenseNumber licenseNumber = null) - { - Condition.Requires(identifier, nameof(identifier)).IsGreaterThan(0); - Condition.Requires(name, nameof(name)).IsNotNullOrWhiteSpace(); - this.Identifier = identifier; - this.Name = name; - this.LicenseNumber = licenseNumber; - } - - #endregion Constructors - - #region Properties - - public int Identifier { get; } - - public HealthFacilityLicenseNumber LicenseNumber { get; } - - public string Name { get; } - - #endregion Properties - - #region Methods - - public override IEnumerable EqualityComponents() - { - yield return this.Identifier; - yield return this.Name; - yield return this.LicenseNumber; - } - - public virtual HealthFacilityState ToState() - { - return new HealthFacilityState - { - Identifier = this.Identifier, - Name = this.Name, - LicenseNumber = this.LicenseNumber?.Value - }; - } - - public override string ToString() - { - return $"{this.GetType().Name} [identifier={this.Identifier}, name={this.Name}, licenseNumber={this.LicenseNumber}]"; - } - - #endregion Methods - - } -} diff --git a/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacilityLicenseNumber.cs b/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacilityLicenseNumber.cs deleted file mode 100644 index 55007bb..0000000 --- a/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacilityLicenseNumber.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace DDD.HealthcareDelivery.Domain.Facilities -{ - using Common.Domain; - - public abstract class HealthFacilityLicenseNumber : IdentificationNumber - { - - #region Constructors - - protected HealthFacilityLicenseNumber(string value) : base(value) - { - } - - #endregion Constructors - - } -} \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacilityState.cs b/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacilityState.cs deleted file mode 100644 index 20c056c..0000000 --- a/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacilityState.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace DDD.HealthcareDelivery.Domain.Facilities -{ - public class HealthFacilityState - { - - #region Properties - - public int Identifier { get; set; } - - public string LicenseNumber { get; set; } - - public string Name { get; set; } - - public string FacilityType { get; set; } - - #endregion Properties - - } -} diff --git a/Src/DDD.HealthcareDelivery/Domain/Facilities/Hospital.cs b/Src/DDD.HealthcareDelivery/Domain/Facilities/Hospital.cs deleted file mode 100644 index 07fc543..0000000 --- a/Src/DDD.HealthcareDelivery/Domain/Facilities/Hospital.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace DDD.HealthcareDelivery.Domain.Facilities -{ - /// - /// Represents a facility that provides ongoing basic care in medicine and surgery with the possibility to stay overnight. - /// - public class Hospital : HealthFacility - { - - #region Constructors - - public Hospital(int identifier, - string name, - HealthFacilityLicenseNumber licenseNumber = null) - : base(identifier, name, licenseNumber) - { - } - - #endregion Constructors - - #region Methods - - public override HealthFacilityState ToState() - { - var state = base.ToState(); - state.FacilityType = HealthFacilityType.Hospital.ToString(); - return state; - } - - #endregion Methods - - } -} diff --git a/Src/DDD.HealthcareDelivery/Domain/Facilities/MedicalOffice.cs b/Src/DDD.HealthcareDelivery/Domain/Facilities/MedicalOffice.cs deleted file mode 100644 index 575ebcc..0000000 --- a/Src/DDD.HealthcareDelivery/Domain/Facilities/MedicalOffice.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace DDD.HealthcareDelivery.Domain.Facilities -{ - /// - /// Represents an outpatient facility in a specific location in which one or more healthcare practitioners (physicians, dentists, ...) receive and treat patients. - /// - public class MedicalOffice : HealthFacility - { - - #region Constructors - - public MedicalOffice(int identifier, - string name, - HealthFacilityLicenseNumber licenseNumber = null) - : base(identifier, name, licenseNumber) - { - } - - #endregion Constructors - - #region Methods - - public override HealthFacilityState ToState() - { - var state = base.ToState(); - state.FacilityType = HealthFacilityType.MedicalOffice.ToString(); - return state; - } - - #endregion Methods - - } -} diff --git a/Src/DDD.HealthcareDelivery/Domain/Patients/BelgianPatientTranslator.cs b/Src/DDD.HealthcareDelivery/Domain/Patients/BelgianPatientTranslator.cs deleted file mode 100644 index 28b8419..0000000 --- a/Src/DDD.HealthcareDelivery/Domain/Patients/BelgianPatientTranslator.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Conditions; -using System.Collections.Generic; - -namespace DDD.HealthcareDelivery.Domain.Patients -{ - using Mapping; - using Common.Domain; - - internal class BelgianPatientTranslator : IObjectTranslator - { - #region Methods - - public Patient Translate(PatientState state, - IDictionary options = null) - { - Condition.Requires(state, nameof(state)).IsNotNull(); - return new Patient - ( - state.Identifier, - FullName.FromState(state.FullName), - Enumeration.ParseCode(state.Sex), - BelgianSocialSecurityNumber.CreateIfNotEmpty(state.SocialSecurityNumber), - ContactInformation.FromState(state.ContactInformation), - state.Birthdate - ); - } - - #endregion Methods - } -} diff --git a/Src/DDD.HealthcareDelivery/Domain/Patients/Patient.cs b/Src/DDD.HealthcareDelivery/Domain/Patients/Patient.cs index fc07522..82522f3 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Patients/Patient.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Patients/Patient.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System; using System.Collections.Generic; @@ -7,7 +7,7 @@ namespace DDD.HealthcareDelivery.Domain.Patients using Common.Domain; using Core.Domain; - public class Patient : ValueObject, IStateObjectConvertible + public class Patient : ValueObject { #region Constructors @@ -19,9 +19,9 @@ public Patient(int identifier, ContactInformation contactInformation = null, DateTime? birthdate = null) { - Condition.Requires(identifier, nameof(identifier)).IsGreaterThan(0); - Condition.Requires(fullName, nameof(fullName)).IsNotNull(); - Condition.Requires(sex, nameof(sex)).IsNotNull(); + Ensure.That(identifier, nameof(identifier)).IsGt(0); + Ensure.That(fullName, nameof(fullName)).IsNotNull(); + Ensure.That(sex, nameof(sex)).IsNotNull(); this.Identifier = identifier; this.FullName = fullName; this.Sex = sex; @@ -30,21 +30,23 @@ public Patient(int identifier, this.Birthdate = birthdate; } + protected Patient() { } + #endregion Constructors #region Properties - public DateTime? Birthdate { get; } + public DateTime? Birthdate { get; private set; } - public ContactInformation ContactInformation { get; } + public ContactInformation ContactInformation { get; private set; } - public FullName FullName { get; } + public FullName FullName { get; private set; } - public int Identifier { get; } + public int Identifier { get; private set; } - public Sex Sex { get; } + public Sex Sex { get; private set; } - public SocialSecurityNumber SocialSecurityNumber { get; } + public SocialSecurityNumber SocialSecurityNumber { get; private set; } #endregion Properties @@ -60,21 +62,6 @@ public override IEnumerable EqualityComponents() yield return this.Birthdate; } - public PatientState ToState() - { - return new PatientState - { - Identifier = this.Identifier, - FullName = this.FullName.ToState(), - Sex = this.Sex.Code, - SocialSecurityNumber = this.SocialSecurityNumber?.Value, - ContactInformation = this.ContactInformation == null ? - new ContactInformationState() // EF6 complex types cannot be null - : this.ContactInformation.ToState(), - Birthdate = this.Birthdate, - }; - } - public override string ToString() { var format = "{0} [identifier={1}, fullName={2}, sex={3}, socialSecurityNumber={4}, contactInformation={5}, birthdate={6}]"; diff --git a/Src/DDD.HealthcareDelivery/Domain/Patients/PatientState.cs b/Src/DDD.HealthcareDelivery/Domain/Patients/PatientState.cs deleted file mode 100644 index a888144..0000000 --- a/Src/DDD.HealthcareDelivery/Domain/Patients/PatientState.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; - -namespace DDD.HealthcareDelivery.Domain.Patients -{ - using Common.Domain; - - public class PatientState - { - - #region Properties - - public DateTime? Birthdate { get; set; } - - public ContactInformationState ContactInformation { get; set; } - - public FullNameState FullName { get; set; } - - public int Identifier { get; set; } - - public string Sex { get; set; } - - public string SocialSecurityNumber { get; set; } - - #endregion Properties - - } -} diff --git a/Src/DDD.HealthcareDelivery/Domain/Practitioners/BelgianHealthcarePractitionerLicenseNumber.cs b/Src/DDD.HealthcareDelivery/Domain/Practitioners/BelgianHealthcarePractitionerLicenseNumber.cs index ce9c723..b0acfa4 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Practitioners/BelgianHealthcarePractitionerLicenseNumber.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Practitioners/BelgianHealthcarePractitionerLicenseNumber.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; namespace DDD.HealthcareDelivery.Domain.Practitioners { @@ -12,11 +12,12 @@ public class BelgianHealthcarePractitionerLicenseNumber : HealthcarePractitioner public BelgianHealthcarePractitionerLicenseNumber(string value) : base(value) { - Condition.Requires(value, nameof(value)) - .HasLength(11) - .Evaluate(n => n.IsNumeric()); + Ensure.That(value, nameof(value)).HasLength(11); + Ensure.That(value, nameof(value)).IsAllDigits(); } + protected BelgianHealthcarePractitionerLicenseNumber() { } + #endregion Constructors #region Enums @@ -62,7 +63,7 @@ public enum Modulus /// public static int ComputeCheckDigit(string value, Modulus modulus = Modulus.Mod97) { - Condition.Requires(value, nameof(value)).IsLongerOrEqual(6); + Ensure.That(value, nameof(value)).HasMinLength(6); var identifier = int.Parse(value.Substring(0, 6)); // old unique practitioner identifier var imodulus = (int)modulus; return imodulus - (identifier % imodulus); diff --git a/Src/DDD.HealthcareDelivery/Domain/Practitioners/BelgianHealthcarePractitionerTranslator.cs b/Src/DDD.HealthcareDelivery/Domain/Practitioners/BelgianHealthcarePractitionerTranslator.cs deleted file mode 100644 index f93c1a1..0000000 --- a/Src/DDD.HealthcareDelivery/Domain/Practitioners/BelgianHealthcarePractitionerTranslator.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Conditions; -using System; -using System.Collections.Generic; - -namespace DDD.HealthcareDelivery.Domain.Practitioners -{ - using Mapping; - using Common.Domain; - - internal class BelgianHealthcarePractitionerTranslator : IObjectTranslator - { - - #region Methods - - public HealthcarePractitioner Translate(HealthcarePractitionerState state, - IDictionary options = null) - { - Condition.Requires(state, nameof(state)).IsNotNull(); - switch (state.PractitionerType.ToEnum()) - { - case HealthcarePractitionerType.Physician: - return CreatePhysician(state); - default: - throw new ArgumentException($"Practitioner type '{state.PractitionerType}' not expected.", nameof(state)); - } - } - - private static Physician CreatePhysician(HealthcarePractitionerState state) - { - return new Physician - ( - state.Identifier, - FullName.FromState(state.FullName), - new BelgianHealthcarePractitionerLicenseNumber(state.LicenseNumber), - BelgianSocialSecurityNumber.CreateIfNotEmpty(state.SocialSecurityNumber), - ContactInformation.FromState(state.ContactInformation), - state.Speciality, - state.DisplayName - ); - } - - #endregion Methods - - } -} diff --git a/Src/DDD.HealthcareDelivery/Domain/Practitioners/HealthcarePractitioner.cs b/Src/DDD.HealthcareDelivery/Domain/Practitioners/HealthcarePractitioner.cs index a91e524..5299ae4 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Practitioners/HealthcarePractitioner.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Practitioners/HealthcarePractitioner.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using Conditions; +using EnsureThat; +using System.Collections.Generic; namespace DDD.HealthcareDelivery.Domain.Practitioners { @@ -9,12 +9,14 @@ namespace DDD.HealthcareDelivery.Domain.Practitioners /// /// Represents a person that provides healthcare services. /// - public abstract class HealthcarePractitioner - : ValueObject, IStateObjectConvertible + public abstract class HealthcarePractitioner + : ValueObject { #region Constructors + protected HealthcarePractitioner() { } + protected HealthcarePractitioner(int identifier, FullName fullName, HealthcarePractitionerLicenseNumber licenseNumber, @@ -23,9 +25,9 @@ protected HealthcarePractitioner(int identifier, string speciality = null, string displayName = null) { - Condition.Requires(identifier, nameof(identifier)).IsGreaterThan(0); - Condition.Requires(fullName, nameof(fullName)).IsNotNull(); - Condition.Requires(licenseNumber, nameof(licenseNumber)).IsNotNull(); + Ensure.That(identifier, nameof(identifier)).IsGt(0); + Ensure.That(fullName, nameof(fullName)).IsNotNull(); + Ensure.That(licenseNumber, nameof(licenseNumber)).IsNotNull(); this.Identifier = identifier; this.FullName = fullName; this.LicenseNumber = licenseNumber; @@ -40,19 +42,19 @@ protected HealthcarePractitioner(int identifier, #region Properties - public ContactInformation ContactInformation { get; } + public ContactInformation ContactInformation { get; private set; } - public FullName FullName { get; } + public string DisplayName { get; private set; } - public string DisplayName { get; } + public FullName FullName { get; private set; } - public int Identifier { get; } + public int Identifier { get; private set; } - public HealthcarePractitionerLicenseNumber LicenseNumber { get; } + public HealthcarePractitionerLicenseNumber LicenseNumber { get; private set; } - public SocialSecurityNumber SocialSecurityNumber { get; } + public SocialSecurityNumber SocialSecurityNumber { get; private set; } - public string Speciality { get; } + public string Speciality { get; private set; } #endregion Properties @@ -69,22 +71,6 @@ public override IEnumerable EqualityComponents() yield return this.DisplayName; } - public virtual HealthcarePractitionerState ToState() - { - return new HealthcarePractitionerState - { - Identifier = this.Identifier, - FullName = this.FullName.ToState(), - LicenseNumber = this.LicenseNumber.Value, - SocialSecurityNumber = this.SocialSecurityNumber?.Value, - ContactInformation = this.ContactInformation == null ? - new ContactInformationState() // EF6 complex types cannot be null - : this.ContactInformation.ToState(), - Speciality = this.Speciality, - DisplayName = this.DisplayName - }; - } - public override string ToString() { var format = "{0} [identifier={1}, fullName={2}, licenseNumber={3}, socialSecurityNumber={4}, contactInformation={5}, speciality={6}, displayName={7}]"; diff --git a/Src/DDD.HealthcareDelivery/Domain/Practitioners/HealthcarePractitionerLicenseNumber.cs b/Src/DDD.HealthcareDelivery/Domain/Practitioners/HealthcarePractitionerLicenseNumber.cs index 462b254..850bade 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Practitioners/HealthcarePractitionerLicenseNumber.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Practitioners/HealthcarePractitionerLicenseNumber.cs @@ -10,6 +10,8 @@ public abstract class HealthcarePractitionerLicenseNumber : IdentificationNumber #region Constructors + protected HealthcarePractitionerLicenseNumber() { } + protected HealthcarePractitionerLicenseNumber(string value) : base(value) { } diff --git a/Src/DDD.HealthcareDelivery/Domain/Practitioners/HealthcarePractitionerState.cs b/Src/DDD.HealthcareDelivery/Domain/Practitioners/HealthcarePractitionerState.cs deleted file mode 100644 index c161bc0..0000000 --- a/Src/DDD.HealthcareDelivery/Domain/Practitioners/HealthcarePractitionerState.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace DDD.HealthcareDelivery.Domain.Practitioners -{ - using Common.Domain; - - public class HealthcarePractitionerState - { - - #region Properties - - public ContactInformationState ContactInformation { get; set; } - - public string DisplayName { get; set; } - - public FullNameState FullName { get; set; } - - public int Identifier { get; set; } - - public string LicenseNumber { get; set; } - - public string PractitionerType { get; set; } - - public string SocialSecurityNumber { get; set; } - - public string Speciality { get; set; } - - #endregion Properties - } -} diff --git a/Src/DDD.HealthcareDelivery/Domain/Practitioners/Physician.cs b/Src/DDD.HealthcareDelivery/Domain/Practitioners/Physician.cs index 3d3603b..2b20bc4 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Practitioners/Physician.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Practitioners/Physician.cs @@ -10,29 +10,19 @@ public class Physician : HealthcarePractitioner #region Constructors - public Physician(int identifier, - FullName fullName, + public Physician(int identifier, + FullName fullName, HealthcarePractitionerLicenseNumber licenseNumber, SocialSecurityNumber socialSecurityNumber = null, - ContactInformation contactInformation = null, + ContactInformation contactInformation = null, string speciality = null, - string displayName = null) + string displayName = null) : base(identifier, fullName, licenseNumber, socialSecurityNumber, contactInformation, speciality, displayName) { } - #endregion Constructors - - #region Methods - - public override HealthcarePractitionerState ToState() - { - var state = base.ToState(); - state.PractitionerType = HealthcarePractitionerType.Physician.ToString(); - return state; - } - - #endregion Methods + protected Physician() { } + #endregion Constructors } } diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/BelgianMedicationCode.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/BelgianMedicationCode.cs index e05264d..d7c1698 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/BelgianMedicationCode.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/BelgianMedicationCode.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; namespace DDD.HealthcareDelivery.Domain.Prescriptions { @@ -12,11 +12,12 @@ public class BelgianMedicationCode : MedicationCode public BelgianMedicationCode(string value) : base(value) { - Condition.Requires(value, nameof(value)) - .HasLength(7) - .Evaluate(c => c.IsNumeric()); + Ensure.That(value, nameof(value)).HasLength(7); + Ensure.That(value, nameof(value)).IsAllDigits(); } + protected BelgianMedicationCode() { } + #endregion Constructors #region Methods @@ -26,9 +27,8 @@ public BelgianMedicationCode(string value) : base(value) /// public static int ComputeCheckDigit(string value) { - Condition.Requires(value, nameof(value)) - .IsLongerOrEqual(6) - .Evaluate(c => c.IsNumeric()); + Ensure.That(value, nameof(value)).HasMinLength(6); + Ensure.That(value, nameof(value)).IsAllDigits(); var identifier = value.Substring(0, 6); var sum = 0; var alternate = true; diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs deleted file mode 100644 index a6de97d..0000000 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Linq; -using Conditions; -using System.Collections.Generic; - -namespace DDD.HealthcareDelivery.Domain.Prescriptions -{ - using Mapping; - using Common.Domain; - using Facilities; - using Patients; - using Practitioners; - - public class BelgianPharmaceuticalPrescriptionTranslator - : IObjectTranslator - { - - #region Fields - - private readonly IObjectTranslator facilityTranslator; - private readonly IObjectTranslator medicationTranslator; - private readonly IObjectTranslator patientTranslator; - private readonly IObjectTranslator practitionerTranslator; - - #endregion Fields - - #region Constructors - - public BelgianPharmaceuticalPrescriptionTranslator() - { - this.practitionerTranslator = new BelgianHealthcarePractitionerTranslator(); - this.patientTranslator = new BelgianPatientTranslator(); - this.facilityTranslator = new BelgianHealthFacilityTranslator(); - this.medicationTranslator = new BelgianPrescribedMedicationTranslator(); - } - - #endregion Constructors - - #region Methods - - public PharmaceuticalPrescription Translate(PharmaceuticalPrescriptionState state, - IDictionary options = null) - { - Condition.Requires(state, nameof(state)).IsNotNull(); - return new PharmaceuticalPrescription - ( - new PrescriptionIdentifier(state.Identifier), - this.practitionerTranslator.Translate(state.Prescriber), - this.patientTranslator.Translate(state.Patient), - this.facilityTranslator.Translate(state.HealthFacility), - state.PrescribedMedications.Select(m => this.medicationTranslator.Translate(m)), - new Alpha2LanguageCode(state.LanguageCode), - Enumeration.ParseCode(state.Status), - state.CreatedOn, - state.DelivrableAt, - state.EntityState - ); - - } - - #endregion Methods - - } -} diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/BelgianPrescribedMedicationTranslator.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/BelgianPrescribedMedicationTranslator.cs deleted file mode 100644 index e0ea38f..0000000 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/BelgianPrescribedMedicationTranslator.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using Conditions; -using System.Collections.Generic; - -namespace DDD.HealthcareDelivery.Domain.Prescriptions -{ - using Mapping; - - internal class BelgianPrescribedMedicationTranslator : IObjectTranslator - { - - #region Methods - - public PrescribedMedication Translate(PrescribedMedicationState state, - IDictionary options = null) - { - Condition.Requires(state, nameof(state)).IsNotNull(); - switch (state.MedicationType.ToEnum()) - { - case PrescribedMedicationType.Product: - return CreateProduct(state); - case PrescribedMedicationType.Substance: - return CreateSubstance(state); - case PrescribedMedicationType.Compounding: - return CreateCompounding(state); - default: - throw new ArgumentException($"Medication type '{state.MedicationType}' not expected.", nameof(state)); - - } - } - - private static PrescribedPharmaceuticalCompounding CreateCompounding(PrescribedMedicationState state) - { - return new PrescribedPharmaceuticalCompounding - ( - state.NameOrDescription, - state.Posology, - state.Quantity, - state.Duration, - state.Identifier, - state.EntityState - ); - } - - private static PrescribedPharmaceuticalProduct CreateProduct(PrescribedMedicationState state) - { - return new PrescribedPharmaceuticalProduct - ( - state.NameOrDescription, - state.Posology, - state.Quantity, - state.Duration, - BelgianMedicationCode.CreateIfNotEmpty(state.Code), - state.Identifier, - state.EntityState - ); - } - - private static PrescribedPharmaceuticalSubstance CreateSubstance(PrescribedMedicationState state) - { - return new PrescribedPharmaceuticalSubstance - ( - state.NameOrDescription, - state.Posology, - state.Quantity, - state.Duration, - BelgianMedicationCode.CreateIfNotEmpty(state.Code), - state.Identifier, - state.EntityState - ); - } - - #endregion Methods - - } -} diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/MedicationCode.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/MedicationCode.cs index e3e736e..ed6f7e7 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/MedicationCode.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/MedicationCode.cs @@ -10,6 +10,8 @@ public abstract class MedicationCode : IdentificationCode #region Constructors + protected MedicationCode() { } + protected MedicationCode(string value) : base(value) { } diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs index 0bbd365..6016ea6 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs @@ -1,22 +1,21 @@ -using Conditions; +using EnsureThat; using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; namespace DDD.HealthcareDelivery.Domain.Prescriptions { using Collections; using Common.Domain; using Core.Domain; - using Facilities; using Patients; using Practitioners; + using Encounters; /// /// Represents a pharmaceutical prescription. /// - public class PharmaceuticalPrescription : Prescription + public class PharmaceuticalPrescription : Prescription { #region Fields @@ -30,23 +29,23 @@ public class PharmaceuticalPrescription : Prescription prescribedMedications, Alpha2LanguageCode languageCode, PrescriptionStatus status, DateTime createdOn, + EncounterIdentifier encounterIdentifier = null, DateTime? delivrableAt = null, - EntityState entityState = EntityState.Added, IEnumerable events = null) - : base(identifier, prescriber, patient, healthFacility, languageCode, status, createdOn, delivrableAt, entityState, events) + : base(identifier, prescriber, patient, languageCode, status, createdOn, encounterIdentifier, delivrableAt, events) { - Condition.Requires(prescribedMedications, nameof(prescribedMedications)) - .IsNotNull() - .IsNotEmpty() - .DoesNotContain(null); + Ensure.That(prescribedMedications, nameof(prescribedMedications)).IsNotNull(); + Ensure.Enumerable.HasItems(prescribedMedications, nameof(prescribedMedications)); + Ensure.Enumerable.HasNoNull(prescribedMedications, nameof(prescribedMedications)); this.prescribedMedications.AddRange(prescribedMedications); } + protected PharmaceuticalPrescription() { } + #endregion Constructors #region Methods @@ -54,10 +53,10 @@ public PharmaceuticalPrescription(PrescriptionIdentifier identifier, public static PharmaceuticalPrescription Create(PrescriptionIdentifier identifier, HealthcarePractitioner prescriber, Patient patient, - HealthFacility healthFacility, IEnumerable prescribedMedications, DateTime createdOn, Alpha2LanguageCode languageCode, + EncounterIdentifier encounterIdentifier = null, DateTime? delivrableAt = null) { var prescription = new PharmaceuticalPrescription @@ -65,11 +64,11 @@ public static PharmaceuticalPrescription Create(PrescriptionIdentifier identifie identifier, prescriber, patient, - healthFacility, prescribedMedications, languageCode, PrescriptionStatus.Created, createdOn, + encounterIdentifier, delivrableAt ); prescription.AddEvent(new PharmaceuticalPrescriptionCreated(identifier.Value, createdOn)); @@ -79,34 +78,19 @@ public static PharmaceuticalPrescription Create(PrescriptionIdentifier identifie public static PharmaceuticalPrescription Create(PrescriptionIdentifier identifier, HealthcarePractitioner prescriber, Patient patient, - HealthFacility healthFacility, IEnumerable prescribedMedications, Alpha2LanguageCode languageCode, + EncounterIdentifier encounterIdentifier = null, DateTime? delivrableAt = null) { - return Create(identifier, prescriber, patient, healthFacility, prescribedMedications, DateTime.Now, languageCode, delivrableAt); + return Create(identifier, prescriber, patient, prescribedMedications, SystemTime.Local(), languageCode, encounterIdentifier, delivrableAt); } public IEnumerable PrescribedMedications() => this.prescribedMedications.ToImmutableHashSet(); - public override PharmaceuticalPrescriptionState ToState() - { - var state = base.ToState(); - state.PrescribedMedications.AddRange(this.prescribedMedications.Select(m => ToPrescribedMedicationState(m, this.Identifier.Value))); - return state; - } - protected override void AddPrescriptionRevokedEvent(string reason) { - this.AddEvent(new PharmaceuticalPrescriptionRevoked(this.Identifier.Value, reason)); - } - - private static PrescribedMedicationState ToPrescribedMedicationState(PrescribedMedication medication, - int prescriptionIdentifier) - { - var state = medication.ToState(); - state.PrescriptionIdentifier = prescriptionIdentifier; - return state; + this.AddEvent(new PharmaceuticalPrescriptionRevoked(this.Identifier.Value, SystemTime.Local(), reason)); } #endregion Methods diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescriptionState.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescriptionState.cs deleted file mode 100644 index 2c34c62..0000000 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescriptionState.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; - -namespace DDD.HealthcareDelivery.Domain.Prescriptions -{ - public class PharmaceuticalPrescriptionState : PrescriptionState - { - - #region Properties - - public ICollection PrescribedMedications { get; set; } = new HashSet(); - - #endregion Properties - - } -} \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedication.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedication.cs index be370e5..5f8921e 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedication.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedication.cs @@ -1,59 +1,53 @@ -using System.Collections.Generic; -using Conditions; -using System; +using EnsureThat; +using System.Collections.Generic; namespace DDD.HealthcareDelivery.Domain.Prescriptions { - using Core; using Core.Domain; - public abstract class PrescribedMedication - : ValueObject, IStateObjectConvertible + public abstract class PrescribedMedication : ValueObject { #region Fields - private readonly EntityState entityState; - private readonly int identifier; + private int identifier; #endregion Fields #region Constructors + protected PrescribedMedication() { } + protected PrescribedMedication(string nameOrDescription, string posology = null, - string quantity = null, - string duration = null, + byte? quantity = null, MedicationCode code = null, - int identifier = 0, - EntityState entityState = EntityState.Added) + int identifier = 0) { - Condition.Requires(nameOrDescription, nameof(nameOrDescription)).IsNotNullOrWhiteSpace(); - Condition.Requires(identifier, nameof(identifier)).IsGreaterOrEqual(0); + Ensure.That(nameOrDescription, nameof(nameOrDescription)).IsNotNullOrWhiteSpace(); + Ensure.That(identifier, nameof(identifier)).IsGte(0); this.NameOrDescription = nameOrDescription; this.Posology = posology; - if (!string.IsNullOrWhiteSpace(quantity)) + if (quantity.HasValue) + { + Ensure.That(quantity.Value, nameof(quantity)).IsGte((byte)1); this.Quantity = quantity; - if (!string.IsNullOrWhiteSpace(duration)) - this.Duration = duration; + } this.Code = code; this.identifier = identifier; - this.entityState = entityState; } #endregion Constructors #region Properties - public MedicationCode Code { get; } + public MedicationCode Code { get; private set; } - public string Duration { get; } + public string NameOrDescription { get; private set; } - public string NameOrDescription { get; } + public string Posology { get; private set; } - public string Posology { get; } - - public string Quantity { get; } + public byte? Quantity { get; private set; } #endregion Properties @@ -64,27 +58,12 @@ public override IEnumerable EqualityComponents() yield return this.NameOrDescription; yield return this.Posology; yield return this.Quantity; - yield return this.Duration; yield return this.Code; } - public virtual PrescribedMedicationState ToState() - { - return new PrescribedMedicationState - { - EntityState = this.entityState, - Identifier = this.identifier, - NameOrDescription = this.NameOrDescription, - Posology = this.Posology, - Quantity = this.Quantity, - Duration = this.Duration, - Code = this.Code?.Value - }; - } - public override string ToString() { - return $"{this.GetType().Name} [nameOrDescription={this.NameOrDescription}, posology={this.Posology}], quantity={this.Quantity}, duration={this.Duration}, code={this.Code}"; + return $"{this.GetType().Name} [nameOrDescription={this.NameOrDescription}, posology={this.Posology}, quantity={this.Quantity}, code={this.Code}]"; } #endregion Methods diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedicationState.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedicationState.cs deleted file mode 100644 index 0852260..0000000 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedicationState.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace DDD.HealthcareDelivery.Domain.Prescriptions -{ - using Core.Domain; - - public class PrescribedMedicationState : IStateEntity - { - - #region Properties - - public string Code { get; set; } - - public string Duration { get; set; } - - public EntityState EntityState { get; set; } - - public int Identifier { get; set; } - - public string MedicationType { get; set; } - - public string NameOrDescription { get; set; } - - public string Posology { get; set; } - - public int PrescriptionIdentifier { get; set; } - - public string Quantity { get; set; } - - #endregion Properties - - } -} diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalCompounding.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalCompounding.cs index e6f1145..08728ce 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalCompounding.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalCompounding.cs @@ -1,7 +1,5 @@ namespace DDD.HealthcareDelivery.Domain.Prescriptions { - using Core.Domain; - /// /// Represents a pharmaceutical compounding. /// @@ -10,28 +8,17 @@ public class PrescribedPharmaceuticalCompounding : PrescribedMedication #region Constructors + protected PrescribedPharmaceuticalCompounding() { } + public PrescribedPharmaceuticalCompounding(string nameOrDescription, string posology = null, - string quantity = null, - string duration = null, - int identifier = 0, - EntityState entityState = EntityState.Added) - : base(nameOrDescription, posology, quantity, duration, null, identifier, entityState) + byte? quantity = null, + int identifier = 0) + : base(nameOrDescription, posology, quantity, null, identifier) { } #endregion Constructors - #region Methods - - public override PrescribedMedicationState ToState() - { - var state = base.ToState(); - state.MedicationType = PrescribedMedicationType.Compounding.ToString(); - return state; - } - - #endregion Methods - } } \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalProduct.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalProduct.cs index b505330..65066fa 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalProduct.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalProduct.cs @@ -1,9 +1,5 @@ -using Conditions; - -namespace DDD.HealthcareDelivery.Domain.Prescriptions +namespace DDD.HealthcareDelivery.Domain.Prescriptions { - using Core.Domain; - /// /// Represents a commercial pharmaceutical product with a brand name. /// @@ -12,29 +8,17 @@ public class PrescribedPharmaceuticalProduct : PrescribedMedication #region Constructors - public PrescribedPharmaceuticalProduct(string nameOrDescription, - string posology =null, - string quantity = null, - string duration = null, + public PrescribedPharmaceuticalProduct(string nameOrDescription, + string posology = null, + byte? quantity = null, MedicationCode code = null, - int identifier = 0, - EntityState entityState = EntityState.Added) - : base(nameOrDescription, posology, quantity, duration, code, identifier, entityState) + int identifier = 0) + : base(nameOrDescription, posology, quantity, code, identifier) { } - #endregion Constructors - - #region Methods - - public override PrescribedMedicationState ToState() - { - var state = base.ToState(); - state.MedicationType = PrescribedMedicationType.Product.ToString(); - return state; - } - - #endregion Methods + protected PrescribedPharmaceuticalProduct() { } + #endregion Constructors } } \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalSubstance.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalSubstance.cs index 13f3949..5ca90f9 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalSubstance.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalSubstance.cs @@ -1,7 +1,5 @@ namespace DDD.HealthcareDelivery.Domain.Prescriptions { - using Core.Domain; - /// /// Represents an active pharmaceutical substance with an International Nonproprietary Name (INN). /// @@ -10,29 +8,17 @@ public class PrescribedPharmaceuticalSubstance : PrescribedMedication #region Constructors - public PrescribedPharmaceuticalSubstance(string nameOrDescription, - string posology = null, - string quantity = null, - string duration = null, + public PrescribedPharmaceuticalSubstance(string nameOrDescription, + string posology = null, + byte? quantity = null, MedicationCode code = null, - int identifier = 0, - EntityState entityState = EntityState.Added) - : base(nameOrDescription, posology, quantity, duration, code, identifier, entityState) - { - } - - #endregion Constructors - - #region Methods - - public override PrescribedMedicationState ToState() + int identifier = 0) + : base(nameOrDescription, posology, quantity, code, identifier) { - var state = base.ToState(); - state.MedicationType = PrescribedMedicationType.Substance.ToString(); - return state; } - #endregion Methods + protected PrescribedPharmaceuticalSubstance() { } + #endregion Constructors } } \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/Prescription.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/Prescription.cs index 039f9f4..779b150 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/Prescription.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/Prescription.cs @@ -1,70 +1,68 @@ -using System; +using EnsureThat; +using System; using System.Collections.Generic; -using Conditions; namespace DDD.HealthcareDelivery.Domain.Prescriptions { - using Core.Domain; using Common.Domain; + using Core.Domain; using Patients; - using Facilities; using Practitioners; + using Encounters; /// /// Represents a health-care program implemented by a qualified healthcare practitioner (physician, dentist, ...) in the form of instructions that govern the plan of care for an individual patient. /// - public abstract class Prescription - : DomainEntity, IStateObjectConvertible - where TState : PrescriptionState, new() + public abstract class Prescription : DomainEntity { #region Constructors + protected Prescription() { } + protected Prescription(PrescriptionIdentifier identifier, HealthcarePractitioner prescriber, Patient patient, - HealthFacility healthFacility, Alpha2LanguageCode languageCode, PrescriptionStatus status, DateTime createdOn, + EncounterIdentifier encounterIdentifier = null, DateTime? delivrableAt = null, - EntityState entityState = EntityState.Added, IEnumerable events = null) - : base(entityState, events) + : base(events) { - Condition.Requires(identifier, nameof(identifier)).IsNotNull(); - Condition.Requires(prescriber, nameof(prescriber)).IsNotNull(); - Condition.Requires(patient, nameof(patient)).IsNotNull(); - Condition.Requires(healthFacility, nameof(healthFacility)).IsNotNull(); - Condition.Requires(status, nameof(status)).IsNotNull(); - Condition.Requires(languageCode, nameof(languageCode)).IsNotNull(); + Ensure.That(identifier, nameof(identifier)).IsNotNull(); + Ensure.That(prescriber, nameof(prescriber)).IsNotNull(); + Ensure.That(patient, nameof(patient)).IsNotNull(); + Ensure.That(status, nameof(status)).IsNotNull(); + Ensure.That(languageCode, nameof(languageCode)).IsNotNull(); this.Identifier = identifier; this.Prescriber = prescriber; this.Patient = patient; - this.HealthFacility = healthFacility; + this.LanguageCode = languageCode; this.Status = status; this.CreatedOn = createdOn; - this.DelivrableAt = delivrableAt; - this.LanguageCode = languageCode; + this.EncounterIdentifier = encounterIdentifier; + this.DeliverableAt = delivrableAt; } #endregion Constructors #region Properties - public DateTime CreatedOn { get; } + public DateTime CreatedOn { get; private set; } - public DateTime? DelivrableAt { get; } + public DateTime? DeliverableAt { get; private set; } - public HealthFacility HealthFacility { get; } + public EncounterIdentifier EncounterIdentifier { get; private set; } - public PrescriptionIdentifier Identifier { get; } + public PrescriptionIdentifier Identifier { get; private set; } - public Alpha2LanguageCode LanguageCode { get; } + public Alpha2LanguageCode LanguageCode { get; private set; } - public Patient Patient { get; } + public Patient Patient { get; private set; } - public HealthcarePractitioner Prescriber { get; } + public HealthcarePractitioner Prescriber { get; private set; } public PrescriptionStatus Status { get; private set; } @@ -76,29 +74,13 @@ protected Prescription(PrescriptionIdentifier identifier, public void Revoke(string reason) { - Condition.Requires(reason, nameof(reason)).IsNotNullOrWhiteSpace(); + Ensure.That(reason, nameof(reason)).IsNotNullOrWhiteSpace(); if (this.IsRevocable()) { this.Status = PrescriptionStatus.Revoked; - this.MarkAsModified(); this.AddPrescriptionRevokedEvent(reason); } } - public virtual TState ToState() - { - return new TState - { - Identifier = this.Identifier.Value, - Prescriber = this.Prescriber.ToState(), - Patient = this.Patient.ToState(), - HealthFacility = this.HealthFacility.ToState(), - Status = this.Status.Code, - CreatedOn = this.CreatedOn, - DelivrableAt = this.DelivrableAt, - LanguageCode = this.LanguageCode.Value, - EntityState = this.EntityState, - }; - } protected abstract void AddPrescriptionRevokedEvent(string reason); diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescriptionIdentifier.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescriptionIdentifier.cs index c740a82..318e7bf 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescriptionIdentifier.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescriptionIdentifier.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; namespace DDD.HealthcareDelivery.Domain.Prescriptions { @@ -11,7 +11,11 @@ public class PrescriptionIdentifier : ArbitraryIdentifier public PrescriptionIdentifier(int value) : base(value) { - Condition.Requires(value, nameof(value)).IsGreaterThan(0); + Ensure.That(value, nameof(value)).IsGt(0); + } + + protected PrescriptionIdentifier() + { } #endregion Constructors diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescriptionState.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescriptionState.cs deleted file mode 100644 index 8cc86a2..0000000 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescriptionState.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -namespace DDD.HealthcareDelivery.Domain.Prescriptions -{ - using Core.Domain; - using Patients; - using Practitioners; - using Facilities; - - public abstract class PrescriptionState : IStateEntity - { - - #region Properties - - public DateTime CreatedOn { get; set; } - - public DateTime? DelivrableAt { get; set; } - - public EntityState EntityState { get; set; } - - public HealthFacilityState HealthFacility { get; set; } - - public int Identifier { get; set; } - - public string LanguageCode { get; set; } - - public PatientState Patient { get; set; } - - public HealthcarePractitionerState Prescriber { get; set; } - - public string Status { get; set; } - - #endregion Properties - } -} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/BelgianOracleHealthcareDeliveryConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/BelgianOracleHealthcareDeliveryConfiguration.cs new file mode 100644 index 0000000..6e72116 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/BelgianOracleHealthcareDeliveryConfiguration.cs @@ -0,0 +1,27 @@ +using NHibernate.Mapping.ByCode; + +namespace DDD.HealthcareDelivery.Infrastructure +{ + using Prescriptions; + using Domain.Prescriptions; + using Domain.Practitioners; + using Common.Domain; + + public class BelgianOracleHealthcareDeliveryConfiguration : OracleHealthcareDeliveryConfiguration + { + + #region Methods + + protected override void AddMappings(ModelMapper modelMapper) + { + base.AddMappings(modelMapper); + modelMapper.AddMapping>(); + modelMapper.AddMapping>(); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/BelgianSqlServerHealthcareDeliveryConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/BelgianSqlServerHealthcareDeliveryConfiguration.cs new file mode 100644 index 0000000..88e2d06 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/BelgianSqlServerHealthcareDeliveryConfiguration.cs @@ -0,0 +1,27 @@ +using NHibernate.Mapping.ByCode; + +namespace DDD.HealthcareDelivery.Infrastructure +{ + using Prescriptions; + using Domain.Prescriptions; + using Domain.Practitioners; + using Common.Domain; + + public class BelgianSqlServerHealthcareDeliveryConfiguration : SqlServerHealthcareDeliveryConfiguration + { + + #region Methods + + protected override void AddMappings(ModelMapper modelMapper) + { + base.AddMappings(modelMapper); + modelMapper.AddMapping>(); + modelMapper.AddMapping>(); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/HealthcareContext.cs b/Src/DDD.HealthcareDelivery/Infrastructure/HealthcareContext.cs deleted file mode 100644 index f13e63e..0000000 --- a/Src/DDD.HealthcareDelivery/Infrastructure/HealthcareContext.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Data.Entity; -using System.Data.Common; -using System.Linq; -using System.Data.Entity.ModelConfiguration.Conventions; - -namespace DDD.HealthcareDelivery.Infrastructure -{ - using Core.Infrastructure.Data; - using Core.Domain; - using Domain.Prescriptions; - using Domain.Patients; - using Domain.Practitioners; - using Prescriptions; - using Common.Domain; - - public abstract class HealthcareContext : StateEntitiesContext - { - - #region Constructors - - protected HealthcareContext(DbConnection connection, bool contextOwnsConnection) - : base(connection, contextOwnsConnection) - { - } - - #endregion Constructors - - #region Properties - - public virtual DbSet Events { get; set; } - - public virtual DbSet PharmaceuticalPrescriptions { get; set; } - - protected bool UseUpperCase { get; set; } = false; - - #endregion Properties - - #region Methods - - protected override void OnModelCreating(DbModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - modelBuilder.Conventions.Remove(); - this.AddConfigurations(modelBuilder); - } - - protected override void SetGeneratedValues() - { - this.SetIdsForEvents(); - this.SetIdsForPrescribedMedications(); - } - - private void AddConfigurations(DbModelBuilder modelBuilder) - { - modelBuilder.Configurations.Add(new PharmaceuticalPrescriptionStateConfiguration(this.UseUpperCase)); - modelBuilder.Configurations.Add(new PrescribedMedicationStateConfiguration(this.UseUpperCase)); - modelBuilder.ComplexType().Ignore(p => p.ContactInformation); - modelBuilder.ComplexType().Ignore(c => c.FaxNumber); - modelBuilder.ComplexType(); - } - private void SetIdsForEvents() - { - var events = this.ChangeTracker.Entries() - .Select(m => m.Entity); - foreach (var evt in events) - evt.Id = this.Database.Connection.NextValue("EventId"); - } - - private void SetIdsForPrescribedMedications() - { - var medications = this.ChangeTracker.Entries() - .Where(m => m.Entity.EntityState == Core.Domain.EntityState.Added) - .Select(m => m.Entity); - foreach (var medication in medications) - medication.Identifier = this.Database.Connection.NextValue("PrescMedicationId"); - } - - #endregion Methods - - } -} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/HealthcareDeliveryConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/HealthcareDeliveryConfiguration.cs new file mode 100644 index 0000000..fec263c --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/HealthcareDeliveryConfiguration.cs @@ -0,0 +1,34 @@ +using NHibernate.Cfg; +using NHibernate.Mapping.ByCode; + +namespace DDD.HealthcareDelivery.Infrastructure +{ + using Prescriptions; + + public abstract class HealthcareDeliveryConfiguration : Configuration + { + + #region Constructors + + protected HealthcareDeliveryConfiguration() + { + var modelMapper = new ModelMapper(); + this.AddMappings(modelMapper); + this.AddMapping(modelMapper.CompileMappingForAllExplicitlyAddedEntities()); + } + + #endregion Constructors + + #region Methods + + protected virtual void AddMappings(ModelMapper modelMapper) + { + modelMapper.AddMapping(); + modelMapper.AddMapping(); + } + + + #endregion Methods + + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/IHealthcareConnectionFactory.cs b/Src/DDD.HealthcareDelivery/Infrastructure/IHealthcareConnectionFactory.cs deleted file mode 100644 index f295737..0000000 --- a/Src/DDD.HealthcareDelivery/Infrastructure/IHealthcareConnectionFactory.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace DDD.HealthcareDelivery.Infrastructure -{ - using Core.Infrastructure.Data; - - /// - /// Defines a connection factory for the context of healthcare delivery. - /// - public interface IHealthcareConnectionFactory : IDbConnectionFactory - { - } -} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareContext.cs b/Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareContext.cs deleted file mode 100644 index cb09401..0000000 --- a/Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareContext.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Data.Entity; -using System.Data.Common; -using System.Reflection; - -namespace DDD.HealthcareDelivery.Infrastructure -{ - using Oracle.ManagedDataAccess.Client; - using Prescriptions; - using Core.Infrastructure.Data; - using Core.Domain; - - public class OracleHealthcareContext : HealthcareContext - { - - #region Constructors - - public OracleHealthcareContext(DbConnection connection, bool contextOwnsConnection) - : base(connection, contextOwnsConnection) - { - this.UseUpperCase = true; - } - - #endregion Constructors - - #region Methods - - protected override void OnModelCreating(DbModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - if (this.UseUpperCase) - modelBuilder.ApplyAllUpperCaseConventions(false, IsIgnoredProperty); - this.AddConfigurations(modelBuilder); - } - - private static bool IsIgnoredProperty(PropertyInfo property) - { - if (typeof(IStateEntity).IsAssignableFrom(property.DeclaringType) && property.Name == nameof(IStateEntity.EntityState)) - return true; - return false; - } - - private void AddConfigurations(DbModelBuilder modelBuilder) - { - var connectionBuilder = new OracleConnectionStringBuilder(this.Database.Connection.ConnectionString); - modelBuilder.HasDefaultSchema(connectionBuilder.UserID); - modelBuilder.Configurations.Add(new OracleEventStateConfiguration(this.UseUpperCase)); - modelBuilder.Configurations.Add(new OraclePrescriptionStateConfiguration(this.UseUpperCase)); - } - - #endregion Methods - - } -} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareContextFactory.cs b/Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareContextFactory.cs deleted file mode 100644 index ccee0dc..0000000 --- a/Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareContextFactory.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Conditions; -using System.Threading.Tasks; - -namespace DDD.HealthcareDelivery.Infrastructure -{ - using Threading; - using Core.Infrastructure.Data; - - public class OracleHealthcareContextFactory : IAsyncDbContextFactory - { - - #region Fields - - private readonly IHealthcareConnectionFactory connectionFactory; - - #endregion Fields - - #region Constructors - - public OracleHealthcareContextFactory(IHealthcareConnectionFactory connectionFactory) - { - Condition.Requires(connectionFactory, nameof(connectionFactory)).IsNotNull(); - this.connectionFactory = connectionFactory; - } - - #endregion Constructors - - #region Methods - - public async Task CreateContextAsync() - { - await new SynchronizationContextRemover(); - return new OracleHealthcareContext(await this.connectionFactory.CreateOpenConnectionAsync(), true); - } - - #endregion Methods - - } -} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareDeliveryConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareDeliveryConfiguration.cs new file mode 100644 index 0000000..bbc2653 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareDeliveryConfiguration.cs @@ -0,0 +1,30 @@ +using NHibernate.Mapping.ByCode; + +namespace DDD.HealthcareDelivery.Infrastructure +{ + using Core.Infrastructure.Data; + + public abstract class OracleHealthcareDeliveryConfiguration : HealthcareDeliveryConfiguration + { + + #region Constructors + + protected OracleHealthcareDeliveryConfiguration() + { + this.SetNamingStrategy(UpperCaseNamingStrategy.Instance); + } + + #endregion Constructors + + #region Methods + + protected override void AddMappings(ModelMapper modelMapper) + { + base.AddMappings(modelMapper); + modelMapper.AddMapping(); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/Alpha2CountryCodeType.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/Alpha2CountryCodeType.cs new file mode 100644 index 0000000..5b5f3ca --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/Alpha2CountryCodeType.cs @@ -0,0 +1,21 @@ +using NHibernate; + +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Core.Infrastructure.Data; + using Common.Domain; + + internal class Alpha2CountryCodeType : CompositeUserType + { + + #region Constructors + + public Alpha2CountryCodeType() + { + this.Mutable(false); + this.Property(c => c.Value, NHibernateUtil.AnsiString); + } + + #endregion Constructors + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/BelgianCreatePharmaceuticalPrescriptionValidator.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/BelgianCreatePharmaceuticalPrescriptionValidator.cs index f992ae4..77dd658 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/BelgianCreatePharmaceuticalPrescriptionValidator.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/BelgianCreatePharmaceuticalPrescriptionValidator.cs @@ -28,10 +28,6 @@ public BelgianCreatePharmaceuticalPrescriptionValidator() RuleFor(c => c.PatientLastName).NotEmpty().WithErrorCode("PatientLastNameEmpty"); RuleFor(c => c.PatientSocialSecurityNumber).Length(11).WithErrorCode("PatientSocialSecurityNumberInvalid") .Numeric().WithErrorCode("PatientSocialSecurityNumberInvalid"); - RuleFor(c => c.FacilityIdentifier).GreaterThan(0).WithErrorCode("FacilityIdentifierInvalid"); - RuleFor(c => c.FacilityName).NotEmpty().WithErrorCode("FacilityNameEmpty"); - RuleFor(c => c.FacilityLicenseNumber).Length(8).WithErrorCode("FacilityLicenseNumberInvalid") - .Numeric().WithErrorCode("FacilityLicenseNumberInvalid"); RuleFor(p => p.PrescriptionIdentifier).GreaterThan(0).WithErrorCode("PrescriptionIdentifierInvalid"); RuleFor(p => p.Medications).NotEmpty().WithErrorCode("MedicationsEmpty") .MaximumCount(10).WithErrorCode("MedicationsCountInvalid"); diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/ContactInformationType.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/ContactInformationType.cs new file mode 100644 index 0000000..9e64f0c --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/ContactInformationType.cs @@ -0,0 +1,27 @@ +using NHibernate; + +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Common.Domain; + using Core.Infrastructure.Data; + + internal class ContactInformationType : CompositeUserType + { + + #region Constructors + + public ContactInformationType() + { + this.Mutable(false); + this.Property(c => c.PrimaryTelephoneNumber, NHibernateUtil.AnsiString); + this.Property(c => c.SecondaryTelephoneNumber, NHibernateUtil.AnsiString); + this.Property(c => c.PrimaryEmailAddress, NHibernateUtil.Custom(typeof(EmailAddressType))); + this.Property(c => c.SecondaryEmailAddress, NHibernateUtil.Custom(typeof(EmailAddressType))); + this.Property(c => c.WebSite, new AnsiUriType()); + this.Property(c => c.PostalAddress, NHibernateUtil.Custom(typeof(PostalAddressType))); + } + + #endregion Constructors + + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/EmailAddressType.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/EmailAddressType.cs new file mode 100644 index 0000000..1e40515 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/EmailAddressType.cs @@ -0,0 +1,21 @@ +using NHibernate; + +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Core.Infrastructure.Data; + using Common.Domain; + + internal class EmailAddressType : CompositeUserType + { + + #region Constructors + + public EmailAddressType() + { + this.Mutable(false); + this.Property(a => a.Value, NHibernateUtil.AnsiString); + } + + #endregion Constructors + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/FullNameType.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/FullNameType.cs new file mode 100644 index 0000000..13ca999 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/FullNameType.cs @@ -0,0 +1,23 @@ +using NHibernate; + +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Common.Domain; + using Core.Infrastructure.Data; + + internal class FullNameType : CompositeUserType + { + + #region Constructors + + public FullNameType() + { + this.Mutable(false); + this.Property(n => n.LastName, NHibernateUtil.AnsiString); + this.Property(n => n.FirstName, NHibernateUtil.AnsiString); + } + + #endregion Constructors + + } +} \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthcarePractitionerLicenseNumberType.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthcarePractitionerLicenseNumberType.cs new file mode 100644 index 0000000..7547cc4 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthcarePractitionerLicenseNumberType.cs @@ -0,0 +1,22 @@ +using NHibernate; + +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Core.Infrastructure.Data; + using Domain.Practitioners; + + internal class HealthcarePractitionerLicenseNumberType : CompositeUserType + where T : HealthcarePractitionerLicenseNumber + { + + #region Constructors + + public HealthcarePractitionerLicenseNumberType() + { + this.Mutable(false); + this.Property(p => p.Value, NHibernateUtil.AnsiString); + } + + #endregion Constructors + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthcarePractitionerType.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthcarePractitionerType.cs new file mode 100644 index 0000000..9dace6a --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthcarePractitionerType.cs @@ -0,0 +1,32 @@ +using NHibernate; + +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Common.Domain; + using Core.Infrastructure.Data; + using Domain.Practitioners; + + internal class HealthcarePractitionerType : CompositeUserType + where TPractitionerLicenseNumber : HealthcarePractitionerLicenseNumber + where TSocialSecurityNumber : SocialSecurityNumber + { + #region Constructors + + public HealthcarePractitionerType() + { + this.Mutable(false); + this.Property(p => p.Identifier); + this.Discriminator("PrescriberType"); + this.Property(p => p.FullName, NHibernateUtil.Custom(typeof(FullNameType))); + this.Property(p => p.DisplayName, NHibernateUtil.AnsiString); + this.Property(p => p.LicenseNumber, NHibernateUtil.Custom(typeof(HealthcarePractitionerLicenseNumberType))); + this.Property(p => p.SocialSecurityNumber, NHibernateUtil.Custom(typeof(SocialSecurityNumberType))); + this.Property(p => p.Speciality, NHibernateUtil.AnsiString); + this.Property(p => p.ContactInformation, NHibernateUtil.Custom(typeof(ContactInformationType))); + this.Subclass(); + } + + #endregion Constructors + + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescribedMedicationMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescribedMedicationMapping.cs new file mode 100644 index 0000000..b2dbcfe --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescribedMedicationMapping.cs @@ -0,0 +1,18 @@ +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Domain.Prescriptions; + + internal class OraclePrescribedMedicationMapping : PrescribedMedicationMapping + where TMedicationCode : MedicationCode + { + #region Constructors + + public OraclePrescribedMedicationMapping() + { + // Fields + this.Discriminator(m => m.Column(m1 => m1.SqlType("varchar2(20)"))); + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionMapping.cs new file mode 100644 index 0000000..6f7292d --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionMapping.cs @@ -0,0 +1,22 @@ +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Common.Domain; + using Domain.Practitioners; + + internal class OraclePrescriptionMapping + : PrescriptionMapping + where TPractitionerLicenseNumber : HealthcarePractitionerLicenseNumber + where TSocialSecurityNumber : SocialSecurityNumber + where TSex : Sex + { + #region Constructors + + public OraclePrescriptionMapping() + { + // Fields + this.Discriminator(m => m.Column(m1 => m1.SqlType("varchar2(5)"))); + } + + #endregion Constructors + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionStateConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionStateConfiguration.cs deleted file mode 100644 index 63d0087..0000000 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionStateConfiguration.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions -{ - internal class OraclePrescriptionStateConfiguration : PrescriptionStateConfiguration - { - - #region Constructors - - public OraclePrescriptionStateConfiguration(bool useUpperCase) : base(useUpperCase) - { - // Table - this.Map(p => - { - p.ToTable(ToCasingConvention(TableName)); - p.Requires(ToCasingConvention(Discriminator)) - .HasValue(string.Empty) - .HasColumnOrder(2) - .HasColumnType("varchar2") - .HasMaxLength(5); - }); - // Fields - this.Property(p => p.CreatedOn) - .HasColumnType("date"); - this.Property(p => p.DelivrableAt) - .HasColumnType("date"); - this.Property(p => p.Patient.Birthdate) - .HasColumnType("date"); - } - - #endregion Constructors - - } -} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionMapping.cs new file mode 100644 index 0000000..252a358 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionMapping.cs @@ -0,0 +1,35 @@ +using NHibernate.Mapping.ByCode; +using NHibernate.Mapping.ByCode.Conformist; + +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Domain.Prescriptions; + + internal class PharmaceuticalPrescriptionMapping : SubclassMapping + { + + #region Constructors + + public PharmaceuticalPrescriptionMapping() + { + this.Lazy(false); + // Fields + this.DiscriminatorValue("PHARM"); + // Associations + this.Set("prescribedMedications", + m => + { + m.Key(m1 => + { + m1.Column("PrescriptionId"); + m1.NotNullable(true); + }); + m.Cascade(Cascade.All); + }, + r => r.OneToMany(m => m.Class(typeof(PrescribedMedication)))); + } + + #endregion Constructors + + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionRepository.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionRepository.cs index b5fbb8e..b3f183d 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionRepository.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionRepository.cs @@ -1,37 +1,23 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; - -namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions { using Mapping; using Core.Domain; + using Core.Application; using Domain.Prescriptions; using Core.Infrastructure.Data; + using Domain; public class PharmaceuticalPrescriptionRepository - : EFRepository + : NHRepository { - #region Constructors - public PharmaceuticalPrescriptionRepository(IObjectTranslator prescriptionTranslator, - IObjectTranslator eventTranslator, - IAsyncDbContextFactory contextFactory) - : base(prescriptionTranslator, eventTranslator, contextFactory) + public PharmaceuticalPrescriptionRepository(ISessionFactory sessionFactory, + IObjectTranslator eventTranslator) + : base(sessionFactory, eventTranslator) { } #endregion Constructors - - #region Methods - - protected override IEnumerable>> RelatedEntitiesPaths() - { - yield return p => p.PrescribedMedications; - } - - #endregion Methods - } } diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionStateConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionStateConfiguration.cs deleted file mode 100644 index 1e39995..0000000 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionStateConfiguration.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Data.Entity.ModelConfiguration; - -namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions -{ - using Domain.Prescriptions; - - internal class PharmaceuticalPrescriptionStateConfiguration : EntityTypeConfiguration - { - - #region Fields - - private readonly bool useUpperCase; - - #endregion Fields - - #region Constructors - - public PharmaceuticalPrescriptionStateConfiguration(bool useUpperCase) - { - this.useUpperCase = useUpperCase; - // Table - this.Map(p => { p.Requires(ToCasingConvention(PrescriptionStateConfiguration.Discriminator)).HasValue("PHARM"); }); - // Relationships - this.HasMany(p => p.PrescribedMedications).WithRequired().HasForeignKey(m => m.PrescriptionIdentifier); - } - - #endregion Constructors - - #region Methods - - protected string ToCasingConvention(string name) => this.useUpperCase ? name.ToUpperInvariant() : name; - - #endregion Methods - - } -} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinder.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinder.cs index 5ed043e..8228e70 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinder.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinder.cs @@ -1,36 +1,88 @@ -using System.Collections.Generic; -using Dapper; -using System.Data; +using EnsureThat; +using System; using System.Threading.Tasks; +using System.Collections.Generic; +using Dapper; namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions { + using Domain; using Application.Prescriptions; + using Core.Application; using Core.Infrastructure.Data; + using Mapping; + using Threading; public class PharmaceuticalPrescriptionsByPatientFinder - : DbQueryHandler> + : IQueryHandler> { + #region Fields + + private readonly IDbConnectionProvider connectionProvider; + private readonly CompositeTranslator exceptionTranslator; + + #endregion Fields + #region Constructors - public PharmaceuticalPrescriptionsByPatientFinder(IHealthcareConnectionFactory connectionFactory) - : base(connectionFactory) + public PharmaceuticalPrescriptionsByPatientFinder(IDbConnectionProvider connectionProvider) + { + Ensure.That(connectionProvider, nameof(connectionProvider)).IsNotNull(); + this.connectionProvider = connectionProvider; + this.exceptionTranslator = new CompositeTranslator(); + this.exceptionTranslator.Register(new DbToQueryExceptionTranslator()); + this.exceptionTranslator.RegisterFallback(); + } + + #endregion Constructors + + #region Methods + + public IEnumerable Handle(FindPharmaceuticalPrescriptionsByPatient query, IMessageContext context) { + Ensure.That(query, nameof(query)).IsNotNull(); + try + { + var connection = this.connectionProvider.GetOpenConnection(); + var expressions = connection.Expressions(); + return connection.Query(SqlScripts.FindPharmaceuticalPrescriptionsByPatient.Replace("@", expressions.ParameterPrefix()), + new { PatientId = query.PatientIdentifier }); + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Query = query }); + } } - protected override Task> ExecuteAsync(FindPharmaceuticalPrescriptionsByPatient query, - IDbConnection connection) + public async Task> HandleAsync(FindPharmaceuticalPrescriptionsByPatient query, IMessageContext context) { - var expressions = connection.Expressions(); - return connection.QueryAsync - ( - SqlScripts.FindPharmaceuticalPrescriptionsByPatient.Replace("@", expressions.ParameterPrefix()), - new { PatientId = query.PatientIdentifier } - ); + Ensure.That(query, nameof(query)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + var cancellationToken = context.CancellationToken(); + var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); + var expressions = connection.Expressions(); + return await connection.QueryAsync + ( + new CommandDefinition + ( + SqlScripts.FindPharmaceuticalPrescriptionsByPatient.Replace("@", expressions.ParameterPrefix()), + new { PatientId = query.PatientIdentifier }, + cancellationToken: cancellationToken + ) + ); + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Query = query }); + } + } - #endregion Constructors + #endregion Methods } } diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PostalAddressType.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PostalAddressType.cs new file mode 100644 index 0000000..93e3adb --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PostalAddressType.cs @@ -0,0 +1,26 @@ +using NHibernate; + +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Common.Domain; + using Core.Infrastructure.Data; + + internal class PostalAddressType : CompositeUserType + { + #region Constructors + + public PostalAddressType() + { + this.Mutable(false); + this.Property(a => a.Street, NHibernateUtil.AnsiString); + this.Property(a => a.HouseNumber, NHibernateUtil.AnsiString); + this.Property(a => a.BoxNumber, NHibernateUtil.AnsiString); + this.Property(a => a.PostalCode, NHibernateUtil.AnsiString); + this.Property(a => a.City, NHibernateUtil.AnsiString); + this.Property(a => a.CountryCode, NHibernateUtil.Custom(typeof(Alpha2CountryCodeType))); + } + + #endregion Constructors + + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationMapping.cs new file mode 100644 index 0000000..528e006 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationMapping.cs @@ -0,0 +1,61 @@ +using NHibernate.Mapping.ByCode.Conformist; + +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Domain.Prescriptions; + using NHibernate; + using NHibernate.Mapping.ByCode; + + internal abstract class PrescribedMedicationMapping : ClassMapping + where TMedicationCode : MedicationCode + { + + #region Constructors + + protected PrescribedMedicationMapping() + { + this.Lazy(false); + // Table + this.Table("PrescMedication"); + // Keys + this.Id("identifier", m => + { + m.Column("PrescMedicationId"); + m.Generator(Generators.Sequence, m1 => m1.Params(new { sequence = "PrescMedicationId" })); + }); + // Fields + this.Discriminator(m => + { + m.Column("MedicationType"); + m.Length(20); + m.NotNullable(true); + }); + this.Property(med => med.NameOrDescription, m => + { + m.Column("NameOrDesc"); + m.Type(NHibernateUtil.AnsiString); + m.Length(1024); + m.NotNullable(true); + }); + this.Property(med => med.Posology, m => + { + m.Type(NHibernateUtil.AnsiString); + m.Length(1024); + }); + this.Property(med => med.Quantity); + this.Component(med => med.Code, m => + { + m.Class(); + m.Property(c => c.Value, m1 => + { + m1.Column("Code"); + m1.Type(NHibernateUtil.AnsiString); + m1.Length(20); + }); + }); + } + + #endregion Constructors + + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationStateConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationStateConfiguration.cs deleted file mode 100644 index ae2d2a5..0000000 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationStateConfiguration.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Data.Entity.ModelConfiguration; -using System.ComponentModel.DataAnnotations.Schema; - -namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions -{ - using Domain.Prescriptions; - - internal class PrescribedMedicationStateConfiguration : EntityTypeConfiguration - { - - #region Fields - - private readonly bool useUpperCase; - - #endregion Fields - - #region Constructors - - public PrescribedMedicationStateConfiguration(bool useUpperCase) - { - this.useUpperCase = useUpperCase; - // Table - this.ToTable(ToCasingConvention("PrescMedication")); - // Keys - this.HasKey(m => m.Identifier); - // Fields - this.Property(m => m.Identifier) - .HasColumnName(ToCasingConvention("PrescMedicationId")) - .HasColumnOrder(1) - .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None); - this.Property(m => m.PrescriptionIdentifier) - .HasColumnName(ToCasingConvention("PrescriptionId")) - .HasColumnOrder(2); - this.Property(m => m.MedicationType) - .HasColumnOrder(3) - .IsUnicode(false) - .HasMaxLength(20) - .IsRequired(); - this.Property(m => m.NameOrDescription) - .HasColumnName(ToCasingConvention("NameOrDesc")) - .HasColumnOrder(4) - .IsUnicode(false) - .HasMaxLength(1024) - .IsRequired(); - this.Property(m => m.Posology) - .HasColumnOrder(5) - .IsUnicode(false) - .HasMaxLength(1024); - this.Property(m => m.Quantity) - .HasColumnOrder(6) - .IsUnicode(false) - .HasMaxLength(100); ; - this.Property(m => m.Duration) - .HasColumnOrder(7) - .IsUnicode(false) - .HasMaxLength(100); - this.Property(m => m.Code) - .HasColumnOrder(8) - .IsUnicode(false) - .HasMaxLength(20); - } - - #endregion Constructors - - #region Methods - - protected string ToCasingConvention(string name) => this.useUpperCase ? name.ToUpperInvariant() : name; - - #endregion Methods - - } -} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinder.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinder.cs index f539bf6..88195ea 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinder.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinder.cs @@ -1,37 +1,84 @@ -using System.Collections.Generic; -using Dapper; -using System.Data; +using EnsureThat; +using System; using System.Threading.Tasks; +using System.Collections.Generic; +using Dapper; namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions { + using Domain; using Application.Prescriptions; + using Core.Application; using Core.Infrastructure.Data; + using Mapping; + using Threading; public class PrescribedMedicationsByPrescriptionFinder - : DbQueryHandler> + : IQueryHandler> { + #region Fields + + private readonly IDbConnectionProvider connectionProvider; + private readonly CompositeTranslator exceptionTranslator; + + #endregion Fields + #region Constructors - public PrescribedMedicationsByPrescriptionFinder(IHealthcareConnectionFactory connectionFactory) - : base(connectionFactory) + public PrescribedMedicationsByPrescriptionFinder(IDbConnectionProvider connectionProvider) { + Ensure.That(connectionProvider, nameof(connectionProvider)).IsNotNull(); + this.connectionProvider = connectionProvider; + this.exceptionTranslator = new CompositeTranslator(); + this.exceptionTranslator.Register(new DbToQueryExceptionTranslator()); + this.exceptionTranslator.RegisterFallback(); } #endregion Constructors #region Methods - protected override Task> ExecuteAsync(FindPrescribedMedicationsByPrescription query, - IDbConnection connection) + public IEnumerable Handle(FindPrescribedMedicationsByPrescription query, IMessageContext context) + { + Ensure.That(query, nameof(query)).IsNotNull(); + try + { + var connection = this.connectionProvider.GetOpenConnection(); + var expressions = connection.Expressions(); + return connection.Query(SqlScripts.FindPrescribedMedicationsByPrescription.Replace("@", expressions.ParameterPrefix()), + new { PrescriptionId = query.PrescriptionIdentifier }); + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Query = query }); + } + } + + public async Task> HandleAsync(FindPrescribedMedicationsByPrescription query, IMessageContext context) { - var expressions = connection.Expressions(); - return connection.QueryAsync - ( - SqlScripts.FindPrescribedMedicationsByPrescription.Replace("@", expressions.ParameterPrefix()), - new { PrescriptionId = query.PrescriptionIdentifier } - ); + Ensure.That(query, nameof(query)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + var cancellationToken = context.CancellationToken(); + var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); + var expressions = connection.Expressions(); + return await connection.QueryAsync + ( + new CommandDefinition + ( + SqlScripts.FindPrescribedMedicationsByPrescription.Replace("@", expressions.ParameterPrefix()), + new { PrescriptionId = query.PrescriptionIdentifier }, + cancellationToken: cancellationToken + ) + ); + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Query = query }); + } } #endregion Methods diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedPharmaceuticalCompoundingMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedPharmaceuticalCompoundingMapping.cs new file mode 100644 index 0000000..326c05d --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedPharmaceuticalCompoundingMapping.cs @@ -0,0 +1,18 @@ +using NHibernate.Mapping.ByCode.Conformist; + +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Domain.Prescriptions; + + internal class PrescribedPharmaceuticalCompoundingMapping : SubclassMapping + { + + public PrescribedPharmaceuticalCompoundingMapping() + { + this.Lazy(false); + // Fields + this.DiscriminatorValue("Compounding"); + } + + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedPharmaceuticalProductMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedPharmaceuticalProductMapping.cs new file mode 100644 index 0000000..a811780 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedPharmaceuticalProductMapping.cs @@ -0,0 +1,18 @@ +using NHibernate.Mapping.ByCode.Conformist; + +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Domain.Prescriptions; + + internal class PrescribedPharmaceuticalProductMapping : SubclassMapping + { + + public PrescribedPharmaceuticalProductMapping() + { + this.Lazy(false); + // Fields + this.DiscriminatorValue("Product"); + } + + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedPharmaceuticalSubstanceMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedPharmaceuticalSubstanceMapping.cs new file mode 100644 index 0000000..fbeb2ad --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedPharmaceuticalSubstanceMapping.cs @@ -0,0 +1,18 @@ +using NHibernate.Mapping.ByCode.Conformist; + +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Domain.Prescriptions; + + internal class PrescribedPharmaceuticalSubstanceMapping : SubclassMapping + { + + public PrescribedPharmaceuticalSubstanceMapping() + { + this.Lazy(false); + // Fields + this.DiscriminatorValue("Substance"); + } + + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionIdentifierGenerator.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionIdentifierGenerator.cs index 726c0fc..a1c9b02 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionIdentifierGenerator.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionIdentifierGenerator.cs @@ -1,28 +1,71 @@ -using System.Data; +using EnsureThat; +using System; using System.Threading.Tasks; namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions { - using Core.Infrastructure.Data; + using Domain; using Application.Prescriptions; + using Core.Application; + using Core.Infrastructure.Data; + using DDD; + using Mapping; + using Threading; - public class PrescriptionIdentifierGenerator : DbQueryHandler + public class PrescriptionIdentifierGenerator : IQueryHandler { + #region Fields + + private readonly IDbConnectionProvider connectionProvider; + private readonly CompositeTranslator exceptionTranslator; + + #endregion Fields + #region Constructors - public PrescriptionIdentifierGenerator(IHealthcareConnectionFactory connectionFactory) - : base(connectionFactory) + public PrescriptionIdentifierGenerator(IDbConnectionProvider connectionProvider) { + Ensure.That(connectionProvider, nameof(connectionProvider)).IsNotNull(); + this.connectionProvider = connectionProvider; + this.exceptionTranslator = new CompositeTranslator(); + this.exceptionTranslator.Register(new DbToQueryExceptionTranslator()); + this.exceptionTranslator.RegisterFallback(); } #endregion Constructors #region Methods - protected override Task ExecuteAsync(GeneratePrescriptionIdentifier query, IDbConnection connection) + public int Handle(GeneratePrescriptionIdentifier query, IMessageContext context) + { + Ensure.That(query, nameof(query)).IsNotNull(); + try + { + var connection = this.connectionProvider.GetOpenConnection(); + return connection.NextValue("PrescriptionId"); + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Query = query }); + } + } + + public async Task HandleAsync(GeneratePrescriptionIdentifier query, IMessageContext context) { - return connection.NextValueAsync("PrescriptionId"); + Ensure.That(query, nameof(query)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + var cancellationToken = context.CancellationToken(); + var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); + return await connection.NextValueAsync("PrescriptionId", null, cancellationToken); + } + catch (Exception ex) when (ex.ShouldBeWrappedIn()) + { + throw this.exceptionTranslator.Translate(ex, new { Query = query }); + } } #endregion Methods diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs new file mode 100644 index 0000000..7d2b606 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs @@ -0,0 +1,232 @@ +using NHibernate; +using ByCode = NHibernate.Mapping.ByCode; +using NHibernate.Mapping.ByCode.Conformist; + +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Common.Domain; + using Common.Infrastructure.Data; + using Domain.Prescriptions; + using Domain.Practitioners; + + + internal abstract class PrescriptionMapping + : ClassMapping + where TPractitionerLicenseNumber : HealthcarePractitionerLicenseNumber + where TSocialSecurityNumber : SocialSecurityNumber + where TSex : Sex + { + + #region Constructors + + protected PrescriptionMapping() + { + this.Lazy(false); + this.SchemaAction(ByCode.SchemaAction.None); + // Table + this.Table("Prescription"); + // Keys + this.ComponentAsId(p => p.Identifier, m1 => + m1.Property(i => i.Value, m2 => + { + m2.Column("PrescriptionId"); + })); + // Fields + this.Discriminator(m => + { + m.Column("PrescriptionType"); + m.Length(5); + m.NotNullable(true); + }); + this.Property(p => p.Status, m => + { + m.Type(new EnumerationCodeType()); + m.Length(3); + m.NotNullable(true); + m.Column(m1 => m1.SqlType("char(3)")); + }); + this.Component(p => p.LanguageCode, m1 => + m1.Property(l => l.Value, m2 => + { + m2.Column(m3 => + { + m3.Name("Language"); + m3.SqlType("char(2)"); + }); + m2.Type(NHibernateUtil.AnsiString); + m2.Length(2); + m2.NotNullable(true); + })); + this.Property(p => p.CreatedOn, m => + { + m.Type(NHibernateUtil.Date); + m.NotNullable(true); + }); + this.Property(p => p.DeliverableAt, m => m.Type(NHibernateUtil.Date)); + // Prescriber + this.Property(p => p.Prescriber, m => + { + m.Type>(); + m.Columns + (m1 => + { + m1.Name("PrescriberId"); + m1.NotNullable(true); + }, + m1 => + { + m1.Name("PrescriberType"); + m1.Length(20); + m1.NotNullable(true); + }, + m1 => + { + m1.Name("PrescriberLastName"); + m1.Length(50); + m1.NotNullable(true); + }, + m1 => + { + m1.Name("PrescriberFirstName"); + m1.Length(50); + m1.NotNullable(true); + }, + m1 => + { + m1.Name("PrescriberDisplayName"); + m1.Length(100); + m1.NotNullable(true); + }, + m1 => + { + m1.Name("PrescriberLicenseNum"); + m1.Length(25); + m1.NotNullable(true); + }, + m1 => + { + m1.Name("PrescriberSSN"); + m1.Length(25); + }, + m1 => + { + m1.Name("PrescriberSpeciality"); + m1.Length(50); + }, + m1 => + { + m1.Name("PrescriberPhone1"); + m1.Length(20); + }, + m1 => + { + m1.Name("PrescriberPhone2"); + m1.Length(20); + }, + m1 => + { + m1.Name("PrescriberEmail1"); + m1.Length(50); + }, + m1 => + { + m1.Name("PrescriberEmail2"); + m1.Length(50); + }, + m1 => + { + m1.Name("PrescriberWebSite"); + m1.Length(255); + }, + m1 => + { + m1.Name("PrescriberStreet"); + m1.Length(50); + }, + m1 => + { + m1.Name("PrescriberHouseNum"); + m1.Length(10); + }, + m1 => + { + m1.Name("PrescriberBoxNum"); + m1.Length(10); + }, + m1 => + { + m1.Name("PrescriberPostCode"); + m1.Length(10); + }, + m1 => + { + m1.Name("PrescriberCity"); + m1.Length(50); + }, + m1 => + { + m1.Name("PrescriberCountry"); + m1.Length(2); + m1.SqlType("char(2)"); + }); + }); + // Patient + this.Component(p => p.Patient, m1 => + { + m1.Property(p => p.Identifier, m2 => + { + m2.Column("PatientId"); + m2.NotNullable(true); + }); + m1.Component(p => p.FullName, m2 => + { + m2.Property(n => n.FirstName, m3 => + { + m3.Column("PatientFirstName"); + m3.Type(NHibernateUtil.AnsiString); + m3.Length(50); + m3.NotNullable(true); + }); + m2.Property(n => n.LastName, m3 => + { + m3.Column("PatientLastName"); + m3.Type(NHibernateUtil.AnsiString); + m3.Length(50); + m3.NotNullable(true); + }); + }); + m1.Property(p => p.Sex, m3 => + { + m3.Column("PatientSex"); + m3.Type(new EnumerationCodeType()); + m3.Length(2); + m3.NotNullable(true); + }); + m1.Component(p => p.SocialSecurityNumber, m2 => + { + m2.Class(); + m2.Property(n => n.Value, m3 => + { + m3.Column("PatientSSN"); + m3.Type(NHibernateUtil.AnsiString); + m3.Length(25); + }); + }); + m1.Property(p => p.Birthdate, m2 => + { + m2.Column("PatientBirthdate"); + m2.Type(NHibernateUtil.Date); + }); + }); + // Encounter + this.Component(p => p.EncounterIdentifier, m1 => + m1.Property(i => i.Value, m2 => + { + m2.Column("EncounterId"); + })); + } + + #endregion Constructors + + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionStateConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionStateConfiguration.cs deleted file mode 100644 index 12a3f90..0000000 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionStateConfiguration.cs +++ /dev/null @@ -1,209 +0,0 @@ -using System.Data.Entity.ModelConfiguration; -using System.ComponentModel.DataAnnotations.Schema; - -namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions -{ - using Domain.Prescriptions; - - internal abstract class PrescriptionStateConfiguration : EntityTypeConfiguration - { - - #region Fields - - public const string Discriminator = "PrescriptionType"; - - public const string TableName = "Prescription"; - - private readonly bool useUpperCase; - - #endregion Fields - - #region Constructors - - protected PrescriptionStateConfiguration(bool useUpperCase) - { - this.useUpperCase = useUpperCase; - // Keys - this.HasKey(p => p.Identifier); - // Fields - this.Property(p => p.Identifier) - .HasColumnName(ToCasingConvention("PrescriptionId")) - .HasColumnOrder(1) - .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None); ; - this.Property(p => p.Status) - .HasColumnOrder(3) - .IsUnicode(false) - .HasMaxLength(3) - .IsFixedLength() - .IsRequired(); - this.Property(p => p.LanguageCode) - .HasColumnName(ToCasingConvention("Language")) - .HasColumnOrder(4) - .IsUnicode(false) - .HasMaxLength(2) - .IsFixedLength() - .IsRequired(); - this.Property(p => p.CreatedOn) - .HasColumnOrder(5); - this.Property(p => p.DelivrableAt) - .HasColumnOrder(6); - this.Property(p => p.Prescriber.Identifier) - .HasColumnName(ToCasingConvention("PrescriberId")) - .HasColumnOrder(7); - this.Property(p => p.Prescriber.PractitionerType) - .HasColumnName(ToCasingConvention("PrescriberType")) - .HasColumnOrder(8) - .IsUnicode(false) - .HasMaxLength(20) - .IsRequired(); - this.Property(p => p.Prescriber.FullName.LastName) - .HasColumnName(ToCasingConvention("PrescriberLastName")) - .HasColumnOrder(9) - .IsUnicode(false) - .HasMaxLength(50) - .IsRequired(); - this.Property(p => p.Prescriber.FullName.FirstName) - .HasColumnName(ToCasingConvention("PrescriberFirstName")) - .HasColumnOrder(10) - .IsUnicode(false) - .HasMaxLength(50) - .IsRequired(); - this.Property(p => p.Prescriber.DisplayName) - .HasColumnName(ToCasingConvention("PrescriberDisplayName")) - .HasColumnOrder(11) - .IsUnicode(false) - .HasMaxLength(100) - .IsRequired(); - this.Property(p => p.Prescriber.LicenseNumber) - .HasColumnName(ToCasingConvention("PrescriberLicenseNum")) - .HasColumnOrder(12) - .IsUnicode(false) - .HasMaxLength(25) - .IsRequired(); - this.Property(p => p.Prescriber.SocialSecurityNumber) - .HasColumnName(ToCasingConvention("PrescriberSSN")) - .HasColumnOrder(13) - .IsUnicode(false) - .HasMaxLength(25); - this.Property(p => p.Prescriber.Speciality) - .HasColumnName(ToCasingConvention("PrescriberSpeciality")) - .HasColumnOrder(14) - .IsUnicode(false) - .HasMaxLength(50); - this.Property(p => p.Prescriber.ContactInformation.PrimaryTelephoneNumber) - .HasColumnName(ToCasingConvention("PrescriberPhone1")) - .HasColumnOrder(15) - .IsUnicode(false) - .HasMaxLength(20); - this.Property(p => p.Prescriber.ContactInformation.SecondaryTelephoneNumber) - .HasColumnName(ToCasingConvention("PrescriberPhone2")) - .HasColumnOrder(16) - .IsUnicode(false) - .HasMaxLength(20); - this.Property(p => p.Prescriber.ContactInformation.PrimaryEmailAddress) - .HasColumnName(ToCasingConvention("PrescriberEmail1")) - .HasColumnOrder(17) - .IsUnicode(false) - .HasMaxLength(50); - this.Property(p => p.Prescriber.ContactInformation.SecondaryEmailAddress) - .HasColumnName(ToCasingConvention("PrescriberEmail2")) - .HasColumnOrder(18) - .IsUnicode(false) - .HasMaxLength(50); - this.Property(p => p.Prescriber.ContactInformation.WebSite) - .HasColumnName(ToCasingConvention("PrescriberWebSite")) - .HasColumnOrder(19) - .IsUnicode(false) - .HasMaxLength(255); - this.Property(p => p.Prescriber.ContactInformation.PostalAddress.Street) - .HasColumnName(ToCasingConvention("PrescriberStreet")) - .HasColumnOrder(20) - .IsUnicode(false) - .HasMaxLength(50); - this.Property(p => p.Prescriber.ContactInformation.PostalAddress.HouseNumber) - .HasColumnName(ToCasingConvention("PrescriberHouseNum")) - .HasColumnOrder(21) - .IsUnicode(false) - .HasMaxLength(10); - this.Property(p => p.Prescriber.ContactInformation.PostalAddress.BoxNumber) - .HasColumnName(ToCasingConvention("PrescriberBoxNum")) - .HasColumnOrder(22) - .IsUnicode(false) - .HasMaxLength(10); - this.Property(p => p.Prescriber.ContactInformation.PostalAddress.PostalCode) - .HasColumnName(ToCasingConvention("PrescriberPostCode")) - .HasColumnOrder(23) - .IsUnicode(false) - .HasMaxLength(10); - this.Property(p => p.Prescriber.ContactInformation.PostalAddress.City) - .HasColumnName(ToCasingConvention("PrescriberCity")) - .HasColumnOrder(24) - .IsUnicode(false) - .HasMaxLength(50); - this.Property(p => p.Prescriber.ContactInformation.PostalAddress.CountryCode) - .HasColumnName(ToCasingConvention("PrescriberCountry")) - .HasColumnOrder(25) - .IsUnicode(false) - .HasMaxLength(2) - .IsFixedLength(); - this.Property(p => p.Patient.Identifier) - .HasColumnName(ToCasingConvention("PatientId")) - .HasColumnOrder(26); - this.Property(p => p.Patient.FullName.FirstName) - .HasColumnName(ToCasingConvention("PatientFirstName")) - .HasColumnOrder(27) - .IsUnicode(false) - .HasMaxLength(50) - .IsRequired(); - this.Property(p => p.Patient.FullName.LastName) - .HasColumnName(ToCasingConvention("PatientLastName")) - .HasColumnOrder(28) - .IsUnicode(false) - .HasMaxLength(50) - .IsRequired(); - this.Property(p => p.Patient.Sex) - .HasColumnName(ToCasingConvention("PatientSex")) - .HasColumnOrder(29) - .IsUnicode(false) - .HasMaxLength(2) - .IsRequired(); - this.Property(p => p.Patient.SocialSecurityNumber) - .HasColumnName(ToCasingConvention("PatientSSN")) - .HasColumnOrder(30) - .IsUnicode(false) - .HasMaxLength(25); - this.Property(p => p.Patient.Birthdate) - .HasColumnName(ToCasingConvention("PatientBirthdate")) - .HasColumnOrder(31); - this.Property(p => p.HealthFacility.Identifier) - .HasColumnName(ToCasingConvention("FacilityId")) - .HasColumnOrder(32); - this.Property(p => p.HealthFacility.FacilityType) - .HasColumnName(ToCasingConvention("FacilityType")) - .HasColumnOrder(33) - .IsUnicode(false) - .HasMaxLength(20) - .IsRequired(); - this.Property(p => p.HealthFacility.Name) - .HasColumnName(ToCasingConvention("FacilityName")) - .HasColumnOrder(34) - .IsUnicode(false) - .HasMaxLength(100) - .IsRequired(); - this.Property(p => p.HealthFacility.LicenseNumber) - .HasColumnName(ToCasingConvention("FacilityLicenseNum")) - .HasColumnOrder(35) - .IsUnicode(false) - .HasMaxLength(25); - } - - #endregion Constructors - - #region Methods - - protected string ToCasingConvention(string name) => this.useUpperCase ? name.ToUpperInvariant() : name; - - #endregion Methods - - } -} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SocialSecurityNumberType.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SocialSecurityNumberType.cs new file mode 100644 index 0000000..a08c0b0 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SocialSecurityNumberType.cs @@ -0,0 +1,22 @@ +using NHibernate; + +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Core.Infrastructure.Data; + using Common.Domain; + + internal class SocialSecurityNumberType : CompositeUserType + where T : SocialSecurityNumber + { + + #region Constructors + + public SocialSecurityNumberType() + { + this.Mutable(false); + this.Property(n => n.Value, NHibernateUtil.AnsiString); + } + + #endregion Constructors + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescribedMedicationMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescribedMedicationMapping.cs new file mode 100644 index 0000000..e9efe0d --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescribedMedicationMapping.cs @@ -0,0 +1,18 @@ +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Domain.Prescriptions; + + internal class SqlServerPrescribedMedicationMapping : PrescribedMedicationMapping + where TMedicationCode : MedicationCode + { + #region Constructors + + public SqlServerPrescribedMedicationMapping() + { + // Fields + this.Discriminator(m => m.Column(m1 => m1.SqlType("varchar(20)"))); + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescriptionMapping.cs new file mode 100644 index 0000000..338bacb --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescriptionMapping.cs @@ -0,0 +1,23 @@ +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Common.Domain; + using Domain.Practitioners; + + internal class SqlServerPrescriptionMapping + : PrescriptionMapping + where TPractitionerLicenseNumber : HealthcarePractitionerLicenseNumber + where TSocialSecurityNumber : SocialSecurityNumber + where TSex : Sex + { + #region Constructors + + public SqlServerPrescriptionMapping() + { + // Fields + this.Discriminator(m => m.Column(m1 => m1.SqlType("varchar(5)"))); + this.Property(p => p.CreatedOn, m => m.Column(m1 => m1.SqlType("smalldatetime"))); + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescriptionStateConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescriptionStateConfiguration.cs deleted file mode 100644 index 2f0bf19..0000000 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescriptionStateConfiguration.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions -{ - internal class SqlServerPrescriptionStateConfiguration : PrescriptionStateConfiguration - { - - #region Constructors - - public SqlServerPrescriptionStateConfiguration(bool useUpperCase) : base(useUpperCase) - { - // Table - this.Map(p => - { - p.ToTable(ToCasingConvention(TableName)); - p.Requires(ToCasingConvention(Discriminator)) - .HasValue(string.Empty) - .HasColumnOrder(2) - .HasColumnType("varchar") - .HasMaxLength(5); - }); - // Fields - this.Property(p => p.CreatedOn) - .HasColumnType("smalldatetime"); - this.Property(p => p.DelivrableAt) - .HasColumnType("date"); - this.Property(p => p.Patient.Birthdate) - .HasColumnType("date"); - } - - #endregion Constructors - - } -} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Scripts/FindPharmaceuticalPrescriptionsByPatient.sql b/Src/DDD.HealthcareDelivery/Infrastructure/Scripts/FindPharmaceuticalPrescriptionsByPatient.sql index 2b73a81..ad2c5bc 100644 Binary files a/Src/DDD.HealthcareDelivery/Infrastructure/Scripts/FindPharmaceuticalPrescriptionsByPatient.sql and b/Src/DDD.HealthcareDelivery/Infrastructure/Scripts/FindPharmaceuticalPrescriptionsByPatient.sql differ diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Scripts/FindPrescribedMedicationsByPrescription.sql b/Src/DDD.HealthcareDelivery/Infrastructure/Scripts/FindPrescribedMedicationsByPrescription.sql index 091e56f..0b38771 100644 Binary files a/Src/DDD.HealthcareDelivery/Infrastructure/Scripts/FindPrescribedMedicationsByPrescription.sql and b/Src/DDD.HealthcareDelivery/Infrastructure/Scripts/FindPrescribedMedicationsByPrescription.sql differ diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/SqlScripts.Designer.cs b/Src/DDD.HealthcareDelivery/Infrastructure/SqlScripts.Designer.cs index ad5abe8..1950955 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/SqlScripts.Designer.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/SqlScripts.Designer.cs @@ -19,7 +19,7 @@ namespace DDD.HealthcareDelivery.Infrastructure { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class SqlScripts { @@ -63,15 +63,15 @@ internal SqlScripts() { /// /// Looks up a localized string similar to SELECT PrescriptionId AS Identifier, /// CASE Status - /// WHEN 'CRT' THEN 1 - /// WHEN 'INP' THEN 2 - /// WHEN 'DLV' THEN 3 - /// WHEN 'RVK' THEN 4 - /// WHEN 'EXP' THEN 5 - /// WHEN 'ARC' THEN 6 + /// WHEN 'CRT' THEN 0 + /// WHEN 'INP' THEN 1 + /// WHEN 'DLV' THEN 2 + /// WHEN 'RVK' THEN 3 + /// WHEN 'EXP' THEN 4 + /// WHEN 'ARC' THEN 5 /// END AS Status, /// CreatedOn, - /// DelivrableAt, + /// DeliverableAt, /// PrescriberDisplayName ///FROM Prescription ///WHERE PrescriptionType = 'PHARM' @@ -89,7 +89,6 @@ internal static string FindPharmaceuticalPrescriptionsByPatient { /// NameOrDesc AS NameOrDescription, /// Posology, /// Quantity, - /// Duration, /// Code ///FROM PrescMedication ///WHERE PrescriptionId = @PrescriptionId diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareContext.cs b/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareContext.cs deleted file mode 100644 index e31e43f..0000000 --- a/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareContext.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Data.Entity; -using System.Data.Common; - -namespace DDD.HealthcareDelivery.Infrastructure -{ - using Core.Infrastructure.Data; - using Prescriptions; - - public class SqlServerHealthcareContext : HealthcareContext - { - - #region Constructors - - public SqlServerHealthcareContext(DbConnection connection, bool contextOwnsConnection) - : base(connection, contextOwnsConnection) - { - this.UseUpperCase = false; - } - - #endregion Constructors - - #region Methods - - protected override void OnModelCreating(DbModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - this.AddConfigurations(modelBuilder); - } - - private void AddConfigurations(DbModelBuilder modelBuilder) - { - modelBuilder.HasDefaultSchema("dbo"); - modelBuilder.Configurations.Add(new SqlServerEventStateConfiguration(this.UseUpperCase)); - modelBuilder.Configurations.Add(new SqlServerPrescriptionStateConfiguration(this.UseUpperCase)); - } - - #endregion Methods - - } -} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareContextFactory.cs b/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareContextFactory.cs deleted file mode 100644 index 54ec753..0000000 --- a/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareContextFactory.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Conditions; -using System.Threading.Tasks; - -namespace DDD.HealthcareDelivery.Infrastructure -{ - using Threading; - using Core.Infrastructure.Data; - - public class SqlServerHealthcareContextFactory : IAsyncDbContextFactory - { - - #region Fields - - private readonly IHealthcareConnectionFactory connectionFactory; - - #endregion Fields - - #region Constructors - - public SqlServerHealthcareContextFactory(IHealthcareConnectionFactory connectionFactory) - { - Condition.Requires(connectionFactory, nameof(connectionFactory)).IsNotNull(); - this.connectionFactory = connectionFactory; - } - - #endregion Constructors - - #region Methods - - public async Task CreateContextAsync() - { - await new SynchronizationContextRemover(); - return new SqlServerHealthcareContext(await this.connectionFactory.CreateOpenConnectionAsync(), true); - } - - #endregion Methods - - } -} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareDeliveryConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareDeliveryConfiguration.cs new file mode 100644 index 0000000..4fbf6ee --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareDeliveryConfiguration.cs @@ -0,0 +1,20 @@ +using NHibernate.Mapping.ByCode; + +namespace DDD.HealthcareDelivery.Infrastructure +{ + using Core.Infrastructure.Data; + + public abstract class SqlServerHealthcareDeliveryConfiguration : HealthcareDeliveryConfiguration + { + + #region Methods + + protected override void AddMappings(ModelMapper modelMapper) + { + base.AddMappings(modelMapper); + modelMapper.AddMapping(); + } + + #endregion Methods + } +} diff --git a/Src/DDD.HealthcareDelivery/packages.config b/Src/DDD.HealthcareDelivery/packages.config deleted file mode 100644 index 45c7fb3..0000000 --- a/Src/DDD.HealthcareDelivery/packages.config +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/Test/DDD.Common.UnitTests/DDD.Common.UnitTests.csproj b/Test/DDD.Common.UnitTests/DDD.Common.UnitTests.csproj index 04ec23a..ea8fd5d 100644 --- a/Test/DDD.Common.UnitTests/DDD.Common.UnitTests.csproj +++ b/Test/DDD.Common.UnitTests/DDD.Common.UnitTests.csproj @@ -1,111 +1,32 @@ - - - - - + - Debug - AnyCPU - {25F6B88A-4EFA-4516-BB7A-34ED68548636} + net48;net6.0 Library - Properties DDD.Common - DDD.Common.UnitTests - v4.7.2 - 512 - - - + false - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - L:\Packages\FluentAssertions.5.6.0\lib\net47\FluentAssertions.dll - - - - - L:\packages\System.ValueTuple.4.5.0\lib\netstandard1.0\System.ValueTuple.dll - True - - - - - - - - - L:\packages\xunit.abstractions.2.0.3\lib\net35\xunit.abstractions.dll - True - - - L:\packages\xunit.assert.2.4.1\lib\netstandard1.1\xunit.assert.dll - - - L:\packages\xunit.extensibility.core.2.4.1\lib\net452\xunit.core.dll - - - L:\packages\xunit.extensibility.execution.2.4.1\lib\net452\xunit.execution.desktop.dll - - - - - - - - - - - - - - + + - - {0b70b4fd-f5a0-4a6c-a3fd-90031e08c1c2} - DDD.Common - - - {c6c3e419-b9aa-44ad-9dbf-789294687ae6} - DDD.Core - + + 6.9.0 + + + + + + 4.5.0 + + + + 2.4.5 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - - \ No newline at end of file diff --git a/Test/DDD.Common.UnitTests/Domain/EnumerationTests.cs b/Test/DDD.Common.UnitTests/Domain/EnumerationTests.cs index 36f590b..530dc70 100644 --- a/Test/DDD.Common.UnitTests/Domain/EnumerationTests.cs +++ b/Test/DDD.Common.UnitTests/Domain/EnumerationTests.cs @@ -110,12 +110,12 @@ public static IEnumerable ValuesAndRespectiveEnumerations() } [Fact] - public void All_WhenCalled_ReturnsAllConstants() + public void AllInstances_WhenCalled_ReturnsExpectedInstances() { // Act - var all = Enumeration.All(); + var all = Enumeration.AllInstances(); // Assert - all.Should().BeEquivalentTo(FakeEnumeration.Fake1, FakeEnumeration.Fake2, FakeEnumeration.Fake3); + all.Should().BeEquivalentTo(new[] { FakeEnumeration.Fake1, FakeEnumeration.Fake2, FakeEnumeration.Fake3 }); } [Theory] @@ -239,7 +239,7 @@ public void TryParseCode_WhenValidCode_ReturnsTrue(string code, bool ignoreCase) public void TryParseName_WhenInvalidName_ReturnsFalse(string name, bool ignoreCase) { // Act - var success = Enumeration.TryParseName(name, ignoreCase, out var result); + var success = Enumeration.TryParseName(name, ignoreCase, out _); // Assert success.Should().BeFalse(); } @@ -261,7 +261,7 @@ public void TryParseName_WhenValidName_HasExpectedEnumeration(string name, bool public void TryParseName_WhenValidName_ReturnsTrue(string name, bool ignoreCase) { // Act - var success = Enumeration.TryParseName(name, ignoreCase, out var result); + var success = Enumeration.TryParseName(name, ignoreCase, out _); // Assert success.Should().BeTrue(); } @@ -272,7 +272,7 @@ public void TryParseName_WhenValidName_ReturnsTrue(string name, bool ignoreCase) public void TryParseValue_WhenInvalidValue_ReturnsFalse(int value) { // Act - var success = Enumeration.TryParseValue(value, out var result); + var success = Enumeration.TryParseValue(value, out _); // Assert success.Should().BeFalse(); } @@ -293,7 +293,7 @@ public void TryParseValue_WhenValidValue_HasExpectedEnumeration(int value, FakeE public void TryParseValue_WhenValidValue_ReturnsTrue(int value) { // Act - var success = Enumeration.TryParseValue(value, out var result); + var success = Enumeration.TryParseValue(value, out _); // Assert success.Should().BeTrue(); } diff --git a/Test/DDD.Common.UnitTests/Domain/FakeFlagsEnumeration.cs b/Test/DDD.Common.UnitTests/Domain/FakeFlagsEnumeration.cs new file mode 100644 index 0000000..1439ac4 --- /dev/null +++ b/Test/DDD.Common.UnitTests/Domain/FakeFlagsEnumeration.cs @@ -0,0 +1,36 @@ +namespace DDD.Common.Domain +{ + public class FakeFlagsEnumeration : FlagsEnumeration + { + + #region Fields + + public static FakeFlagsEnumeration + + None = new FakeFlagsEnumeration(0, "FFK0", nameof(None)), + Fake1 = new FakeFlagsEnumeration(1, "FFK1", nameof(Fake1)), + Fake2 = new FakeFlagsEnumeration(2, "FFK2", nameof(Fake2)), + Fake3 = new FakeFlagsEnumeration(4, "FFK3", nameof(Fake3)), + Fake4 = new FakeFlagsEnumeration(8, "FFK4", nameof(Fake4)); + + #endregion Fields + + #region Constructors + + private FakeFlagsEnumeration(int value, string code, string name) : base(value, code, name) + { + } + + #endregion Constructors + + #region Methods + + // For tests + public static FakeFlagsEnumeration Create(int value, string code, string name) + { + return new FakeFlagsEnumeration(value, code, name); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/Test/DDD.Common.UnitTests/Domain/FlagsEnumerationTests.cs b/Test/DDD.Common.UnitTests/Domain/FlagsEnumerationTests.cs new file mode 100644 index 0000000..19b97b9 --- /dev/null +++ b/Test/DDD.Common.UnitTests/Domain/FlagsEnumerationTests.cs @@ -0,0 +1,97 @@ +using Xunit; +using System.Collections.Generic; +using FluentAssertions; + +namespace DDD.Common.Domain +{ + public class FlagsEnumerationTests + { + + #region Methods + + public static IEnumerable ValidCodesAndResults() + { + yield return new object[] { "FFK1, FFK3", FakeFlagsEnumeration.Create(5, "FFK1, FFK3", "Fake1, Fake3") }; + yield return new object[] { "FFK3, FFK1", FakeFlagsEnumeration.Create(5, "FFK1, FFK3", "Fake1, Fake3") }; + yield return new object[] { "FFK1, FFK2, FFK3", FakeFlagsEnumeration.Create(7, "FFK1, FFK2, FFK3", "Fake1, Fake2, Fake3") }; + yield return new object[] { "FFK2, FFK1, FFK3", FakeFlagsEnumeration.Create(7, "FFK1, FFK2, FFK3", "Fake1, Fake2, Fake3") }; + yield return new object[] { "FFK2, FFK3, FFK1", FakeFlagsEnumeration.Create(7, "FFK1, FFK2, FFK3", "Fake1, Fake2, Fake3") }; + } + + public static IEnumerable ValidNamesAndResults() + { + yield return new object[] { "Fake1, Fake3", FakeFlagsEnumeration.Create(5, "FFK1, FFK3", "Fake1, Fake3") }; + yield return new object[] { "Fake3, Fake1", FakeFlagsEnumeration.Create(5, "FFK1, FFK3", "Fake1, Fake3") }; + yield return new object[] { "Fake1, Fake2, Fake3", FakeFlagsEnumeration.Create(7, "FFK1, FFK2, FFK3", "Fake1, Fake2, Fake3") }; + yield return new object[] { "Fake2, Fake1, Fake3", FakeFlagsEnumeration.Create(7, "FFK1, FFK2, FFK3", "Fake1, Fake2, Fake3") }; + yield return new object[] { "Fake2, Fake3, Fake1", FakeFlagsEnumeration.Create(7, "FFK1, FFK2, FFK3", "Fake1, Fake2, Fake3") }; + } + + public static IEnumerable ValidValuesAndResults() + { + yield return new object[] { 5, FakeFlagsEnumeration.Create(5, "FFK1, FFK3", "Fake1, Fake3") }; + yield return new object[] { 7, FakeFlagsEnumeration.Create(7, "FFK1, FFK2, FFK3", "Fake1, Fake2, Fake3") }; + } + [Theory] + [MemberData(nameof(ValidCodesAndResults))] + public void ParseCode_WithValidCodes_ReturnsExpectedResult(string code, FakeFlagsEnumeration expectedResult) + { + // Act + var result = FlagsEnumeration.ParseCode(code); + // Assert + result.Should().BeEquivalentTo(expectedResult, o => o.ComparingByMembers()); + } + + [Theory] + [MemberData(nameof(ValidNamesAndResults))] + public void ParseName_WithValidNames_ReturnsExpectedResult(string name, FakeFlagsEnumeration expectedResult) + { + // Act + var result = FlagsEnumeration.ParseName(name); + // Assert + result.Should().BeEquivalentTo(expectedResult, o => o.ComparingByMembers()); + } + + [Theory] + [MemberData(nameof(ValidValuesAndResults))] + public void ParseValue_WithValidValues_ReturnsExpectedResult(int value, FakeFlagsEnumeration expectedResult) + { + // Act + var result = FlagsEnumeration.ParseValue(value); + // Assert + result.Should().BeEquivalentTo(expectedResult, o => o.ComparingByMembers()); + } + + [Theory] + [MemberData(nameof(ValidCodesAndResults))] + public void TryParseCode_WithValidCodes_SetsExpectedResult(string code, FakeFlagsEnumeration expectedResult) + { + // Act + FlagsEnumeration.TryParseCode(code, false, out var result); + // Assert + result.Should().BeEquivalentTo(expectedResult, o => o.ComparingByMembers()); + } + + [Theory] + [MemberData(nameof(ValidNamesAndResults))] + public void TryParseName_WithValidNames_SetsExpectedResult(string name, FakeFlagsEnumeration expectedResult) + { + // Act + FlagsEnumeration.TryParseName(name, false, out var result); + // Assert + result.Should().BeEquivalentTo(expectedResult, o => o.ComparingByMembers()); + } + + [Theory] + [MemberData(nameof(ValidValuesAndResults))] + public void TryParseValue_WithValidValues_SetsExpectedResult(int value, FakeFlagsEnumeration expectedResult) + { + // Act + FlagsEnumeration.TryParseValue(value, out var result); + // Assert + result.Should().BeEquivalentTo(expectedResult, o => o.ComparingByMembers()); + } + + #endregion Methods + } +} diff --git a/Test/DDD.Common.UnitTests/Properties/AssemblyInfo.cs b/Test/DDD.Common.UnitTests/Properties/AssemblyInfo.cs index 1c423aa..a9c147f 100644 --- a/Test/DDD.Common.UnitTests/Properties/AssemblyInfo.cs +++ b/Test/DDD.Common.UnitTests/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Xunit; diff --git a/Test/DDD.Common.UnitTests/app.config b/Test/DDD.Common.UnitTests/app.config deleted file mode 100644 index 81313c0..0000000 --- a/Test/DDD.Common.UnitTests/app.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/Test/DDD.Common.UnitTests/packages.config b/Test/DDD.Common.UnitTests/packages.config deleted file mode 100644 index fcdf1b0..0000000 --- a/Test/DDD.Common.UnitTests/packages.config +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/Test/DDD.Core.Abstractions.UnitTests/AnyArgExtensionsTests.cs b/Test/DDD.Core.Abstractions.UnitTests/AnyArgExtensionsTests.cs new file mode 100644 index 0000000..889b161 --- /dev/null +++ b/Test/DDD.Core.Abstractions.UnitTests/AnyArgExtensionsTests.cs @@ -0,0 +1,52 @@ +using FluentAssertions; +using System; +using Xunit; +using EnsureThat; + +namespace DDD +{ + public class AnyArgExtensionsTests + { + + #region Methods + + [Fact] + public void Satisfy_WhenConditionNotSatisfied_ThrowsArgumentException() + { + // Arrange + var value = -1; + Func predicate = i => i > 0; + // Act + Action satisfy = () => Ensure.Any.Satisfy(value, predicate, nameof(value)); + // Assert + satisfy.Should().ThrowExactly(); + } + + [Fact] + public void Satisfy_WhenConditionNull_DoesNotThrow() + { + // Arrange + var value = 1; + Func predicate = null; + // Act + Action satisfy = () => Ensure.Any.Satisfy(value, predicate, nameof(value)); + // Assert + satisfy.Should().NotThrow(); + } + + [Fact] + public void Satisfy_WhenConditionSatisfied_DoesNotThrow() + { + // Arrange + var value = 1; + Func predicate = i => i > 0; + // Act + Action satisfy = () => Ensure.Any.Satisfy(value, predicate, nameof(value)); + // Assert + satisfy.Should().NotThrow(); + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.Abstractions.UnitTests/DDD.Core.Abstractions.UnitTests.csproj b/Test/DDD.Core.Abstractions.UnitTests/DDD.Core.Abstractions.UnitTests.csproj index 6e946df..5c5de54 100644 --- a/Test/DDD.Core.Abstractions.UnitTests/DDD.Core.Abstractions.UnitTests.csproj +++ b/Test/DDD.Core.Abstractions.UnitTests/DDD.Core.Abstractions.UnitTests.csproj @@ -1,122 +1,35 @@ - - - - - + - Debug - AnyCPU - {9CC062C7-73EA-49B4-B694-31A5F48FD09D} + net48;net6.0 Library - Properties DDD - DDD.Core.Abstractions.UnitTests - v4.7.2 - 512 - - - + false - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - L:\packages\Castle.Core.4.3.1\lib\net45\Castle.Core.dll - True - - - L:\Packages\FluentAssertions.5.6.0\lib\net47\FluentAssertions.dll - - - L:\Packages\NSubstitute.4.0.0\lib\net46\NSubstitute.dll - - - - - - L:\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll - - - L:\Packages\System.Threading.Tasks.Extensions.4.5.2\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll - - - L:\packages\System.ValueTuple.4.5.0\lib\netstandard1.0\System.ValueTuple.dll - True - - - - - - - - - L:\packages\xunit.abstractions.2.0.3\lib\net35\xunit.abstractions.dll - True - - - L:\packages\xunit.assert.2.4.1\lib\netstandard1.1\xunit.assert.dll - - - L:\packages\xunit.extensibility.core.2.4.1\lib\net452\xunit.core.dll - - - L:\packages\xunit.extensibility.execution.2.4.1\lib\net452\xunit.execution.desktop.dll - - - - - - - - - - - - - - - + - - {596a8700-3d18-4a62-b200-1f78a9ea4617} - DDD.Core.Abstractions - + + 6.9.0 + + + + + + + 4.5.4 + + + 4.5.0 + + + + 2.4.5 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - - \ No newline at end of file diff --git a/Test/DDD.Core.Abstractions.UnitTests/EnumerableArgExtensionsTests.cs b/Test/DDD.Core.Abstractions.UnitTests/EnumerableArgExtensionsTests.cs new file mode 100644 index 0000000..3c2cae5 --- /dev/null +++ b/Test/DDD.Core.Abstractions.UnitTests/EnumerableArgExtensionsTests.cs @@ -0,0 +1,59 @@ +using EnsureThat; +using FluentAssertions; +using System; +using System.Collections.Generic; +using Xunit; + +namespace DDD +{ + public class EnumerableArgExtensionsTests + { + + #region Methods + + public static IEnumerable SequencesWithNull() + { + yield return new object[] { new string[] { "", null, "aajs" } }; + yield return new object[] { new object[] { "", null, "aajs" } }; + } + + public static IEnumerable SequencesWithoutNull() + { + yield return new object[] { new string[] { "" } }; + yield return new object[] { new object[] { "", "aajs" } }; + } + + [Theory] + [MemberData(nameof(SequencesWithNull))] + public void HasNoNull_WhenAnyNull_ThrowsArgumentException(IEnumerable value) + { + // Act + Action hasNoNull = () => Ensure.Enumerable.HasNoNull(value, nameof(value)); + // Assert + hasNoNull.Should().ThrowExactly(); + } + + [Theory] + [MemberData(nameof(SequencesWithoutNull))] + public void HasNoNull_WhenNoNull_DoesNotThrow(IEnumerable value) + { + // Act + Action hasNoNull = () => Ensure.Enumerable.HasNoNull(value, nameof(value)); + // Assert + hasNoNull.Should().NotThrow(); + } + + [Fact] + public void HasNoNull_WhenSequenceNull_ThrowsArgumentNullException() + { + // Arrange + IEnumerable value = null; + // Act + Action hasNoNull = () => Ensure.Enumerable.HasNoNull(value, nameof(value)); + // Assert + hasNoNull.Should().ThrowExactly(); + } + + #endregion Methods + } +} diff --git a/Test/DDD.Core.Abstractions.UnitTests/Linq/IEnumerableExtensionsTests.cs b/Test/DDD.Core.Abstractions.UnitTests/Linq/IEnumerableExtensionsTests.cs new file mode 100644 index 0000000..c3f5692 --- /dev/null +++ b/Test/DDD.Core.Abstractions.UnitTests/Linq/IEnumerableExtensionsTests.cs @@ -0,0 +1,47 @@ +using FluentAssertions; +using System.Collections.Generic; +using Xunit; + +namespace DDD.Linq +{ + public class IEnumerableExtensionsTests + { + + #region Methods + + public static IEnumerable SkipLastMemberData() + { + yield return new object[] + { + new [] {1, 2, 3, 4, 5 }, + 2, + new[] {1, 2, 3 } + }; + yield return new object[] + { + new [] {1, 2, 3, 4, 5 }, + 1, + new[] {1, 2, 3, 4 } + }; + yield return new object[] + { + new [] {1, 2, 3, 4, 5 }, + -5, + new[] {1, 2, 3, 4, 5 } + }; + } + + [Theory] + [MemberData(nameof(SkipLastMemberData))] + public void SkipLast_WhenCalled_ReturnsExpectedResult(IEnumerable source, int count, IEnumerable expected) + { + // Act + var result = IEnumerableExtensions.SkipLast(source, count); + // Assert + result.Should().BeEquivalentTo(expected); + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.Abstractions.UnitTests/Mapping/FakeObject3.cs b/Test/DDD.Core.Abstractions.UnitTests/Mapping/FakeObject3.cs new file mode 100644 index 0000000..9c0455b --- /dev/null +++ b/Test/DDD.Core.Abstractions.UnitTests/Mapping/FakeObject3.cs @@ -0,0 +1,6 @@ +namespace DDD.Mapping +{ + public class FakeObject3 : FakeObject1 + { + } +} diff --git a/Test/DDD.Core.Abstractions.UnitTests/Mapping/IMappingProcessorExtensionsTests.cs b/Test/DDD.Core.Abstractions.UnitTests/Mapping/IMappingProcessorExtensionsTests.cs new file mode 100644 index 0000000..06ae1f3 --- /dev/null +++ b/Test/DDD.Core.Abstractions.UnitTests/Mapping/IMappingProcessorExtensionsTests.cs @@ -0,0 +1,85 @@ +using NSubstitute; +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace DDD.Mapping +{ + public class IMappingProcessorExtensionsTests + { + + #region Fields + + private readonly MappingProcessor processor; + private readonly IObjectTranslator translator1To2; + private readonly IObjectTranslator translator2To1; + + #endregion Fields + + #region Constructors + + public IMappingProcessorExtensionsTests() + { + this.translator1To2 = Substitute.For>(); + this.translator2To1 = Substitute.For>(); + var serviceProvider = Substitute.For(); + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(IObjectTranslator)))) + .Returns(this.translator1To2); + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(IObjectTranslator)))) + .Returns(this.translator2To1); + processor = new MappingProcessor(serviceProvider, new MappingProcessorSettings()); + } + + #endregion Constructors + + #region Methods + + public static IEnumerable Sources() + { + var emptyArray = new FakeObject1[] { }; + yield return new object[] { emptyArray }; + var array = new FakeObject1[] + { + new FakeObject1(), + new FakeObject1(), + new FakeObject1() + }; + yield return new object[] { array }; + var emptyList = new List(); + yield return new object[] { emptyList }; + var list = new List(); + list.Add(new FakeObject1()); + yield return new object[] { list }; + } + + [Theory] + [MemberData(nameof(Sources))] + public void TranslateCollection_OfObject_CallsExpectedTranslator(IEnumerable source) + { + // Arrange + var context = new MappingContext(); + // Act + IMappingProcessorExtensions.TranslateCollection(this.processor, source, context) + .ToList(); + // Assert + this.translator1To2.Received(source.Count()).Translate(Arg.Is(x => source.Contains(x)), context); + } + + [Theory] + [MemberData(nameof(Sources))] + public void TranslateCollection_OfSpecifiedType_CallsExpectedTranslator(IEnumerable source) + { + // Arrange + var context = new MappingContext(); + // Act + IMappingProcessorExtensions.TranslateCollection(this.processor, source, context) + .ToList(); + // Assert + this.translator1To2.Received(source.Count()).Translate(Arg.Is(x => source.Contains(x)), context); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Abstractions.UnitTests/Mapping/MappingProcessorTests.cs b/Test/DDD.Core.Abstractions.UnitTests/Mapping/MappingProcessorTests.cs index 279e1d4..6e5b15e 100644 --- a/Test/DDD.Core.Abstractions.UnitTests/Mapping/MappingProcessorTests.cs +++ b/Test/DDD.Core.Abstractions.UnitTests/Mapping/MappingProcessorTests.cs @@ -33,7 +33,7 @@ public MappingProcessorTests() .Returns(this.translator1To2); serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(IObjectTranslator)))) .Returns(this.translator2To1); - processor = new MappingProcessor(serviceProvider); + processor = new MappingProcessor(serviceProvider, new MappingProcessorSettings()); } #endregion Constructors @@ -46,21 +46,35 @@ public void Map_WhenCalled_CallsExpectedMapper() // Arrange var source = new FakeObject1(); var destination = new FakeObject2(); + var context = new MappingContext(); // Act - this.processor.Map(source, destination); + this.processor.Map(source, destination, context); // Assert - this.mapper1To2.Received(1).Map(source, destination); + this.mapper1To2.Received(1).Map(source, destination, context); } [Fact] - public void Translate_WhenCalled_CallsExpectedTranslator() + public void Translate_SpecifiedType_CallsExpectedTranslator() { // Arrange var source = new FakeObject1(); + var context = new MappingContext(); // Act - var destination = this.processor.Translate(source); + this.processor.Translate(source, context); // Assert - this.translator1To2.Received(1).Translate(source); + this.translator1To2.Received(1).Translate(source, context); + } + + [Fact] + public void Translate_Object_CallsExpectedTranslator() + { + // Arrange + var source = new FakeObject1(); + var context = new MappingContext(); + // Act + this.processor.Translate(source, context); + // Assert + this.translator1To2.Received(1).Translate(source, context); } #endregion Methods diff --git a/Test/DDD.Core.Abstractions.UnitTests/ParamExtensionsTests.cs b/Test/DDD.Core.Abstractions.UnitTests/ParamExtensionsTests.cs new file mode 100644 index 0000000..bb24e4e --- /dev/null +++ b/Test/DDD.Core.Abstractions.UnitTests/ParamExtensionsTests.cs @@ -0,0 +1,52 @@ +using EnsureThat; +using FluentAssertions; +using System; +using Xunit; + +namespace DDD +{ + public class ParamExtensionsTests + { + + #region Methods + + [Fact] + public void Satisfy_WhenConditionNotSatisfied_ThrowsArgumentException() + { + // Arrange + var value = -1; + Func predicate = i => i > 0; + // Act + Action satisfy = () => Ensure.That(value, nameof(value)).Satisfy(predicate); + // Assert + satisfy.Should().ThrowExactly(); + } + + [Fact] + public void Satisfy_WhenConditionNull_DoesNotThrow() + { + // Arrange + var value = 1; + Func predicate = null; + // Act + Action satisfy = () => Ensure.That(value, nameof(value)).Satisfy(predicate); + // Assert + satisfy.Should().NotThrow(); + } + + [Fact] + public void Satisfy_WhenConditionSatisfied_DoesNotThrow() + { + // Arrange + var value = 1; + Func predicate = i => i > 0; + // Act + Action satisfy = () => Ensure.That(value, nameof(value)).Satisfy(predicate); + // Assert + satisfy.Should().NotThrow(); + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.Abstractions.UnitTests/StringArgExtensionsTests.cs b/Test/DDD.Core.Abstractions.UnitTests/StringArgExtensionsTests.cs new file mode 100644 index 0000000..87d9f96 --- /dev/null +++ b/Test/DDD.Core.Abstractions.UnitTests/StringArgExtensionsTests.cs @@ -0,0 +1,157 @@ +using System; +using Xunit; +using FluentAssertions; +using EnsureThat; + +namespace DDD +{ + public class StringArgExtensionsTests + { + + #region Methods + + [Theory] + [InlineData("abc", 4)] + [InlineData("abc", 10)] + public void HasMinLength_WhenInvalidLength_ThrowsArgumentException(string value, int minLength) + { + // Act + Action hasMinLength = () => Ensure.String.HasMinLength(value, minLength, nameof(value)); + // Assert + hasMinLength.Should().ThrowExactly(); + } + + [Fact] + public void HasMinLength_WhenStringNull_ThrowsArgumentNullException() + { + // Arrange + string value = null; + var minLength = 0; + // Act + Action hasMinLength = () => Ensure.String.HasMinLength(value, minLength, nameof(value)); + // Assert + hasMinLength.Should().ThrowExactly(); + } + + [Theory] + [InlineData("abc", 3)] + [InlineData("abc", 1)] + public void HasMinLength_WhenValidLength_DoesNotThrow(string value, int minLength) + { + // Act + Action hasMinLength = () => Ensure.String.HasMinLength(value, minLength, nameof(value)); + // Assert + hasMinLength.Should().NotThrow(); + } + + [Theory] + [InlineData("4s58")] + [InlineData("_00951")] + [InlineData("10@")] + [InlineData("1a5")] + public void IsAllDigits_WhenInvalidCharacters_ThrowsArgumentException(string value) + { + // Act + Action isAllDigits = () => Ensure.String.IsAllDigits(value, nameof(value)); + // Assert + isAllDigits.Should().ThrowExactly(); + } + + [Fact] + public void IsAllDigits_WhenStringNull_ThrowsArgumentNullException() + { + // Arrange + string value = null; + // Act + Action isAllDigits = () => Ensure.String.IsAllDigits(value, nameof(value)); + // Assert + isAllDigits.Should().ThrowExactly(); + } + + [Theory] + [InlineData("458")] + [InlineData("00951")] + public void IsAllDigits_WhenValidCharacters_DoesNotThrow(string value) + { + // Act + Action isAllDigits = () => Ensure.String.IsAllDigits(value, nameof(value)); + // Assert + isAllDigits.Should().NotThrow(); + } + + [Theory] + [InlineData("ajdz4dd")] + [InlineData("_aekdl")] + [InlineData("ol@")] + [InlineData("PAZ.")] + public void IsAllLetters_WhenInvalidCharacters_ThrowsArgumentException(string value) + { + // Act + Action isAllLetters = () => Ensure.String.IsAllLetters(value, nameof(value)); + // Assert + isAllLetters.Should().ThrowExactly(); + } + + [Fact] + public void IsAllLetters_WhenStringNull_ThrowsArgumentNullException() + { + // Arrange + string value = null; + // Act + Action isAllLetters = () => Ensure.String.IsAllLetters(value, nameof(value)); + // Assert + isAllLetters.Should().ThrowExactly(); + } + [Theory] + [InlineData("ajdzdd")] + [InlineData("aekdl")] + [InlineData("ol")] + [InlineData("PAZ")] + public void IsAllLetters_WhenValidCharacters_DoesNotThrow(string value) + { + // Act + Action isAllLetters = () => Ensure.String.IsAllLetters(value, nameof(value)); + // Assert + isAllLetters.Should().NotThrow(); + } + + [Fact] + public void Satisfy_WhenConditionNotSatisfied_ThrowsArgumentException() + { + // Arrange + var value = "BE115"; + Func predicate = s => s.StartsWith("BE") && s.EndsWith("16"); + // Act + Action satisfy = () => Ensure.String.Satisfy(value, predicate, nameof(value)); + // Assert + satisfy.Should().ThrowExactly(); + } + + [Fact] + public void Satisfy_WhenConditionNull_DoesNotThrow() + { + // Arrange + var value = "BE115"; + Func predicate = null; + // Act + Action satisfy = () => Ensure.String.Satisfy(value, predicate, nameof(value)); + // Assert + satisfy.Should().NotThrow(); + } + + [Fact] + public void Satisfy_WhenConditionSatisfied_DoesNotThrow() + { + // Arrange + var value = "BE116"; + Func predicate = s => s.StartsWith("BE") && s.EndsWith("16"); + // Act + Action satisfy = () => Ensure.String.Satisfy(value, predicate, nameof(value)); + // Assert + satisfy.Should().NotThrow(); + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.Abstractions.UnitTests/StringExtensionsTests.cs b/Test/DDD.Core.Abstractions.UnitTests/StringExtensionsTests.cs index 3589abb..d857b3e 100644 --- a/Test/DDD.Core.Abstractions.UnitTests/StringExtensionsTests.cs +++ b/Test/DDD.Core.Abstractions.UnitTests/StringExtensionsTests.cs @@ -53,93 +53,107 @@ public static IEnumerable IsShortDateString() } [Theory] - [InlineData("Olivier", 0, "")] - [InlineData("Olivier", 10, "Olivier")] - [InlineData("Olivier", 3, "ier")] - [InlineData("Olivier", 4, "vier")] - public void Right_WhenSpecifiedLengthGreaterOrEqualToZero_ReturnsExpectedString(string instance, int length, string expected) + [InlineData("asd", true)] + [InlineData("aSd", true)] + [InlineData("123345", false)] + [InlineData("123asd", false)] + [InlineData("!##$", false)] + public void IsAlphabetic_WhenStringNotNull_ReturnsExpectedValue(string instance, bool expected) { // Act - var result = instance.Right(length); + var result = instance.IsAlphabetic(); // Assert result.Should().Be(expected); } - [Fact] - public void Right_WhenSpecifiedLengthLessThanZero_ThrowsArgumentException() + [Theory] + [InlineData("asd", true)] + [InlineData("aSd", true)] + [InlineData("123345", true)] + [InlineData("123asd", true)] + [InlineData("!##$", false)] + [InlineData("a!##$", false)] + public void IsAlphanumeric_WhenStringNotNull_ReturnsExpectedValue(string instance, bool expected) { - // Arrange - var instance = "Olivier"; // Act - Action action = () => instance.Right(-1); + var result = instance.IsAlphanumeric(); // Assert - action.Should().Throw(); + result.Should().Be(expected); } [Theory] - [InlineData("Olivier", 0, "")] - [InlineData("Olivier", 10, "Olivier")] - [InlineData("Olivier", 3, "Oli")] - [InlineData("Olivier", 4, "Oliv")] - public void Left_WhenSpecifiedLengthGreaterOrEqualToZero_ReturnsExpectedString(string instance, int length, string expected) + [InlineData("01/01/2001", true)] + [InlineData("10/01/2001", true)] + [InlineData("01/10/2001", true)] + [InlineData("01.01.2001", false)] + [InlineData("01-01-2001", false)] + [InlineData("01/01/01", false)] + [InlineData("1/1/2001", false)] + [InlineData("10/1/2001", false)] + [InlineData("1/10/2001", false)] + public void IsFrenchShortDateString_WhenStringNotNull_ReturnsExpectedValue(string instance, bool expected) { // Act - var result = instance.Left(length); + var result = instance.IsFrenchShortDateString(); // Assert result.Should().Be(expected); } - [Fact] - public void Left_WhenSpecifiedLengthLessThanZero_ThrowsArgumentException() + [Theory] + [InlineData("123345", true)] + [InlineData("asd", false)] + [InlineData("aSd", false)] + [InlineData("123asd", false)] + [InlineData("!##$", false)] + public void IsNumeric_WhenStringNotNull_ReturnsExpectedValue(string instance, bool expected) { - // Arrange - var instance = "Olivier"; // Act - Action action = () => instance.Left(-1); + var result = instance.IsNumeric(); // Assert - action.Should().Throw(); + result.Should().Be(expected); } [Theory] - [InlineData("asd", true)] - [InlineData("aSd", true)] - [InlineData("123345", false)] - [InlineData("123asd", false)] - [InlineData("!##$", false)] - public void IsAlphabetic_WhenStringNotNull_ReturnsExpectedValue(string instance, bool expected) + [MemberData(nameof(IsShortDateString))] + public void IsShortDateString_WhenStringNotNull_ReturnsExpectedValue(string instance, IFormatProvider provider, bool expected) { // Act - var result = instance.IsAlphabetic(); + var result = instance.IsShortDateString(provider); // Assert result.Should().Be(expected); } - [Theory] - [InlineData("123345", true)] - [InlineData("asd", false)] - [InlineData("aSd", false)] - [InlineData("123asd", false)] - [InlineData("!##$", false)] - public void IsNumeric_WhenStringNotNull_ReturnsExpectedValue(string instance, bool expected) + [InlineData("Olivier", 0, "")] + [InlineData("Olivier", 10, "Olivier")] + [InlineData("Olivier", 3, "Oli")] + [InlineData("Olivier", 4, "Oliv")] + public void Left_WhenSpecifiedLengthGreaterOrEqualToZero_ReturnsExpectedString(string instance, int length, string expected) { // Act - var result = instance.IsNumeric(); + var result = instance.Left(length); // Assert result.Should().Be(expected); } + [Fact] + public void Left_WhenSpecifiedLengthLessThanZero_ThrowsArgumentException() + { + // Arrange + var instance = "Olivier"; + // Act + Action action = () => instance.Left(-1); + // Assert + action.Should().Throw(); + } + [Theory] - [InlineData("asd", true)] - [InlineData("aSd", true)] - [InlineData("123345", true)] - [InlineData("123asd", true)] - [InlineData("!##$", false)] - [InlineData("a!##$", false)] - public void IsAlphanumeric_WhenStringNotNull_ReturnsExpectedValue(string instance, bool expected) + [InlineData("Olivier", new char[] { 'o', 'v', 'e' }, "Oliir")] + [InlineData("Olivier", new char[] { 'O', 'v', 'e' }, "liir")] + public void Remove_WhenArgumentsNotNull_ReturnsExpectedValue(string instance, char[] oldChars, string expected) { // Act - var result = instance.IsAlphanumeric(); + var result = instance.Remove(oldChars); // Assert result.Should().Be(expected); } @@ -157,29 +171,36 @@ public void Reverse_WhenStringNotNull_ReturnsExpectedValue(string instance, stri } [Theory] - [MemberData(nameof(IsShortDateString))] - public void IsShortDateString_WhenStringNotNull_ReturnsExpectedValue(string instance, IFormatProvider provider, bool expected) + [InlineData("Olivier", 0, "")] + [InlineData("Olivier", 10, "Olivier")] + [InlineData("Olivier", 3, "ier")] + [InlineData("Olivier", 4, "vier")] + public void Right_WhenSpecifiedLengthGreaterOrEqualToZero_ReturnsExpectedString(string instance, int length, string expected) { // Act - var result = instance.IsShortDateString(provider); + var result = instance.Right(length); // Assert result.Should().Be(expected); } + [Fact] + public void Right_WhenSpecifiedLengthLessThanZero_ThrowsArgumentException() + { + // Arrange + var instance = "Olivier"; + // Act + Action action = () => instance.Right(-1); + // Assert + action.Should().Throw(); + } + [Theory] - [InlineData("01/01/2001", true)] - [InlineData("10/01/2001", true)] - [InlineData("01/10/2001", true)] - [InlineData("01.01.2001", false)] - [InlineData("01-01-2001", false)] - [InlineData("01/01/01", false)] - [InlineData("1/1/2001", false)] - [InlineData("10/1/2001", false)] - [InlineData("1/10/2001", false)] - public void IsFrenchShortDateString_WhenStringNotNull_ReturnsExpectedValue(string instance, bool expected) + [InlineData("iPhone", "i_Phone")] + [InlineData("DreamWorks", "Dream_Works")] + public void ToSnakeCase_WhenStringNotNull_ReturnsExpectedValue(string instance, string expected) { // Act - var result = instance.IsFrenchShortDateString(); + var result = instance.ToSnakeCase(); // Assert result.Should().Be(expected); } @@ -197,18 +218,6 @@ public void ToTitleCase_WhenStringNotNull_ReturnsExpectedValue(string instance, result.Should().Be(expected); } - [Theory] - [InlineData("iPhone", "i_Phone")] - [InlineData("DreamWorks", "Dream_Works")] - public void ToSnakeCase_WhenStringNotNull_ReturnsExpectedValue(string instance, string expected) - { - // Act - var result = instance.ToSnakeCase(); - // Assert - result.Should().Be(expected); - } - #endregion Methods - } } diff --git a/Test/DDD.Core.Abstractions.UnitTests/StringParamExtensionsTests.cs b/Test/DDD.Core.Abstractions.UnitTests/StringParamExtensionsTests.cs new file mode 100644 index 0000000..c8db901 --- /dev/null +++ b/Test/DDD.Core.Abstractions.UnitTests/StringParamExtensionsTests.cs @@ -0,0 +1,158 @@ +using System; +using Xunit; +using FluentAssertions; +using EnsureThat; + +namespace DDD +{ + public class StringParamExtensionsTests + { + + #region Methods + + [Theory] + [InlineData("abc", 4)] + [InlineData("abc", 10)] + public void HasMinLength_WhenInvalidLength_ThrowsArgumentException(string value, int minLength) + { + // Act + Action hasMinLength = () => Ensure.That(value, nameof(value)).HasMinLength(minLength); + // Assert + hasMinLength.Should().ThrowExactly(); + } + + [Fact] + public void HasMinLength_WhenStringNull_ThrowsArgumentNullException() + { + // Arrange + string value = null; + var minLength = 0; + // Act + Action hasMinLength = () => Ensure.That(value, nameof(value)).HasMinLength(minLength); + // Assert + hasMinLength.Should().ThrowExactly(); + } + + [Theory] + [InlineData("abc", 3)] + [InlineData("abc", 1)] + public void HasMinLength_WhenValidLength_DoesNotThrow(string value, int minLength) + { + // Act + Action hasMinLength = () => Ensure.That(value, nameof(value)).HasMinLength(minLength); + // Assert + hasMinLength.Should().NotThrow(); + } + + [Theory] + [InlineData("4s58")] + [InlineData("_00951")] + [InlineData("10@")] + [InlineData("1a5")] + public void IsAllDigits_WhenInvalidCharacters_ThrowsArgumentException(string value) + { + // Act + Action isAllDigits = () => Ensure.That(value, nameof(value)).IsAllDigits(); + // Assert + isAllDigits.Should().ThrowExactly(); + } + + [Fact] + public void IsAllDigits_WhenStringNull_ThrowsArgumentNullException() + { + // Arrange + string value = null; + // Act + Action isAllDigits = () => Ensure.That(value, nameof(value)).IsAllDigits(); + // Assert + isAllDigits.Should().ThrowExactly(); + } + + [Theory] + [InlineData("458")] + [InlineData("00951")] + public void IsAllDigits_WhenValidCharacters_DoesNotThrow(string value) + { + // Act + Action isAllDigits = () => Ensure.That(value, nameof(value)).IsAllDigits(); + // Assert + isAllDigits.Should().NotThrow(); + } + + [Theory] + [InlineData("ajdz4dd")] + [InlineData("_aekdl")] + [InlineData("ol@")] + [InlineData("PAZ.")] + public void IsAllLetters_WhenInvalidCharacters_ThrowsArgumentException(string value) + { + // Act + Action isAllLetters = () => Ensure.That(value, nameof(value)).IsAllLetters(); + // Assert + isAllLetters.Should().ThrowExactly(); + } + + [Fact] + public void IsAllLetters_WhenStringNull_ThrowsArgumentNullException() + { + // Arrange + string value = null; + // Act + Action isAllLetters = () => Ensure.That(value, nameof(value)).IsAllLetters(); + // Assert + isAllLetters.Should().ThrowExactly(); + } + + [Theory] + [InlineData("ajdzdd")] + [InlineData("aekdl")] + [InlineData("ol")] + [InlineData("PAZ")] + public void IsAllLetters_WhenValidCharacters_DoesNotThrow(string value) + { + // Act + Action isAllLetters = () => Ensure.That(value, nameof(value)).IsAllLetters(); + // Assert + isAllLetters.Should().NotThrow(); + } + + [Fact] + public void Satisfy_WhenConditionNotSatisfied_ThrowsArgumentException() + { + // Arrange + var value = "BE115"; + Func predicate = s => s.StartsWith("BE") && s.EndsWith("16"); + // Act + Action satisfy = () => Ensure.That(value, nameof(value)).Satisfy(predicate); + // Assert + satisfy.Should().ThrowExactly(); + } + + [Fact] + public void Satisfy_WhenConditionNull_DoesNotThrow() + { + // Arrange + var value = "BE115"; + Func predicate = null; + // Act + Action satisfy = () => Ensure.That(value, nameof(value)).Satisfy(predicate); + // Assert + satisfy.Should().NotThrow(); + } + + [Fact] + public void Satisfy_WhenConditionSatisfied_DoesNotThrow() + { + // Arrange + var value = "BE116"; + Func predicate = s => s.StartsWith("BE") && s.EndsWith("16"); + // Act + Action satisfy = () => Ensure.That(value, nameof(value)).Satisfy(predicate); + // Assert + satisfy.Should().NotThrow(); + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.Abstractions.UnitTests/app.config b/Test/DDD.Core.Abstractions.UnitTests/app.config deleted file mode 100644 index ce1896c..0000000 --- a/Test/DDD.Core.Abstractions.UnitTests/app.config +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/Test/DDD.Core.Abstractions.UnitTests/packages.config b/Test/DDD.Core.Abstractions.UnitTests/packages.config deleted file mode 100644 index 6878c7a..0000000 --- a/Test/DDD.Core.Abstractions.UnitTests/packages.config +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Test/DDD.Core.Abstractions.UnitTests/runtimeconfig.template.json b/Test/DDD.Core.Abstractions.UnitTests/runtimeconfig.template.json new file mode 100644 index 0000000..11cefbd --- /dev/null +++ b/Test/DDD.Core.Abstractions.UnitTests/runtimeconfig.template.json @@ -0,0 +1,5 @@ +{ + "configProperties": { + "System.Globalization.UseNls": true + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/App.config b/Test/DDD.Core.Dapper.IntegrationTests/App.config new file mode 100644 index 0000000..b8f27c8 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/App.config @@ -0,0 +1,22 @@ + + + + +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/DDD.Core.Dapper.IntegrationTests.csproj b/Test/DDD.Core.Dapper.IntegrationTests/DDD.Core.Dapper.IntegrationTests.csproj new file mode 100644 index 0000000..28a1945 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/DDD.Core.Dapper.IntegrationTests.csproj @@ -0,0 +1,54 @@ + + + net48;net6.0 + Library + DDD.Core + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + True + True + OracleScripts.resx + + + True + True + SqlServerScripts.resx + + + + + ResXFileCodeGenerator + OracleScripts.Designer.cs + + + ResXFileCodeGenerator + SqlServerScripts.Designer.cs + + + + + PreserveNewest + + + diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Domain/TestContext.cs b/Test/DDD.Core.Dapper.IntegrationTests/Domain/TestContext.cs new file mode 100644 index 0000000..f0e5443 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Domain/TestContext.cs @@ -0,0 +1,14 @@ +namespace DDD.Core.Domain +{ + public class TestContext : BoundedContext + { + #region Constructors + + public TestContext() : base("TST", "Test") + { + } + + #endregion Constructors + + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/EventStreamPositionUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/EventStreamPositionUpdaterTests.cs new file mode 100644 index 0000000..cdc76db --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/EventStreamPositionUpdaterTests.cs @@ -0,0 +1,79 @@ +using FluentAssertions; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + + public abstract class EventStreamPositionUpdaterTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected EventStreamPositionUpdaterTests(TFixture fixture) + { + this.Fixture = fixture; + this.ConnectionProvider = fixture.CreateConnectionProvider(); + } + + #endregion Constructors + + #region Properties + + protected TFixture Fixture { get; } + + protected IDbConnectionProvider ConnectionProvider { get; } + + #endregion Properties + + #region Methods + + public void Dispose() + { + this.ConnectionProvider.Dispose(); + } + + [Fact] + public void Handle_WhenCalled_DoesNotThrowException() + { + // Arrange + this.Fixture.ExecuteScriptFromResources("UpdateEventStreamPosition"); + var handler = new EventStreamPositionUpdater(this.ConnectionProvider); + var command = CreateCommand(); + // Act + Action handle = () => handler.Handle(command); + // Assert + handle.Should().NotThrow(); + } + + [Fact] + public async Task HandleAsync_WhenCalled_DoesNotThrowException() + { + // Arrange + this.Fixture.ExecuteScriptFromResources("UpdateEventStreamPosition"); + var handler = new EventStreamPositionUpdater(this.ConnectionProvider); + var command = CreateCommand(); + // Act + Func handle = async () => await handler.HandleAsync(command); + // Assert + await handle.Should().NotThrowAsync(); + } + + private static UpdateEventStreamPosition CreateCommand() + { + return new UpdateEventStreamPosition + { + Type = "Person", + Source = "ID", + Position = new Guid("f7df5bd0-8763-677e-7e6b-3a0044746810") + }; + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/EventStreamReaderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/EventStreamReaderTests.cs new file mode 100644 index 0000000..0de320a --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/EventStreamReaderTests.cs @@ -0,0 +1,198 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + + public abstract class EventStreamReaderTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected EventStreamReaderTests(TFixture fixture) + { + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); + } + + #endregion Constructors + + #region Properties + + protected TFixture Fixture { get; } + + protected IDbConnectionProvider ConnectionProvider { get; } + + #endregion Properties + + #region Methods + + public static IEnumerable QueriesAndResults_1() + { + yield return new object[] + { + new ReadEventStream + { + StreamType = "MessageBox" + }, + new [] + { + new Event + { + Body = "{\"boxId\":2,\"occurredOn\":\"2021-11-18T10:01:23.5277314+01:00\"}", + BodyFormat = "JSON", + EventType = "DDD.Collaboration.Domain.Messages.MessageBoxCreated, DDD.Collaboration.Messages", + OccurredOn = new DateTime(2021, 11, 18, 10, 1, 23, 528), + StreamId = "2", + StreamType = "MessageBox", + IssuedBy = "Dr Maboul" + }, + new Event + { + Body = "{\"boxId\":3,\"occurredOn\":\"2022-01-19T14:22:00.3683143+01:00\"}", + BodyFormat = "JSON", + EventType = "DDD.Collaboration.Domain.Messages.MessageBoxCreated, DDD.Collaboration.Messages", + OccurredOn = new DateTime(2022, 1, 19, 14, 22, 0, 368), + StreamId = "3", + StreamType = "MessageBox", + IssuedBy = "Dr Folamour" + } + } + }; + yield return new object[] + { + new ReadEventStream + { + StreamType = "Message", + ExcludedStreamIds = new [] { "1", "2", "5"} + }, + new [] + { + new Event + { + Body = "{\"messageId\":3,\"occurredOn\":\"2021-11-16T00:00:00+01:00\"}", + BodyFormat = "JSON", + EventType = "DDD.Collaboration.Domain.Messages.MessageDelivered, DDD.Collaboration.Messages", + OccurredOn = new DateTime(2021, 11, 16), + StreamId = "3", + StreamType = "Message", + IssuedBy = "Dr Maboul" + }, + new Event + { + Body = "{\"messageId\":3,\"occurredOn\":\"2021-11-18T10:01:30.7417655+01:00\"}", + BodyFormat = "JSON", + EventType = "DDD.Collaboration.Domain.Messages.MessageReceived, DDD.Collaboration.Messages", + OccurredOn = new DateTime(2021, 11, 18, 10, 1, 30, 742), + StreamId = "3", + StreamType = "Message", + IssuedBy = "Dr Maboul" + }, + new Event + { + Body = "{\"messageId\":4,\"occurredOn\":\"2022-01-20T09:24:59.6050066+01:00\"}", + BodyFormat = "JSON", + EventType = "DDD.Collaboration.Domain.Messages.MessageCreated, DDD.Collaboration.Messages", + OccurredOn = new DateTime(2022, 1, 20, 9, 24, 59, 605), + StreamId = "4", + StreamType = "Message", + IssuedBy = "Dr Folamour" + }, + new Event + { + Body = "{\"messageId\":4,\"occurredOn\":\"2022-01-20T09:25:06.077499+01:00\"}", + BodyFormat = "JSON", + EventType = "DDD.Collaboration.Domain.Messages.MessageSent, DDD.Collaboration.Messages", + OccurredOn = new DateTime(2022, 1, 20 , 9, 25, 6, 77), + StreamId = "4", + StreamType = "Message", + IssuedBy = "Dr Folamour" + } + } + }; + yield return new object[] + { + new ReadEventStream + { + StreamType = "Message", + Top = 3 + }, + new [] + { + new Event + { + Body = "{\"messageId\":1,\"occurredOn\":\"2021-11-05T00:00:00+01:00\"}", + BodyFormat = "JSON", + EventType = "DDD.Collaboration.Domain.Messages.MessageDelivered, DDD.Collaboration.Messages", + OccurredOn = new DateTime(2021, 11, 05), + StreamId = "1", + StreamType = "Message", + IssuedBy = "Dr Maboul" + }, + new Event + { + Body = "{\"messageId\":1,\"occurredOn\":\"2021-11-18T10:01:30.4670777+01:00\"}", + BodyFormat = "JSON", + EventType = "DDD.Collaboration.Domain.Messages.MessageReceived, DDD.Collaboration.Messages", + OccurredOn = new DateTime(2021, 11, 18, 10, 1, 30, 467), + StreamId = "1", + StreamType = "Message", + IssuedBy = "Dr Maboul" + }, + new Event + { + Body = "{\"messageId\":2,\"occurredOn\":\"2021-11-09T00:00:00+01:00\"}", + BodyFormat = "JSON", + EventType = "DDD.Collaboration.Domain.Messages.MessageDelivered, DDD.Collaboration.Messages", + OccurredOn = new DateTime(2021, 11, 09), + StreamId = "2", + StreamType = "Message", + IssuedBy = "Dr Maboul" + } + } + }; + } + + [Theory] + [MemberData(nameof(QueriesAndResults_1))] + public void Handle_WhenCalled_ReturnsExpectedResults_1(ReadEventStream query, IEnumerable expectedResults) + { + // Arrange + Fixture.ExecuteScriptFromResources("ReadEventStream"); + var handler = new EventStreamReader(ConnectionProvider); + // Act + var results = handler.Handle(query); + // Assert + results.Should().BeEquivalentTo(expectedResults, context => context.WithStrictOrdering() + .Excluding(e => e.EventId)); + } + + [Theory] + [MemberData(nameof(QueriesAndResults_1))] + public async Task HandleAsync_WhenCalled_ReturnsExpectedResults_1(ReadEventStream query, IEnumerable expectedResults) + { + // Arrange + Fixture.ExecuteScriptFromResources("ReadEventStream"); + var handler = new EventStreamReader(ConnectionProvider); + // Act + var results = await handler.HandleAsync(query); + // Assert + results.Should().BeEquivalentTo(expectedResults, context => context.WithStrictOrdering() + .Excluding(e => e.EventId)); + } + + public void Dispose() + { + ConnectionProvider.Dispose(); + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/EventStreamSubscriberTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/EventStreamSubscriberTests.cs new file mode 100644 index 0000000..3a69772 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/EventStreamSubscriberTests.cs @@ -0,0 +1,81 @@ +using FluentAssertions; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + + public abstract class EventStreamSubscriberTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected EventStreamSubscriberTests(TFixture fixture) + { + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); + } + + #endregion Constructors + + #region Properties + + protected TFixture Fixture { get; } + + protected IDbConnectionProvider ConnectionProvider { get; } + + #endregion Properties + + #region Methods + + [Fact] + public void Handle_WhenCalled_DoesNotThrowException() + { + // Arrange + Fixture.ExecuteScriptFromResources("SubscribeToEventStream"); + var handler = new EventStreamSubscriber(ConnectionProvider); + var command = CreateCommand(); + // Act + Action handle = () => handler.Handle(command); + // Assert + handle.Should().NotThrow(); + } + + [Fact] + public async Task HandleAsync_WhenCalled_DoesNotThrowException() + { + // Arrange + Fixture.ExecuteScriptFromResources("SubscribeToEventStream"); + var handler = new EventStreamSubscriber(ConnectionProvider); + var command = CreateCommand(); + // Act + Func handle = async () => await handler.HandleAsync(command); + // Assert + await handle.Should().NotThrowAsync(); + } + + public void Dispose() + { + ConnectionProvider.Dispose(); + } + + private static SubscribeToEventStream CreateCommand() + { + return new SubscribeToEventStream + { + Type = "Message", + Source = "COL", + Position = Guid.Empty, + RetryMax = 3, + RetryDelays = new[] { new IncrementalDelay { Delay = 60, Increment = 30 } }, + BlockSize = 100 + }; + } + + #endregion Methods + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/EventStreamsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/EventStreamsFinderTests.cs new file mode 100644 index 0000000..e6b9842 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/EventStreamsFinderTests.cs @@ -0,0 +1,101 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + + public abstract class EventStreamsFinderTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected EventStreamsFinderTests(TFixture fixture) + { + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); + } + + #endregion Constructors + + #region Properties + + protected TFixture Fixture { get; } + + protected IDbConnectionProvider ConnectionProvider { get; } + + #endregion Properties + + #region Methods + + [Fact] + public void Handle_WhenCalled_ReturnsExpectedResults() + { + // Arrange + var query = new FindEventStreams(); + var expectedResults = ExpectedResults(); + Fixture.ExecuteScriptFromResources("FindEventStreams"); + var handler = new EventStreamsFinder(ConnectionProvider); + // Act + var results = handler.Handle(query); + // Assert + results.Should().BeEquivalentTo(expectedResults, context => context.WithStrictOrdering()); + } + + [Fact] + public async Task HandleAsync_WhenCalled_ReturnsExpectedResults() + { + // Arrange + var query = new FindEventStreams(); + var expectedResults = ExpectedResults(); + Fixture.ExecuteScriptFromResources("FindEventStreams"); + var handler = new EventStreamsFinder(ConnectionProvider); + // Act + var results = await handler.HandleAsync(query); + // Assert + results.Should().BeEquivalentTo(expectedResults, context => context.WithStrictOrdering()); + } + + public void Dispose() + { + ConnectionProvider.Dispose(); + } + + private static IEnumerable ExpectedResults() + { + yield return new EventStream + { + Type = "Person", + Source = "ID", + Position = new Guid("f7df5bd0-8763-677e-7e6b-3a0044746810"), + RetryMax = 5, + RetryDelays = new[] + { + new IncrementalDelay { Delay = 10 }, + new IncrementalDelay { Delay = 60 }, + new IncrementalDelay { Delay = 120, Increment = 80 } + }, + BlockSize = 50 + }; + yield return new EventStream + { + Type = "MedicalProduct", + Source = "OFR", + Position = Guid.Empty, + RetryMax = 3, + RetryDelays = new[] + { + new IncrementalDelay { Delay = 60 } + }, + BlockSize = 100 + }; + } + + #endregion Methods + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamExcluderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamExcluderTests.cs new file mode 100644 index 0000000..a9407ad --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamExcluderTests.cs @@ -0,0 +1,98 @@ +using FluentAssertions; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + + public abstract class FailedEventStreamExcluderTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected FailedEventStreamExcluderTests(TFixture fixture) + { + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); + } + + #endregion Constructors + + #region Properties + + protected TFixture Fixture { get; } + + protected IDbConnectionProvider ConnectionProvider { get; } + + #endregion Properties + + #region Methods + + [Fact] + public void Handle_WhenCalled_DoesNotThrowException() + { + // Arrange + Fixture.ExecuteScriptFromResources("ExcludeFailedEventStream"); + var handler = new FailedEventStreamCreator(ConnectionProvider); + var command = CreateCommand(); + // Act + Action handle = () => handler.Handle(command); + // Assert + handle.Should().NotThrow(); + } + + [Fact] + public async Task HandleAsync_WhenCalled_DoesNotThrowException() + { + // Arrange + Fixture.ExecuteScriptFromResources("ExcludeFailedEventStream"); + var handler = new FailedEventStreamCreator(ConnectionProvider); + var command = CreateCommand(); + // Act + Func handle = async () => await handler.HandleAsync(command); + // Assert + await handle.Should().NotThrowAsync(); + } + + public void Dispose() + { + ConnectionProvider.Dispose(); + } + + private static ExcludeFailedEventStream CreateCommand() + { + return new ExcludeFailedEventStream + { + StreamId = "2", + StreamType = "MessageBox", + StreamSource = "COL", + StreamPosition = new Guid("0a77707a-c147-9e1b-883a-08da0e368663"), + EventId = new Guid("e10add4d-1851-7ede-883b-08da0e368663"), + EventType = "DDD.Collaboration.Domain.Messages.MessageBoxCreated, DDD.Collaboration.Messages", + ExceptionTime = new DateTime(2021, 11, 19), + ExceptionType = "System.Exception, mscorlib", + ExceptionMessage = "Invalid event", + ExceptionSource = "DDD.IdentityManagement, DDD.IdentityManagement.Application.MessageBoxCreatedEventHandler, Void Handle()", + ExceptionInfo = "System.Exception: Invalid event ---> System.Exception: Format not supported.\r\n --- End of inner exception stack trace ---", + BaseExceptionType = "System.Exception, mscorlib", + BaseExceptionMessage = "Format not supported.", + RetryCount = 0, + RetryMax = 5, + RetryDelays = new[] + { + new IncrementalDelay{ Delay = 10 }, + new IncrementalDelay{ Delay = 60 }, + new IncrementalDelay{ Delay = 360 } + }, + BlockSize = 100 + }; + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamIncluderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamIncluderTests.cs new file mode 100644 index 0000000..32e70d2 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamIncluderTests.cs @@ -0,0 +1,79 @@ +using FluentAssertions; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + + public abstract class FailedEventStreamIncluderTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected FailedEventStreamIncluderTests(TFixture fixture) + { + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); + } + + #endregion Constructors + + #region Properties + + protected TFixture Fixture { get; } + + protected IDbConnectionProvider ConnectionProvider { get; } + + #endregion Properties + + #region Methods + + [Fact] + public void Handle_WhenCalled_DoesNotThrowException() + { + // Arrange + Fixture.ExecuteScriptFromResources("IncludeFailedEventStream"); + var handler = new FailedEventStreamDeleter(ConnectionProvider); + var command = CreateCommand(); + // Act + Action handle = () => handler.Handle(command); + // Assert + handle.Should().NotThrow(); + } + + [Fact] + public async Task HandleAsync_WhenCalled_DoesNotThrowException() + { + // Arrange + Fixture.ExecuteScriptFromResources("IncludeFailedEventStream"); + var handler = new FailedEventStreamDeleter(ConnectionProvider); + var command = CreateCommand(); + // Act + Func handle = async () => await handler.HandleAsync(command); + // Assert + await handle.Should().NotThrowAsync(); + } + + public void Dispose() + { + ConnectionProvider.Dispose(); + } + + private static IncludeFailedEventStream CreateCommand() + { + return new IncludeFailedEventStream + { + Id = "2", + Type = "MessageBox", + Source = "COL" + }; + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamPositionUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamPositionUpdaterTests.cs new file mode 100644 index 0000000..980e9e3 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamPositionUpdaterTests.cs @@ -0,0 +1,80 @@ +using FluentAssertions; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + + public abstract class FailedEventStreamPositionUpdaterTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected FailedEventStreamPositionUpdaterTests(TFixture fixture) + { + this.Fixture = fixture; + this.ConnectionProvider = fixture.CreateConnectionProvider(); + } + + #endregion Constructors + + #region Properties + + protected TFixture Fixture { get; } + + protected IDbConnectionProvider ConnectionProvider { get; } + + #endregion Properties + + #region Methods + + public void Dispose() + { + this.ConnectionProvider.Dispose(); + } + + [Fact] + public void Handle_WhenCalled_DoesNotThrowException() + { + // Arrange + this.Fixture.ExecuteScriptFromResources("UpdateFailedEventStreamPosition"); + var handler = new FailedEventStreamPositionUpdater(this.ConnectionProvider); + var command = CreateCommand(); + // Act + Action handle = () => handler.Handle(command); + // Assert + handle.Should().NotThrow(); + } + + [Fact] + public async Task HandleAsync_WhenCalled_DoesNotThrowException() + { + // Arrange + this.Fixture.ExecuteScriptFromResources("UpdateFailedEventStreamPosition"); + var handler = new FailedEventStreamPositionUpdater(this.ConnectionProvider); + var command = CreateCommand(); + // Act + Func handle = async () => await handler.HandleAsync(command); + // Assert + await handle.Should().NotThrowAsync(); + } + + private static UpdateFailedEventStreamPosition CreateCommand() + { + return new UpdateFailedEventStreamPosition + { + Id = "2", + Type = "MessageBox", + Source = "COL", + Position = new Guid("f7df5bd0-8763-677e-7e6b-3a0044746810") + }; + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamReaderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamReaderTests.cs new file mode 100644 index 0000000..6c9b15b --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamReaderTests.cs @@ -0,0 +1,39 @@ +using System; + +namespace DDD.Core.Infrastructure.Data +{ + using Domain; + + public abstract class EventsByStreamIdFinderTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected EventsByStreamIdFinderTests(TFixture fixture) + { + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); + } + + #endregion Constructors + + #region Properties + + protected TFixture Fixture { get; } + + protected IDbConnectionProvider ConnectionProvider { get; } + + #endregion Properties + + #region Methods + + public void Dispose() + { + ConnectionProvider.Dispose(); + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamUpdaterTests.cs new file mode 100644 index 0000000..cbd18dc --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamUpdaterTests.cs @@ -0,0 +1,96 @@ +using FluentAssertions; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + + public abstract class FailedEventStreamUpdaterTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected FailedEventStreamUpdaterTests(TFixture fixture) + { + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); + } + + #endregion Constructors + + #region Properties + + protected TFixture Fixture { get; } + + protected IDbConnectionProvider ConnectionProvider { get; } + + #endregion Properties + + #region Methods + + [Fact] + public void Handle_WhenCalled_DoesNotThrowException() + { + // Arrange + Fixture.ExecuteScriptFromResources("UpdateFailedEventStream"); + var handler = new FailedEventStreamUpdater(ConnectionProvider); + var command = CreateCommand(); + // Act + Action handle = () => handler.Handle(command); + // Assert + handle.Should().NotThrow(); + } + + [Fact] + public async Task HandleAsync_WhenCalled_DoesNotThrowException() + { + // Arrange + Fixture.ExecuteScriptFromResources("UpdateFailedEventStream"); + var handler = new FailedEventStreamUpdater(ConnectionProvider); + var command = CreateCommand(); + // Act + Func handle = async () => await handler.HandleAsync(command); + // Assert + await handle.Should().NotThrowAsync(); + } + + public void Dispose() + { + ConnectionProvider.Dispose(); + } + + private static UpdateFailedEventStream CreateCommand() + { + return new UpdateFailedEventStream + { + StreamId = "2", + StreamType = "MessageBox", + StreamSource = "COL", + StreamPosition = new Guid("0a77707a-c147-9e1b-883a-08da0e368664"), + EventId = new Guid("e10add4d-1851-7ede-883b-08da0e368664"), + EventType = "DDD.Collaboration.Domain.Messages.MessageBoxDisabled, DDD.Collaboration.Messages", + ExceptionTime = new DateTime(2021, 11, 20), + ExceptionType = "System.NotImplementedException, mscorlib", + ExceptionMessage = "The method or operation is not implemented.", + ExceptionSource = "DDD.IdentityManagement, DDD.IdentityManagement.Application.MessageBoxDisabledEventHandler, Void Handle()", + ExceptionInfo = "System.NotImplementedException: The method or operation is not implemented.", + BaseExceptionType = "System.NotImplementedException, mscorlib", + BaseExceptionMessage = "The method or operation is not implemented.", + RetryCount = 1, + RetryMax = 5, + RetryDelays = new[] + { + new IncrementalDelay{ Delay = 10 }, + new IncrementalDelay{ Delay = 60 }, + new IncrementalDelay{ Delay = 360 } + } + }; + } + + #endregion Methods + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamsFinderTests.cs new file mode 100644 index 0000000..060497b --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamsFinderTests.cs @@ -0,0 +1,93 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + + public abstract class FailedEventStreamsFinderTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected FailedEventStreamsFinderTests(TFixture fixture) + { + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); + } + + #endregion Constructors + + #region Properties + + protected TFixture Fixture { get; } + + protected IDbConnectionProvider ConnectionProvider { get; } + + #endregion Properties + + #region Methods + + [Fact] + public void Handle_WhenCalled_ReturnsExpectedResults() + { + // Arrange + var query = new FindFailedEventStreams(); + var expectedResults = ExpectedResults(); + Fixture.ExecuteScriptFromResources("FindFailedEventStreams"); + var handler = new FailedEventStreamsFinder(ConnectionProvider); + // Act + var results = handler.Handle(query); + // Assert + results.Should().BeEquivalentTo(expectedResults, context => context.WithStrictOrdering()); + } + + [Fact] + public async Task HandleAsync_WhenCalled_ReturnsExpectedResults() + { + // Arrange + var query = new FindFailedEventStreams(); + var expectedResults = ExpectedResults(); + Fixture.ExecuteScriptFromResources("FindFailedEventStreams"); + var handler = new FailedEventStreamsFinder(ConnectionProvider); + // Act + var results = await handler.HandleAsync(query); + // Assert + results.Should().BeEquivalentTo(expectedResults, context => context.WithStrictOrdering()); + } + + public void Dispose() + { + ConnectionProvider.Dispose(); + } + + private static IEnumerable ExpectedResults() + { + yield return new FailedEventStream + { + StreamPosition = new Guid("0a77707a-c147-9e1b-883a-08da0e368663"), + StreamId = "2", + StreamType = "MessageBox", + StreamSource = "COL", + EventId = new Guid("e10add4d-1851-7ede-883b-08da0e368663"), + ExceptionTime = new DateTime(2021, 11, 19), + RetryCount = 0, + RetryMax = 5, + RetryDelays = new[] + { + new IncrementalDelay{ Delay = 10 }, + new IncrementalDelay{ Delay = 60 }, + new IncrementalDelay{ Delay = 360 } + }, + BlockSize = 100 + }; + } + + #endregion Methods + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedRecurringCommandUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedRecurringCommandUpdaterTests.cs new file mode 100644 index 0000000..d075c2b --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedRecurringCommandUpdaterTests.cs @@ -0,0 +1,118 @@ +using FluentAssertions; +using Dapper; +using System; +using System.Threading.Tasks; +using System.Data.Common; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + + public abstract class FailedRecurringCommandUpdaterTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected FailedRecurringCommandUpdaterTests(TFixture fixture) + { + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); + } + + #endregion Constructors + + #region Properties + + protected TFixture Fixture { get; } + + protected IDbConnectionProvider ConnectionProvider { get; } + + #endregion Properties + + #region Methods + + [Fact] + public void Handle_WhenCalled_UpdatesCommand() + { + // Arrange + Fixture.ExecuteScriptFromResources("MarkRecurringCommandAsFailed"); + var handler = new FailedRecurringCommandUpdater(ConnectionProvider); + var command = CreateCommand(); + var expectedCommand = ExpectedCommand(); + // Act + handler.Handle(command); + // Assert + UpdatedCommand().Should().BeEquivalentTo(expectedCommand); + } + + [Fact] + public async Task HandleAsync_WhenCalled_UpdatesCommand() + { + // Arrange + Fixture.ExecuteScriptFromResources("MarkRecurringCommandAsFailed"); + var handler = new FailedRecurringCommandUpdater(ConnectionProvider); + var command = CreateCommand(); + var expectedCommand = ExpectedCommand(); + // Act + await handler.HandleAsync(command); + // Assert + UpdatedCommand().Should().BeEquivalentTo(expectedCommand); + + } + + public void Dispose() + { + ConnectionProvider.Dispose(); + } + + private static MarkRecurringCommandAsFailed CreateCommand() + { + return new MarkRecurringCommandAsFailed + { + CommandId = new Guid("36beb37d-1e01-bb7d-fb2a-3a0044745345"), + ExecutionTime = new DateTime(2022, 1, 1), + ExceptionInfo = "System.TimeoutException: The operation has timed-out." + }; + } + + private RecurringCommandDetail UpdatedCommand() + { + using (var connection = Fixture.CreateConnection()) + { + connection.Open(); + var sql = "SELECT CommandId, CommandType, Body, BodyFormat, RecurringExpression, RecurringExpressionFormat, LastExecutionTime, CASE LastExecutionStatus WHEN 'F' THEN 'Failed' WHEN 'S' THEN 'Successful' END LastExecutionStatus, LastExceptionInfo FROM Command WHERE CommandId = @CommandId"; + sql = sql.Replace("@", connection.Expressions().ParameterPrefix()); + return connection.QuerySingle(sql, Parameters(connection)); + } + } + + private static object Parameters(DbConnection connection) + { + if (connection.HasOracleProvider()) + return new { CommandId = new Guid("36beb37d-1e01-bb7d-fb2a-3a0044745345").ToByteArray() }; + return new { CommandId = new Guid("36beb37d-1e01-bb7d-fb2a-3a0044745345") }; + } + + + private static RecurringCommandDetail ExpectedCommand() + { + return new RecurringCommandDetail + { + CommandId = new Guid("36beb37d-1e01-bb7d-fb2a-3a0044745345"), + CommandType = "DDD.Core.Application.FakeCommand1, DDD.Core.Messages", + Body = "{\"Property1\":\"dummy\",\"Property2\":10}", + BodyFormat = "JSON", + RecurringExpression = "* * * * *", + RecurringExpressionFormat = "CRON", + LastExecutionTime = new DateTime(2022, 1, 1), + LastExecutionStatus = CommandExecutionStatus.Failed, + LastExceptionInfo = "System.TimeoutException: The operation has timed-out." + }; + } + + #endregion Methods + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/IDbConnectionExtensionsTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/IDbConnectionExtensionsTests.cs new file mode 100644 index 0000000..b6e158d --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/IDbConnectionExtensionsTests.cs @@ -0,0 +1,90 @@ +using FluentAssertions; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Domain; + + public abstract class IDbConnectionExtensionsTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected IDbConnectionExtensionsTests(TFixture fixture) + { + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); + } + + #endregion Constructors + + #region Properties + + protected TFixture Fixture { get; } + + protected IDbConnectionProvider ConnectionProvider { get; } + + #endregion Properties + + #region Methods + + public void Dispose() + { + ConnectionProvider.Dispose(); + } + + [Fact] + public void NextId_WhenExistingRows_ReturnsExpectedId() + { + // Arrange + Fixture.ExecuteScriptFromResources("NextId_ExistingRows"); + var connection = ConnectionProvider.GetOpenConnection(); + // Act + var id = connection.NextId("TableWithId", "Id"); + // Assert + id.Should().Be(5); + } + + [Fact] + public void NextId_WhenNoRow_ReturnsExpectedId() + { + // Arrange + Fixture.ExecuteScriptFromResources("NextId_NoRow"); + var connection = ConnectionProvider.GetOpenConnection(); + // Act + var id = connection.NextId("TableWithId", "Id"); + // Assert + id.Should().Be(1); + } + + [Fact] + public async Task NextIdAsync_WhenExistingRows_ReturnsExpectedId() + { + // Arrange + Fixture.ExecuteScriptFromResources("NextId_ExistingRows"); + var connection = await ConnectionProvider.GetOpenConnectionAsync(); + // Act + var id = await connection.NextIdAsync("TableWithId", "Id"); + // Assert + id.Should().Be(5); + } + + [Fact] + public async Task NextIdAsync_WhenNoRow_ReturnsExpectedId() + { + // Arrange + Fixture.ExecuteScriptFromResources("NextId_NoRow"); + var connection = await ConnectionProvider.GetOpenConnectionAsync(); + // Act + var id = await connection.NextIdAsync("TableWithId", "Id"); + // Assert + id.Should().Be(1); + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/IPersistenceFixture.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/IPersistenceFixture.cs new file mode 100644 index 0000000..236a423 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/IPersistenceFixture.cs @@ -0,0 +1,18 @@ +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + using Testing; + using Mapping; + + public interface IPersistenceFixture : IDbFixture + { + + #region Methods + + IObjectTranslator CreateEventTranslator(); + + #endregion Methods + + } +} diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleCollection.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleCollection.cs similarity index 78% rename from Test/DDD.HealthcareDelivery.IntegrationTests/OracleCollection.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleCollection.cs index 29bb075..d84423a 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleCollection.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleCollection.cs @@ -1,6 +1,6 @@ using Xunit; -namespace DDD.HealthcareDelivery +namespace DDD.Core.Infrastructure.Data { [CollectionDefinition("Oracle")] public class OracleCollection : ICollectionFixture diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleEventStreamPositionUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleEventStreamPositionUpdaterTests.cs new file mode 100644 index 0000000..cdd3a69 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleEventStreamPositionUpdaterTests.cs @@ -0,0 +1,17 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("Oracle")] + public class OracleEventStreamPositionUpdaterTests : EventStreamPositionUpdaterTests + { + + #region Constructors + + public OracleEventStreamPositionUpdaterTests(OracleFixture fixture) : base(fixture) + { + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleEventStreamReaderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleEventStreamReaderTests.cs new file mode 100644 index 0000000..40033a5 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleEventStreamReaderTests.cs @@ -0,0 +1,81 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + + [Collection("Oracle")] + public class OracleEventStreamReaderTests : EventStreamReaderTests + { + + #region Constructors + + public OracleEventStreamReaderTests(OracleFixture fixture) : base(fixture) + { + } + + #endregion Constructors + + #region Methods + + public static IEnumerable QueriesAndResults_2() + { + yield return new object[] + { + new ReadEventStream + { + StreamType = "MessageBox", + StreamPosition = new Guid("d9fdd908-9d0a-b6e8-0802-5680bf57bb60") + }, + new [] + { + new Event + { + Body = "{\"boxId\":3,\"occurredOn\":\"2022-01-19T14:22:00.3683143+01:00\"}", + BodyFormat = "JSON", + EventId = new Guid("d9fdd908-9e0a-c30f-fe28-f1dd2406a828"), + EventType = "DDD.Collaboration.Domain.Messages.MessageBoxCreated, DDD.Collaboration.Messages", + OccurredOn = new DateTime(2022, 1, 19, 14, 22, 0, 368), + StreamId = "3", + StreamType = "MessageBox", + IssuedBy = "Dr Folamour" + } + } + }; + } + + [Theory] + [MemberData(nameof(QueriesAndResults_2))] + public void Handle_WhenCalled_ReturnsExpectedResults_2(ReadEventStream query, IEnumerable expectedResults) + { + // Arrange + Fixture.ExecuteScriptFromResources("ReadEventStream"); + var handler = new EventStreamReader(ConnectionProvider); + // Act + var results = handler.Handle(query); + // Assert + results.Should().BeEquivalentTo(expectedResults, context => context.WithStrictOrdering()); + } + + [Theory] + [MemberData(nameof(QueriesAndResults_2))] + public async Task HandleAsync_WhenCalled_ReturnsExpectedResults_2(ReadEventStream query, IEnumerable expectedResults) + { + // Arrange + Fixture.ExecuteScriptFromResources("ReadEventStream"); + var handler = new EventStreamReader(ConnectionProvider); + // Act + var results = await handler.HandleAsync(query); + // Assert + results.Should().BeEquivalentTo(expectedResults, context => context.WithStrictOrdering()); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleEventStreamSubscriberTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleEventStreamSubscriberTests.cs new file mode 100644 index 0000000..90dacc8 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleEventStreamSubscriberTests.cs @@ -0,0 +1,17 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("Oracle")] + public class OracleEventStreamSubscriberTests : EventStreamSubscriberTests + { + + #region Constructors + + public OracleEventStreamSubscriberTests(OracleFixture fixture) : base(fixture) + { + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleEventStreamsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleEventStreamsFinderTests.cs new file mode 100644 index 0000000..3ef7345 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleEventStreamsFinderTests.cs @@ -0,0 +1,17 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("Oracle")] + public class OracleEventStreamsFinderTests : EventStreamsFinderTests + { + + #region Constructors + + public OracleEventStreamsFinderTests(OracleFixture fixture) : base(fixture) + { + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamExcluderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamExcluderTests.cs new file mode 100644 index 0000000..4f985fb --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamExcluderTests.cs @@ -0,0 +1,17 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("Oracle")] + public class OracleFailedEventStreamExcluderTests : FailedEventStreamExcluderTests + { + + #region Constructors + + public OracleFailedEventStreamExcluderTests(OracleFixture fixture) : base(fixture) + { + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamIncluderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamIncluderTests.cs new file mode 100644 index 0000000..13b2cc0 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamIncluderTests.cs @@ -0,0 +1,17 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("Oracle")] + public class OracleFailedEventStreamIncluderTests : FailedEventStreamIncluderTests + { + + #region Constructors + + public OracleFailedEventStreamIncluderTests(OracleFixture fixture) : base(fixture) + { + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamPositionUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamPositionUpdaterTests.cs new file mode 100644 index 0000000..865030e --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamPositionUpdaterTests.cs @@ -0,0 +1,17 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("Oracle")] + public class OracleFailedEventStreamPositionUpdaterTests : FailedEventStreamPositionUpdaterTests + { + + #region Constructors + + public OracleFailedEventStreamPositionUpdaterTests(OracleFixture fixture) : base(fixture) + { + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamReaderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamReaderTests.cs new file mode 100644 index 0000000..7f22b3c --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamReaderTests.cs @@ -0,0 +1,105 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + + [Collection("Oracle")] + public class OracleFailedEventStreamReaderTests : EventsByStreamIdFinderTests + { + + #region Constructors + + public OracleFailedEventStreamReaderTests(OracleFixture fixture) : base(fixture) + { + } + + #endregion Constructors + + #region Methods + + public static IEnumerable QueriesAndResults_2() + { + yield return new object[] + { + new ReadFailedEventStream + { + StreamType = "Message", + StreamId = "5", + EventIdMin = new Guid("d9fdd908-9e0a-c80f-e72d-e94a0f7d4902"), + EventIdMax = new Guid("d9fdd908-9e0a-ca0f-7c1b-78288db70ee6") + }, + new [] + { + new Event + { + Body = "{\"messageId\":5,\"occurredOn\":\"2022-01-20T14:01:41.3251974+01:00\"}", + BodyFormat = "JSON", + EventId = new Guid("d9fdd908-9e0a-c80f-e72d-e94a0f7d4902"), + EventType = "DDD.Collaboration.Domain.Messages.MessageRead, DDD.Collaboration.Messages", + OccurredOn = new DateTime(2022, 1, 20, 14, 1, 41, 325), + StreamId = "5", + StreamType = "Message", + IssuedBy = "Dr Folamour" + }, + new Event + { + Body = "{\"messageId\":5,\"source\":\"Inbox\",\"destination\":\"Binbox\",\"occurredOn\":\"2022-01-20T14:01:48.6157105+01:00\"}", + BodyFormat = "JSON", + EventId = new Guid("d9fdd908-9e0a-c90f-3cfe-09c19f22f068"), + EventType = "DDD.Collaboration.Domain.Messages.MessageSentToBin, DDD.Collaboration.Messages", + OccurredOn = new DateTime(2022, 1, 20, 14, 1, 48, 616), + StreamId = "5", + StreamType = "Message", + IssuedBy = "Dr Folamour" + }, + new Event + { + Body = "{\"messageId\":5,\"occurredOn\":\"2022-01-20T14:02:05.9149942+01:00\"}", + BodyFormat = "JSON", + EventId = new Guid("d9fdd908-9e0a-ca0f-7c1b-78288db70ee6"), + EventType = "DDD.Collaboration.Domain.Messages.MessageDeleted, DDD.Collaboration.Messages", + OccurredOn = new DateTime(2022, 1, 20, 14, 2, 5, 915), + StreamId = "5", + StreamType = "Message", + IssuedBy = "Dr Folamour" + } + } + }; + } + + [Theory] + [MemberData(nameof(QueriesAndResults_2))] + public void Handle_WhenCalled_ReturnsExpectedResults_2(ReadFailedEventStream query, IEnumerable expectedResults) + { + // Arrange + Fixture.ExecuteScriptFromResources("ReadFailedEventStream"); + var handler = new FailedEventStreamReader(ConnectionProvider); + // Act + var results = handler.Handle(query); + // Assert + results.Should().BeEquivalentTo(expectedResults, context => context.WithStrictOrdering()); + } + + [Theory] + [MemberData(nameof(QueriesAndResults_2))] + public async Task HandleAsync_WhenCalled_ReturnsExpectedResults_2(ReadFailedEventStream query, IEnumerable expectedResults) + { + // Arrange + Fixture.ExecuteScriptFromResources("ReadFailedEventStream"); + var handler = new FailedEventStreamReader(ConnectionProvider); + // Act + var results = await handler.HandleAsync(query); + // Assert + results.Should().BeEquivalentTo(expectedResults, context => context.WithStrictOrdering()); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamUpdaterTests.cs new file mode 100644 index 0000000..e7a06eb --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamUpdaterTests.cs @@ -0,0 +1,17 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("Oracle")] + public class OracleFailedEventStreamUpdaterTests : FailedEventStreamUpdaterTests + { + + #region Constructors + + public OracleFailedEventStreamUpdaterTests(OracleFixture fixture) : base(fixture) + { + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamsFinderTests.cs new file mode 100644 index 0000000..f3f14ab --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamsFinderTests.cs @@ -0,0 +1,17 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("Oracle")] + public class OracleFailedEventStreamsFinderTests : FailedEventStreamsFinderTests + { + + #region Constructors + + public OracleFailedEventStreamsFinderTests(OracleFixture fixture) : base(fixture) + { + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedRecurringCommandUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedRecurringCommandUpdaterTests.cs new file mode 100644 index 0000000..f34afdd --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedRecurringCommandUpdaterTests.cs @@ -0,0 +1,18 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("Oracle")] + public class OracleFailedRecurringCommandUpdaterTests : FailedRecurringCommandUpdaterTests + { + + #region Constructors + + public OracleFailedRecurringCommandUpdaterTests(OracleFixture fixture) : base(fixture) + { + } + + #endregion Constructors + + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFixture.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFixture.cs new file mode 100644 index 0000000..899a0c1 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFixture.cs @@ -0,0 +1,71 @@ +using Dapper; +using Oracle.ManagedDataAccess.Client; +using System.Data; +#if (NET6_0) +using System.Data.Common; +#endif +using System.Configuration; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + using Serialization; + using Mapping; + using Testing; + + public class OracleFixture : DbFixture, IPersistenceFixture + { + + #region Constructors + + public OracleFixture() : base("OracleScripts", ConfigurationManager.ConnectionStrings["Oracle"]) + { + SqlMapper.ResetTypeHandlers(); + SqlMapper.AddTypeHandler(new OracleGuidTypeHandler()); + SqlMapper.AddTypeHandler(new IncrementalDelaysTypeMapper()); + } + + #endregion Constructors + + #region Methods + + public IObjectTranslator CreateEventTranslator() + { + return new EventTranslator(JsonSerializerWrapper.Create(false)); + } + + protected override void CreateDatabase() + { + using (var connection = CreateConnection()) + { + var builder = new OracleConnectionStringBuilder(connection.ConnectionString) { UserID = "SYS", DBAPrivilege = "SYSDBA" }; + connection.ConnectionString = builder.ConnectionString; + connection.Open(); + ExecuteScript(OracleScripts.CreateSchema, connection); + } + ExecuteScript(OracleScripts.FillSchema); + } + + protected override int[] ExecuteScript(string script, IDbConnection connection) + { + return connection.ExecuteScript(script, batchSeparator: "/"); + } + + protected override void LoadConfiguration() + { +#if (NET6_0) + DbProviderFactories.RegisterFactory("Oracle.ManagedDataAccess.Client", OracleClientFactory.Instance); +#endif + } + + protected override string SetPooling(string connectionString, bool pooling) + { + var builder = new OracleConnectionStringBuilder(connectionString) { Pooling = pooling }; + return builder.ConnectionString; + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleIDbConnectionExtensionsTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleIDbConnectionExtensionsTests.cs new file mode 100644 index 0000000..90be6bb --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleIDbConnectionExtensionsTests.cs @@ -0,0 +1,17 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("Oracle")] + public class OracleIDbConnectionExtensionsTests : IDbConnectionExtensionsTests + { + + #region Constructors + + public OracleIDbConnectionExtensionsTests(OracleFixture fixture) : base(fixture) + { + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleRecurringCommandRegisterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleRecurringCommandRegisterTests.cs new file mode 100644 index 0000000..f3ffd0a --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleRecurringCommandRegisterTests.cs @@ -0,0 +1,18 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("Oracle")] + public class OracleRecurringCommandRegisterTests : RecurringCommandRegisterTests + { + + #region Constructors + + public OracleRecurringCommandRegisterTests(OracleFixture fixture) : base(fixture) + { + } + + #endregion Constructors + + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleRecurringCommandsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleRecurringCommandsFinderTests.cs new file mode 100644 index 0000000..6a940c0 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleRecurringCommandsFinderTests.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using DDD.Core.Application; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("Oracle")] + public class OracleRecurringCommandsFinderTests : RecurringCommandsFinderTests + { + + #region Constructors + + public OracleRecurringCommandsFinderTests(OracleFixture fixture) : base(fixture) + { + } + + #endregion Constructors + + #region Methods + + protected override IEnumerable ExpectedResults() + { + yield return new RecurringCommand + { + CommandId = new Guid("d9fdd908-9d0a-b6e8-0802-5680bf57bb60"), + CommandType = "DDD.Core.Application.FakeCommand1, DDD.Core.Messages", + Body = "{\"Property1\":\"dummy\",\"Property2\":10}", + BodyFormat = "JSON", + RecurringExpression = "* * * * *", + RecurringExpressionFormat = "CRON" + }; + yield return new RecurringCommand + { + CommandId = new Guid("d9fdd908-9e0a-bd0f-9ec5-ae08e4f34ff4"), + CommandType = "DDD.Core.Application.FakeCommand2, DDD.Core.Messages", + Body = "{\"Property1\":\"dummy\",\"Property2\":10,\"Property3\":\"2022-12-24T14:49:44.361964+01:00\"}", + BodyFormat = "JSON", + RecurringExpression = "0 0 1 * *", + RecurringExpressionFormat = "CRON" + }; + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleScripts.Designer.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleScripts.Designer.cs new file mode 100644 index 0000000..569d1d6 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleScripts.Designer.cs @@ -0,0 +1,331 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace DDD.Core.Infrastructure.Data { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class OracleScripts { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal OracleScripts() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DDD.Core.Infrastructure.Data.OracleScripts", typeof(OracleScripts).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to DECLARE + /// usercount NUMBER; + ///BEGIN + /// SELECT COUNT(*) INTO usercount FROM dba_users WHERE username = 'TEST'; + /// IF usercount > 0 THEN + /// EXECUTE IMMEDIATE 'DROP USER TEST CASCADE'; + /// END IF; + /// EXECUTE IMMEDIATE 'CREATE USER TEST IDENTIFIED BY dev'; + /// EXECUTE IMMEDIATE 'GRANT ALL PRIVILEGES TO TEST'; + ///END;. + /// + internal static string CreateSchema { + get { + return ResourceManager.GetString("CreateSchema", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to BEGIN + /// SPCLEARSCHEMA(); + ///END; + ////. + /// + internal static string ExcludeFailedEventStream { + get { + return ResourceManager.GetString("ExcludeFailedEventStream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to -------------------------------------------------------- + ///-- File created - Thursday-March-03-2022 + ///-------------------------------------------------------- + ///-------------------------------------------------------- + ///-- DDL for Procedure SPCLEARSCHEMA + ///-------------------------------------------------------- + /// + ///CREATE OR REPLACE PROCEDURE TEST.SPCLEARSCHEMA AS + ///BEGIN + /// FOR c IN (SELECT table_name, constraint_name FROM user_constraints WHERE constraint_type = 'R') + /// LOOP + /// EXECUTE IMMEDIATE ('alter [rest of string was truncated]";. + /// + internal static string FillSchema { + get { + return ResourceManager.GetString("FillSchema", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to BEGIN + /// SPCLEARSCHEMA(); + ///END; + //// + ///INSERT INTO TEST.EventStream(Type, Source, Position, RetryMax, RetryDelays, BlockSize) VALUES ('Person', 'ID', 'D05BDFF763877E677E6B3A0044746810', 5, '10,60,120/80', 50) + //// + ///INSERT INTO TEST.EventStream(Type, Source, Position, RetryMax, RetryDelays, BlockSize) VALUES ('MedicalProduct', 'OFR', '00000000000000000000000000000000', 3, '60', 100) + //// . + /// + internal static string FindEventStreams { + get { + return ResourceManager.GetString("FindEventStreams", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to BEGIN + /// SPCLEARSCHEMA(); + ///END; + //// + ///Insert into TEST.FAILEDEVENTSTREAM (STREAMID,STREAMTYPE,STREAMSOURCE,STREAMPOSITION,EVENTID,EVENTTYPE,EXCEPTIONTIME,EXCEPTIONTYPE,EXCEPTIONMESSAGE,EXCEPTIONSOURCE,EXCEPTIONINFO,BASEEXCEPTIONTYPE,BASEEXCEPTIONMESSAGE,RETRYCOUNT,RETRYMAX,RETRYDELAYS,BLOCKSIZE) values ('2','MessageBox','COL','7A70770A47C11B9E883A08DA0E368663','4DDD0AE15118DE7E883B08DA0E368663','DDD.Collaboration.Domain.Messages.MessageBoxCreated, DDD.Collaboration.Messages',to_date('19/11/21','DD/MM/RR'),'Sy [rest of string was truncated]";. + /// + internal static string FindFailedEventStreams { + get { + return ResourceManager.GetString("FindFailedEventStreams", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to BEGIN + /// SPCLEARSCHEMA(); + ///END; + //// + ///INSERT INTO TEST.COMMAND (COMMANDID, COMMANDTYPE, BODY, BODYFORMAT, RECURRINGEXPRESSION, LASTEXECUTIONTIME, LASTEXECUTIONSTATUS, LASTEXCEPTIONINFO) + ///VALUES (N'08D9FDD90A9DE8B608025680BF57BB60', N'DDD.Core.Application.FakeCommand1, DDD.Core.Messages', '{"Property1":"dummy","Property2":10}', N'JSON', N'* * * * *', NULL, NULL, NULL) + //// + ///INSERT INTO TEST.COMMAND (COMMANDID, COMMANDTYPE, BODY, BODYFORMAT, RECURRINGEXPRESSION, LASTEXECUTIONTIME, LASTEXECUTIONSTATUS, LASTEXCEPTI [rest of string was truncated]";. + /// + internal static string FindRecurringCommands { + get { + return ResourceManager.GetString("FindRecurringCommands", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to BEGIN + /// SPCLEARSCHEMA(); + ///END; + //// + ///Insert into TEST.FAILEDEVENTSTREAM (STREAMID,STREAMTYPE,STREAMSOURCE,STREAMPOSITION,EVENTID,EVENTTYPE,EXCEPTIONTIME,EXCEPTIONTYPE,EXCEPTIONMESSAGE,EXCEPTIONSOURCE,EXCEPTIONINFO,BASEEXCEPTIONTYPE,BASEEXCEPTIONMESSAGE,RETRYCOUNT,RETRYMAX,RETRYDELAYS,BLOCKSIZE) values ('2','MessageBox','COL','7A70770A47C11B9E883A08DA0E368663','4DDD0AE15118DE7E883B08DA0E368663','DDD.Collaboration.Domain.Messages.MessageBoxCreated, DDD.Collaboration.Messages',to_date('19/11/21','DD/MM/RR'),'Sy [rest of string was truncated]";. + /// + internal static string IncludeFailedEventStream { + get { + return ResourceManager.GetString("IncludeFailedEventStream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to BEGIN + /// SPCLEARSCHEMA(); + ///END; + //// + ///INSERT INTO TEST.COMMAND (COMMANDID, COMMANDTYPE, BODY, BODYFORMAT, RECURRINGEXPRESSION, LASTEXECUTIONTIME, LASTEXECUTIONSTATUS, LASTEXCEPTIONINFO) + ///VALUES (N'7db3be36011e7dbbfb2a3a0044745345', N'DDD.Core.Application.FakeCommand1, DDD.Core.Messages', '{"Property1":"dummy","Property2":10}', N'JSON', N'* * * * *', NULL, NULL, NULL) + //// + ///INSERT INTO TEST.COMMAND (COMMANDID, COMMANDTYPE, BODY, BODYFORMAT, RECURRINGEXPRESSION, LASTEXECUTIONTIME, LASTEXECUTIONSTATUS, LASTEXCEPTI [rest of string was truncated]";. + /// + internal static string MarkRecurringCommandAsFailed { + get { + return ResourceManager.GetString("MarkRecurringCommandAsFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to BEGIN + /// SPCLEARSCHEMA(); + ///END; + //// + ///INSERT INTO TEST.COMMAND (COMMANDID, COMMANDTYPE, BODY, BODYFORMAT, RECURRINGEXPRESSION, LASTEXECUTIONTIME, LASTEXECUTIONSTATUS, LASTEXCEPTIONINFO) + ///VALUES (N'7db3be36011e7dbbfb2a3a0044745345', N'DDD.Core.Application.FakeCommand1, DDD.Core.Messages', '{"Property1":"dummy","Property2":10}', N'JSON', N'* * * * *', to_timestamp('01/01/22 00:00:00,000000000','DD/MM/RR HH24:MI:SSXFF'),'F','System.TimeoutException: The operation has timed-out.') + //// + ///INSERT INTO TEST.COMMAND (CO [rest of string was truncated]";. + /// + internal static string MarkRecurringCommandAsSuccessful { + get { + return ResourceManager.GetString("MarkRecurringCommandAsSuccessful", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to BEGIN + /// SPCLEARSCHEMA(); + ///END; + //// + ///INSERT INTO TABLEWITHID (ID) VALUES (1) + //// + ///INSERT INTO TABLEWITHID (ID) VALUES (2) + //// + ///INSERT INTO TABLEWITHID (ID) VALUES (3) + //// + ///INSERT INTO TABLEWITHID (ID) VALUES (4) + ////. + /// + internal static string NextId_ExistingRows { + get { + return ResourceManager.GetString("NextId_ExistingRows", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to BEGIN + /// SPCLEARSCHEMA(); + ///END; + ////. + /// + internal static string NextId_NoRow { + get { + return ResourceManager.GetString("NextId_NoRow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to BEGIN + /// SPCLEARSCHEMA(); + ///END; + //// + ///INSERT INTO TEST.Event (EventId, EventType, OccurredOn, Body, BodyFormat, StreamId, StreamType, IssuedBy) VALUES (N'08D9FDD90A9DE8B608025680BF57BB60', N'DDD.Collaboration.Domain.Messages.MessageBoxCreated, DDD.Collaboration.Messages', TO_TIMESTAMP(N'2021-11-18 10:01:23.5280000', 'YYYY-MM-DD HH24:MI:SS.FF7'), N'{"boxId":2,"occurredOn":"2021-11-18T10:01:23.5277314+01:00"}', N'JSON', N'2', N'MessageBox', N'Dr Maboul') + //// + ///INSERT INTO TEST.Event (EventId, EventType, OccurredO [rest of string was truncated]";. + /// + internal static string ReadEventStream { + get { + return ResourceManager.GetString("ReadEventStream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to BEGIN + /// SPCLEARSCHEMA(); + ///END; + //// + ///INSERT INTO TEST.Event (EventId, EventType, OccurredOn, Body, BodyFormat, StreamId, StreamType, IssuedBy) VALUES (N'08D9FDD90A9DE8B608025680BF57BB60', N'DDD.Collaboration.Domain.Messages.MessageBoxCreated, DDD.Collaboration.Messages', TO_TIMESTAMP(N'2021-11-18 10:01:23.5280000', 'YYYY-MM-DD HH24:MI:SS.FF7'), N'{"boxId":2,"occurredOn":"2021-11-18T10:01:23.5277314+01:00"}', N'JSON', N'2', N'MessageBox', N'Dr Maboul') + //// + ///INSERT INTO TEST.Event (EventId, EventType, OccurredO [rest of string was truncated]";. + /// + internal static string ReadFailedEventStream { + get { + return ResourceManager.GetString("ReadFailedEventStream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to BEGIN + /// SPCLEARSCHEMA(); + ///END; + ////. + /// + internal static string RegisterRecurringCommand { + get { + return ResourceManager.GetString("RegisterRecurringCommand", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to BEGIN + /// SPCLEARSCHEMA(); + ///END; + ////. + /// + internal static string SubscribeToEventStream { + get { + return ResourceManager.GetString("SubscribeToEventStream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to BEGIN + /// SPCLEARSCHEMA(); + ///END; + //// + ///INSERT INTO TEST.EventStream(Type, Source, Position, RetryMax, RetryDelays, BlockSize) VALUES ('Person', 'ID', 'D05BDFF763877E677E6B3A0044746810', 5, '10,60,120/80', 50) + //// + ///INSERT INTO TEST.EventStream(Type, Source, Position, RetryMax, RetryDelays, BlockSize) VALUES ('MedicalProduct', 'OFR', '00000000000000000000000000000000', 3, '60', 100) + //// . + /// + internal static string UpdateEventStreamPosition { + get { + return ResourceManager.GetString("UpdateEventStreamPosition", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to BEGIN + /// SPCLEARSCHEMA(); + ///END; + //// + ///Insert into TEST.FAILEDEVENTSTREAM (STREAMID,STREAMTYPE,STREAMSOURCE,STREAMPOSITION,EVENTID,EVENTTYPE,EXCEPTIONTIME,EXCEPTIONTYPE,EXCEPTIONMESSAGE,EXCEPTIONSOURCE,EXCEPTIONINFO,BASEEXCEPTIONTYPE,BASEEXCEPTIONMESSAGE,RETRYCOUNT,RETRYMAX,RETRYDELAYS,BLOCKSIZE) values ('2','MessageBox','COL','7A70770A47C11B9E883A08DA0E368663','4DDD0AE15118DE7E883B08DA0E368663','DDD.Collaboration.Domain.Messages.MessageBoxCreated, DDD.Collaboration.Messages',to_date('19/11/21','DD/MM/RR'),'Sy [rest of string was truncated]";. + /// + internal static string UpdateFailedEventStream { + get { + return ResourceManager.GetString("UpdateFailedEventStream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to BEGIN + /// SPCLEARSCHEMA(); + ///END; + //// + ///Insert into TEST.FAILEDEVENTSTREAM (STREAMID,STREAMTYPE,STREAMSOURCE,STREAMPOSITION,EVENTID,EVENTTYPE,EXCEPTIONTIME,EXCEPTIONTYPE,EXCEPTIONMESSAGE,EXCEPTIONSOURCE,EXCEPTIONINFO,BASEEXCEPTIONTYPE,BASEEXCEPTIONMESSAGE,RETRYCOUNT,RETRYMAX,RETRYDELAYS,BLOCKSIZE) values ('2','MessageBox','COL','7A70770A47C11B9E883A08DA0E368663','4DDD0AE15118DE7E883B08DA0E368663','Xperthis.Collaboration.Domain.Messages.MessageBoxCreated, Xperthis.Collaboration.Messages',to_date('19/11/21','DD/M [rest of string was truncated]";. + /// + internal static string UpdateFailedEventStreamPosition { + get { + return ResourceManager.GetString("UpdateFailedEventStreamPosition", resourceCulture); + } + } + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleScripts.resx b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleScripts.resx new file mode 100644 index 0000000..732bcd1 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleScripts.resx @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + scripts\oracle\createschema.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 + + + scripts\oracle\excludefailedeventstream.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\oracle\fillschema.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 + + + scripts\oracle\findeventstreams.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\oracle\findfailedeventstreams.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\oracle\findrecurringcommands.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\oracle\includefailedeventstream.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\oracle\markrecurringcommandasfailed.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\oracle\markrecurringcommandassuccessful.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\oracle\nextid_existingrows.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\oracle\nextid_norow.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\oracle\readeventstream.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\oracle\readfailedeventstream.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\oracle\registerrecurringcommand.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\oracle\subscribetoeventstream.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\oracle\updateeventstreamposition.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\oracle\updatefailedeventstream.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\oracle\updatefailedeventstreamposition.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleSuccessfulRecurringCommandUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleSuccessfulRecurringCommandUpdaterTests.cs new file mode 100644 index 0000000..d3c8fc5 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleSuccessfulRecurringCommandUpdaterTests.cs @@ -0,0 +1,18 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("Oracle")] + public class OracleSuccessfulRecurringCommandUpdaterTests : SuccessfulRecurringCommandUpdaterTests + { + + #region Constructors + + public OracleSuccessfulRecurringCommandUpdaterTests(OracleFixture fixture) : base(fixture) + { + } + + #endregion Constructors + + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/RecurringCommandRegisterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/RecurringCommandRegisterTests.cs new file mode 100644 index 0000000..0d61814 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/RecurringCommandRegisterTests.cs @@ -0,0 +1,107 @@ +using FluentAssertions; +using Dapper; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + + public abstract class RecurringCommandRegisterTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected RecurringCommandRegisterTests(TFixture fixture) + { + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); + } + + #endregion Constructors + + #region Properties + + protected TFixture Fixture { get; } + + protected IDbConnectionProvider ConnectionProvider { get; } + + #endregion Properties + + #region Methods + + [Fact] + public void Handle_WhenCalled_RegistersCommand() + { + // Arrange + Fixture.ExecuteScriptFromResources("RegisterRecurringCommand"); + var handler = new RecurringCommandRegister(ConnectionProvider); + var command = CreateCommand(); + var expectedCommands = ExpectedCommands(); + // Act + handler.Handle(command); + // Assert + RegistredCommands().Should().BeEquivalentTo(expectedCommands); + } + + [Fact] + public async Task HandleAsync_WhenCalled_RegistersCommand() + { + // Arrange + Fixture.ExecuteScriptFromResources("RegisterRecurringCommand"); + var handler = new RecurringCommandRegister(ConnectionProvider); + var command = CreateCommand(); + var expectedCommands = ExpectedCommands(); + // Act + await handler.HandleAsync(command); + // Assert + RegistredCommands().Should().BeEquivalentTo(expectedCommands); + } + + public void Dispose() + { + ConnectionProvider.Dispose(); + } + + private static RegisterRecurringCommand CreateCommand() + { + return new RegisterRecurringCommand + { + CommandId = new Guid("36beb37d-1e01-bb7d-fb2a-3a0044745345"), + CommandType = "DDD.Core.Application.FakeCommand1, DDD.Core.Messages", + Body = "{\"Property1\":\"dummy\",\"Property2\":10}", + BodyFormat = "JSON", + RecurringExpression = "* * * * *", + RecurringExpressionFormat = "CRON" + }; + } + + private IEnumerable RegistredCommands() + { + using (var connection = Fixture.CreateConnection()) + { + connection.Open(); + return connection.Query("SELECT CommandId, CommandType, Body, BodyFormat, RecurringExpression, RecurringExpressionFormat FROM Command"); + } + } + + private static IEnumerable ExpectedCommands() + { + yield return new RecurringCommand + { + CommandId = new Guid("36beb37d-1e01-bb7d-fb2a-3a0044745345"), + CommandType = "DDD.Core.Application.FakeCommand1, DDD.Core.Messages", + Body = "{\"Property1\":\"dummy\",\"Property2\":10}", + BodyFormat = "JSON", + RecurringExpression = "* * * * *", + RecurringExpressionFormat = "CRON" + }; + } + + #endregion Methods + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/RecurringCommandsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/RecurringCommandsFinderTests.cs new file mode 100644 index 0000000..8e97021 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/RecurringCommandsFinderTests.cs @@ -0,0 +1,73 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + + public abstract class RecurringCommandsFinderTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected RecurringCommandsFinderTests(TFixture fixture) + { + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); + } + + #endregion Constructors + + #region Properties + + protected TFixture Fixture { get; } + + protected IDbConnectionProvider ConnectionProvider { get; } + + #endregion Properties + + #region Methods + + [Fact] + public void Handle_WhenCalled_ReturnsExpectedResults() + { + // Arrange + Fixture.ExecuteScriptFromResources("FindRecurringCommands"); + var query = new FindRecurringCommands(); + var expectedResults = ExpectedResults(); + var handler = new RecurringCommandsFinder(ConnectionProvider); + // Act + var results = handler.Handle(query); + // Assert + results.Should().BeEquivalentTo(expectedResults, context => context.WithStrictOrdering()); + } + + [Fact] + public async Task HandleAsync_WhenCalled_ReturnsExpectedResults() + { + // Arrange + Fixture.ExecuteScriptFromResources("FindRecurringCommands"); + var query = new FindRecurringCommands(); + var expectedResults = ExpectedResults(); + var handler = new RecurringCommandsFinder(ConnectionProvider); + // Act + var results = await handler.HandleAsync(query); + // Assert + results.Should().BeEquivalentTo(expectedResults, context => context.WithStrictOrdering()); + } + + public void Dispose() + { + ConnectionProvider.Dispose(); + } + + protected abstract IEnumerable ExpectedResults(); + + #endregion Methods + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/CreateSchema.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/CreateSchema.sql new file mode 100644 index 0000000..8b7bb14 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/CreateSchema.sql @@ -0,0 +1,10 @@ +DECLARE + usercount NUMBER; +BEGIN + SELECT COUNT(*) INTO usercount FROM dba_users WHERE username = 'TEST'; + IF usercount > 0 THEN + EXECUTE IMMEDIATE 'DROP USER TEST CASCADE'; + END IF; + EXECUTE IMMEDIATE 'CREATE USER TEST IDENTIFIED BY dev'; + EXECUTE IMMEDIATE 'GRANT ALL PRIVILEGES TO TEST'; +END; \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/ExcludeFailedEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/ExcludeFailedEventStream.sql new file mode 100644 index 0000000..f809473 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/ExcludeFailedEventStream.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/FillSchema.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/FillSchema.sql new file mode 100644 index 0000000..c65d3d9 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/FillSchema.sql @@ -0,0 +1,103 @@ +-------------------------------------------------------- +-- File created - Thursday-March-03-2022 +-------------------------------------------------------- +-------------------------------------------------------- +-- DDL for Procedure SPCLEARSCHEMA +-------------------------------------------------------- + +CREATE OR REPLACE PROCEDURE TEST.SPCLEARSCHEMA AS +BEGIN + FOR c IN (SELECT table_name, constraint_name FROM user_constraints WHERE constraint_type = 'R') + LOOP + EXECUTE IMMEDIATE ('alter table ' || c.table_name || ' disable constraint ' || c.constraint_name); + END LOOP; + FOR c IN (SELECT table_name FROM user_tables) + LOOP + EXECUTE IMMEDIATE ('truncate table ' || c.table_name); + END LOOP; + FOR c IN (SELECT table_name, constraint_name FROM user_constraints WHERE constraint_type = 'R') + LOOP + EXECUTE IMMEDIATE ('alter table ' || c.table_name || ' enable constraint ' || c.constraint_name); + END LOOP; +END SPCLEARSCHEMA; +/ +-------------------------------------------------------- +-- DDL for Table COMMAND +-------------------------------------------------------- +CREATE TABLE TEST.COMMAND + (COMMANDID RAW(16) NOT NULL, + COMMANDTYPE VARCHAR2(250 CHAR) NOT NULL, + BODY VARCHAR2(4000 CHAR) NOT NULL, + BODYFORMAT VARCHAR2(20 CHAR) NOT NULL, + RECURRINGEXPRESSION VARCHAR2(150 CHAR) NOT NULL, + RECURRINGEXPRESSIONFORMAT VARCHAR2(20 CHAR) NOT NULL, + LASTEXECUTIONTIME TIMESTAMP(3) NULL, + LASTEXECUTIONSTATUS CHAR(1 CHAR) NULL, + LASTEXCEPTIONINFO VARCHAR2(4000 CHAR) NULL, + CONSTRAINT PK_COMMAND PRIMARY KEY(COMMANDID)) +/ +-------------------------------------------------------- +-- DDL for Index IX_COMMAND +-------------------------------------------------------- +CREATE UNIQUE INDEX TEST.IX_COMMAND ON TEST.COMMAND (COMMANDTYPE) +/ +-------------------------------------------------------- +-- DDL for Table EVENT +-------------------------------------------------------- + + CREATE TABLE TEST.EVENT + (EVENTID RAW(16) NOT NULL, + EVENTTYPE VARCHAR2(250 CHAR) NOT NULL, + OCCURREDON TIMESTAMP(3) NOT NULL, + BODY VARCHAR2(4000 CHAR) NOT NULL, + BODYFORMAT VARCHAR2(20 CHAR) NOT NULL, + STREAMID VARCHAR2(50 CHAR) NOT NULL, + STREAMTYPE VARCHAR2(50 CHAR) NOT NULL, + ISSUEDBY VARCHAR2(100 CHAR), + CONSTRAINT PK_EVENT PRIMARY KEY(EVENTID)) +/ +-------------------------------------------------------- +-- DDL for Table EVENTSTREAM +-------------------------------------------------------- + + CREATE TABLE TEST.EVENTSTREAM + (TYPE VARCHAR2(50 CHAR) NOT NULL, + SOURCE VARCHAR2(5 CHAR) NOT NULL, + POSITION RAW(16) NOT NULL, + RETRYMAX NUMBER(3,0) NOT NULL, + RETRYDELAYS VARCHAR2(50 CHAR) NOT NULL, + BLOCKSIZE NUMBER(5,0) NOT NULL, + CONSTRAINT PK_EVENTSTREAM PRIMARY KEY(TYPE, SOURCE)) +/ +-------------------------------------------------------- +-- DDL for Table FAILEDEVENTSTREAM +-------------------------------------------------------- + + CREATE TABLE TEST.FAILEDEVENTSTREAM + (STREAMID VARCHAR2(50 CHAR) NOT NULL, + STREAMTYPE VARCHAR2(50 CHAR) NOT NULL, + STREAMSOURCE VARCHAR2(5 CHAR) NOT NULL, + STREAMPOSITION RAW(16) NOT NULL, + EVENTID RAW(16) NOT NULL, + EVENTTYPE VARCHAR2(250 CHAR) NOT NULL, + EXCEPTIONTIME TIMESTAMP(3) NOT NULL, + EXCEPTIONTYPE VARCHAR2(250 CHAR) NOT NULL, + EXCEPTIONMESSAGE VARCHAR2(4000 CHAR) NOT NULL, + EXCEPTIONSOURCE VARCHAR2(250 CHAR) NOT NULL, + EXCEPTIONINFO VARCHAR2(4000 CHAR) NOT NULL, + BASEEXCEPTIONTYPE VARCHAR2(250 CHAR) NOT NULL, + BASEEXCEPTIONMESSAGE VARCHAR2(4000 CHAR) NOT NULL, + RETRYCOUNT NUMBER(3,0) NOT NULL, + RETRYMAX NUMBER(3,0) NOT NULL, + RETRYDELAYS VARCHAR2(50 CHAR), + BLOCKSIZE NUMBER(5,0) NOT NULL, + CONSTRAINT PK_FAILEDEVENTSTREAM PRIMARY KEY(STREAMID, STREAMTYPE, STREAMSOURCE)) +/ +-------------------------------------------------------- +-- DDL for Table TABLEWITHID +-------------------------------------------------------- + + CREATE TABLE TEST.TABLEWITHID + (ID NUMBER(10,0) NOT NULL, + CONSTRAINT PK_TABLEWITHID PRIMARY KEY(ID)) +/ \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/FindEventStreams.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/FindEventStreams.sql new file mode 100644 index 0000000..c372bd0 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/FindEventStreams.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/FindFailedEventStreams.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/FindFailedEventStreams.sql new file mode 100644 index 0000000..edb0b10 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/FindFailedEventStreams.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/FindRecurringCommands.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/FindRecurringCommands.sql new file mode 100644 index 0000000..93f9d69 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/FindRecurringCommands.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/IncludeFailedEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/IncludeFailedEventStream.sql new file mode 100644 index 0000000..edb0b10 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/IncludeFailedEventStream.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/MarkRecurringCommandAsFailed.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/MarkRecurringCommandAsFailed.sql new file mode 100644 index 0000000..34e5adc Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/MarkRecurringCommandAsFailed.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/MarkRecurringCommandAsSuccessful.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/MarkRecurringCommandAsSuccessful.sql new file mode 100644 index 0000000..0f44de7 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/MarkRecurringCommandAsSuccessful.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/NextId_ExistingRows.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/NextId_ExistingRows.sql new file mode 100644 index 0000000..0fd406e Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/NextId_ExistingRows.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/NextId_NoRow.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/NextId_NoRow.sql new file mode 100644 index 0000000..f809473 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/NextId_NoRow.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/ReadEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/ReadEventStream.sql new file mode 100644 index 0000000..35f2a21 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/ReadEventStream.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/ReadFailedEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/ReadFailedEventStream.sql new file mode 100644 index 0000000..35f2a21 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/ReadFailedEventStream.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/RegisterRecurringCommand.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/RegisterRecurringCommand.sql new file mode 100644 index 0000000..f809473 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/RegisterRecurringCommand.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/SubscribeToEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/SubscribeToEventStream.sql new file mode 100644 index 0000000..f809473 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/SubscribeToEventStream.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/UpdateEventStreamPosition.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/UpdateEventStreamPosition.sql new file mode 100644 index 0000000..82705d8 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/UpdateEventStreamPosition.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/UpdateFailedEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/UpdateFailedEventStream.sql new file mode 100644 index 0000000..edb0b10 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/UpdateFailedEventStream.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/UpdateFailedEventStreamPosition.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/UpdateFailedEventStreamPosition.sql new file mode 100644 index 0000000..9a7d65a Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/UpdateFailedEventStreamPosition.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/CreateDatabase.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/CreateDatabase.sql new file mode 100644 index 0000000..2b70632 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/CreateDatabase.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/ExcludeFailedEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/ExcludeFailedEventStream.sql new file mode 100644 index 0000000..c969f40 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/ExcludeFailedEventStream.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/FindEventStreams.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/FindEventStreams.sql new file mode 100644 index 0000000..b8fea7a Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/FindEventStreams.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/FindFailedEventStreams.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/FindFailedEventStreams.sql new file mode 100644 index 0000000..5750841 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/FindFailedEventStreams.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/FindRecurringCommands.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/FindRecurringCommands.sql new file mode 100644 index 0000000..83cbb33 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/FindRecurringCommands.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/IncludeFailedEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/IncludeFailedEventStream.sql new file mode 100644 index 0000000..5750841 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/IncludeFailedEventStream.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/MarkRecurringCommandAsFailed.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/MarkRecurringCommandAsFailed.sql new file mode 100644 index 0000000..83cbb33 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/MarkRecurringCommandAsFailed.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/MarkRecurringCommandAsSuccessful.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/MarkRecurringCommandAsSuccessful.sql new file mode 100644 index 0000000..192da13 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/MarkRecurringCommandAsSuccessful.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/NextId_ExistingRows.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/NextId_ExistingRows.sql new file mode 100644 index 0000000..f32e473 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/NextId_ExistingRows.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/NextId_NoRow.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/NextId_NoRow.sql new file mode 100644 index 0000000..1cd8752 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/NextId_NoRow.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/ReadEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/ReadEventStream.sql new file mode 100644 index 0000000..05a3317 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/ReadEventStream.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/ReadFailedEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/ReadFailedEventStream.sql new file mode 100644 index 0000000..05a3317 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/ReadFailedEventStream.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/RegisterRecurringCommand.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/RegisterRecurringCommand.sql new file mode 100644 index 0000000..c969f40 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/RegisterRecurringCommand.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/SubscribeToEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/SubscribeToEventStream.sql new file mode 100644 index 0000000..c969f40 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/SubscribeToEventStream.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/UpdateEventStreamPosition.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/UpdateEventStreamPosition.sql new file mode 100644 index 0000000..560253d Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/UpdateEventStreamPosition.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/UpdateFailedEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/UpdateFailedEventStream.sql new file mode 100644 index 0000000..5750841 Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/UpdateFailedEventStream.sql differ diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/UpdateFailedEventStreamPosition.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/UpdateFailedEventStreamPosition.sql new file mode 100644 index 0000000..076e93e Binary files /dev/null and b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/UpdateFailedEventStreamPosition.sql differ diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerCollection.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerCollection.cs similarity index 79% rename from Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerCollection.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerCollection.cs index dc5b994..2123526 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerCollection.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerCollection.cs @@ -1,6 +1,6 @@ using Xunit; -namespace DDD.HealthcareDelivery +namespace DDD.Core.Infrastructure.Data { [CollectionDefinition("SqlServer")] public class SqlServerCollection : ICollectionFixture diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerEventStreamPositionUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerEventStreamPositionUpdaterTests.cs new file mode 100644 index 0000000..5e28a28 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerEventStreamPositionUpdaterTests.cs @@ -0,0 +1,17 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("SqlServer")] + public class SqlServerEventStreamPositionUpdaterTests : EventStreamPositionUpdaterTests + { + + #region Constructors + + public SqlServerEventStreamPositionUpdaterTests(SqlServerFixture fixture) : base(fixture) + { + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerEventStreamReaderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerEventStreamReaderTests.cs new file mode 100644 index 0000000..7c4cc62 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerEventStreamReaderTests.cs @@ -0,0 +1,81 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + + [Collection("SqlServer")] + public class SqlServerEventStreamReaderTests : EventStreamReaderTests + { + + #region Constructors + + public SqlServerEventStreamReaderTests(SqlServerFixture fixture) : base(fixture) + { + } + + #endregion Constructors + + #region Methods + + public static IEnumerable QueriesAndResults_2() + { + yield return new object[] + { + new ReadEventStream + { + StreamType = "MessageBox", + StreamPosition = new Guid("36beb37d-1e01-bb7d-fb2a-3a0044745345") + }, + new [] + { + new Event + { + Body = "{\"boxId\":3,\"occurredOn\":\"2022-01-19T14:22:00.3683143+01:00\"}", + BodyFormat = "JSON", + EventId = new Guid("097d449e-539e-bdf1-b50d-3a0184ad3555"), + EventType = "DDD.Collaboration.Domain.Messages.MessageBoxCreated, DDD.Collaboration.Messages", + OccurredOn = new DateTime(2022, 1, 19, 14, 22, 0, 368), + StreamId = "3", + StreamType = "MessageBox", + IssuedBy = "Dr Folamour" + } + } + }; + } + + [Theory] + [MemberData(nameof(QueriesAndResults_2))] + public void Handle_WhenCalled_ReturnsExpectedResults_2(ReadEventStream query, IEnumerable expectedResults) + { + // Arrange + Fixture.ExecuteScriptFromResources("ReadEventStream"); + var handler = new EventStreamReader(ConnectionProvider); + // Act + var results = handler.Handle(query); + // Assert + results.Should().BeEquivalentTo(expectedResults, context => context.WithStrictOrdering()); + } + + [Theory] + [MemberData(nameof(QueriesAndResults_2))] + public async Task HandleAsync_WhenCalled_ReturnsExpectedResults_2(ReadEventStream query, IEnumerable expectedResults) + { + // Arrange + Fixture.ExecuteScriptFromResources("ReadEventStream"); + var handler = new EventStreamReader(ConnectionProvider); + // Act + var results = await handler.HandleAsync(query); + // Assert + results.Should().BeEquivalentTo(expectedResults, context => context.WithStrictOrdering()); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerEventStreamSubscriberTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerEventStreamSubscriberTests.cs new file mode 100644 index 0000000..4a496d8 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerEventStreamSubscriberTests.cs @@ -0,0 +1,17 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("SqlServer")] + public class SqlServerEventStreamSubscriberTests : EventStreamSubscriberTests + { + + #region Constructors + + public SqlServerEventStreamSubscriberTests(SqlServerFixture fixture) : base(fixture) + { + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerEventStreamsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerEventStreamsFinderTests.cs new file mode 100644 index 0000000..eb05829 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerEventStreamsFinderTests.cs @@ -0,0 +1,18 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("SqlServer")] + public class SqlServerEventStreamsFinderTests : EventStreamsFinderTests + { + + #region Constructors + + public SqlServerEventStreamsFinderTests(SqlServerFixture fixture) : base(fixture) + { + } + + #endregion Constructors + + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamExcluderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamExcluderTests.cs new file mode 100644 index 0000000..9e69843 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamExcluderTests.cs @@ -0,0 +1,17 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("SqlServer")] + public class SqlServerFailedEventStreamExcluderTests : FailedEventStreamExcluderTests + { + + #region Constructors + + public SqlServerFailedEventStreamExcluderTests(SqlServerFixture fixture) : base(fixture) + { + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamIncluderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamIncluderTests.cs new file mode 100644 index 0000000..1d5bad0 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamIncluderTests.cs @@ -0,0 +1,17 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("SqlServer")] + public class SqlServerFailedEventStreamIncluderTests : FailedEventStreamIncluderTests + { + + #region Constructors + + public SqlServerFailedEventStreamIncluderTests(SqlServerFixture fixture) : base(fixture) + { + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamPositionUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamPositionUpdaterTests.cs new file mode 100644 index 0000000..1a0810e --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamPositionUpdaterTests.cs @@ -0,0 +1,17 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("SqlServer")] + public class SqlServerFailedEventStreamPositionUpdaterTests : FailedEventStreamPositionUpdaterTests + { + + #region Constructors + + public SqlServerFailedEventStreamPositionUpdaterTests(SqlServerFixture fixture) : base(fixture) + { + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamReaderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamReaderTests.cs new file mode 100644 index 0000000..0e8ff9a --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamReaderTests.cs @@ -0,0 +1,105 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + + [Collection("SqlServer")] + public class SqlServerFailedEventStreamReaderTests : EventsByStreamIdFinderTests + { + + #region Constructors + + public SqlServerFailedEventStreamReaderTests(SqlServerFixture fixture) : base(fixture) + { + } + + #endregion Constructors + + #region Methods + + public static IEnumerable QueriesAndResults() + { + yield return new object[] + { + new ReadFailedEventStream + { + StreamType = "Message", + StreamId = "5", + EventIdMin = new Guid("321ea720-affd-6c91-3782-3a0189c0f051"), + EventIdMax = new Guid("0096f748-41f4-2e2b-87f3-3a0189c1505f") + }, + new [] + { + new Event + { + Body = "{\"messageId\":5,\"occurredOn\":\"2022-01-20T14:01:41.3251974+01:00\"}", + BodyFormat = "JSON", + EventId = new Guid("321ea720-affd-6c91-3782-3a0189c0f051"), + EventType = "DDD.Collaboration.Domain.Messages.MessageRead, DDD.Collaboration.Messages", + OccurredOn = new DateTime(2022, 1, 20, 14, 1, 41, 325), + StreamId = "5", + StreamType = "Message", + IssuedBy = "Dr Folamour" + }, + new Event + { + Body = "{\"messageId\":5,\"source\":\"Inbox\",\"destination\":\"Binbox\",\"occurredOn\":\"2022-01-20T14:01:48.6157105+01:00\"}", + BodyFormat = "JSON", + EventId = new Guid("a224a074-c1d9-6c6f-0adc-3a0189c10ccc"), + EventType = "DDD.Collaboration.Domain.Messages.MessageSentToBin, DDD.Collaboration.Messages", + OccurredOn = new DateTime(2022, 1, 20, 14, 1, 48, 616), + StreamId = "5", + StreamType = "Message", + IssuedBy = "Dr Folamour" + }, + new Event + { + Body = "{\"messageId\":5,\"occurredOn\":\"2022-01-20T14:02:05.9149942+01:00\"}", + BodyFormat = "JSON", + EventId = new Guid("0096f748-41f4-2e2b-87f3-3a0189c1505f"), + EventType = "DDD.Collaboration.Domain.Messages.MessageDeleted, DDD.Collaboration.Messages", + OccurredOn = new DateTime(2022, 1, 20, 14, 2, 5, 915), + StreamId = "5", + StreamType = "Message", + IssuedBy = "Dr Folamour" + } + } + }; + } + + [Theory] + [MemberData(nameof(QueriesAndResults))] + public void Handle_WhenCalled_ReturnsExpectedResults(ReadFailedEventStream query, IEnumerable expectedResults) + { + // Arrange + Fixture.ExecuteScriptFromResources("ReadFailedEventStream"); + var handler = new FailedEventStreamReader(ConnectionProvider); + // Act + var results = handler.Handle(query); + // Assert + results.Should().BeEquivalentTo(expectedResults, context => context.WithStrictOrdering()); + } + + [Theory] + [MemberData(nameof(QueriesAndResults))] + public async Task HandleAsync_WhenCalled_ReturnsExpectedResults(ReadFailedEventStream query, IEnumerable expectedResults) + { + // Arrange + Fixture.ExecuteScriptFromResources("ReadFailedEventStream"); + var handler = new FailedEventStreamReader(ConnectionProvider); + // Act + var results = await handler.HandleAsync(query); + // Assert + results.Should().BeEquivalentTo(expectedResults, context => context.WithStrictOrdering()); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamUpdaterTests.cs new file mode 100644 index 0000000..95846ce --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamUpdaterTests.cs @@ -0,0 +1,17 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("SqlServer")] + public class SqlServerFailedEventStreamUpdaterTests : FailedEventStreamUpdaterTests + { + + #region Constructors + + public SqlServerFailedEventStreamUpdaterTests(SqlServerFixture fixture) : base(fixture) + { + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamsFinderTests.cs new file mode 100644 index 0000000..6a70691 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamsFinderTests.cs @@ -0,0 +1,18 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("SqlServer")] + public class SqlServerFailedEventStreamsFinderTests : FailedEventStreamsFinderTests + { + + #region Constructors + + public SqlServerFailedEventStreamsFinderTests(SqlServerFixture fixture) : base(fixture) + { + } + + #endregion Constructors + + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedRecurringCommandUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedRecurringCommandUpdaterTests.cs new file mode 100644 index 0000000..3ac5f14 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedRecurringCommandUpdaterTests.cs @@ -0,0 +1,18 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("SqlServer")] + public class SqlServerFailedRecurringCommandUpdaterTests : FailedRecurringCommandUpdaterTests + { + + #region Constructors + + public SqlServerFailedRecurringCommandUpdaterTests(SqlServerFixture fixture) : base(fixture) + { + } + + #endregion Constructors + + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFixture.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFixture.cs new file mode 100644 index 0000000..389311b --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFixture.cs @@ -0,0 +1,69 @@ +using Dapper; +using System.Data; +#if (NET6_0) +using System.Data.Common; +#endif +using Microsoft.Data.SqlClient; +using System.Configuration; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + using Serialization; + using Mapping; + using Testing; + + public class SqlServerFixture : DbFixture, IPersistenceFixture + { + + #region Constructors + + public SqlServerFixture() : base("SqlServerScripts", ConfigurationManager.ConnectionStrings["SqlServer"]) + { + SqlMapper.ResetTypeHandlers(); + SqlMapper.AddTypeHandler(new IncrementalDelaysTypeMapper()); + } + + #endregion Constructors + + #region Methods + + public IObjectTranslator CreateEventTranslator() + { + return new EventTranslator(JsonSerializerWrapper.Create(false)); + } + + protected override void CreateDatabase() + { + using (var connection = CreateConnection()) + { + var builder = new SqlConnectionStringBuilder(connection.ConnectionString) { InitialCatalog = "master" }; + connection.ConnectionString = builder.ConnectionString; + connection.Open(); + ExecuteScript(SqlServerScripts.CreateDatabase, connection); + } + } + + protected override int[] ExecuteScript(string script, IDbConnection connection) + { + return connection.ExecuteScript(script); + } + + protected override void LoadConfiguration() + { +#if (NET6_0) + DbProviderFactories.RegisterFactory("Microsoft.Data.SqlClient", SqlClientFactory.Instance); +#endif + } + + protected override string SetPooling(string connectionString, bool pooling) + { + var builder = new SqlConnectionStringBuilder(connectionString) { Pooling = pooling }; + return builder.ConnectionString; + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerIDbConnectionExtensionsTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerIDbConnectionExtensionsTests.cs new file mode 100644 index 0000000..c700e67 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerIDbConnectionExtensionsTests.cs @@ -0,0 +1,17 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("SqlServer")] + public class SqlServerIDbConnectionExtensionsTests : IDbConnectionExtensionsTests + { + + #region Constructors + + public SqlServerIDbConnectionExtensionsTests(SqlServerFixture fixture) : base(fixture) + { + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerRecurringCommandRegisterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerRecurringCommandRegisterTests.cs new file mode 100644 index 0000000..28c25a7 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerRecurringCommandRegisterTests.cs @@ -0,0 +1,18 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("SqlServer")] + public class SqlServerRecurringCommandRegisterTests : RecurringCommandRegisterTests + { + + #region Constructors + + public SqlServerRecurringCommandRegisterTests(SqlServerFixture fixture) : base(fixture) + { + } + + #endregion Constructors + + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerRecurringCommandsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerRecurringCommandsFinderTests.cs new file mode 100644 index 0000000..ce92573 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerRecurringCommandsFinderTests.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using DDD.Core.Application; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("SqlServer")] + public class SqlServerRecurringCommandsFinderTests : RecurringCommandsFinderTests + { + + #region Constructors + + public SqlServerRecurringCommandsFinderTests(SqlServerFixture fixture) : base(fixture) + { + } + + #endregion Constructors + + #region Methods + + protected override IEnumerable ExpectedResults() + { + yield return new RecurringCommand + { + CommandId = new Guid("36beb37d-1e01-bb7d-fb2a-3a0044745345"), + CommandType = "DDD.Core.Application.FakeCommand1, DDD.Core.Messages", + Body = "{\"Property1\":\"dummy\",\"Property2\":10}", + BodyFormat = "JSON", + RecurringExpression = "* * * * *", + RecurringExpressionFormat = "CRON" + }; + yield return new RecurringCommand + { + CommandId = new Guid("f7df5bd0-8763-677e-7e6b-3a0044746810"), + CommandType = "DDD.Core.Application.FakeCommand2, DDD.Core.Messages", + Body = "{\"Property1\":\"dummy\",\"Property2\":10,\"Property3\":\"2022-12-24T14:49:44.361964+01:00\"}", + BodyFormat = "JSON", + RecurringExpression = "0 0 1 * *", + RecurringExpressionFormat = "CRON" + }; + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerScripts.Designer.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerScripts.Designer.cs new file mode 100644 index 0000000..7095a4c --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerScripts.Designer.cs @@ -0,0 +1,316 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace DDD.Core.Infrastructure.Data { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class SqlServerScripts { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal SqlServerScripts() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DDD.Core.Infrastructure.Data.SqlServerScripts", typeof(SqlServerScripts).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to /****** Object: Database [Test] Script Date: 03/03/2022 10:29:20 ******/ + ///USE [master] + ///GO + ///IF EXISTS(SELECT * FROM [sys].[databases] where [name] = 'Test') + ///begin + ///ALTER DATABASE [Test] SET SINGLE_USER WITH ROLLBACK IMMEDIATE + ///DROP DATABASE [Test] + ///end + ///GO + ///CREATE DATABASE [Test] + /// CONTAINMENT = NONE + /// ON PRIMARY + ///( NAME = N'Test', FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL11.SQLEXPRESS\MSSQL\DATA\Test.mdf' , SIZE = 5120KB , MAXSIZE = UNLIMITED, FILEGROWTH = 1024KB ) + /// LOG ON + ///( NAME = [rest of string was truncated]";. + /// + internal static string CreateDatabase { + get { + return ResourceManager.GetString("CreateDatabase", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to USE [Test] + ///GO + ///EXEC spClearDatabase + ///GO. + /// + internal static string ExcludeFailedEventStream { + get { + return ResourceManager.GetString("ExcludeFailedEventStream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to USE [Test] + ///GO + ///EXEC spClearDatabase + ///GO + ///INSERT [dbo].[EventStream]([Type], [Source], [Position], [RetryMax], [RetryDelays], [BlockSize]) VALUES ('Person', 'ID', 'f7df5bd0-8763-677e-7e6b-3a0044746810', 5, '10,60,120/80', 50) + ///GO + ///INSERT [dbo].[EventStream]([Type], [Source], [Position], [RetryMax], [RetryDelays], [BlockSize]) VALUES ('MedicalProduct', 'OFR', '00000000-0000-0000-0000-000000000000', 3, '60', 100) + ///GO . + /// + internal static string FindEventStreams { + get { + return ResourceManager.GetString("FindEventStreams", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to USE [Test] + ///GO + ///EXEC spClearDatabase + ///GO + ///INSERT [dbo].[FailedEventStream] ([StreamId], [StreamType], [StreamSource], [StreamPosition], [EventId], [EventType], [ExceptionTime], [ExceptionType], [ExceptionMessage], [ExceptionSource], [ExceptionInfo], [BaseExceptionType], [BaseExceptionMessage], [RetryCount], [RetryMax], [RetryDelays], [BlockSize]) VALUES (N'2', N'MessageBox', N'COL', N'0a77707a-c147-9e1b-883a-08da0e368663', N'e10add4d-1851-7ede-883b-08da0e368663', N'DDD.Collaboration.Domain.Messages.MessageB [rest of string was truncated]";. + /// + internal static string FindFailedEventStreams { + get { + return ResourceManager.GetString("FindFailedEventStreams", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to USE [Test] + ///GO + ///EXEC spClearDatabase + ///GO + ///INSERT INTO [dbo].[Command] ([CommandId], [CommandType], [Body], [BodyFormat], [RecurringExpression], [LastExecutionTime], [LastExecutionStatus], [LastExceptionInfo]) + ///VALUES (N'36beb37d-1e01-bb7d-fb2a-3a0044745345', N'DDD.Core.Application.FakeCommand1, DDD.Core.Messages', '{"Property1":"dummy","Property2":10}', N'JSON', N'* * * * *', NULL, NULL, NULL) + ///GO + ///INSERT INTO [dbo].[Command] ([CommandId], [CommandType], [Body], [BodyFormat], [RecurringExpression], [LastExe [rest of string was truncated]";. + /// + internal static string FindRecurringCommands { + get { + return ResourceManager.GetString("FindRecurringCommands", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to USE [Test] + ///GO + ///EXEC spClearDatabase + ///GO + ///INSERT [dbo].[FailedEventStream] ([StreamId], [StreamType], [StreamSource], [StreamPosition], [EventId], [EventType], [ExceptionTime], [ExceptionType], [ExceptionMessage], [ExceptionSource], [ExceptionInfo], [BaseExceptionType], [BaseExceptionMessage], [RetryCount], [RetryMax], [RetryDelays], [BlockSize]) VALUES (N'2', N'MessageBox', N'COL', N'0a77707a-c147-9e1b-883a-08da0e368663', N'e10add4d-1851-7ede-883b-08da0e368663', N'DDD.Collaboration.Domain.Messages.MessageB [rest of string was truncated]";. + /// + internal static string IncludeFailedEventStream { + get { + return ResourceManager.GetString("IncludeFailedEventStream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to USE [Test] + ///GO + ///EXEC spClearDatabase + ///GO + ///INSERT INTO [dbo].[Command] ([CommandId], [CommandType], [Body], [BodyFormat], [RecurringExpression], [LastExecutionTime], [LastExecutionStatus], [LastExceptionInfo]) + ///VALUES (N'36beb37d-1e01-bb7d-fb2a-3a0044745345', N'DDD.Core.Application.FakeCommand1, DDD.Core.Messages', '{"Property1":"dummy","Property2":10}', N'JSON', N'* * * * *', NULL, NULL, NULL) + ///GO + ///INSERT INTO [dbo].[Command] ([CommandId], [CommandType], [Body], [BodyFormat], [RecurringExpression], [LastExe [rest of string was truncated]";. + /// + internal static string MarkRecurringCommandAsFailed { + get { + return ResourceManager.GetString("MarkRecurringCommandAsFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to USE [Test] + ///GO + ///EXEC spClearDatabase + ///GO + ///INSERT INTO [dbo].[Command] ([CommandId], [CommandType], [Body], [BodyFormat], [RecurringExpression], [LastExecutionTime], [LastExecutionStatus], [LastExceptionInfo]) + ///VALUES (N'36beb37d-1e01-bb7d-fb2a-3a0044745345', N'DDD.Core.Application.FakeCommand1, DDD.Core.Messages', '{"Property1":"dummy","Property2":10}', N'JSON', N'* * * * *', CAST(N'2022-01-01T00:00:00.0000000' AS DateTime2), N'F', N'System.TimeoutException: The operation has timed-out.') + ///GO + ///INSERT INTO [ [rest of string was truncated]";. + /// + internal static string MarkRecurringCommandAsSuccessful { + get { + return ResourceManager.GetString("MarkRecurringCommandAsSuccessful", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to USE [Test] + ///GO + ///EXEC spClearDatabase + ///GO + ///INSERT INTO TableWithId (Id) VALUES (1) + ///GO + ///INSERT INTO TableWithId (Id) VALUES (2) + ///GO + ///INSERT INTO TableWithId (Id) VALUES (3) + ///GO + ///INSERT INTO TableWithId (Id) VALUES (4) + ///GO. + /// + internal static string NextId_ExistingRows { + get { + return ResourceManager.GetString("NextId_ExistingRows", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to USE [Test] + ///GO + ///EXEC spClearDatabase + ///GO . + /// + internal static string NextId_NoRow { + get { + return ResourceManager.GetString("NextId_NoRow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to USE [Test] + ///GO + ///EXEC spClearDatabase + ///GO + ///INSERT [dbo].[Event] ([EventId], [EventType], [OccurredOn], [Body], [BodyFormat], [StreamId], [StreamType], [IssuedBy]) VALUES (N'36beb37d-1e01-bb7d-fb2a-3a0044745345', N'DDD.Collaboration.Domain.Messages.MessageBoxCreated, DDD.Collaboration.Messages', CAST(N'2021-11-18T10:01:23.5280000' AS DateTime2), N'{"boxId":2,"occurredOn":"2021-11-18T10:01:23.5277314+01:00"}', N'JSON', N'2', N'MessageBox', N'Dr Maboul') + ///GO + ///INSERT [dbo].[Event] ([EventId], [EventType], [Occur [rest of string was truncated]";. + /// + internal static string ReadEventStream { + get { + return ResourceManager.GetString("ReadEventStream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to USE [Test] + ///GO + ///EXEC spClearDatabase + ///GO + ///INSERT [dbo].[Event] ([EventId], [EventType], [OccurredOn], [Body], [BodyFormat], [StreamId], [StreamType], [IssuedBy]) VALUES (N'36beb37d-1e01-bb7d-fb2a-3a0044745345', N'DDD.Collaboration.Domain.Messages.MessageBoxCreated, DDD.Collaboration.Messages', CAST(N'2021-11-18T10:01:23.5280000' AS DateTime2), N'{"boxId":2,"occurredOn":"2021-11-18T10:01:23.5277314+01:00"}', N'JSON', N'2', N'MessageBox', N'Dr Maboul') + ///GO + ///INSERT [dbo].[Event] ([EventId], [EventType], [Occur [rest of string was truncated]";. + /// + internal static string ReadFailedEventStream { + get { + return ResourceManager.GetString("ReadFailedEventStream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to USE [Test] + ///GO + ///EXEC spClearDatabase + ///GO. + /// + internal static string RegisterRecurringCommand { + get { + return ResourceManager.GetString("RegisterRecurringCommand", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to USE [Test] + ///GO + ///EXEC spClearDatabase + ///GO. + /// + internal static string SubscribeToEventStream { + get { + return ResourceManager.GetString("SubscribeToEventStream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to USE [Test] + ///GO + ///EXEC spClearDatabase + ///GO + ///INSERT [dbo].[EventStream]([Type], [Source], [Position], [RetryMax], [RetryDelays], [BlockSize]) VALUES ('Person', 'ID', 'f7df5bd0-8763-677e-7e6b-3a0044746810', 5, '10,60,120/80', 50) + ///GO + ///INSERT [dbo].[EventStream]([Type], [Source], [Position], [RetryMax], [RetryDelays], [BlockSize]) VALUES ('MedicalProduct', 'OFR', '00000000-0000-0000-0000-000000000000', 3, '60', 100) + ///GO . + /// + internal static string UpdateEventStreamPosition { + get { + return ResourceManager.GetString("UpdateEventStreamPosition", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to USE [Test] + ///GO + ///EXEC spClearDatabase + ///GO + ///INSERT [dbo].[FailedEventStream] ([StreamId], [StreamType], [StreamSource], [StreamPosition], [EventId], [EventType], [ExceptionTime], [ExceptionType], [ExceptionMessage], [ExceptionSource], [ExceptionInfo], [BaseExceptionType], [BaseExceptionMessage], [RetryCount], [RetryMax], [RetryDelays], [BlockSize]) VALUES (N'2', N'MessageBox', N'COL', N'0a77707a-c147-9e1b-883a-08da0e368663', N'e10add4d-1851-7ede-883b-08da0e368663', N'DDD.Collaboration.Domain.Messages.MessageB [rest of string was truncated]";. + /// + internal static string UpdateFailedEventStream { + get { + return ResourceManager.GetString("UpdateFailedEventStream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to USE [Test] + ///GO + ///EXEC spClearDatabase + ///GO + ///INSERT [dbo].[FailedEventStream] ([StreamId], [StreamType], [StreamSource], [StreamPosition], [EventId], [EventType], [ExceptionTime], [ExceptionType], [ExceptionMessage], [ExceptionSource], [ExceptionInfo], [BaseExceptionType], [BaseExceptionMessage], [RetryCount], [RetryMax], [RetryDelays], [BlockSize]) VALUES (N'2', N'MessageBox', N'COL', N'0a77707a-c147-9e1b-883a-08da0e368663', N'e10add4d-1851-7ede-883b-08da0e368663', N'Xperthis.Collaboration.Domain.Messages.Mes [rest of string was truncated]";. + /// + internal static string UpdateFailedEventStreamPosition { + get { + return ResourceManager.GetString("UpdateFailedEventStreamPosition", resourceCulture); + } + } + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerScripts.resx b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerScripts.resx new file mode 100644 index 0000000..0d2a903 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerScripts.resx @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + scripts\sqlserver\createdatabase.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\sqlserver\excludefailedeventstream.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\sqlserver\findeventstreams.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\sqlserver\findfailedeventstreams.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\sqlserver\findrecurringcommands.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\sqlserver\includefailedeventstream.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\sqlserver\markrecurringcommandasfailed.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\sqlserver\markrecurringcommandassuccessful.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\sqlserver\nextid_existingrows.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\sqlserver\nextid_norow.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\sqlserver\readeventstream.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\sqlserver\readfailedeventstream.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\sqlserver\registerrecurringcommand.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\sqlserver\subscribetoeventstream.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\sqlserver\updateeventstreamposition.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\sqlserver\updatefailedeventstream.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + scripts\sqlserver\updatefailedeventstreamposition.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerSuccessfulRecurringCommandUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerSuccessfulRecurringCommandUpdaterTests.cs new file mode 100644 index 0000000..3355106 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerSuccessfulRecurringCommandUpdaterTests.cs @@ -0,0 +1,18 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [Collection("SqlServer")] + public class SqlServerSuccessfulRecurringCommandUpdaterTests : SuccessfulRecurringCommandUpdaterTests + { + + #region Constructors + + public SqlServerSuccessfulRecurringCommandUpdaterTests(SqlServerFixture fixture) : base(fixture) + { + } + + #endregion Constructors + + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SuccessfulRecurringCommandUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SuccessfulRecurringCommandUpdaterTests.cs new file mode 100644 index 0000000..c250017 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SuccessfulRecurringCommandUpdaterTests.cs @@ -0,0 +1,117 @@ +using FluentAssertions; +using Dapper; +using System; +using System.Threading.Tasks; +using System.Data.Common; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Domain; + + public abstract class SuccessfulRecurringCommandUpdaterTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected SuccessfulRecurringCommandUpdaterTests(TFixture fixture) + { + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); + } + + #endregion Constructors + + #region Properties + + protected TFixture Fixture { get; } + + protected IDbConnectionProvider ConnectionProvider { get; } + + #endregion Properties + + #region Methods + + [Fact] + public void Handle_WhenCalled_UpdatesCommand() + { + // Arrange + Fixture.ExecuteScriptFromResources("MarkRecurringCommandAsSuccessful"); + var handler = new SuccessfulRecurringCommandUpdater(ConnectionProvider); + var command = CreateCommand(); + var expectedCommand = ExpectedCommand(); + // Act + handler.Handle(command); + // Assert + UpdatedCommand().Should().BeEquivalentTo(expectedCommand); + } + + [Fact] + public async Task HandleAsync_WhenCalled_UpdatesCommand() + { + // Arrange + Fixture.ExecuteScriptFromResources("MarkRecurringCommandAsSuccessful"); + var handler = new SuccessfulRecurringCommandUpdater(ConnectionProvider); + var command = CreateCommand(); + var expectedCommand = ExpectedCommand(); + // Act + await handler.HandleAsync(command); + // Assert + UpdatedCommand().Should().BeEquivalentTo(expectedCommand); + + } + + public void Dispose() + { + ConnectionProvider.Dispose(); + } + + private static MarkRecurringCommandAsSuccessful CreateCommand() + { + return new MarkRecurringCommandAsSuccessful + { + CommandId = new Guid("36beb37d-1e01-bb7d-fb2a-3a0044745345"), + ExecutionTime = new DateTime(2022, 2, 1) + }; + } + + private RecurringCommandDetail UpdatedCommand() + { + using (var connection = Fixture.CreateConnection()) + { + connection.Open(); + var sql = "SELECT CommandId, CommandType, Body, BodyFormat, RecurringExpression, RecurringExpressionFormat, LastExecutionTime, CASE LastExecutionStatus WHEN 'F' THEN 'Successful' WHEN 'S' THEN 'Successful' END LastExecutionStatus, LastExceptionInfo FROM Command WHERE CommandId = @CommandId"; + sql = sql.Replace("@", connection.Expressions().ParameterPrefix()); + return connection.QuerySingle(sql, Parameters(connection)); + } + } + + private static object Parameters(DbConnection connection) + { + if (connection.HasOracleProvider()) + return new { CommandId = new Guid("36beb37d-1e01-bb7d-fb2a-3a0044745345").ToByteArray() }; + return new { CommandId = new Guid("36beb37d-1e01-bb7d-fb2a-3a0044745345") }; + } + + + private static RecurringCommandDetail ExpectedCommand() + { + return new RecurringCommandDetail + { + CommandId = new Guid("36beb37d-1e01-bb7d-fb2a-3a0044745345"), + CommandType = "DDD.Core.Application.FakeCommand1, DDD.Core.Messages", + Body = "{\"Property1\":\"dummy\",\"Property2\":10}", + BodyFormat = "JSON", + RecurringExpression = "* * * * *", + RecurringExpressionFormat = "CRON", + LastExecutionTime = new DateTime(2022, 2, 1), + LastExecutionStatus = CommandExecutionStatus.Successful, + LastExceptionInfo = null + }; + } + + #endregion Methods + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Properties/AssemblyInfo.cs b/Test/DDD.Core.Dapper.IntegrationTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..7a68527 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Runtime.InteropServices; +using Xunit; + +// In SDK-style projects such as this one, several assembly attributes that were historically +// defined in this file are now automatically added during build and populated with +// values defined in project properties. For details of which attributes are included +// and how to customise this process see: https://aka.ms/assembly-info-properties + + +// Setting ComVisible to false makes the types in this assembly not visible to COM +// components. If you need to access a type in this assembly from COM, set the ComVisible +// attribute to true on that type. + +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM. + +[assembly: Guid("1f5d550f-33e0-4697-ad05-ddaad56ac2ec")] +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/Test/DDD.Core.Dapper.IntegrationTests/testhost.dll.config b/Test/DDD.Core.Dapper.IntegrationTests/testhost.dll.config new file mode 100644 index 0000000..c013f8d --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/testhost.dll.config @@ -0,0 +1,12 @@ + + + + +
+ + + + + + \ No newline at end of file diff --git a/Test/DDD.Core.Newtonsoft.UnitTests/DDD.Core.Newtonsoft.UnitTests.csproj b/Test/DDD.Core.Newtonsoft.UnitTests/DDD.Core.Newtonsoft.UnitTests.csproj new file mode 100644 index 0000000..ec20d98 --- /dev/null +++ b/Test/DDD.Core.Newtonsoft.UnitTests/DDD.Core.Newtonsoft.UnitTests.csproj @@ -0,0 +1,20 @@ + + + net48;net6.0 + DDD.Core.Infrastructure.Serialization + + + + + + + + 2.4.5 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + \ No newline at end of file diff --git a/Src/DDD.Common/Domain/FullNameState.cs b/Test/DDD.Core.Newtonsoft.UnitTests/FakePerson.cs similarity index 54% rename from Src/DDD.Common/Domain/FullNameState.cs rename to Test/DDD.Core.Newtonsoft.UnitTests/FakePerson.cs index 94498a5..3e38a76 100644 --- a/Src/DDD.Common/Domain/FullNameState.cs +++ b/Test/DDD.Core.Newtonsoft.UnitTests/FakePerson.cs @@ -1,15 +1,18 @@ -namespace DDD.Common.Domain +using System; + +namespace DDD.Core.Infrastructure.Serialization { - public class FullNameState + public class FakePerson { #region Properties + public DateTime Birthdate { get; set; } + public string FirstName { get; set; } public string LastName { get; set; } #endregion Properties - } } diff --git a/Test/DDD.Core.Newtonsoft.UnitTests/JsonSerializerWrapperTests.cs b/Test/DDD.Core.Newtonsoft.UnitTests/JsonSerializerWrapperTests.cs new file mode 100644 index 0000000..2936ea5 --- /dev/null +++ b/Test/DDD.Core.Newtonsoft.UnitTests/JsonSerializerWrapperTests.cs @@ -0,0 +1,47 @@ +using System; +using System.IO; +using Xunit; +using FluentAssertions; + +namespace DDD.Core.Infrastructure.Serialization +{ + public class JsonSerializerWrapperTests : IDisposable + { + + #region Fields + + private readonly Stream stream = new MemoryStream(); + + #endregion Fields + + #region Methods + + public void Dispose() + { + this.stream.Dispose(); + GC.SuppressFinalize(this); + } + + [Fact] + public void SerializeAndDeserialize_AreSymmetric() + { + // Arrange + var obj1 = new FakePerson + { + FirstName = "Donald", + LastName = "Duck", + Birthdate = new DateTime(1934, 4, 29) + }; + var serializer = JsonSerializerWrapper.Create(); + // Act + serializer.Serialize(this.stream, obj1); + this.stream.Position = 0; + var obj2 = serializer.Deserialize(this.stream, typeof(FakePerson)); + // Assert + obj2.Should().BeEquivalentTo(obj1); + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.SimpleInjector.FluentValidation.UnitTests/ContainerExtensionsTests.cs b/Test/DDD.Core.SimpleInjector.FluentValidation.UnitTests/ContainerExtensionsTests.cs new file mode 100644 index 0000000..6bc5c0d --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.FluentValidation.UnitTests/ContainerExtensionsTests.cs @@ -0,0 +1,87 @@ +using System; +using FluentValidation; +using FluentAssertions; +using System.Reflection; +using SimpleInjector; +using Xunit; +using DDD.Validation; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Validation; + + public class ContainerExtensionsTests : IDisposable + { + + #region Fields + + private readonly Container container; + + #endregion Fields + + #region Constructors + + public ContainerExtensionsTests() + { + this.container = new Container(); + this.container.Options.EnableAutoVerification = false; + } + + #endregion Constructors + + #region Methods + + public void Dispose() + { + this.container.Dispose(); + } + + [Fact] + public void ConfigureApp_WithConfigureValidation_RegistersValidators() + { + // Act + this.container.ConfigureApp(o => + { + o.RegisterTypesFrom(Assembly.GetExecutingAssembly()); + o.ConfigureValidation(); + }); + // Assert + var registration = container.GetRegistration>(); + registration.Should().NotBeNull().And.Match(r => r.ImplementationType == typeof(FakeObjectValidator) + && r.Lifestyle == Lifestyle.Transient); + } + + [Fact] + public void ConfigureApp_WithConfigureValidation_RegistersSyncObjectValidators() + { + // Act + this.container.ConfigureApp(o => + { + o.RegisterTypesFrom(Assembly.GetExecutingAssembly()); + o.ConfigureValidation(); + }); + // Assert + var registration = container.GetRegistration>(); + registration.Should().NotBeNull().And.Match(r => r.ImplementationType == typeof(FluentValidatorAdapter) + && r.Lifestyle == Lifestyle.Transient); + } + + [Fact] + public void ConfigureApp_WithConfigureValidation_RegistersAsyncObjectValidators() + { + // Act + this.container.ConfigureApp(o => + { + o.RegisterTypesFrom(Assembly.GetExecutingAssembly()); + o.ConfigureValidation(); + }); + // Assert + var registration = container.GetRegistration>(); + registration.Should().NotBeNull().And.Match(r => r.ImplementationType == typeof(FluentValidatorAdapter) + && r.Lifestyle == Lifestyle.Transient); + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.SimpleInjector.FluentValidation.UnitTests/DDD.Core.SimpleInjector.FluentValidation.UnitTests.csproj b/Test/DDD.Core.SimpleInjector.FluentValidation.UnitTests/DDD.Core.SimpleInjector.FluentValidation.UnitTests.csproj new file mode 100644 index 0000000..ed7cbcb --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.FluentValidation.UnitTests/DDD.Core.SimpleInjector.FluentValidation.UnitTests.csproj @@ -0,0 +1,19 @@ + + + net48;net6.0 + DDD.Core.Infrastructure.DependencyInjection + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/Test/DDD.Core.SimpleInjector.FluentValidation.UnitTests/FakeObject.cs b/Test/DDD.Core.SimpleInjector.FluentValidation.UnitTests/FakeObject.cs new file mode 100644 index 0000000..b6a176f --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.FluentValidation.UnitTests/FakeObject.cs @@ -0,0 +1,6 @@ +namespace DDD.Core.Infrastructure.DependencyInjection +{ + public class FakeObject + { + } +} diff --git a/Test/DDD.Core.SimpleInjector.FluentValidation.UnitTests/FakeObjectValidator.cs b/Test/DDD.Core.SimpleInjector.FluentValidation.UnitTests/FakeObjectValidator.cs new file mode 100644 index 0000000..e1ad4ef --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.FluentValidation.UnitTests/FakeObjectValidator.cs @@ -0,0 +1,8 @@ +using FluentValidation; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + public class FakeObjectValidator : AbstractValidator + { + } +} diff --git a/Test/DDD.Core.SimpleInjector.NHibernate.UnitTests/DDD.Core.SimpleInjector.NHibernate.UnitTests.csproj b/Test/DDD.Core.SimpleInjector.NHibernate.UnitTests/DDD.Core.SimpleInjector.NHibernate.UnitTests.csproj new file mode 100644 index 0000000..e501817 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.NHibernate.UnitTests/DDD.Core.SimpleInjector.NHibernate.UnitTests.csproj @@ -0,0 +1,18 @@ + + + net48;net6.0 + DDD.Core + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + \ No newline at end of file diff --git a/Test/DDD.Core.SimpleInjector.NHibernate.UnitTests/Domain/FakeContext.cs b/Test/DDD.Core.SimpleInjector.NHibernate.UnitTests/Domain/FakeContext.cs new file mode 100644 index 0000000..e0c004c --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.NHibernate.UnitTests/Domain/FakeContext.cs @@ -0,0 +1,13 @@ +namespace DDD.Core.Domain +{ + public class FakeContext : BoundedContext + { + #region Constructors + + public FakeContext() : base("FK", "Fake") + { + } + + #endregion Constructors + } +} diff --git a/Test/DDD.Core.SimpleInjector.NHibernate.UnitTests/Infrastructure/DependencyInjection/ContainerExtensionsTests.cs b/Test/DDD.Core.SimpleInjector.NHibernate.UnitTests/Infrastructure/DependencyInjection/ContainerExtensionsTests.cs new file mode 100644 index 0000000..3574192 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.NHibernate.UnitTests/Infrastructure/DependencyInjection/ContainerExtensionsTests.cs @@ -0,0 +1,58 @@ +using System; +using System.Reflection; +using SimpleInjector; +using NHibernate.Cfg; +using Xunit; +using FluentAssertions; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Data; + using Domain; + + public class ContainerExtensionsTests : IDisposable + { + + #region Fields + + private readonly Container container; + + #endregion Fields + + #region Constructors + + public ContainerExtensionsTests() + { + this.container = new Container(); + this.container.Options.EnableAutoVerification = false; + } + + #endregion Constructors + + #region Methods + + public void Dispose() + { + this.container.Dispose(); + } + + [Fact] + public void ConfigureApp_WithRegisterSessionFactoryFor_RegistersFactory() + { + // Act + this.container.ConfigureApp(o => + { + o.RegisterTypesFrom(Assembly.GetExecutingAssembly()); + o.RegisterSessionFactoryFor(new Configuration()); + }); + // Assert + var registration = container.GetRegistration>(); + registration.Should().NotBeNull().And.Match(r => r.Lifestyle == Lifestyle.Singleton); + var sessionFactory = registration.GetInstance(); + sessionFactory.Should().BeOfType>(); + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeCommand.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeCommand.cs new file mode 100644 index 0000000..3230402 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeCommand.cs @@ -0,0 +1,6 @@ +namespace DDD.Core.Application +{ + public class FakeCommand : ICommand + { + } +} \ No newline at end of file diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeCommandHandler.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeCommandHandler.cs new file mode 100644 index 0000000..2d805c8 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeCommandHandler.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + public class FakeCommandHandler : ICommandHandler + { + #region Methods + + public void Handle(FakeCommand command, IMessageContext context) + { + } + + public Task HandleAsync(FakeCommand command, IMessageContext context) + { + return Task.CompletedTask; + } + + #endregion Methods + } +} diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeEventHandler.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeEventHandler.cs new file mode 100644 index 0000000..2c1e1aa --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeEventHandler.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + using Domain; + + public class FakeEventHandler + : ISyncEventHandler, + IAsyncEventHandler + { + + #region Properties + + public FakeContext Context { get; } = new FakeContext(); + + public Type EventType => typeof(FakeEvent); + + BoundedContext ISyncEventHandler.Context => Context; + + BoundedContext IAsyncEventHandler.Context => Context; + + #endregion Properties + + #region Methods + + public void Handle(FakeEvent @event, IMessageContext context) + { + } + + public void Handle(IEvent @event, IMessageContext context) + { + } + + public Task HandleAsync(FakeEvent @event, IMessageContext context) + { + return Task.CompletedTask; + } + + public Task HandleAsync(IEvent @event, IMessageContext context) + { + return Task.CompletedTask; + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeMapper.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeMapper.cs new file mode 100644 index 0000000..d6e1a6a --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeMapper.cs @@ -0,0 +1,16 @@ +namespace DDD.Core.Application +{ + using Domain; + using Mapping; + + public class FakeMapper : IObjectMapper + { + #region Methods + + public void Map(FakeEvent source, FakeCommand destination, IMappingContext context) + { + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeQuery.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeQuery.cs new file mode 100644 index 0000000..62a72c9 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeQuery.cs @@ -0,0 +1,6 @@ +namespace DDD.Core.Application +{ + public class FakeQuery : IQuery + { + } +} diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeQueryHandler.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeQueryHandler.cs new file mode 100644 index 0000000..818405e --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeQueryHandler.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + public class FakeQueryHandler : IQueryHandler + { + #region Methods + + public string Handle(FakeQuery query, IMessageContext context) + { + return null; + } + + public Task HandleAsync(FakeQuery query, IMessageContext context) + { + return Task.FromResult(null); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeTranslator.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeTranslator.cs new file mode 100644 index 0000000..ffd46b7 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeTranslator.cs @@ -0,0 +1,33 @@ +using System; + +namespace DDD.Core.Application +{ + using Domain; + using Mapping; + + public class FakeTranslator : IObjectTranslator + { + + #region Properties + + public Type DestinationType => typeof(FakeCommand); + public Type SourceType => typeof(FakeEvent); + + #endregion Properties + + #region Methods + + public FakeCommand Translate(FakeEvent source, IMappingContext context) + { + return new FakeCommand(); + } + + public object Translate(object source, IMappingContext context) + { + return new FakeCommand(); + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/DDD.Core.SimpleInjector.UnitTests.csproj b/Test/DDD.Core.SimpleInjector.UnitTests/DDD.Core.SimpleInjector.UnitTests.csproj new file mode 100644 index 0000000..18157c0 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/DDD.Core.SimpleInjector.UnitTests.csproj @@ -0,0 +1,19 @@ + + + net48;net6.0 + DDD.Core + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/Domain/DummyContext.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Domain/DummyContext.cs new file mode 100644 index 0000000..434861b --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Domain/DummyContext.cs @@ -0,0 +1,13 @@ +namespace DDD.Core.Domain +{ + public class DummyContext : BoundedContext + { + #region Constructors + + public DummyContext() : base("DUM", "Dummy") + { + } + + #endregion Constructors + } +} diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/Domain/FakeContext.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Domain/FakeContext.cs new file mode 100644 index 0000000..e0c004c --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Domain/FakeContext.cs @@ -0,0 +1,13 @@ +namespace DDD.Core.Domain +{ + public class FakeContext : BoundedContext + { + #region Constructors + + public FakeContext() : base("FK", "Fake") + { + } + + #endregion Constructors + } +} diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/Domain/FakeEvent.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Domain/FakeEvent.cs new file mode 100644 index 0000000..7557110 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Domain/FakeEvent.cs @@ -0,0 +1,15 @@ +using System; + +namespace DDD.Core.Domain +{ + public class FakeEvent : IEvent + { + + #region Properties + + public DateTime OccurredOn { get; } + + #endregion Properties + + } +} diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/ContainerExtensionsTests.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/ContainerExtensionsTests.cs new file mode 100644 index 0000000..f11ed62 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/ContainerExtensionsTests.cs @@ -0,0 +1,675 @@ +using Xunit; +using FluentAssertions; +using SimpleInjector; +using System; +using System.Reflection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using NSubstitute; +using System.Collections.Generic; +using DDD.Serialization; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Application; + using Domain; + using Mapping; + using Data; + + public class ContainerExtensionsTests : IDisposable + { + + #region Fields + + private readonly Container container; + + #endregion Fields + + #region Constructors + + public ContainerExtensionsTests() + { + this.container = new Container(); + this.container.Options.EnableAutoVerification = false; + } + + #endregion Constructors + + #region Methods + + public void Dispose() + { + this.container.Dispose(); + } + + [Fact] + public void GetDecoratorChainOf_WithSimpleDecoratorsAndFromDecorated_ReturnsExpectedChain() + { + // Arrange + this.container.Register(); + this.container.RegisterDecorator(); + this.container.RegisterDecorator(); + var expectedChain = new [] + { + new RegisteredType(typeof(Decorated), Lifestyle.Transient), + new RegisteredType(typeof(FirstDecorator), Lifestyle.Transient), + new RegisteredType(typeof(SecondDecorator), Lifestyle.Transient) + }; + // Act + var chain = this.container.GetDecoratorChainOf(fromDecorated:true); + // Assert + chain.Should().BeEquivalentTo(expectedChain); + } + + [Fact] + public void GetDecoratorChainOf_WithSimpleDecoratorsAndToDecorated_ReturnsExpectedChain() + { + // Arrange + this.container.Register(); + this.container.RegisterDecorator(); + this.container.RegisterDecorator(); + var expectedChain = new[] + { + new RegisteredType(typeof(SecondDecorator), Lifestyle.Transient), + new RegisteredType(typeof(FirstDecorator), Lifestyle.Transient), + new RegisteredType(typeof(Decorated), Lifestyle.Transient) + }; + // Act + var chain = this.container.GetDecoratorChainOf(fromDecorated:false); + // Assert + chain.Should().BeEquivalentTo(expectedChain); + } + + [Fact] + public void GetDecoratorChainOf_WithDelegatingDecoratorsAndFromDecorated_ReturnsExpectedChain() + { + // Arrange + this.container.Register(); + this.container.RegisterDecorator(Lifestyle.Singleton); + this.container.RegisterDecorator(Lifestyle.Singleton); + var expectedChain = new[] + { + new RegisteredType(typeof(Decorated), Lifestyle.Transient), + new RegisteredType(typeof(FirstDelegatingDecorator), Lifestyle.Singleton), + new RegisteredType(typeof(SecondDelegatingDecorator), Lifestyle.Singleton) + }; + // Act + var chain = this.container.GetDecoratorChainOf(fromDecorated: true); + // Assert + chain.Should().BeEquivalentTo(expectedChain); + } + + [Fact] + public void GetDecoratorChainOf_WithDelegatingDecoratorsAndToDecorated_ReturnsExpectedChain() + { + // Arrange + this.container.Register(); + this.container.RegisterDecorator(Lifestyle.Singleton); + this.container.RegisterDecorator(Lifestyle.Singleton); + var expectedChain = new[] + { + new RegisteredType(typeof(SecondDelegatingDecorator), Lifestyle.Singleton), + new RegisteredType(typeof(FirstDelegatingDecorator), Lifestyle.Singleton), + new RegisteredType(typeof(Decorated), Lifestyle.Transient) + }; + // Act + var chain = this.container.GetDecoratorChainOf(fromDecorated: false); + // Assert + chain.Should().BeEquivalentTo(expectedChain); + } + + [Fact] + public void ConfigureApp_WhithoutOptions_RegistersNullLoggerFactory() + { + // Act + this.container.ConfigureApp(_ => { }); + // Assert + var registration = container.GetRegistration(); + registration.Should().NotBeNull().And.Match(r => r.Lifestyle == Lifestyle.Singleton); + var loggerFactory = registration.GetInstance(); + loggerFactory.Should().BeOfType(); + } + + [Fact] + public void ConfigureApp_WhithoutOptions_RegistersServiceProvider() + { + // Act + this.container.ConfigureApp(_ => { }); + // Assert + var registration = container.GetRegistration(); + registration.Should().NotBeNull().And.Match(r => r.Lifestyle == Lifestyle.Singleton); + var serviceProvider = registration.GetInstance(); + serviceProvider.Should().Be(container); + } + + [Fact] + public void ConfigureApp_WithConfigureDbConnectionFor_RegistersDbConnectionSettings() + { + // Act + this.container.ConfigureApp(o => + { + o.ConfigureDbConnectionFor(oc => + { + oc.SetProviderName("FakeProviderName"); + oc.SetConnectionString("FakeConnectionString"); + }); + }); + // Assert + var registration = container.GetRegistration>(); + registration.Should().NotBeNull().And.Match(r => r.Lifestyle == Lifestyle.Singleton); + } + + [Fact] + public void ConfigureApp_WithConfigureDbConnectionFor_RegistersDbConnectionProvider() + { + // Act + this.container.ConfigureApp(o => + { + o.ConfigureDbConnectionFor(oc => + { + oc.SetProviderName("FakeProviderName"); + oc.SetConnectionString("FakeConnectionString"); + }); + }); + // Assert + var registration = container.GetRegistration>(); + registration.Should().NotBeNull().And.Match(r => r.ImplementationType == typeof(LazyDbConnectionProvider) + && r.Lifestyle == container.Options.DefaultScopedLifestyle); + } + + [Fact] + public void ConfigureApp_WithConfigureConsumerFor_RegistersEventConsumerAsSingleton() + { + // Act + this.container.ConfigureApp(o => + { + o.ConfigureEvents(oe => + { + oe.ConfigureConsumerFor(); + }); + }); + // Assert + var registration = container.GetRegistration>(); + registration.Should().NotBeNull().And.Match(r => r.ImplementationType == typeof(EventConsumer) + && r.Lifestyle == Lifestyle.Singleton); + } + + [Fact] + public void ConfigureApp_WithConfigureConsumerFor_RegistersEventConsumerInCollection() + { + // Act + this.container.ConfigureApp(o => + { + o.RegisterTypesFrom(Assembly.GetExecutingAssembly()); + o.RegisterSerializer(SerializationFormat.Json, () => Substitute.For()); + o.ConfigureCommands(); + o.ConfigureQueries(); + o.ConfigureEvents(oe => + { + oe.ConfigureConsumerFor(); + }); + }); + // Assert + var registration = container.GetRegistration>(); + registration.Should().NotBeNull().And.Match(r => r.Lifestyle == Lifestyle.Singleton); + var consumers = (IEnumerable)registration.GetInstance(); + consumers.Should().ContainSingle(c => c is IEventConsumer); + } + + [Fact] + public void ConfigureApp_WithConfigureConsumerFor_RegistersEventConsumerSettings() + { + // Act + this.container.ConfigureApp(o => + { + o.ConfigureEvents(oe => + { + oe.ConfigureConsumerFor(); + }); + }); + // Assert + var registration = container.GetRegistration>(); + registration.Should().NotBeNull().And.Match(r => r.Lifestyle == Lifestyle.Singleton); + } + + [Fact] + public void ConfigureApp_WithConfigureConsumerFor_RegistersSameInstanceOfEventConsumerAsSingletonAndInCollection() + { + // Act + this.container.ConfigureApp(o => + { + o.RegisterTypesFrom(Assembly.GetExecutingAssembly()); + o.RegisterSerializer(SerializationFormat.Json, () => Substitute.For()); + o.ConfigureCommands(); + o.ConfigureQueries(); + o.ConfigureEvents(oe => + { + oe.ConfigureConsumerFor(); + }); + }); + // Assert + var fakeConsumer = container.GetInstance>(); + var consumers = container.GetAllInstances(); + consumers.Should().BeEquivalentTo(new[] { fakeConsumer }); + } + + [Fact] + public void ConfigureApp_WithConfigureManagerFor_RegistersRecurringCommandManagerAsSingleton() + { + // Act + this.container.ConfigureApp(o => + { + o.ConfigureCommands(oc => + { + oc.ConfigureManagerFor(); + }); + }); + // Assert + var registration = container.GetRegistration>(); + registration.Should().NotBeNull().And.Match(r => r.ImplementationType == typeof(RecurringCommandManager) + && r.Lifestyle == Lifestyle.Singleton); + } + + [Fact] + public void ConfigureApp_WithConfigureManagerFor_RegistersRecurringCommandManagerInCollection() + { + // Act + this.container.ConfigureApp(o => + { + o.RegisterTypesFrom(Assembly.GetExecutingAssembly()); + o.RegisterSerializer(SerializationFormat.Json, () => Substitute.For()); + o.ConfigureQueries(); + o.ConfigureCommands(oc => + { + oc.RegisterScheduleFactory(RecurringExpressionFormat.Cron, () => Substitute.For()); + oc.ConfigureManagerFor(); + }); + }); + // Assert + var registration = container.GetRegistration>(); + registration.Should().NotBeNull().And.Match(r => r.Lifestyle == Lifestyle.Singleton); + var managers = (IEnumerable)registration.GetInstance(); + managers.Should().ContainSingle(c => c is IRecurringCommandManager); + } + + [Fact] + public void ConfigureApp_WithConfigureManagerFor_RegistersRecurringCommandManagerSettings() + { + // Act + this.container.ConfigureApp(o => + { + o.ConfigureCommands(oc => + { + oc.ConfigureManagerFor(); + }); + }); + // Assert + var registration = container.GetRegistration>(); + registration.Should().NotBeNull().And.Match(r => r.Lifestyle == Lifestyle.Singleton); + } + + [Fact] + public void ConfigureApp_WithConfigureManagerFor_RegistersSameinstanceOfRecurringCommandManagerAsSingletonAndInCollection() + { + // Act + this.container.ConfigureApp(o => + { + o.RegisterTypesFrom(Assembly.GetExecutingAssembly()); + o.RegisterSerializer(SerializationFormat.Json, () => Substitute.For()); + o.ConfigureQueries(); + o.ConfigureCommands(oc => + { + oc.RegisterScheduleFactory(RecurringExpressionFormat.Cron, () => Substitute.For()); + oc.ConfigureManagerFor(); + }); + }); + // Assert + var fakeManager = container.GetInstance>(); + var managers = container.GetAllInstances(); + managers.Should().ContainSingle(m => ReferenceEquals(m, fakeManager)); + } + + [Fact] + public void ConfigureApp_WithRegisterScheduleFactory_RegistersRecurringScheduleFactoryInCollection() + { + var fakeScheduleFactory = Substitute.For(); + var expectedScheduleFactories = new[] { fakeScheduleFactory }; + // Act + this.container.ConfigureApp(o => + { + o.ConfigureCommands(oe => + { + oe.RegisterScheduleFactory(RecurringExpressionFormat.Cron, () => fakeScheduleFactory); + }); + }); + // Assert + var registration = container.GetRegistration>(); + registration.Should().NotBeNull().And.Match(r => r.Lifestyle == Lifestyle.Singleton); + var scheduleFactories = registration.GetInstance(); + scheduleFactories.Should().BeEquivalentTo(expectedScheduleFactories); + } + + [Fact] + public void ConfigureApp_WithRegisterSerializer_RegistersTextSerializerInCollection() + { + // Arrange + var fakeSerializer = Substitute.For(); + var expectedSerializers = new[] { fakeSerializer }; + // Act + this.container.ConfigureApp(o => + { + o.RegisterSerializer(SerializationFormat.Json, () => fakeSerializer); + }); + // Assert + var registration = container.GetRegistration>(); + registration.Should().NotBeNull().And.Match(r => r.Lifestyle == Lifestyle.Singleton); + var serializers = registration.GetInstance(); + serializers.Should().BeEquivalentTo(expectedSerializers); + } + + [Fact] + public void ConfigureApp_WithRegisterTypesFrom_RegistersBoundedContextsAsSingleton() + { + // Act + this.container.ConfigureApp(o => + { + o.RegisterTypesFrom(Assembly.GetExecutingAssembly()); + }); + // Assert + var registration = container.GetRegistration(); + registration.Should().NotBeNull().And.Match(r => r.ImplementationType == typeof(FakeContext) + && r.Lifestyle == Lifestyle.Singleton); + } + + [Fact] + public void ConfigureApp_WithRegisterTypesFrom_RegistersBoundedContextsInCollection() + { + // Arrange + var expectedContexts = new BoundedContext[] + { + new FakeContext(), + new DummyContext() + }; + // Act + this.container.ConfigureApp(o => + { + o.RegisterTypesFrom(Assembly.GetExecutingAssembly()); + }); + // Assert + var registration = container.GetRegistration>(); + registration.Should().NotBeNull().And.Match(r => r.Lifestyle == Lifestyle.Singleton); + var contexts = registration.GetInstance(); + contexts.Should().BeEquivalentTo(expectedContexts); + } + + [Fact] + public void ConfigureApp_WithRegisterTypesFrom_RegistersSameInstancesOfBoundedContextsAsSingletonAndInCollection() + { + // Act + this.container.ConfigureApp(o => + { + o.RegisterTypesFrom(Assembly.GetExecutingAssembly()); + }); + // Assert + var fakeContext = container.GetInstance(); + var contexts = container.GetAllInstances(); + contexts.Should().ContainSingle(c => ReferenceEquals(c, fakeContext)); + } + + + [Fact] + public void ConfigureApp_WithConfigureCommands_RegistersAsyncCommandHandlers() + { + // Arrange + var expectedChain = new[] + { + new RegisteredType(typeof(FakeCommandHandler), Lifestyle.Transient), + new RegisteredType(typeof(AsyncCommandHandlerWithLogging), Lifestyle.Transient), + new RegisteredType(typeof(AsyncScopedCommandHandler), Lifestyle.Singleton) + }; + // Act + this.container.ConfigureApp(o => + { + o.RegisterTypesFrom(Assembly.GetExecutingAssembly()); + o.ConfigureCommands(); + }); + // Assert + var chain = container.GetDecoratorChainOf>(); + chain.Should().BeEquivalentTo(expectedChain); + } + + [Fact] + public void ConfigureApp_WithConfigureCommands_RegistersCommandProcessor() + { + // Act + this.container.ConfigureApp(o => + { + o.ConfigureCommands(); + }); + // Assert + var registration = container.GetRegistration(); + registration.Should().NotBeNull().And.Match(r => r.ImplementationType == typeof(CommandProcessor) + && r.Lifestyle == Lifestyle.Singleton); + } + + [Fact] + public void ConfigureApp_WithConfigureCommands_RegistersSyncCommandHandlers() + { + // Arrange + var expectedChain = new[] + { + new RegisteredType(typeof(FakeCommandHandler), Lifestyle.Transient), + new RegisteredType(typeof(SyncCommandHandlerWithLogging), Lifestyle.Transient), + new RegisteredType(typeof(ThreadScopedCommandHandler), Lifestyle.Singleton) + }; + // Act + this.container.ConfigureApp(o => + { + o.RegisterTypesFrom(Assembly.GetExecutingAssembly()); + o.ConfigureCommands(); + }); + // Assert + var chain = container.GetDecoratorChainOf>(); + chain.Should().BeEquivalentTo(expectedChain); + } + + [Fact] + public void ConfigureApp_WithConfigureEvents_RegistersAsyncEventHandlers() + { + // Arrange + var expectedChain = new[] + { + new RegisteredType(typeof(FakeEventHandler), Lifestyle.Transient), + new RegisteredType(typeof(AsyncEventHandlerWithLogging), Lifestyle.Transient) + }; + // Act + this.container.ConfigureApp(o => + { + o.RegisterTypesFrom(Assembly.GetExecutingAssembly()); + o.ConfigureEvents(_ => { }); + }); + // Assert + var chain = container.GetDecoratorChainOf>(); + chain.Should().BeEquivalentTo(expectedChain); + } + + [Fact] + public void ConfigureApp_WithConfigureEvents_RegistersEventPublisher() + { + // Act + this.container.ConfigureApp(o => + { + o.ConfigureEvents(_ => { }); + }); + // Assert + var registration = container.GetRegistration>(); + registration.Should().NotBeNull().And.Match(r => r.ImplementationType == typeof(EventPublisher) + && r.Lifestyle == Lifestyle.Singleton); + } + + [Fact] + public void ConfigureApp_WithConfigureEvents_RegistersEventSerializer() + { + // Act + this.container.ConfigureApp(o => + { + o.ConfigureEvents(_ => { }); + }); + // Assert + var registration = container.GetRegistration(); + registration.Should().NotBeNull().And.Match(r => r.Lifestyle == Lifestyle.Singleton); + } + + [Fact] + public void ConfigureApp_WithConfigureEvents_RegistersSyncEventHandlers() + { + // Arrange + var expectedChain = new[] + { + new RegisteredType(typeof(FakeEventHandler), Lifestyle.Transient), + new RegisteredType(typeof(SyncEventHandlerWithLogging), Lifestyle.Transient) + }; + // Act + this.container.ConfigureApp(o => + { + o.RegisterTypesFrom(Assembly.GetExecutingAssembly()); + o.ConfigureEvents(_ => { }); + }); + // Assert + var chain = container.GetDecoratorChainOf>(); + chain.Should().BeEquivalentTo(expectedChain); + } + + [Fact] + public void ConfigureApp_WithConfigureLogging_RegistersCustomLoggerFactory() + { + // Arrange + var customLoggerFactory = Substitute.For(); + // Act + this.container.ConfigureApp(o => + { + o.ConfigureLogging(() => customLoggerFactory); + }); + // Assert + var registration = container.GetRegistration(); + registration.Should().NotBeNull().And.Match(r => r.Lifestyle == Lifestyle.Singleton); + var loggerFactory = registration.GetInstance(); + loggerFactory.Should().Be(customLoggerFactory); + } + + [Fact] + public void ConfigureApp_WithConfigureMapping_RegistersMappers() + { + // Act + this.container.ConfigureApp(o => + { + o.RegisterTypesFrom(Assembly.GetExecutingAssembly()); + o.ConfigureMapping(); + }); + // Assert + var registration = container.GetRegistration>(); + registration.Should().NotBeNull().And.Match(r => r.ImplementationType == typeof(FakeMapper) + && r.Lifestyle == Lifestyle.Transient); + } + + [Fact] + public void ConfigureApp_WithConfigureMapping_RegistersMappingProcessor() + { + // Act + this.container.ConfigureApp(o => + { + o.ConfigureMapping(); + }); + // Assert + var registration = container.GetRegistration(); + registration.Should().NotBeNull().And.Match(r => r.ImplementationType == typeof(MappingProcessor) + && r.Lifestyle == Lifestyle.Singleton); + } + + [Fact] + public void ConfigureApp_WithConfigureMapping_RegistersMappingProcessorSettings() + { + // Act + this.container.ConfigureApp(o => + { + o.ConfigureMapping(); + }); + // Assert + var registration = container.GetRegistration(); + registration.Should().NotBeNull().And.Match(r => r.Lifestyle == Lifestyle.Singleton); + } + + [Fact] + public void ConfigureApp_WithConfigureMapping_RegistersTranslators() + { + // Act + this.container.ConfigureApp(o => + { + o.RegisterTypesFrom(Assembly.GetExecutingAssembly()); + o.ConfigureMapping(); + }); + // Assert + var registration = container.GetRegistration>(); + registration.Should().NotBeNull().And.Match(r => r.ImplementationType == typeof(FakeTranslator) + && r.Lifestyle == Lifestyle.Transient); + } + [Fact] + public void ConfigureApp_WithConfigureQueries_RegistersAsyncQueryHandlers() + { + // Arrange + var expectedChain = new[] + { + new RegisteredType(typeof(FakeQueryHandler), Lifestyle.Transient), + new RegisteredType(typeof(AsyncQueryHandlerWithLogging), Lifestyle.Transient), + new RegisteredType(typeof(AsyncScopedQueryHandler), Lifestyle.Singleton) + }; + // Act + this.container.ConfigureApp(o => + { + o.RegisterTypesFrom(Assembly.GetExecutingAssembly()); + o.ConfigureQueries(); + }); + // Assert + var chain = container.GetDecoratorChainOf>(); + chain.Should().BeEquivalentTo(expectedChain); + } + + [Fact] + public void ConfigureApp_WithConfigureQueries_RegistersQueryProcessor() + { + // Act + this.container.ConfigureApp(o => + { + o.ConfigureQueries(); + }); + // Assert + var registration = container.GetRegistration(); + registration.Should().NotBeNull().And.Match(r => r.ImplementationType == typeof(QueryProcessor) + && r.Lifestyle == Lifestyle.Singleton); + } + + [Fact] + public void ConfigureApp_WithConfigureQueries_RegistersSyncQueryHandlers() + { + // Arrange + var expectedChain = new[] + { + new RegisteredType(typeof(FakeQueryHandler), Lifestyle.Transient), + new RegisteredType(typeof(SyncQueryHandlerWithLogging), Lifestyle.Transient), + new RegisteredType(typeof(ThreadScopedQueryHandler), Lifestyle.Singleton) + }; + // Act + this.container.ConfigureApp(o => + { + o.RegisterTypesFrom(Assembly.GetExecutingAssembly()); + o.ConfigureQueries(); + }); + // Assert + var chain = container.GetDecoratorChainOf>(); + chain.Should().BeEquivalentTo(expectedChain); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/Decorated.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/Decorated.cs new file mode 100644 index 0000000..6ee1a71 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/Decorated.cs @@ -0,0 +1,15 @@ +namespace DDD.Core.Infrastructure.DependencyInjection +{ + public class Decorated : IDoSomething + { + + #region Methods + + public void DoSomething() + { + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/FirstDecorator.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/FirstDecorator.cs new file mode 100644 index 0000000..72fd617 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/FirstDecorator.cs @@ -0,0 +1,30 @@ +namespace DDD.Core.Infrastructure.DependencyInjection +{ + public class FirstDecorator : IDoSomething + { + #region Fields + + private readonly IDoSomething decorated; + + #endregion Fields + + #region Constructors + + public FirstDecorator(IDoSomething decorated) + { + this.decorated = decorated; + } + + #endregion Constructors + + #region Methods + + public void DoSomething() + { + this.decorated.DoSomething(); + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/FirstDelegatingDecorator.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/FirstDelegatingDecorator.cs new file mode 100644 index 0000000..a78b562 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/FirstDelegatingDecorator.cs @@ -0,0 +1,32 @@ +using System; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + public class FirstDelegatingDecorator : IDoSomething + { + #region Fields + + private readonly Func decoratedFactory; + + #endregion Fields + + #region Constructors + + public FirstDelegatingDecorator(Func decoratedFactory) + { + this.decoratedFactory = decoratedFactory; + } + + #endregion Constructors + + #region Methods + + public void DoSomething() + { + this.decoratedFactory().DoSomething(); + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/IDoSomething.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/IDoSomething.cs new file mode 100644 index 0000000..fa9e2f2 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/IDoSomething.cs @@ -0,0 +1,13 @@ +namespace DDD.Core.Infrastructure.DependencyInjection +{ + public interface IDoSomething + { + + #region Methods + + void DoSomething(); + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/SecondDecorator.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/SecondDecorator.cs new file mode 100644 index 0000000..0388fb2 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/SecondDecorator.cs @@ -0,0 +1,31 @@ +namespace DDD.Core.Infrastructure.DependencyInjection +{ + public class SecondDecorator: IDoSomething + { + + #region Fields + + private readonly IDoSomething decorated; + + #endregion Fields + + #region Constructors + + public SecondDecorator(IDoSomething decorated) + { + this.decorated = decorated; + } + + #endregion Constructors + + #region Methods + + public void DoSomething() + { + this.decorated.DoSomething(); + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/SecondDelegatingDecorator.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/SecondDelegatingDecorator.cs new file mode 100644 index 0000000..891b9af --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/SecondDelegatingDecorator.cs @@ -0,0 +1,32 @@ +using System; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + public class SecondDelegatingDecorator : IDoSomething + { + #region Fields + + private readonly Func decoratedFactory; + + #endregion Fields + + #region Constructors + + public SecondDelegatingDecorator(Func decoratedFactory) + { + this.decoratedFactory = decoratedFactory; + } + + #endregion Constructors + + #region Methods + + public void DoSomething() + { + this.decoratedFactory().DoSomething(); + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs b/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs index 3bdcf24..177a566 100644 --- a/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs +++ b/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs @@ -1,6 +1,8 @@ using NSubstitute; using System; +using System.Threading.Tasks; using Xunit; +using DDD.Validation; namespace DDD.Core.Application { @@ -8,10 +10,14 @@ public class CommandProcessorTests { #region Fields - private readonly ICommandHandler handlerOfCommand1; - private readonly ICommandHandler handlerOfCommand2; - private readonly ICommandValidator validatorOfCommand1; - private readonly ICommandValidator validatorOfCommand2; + private readonly ISyncCommandHandler syncHandlerOfCommand1; + private readonly ISyncCommandHandler syncHandlerOfCommand2; + private readonly ISyncObjectValidator syncValidatorOfCommand1; + private readonly ISyncObjectValidator syncValidatorOfCommand2; + private readonly IAsyncCommandHandler asyncHandlerOfCommand1; + private readonly IAsyncCommandHandler asyncHandlerOfCommand2; + private readonly IAsyncObjectValidator asyncValidatorOfCommand1; + private readonly IAsyncObjectValidator asyncValidatorOfCommand2; private readonly CommandProcessor processor; #endregion Fields @@ -20,20 +26,32 @@ public class CommandProcessorTests public CommandProcessorTests() { - this.handlerOfCommand1 = Substitute.For>(); - this.handlerOfCommand2 = Substitute.For>(); - this.validatorOfCommand1 = Substitute.For>(); - this.validatorOfCommand2 = Substitute.For>(); + this.syncHandlerOfCommand1 = Substitute.For>(); + this.syncHandlerOfCommand2 = Substitute.For>(); + this.syncValidatorOfCommand1 = Substitute.For>(); + this.syncValidatorOfCommand2 = Substitute.For>(); + this.asyncHandlerOfCommand1 = Substitute.For>(); + this.asyncHandlerOfCommand2 = Substitute.For>(); + this.asyncValidatorOfCommand1 = Substitute.For>(); + this.asyncValidatorOfCommand2 = Substitute.For>(); var serviceProvider = Substitute.For(); - serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ICommandHandler)))) - .Returns(this.handlerOfCommand1); - serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ICommandHandler)))) - .Returns(this.handlerOfCommand2); - serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ICommandValidator)))) - .Returns(this.validatorOfCommand1); - serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ICommandValidator)))) - .Returns(this.validatorOfCommand2); - processor = new CommandProcessor(serviceProvider); + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncCommandHandler)))) + .Returns(this.syncHandlerOfCommand1); + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncCommandHandler)))) + .Returns(this.syncHandlerOfCommand2); + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncObjectValidator)))) + .Returns(this.syncValidatorOfCommand1); + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncObjectValidator)))) + .Returns(this.syncValidatorOfCommand2); + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(IAsyncCommandHandler)))) + .Returns(this.asyncHandlerOfCommand1); + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(IAsyncCommandHandler)))) + .Returns(this.asyncHandlerOfCommand2); + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(IAsyncObjectValidator)))) + .Returns(this.asyncValidatorOfCommand1); + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(IAsyncObjectValidator)))) + .Returns(this.asyncValidatorOfCommand2); + processor = new CommandProcessor(serviceProvider, new CommandProcessorSettings()); } #endregion Constructors @@ -41,25 +59,99 @@ public CommandProcessorTests() #region Methods [Fact] - public void Process_WhenCommandDefined_CallsExpectedHandler() + public void Process_WhenConcreteCommand_CallsExpectedHandler() { // Arrange var command = new FakeCommand1(); + var context = new MessageContext(); // Act - this.processor.Process(command); + this.processor.Process(command, context); // Assert - this.handlerOfCommand1.Received(1).Handle(command); + this.syncHandlerOfCommand1.Received(1).Handle(command, context); } [Fact] - public void Validate_WhenCommandDefined_CallsExpectedValidator() + public async Task ProcessAsync_WhenConcreteCommand_CallsExpectedHandler() { // Arrange var command = new FakeCommand1(); + var context = new MessageContext(); // Act - this.processor.Validate(command); + await this.processor.ProcessAsync(command, context); // Assert - this.validatorOfCommand1.Received(1).Validate(command); + await this.asyncHandlerOfCommand1.Received(1).HandleAsync(command, context); + } + + [Fact] + public void Process_WhenAbstractCommand_CallsExpectedHandler() + { + // Arrange + var command = new FakeCommand1(); + var context = new MessageContext(); + // Act + this.processor.Process((ICommand)command, context); + // Assert + this.syncHandlerOfCommand1.Received(1).Handle(command, context); + } + + [Fact] + public async Task ProcessAsync_WhenAbstractCommand_CallsExpectedHandler() + { + // Arrange + var command = new FakeCommand1(); + var context = new MessageContext(); + // Act + await this.processor.ProcessAsync((ICommand)command, context); + // Assert + await this.asyncHandlerOfCommand1.Received(1).HandleAsync(command, context); + } + + [Fact] + public void Validate_WhenConcreteCommand_CallsExpectedValidator() + { + // Arrange + var command = new FakeCommand1(); + var context = new ValidationContext(); + // Act + this.processor.Validate(command, context); + // Assert + this.syncValidatorOfCommand1.Received(1).Validate(command, context); + } + + [Fact] + public async Task ValidateAsync_WhenConcreteCommand_CallsExpectedValidator() + { + // Arrange + var command = new FakeCommand1(); + var context = new ValidationContext(); + // Act + await this.processor.ValidateAsync(command, context); + // Assert + await this.asyncValidatorOfCommand1.Received(1).ValidateAsync(command, context); + } + + [Fact] + public void Validate_WhenAbstractCommand_CallsExpectedValidator() + { + // Arrange + var command = new FakeCommand1(); + var context = new ValidationContext(); + // Act + this.processor.Validate((ICommand)command, context); + // Assert + this.syncValidatorOfCommand1.Received(1).Validate(command, context); + } + + [Fact] + public async Task ValidateAsync_WhenAbstractCommand_CallsExpectedValidator() + { + // Arrange + var command = new FakeCommand1(); + var context = new ValidationContext(); + // Act + await this.processor.ValidateAsync((ICommand)command, context); + // Assert + await this.asyncValidatorOfCommand1.Received(1).ValidateAsync(command, context); } #endregion Methods diff --git a/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs b/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs new file mode 100644 index 0000000..a995f46 --- /dev/null +++ b/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs @@ -0,0 +1,603 @@ +using Microsoft.Extensions.Logging; +using NSubstitute; +using NSubstitute.Core; +using System.Threading.Tasks; +using System; +using System.IO; +using System.Collections.Generic; +using Xunit; +using FluentAssertions; + +namespace DDD.Core.Application +{ + using Serialization; + using Domain; + using Infrastructure.Testing; + + public class EventConsumerTests : IDisposable + { + + #region Fields + + private EventConsumer consumer; + + #endregion Fields + + #region Methods + + public void Dispose() + { + this.consumer?.Dispose(); + } + + [Fact] + public void Start_WhenIsNotRunning_SetsIsRunningToTrue() + { + // Arrange + var @event = FakeEvent(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorReadingEventStream(@event); + var eventPublisher = FakeEventPublisher(); + var boundedContexts = FakeBoundedContexts(); + var eventSerializers = FakeEventSerializers(); + var logger = FakeLogger(); + var settings = FakeSettings(); + consumer = new EventConsumer(commandProcessor, queryProcessor, eventPublisher, boundedContexts, eventSerializers, logger, settings); + // Act + consumer.Start(); + // Assert + consumer.IsRunning.Should().BeTrue(); + } + + [Fact] + public void Stop_WhenIsRunning_SetsIsRunningToFalse() + { + // Arrange + var @event = FakeEvent(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorReadingEventStream(@event); + var eventPublisher = FakeEventPublisher(); + var boundedContexts = FakeBoundedContexts(); + var eventSerializers = FakeEventSerializers(); + var logger = FakeLogger(); + var settings = FakeSettings(); + consumer = new EventConsumer(commandProcessor, queryProcessor, eventPublisher, boundedContexts, eventSerializers, logger, settings); + consumer.Start(); + // Act + consumer.Stop(); + // Assert + consumer.IsRunning.Should().BeFalse(); + } + + [Fact] + public async Task StartAndWait_WhenExceptionInDeserializingEvent_ExcludesFailedEventStream() + { + // Arrange + var @event = FakeEvent(); + var exception = FakeException(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorReadingEventStream(@event); + var eventPublisher = FakeEventPublisher(); + var fakeContext = new FakeContext(); + var boundedContexts = FakeBoundedContexts(); + var eventSerializers = FakeEventSerializersThrowingException(exception); + var logger = FakeLogger(); + var settings = FakeSettings(); + consumer = new EventConsumer(commandProcessor, queryProcessor, eventPublisher, boundedContexts, eventSerializers, logger, settings); + // Act + consumer.Start(); + consumer.Wait(TimeSpan.FromSeconds(5)); + // Assert + await commandProcessor.InGeneric(fakeContext) + .Received(1) + .ProcessAsync(Arg.Is(c => c.EventId == @event.EventId && c.ExceptionMessage == exception.Message), Arg.Any()); + } + + [Fact] + public void StartAndWait_WhenExceptionInDeserializingEvent_LogsException() + { + // Arrange + var @event = FakeEvent(); + var exception = FakeException(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorReadingEventStream(@event); + var eventPublisher = FakeEventPublisher(); + var boundedContexts = FakeBoundedContexts(); + var eventSerializers = FakeEventSerializersThrowingException(exception); + var logger = FakeLogger(); + var settings = FakeSettings(); + consumer = new EventConsumer(commandProcessor, queryProcessor, eventPublisher, boundedContexts, eventSerializers, logger, settings); + // Act + consumer.Start(); + consumer.Wait(TimeSpan.FromSeconds(5)); + // Assert + logger.Received(c => IsExpectedLogCall(c, LogLevel.Error, exception)); + } + + [Fact] + public void StartAndWait_WhenExceptionInDeserializingFailedEvent_LogsException() + { + // Arrange + var @event = FakeEvent(); + var exception = FakeException(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorReadingFailedEventStream(@event); + var eventPublisher = FakeEventPublisher(); + var boundedContexts = FakeBoundedContexts(); + var eventSerializers = FakeEventSerializersThrowingException(exception); + var logger = FakeLogger(); + var settings = FakeSettings(); + consumer = new EventConsumer(commandProcessor, queryProcessor, eventPublisher, boundedContexts, eventSerializers, logger, settings); + // Act + consumer.Start(); + consumer.Wait(TimeSpan.FromSeconds(5)); + // Assert + logger.Received(c => IsExpectedLogCall(c, LogLevel.Error, exception)); + } + + [Fact] + public async Task StartAndWait_WhenExceptionInDeserializingFailedEvent_UpdatesFailedEventStream() + { + // Arrange + var @event = FakeEvent(); + var exception = FakeException(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorReadingFailedEventStream(@event); + var eventPublisher = FakeEventPublisher(); + var fakeContext = new FakeContext(); + var boundedContexts = FakeBoundedContexts(); + var eventSerializers = FakeEventSerializersThrowingException(exception); + var logger = FakeLogger(); + var settings = FakeSettings(); + consumer = new EventConsumer(commandProcessor, queryProcessor, eventPublisher, boundedContexts, eventSerializers, logger, settings); + // Act + consumer.Start(); + consumer.Wait(TimeSpan.FromSeconds(5)); + // Assert + await commandProcessor.InGeneric(fakeContext) + .Received(1) + .ProcessAsync(Arg.Is(c => c.EventId == @event.EventId && c.ExceptionMessage == exception.Message), Arg.Any()); + } + + [Fact] + public void StartAndWait_WhenExceptionInExcludingFailedEventStream_LogsException() + { + // Arrange + var @event = FakeEvent(); + var exception = FakeException(); + var commandProcessor = FakeCommandProcessorThrowingExceptionWhenExcludingFailedEventStream(exception); + var queryProcessor = FakeQueryProcessorReadingEventStream(@event); + var eventPublisher = FakeEventPublisherThrowingException(FakeException()); + var boundedContexts = FakeBoundedContexts(); + var eventSerializers = FakeEventSerializers(); + var logger = FakeLogger(); + var settings = FakeSettings(); + consumer = new EventConsumer(commandProcessor, queryProcessor, eventPublisher, boundedContexts, eventSerializers, logger, settings); + // Act + consumer.Start(); + consumer.Wait(TimeSpan.FromSeconds(5)); + // Assert + logger.Received(c => IsExpectedLogCall(c, LogLevel.Critical, exception)); + } + + [Fact] + public void StartAndWait_WhenExceptionInFindingEventStreams_LogsException() + { + // Arrange + var @event = FakeEvent(); + var exception = FakeException(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorThrowingExceptionWhenFindingEventStreams(exception); + var eventPublisher = FakeEventPublisher(); + var boundedContexts = FakeBoundedContexts(); + var eventSerializers = FakeEventSerializers(); + var logger = FakeLogger(); + var settings = FakeSettings(); + consumer = new EventConsumer(commandProcessor, queryProcessor, eventPublisher, boundedContexts, eventSerializers, logger, settings); + // Act + consumer.Start(); + consumer.Wait(TimeSpan.FromSeconds(5)); + // Assert + logger.Received(c => IsExpectedLogCall(c, LogLevel.Critical, exception)); + } + + [Fact] + public void StartAndWait_WhenExceptionInFindingFailedEventStreams_LogsException() + { + // Arrange + var @event = FakeEvent(); + var exception = FakeException(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorThrowingExceptionWhenFindingFailedEventStreams(exception); + var eventPublisher = FakeEventPublisher(); + var boundedContexts = FakeBoundedContexts(); + var eventSerializers = FakeEventSerializers(); + var logger = FakeLogger(); + var settings = FakeSettings(); + consumer = new EventConsumer(commandProcessor, queryProcessor, eventPublisher, boundedContexts, eventSerializers, logger, settings); + // Act + consumer.Start(); + consumer.Wait(TimeSpan.FromSeconds(5)); + // Assert + logger.Received(c => IsExpectedLogCall(c, LogLevel.Critical, exception)); + } + + [Fact] + public void StartAndWait_WhenExceptionInIncludingFailedEventStream_LogsException() + { + // Arrange + var @event = FakeEvent(); + var exception = FakeException(); + var commandProcessor = FakeCommandProcessorThrowingExceptionWhenIncludingFailedEventStream(exception); + var queryProcessor = FakeQueryProcessorReadingFailedEventStream(@event); + var eventPublisher = FakeEventPublisher(); + var boundedContexts = FakeBoundedContexts(); + var eventSerializers = FakeEventSerializers(); + var logger = FakeLogger(); + var settings = FakeSettings(); + consumer = new EventConsumer(commandProcessor, queryProcessor, eventPublisher, boundedContexts, eventSerializers, logger, settings); + // Act + consumer.Start(); + consumer.Wait(TimeSpan.FromSeconds(5)); + // Assert + logger.Received(c => IsExpectedLogCall(c, LogLevel.Critical, exception)); + } + + [Fact] + public async Task StartAndWait_WhenExceptionInPublishingEvent_ExcludesFailedEventStream() + { + // Arrange + var @event = FakeEvent(); + var exception = FakeException(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorReadingEventStream(@event); + var eventPublisher = FakeEventPublisherThrowingException(exception); + var fakeContext = new FakeContext(); + var boundedContexts = FakeBoundedContexts(); + var eventSerializers = FakeEventSerializers(); + var logger = FakeLogger(); + var settings = FakeSettings(); + consumer = new EventConsumer(commandProcessor, queryProcessor, eventPublisher, boundedContexts, eventSerializers, logger, settings); + // Act + consumer.Start(); + consumer.Wait(TimeSpan.FromSeconds(5)); + // Assert + await commandProcessor.InGeneric(fakeContext) + .Received(1) + .ProcessAsync(Arg.Is(c => c.EventId == @event.EventId && c.ExceptionMessage == exception.Message), Arg.Any()); + } + + [Fact] + public void StartAndWait_WhenExceptionInPublishingEvent_LogsException() + { + // Arrange + var @event = FakeEvent(); + var exception = FakeException(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorReadingEventStream(@event); + var eventPublisher = FakeEventPublisherThrowingException(exception); + var boundedContexts = FakeBoundedContexts(); + var eventSerializers = FakeEventSerializers(); + var logger = FakeLogger(); + var settings = FakeSettings(); + consumer = new EventConsumer(commandProcessor, queryProcessor, eventPublisher, boundedContexts, eventSerializers, logger, settings); + // Act + consumer.Start(); + consumer.Wait(TimeSpan.FromSeconds(5)); + // Assert + logger.Received(c => IsExpectedLogCall(c, LogLevel.Error, exception)); + } + + [Fact] + public void StartAndWait_WhenExceptionInPublishingFailedEvent_LogsException() + { + // Arrange + var @event = FakeEvent(); + var exception = FakeException(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorReadingFailedEventStream(@event); + var eventPublisher = FakeEventPublisherThrowingException(exception); + var boundedContexts = FakeBoundedContexts(); + var eventSerializers = FakeEventSerializers(); + var logger = FakeLogger(); + var settings = FakeSettings(); + consumer = new EventConsumer(commandProcessor, queryProcessor, eventPublisher, boundedContexts, eventSerializers, logger, settings); + // Act + consumer.Start(); + consumer.Wait(TimeSpan.FromSeconds(5)); + // Assert + logger.Received(c => IsExpectedLogCall(c, LogLevel.Error, exception)); + } + + [Fact] + public async Task StartAndWait_WhenExceptionInPublishingFailedEvent_UpdatesFailedEventStream() + { + // Arrange + var @event = FakeEvent(); + var exception = FakeException(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorReadingFailedEventStream(@event); + var eventPublisher = FakeEventPublisherThrowingException(exception); + var fakeContext = new FakeContext(); + var boundedContexts = FakeBoundedContexts(); + var eventSerializers = FakeEventSerializers(); + var logger = FakeLogger(); + var settings = FakeSettings(); + consumer = new EventConsumer(commandProcessor, queryProcessor, eventPublisher, boundedContexts, eventSerializers, logger, settings); + // Act + consumer.Start(); + consumer.Wait(TimeSpan.FromSeconds(5)); + // Assert + await commandProcessor.InGeneric(fakeContext) + .Received(1) + .ProcessAsync(Arg.Is(c => c.EventId == @event.EventId && c.ExceptionMessage == exception.Message), Arg.Any()); + } + + [Fact] + public void StartAndWait_WhenExceptionInReadingEventStream_LogsException() + { + // Arrange + var @event = FakeEvent(); + var exception = FakeException(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorThrowingExceptionWhenReadingEventStream(exception); + var eventPublisher = FakeEventPublisher(); + var boundedContexts = FakeBoundedContexts(); + var eventSerializers = FakeEventSerializers(); + var logger = FakeLogger(); + var settings = FakeSettings(); + consumer = new EventConsumer(commandProcessor, queryProcessor, eventPublisher, boundedContexts, eventSerializers, logger, settings); + // Act + consumer.Start(); + consumer.Wait(TimeSpan.FromSeconds(5)); + // Assert + logger.Received(c => IsExpectedLogCall(c, LogLevel.Critical, exception)); + } + + [Fact] + public void StartAndWait_WhenExceptionInReadingFailedEventStream_LogsException() + { + // Arrange + var @event = FakeEvent(); + var exception = FakeException(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorThrowingExceptionWhenReadingFailedEventStream(exception); + var eventPublisher = FakeEventPublisher(); + var boundedContexts = FakeBoundedContexts(); + var eventSerializers = FakeEventSerializers(); + var logger = FakeLogger(); + var settings = FakeSettings(); + consumer = new EventConsumer(commandProcessor, queryProcessor, eventPublisher, boundedContexts, eventSerializers, logger, settings); + // Act + consumer.Start(); + consumer.Wait(TimeSpan.FromSeconds(5)); + // Assert + logger.Received(c => IsExpectedLogCall(c, LogLevel.Critical, exception)); + } + + [Fact] + public void StartAndWait_WhenExceptionInUpdatingFailedEventStream_LogsException() + { + // Arrange + var @event = FakeEvent(); + var exception = FakeException(); + var commandProcessor = FakeCommandProcessorThrowingExceptionWhenUpdatingFailedEventStream(exception); + var queryProcessor = FakeQueryProcessorReadingFailedEventStream(@event); + var eventPublisher = FakeEventPublisherThrowingException(FakeException()); + var boundedContexts = FakeBoundedContexts(); + var eventSerializers = FakeEventSerializers(); + var logger = FakeLogger(); + var settings = FakeSettings(); + consumer = new EventConsumer(commandProcessor, queryProcessor, eventPublisher, boundedContexts, eventSerializers, logger, settings); + // Act + consumer.Start(); + consumer.Wait(TimeSpan.FromSeconds(5)); + // Assert + logger.Received(c => IsExpectedLogCall(c, LogLevel.Critical, exception)); + } + + [Fact] + public async Task StartAndWait_WhenFailedEventStreamSuccessfullyProcessed_DeletesFailedEventStream() + { + // Arrange + var @event = FakeEvent(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorReadingFailedEventStream(@event); + var eventPublisher = FakeEventPublisher(); + var fakeContext = new FakeContext(); + var boundedContexts = FakeBoundedContexts(); + var eventSerializers = FakeEventSerializers(); + var logger = FakeLogger(); + var settings = FakeSettings(); + consumer = new EventConsumer(commandProcessor, queryProcessor, eventPublisher, boundedContexts, eventSerializers, logger, settings); + // Act + consumer.Start(); + consumer.Wait(TimeSpan.FromSeconds(5)); + // Assert + await commandProcessor.InGeneric(fakeContext).Received(1) + .ProcessAsync(Arg.Any(), Arg.Any()); + } + private static ICommandProcessor FakeCommandProcessor() + { + var contextualProcessor = Substitute.For>(); + var commandProcessor = Substitute.For(); + commandProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); + return commandProcessor; + } + + private static ICommandProcessor FakeCommandProcessorThrowingExceptionWhenExcludingFailedEventStream(Exception exception) + { + var contextualProcessor = Substitute.For>(); + contextualProcessor.When(p => p.ProcessAsync(Arg.Any(), Arg.Any())) + .Throw(exception); + var commandProcessor = Substitute.For(); + commandProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); + return commandProcessor; + } + + private static ICommandProcessor FakeCommandProcessorThrowingExceptionWhenIncludingFailedEventStream(Exception exception) + { + var contextualProcessor = Substitute.For>(); + contextualProcessor.When(p => p.ProcessAsync(Arg.Any(), Arg.Any())) + .Throw(exception); + var commandProcessor = Substitute.For(); + commandProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); + return commandProcessor; + } + + private static ICommandProcessor FakeCommandProcessorThrowingExceptionWhenUpdatingFailedEventStream(Exception exception) + { + var contextualProcessor = Substitute.For>(); + contextualProcessor.When(p => p.ProcessAsync(Arg.Any(), Arg.Any())) + .Throw(exception); + var commandProcessor = Substitute.For(); + commandProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); + return commandProcessor; + } + + private static Event FakeEvent() + { + return new Event + { + EventId = new Guid("36beb37d-1e01-bb7d-fb2a-3a0044745345"), + EventType = "DDD.Core.Domain.FakeEvent1, DDD.Core.UnitTests", + OccurredOn = new DateTime(2021, 11, 18, 10, 1, 23, 528), + Body = "{\"occurredOn\":\"2021-11-18T10:01:23.5277314+01:00\"}", + BodyFormat = "Json", + StreamId = "1", + StreamType = "FakeStream", + IssuedBy = "Dr Maboul" + }; + } + + private static IEventPublisher FakeEventPublisher() => Substitute.For>(); + + private static IEventPublisher FakeEventPublisherThrowingException(Exception exception) + { + var eventPublisher = Substitute.For>(); + eventPublisher.When(p => p.PublishAsync(Arg.Any(), Arg.Any())) + .Throw(exception); + return eventPublisher; + } + + private static IEnumerable FakeEventSerializers() + { + var jsonSerializer = Substitute.For(); + jsonSerializer.Encoding.Returns(JsonSerializationOptions.Encoding); + jsonSerializer.Deserialize(Arg.Any(), Arg.Any()).Returns(Substitute.For()); + yield return jsonSerializer; + } + + private static IEnumerable FakeEventSerializersThrowingException(Exception exception) + { + var jsonSerializer = Substitute.For(); + jsonSerializer.Encoding.Returns(JsonSerializationOptions.Encoding); + jsonSerializer.When(s => s.Deserialize(Arg.Any(), Arg.Any())).Throw(exception); + yield return jsonSerializer; + } + + private static Exception FakeException() => new Exception("Fake exception."); + + private static ILogger FakeLogger() => Substitute.For(); + + private static IQueryProcessor FakeQueryProcessorReadingEventStream(params Event[] events) + { + var contextualProcessorForFakeContext = Substitute.For>(); + contextualProcessorForFakeContext.ProcessAsync(Arg.Any(), Arg.Any()) + .Returns(new[] { new EventStream { Source = "FKS" } }); + var contextualProcessorForFakeSourceContext = Substitute.For>(); + contextualProcessorForFakeSourceContext.ProcessAsync(Arg.Any(), Arg.Any()) + .Returns(events); + var queryProcessor = Substitute.For(); + queryProcessor.InGeneric(Arg.Any()).Returns(contextualProcessorForFakeContext); + queryProcessor.InSpecific(Arg.Any()).Returns(contextualProcessorForFakeSourceContext); + return queryProcessor; + } + + private static IQueryProcessor FakeQueryProcessorReadingFailedEventStream(params Event[] events) + { + var contextualProcessorForFakeContext = Substitute.For>(); + contextualProcessorForFakeContext.ProcessAsync(Arg.Any(), Arg.Any()) + .Returns(new[] { new EventStream { Source = "FKS" } }); + contextualProcessorForFakeContext.ProcessAsync(Arg.Any(), Arg.Any()) + .Returns(new[] { new FailedEventStream { StreamSource = "FKS", RetryMax = 5, RetryDelays = new[] { new IncrementalDelay { Delay = 10 } } } }); + var contextualProcessorForFakeSourceContext = Substitute.For>(); + contextualProcessorForFakeSourceContext.ProcessAsync(Arg.Any(), Arg.Any()) + .Returns(events); + var queryProcessor = Substitute.For(); + queryProcessor.InGeneric(Arg.Any()).Returns(contextualProcessorForFakeContext); + queryProcessor.InSpecific(Arg.Any()).Returns(contextualProcessorForFakeSourceContext); + return queryProcessor; + } + + private static IQueryProcessor FakeQueryProcessorThrowingExceptionWhenFindingEventStreams(Exception exception) + { + var contextualProcessor = Substitute.For>(); + contextualProcessor.When(p => p.ProcessAsync(Arg.Any(), Arg.Any())) + .Throw(exception); + var queryProcessor = Substitute.For(); + queryProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); + return queryProcessor; + } + + private static IQueryProcessor FakeQueryProcessorThrowingExceptionWhenFindingFailedEventStreams(Exception exception) + { + var contextualProcessor = Substitute.For>(); + contextualProcessor.When(p => p.ProcessAsync(Arg.Any(), Arg.Any())) + .Throw(exception); + var queryProcessor = Substitute.For(); + queryProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); + return queryProcessor; + } + + private static IQueryProcessor FakeQueryProcessorThrowingExceptionWhenReadingEventStream(Exception exception) + { + var contextualProcessorForFakeContext = Substitute.For>(); + contextualProcessorForFakeContext.ProcessAsync(Arg.Any(), Arg.Any()) + .Returns(new[] { new EventStream { Source = "FKS" } }); + var contextualProcessorForFakeSourceContext = Substitute.For>(); + contextualProcessorForFakeSourceContext.When(p => p.ProcessAsync(Arg.Any(), Arg.Any())) + .Throw(exception); + var queryProcessor = Substitute.For(); + queryProcessor.InGeneric(Arg.Any()).Returns(contextualProcessorForFakeContext); + queryProcessor.InSpecific(Arg.Any()).Returns(contextualProcessorForFakeSourceContext); + return queryProcessor; + } + + private static IQueryProcessor FakeQueryProcessorThrowingExceptionWhenReadingFailedEventStream(Exception exception) + { + var contextualProcessorForFakeContext = Substitute.For>(); + contextualProcessorForFakeContext.ProcessAsync(Arg.Any(), Arg.Any()) + .Returns(new[] { new EventStream { Source = "FKS" } }); + contextualProcessorForFakeContext.ProcessAsync(Arg.Any(), Arg.Any()) + .Returns(new[] { new FailedEventStream { StreamSource = "FKS", RetryMax = 5, RetryDelays = new[] { new IncrementalDelay { Delay = 10 } } } }); + var contextualProcessorForFakeSourceContext = Substitute.For>(); + contextualProcessorForFakeSourceContext.When(p => p.ProcessAsync(Arg.Any(), Arg.Any())) + .Throw(exception); + var queryProcessor = Substitute.For(); + queryProcessor.InGeneric(Arg.Any()).Returns(contextualProcessorForFakeContext); + queryProcessor.InSpecific(Arg.Any()).Returns(contextualProcessorForFakeSourceContext); + return queryProcessor; + } + + private static EventConsumerSettings FakeSettings() => new EventConsumerSettings(TimeSpan.FromSeconds(1), 1); + + private static IEnumerable FakeBoundedContexts() + { + yield return new FakeContext(); + yield return new FakeSourceContext(); + } + + private static bool IsExpectedLogCall(ICall call, LogLevel level, Exception exception) + { + if (call.GetMethodInfo().Name != "Log") return false; + var args = call.GetArguments(); + if ((LogLevel)args[0] != level) return false; + if (args[3] != exception) return false; + return true; + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.UnitTests/Application/EventPublisherTests.cs b/Test/DDD.Core.UnitTests/Application/EventPublisherTests.cs new file mode 100644 index 0000000..b793947 --- /dev/null +++ b/Test/DDD.Core.UnitTests/Application/EventPublisherTests.cs @@ -0,0 +1,124 @@ +using NSubstitute; +using System.Collections.Generic; +using Xunit; +using SimpleInjector; +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + using Domain; + using Collections; + + public class EventPublisherTests + { + + #region Methods + + public static IEnumerable HandlersOfOtherEventsOrContexts() + { + var fakeHandler1 = FakeHandler(); + var fakeHandler2 = FakeHandler(); + var fakeHandler3 = FakeHandler(); + var fakeHandler4 = FakeHandler(); + var container = new Container(); + container.RegisterInstance(fakeHandler1); + container.RegisterInstance(fakeHandler2); + container.RegisterInstance(fakeHandler3); + var publisher = new EventPublisher(new FakeContext(), container); + yield return new object[] + { + publisher, + new FakeEvent1(), + new IAsyncEventHandler[] { fakeHandler2, fakeHandler3, fakeHandler4 } + }; + yield return new object[] + { + publisher, + new FakeEvent2(), + new IAsyncEventHandler[] { fakeHandler1, fakeHandler3, fakeHandler4 } + }; + yield return new object[] + { + publisher, + new FakeEvent3(), + new IAsyncEventHandler[] { fakeHandler1, fakeHandler2, fakeHandler4 } + }; + } + + public static IEnumerable HandlerOfThisEventAndThisContext() + { + var fakeHandler1 = FakeHandler(); + var fakeHandler2 = FakeHandler(); + var fakeHandler3 = FakeHandler(); + var container = new Container(); + container.RegisterInstance(fakeHandler1); + container.RegisterInstance(fakeHandler2); + container.RegisterInstance(fakeHandler3); + var publisher = new EventPublisher(new FakeContext(), container); + yield return new object[] + { + publisher, + new FakeEvent1(), + fakeHandler1 + }; + yield return new object[] + { + publisher, + new FakeEvent2(), + fakeHandler2 + }; + yield return new object[] + { + publisher, + new FakeEvent3(), + fakeHandler3 + }; + } + + + [Theory] + [MemberData(nameof(HandlerOfThisEventAndThisContext))] + public async Task PublishAsync_WhenCalled_CallsHandlerOfThisEventAndThisContext(EventPublisher publisher, + IEvent @event, + IAsyncEventHandler handlerOfThisEvent) + { + // Arrange + var context = new MessageContext(); + handlerOfThisEvent.ClearReceivedCalls(); + // Act + await publisher.PublishAsync(@event, context); + // Assert + await handlerOfThisEvent.Received(1).HandleAsync(@event, context); + } + + + [Theory] + [MemberData(nameof(HandlersOfOtherEventsOrContexts))] + public async Task PublishAsync_WhenCalled_DoesNotCallHandlersOfOtherEventsOrContexts(EventPublisher publisher, + IEvent @event, + IEnumerable handlersOfOtherEvents) + { + // Arrange + var context = new MessageContext(); + handlersOfOtherEvents.ForEach(s => s.ClearReceivedCalls()); + // Act + await publisher.PublishAsync(@event, context); + // Assert + Assert.All(handlersOfOtherEvents, s => s.DidNotReceive().HandleAsync(Arg.Any(), context)); + } + + private static IAsyncEventHandler FakeHandler() + where TEvent : class, IEvent + where TContext : BoundedContext, new() + { + var fakeHandler = Substitute.For>(); + fakeHandler.EventType.Returns(typeof(TEvent)); + fakeHandler.Context.Returns(new TContext()); + return fakeHandler; + } + + #endregion Methods + + } + +} \ No newline at end of file diff --git a/Test/DDD.Core.UnitTests/Application/QueryProcessorTests.cs b/Test/DDD.Core.UnitTests/Application/QueryProcessorTests.cs index d0eaad6..268a091 100644 --- a/Test/DDD.Core.UnitTests/Application/QueryProcessorTests.cs +++ b/Test/DDD.Core.UnitTests/Application/QueryProcessorTests.cs @@ -1,6 +1,8 @@ using NSubstitute; using System; +using System.Threading.Tasks; using Xunit; +using DDD.Validation; namespace DDD.Core.Application { @@ -8,10 +10,14 @@ public class QueryProcessorTests { #region Fields - private readonly IQueryHandler handlerOfQuery1; - private readonly IQueryHandler handlerOfQuery2; - private readonly IQueryValidator validatorOfQuery1; - private readonly IQueryValidator validatorOfQuery2; + private readonly ISyncQueryHandler syncHandlerOfQuery1; + private readonly ISyncQueryHandler syncHandlerOfQuery2; + private readonly ISyncObjectValidator syncValidatorOfQuery1; + private readonly ISyncObjectValidator syncValidatorOfQuery2; + private readonly IAsyncQueryHandler asyncHandlerOfQuery1; + private readonly IAsyncQueryHandler asyncHandlerOfQuery2; + private readonly IAsyncObjectValidator asyncValidatorOfQuery1; + private readonly IAsyncObjectValidator asyncValidatorOfQuery2; private readonly QueryProcessor processor; #endregion Fields @@ -20,20 +26,32 @@ public class QueryProcessorTests public QueryProcessorTests() { - this.handlerOfQuery1 = Substitute.For>(); - this.handlerOfQuery2 = Substitute.For>(); - this.validatorOfQuery1 = Substitute.For>(); - this.validatorOfQuery2 = Substitute.For>(); + this.syncHandlerOfQuery1 = Substitute.For>(); + this.syncHandlerOfQuery2 = Substitute.For>(); + this.syncValidatorOfQuery1 = Substitute.For>(); + this.syncValidatorOfQuery2 = Substitute.For>(); + this.asyncHandlerOfQuery1 = Substitute.For>(); + this.asyncHandlerOfQuery2 = Substitute.For>(); + this.asyncValidatorOfQuery1 = Substitute.For>(); + this.asyncValidatorOfQuery2 = Substitute.For>(); var serviceProvider = Substitute.For(); - serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(IQueryHandler)))) - .Returns(this.handlerOfQuery1); - serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(IQueryHandler)))) - .Returns(this.handlerOfQuery2); - serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(IQueryValidator)))) - .Returns(this.validatorOfQuery1); - serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(IQueryValidator)))) - .Returns(this.validatorOfQuery2); - processor = new QueryProcessor(serviceProvider); + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncQueryHandler)))) + .Returns(this.syncHandlerOfQuery1); + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncQueryHandler)))) + .Returns(this.syncHandlerOfQuery2); + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncObjectValidator)))) + .Returns(this.syncValidatorOfQuery1); + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncObjectValidator)))) + .Returns(this.syncValidatorOfQuery2); + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(IAsyncQueryHandler)))) + .Returns(this.asyncHandlerOfQuery1); + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(IAsyncQueryHandler)))) + .Returns(this.asyncHandlerOfQuery2); + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(IAsyncObjectValidator)))) + .Returns(this.asyncValidatorOfQuery1); + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(IAsyncObjectValidator)))) + .Returns(this.asyncValidatorOfQuery2); + processor = new QueryProcessor(serviceProvider, new QueryProcessorSettings()); } #endregion Constructors @@ -41,25 +59,99 @@ public QueryProcessorTests() #region Methods [Fact] - public void Process_WhenQueryDefined_CallsExpectedHandler() + public void Process_WhenConcreteQuery_CallsExpectedHandler() { // Arrange var query = new FakeQuery1(); + var context = new MessageContext(); // Act - this.processor.Process(query); + this.processor.Process(query, context); // Assert - this.handlerOfQuery1.Received(1).Handle(query); + this.syncHandlerOfQuery1.Received(1).Handle(query, context); } [Fact] - public void Validate_WhenQueryDefined_CallsExpectedValidator() + public async Task ProcessAsync_WhenConcreteQuery_CallsExpectedHandler() { // Arrange var query = new FakeQuery1(); + var context = new MessageContext(); // Act - this.processor.Validate(query); + await this.processor.ProcessAsync(query, context); // Assert - this.validatorOfQuery1.Received(1).Validate(query); + await this.asyncHandlerOfQuery1.Received(1).HandleAsync(query, context); + } + + [Fact] + public void Process_WhenAbstractQuery_CallsExpectedHandler() + { + // Arrange + var query = new FakeQuery1(); + var context = new MessageContext(); + // Act + this.processor.Process((IQuery)query, context); + // Assert + this.syncHandlerOfQuery1.Received(1).Handle(query, context); + } + + [Fact] + public async Task ProcessAsync_WhenAbstractQuery_CallsExpectedHandler() + { + // Arrange + var query = new FakeQuery1(); + var context = new MessageContext(); + // Act + await this.processor.ProcessAsync((IQuery)query, context); + // Assert + await this.asyncHandlerOfQuery1.Received(1).HandleAsync(query, context); + } + + [Fact] + public void Validate_WhenConcreteQuery_CallsExpectedValidator() + { + // Arrange + var query = new FakeQuery1(); + var context = new ValidationContext(); + // Act + this.processor.Validate(query, context); + // Assert + this.syncValidatorOfQuery1.Received(1).Validate(query, context); + } + + [Fact] + public async Task ValidateAsync_WhenConcreteQuery_CallsExpectedValidator() + { + // Arrange + var query = new FakeQuery1(); + var context = new ValidationContext(); + // Act + await this.processor.ValidateAsync(query, context); + // Assert + await this.asyncValidatorOfQuery1.Received(1).ValidateAsync(query, context); + } + + [Fact] + public void Validate_WhenAbstractQuery_CallsExpectedValidator() + { + // Arrange + var query = new FakeQuery1(); + var context = new ValidationContext(); + // Act + this.processor.Validate((IQuery)query, context); + // Assert + this.syncValidatorOfQuery1.Received(1).Validate(query, context); + } + + [Fact] + public async Task ValidateAsync_WhenAbstractQuery_CallsExpectedValidator() + { + // Arrange + var query = new FakeQuery1(); + var context = new ValidationContext(); + // Act + await this.processor.ValidateAsync((IQuery)query, context); + // Assert + await this.asyncValidatorOfQuery1.Received(1).ValidateAsync(query, context); } #endregion Methods diff --git a/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs b/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs new file mode 100644 index 0000000..0221170 --- /dev/null +++ b/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs @@ -0,0 +1,570 @@ +using Microsoft.Extensions.Logging; +using FluentAssertions; +using NSubstitute; +using NSubstitute.Core; +using System.Threading.Tasks; +using System; +using System.IO; +using System.Collections.Generic; +using Xunit; + +namespace DDD.Core.Application +{ + using Serialization; + using Infrastructure.Testing; + using Domain; + + public class RecurringCommandManagerTests : IDisposable + { + + #region Fields + + private RecurringCommandManager manager; + + #endregion Fields + + #region Methods + + public void Dispose() + { + this.manager?.Dispose(); + } + + [Fact] + public async Task RegisterAsync_WhenRecurringExpressionIsInvalid_ThrowsArgumentException() + { + // Arrange + var command = FakeCommand1(); + var recurringExpression = InvalidRecurringExpression(); + var recurringCommand = FakeCommand1ForSingleExecution(); + var context = new FakeContext(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactories = FakeRecurringScheduleFactories(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactories, logger, settings); + // Act + Func registerAsync = async () => await manager.RegisterAsync(command, recurringExpression); + // Assert + await registerAsync.Should().ThrowAsync(); + } + + [Fact] + public void Start_WhenIsNotRunning_SetsIsRunningToTrue() + { + // Arrange + var recurringCommand = FakeCommand1ForRecurringExecution(); + var context = new FakeContext(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + // Act + manager.Start(); + // Assert + manager.IsRunning.Should().BeTrue(); + } + + [Fact] + public void StartAndWait_WhenExceptionInDeserializingRecurringCommand_LogsException() + { + // Arrange + var exception = FakeException(); + var recurringCommand = FakeCommand1ForSingleExecution(); + var context = new FakeContext(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); + var commandSerializers = FakeCommandSerializersThrowingException(exception); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + // Act + manager.Start(); + manager.Wait(TimeSpan.FromSeconds(5)); + // Assert + logger.Received(c => IsExpectedLogCall(c, LogLevel.Error, exception)); + } + + [Fact] + public void StartAndWait_WhenExceptionInFindingRecurringCommands_LogsException() + { + // Arrange + var exception = FakeException(); + var context = new FakeContext(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorThrowingExceptionWhenFindingRecurringCommands(exception); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + // Act + manager.Start(); + manager.Wait(TimeSpan.FromSeconds(5)); + // Assert + logger.Received(c => IsExpectedLogCall(c, LogLevel.Critical, exception)); + } + + [Fact] + public void StartAndWait_WhenExceptionInFindingRecurringCommands_SetsIsRunningToFalse() + { + // Arrange + var exception = FakeException(); + var context = new FakeContext(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorThrowingExceptionWhenFindingRecurringCommands(exception); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + // Act + manager.Start(); + manager.Wait(TimeSpan.FromSeconds(5)); + // Assert + manager.IsRunning.Should().BeFalse(); + } + + [Fact] + public async Task StartAndWait_WhenExceptionInMarkingRecurringCommandAsFailed_DoesNotRetryRecurringCommandOnNextOccurrence() + { + // Arrange + var exception = FakeException(); + var recurringCommand = FakeCommand1ForRecurringExecution(); + var context = new FakeContext(); + var commandProcessor = FakeCommandProcessorThrowingExceptionWhenMarkingRecurringCommandAsFailed(exception); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + // Act + manager.Start(); + manager.Wait(TimeSpan.FromSeconds(5)); + // Assert + await commandProcessor.Received(1) + .ProcessAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public void StartAndWait_WhenExceptionInMarkingRecurringCommandAsFailed_LogsException() + { + // Arrange + var exception = FakeException(); + var recurringCommand = FakeCommand1ForSingleExecution(); + var context = new FakeContext(); + var commandProcessor = FakeCommandProcessorThrowingExceptionWhenMarkingRecurringCommandAsFailed(exception); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + // Act + manager.Start(); + manager.Wait(TimeSpan.FromSeconds(5)); + // Assert + logger.Received(c => IsExpectedLogCall(c, LogLevel.Error, exception)); + } + + [Fact] + public async Task StartAndWait_WhenExceptionInMarkingRecurringCommandAsSuccessful_DoesNotRetryRecurringCommandOnNextOccurrence() + { + // Arrange + var exception = FakeException(); + var recurringCommand = FakeCommand1ForRecurringExecution(); + var context = new FakeContext(); + var commandProcessor = FakeCommandProcessorThrowingExceptionWhenMarkingRecurringCommandAsSuccessful(exception); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + // Act + manager.Start(); + manager.Wait(TimeSpan.FromSeconds(5)); + // Assert + await commandProcessor.Received(1) + .ProcessAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public void StartAndWait_WhenExceptionInMarkingRecurringCommandAsSuccessful_LogsException() + { + // Arrange + var exception = FakeException(); + var recurringCommand = FakeCommand1ForSingleExecution(); + var context = new FakeContext(); + var commandProcessor = FakeCommandProcessorThrowingExceptionWhenMarkingRecurringCommandAsSuccessful(exception); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + // Act + manager.Start(); + manager.Wait(TimeSpan.FromSeconds(5)); + // Assert + logger.Received(c => IsExpectedLogCall(c, LogLevel.Error, exception)); + } + [Fact] + public void StartAndWait_WhenExceptionInProcessingRecurringCommand_LogsException() + { + // Arrange + var exception = FakeException(); + var recurringCommand = FakeCommand1ForSingleExecution(); + var context = new FakeContext(); + var commandProcessor = FakeCommandProcessorThrowingExceptionWhenProcessingRecurringCommand(exception); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + // Act + manager.Start(); + manager.Wait(TimeSpan.FromSeconds(5)); + // Assert + logger.Received(c => IsExpectedLogCall(c, LogLevel.Error, exception)); + } + [Fact] + public async Task StartAndWait_WhenExceptionInProcessingRecurringCommand_MarksRecurringCommandAsFailed() + { + // Arrange + var exception = FakeException(); + var recurringCommand = FakeCommand1ForSingleExecution(); + var context = new FakeContext(); + var commandProcessor = FakeCommandProcessorThrowingExceptionWhenProcessingRecurringCommand(exception); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + // Act + manager.Start(); + manager.Wait(TimeSpan.FromSeconds(5)); + // Assert + await commandProcessor.InGeneric(context) + .Received(1) + .ProcessAsync(Arg.Is(c => c.CommandId == recurringCommand.CommandId && c.ExceptionInfo == exception.ToString()), Arg.Any()); + } + + [Fact] + public async Task StartAndWait_WhenExceptionInProcessingRecurringCommand_RetriesRecurringCommandOnNextOccurrence() + { + // Arrange + var exception = FakeException(); + var recurringCommand = FakeCommand1ForDoubleExecution(); + var context = new FakeContext(); + var commandProcessor = FakeCommandProcessorThrowingExceptionWhenProcessingRecurringCommand(exception); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + // Act + manager.Start(); + manager.Wait(TimeSpan.FromSeconds(5)); + // Assert + await commandProcessor.Received(2) + .ProcessAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task StartAndWait_WhenExceptionInProcessingSpecificRecurringCommand_ContinuesToProcessOtherCommands() + { + // Arrange + var exception = FakeException(); + var recurringCommand1 = FakeCommand1ForDoubleExecution(); + var recurringCommand2 = FakeCommand2ForDoubleExecution(); + var context = new FakeContext(); + var commandProcessor = FakeCommandProcessorThrowingExceptionWhenProcessingFakeCommand1(exception); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand1, recurringCommand2); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + // Act + manager.Start(); + manager.Wait(TimeSpan.FromSeconds(5)); + // Assert + await commandProcessor.Received(2) + .ProcessAsync(Arg.Is(c => c is FakeCommand2), Arg.Any()); + } + + + [Fact] + public async Task StartAndWait_WhenRecurringCommandSuccessfullyProcessed_MarksRecurringCommandAsSuccessful() + { + // Arrange + var recurringCommand = FakeCommand1ForSingleExecution(); + var context = new FakeContext(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + // Act + manager.Start(); + manager.Wait(TimeSpan.FromSeconds(5)); + // Assert + await commandProcessor.InGeneric(context) + .Received(1) + .ProcessAsync(Arg.Is(c => c.CommandId == recurringCommand.CommandId), Arg.Any()); + } + + [Fact] + public void Stop_WhenIsRunning_SetsIsRunningToFalse() + { + // Arrange + var recurringCommand = FakeCommand1ForRecurringExecution(); + var context = new FakeContext(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand);; + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + manager.Start(); + // Act + manager.Stop(); + // Assert + manager.IsRunning.Should().BeFalse(); + } + + private static string RecurringExpressionForDoubleExecution() => "0,1 0 0 1 1"; + + private static string RecurringExpressionForRecurringExecution() => "* * * * *"; + + private static string RecurringExpressionForSingleExecution() => "0 0 0 1 1"; + + private static ICommand FakeCommand1() => new FakeCommand1(); + + private static RecurringCommand FakeCommand1ForDoubleExecution() + { + return new RecurringCommand + { + CommandId = new Guid("36beb37d-1e01-bb7d-fb2a-3a0044745345"), + CommandType = "DDD.Core.Application.FakeCommand1, DDD.Core.UnitTests", + Body = "{\"Property1\":\"dummy\",\"Property2\":10}", + BodyFormat = "JSON", + RecurringExpression = RecurringExpressionForDoubleExecution(), + RecurringExpressionFormat = "CRON" + }; + } + + private static RecurringCommand FakeCommand1ForRecurringExecution() + { + return new RecurringCommand + { + CommandId = new Guid("36beb37d-1e01-bb7d-fb2a-3a0044745345"), + CommandType = "DDD.Core.Application.FakeCommand1, DDD.Core.UnitTests", + Body = "{\"Property1\":\"dummy\",\"Property2\":10}", + BodyFormat = "JSON", + RecurringExpression = RecurringExpressionForRecurringExecution(), + RecurringExpressionFormat = "CRON" + }; + } + + private static RecurringCommand FakeCommand1ForSingleExecution() + { + return new RecurringCommand + { + CommandId = new Guid("36beb37d-1e01-bb7d-fb2a-3a0044745345"), + CommandType = "DDD.Core.Application.FakeCommand1, DDD.Core.UnitTests", + Body = "{\"Property1\":\"dummy\",\"Property2\":10}", + BodyFormat = "JSON", + RecurringExpression = RecurringExpressionForSingleExecution(), + RecurringExpressionFormat = "CRON" + }; + } + + private static ICommand FakeCommand2() => new FakeCommand2(); + + private static RecurringCommand FakeCommand2ForDoubleExecution() + { + return new RecurringCommand + { + CommandId = new Guid("36beb37d-1e01-bb7d-fb2a-3a0044745345"), + CommandType = "DDD.Core.Application.FakeCommand2, DDD.Core.UnitTests", + Body = "{\"Property1\":\"dummy\",\"Property2\":10}", + BodyFormat = "JSON", + RecurringExpression = RecurringExpressionForDoubleExecution(), + RecurringExpressionFormat = "CRON" + }; + } + private static Guid FakeCommandId() => new Guid("f7df5bd0-8763-677e-7e6b-3a0044746810"); + + private static IEnumerable FakeCommandSerializers() + { + var jsonSerializer = Substitute.For(); + jsonSerializer.Encoding.Returns(JsonSerializationOptions.Encoding); + jsonSerializer.Deserialize(Arg.Any(), Arg.Is(typeof(FakeCommand1))).Returns(FakeCommand1()); + jsonSerializer.Deserialize(Arg.Any(), Arg.Is(typeof(FakeCommand2))).Returns(FakeCommand2()); + yield return jsonSerializer; + } + + private static IEnumerable FakeCommandSerializersThrowingException(Exception exception) + { + var jsonSerializer = Substitute.For(); + jsonSerializer.Encoding.Returns(JsonSerializationOptions.Encoding); + jsonSerializer.When(s => s.Deserialize(Arg.Any(), Arg.Any())).Throw(exception); + yield return jsonSerializer; + } + + private static ICommandProcessor FakeCommandProcessor() + { + var contextualProcessor = Substitute.For>(); + var commandProcessor = Substitute.For(); + commandProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); + return commandProcessor; + } + + private static ICommandProcessor FakeCommandProcessorThrowingExceptionWhenMarkingRecurringCommandAsFailed(Exception exception) + { + var contextualProcessor = Substitute.For>(); + contextualProcessor.When(p => p.ProcessAsync(Arg.Any(), Arg.Any())) + .Throw(exception); + var commandProcessor = Substitute.For(); + commandProcessor.When(p => p.ProcessAsync(Arg.Any(), Arg.Any())) + .Throw(FakeException()); + commandProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); + return commandProcessor; + } + + private static ICommandProcessor FakeCommandProcessorThrowingExceptionWhenMarkingRecurringCommandAsSuccessful(Exception exception) + { + var contextualProcessor = Substitute.For>(); + contextualProcessor.When(p => p.ProcessAsync(Arg.Any(), Arg.Any())) + .Throw(exception); + var commandProcessor = Substitute.For(); + commandProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); + return commandProcessor; + } + + private static ICommandProcessor FakeCommandProcessorThrowingExceptionWhenProcessingRecurringCommand(Exception exception) + { + var contextualProcessor = Substitute.For>(); + var commandProcessor = Substitute.For(); + commandProcessor.When(p => p.ProcessAsync(Arg.Any(), Arg.Any())) + .Throw(exception); + commandProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); + return commandProcessor; + } + + private static ICommandProcessor FakeCommandProcessorThrowingExceptionWhenProcessingFakeCommand1(Exception exception) + { + var contextualProcessor = Substitute.For>(); + var commandProcessor = Substitute.For(); + commandProcessor.When(p => p.ProcessAsync(Arg.Any(), Arg.Any())) + .Throw(exception); + commandProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); + return commandProcessor; + } + + private static IQueryProcessor FakeQueryProcessorFindingRecurringCommands(params RecurringCommand[] recurringCommands) + { + var contextualProcessor = Substitute.For>(); + contextualProcessor.ProcessAsync(Arg.Any(), Arg.Any()) + .Returns(recurringCommands); + contextualProcessor.ProcessAsync(Arg.Any(), Arg.Any()) + .Returns(FakeCommandId()); + var queryProcessor = Substitute.For(); + queryProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); + return queryProcessor; + } + + private static IQueryProcessor FakeQueryProcessorThrowingExceptionWhenFindingRecurringCommands(Exception exception) + { + var contextualProcessor = Substitute.For>(); + contextualProcessor.When(p => p.ProcessAsync(Arg.Any(), Arg.Any())) + .Throw(exception); + contextualProcessor.ProcessAsync(Arg.Any(), Arg.Any()) + .Returns(FakeCommandId()); + var queryProcessor = Substitute.For(); + queryProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); + return queryProcessor; + } + + private static IEnumerable FakeRecurringScheduleFactories() + { + var recurringScheduleForSingleExecution1 = FakeRecurringScheduleForSingleExecution(); + var recurringScheduleForDoubleExecution1 = FakeRecurringScheduleForDoubleExecution(); + var recurringScheduleForRecurringExecution1 = FakeRecurringScheduleForRecurringExecution(); + var recurringScheduleForSingleExecution2 = FakeRecurringScheduleForSingleExecution(); + var recurringScheduleForDoubleExecution2 = FakeRecurringScheduleForDoubleExecution(); + var recurringScheduleForRecurringExecution2 = FakeRecurringScheduleForRecurringExecution(); + var recurringSchedulefactory = Substitute.For(); + recurringSchedulefactory.Format.Returns(RecurringExpressionFormat.Cron); + recurringSchedulefactory.Create(RecurringExpressionForSingleExecution()) + .Returns(recurringScheduleForSingleExecution1, recurringScheduleForSingleExecution2); + recurringSchedulefactory.Create(RecurringExpressionForDoubleExecution()) + .Returns(recurringScheduleForDoubleExecution1, recurringScheduleForDoubleExecution2); + recurringSchedulefactory.Create(RecurringExpressionForRecurringExecution()) + .Returns(recurringScheduleForRecurringExecution1, recurringScheduleForRecurringExecution2); + recurringSchedulefactory.When(f => f.Create(InvalidRecurringExpression())) + .Throw(); + yield return recurringSchedulefactory; + } + + private static IRecurringSchedule FakeRecurringScheduleForDoubleExecution() + { + var recurringSchedule = Substitute.For(); + recurringSchedule.GetNextOccurrence(Arg.Any()) + .Returns(x => ((DateTime)x[0]).AddSeconds(1), x => ((DateTime)x[0]).AddSeconds(1), x => null); + return recurringSchedule; + } + + private static IRecurringSchedule FakeRecurringScheduleForRecurringExecution() + { + var recurringSchedule = Substitute.For(); + recurringSchedule.GetNextOccurrence(Arg.Any()) + .Returns(x => ((DateTime)x[0]).AddSeconds(1)); + return recurringSchedule; + } + + private static IRecurringSchedule FakeRecurringScheduleForSingleExecution() + { + var recurringSchedule = Substitute.For(); + recurringSchedule.GetNextOccurrence(Arg.Any()) + .Returns(x => ((DateTime)x[0]).AddSeconds(1), x => null); + return recurringSchedule; + } + + private static Exception FakeException() => new Exception("Fake exception."); + + private static ILogger FakeLogger() => Substitute.For(); + + private static RecurringCommandManagerSettings FakeSettings() + => new RecurringCommandManagerSettings(SerializationFormat.Json, RecurringExpressionFormat.Cron); + + private static string InvalidRecurringExpression() => "* * * * * * * *"; + + private static bool IsExpectedLogCall(ICall call, LogLevel level, Exception exception) + { + if (call.GetMethodInfo().Name != "Log") return false; + var args = call.GetArguments(); + if ((LogLevel)args[0] != level) return false; + if (args[3] != exception) return false; + return true; + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.UnitTests/DDD.Core.UnitTests.csproj b/Test/DDD.Core.UnitTests/DDD.Core.UnitTests.csproj index 7b4bcb6..56d9bee 100644 --- a/Test/DDD.Core.UnitTests/DDD.Core.UnitTests.csproj +++ b/Test/DDD.Core.UnitTests/DDD.Core.UnitTests.csproj @@ -1,158 +1,38 @@ - - - - - + - Debug - AnyCPU - {63505329-BD97-4C75-B7D7-77FB42670E79} + net48;net6.0 Library - Properties DDD.Core - DDD.Core.UnitTests - v4.7.2 - 512 - - - + false - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - L:\packages\Castle.Core.4.3.1\lib\net45\Castle.Core.dll - True - - - L:\Packages\FluentAssertions.5.6.0\lib\net47\FluentAssertions.dll - - - L:\Packages\NSubstitute.4.0.0\lib\net46\NSubstitute.dll - - - L:\packages\Oracle.ManagedDataAccess.18.3.0\lib\net40\Oracle.ManagedDataAccess.dll - True - - - - - - L:\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll - - - L:\Packages\System.Threading.Tasks.Extensions.4.5.2\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll - - - L:\packages\System.ValueTuple.4.5.0\lib\netstandard1.0\System.ValueTuple.dll - True - - - - - - - - - L:\packages\xunit.abstractions.2.0.3\lib\net35\xunit.abstractions.dll - True - - - L:\packages\xunit.assert.2.4.1\lib\netstandard1.1\xunit.assert.dll - - - L:\packages\xunit.extensibility.core.2.4.1\lib\net452\xunit.core.dll - - - L:\packages\xunit.extensibility.execution.2.4.1\lib\net452\xunit.execution.desktop.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - {596a8700-3d18-4a62-b200-1f78a9ea4617} - DDD.Core.Abstractions - - - {2438b31a-3a39-4878-81fa-be5ae715eae5} - DDD.Core.Messages - - - {c6c3e419-b9aa-44ad-9dbf-789294687ae6} - DDD.Core - + + 6.9.0 + + + + + + + 4.5.4 + + + 4.5.0 + + + + 2.4.5 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - - \ No newline at end of file diff --git a/Test/DDD.Core.UnitTests/Domain/EventPublisherTests.cs b/Test/DDD.Core.UnitTests/Domain/EventPublisherTests.cs deleted file mode 100644 index 20ee298..0000000 --- a/Test/DDD.Core.UnitTests/Domain/EventPublisherTests.cs +++ /dev/null @@ -1,161 +0,0 @@ -using NSubstitute; -using System.Collections.Generic; -using Xunit; - -namespace DDD.Core.Domain -{ - using Collections; - - public class EventPublisherTests - { - #region Methods - - public static IEnumerable SubscribersToOtherEventsThanThisEvent() - { - var fakeSubscriber1 = FakeSubscriber(); - var fakeSubscriber2 = FakeSubscriber(); - var fakeSubscriber3 = FakeSubscriber(); - var fakeSubscriber4 = FakeSubscriber(); - var publisher = new EventPublisher(); - publisher.Subscribe(fakeSubscriber1); - publisher.Subscribe(fakeSubscriber2); - publisher.Subscribe(fakeSubscriber3); - publisher.Subscribe(fakeSubscriber4); - yield return new object[] - { - publisher, - new FakeEvent1(), - new IEventHandler[] { fakeSubscriber2, fakeSubscriber3, fakeSubscriber4 } - }; - yield return new object[] - { - publisher, - new FakeEvent2(), - new IEventHandler[] { fakeSubscriber3, fakeSubscriber4 } - }; - yield return new object[] - { - publisher, - new FakeEvent3(), - new IEventHandler[] { fakeSubscriber1, fakeSubscriber2 } - }; - } - - public static IEnumerable SubscribersToThisEvent() - { - var fakeSubscriber1 = FakeSubscriber(); - var fakeSubscriber2 = FakeSubscriber(); - var fakeSubscriber3 = FakeSubscriber(); - var fakeSubscriber4 = FakeSubscriber(); - var publisher = new EventPublisher(); - publisher.Subscribe(fakeSubscriber1); - publisher.Subscribe(fakeSubscriber2); - publisher.Subscribe(fakeSubscriber3); - publisher.Subscribe(fakeSubscriber4); - yield return new object[] - { - publisher, - new FakeEvent1(), - new IEventHandler[] { fakeSubscriber1 } - }; - yield return new object[] - { - publisher, - new FakeEvent2(), - new IEventHandler[] { fakeSubscriber1, fakeSubscriber2 } - }; - yield return new object[] - { - publisher, - new FakeEvent3(), - new IEventHandler[] { fakeSubscriber3, fakeSubscriber4 } - }; - } - - public static IEnumerable UnSubscribersToThisEvent() - { - var fakeSubscriber1 = FakeSubscriber(); - var fakeSubscriber2 = FakeSubscriber(); - var fakeSubscriber3 = FakeSubscriber(); - var fakeSubscriber4 = FakeSubscriber(); - var publisher = new EventPublisher(); - publisher.Subscribe(fakeSubscriber1); - publisher.Subscribe(fakeSubscriber2); - publisher.Subscribe(fakeSubscriber3); - publisher.Subscribe(fakeSubscriber4); - publisher.UnSubscribe(fakeSubscriber1); - publisher.UnSubscribe(fakeSubscriber2); - publisher.UnSubscribe(fakeSubscriber3); - publisher.UnSubscribe(fakeSubscriber4); - yield return new object[] - { - publisher, - new FakeEvent1(), - new IEventHandler[] { fakeSubscriber1 } - }; - yield return new object[] - { - publisher, - new FakeEvent2(), - new IEventHandler[] { fakeSubscriber1, fakeSubscriber2 } - }; - yield return new object[] - { - publisher, - new FakeEvent3(), - new IEventHandler[] { fakeSubscriber3, fakeSubscriber4 } - }; - } - - [Theory] - [MemberData(nameof(SubscribersToThisEvent))] - public void Publish_WhenCalled_CallsSubscribersToThisEvent(EventPublisher publisher, - IEvent @event, - IEnumerable subscribersToThisEvent) - { - // Arrange - subscribersToThisEvent.ForEach(s => s.ClearReceivedCalls()); - // Act - publisher.Publish(@event); - // Assert - Assert.All(subscribersToThisEvent, s => s.Received(1).Handle(@event)); - } - - [Theory] - [MemberData(nameof(UnSubscribersToThisEvent))] - public void Publish_WhenCalled_DoesNotCallUnSubscribersToThisEvent(EventPublisher publisher, - IEvent @event, - IEnumerable unSubscribersToThisEvent) - { - // Arrange - unSubscribersToThisEvent.ForEach(s => s.ClearReceivedCalls()); - // Act - publisher.Publish(@event); - // Assert - Assert.All(unSubscribersToThisEvent, s => s.DidNotReceive().Handle(Arg.Any())); - } - - [Theory] - [MemberData(nameof(SubscribersToOtherEventsThanThisEvent))] - public void Publish_WhenCalled_DoesNotCallSubscribersToOtherEvents(EventPublisher publisher, - IEvent @event, - IEnumerable subscribersToOtherEvents) - { - // Arrange - subscribersToOtherEvents.ForEach(s => s.ClearReceivedCalls()); - // Act - publisher.Publish(@event); - // Assert - Assert.All(subscribersToOtherEvents, s => s.DidNotReceive().Handle(Arg.Any())); - } - - private static IEventHandler FakeSubscriber() where TEvent : IEvent - { - var fakeSubscriber = Substitute.For>(); - fakeSubscriber.EventType.Returns(typeof(TEvent)); - return fakeSubscriber; - } - - #endregion Methods - } -} \ No newline at end of file diff --git a/Test/DDD.Core.UnitTests/Domain/FakeContext.cs b/Test/DDD.Core.UnitTests/Domain/FakeContext.cs new file mode 100644 index 0000000..0d4d3a2 --- /dev/null +++ b/Test/DDD.Core.UnitTests/Domain/FakeContext.cs @@ -0,0 +1,13 @@ +namespace DDD.Core.Domain +{ + public class FakeContext : BoundedContext + { + #region Constructors + + public FakeContext() : base("FK", "Fake") { } + + #endregion Constructors + + + } +} diff --git a/Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs b/Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs index f3acba1..970dbfb 100644 --- a/Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs +++ b/Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs @@ -4,10 +4,21 @@ namespace DDD.Core.Domain { public class FakeEvent1 : IEvent { + + #region Constructors + + public FakeEvent1() + { + this.OccurredOn = SystemTime.Local(); + } + + #endregion Constructors + #region Properties - public DateTime OccurredOn => DateTime.Now; + public DateTime OccurredOn { get; private set; } #endregion Properties + } } \ No newline at end of file diff --git a/Test/DDD.Core.UnitTests/Domain/FakeSourceContext.cs b/Test/DDD.Core.UnitTests/Domain/FakeSourceContext.cs new file mode 100644 index 0000000..49035f9 --- /dev/null +++ b/Test/DDD.Core.UnitTests/Domain/FakeSourceContext.cs @@ -0,0 +1,12 @@ +namespace DDD.Core.Domain +{ + public class FakeSourceContext : BoundedContext + { + #region Constructors + + public FakeSourceContext() : base("FKS", "FakeSource") { } + + #endregion Constructors + + } +} diff --git a/Test/DDD.Core.UnitTests/Infrastructure/Data/IDbConnectionExtensionsTests.cs b/Test/DDD.Core.UnitTests/Infrastructure/Data/IDbConnectionExtensionsTests.cs deleted file mode 100644 index d29a0ba..0000000 --- a/Test/DDD.Core.UnitTests/Infrastructure/Data/IDbConnectionExtensionsTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -using FluentAssertions; -using System; -using System.Collections.Generic; -using System.Data; -using System.Data.Common; -using Xunit; - -namespace DDD.Core.Infrastructure.Data -{ - public class IDbConnectionExtensionsTests - { - #region Methods - - public static IEnumerable Connections() - { - yield return new object[] - { - NewConnection("System.Data.SqlClient"), typeof(SqlServer2012Expressions) - }; - yield return new object[] - { - NewConnection("Oracle.ManagedDataAccess.Client"), typeof(Oracle11Expressions) - }; - } - - [Theory] - [MemberData(nameof(Connections))] - public void StandardExpressions_WhenCalled_ReturnsExpectedExpressions(IDbConnection connection, Type expectedExpressionsType) - { - // Act - var expressions = connection.Expressions(); - // Assert - expressions.Should().BeOfType(expectedExpressionsType); - } - - private static IDbConnection NewConnection(string providerInvariantName) - { - var providerFactory = DbProviderFactories.GetFactory(providerInvariantName); - return providerFactory.CreateConnection(); - } - - #endregion Methods - } -} \ No newline at end of file diff --git a/Test/DDD.Core.UnitTests/Infrastructure/Data/SequentialBinaryGuidGeneratorTests.cs b/Test/DDD.Core.UnitTests/Infrastructure/Data/SequentialBinaryGuidGeneratorTests.cs new file mode 100644 index 0000000..23269ee --- /dev/null +++ b/Test/DDD.Core.UnitTests/Infrastructure/Data/SequentialBinaryGuidGeneratorTests.cs @@ -0,0 +1,31 @@ +using FluentAssertions; +using System.Collections.Generic; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Collections; + + public class SequentialBinaryGuidGeneratorTests + { + + #region Methods + + [Fact] + public void Generate_WhenCalledConsecutively_GeneratesSequentialBinaryValues() + { + // Arrange + var generator = new SequentialBinaryGuidGenerator(); + var binaryValues = new List(); + var comparer = Comparer.Create((x, y) => x.StructuralCompare(y)); + // Act + for (var i = 0; i < 100; i++) + binaryValues.Add(generator.Generate().ToByteArray()); + // Assert + binaryValues.Should().BeInAscendingOrder(comparer); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Test/DDD.Core.UnitTests/Infrastructure/Data/SequentialSqlServerGuidGeneratorTests.cs b/Test/DDD.Core.UnitTests/Infrastructure/Data/SequentialSqlServerGuidGeneratorTests.cs new file mode 100644 index 0000000..22068f1 --- /dev/null +++ b/Test/DDD.Core.UnitTests/Infrastructure/Data/SequentialSqlServerGuidGeneratorTests.cs @@ -0,0 +1,29 @@ +using FluentAssertions; +using System.Collections.Generic; +using System.Data.SqlTypes; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + public class SequentialSqlServerGuidGeneratorTests + { + + #region Methods + + [Fact] + public void Generate_WhenCalledConsecutively_GeneratesSequentialSqlServerValues() + { + // Arrange + var generator = new SequentialSqlServerGuidGenerator(); + var sqlServerValues = new List(); + // Act + for (var i = 0; i < 100; i++) + sqlServerValues.Add(new SqlGuid(generator.Generate())); + // Assert + sqlServerValues.Should().BeInAscendingOrder(); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Test/DDD.Core.UnitTests/Infrastructure/Data/SequentialStringGuidGeneratorTests.cs b/Test/DDD.Core.UnitTests/Infrastructure/Data/SequentialStringGuidGeneratorTests.cs new file mode 100644 index 0000000..149adfe --- /dev/null +++ b/Test/DDD.Core.UnitTests/Infrastructure/Data/SequentialStringGuidGeneratorTests.cs @@ -0,0 +1,28 @@ +using FluentAssertions; +using System.Collections.Generic; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + public class SequentialStringGuidGeneratorTests + { + + #region Methods + + [Fact] + public void Generate_WhenCalledConsecutively_GeneratesSequentialStringValues() + { + // Arrange + var generator = new SequentialStringGuidGenerator(); + var stringValues = new List(); + // Act + for (var i = 0; i < 100; i++) + stringValues.Add(generator.Generate().ToString()); + // Assert + stringValues.Should().BeInAscendingOrder(); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Test/DDD.Core.UnitTests/Infrastructure/Serialization/DataContractSerializerWrapperTests.cs b/Test/DDD.Core.UnitTests/Infrastructure/Serialization/DataContractSerializerWrapperTests.cs new file mode 100644 index 0000000..69beff0 --- /dev/null +++ b/Test/DDD.Core.UnitTests/Infrastructure/Serialization/DataContractSerializerWrapperTests.cs @@ -0,0 +1,47 @@ +using System; +using System.IO; +using Xunit; +using FluentAssertions; + +namespace DDD.Core.Infrastructure.Serialization +{ + public class DataContractSerializerWrapperTests : IDisposable + { + + #region Fields + + private readonly Stream stream = new MemoryStream(); + + #endregion Fields + + #region Methods + + public void Dispose() + { + this.stream.Dispose(); + GC.SuppressFinalize(this); + } + + [Fact] + public void SerializeAndDeserialize_AreSymmetric() + { + // Arrange + var obj1 = new FakePerson + { + FirstName = "Donald", + LastName = "Duck", + Birthdate = new DateTime(1934, 4, 29) + }; + var serializer = DataContractSerializerWrapper.Create(); + // Act + serializer.Serialize(this.stream, obj1); + this.stream.Position = 0; + var obj2 = serializer.Deserialize(this.stream, typeof(FakePerson)); + // Assert + obj2.Should().BeEquivalentTo(obj1); + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.UnitTests/Infrastructure/Serialization/FakePerson.cs b/Test/DDD.Core.UnitTests/Infrastructure/Serialization/FakePerson.cs new file mode 100644 index 0000000..3e38a76 --- /dev/null +++ b/Test/DDD.Core.UnitTests/Infrastructure/Serialization/FakePerson.cs @@ -0,0 +1,18 @@ +using System; + +namespace DDD.Core.Infrastructure.Serialization +{ + public class FakePerson + { + + #region Properties + + public DateTime Birthdate { get; set; } + + public string FirstName { get; set; } + + public string LastName { get; set; } + + #endregion Properties + } +} diff --git a/Test/DDD.Core.UnitTests/Infrastructure/Serialization/XmlSerializerWrapperTests.cs b/Test/DDD.Core.UnitTests/Infrastructure/Serialization/XmlSerializerWrapperTests.cs new file mode 100644 index 0000000..ed10f3f --- /dev/null +++ b/Test/DDD.Core.UnitTests/Infrastructure/Serialization/XmlSerializerWrapperTests.cs @@ -0,0 +1,47 @@ +using System; +using System.IO; +using Xunit; +using FluentAssertions; + +namespace DDD.Core.Infrastructure.Serialization +{ + public class XmlSerializerWrapperTests : IDisposable + { + + #region Fields + + private readonly Stream stream = new MemoryStream(); + + #endregion Fields + + #region Methods + + public void Dispose() + { + this.stream.Dispose(); + GC.SuppressFinalize(this); + } + + [Fact] + public void SerializeAndDeserialize_AreSymmetric() + { + // Arrange + var obj1 = new FakePerson + { + FirstName = "Donald", + LastName = "Duck", + Birthdate = new DateTime(1934, 4, 29) + }; + var serializer = XmlSerializerWrapper.Create(); + // Act + serializer.Serialize(this.stream, obj1); + this.stream.Position = 0; + var obj2 = serializer.Deserialize(this.stream, typeof(FakePerson)); + // Assert + obj2.Should().BeEquivalentTo(obj1); + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.UnitTests/app.config b/Test/DDD.Core.UnitTests/app.config deleted file mode 100644 index 367590e..0000000 --- a/Test/DDD.Core.UnitTests/app.config +++ /dev/null @@ -1,40 +0,0 @@ - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Test/DDD.Core.UnitTests/packages.config b/Test/DDD.Core.UnitTests/packages.config deleted file mode 100644 index 40ab73a..0000000 --- a/Test/DDD.Core.UnitTests/packages.config +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/App.config b/Test/DDD.HealthcareDelivery.IntegrationTests/App.config index bb3de7a..34c9916 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/App.config +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/App.config @@ -1,54 +1,22 @@  +
- -
+ type="OracleInternal.Common.ODPMSectionHandler, Oracle.ManagedDataAccess, Version=2.0.19.1, Culture=neutral, PublicKeyToken=89b483f429c47342"/> - - - - - - - - + + + + + + + type="Oracle.ManagedDataAccess.Client.OracleClientFactory, Oracle.ManagedDataAccess, Version=2.0.19.1, Culture=neutral, PublicKeyToken=89b483f429c47342"/> - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionCreatorTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionCreatorTests.cs index a248bca..73a0acb 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionCreatorTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionCreatorTests.cs @@ -1,12 +1,7 @@ using Xunit; -using System.Text; namespace DDD.HealthcareDelivery.Application.Prescriptions { - using Core.Domain; - using Core.Infrastructure.Serialization; - using Domain.Prescriptions; - using Infrastructure.Prescriptions; using Infrastructure; [Collection("Oracle")] @@ -22,19 +17,5 @@ public OraclePharmaceuticalPrescriptionCreatorTests(OracleFixture fixture) : bas #endregion Constructors - #region Methods - - protected override IAsyncRepository CreateRepository() - { - return new PharmaceuticalPrescriptionRepository - ( - new Domain.Prescriptions.BelgianPharmaceuticalPrescriptionTranslator(), - new EventTranslator(DataContractSerializerWrapper.Create(new UTF8Encoding(false))), - new OracleHealthcareContextFactory(this.Fixture.ConnectionFactory) - ); - } - - #endregion Methods - } } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionRevokerTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionRevokerTests.cs index 404ac7a..eae52e0 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionRevokerTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionRevokerTests.cs @@ -1,12 +1,7 @@ using Xunit; -using System.Text; namespace DDD.HealthcareDelivery.Application.Prescriptions { - using Core.Domain; - using Core.Infrastructure.Serialization; - using Domain.Prescriptions; - using Infrastructure.Prescriptions; using Infrastructure; [Collection("Oracle")] @@ -22,19 +17,5 @@ public OraclePharmaceuticalPrescriptionRevokerTests(OracleFixture fixture) : bas #endregion Constructors - #region Methods - - protected override IAsyncRepository CreateRepository() - { - return new PharmaceuticalPrescriptionRepository - ( - new Domain.Prescriptions.BelgianPharmaceuticalPrescriptionTranslator(), - new EventTranslator(DataContractSerializerWrapper.Create(new UTF8Encoding(false))), - new OracleHealthcareContextFactory(this.Fixture.ConnectionFactory) - ); - } - - #endregion Methods - } } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs index e6ba705..3a947d8 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs @@ -7,16 +7,17 @@ namespace DDD.HealthcareDelivery.Application.Prescriptions { + using Core.Application; using Common.Application; - using Core.Domain; - using Core.Infrastructure.Testing; - using Domain.Facilities; - using Domain.Practitioners; + using Practitioners; + using Domain; using Domain.Prescriptions; using Infrastructure; + using Infrastructure.Prescriptions; + using Core.Infrastructure.Data; - public abstract class PharmaceuticalPrescriptionCreatorTests - where TFixture : IDbFixture + public abstract class PharmaceuticalPrescriptionCreatorTests : IDisposable + where TFixture : IPersistenceFixture { #region Constructors @@ -24,11 +25,12 @@ public abstract class PharmaceuticalPrescriptionCreatorTests protected PharmaceuticalPrescriptionCreatorTests(TFixture fixture) { this.Fixture = fixture; + this.ConnectionProvider = fixture.CreateConnectionProvider(pooling: false); // To check transaction escalation (MSDTC) + this.SessionFactory = this.Fixture.CreateSessionFactory(this.ConnectionProvider); this.Repository = this.CreateRepository(); this.Handler = new PharmaceuticalPrescriptionCreator ( Repository, - new EventPublisher(), new BelgianPharmaceuticalPrescriptionTranslator() ); } @@ -37,14 +39,23 @@ protected PharmaceuticalPrescriptionCreatorTests(TFixture fixture) #region Properties + protected IDbConnectionProvider ConnectionProvider { get; } protected TFixture Fixture { get; } protected PharmaceuticalPrescriptionCreator Handler { get; } - protected IAsyncRepository Repository { get; } + protected PharmaceuticalPrescriptionRepository Repository { get; } + protected DelegatingSessionFactory SessionFactory { get; } #endregion Properties #region Methods + public void Dispose() + { + this.ConnectionProvider.Dispose(); + this.Repository.Dispose(); + this.SessionFactory.Dispose(); + } + [Fact] public async Task HandleAsync_WhenCalled_CreatePharmaceuticalPrescription() { @@ -52,8 +63,9 @@ public async Task HandleAsync_WhenCalled_CreatePharmaceuticalPrescription() this.Fixture.ExecuteScriptFromResources("CreatePharmaceuticalPrescription"); Thread.CurrentPrincipal = new GenericPrincipal(new GenericIdentity("d.duck"), new string[] { "User" }); var command = CreateCommand(); + var context = new MessageContext(); // Act - await this.Handler.HandleAsync(command); + await this.Handler.HandleAsync(command, context); // Assert var prescription = await this.Repository.FindAsync(new PrescriptionIdentifier(command.PrescriptionIdentifier)); prescription.Should().NotBeNull(); @@ -61,8 +73,6 @@ public async Task HandleAsync_WhenCalled_CreatePharmaceuticalPrescription() prescription.PrescribedMedications().Should().NotBeNullOrEmpty(); } - protected abstract IAsyncRepository CreateRepository(); - private static CreatePharmaceuticalPrescription CreateCommand() { return new CreatePharmaceuticalPrescription @@ -86,12 +96,10 @@ private static CreatePharmaceuticalPrescription CreateCommand() PatientLastName = "Flintstone", PatientBirthdate = new DateTime(1976, 2, 7), PatientSex = Sex.Male, - FacilityIdentifier = 1, - FacilityType = HealthFacilityType.MedicalOffice, - FacilityName = "Medical Office Donald Duck", PrescriptionIdentifier = 1, CreatedOn = new DateTime(2018, 1, 1, 10, 6, 0), - DelivrableAt = new DateTime(2018, 2, 1), + EncounterIdentifier = 1, + DeliverableAt = new DateTime(2018, 2, 1), Medications = new PrescribedMedicationDescriptor[] { new PrescribedMedicationDescriptor @@ -99,13 +107,22 @@ private static CreatePharmaceuticalPrescription CreateCommand() MedicationType = PrescribedMedicationType.Product, Code = "0318717", NameOrDescription = "ADALAT OROS 30 COMP 28 X 30 MG", - Quantity = "1 boîte", + Quantity = 1, Posology = "appliquer 2 fois par jour jusqu'au 3 octobre 2018" } } }; } + private PharmaceuticalPrescriptionRepository CreateRepository() + { + return new PharmaceuticalPrescriptionRepository + ( + this.SessionFactory, + this.Fixture.CreateEventTranslator() + ); + } + #endregion Methods } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs index 6716d71..5fa79e3 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs @@ -2,17 +2,20 @@ using System.Threading; using System.Threading.Tasks; using System.Security.Principal; +using System; using FluentAssertions; namespace DDD.HealthcareDelivery.Application.Prescriptions { - using Core.Domain; + using Core.Application; + using Domain; + using Core.Infrastructure.Data; using Domain.Prescriptions; - using Core.Infrastructure.Testing; using Infrastructure; + using Infrastructure.Prescriptions; - public abstract class PharmaceuticalPrescriptionRevokerTests - where TFixture : IDbFixture + public abstract class PharmaceuticalPrescriptionRevokerTests : IDisposable + where TFixture : IPersistenceFixture { #region Constructors @@ -20,12 +23,10 @@ public abstract class PharmaceuticalPrescriptionRevokerTests protected PharmaceuticalPrescriptionRevokerTests(TFixture fixture) { this.Fixture = fixture; + this.ConnectionProvider = fixture.CreateConnectionProvider(pooling: false); // To check transaction escalation (MSDTC) + this.SessionFactory = this.Fixture.CreateSessionFactory(this.ConnectionProvider); this.Repository = this.CreateRepository(); - this.Handler = new PharmaceuticalPrescriptionRevoker - ( - Repository, - new EventPublisher() - ); + this.Handler = new PharmaceuticalPrescriptionRevoker(Repository); } #endregion Constructors @@ -33,13 +34,26 @@ protected PharmaceuticalPrescriptionRevokerTests(TFixture fixture) #region Properties protected TFixture Fixture { get; } + + protected IDbConnectionProvider ConnectionProvider { get; } + protected PharmaceuticalPrescriptionRevoker Handler { get; } - protected IAsyncRepository Repository { get; } + + protected PharmaceuticalPrescriptionRepository Repository { get; } + + protected DelegatingSessionFactory SessionFactory { get; } #endregion Properties #region Methods + public void Dispose() + { + this.ConnectionProvider.Dispose(); + this.Repository.Dispose(); + this.SessionFactory.Dispose(); + } + [Fact] public async Task HandleAsync_WhenCalled_RevokePharmaceuticalPrescription() { @@ -47,15 +61,14 @@ public async Task HandleAsync_WhenCalled_RevokePharmaceuticalPrescription() this.Fixture.ExecuteScriptFromResources("RevokePharmaceuticalPrescription"); Thread.CurrentPrincipal = new GenericPrincipal(new GenericIdentity("d.duck"), new string[] { "User" }); var command = CreateCommand(); + var context = new MessageContext(); // Act - await this.Handler.HandleAsync(command); + await this.Handler.HandleAsync(command, context); // Assert var prescription = await this.Repository.FindAsync(new PrescriptionIdentifier(command.PrescriptionIdentifier)); prescription.Status.Should().Be(Domain.Prescriptions.PrescriptionStatus.Revoked); } - protected abstract IAsyncRepository CreateRepository(); - private static RevokePharmaceuticalPrescription CreateCommand() { return new RevokePharmaceuticalPrescription @@ -65,6 +78,15 @@ private static RevokePharmaceuticalPrescription CreateCommand() }; } + private PharmaceuticalPrescriptionRepository CreateRepository() + { + return new PharmaceuticalPrescriptionRepository + ( + this.SessionFactory, + this.Fixture.CreateEventTranslator() + ); + } + #endregion Methods } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionCreatorTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionCreatorTests.cs index 6fb3156..6c5d554 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionCreatorTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionCreatorTests.cs @@ -1,12 +1,7 @@ using Xunit; -using System.Text; namespace DDD.HealthcareDelivery.Application.Prescriptions { - using Core.Domain; - using Core.Infrastructure.Serialization; - using Domain.Prescriptions; - using Infrastructure.Prescriptions; using Infrastructure; [Collection("SqlServer")] @@ -22,19 +17,5 @@ public SqlServerPharmaceuticalPrescriptionCreatorTests(SqlServerFixture fixture) #endregion Constructors - #region Methods - - protected override IAsyncRepository CreateRepository() - { - return new PharmaceuticalPrescriptionRepository - ( - new Domain.Prescriptions.BelgianPharmaceuticalPrescriptionTranslator(), - new EventTranslator(DataContractSerializerWrapper.Create(Encoding.Unicode)), - new SqlServerHealthcareContextFactory(this.Fixture.ConnectionFactory) - ); - } - - #endregion Methods - } } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionRevokerTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionRevokerTests.cs index 8eeecd4..276fc11 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionRevokerTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionRevokerTests.cs @@ -1,12 +1,7 @@ using Xunit; -using System.Text; namespace DDD.HealthcareDelivery.Application.Prescriptions { - using Core.Domain; - using Core.Infrastructure.Serialization; - using Domain.Prescriptions; - using Infrastructure.Prescriptions; using Infrastructure; [Collection("SqlServer")] @@ -22,19 +17,5 @@ public SqlServerPharmaceuticalPrescriptionRevokerTests(SqlServerFixture fixture) #endregion Constructors - #region Methods - - protected override IAsyncRepository CreateRepository() - { - return new PharmaceuticalPrescriptionRepository - ( - new Domain.Prescriptions.BelgianPharmaceuticalPrescriptionTranslator(), - new EventTranslator(DataContractSerializerWrapper.Create(Encoding.Unicode)), - new SqlServerHealthcareContextFactory(this.Fixture.ConnectionFactory) - ); - } - - #endregion Methods - } } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj b/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj index 3de00e4..f5c7af9 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj @@ -1,181 +1,53 @@ - - - - - + - Debug - AnyCPU - {B53007C7-B314-40DF-B6E7-6C6576A5611C} + net48;net6.0 Library - Properties DDD.HealthcareDelivery - DDD.HealthcareDelivery.IntegrationTests - v4.7.2 - 512 - - - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 + false - - L:\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll - - - L:\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll - - - L:\Packages\FluentAssertions.5.6.0\lib\net47\FluentAssertions.dll - - - L:\packages\Oracle.ManagedDataAccess.18.3.0\lib\net40\Oracle.ManagedDataAccess.dll - True - - - L:\packages\Oracle.ManagedDataAccess.EntityFramework.18.3.0\lib\net45\Oracle.ManagedDataAccess.EntityFramework.dll - - - - - - L:\packages\System.ValueTuple.4.5.0\lib\netstandard1.0\System.ValueTuple.dll - True - - - - - - - - - L:\packages\xunit.abstractions.2.0.3\lib\net35\xunit.abstractions.dll - True - - - L:\packages\xunit.assert.2.4.1\lib\netstandard1.1\xunit.assert.dll - - - L:\packages\xunit.extensibility.core.2.4.1\lib\net452\xunit.core.dll - - - L:\packages\xunit.extensibility.execution.2.4.1\lib\net452\xunit.execution.desktop.dll - + + + + - - - - - - - - - - - - - - + True True OracleScripts.resx - - - - - - - + True True SqlServerScripts.resx - - - - - - {40A849C5-C8D7-4F76-856A-138AED73A6C3} - DDD.Common.Messages - - - {0B70B4FD-F5A0-4A6C-A3FD-90031E08C1C2} - DDD.Common - - - {6d227aa7-ff90-48ca-b13d-ed23c1fffba5} - DDD.Core.EF - - - {2438B31A-3A39-4878-81FA-BE5AE715EAE5} - DDD.Core.Messages - - - {C6C3E419-B9AA-44AD-9DBF-789294687AE6} - DDD.Core - - - {701da58b-ae36-429f-8621-64109b8d29d7} - DDD.Core.Dapper - - - {62b652de-4cc9-4b85-8ae9-d66ceea358dd} - DDD.Core.Xunit - - - {596a8700-3d18-4a62-b200-1f78a9ea4617} - DDD.Core.Abstractions - - - {b8bb212c-8afc-4258-a023-eb1f6937f53d} - DDD.HealthcareDelivery.Messages - - - {5b8fffd3-9a1c-4620-9db3-cd76cd9e79bf} - DDD.HealthcareDelivery - - - - + + + + + + + + + + + - + ResXFileCodeGenerator Designer OracleScripts.Designer.cs - - ResXFileCodeGenerator - SqlServerScripts.Designer.cs - - + - - - + @@ -183,24 +55,27 @@ - + + 6.9.0 + + + + + + + + + 2.4.5 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + PreserveNewest + - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - - \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianOracleHealthcareDeliveryConfigurationTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianOracleHealthcareDeliveryConfigurationTests.cs new file mode 100644 index 0000000..66c42db --- /dev/null +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianOracleHealthcareDeliveryConfigurationTests.cs @@ -0,0 +1,33 @@ +using NHibernate.Dialect; +using NHibernate.Driver; +using Xunit; + +namespace DDD.HealthcareDelivery.Infrastructure +{ + [Collection("Oracle")] + public class BelgianOracleHealthcareDeliveryConfigurationTests : HealthcareDeliveryConfigurationTests + { + #region Constructors + + public BelgianOracleHealthcareDeliveryConfigurationTests(OracleFixture fixture) : base(fixture) + { + } + + #endregion Constructors + + #region Methods + + protected override HealthcareDeliveryConfiguration CreateConfiguration() + { + var configuration = new BelgianOracleHealthcareDeliveryConfiguration().DataBaseIntegration(db => + { + db.Dialect(); + db.Driver(); + db.ConnectionStringName = "Oracle"; + }); + return (HealthcareDeliveryConfiguration)configuration; + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareDeliveryConfigurationTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareDeliveryConfigurationTests.cs new file mode 100644 index 0000000..17652e8 --- /dev/null +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareDeliveryConfigurationTests.cs @@ -0,0 +1,33 @@ +using NHibernate.Dialect; +using NHibernate.Driver; +using Xunit; + +namespace DDD.HealthcareDelivery.Infrastructure +{ + [Collection("SqlServer")] + public class BelgianSqlServerHealthcareDeliveryConfigurationTests : HealthcareDeliveryConfigurationTests + { + #region Constructors + + public BelgianSqlServerHealthcareDeliveryConfigurationTests(SqlServerFixture fixture) : base(fixture) + { + } + + #endregion Constructors + + #region Methods + + protected override HealthcareDeliveryConfiguration CreateConfiguration() + { + var configuration = new BelgianSqlServerHealthcareDeliveryConfiguration().DataBaseIntegration(db => + { + db.Dialect(); + db.Driver(); + db.ConnectionStringName = "SqlServer"; + }); + return (HealthcareDeliveryConfiguration)configuration; + } + + #endregion Methods + } +} diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareDeliveryConfigurationTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareDeliveryConfigurationTests.cs new file mode 100644 index 0000000..d5a1f7f --- /dev/null +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareDeliveryConfigurationTests.cs @@ -0,0 +1,177 @@ +using FluentAssertions; +using NHibernate; +using NHibernate.Tool.hbm2ddl; +using System; +using Xunit; + +namespace DDD.HealthcareDelivery.Infrastructure +{ + using Core.Application; + using Common.Domain; + using Domain.Patients; + using Domain.Practitioners; + using Domain.Prescriptions; + + public abstract class HealthcareDeliveryConfigurationTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Fields + + private readonly HealthcareDeliveryConfiguration configuration; + private readonly ISessionFactory sessionFactory; + private readonly ISession session; + + #endregion Fields + + #region Constructors + + protected HealthcareDeliveryConfigurationTests(TFixture fixture) + { + this.Fixture = fixture; + this.configuration = this.CreateConfiguration(); + this.sessionFactory = this.configuration.BuildSessionFactory(); + this.session = this.sessionFactory.OpenSession(); + } + + #endregion Constructors + + #region Properties + + protected TFixture Fixture { get; } + + #endregion Properties + + [Fact(Skip = "This test recreates the schema and thus can cause conflicts with other tests.")] + public void HealthcareConfiguration_WhenMappingValid_CanExportSchema() + { + // Arrange + var schemaExport = new SchemaExport(this.configuration); + // Act + Action action = () => schemaExport.Execute(useStdOut: true, + execute: true, + justDrop: false, + connection: this.session.Connection, + exportOutput: Console.Out); + // Assert + action.Should().NotThrow(); + } + + #region Methods + + public void Dispose() + { + this.session.Dispose(); + this.sessionFactory.Dispose(); + } + + [Fact] + public void HealthcareConfiguration_WhenMappingValid_CanSaveAndRestoreEvents() + { + // Arrange + this.Fixture.ExecuteScriptFromResources("ClearDatabase"); + var event1 = CreateEvent(); + // Act + using (var transaction = this.session.BeginTransaction()) + { + this.session.Save(event1); + transaction.Commit(); + } + this.session.Clear(); + Event event2; + using (var transaction = this.session.BeginTransaction()) + { + event2 = this.session.Get(event1.EventId); + transaction.Commit(); + } + // Assert + event2.Should().BeEquivalentTo(event1); + } + + [Fact] + public void HealthcareConfiguration_WhenMappingValid_CanSaveAndRestorePrescriptions() + { + // Arrange + this.Fixture.ExecuteScriptFromResources("ClearDatabase"); + var prescription1 = CreatePrescription(); + // Act + using (var transaction = this.session.BeginTransaction()) + { + this.session.Save(prescription1); + transaction.Commit(); + } + this.session.Clear(); + PharmaceuticalPrescription prescription2; + using (var transaction = this.session.BeginTransaction()) + { + prescription2 = this.session.Get(prescription1.Identifier); + transaction.Commit(); + } + // Assert + prescription2.Should().BeEquivalentTo(prescription1); + } + protected abstract HealthcareDeliveryConfiguration CreateConfiguration(); + + private static Event CreateEvent() + { + return new Event + { + EventId = new Guid("d9fdd908-9e0a-c80f-e72d-e94a0f7d4902"), + EventType = "DDD.HealthcareDelivery.Domain.Prescriptions.PharmaceuticalPrescriptionCreated, DDD.HealthcareDelivery.Messages", + OccurredOn = new DateTime(2018, 1, 1), + Body = "{\"prescriptionId\":1,\"occurredOn\":\"2018 - 01 - 01T10:06:00\"}", + BodyFormat = "JSON", + StreamId = "1", + StreamType = "PharmaceuticalPrescription", + IssuedBy = "draphyz" + }; + } + + private static PharmaceuticalPrescription CreatePrescription() + { + return PharmaceuticalPrescription.Create + ( + new PrescriptionIdentifier(1), + new Physician + ( + 1, + new FullName("Duck", "Donald"), + new BelgianHealthcarePractitionerLicenseNumber("19006951001"), + null, + new ContactInformation + ( + new PostalAddress + ( + "Grote Markt", + "Brussel", + "1000", + new Alpha2CountryCode("BE"), + "7" + ), + primaryTelephoneNumber: "02/221.21.21" + ), + displayName: "Dr. Duck Donald" + ), + new Patient + ( + 1, + new FullName("Flintstone", "Fred"), + BelgianSex.Male, + new BelgianSocialSecurityNumber("60207273601") + ), + new PrescribedMedication[] + { + new PrescribedPharmaceuticalProduct + ( + nameOrDescription: "ADALAT OROS 30 COMP 28 X 30 MG", + posology: "appliquer 2 fois par jour jusqu'au 3 octobre 2018" + ) + }, + new Alpha2LanguageCode("FR") + ); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/IPersistenceFixture.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/IPersistenceFixture.cs new file mode 100644 index 0000000..019978e --- /dev/null +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/IPersistenceFixture.cs @@ -0,0 +1,22 @@ +namespace DDD.HealthcareDelivery.Infrastructure +{ + using Domain; + using Core.Domain; + using Core.Application; + using Core.Infrastructure.Testing; + using Core.Infrastructure.Data; + using Mapping; + + public interface IPersistenceFixture : IDbFixture + { + + #region Methods + + IObjectTranslator CreateEventTranslator(); + + DelegatingSessionFactory CreateSessionFactory(IDbConnectionProvider connectionProvider); + + #endregion Methods + + } +} diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleCollection.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleCollection.cs new file mode 100644 index 0000000..4afad64 --- /dev/null +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleCollection.cs @@ -0,0 +1,9 @@ +using Xunit; + +namespace DDD.HealthcareDelivery.Infrastructure +{ + [CollectionDefinition("Oracle")] + public class OracleCollection : ICollectionFixture + { + } +} diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleFixture.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleFixture.cs new file mode 100644 index 0000000..d58dd69 --- /dev/null +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleFixture.cs @@ -0,0 +1,97 @@ +using System.Data; +#if (NET6_0) +using System.Data.Common; +#endif +using System.Configuration; +using Oracle.ManagedDataAccess.Client; + +namespace DDD.HealthcareDelivery.Infrastructure +{ + using Domain; + using Core.Infrastructure.Testing; + using Core.Infrastructure.Data; + using Core.Domain; + using Core.Application; + using Core.Infrastructure.Serialization; + using Mapping; + using NHibernate.Dialect; + using NHibernate.Driver; + using NHibernate.Cfg; + + public class OracleFixture : DbFixture, IPersistenceFixture + { + +#region Constructors + + public OracleFixture() : base("OracleScripts", ConfigurationManager.ConnectionStrings["Oracle"]) + { + } + +#endregion Constructors + +#region Methods + + public IObjectTranslator CreateEventTranslator() + { + return new EventTranslator(JsonSerializerWrapper.Create(false)); + } + + public DelegatingSessionFactory CreateSessionFactory(IDbConnectionProvider connectionProvider) + { + var configuration = new BelgianOracleHealthcareDeliveryConfiguration().DataBaseIntegration(db => + { + db.Dialect(); + db.Driver(); + db.KeywordsAutoImport = Hbm2DDLKeyWords.None; + }); + return new DelegatingSessionFactory + ( + new HealthcareDeliveryContext(), + configuration, + options => + { + var connection = connectionProvider.GetOpenConnection(); + options.Connection(connection); + }, + async (options, cancellationToken) => + { + var connection = await connectionProvider.GetOpenConnectionAsync(cancellationToken); + options.Connection(connection); + } + ); + } + + protected override void CreateDatabase() + { + using (var connection = this.CreateConnection()) + { + var builder = new OracleConnectionStringBuilder(connection.ConnectionString) { UserID = "SYS", DBAPrivilege = "SYSDBA" }; + connection.ConnectionString = builder.ConnectionString; + connection.Open(); + this.ExecuteScript(OracleScripts.CreateSchema, connection); + } + this.ExecuteScript(OracleScripts.FillSchema); + } + + protected override int[] ExecuteScript(string script, IDbConnection connection) + { + return connection.ExecuteScript(script, batchSeparator: "/"); + } + + protected override void LoadConfiguration() + { +#if (NET6_0) + DbProviderFactories.RegisterFactory("Oracle.ManagedDataAccess.Client", OracleClientFactory.Instance); +#endif + } + + protected override string SetPooling(string connectionString, bool pooling) + { + var builder = new OracleConnectionStringBuilder(connectionString) { Pooling = pooling }; + return builder.ConnectionString; + } + +#endregion Methods + + } +} \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinderTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinderTests.cs index f20ac34..80bfcde 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinderTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinderTests.cs @@ -6,27 +6,33 @@ namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions { + using Core.Application; + using Domain; using Application.Prescriptions; using Core.Infrastructure.Testing; + using Core.Infrastructure.Data; - public abstract class PharmaceuticalPrescriptionsByPatientFinderTests - where TFixture : IDbFixture + public abstract class PharmaceuticalPrescriptionsByPatientFinderTests : IDisposable + where TFixture : IDbFixture { - #region Fields - - private readonly TFixture fixture; - - #endregion Fields #region Constructors protected PharmaceuticalPrescriptionsByPatientFinderTests(TFixture fixture) { - this.fixture = fixture; + this.Fixture = fixture; + this.ConnectionProvider = fixture.CreateConnectionProvider(); } #endregion Constructors + #region Properties + + protected IDbConnectionProvider ConnectionProvider { get; } + protected TFixture Fixture { get; } + + #endregion Properties + #region Methods public static IEnumerable QueriesAndResults() @@ -46,7 +52,7 @@ public static IEnumerable QueriesAndResults() Identifier = 1, Status = PrescriptionStatus.Created, CreatedOn = new DateTime(2016, 12, 18), - DelivrableAt = null, + DeliverableAt = null, PrescriberDisplayName = "Dr. Duck Donald" }, new PharmaceuticalPrescriptionSummary @@ -54,7 +60,7 @@ public static IEnumerable QueriesAndResults() Identifier = 2, Status = PrescriptionStatus.Created, CreatedOn = new DateTime(2016, 12, 18), - DelivrableAt = new DateTime(2017, 2, 18), + DeliverableAt = new DateTime(2017, 2, 18), PrescriberDisplayName = "Dr. Duck Donald" }, new PharmaceuticalPrescriptionSummary @@ -62,7 +68,7 @@ public static IEnumerable QueriesAndResults() Identifier = 3, Status = PrescriptionStatus.Created, CreatedOn = new DateTime(2016, 12, 18), - DelivrableAt = new DateTime(2017, 3, 18), + DeliverableAt = new DateTime(2017, 3, 18), PrescriberDisplayName = "Dr. Duck Donald" }, new PharmaceuticalPrescriptionSummary @@ -70,7 +76,7 @@ public static IEnumerable QueriesAndResults() Identifier = 4, Status = PrescriptionStatus.Created, CreatedOn = new DateTime(2016, 12, 18), - DelivrableAt = new DateTime(2017, 4, 18), + DeliverableAt = new DateTime(2017, 4, 18), PrescriberDisplayName = "Dr. Duck Donald" } @@ -86,7 +92,7 @@ public static IEnumerable QueriesAndResults() Identifier = 5, Status = PrescriptionStatus.Created, CreatedOn = new DateTime(2017, 9, 25), - DelivrableAt = null, + DeliverableAt = null, PrescriberDisplayName = "Dr. Duck Donald" }, new PharmaceuticalPrescriptionSummary @@ -94,7 +100,7 @@ public static IEnumerable QueriesAndResults() Identifier = 6, Status = PrescriptionStatus.Created, CreatedOn = new DateTime(2017, 9, 25), - DelivrableAt = new DateTime(2017, 12, 25), + DeliverableAt = new DateTime(2017, 12, 25), PrescriberDisplayName = "Dr. Duck Donald" } } @@ -106,14 +112,21 @@ public static IEnumerable QueriesAndResults() public async Task HandleAsync_WhenCalled_ReturnsValidResults(FindPharmaceuticalPrescriptionsByPatient query, IEnumerable expectedResults) { // Arrange - this.fixture.ExecuteScriptFromResources("FindPharmaceuticalPrescriptionsByPatient"); - var handler = new PharmaceuticalPrescriptionsByPatientFinder(this.fixture.ConnectionFactory); + this.Fixture.ExecuteScriptFromResources("FindPharmaceuticalPrescriptionsByPatient"); + var handler = new PharmaceuticalPrescriptionsByPatientFinder(this.ConnectionProvider); + var context = new MessageContext(); // Act - var results = await handler.HandleAsync(query); + var results = await handler.HandleAsync(query, context); // Assert - results.Should().BeEquivalentTo(expectedResults, options => options.WithStrictOrdering()); + results.Should().BeEquivalentTo(expectedResults, c => c.WithStrictOrdering()); + } + + public void Dispose() + { + this.ConnectionProvider.Dispose(); } #endregion Methods + } } \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinderTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinderTests.cs index 48a3d27..45ce4a0 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinderTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinderTests.cs @@ -1,31 +1,39 @@ using FluentAssertions; +using System; using System.Collections.Generic; using System.Threading.Tasks; using Xunit; namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions { + using Core.Application; + using Domain; using Application.Prescriptions; using Core.Infrastructure.Testing; + using Core.Infrastructure.Data; - public abstract class PrescribedMedicationsByPrescriptionFinderTests - where TFixture : IDbFixture + public abstract class PrescribedMedicationsByPrescriptionFinderTests : IDisposable + where TFixture : IDbFixture { - #region Fields - - private readonly TFixture fixture; - - #endregion Fields #region Constructors protected PrescribedMedicationsByPrescriptionFinderTests(TFixture fixture) { - this.fixture = fixture; + this.Fixture = fixture; + this.ConnectionProvider = fixture.CreateConnectionProvider(); } #endregion Constructors + #region Properties + + protected TFixture Fixture { get; } + + protected IDbConnectionProvider ConnectionProvider { get; } + + #endregion Properties + #region Methods public static IEnumerable QueriesAndResults() @@ -45,8 +53,7 @@ public static IEnumerable QueriesAndResults() Identifier = 2, NameOrDescription = "Dualkopt Coll. 10 ml", Posology = "1 goutte 2 x/jour", - Quantity = "1 flacon", - Duration = null, + Quantity = 1, Code = "3260072" }, new PrescribedMedicationDetails @@ -54,8 +61,7 @@ public static IEnumerable QueriesAndResults() Identifier = 1, NameOrDescription = "Latansoc Mylan Coll. 2,5 ml X 3", Posology = "1 goutte le soir", - Quantity = "1 boîte de 3 flacons", - Duration = null, + Quantity = 1, Code = null } } @@ -70,8 +76,7 @@ public static IEnumerable QueriesAndResults() Identifier = 3, NameOrDescription = "Dualkopt Coll. 10 ml", Posology = "1 goutte 2 x/jour", - Quantity = "1 flacon", - Duration = null, + Quantity = 1, Code = "3260072" } } @@ -83,14 +88,20 @@ public static IEnumerable QueriesAndResults() public async Task HandleAsync_WhenCalled_ReturnsValidResults(FindPrescribedMedicationsByPrescription query, IEnumerable expectedResults) { // Arrange - this.fixture.ExecuteScriptFromResources("FindPrescribedMedicationsByPrescription"); - var handler = new PrescribedMedicationsByPrescriptionFinder(this.fixture.ConnectionFactory); + this.Fixture.ExecuteScriptFromResources("FindPrescribedMedicationsByPrescription"); + var handler = new PrescribedMedicationsByPrescriptionFinder(this.ConnectionProvider); + var context = new MessageContext(); // Act - var results = await handler.HandleAsync(query); + var results = await handler.HandleAsync(query, context); // Assert results.Should().BeEquivalentTo(expectedResults); } + public void Dispose() + { + this.ConnectionProvider.Dispose(); + } + #endregion Methods } } \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerCollection.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerCollection.cs new file mode 100644 index 0000000..86c6f8d --- /dev/null +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerCollection.cs @@ -0,0 +1,9 @@ +using Xunit; + +namespace DDD.HealthcareDelivery.Infrastructure +{ + [CollectionDefinition("SqlServer")] + public class SqlServerCollection : ICollectionFixture + { + } +} diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerFixture.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerFixture.cs new file mode 100644 index 0000000..7e5da39 --- /dev/null +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerFixture.cs @@ -0,0 +1,97 @@ +using Microsoft.Data.SqlClient; +using System.Data; +#if (NET6_0) +using System.Data.Common; +#endif +using System.Configuration; +using NHibernate.Cfg; +using NHibernate.Dialect; +using NHibernate.Driver; + +namespace DDD.HealthcareDelivery.Infrastructure +{ + using Domain; + using Core.Infrastructure.Testing; + using Core.Infrastructure.Data; + using Core.Domain; + using Core.Application; + using Core.Infrastructure.Serialization; + using Mapping; + + public class SqlServerFixture : DbFixture, IPersistenceFixture + { + + #region Constructors + + public SqlServerFixture() : base("SqlServerScripts", ConfigurationManager.ConnectionStrings["SqlServer"]) + { + } + + #endregion Constructors + + #region Methods + + public IObjectTranslator CreateEventTranslator() + { + return new EventTranslator(JsonSerializerWrapper.Create(false)); + } + + public DelegatingSessionFactory CreateSessionFactory(IDbConnectionProvider connectionProvider) + { + var configuration = new BelgianSqlServerHealthcareDeliveryConfiguration().DataBaseIntegration(db => + { + db.Dialect(); + db.Driver(); + db.KeywordsAutoImport = Hbm2DDLKeyWords.None; + }); + return new DelegatingSessionFactory + ( + new HealthcareDeliveryContext(), + configuration, + options => + { + var connection = connectionProvider.GetOpenConnection(); + options.Connection(connection); + }, + async (options, cancellationToken) => + { + var connection = await connectionProvider.GetOpenConnectionAsync(cancellationToken); + options.Connection(connection); + } + ) ; + } + + protected override void CreateDatabase() + { + using (var connection = this.CreateConnection()) + { + var builder = new SqlConnectionStringBuilder(connection.ConnectionString) { InitialCatalog = "master" }; + connection.ConnectionString = builder.ConnectionString; + connection.Open(); + this.ExecuteScript(SqlServerScripts.CreateDatabase, connection); + } + } + + protected override int[] ExecuteScript(string script, IDbConnection connection) + { + return connection.ExecuteScript(script); + } + + protected override void LoadConfiguration() + { +#if (NET6_0) + DbProviderFactories.RegisterFactory("Microsoft.Data.SqlClient", SqlClientFactory.Instance); +#endif + } + + protected override string SetPooling(string connectionString, bool pooling) + { + var builder = new SqlConnectionStringBuilder(connectionString) { Pooling = pooling }; + return builder.ConnectionString; + } + + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleConnectionFactory.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/OracleConnectionFactory.cs deleted file mode 100644 index 2924055..0000000 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleConnectionFactory.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace DDD.HealthcareDelivery -{ - using Infrastructure; - using Core.Infrastructure.Data; - - public class OracleConnectionFactory : DbConnectionFactory, IHealthcareConnectionFactory - { - public OracleConnectionFactory() - : base("Oracle.ManagedDataAccess.Client", - @"Data Source=Local;Persist Security Info=true;User Id=TEST;Password=dev;Pooling=false") - { - } - } -} diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleFixture.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/OracleFixture.cs deleted file mode 100644 index 7b7e976..0000000 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleFixture.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Data; -using Oracle.ManagedDataAccess.Client; - -namespace DDD.HealthcareDelivery -{ - using Core.Infrastructure.Testing; - using Core.Infrastructure.Data; - - public class OracleFixture : DbFixture - { - - #region Constructors - - public OracleFixture() : base(new OracleConnectionFactory(), "OracleScripts") - { - } - - #endregion Constructors - - #region Methods - - protected override void CreateDatabase() - { - using (var connection = this.ConnectionFactory.CreateConnection()) - { - var builder = new OracleConnectionStringBuilder(connection.ConnectionString) { UserID = "SYS", DBAPrivilege = "SYSDBA" }; - connection.ConnectionString = builder.ConnectionString; - connection.Open(); - this.ExecuteScript(OracleScripts.CreateSchema, connection); - } - this.ExecuteScript(OracleScripts.FillSchema); - } - - protected override int[] ExecuteScript(string script, IDbConnection connection) - { - return connection.ExecuteScript(script, batchSeparator: "/"); - } - - #endregion Methods - - } -} \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.Designer.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.Designer.cs index 764b388..c3f064a 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.Designer.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.Designer.cs @@ -19,7 +19,7 @@ namespace DDD.HealthcareDelivery { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class OracleScripts { @@ -60,6 +60,18 @@ internal OracleScripts() { } } + /// + /// Looks up a localized string similar to BEGIN + /// SPCLEARSCHEMA(); + ///END; + ////. + /// + internal static string ClearDatabase { + get { + return ResourceManager.GetString("ClearDatabase", resourceCulture); + } + } + /// /// Looks up a localized string similar to BEGIN /// SPCLEARSCHEMA(); @@ -114,7 +126,7 @@ internal static string FillSchema { /// SPCLEARSCHEMA(); ///END; //// - ///INSERT INTO TEST.Prescription (PrescriptionId, PrescriptionType, Status, Language, CreatedOn, DelivrableAt, PrescriberId, PrescriberType, PrescriberLastName, PrescriberFirstName, PrescriberDisplayName, PrescriberLicenseNum, PrescriberSSN, PrescriberSpeciality, PrescriberPhone1, PrescriberPhone2, PrescriberEmail1, PrescriberEmail2, PrescriberWebSite, PrescriberStreet, PrescriberHouseNum, PrescriberBoxNum, PrescriberPostCode, PrescriberCity, PrescriberCountry, PatientId, Pa [rest of string was truncated]";. + ///INSERT INTO TEST.Prescription (PrescriptionId, PrescriptionType, Status, Language, CreatedOn, DeliverableAt, PrescriberId, PrescriberType, PrescriberLastName, PrescriberFirstName, PrescriberDisplayName, PrescriberLicenseNum, PrescriberSSN, PrescriberSpeciality, PrescriberPhone1, PrescriberPhone2, PrescriberEmail1, PrescriberEmail2, PrescriberWebSite, PrescriberStreet, PrescriberHouseNum, PrescriberBoxNum, PrescriberPostCode, PrescriberCity, PrescriberCountry, PatientId, P [rest of string was truncated]";. /// internal static string FindPharmaceuticalPrescriptionsByPatient { get { @@ -127,7 +139,7 @@ internal static string FindPharmaceuticalPrescriptionsByPatient { /// SPCLEARSCHEMA(); ///END; //// - ///INSERT INTO TEST.Prescription (PrescriptionId, PrescriptionType, Status, Language, CreatedOn, DelivrableAt, PrescriberId, PrescriberType, PrescriberLastName, PrescriberFirstName, PrescriberDisplayName, PrescriberLicenseNum, PrescriberSSN, PrescriberSpeciality, PrescriberPhone1, PrescriberPhone2, PrescriberEmail1, PrescriberEmail2, PrescriberWebSite, PrescriberStreet, PrescriberHouseNum, PrescriberBoxNum, PrescriberPostCode, PrescriberCity, PrescriberCountry, PatientId, Pa [rest of string was truncated]";. + ///INSERT INTO TEST.Prescription (PrescriptionId, PrescriptionType, Status, Language, CreatedOn, DeliverableAt, PrescriberId, PrescriberType, PrescriberLastName, PrescriberFirstName, PrescriberDisplayName, PrescriberLicenseNum, PrescriberSSN, PrescriberSpeciality, PrescriberPhone1, PrescriberPhone2, PrescriberEmail1, PrescriberEmail2, PrescriberWebSite, PrescriberStreet, PrescriberHouseNum, PrescriberBoxNum, PrescriberPostCode, PrescriberCity, PrescriberCountry, PatientId, P [rest of string was truncated]";. /// internal static string FindPrescribedMedicationsByPrescription { get { @@ -140,7 +152,7 @@ internal static string FindPrescribedMedicationsByPrescription { /// SPCLEARSCHEMA(); ///END; //// - ///INSERT INTO TEST.Prescription (PrescriptionId, PrescriptionType, Status, Language, CreatedOn, DelivrableAt, PrescriberId, PrescriberType, PrescriberLastName, PrescriberFirstName, PrescriberDisplayName, PrescriberLicenseNum, PrescriberSSN, PrescriberSpeciality, PrescriberPhone1, PrescriberPhone2, PrescriberEmail1, PrescriberEmail2, PrescriberWebSite, PrescriberStreet, PrescriberHouseNum, PrescriberBoxNum, PrescriberPostCode, PrescriberCity, PrescriberCountry, PatientId, Pa [rest of string was truncated]";. + ///INSERT INTO TEST.Prescription (PrescriptionId, PrescriptionType, Status, Language, CreatedOn, DeliverableAt, PrescriberId, PrescriberType, PrescriberLastName, PrescriberFirstName, PrescriberDisplayName, PrescriberLicenseNum, PrescriberSSN, PrescriberSpeciality, PrescriberPhone1, PrescriberPhone2, PrescriberEmail1, PrescriberEmail2, PrescriberWebSite, PrescriberStreet, PrescriberHouseNum, PrescriberBoxNum, PrescriberPostCode, PrescriberCity, PrescriberCountry, PatientId, P [rest of string was truncated]";. /// internal static string RevokePharmaceuticalPrescription { get { diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.resx b/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.resx index fcef308..ca655d9 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.resx +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.resx @@ -118,6 +118,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + scripts\oracle\cleardatabase.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 + scripts\oracle\createpharmaceuticalprescription.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Properties/AssemblyInfo.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Properties/AssemblyInfo.cs index 869e06e..61ffb3e 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Properties/AssemblyInfo.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Properties/AssemblyInfo.cs @@ -1,20 +1,6 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; using Xunit; -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("DDD.HealthcareDelivery.IntegrationTests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("DDD.HealthcareDelivery.IntegrationTests")] -[assembly: AssemblyCopyright("Copyright © 2017")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. @@ -22,17 +8,4 @@ // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("b53007c7-b314-40df-b6e7-6c6576a5611c")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyTrait("Category", "Integration")] diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/ClearDatabase.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/ClearDatabase.sql new file mode 100644 index 0000000..e14a84a --- /dev/null +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/ClearDatabase.sql @@ -0,0 +1,4 @@ +BEGIN + SPCLEARSCHEMA(); +END; +/ \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql index c6d6ed7..ba0b0f6 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql @@ -1,12 +1,7 @@ -------------------------------------------------------- -- File created - Monday-November-13-2017 --------------------------------------------------------- --------------------------------------------------------- --- DDL for Sequence EVENTID -------------------------------------------------------- - CREATE SEQUENCE TEST.EVENTID MINVALUE 1 MAXVALUE 9999999999999999999999999999 INCREMENT BY 1 START WITH 1 CACHE 20 NOORDER NOCYCLE -/ -------------------------------------------------------- -- DDL for Sequence PRESCMEDICATIONID -------------------------------------------------------- @@ -44,24 +39,15 @@ END SPCLEARSCHEMA; -------------------------------------------------------- CREATE TABLE TEST.EVENT - ( EVENTID NUMBER(19,0), - EVENTTYPE VARCHAR2(50 CHAR), - STREAMID VARCHAR2(50 CHAR), - COMMITID RAW(16), - OCCURREDON TIMESTAMP (6), - SUBJECT VARCHAR2(100 CHAR), - BODY XMLTYPE, - DISPATCHED NUMBER(1,0) - ) SEGMENT CREATION IMMEDIATE - PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING - STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645 - PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT) - TABLESPACE USERS - XMLTYPE COLUMN BODY STORE AS BASICFILE CLOB ( - TABLESPACE USERS ENABLE STORAGE IN ROW CHUNK 8192 PCTVERSION 10 - NOCACHE LOGGING - STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645 - PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT)) + (EVENTID RAW(16) NOT NULL, + EVENTTYPE VARCHAR2(250 CHAR) NOT NULL, + OCCURREDON TIMESTAMP(3) NOT NULL, + BODY VARCHAR2(4000 CHAR) NOT NULL, + BODYFORMAT VARCHAR2(20 CHAR) NOT NULL, + STREAMID VARCHAR2(50 CHAR) NOT NULL, + STREAMTYPE VARCHAR2(50 CHAR) NOT NULL, + ISSUEDBY VARCHAR2(100 CHAR), + CONSTRAINT PK_EVENT PRIMARY KEY(EVENTID)) / -------------------------------------------------------- -- DDL for Table PRESCMEDICATION @@ -73,8 +59,7 @@ END SPCLEARSCHEMA; MEDICATIONTYPE VARCHAR2(20 CHAR), NAMEORDESC VARCHAR2(1024 CHAR), POSOLOGY VARCHAR2(1024 CHAR), - QUANTITY VARCHAR2(100 CHAR), - DURATION VARCHAR2(100 CHAR), + QUANTITY NUMBER(3,0), CODE VARCHAR2(20 CHAR) ) SEGMENT CREATION IMMEDIATE PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING @@ -92,7 +77,7 @@ END SPCLEARSCHEMA; STATUS CHAR(3 CHAR), LANGUAGE CHAR(2 CHAR), CREATEDON DATE, - DELIVRABLEAT DATE, + DELIVERABLEAT DATE, PRESCRIBERID NUMBER(10,0), PRESCRIBERTYPE VARCHAR2(20 CHAR), PRESCRIBERLASTNAME VARCHAR2(50 CHAR), @@ -117,11 +102,8 @@ END SPCLEARSCHEMA; PATIENTLASTNAME VARCHAR2(50 CHAR), PATIENTSEX VARCHAR2(2 CHAR), PATIENTSSN VARCHAR2(25 CHAR), - PATIENTBIRTHDATE DATE, - FACILITYID NUMBER(10,0), - FACILITYTYPE VARCHAR2(20 CHAR), - FACILITYNAME VARCHAR2(100 CHAR), - FACILITYLICENSENUM VARCHAR2(25 CHAR) + PATIENTBIRTHDATE DATE, + ENCOUNTERID NUMBER(10,0) ) SEGMENT CREATION IMMEDIATE PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645 @@ -139,16 +121,6 @@ END SPCLEARSCHEMA; TABLESPACE USERS / -------------------------------------------------------- --- DDL for Index PK_EVENT --------------------------------------------------------- - - CREATE UNIQUE INDEX TEST.PK_EVENT ON TEST.EVENT (EVENTID) - PCTFREE 10 INITRANS 2 MAXTRANS 255 - STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645 - PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT) - TABLESPACE USERS -/ --------------------------------------------------------- -- DDL for Index PK_PRESCMEDICATION -------------------------------------------------------- @@ -169,37 +141,6 @@ END SPCLEARSCHEMA; TABLESPACE USERS / -------------------------------------------------------- --- Constraints for Table EVENT --------------------------------------------------------- - - ALTER TABLE TEST.EVENT ADD CONSTRAINT PK_EVENT PRIMARY KEY (EVENTID) - USING INDEX PCTFREE 10 INITRANS 2 MAXTRANS 255 - STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645 - PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT) - TABLESPACE USERS ENABLE -/ - - ALTER TABLE TEST.EVENT MODIFY (EVENTID NOT NULL ENABLE) -/ - - ALTER TABLE TEST.EVENT MODIFY (EVENTTYPE NOT NULL ENABLE) -/ - - ALTER TABLE TEST.EVENT MODIFY (STREAMID NOT NULL ENABLE) -/ - - ALTER TABLE TEST.EVENT MODIFY (COMMITID NOT NULL ENABLE) -/ - - ALTER TABLE TEST.EVENT MODIFY (OCCURREDON NOT NULL ENABLE) -/ - - ALTER TABLE TEST.EVENT MODIFY (DISPATCHED NOT NULL ENABLE) -/ - - ALTER TABLE TEST.EVENT MODIFY (BODY NOT NULL ENABLE) -/ --------------------------------------------------------- -- Constraints for Table PRESCMEDICATION -------------------------------------------------------- @@ -276,16 +217,6 @@ END SPCLEARSCHEMA; ALTER TABLE TEST.PRESCRIPTION MODIFY (PATIENTSEX NOT NULL ENABLE) / - - ALTER TABLE TEST.PRESCRIPTION MODIFY (FACILITYID NOT NULL ENABLE) -/ - - ALTER TABLE TEST.PRESCRIPTION MODIFY (FACILITYTYPE NOT NULL ENABLE) -/ - - ALTER TABLE TEST.PRESCRIPTION MODIFY (FACILITYNAME NOT NULL ENABLE) -/ - -------------------------------------------------------- -- Ref Constraints for Table PRESCMEDICATION -------------------------------------------------------- diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FindPharmaceuticalPrescriptionsByPatient.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FindPharmaceuticalPrescriptionsByPatient.sql index 4d49bfa..fe9a9d1 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FindPharmaceuticalPrescriptionsByPatient.sql +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FindPharmaceuticalPrescriptionsByPatient.sql @@ -2,15 +2,15 @@ BEGIN SPCLEARSCHEMA(); END; / -INSERT INTO TEST.Prescription (PrescriptionId, PrescriptionType, Status, Language, CreatedOn, DelivrableAt, PrescriberId, PrescriberType, PrescriberLastName, PrescriberFirstName, PrescriberDisplayName, PrescriberLicenseNum, PrescriberSSN, PrescriberSpeciality, PrescriberPhone1, PrescriberPhone2, PrescriberEmail1, PrescriberEmail2, PrescriberWebSite, PrescriberStreet, PrescriberHouseNum, PrescriberBoxNum, PrescriberPostCode, PrescriberCity, PrescriberCountry, PatientId, PatientFirstName, PatientLastName, PatientSex, PatientSSN, PatientBirthdate, FacilityId, FacilityType, FacilityName, FacilityLicenseNum) VALUES (1, N'PHARM', N'CRT', N'FR', TO_DATE(N'2016-12-18','YYYY-MM-DD'), NULL, 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 12601, N'Archibald', N'Haddock', N'M', NULL, TO_DATE(N'1940-12-12','YYYY-MM-DD'), 1, N'MedicalOffice', N'Medical Office Donald Duck', NULL) -/ -INSERT INTO TEST.Prescription (PrescriptionId, PrescriptionType, Status, Language, CreatedOn, DelivrableAt, PrescriberId, PrescriberType, PrescriberLastName, PrescriberFirstName, PrescriberDisplayName, PrescriberLicenseNum, PrescriberSSN, PrescriberSpeciality, PrescriberPhone1, PrescriberPhone2, PrescriberEmail1, PrescriberEmail2, PrescriberWebSite, PrescriberStreet, PrescriberHouseNum, PrescriberBoxNum, PrescriberPostCode, PrescriberCity, PrescriberCountry, PatientId, PatientFirstName, PatientLastName, PatientSex, PatientSSN, PatientBirthdate, FacilityId, FacilityType, FacilityName, FacilityLicenseNum) VALUES (2, N'PHARM', N'CRT', N'FR', TO_DATE(N'2016-12-18','YYYY-MM-DD'), TO_DATE(N'2017-02-18','YYYY-MM-DD'), 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 12601, N'Archibald', N'Haddock', N'M', NULL, TO_DATE(N'1940-12-12','YYYY-MM-DD'), 1, N'MedicalOffice', N'Medical Office Donald Duck', NULL) -/ -INSERT INTO TEST.Prescription (PrescriptionId, PrescriptionType, Status, Language, CreatedOn, DelivrableAt, PrescriberId, PrescriberType, PrescriberLastName, PrescriberFirstName, PrescriberDisplayName, PrescriberLicenseNum, PrescriberSSN, PrescriberSpeciality, PrescriberPhone1, PrescriberPhone2, PrescriberEmail1, PrescriberEmail2, PrescriberWebSite, PrescriberStreet, PrescriberHouseNum, PrescriberBoxNum, PrescriberPostCode, PrescriberCity, PrescriberCountry, PatientId, PatientFirstName, PatientLastName, PatientSex, PatientSSN, PatientBirthdate, FacilityId, FacilityType, FacilityName, FacilityLicenseNum) VALUES (3, N'PHARM', N'CRT', N'FR', TO_DATE(N'2016-12-18','YYYY-MM-DD'), TO_DATE(N'2017-03-18','YYYY-MM-DD'), 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 12601, N'Archibald', N'Haddock', N'M', NULL, TO_DATE(N'1940-12-12','YYYY-MM-DD'), 1, N'MedicalOffice', N'Medical Office Donald Duck', NULL) -/ -INSERT INTO TEST.Prescription (PrescriptionId, PrescriptionType, Status, Language, CreatedOn, DelivrableAt, PrescriberId, PrescriberType, PrescriberLastName, PrescriberFirstName, PrescriberDisplayName, PrescriberLicenseNum, PrescriberSSN, PrescriberSpeciality, PrescriberPhone1, PrescriberPhone2, PrescriberEmail1, PrescriberEmail2, PrescriberWebSite, PrescriberStreet, PrescriberHouseNum, PrescriberBoxNum, PrescriberPostCode, PrescriberCity, PrescriberCountry, PatientId, PatientFirstName, PatientLastName, PatientSex, PatientSSN, PatientBirthdate, FacilityId, FacilityType, FacilityName, FacilityLicenseNum) VALUES (4, N'PHARM', N'CRT', N'FR', TO_DATE(N'2016-12-18','YYYY-MM-DD'), TO_DATE(N'2017-04-18','YYYY-MM-DD'), 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 12601, N'Archibald', N'Haddock', N'M', NULL, TO_DATE(N'1940-12-12','YYYY-MM-DD'), 1, N'MedicalOffice', N'Medical Office Donald Duck', NULL) -/ -INSERT INTO TEST.Prescription (PrescriptionId, PrescriptionType, Status, Language, CreatedOn, DelivrableAt, PrescriberId, PrescriberType, PrescriberLastName, PrescriberFirstName, PrescriberDisplayName, PrescriberLicenseNum, PrescriberSSN, PrescriberSpeciality, PrescriberPhone1, PrescriberPhone2, PrescriberEmail1, PrescriberEmail2, PrescriberWebSite, PrescriberStreet, PrescriberHouseNum, PrescriberBoxNum, PrescriberPostCode, PrescriberCity, PrescriberCountry, PatientId, PatientFirstName, PatientLastName, PatientSex, PatientSSN, PatientBirthdate, FacilityId, FacilityType, FacilityName, FacilityLicenseNum) VALUES (5, N'PHARM', N'CRT', N'FR', TO_DATE(N'2017-09-25','YYYY-MM-DD'), NULL, 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 14314, N'Blanche', N'Neige', N'F', N'54071102651', TO_DATE(N'1954-07-11','YYYY-MM-DD'), 1, N'MedicalOffice', N'Medical Office Donald Duck', NULL) -/ -INSERT INTO TEST.Prescription (PrescriptionId, PrescriptionType, Status, Language, CreatedOn, DelivrableAt, PrescriberId, PrescriberType, PrescriberLastName, PrescriberFirstName, PrescriberDisplayName, PrescriberLicenseNum, PrescriberSSN, PrescriberSpeciality, PrescriberPhone1, PrescriberPhone2, PrescriberEmail1, PrescriberEmail2, PrescriberWebSite, PrescriberStreet, PrescriberHouseNum, PrescriberBoxNum, PrescriberPostCode, PrescriberCity, PrescriberCountry, PatientId, PatientFirstName, PatientLastName, PatientSex, PatientSSN, PatientBirthdate, FacilityId, FacilityType, FacilityName, FacilityLicenseNum) VALUES (6, N'PHARM', N'CRT', N'FR', TO_DATE(N'2017-09-25','YYYY-MM-DD'), TO_DATE(N'2017-12-25','YYYY-MM-DD'), 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 14314, N'Blanche', N'Neige', N'F', N'54071102651', TO_DATE(N'1954-07-11','YYYY-MM-DD'), 1, N'MedicalOffice', N'Medical Office Donald Duck', NULL) +INSERT INTO TEST.Prescription (PrescriptionId, PrescriptionType, Status, Language, CreatedOn, DeliverableAt, PrescriberId, PrescriberType, PrescriberLastName, PrescriberFirstName, PrescriberDisplayName, PrescriberLicenseNum, PrescriberSSN, PrescriberSpeciality, PrescriberPhone1, PrescriberPhone2, PrescriberEmail1, PrescriberEmail2, PrescriberWebSite, PrescriberStreet, PrescriberHouseNum, PrescriberBoxNum, PrescriberPostCode, PrescriberCity, PrescriberCountry, PatientId, PatientFirstName, PatientLastName, PatientSex, PatientSSN, PatientBirthdate, EncounterId) VALUES (1, N'PHARM', N'CRT', N'FR', TO_DATE(N'2016-12-18','YYYY-MM-DD'), NULL, 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 12601, N'Archibald', N'Haddock', N'M', NULL, TO_DATE(N'1940-12-12','YYYY-MM-DD'), 1) +/ +INSERT INTO TEST.Prescription (PrescriptionId, PrescriptionType, Status, Language, CreatedOn, DeliverableAt, PrescriberId, PrescriberType, PrescriberLastName, PrescriberFirstName, PrescriberDisplayName, PrescriberLicenseNum, PrescriberSSN, PrescriberSpeciality, PrescriberPhone1, PrescriberPhone2, PrescriberEmail1, PrescriberEmail2, PrescriberWebSite, PrescriberStreet, PrescriberHouseNum, PrescriberBoxNum, PrescriberPostCode, PrescriberCity, PrescriberCountry, PatientId, PatientFirstName, PatientLastName, PatientSex, PatientSSN, PatientBirthdate, EncounterId) VALUES (2, N'PHARM', N'CRT', N'FR', TO_DATE(N'2016-12-18','YYYY-MM-DD'), TO_DATE(N'2017-02-18','YYYY-MM-DD'), 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 12601, N'Archibald', N'Haddock', N'M', NULL, TO_DATE(N'1940-12-12','YYYY-MM-DD'), NULL) +/ +INSERT INTO TEST.Prescription (PrescriptionId, PrescriptionType, Status, Language, CreatedOn, DeliverableAt, PrescriberId, PrescriberType, PrescriberLastName, PrescriberFirstName, PrescriberDisplayName, PrescriberLicenseNum, PrescriberSSN, PrescriberSpeciality, PrescriberPhone1, PrescriberPhone2, PrescriberEmail1, PrescriberEmail2, PrescriberWebSite, PrescriberStreet, PrescriberHouseNum, PrescriberBoxNum, PrescriberPostCode, PrescriberCity, PrescriberCountry, PatientId, PatientFirstName, PatientLastName, PatientSex, PatientSSN, PatientBirthdate, EncounterId) VALUES (3, N'PHARM', N'CRT', N'FR', TO_DATE(N'2016-12-18','YYYY-MM-DD'), TO_DATE(N'2017-03-18','YYYY-MM-DD'), 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 12601, N'Archibald', N'Haddock', N'M', NULL, TO_DATE(N'1940-12-12','YYYY-MM-DD'), NULL) +/ +INSERT INTO TEST.Prescription (PrescriptionId, PrescriptionType, Status, Language, CreatedOn, DeliverableAt, PrescriberId, PrescriberType, PrescriberLastName, PrescriberFirstName, PrescriberDisplayName, PrescriberLicenseNum, PrescriberSSN, PrescriberSpeciality, PrescriberPhone1, PrescriberPhone2, PrescriberEmail1, PrescriberEmail2, PrescriberWebSite, PrescriberStreet, PrescriberHouseNum, PrescriberBoxNum, PrescriberPostCode, PrescriberCity, PrescriberCountry, PatientId, PatientFirstName, PatientLastName, PatientSex, PatientSSN, PatientBirthdate, EncounterId) VALUES (4, N'PHARM', N'CRT', N'FR', TO_DATE(N'2016-12-18','YYYY-MM-DD'), TO_DATE(N'2017-04-18','YYYY-MM-DD'), 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 12601, N'Archibald', N'Haddock', N'M', NULL, TO_DATE(N'1940-12-12','YYYY-MM-DD'), NULL) +/ +INSERT INTO TEST.Prescription (PrescriptionId, PrescriptionType, Status, Language, CreatedOn, DeliverableAt, PrescriberId, PrescriberType, PrescriberLastName, PrescriberFirstName, PrescriberDisplayName, PrescriberLicenseNum, PrescriberSSN, PrescriberSpeciality, PrescriberPhone1, PrescriberPhone2, PrescriberEmail1, PrescriberEmail2, PrescriberWebSite, PrescriberStreet, PrescriberHouseNum, PrescriberBoxNum, PrescriberPostCode, PrescriberCity, PrescriberCountry, PatientId, PatientFirstName, PatientLastName, PatientSex, PatientSSN, PatientBirthdate, EncounterId) VALUES (5, N'PHARM', N'CRT', N'FR', TO_DATE(N'2017-09-25','YYYY-MM-DD'), NULL, 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 14314, N'Blanche', N'Neige', N'F', N'54071102651', TO_DATE(N'1954-07-11','YYYY-MM-DD'), NULL) +/ +INSERT INTO TEST.Prescription (PrescriptionId, PrescriptionType, Status, Language, CreatedOn, DeliverableAt, PrescriberId, PrescriberType, PrescriberLastName, PrescriberFirstName, PrescriberDisplayName, PrescriberLicenseNum, PrescriberSSN, PrescriberSpeciality, PrescriberPhone1, PrescriberPhone2, PrescriberEmail1, PrescriberEmail2, PrescriberWebSite, PrescriberStreet, PrescriberHouseNum, PrescriberBoxNum, PrescriberPostCode, PrescriberCity, PrescriberCountry, PatientId, PatientFirstName, PatientLastName, PatientSex, PatientSSN, PatientBirthdate, EncounterId) VALUES (6, N'PHARM', N'CRT', N'FR', TO_DATE(N'2017-09-25','YYYY-MM-DD'), TO_DATE(N'2017-12-25','YYYY-MM-DD'), 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 14314, N'Blanche', N'Neige', N'F', N'54071102651', TO_DATE(N'1954-07-11','YYYY-MM-DD'), NULL) / diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FindPrescribedMedicationsByPrescription.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FindPrescribedMedicationsByPrescription.sql index 5a3cbd8..48c1dc7 100644 Binary files a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FindPrescribedMedicationsByPrescription.sql and b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FindPrescribedMedicationsByPrescription.sql differ diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/RevokePharmaceuticalPrescription.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/RevokePharmaceuticalPrescription.sql index e2ad356..8995718 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/RevokePharmaceuticalPrescription.sql +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/RevokePharmaceuticalPrescription.sql @@ -2,9 +2,9 @@ BEGIN SPCLEARSCHEMA(); END; / -INSERT INTO TEST.Prescription (PrescriptionId, PrescriptionType, Status, Language, CreatedOn, DelivrableAt, PrescriberId, PrescriberType, PrescriberLastName, PrescriberFirstName, PrescriberDisplayName, PrescriberLicenseNum, PrescriberSSN, PrescriberSpeciality, PrescriberPhone1, PrescriberPhone2, PrescriberEmail1, PrescriberEmail2, PrescriberWebSite, PrescriberStreet, PrescriberHouseNum, PrescriberBoxNum, PrescriberPostCode, PrescriberCity, PrescriberCountry, PatientId, PatientFirstName, PatientLastName, PatientSex, PatientSSN, PatientBirthdate, FacilityId, FacilityType, FacilityName, FacilityLicenseNum) VALUES (1, N'PHARM', N'CRT', N'FR', TO_DATE(N'2016-12-18','YYYY-MM-DD'), NULL, 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 12601, N'Archibald', N'Haddock', N'M', NULL, TO_DATE(N'1940-12-12','YYYY-MM-DD'), 1, N'MedicalOffice', N'Medical Office Donald Duck', NULL) +INSERT INTO TEST.Prescription (PrescriptionId, PrescriptionType, Status, Language, CreatedOn, DeliverableAt, PrescriberId, PrescriberType, PrescriberLastName, PrescriberFirstName, PrescriberDisplayName, PrescriberLicenseNum, PrescriberSSN, PrescriberSpeciality, PrescriberPhone1, PrescriberPhone2, PrescriberEmail1, PrescriberEmail2, PrescriberWebSite, PrescriberStreet, PrescriberHouseNum, PrescriberBoxNum, PrescriberPostCode, PrescriberCity, PrescriberCountry, PatientId, PatientFirstName, PatientLastName, PatientSex, PatientSSN, PatientBirthdate, EncounterId) VALUES (1, N'PHARM', N'CRT', N'FR', TO_DATE(N'2016-12-18','YYYY-MM-DD'), NULL, 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 12601, N'Archibald', N'Haddock', N'M', NULL, TO_DATE(N'1940-12-12','YYYY-MM-DD'), 1) / -INSERT INTO TEST.PrescMedication (PrescMedicationId, PrescriptionId, MedicationType, NameOrDesc, Posology, Quantity, Duration, Code) VALUES (1, 1, N'Product', N'Latansoc Mylan Coll. 2,5 ml X 3', N'1 goutte le soir', N'1 bote de 3 flacons', NULL, NULL) +INSERT INTO TEST.PrescMedication (PrescMedicationId, PrescriptionId, MedicationType, NameOrDesc, Posology, Quantity, Code) VALUES (1, 1, N'Product', N'Latansoc Mylan Coll. 2,5 ml X 3', N'1 goutte le soir', 1, NULL) / -INSERT INTO TEST.PrescMedication (PrescMedicationId, PrescriptionId, MedicationType, NameOrDesc, Posology, Quantity, Duration, Code) VALUES (2, 1, N'Product', N'Dualkopt Coll. 10 ml', N'1 goutte 2 x/jour', N'1 flacon', NULL, N'3260072') +INSERT INTO TEST.PrescMedication (PrescMedicationId, PrescriptionId, MedicationType, NameOrDesc, Posology, Quantity, Code) VALUES (2, 1, N'Product', N'Dualkopt Coll. 10 ml', N'1 goutte 2 x/jour', 1, N'3260072') / \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/ClearDatabase.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/ClearDatabase.sql new file mode 100644 index 0000000..f8d4eae --- /dev/null +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/ClearDatabase.sql @@ -0,0 +1,4 @@ +USE [Test] +GO +EXEC spClearDatabase +GO \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql index c11bd20..91d9d5a 100644 Binary files a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql and b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql differ diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/FindPharmaceuticalPrescriptionsByPatient.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/FindPharmaceuticalPrescriptionsByPatient.sql index 8cf2915..173ba4f 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/FindPharmaceuticalPrescriptionsByPatient.sql +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/FindPharmaceuticalPrescriptionsByPatient.sql @@ -2,15 +2,15 @@ USE [Test] GO EXEC spClearDatabase GO -INSERT [dbo].[Prescription] ([PrescriptionId], [PrescriptionType], [Status], [Language], [CreatedOn], [DelivrableAt], [PrescriberId], [PrescriberType], [PrescriberLastName], [PrescriberFirstName], [PrescriberDisplayName], [PrescriberLicenseNum], [PrescriberSSN], [PrescriberSpeciality], [PrescriberPhone1], [PrescriberPhone2], [PrescriberEmail1], [PrescriberEmail2], [PrescriberWebSite], [PrescriberStreet], [PrescriberHouseNum], [PrescriberBoxNum], [PrescriberPostCode], [PrescriberCity], [PrescriberCountry], [PatientId], [PatientFirstName], [PatientLastName], [PatientSex], [PatientSSN], [PatientBirthdate], [FacilityId], [FacilityType], [FacilityName], [FacilityLicenseNum]) VALUES (1, N'PHARM', N'CRT', N'FR', CAST(N'2016-12-18 00:00:00.000' AS DateTime), NULL, 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 12601, N'Archibald', N'Haddock', N'M', NULL, CAST(N'1940-12-12 00:00:00.000' AS DateTime), 1, N'MedicalOffice', N'Medical Office Donald Duck', NULL) -GO -INSERT [dbo].[Prescription] ([PrescriptionId], [PrescriptionType], [Status], [Language], [CreatedOn], [DelivrableAt], [PrescriberId], [PrescriberType], [PrescriberLastName], [PrescriberFirstName], [PrescriberDisplayName], [PrescriberLicenseNum], [PrescriberSSN], [PrescriberSpeciality], [PrescriberPhone1], [PrescriberPhone2], [PrescriberEmail1], [PrescriberEmail2], [PrescriberWebSite], [PrescriberStreet], [PrescriberHouseNum], [PrescriberBoxNum], [PrescriberPostCode], [PrescriberCity], [PrescriberCountry], [PatientId], [PatientFirstName], [PatientLastName], [PatientSex], [PatientSSN], [PatientBirthdate], [FacilityId], [FacilityType], [FacilityName], [FacilityLicenseNum]) VALUES (2, N'PHARM', N'CRT', N'FR', CAST(N'2016-12-18 00:00:00.000' AS DateTime), CAST(N'2017-02-18 00:00:00.000' AS DateTime), 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 12601, N'Archibald', N'Haddock', N'M', NULL, CAST(N'1940-12-12 00:00:00.000' AS DateTime), 1, N'MedicalOffice', N'Medical Office Donald Duck', NULL) -GO -INSERT [dbo].[Prescription] ([PrescriptionId], [PrescriptionType], [Status], [Language], [CreatedOn], [DelivrableAt], [PrescriberId], [PrescriberType], [PrescriberLastName], [PrescriberFirstName], [PrescriberDisplayName], [PrescriberLicenseNum], [PrescriberSSN], [PrescriberSpeciality], [PrescriberPhone1], [PrescriberPhone2], [PrescriberEmail1], [PrescriberEmail2], [PrescriberWebSite], [PrescriberStreet], [PrescriberHouseNum], [PrescriberBoxNum], [PrescriberPostCode], [PrescriberCity], [PrescriberCountry], [PatientId], [PatientFirstName], [PatientLastName], [PatientSex], [PatientSSN], [PatientBirthdate], [FacilityId], [FacilityType], [FacilityName], [FacilityLicenseNum]) VALUES (3, N'PHARM', N'CRT', N'FR', CAST(N'2016-12-18 00:00:00.000' AS DateTime), CAST(N'2017-03-18 00:00:00.000' AS DateTime), 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 12601, N'Archibald', N'Haddock', N'M', NULL, CAST(N'1940-12-12 00:00:00.000' AS DateTime), 1, N'MedicalOffice', N'Medical Office Donald Duck', NULL) -GO -INSERT [dbo].[Prescription] ([PrescriptionId], [PrescriptionType], [Status], [Language], [CreatedOn], [DelivrableAt], [PrescriberId], [PrescriberType], [PrescriberLastName], [PrescriberFirstName], [PrescriberDisplayName], [PrescriberLicenseNum], [PrescriberSSN], [PrescriberSpeciality], [PrescriberPhone1], [PrescriberPhone2], [PrescriberEmail1], [PrescriberEmail2], [PrescriberWebSite], [PrescriberStreet], [PrescriberHouseNum], [PrescriberBoxNum], [PrescriberPostCode], [PrescriberCity], [PrescriberCountry], [PatientId], [PatientFirstName], [PatientLastName], [PatientSex], [PatientSSN], [PatientBirthdate], [FacilityId], [FacilityType], [FacilityName], [FacilityLicenseNum]) VALUES (4, N'PHARM', N'CRT', N'FR', CAST(N'2016-12-18 00:00:00.000' AS DateTime), CAST(N'2017-04-18 00:00:00.000' AS DateTime), 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 12601, N'Archibald', N'Haddock', N'M', NULL, CAST(N'1940-12-12 00:00:00.000' AS DateTime), 1, N'MedicalOffice', N'Medical Office Donald Duck', NULL) -GO -INSERT [dbo].[Prescription] ([PrescriptionId], [PrescriptionType], [Status], [Language], [CreatedOn], [DelivrableAt], [PrescriberId], [PrescriberType], [PrescriberLastName], [PrescriberFirstName], [PrescriberDisplayName], [PrescriberLicenseNum], [PrescriberSSN], [PrescriberSpeciality], [PrescriberPhone1], [PrescriberPhone2], [PrescriberEmail1], [PrescriberEmail2], [PrescriberWebSite], [PrescriberStreet], [PrescriberHouseNum], [PrescriberBoxNum], [PrescriberPostCode], [PrescriberCity], [PrescriberCountry], [PatientId], [PatientFirstName], [PatientLastName], [PatientSex], [PatientSSN], [PatientBirthdate], [FacilityId], [FacilityType], [FacilityName], [FacilityLicenseNum]) VALUES (5, N'PHARM', N'CRT', N'FR', CAST(N'2017-09-25 00:00:00.000' AS DateTime), NULL, 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 14314, N'Blanche', N'Neige', N'F', N'54071102651', CAST(N'1954-07-11 00:00:00.000' AS DateTime), 1, N'MedicalOffice', N'Medical Office Donald Duck', NULL) -GO -INSERT [dbo].[Prescription] ([PrescriptionId], [PrescriptionType], [Status], [Language], [CreatedOn], [DelivrableAt], [PrescriberId], [PrescriberType], [PrescriberLastName], [PrescriberFirstName], [PrescriberDisplayName], [PrescriberLicenseNum], [PrescriberSSN], [PrescriberSpeciality], [PrescriberPhone1], [PrescriberPhone2], [PrescriberEmail1], [PrescriberEmail2], [PrescriberWebSite], [PrescriberStreet], [PrescriberHouseNum], [PrescriberBoxNum], [PrescriberPostCode], [PrescriberCity], [PrescriberCountry], [PatientId], [PatientFirstName], [PatientLastName], [PatientSex], [PatientSSN], [PatientBirthdate], [FacilityId], [FacilityType], [FacilityName], [FacilityLicenseNum]) VALUES (6, N'PHARM', N'CRT', N'FR', CAST(N'2017-09-25 00:00:00.000' AS DateTime), CAST(N'2017-12-25 00:00:00.000' AS DateTime), 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 14314, N'Blanche', N'Neige', N'F', N'54071102651', CAST(N'1954-07-11 00:00:00.000' AS DateTime), 1, N'MedicalOffice', N'Medical Office Donald Duck', NULL) +INSERT [dbo].[Prescription] ([PrescriptionId], [PrescriptionType], [Status], [Language], [CreatedOn], [DeliverableAt], [PrescriberId], [PrescriberType], [PrescriberLastName], [PrescriberFirstName], [PrescriberDisplayName], [PrescriberLicenseNum], [PrescriberSSN], [PrescriberSpeciality], [PrescriberPhone1], [PrescriberPhone2], [PrescriberEmail1], [PrescriberEmail2], [PrescriberWebSite], [PrescriberStreet], [PrescriberHouseNum], [PrescriberBoxNum], [PrescriberPostCode], [PrescriberCity], [PrescriberCountry], [PatientId], [PatientFirstName], [PatientLastName], [PatientSex], [PatientSSN], [PatientBirthdate], [EncounterId]) VALUES (1, N'PHARM', N'CRT', N'FR', CAST(N'2016-12-18 00:00:00.000' AS DateTime), NULL, 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 12601, N'Archibald', N'Haddock', N'M', NULL, CAST(N'1940-12-12 00:00:00.000' AS DateTime), 1) +GO +INSERT [dbo].[Prescription] ([PrescriptionId], [PrescriptionType], [Status], [Language], [CreatedOn], [DeliverableAt], [PrescriberId], [PrescriberType], [PrescriberLastName], [PrescriberFirstName], [PrescriberDisplayName], [PrescriberLicenseNum], [PrescriberSSN], [PrescriberSpeciality], [PrescriberPhone1], [PrescriberPhone2], [PrescriberEmail1], [PrescriberEmail2], [PrescriberWebSite], [PrescriberStreet], [PrescriberHouseNum], [PrescriberBoxNum], [PrescriberPostCode], [PrescriberCity], [PrescriberCountry], [PatientId], [PatientFirstName], [PatientLastName], [PatientSex], [PatientSSN], [PatientBirthdate], [EncounterId]) VALUES (2, N'PHARM', N'CRT', N'FR', CAST(N'2016-12-18 00:00:00.000' AS DateTime), CAST(N'2017-02-18 00:00:00.000' AS DateTime), 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 12601, N'Archibald', N'Haddock', N'M', NULL, CAST(N'1940-12-12 00:00:00.000' AS DateTime), NULL) +GO +INSERT [dbo].[Prescription] ([PrescriptionId], [PrescriptionType], [Status], [Language], [CreatedOn], [DeliverableAt], [PrescriberId], [PrescriberType], [PrescriberLastName], [PrescriberFirstName], [PrescriberDisplayName], [PrescriberLicenseNum], [PrescriberSSN], [PrescriberSpeciality], [PrescriberPhone1], [PrescriberPhone2], [PrescriberEmail1], [PrescriberEmail2], [PrescriberWebSite], [PrescriberStreet], [PrescriberHouseNum], [PrescriberBoxNum], [PrescriberPostCode], [PrescriberCity], [PrescriberCountry], [PatientId], [PatientFirstName], [PatientLastName], [PatientSex], [PatientSSN], [PatientBirthdate], [EncounterId]) VALUES (3, N'PHARM', N'CRT', N'FR', CAST(N'2016-12-18 00:00:00.000' AS DateTime), CAST(N'2017-03-18 00:00:00.000' AS DateTime), 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 12601, N'Archibald', N'Haddock', N'M', NULL, CAST(N'1940-12-12 00:00:00.000' AS DateTime), NULL) +GO +INSERT [dbo].[Prescription] ([PrescriptionId], [PrescriptionType], [Status], [Language], [CreatedOn], [DeliverableAt], [PrescriberId], [PrescriberType], [PrescriberLastName], [PrescriberFirstName], [PrescriberDisplayName], [PrescriberLicenseNum], [PrescriberSSN], [PrescriberSpeciality], [PrescriberPhone1], [PrescriberPhone2], [PrescriberEmail1], [PrescriberEmail2], [PrescriberWebSite], [PrescriberStreet], [PrescriberHouseNum], [PrescriberBoxNum], [PrescriberPostCode], [PrescriberCity], [PrescriberCountry], [PatientId], [PatientFirstName], [PatientLastName], [PatientSex], [PatientSSN], [PatientBirthdate], [EncounterId]) VALUES (4, N'PHARM', N'CRT', N'FR', CAST(N'2016-12-18 00:00:00.000' AS DateTime), CAST(N'2017-04-18 00:00:00.000' AS DateTime), 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 12601, N'Archibald', N'Haddock', N'M', NULL, CAST(N'1940-12-12 00:00:00.000' AS DateTime), NULL) +GO +INSERT [dbo].[Prescription] ([PrescriptionId], [PrescriptionType], [Status], [Language], [CreatedOn], [DeliverableAt], [PrescriberId], [PrescriberType], [PrescriberLastName], [PrescriberFirstName], [PrescriberDisplayName], [PrescriberLicenseNum], [PrescriberSSN], [PrescriberSpeciality], [PrescriberPhone1], [PrescriberPhone2], [PrescriberEmail1], [PrescriberEmail2], [PrescriberWebSite], [PrescriberStreet], [PrescriberHouseNum], [PrescriberBoxNum], [PrescriberPostCode], [PrescriberCity], [PrescriberCountry], [PatientId], [PatientFirstName], [PatientLastName], [PatientSex], [PatientSSN], [PatientBirthdate], [EncounterId]) VALUES (5, N'PHARM', N'CRT', N'FR', CAST(N'2017-09-25 00:00:00.000' AS DateTime), NULL, 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 14314, N'Blanche', N'Neige', N'F', N'54071102651', CAST(N'1954-07-11 00:00:00.000' AS DateTime), NULL) +GO +INSERT [dbo].[Prescription] ([PrescriptionId], [PrescriptionType], [Status], [Language], [CreatedOn], [DeliverableAt], [PrescriberId], [PrescriberType], [PrescriberLastName], [PrescriberFirstName], [PrescriberDisplayName], [PrescriberLicenseNum], [PrescriberSSN], [PrescriberSpeciality], [PrescriberPhone1], [PrescriberPhone2], [PrescriberEmail1], [PrescriberEmail2], [PrescriberWebSite], [PrescriberStreet], [PrescriberHouseNum], [PrescriberBoxNum], [PrescriberPostCode], [PrescriberCity], [PrescriberCountry], [PatientId], [PatientFirstName], [PatientLastName], [PatientSex], [PatientSSN], [PatientBirthdate], [EncounterId]) VALUES (6, N'PHARM', N'CRT', N'FR', CAST(N'2017-09-25 00:00:00.000' AS DateTime), CAST(N'2017-12-25 00:00:00.000' AS DateTime), 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 14314, N'Blanche', N'Neige', N'F', N'54071102651', CAST(N'1954-07-11 00:00:00.000' AS DateTime), NULL) GO diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/FindPrescribedMedicationsByPrescription.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/FindPrescribedMedicationsByPrescription.sql index 344c68e..2d56e5b 100644 Binary files a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/FindPrescribedMedicationsByPrescription.sql and b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/FindPrescribedMedicationsByPrescription.sql differ diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/RevokePharmaceuticalPrescription.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/RevokePharmaceuticalPrescription.sql index e8dec88..95c8a16 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/RevokePharmaceuticalPrescription.sql +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/RevokePharmaceuticalPrescription.sql @@ -2,9 +2,9 @@ USE [Test] GO EXEC spClearDatabase GO -INSERT [dbo].[Prescription] ([PrescriptionId], [PrescriptionType], [Status], [Language], [CreatedOn], [DelivrableAt], [PrescriberId], [PrescriberType], [PrescriberLastName], [PrescriberFirstName], [PrescriberDisplayName], [PrescriberLicenseNum], [PrescriberSSN], [PrescriberSpeciality], [PrescriberPhone1], [PrescriberPhone2], [PrescriberEmail1], [PrescriberEmail2], [PrescriberWebSite], [PrescriberStreet], [PrescriberHouseNum], [PrescriberBoxNum], [PrescriberPostCode], [PrescriberCity], [PrescriberCountry], [PatientId], [PatientFirstName], [PatientLastName], [PatientSex], [PatientSSN], [PatientBirthdate], [FacilityId], [FacilityType], [FacilityName], [FacilityLicenseNum]) VALUES (1, N'PHARM', N'CRT', N'FR', CAST(N'2016-12-18 00:00:00.000' AS DateTime), NULL, 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 12601, N'Archibald', N'Haddock', N'M', NULL, CAST(N'1940-12-12 00:00:00.000' AS DateTime), 1, N'MedicalOffice', N'Medical Office Donald Duck', NULL) +INSERT [dbo].[Prescription] ([PrescriptionId], [PrescriptionType], [Status], [Language], [CreatedOn], [DeliverableAt], [PrescriberId], [PrescriberType], [PrescriberLastName], [PrescriberFirstName], [PrescriberDisplayName], [PrescriberLicenseNum], [PrescriberSSN], [PrescriberSpeciality], [PrescriberPhone1], [PrescriberPhone2], [PrescriberEmail1], [PrescriberEmail2], [PrescriberWebSite], [PrescriberStreet], [PrescriberHouseNum], [PrescriberBoxNum], [PrescriberPostCode], [PrescriberCity], [PrescriberCountry], [PatientId], [PatientFirstName], [PatientLastName], [PatientSex], [PatientSSN], [PatientBirthdate], [EncounterId]) VALUES (1, N'PHARM', N'CRT', N'FR', CAST(N'2016-12-18 00:00:00.000' AS DateTime), NULL, 1, N'Physician', N'Duck', N'Donald', N'Dr. Duck Donald', N'16480793370', NULL, N'Ophtalmologie', N'02/221.21.21', NULL, N'donald.duck@gmail.com', NULL, NULL, N'Grote Markt 7', NULL, NULL, N'1000', N'Brussel', NULL, 12601, N'Archibald', N'Haddock', N'M', NULL, CAST(N'1940-12-12 00:00:00.000' AS DateTime), 1) GO -INSERT [dbo].[PrescMedication] ([PrescMedicationId], [PrescriptionId], [MedicationType], [NameOrDesc], [Posology], [Quantity], [Duration], [Code]) VALUES (1, 1, N'Product', N'Latansoc Mylan Coll. 2,5 ml X 3', N'1 goutte le soir', N'1 bote de 3 flacons', NULL, NULL) +INSERT [dbo].[PrescMedication] ([PrescMedicationId], [PrescriptionId], [MedicationType], [NameOrDesc], [Posology], [Quantity], [Code]) VALUES (1, 1, N'Product', N'Latansoc Mylan Coll. 2,5 ml X 3', N'1 goutte le soir', 1, NULL) GO -INSERT [dbo].[PrescMedication] ([PrescMedicationId], [PrescriptionId], [MedicationType], [NameOrDesc], [Posology], [Quantity], [Duration], [Code]) VALUES (2, 1, N'Product', N'Dualkopt Coll. 10 ml', N'1 goutte 2 x/jour', N'1 flacon', NULL, N'3260072') +INSERT [dbo].[PrescMedication] ([PrescMedicationId], [PrescriptionId], [MedicationType], [NameOrDesc], [Posology], [Quantity], [Code]) VALUES (2, 1, N'Product', N'Dualkopt Coll. 10 ml', N'1 goutte 2 x/jour', 1, N'3260072') GO \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerConnectionFactory.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerConnectionFactory.cs deleted file mode 100644 index ba0ac05..0000000 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerConnectionFactory.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace DDD.HealthcareDelivery -{ - using Infrastructure; - using Core.Infrastructure.Data; - - public class SqlServerConnectionFactory : DbConnectionFactory, IHealthcareConnectionFactory - { - public SqlServerConnectionFactory() - : base("System.Data.SqlClient", - @"Data Source=(local)\SQLEXPRESS;Database=Test;Integrated Security=False;User ID=sa;Password=dev;Pooling=false") - { - } - } -} diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerFixture.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerFixture.cs deleted file mode 100644 index 2c5c8ef..0000000 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerFixture.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Data.SqlClient; -using System.Data; - -namespace DDD.HealthcareDelivery -{ - using Core.Infrastructure.Testing; - using Core.Infrastructure.Data; - - public class SqlServerFixture : DbFixture - { - - #region Constructors - - public SqlServerFixture() : base(new SqlServerConnectionFactory(), "SqlServerScripts") - { - } - - #endregion Constructors - - #region Methods - - protected override void CreateDatabase() - { - using (var connection = this.ConnectionFactory.CreateConnection()) - { - var builder = new SqlConnectionStringBuilder(connection.ConnectionString) { InitialCatalog = "master" }; - connection.ConnectionString = builder.ConnectionString; - connection.Open(); - this.ExecuteScript(SqlServerScripts.CreateDatabase, connection); - } - } - - protected override int[] ExecuteScript(string script, IDbConnection connection) - { - return connection.ExecuteScript(script); - } - - #endregion Methods - } -} \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerScripts.Designer.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerScripts.Designer.cs index 3421b35..0be59f3 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerScripts.Designer.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerScripts.Designer.cs @@ -60,6 +60,18 @@ internal SqlServerScripts() { } } + /// + /// Looks up a localized string similar to USE [Test] + ///GO + ///EXEC spClearDatabase + ///GO. + /// + internal static string ClearDatabase { + get { + return ResourceManager.GetString("ClearDatabase", resourceCulture); + } + } + /// /// Looks up a localized string similar to /****** Object: Database [Test] Script Date: 16/08/2017 12:09:20 ******/ ///USE [master] @@ -83,24 +95,12 @@ internal static string CreateDatabase { } } - /// - /// Looks up a localized string similar to USE [Test] - ///GO - ///EXEC spClearDatabase - ///GO. - /// - internal static string CreatePharmaceuticalPrescription { - get { - return ResourceManager.GetString("CreatePharmaceuticalPrescription", resourceCulture); - } - } - /// /// Looks up a localized string similar to USE [Test] ///GO ///EXEC spClearDatabase ///GO - ///INSERT [dbo].[Prescription] ([PrescriptionId], [PrescriptionType], [Status], [Language], [CreatedOn], [DelivrableAt], [PrescriberId], [PrescriberType], [PrescriberLastName], [PrescriberFirstName], [PrescriberDisplayName], [PrescriberLicenseNum], [PrescriberSSN], [PrescriberSpeciality], [PrescriberPhone1], [PrescriberPhone2], [PrescriberEmail1], [PrescriberEmail2], [PrescriberWebSite], [PrescriberStreet], [PrescriberHouseNum], [PrescriberBoxNum], [PrescriberPostCode] [rest of string was truncated]";. + ///INSERT [dbo].[Prescription] ([PrescriptionId], [PrescriptionType], [Status], [Language], [CreatedOn], [DeliverableAt], [PrescriberId], [PrescriberType], [PrescriberLastName], [PrescriberFirstName], [PrescriberDisplayName], [PrescriberLicenseNum], [PrescriberSSN], [PrescriberSpeciality], [PrescriberPhone1], [PrescriberPhone2], [PrescriberEmail1], [PrescriberEmail2], [PrescriberWebSite], [PrescriberStreet], [PrescriberHouseNum], [PrescriberBoxNum], [PrescriberPostCode] [rest of string was truncated]";. /// internal static string FindPharmaceuticalPrescriptionsByPatient { get { @@ -113,7 +113,7 @@ internal static string FindPharmaceuticalPrescriptionsByPatient { ///GO ///EXEC spClearDatabase ///GO - ///INSERT [dbo].[Prescription] ([PrescriptionId], [PrescriptionType], [Status], [Language], [CreatedOn], [DelivrableAt], [PrescriberId], [PrescriberType], [PrescriberLastName], [PrescriberFirstName], [PrescriberDisplayName], [PrescriberLicenseNum], [PrescriberSSN], [PrescriberSpeciality], [PrescriberPhone1], [PrescriberPhone2], [PrescriberEmail1], [PrescriberEmail2], [PrescriberWebSite], [PrescriberStreet], [PrescriberHouseNum], [PrescriberBoxNum], [PrescriberPostCode] [rest of string was truncated]";. + ///INSERT [dbo].[Prescription] ([PrescriptionId], [PrescriptionType], [Status], [Language], [CreatedOn], [DeliverableAt], [PrescriberId], [PrescriberType], [PrescriberLastName], [PrescriberFirstName], [PrescriberDisplayName], [PrescriberLicenseNum], [PrescriberSSN], [PrescriberSpeciality], [PrescriberPhone1], [PrescriberPhone2], [PrescriberEmail1], [PrescriberEmail2], [PrescriberWebSite], [PrescriberStreet], [PrescriberHouseNum], [PrescriberBoxNum], [PrescriberPostCode] [rest of string was truncated]";. /// internal static string FindPrescribedMedicationsByPrescription { get { @@ -126,7 +126,7 @@ internal static string FindPrescribedMedicationsByPrescription { ///GO ///EXEC spClearDatabase ///GO - ///INSERT [dbo].[Prescription] ([PrescriptionId], [PrescriptionType], [Status], [Language], [CreatedOn], [DelivrableAt], [PrescriberId], [PrescriberType], [PrescriberLastName], [PrescriberFirstName], [PrescriberDisplayName], [PrescriberLicenseNum], [PrescriberSSN], [PrescriberSpeciality], [PrescriberPhone1], [PrescriberPhone2], [PrescriberEmail1], [PrescriberEmail2], [PrescriberWebSite], [PrescriberStreet], [PrescriberHouseNum], [PrescriberBoxNum], [PrescriberPostCode] [rest of string was truncated]";. + ///INSERT [dbo].[Prescription] ([PrescriptionId], [PrescriptionType], [Status], [Language], [CreatedOn], [DeliverableAt], [PrescriberId], [PrescriberType], [PrescriberLastName], [PrescriberFirstName], [PrescriberDisplayName], [PrescriberLicenseNum], [PrescriberSSN], [PrescriberSpeciality], [PrescriberPhone1], [PrescriberPhone2], [PrescriberEmail1], [PrescriberEmail2], [PrescriberWebSite], [PrescriberStreet], [PrescriberHouseNum], [PrescriberBoxNum], [PrescriberPostCode] [rest of string was truncated]";. /// internal static string RevokePharmaceuticalPrescription { get { diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerScripts.resx b/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerScripts.resx index b600bbc..fe2e0d5 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerScripts.resx +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerScripts.resx @@ -118,6 +118,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + scripts\sqlserver\cleardatabase.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 + scripts\sqlserver\createdatabase.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/packages.config b/Test/DDD.HealthcareDelivery.IntegrationTests/packages.config deleted file mode 100644 index 8964d78..0000000 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/packages.config +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/testhost.dll.config b/Test/DDD.HealthcareDelivery.IntegrationTests/testhost.dll.config new file mode 100644 index 0000000..c013f8d --- /dev/null +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/testhost.dll.config @@ -0,0 +1,12 @@ + + + + +
+ + + + + + \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj b/Test/DDD.HealthcareDelivery.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj index 02d1a05..88925ce 100644 --- a/Test/DDD.HealthcareDelivery.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj +++ b/Test/DDD.HealthcareDelivery.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj @@ -1,146 +1,39 @@ - - - - - + - Debug - AnyCPU - {CA376D7C-2A71-4518-B297-4C4A08DBF19D} + net48;net6.0 Library - Properties DDD.HealthcareDelivery - DDD.HealthcareDelivery.UnitTests - v4.7.2 - 512 - - - + false - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - L:\Packages\FluentAssertions.5.6.0\lib\net47\FluentAssertions.dll - - - L:\Packages\FluentValidation.8.1.3\lib\net45\FluentValidation.dll - - - - - L:\packages\System.ComponentModel.Primitives.4.3.0\lib\net45\System.ComponentModel.Primitives.dll - True - - - - L:\packages\System.ValueTuple.4.5.0\lib\netstandard1.0\System.ValueTuple.dll - True - - - - - - - - - L:\packages\xunit.abstractions.2.0.3\lib\net35\xunit.abstractions.dll - True - - - L:\packages\xunit.assert.2.4.1\lib\netstandard1.1\xunit.assert.dll - - - L:\packages\xunit.extensibility.core.2.4.1\lib\net452\xunit.core.dll - - - L:\packages\xunit.extensibility.execution.2.4.1\lib\net452\xunit.execution.desktop.dll - - - - - - - - - - - - - {0b70b4fd-f5a0-4a6c-a3fd-90031e08c1c2} - DDD.Common - - - {5e3745fc-ca80-4d0f-8a25-20ee0f9cf163} - DDD.Core.FluentValidation - - - {596a8700-3d18-4a62-b200-1f78a9ea4617} - DDD.Core.Abstractions - - - {2438B31A-3A39-4878-81FA-BE5AE715EAE5} - DDD.Core.Messages - - - {62b652de-4cc9-4b85-8ae9-d66ceea358dd} - DDD.Core.Xunit - - - {c6c3e419-b9aa-44ad-9dbf-789294687ae6} - DDD.Core - - - {b8bb212c-8afc-4258-a023-eb1f6937f53d} - DDD.HealthcareDelivery.Messages - - - {5b8fffd3-9a1c-4620-9db3-cd76cd9e79bf} - DDD.HealthcareDelivery - + + + + + + + + - - - - - + + 6.9.0 + + + 11.4.0 + + + + + + + + 2.4.5 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - - \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.UnitTests/Domain/Facilities/BelgianHealthFacilityLicenseNumberTests.cs b/Test/DDD.HealthcareDelivery.UnitTests/Domain/Facilities/BelgianHealthFacilityLicenseNumberTests.cs deleted file mode 100644 index 8db3b2f..0000000 --- a/Test/DDD.HealthcareDelivery.UnitTests/Domain/Facilities/BelgianHealthFacilityLicenseNumberTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -using FluentAssertions; -using Xunit; - -namespace DDD.HealthcareDelivery.Domain.Facilities -{ - public class BelgianHealthFacilityLicenseNumberTests - { - - #region Methods - - [Theory] - [InlineData("78901184", 84)] - [InlineData("73000418210", 18)] - public void CheckDigit_WhenValidNumber_ReturnsExpectedValue(string number, int expectedValue) - { - // Arrange - var facilityNumber = new BelgianHealthFacilityLicenseNumber(number); - // Act - var checkDigit = facilityNumber.CheckDigit(); - // Assert - checkDigit.Should().Be(expectedValue); - } - - [Theory] - [InlineData("73000418210", 18)] - [InlineData("78901184", 84)] - [InlineData("75500543", 43)] - [InlineData("82662608", 8)] - [InlineData("71000436", 36)] - [InlineData("789011", 84)] - public void ComputeCheckDigit_WhenValidNumber_ReturnsExpectedValue(string number, int expectedValue) - { - // Act - var checkDigit = BelgianHealthFacilityLicenseNumber.ComputeCheckDigit(number); - // Assert - checkDigit.Should().Be(expectedValue); - } - - [Theory] - [InlineData("78901184", 789011)] - [InlineData("73000418210", 730004)] - public void FacilityUniqueIdentifier_WhenValidNumber_ReturnsExpectedValue(string number, int expectedValue) - { - // Arrange - var facilityNumber = new BelgianHealthFacilityLicenseNumber(number); - // Act - var facilityUniqueIdentifier = facilityNumber.FacilityUniqueIdentifier(); - // Assert - facilityUniqueIdentifier.Should().Be(expectedValue); - } - - #endregion Methods - - } -} diff --git a/Test/DDD.HealthcareDelivery.UnitTests/Domain/Prescriptions/PharmaceuticalPrescriptionTests.cs b/Test/DDD.HealthcareDelivery.UnitTests/Domain/Prescriptions/PharmaceuticalPrescriptionTests.cs index 48a3d57..7c8f36a 100644 --- a/Test/DDD.HealthcareDelivery.UnitTests/Domain/Prescriptions/PharmaceuticalPrescriptionTests.cs +++ b/Test/DDD.HealthcareDelivery.UnitTests/Domain/Prescriptions/PharmaceuticalPrescriptionTests.cs @@ -7,9 +7,8 @@ namespace DDD.HealthcareDelivery.Domain.Prescriptions using Common.Domain; using Patients; using Practitioners; - using Facilities; - public class PharmaceuticalPrescriptionTests : PrescriptionTests + public class PharmaceuticalPrescriptionTests : PrescriptionTests { #region Constructors @@ -37,7 +36,6 @@ public void Create_CreationDateNotSpecified_AddsPrescriptionCreatedEvent() new PrescriptionIdentifier(1), new Physician(1, new FullName("Duck", "Donald"), new BelgianHealthcarePractitionerLicenseNumber("19006951001")), new Patient(1, new FullName("Fred", "Flintstone"), BelgianSex.Male), - new MedicalOffice(1, "Medical Office Donald Duck"), new PrescribedMedication[] { new PrescribedPharmaceuticalProduct("ADALAT OROS 30 COMP 28 X 30 MG", "appliquer 2 fois par jour") }, new Alpha2LanguageCode("FR") ); @@ -54,7 +52,6 @@ public void Create_CreationDateNotSpecified_MarksPrescriptionAsCreated() new PrescriptionIdentifier(1), new Physician(1, new FullName("Duck", "Donald"), new BelgianHealthcarePractitionerLicenseNumber("19006951001")), new Patient(1, new FullName("Fred", "Flintstone"), BelgianSex.Male), - new MedicalOffice(1, "Medical Office Donald Duck"), new PrescribedMedication[] { new PrescribedPharmaceuticalProduct("ADALAT OROS 30 COMP 28 X 30 MG", "appliquer 2 fois par jour") }, new Alpha2LanguageCode("FR") ); @@ -71,7 +68,6 @@ public void Create_CreationDateSpecified_AddsPrescriptionCreatedEvent() new PrescriptionIdentifier(1), new Physician(1, new FullName("Duck", "Donald"), new BelgianHealthcarePractitionerLicenseNumber("19006951001")), new Patient(1, new FullName("Fred", "Flintstone"), BelgianSex.Male), - new MedicalOffice(1, "Medical Office Donald Duck"), new PrescribedMedication[] { new PrescribedPharmaceuticalProduct("ADALAT OROS 30 COMP 28 X 30 MG", "appliquer 2 fois par jour") }, new DateTime(2016, 2, 7), new Alpha2LanguageCode("FR") @@ -89,7 +85,6 @@ public void Create_CreationDateSpecified_MarksPrescriptionAsCreated() new PrescriptionIdentifier(1), new Physician(1, new FullName("Duck", "Donald"), new BelgianHealthcarePractitionerLicenseNumber("19006951001")), new Patient(1, new FullName("Fred", "Flintstone"), BelgianSex.Male), - new MedicalOffice(1, "Medical Office Donald Duck"), new PrescribedMedication[] { new PrescribedPharmaceuticalProduct("ADALAT OROS 30 COMP 28 X 30 MG", "appliquer 2 fois par jour") }, new DateTime(2016, 2, 7), new Alpha2LanguageCode("FR") @@ -98,7 +93,7 @@ public void Create_CreationDateSpecified_MarksPrescriptionAsCreated() prescription.Status.Should().Be(PrescriptionStatus.Created); } - public override void Revoke_RevocablePrescription_AddsPrescriptionRevokedEvent(Prescription prescription) + public override void Revoke_RevocablePrescription_AddsPrescriptionRevokedEvent(Prescription prescription) { // Act prescription.Revoke("Erreur"); @@ -113,7 +108,6 @@ private static PharmaceuticalPrescription CreatePrescription(PrescriptionStatus new PrescriptionIdentifier(1), new Physician(1, new FullName("Duck", "Donald"), new BelgianHealthcarePractitionerLicenseNumber("19006951001")), new Patient(1, new FullName("Fred", "Flintstone"), BelgianSex.Male), - new MedicalOffice(1, "Medical Office Donald Duck"), new PrescribedMedication[] { new PrescribedPharmaceuticalProduct("ADALAT OROS 30 COMP 28 X 30 MG", "appliquer 2 fois par jour") }, new Alpha2LanguageCode("FR"), status, diff --git a/Test/DDD.HealthcareDelivery.UnitTests/Domain/Prescriptions/PrescriptionTests.cs b/Test/DDD.HealthcareDelivery.UnitTests/Domain/Prescriptions/PrescriptionTests.cs index 0526554..3703884 100644 --- a/Test/DDD.HealthcareDelivery.UnitTests/Domain/Prescriptions/PrescriptionTests.cs +++ b/Test/DDD.HealthcareDelivery.UnitTests/Domain/Prescriptions/PrescriptionTests.cs @@ -5,13 +5,13 @@ namespace DDD.HealthcareDelivery.Domain.Prescriptions { using Core.Infrastructure.Testing; - public abstract class PrescriptionTests where TState : PrescriptionState, new() + public abstract class PrescriptionTests { #region Properties - public static TheoryData> NotRevocablePrescriptions { get; } = new TheoryData>(); - public static TheoryData> RevocablePrescriptions { get; } = new TheoryData>(); + public static TheoryData NotRevocablePrescriptions { get; } = new TheoryData(); + public static TheoryData RevocablePrescriptions { get; } = new TheoryData(); #endregion Properties @@ -19,7 +19,7 @@ namespace DDD.HealthcareDelivery.Domain.Prescriptions [Theory] [CustomMemberData(nameof(NotRevocablePrescriptions))] - public void Revoke_NotRevocablePrescription_DoesNotAddEvent(Prescription prescription) + public void Revoke_NotRevocablePrescription_DoesNotAddEvent(Prescription prescription) { // Act prescription.Revoke("Erreur"); @@ -29,7 +29,7 @@ public void Revoke_NotRevocablePrescription_DoesNotAddEvent(Prescription prescription) + public void Revoke_NotRevocablePrescription_DoesNotChangeStatus(Prescription prescription) { // Arrange var initialStatus = prescription.Status; @@ -41,11 +41,11 @@ public void Revoke_NotRevocablePrescription_DoesNotChangeStatus(Prescription prescription); + public abstract void Revoke_RevocablePrescription_AddsPrescriptionRevokedEvent(Prescription prescription); [Theory] [CustomMemberData(nameof(RevocablePrescriptions))] - public void Revoke_RevocablePrescription_MarksPrescriptionAsRevoked(Prescription prescription) + public void Revoke_RevocablePrescription_MarksPrescriptionAsRevoked(Prescription prescription) { // Act prescription.Revoke("Erreur"); diff --git a/Test/DDD.HealthcareDelivery.UnitTests/Infrastructure/Prescriptions/BelgianCreatePharmaceuticalPrescriptionValidatorTests.cs b/Test/DDD.HealthcareDelivery.UnitTests/Infrastructure/Prescriptions/BelgianCreatePharmaceuticalPrescriptionValidatorTests.cs index ac42534..8f41365 100644 --- a/Test/DDD.HealthcareDelivery.UnitTests/Infrastructure/Prescriptions/BelgianCreatePharmaceuticalPrescriptionValidatorTests.cs +++ b/Test/DDD.HealthcareDelivery.UnitTests/Infrastructure/Prescriptions/BelgianCreatePharmaceuticalPrescriptionValidatorTests.cs @@ -2,7 +2,6 @@ using FluentValidation; using FluentAssertions; using System.Collections.Generic; -using System.Linq; namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions { @@ -65,94 +64,6 @@ public void Validate_WhenMedicationCodeValid_ReturnsNoSpecificFailure(string med results.Errors.Should().NotContain(f => f.ErrorCode == "MedicationCodeInvalid"); } - [Theory] - [InlineData(0)] - [InlineData(-1)] - [InlineData(-5)] - public void Validate_WhenFacilityIdentifierInvalid_ReturnsExpectedFailure(int facilityIdentifier) - { - // Arrange - var validator = CreateValidator(); - var command = new CreatePharmaceuticalPrescription { FacilityIdentifier = facilityIdentifier }; - // Act - var results = validator.Validate(command); - // Assert - results.Errors.Should().ContainSingle(f => f.ErrorCode == "FacilityIdentifierInvalid" && f.Severity == Severity.Error); - } - - [Theory] - [InlineData(1)] - [InlineData(2)] - [InlineData(5)] - public void Validate_WhenFacilityIdentifierValid_ReturnsNoSpecificFailure(int facilityIdentifier) - { - // Arrange - var validator = CreateValidator(); - var command = new CreatePharmaceuticalPrescription { FacilityIdentifier = facilityIdentifier }; - // Act - var results = validator.Validate(command); - // Assert - results.Errors.Should().NotContain(f => f.ErrorCode == "FacilityIdentifierInvalid"); - } - - [Theory] - [InlineData("aa")] - [InlineData("11111")] - [InlineData("1111111a")] - public void Validate_WhenFacilityLicenseNumberInvalid_ReturnsExpectedFailure(string facilityLicenseNumber) - { - // Arrange - var validator = CreateValidator(); - var command = new CreatePharmaceuticalPrescription { FacilityLicenseNumber = facilityLicenseNumber }; - // Act - var results = validator.Validate(command); - // Assert - results.Errors.Should().Contain(f => f.ErrorCode == "FacilityLicenseNumberInvalid" && f.Severity == Severity.Error); - } - - [Theory] - [InlineData("11111111")] - [InlineData("01234567")] - public void Validate_WhenFacilityLicenseNumberValid_ReturnsNoSpecificFailure(string facilityLicenseNumber) - { - // Arrange - var validator = CreateValidator(); - var command = new CreatePharmaceuticalPrescription { FacilityLicenseNumber = facilityLicenseNumber }; - // Act - var results = validator.Validate(command); - // Assert - results.Errors.Should().NotContain(f => f.ErrorCode == "FacilityLicenseNumberInvalid"); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public void Validate_WhenFacilityNameEmpty_ReturnsExpectedFailure(string facilityName) - { - // Arrange - var validator = CreateValidator(); - var command = new CreatePharmaceuticalPrescription { FacilityName = facilityName }; - // Act - var results = validator.Validate(command); - // Assert - results.Errors.Should().ContainSingle(f => f.ErrorCode == "FacilityNameEmpty" && f.Severity == Severity.Error); - } - - [Theory] - [InlineData("Medical Office Donald Duck")] - [InlineData("Centre ophtalmo")] - public void Validate_WhenFacilityNameNotEmpty_ReturnsNoSpecificFailure(string facilityName) - { - // Arrange - var validator = CreateValidator(); - var command = new CreatePharmaceuticalPrescription { FacilityName = facilityName }; - // Act - var results = validator.Validate(command); - // Assert - results.Errors.Should().NotContain(f => f.ErrorCode == "FacilityNameEmpty"); - } - [Theory] [InlineData(null)] [InlineData("")] @@ -659,4 +570,4 @@ private static CreatePharmaceuticalPrescription CommandWithMedications(params Pr #endregion Methods } -} +} \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.UnitTests/Properties/AssemblyInfo.cs b/Test/DDD.HealthcareDelivery.UnitTests/Properties/AssemblyInfo.cs index fa89750..a35c29f 100644 --- a/Test/DDD.HealthcareDelivery.UnitTests/Properties/AssemblyInfo.cs +++ b/Test/DDD.HealthcareDelivery.UnitTests/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Xunit; diff --git a/Test/DDD.HealthcareDelivery.UnitTests/app.config b/Test/DDD.HealthcareDelivery.UnitTests/app.config deleted file mode 100644 index 81313c0..0000000 --- a/Test/DDD.HealthcareDelivery.UnitTests/app.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/Test/DDD.HealthcareDelivery.UnitTests/packages.config b/Test/DDD.HealthcareDelivery.UnitTests/packages.config deleted file mode 100644 index 39669f8..0000000 --- a/Test/DDD.HealthcareDelivery.UnitTests/packages.config +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file