From a8d63da162cceed84d39f3d7775d24a17dc90f40 Mon Sep 17 00:00:00 2001 From: draphyz Date: Thu, 23 May 2019 20:22:00 +0200 Subject: [PATCH 001/111] Remove state model and entity framework references --- DDD.sln | 7 - Src/DDD.Common/DDD.Common.csproj | 3 - Src/DDD.Common/Domain/ContactInformation.cs | 40 +--- .../Domain/ContactInformationState.cs | 25 --- Src/DDD.Common/Domain/FullName.cs | 24 +- Src/DDD.Common/Domain/FullNameState.cs | 15 -- Src/DDD.Common/Domain/PostalAddress.cs | 32 +-- Src/DDD.Common/Domain/PostalAddressState.cs | 23 -- Src/DDD.Core.EF/App.config | 13 -- Src/DDD.Core.EF/DDD.Core.EF.csproj | 101 --------- Src/DDD.Core.EF/DbContextExtensions.cs | 36 --- .../DbEntityValidationExceptionExtensions.cs | 36 --- Src/DDD.Core.EF/DbModelBuilderExtensions.cs | 34 --- Src/DDD.Core.EF/EFRepository.cs | 160 -------------- Src/DDD.Core.EF/EventStateConfiguration.cs | 66 ------ Src/DDD.Core.EF/IAsyncDbContextFactory.cs | 20 -- .../OracleEventStateConfiguration.cs | 20 -- Src/DDD.Core.EF/Properties/AssemblyInfo.cs | 7 - .../SqlServerEventStateConfiguration.cs | 20 -- Src/DDD.Core.EF/StateEntitiesContext.cs | 113 ---------- Src/DDD.Core.EF/StateEntityConvention.cs | 29 --- .../UpperCaseColumnNameConvention.cs | 48 ---- .../UpperCaseForeignKeyNameConvention.cs | 52 ----- .../UpperCaseTableNameConvention.cs | 44 ---- Src/DDD.Core.EF/packages.config | 5 - Src/DDD.Core/DDD.Core.csproj | 7 +- Src/DDD.Core/Domain/DomainEntity.cs | 25 +-- Src/DDD.Core/Domain/EntityState.cs | 10 - Src/DDD.Core/Domain/IStateEntity.cs | 14 -- .../Domain/IStateObjectConvertible.cs | 14 -- .../Domain/{EventState.cs => StoredEvent.cs} | 2 +- ...Translator.cs => StoredEventTranslator.cs} | 8 +- .../DDD.HealthcareDelivery.csproj | 26 --- .../BelgianHealthFacilityTranslator.cs | 47 ---- .../Domain/Facilities/HealthFacility.cs | 20 +- .../Domain/Facilities/HealthFacilityState.cs | 19 -- .../Domain/Facilities/Hospital.cs | 15 +- .../Domain/Facilities/MedicalOffice.cs | 11 - .../Patients/BelgianPatientTranslator.cs | 30 --- .../Domain/Patients/Patient.cs | 17 +- .../Domain/Patients/PatientState.cs | 27 --- ...BelgianHealthcarePractitionerTranslator.cs | 45 ---- .../Practitioners/HealthcarePractitioner.cs | 18 +- .../HealthcarePractitionerState.cs | 28 --- .../Domain/Practitioners/Physician.cs | 19 +- ...ianPharmaceuticalPrescriptionTranslator.cs | 63 ------ .../BelgianPrescribedMedicationTranslator.cs | 76 ------- .../PharmaceuticalPrescription.cs | 21 +- .../PharmaceuticalPrescriptionState.cs | 15 -- .../Prescriptions/PrescribedMedication.cs | 28 +-- .../PrescribedMedicationState.cs | 31 --- .../PrescribedPharmaceuticalCompounding.cs | 18 +- .../PrescribedPharmaceuticalProduct.cs | 28 +-- .../PrescribedPharmaceuticalSubstance.cs | 24 +- .../Domain/Prescriptions/Prescription.cs | 31 +-- .../Domain/Prescriptions/PrescriptionState.cs | 35 --- .../Infrastructure/HealthcareContext.cs | 81 ------- .../Infrastructure/OracleHealthcareContext.cs | 53 ----- .../OracleHealthcareContextFactory.cs | 39 ---- .../OraclePrescriptionStateConfiguration.cs | 32 --- .../PharmaceuticalPrescriptionRepository.cs | 37 ---- ...aceuticalPrescriptionStateConfiguration.cs | 36 --- .../PrescribedMedicationStateConfiguration.cs | 72 ------ .../PrescriptionStateConfiguration.cs | 209 ------------------ ...SqlServerPrescriptionStateConfiguration.cs | 32 --- .../SqlServerHealthcareContext.cs | 40 ---- .../SqlServerHealthcareContextFactory.cs | 39 ---- ...ePharmaceuticalPrescriptionCreatorTests.cs | 2 +- ...ePharmaceuticalPrescriptionRevokerTests.cs | 2 +- ...rPharmaceuticalPrescriptionCreatorTests.cs | 2 +- ...rPharmaceuticalPrescriptionRevokerTests.cs | 2 +- ...HealthcareDelivery.IntegrationTests.csproj | 14 +- .../PharmaceuticalPrescriptionTests.cs | 4 +- .../Domain/Prescriptions/PrescriptionTests.cs | 14 +- 74 files changed, 77 insertions(+), 2378 deletions(-) delete mode 100644 Src/DDD.Common/Domain/ContactInformationState.cs delete mode 100644 Src/DDD.Common/Domain/FullNameState.cs delete mode 100644 Src/DDD.Common/Domain/PostalAddressState.cs delete mode 100644 Src/DDD.Core.EF/App.config delete mode 100644 Src/DDD.Core.EF/DDD.Core.EF.csproj delete mode 100644 Src/DDD.Core.EF/DbContextExtensions.cs delete mode 100644 Src/DDD.Core.EF/DbEntityValidationExceptionExtensions.cs delete mode 100644 Src/DDD.Core.EF/DbModelBuilderExtensions.cs delete mode 100644 Src/DDD.Core.EF/EFRepository.cs delete mode 100644 Src/DDD.Core.EF/EventStateConfiguration.cs delete mode 100644 Src/DDD.Core.EF/IAsyncDbContextFactory.cs delete mode 100644 Src/DDD.Core.EF/OracleEventStateConfiguration.cs delete mode 100644 Src/DDD.Core.EF/Properties/AssemblyInfo.cs delete mode 100644 Src/DDD.Core.EF/SqlServerEventStateConfiguration.cs delete mode 100644 Src/DDD.Core.EF/StateEntitiesContext.cs delete mode 100644 Src/DDD.Core.EF/StateEntityConvention.cs delete mode 100644 Src/DDD.Core.EF/UpperCaseColumnNameConvention.cs delete mode 100644 Src/DDD.Core.EF/UpperCaseForeignKeyNameConvention.cs delete mode 100644 Src/DDD.Core.EF/UpperCaseTableNameConvention.cs delete mode 100644 Src/DDD.Core.EF/packages.config delete mode 100644 Src/DDD.Core/Domain/EntityState.cs delete mode 100644 Src/DDD.Core/Domain/IStateEntity.cs delete mode 100644 Src/DDD.Core/Domain/IStateObjectConvertible.cs rename Src/DDD.Core/Domain/{EventState.cs => StoredEvent.cs} (94%) rename Src/DDD.Core/Domain/{EventTranslator.cs => StoredEventTranslator.cs} (74%) delete mode 100644 Src/DDD.HealthcareDelivery/Domain/Facilities/BelgianHealthFacilityTranslator.cs delete mode 100644 Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacilityState.cs delete mode 100644 Src/DDD.HealthcareDelivery/Domain/Patients/BelgianPatientTranslator.cs delete mode 100644 Src/DDD.HealthcareDelivery/Domain/Patients/PatientState.cs delete mode 100644 Src/DDD.HealthcareDelivery/Domain/Practitioners/BelgianHealthcarePractitionerTranslator.cs delete mode 100644 Src/DDD.HealthcareDelivery/Domain/Practitioners/HealthcarePractitionerState.cs delete mode 100644 Src/DDD.HealthcareDelivery/Domain/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs delete mode 100644 Src/DDD.HealthcareDelivery/Domain/Prescriptions/BelgianPrescribedMedicationTranslator.cs delete mode 100644 Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescriptionState.cs delete mode 100644 Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedicationState.cs delete mode 100644 Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescriptionState.cs delete mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/HealthcareContext.cs delete mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareContext.cs delete mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareContextFactory.cs delete mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionStateConfiguration.cs delete mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionRepository.cs delete mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionStateConfiguration.cs delete mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationStateConfiguration.cs delete mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionStateConfiguration.cs delete mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescriptionStateConfiguration.cs delete mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareContext.cs delete mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareContextFactory.cs diff --git a/DDD.sln b/DDD.sln index 8a576a2..f1e3312 100644 --- a/DDD.sln +++ b/DDD.sln @@ -25,8 +25,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Common", "Src\DDD.Commo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.HealthcareDelivery.IntegrationTests", "Test\DDD.HealthcareDelivery.IntegrationTests\DDD.HealthcareDelivery.IntegrationTests.csproj", "{B53007C7-B314-40DF-B6E7-6C6576A5611C}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.FluentValidation", "Src\DDD.Core.FluentValidation\DDD.Core.FluentValidation.csproj", "{5E3745FC-CA80-4D0F-8A25-20EE0F9CF163}" @@ -97,10 +95,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 @@ -147,7 +141,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} diff --git a/Src/DDD.Common/DDD.Common.csproj b/Src/DDD.Common/DDD.Common.csproj index da0c442..6d6ab6f 100644 --- a/Src/DDD.Common/DDD.Common.csproj +++ b/Src/DDD.Common/DDD.Common.csproj @@ -57,15 +57,12 @@ - - - diff --git a/Src/DDD.Common/Domain/ContactInformation.cs b/Src/DDD.Common/Domain/ContactInformation.cs index 9b5227d..9011d52 100644 --- a/Src/DDD.Common/Domain/ContactInformation.cs +++ b/Src/DDD.Common/Domain/ContactInformation.cs @@ -1,11 +1,11 @@ -using System.Collections.Generic; -using System; +using System; +using System.Collections.Generic; namespace DDD.Common.Domain { using Core.Domain; - public class ContactInformation : ValueObject, IStateObjectConvertible + public class ContactInformation : ValueObject { #region Constructors @@ -15,7 +15,7 @@ public ContactInformation(PostalAddress postalAddress, string secondaryTelephoneNumber, string faxNumber, EmailAddress primaryEmailAddress, - EmailAddress secondaryEmailAddress, + EmailAddress secondaryEmailAddress, Uri webSite) { this.PostalAddress = postalAddress; @@ -52,21 +52,6 @@ public ContactInformation(PostalAddress postalAddress, #region Methods - public static ContactInformation FromState(ContactInformationState state) - { - if (state == null) return null; - return new ContactInformation - ( - 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 override IEnumerable EqualityComponents() { yield return this.PostalAddress; @@ -78,22 +63,6 @@ public override IEnumerable EqualityComponents() yield return this.WebSite; } - public ContactInformationState ToState() - { - return new ContactInformationState - { - PostalAddress = this.PostalAddress == null ? - new PostalAddressState() - : this.PostalAddress.ToState(), // EF6 complex types cannot be null - PrimaryTelephoneNumber = this.PrimaryTelephoneNumber, - SecondaryTelephoneNumber = this.SecondaryTelephoneNumber, - FaxNumber = this.FaxNumber, - PrimaryEmailAddress = this.PrimaryEmailAddress?.Address, - SecondaryEmailAddress = this.SecondaryEmailAddress?.Address, - 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}]"; @@ -101,5 +70,6 @@ public override string ToString() } #endregion Methods + } } 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/FullName.cs b/Src/DDD.Common/Domain/FullName.cs index 3f17e28..1bebf55 100644 --- a/Src/DDD.Common/Domain/FullName.cs +++ b/Src/DDD.Common/Domain/FullName.cs @@ -6,7 +6,7 @@ namespace DDD.Common.Domain { using Core.Domain; - public class FullName : ComparableValueObject, IStateObjectConvertible + public class FullName : ComparableValueObject { #region Constructors @@ -19,7 +19,7 @@ public FullName(string lastName, string firstName) this.FirstName = firstName.ToTitleCase(); } - #endregion Constructors + #endregion Constructors #region Properties @@ -31,16 +31,6 @@ public FullName(string lastName, string firstName) #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,15 +45,6 @@ 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}]"; @@ -82,5 +63,6 @@ public FullName WithLastName(string lastName) } #endregion Methods + } } \ No newline at end of file diff --git a/Src/DDD.Common/Domain/FullNameState.cs b/Src/DDD.Common/Domain/FullNameState.cs deleted file mode 100644 index 94498a5..0000000 --- a/Src/DDD.Common/Domain/FullNameState.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace DDD.Common.Domain -{ - public class FullNameState - { - - #region Properties - - public string FirstName { get; set; } - - public string LastName { get; set; } - - #endregion Properties - - } -} diff --git a/Src/DDD.Common/Domain/PostalAddress.cs b/Src/DDD.Common/Domain/PostalAddress.cs index 475ffd6..912cf3e 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 Conditions; +using System.Collections.Generic; namespace DDD.Common.Domain { using Core.Domain; - public class PostalAddress : ValueObject, IStateObjectConvertible + public class PostalAddress : ValueObject { #region Constructors @@ -50,20 +50,6 @@ public PostalAddress(string street, #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,18 +79,6 @@ 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?.Code - }; - } public override string ToString() { 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.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/DDD.Core.csproj b/Src/DDD.Core/DDD.Core.csproj index 28ab712..f58bb5c 100644 --- a/Src/DDD.Core/DDD.Core.csproj +++ b/Src/DDD.Core/DDD.Core.csproj @@ -69,10 +69,9 @@ - - + @@ -81,8 +80,6 @@ - - @@ -98,7 +95,7 @@ - + diff --git a/Src/DDD.Core/Domain/DomainEntity.cs b/Src/DDD.Core/Domain/DomainEntity.cs index 3f44036..33cbd5d 100644 --- a/Src/DDD.Core/Domain/DomainEntity.cs +++ b/Src/DDD.Core/Domain/DomainEntity.cs @@ -1,17 +1,15 @@ using Conditions; using System; using System.Collections.Generic; -using System.Linq; namespace DDD.Core.Domain { - using Collections; - /// /// Base class for entities of the Domain Model. /// public abstract class DomainEntity : IEquatable { + #region Fields private readonly List events = new List(); @@ -20,21 +18,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) @@ -68,25 +59,19 @@ public bool Equals(DomainEntity other) public override int GetHashCode() => this.Identity().GetHashCode(); + public abstract ComparableValueObject Identity(); + public virtual string IdentityAsString() { return string.Join("/", this.Identity().PrimitiveEqualityComponents()); } - - public abstract ComparableValueObject Identity(); - protected void AddEvent(IDomainEvent @event) { Condition.Requires(@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/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/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/EventState.cs b/Src/DDD.Core/Domain/StoredEvent.cs similarity index 94% rename from Src/DDD.Core/Domain/EventState.cs rename to Src/DDD.Core/Domain/StoredEvent.cs index f921d38..2c9e365 100644 --- a/Src/DDD.Core/Domain/EventState.cs +++ b/Src/DDD.Core/Domain/StoredEvent.cs @@ -2,7 +2,7 @@ namespace DDD.Core.Domain { - public class EventState + public class StoredEvent { #region Properties diff --git a/Src/DDD.Core/Domain/EventTranslator.cs b/Src/DDD.Core/Domain/StoredEventTranslator.cs similarity index 74% rename from Src/DDD.Core/Domain/EventTranslator.cs rename to Src/DDD.Core/Domain/StoredEventTranslator.cs index 5bd7418..036de56 100644 --- a/Src/DDD.Core/Domain/EventTranslator.cs +++ b/Src/DDD.Core/Domain/StoredEventTranslator.cs @@ -6,7 +6,7 @@ namespace DDD.Core.Domain using Mapping; using Serialization; - public class EventTranslator : IObjectTranslator + public class StoredEventTranslator : IObjectTranslator { #region Fields @@ -16,7 +16,7 @@ public class EventTranslator : IObjectTranslator #region Constructors - public EventTranslator(ITextSerializer eventSerializer) + public StoredEventTranslator(ITextSerializer eventSerializer) { Condition.Requires(eventSerializer, nameof(eventSerializer)).IsNotNull(); this.eventSerializer = eventSerializer; @@ -26,10 +26,10 @@ public EventTranslator(ITextSerializer eventSerializer) #region Methods - public EventState Translate(IEvent @event, IDictionary options = null) + public StoredEvent Translate(IEvent @event, IDictionary options = null) { Condition.Requires(@event, nameof(@event)).IsNotNull(); - return new EventState() + return new StoredEvent() { OccurredOn = @event.OccurredOn, EventType = @event.GetType().Name, diff --git a/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj b/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj index 50e61ea..0085c10 100644 --- a/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj +++ b/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj @@ -85,24 +85,16 @@ - - - - - - - - @@ -112,32 +104,18 @@ - - - - - - - - - - - - SqlScripts.resx True True - - @@ -157,10 +135,6 @@ {701da58b-ae36-429f-8621-64109b8d29d7} DDD.Core.Dapper - - {6d227aa7-ff90-48ca-b13d-ed23c1fffba5} - DDD.Core.EF - {5e3745fc-ca80-4d0f-8a25-20ee0f9cf163} DDD.Core.FluentValidation 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 index 9439c19..71813a9 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacility.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacility.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using Conditions; +using Conditions; +using System.Collections.Generic; namespace DDD.HealthcareDelivery.Domain.Facilities { @@ -8,13 +8,13 @@ namespace DDD.HealthcareDelivery.Domain.Facilities /// /// Represents any location where healthcare is provided. /// - public abstract class HealthFacility : ValueObject, IStateObjectConvertible + public abstract class HealthFacility : ValueObject { #region Constructors - protected HealthFacility(int identifier, - string name, + protected HealthFacility(int identifier, + string name, HealthFacilityLicenseNumber licenseNumber = null) { Condition.Requires(identifier, nameof(identifier)).IsGreaterThan(0); @@ -45,16 +45,6 @@ public override IEnumerable EqualityComponents() yield return this.LicenseNumber; } - public virtual HealthFacilityState ToState() - { - return new HealthFacilityState - { - Identifier = this.Identifier, - Name = this.Name, - LicenseNumber = this.LicenseNumber?.Number - }; - } - public override string ToString() { return $"{this.GetType().Name} [identifier={this.Identifier}, name={this.Name}, licenseNumber={this.LicenseNumber}]"; 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 index 07fc543..eeb19f0 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Facilities/Hospital.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Facilities/Hospital.cs @@ -9,24 +9,13 @@ public class Hospital : HealthFacility #region Constructors public Hospital(int identifier, - string name, - HealthFacilityLicenseNumber licenseNumber = null) + 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 index 575ebcc..8e4c4fb 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Facilities/MedicalOffice.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Facilities/MedicalOffice.cs @@ -17,16 +17,5 @@ public MedicalOffice(int identifier, #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 c32310d..b0fa0f9 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Patients/Patient.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Patients/Patient.cs @@ -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 @@ -60,21 +60,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?.Number, - 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/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 a77a81a..41f123c 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Practitioners/HealthcarePractitioner.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Practitioners/HealthcarePractitioner.cs @@ -10,7 +10,7 @@ namespace DDD.HealthcareDelivery.Domain.Practitioners /// Represents a person that provides healthcare services. /// public abstract class HealthcarePractitioner - : ValueObject, IStateObjectConvertible + : ValueObject { #region Constructors @@ -69,22 +69,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.Number, - SocialSecurityNumber = this.SocialSecurityNumber?.Number, - 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/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..d8e3649 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Practitioners/Physician.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Practitioners/Physician.cs @@ -10,29 +10,18 @@ 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 - } } 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/PharmaceuticalPrescription.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs index b8d026a..e39a1cb 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; namespace DDD.HealthcareDelivery.Domain.Prescriptions { @@ -16,7 +15,7 @@ namespace DDD.HealthcareDelivery.Domain.Prescriptions /// /// Represents a pharmaceutical prescription. /// - public class PharmaceuticalPrescription : Prescription + public class PharmaceuticalPrescription : Prescription { #region Fields @@ -36,9 +35,8 @@ public PharmaceuticalPrescription(PrescriptionIdentifier identifier, PrescriptionStatus status, DateTime createdOn, 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, healthFacility, languageCode, status, createdOn, delivrableAt, events) { Condition.Requires(prescribedMedications, nameof(prescribedMedications)) .IsNotNull() @@ -89,26 +87,11 @@ public static PharmaceuticalPrescription Create(PrescriptionIdentifier identifie 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.Identifier))); - return state; - } - protected override void AddPrescriptionRevokedEvent(string reason) { this.AddEvent(new PharmaceuticalPrescriptionRevoked(this.Identifier.Identifier, reason)); } - private static PrescribedMedicationState ToPrescribedMedicationState(PrescribedMedication medication, - int prescriptionIdentifier) - { - var state = medication.ToState(); - state.PrescriptionIdentifier = prescriptionIdentifier; - return state; - } - #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 1af37d4..4e2c59b 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedication.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedication.cs @@ -1,19 +1,15 @@ -using System.Collections.Generic; -using Conditions; -using System; +using Conditions; +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; #endregion Fields @@ -25,8 +21,7 @@ protected PrescribedMedication(string nameOrDescription, string quantity = null, string duration = 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); @@ -38,7 +33,6 @@ protected PrescribedMedication(string nameOrDescription, this.Duration = duration; this.Code = code; this.identifier = identifier; - this.entityState = entityState; } #endregion Constructors @@ -68,20 +62,6 @@ public override IEnumerable EqualityComponents() 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?.Code - }; - } - public override string ToString() { return $"{this.GetType().Name} [nameOrDescription={this.NameOrDescription}, posology={this.Posology}], quantity={this.Quantity}, duration={this.Duration}, code={this.Code}"; 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..3554260 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. /// @@ -14,24 +12,12 @@ 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) + int identifier = 0) + : base(nameOrDescription, posology, quantity, duration, 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..3a0b9f0 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, + public PrescribedPharmaceuticalProduct(string nameOrDescription, + string posology = null, + string quantity = null, string duration = 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, duration, code, identifier) { } #endregion Constructors - #region Methods - - public override PrescribedMedicationState ToState() - { - var state = base.ToState(); - state.MedicationType = PrescribedMedicationType.Product.ToString(); - return state; - } - - #endregion Methods - } } \ 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..720db4c 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, + public PrescribedPharmaceuticalSubstance(string nameOrDescription, + string posology = null, + string quantity = null, string duration = 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, duration, code, identifier) { } #endregion Constructors - #region Methods - - public override PrescribedMedicationState ToState() - { - var state = base.ToState(); - state.MedicationType = PrescribedMedicationType.Substance.ToString(); - return state; - } - - #endregion Methods - } } \ 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 189f35e..86b17ef 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/Prescription.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/Prescription.cs @@ -1,21 +1,19 @@ -using System; +using Conditions; +using System; using System.Collections.Generic; -using Conditions; namespace DDD.HealthcareDelivery.Domain.Prescriptions { - using Core.Domain; using Common.Domain; - using Patients; + using Core.Domain; using Facilities; + using Patients; using Practitioners; /// /// 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 @@ -28,9 +26,8 @@ protected Prescription(PrescriptionIdentifier identifier, PrescriptionStatus status, DateTime createdOn, 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(); @@ -80,25 +77,9 @@ public void Revoke(string reason) if (this.IsRevocable()) { this.Status = PrescriptionStatus.Revoked; - this.MarkAsModified(); this.AddPrescriptionRevokedEvent(reason); } } - public virtual TState ToState() - { - return new TState - { - Identifier = this.Identifier.Identifier, - 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.Code, - EntityState = this.EntityState, - }; - } protected abstract void AddPrescriptionRevokedEvent(string reason); 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/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/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/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/PharmaceuticalPrescriptionRepository.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionRepository.cs deleted file mode 100644 index b5fbb8e..0000000 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionRepository.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; - -namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions -{ - using Mapping; - using Core.Domain; - using Domain.Prescriptions; - using Core.Infrastructure.Data; - - public class PharmaceuticalPrescriptionRepository - : EFRepository - { - - #region Constructors - - public PharmaceuticalPrescriptionRepository(IObjectTranslator prescriptionTranslator, - IObjectTranslator eventTranslator, - IAsyncDbContextFactory contextFactory) - : base(prescriptionTranslator, eventTranslator, contextFactory) - { - } - - #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/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/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/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/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/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionCreatorTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionCreatorTests.cs index a248bca..a7627cf 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionCreatorTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionCreatorTests.cs @@ -29,7 +29,7 @@ protected override IAsyncRepository CreateRepository return new PharmaceuticalPrescriptionRepository ( new Domain.Prescriptions.BelgianPharmaceuticalPrescriptionTranslator(), - new EventTranslator(DataContractSerializerWrapper.Create(new UTF8Encoding(false))), + new StoredEventTranslator(DataContractSerializerWrapper.Create(new UTF8Encoding(false))), new OracleHealthcareContextFactory(this.Fixture.ConnectionFactory) ); } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionRevokerTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionRevokerTests.cs index 404ac7a..cccde33 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionRevokerTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionRevokerTests.cs @@ -29,7 +29,7 @@ protected override IAsyncRepository CreateRepository return new PharmaceuticalPrescriptionRepository ( new Domain.Prescriptions.BelgianPharmaceuticalPrescriptionTranslator(), - new EventTranslator(DataContractSerializerWrapper.Create(new UTF8Encoding(false))), + new StoredEventTranslator(DataContractSerializerWrapper.Create(new UTF8Encoding(false))), new OracleHealthcareContextFactory(this.Fixture.ConnectionFactory) ); } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionCreatorTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionCreatorTests.cs index 6fb3156..0b01412 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionCreatorTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionCreatorTests.cs @@ -29,7 +29,7 @@ protected override IAsyncRepository CreateRepository return new PharmaceuticalPrescriptionRepository ( new Domain.Prescriptions.BelgianPharmaceuticalPrescriptionTranslator(), - new EventTranslator(DataContractSerializerWrapper.Create(Encoding.Unicode)), + new StoredEventTranslator(DataContractSerializerWrapper.Create(Encoding.Unicode)), new SqlServerHealthcareContextFactory(this.Fixture.ConnectionFactory) ); } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionRevokerTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionRevokerTests.cs index 8eeecd4..12ccf00 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionRevokerTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionRevokerTests.cs @@ -29,7 +29,7 @@ protected override IAsyncRepository CreateRepository return new PharmaceuticalPrescriptionRepository ( new Domain.Prescriptions.BelgianPharmaceuticalPrescriptionTranslator(), - new EventTranslator(DataContractSerializerWrapper.Create(Encoding.Unicode)), + new StoredEventTranslator(DataContractSerializerWrapper.Create(Encoding.Unicode)), new SqlServerHealthcareContextFactory(this.Fixture.ConnectionFactory) ); } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj b/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj index 3de00e4..420a2fc 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj @@ -79,12 +79,6 @@ - - - - - - @@ -122,10 +116,6 @@ {0B70B4FD-F5A0-4A6C-A3FD-90031E08C1C2} DDD.Common - - {6d227aa7-ff90-48ca-b13d-ed23c1fffba5} - DDD.Core.EF - {2438B31A-3A39-4878-81FA-BE5AE715EAE5} DDD.Core.Messages @@ -185,7 +175,9 @@ - + + + diff --git a/Test/DDD.HealthcareDelivery.UnitTests/Domain/Prescriptions/PharmaceuticalPrescriptionTests.cs b/Test/DDD.HealthcareDelivery.UnitTests/Domain/Prescriptions/PharmaceuticalPrescriptionTests.cs index 48a3d57..60e4f90 100644 --- a/Test/DDD.HealthcareDelivery.UnitTests/Domain/Prescriptions/PharmaceuticalPrescriptionTests.cs +++ b/Test/DDD.HealthcareDelivery.UnitTests/Domain/Prescriptions/PharmaceuticalPrescriptionTests.cs @@ -9,7 +9,7 @@ namespace DDD.HealthcareDelivery.Domain.Prescriptions using Practitioners; using Facilities; - public class PharmaceuticalPrescriptionTests : PrescriptionTests + public class PharmaceuticalPrescriptionTests : PrescriptionTests { #region Constructors @@ -98,7 +98,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"); 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"); From f4c2bef486b264ea16655a9ed8dac830766a5e66 Mon Sep 17 00:00:00 2001 From: draphyz Date: Thu, 23 May 2019 20:38:58 +0200 Subject: [PATCH 002/111] Add project DDD.Core.NHibernate --- DDD.sln | 7 + Src/DDD.Core.NHibernate/CompositeUserType.cs | 566 ++++++++++++++++++ .../DDD.Core.NHibernate.csproj | 86 +++ .../ISubcomponentMapper.cs | 20 + .../MemberInfoExtensions.cs | 51 ++ .../NHibernateRepository.cs | 46 ++ .../Properties/AssemblyInfo.cs | 7 + Src/DDD.Core.NHibernate/packages.config | 9 + 8 files changed, 792 insertions(+) create mode 100644 Src/DDD.Core.NHibernate/CompositeUserType.cs create mode 100644 Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj create mode 100644 Src/DDD.Core.NHibernate/ISubcomponentMapper.cs create mode 100644 Src/DDD.Core.NHibernate/MemberInfoExtensions.cs create mode 100644 Src/DDD.Core.NHibernate/NHibernateRepository.cs create mode 100644 Src/DDD.Core.NHibernate/Properties/AssemblyInfo.cs create mode 100644 Src/DDD.Core.NHibernate/packages.config diff --git a/DDD.sln b/DDD.sln index f1e3312..061505f 100644 --- a/DDD.sln +++ b/DDD.sln @@ -53,6 +53,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Common.Messages", "Src\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.NServiceBus", "Src\DDD.Core.NServiceBus\DDD.Core.NServiceBus.csproj", "{436D869F-7566-4436-90A8-B655145C5BCA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.NHibernate", "Src\DDD.Core.NHibernate\DDD.Core.NHibernate.csproj", "{D4FC7E1B-CAE8-40F9-9FAF-0029D91CC6C4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -127,6 +129,10 @@ Global {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -149,6 +155,7 @@ Global {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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {983C3AB4-301E-47A3-93FF-2E4BD8F2090F} diff --git a/Src/DDD.Core.NHibernate/CompositeUserType.cs b/Src/DDD.Core.NHibernate/CompositeUserType.cs new file mode 100644 index 0000000..61e1b93 --- /dev/null +++ b/Src/DDD.Core.NHibernate/CompositeUserType.cs @@ -0,0 +1,566 @@ +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; + + public abstract class CompositeUserType : ICompositeUserType + { + + #region Fields + + public const string NotNullDiscriminatorValue = "not null"; + public const string NullDiscriminatorValue = "null"; + private readonly Type returnedClass; + private Dictionary classes; + private bool isMutable; + private 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 discriminatorValue = dr[this.DiscriminatorName()]; + 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 Subcomponent(Action> mapping = null) where TSubcomponent : 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..ec9db87 --- /dev/null +++ b/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj @@ -0,0 +1,86 @@ + + + + + Debug + AnyCPU + {D4FC7E1B-CAE8-40F9-9FAF-0029D91CC6C4} + Library + Properties + DDD.Core.Infrastructure.Data + DDD.Core.NHibernate + v4.7.2 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + 7.1 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + 7.1 + + + + L:\Packages\Antlr3.Runtime.3.5.1\lib\net40-client\Antlr3.Runtime.dll + + + L:\Packages\Conditions.2.1.0\lib\portable-net45+win+wpa81+wp80+MonoAndroid10+MonoTouch10\Conditions.dll + + + L:\Packages\Iesi.Collections.4.0.4\lib\net461\Iesi.Collections.dll + + + L:\Packages\NHibernate.5.2.5\lib\net461\NHibernate.dll + + + L:\Packages\Remotion.Linq.2.2.0\lib\net45\Remotion.Linq.dll + + + L:\Packages\Remotion.Linq.EagerFetching.2.2.0\lib\net45\Remotion.Linq.EagerFetching.dll + + + + + + + + + + + + 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.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..084c6ff --- /dev/null +++ b/Src/DDD.Core.NHibernate/MemberInfoExtensions.cs @@ -0,0 +1,51 @@ +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); + if (property.GetMethod == null) + throw new PropertyNotFoundException(obj.GetType(), property.Name, "getter"); + return property.GetValue(obj); + 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); + if (property.SetMethod == null) + throw new PropertyNotFoundException(obj.GetType(), property.Name, "setter"); + property.SetValue(obj, value); + break; + case MemberTypes.Field: + ((FieldInfo)propertyOrField).SetValue(obj, value); + break; + default: + throw new ArgumentOutOfRangeException(nameof(propertyOrField), + $"Expected PropertyInfo or FieldInfo; found :{propertyOrField.MemberType}"); + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.NHibernate/NHibernateRepository.cs b/Src/DDD.Core.NHibernate/NHibernateRepository.cs new file mode 100644 index 0000000..b680268 --- /dev/null +++ b/Src/DDD.Core.NHibernate/NHibernateRepository.cs @@ -0,0 +1,46 @@ +using NHibernate; +using System.Threading.Tasks; +using Conditions; + +namespace DDD.Core.Infrastructure.Data +{ + using Domain; + using Threading; + + public class NHibernateRepository : IAsyncRepository + where TDomainEntity : DomainEntity + { + #region Fields + + private readonly ISession session; + + #endregion Fields + + #region Constructors + + public NHibernateRepository(ISession session) + { + Condition.Requires(session, nameof(session)).IsNotNull(); + this.session = session; + } + + #endregion Constructors + + #region Methods + + public async Task FindAsync(ComparableValueObject identity) + { + Condition.Requires(identity, nameof(identity)).IsNotNull(); + await new SynchronizationContextRemover(); + return await this.session.GetAsync(identity); + } + + public Task SaveAsync(TDomainEntity aggregate) + { + throw new System.NotImplementedException(); + } + + #endregion Methods + + } +} 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/packages.config b/Src/DDD.Core.NHibernate/packages.config new file mode 100644 index 0000000..1364b34 --- /dev/null +++ b/Src/DDD.Core.NHibernate/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file From cefdd4f8d7456ea09c0e1b300e87b82c7abf07b6 Mon Sep 17 00:00:00 2001 From: draphyz Date: Thu, 23 May 2019 20:53:49 +0200 Subject: [PATCH 003/111] Add project DDD.Common.NHibernate --- DDD.sln | 7 ++ .../DDD.Common.NHibernate.csproj | 82 +++++++++++++++++++ .../EnumerationCodeType.cs | 80 ++++++++++++++++++ .../EnumerationNameType.cs | 80 ++++++++++++++++++ .../EnumerationValueType.cs | 71 ++++++++++++++++ .../Properties/AssemblyInfo.cs | 7 ++ Src/DDD.Common.NHibernate/packages.config | 8 ++ 7 files changed, 335 insertions(+) create mode 100644 Src/DDD.Common.NHibernate/DDD.Common.NHibernate.csproj create mode 100644 Src/DDD.Common.NHibernate/EnumerationCodeType.cs create mode 100644 Src/DDD.Common.NHibernate/EnumerationNameType.cs create mode 100644 Src/DDD.Common.NHibernate/EnumerationValueType.cs create mode 100644 Src/DDD.Common.NHibernate/Properties/AssemblyInfo.cs create mode 100644 Src/DDD.Common.NHibernate/packages.config diff --git a/DDD.sln b/DDD.sln index 061505f..d4561dd 100644 --- a/DDD.sln +++ b/DDD.sln @@ -55,6 +55,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.NServiceBus", "Src EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.NHibernate", "Src\DDD.Core.NHibernate\DDD.Core.NHibernate.csproj", "{D4FC7E1B-CAE8-40F9-9FAF-0029D91CC6C4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Common.NHibernate", "Src\DDD.Common.NHibernate\DDD.Common.NHibernate.csproj", "{7466B831-1642-4B2B-9EA3-1C9299596C2A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -133,6 +135,10 @@ Global {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -156,6 +162,7 @@ Global {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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {983C3AB4-301E-47A3-93FF-2E4BD8F2090F} 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..91a3a14 --- /dev/null +++ b/Src/DDD.Common.NHibernate/DDD.Common.NHibernate.csproj @@ -0,0 +1,82 @@ + + + + + Debug + AnyCPU + {7466B831-1642-4B2B-9EA3-1C9299596C2A} + Library + Properties + DDD.Common.Infrastructure.Data + DDD.Common.NHibernate + v4.7.2 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + 7.1 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + 7.1 + + + + L:\Packages\Antlr3.Runtime.3.5.1\lib\net40-client\Antlr3.Runtime.dll + + + L:\Packages\Iesi.Collections.4.0.4\lib\net461\Iesi.Collections.dll + + + L:\Packages\NHibernate.5.2.5\lib\net461\NHibernate.dll + + + L:\Packages\Remotion.Linq.2.2.0\lib\net45\Remotion.Linq.dll + + + L:\Packages\Remotion.Linq.EagerFetching.2.2.0\lib\net45\Remotion.Linq.EagerFetching.dll + + + + + + + + + + + + Properties\CommonAssemblyInfo.cs + + + + + + + + + + + + {0b70b4fd-f5a0-4a6c-a3fd-90031e08c1c2} + DDD.Common + + + {C6C3E419-B9AA-44AD-9DBF-789294687AE6} + DDD.Core + + + + \ 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.NHibernate/packages.config b/Src/DDD.Common.NHibernate/packages.config new file mode 100644 index 0000000..23ea25b --- /dev/null +++ b/Src/DDD.Common.NHibernate/packages.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file From a3c6d744a740a85cfedec16d75a3955533ff1de9 Mon Sep 17 00:00:00 2001 From: draphyz Date: Thu, 23 May 2019 21:24:17 +0200 Subject: [PATCH 004/111] Remove Entity Framework references --- Src/DDD.HealthcareDelivery/App.config | 24 +++++------- .../DDD.HealthcareDelivery.csproj | 6 --- Src/DDD.HealthcareDelivery/packages.config | 1 - .../App.config | 38 +++++++------------ ...HealthcareDelivery.IntegrationTests.csproj | 9 ----- .../packages.config | 2 - 6 files changed, 24 insertions(+), 56 deletions(-) diff --git a/Src/DDD.HealthcareDelivery/App.config b/Src/DDD.HealthcareDelivery/App.config index a3b6280..dd96560 100644 --- a/Src/DDD.HealthcareDelivery/App.config +++ b/Src/DDD.HealthcareDelivery/App.config @@ -2,38 +2,34 @@ -
-
+
+
- + - + - - + + - - - + + + - + diff --git a/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj b/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj index 0085c10..cc48170 100644 --- a/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj +++ b/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj @@ -42,12 +42,6 @@ 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 diff --git a/Src/DDD.HealthcareDelivery/packages.config b/Src/DDD.HealthcareDelivery/packages.config index 45c7fb3..30886ed 100644 --- a/Src/DDD.HealthcareDelivery/packages.config +++ b/Src/DDD.HealthcareDelivery/packages.config @@ -2,7 +2,6 @@ - diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/App.config b/Test/DDD.HealthcareDelivery.IntegrationTests/App.config index bb3de7a..71e615a 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/App.config +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/App.config @@ -1,54 +1,44 @@  -
+
-
+
- - + + - - + + - - - + + + - - + + - + - + - - + - - - \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj b/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj index 420a2fc..9cd52e5 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj @@ -35,12 +35,6 @@ 4 - - 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 @@ -48,9 +42,6 @@ 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 - diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/packages.config b/Test/DDD.HealthcareDelivery.IntegrationTests/packages.config index 8964d78..a70dcec 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/packages.config +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/packages.config @@ -1,9 +1,7 @@  - - From a11b6c654ae0775f34c5fe148777343e8698789a Mon Sep 17 00:00:00 2001 From: draphyz Date: Fri, 24 May 2019 16:13:12 +0200 Subject: [PATCH 005/111] Add NHibernate reference --- .../DDD.HealthcareDelivery.csproj | 21 ++++++++++++++++++- Src/DDD.HealthcareDelivery/packages.config | 5 +++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj b/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj index cc48170..cf5012c 100644 --- a/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj +++ b/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj @@ -35,6 +35,9 @@ 1591 + + L:\Packages\Antlr3.Runtime.3.5.1\lib\net40-client\Antlr3.Runtime.dll + L:\Packages\Conditions.2.1.0\lib\portable-net45+win+wpa81+wp80+MonoAndroid10+MonoTouch10\Conditions.dll True @@ -45,9 +48,21 @@ L:\Packages\FluentValidation.8.1.3\lib\net45\FluentValidation.dll + + L:\Packages\Iesi.Collections.4.0.4\lib\net461\Iesi.Collections.dll + + + L:\Packages\NHibernate.5.2.5\lib\net461\NHibernate.dll + L:\packages\Oracle.ManagedDataAccess.18.3.0\lib\net40\Oracle.ManagedDataAccess.dll + + L:\Packages\Remotion.Linq.2.2.0\lib\net45\Remotion.Linq.dll + + + L:\Packages\Remotion.Linq.EagerFetching.2.2.0\lib\net45\Remotion.Linq.EagerFetching.dll + L:\Packages\System.Collections.Immutable.1.5.0\lib\netstandard2.0\System.Collections.Immutable.dll @@ -61,10 +76,12 @@ True True + + L:\packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll @@ -114,7 +131,9 @@ - + + Designer + diff --git a/Src/DDD.HealthcareDelivery/packages.config b/Src/DDD.HealthcareDelivery/packages.config index 30886ed..a30f14e 100644 --- a/Src/DDD.HealthcareDelivery/packages.config +++ b/Src/DDD.HealthcareDelivery/packages.config @@ -1,9 +1,14 @@  + + + + + From 3f2a0993f860e10d2fa80ffd6019c66ab5cfa5ac Mon Sep 17 00:00:00 2001 From: draphyz Date: Fri, 24 May 2019 19:14:36 +0200 Subject: [PATCH 006/111] Rename some properties --- Src/DDD.Common/Domain/Alpha2CountryCode.cs | 10 +++--- Src/DDD.Common/Domain/Alpha2LanguageCode.cs | 4 +-- Src/DDD.Common/Domain/ArbitraryIdentifier.cs | 12 +++---- .../Domain/BelgianSocialSecurityNumber.cs | 36 +++++++++---------- Src/DDD.Common/Domain/BinaryContent.cs | 20 +++++------ Src/DDD.Common/Domain/CountryCode.cs | 2 +- Src/DDD.Common/Domain/IdentificationCode.cs | 14 ++++---- Src/DDD.Common/Domain/IdentificationNumber.cs | 14 ++++---- Src/DDD.Common/Domain/LanguageCode.cs | 2 +- Src/DDD.Common/Domain/SocialSecurityNumber.cs | 2 +- .../BelgianHealthFacilityLicenseNumber.cs | 20 +++++------ .../Facilities/HealthFacilityLicenseNumber.cs | 2 +- ...gianHealthcarePractitionerLicenseNumber.cs | 22 ++++++------ .../HealthcarePractitionerLicenseNumber.cs | 2 +- .../Prescriptions/BelgianMedicationCode.cs | 20 +++++------ .../Domain/Prescriptions/MedicationCode.cs | 2 +- .../PharmaceuticalPrescription.cs | 4 +-- .../Prescriptions/PrescriptionIdentifier.cs | 4 +-- 18 files changed, 95 insertions(+), 97 deletions(-) diff --git a/Src/DDD.Common/Domain/Alpha2CountryCode.cs b/Src/DDD.Common/Domain/Alpha2CountryCode.cs index 66f43f1..ad12dfb 100644 --- a/Src/DDD.Common/Domain/Alpha2CountryCode.cs +++ b/Src/DDD.Common/Domain/Alpha2CountryCode.cs @@ -10,9 +10,9 @@ public class Alpha2CountryCode : CountryCode #region Constructors - public Alpha2CountryCode(string code) : base(code) + public Alpha2CountryCode(string value) : base(value) { - Condition.Requires(code, nameof(code)) + Condition.Requires(value, nameof(value)) .HasLength(2) .Evaluate(c => c.IsAlphabetic()); } @@ -21,10 +21,10 @@ public Alpha2CountryCode(string code) : base(code) #region Methods - public static Alpha2CountryCode CreateIfNotEmpty(string code) + public static Alpha2CountryCode CreateIfNotEmpty(string value) { - if (string.IsNullOrWhiteSpace(code)) return null; - return new Alpha2CountryCode(code); + if (string.IsNullOrWhiteSpace(value)) return null; + return new Alpha2CountryCode(value); } #endregion Methods diff --git a/Src/DDD.Common/Domain/Alpha2LanguageCode.cs b/Src/DDD.Common/Domain/Alpha2LanguageCode.cs index e283826..b046664 100644 --- a/Src/DDD.Common/Domain/Alpha2LanguageCode.cs +++ b/Src/DDD.Common/Domain/Alpha2LanguageCode.cs @@ -10,9 +10,9 @@ public class Alpha2LanguageCode : LanguageCode #region Constructors - public Alpha2LanguageCode(string code) : base(code) + public Alpha2LanguageCode(string value) : base(value) { - Condition.Requires(code, nameof(code)) + Condition.Requires(value, nameof(value)) .HasLength(2) .Evaluate(c => c.IsAlphabetic()); } diff --git a/Src/DDD.Common/Domain/ArbitraryIdentifier.cs b/Src/DDD.Common/Domain/ArbitraryIdentifier.cs index c998845..e68ac01 100644 --- a/Src/DDD.Common/Domain/ArbitraryIdentifier.cs +++ b/Src/DDD.Common/Domain/ArbitraryIdentifier.cs @@ -14,16 +14,16 @@ public abstract class ArbitraryIdentifier : ComparableValueObject #region Constructors - protected ArbitraryIdentifier(TId identifier) + protected ArbitraryIdentifier(TId value) { - this.Identifier = identifier; + this.Value = value; } #endregion Constructors #region Properties - public TId Identifier { get; } + public TId Value { get; } #endregion Properties @@ -31,15 +31,15 @@ protected ArbitraryIdentifier(TId identifier) public override IEnumerable ComparableComponents() { - yield return this.Identifier; + yield return this.Value; } public override IEnumerable EqualityComponents() { - yield return this.Identifier; + yield return this.Value; } - public override string ToString() => $"{this.GetType().Name} [identifier={this.Identifier}]"; + public override string ToString() => $"{this.GetType().Name} [value={this.Value}]"; #endregion Methods diff --git a/Src/DDD.Common/Domain/BelgianSocialSecurityNumber.cs b/Src/DDD.Common/Domain/BelgianSocialSecurityNumber.cs index 3dd32db..15bbdf7 100644 --- a/Src/DDD.Common/Domain/BelgianSocialSecurityNumber.cs +++ b/Src/DDD.Common/Domain/BelgianSocialSecurityNumber.cs @@ -11,9 +11,9 @@ public class BelgianSocialSecurityNumber : SocialSecurityNumber #region Constructors - public BelgianSocialSecurityNumber(string number) : base(number) + public BelgianSocialSecurityNumber(string value) : base(value) { - Condition.Requires(Number, nameof(number)) + Condition.Requires(Value, nameof(value)) .HasLength(11) .Evaluate(c => c.IsNumeric()); } @@ -35,24 +35,24 @@ public enum Sex /// /// Computes the check digit based on the 9 first digits. /// - public static int ComputeCheckDigit(string number, bool bornBefore2000 = true) + public static int ComputeCheckDigit(string value, bool bornBefore2000 = true) { - Condition.Requires(number, nameof(number)).IsLongerOrEqual(9); + Condition.Requires(value, nameof(value)).IsLongerOrEqual(9); long identifier; if (bornBefore2000) - identifier = long.Parse(number.Substring(0, 9)); + identifier = long.Parse(value.Substring(0, 9)); else - identifier = long.Parse($"2{number.Substring(0, 9)}"); + identifier = long.Parse($"2{value.Substring(0, 9)}"); var modulus = 97; var remainder = modulus - (identifier % modulus); if (remainder == 0) return modulus; return (int)remainder; } - public static BelgianSocialSecurityNumber CreateIfNotEmpty(string number) + public static BelgianSocialSecurityNumber CreateIfNotEmpty(string value) { - if (string.IsNullOrWhiteSpace(number)) return null; - return new BelgianSocialSecurityNumber(number); + if (string.IsNullOrWhiteSpace(value)) return null; + return new BelgianSocialSecurityNumber(value); } /// @@ -71,7 +71,7 @@ public static BelgianSocialSecurityNumber CreateIfNotEmpty(string number) public int? BirthDay() { if (this.BirthMonth() == null) return null; - return int.Parse(this.Number.Substring(4, 2)); + return int.Parse(this.Value.Substring(4, 2)); } /// @@ -93,7 +93,7 @@ public static BelgianSocialSecurityNumber CreateIfNotEmpty(string number) /// public int? BirthYear() { - var year = this.Number.Substring(0, 2); + var year = this.Value.Substring(0, 2); if (year == "00") return null; if (this.BornBefore2000()) return int.Parse($"19{year}"); @@ -105,9 +105,9 @@ public static BelgianSocialSecurityNumber CreateIfNotEmpty(string number) public bool BornBefore2000() { var checkDigit = this.CheckDigit(); - var computedCheckDigit = ComputeCheckDigit(this.Number, bornBefore2000: true); + var computedCheckDigit = ComputeCheckDigit(this.Value, bornBefore2000: true); if (computedCheckDigit == checkDigit) return true; - computedCheckDigit = ComputeCheckDigit(this.Number, bornBefore2000: false); + computedCheckDigit = ComputeCheckDigit(this.Value, bornBefore2000: false); if (computedCheckDigit == checkDigit) return false; throw new InvalidOperationException("The check digit of the number is invalid."); } @@ -115,7 +115,7 @@ public bool BornBefore2000() /// /// Returns the check digit based on the 9 first digits. /// - public int CheckDigit() => int.Parse(this.Number.Substring(9, 2)); + public int CheckDigit() => int.Parse(this.Value.Substring(9, 2)); /// /// Determines whether the birthdate is only partially known. @@ -140,7 +140,7 @@ public bool BornBefore2000() /// /// Returns the unique identifier of the person. /// - public int PersonUniqueIdentifier() => int.Parse(this.Number.Substring(0, 9)); + public int PersonUniqueIdentifier() => int.Parse(this.Value.Substring(0, 9)); /// /// Returns a sequence number used for distinguishing people with identical birthdates. @@ -150,8 +150,8 @@ public bool BornBefore2000() /// public int SequenceNumber() { - if (this.HasPartialBirthdate()) return int.Parse(this.Number.Substring(5, 4)); - return int.Parse(this.Number.Substring(6, 3)); + if (this.HasPartialBirthdate()) return int.Parse(this.Value.Substring(5, 4)); + return int.Parse(this.Value.Substring(6, 3)); } /// @@ -159,7 +159,7 @@ public int SequenceNumber() /// public Sex SexAtBirth() => this.SequenceNumber() % 2 == 0 ? Sex.Female : Sex.Male; - private int MonthNumber() => int.Parse(this.Number.Substring(2, 2)); + private int MonthNumber() => int.Parse(this.Value.Substring(2, 2)); #endregion Methods diff --git a/Src/DDD.Common/Domain/BinaryContent.cs b/Src/DDD.Common/Domain/BinaryContent.cs index fed5fec..12dfaa8 100644 --- a/Src/DDD.Common/Domain/BinaryContent.cs +++ b/Src/DDD.Common/Domain/BinaryContent.cs @@ -13,33 +13,31 @@ public class BinaryContent : ValueObject #region Fields - private static readonly BinaryContent emptyContent = new BinaryContent(new byte[0]); + private static readonly BinaryContent Empty = new BinaryContent(new byte[0]); #endregion Fields #region Constructors - public BinaryContent(byte[] content) + public BinaryContent(byte[] data) { - Condition.Requires(content, nameof(content)).IsNotNull(); - this.Content = content; + Condition.Requires(data, nameof(data)).IsNotNull(); + this.Data = data; } #endregion Constructors #region Properties - public byte[] Content { get; } + public byte[] Data { get; } #endregion Properties #region Methods - public static BinaryContent Empty() => emptyContent; - public override IEnumerable EqualityComponents() { - foreach (var component in this.Content) + foreach (var component in this.Data) yield return component; } @@ -48,9 +46,9 @@ public override IEnumerable EqualityComponents() /// public override IEnumerable HashCodeComponents() { - var startIndex = this.Content.Length > 8 ? this.Content.Length - 8 : 0; - for (int i = startIndex; i < this.Content.Length; i++) - yield return this.Content[i]; + var startIndex = this.Data.Length > 8 ? this.Data.Length - 8 : 0; + for (int i = startIndex; i < this.Data.Length; i++) + yield return this.Data[i]; } #endregion Methods diff --git a/Src/DDD.Common/Domain/CountryCode.cs b/Src/DDD.Common/Domain/CountryCode.cs index 74dea04..8a1b8e4 100644 --- a/Src/DDD.Common/Domain/CountryCode.cs +++ b/Src/DDD.Common/Domain/CountryCode.cs @@ -5,7 +5,7 @@ public abstract class CountryCode : IdentificationCode #region Constructors - protected CountryCode(string code) : base(code) + protected CountryCode(string value) : base(value) { } diff --git a/Src/DDD.Common/Domain/IdentificationCode.cs b/Src/DDD.Common/Domain/IdentificationCode.cs index 01ededa..8be9606 100644 --- a/Src/DDD.Common/Domain/IdentificationCode.cs +++ b/Src/DDD.Common/Domain/IdentificationCode.cs @@ -14,17 +14,17 @@ public abstract class IdentificationCode : ComparableValueObject #region Constructors - protected IdentificationCode(string code) + protected IdentificationCode(string value) { - Condition.Requires(code, nameof(code)).IsNotNullOrWhiteSpace(); - this.Code = code.ToUpper(); + Condition.Requires(value, nameof(value)).IsNotNullOrWhiteSpace(); + this.Value = value.ToUpper(); } #endregion Constructors #region Properties - public string Code { get; } + public string Value { get; } #endregion Properties @@ -32,15 +32,15 @@ protected IdentificationCode(string code) public override IEnumerable ComparableComponents() { - yield return this.Code; + yield return this.Value; } public override IEnumerable EqualityComponents() { - yield return this.Code; + yield return this.Value; } - public override string ToString() => $"{this.GetType().Name} [code={this.Code}]"; + public override string ToString() => $"{this.GetType().Name} [value={this.Value}]"; #endregion Methods diff --git a/Src/DDD.Common/Domain/IdentificationNumber.cs b/Src/DDD.Common/Domain/IdentificationNumber.cs index ae1ce0b..092664a 100644 --- a/Src/DDD.Common/Domain/IdentificationNumber.cs +++ b/Src/DDD.Common/Domain/IdentificationNumber.cs @@ -14,17 +14,17 @@ public abstract class IdentificationNumber : ComparableValueObject #region Constructors - protected IdentificationNumber(string number) + protected IdentificationNumber(string value) { - Condition.Requires(number, nameof(number)).IsNotNullOrWhiteSpace(); - this.Number = number.ToUpper(); + Condition.Requires(value, nameof(value)).IsNotNullOrWhiteSpace(); + this.Value = value.ToUpper(); } #endregion Constructors #region Properties - public string Number { get; } + public string Value { get; } #endregion Properties @@ -32,15 +32,15 @@ protected IdentificationNumber(string number) public override IEnumerable ComparableComponents() { - yield return this.Number; + yield return this.Value; } public override IEnumerable EqualityComponents() { - yield return this.Number; + yield return this.Value; } - public override string ToString() => $"{this.GetType().Name} [number={this.Number}]"; + public override string ToString() => $"{this.GetType().Name} [value={this.Value}]"; #endregion Methods diff --git a/Src/DDD.Common/Domain/LanguageCode.cs b/Src/DDD.Common/Domain/LanguageCode.cs index c77e52c..b181dad 100644 --- a/Src/DDD.Common/Domain/LanguageCode.cs +++ b/Src/DDD.Common/Domain/LanguageCode.cs @@ -5,7 +5,7 @@ public abstract class LanguageCode : IdentificationCode #region Constructors - protected LanguageCode(string code) : base(code) + protected LanguageCode(string value) : base(value) { } diff --git a/Src/DDD.Common/Domain/SocialSecurityNumber.cs b/Src/DDD.Common/Domain/SocialSecurityNumber.cs index 283662a..dc871d2 100644 --- a/Src/DDD.Common/Domain/SocialSecurityNumber.cs +++ b/Src/DDD.Common/Domain/SocialSecurityNumber.cs @@ -4,7 +4,7 @@ public abstract class SocialSecurityNumber : IdentificationNumber { #region Constructors - protected SocialSecurityNumber(string number) : base(number) + protected SocialSecurityNumber(string value) : base(value) { } diff --git a/Src/DDD.HealthcareDelivery/Domain/Facilities/BelgianHealthFacilityLicenseNumber.cs b/Src/DDD.HealthcareDelivery/Domain/Facilities/BelgianHealthFacilityLicenseNumber.cs index 72699f1..24d8536 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Facilities/BelgianHealthFacilityLicenseNumber.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Facilities/BelgianHealthFacilityLicenseNumber.cs @@ -10,9 +10,9 @@ public class BelgianHealthFacilityLicenseNumber : HealthFacilityLicenseNumber #region Constructors - public BelgianHealthFacilityLicenseNumber(string number) : base(number) + public BelgianHealthFacilityLicenseNumber(string value) : base(value) { - Condition.Requires(number, nameof(number)) + Condition.Requires(value, nameof(value)) .Evaluate(n => n.IsNumeric() && (n.Length == 11 || n.Length == 8)); } @@ -23,30 +23,30 @@ public BelgianHealthFacilityLicenseNumber(string number) : base(number) /// /// Computes the check digit based on the 6 first digits. /// - public static int ComputeCheckDigit(string number) + public static int ComputeCheckDigit(string value) { - Condition.Requires(number, nameof(number)).IsLongerOrEqual(6); - var identifier = int.Parse(number.Substring(0, 6)); + 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 number) + public static BelgianHealthFacilityLicenseNumber CreateIfNotEmpty(string value) { - if (string.IsNullOrWhiteSpace(number)) return null; - return new BelgianHealthFacilityLicenseNumber(number); + 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.Number.Substring(6, 2)); + public int CheckDigit() => int.Parse(this.Value.Substring(6, 2)); /// /// Returns the unique identifier of the health facility. /// - public int FacilityUniqueIdentifier() => int.Parse(this.Number.Substring(0, 6)); + public int FacilityUniqueIdentifier() => int.Parse(this.Value.Substring(0, 6)); #endregion Methods } diff --git a/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacilityLicenseNumber.cs b/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacilityLicenseNumber.cs index 3d188f1..55007bb 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacilityLicenseNumber.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacilityLicenseNumber.cs @@ -7,7 +7,7 @@ public abstract class HealthFacilityLicenseNumber : IdentificationNumber #region Constructors - protected HealthFacilityLicenseNumber(string number) : base(number) + protected HealthFacilityLicenseNumber(string value) : base(value) { } diff --git a/Src/DDD.HealthcareDelivery/Domain/Practitioners/BelgianHealthcarePractitionerLicenseNumber.cs b/Src/DDD.HealthcareDelivery/Domain/Practitioners/BelgianHealthcarePractitionerLicenseNumber.cs index 97de0e7..ce9c723 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Practitioners/BelgianHealthcarePractitionerLicenseNumber.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Practitioners/BelgianHealthcarePractitionerLicenseNumber.cs @@ -10,9 +10,9 @@ public class BelgianHealthcarePractitionerLicenseNumber : HealthcarePractitioner #region Constructors - public BelgianHealthcarePractitionerLicenseNumber(string number) : base(number) + public BelgianHealthcarePractitionerLicenseNumber(string value) : base(value) { - Condition.Requires(number, nameof(number)) + Condition.Requires(value, nameof(value)) .HasLength(11) .Evaluate(n => n.IsNumeric()); } @@ -60,10 +60,10 @@ public enum Modulus /// /// Computes the check digit based on the 6 first digits. /// - public static int ComputeCheckDigit(string number, Modulus modulus = Modulus.Mod97) + public static int ComputeCheckDigit(string value, Modulus modulus = Modulus.Mod97) { - Condition.Requires(number, nameof(number)).IsLongerOrEqual(6); - var identifier = int.Parse(number.Substring(0, 6)); // old unique practitioner identifier + Condition.Requires(value, nameof(value)).IsLongerOrEqual(6); + var identifier = int.Parse(value.Substring(0, 6)); // old unique practitioner identifier var imodulus = (int)modulus; return imodulus - (identifier % imodulus); } @@ -71,32 +71,32 @@ public static int ComputeCheckDigit(string number, Modulus modulus = Modulus.Mod /// /// Returns the check digit based on the 6 first digits. /// - public int CheckDigit() => int.Parse(this.Number.Substring(6, 2)); + public int CheckDigit() => int.Parse(this.Value.Substring(6, 2)); /// /// Returns the registration number attributed to the healthcare practitioner by the Provincial Medical Commission. /// - public int PractitionerIdentifier() => int.Parse(this.Number.Substring(2, 4)); + public int PractitionerIdentifier() => int.Parse(this.Value.Substring(2, 4)); /// /// Returns the unique identifier of the healthcare practitioner. /// - public int PractitionerUniqueIdentifier() => int.Parse(this.Number.Substring(0, 8)); + public int PractitionerUniqueIdentifier() => int.Parse(this.Value.Substring(0, 8)); /// /// Returns the profession of the healthcare practitioner. /// - public HealthProfession Profession() => (HealthProfession)int.Parse(this.Number[0].ToString()); + public HealthProfession Profession() => (HealthProfession)int.Parse(this.Value[0].ToString()); /// /// Returns the province of the healthcare practitioner at the registration time. /// - public BelgianProvince Province() => (BelgianProvince)int.Parse(this.Number[1].ToString()); + public BelgianProvince Province() => (BelgianProvince)int.Parse(this.Value[1].ToString()); /// /// Returns the professional qualification code of the healthcare practitioner. /// - public string QualificationCode() => this.Number.Substring(8, 3); + public string QualificationCode() => this.Value.Substring(8, 3); #endregion Methods diff --git a/Src/DDD.HealthcareDelivery/Domain/Practitioners/HealthcarePractitionerLicenseNumber.cs b/Src/DDD.HealthcareDelivery/Domain/Practitioners/HealthcarePractitionerLicenseNumber.cs index 67b934d..462b254 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Practitioners/HealthcarePractitionerLicenseNumber.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Practitioners/HealthcarePractitionerLicenseNumber.cs @@ -10,7 +10,7 @@ public abstract class HealthcarePractitionerLicenseNumber : IdentificationNumber #region Constructors - protected HealthcarePractitionerLicenseNumber(string number) : base(number) + protected HealthcarePractitionerLicenseNumber(string value) : base(value) { } diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/BelgianMedicationCode.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/BelgianMedicationCode.cs index 9b0c568..e05264d 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/BelgianMedicationCode.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/BelgianMedicationCode.cs @@ -10,9 +10,9 @@ public class BelgianMedicationCode : MedicationCode #region Constructors - public BelgianMedicationCode(string code) : base(code) + public BelgianMedicationCode(string value) : base(value) { - Condition.Requires(code, nameof(code)) + Condition.Requires(value, nameof(value)) .HasLength(7) .Evaluate(c => c.IsNumeric()); } @@ -24,12 +24,12 @@ public BelgianMedicationCode(string code) : base(code) /// /// Computes the check digit based on the 6 first digits. /// - public static int ComputeCheckDigit(string code) + public static int ComputeCheckDigit(string value) { - Condition.Requires(code, nameof(code)) + Condition.Requires(value, nameof(value)) .IsLongerOrEqual(6) .Evaluate(c => c.IsNumeric()); - var identifier = code.Substring(0, 6); + var identifier = value.Substring(0, 6); var sum = 0; var alternate = true; for (int i = identifier.Length - 1; i >= 0; i--) @@ -51,21 +51,21 @@ public static int ComputeCheckDigit(string code) return 10 - remainder; } - public static BelgianMedicationCode CreateIfNotEmpty(string code) + public static BelgianMedicationCode CreateIfNotEmpty(string value) { - if (string.IsNullOrWhiteSpace(code)) return null; - return new BelgianMedicationCode(code); + if (string.IsNullOrWhiteSpace(value)) return null; + return new BelgianMedicationCode(value); } /// /// Returns the check digit based on the 6 first digits. /// - public int CheckDigit() => int.Parse(this.Code[6].ToString()); + public int CheckDigit() => int.Parse(this.Value[6].ToString()); /// /// Returns the unique identifier of the medication. /// - public int MedicationUniqueIdentifier() => int.Parse(this.Code.Substring(0, 6)); + public int MedicationUniqueIdentifier() => int.Parse(this.Value.Substring(0, 6)); #endregion Methods diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/MedicationCode.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/MedicationCode.cs index 22049f2..e3e736e 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/MedicationCode.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/MedicationCode.cs @@ -10,7 +10,7 @@ public abstract class MedicationCode : IdentificationCode #region Constructors - protected MedicationCode(string code) : base(code) + 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 e39a1cb..81e6af0 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs @@ -70,7 +70,7 @@ public static PharmaceuticalPrescription Create(PrescriptionIdentifier identifie createdOn, delivrableAt ); - prescription.AddEvent(new PharmaceuticalPrescriptionCreated(identifier.Identifier, createdOn)); + prescription.AddEvent(new PharmaceuticalPrescriptionCreated(identifier.Value, createdOn)); return prescription; } @@ -89,7 +89,7 @@ public static PharmaceuticalPrescription Create(PrescriptionIdentifier identifie protected override void AddPrescriptionRevokedEvent(string reason) { - this.AddEvent(new PharmaceuticalPrescriptionRevoked(this.Identifier.Identifier, reason)); + this.AddEvent(new PharmaceuticalPrescriptionRevoked(this.Identifier.Value, reason)); } #endregion Methods diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescriptionIdentifier.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescriptionIdentifier.cs index 20c4dec..c740a82 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescriptionIdentifier.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescriptionIdentifier.cs @@ -9,9 +9,9 @@ public class PrescriptionIdentifier : ArbitraryIdentifier #region Constructors - public PrescriptionIdentifier(int identifier) : base(identifier) + public PrescriptionIdentifier(int value) : base(value) { - Condition.Requires(identifier, nameof(identifier)).IsGreaterThan(0); + Condition.Requires(value, nameof(value)).IsGreaterThan(0); } #endregion Constructors From 31eb040add9437e66e0b35a0b21ad5dcd6705f84 Mon Sep 17 00:00:00 2001 From: draphyz Date: Wed, 5 Jun 2019 08:57:57 +0200 Subject: [PATCH 007/111] Add private setters and protected constructors --- Src/DDD.Common/Domain/Alpha2CountryCode.cs | 2 + Src/DDD.Common/Domain/Alpha2LanguageCode.cs | 2 + Src/DDD.Common/Domain/ArbitraryIdentifier.cs | 4 +- .../Domain/BelgianSocialSecurityNumber.cs | 2 + Src/DDD.Common/Domain/BinaryContent.cs | 4 +- Src/DDD.Common/Domain/ContactInformation.cs | 16 +- Src/DDD.Common/Domain/CountryCode.cs | 2 + Src/DDD.Common/Domain/EmailAddress.cs | 6 +- Src/DDD.Common/Domain/FullName.cs | 6 +- Src/DDD.Common/Domain/IdentificationCode.cs | 4 +- Src/DDD.Common/Domain/IdentificationNumber.cs | 4 +- Src/DDD.Common/Domain/LanguageCode.cs | 2 + Src/DDD.Common/Domain/PostalAddress.cs | 14 +- Src/DDD.Common/Domain/SocialSecurityNumber.cs | 3 + Src/DDD.Core.NHibernate/CompositeUserType.cs | 3 +- .../MemberInfoExtensions.cs | 56 ++++- .../DDD.HealthcareDelivery.csproj | 22 ++ .../BelgianHealthFacilityLicenseNumber.cs | 3 + .../Domain/Facilities/HealthFacility.cs | 8 +- .../Facilities/HealthFacilityLicenseNumber.cs | 2 + .../Domain/Facilities/Hospital.cs | 3 +- .../Domain/Facilities/MedicalOffice.cs | 9 +- .../Domain/Patients/Patient.cs | 14 +- ...gianHealthcarePractitionerLicenseNumber.cs | 2 + .../Practitioners/HealthcarePractitioner.cs | 22 +- .../HealthcarePractitionerLicenseNumber.cs | 2 + .../Domain/Practitioners/Physician.cs | 3 +- .../Prescriptions/BelgianMedicationCode.cs | 2 + .../Domain/Prescriptions/MedicationCode.cs | 2 + .../PharmaceuticalPrescription.cs | 2 + .../Prescriptions/PrescribedMedication.cs | 12 +- .../PrescribedPharmaceuticalCompounding.cs | 2 + .../PrescribedPharmaceuticalProduct.cs | 3 +- .../PrescribedPharmaceuticalSubstance.cs | 3 +- .../Domain/Prescriptions/Prescription.cs | 16 +- .../Prescriptions/PrescriptionIdentifier.cs | 4 + ...BelgianSqlServerHealthcareConfiguration.cs | 29 +++ .../Infrastructure/HealthcareConfiguration.cs | 42 ++++ .../OracleHealthcareConfiguration.cs | 22 ++ .../BelgianSqlServerPrescriptionMapping.cs | 9 + .../Prescriptions/ContactInformationType.cs | 49 +++++ .../Prescriptions/FullNameType.cs | 23 ++ ...HealthcarePractitionerLicenseNumberType.cs | 21 ++ .../HealthcarePractitionerType.cs | 48 +++++ .../OraclePrescriptionMapping.cs | 20 ++ .../PharmaceuticalPrescriptionMapping.cs | 16 ++ .../Prescriptions/PostalAddressType.cs | 57 +++++ .../Prescriptions/PrescriptionMapping.cs | 200 ++++++++++++++++++ .../SqlServerPrescriptionMapping.cs | 16 ++ .../SqlServerHealthcareConfiguration.cs | 22 ++ ...HealthcareDelivery.IntegrationTests.csproj | 20 ++ ...anSqlServerHealthcareConfigurationTests.cs | 10 + .../HealthcareConfigurationTests.cs | 113 ++++++++++ .../packages.config | 5 + 54 files changed, 923 insertions(+), 65 deletions(-) create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/BelgianSqlServerHealthcareConfiguration.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/HealthcareConfiguration.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareConfiguration.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/BelgianSqlServerPrescriptionMapping.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/ContactInformationType.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/FullNameType.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthcarePractitionerLicenseNumberType.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthcarePractitionerType.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionMapping.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionMapping.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PostalAddressType.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescriptionMapping.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareConfiguration.cs create mode 100644 Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareConfigurationTests.cs create mode 100644 Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareConfigurationTests.cs diff --git a/Src/DDD.Common/Domain/Alpha2CountryCode.cs b/Src/DDD.Common/Domain/Alpha2CountryCode.cs index ad12dfb..08f9203 100644 --- a/Src/DDD.Common/Domain/Alpha2CountryCode.cs +++ b/Src/DDD.Common/Domain/Alpha2CountryCode.cs @@ -17,6 +17,8 @@ public Alpha2CountryCode(string value) : base(value) .Evaluate(c => c.IsAlphabetic()); } + protected Alpha2CountryCode() { } + #endregion Constructors #region Methods diff --git a/Src/DDD.Common/Domain/Alpha2LanguageCode.cs b/Src/DDD.Common/Domain/Alpha2LanguageCode.cs index b046664..55f6f43 100644 --- a/Src/DDD.Common/Domain/Alpha2LanguageCode.cs +++ b/Src/DDD.Common/Domain/Alpha2LanguageCode.cs @@ -17,6 +17,8 @@ public Alpha2LanguageCode(string value) : base(value) .Evaluate(c => c.IsAlphabetic()); } + protected Alpha2LanguageCode() { } + #endregion Constructors } diff --git a/Src/DDD.Common/Domain/ArbitraryIdentifier.cs b/Src/DDD.Common/Domain/ArbitraryIdentifier.cs index e68ac01..6561727 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 diff --git a/Src/DDD.Common/Domain/BelgianSocialSecurityNumber.cs b/Src/DDD.Common/Domain/BelgianSocialSecurityNumber.cs index 15bbdf7..80b62a5 100644 --- a/Src/DDD.Common/Domain/BelgianSocialSecurityNumber.cs +++ b/Src/DDD.Common/Domain/BelgianSocialSecurityNumber.cs @@ -18,6 +18,8 @@ public BelgianSocialSecurityNumber(string value) : base(value) .Evaluate(c => c.IsNumeric()); } + protected BelgianSocialSecurityNumber() { } + #endregion Constructors #region Enums diff --git a/Src/DDD.Common/Domain/BinaryContent.cs b/Src/DDD.Common/Domain/BinaryContent.cs index 12dfaa8..1d196b9 100644 --- a/Src/DDD.Common/Domain/BinaryContent.cs +++ b/Src/DDD.Common/Domain/BinaryContent.cs @@ -25,11 +25,13 @@ public BinaryContent(byte[] data) this.Data = data; } + protected BinaryContent() { } + #endregion Constructors #region Properties - public byte[] Data { get; } + public byte[] Data { get; private set; } #endregion Properties diff --git a/Src/DDD.Common/Domain/ContactInformation.cs b/Src/DDD.Common/Domain/ContactInformation.cs index 9011d52..65a8863 100644 --- a/Src/DDD.Common/Domain/ContactInformation.cs +++ b/Src/DDD.Common/Domain/ContactInformation.cs @@ -30,23 +30,25 @@ public ContactInformation(PostalAddress postalAddress, 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 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 a905a6a..1145d4a 100644 --- a/Src/DDD.Common/Domain/EmailAddress.cs +++ b/Src/DDD.Common/Domain/EmailAddress.cs @@ -1,8 +1,8 @@ using Conditions; +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; -using System; namespace DDD.Common.Domain { @@ -21,11 +21,13 @@ public EmailAddress(string address) this.Address = address; } + protected EmailAddress() { } + #endregion Constructors #region Properties - public string Address { get; } + public string Address { get; private set; } #endregion Properties diff --git a/Src/DDD.Common/Domain/FullName.cs b/Src/DDD.Common/Domain/FullName.cs index 1bebf55..46b7433 100644 --- a/Src/DDD.Common/Domain/FullName.cs +++ b/Src/DDD.Common/Domain/FullName.cs @@ -19,13 +19,15 @@ public FullName(string lastName, string firstName) this.FirstName = firstName.ToTitleCase(); } + 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 diff --git a/Src/DDD.Common/Domain/IdentificationCode.cs b/Src/DDD.Common/Domain/IdentificationCode.cs index 8be9606..acde891 100644 --- a/Src/DDD.Common/Domain/IdentificationCode.cs +++ b/Src/DDD.Common/Domain/IdentificationCode.cs @@ -14,6 +14,8 @@ public abstract class IdentificationCode : ComparableValueObject #region Constructors + protected IdentificationCode() { } + protected IdentificationCode(string value) { Condition.Requires(value, nameof(value)).IsNotNullOrWhiteSpace(); @@ -24,7 +26,7 @@ protected IdentificationCode(string value) #region Properties - public string Value { get; } + public string Value { get; private set; } #endregion Properties diff --git a/Src/DDD.Common/Domain/IdentificationNumber.cs b/Src/DDD.Common/Domain/IdentificationNumber.cs index 092664a..b7609fc 100644 --- a/Src/DDD.Common/Domain/IdentificationNumber.cs +++ b/Src/DDD.Common/Domain/IdentificationNumber.cs @@ -14,6 +14,8 @@ public abstract class IdentificationNumber : ComparableValueObject #region Constructors + protected IdentificationNumber() { } + protected IdentificationNumber(string value) { Condition.Requires(value, nameof(value)).IsNotNullOrWhiteSpace(); @@ -24,7 +26,7 @@ protected IdentificationNumber(string value) #region Properties - public string Value { get; } + public string Value { get; private set; } #endregion Properties 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 912cf3e..6350380 100644 --- a/Src/DDD.Common/Domain/PostalAddress.cs +++ b/Src/DDD.Common/Domain/PostalAddress.cs @@ -30,21 +30,23 @@ 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 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.Core.NHibernate/CompositeUserType.cs b/Src/DDD.Core.NHibernate/CompositeUserType.cs index 61e1b93..4467002 100644 --- a/Src/DDD.Core.NHibernate/CompositeUserType.cs +++ b/Src/DDD.Core.NHibernate/CompositeUserType.cs @@ -201,7 +201,8 @@ object ICompositeUserType.NullSafeGet(DbDataReader dr, string[] names, ISessionI var mappedClass = this.returnedClass; if (this.HasSubComponents()) { - var discriminatorValue = dr[this.DiscriminatorName()]; + var discriminatorColumn = names.First(n => n.StartsWith(this.DiscriminatorName(), StringComparison.OrdinalIgnoreCase)); + var discriminatorValue = dr[discriminatorColumn]; mappedClass = this.GetMappedClass(discriminatorValue); } var result = this.Instantiate(mappedClass); diff --git a/Src/DDD.Core.NHibernate/MemberInfoExtensions.cs b/Src/DDD.Core.NHibernate/MemberInfoExtensions.cs index 084c6ff..5a97065 100644 --- a/Src/DDD.Core.NHibernate/MemberInfoExtensions.cs +++ b/Src/DDD.Core.NHibernate/MemberInfoExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Reflection; using NHibernate; +using System.Linq; namespace DDD.Core.Infrastructure.Data { @@ -15,9 +16,10 @@ public static object GetPropertyOrFieldValue(this MemberInfo propertyOrField, ob { case MemberTypes.Property: var property = ((PropertyInfo)propertyOrField); - if (property.GetMethod == null) + var getter = property.GetGetMethodRecursively(); + if (getter == null) throw new PropertyNotFoundException(obj.GetType(), property.Name, "getter"); - return property.GetValue(obj); + return getter.Invoke(obj, new object[] { }); case MemberTypes.Field: return ((FieldInfo)propertyOrField).GetValue(obj); default: @@ -32,9 +34,10 @@ public static void SetPropertyOrFieldValue(this MemberInfo propertyOrField, obje { case MemberTypes.Property: var property = ((PropertyInfo)propertyOrField); - if (property.SetMethod == null) + var setter = property.GetSetMethodRecursively(); + if (setter == null) throw new PropertyNotFoundException(obj.GetType(), property.Name, "setter"); - property.SetValue(obj, value); + setter.Invoke(obj, new [] { value }); break; case MemberTypes.Field: ((FieldInfo)propertyOrField).SetValue(obj, value); @@ -45,6 +48,51 @@ public static void SetPropertyOrFieldValue(this MemberInfo propertyOrField, obje } } + 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.HealthcareDelivery/DDD.HealthcareDelivery.csproj b/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj index cf5012c..d08ee1e 100644 --- a/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj +++ b/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj @@ -116,17 +116,31 @@ + + + + + + + + + + + + + SqlScripts.resx True True + @@ -140,6 +154,10 @@ {40A849C5-C8D7-4F76-856A-138AED73A6C3} DDD.Common.Messages + + {7466b831-1642-4b2b-9ea3-1c9299596c2a} + DDD.Common.NHibernate + {0b70b4fd-f5a0-4a6c-a3fd-90031e08c1c2} DDD.Common @@ -156,6 +174,10 @@ {2438B31A-3A39-4878-81FA-BE5AE715EAE5} DDD.Core.Messages + + {d4fc7e1b-cae8-40f9-9faf-0029d91cc6c4} + DDD.Core.NHibernate + {c6c3e419-b9aa-44ad-9dbf-789294687ae6} DDD.Core diff --git a/Src/DDD.HealthcareDelivery/Domain/Facilities/BelgianHealthFacilityLicenseNumber.cs b/Src/DDD.HealthcareDelivery/Domain/Facilities/BelgianHealthFacilityLicenseNumber.cs index 24d8536..94e067e 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Facilities/BelgianHealthFacilityLicenseNumber.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Facilities/BelgianHealthFacilityLicenseNumber.cs @@ -16,6 +16,8 @@ public BelgianHealthFacilityLicenseNumber(string value) : base(value) .Evaluate(n => n.IsNumeric() && (n.Length == 11 || n.Length == 8)); } + protected BelgianHealthFacilityLicenseNumber() { } + #endregion Constructors #region Methods @@ -49,5 +51,6 @@ public static BelgianHealthFacilityLicenseNumber CreateIfNotEmpty(string value) public int FacilityUniqueIdentifier() => int.Parse(this.Value.Substring(0, 6)); #endregion Methods + } } diff --git a/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacility.cs b/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacility.cs index 71813a9..849b4d5 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacility.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacility.cs @@ -13,6 +13,8 @@ public abstract class HealthFacility : ValueObject #region Constructors + protected HealthFacility() { } + protected HealthFacility(int identifier, string name, HealthFacilityLicenseNumber licenseNumber = null) @@ -28,11 +30,11 @@ protected HealthFacility(int identifier, #region Properties - public int Identifier { get; } + public int Identifier { get; private set; } - public HealthFacilityLicenseNumber LicenseNumber { get; } + public HealthFacilityLicenseNumber LicenseNumber { get; private set; } - public string Name { get; } + public string Name { get; private set; } #endregion Properties diff --git a/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacilityLicenseNumber.cs b/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacilityLicenseNumber.cs index 55007bb..4a48c04 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacilityLicenseNumber.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacilityLicenseNumber.cs @@ -7,6 +7,8 @@ public abstract class HealthFacilityLicenseNumber : IdentificationNumber #region Constructors + protected HealthFacilityLicenseNumber() { } + protected HealthFacilityLicenseNumber(string value) : base(value) { } diff --git a/Src/DDD.HealthcareDelivery/Domain/Facilities/Hospital.cs b/Src/DDD.HealthcareDelivery/Domain/Facilities/Hospital.cs index eeb19f0..61d14fc 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Facilities/Hospital.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Facilities/Hospital.cs @@ -15,7 +15,8 @@ public Hospital(int identifier, { } - #endregion Constructors + protected Hospital() { } + #endregion Constructors } } diff --git a/Src/DDD.HealthcareDelivery/Domain/Facilities/MedicalOffice.cs b/Src/DDD.HealthcareDelivery/Domain/Facilities/MedicalOffice.cs index 8e4c4fb..2905672 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Facilities/MedicalOffice.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Facilities/MedicalOffice.cs @@ -8,14 +8,15 @@ public class MedicalOffice : HealthFacility #region Constructors - public MedicalOffice(int identifier, - string name, - HealthFacilityLicenseNumber licenseNumber = null) + public MedicalOffice(int identifier, + string name, + HealthFacilityLicenseNumber licenseNumber = null) : base(identifier, name, licenseNumber) { } - #endregion Constructors + protected MedicalOffice() { } + #endregion Constructors } } diff --git a/Src/DDD.HealthcareDelivery/Domain/Patients/Patient.cs b/Src/DDD.HealthcareDelivery/Domain/Patients/Patient.cs index b0fa0f9..e7cff33 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Patients/Patient.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Patients/Patient.cs @@ -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 diff --git a/Src/DDD.HealthcareDelivery/Domain/Practitioners/BelgianHealthcarePractitionerLicenseNumber.cs b/Src/DDD.HealthcareDelivery/Domain/Practitioners/BelgianHealthcarePractitionerLicenseNumber.cs index ce9c723..d91802e 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Practitioners/BelgianHealthcarePractitionerLicenseNumber.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Practitioners/BelgianHealthcarePractitionerLicenseNumber.cs @@ -17,6 +17,8 @@ public BelgianHealthcarePractitionerLicenseNumber(string value) : base(value) .Evaluate(n => n.IsNumeric()); } + protected BelgianHealthcarePractitionerLicenseNumber() { } + #endregion Constructors #region Enums diff --git a/Src/DDD.HealthcareDelivery/Domain/Practitioners/HealthcarePractitioner.cs b/Src/DDD.HealthcareDelivery/Domain/Practitioners/HealthcarePractitioner.cs index 41f123c..3c91d97 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 Conditions; +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 + public abstract class HealthcarePractitioner : ValueObject { #region Constructors + protected HealthcarePractitioner() { } + protected HealthcarePractitioner(int identifier, FullName fullName, HealthcarePractitionerLicenseNumber 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 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/Physician.cs b/Src/DDD.HealthcareDelivery/Domain/Practitioners/Physician.cs index d8e3649..2b20bc4 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Practitioners/Physician.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Practitioners/Physician.cs @@ -21,7 +21,8 @@ public Physician(int identifier, { } - #endregion Constructors + protected Physician() { } + #endregion Constructors } } diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/BelgianMedicationCode.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/BelgianMedicationCode.cs index e05264d..1916132 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/BelgianMedicationCode.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/BelgianMedicationCode.cs @@ -17,6 +17,8 @@ public BelgianMedicationCode(string value) : base(value) .Evaluate(c => c.IsNumeric()); } + protected BelgianMedicationCode() { } + #endregion Constructors #region 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 81e6af0..26d0bf7 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs @@ -45,6 +45,8 @@ public PharmaceuticalPrescription(PrescriptionIdentifier identifier, this.prescribedMedications.AddRange(prescribedMedications); } + protected PharmaceuticalPrescription() { } + #endregion Constructors #region Methods diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedication.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedication.cs index 4e2c59b..fc0b59e 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedication.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedication.cs @@ -16,6 +16,8 @@ public abstract class PrescribedMedication : ValueObject #region Constructors + protected PrescribedMedication() { } + protected PrescribedMedication(string nameOrDescription, string posology = null, string quantity = null, @@ -39,15 +41,15 @@ protected PrescribedMedication(string nameOrDescription, #region Properties - public MedicationCode Code { get; } + public MedicationCode Code { get; private set; } - public string Duration { get; } + public string Duration { get; private set; } - public string NameOrDescription { get; } + public string NameOrDescription { get; private set; } - public string Posology { get; } + public string Posology { get; private set; } - public string Quantity { get; } + public string Quantity { get; private set; } #endregion Properties diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalCompounding.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalCompounding.cs index 3554260..7ab43f4 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalCompounding.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalCompounding.cs @@ -8,6 +8,8 @@ public class PrescribedPharmaceuticalCompounding : PrescribedMedication #region Constructors + protected PrescribedPharmaceuticalCompounding() { } + public PrescribedPharmaceuticalCompounding(string nameOrDescription, string posology = null, string quantity = null, diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalProduct.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalProduct.cs index 3a0b9f0..0b09031 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalProduct.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalProduct.cs @@ -18,7 +18,8 @@ public PrescribedPharmaceuticalProduct(string nameOrDescription, { } - #endregion Constructors + 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 720db4c..dd23cf0 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalSubstance.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalSubstance.cs @@ -18,7 +18,8 @@ public PrescribedPharmaceuticalSubstance(string nameOrDescription, { } - #endregion Constructors + 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 86b17ef..3a52015 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/Prescription.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/Prescription.cs @@ -18,6 +18,8 @@ public abstract class Prescription : DomainEntity #region Constructors + protected Prescription() { } + protected Prescription(PrescriptionIdentifier identifier, HealthcarePractitioner prescriber, Patient patient, @@ -49,19 +51,19 @@ protected Prescription(PrescriptionIdentifier identifier, #region Properties - public DateTime CreatedOn { get; } + public DateTime CreatedOn { get; private set; } - public DateTime? DelivrableAt { get; } + public DateTime? DelivrableAt { get; private set; } - public HealthFacility HealthFacility { get; } + public HealthFacility HealthFacility { 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; } diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescriptionIdentifier.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescriptionIdentifier.cs index c740a82..6baa80a 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescriptionIdentifier.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescriptionIdentifier.cs @@ -14,6 +14,10 @@ public PrescriptionIdentifier(int value) : base(value) Condition.Requires(value, nameof(value)).IsGreaterThan(0); } + protected PrescriptionIdentifier() + { + } + #endregion Constructors } diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/BelgianSqlServerHealthcareConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/BelgianSqlServerHealthcareConfiguration.cs new file mode 100644 index 0000000..6179dd1 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/BelgianSqlServerHealthcareConfiguration.cs @@ -0,0 +1,29 @@ +using NHibernate.Mapping.ByCode; + +namespace DDD.HealthcareDelivery.Infrastructure +{ + using Prescriptions; + + public class BelgianSqlServerHealthcareConfiguration : SqlServerHealthcareConfiguration + { + + #region Constructors + + public BelgianSqlServerHealthcareConfiguration(string connectionString) : base(connectionString) + { + } + + #endregion Constructors + + #region Methods + + protected override void InitializeModel(ModelMapper modelMapper) + { + base.InitializeModel(modelMapper); + modelMapper.AddMapping(); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/HealthcareConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/HealthcareConfiguration.cs new file mode 100644 index 0000000..499773d --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/HealthcareConfiguration.cs @@ -0,0 +1,42 @@ +using Conditions; +using NHibernate; +using NHibernate.Cfg; +using NHibernate.Mapping.ByCode; + +namespace DDD.HealthcareDelivery.Infrastructure +{ + using Prescriptions; + + public abstract class HealthcareConfiguration : Configuration + { + + #region Constructors + + protected HealthcareConfiguration(string connectionString) + { + Condition.Requires(connectionString, nameof(connectionString)).IsNotNullOrWhiteSpace(); + var modelMapper = new ModelMapper(); + this.InitializeModel(modelMapper); + this.DataBaseIntegration(db => + { + db.ConnectionString = connectionString; + db.ConnectionReleaseMode = ConnectionReleaseMode.OnClose; + db.LogSqlInConsole = true; + db.LogFormattedSql = true; + }); + this.AddMapping(modelMapper.CompileMappingForAllExplicitlyAddedEntities()); + } + + #endregion Constructors + + #region Methods + + protected virtual void InitializeModel(ModelMapper modelMapper) + { + modelMapper.AddMapping(); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareConfiguration.cs new file mode 100644 index 0000000..5686780 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareConfiguration.cs @@ -0,0 +1,22 @@ +using NHibernate.Cfg; +using NHibernate.Dialect; +using NHibernate.Driver; + +namespace DDD.HealthcareDelivery.Infrastructure +{ + public abstract class OracleHealthcareConfiguration : HealthcareConfiguration + { + #region Constructors + + protected OracleHealthcareConfiguration(string connectionString) : base(connectionString) + { + this.DataBaseIntegration(db => + { + db.Dialect(); + db.Driver(); + }); + } + + #endregion Constructors + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/BelgianSqlServerPrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/BelgianSqlServerPrescriptionMapping.cs new file mode 100644 index 0000000..a33d353 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/BelgianSqlServerPrescriptionMapping.cs @@ -0,0 +1,9 @@ +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Common.Domain; + + internal class BelgianSqlServerPrescriptionMapping + : SqlServerPrescriptionMapping + { + } +} \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/ContactInformationType.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/ContactInformationType.cs new file mode 100644 index 0000000..539b7c0 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/ContactInformationType.cs @@ -0,0 +1,49 @@ +using NHibernate; + +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Common.Domain; + using Core.Infrastructure.Data; + + public class ContactInformationType : CompositeUserType + { + + public ContactInformationType() + { + this.Mutable(false); + this.Property(i => i.PrimaryTelephoneNumber, NHibernateUtil.AnsiString); + this.Property(i => i.SecondaryTelephoneNumber, NHibernateUtil.AnsiString); + //this.Property(i => i.PrimaryEmailAddress, NHibernateUtil.AnsiString); + //this.Property(i => i.SecondaryEmailAddress, NHibernateUtil.AnsiString); + //this.Property(i => i.WebSite, NHibernateUtil.AnsiString); + } + + + // 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); + + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/FullNameType.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/FullNameType.cs new file mode 100644 index 0000000..de61581 --- /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; + + public 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..69dc22d --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthcarePractitionerLicenseNumberType.cs @@ -0,0 +1,21 @@ +using NHibernate; + +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Core.Infrastructure.Data; + using Domain.Practitioners; + + public class HealthcarePractitionerLicenseNumberType : CompositeUserType + where T : HealthcarePractitionerLicenseNumber + { + + #region Constructors + + public HealthcarePractitionerLicenseNumberType() + { + 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..467a1a2 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthcarePractitionerType.cs @@ -0,0 +1,48 @@ +using NHibernate; + +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Core.Infrastructure.Data; + using Domain.Practitioners; + + public class HealthcarePractitionerType : CompositeUserType + { + 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.Subcomponent(); + } + + + // this.Property(p => p.Prescriber.PractitionerType) + // .HasColumnName(ToCasingConvention("PrescriberType")) + // .HasColumnOrder(8) + // .IsUnicode(false) + // .HasMaxLength(20) + // .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); + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionMapping.cs new file mode 100644 index 0000000..607ff46 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionMapping.cs @@ -0,0 +1,20 @@ +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Common.Domain; + + internal class OraclePrescriptionMapping + : PrescriptionMapping + where TSocialSecurityNumber : SocialSecurityNumber + where TSex : Sex + { + #region Constructors + + public OraclePrescriptionMapping(bool useUpperCase) : base(useUpperCase) + { + // Fields + this.Discriminator(m => m.Column(m1 => m1.SqlType("varchar2(5)"))); + } + + #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..ef48ea7 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionMapping.cs @@ -0,0 +1,16 @@ +using NHibernate.Mapping.ByCode.Conformist; + +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Domain.Prescriptions; + + internal class PharmaceuticalPrescriptionMapping : SubclassMapping + { + public PharmaceuticalPrescriptionMapping() + { + this.Lazy(false); + // Fields + this.DiscriminatorValue("PHARM"); + } + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PostalAddressType.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PostalAddressType.cs new file mode 100644 index 0000000..971cf56 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PostalAddressType.cs @@ -0,0 +1,57 @@ +using NHibernate; + +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Common.Domain; + using Core.Infrastructure.Data; + + public 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.AnsiString); + } + + #endregion Constructors + + // 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(); + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs new file mode 100644 index 0000000..23a024a --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs @@ -0,0 +1,200 @@ +using NHibernate; +using NHibernate.Mapping.ByCode.Conformist; + +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Core.Infrastructure.Data; + using Domain.Prescriptions; + using Domain.Practitioners; + using Common.Infrastructure.Data; + using Common.Domain; + + internal abstract class PrescriptionMapping : ClassMapping + where TSocialSecurityNumber : SocialSecurityNumber + where TSex : Sex + { + + #region Fields + + private readonly bool useUpperCase; + + #endregion Fields + + #region Constructors + + public PrescriptionMapping(bool useUpperCase) + { + this.useUpperCase = useUpperCase; + this.Lazy(false); + // Table + this.Table(ToCasingConvention("Prescription")); + // Keys + this.ComponentAsId(p => p.Identifier, m1 => + m1.Property(i => i.Value, m2 => + { + m2.Column(ToCasingConvention("PrescriptionId")); + })); + // Fields + this.Discriminator(m => + { + m.Column(ToCasingConvention("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(ToCasingConvention("Language")); + m3.SqlType("char(2)"); + }); + m2.Type(NHibernateUtil.AnsiString); + m2.Length(2); + m2.NotNullable(true); + })); + + this.Property(p => p.CreatedOn, m => m.Type(NHibernateUtil.DateTimeNoMs)); + this.Property(p => p.DelivrableAt, m => m.Type(NHibernateUtil.Date)); + // Prescriber + this.Property(p => p.Prescriber, m => + { + m.Type(); + m.Columns + (m1 => + { + m1.Name(ToCasingConvention("PrescriberId")); + m1.NotNullable(true); + }, + m1 => + { + m1.Name(ToCasingConvention("PrescriberType")); + m1.Length(20); + m1.NotNullable(true); + }, + m1 => + { + m1.Name(ToCasingConvention("PrescriberLastName")); + m1.Length(50); + m1.NotNullable(true); + }, + m1 => + { + m1.Name(ToCasingConvention("PrescriberFirstName")); + m1.Length(50); + m1.NotNullable(true); + }, + m1 => + { + m1.Name(ToCasingConvention("PrescriberDisplayName")); + m1.Length(100); + m1.NotNullable(true); + }, + m1 => + { + m1.Name(ToCasingConvention("PrescriberLicenseNum")); + m1.Length(25); + m1.NotNullable(true); + }); + //m1 => + //{ + // m1.Name(ToCasingConvention("PrescriberPhone1")); + // m1.Length(20); + // m1.NotNullable(true); + //}, + //m1 => + //{ + // m1.Name(ToCasingConvention("PrescriberPhone2")); + // m1.Length(20); + // m1.NotNullable(true); + //}); + }); + // Patient + this.Component(p => p.Patient, m1 => + { + m1.Property(p => p.Identifier, m2 => m2.Column(ToCasingConvention("PatientId"))); + m1.Component(p => p.FullName, m2 => + { + m2.Property(n => n.FirstName, m3 => + { + m3.Column(ToCasingConvention("PatientFirstName")); + m3.Type(NHibernateUtil.AnsiString); + m3.Length(50); + m3.NotNullable(true); + }); + m2.Property(n => n.LastName, m3 => + { + m3.Column(ToCasingConvention("PatientLastName")); + m3.Type(NHibernateUtil.AnsiString); + m3.Length(50); + m3.NotNullable(true); + }); + }); + m1.Property(p => p.Sex, m3 => + { + m3.Column(ToCasingConvention("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(ToCasingConvention("PatientSSN")); + m3.Type(NHibernateUtil.AnsiString); + m3.Length(25); + }); + }); + m1.Property(p => p.Birthdate, m2 => + { + m2.Column(ToCasingConvention("PatientBirthdate")); + m2.Type(NHibernateUtil.Date); + }); + }); + } + + + + + + + + + + // 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); + //} + + protected string ToCasingConvention(string name) => this.useUpperCase ? name.ToUpperInvariant() : name; + + #endregion Constructors + + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescriptionMapping.cs new file mode 100644 index 0000000..4537779 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescriptionMapping.cs @@ -0,0 +1,16 @@ +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Common.Domain; + + internal class SqlServerPrescriptionMapping + : PrescriptionMapping + where TSocialSecurityNumber : SocialSecurityNumber + where TSex : Sex + { + public SqlServerPrescriptionMapping() : base(false) + { + // Fields + this.Discriminator(m => m.Column(m1 => m1.SqlType("varchar(5)"))); + } + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareConfiguration.cs new file mode 100644 index 0000000..4606105 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareConfiguration.cs @@ -0,0 +1,22 @@ +using NHibernate.Cfg; +using NHibernate.Dialect; +using NHibernate.Driver; + +namespace DDD.HealthcareDelivery.Infrastructure +{ + public abstract class SqlServerHealthcareConfiguration : HealthcareConfiguration + { + #region Constructors + + protected SqlServerHealthcareConfiguration(string connectionString) : base(connectionString) + { + this.DataBaseIntegration(db => + { + db.Dialect(); + db.Driver(); + }); + } + + #endregion Constructors + } +} diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj b/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj index 9cd52e5..1a3f6a2 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj @@ -35,16 +35,34 @@ 4 + + L:\Packages\Antlr3.Runtime.3.5.1\lib\net40-client\Antlr3.Runtime.dll + L:\Packages\FluentAssertions.5.6.0\lib\net47\FluentAssertions.dll + + L:\Packages\Iesi.Collections.4.0.4\lib\net461\Iesi.Collections.dll + + + L:\Packages\NHibernate.5.2.5\lib\net461\NHibernate.dll + L:\packages\Oracle.ManagedDataAccess.18.3.0\lib\net40\Oracle.ManagedDataAccess.dll True + + L:\Packages\Remotion.Linq.2.2.0\lib\net45\Remotion.Linq.dll + + + L:\Packages\Remotion.Linq.EagerFetching.2.2.0\lib\net45\Remotion.Linq.EagerFetching.dll + + + + L:\packages\System.ValueTuple.4.5.0\lib\netstandard1.0\System.ValueTuple.dll True @@ -70,6 +88,8 @@ + + diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareConfigurationTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareConfigurationTests.cs new file mode 100644 index 0000000..a607a2e --- /dev/null +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareConfigurationTests.cs @@ -0,0 +1,10 @@ +namespace DDD.HealthcareDelivery.Infrastructure +{ + public class BelgianSqlServerHealthcareConfigurationTests : HealthcareConfigurationTests + { + protected override HealthcareConfiguration CreateConfiguration() + { + return new BelgianSqlServerHealthcareConfiguration(@"Data Source=(local)\SQLEXPRESS;Database=Test;Integrated Security=False;User ID=sa;Password=mathib;Pooling=false"); + } + } +} diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareConfigurationTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareConfigurationTests.cs new file mode 100644 index 0000000..7cbd5b4 --- /dev/null +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareConfigurationTests.cs @@ -0,0 +1,113 @@ +using NHibernate; +using NHibernate.Tool.hbm2ddl; +using System; +using Xunit; +using FluentAssertions; + +namespace DDD.HealthcareDelivery.Infrastructure +{ + using Common.Domain; + using Domain.Facilities; + using Domain.Patients; + using Domain.Practitioners; + using Domain.Prescriptions; + + public abstract class HealthcareConfigurationTests + { + + #region Fields + + private readonly HealthcareConfiguration configuration; + private readonly ISession session; + + #endregion Fields + + #region Constructors + + protected HealthcareConfigurationTests() + { + this.configuration = this.CreateConfiguration(); + var sessionfactory = this.configuration.BuildSessionFactory(); + this.session = sessionfactory.OpenSession(); + } + + #endregion Constructors + + #region Methods + + [Fact] + public void ExportSchema_WhenValidConfiguration_ThrowsNoException() + { + // Arrange + var schemaExport = new SchemaExport(this.configuration); + // Act + schemaExport.Execute(useStdOut: true, + execute: true, + justDrop: false, + connection: this.session.Connection, + exportOutput: Console.Out); + // Assert + } + + [Fact] + public void SavePrescription() + { + // Arrange + var prescription1 = CreatePrescription(); + // Act + using (var transaction = this.session.BeginTransaction()) + { + this.session.Save(prescription1); + transaction.Commit(); + } + this.session.Clear(); + using (var transaction = this.session.BeginTransaction()) + { + var prescription2 = this.session.Get(new PrescriptionIdentifier(1)); + transaction.Commit(); + } + // Assert + + } + + protected abstract HealthcareConfiguration CreateConfiguration(); + + private static PharmaceuticalPrescription CreatePrescription() + { + return PharmaceuticalPrescription.Create + ( + new PrescriptionIdentifier(1), + new Physician + ( + 1, + new FullName("Duck", "Donald"), + new BelgianHealthcarePractitionerLicenseNumber("19006951001") + ), + new Patient + ( + 1, + new FullName("Flintstone", "Fred"), + BelgianSex.Male, + new BelgianSocialSecurityNumber("60207273601") + ), + new MedicalOffice + ( + 1, + "Medical Office Donald Duck" + ), + 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/packages.config b/Test/DDD.HealthcareDelivery.IntegrationTests/packages.config index a70dcec..cd2c372 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/packages.config +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/packages.config @@ -1,7 +1,12 @@  + + + + + From 484abd5c4c9f74cf2325f15022faf1d0488b208d Mon Sep 17 00:00:00 2001 From: draphyz Date: Wed, 5 Jun 2019 09:49:59 +0200 Subject: [PATCH 008/111] Add PropertyName and PropertyValue to ValidationFailure --- .../Validation/ValidationFailure.cs | 14 +++++++++++++- .../Validation/ValidationResult.cs | 1 + .../ValidationResultTranslator.cs | 6 ++++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Src/DDD.Core.Abstractions/Validation/ValidationFailure.cs b/Src/DDD.Core.Abstractions/Validation/ValidationFailure.cs index 4687d46..8a2c6c1 100644 --- a/Src/DDD.Core.Abstractions/Validation/ValidationFailure.cs +++ b/Src/DDD.Core.Abstractions/Validation/ValidationFailure.cs @@ -10,15 +10,23 @@ public class ValidationFailure #region Constructors - public ValidationFailure(string message, string code, FailureLevel level = FailureLevel.Warning) + public ValidationFailure(string message, + string code, + FailureLevel level = FailureLevel.Warning, + string propertyName = null, + object propertyValue = null) { Condition.Requires(message, nameof(message)).IsNotNullOrWhiteSpace(); Condition.Requires(code, nameof(code)).IsNotNullOrWhiteSpace(); this.Message = message; this.Code = code; this.Level = level; + if (!string.IsNullOrWhiteSpace(propertyName)) + this.PropertyName = propertyName; + this.PropertyValue = propertyValue; } + /// For serialization private ValidationFailure() { } @@ -33,6 +41,10 @@ private ValidationFailure() public string Message { get; private set; } + public string PropertyName { get; private set; } + + public object PropertyValue { get; private set; } + #endregion Properties } diff --git a/Src/DDD.Core.Abstractions/Validation/ValidationResult.cs b/Src/DDD.Core.Abstractions/Validation/ValidationResult.cs index 426026a..de8dca4 100644 --- a/Src/DDD.Core.Abstractions/Validation/ValidationResult.cs +++ b/Src/DDD.Core.Abstractions/Validation/ValidationResult.cs @@ -18,6 +18,7 @@ public ValidationResult(bool isSuccessful, IEnumerable failur this.Failures = failures; } + /// For serialization private ValidationResult() { } diff --git a/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs b/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs index 0ede251..fb0760c 100644 --- a/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs +++ b/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs @@ -28,9 +28,11 @@ private static DDD.Validation.ValidationFailure ToFailure(ValidationFailure fail Condition.Requires(failure, nameof(failure)).IsNotNull(); return new DDD.Validation.ValidationFailure ( - $"{failure.ErrorMessage} (property: {failure.PropertyName}, value: {failure.AttemptedValue})", + failure.ErrorMessage, failure.ErrorCode, - failure.Severity.ToString().ToEnum() + failure.Severity.ToString().ToEnum(), + failure.PropertyName, + failure.AttemptedValue ); } From 230dce28d78d845e5c48d16ae27b423702d0e23b Mon Sep 17 00:00:00 2001 From: draphyz Date: Wed, 5 Jun 2019 09:54:13 +0200 Subject: [PATCH 009/111] Add private default constructor to events --- .../PharmaceuticalPrescriptionCreated.cs | 10 ++++++---- .../PharmaceuticalPrescriptionRevoked.cs | 11 +++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionCreated.cs b/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionCreated.cs index 3c05ab3..1850d1d 100644 --- a/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionCreated.cs +++ b/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionCreated.cs @@ -19,17 +19,19 @@ public PharmaceuticalPrescriptionCreated(int prescriptionIdentifier, DateTime oc this.OccurredOn = occuredOn; } + /// For serialization + private PharmaceuticalPrescriptionCreated() { } + #endregion Constructors #region Properties - [DataMember(Name = "PrescriptionId")] - public int PrescriptionIdentifier { get; private set; } - [DataMember(Name = "OccurredOn")] public DateTime OccurredOn { get; private set; } - #endregion Properties + [DataMember(Name = "PrescriptionId")] + public int PrescriptionIdentifier { get; private set; } + #endregion Properties } } diff --git a/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionRevoked.cs b/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionRevoked.cs index d0e37c6..b715b7a 100644 --- a/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionRevoked.cs +++ b/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionRevoked.cs @@ -21,21 +21,24 @@ public PharmaceuticalPrescriptionRevoked(int prescriptionIdentifier, DateTime oc this.OccurredOn = occuredOn; } - public PharmaceuticalPrescriptionRevoked(int prescriptionIdentifier, string reason = null) + public PharmaceuticalPrescriptionRevoked(int prescriptionIdentifier, string reason = null) : this(prescriptionIdentifier, DateTime.Now, reason) { } + /// For serialization + private PharmaceuticalPrescriptionRevoked() { } + #endregion Constructors #region Properties - [DataMember(Name = "PrescriptionId")] - public int PrescriptionIdentifier { get; private set; } - [DataMember(Name = "OccurredOn")] public DateTime OccurredOn { get; private set; } + [DataMember(Name = "PrescriptionId")] + public int PrescriptionIdentifier { get; private set; } + [DataMember(Name = "Reason")] public string Reason { get; private set; } From 31651b98a9d9fd9b9a73c478d97844ca0a3fbf0c Mon Sep 17 00:00:00 2001 From: draphyz Date: Sat, 8 Jun 2019 12:45:52 +0200 Subject: [PATCH 010/111] Map phone numbers --- .../HealthcarePractitionerType.cs | 7 +----- .../Prescriptions/PrescriptionMapping.cs | 24 +++++++++---------- ...anSqlServerHealthcareConfigurationTests.cs | 2 +- 3 files changed, 13 insertions(+), 20 deletions(-) diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthcarePractitionerType.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthcarePractitionerType.cs index 467a1a2..7675c21 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthcarePractitionerType.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthcarePractitionerType.cs @@ -15,6 +15,7 @@ public HealthcarePractitionerType() 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.ContactInformation, NHibernateUtil.Custom(typeof(ContactInformationType))); this.Subcomponent(); } @@ -28,12 +29,6 @@ public HealthcarePractitionerType() - // 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) diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs index 23a024a..1e2b847 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs @@ -102,20 +102,18 @@ public PrescriptionMapping(bool useUpperCase) m1.Name(ToCasingConvention("PrescriberLicenseNum")); m1.Length(25); m1.NotNullable(true); + }, + m1 => + { + m1.Name(ToCasingConvention("PrescriberPhone1")); + m1.Length(20); + }, + m1 => + { + m1.Name(ToCasingConvention("PrescriberPhone2")); + m1.Length(20); }); - //m1 => - //{ - // m1.Name(ToCasingConvention("PrescriberPhone1")); - // m1.Length(20); - // m1.NotNullable(true); - //}, - //m1 => - //{ - // m1.Name(ToCasingConvention("PrescriberPhone2")); - // m1.Length(20); - // m1.NotNullable(true); - //}); - }); + }); // Patient this.Component(p => p.Patient, m1 => { diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareConfigurationTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareConfigurationTests.cs index a607a2e..a8667ad 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareConfigurationTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareConfigurationTests.cs @@ -4,7 +4,7 @@ public class BelgianSqlServerHealthcareConfigurationTests : HealthcareConfigurat { protected override HealthcareConfiguration CreateConfiguration() { - return new BelgianSqlServerHealthcareConfiguration(@"Data Source=(local)\SQLEXPRESS;Database=Test;Integrated Security=False;User ID=sa;Password=mathib;Pooling=false"); + return new BelgianSqlServerHealthcareConfiguration(@"Data Source=(local)\SQLEXPRESS;Database=Test;Integrated Security=False;User ID=sa;Password=dev;Pooling=false"); } } } From a6cc29dd3908e674119c4b420266856ea11653f4 Mon Sep 17 00:00:00 2001 From: draphyz Date: Sat, 8 Jun 2019 21:12:09 +0200 Subject: [PATCH 011/111] Merge from master --- Src/DDD.Common/Domain/ContactInformation.cs | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Src/DDD.Common/Domain/ContactInformation.cs b/Src/DDD.Common/Domain/ContactInformation.cs index f883df8..e3b8a92 100644 --- a/Src/DDD.Common/Domain/ContactInformation.cs +++ b/Src/DDD.Common/Domain/ContactInformation.cs @@ -62,6 +62,31 @@ protected ContactInformation() { } #region Methods + public static ContactInformation CreateIfNotEmpty(PostalAddress postalAddress = null, + string primaryTelephoneNumber = null, + string secondaryTelephoneNumber = null, + string faxNumber = null, + EmailAddress primaryEmailAddress = null, + EmailAddress secondaryEmailAddress = null, + Uri webSite = null) + { + if (IsEmpty(postalAddress, + primaryTelephoneNumber, + secondaryTelephoneNumber, + faxNumber, + primaryEmailAddress, + secondaryEmailAddress, + webSite)) + return null; + return new ContactInformation(postalAddress, + primaryTelephoneNumber, + secondaryTelephoneNumber, + faxNumber, + primaryEmailAddress, + secondaryEmailAddress, + webSite); + } + public override IEnumerable EqualityComponents() { yield return this.PostalAddress; From 6418d90f863c5dc16ee3a915717178ecac8750ba Mon Sep 17 00:00:00 2001 From: draphyz Date: Thu, 13 Jun 2019 11:48:29 +0200 Subject: [PATCH 012/111] Map prescription --- Src/DDD.Core.NHibernate/CompositeUserType.cs | 4 +- .../DDD.HealthcareDelivery.csproj | 6 + .../Prescriptions/Alpha2CountryCodeType.cs | 21 +++ .../BelgianSqlServerPrescriptionMapping.cs | 8 +- .../Prescriptions/ContactInformationType.cs | 42 ++---- .../Prescriptions/EmailAddressType.cs | 21 +++ .../Prescriptions/FullNameType.cs | 2 +- .../HealthFacilityLicenseNumberType.cs | 22 +++ .../Prescriptions/HealthFacilityType.cs | 27 ++++ ...HealthcarePractitionerLicenseNumberType.cs | 3 +- .../HealthcarePractitionerType.cs | 33 ++--- .../OraclePrescriptionMapping.cs | 8 +- .../Prescriptions/PostalAddressType.cs | 35 +---- .../PrescribedMedicationMapping.cs | 71 +++++++++ .../Prescriptions/PrescriptionMapping.cs | 136 +++++++++++++----- .../Prescriptions/SocialSecurityNumberType.cs | 22 +++ .../SqlServerPrescriptionMapping.cs | 10 +- .../HealthcareConfigurationTests.cs | 16 ++- 18 files changed, 351 insertions(+), 136 deletions(-) create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/Alpha2CountryCodeType.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/EmailAddressType.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthFacilityLicenseNumberType.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthFacilityType.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationMapping.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SocialSecurityNumberType.cs diff --git a/Src/DDD.Core.NHibernate/CompositeUserType.cs b/Src/DDD.Core.NHibernate/CompositeUserType.cs index 4467002..dac131f 100644 --- a/Src/DDD.Core.NHibernate/CompositeUserType.cs +++ b/Src/DDD.Core.NHibernate/CompositeUserType.cs @@ -328,9 +328,9 @@ protected void Property(Expression> prope /// /// Maps a subcomponent. /// - protected void Subcomponent(Action> mapping = null) where TSubcomponent : TComponent + protected void Subclass(Action> mapping = null) where TSubclass : TComponent { - var mapper = new SubcomponentMapper(this); + var mapper = new SubcomponentMapper(this); mapping?.Invoke(mapper); } diff --git a/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj b/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj index d08ee1e..5ce64d1 100644 --- a/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj +++ b/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj @@ -123,7 +123,13 @@ + + + + + + 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/BelgianSqlServerPrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/BelgianSqlServerPrescriptionMapping.cs index a33d353..8134f68 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/BelgianSqlServerPrescriptionMapping.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/BelgianSqlServerPrescriptionMapping.cs @@ -1,9 +1,13 @@ namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions { using Common.Domain; + using Domain.Practitioners; + using Domain.Facilities; - internal class BelgianSqlServerPrescriptionMapping - : SqlServerPrescriptionMapping + internal class BelgianSqlServerPrescriptionMapping: SqlServerPrescriptionMapping { } } \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/ContactInformationType.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/ContactInformationType.cs index 539b7c0..26f7175 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/ContactInformationType.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/ContactInformationType.cs @@ -5,45 +5,23 @@ namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions using Common.Domain; using Core.Infrastructure.Data; - public class ContactInformationType : CompositeUserType + internal class ContactInformationType : CompositeUserType { + #region Constructors + public ContactInformationType() { this.Mutable(false); - this.Property(i => i.PrimaryTelephoneNumber, NHibernateUtil.AnsiString); - this.Property(i => i.SecondaryTelephoneNumber, NHibernateUtil.AnsiString); - //this.Property(i => i.PrimaryEmailAddress, NHibernateUtil.AnsiString); - //this.Property(i => i.SecondaryEmailAddress, NHibernateUtil.AnsiString); - //this.Property(i => i.WebSite, NHibernateUtil.AnsiString); + 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, NHibernateUtil.Uri); + this.Property(c => c.PostalAddress, NHibernateUtil.Custom(typeof(PostalAddressType))); } - - // 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); + #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..d4d63b3 --- /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.Address, NHibernateUtil.AnsiString); + } + + #endregion Constructors + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/FullNameType.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/FullNameType.cs index de61581..13ca999 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/FullNameType.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/FullNameType.cs @@ -5,7 +5,7 @@ namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions using Common.Domain; using Core.Infrastructure.Data; - public class FullNameType : CompositeUserType + internal class FullNameType : CompositeUserType { #region Constructors diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthFacilityLicenseNumberType.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthFacilityLicenseNumberType.cs new file mode 100644 index 0000000..ed7dd2d --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthFacilityLicenseNumberType.cs @@ -0,0 +1,22 @@ +using NHibernate; + +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Core.Infrastructure.Data; + using Domain.Facilities; + + internal class HealthFacilityLicenseNumberType : CompositeUserType + where T : HealthFacilityLicenseNumber + { + + #region Constructors + + public HealthFacilityLicenseNumberType() + { + this.Mutable(false); + this.Property(p => p.Value, NHibernateUtil.AnsiString); + } + + #endregion Constructors + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthFacilityType.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthFacilityType.cs new file mode 100644 index 0000000..7e0ac0b --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthFacilityType.cs @@ -0,0 +1,27 @@ +using NHibernate; + +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Core.Infrastructure.Data; + using Domain.Facilities; + + internal class HealthFacilityType : CompositeUserType + where TFacilityLicenseNumber : HealthFacilityLicenseNumber + { + #region Constructors + + public HealthFacilityType() + { + this.Mutable(false); + this.Property(p => p.Identifier); + this.Discriminator("FacilityType"); + this.Property(p => p.Name, NHibernateUtil.AnsiString); + this.Property(p => p.LicenseNumber, NHibernateUtil.Custom(typeof(HealthFacilityLicenseNumberType))); + this.Subclass(); + this.Subclass(); + } + + #endregion Constructors + + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthcarePractitionerLicenseNumberType.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthcarePractitionerLicenseNumberType.cs index 69dc22d..7547cc4 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthcarePractitionerLicenseNumberType.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthcarePractitionerLicenseNumberType.cs @@ -5,7 +5,7 @@ namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions using Core.Infrastructure.Data; using Domain.Practitioners; - public class HealthcarePractitionerLicenseNumberType : CompositeUserType + internal class HealthcarePractitionerLicenseNumberType : CompositeUserType where T : HealthcarePractitionerLicenseNumber { @@ -13,6 +13,7 @@ public class HealthcarePractitionerLicenseNumberType : CompositeUserType public HealthcarePractitionerLicenseNumberType() { + this.Mutable(false); this.Property(p => p.Value, NHibernateUtil.AnsiString); } diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthcarePractitionerType.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthcarePractitionerType.cs index 7675c21..9dace6a 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthcarePractitionerType.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthcarePractitionerType.cs @@ -2,11 +2,16 @@ namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions { + using Common.Domain; using Core.Infrastructure.Data; using Domain.Practitioners; - public class HealthcarePractitionerType : CompositeUserType + internal class HealthcarePractitionerType : CompositeUserType + where TPractitionerLicenseNumber : HealthcarePractitionerLicenseNumber + where TSocialSecurityNumber : SocialSecurityNumber { + #region Constructors + public HealthcarePractitionerType() { this.Mutable(false); @@ -14,30 +19,14 @@ public HealthcarePractitionerType() 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.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.Subcomponent(); + this.Subclass(); } + #endregion Constructors - // this.Property(p => p.Prescriber.PractitionerType) - // .HasColumnName(ToCasingConvention("PrescriberType")) - // .HasColumnOrder(8) - // .IsUnicode(false) - // .HasMaxLength(20) - // .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); } } diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionMapping.cs index 607ff46..d46c89d 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionMapping.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionMapping.cs @@ -1,9 +1,13 @@ namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions { using Common.Domain; + using Domain.Practitioners; + using Domain.Facilities; - internal class OraclePrescriptionMapping - : PrescriptionMapping + internal class OraclePrescriptionMapping + : PrescriptionMapping + where TPractitionerLicenseNumber : HealthcarePractitionerLicenseNumber + where TFacilityLicenseNumber : HealthFacilityLicenseNumber where TSocialSecurityNumber : SocialSecurityNumber where TSex : Sex { diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PostalAddressType.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PostalAddressType.cs index 971cf56..93e3adb 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PostalAddressType.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PostalAddressType.cs @@ -5,7 +5,7 @@ namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions using Common.Domain; using Core.Infrastructure.Data; - public class PostalAddressType : CompositeUserType + internal class PostalAddressType : CompositeUserType { #region Constructors @@ -17,41 +17,10 @@ public PostalAddressType() 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.AnsiString); + this.Property(a => a.CountryCode, NHibernateUtil.Custom(typeof(Alpha2CountryCodeType))); } #endregion Constructors - // 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(); } } diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationMapping.cs new file mode 100644 index 0000000..af25b04 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationMapping.cs @@ -0,0 +1,71 @@ +using NHibernate; +using NHibernate.Mapping.ByCode.Conformist; + +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + using Domain.Prescriptions; + + internal abstract class PrescribedMedicationMapping : ClassMapping + { + + #region Fields + + private readonly bool useUpperCase; + + #endregion Fields + + #region Constructors + + protected PrescribedMedicationMapping(bool useUpperCase) + { + this.useUpperCase = useUpperCase; + this.Lazy(false); + // Table + this.Table(ToCasingConvention("PrescMedication")); + // Keys + this.Id("identifier", m => m.Column(ToCasingConvention("PrescMedicationId"))); + // Fields + + } + + //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/PrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs index 1e2b847..0b5cccf 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs @@ -3,14 +3,17 @@ namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions { - using Core.Infrastructure.Data; + using Common.Domain; + using Common.Infrastructure.Data; using Domain.Prescriptions; using Domain.Practitioners; - using Common.Infrastructure.Data; - using Common.Domain; + using Domain.Facilities; - internal abstract class PrescriptionMapping : ClassMapping - where TSocialSecurityNumber : SocialSecurityNumber + internal abstract class PrescriptionMapping + : ClassMapping + where TPractitionerLicenseNumber : HealthcarePractitionerLicenseNumber + where TFacilityLicenseNumber : HealthFacilityLicenseNumber + where TSocialSecurityNumber : SocialSecurityNumber where TSex : Sex { @@ -60,13 +63,13 @@ public PrescriptionMapping(bool useUpperCase) m2.Length(2); m2.NotNullable(true); })); - + this.Property(p => p.CreatedOn, m => m.Type(NHibernateUtil.DateTimeNoMs)); this.Property(p => p.DelivrableAt, m => m.Type(NHibernateUtil.Date)); // Prescriber this.Property(p => p.Prescriber, m => { - m.Type(); + m.Type>(); m.Columns (m1 => { @@ -104,6 +107,16 @@ public PrescriptionMapping(bool useUpperCase) m1.NotNullable(true); }, m1 => + { + m1.Name(ToCasingConvention("PrescriberSSN")); + m1.Length(25); + }, + m1 => + { + m1.Name(ToCasingConvention("PrescriberSpeciality")); + m1.Length(50); + }, + m1 => { m1.Name(ToCasingConvention("PrescriberPhone1")); m1.Length(20); @@ -112,8 +125,55 @@ public PrescriptionMapping(bool useUpperCase) { m1.Name(ToCasingConvention("PrescriberPhone2")); m1.Length(20); + }, + m1 => + { + m1.Name(ToCasingConvention("PrescriberEmail1")); + m1.Length(50); + }, + m1 => + { + m1.Name(ToCasingConvention("PrescriberEmail2")); + m1.Length(50); + }, + m1 => + { + m1.Name(ToCasingConvention("PrescriberWebSite")); + m1.Length(255); + m1.SqlType("varchar(255)"); + }, + m1 => + { + m1.Name(ToCasingConvention("PrescriberStreet")); + m1.Length(50); + }, + m1 => + { + m1.Name(ToCasingConvention("PrescriberHouseNum")); + m1.Length(10); + }, + m1 => + { + m1.Name(ToCasingConvention("PrescriberBoxNum")); + m1.Length(10); + }, + m1 => + { + m1.Name(ToCasingConvention("PrescriberPostCode")); + m1.Length(10); + }, + m1 => + { + m1.Name(ToCasingConvention("PrescriberCity")); + m1.Length(50); + }, + m1 => + { + m1.Name(ToCasingConvention("PrescriberCountry")); + m1.Length(2); + m1.SqlType("char(2)"); }); - }); + }); // Patient this.Component(p => p.Patient, m1 => { @@ -158,41 +218,43 @@ public PrescriptionMapping(bool useUpperCase) m2.Type(NHibernateUtil.Date); }); }); + // Facility + this.Property(p => p.HealthFacility, m => + { + m.Type>(); + m.Columns + (m1 => + { + m1.Name(ToCasingConvention("FacilityId")); + m1.NotNullable(true); + }, + m1 => + { + m1.Name(ToCasingConvention("FacilityType")); + m1.Length(20); + m1.NotNullable(true); + }, + m1 => + { + m1.Name(ToCasingConvention("FacilityName")); + m1.Length(100); + m1.NotNullable(true); + }, + m1 => + { + m1.Name(ToCasingConvention("FacilityLicenseNum")); + m1.Length(25); + }); + }); } - - - - - - - + #endregion Constructors - // 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); - //} + #region Methods protected string ToCasingConvention(string name) => this.useUpperCase ? name.ToUpperInvariant() : name; - #endregion Constructors + #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/SqlServerPrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescriptionMapping.cs index 4537779..06e058d 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescriptionMapping.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescriptionMapping.cs @@ -1,9 +1,13 @@ namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions { using Common.Domain; + using Domain.Practitioners; + using Domain.Facilities; - internal class SqlServerPrescriptionMapping - : PrescriptionMapping + internal class SqlServerPrescriptionMapping + : PrescriptionMapping + where TPractitionerLicenseNumber : HealthcarePractitionerLicenseNumber + where TFacilityLicenseNumber : HealthFacilityLicenseNumber where TSocialSecurityNumber : SocialSecurityNumber where TSex : Sex { @@ -13,4 +17,4 @@ public SqlServerPrescriptionMapping() : base(false) this.Discriminator(m => m.Column(m1 => m1.SqlType("varchar(5)"))); } } -} +} \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareConfigurationTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareConfigurationTests.cs index 7cbd5b4..c9251f5 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareConfigurationTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareConfigurationTests.cs @@ -81,7 +81,21 @@ private static PharmaceuticalPrescription CreatePrescription() ( 1, new FullName("Duck", "Donald"), - new BelgianHealthcarePractitionerLicenseNumber("19006951001") + 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 ( From 0c9a0cf4a74445abef4acc9851777f61a23f32e9 Mon Sep 17 00:00:00 2001 From: draphyz Date: Thu, 13 Jun 2019 14:56:09 +0200 Subject: [PATCH 013/111] Merge --- .../Infrastructure/Prescriptions/EmailAddressType.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/EmailAddressType.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/EmailAddressType.cs index d4d63b3..1e40515 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/EmailAddressType.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/EmailAddressType.cs @@ -13,7 +13,7 @@ internal class EmailAddressType : CompositeUserType public EmailAddressType() { this.Mutable(false); - this.Property(a => a.Address, NHibernateUtil.AnsiString); + this.Property(a => a.Value, NHibernateUtil.AnsiString); } #endregion Constructors From 25f61fe5be87505424548147cc3535159e791100 Mon Sep 17 00:00:00 2001 From: draphyz Date: Tue, 20 Aug 2019 13:46:10 +0200 Subject: [PATCH 014/111] Map domain model to database using NHibernate --- Src/DDD.Common/Domain/EmailAddress.cs | 2 +- Src/DDD.Core.NHibernate/AinsiUriType.cs | 29 ++++++ Src/DDD.Core.NHibernate/CompositeUserType.cs | 3 + .../DDD.Core.NHibernate.csproj | 9 ++ .../NHibernateRepository.cs | 58 ++++++++++-- .../OracleStoredEventMapping.cs | 16 ++++ .../SqlServerStoredEventMapping.cs | 15 ++++ Src/DDD.Core.NHibernate/StoredEventMapping.cs | 59 ++++++++++++ .../UpperCaseNamingStrategy.cs | 60 +++++++++++++ Src/DDD.Core/Domain/IAsyncRepository.cs | 5 +- Src/DDD.Core/Domain/IRepository.cs | 5 +- .../PharmaceuticalPrescriptionCreated.cs | 4 +- .../PharmaceuticalPrescriptionRevoked.cs | 6 +- .../PharmaceuticalPrescriptionCreator.cs | 4 +- .../PharmaceuticalPrescriptionRevoker.cs | 4 +- .../DDD.HealthcareDelivery.csproj | 11 ++- .../PharmaceuticalPrescription.cs | 2 +- .../Prescriptions/PrescribedMedication.cs | 2 +- .../BelgianOracleHealthcareConfiguration.cs | 37 ++++++++ ...BelgianSqlServerHealthcareConfiguration.cs | 10 ++- .../Infrastructure/HealthcareConfiguration.cs | 2 +- .../OracleHealthcareConfiguration.cs | 18 ++++ .../OracleStoredEventMapping.cs | 15 ++++ .../BelgianSqlServerPrescriptionMapping.cs | 13 --- .../Prescriptions/ContactInformationType.cs | 2 +- ...OraclePharmaceuticalPrescriptionMapping.cs | 13 +++ .../OraclePrescribedMedicationMapping.cs | 18 ++++ .../OraclePrescriptionMapping.cs | 2 +- .../PharmaceuticalPrescriptionMapping.cs | 38 +++++++- .../PrescribedMedicationMapping.cs | 84 +++++++++-------- ...scribedPharmaceuticalCompoundingMapping.cs | 18 ++++ .../PrescribedPharmaceuticalProductMapping.cs | 18 ++++ ...rescribedPharmaceuticalSubstanceMapping.cs | 18 ++++ .../Prescriptions/PrescriptionMapping.cs | 18 ++-- ...ServerPharmaceuticalPrescriptionMapping.cs | 13 +++ .../SqlServerPrescribedMedicationMapping.cs | 18 ++++ .../SqlServerPrescriptionMapping.cs | 7 +- .../SqlServerHealthcareConfiguration.cs | 15 ++++ .../SqlServerStoredEventMapping.cs | 15 ++++ ...ePharmaceuticalPrescriptionCreatorTests.cs | 15 ++-- ...ePharmaceuticalPrescriptionRevokerTests.cs | 15 ++-- .../PharmaceuticalPrescriptionCreatorTests.cs | 6 +- .../PharmaceuticalPrescriptionRevokerTests.cs | 4 +- ...rPharmaceuticalPrescriptionCreatorTests.cs | 15 ++-- ...rPharmaceuticalPrescriptionRevokerTests.cs | 15 ++-- ...HealthcareDelivery.IntegrationTests.csproj | 19 ++-- ...lgianOracleHealthcareConfigurationTests.cs | 25 ++++++ ...anSqlServerHealthcareConfigurationTests.cs | 21 ++++- .../HealthcareConfigurationTests.cs | 89 +++++++++++++++---- .../OracleConnectionFactory.cs | 17 +++- .../OracleScripts.Designer.cs | 4 +- .../OracleScripts.resx | 4 +- ...icalPrescription.sql => ClearDatabase.sql} | 0 ...icalPrescription.sql => ClearDatabase.sql} | 0 .../SqlServerConnectionFactory.cs | 17 +++- .../SqlServerScripts.Designer.cs | 24 ++--- .../SqlServerScripts.resx | 6 +- 57 files changed, 810 insertions(+), 172 deletions(-) create mode 100644 Src/DDD.Core.NHibernate/AinsiUriType.cs create mode 100644 Src/DDD.Core.NHibernate/OracleStoredEventMapping.cs create mode 100644 Src/DDD.Core.NHibernate/SqlServerStoredEventMapping.cs create mode 100644 Src/DDD.Core.NHibernate/StoredEventMapping.cs create mode 100644 Src/DDD.Core.NHibernate/UpperCaseNamingStrategy.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/BelgianOracleHealthcareConfiguration.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/OracleStoredEventMapping.cs delete mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/BelgianSqlServerPrescriptionMapping.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePharmaceuticalPrescriptionMapping.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescribedMedicationMapping.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedPharmaceuticalCompoundingMapping.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedPharmaceuticalProductMapping.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedPharmaceuticalSubstanceMapping.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPharmaceuticalPrescriptionMapping.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescribedMedicationMapping.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/SqlServerStoredEventMapping.cs create mode 100644 Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianOracleHealthcareConfigurationTests.cs rename Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/{CreatePharmaceuticalPrescription.sql => ClearDatabase.sql} (100%) rename Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/{CreatePharmaceuticalPrescription.sql => ClearDatabase.sql} (100%) diff --git a/Src/DDD.Common/Domain/EmailAddress.cs b/Src/DDD.Common/Domain/EmailAddress.cs index 1d39a70..3f654f2 100644 --- a/Src/DDD.Common/Domain/EmailAddress.cs +++ b/Src/DDD.Common/Domain/EmailAddress.cs @@ -27,7 +27,7 @@ protected EmailAddress() { } #region Properties - public string Value { get; } + public string Value { get; private set; } #endregion Properties 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 index dac131f..616b462 100644 --- a/Src/DDD.Core.NHibernate/CompositeUserType.cs +++ b/Src/DDD.Core.NHibernate/CompositeUserType.cs @@ -14,6 +14,9 @@ namespace DDD.Core.Infrastructure.Data { using NHibernate.Mapping.ByCode; + /// + /// This class must be unit tested. + /// public abstract class CompositeUserType : ICompositeUserType { diff --git a/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj b/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj index ec9db87..0b8ccdb 100644 --- a/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj +++ b/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj @@ -63,11 +63,16 @@ Properties\CommonAssemblyInfo.cs + + + + + @@ -77,6 +82,10 @@ {596a8700-3d18-4a62-b200-1f78a9ea4617} DDD.Core.Abstractions + + {2438B31A-3A39-4878-81FA-BE5AE715EAE5} + DDD.Core.Messages + {c6c3e419-b9aa-44ad-9dbf-789294687ae6} DDD.Core diff --git a/Src/DDD.Core.NHibernate/NHibernateRepository.cs b/Src/DDD.Core.NHibernate/NHibernateRepository.cs index b680268..b3dae41 100644 --- a/Src/DDD.Core.NHibernate/NHibernateRepository.cs +++ b/Src/DDD.Core.NHibernate/NHibernateRepository.cs @@ -1,43 +1,85 @@ -using NHibernate; +using Conditions; +using NHibernate; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; using System.Threading.Tasks; -using Conditions; namespace DDD.Core.Infrastructure.Data { using Domain; + using Mapping; using Threading; - public class NHibernateRepository : IAsyncRepository + public class NHibernateRepository : IAsyncRepository where TDomainEntity : DomainEntity + where TIdentity : ComparableValueObject { + #region Fields + private readonly IObjectTranslator eventTranslator; private readonly ISession session; #endregion Fields #region Constructors - public NHibernateRepository(ISession session) + public NHibernateRepository(ISession session, IObjectTranslator eventTranslator) { Condition.Requires(session, nameof(session)).IsNotNull(); + Condition.Requires(eventTranslator, nameof(eventTranslator)).IsNotNull(); this.session = session; + this.eventTranslator = eventTranslator; } #endregion Constructors #region Methods - public async Task FindAsync(ComparableValueObject identity) + public async Task FindAsync(TIdentity identity) { Condition.Requires(identity, nameof(identity)).IsNotNull(); await new SynchronizationContextRemover(); - return await this.session.GetAsync(identity); + try + { + return await this.session.GetAsync(identity); + } + catch (HibernateException ex) + { + throw new RepositoryException(ex, typeof(TDomainEntity)); + } + } + + public async Task SaveAsync(TDomainEntity aggregate) + { + var events = ToStoredEvents(aggregate); + await new SynchronizationContextRemover(); + try + { + await this.session.SaveOrUpdateAsync(aggregate); + foreach(var @event in events) + await this.session.SaveAsync(@event); + } + catch (HibernateException ex) + { + throw new RepositoryException(ex, typeof(TDomainEntity)); + } } - public Task SaveAsync(TDomainEntity aggregate) + private IEnumerable ToStoredEvents(TDomainEntity aggregate) { - throw new System.NotImplementedException(); + 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 diff --git a/Src/DDD.Core.NHibernate/OracleStoredEventMapping.cs b/Src/DDD.Core.NHibernate/OracleStoredEventMapping.cs new file mode 100644 index 0000000..388ece9 --- /dev/null +++ b/Src/DDD.Core.NHibernate/OracleStoredEventMapping.cs @@ -0,0 +1,16 @@ +namespace DDD.Core.Infrastructure.Data +{ + public class OracleStoredEventMapping : StoredEventMapping + { + #region Constructors + + public OracleStoredEventMapping(bool useUpperCase) : base(useUpperCase) + { + // Fields + this.Id(e => e.Id, m => m.Column(m1 => m1.SqlType("number(19,0)"))); + this.Property(e => e.Body, m => m.Column(m1 => m1.SqlType("xmltype"))); + } + + #endregion Constructors + } +} diff --git a/Src/DDD.Core.NHibernate/SqlServerStoredEventMapping.cs b/Src/DDD.Core.NHibernate/SqlServerStoredEventMapping.cs new file mode 100644 index 0000000..96a8fc0 --- /dev/null +++ b/Src/DDD.Core.NHibernate/SqlServerStoredEventMapping.cs @@ -0,0 +1,15 @@ +namespace DDD.Core.Infrastructure.Data +{ + public class SqlServerStoredEventMapping : StoredEventMapping + { + #region Constructors + + public SqlServerStoredEventMapping(bool useUpperCase) : base(useUpperCase) + { + // Fields + this.Property(e => e.Body, m => m.Column(m1 => m1.SqlType("xml"))); + } + + #endregion Constructors + } +} diff --git a/Src/DDD.Core.NHibernate/StoredEventMapping.cs b/Src/DDD.Core.NHibernate/StoredEventMapping.cs new file mode 100644 index 0000000..99a2cc1 --- /dev/null +++ b/Src/DDD.Core.NHibernate/StoredEventMapping.cs @@ -0,0 +1,59 @@ +using NHibernate; +using NHibernate.Mapping.ByCode.Conformist; + +namespace DDD.Core.Infrastructure.Data +{ + using Core.Domain; + + public abstract class StoredEventMapping : ClassMapping + { + #region Fields + + private readonly bool useUpperCase; + + #endregion Fields + + #region Constructors + + protected StoredEventMapping(bool useUpperCase) + { + this.useUpperCase = useUpperCase; + this.Lazy(false); + // Table + this.Table(ToCasingConvention("Event")); + // Keys + this.Id(e => e.Id, m1 => m1.Column(ToCasingConvention("EventId"))); + // Fields + this.Property(e => e.EventType, m => + { + m.Type(NHibernateUtil.AnsiString); + m.Length(50); + m.NotNullable(true); + }); + this.Property(e => e.StreamId, m => + { + m.Type(NHibernateUtil.AnsiString); + m.Length(50); + m.NotNullable(true); + }); + this.Property(e => e.CommitId, m => m.NotNullable(true)); + this.Property(e => e.OccurredOn, m => m.Precision(2)); + this.Property(e => e.Subject, m => + { + m.Type(NHibernateUtil.AnsiString); + m.Length(100); + }); + this.Property(e => e.Body, m => m.NotNullable(true)); + this.Property(e => e.Dispatched); + } + + #endregion Constructors + + #region Methods + + protected string ToCasingConvention(string name) => this.useUpperCase ? name.ToUpperInvariant() : name; + + #endregion Methods + + } +} 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/Domain/IAsyncRepository.cs b/Src/DDD.Core/Domain/IAsyncRepository.cs index a1d7d75..b8ba09a 100644 --- a/Src/DDD.Core/Domain/IAsyncRepository.cs +++ b/Src/DDD.Core/Domain/IAsyncRepository.cs @@ -2,12 +2,13 @@ namespace DDD.Core.Domain { - public interface IAsyncRepository + public interface IAsyncRepository where TDomainEntity : DomainEntity + where TIdentity : ComparableValueObject { #region Methods - Task FindAsync(ComparableValueObject identity); + Task FindAsync(TIdentity identity); Task SaveAsync(TDomainEntity aggregate); diff --git a/Src/DDD.Core/Domain/IRepository.cs b/Src/DDD.Core/Domain/IRepository.cs index 477656d..6d0bbe1 100644 --- a/Src/DDD.Core/Domain/IRepository.cs +++ b/Src/DDD.Core/Domain/IRepository.cs @@ -1,11 +1,12 @@ namespace DDD.Core.Domain { - public interface IRepository + public interface IRepository where TDomainEntity : DomainEntity + where TIdentity : ComparableValueObject { #region Methods - TDomainEntity Find(ComparableValueObject identity); + TDomainEntity Find(TIdentity identity); void Save(TDomainEntity aggregate); diff --git a/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionCreated.cs b/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionCreated.cs index 1850d1d..fda94cb 100644 --- a/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionCreated.cs +++ b/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionCreated.cs @@ -26,10 +26,10 @@ private PharmaceuticalPrescriptionCreated() { } #region Properties - [DataMember(Name = "OccurredOn")] + [DataMember(Name = "OccurredOn", Order = 2)] public DateTime OccurredOn { get; private set; } - [DataMember(Name = "PrescriptionId")] + [DataMember(Name = "PrescriptionId", Order = 1)] public int PrescriptionIdentifier { get; private set; } #endregion Properties diff --git a/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionRevoked.cs b/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionRevoked.cs index b715b7a..f677359 100644 --- a/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionRevoked.cs +++ b/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionRevoked.cs @@ -33,13 +33,13 @@ private PharmaceuticalPrescriptionRevoked() { } #region Properties - [DataMember(Name = "OccurredOn")] + [DataMember(Name = "OccurredOn", Order = 3)] public DateTime OccurredOn { get; private set; } - [DataMember(Name = "PrescriptionId")] + [DataMember(Name = "PrescriptionId", Order = 1)] public int PrescriptionIdentifier { get; private set; } - [DataMember(Name = "Reason")] + [DataMember(Name = "Reason", Order = 2)] public string Reason { get; private set; } #endregion Properties diff --git a/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionCreator.cs b/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionCreator.cs index c1aac94..34d8bc9 100644 --- a/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionCreator.cs +++ b/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionCreator.cs @@ -16,14 +16,14 @@ public class PharmaceuticalPrescriptionCreator #region Fields private readonly IEventPublisher publisher; - private readonly IAsyncRepository repository; + private readonly IAsyncRepository repository; private readonly IObjectTranslator translator; #endregion Fields #region Constructors - public PharmaceuticalPrescriptionCreator(IAsyncRepository repository, + public PharmaceuticalPrescriptionCreator(IAsyncRepository repository, IEventPublisher publisher, IObjectTranslator translator) { diff --git a/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionRevoker.cs b/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionRevoker.cs index f5d53f4..7327caa 100644 --- a/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionRevoker.cs +++ b/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionRevoker.cs @@ -15,13 +15,13 @@ public class PharmaceuticalPrescriptionRevoker #region Fields private readonly IEventPublisher publisher; - private readonly IAsyncRepository repository; + private readonly IAsyncRepository repository; #endregion Fields #region Constructors - public PharmaceuticalPrescriptionRevoker(IAsyncRepository repository, + public PharmaceuticalPrescriptionRevoker(IAsyncRepository repository, IEventPublisher publisher) { Condition.Requires(repository, nameof(repository)).IsNotNull(); diff --git a/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj b/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj index 5ce64d1..9457632 100644 --- a/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj +++ b/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj @@ -116,6 +116,7 @@ + @@ -129,6 +130,9 @@ + + + @@ -139,7 +143,10 @@ - + + + + SqlScripts.resx @@ -147,6 +154,8 @@ True + + diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs index 26d0bf7..2534a55 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs @@ -20,7 +20,7 @@ public class PharmaceuticalPrescription : Prescription #region Fields - private readonly ISet prescribedMedications = new HashSet(); + private ISet prescribedMedications = new HashSet(); #endregion Fields diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedication.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedication.cs index fc0b59e..b666513 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedication.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedication.cs @@ -10,7 +10,7 @@ public abstract class PrescribedMedication : ValueObject #region Fields - private readonly int identifier; + private int identifier; #endregion Fields diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/BelgianOracleHealthcareConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/BelgianOracleHealthcareConfiguration.cs new file mode 100644 index 0000000..6462d6c --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/BelgianOracleHealthcareConfiguration.cs @@ -0,0 +1,37 @@ +using NHibernate.Mapping.ByCode; + +namespace DDD.HealthcareDelivery.Infrastructure +{ + using Prescriptions; + using Domain.Prescriptions; + using Domain.Practitioners; + using Domain.Facilities; + using Common.Domain; + + public class BelgianOracleHealthcareConfiguration : OracleHealthcareConfiguration + { + + #region Constructors + + public BelgianOracleHealthcareConfiguration(string connectionString) : base(connectionString) + { + } + + #endregion Constructors + + #region Methods + + protected override void InitializeModel(ModelMapper modelMapper) + { + base.InitializeModel(modelMapper); + modelMapper.AddMapping>(); + modelMapper.AddMapping>(); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/BelgianSqlServerHealthcareConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/BelgianSqlServerHealthcareConfiguration.cs index 6179dd1..32cd767 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/BelgianSqlServerHealthcareConfiguration.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/BelgianSqlServerHealthcareConfiguration.cs @@ -3,6 +3,10 @@ namespace DDD.HealthcareDelivery.Infrastructure { using Prescriptions; + using Domain.Prescriptions; + using Domain.Practitioners; + using Domain.Facilities; + using Common.Domain; public class BelgianSqlServerHealthcareConfiguration : SqlServerHealthcareConfiguration { @@ -20,7 +24,11 @@ public BelgianSqlServerHealthcareConfiguration(string connectionString) : base(c protected override void InitializeModel(ModelMapper modelMapper) { base.InitializeModel(modelMapper); - modelMapper.AddMapping(); + modelMapper.AddMapping>(); + modelMapper.AddMapping>(); } #endregion Methods diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/HealthcareConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/HealthcareConfiguration.cs index 499773d..1deb607 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/HealthcareConfiguration.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/HealthcareConfiguration.cs @@ -33,7 +33,7 @@ protected HealthcareConfiguration(string connectionString) protected virtual void InitializeModel(ModelMapper modelMapper) { - modelMapper.AddMapping(); + modelMapper.AddMapping(); } #endregion Methods diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareConfiguration.cs index 5686780..dbad3f7 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareConfiguration.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareConfiguration.cs @@ -1,11 +1,16 @@ using NHibernate.Cfg; using NHibernate.Dialect; using NHibernate.Driver; +using NHibernate.Mapping.ByCode; namespace DDD.HealthcareDelivery.Infrastructure { + using Prescriptions; + using Core.Infrastructure.Data; + public abstract class OracleHealthcareConfiguration : HealthcareConfiguration { + #region Constructors protected OracleHealthcareConfiguration(string connectionString) : base(connectionString) @@ -15,8 +20,21 @@ protected OracleHealthcareConfiguration(string connectionString) : base(connecti db.Dialect(); db.Driver(); }); + this.SetNamingStrategy(UpperCaseNamingStrategy.Instance); } #endregion Constructors + + #region Methods + + protected override void InitializeModel(ModelMapper modelMapper) + { + base.InitializeModel(modelMapper); + modelMapper.AddMapping(); + modelMapper.AddMapping(); + } + + #endregion Methods + } } diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/OracleStoredEventMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/OracleStoredEventMapping.cs new file mode 100644 index 0000000..8d7c3b8 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/OracleStoredEventMapping.cs @@ -0,0 +1,15 @@ +namespace DDD.HealthcareDelivery.Infrastructure +{ + using Data = Core.Infrastructure.Data; + + public class OracleStoredEventMapping : Data.OracleStoredEventMapping + { + #region Constructors + + public OracleStoredEventMapping() : base(true) + { + } + + #endregion Constructors + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/BelgianSqlServerPrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/BelgianSqlServerPrescriptionMapping.cs deleted file mode 100644 index 8134f68..0000000 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/BelgianSqlServerPrescriptionMapping.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions -{ - using Common.Domain; - using Domain.Practitioners; - using Domain.Facilities; - - internal class BelgianSqlServerPrescriptionMapping: SqlServerPrescriptionMapping - { - } -} \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/ContactInformationType.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/ContactInformationType.cs index 26f7175..9e64f0c 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/ContactInformationType.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/ContactInformationType.cs @@ -17,7 +17,7 @@ public ContactInformationType() 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, NHibernateUtil.Uri); + this.Property(c => c.WebSite, new AnsiUriType()); this.Property(c => c.PostalAddress, NHibernateUtil.Custom(typeof(PostalAddressType))); } diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePharmaceuticalPrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePharmaceuticalPrescriptionMapping.cs new file mode 100644 index 0000000..c80e3a7 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePharmaceuticalPrescriptionMapping.cs @@ -0,0 +1,13 @@ +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + internal class OraclePharmaceuticalPrescriptionMapping : PharmaceuticalPrescriptionMapping + { + #region Constructors + + public OraclePharmaceuticalPrescriptionMapping() : base(true) + { + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescribedMedicationMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescribedMedicationMapping.cs new file mode 100644 index 0000000..5c64657 --- /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() : base(false) + { + // 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 index d46c89d..17af862 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionMapping.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionMapping.cs @@ -13,7 +13,7 @@ internal class OraclePrescriptionMapping m.Column(m1 => m1.SqlType("varchar2(5)"))); diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionMapping.cs index ef48ea7..5b8eddb 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionMapping.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionMapping.cs @@ -1,16 +1,48 @@ -using NHibernate.Mapping.ByCode.Conformist; +using NHibernate.Mapping.ByCode; +using NHibernate.Mapping.ByCode.Conformist; namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions { using Domain.Prescriptions; - internal class PharmaceuticalPrescriptionMapping : SubclassMapping + internal abstract class PharmaceuticalPrescriptionMapping : SubclassMapping { - public PharmaceuticalPrescriptionMapping() + + #region Fields + + private readonly bool useUpperCase; + + #endregion Fields + + #region Constructors + + protected PharmaceuticalPrescriptionMapping(bool useUpperCase) { + this.useUpperCase = useUpperCase; this.Lazy(false); // Fields this.DiscriminatorValue("PHARM"); + // Associations + this.Set("prescribedMedications", + m => + { + m.Key(m1 => + { + m1.Column(ToCasingConvention("PrescriptionId")); + m1.NotNullable(true); + }); + m.Cascade(Cascade.All); + }, + r => r.OneToMany(m => m.Class(typeof(PrescribedMedication)))); } + + #endregion Constructors + + #region Methods + + protected string ToCasingConvention(string name) => this.useUpperCase ? name.ToUpperInvariant() : name; + + #endregion Methods + } } diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationMapping.cs index af25b04..45ea0ad 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationMapping.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationMapping.cs @@ -1,11 +1,13 @@ -using NHibernate; -using NHibernate.Mapping.ByCode.Conformist; +using NHibernate.Mapping.ByCode.Conformist; namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions { using Domain.Prescriptions; + using NHibernate; + using NHibernate.Mapping.ByCode; - internal abstract class PrescribedMedicationMapping : ClassMapping + internal abstract class PrescribedMedicationMapping : ClassMapping + where TMedicationCode : MedicationCode { #region Fields @@ -23,42 +25,52 @@ protected PrescribedMedicationMapping(bool useUpperCase) // Table this.Table(ToCasingConvention("PrescMedication")); // Keys - this.Id("identifier", m => m.Column(ToCasingConvention("PrescMedicationId"))); + this.Id("identifier", m => + { + m.Column(ToCasingConvention("PrescMedicationId")); + m.Generator(Generators.Sequence, m1 => m1.Params(new { sequence = ToCasingConvention("PrescMedicationId") })); + }); // Fields - + this.Discriminator(m => + { + m.Column(ToCasingConvention("MedicationType")); + m.Length(20); + m.NotNullable(true); + }); + this.Property(med => med.NameOrDescription, m => + { + m.Column(ToCasingConvention("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, m => + { + m.Type(NHibernateUtil.AnsiString); + m.Length(100); + }); + this.Property(med => med.Duration, m => + { + m.Type(NHibernateUtil.AnsiString); + m.Length(100); + }); + this.Component(med => med.Code, m => + { + m.Class(); + m.Property(c => c.Value, m1 => + { + m1.Column(ToCasingConvention("Code")); + m1.Type(NHibernateUtil.AnsiString); + m1.Length(20); + }); + }); } - //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 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/PrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs index 0b5cccf..cfeb2b5 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs @@ -25,14 +25,14 @@ internal abstract class PrescriptionMapping p.Identifier, m1 => + this.ComponentAsId(p => p.Identifier, m1 => m1.Property(i => i.Value, m2 => { m2.Column(ToCasingConvention("PrescriptionId")); @@ -63,8 +63,11 @@ public PrescriptionMapping(bool useUpperCase) m2.Length(2); m2.NotNullable(true); })); - - this.Property(p => p.CreatedOn, m => m.Type(NHibernateUtil.DateTimeNoMs)); + this.Property(p => p.CreatedOn, m => + { + m.Type(NHibernateUtil.Date); + m.NotNullable(true); + }); this.Property(p => p.DelivrableAt, m => m.Type(NHibernateUtil.Date)); // Prescriber this.Property(p => p.Prescriber, m => @@ -140,7 +143,6 @@ public PrescriptionMapping(bool useUpperCase) { m1.Name(ToCasingConvention("PrescriberWebSite")); m1.Length(255); - m1.SqlType("varchar(255)"); }, m1 => { @@ -177,7 +179,11 @@ public PrescriptionMapping(bool useUpperCase) // Patient this.Component(p => p.Patient, m1 => { - m1.Property(p => p.Identifier, m2 => m2.Column(ToCasingConvention("PatientId"))); + m1.Property(p => p.Identifier, m2 => + { + m2.Column(ToCasingConvention("PatientId")); + m2.NotNullable(true); + }); m1.Component(p => p.FullName, m2 => { m2.Property(n => n.FirstName, m3 => diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPharmaceuticalPrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPharmaceuticalPrescriptionMapping.cs new file mode 100644 index 0000000..51948c2 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPharmaceuticalPrescriptionMapping.cs @@ -0,0 +1,13 @@ +namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions +{ + internal class SqlServerPharmaceuticalPrescriptionMapping : PharmaceuticalPrescriptionMapping + { + #region Constructors + + public SqlServerPharmaceuticalPrescriptionMapping() : base(false) + { + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescribedMedicationMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescribedMedicationMapping.cs new file mode 100644 index 0000000..c0440b4 --- /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() : base(false) + { + // 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 index 06e058d..6f4e3f7 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescriptionMapping.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescriptionMapping.cs @@ -1,8 +1,8 @@ namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions { using Common.Domain; - using Domain.Practitioners; using Domain.Facilities; + using Domain.Practitioners; internal class SqlServerPrescriptionMapping : PrescriptionMapping @@ -11,10 +11,15 @@ internal class SqlServerPrescriptionMapping 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/SqlServerHealthcareConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareConfiguration.cs index 4606105..012b14a 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareConfiguration.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareConfiguration.cs @@ -1,11 +1,15 @@ using NHibernate.Cfg; using NHibernate.Dialect; using NHibernate.Driver; +using NHibernate.Mapping.ByCode; namespace DDD.HealthcareDelivery.Infrastructure { + using Prescriptions; + public abstract class SqlServerHealthcareConfiguration : HealthcareConfiguration { + #region Constructors protected SqlServerHealthcareConfiguration(string connectionString) : base(connectionString) @@ -18,5 +22,16 @@ protected SqlServerHealthcareConfiguration(string connectionString) : base(conne } #endregion Constructors + + #region Methods + + protected override void InitializeModel(ModelMapper modelMapper) + { + base.InitializeModel(modelMapper); + modelMapper.AddMapping(); + modelMapper.AddMapping(); + } + + #endregion Methods } } diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerStoredEventMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerStoredEventMapping.cs new file mode 100644 index 0000000..a92c422 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerStoredEventMapping.cs @@ -0,0 +1,15 @@ +namespace DDD.HealthcareDelivery.Infrastructure +{ + using Data = Core.Infrastructure.Data; + + public class SqlServerStoredEventMapping : Data.SqlServerStoredEventMapping + { + #region Constructors + + public SqlServerStoredEventMapping() : base(false) + { + } + + #endregion Constructors + } +} diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionCreatorTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionCreatorTests.cs index a7627cf..f4cc0ac 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionCreatorTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionCreatorTests.cs @@ -5,8 +5,8 @@ namespace DDD.HealthcareDelivery.Application.Prescriptions { using Core.Domain; using Core.Infrastructure.Serialization; + using Core.Infrastructure.Data; using Domain.Prescriptions; - using Infrastructure.Prescriptions; using Infrastructure; [Collection("Oracle")] @@ -24,13 +24,14 @@ public OraclePharmaceuticalPrescriptionCreatorTests(OracleFixture fixture) : bas #region Methods - protected override IAsyncRepository CreateRepository() + protected override IAsyncRepository CreateRepository() { - return new PharmaceuticalPrescriptionRepository - ( - new Domain.Prescriptions.BelgianPharmaceuticalPrescriptionTranslator(), - new StoredEventTranslator(DataContractSerializerWrapper.Create(new UTF8Encoding(false))), - new OracleHealthcareContextFactory(this.Fixture.ConnectionFactory) + var configuration = new BelgianOracleHealthcareConfiguration(OracleConnectionFactory.ConnectionString); + var session = configuration.BuildSessionFactory().OpenSession(); + return new NHibernateRepository + ( + session, + new StoredEventTranslator(DataContractSerializerWrapper.Create(new UTF8Encoding(false))) ); } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionRevokerTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionRevokerTests.cs index cccde33..a90cad2 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionRevokerTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionRevokerTests.cs @@ -6,8 +6,8 @@ namespace DDD.HealthcareDelivery.Application.Prescriptions using Core.Domain; using Core.Infrastructure.Serialization; using Domain.Prescriptions; - using Infrastructure.Prescriptions; using Infrastructure; + using Core.Infrastructure.Data; [Collection("Oracle")] public class OraclePharmaceuticalPrescriptionRevokerTests @@ -24,13 +24,14 @@ public OraclePharmaceuticalPrescriptionRevokerTests(OracleFixture fixture) : bas #region Methods - protected override IAsyncRepository CreateRepository() + protected override IAsyncRepository CreateRepository() { - return new PharmaceuticalPrescriptionRepository - ( - new Domain.Prescriptions.BelgianPharmaceuticalPrescriptionTranslator(), - new StoredEventTranslator(DataContractSerializerWrapper.Create(new UTF8Encoding(false))), - new OracleHealthcareContextFactory(this.Fixture.ConnectionFactory) + var configuration = new BelgianOracleHealthcareConfiguration(OracleConnectionFactory.ConnectionString); + var session = configuration.BuildSessionFactory().OpenSession(); + return new NHibernateRepository + ( + session, + new StoredEventTranslator(DataContractSerializerWrapper.Create(new UTF8Encoding(false))) ); } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs index e6ba705..4624a04 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs @@ -39,7 +39,7 @@ protected PharmaceuticalPrescriptionCreatorTests(TFixture fixture) protected TFixture Fixture { get; } protected PharmaceuticalPrescriptionCreator Handler { get; } - protected IAsyncRepository Repository { get; } + protected IAsyncRepository Repository { get; } #endregion Properties @@ -49,7 +49,7 @@ protected PharmaceuticalPrescriptionCreatorTests(TFixture fixture) public async Task HandleAsync_WhenCalled_CreatePharmaceuticalPrescription() { // Arrange - this.Fixture.ExecuteScriptFromResources("CreatePharmaceuticalPrescription"); + this.Fixture.ExecuteScriptFromResources("ClearDatabase"); Thread.CurrentPrincipal = new GenericPrincipal(new GenericIdentity("d.duck"), new string[] { "User" }); var command = CreateCommand(); // Act @@ -61,7 +61,7 @@ public async Task HandleAsync_WhenCalled_CreatePharmaceuticalPrescription() prescription.PrescribedMedications().Should().NotBeNullOrEmpty(); } - protected abstract IAsyncRepository CreateRepository(); + protected abstract IAsyncRepository CreateRepository(); private static CreatePharmaceuticalPrescription CreateCommand() { diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs index 6716d71..7fce8ef 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs @@ -34,7 +34,7 @@ protected PharmaceuticalPrescriptionRevokerTests(TFixture fixture) protected TFixture Fixture { get; } protected PharmaceuticalPrescriptionRevoker Handler { get; } - protected IAsyncRepository Repository { get; } + protected IAsyncRepository Repository { get; } #endregion Properties @@ -54,7 +54,7 @@ public async Task HandleAsync_WhenCalled_RevokePharmaceuticalPrescription() prescription.Status.Should().Be(Domain.Prescriptions.PrescriptionStatus.Revoked); } - protected abstract IAsyncRepository CreateRepository(); + protected abstract IAsyncRepository CreateRepository(); private static RevokePharmaceuticalPrescription CreateCommand() { diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionCreatorTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionCreatorTests.cs index 0b01412..8db9dd3 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionCreatorTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionCreatorTests.cs @@ -5,8 +5,8 @@ namespace DDD.HealthcareDelivery.Application.Prescriptions { using Core.Domain; using Core.Infrastructure.Serialization; + using Core.Infrastructure.Data; using Domain.Prescriptions; - using Infrastructure.Prescriptions; using Infrastructure; [Collection("SqlServer")] @@ -24,13 +24,14 @@ public SqlServerPharmaceuticalPrescriptionCreatorTests(SqlServerFixture fixture) #region Methods - protected override IAsyncRepository CreateRepository() + protected override IAsyncRepository CreateRepository() { - return new PharmaceuticalPrescriptionRepository - ( - new Domain.Prescriptions.BelgianPharmaceuticalPrescriptionTranslator(), - new StoredEventTranslator(DataContractSerializerWrapper.Create(Encoding.Unicode)), - new SqlServerHealthcareContextFactory(this.Fixture.ConnectionFactory) + var configuration = new BelgianSqlServerHealthcareConfiguration(SqlServerConnectionFactory.ConnectionString); + var session = configuration.BuildSessionFactory().OpenSession(); + return new NHibernateRepository + ( + session, + new StoredEventTranslator(DataContractSerializerWrapper.Create(Encoding.Unicode)) ); } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionRevokerTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionRevokerTests.cs index 12ccf00..c4ef374 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionRevokerTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionRevokerTests.cs @@ -5,8 +5,8 @@ namespace DDD.HealthcareDelivery.Application.Prescriptions { using Core.Domain; using Core.Infrastructure.Serialization; + using Core.Infrastructure.Data; using Domain.Prescriptions; - using Infrastructure.Prescriptions; using Infrastructure; [Collection("SqlServer")] @@ -24,13 +24,14 @@ public SqlServerPharmaceuticalPrescriptionRevokerTests(SqlServerFixture fixture) #region Methods - protected override IAsyncRepository CreateRepository() + protected override IAsyncRepository CreateRepository() { - return new PharmaceuticalPrescriptionRepository - ( - new Domain.Prescriptions.BelgianPharmaceuticalPrescriptionTranslator(), - new StoredEventTranslator(DataContractSerializerWrapper.Create(Encoding.Unicode)), - new SqlServerHealthcareContextFactory(this.Fixture.ConnectionFactory) + var configuration = new BelgianSqlServerHealthcareConfiguration(SqlServerConnectionFactory.ConnectionString); + var session = configuration.BuildSessionFactory().OpenSession(); + return new NHibernateRepository + ( + session, + new StoredEventTranslator(DataContractSerializerWrapper.Create(Encoding.Unicode)) ); } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj b/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj index 1a3f6a2..9c553b5 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj @@ -88,6 +88,13 @@ + + + + + + + @@ -131,6 +138,10 @@ {2438B31A-3A39-4878-81FA-BE5AE715EAE5} DDD.Core.Messages + + {d4fc7e1b-cae8-40f9-9faf-0029d91cc6c4} + DDD.Core.NHibernate + {C6C3E419-B9AA-44AD-9DBF-789294687AE6} DDD.Core @@ -171,12 +182,12 @@ - + - + @@ -186,9 +197,7 @@ - - - + diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianOracleHealthcareConfigurationTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianOracleHealthcareConfigurationTests.cs new file mode 100644 index 0000000..d4906a1 --- /dev/null +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianOracleHealthcareConfigurationTests.cs @@ -0,0 +1,25 @@ +using Xunit; + +namespace DDD.HealthcareDelivery.Infrastructure +{ + [Collection("Oracle")] + public class BelgianOracleHealthcareConfigurationTests : HealthcareConfigurationTests + { + #region Constructors + + public BelgianOracleHealthcareConfigurationTests(OracleFixture fixture) : base(fixture) + { + } + + #endregion Constructors + + #region Methods + + protected override HealthcareConfiguration CreateConfiguration() + { + return new BelgianOracleHealthcareConfiguration(OracleConnectionFactory.ConnectionString); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareConfigurationTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareConfigurationTests.cs index a8667ad..220fd56 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareConfigurationTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareConfigurationTests.cs @@ -1,10 +1,25 @@ -namespace DDD.HealthcareDelivery.Infrastructure +using Xunit; + +namespace DDD.HealthcareDelivery.Infrastructure { - public class BelgianSqlServerHealthcareConfigurationTests : HealthcareConfigurationTests + [Collection("SqlServer")] + public class BelgianSqlServerHealthcareConfigurationTests : HealthcareConfigurationTests { + #region Constructors + + public BelgianSqlServerHealthcareConfigurationTests(SqlServerFixture fixture) : base(fixture) + { + } + + #endregion Constructors + + #region Methods + protected override HealthcareConfiguration CreateConfiguration() { - return new BelgianSqlServerHealthcareConfiguration(@"Data Source=(local)\SQLEXPRESS;Database=Test;Integrated Security=False;User ID=sa;Password=dev;Pooling=false"); + return new BelgianSqlServerHealthcareConfiguration(SqlServerConnectionFactory.ConnectionString); } + + #endregion Methods } } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareConfigurationTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareConfigurationTests.cs index c9251f5..e155fba 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareConfigurationTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareConfigurationTests.cs @@ -1,18 +1,20 @@ -using NHibernate; -using NHibernate.Tool.hbm2ddl; +using FluentAssertions; +using NHibernate; using System; using Xunit; -using FluentAssertions; namespace DDD.HealthcareDelivery.Infrastructure { using Common.Domain; + using Core.Domain; + using Core.Infrastructure.Testing; using Domain.Facilities; using Domain.Patients; using Domain.Practitioners; using Domain.Prescriptions; - public abstract class HealthcareConfigurationTests + public abstract class HealthcareConfigurationTests : IDisposable + where TFixture : IDbFixture { #region Fields @@ -24,8 +26,9 @@ public abstract class HealthcareConfigurationTests #region Constructors - protected HealthcareConfigurationTests() + protected HealthcareConfigurationTests(TFixture fixture) { + this.Fixture = fixture; this.configuration = this.CreateConfiguration(); var sessionfactory = this.configuration.BuildSessionFactory(); this.session = sessionfactory.OpenSession(); @@ -33,26 +36,62 @@ protected HealthcareConfigurationTests() #endregion Constructors + #region Properties + + protected TFixture Fixture { get; } + + #endregion Properties + + //[Fact] + //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(); + } + [Fact] - public void ExportSchema_WhenValidConfiguration_ThrowsNoException() + public void HealthcareConfiguration_WhenMappingValid_CanSaveAndRestoreEvents() { // Arrange - var schemaExport = new SchemaExport(this.configuration); + this.Fixture.ExecuteScriptFromResources("ClearDatabase"); + var event1 = CreateEvent(); // Act - schemaExport.Execute(useStdOut: true, - execute: true, - justDrop: false, - connection: this.session.Connection, - exportOutput: Console.Out); + using (var transaction = this.session.BeginTransaction()) + { + this.session.Save(event1); + transaction.Commit(); + } + this.session.Clear(); + StoredEvent event2; + using (var transaction = this.session.BeginTransaction()) + { + event2 = this.session.Get(event1.Id); + transaction.Commit(); + } // Assert + event2.Should().BeEquivalentTo(event1); } [Fact] - public void SavePrescription() + public void HealthcareConfiguration_WhenMappingValid_CanSaveAndRestorePrescriptions() { // Arrange + this.Fixture.ExecuteScriptFromResources("ClearDatabase"); var prescription1 = CreatePrescription(); // Act using (var transaction = this.session.BeginTransaction()) @@ -61,17 +100,31 @@ public void SavePrescription() transaction.Commit(); } this.session.Clear(); + PharmaceuticalPrescription prescription2; using (var transaction = this.session.BeginTransaction()) { - var prescription2 = this.session.Get(new PrescriptionIdentifier(1)); + prescription2 = this.session.Get(prescription1.Identifier); transaction.Commit(); } // Assert - + prescription2.Should().BeEquivalentTo(prescription1); } - protected abstract HealthcareConfiguration CreateConfiguration(); + private static StoredEvent CreateEvent() + { + return new StoredEvent + { + Id = 1, + Body = @"12018-01-01T10:06:00", + StreamId = "1", + CommitId = Guid.NewGuid(), + Subject = "draphyz", + EventType = "PharmaceuticalPrescriptionCreated", + OccurredOn = new DateTime(2018, 1, 1) + }; + } + private static PharmaceuticalPrescription CreatePrescription() { return PharmaceuticalPrescription.Create @@ -90,7 +143,7 @@ private static PharmaceuticalPrescription CreatePrescription() "Grote Markt", "Brussel", "1000", - new Alpha2CountryCode("BE"), + new Alpha2CountryCode("BE"), "7" ), primaryTelephoneNumber: "02/221.21.21" @@ -101,7 +154,7 @@ private static PharmaceuticalPrescription CreatePrescription() ( 1, new FullName("Flintstone", "Fred"), - BelgianSex.Male, + BelgianSex.Male, new BelgianSocialSecurityNumber("60207273601") ), new MedicalOffice diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleConnectionFactory.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/OracleConnectionFactory.cs index 2924055..c7bde6b 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleConnectionFactory.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/OracleConnectionFactory.cs @@ -1,14 +1,25 @@ namespace DDD.HealthcareDelivery { - using Infrastructure; using Core.Infrastructure.Data; + using Infrastructure; public class OracleConnectionFactory : DbConnectionFactory, IHealthcareConnectionFactory { + + #region Fields + + public const string ConnectionString = @"Data Source=Local;Persist Security Info=true;User Id=TEST;Password=dev;Pooling=false"; + + #endregion Fields + + #region Constructors + public OracleConnectionFactory() - : base("Oracle.ManagedDataAccess.Client", - @"Data Source=Local;Persist Security Info=true;User Id=TEST;Password=dev;Pooling=false") + : base("Oracle.ManagedDataAccess.Client", ConnectionString) { } + + #endregion Constructors + } } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.Designer.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.Designer.cs index 764b388..a235d22 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.Designer.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.Designer.cs @@ -66,9 +66,9 @@ internal OracleScripts() { ///END; ////. /// - internal static string CreatePharmaceuticalPrescription { + internal static string ClearDatabase { get { - return ResourceManager.GetString("CreatePharmaceuticalPrescription", resourceCulture); + return ResourceManager.GetString("ClearDatabase", resourceCulture); } } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.resx b/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.resx index fcef308..182091b 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.resx +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.resx @@ -118,8 +118,8 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - scripts\oracle\createpharmaceuticalprescription.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 + + scripts\oracle\cleardatabase.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 scripts\oracle\createschema.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/CreatePharmaceuticalPrescription.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/ClearDatabase.sql similarity index 100% rename from Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/CreatePharmaceuticalPrescription.sql rename to Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/ClearDatabase.sql diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/CreatePharmaceuticalPrescription.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/ClearDatabase.sql similarity index 100% rename from Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/CreatePharmaceuticalPrescription.sql rename to Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/ClearDatabase.sql diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerConnectionFactory.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerConnectionFactory.cs index ba0ac05..7647897 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerConnectionFactory.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerConnectionFactory.cs @@ -1,14 +1,25 @@ namespace DDD.HealthcareDelivery { - using Infrastructure; using Core.Infrastructure.Data; + using Infrastructure; public class SqlServerConnectionFactory : DbConnectionFactory, IHealthcareConnectionFactory { + + #region Fields + + public const string ConnectionString = @"Data Source=(local)\SQLEXPRESS;Database=Test;Integrated Security=False;User ID=sa;Password=dev;Pooling=false"; + + #endregion Fields + + #region Constructors + public SqlServerConnectionFactory() - : base("System.Data.SqlClient", - @"Data Source=(local)\SQLEXPRESS;Database=Test;Integrated Security=False;User ID=sa;Password=dev;Pooling=false") + : base("System.Data.SqlClient", ConnectionString) { } + + #endregion Constructors + } } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerScripts.Designer.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerScripts.Designer.cs index 3421b35..723edec 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,18 +95,6 @@ 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 diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerScripts.resx b/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerScripts.resx index b600bbc..98dc939 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerScripts.resx +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerScripts.resx @@ -118,12 +118,12 @@ 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 - - scripts\sqlserver\createpharmaceuticalprescription.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 - scripts\sqlserver\findpharmaceuticalprescriptionsbypatient.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 From d6240669b535bd3c27d15756cdd8ac0e532889cf Mon Sep 17 00:00:00 2001 From: draphyz Date: Tue, 20 Aug 2019 17:09:00 +0200 Subject: [PATCH 015/111] Set default mode (auto) for ConnectionReleaseMode --- .../Infrastructure/HealthcareConfiguration.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/HealthcareConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/HealthcareConfiguration.cs index 1deb607..e0b751d 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/HealthcareConfiguration.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/HealthcareConfiguration.cs @@ -20,9 +20,6 @@ protected HealthcareConfiguration(string connectionString) this.DataBaseIntegration(db => { db.ConnectionString = connectionString; - db.ConnectionReleaseMode = ConnectionReleaseMode.OnClose; - db.LogSqlInConsole = true; - db.LogFormattedSql = true; }); this.AddMapping(modelMapper.CompileMappingForAllExplicitlyAddedEntities()); } From d0627ff81a54fd351a7d5a082ae2ee88fd413081 Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 23 Dec 2019 13:05:17 +0100 Subject: [PATCH 016/111] Catch domain exceptions --- Src/DDD.Core/Application/AsyncDomainCommandHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/DDD.Core/Application/AsyncDomainCommandHandler.cs b/Src/DDD.Core/Application/AsyncDomainCommandHandler.cs index 825c219..91b6195 100644 --- a/Src/DDD.Core/Application/AsyncDomainCommandHandler.cs +++ b/Src/DDD.Core/Application/AsyncDomainCommandHandler.cs @@ -25,7 +25,7 @@ public async Task HandleAsync(TCommand command) { await this.ExecuteAsync(command); } - catch (Exception ex) when (ex is DomainServiceException || ex is RepositoryException) + catch (DomainException ex) { throw new CommandException(ex, command); } From 1b35a79fb8efa81e6922f3b157928efadcfc7f46 Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 23 Dec 2019 13:25:35 +0100 Subject: [PATCH 017/111] Dispose session in tests --- ...ePharmaceuticalPrescriptionCreatorTests.cs | 18 +++++------ ...ePharmaceuticalPrescriptionRevokerTests.cs | 18 +++++------ .../PharmaceuticalPrescriptionCreatorTests.cs | 31 +++++++++++++++++-- .../PharmaceuticalPrescriptionRevokerTests.cs | 31 +++++++++++++++++-- ...rPharmaceuticalPrescriptionCreatorTests.cs | 16 +++++----- ...rPharmaceuticalPrescriptionRevokerTests.cs | 18 +++++------ 6 files changed, 93 insertions(+), 39 deletions(-) diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionCreatorTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionCreatorTests.cs index f4cc0ac..45aec04 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionCreatorTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionCreatorTests.cs @@ -1,13 +1,13 @@ using Xunit; using System.Text; +using NHibernate; namespace DDD.HealthcareDelivery.Application.Prescriptions { using Core.Domain; using Core.Infrastructure.Serialization; - using Core.Infrastructure.Data; - using Domain.Prescriptions; using Infrastructure; + using Mapping; [Collection("Oracle")] public class OraclePharmaceuticalPrescriptionCreatorTests @@ -24,15 +24,15 @@ public OraclePharmaceuticalPrescriptionCreatorTests(OracleFixture fixture) : bas #region Methods - protected override IAsyncRepository CreateRepository() + protected override IObjectTranslator CreateEventTranslator() + { + return new StoredEventTranslator(DataContractSerializerWrapper.Create(new UTF8Encoding(false))); + } + + protected override ISession CreateSession() { var configuration = new BelgianOracleHealthcareConfiguration(OracleConnectionFactory.ConnectionString); - var session = configuration.BuildSessionFactory().OpenSession(); - return new NHibernateRepository - ( - session, - new StoredEventTranslator(DataContractSerializerWrapper.Create(new UTF8Encoding(false))) - ); + return configuration.BuildSessionFactory().OpenSession(); } #endregion Methods diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionRevokerTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionRevokerTests.cs index a90cad2..be4c92e 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionRevokerTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionRevokerTests.cs @@ -1,13 +1,13 @@ using Xunit; using System.Text; +using NHibernate; namespace DDD.HealthcareDelivery.Application.Prescriptions { using Core.Domain; using Core.Infrastructure.Serialization; - using Domain.Prescriptions; using Infrastructure; - using Core.Infrastructure.Data; + using Mapping; [Collection("Oracle")] public class OraclePharmaceuticalPrescriptionRevokerTests @@ -24,15 +24,15 @@ public OraclePharmaceuticalPrescriptionRevokerTests(OracleFixture fixture) : bas #region Methods - protected override IAsyncRepository CreateRepository() + protected override IObjectTranslator CreateEventTranslator() + { + return new StoredEventTranslator(DataContractSerializerWrapper.Create(new UTF8Encoding(false))); + } + + protected override ISession CreateSession() { var configuration = new BelgianOracleHealthcareConfiguration(OracleConnectionFactory.ConnectionString); - var session = configuration.BuildSessionFactory().OpenSession(); - return new NHibernateRepository - ( - session, - new StoredEventTranslator(DataContractSerializerWrapper.Create(new UTF8Encoding(false))) - ); + return configuration.BuildSessionFactory().OpenSession(); } #endregion Methods diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs index 4624a04..97e33d3 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs @@ -4,20 +4,28 @@ using System.Threading; using System.Threading.Tasks; using Xunit; +using NHibernate; namespace DDD.HealthcareDelivery.Application.Prescriptions { using Common.Application; using Core.Domain; using Core.Infrastructure.Testing; + using Core.Infrastructure.Data; using Domain.Facilities; using Domain.Practitioners; using Domain.Prescriptions; using Infrastructure; + using Mapping; - public abstract class PharmaceuticalPrescriptionCreatorTests + public abstract class PharmaceuticalPrescriptionCreatorTests : IDisposable where TFixture : IDbFixture { + #region Fields + + private ISession session; + + #endregion Fields #region Constructors @@ -45,6 +53,11 @@ protected PharmaceuticalPrescriptionCreatorTests(TFixture fixture) #region Methods + public void Dispose() + { + this.session.Dispose(); + } + [Fact] public async Task HandleAsync_WhenCalled_CreatePharmaceuticalPrescription() { @@ -61,7 +74,10 @@ public async Task HandleAsync_WhenCalled_CreatePharmaceuticalPrescription() prescription.PrescribedMedications().Should().NotBeNullOrEmpty(); } - protected abstract IAsyncRepository CreateRepository(); + + protected abstract IObjectTranslator CreateEventTranslator(); + + protected abstract ISession CreateSession(); private static CreatePharmaceuticalPrescription CreateCommand() { @@ -106,7 +122,16 @@ private static CreatePharmaceuticalPrescription CreateCommand() }; } - #endregion Methods + private IAsyncRepository CreateRepository() + { + this.session = this.CreateSession(); + return new NHibernateRepository + ( + this.session, + this.CreateEventTranslator() + ); + } + #endregion Methods } } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs index 7fce8ef..a57f3e0 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs @@ -3,18 +3,28 @@ using System.Threading.Tasks; using System.Security.Principal; using FluentAssertions; +using NHibernate; +using System; namespace DDD.HealthcareDelivery.Application.Prescriptions { using Core.Domain; + using Core.Infrastructure.Data; using Domain.Prescriptions; using Core.Infrastructure.Testing; using Infrastructure; + using Mapping; - public abstract class PharmaceuticalPrescriptionRevokerTests + public abstract class PharmaceuticalPrescriptionRevokerTests : IDisposable where TFixture : IDbFixture { + #region Fields + + private ISession session; + + #endregion Fields + #region Constructors protected PharmaceuticalPrescriptionRevokerTests(TFixture fixture) @@ -40,6 +50,11 @@ protected PharmaceuticalPrescriptionRevokerTests(TFixture fixture) #region Methods + public void Dispose() + { + this.session.Dispose(); + } + [Fact] public async Task HandleAsync_WhenCalled_RevokePharmaceuticalPrescription() { @@ -54,7 +69,9 @@ public async Task HandleAsync_WhenCalled_RevokePharmaceuticalPrescription() prescription.Status.Should().Be(Domain.Prescriptions.PrescriptionStatus.Revoked); } - protected abstract IAsyncRepository CreateRepository(); + protected abstract IObjectTranslator CreateEventTranslator(); + + protected abstract ISession CreateSession(); private static RevokePharmaceuticalPrescription CreateCommand() { @@ -65,6 +82,16 @@ private static RevokePharmaceuticalPrescription CreateCommand() }; } + private IAsyncRepository CreateRepository() + { + this.session = this.CreateSession(); + return new NHibernateRepository + ( + this.session, + this.CreateEventTranslator() + ); + } + #endregion Methods } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionCreatorTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionCreatorTests.cs index 8db9dd3..a3c5ed1 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionCreatorTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionCreatorTests.cs @@ -8,6 +8,8 @@ namespace DDD.HealthcareDelivery.Application.Prescriptions using Core.Infrastructure.Data; using Domain.Prescriptions; using Infrastructure; + using DDD.Mapping; + using NHibernate; [Collection("SqlServer")] public class SqlServerPharmaceuticalPrescriptionCreatorTests @@ -24,15 +26,15 @@ public SqlServerPharmaceuticalPrescriptionCreatorTests(SqlServerFixture fixture) #region Methods - protected override IAsyncRepository CreateRepository() + protected override IObjectTranslator CreateEventTranslator() + { + return new StoredEventTranslator(DataContractSerializerWrapper.Create(Encoding.Unicode)); + } + + protected override ISession CreateSession() { var configuration = new BelgianSqlServerHealthcareConfiguration(SqlServerConnectionFactory.ConnectionString); - var session = configuration.BuildSessionFactory().OpenSession(); - return new NHibernateRepository - ( - session, - new StoredEventTranslator(DataContractSerializerWrapper.Create(Encoding.Unicode)) - ); + return configuration.BuildSessionFactory().OpenSession(); } #endregion Methods diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionRevokerTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionRevokerTests.cs index c4ef374..2bb7e74 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionRevokerTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionRevokerTests.cs @@ -1,12 +1,12 @@ using Xunit; using System.Text; +using NHibernate; namespace DDD.HealthcareDelivery.Application.Prescriptions { using Core.Domain; using Core.Infrastructure.Serialization; - using Core.Infrastructure.Data; - using Domain.Prescriptions; + using Mapping; using Infrastructure; [Collection("SqlServer")] @@ -24,15 +24,15 @@ public SqlServerPharmaceuticalPrescriptionRevokerTests(SqlServerFixture fixture) #region Methods - protected override IAsyncRepository CreateRepository() + protected override IObjectTranslator CreateEventTranslator() + { + return new StoredEventTranslator(DataContractSerializerWrapper.Create(Encoding.Unicode)); + } + + protected override ISession CreateSession() { var configuration = new BelgianSqlServerHealthcareConfiguration(SqlServerConnectionFactory.ConnectionString); - var session = configuration.BuildSessionFactory().OpenSession(); - return new NHibernateRepository - ( - session, - new StoredEventTranslator(DataContractSerializerWrapper.Create(Encoding.Unicode)) - ); + return configuration.BuildSessionFactory().OpenSession(); } #endregion Methods From e482ea66cd58efa57e4fcbb0a109de480f8d92d0 Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 23 Dec 2019 19:35:37 +0100 Subject: [PATCH 018/111] Add decorators for dependency injection --- DDD.sln | 7 ++ .../AsyncScopedCommandHandler.cs | 49 +++++++++++++ .../AsyncScopedQueryHandler.cs | 49 +++++++++++++ .../DDD.Core.SimpleInjector.csproj | 72 +++++++++++++++++++ .../Properties/AssemblyInfo.cs | 7 ++ .../ThreadScopedCommandHandler.cs | 48 +++++++++++++ .../ThreadScopedQueryHandler.cs | 48 +++++++++++++ Src/DDD.Core.SimpleInjector/packages.config | 5 ++ 8 files changed, 285 insertions(+) create mode 100644 Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs create mode 100644 Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs create mode 100644 Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj create mode 100644 Src/DDD.Core.SimpleInjector/Properties/AssemblyInfo.cs create mode 100644 Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs create mode 100644 Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler.cs create mode 100644 Src/DDD.Core.SimpleInjector/packages.config diff --git a/DDD.sln b/DDD.sln index 9248cdc..f558f62 100644 --- a/DDD.sln +++ b/DDD.sln @@ -59,6 +59,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.NHibernate", "Src\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Common.NHibernate", "Src\DDD.Common.NHibernate\DDD.Common.NHibernate.csproj", "{7466B831-1642-4B2B-9EA3-1C9299596C2A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.SimpleInjector", "Src\DDD.Core.SimpleInjector\DDD.Core.SimpleInjector.csproj", "{3EFAACD8-CF5E-4E31-884B-6B9F87F1E495}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -141,6 +143,10 @@ Global {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -165,6 +171,7 @@ Global {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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {983C3AB4-301E-47A3-93FF-2E4BD8F2090F} diff --git a/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs new file mode 100644 index 0000000..97cedb7 --- /dev/null +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs @@ -0,0 +1,49 @@ +using SimpleInjector; +using SimpleInjector.Lifestyles; +using Conditions; +using System.Threading.Tasks; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Application; + + /// + /// A decorator that defines a scope around the asynchronous execution of a command. + /// + public class AsyncScopedCommandHandler : IAsyncCommandHandler + where TCommand : class, ICommand + { + + #region Fields + + private readonly Container container; + private readonly IAsyncCommandHandler handler; + + #endregion Fields + + #region Constructors + + public AsyncScopedCommandHandler(IAsyncCommandHandler handler, Container container) + { + Condition.Requires(handler, nameof(handler)).IsNotNull(); + Condition.Requires(container, nameof(container)).IsNotNull(); + this.handler = handler; + this.container = container; + } + + #endregion Constructors + + #region Methods + + public async Task HandleAsync(TCommand command) + { + using (AsyncScopedLifestyle.BeginScope(container)) + { + await this.handler.HandleAsync(command); + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs b/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs new file mode 100644 index 0000000..dbbf532 --- /dev/null +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs @@ -0,0 +1,49 @@ +using SimpleInjector; +using SimpleInjector.Lifestyles; +using Conditions; +using System.Threading.Tasks; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Application; + + /// + /// 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 IAsyncQueryHandler handler; + + #endregion Fields + + #region Constructors + + public AsyncScopedQueryHandler(IAsyncQueryHandler handler, Container container) + { + Condition.Requires(handler, nameof(handler)).IsNotNull(); + Condition.Requires(container, nameof(container)).IsNotNull(); + this.handler = handler; + this.container = container; + } + + #endregion Constructors + + #region Methods + + public async Task HandleAsync(TQuery query) + { + using (AsyncScopedLifestyle.BeginScope(container)) + { + return await this.handler.HandleAsync(query); + } + } + + #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..3cecd48 --- /dev/null +++ b/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj @@ -0,0 +1,72 @@ + + + + + Debug + AnyCPU + {3EFAACD8-CF5E-4E31-884B-6B9F87F1E495} + Library + Properties + DDD.Core.Infrastructure.DependencyInjection + DDD.Core.SimpleInjector + v4.7.2 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + bin\Debug\DDD.Core.SimpleInjector.xml + 1591 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + 1591 + bin\Release\DDD.Core.SimpleInjector.xml + + + + L:\Packages\Conditions.2.1.0\lib\portable-net45+win+wpa81+wp80+MonoAndroid10+MonoTouch10\Conditions.dll + + + L:\Packages\SimpleInjector.4.8.1\lib\net45\SimpleInjector.dll + + + + + + + + Properties\CommonAssemblyInfo.cs + + + + + + + + + + {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.SimpleInjector/Properties/AssemblyInfo.cs b/Src/DDD.Core.SimpleInjector/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..d89334f --- /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("Useful decorators for dependency injection.")] +[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/ThreadScopedCommandHandler.cs b/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs new file mode 100644 index 0000000..70ebe51 --- /dev/null +++ b/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs @@ -0,0 +1,48 @@ +using SimpleInjector; +using SimpleInjector.Lifestyles; +using Conditions; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Application; + + /// + /// A decorator that defines a scope around the synchronous execution of a command. + /// + public class ThreadScopedCommandHandler : ICommandHandler + where TCommand : class, ICommand + { + + #region Fields + + private readonly Container container; + private readonly ICommandHandler handler; + + #endregion Fields + + #region Constructors + + public ThreadScopedCommandHandler(ICommandHandler handler, Container container) + { + Condition.Requires(handler, nameof(handler)).IsNotNull(); + Condition.Requires(container, nameof(container)).IsNotNull(); + this.handler = handler; + this.container = container; + } + + #endregion Constructors + + #region Methods + + public void Handle(TCommand command) + { + using (ThreadScopedLifestyle.BeginScope(container)) + { + this.handler.Handle(command); + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler.cs b/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler.cs new file mode 100644 index 0000000..44306d6 --- /dev/null +++ b/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler.cs @@ -0,0 +1,48 @@ +using SimpleInjector; +using SimpleInjector.Lifestyles; +using Conditions; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Application; + + /// + /// A decorator that defines a scope around the synchronous execution of a query. + /// + public class ThreadScopedQueryHandler : IQueryHandler + where TQuery : class, IQuery + { + + #region Fields + + private readonly Container container; + private readonly IQueryHandler handler; + + #endregion Fields + + #region Constructors + + public ThreadScopedQueryHandler(IQueryHandler handler, Container container) + { + Condition.Requires(handler, nameof(handler)).IsNotNull(); + Condition.Requires(container, nameof(container)).IsNotNull(); + this.handler = handler; + this.container = container; + } + + #endregion Constructors + + #region Methods + + public TResult Handle(TQuery query) + { + using (ThreadScopedLifestyle.BeginScope(container)) + { + return this.handler.Handle(query); + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.SimpleInjector/packages.config b/Src/DDD.Core.SimpleInjector/packages.config new file mode 100644 index 0000000..1f6a2b1 --- /dev/null +++ b/Src/DDD.Core.SimpleInjector/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file From 1c26429798907c50917fc8db78187ea028415ae6 Mon Sep 17 00:00:00 2001 From: draphyz Date: Wed, 15 Jan 2020 12:40:21 +0100 Subject: [PATCH 019/111] Replace handler by handlerProvider --- .../AsyncScopedCommandHandler.cs | 14 ++++++++------ .../AsyncScopedQueryHandler.cs | 14 ++++++++------ .../ThreadScopedCommandHandler.cs | 14 ++++++++------ .../ThreadScopedQueryHandler.cs | 14 ++++++++------ 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs index 97cedb7..c6c135a 100644 --- a/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs @@ -2,6 +2,7 @@ using SimpleInjector.Lifestyles; using Conditions; using System.Threading.Tasks; +using System; namespace DDD.Core.Infrastructure.DependencyInjection { @@ -17,17 +18,17 @@ public class AsyncScopedCommandHandler : IAsyncCommandHandler handler; + private readonly Func> handlerProvider; #endregion Fields #region Constructors - public AsyncScopedCommandHandler(IAsyncCommandHandler handler, Container container) + public AsyncScopedCommandHandler(Func> handlerProvider, Container container) { - Condition.Requires(handler, nameof(handler)).IsNotNull(); + Condition.Requires(handlerProvider, nameof(handlerProvider)).IsNotNull(); Condition.Requires(container, nameof(container)).IsNotNull(); - this.handler = handler; + this.handlerProvider = handlerProvider; this.container = container; } @@ -39,11 +40,12 @@ public async Task HandleAsync(TCommand command) { using (AsyncScopedLifestyle.BeginScope(container)) { - await this.handler.HandleAsync(command); + var handler = this.handlerProvider(); + await handler.HandleAsync(command); } } #endregion Methods } -} +} \ No newline at end of file diff --git a/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs b/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs index dbbf532..3cfa7fc 100644 --- a/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs @@ -2,6 +2,7 @@ using SimpleInjector.Lifestyles; using Conditions; using System.Threading.Tasks; +using System; namespace DDD.Core.Infrastructure.DependencyInjection { @@ -17,17 +18,17 @@ public class AsyncScopedQueryHandler : IAsyncQueryHandler handler; + private readonly Func> handlerProvider; #endregion Fields #region Constructors - public AsyncScopedQueryHandler(IAsyncQueryHandler handler, Container container) + public AsyncScopedQueryHandler(Func> handlerProvider, Container container) { - Condition.Requires(handler, nameof(handler)).IsNotNull(); + Condition.Requires(handlerProvider, nameof(handlerProvider)).IsNotNull(); Condition.Requires(container, nameof(container)).IsNotNull(); - this.handler = handler; + this.handlerProvider = handlerProvider; this.container = container; } @@ -39,11 +40,12 @@ public async Task HandleAsync(TQuery query) { using (AsyncScopedLifestyle.BeginScope(container)) { - return await this.handler.HandleAsync(query); + var handler = this.handlerProvider(); + return await handler.HandleAsync(query); } } #endregion Methods } -} +} \ No newline at end of file diff --git a/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs b/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs index 70ebe51..ae58ccf 100644 --- a/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs +++ b/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs @@ -1,6 +1,7 @@ using SimpleInjector; using SimpleInjector.Lifestyles; using Conditions; +using System; namespace DDD.Core.Infrastructure.DependencyInjection { @@ -16,17 +17,17 @@ public class ThreadScopedCommandHandler : ICommandHandler #region Fields private readonly Container container; - private readonly ICommandHandler handler; + private readonly Func> handlerProvider; #endregion Fields #region Constructors - public ThreadScopedCommandHandler(ICommandHandler handler, Container container) + public ThreadScopedCommandHandler(Func> handlerProvider, Container container) { - Condition.Requires(handler, nameof(handler)).IsNotNull(); + Condition.Requires(handlerProvider, nameof(handlerProvider)).IsNotNull(); Condition.Requires(container, nameof(container)).IsNotNull(); - this.handler = handler; + this.handlerProvider = handlerProvider; this.container = container; } @@ -38,11 +39,12 @@ public void Handle(TCommand command) { using (ThreadScopedLifestyle.BeginScope(container)) { - this.handler.Handle(command); + var handler = this.handlerProvider(); + handler.Handle(command); } } #endregion Methods } -} +} \ No newline at end of file diff --git a/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler.cs b/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler.cs index 44306d6..9e4bf08 100644 --- a/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler.cs +++ b/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler.cs @@ -1,6 +1,7 @@ using SimpleInjector; using SimpleInjector.Lifestyles; using Conditions; +using System; namespace DDD.Core.Infrastructure.DependencyInjection { @@ -16,17 +17,17 @@ public class ThreadScopedQueryHandler : IQueryHandler handler; + private readonly Func> handlerProvider; #endregion Fields #region Constructors - public ThreadScopedQueryHandler(IQueryHandler handler, Container container) + public ThreadScopedQueryHandler(Func> handlerProvider, Container container) { - Condition.Requires(handler, nameof(handler)).IsNotNull(); + Condition.Requires(handlerProvider, nameof(handlerProvider)).IsNotNull(); Condition.Requires(container, nameof(container)).IsNotNull(); - this.handler = handler; + this.handlerProvider = handlerProvider; this.container = container; } @@ -38,11 +39,12 @@ public TResult Handle(TQuery query) { using (ThreadScopedLifestyle.BeginScope(container)) { - return this.handler.Handle(query); + var handler = this.handlerProvider(); + return handler.Handle(query); } } #endregion Methods } -} +} \ No newline at end of file From a6b0bde0be478d0aa731782b9fde3d5235deae2e Mon Sep 17 00:00:00 2001 From: draphyz Date: Wed, 26 Feb 2020 14:54:52 +0100 Subject: [PATCH 020/111] Add static instances for implementations of IDbStandardExpressions --- Src/DDD.Core/Infrastructure/Data/DbStandardExpressions.cs | 8 ++++++++ .../Infrastructure/Data/IDbConnectionExtensions.cs | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) 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/IDbConnectionExtensions.cs b/Src/DDD.Core/Infrastructure/Data/IDbConnectionExtensions.cs index 7e5f587..15ffd25 100644 --- a/Src/DDD.Core/Infrastructure/Data/IDbConnectionExtensions.cs +++ b/Src/DDD.Core/Infrastructure/Data/IDbConnectionExtensions.cs @@ -15,10 +15,10 @@ public static IDbStandardExpressions Expressions(this IDbConnection connection) switch (connection.GetType().ToString()) { case "System.Data.SqlClient.SqlConnection": - return new SqlServer2012Expressions(); + 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"); From a5bb20357670f70a1337bcb6d995a1907dea1016 Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 9 Mar 2020 09:34:05 +0100 Subject: [PATCH 021/111] Add decorators for logging --- .../AsyncCommandHandlerWithLogging.cs | 56 +++++++++++++++++++ .../AsyncQueryHandlerWithLogging.cs | 50 +++++++++++++++++ .../Application/CommandHandlerWithLogging.cs | 55 ++++++++++++++++++ .../Application/QueryHandlerWithLogging.cs | 49 ++++++++++++++++ Src/DDD.Core/DDD.Core.csproj | 7 +++ Src/DDD.Core/packages.config | 1 + 6 files changed, 218 insertions(+) create mode 100644 Src/DDD.Core/Application/AsyncCommandHandlerWithLogging.cs create mode 100644 Src/DDD.Core/Application/AsyncQueryHandlerWithLogging.cs create mode 100644 Src/DDD.Core/Application/CommandHandlerWithLogging.cs create mode 100644 Src/DDD.Core/Application/QueryHandlerWithLogging.cs diff --git a/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging.cs b/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging.cs new file mode 100644 index 0000000..43ec804 --- /dev/null +++ b/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging.cs @@ -0,0 +1,56 @@ +using Conditions; +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 + +#pragma warning disable CS3001 // Argument type is not CLS-compliant + public AsyncCommandHandlerWithLogging(IAsyncCommandHandler commandHandler, ILogger logger) +#pragma warning restore CS3001 // Argument type is not CLS-compliant + { + Condition.Requires(commandHandler, nameof(commandHandler)).IsNotNull(); + Condition.Requires(logger, nameof(logger)).IsNotNull(); + this.commandHandler = commandHandler; + this.logger = logger; + } + + #endregion Constructors + + #region Methods + + public async Task HandleAsync(TCommand command) + { + if (this.logger.IsEnabled(LogLevel.Information)) + { + this.logger.LogInformation("Executing command {Command}.", command); + var stopWatch = Stopwatch.StartNew(); + await this.commandHandler.HandleAsync(command); + stopWatch.Stop(); + this.logger.LogInformation("Command executed in {CommandExecutionTime} ms.", stopWatch.ElapsedMilliseconds); + } + else + await this.commandHandler.HandleAsync(command); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging.cs b/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging.cs new file mode 100644 index 0000000..a7f3d69 --- /dev/null +++ b/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging.cs @@ -0,0 +1,50 @@ +using Conditions; +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; + +#pragma warning disable CS3001 // Argument type is not CLS-compliant + public AsyncQueryHandlerWithLogging(IAsyncQueryHandler queryHandler, ILogger logger) +#pragma warning restore CS3001 // Argument type is not CLS-compliant + { + Condition.Requires(queryHandler, nameof(queryHandler)).IsNotNull(); + Condition.Requires(logger, nameof(logger)).IsNotNull(); + this.queryHandler = queryHandler; + this.logger = logger; + } + + #endregion Fields + + #region Methods + + public async Task HandleAsync(TQuery query) + { + if (this.logger.IsEnabled(LogLevel.Information)) + { + this.logger.LogInformation("Executing query {Query}.", query); + var stopWatch = Stopwatch.StartNew(); + var result = await this.queryHandler.HandleAsync(query); + stopWatch.Stop(); + this.logger.LogInformation("Query executed in {QueryExecutionTime} ms.", stopWatch.ElapsedMilliseconds); + return result; + } + else + return await this.queryHandler.HandleAsync(query); + } + + #endregion Methods + } +} diff --git a/Src/DDD.Core/Application/CommandHandlerWithLogging.cs b/Src/DDD.Core/Application/CommandHandlerWithLogging.cs new file mode 100644 index 0000000..ed608ab --- /dev/null +++ b/Src/DDD.Core/Application/CommandHandlerWithLogging.cs @@ -0,0 +1,55 @@ +using Conditions; +using Microsoft.Extensions.Logging; +using System.Diagnostics; + +namespace DDD.Core.Application +{ + /// + /// A decorator that logs information about commands. + /// + public class CommandHandlerWithLogging : ICommandHandler + where TCommand : class, ICommand + { + + #region Fields + + private readonly ICommandHandler commandHandler; + private readonly ILogger logger; + + #endregion Fields + + + #region Constructors + +#pragma warning disable CS3001 // Argument type is not CLS-compliant + public CommandHandlerWithLogging(ICommandHandler commandHandler, ILogger logger) +#pragma warning restore CS3001 // Argument type is not CLS-compliant + { + Condition.Requires(commandHandler, nameof(commandHandler)).IsNotNull(); + Condition.Requires(logger, nameof(logger)).IsNotNull(); + this.commandHandler = commandHandler; + this.logger = logger; + } + + #endregion Constructors + + #region Methods + + public void Handle(TCommand command) + { + if (this.logger.IsEnabled(LogLevel.Information)) + { + this.logger.LogInformation("Executing command {Command}.", command); + var stopWatch = Stopwatch.StartNew(); + this.commandHandler.Handle(command); + stopWatch.Stop(); + this.logger.LogInformation("Command executed in {CommandExecutionTime} ms.", stopWatch.ElapsedMilliseconds); + } + else + this.commandHandler.Handle(command); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/QueryHandlerWithLogging.cs b/Src/DDD.Core/Application/QueryHandlerWithLogging.cs new file mode 100644 index 0000000..1bce003 --- /dev/null +++ b/Src/DDD.Core/Application/QueryHandlerWithLogging.cs @@ -0,0 +1,49 @@ +using Conditions; +using Microsoft.Extensions.Logging; +using System.Diagnostics; + +namespace DDD.Core.Application +{ + /// + /// A decorator that logs information about queries. + /// + public class QueryHandlerWithLogging : IQueryHandler + where TQuery : class, IQuery + { + #region Fields + + private readonly IQueryHandler queryHandler; + private readonly ILogger logger; + +#pragma warning disable CS3001 // Argument type is not CLS-compliant + public QueryHandlerWithLogging(IQueryHandler queryHandler, ILogger logger) +#pragma warning restore CS3001 // Argument type is not CLS-compliant + { + Condition.Requires(queryHandler, nameof(queryHandler)).IsNotNull(); + Condition.Requires(logger, nameof(logger)).IsNotNull(); + this.queryHandler = queryHandler; + this.logger = logger; + } + + #endregion Fields + + #region Methods + + public TResult Handle(TQuery query) + { + if (this.logger.IsEnabled(LogLevel.Information)) + { + this.logger.LogInformation("Executing query {Query}.", query); + var stopWatch = Stopwatch.StartNew(); + var result = this.queryHandler.Handle(query); + stopWatch.Stop(); + this.logger.LogInformation("Query executed in {QueryExecutionTime} ms.", stopWatch.ElapsedMilliseconds); + return result; + } + else + return this.queryHandler.Handle(query); + } + + #endregion Methods + } +} diff --git a/Src/DDD.Core/DDD.Core.csproj b/Src/DDD.Core/DDD.Core.csproj index 8d6d646..9d89b66 100644 --- a/Src/DDD.Core/DDD.Core.csproj +++ b/Src/DDD.Core/DDD.Core.csproj @@ -39,6 +39,9 @@ L:\Packages\Conditions.2.1.0\lib\portable-net45+win+wpa81+wp80+MonoAndroid10+MonoTouch10\Conditions.dll True + + L:\Packages\Microsoft.Extensions.Logging.Abstractions.3.1.0\lib\netstandard2.0\Microsoft.Extensions.Logging.Abstractions.dll + @@ -51,8 +54,11 @@ Properties\CommonAssemblyInfo.cs + + + @@ -65,6 +71,7 @@ + diff --git a/Src/DDD.Core/packages.config b/Src/DDD.Core/packages.config index a105d3f..18db3fc 100644 --- a/Src/DDD.Core/packages.config +++ b/Src/DDD.Core/packages.config @@ -1,4 +1,5 @@  + \ No newline at end of file From edf7e8dfa842b80d7c173a4d11cab9c5545bc515 Mon Sep 17 00:00:00 2001 From: draphyz Date: Fri, 5 Jun 2020 22:04:51 +0200 Subject: [PATCH 022/111] Translate exceptions --- .../DDD.Core.NHibernate.csproj | 5 +- .../NHibernateRepository.cs | 5 +- ...NHibernateRepositoryExceptionTranslator.cs | 50 +++++++ .../Application/ApplicationException.cs | 36 ++++- .../Application/AsyncDomainCommandHandler.cs | 9 +- .../Application/CommandConflictException.cs | 37 ++++++ Src/DDD.Core/Application/CommandException.cs | 32 ++--- .../Application/CommandTimeoutException.cs | 37 ++++++ .../CommandUnauthorizedException.cs | 37 ++++++ .../CommandUnavailableException.cs | 37 ++++++ .../Application/DomainCommandHandler.cs | 12 +- .../DomainToCommandExceptionTranslator.cs | 49 +++++++ Src/DDD.Core/Application/QueryException.cs | 29 ++-- .../Application/QueryTimeoutException.cs | 37 ++++++ .../Application/QueryUnauthorizedException.cs | 37 ++++++ .../Application/QueryUnavailableException.cs | 37 ++++++ Src/DDD.Core/DDD.Core.csproj | 34 ++++- Src/DDD.Core/Domain/DomainException.cs | 36 ++++- .../Domain/DomainServiceConflictException.cs | 37 ++++++ Src/DDD.Core/Domain/DomainServiceException.cs | 31 ++--- .../Domain/DomainServiceTimeoutException.cs | 37 ++++++ .../DomainServiceUnauthorizedException.cs | 37 ++++++ .../DomainServiceUnavailableException.cs | 37 ++++++ .../Domain/RepositoryConcurrencyException.cs | 39 ------ .../Domain/RepositoryConflictException.cs | 37 ++++++ Src/DDD.Core/Domain/RepositoryException.cs | 33 ++--- .../Domain/RepositoryTimeoutException.cs | 37 ++++++ .../Domain/RepositoryUnauthorizedException.cs | 37 ++++++ .../Domain/RepositoryUnavailableException.cs | 37 ++++++ .../Infrastructure/Data/DbCommandHander.cs | 69 ++++++++++ .../Infrastructure/Data/DbQueryHandler.cs | 9 +- .../Data/DbToCommandExceptionTranslator.cs | 52 ++++++++ .../Data/DbToQueryExceptionTranslator.cs | 52 ++++++++ .../Data/DbToRepositoryExceptionTranslator.cs | 54 ++++++++ .../Infrastructure/Data/OracleErrorHelper.cs | 56 ++++++++ .../OracleToCommandExceptionTranslator.cs | 38 ++++++ .../Data/OracleToQueryExceptionTranslator.cs | 38 ++++++ .../OracleToRepositoryExceptionTranslator.cs | 40 ++++++ .../Infrastructure/Data/SqlErrorExtensions.cs | 125 ++++++++++++++++++ .../SqlServerToCommandExceptionTranslator.cs | 39 ++++++ .../SqlServerToQueryExceptionTranslator.cs | 39 ++++++ ...qlServerToRepositoryExceptionTranslator.cs | 41 ++++++ .../Infrastructure/InfrastructureException.cs | 29 ---- 43 files changed, 1470 insertions(+), 166 deletions(-) create mode 100644 Src/DDD.Core.NHibernate/NHibernateRepositoryExceptionTranslator.cs create mode 100644 Src/DDD.Core/Application/CommandConflictException.cs create mode 100644 Src/DDD.Core/Application/CommandTimeoutException.cs create mode 100644 Src/DDD.Core/Application/CommandUnauthorizedException.cs create mode 100644 Src/DDD.Core/Application/CommandUnavailableException.cs create mode 100644 Src/DDD.Core/Application/DomainToCommandExceptionTranslator.cs create mode 100644 Src/DDD.Core/Application/QueryTimeoutException.cs create mode 100644 Src/DDD.Core/Application/QueryUnauthorizedException.cs create mode 100644 Src/DDD.Core/Application/QueryUnavailableException.cs create mode 100644 Src/DDD.Core/Domain/DomainServiceConflictException.cs create mode 100644 Src/DDD.Core/Domain/DomainServiceTimeoutException.cs create mode 100644 Src/DDD.Core/Domain/DomainServiceUnauthorizedException.cs create mode 100644 Src/DDD.Core/Domain/DomainServiceUnavailableException.cs delete mode 100644 Src/DDD.Core/Domain/RepositoryConcurrencyException.cs create mode 100644 Src/DDD.Core/Domain/RepositoryConflictException.cs create mode 100644 Src/DDD.Core/Domain/RepositoryTimeoutException.cs create mode 100644 Src/DDD.Core/Domain/RepositoryUnauthorizedException.cs create mode 100644 Src/DDD.Core/Domain/RepositoryUnavailableException.cs create mode 100644 Src/DDD.Core/Infrastructure/Data/DbCommandHander.cs create mode 100644 Src/DDD.Core/Infrastructure/Data/DbToCommandExceptionTranslator.cs create mode 100644 Src/DDD.Core/Infrastructure/Data/DbToQueryExceptionTranslator.cs create mode 100644 Src/DDD.Core/Infrastructure/Data/DbToRepositoryExceptionTranslator.cs create mode 100644 Src/DDD.Core/Infrastructure/Data/OracleErrorHelper.cs create mode 100644 Src/DDD.Core/Infrastructure/Data/OracleToCommandExceptionTranslator.cs create mode 100644 Src/DDD.Core/Infrastructure/Data/OracleToQueryExceptionTranslator.cs create mode 100644 Src/DDD.Core/Infrastructure/Data/OracleToRepositoryExceptionTranslator.cs create mode 100644 Src/DDD.Core/Infrastructure/Data/SqlErrorExtensions.cs create mode 100644 Src/DDD.Core/Infrastructure/Data/SqlServerToCommandExceptionTranslator.cs create mode 100644 Src/DDD.Core/Infrastructure/Data/SqlServerToQueryExceptionTranslator.cs create mode 100644 Src/DDD.Core/Infrastructure/Data/SqlServerToRepositoryExceptionTranslator.cs delete mode 100644 Src/DDD.Core/Infrastructure/InfrastructureException.cs diff --git a/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj b/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj index 0b8ccdb..9876b03 100644 --- a/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj +++ b/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj @@ -21,7 +21,7 @@ DEBUG;TRACE prompt 4 - 7.1 + 7.2 pdbonly @@ -30,7 +30,7 @@ TRACE prompt 4 - 7.1 + 7.2 @@ -64,6 +64,7 @@ Properties\CommonAssemblyInfo.cs + diff --git a/Src/DDD.Core.NHibernate/NHibernateRepository.cs b/Src/DDD.Core.NHibernate/NHibernateRepository.cs index b3dae41..4da9444 100644 --- a/Src/DDD.Core.NHibernate/NHibernateRepository.cs +++ b/Src/DDD.Core.NHibernate/NHibernateRepository.cs @@ -21,6 +21,7 @@ public class NHibernateRepository : IAsyncRepository eventTranslator; private readonly ISession session; + private readonly IObjectTranslator exceptionTranslator = NHibernateRepositoryExceptionTranslator.Default; #endregion Fields @@ -48,7 +49,7 @@ public async Task FindAsync(TIdentity identity) } catch (HibernateException ex) { - throw new RepositoryException(ex, typeof(TDomainEntity)); + throw this.exceptionTranslator.Translate(ex, new { EntityType = typeof(TDomainEntity) }); } } @@ -64,7 +65,7 @@ public async Task SaveAsync(TDomainEntity aggregate) } catch (HibernateException ex) { - throw new RepositoryException(ex, typeof(TDomainEntity)); + throw this.exceptionTranslator.Translate(ex, new { EntityType = typeof(TDomainEntity) }); } } diff --git a/Src/DDD.Core.NHibernate/NHibernateRepositoryExceptionTranslator.cs b/Src/DDD.Core.NHibernate/NHibernateRepositoryExceptionTranslator.cs new file mode 100644 index 0000000..26c2fe6 --- /dev/null +++ b/Src/DDD.Core.NHibernate/NHibernateRepositoryExceptionTranslator.cs @@ -0,0 +1,50 @@ +using System; +using NHibernate; +using NHibernate.Exceptions; +using System.Collections.Generic; +using System.Data.Common; +using Conditions; + +namespace DDD.Core.Infrastructure.Data +{ + using Mapping; + using Core.Domain; + + internal class NHibernateRepositoryExceptionTranslator : IObjectTranslator + { + + #region Fields + + public static readonly IObjectTranslator Default = new NHibernateRepositoryExceptionTranslator(); + + private readonly IObjectTranslator dbExceptionTranslator = DbToRepositoryExceptionTranslator.Default; + + #endregion Fields + + #region Methods + + public RepositoryException Translate(HibernateException exception, IDictionary options = null) + { + Condition.Requires(exception, nameof(exception)).IsNotNull(); + Condition.Requires(options, nameof(options)) + .IsNotNull() + .Evaluate(options.ContainsKey("EntityType")); + var entityType = (Type)options["EntityType"]; + switch (exception) + { + case ADOException _: + var dbException = ADOExceptionHelper.ExtractDbException(exception); + if (dbException != null) + { + options.Add("OuterException", exception); + return dbExceptionTranslator.Translate(dbException, options); + } + break; + } + return new RepositoryException(isTransient: false, entityType, exception); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/ApplicationException.cs b/Src/DDD.Core/Application/ApplicationException.cs index 4ebda82..ad82799 100644 --- a/Src/DDD.Core/Application/ApplicationException.cs +++ b/Src/DDD.Core/Application/ApplicationException.cs @@ -10,20 +10,44 @@ public abstract class ApplicationException : Exception #region Constructors - protected ApplicationException() - : base("An error has occurred in the application layer.") + protected ApplicationException(bool isTransient, Exception innerException = null) + : base(DefaultMessage(), innerException) { + this.IsTransient = isTransient; } - protected ApplicationException(string message) : base(message) + protected ApplicationException(bool isTransient, string message, Exception innerException = null) + : base(message, innerException) { + this.IsTransient = isTransient; } - protected ApplicationException(string message, Exception innerException) : base(message, innerException) + #endregion Constructors + + #region Properties + + /// + /// Gets a value indicating whether the exception is transient. + /// + public bool IsTransient { get; } + + #endregion Properties + + #region Methods + + public static string DefaultMessage() => "An error occurred in the application layer."; + + public override string ToString() { + var s = $"{this.GetType()}: {this.Message} "; + s += $"{Environment.NewLine}IsTransient: {this.IsTransient}"; + if (this.InnerException != null) + s += $" ---> {this.InnerException}"; + if (this.StackTrace != null) + s += $"{Environment.NewLine}{this.StackTrace}"; + return s; } - #endregion Constructors - + #endregion Methods } } diff --git a/Src/DDD.Core/Application/AsyncDomainCommandHandler.cs b/Src/DDD.Core/Application/AsyncDomainCommandHandler.cs index 91b6195..8fa5946 100644 --- a/Src/DDD.Core/Application/AsyncDomainCommandHandler.cs +++ b/Src/DDD.Core/Application/AsyncDomainCommandHandler.cs @@ -1,10 +1,10 @@ using Conditions; -using System; using System.Threading.Tasks; namespace DDD.Core.Application { using Domain; + using Mapping; using Threading; /// @@ -14,6 +14,11 @@ namespace DDD.Core.Application public abstract class AsyncDomainCommandHandler : IAsyncCommandHandler where TCommand : class, ICommand { + #region Fields + + private readonly IObjectTranslator exceptionTranslator = DomainToCommandExceptionTranslator.Default; + + #endregion Fields #region Methods @@ -27,7 +32,7 @@ public async Task HandleAsync(TCommand command) } catch (DomainException ex) { - throw new CommandException(ex, command); + throw this.exceptionTranslator.Translate(ex, new { Command = command }); } } 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..d3acd29 100644 --- a/Src/DDD.Core/Application/CommandException.cs +++ b/Src/DDD.Core/Application/CommandException.cs @@ -10,43 +10,39 @@ 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 static string DefaultMessage(ICommand command = null) + { + if (command == null) + return "A command failed."; + return $"The command '{command.GetType().Name}' failed."; + } + public override string ToString() { var s = $"{this.GetType()}: {this.Message} "; + s += $"{Environment.NewLine}IsTransient: {this.IsTransient}"; if (this.Command != null) s += $"{Environment.NewLine}Command: {this.Command}"; if (this.InnerException != null) 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/DomainCommandHandler.cs b/Src/DDD.Core/Application/DomainCommandHandler.cs index 54c3df2..fc0958c 100644 --- a/Src/DDD.Core/Application/DomainCommandHandler.cs +++ b/Src/DDD.Core/Application/DomainCommandHandler.cs @@ -3,7 +3,7 @@ namespace DDD.Core.Application { using Domain; - using System; + using Mapping; /// /// Base class for handling synchronously commands using a domain model. @@ -13,6 +13,12 @@ public abstract class DomainCommandHandler : ICommandHandler where TCommand : class, ICommand { + #region Fields + + private readonly IObjectTranslator exceptionTranslator = DomainToCommandExceptionTranslator.Default; + + #endregion Fields + #region Methods public void Handle(TCommand command) @@ -24,7 +30,7 @@ public void Handle(TCommand command) } catch (DomainException ex) { - throw new CommandException(ex, command); + throw this.exceptionTranslator.Translate(ex, new { Command = command }); } } @@ -34,4 +40,4 @@ public void Handle(TCommand command) } -} \ 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..3bbfef6 --- /dev/null +++ b/Src/DDD.Core/Application/DomainToCommandExceptionTranslator.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using Conditions; + +namespace DDD.Core.Application +{ + using Mapping; + using Domain; + + internal class DomainToCommandExceptionTranslator : IObjectTranslator + { + + #region Fields + + public static readonly IObjectTranslator Default = new DomainToCommandExceptionTranslator(); + + #endregion Fields + + #region Methods + + public CommandException Translate(DomainException exception, IDictionary options = null) + { + Condition.Requires(exception, nameof(exception)).IsNotNull(); + Condition.Requires(options, nameof(options)) + .IsNotNull() + .Evaluate(options.ContainsKey("Command")); + var command = (ICommand)options["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); + default: + return new CommandException(exception.IsTransient, command, exception); + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/QueryException.cs b/Src/DDD.Core/Application/QueryException.cs index 0529629..ea98619 100644 --- a/Src/DDD.Core/Application/QueryException.cs +++ b/Src/DDD.Core/Application/QueryException.cs @@ -5,32 +5,19 @@ 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,9 +32,17 @@ 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 string ToString() { var s = $"{this.GetType()}: {this.Message} "; + s += $"{Environment.NewLine}IsTransient: {this.IsTransient}"; if (this.Query != null) s += $"{Environment.NewLine}Query: {this.Query}"; if (this.InnerException != null) 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/DDD.Core.csproj b/Src/DDD.Core/DDD.Core.csproj index 9d89b66..88f0ee1 100644 --- a/Src/DDD.Core/DDD.Core.csproj +++ b/Src/DDD.Core/DDD.Core.csproj @@ -57,9 +57,15 @@ + + + + + + @@ -73,13 +79,24 @@ - + + + + + + + + + + + + @@ -89,22 +106,31 @@ + + + + + + + + + - + + + - - diff --git a/Src/DDD.Core/Domain/DomainException.cs b/Src/DDD.Core/Domain/DomainException.cs index 5a6d9dd..f25dab2 100644 --- a/Src/DDD.Core/Domain/DomainException.cs +++ b/Src/DDD.Core/Domain/DomainException.cs @@ -10,20 +10,44 @@ public abstract class DomainException : Exception #region Constructors - protected DomainException() - : base("An error has occurred in the domain layer.") + protected DomainException(bool isTransient, Exception innerException = null) + : base(DefaultMessage(), innerException) { + this.IsTransient = isTransient; } - protected DomainException(string message) : base(message) + protected DomainException(bool isTransient, string message, Exception innerException = null) + : base(message, innerException) { + this.IsTransient = isTransient; } - protected DomainException(string message, Exception innerException) : base(message, innerException) + #endregion Constructors + + #region Properties + + /// + /// Gets a value indicating whether the exception is transient. + /// + public bool IsTransient { get; } + + #endregion Properties + + #region Methods + + public static string DefaultMessage() => "An error occurred in the domain layer."; + + public override string ToString() { + var s = $"{this.GetType()}: {this.Message} "; + s += $"{Environment.NewLine}IsTransient: {this.IsTransient}"; + if (this.InnerException != null) + s += $" ---> {this.InnerException}"; + if (this.StackTrace != null) + s += $"{Environment.NewLine}{this.StackTrace}"; + return s; } - #endregion Constructors - + #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..ec146e4 100644 --- a/Src/DDD.Core/Domain/DomainServiceException.cs +++ b/Src/DDD.Core/Domain/DomainServiceException.cs @@ -3,33 +3,21 @@ 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,9 +32,17 @@ 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 string ToString() { var s = $"{this.GetType()}: {this.Message} "; + s += $"{Environment.NewLine}IsTransient: {this.IsTransient}"; if (this.ServiceType != null) s += $"{Environment.NewLine}ServiceType: {this.ServiceType}"; if (this.InnerException != null) @@ -57,6 +53,5 @@ public override string ToString() } #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/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..08d3cad 100644 --- a/Src/DDD.Core/Domain/RepositoryException.cs +++ b/Src/DDD.Core/Domain/RepositoryException.cs @@ -3,33 +3,21 @@ 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 +32,19 @@ 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 string ToString() { var s = $"{this.GetType()}: {this.Message} "; + s += $"{Environment.NewLine}IsTransient: {this.IsTransient}"; if (this.EntityType != null) - s += $"{Environment.NewLine}Entity type: {this.EntityType}"; + s += $"{Environment.NewLine}EntityType: {this.EntityType}"; if (this.InnerException != null) s += $" ---> {this.InnerException}"; if (this.StackTrace != null) @@ -57,6 +53,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/Infrastructure/Data/DbCommandHander.cs b/Src/DDD.Core/Infrastructure/Data/DbCommandHander.cs new file mode 100644 index 0000000..6dcfda9 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/DbCommandHander.cs @@ -0,0 +1,69 @@ +using Conditions; +using System.Data; +using System.Data.Common; +using System; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using Mapping; + + /// + /// Base class for handling database commands. + /// + /// The type of the command. + /// + public abstract class DbCommandHandler : ICommandHandler + where TCommand : class, ICommand + { + + #region Fields + + private readonly IObjectTranslator exceptionTranslator = DbToCommandExceptionTranslator.Default; + + #endregion Fields + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The database connection factory. + protected DbCommandHandler(IDbConnectionFactory connectionFactory) + { + Condition.Requires(connectionFactory, nameof(connectionFactory)).IsNotNull(); + this.ConnectionFactory = connectionFactory; + } + + #endregion Constructors + + #region Properties + + protected IDbConnectionFactory ConnectionFactory { get; } + + #endregion Properties + + #region Methods + + public void Handle(TCommand command) + { + Condition.Requires(command, nameof(command)).IsNotNull(); + try + { + using (var connection = this.ConnectionFactory.CreateOpenConnection()) + { + this.Execute(command, connection); + } + } + catch (DbException ex) + { + throw this.exceptionTranslator.Translate(ex, new { Command = command }); + } + } + + protected abstract void Execute(TCommand command, IDbConnection connection); + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Infrastructure/Data/DbQueryHandler.cs b/Src/DDD.Core/Infrastructure/Data/DbQueryHandler.cs index 31ce33a..f2476b9 100644 --- a/Src/DDD.Core/Infrastructure/Data/DbQueryHandler.cs +++ b/Src/DDD.Core/Infrastructure/Data/DbQueryHandler.cs @@ -7,6 +7,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; using Threading; + using Mapping; /// /// Base class for handling database queries. @@ -18,6 +19,12 @@ public abstract class DbQueryHandler : IAsyncQueryHandler { + #region Fields + + private readonly IObjectTranslator exceptionTranslator = DbToQueryExceptionTranslator.Default; + + #endregion Fields + #region Constructors /// @@ -53,7 +60,7 @@ public async Task HandleAsync(TQuery query) } catch(DbException ex) { - throw new QueryException(ex, query); + throw this.exceptionTranslator.Translate(ex, new { Query = query }); } } diff --git a/Src/DDD.Core/Infrastructure/Data/DbToCommandExceptionTranslator.cs b/Src/DDD.Core/Infrastructure/Data/DbToCommandExceptionTranslator.cs new file mode 100644 index 0000000..896b78d --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/DbToCommandExceptionTranslator.cs @@ -0,0 +1,52 @@ +using System.Data.Common; +using System.Collections.Generic; +using Conditions; + +namespace DDD.Core.Infrastructure.Data +{ + using Mapping; + using Application; + + public class DbToCommandExceptionTranslator : IObjectTranslator + { + + #region Fields + + public static readonly IObjectTranslator Default = new DbToCommandExceptionTranslator(); + + private readonly Dictionary> translators = new Dictionary>(); + + #endregion Fields + + #region Constructors + + public DbToCommandExceptionTranslator() + { + this.translators.Add("System.Data.SqlClient.SqlException", new SqlServerToCommandExceptionTranslator()); + this.translators.Add("Oracle.DataAccess.Client.OracleException", new OracleToCommandExceptionTranslator()); + } + + #endregion Constructors + + #region Methods + + public CommandException Translate(DbException exception, IDictionary options = null) + { + Condition.Requires(exception, nameof(exception)).IsNotNull(); + Condition.Requires(options, nameof(options)) + .IsNotNull() + .Evaluate(options.ContainsKey("Command")); + var exceptionType = exception.GetType().FullName; + if (this.translators.TryGetValue(exceptionType, out var translator)) + return translator.Translate(exception, options); + else + { + var command = (ICommand)options["Command"]; + return new CommandException(isTransient: false, command, exception); + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Infrastructure/Data/DbToQueryExceptionTranslator.cs b/Src/DDD.Core/Infrastructure/Data/DbToQueryExceptionTranslator.cs new file mode 100644 index 0000000..17d776c --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/DbToQueryExceptionTranslator.cs @@ -0,0 +1,52 @@ +using System.Data.Common; +using System.Collections.Generic; +using Conditions; + +namespace DDD.Core.Infrastructure.Data +{ + using Mapping; + using Application; + + public class DbToQueryExceptionTranslator : IObjectTranslator + { + + #region Fields + + public static readonly IObjectTranslator Default = new DbToQueryExceptionTranslator(); + + private readonly Dictionary> translators = new Dictionary>(); + + #endregion Fields + + #region Constructors + + public DbToQueryExceptionTranslator() + { + this.translators.Add("System.Data.SqlClient.SqlException", new SqlServerToQueryExceptionTranslator()); + this.translators.Add("Oracle.DataAccess.Client.OracleException", new OracleToQueryExceptionTranslator()); + } + + #endregion Constructors + + #region Methods + + public QueryException Translate(DbException exception, IDictionary options = null) + { + Condition.Requires(exception, nameof(exception)).IsNotNull(); + Condition.Requires(options, nameof(options)) + .IsNotNull() + .Evaluate(options.ContainsKey("Query")); + var exceptionType = exception.GetType().FullName; + if (this.translators.TryGetValue(exceptionType, out var translator)) + return translator.Translate(exception, options); + else + { + var query = (IQuery)options["Query"]; + return new QueryException(isTransient: false, query, exception); + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Infrastructure/Data/DbToRepositoryExceptionTranslator.cs b/Src/DDD.Core/Infrastructure/Data/DbToRepositoryExceptionTranslator.cs new file mode 100644 index 0000000..3c44183 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/DbToRepositoryExceptionTranslator.cs @@ -0,0 +1,54 @@ +using System.Data.Common; +using System.Collections.Generic; +using System; +using Conditions; + +namespace DDD.Core.Infrastructure.Data +{ + using Mapping; + using Domain; + + public class DbToRepositoryExceptionTranslator : IObjectTranslator + { + + #region Fields + + public static readonly IObjectTranslator Default = new DbToRepositoryExceptionTranslator(); + + private readonly Dictionary> translators = new Dictionary>(); + + #endregion Fields + + #region Constructors + + public DbToRepositoryExceptionTranslator() + { + this.translators.Add("System.Data.SqlClient.SqlException", new SqlServerToRepositoryExceptionTranslator()); + this.translators.Add("Oracle.DataAccess.Client.OracleException", new OracleToRepositoryExceptionTranslator()); + } + + #endregion Constructors + + #region Methods + + public RepositoryException Translate(DbException exception, IDictionary options = null) + { + Condition.Requires(exception, nameof(exception)).IsNotNull(); + Condition.Requires(options, nameof(options)) + .IsNotNull() + .Evaluate(options.ContainsKey("EntityType")); + var exceptionType = exception.GetType().FullName; + if (this.translators.TryGetValue(exceptionType, out var translator)) + return translator.Translate(exception, options); + else + { + var entityType = (Type)options["EntityType"]; + var outerException = options.ContainsKey("OuterException") ? (Exception)options["OuterException"] : exception; + return new RepositoryException(isTransient: false, entityType, outerException); + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Infrastructure/Data/OracleErrorHelper.cs b/Src/DDD.Core/Infrastructure/Data/OracleErrorHelper.cs new file mode 100644 index 0000000..4e0676d --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/OracleErrorHelper.cs @@ -0,0 +1,56 @@ +using Conditions; + +namespace DDD.Core.Infrastructure.Data +{ + /// + /// To Improve. + /// + internal class OracleErrorHelper + { + #region Methods + + public static bool IsUnavailableError(dynamic error) + { + Condition.Requires(error, nameof(error)).IsNotNull(); + switch (error.Number) + { + // Oracle Error Code: 3114 + // not connected to ORACLE + case 3114: + return true; + default: + return false; + } + } + + public static bool IsUnauthorizedError(dynamic error) + { + Condition.Requires(error, nameof(error)).IsNotNull(); + switch (error.Number) + { + // Oracle Error Code: 1017 + // invalid username/password; logon denied + case 4060: + return true; + default: + return false; + } + } + + public static bool IsTimeoutError(dynamic error) + { + Condition.Requires(error, nameof(error)).IsNotNull(); + switch (error.Number) + { + // Oracle Error Code: 1013 + // user requested cancel of current operation + case 1013: + return true; + default: + return 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..1809863 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/OracleToCommandExceptionTranslator.cs @@ -0,0 +1,38 @@ +using System.Data.Common; +using System.Collections.Generic; +using Conditions; + +namespace DDD.Core.Infrastructure.Data +{ + using Mapping; + using Application; + + internal class OracleToCommandExceptionTranslator : IObjectTranslator + { + #region Methods + + public CommandException Translate(DbException exception, IDictionary options = null) + { + Condition.Requires(exception, nameof(exception)).IsNotNull(); + Condition.Requires(options, nameof(options)) + .IsNotNull() + .Evaluate(options.ContainsKey("Command")); + var command = (ICommand)options["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); + } + 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..45fae2d --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/OracleToQueryExceptionTranslator.cs @@ -0,0 +1,38 @@ +using System.Data.Common; +using System.Collections.Generic; +using Conditions; + +namespace DDD.Core.Infrastructure.Data +{ + using Mapping; + using Application; + + internal class OracleToQueryExceptionTranslator : IObjectTranslator + { + #region Methods + + public QueryException Translate(DbException exception, IDictionary options = null) + { + Condition.Requires(exception, nameof(exception)).IsNotNull(); + Condition.Requires(options, nameof(options)) + .IsNotNull() + .Evaluate(options.ContainsKey("Query")); + var query = (IQuery)options["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); + } + 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..9de0693 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/OracleToRepositoryExceptionTranslator.cs @@ -0,0 +1,40 @@ +using System.Data.Common; +using System.Collections.Generic; +using System; +using Conditions; + +namespace DDD.Core.Infrastructure.Data +{ + using Mapping; + using Domain; + + internal class OracleToRepositoryExceptionTranslator : IObjectTranslator + { + #region Methods + + public RepositoryException Translate(DbException exception, IDictionary options = null) + { + Condition.Requires(exception, nameof(exception)).IsNotNull(); + Condition.Requires(options, nameof(options)) + .IsNotNull() + .Evaluate(options.ContainsKey("EntityType")); + var entityType = (Type)options["EntityType"]; + var outerException = options.ContainsKey("OuterException") ? (Exception)options["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); + } + return new RepositoryException(isTransient: false, entityType, outerException); + } + + #endregion Methods + } +} diff --git a/Src/DDD.Core/Infrastructure/Data/SqlErrorExtensions.cs b/Src/DDD.Core/Infrastructure/Data/SqlErrorExtensions.cs new file mode 100644 index 0000000..b40f615 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/SqlErrorExtensions.cs @@ -0,0 +1,125 @@ +using Conditions; +using System.Data.SqlClient; + +namespace DDD.Core.Infrastructure.Data +{ + internal static class SqlErrorExtensions + { + + #region Methods + + public static bool IsUnavailableError(this SqlError error) + { + Condition.Requires(error, nameof(error)).IsNotNull(); + 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(this SqlError error) + { + Condition.Requires(error, nameof(error)).IsNotNull(); + 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(this SqlError error) + { + Condition.Requires(error, nameof(error)).IsNotNull(); + 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; + } + } + + #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..3327e54 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/SqlServerToCommandExceptionTranslator.cs @@ -0,0 +1,39 @@ +using System.Data.SqlClient; +using System.Data.Common; +using System.Collections.Generic; +using Conditions; + +namespace DDD.Core.Infrastructure.Data +{ + using Mapping; + using Application; + + internal class SqlServerToCommandExceptionTranslator : IObjectTranslator + { + #region Methods + + public CommandException Translate(DbException exception, IDictionary options = null) + { + Condition.Requires(exception, nameof(exception)).IsNotNull(); + Condition.Requires(options, nameof(options)) + .IsNotNull() + .Evaluate(options.ContainsKey("Command")); + var command = (ICommand)options["Command"]; + var sqlServerException = (SqlException)exception; + foreach (SqlError error in sqlServerException.Errors) + { + if (error.IsUnavailableError()) + return new CommandUnavailableException(command, exception); + + if (error.IsUnauthorizedError()) + return new CommandUnauthorizedException(command, exception); + + if (error.IsTimeoutError()) + return new CommandTimeoutException(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..4640df9 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/SqlServerToQueryExceptionTranslator.cs @@ -0,0 +1,39 @@ +using System.Data.SqlClient; +using System.Data.Common; +using System.Collections.Generic; +using Conditions; + +namespace DDD.Core.Infrastructure.Data +{ + using Mapping; + using Application; + + internal class SqlServerToQueryExceptionTranslator : IObjectTranslator + { + #region Methods + + public QueryException Translate(DbException exception, IDictionary options = null) + { + Condition.Requires(exception, nameof(exception)).IsNotNull(); + Condition.Requires(options, nameof(options)) + .IsNotNull() + .Evaluate(options.ContainsKey("Query")); + var query = (IQuery)options["Query"]; + var sqlServerException = (SqlException)exception; + foreach (SqlError error in sqlServerException.Errors) + { + if (error.IsUnavailableError()) + return new QueryUnavailableException(query, exception); + + if (error.IsUnauthorizedError()) + return new QueryUnauthorizedException(query, exception); + + if (error.IsTimeoutError()) + return new QueryTimeoutException(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..374191d --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/SqlServerToRepositoryExceptionTranslator.cs @@ -0,0 +1,41 @@ +using System.Data.SqlClient; +using System.Data.Common; +using System.Collections.Generic; +using Conditions; +using System; + +namespace DDD.Core.Infrastructure.Data +{ + using Mapping; + using Domain; + + internal class SqlServerToRepositoryExceptionTranslator : IObjectTranslator + { + #region Methods + + public RepositoryException Translate(DbException exception, IDictionary options = null) + { + Condition.Requires(exception, nameof(exception)).IsNotNull(); + Condition.Requires(options, nameof(options)) + .IsNotNull() + .Evaluate(options.ContainsKey("EntityType")); + var entityType = (Type)options["EntityType"]; + var outerException = options.ContainsKey("OuterException") ? (Exception)options["OuterException"] : exception; + var sqlServerException = (SqlException)exception; + foreach (SqlError error in sqlServerException.Errors) + { + if (error.IsUnavailableError()) + return new RepositoryUnavailableException(entityType, outerException); + + if (error.IsUnauthorizedError()) + return new RepositoryUnauthorizedException(entityType, outerException); + + if (error.IsTimeoutError()) + return new RepositoryTimeoutException(entityType, outerException); + } + return new RepositoryException(isTransient: false, entityType, outerException); + } + + #endregion Methods + } +} 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 - - } -} From fb30e0417230a973d2c7c2c11ce49ae544e4923b Mon Sep 17 00:00:00 2001 From: draphyz Date: Tue, 16 Jun 2020 18:37:33 +0200 Subject: [PATCH 023/111] Add extensions to SimpleInjector container --- .../DDD.Core.Abstractions.csproj | 1 + .../IKeyedServiceProvider.cs | 14 +++++ .../ContainerExtensions.cs | 42 +++++++++++++ .../DDD.Core.SimpleInjector.csproj | 6 ++ .../KeyedServiceProvider.cs | 59 +++++++++++++++++++ 5 files changed, 122 insertions(+) create mode 100644 Src/DDD.Core.Abstractions/DependencyInjection/IKeyedServiceProvider.cs create mode 100644 Src/DDD.Core.SimpleInjector/ContainerExtensions.cs create mode 100644 Src/DDD.Core.SimpleInjector/KeyedServiceProvider.cs diff --git a/Src/DDD.Core.Abstractions/DDD.Core.Abstractions.csproj b/Src/DDD.Core.Abstractions/DDD.Core.Abstractions.csproj index dc25e82..a09ddd8 100644 --- a/Src/DDD.Core.Abstractions/DDD.Core.Abstractions.csproj +++ b/Src/DDD.Core.Abstractions/DDD.Core.Abstractions.csproj @@ -52,6 +52,7 @@ + diff --git a/Src/DDD.Core.Abstractions/DependencyInjection/IKeyedServiceProvider.cs b/Src/DDD.Core.Abstractions/DependencyInjection/IKeyedServiceProvider.cs new file mode 100644 index 0000000..c8d1b88 --- /dev/null +++ b/Src/DDD.Core.Abstractions/DependencyInjection/IKeyedServiceProvider.cs @@ -0,0 +1,14 @@ +namespace DDD.DependencyInjection +{ + public interface IKeyedServiceProvider + where TService : class + { + + #region Methods + + TService GetService(TKey key); + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs new file mode 100644 index 0000000..9f1ede7 --- /dev/null +++ b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs @@ -0,0 +1,42 @@ +using SimpleInjector; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Conditions; +using DDD.DependencyInjection; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + public static class ContainerExtensions + { + + #region Methods + + public static TService GetNamedInstance(this Container container, string name) where TService : class + { + Condition.Requires(container, nameof(container)).IsNotNull(); + var provider = container.GetInstance>(); + return provider.GetService(name); + } + + public static void RegisterConditional(this Container container, Func instanceCreator, Predicate predicate) + where TService : class + { + Condition.Requires(container, nameof(container)).IsNotNull(); + var registration = Lifestyle.Transient.CreateRegistration(instanceCreator, container); + container.RegisterConditional(registration, predicate); + } + + public static void RegisterConditional(this Container container, Type openGenericServiceType, IEnumerable assemblies, Func predicate) + { + Condition.Requires(container, nameof(container)).IsNotNull(); + var implementationTypes = container.GetTypesToRegister(openGenericServiceType, assemblies) + .Where(predicate); + container.Register(openGenericServiceType, implementationTypes); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj b/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj index 3cecd48..6658e8f 100644 --- a/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj +++ b/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj @@ -49,6 +49,8 @@ Properties\CommonAssemblyInfo.cs + + @@ -56,6 +58,10 @@ + + {596a8700-3d18-4a62-b200-1f78a9ea4617} + DDD.Core.Abstractions + {2438b31a-3a39-4878-81fa-be5ae715eae5} DDD.Core.Messages diff --git a/Src/DDD.Core.SimpleInjector/KeyedServiceProvider.cs b/Src/DDD.Core.SimpleInjector/KeyedServiceProvider.cs new file mode 100644 index 0000000..67af283 --- /dev/null +++ b/Src/DDD.Core.SimpleInjector/KeyedServiceProvider.cs @@ -0,0 +1,59 @@ +using DDD.DependencyInjection; +using SimpleInjector; +using Conditions; +using System.Collections.Generic; +using System; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + public class KeyedServiceProvider : IKeyedServiceProvider + where TService : class + { + + #region Fields + + private readonly Container container; + private readonly Dictionary> producers; + + #endregion Fields + + #region Constructors + + public KeyedServiceProvider(Container container, IEqualityComparer keyComparer = null) + { + Condition.Requires(container, nameof(container)).IsNotNull(); + this.container = container; + this.producers = new Dictionary>(keyComparer); + } + + #endregion Constructors + + #region Methods + + public TService GetService(TKey key) + { + if (this.producers.TryGetValue(key, out var producer)) + return producer.GetInstance(); + throw new InvalidOperationException($"No registration for key {key}."); + } + + public void Register(TKey key, Lifestyle lifestyle) + where TImplementation : class, TService + { + Condition.Requires(lifestyle, nameof(lifestyle)).IsNotNull(); + var producer = lifestyle.CreateProducer(container); + this.producers.Add(key, producer); + } + + public void Register(TKey key, Func instanceCreator, Lifestyle lifestyle) + { + Condition.Requires(instanceCreator, nameof(instanceCreator)).IsNotNull(); + Condition.Requires(lifestyle, nameof(lifestyle)).IsNotNull(); + var producer = lifestyle.CreateProducer(instanceCreator, container); + this.producers.Add(key, producer); + } + + #endregion Methods + + } +} \ No newline at end of file From 6e3c3026e6ed45e968b9c6790cb21d8cfb60ca27 Mon Sep 17 00:00:00 2001 From: draphyz Date: Tue, 16 Jun 2020 20:39:58 +0200 Subject: [PATCH 024/111] Renaming --- .../CreatePharmaceuticalPrescription.cs | 2 +- .../PharmaceuticalPrescriptionSummary.cs | 2 +- ...ianPharmaceuticalPrescriptionTranslator.cs | 2 +- .../Domain/Prescriptions/Prescription.cs | 4 ++-- .../Prescriptions/PrescriptionMapping.cs | 2 +- ...ndPharmaceuticalPrescriptionsByPatient.sql | Bin 824 -> 826 bytes .../Infrastructure/SqlScripts.Designer.cs | 2 +- .../PharmaceuticalPrescriptionCreatorTests.cs | 2 +- ...uticalPrescriptionsByPatientFinderTests.cs | 12 ++++++------ .../OracleScripts.Designer.cs | 6 +++--- .../Scripts/Oracle/FillSchema.sql | 2 +- ...ndPharmaceuticalPrescriptionsByPatient.sql | 12 ++++++------ ...indPrescribedMedicationsByPrescription.sql | Bin 11122 -> 11130 bytes .../RevokePharmaceuticalPrescription.sql | 2 +- .../Scripts/SqlServer/CreateDatabase.sql | Bin 16644 -> 16646 bytes ...ndPharmaceuticalPrescriptionsByPatient.sql | 12 ++++++------ ...indPrescribedMedicationsByPrescription.sql | Bin 12034 -> 12042 bytes .../RevokePharmaceuticalPrescription.sql | 2 +- .../SqlServerScripts.Designer.cs | 6 +++--- 19 files changed, 35 insertions(+), 35 deletions(-) diff --git a/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/CreatePharmaceuticalPrescription.cs b/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/CreatePharmaceuticalPrescription.cs index ce4cb68..6441509 100644 --- a/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/CreatePharmaceuticalPrescription.cs +++ b/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/CreatePharmaceuticalPrescription.cs @@ -18,7 +18,7 @@ public class CreatePharmaceuticalPrescription : ICommand public DateTime CreatedOn { get; set; } = DateTime.Now; - public DateTime? DelivrableAt { get; set; } + public DateTime? DeliverableAt { get; set; } public int FacilityIdentifier { get; set; } 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/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs b/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs index 24ce439..e47fd20 100644 --- a/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs +++ b/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs @@ -31,7 +31,7 @@ public PharmaceuticalPrescription Translate(CreatePharmaceuticalPrescription com command.Medications.Select(m => ToPrescribedMedication(m)), command.CreatedOn, new Alpha2LanguageCode(command.LanguageCode), - command.DelivrableAt + command.DeliverableAt ); } diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/Prescription.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/Prescription.cs index 3a52015..70d84a9 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/Prescription.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/Prescription.cs @@ -43,7 +43,7 @@ protected Prescription(PrescriptionIdentifier identifier, this.HealthFacility = healthFacility; this.Status = status; this.CreatedOn = createdOn; - this.DelivrableAt = delivrableAt; + this.DeliverableAt = delivrableAt; this.LanguageCode = languageCode; } @@ -53,7 +53,7 @@ protected Prescription(PrescriptionIdentifier identifier, public DateTime CreatedOn { get; private set; } - public DateTime? DelivrableAt { get; private set; } + public DateTime? DeliverableAt { get; private set; } public HealthFacility HealthFacility { get; private set; } diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs index cfeb2b5..f090446 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs @@ -68,7 +68,7 @@ protected PrescriptionMapping(bool useUpperCase) m.Type(NHibernateUtil.Date); m.NotNullable(true); }); - this.Property(p => p.DelivrableAt, m => m.Type(NHibernateUtil.Date)); + this.Property(p => p.DeliverableAt, m => m.Type(NHibernateUtil.Date)); // Prescriber this.Property(p => p.Prescriber, m => { diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Scripts/FindPharmaceuticalPrescriptionsByPatient.sql b/Src/DDD.HealthcareDelivery/Infrastructure/Scripts/FindPharmaceuticalPrescriptionsByPatient.sql index 2b73a81b648c7e1db31f14253b264ee3615a6b66..ad2c5bcae7674ecc05c9a427beaac4b19c5e5c5a 100644 GIT binary patch delta 14 VcmdnNwu^1UCq~B9%^w-v838Q{1$6)b delta 12 TcmdnRwu5cMC&tZR7~L2FByR-~ diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/SqlScripts.Designer.cs b/Src/DDD.HealthcareDelivery/Infrastructure/SqlScripts.Designer.cs index ad5abe8..faf0bba 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/SqlScripts.Designer.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/SqlScripts.Designer.cs @@ -71,7 +71,7 @@ internal SqlScripts() { /// WHEN 'ARC' THEN 6 /// END AS Status, /// CreatedOn, - /// DelivrableAt, + /// DeliverableAt, /// PrescriberDisplayName ///FROM Prescription ///WHERE PrescriptionType = 'PHARM' diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs index 97e33d3..a8f64cc 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs @@ -107,7 +107,7 @@ private static CreatePharmaceuticalPrescription CreateCommand() FacilityName = "Medical Office Donald Duck", PrescriptionIdentifier = 1, CreatedOn = new DateTime(2018, 1, 1, 10, 6, 0), - DelivrableAt = new DateTime(2018, 2, 1), + DeliverableAt = new DateTime(2018, 2, 1), Medications = new PrescribedMedicationDescriptor[] { new PrescribedMedicationDescriptor diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinderTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinderTests.cs index f20ac34..27fb953 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinderTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinderTests.cs @@ -46,7 +46,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 +54,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 +62,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 +70,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 +86,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 +94,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" } } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.Designer.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.Designer.cs index a235d22..f10606a 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.Designer.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.Designer.cs @@ -114,7 +114,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, Pa [rest of string was truncated]";. /// internal static string FindPharmaceuticalPrescriptionsByPatient { get { @@ -127,7 +127,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, Pa [rest of string was truncated]";. /// internal static string FindPrescribedMedicationsByPrescription { get { @@ -140,7 +140,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, Pa [rest of string was truncated]";. /// internal static string RevokePharmaceuticalPrescription { get { diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql index c6d6ed7..0517530 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql @@ -92,7 +92,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), diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FindPharmaceuticalPrescriptionsByPatient.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FindPharmaceuticalPrescriptionsByPatient.sql index 4d49bfa..845a71d 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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) / diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FindPrescribedMedicationsByPrescription.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FindPrescribedMedicationsByPrescription.sql index 5a3cbd846e787fca6e15390bc5d0a13fd4248c17..73a5c347da10aa3612fe7a10c21f1571b6c5eebd 100644 GIT binary patch delta 40 ncmewq_A6|I03&1SW=Bpzmdy&RN-SV@AqSA!Cd delta 46 scmewr_9<+G0OMvMMk5X&A;2>Eg9_W`7~UurFmLk-(F7>VO3r~70Aj!mTL1t6 diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/RevokePharmaceuticalPrescription.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/RevokePharmaceuticalPrescription.sql index e2ad356..bba7ec8 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/RevokePharmaceuticalPrescription.sql +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/RevokePharmaceuticalPrescription.sql @@ -2,7 +2,7 @@ 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, 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.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) / diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql index c11bd20a4b1c15c9c9a0e6189b21816d4bcc528f..da4ceb0a2e6c2c0def10cf28b6024516e77f0335 100644 GIT binary patch delta 16 XcmZo^Vr*+-+)$v;n7TP%f2tq=GMxpz delta 14 VcmZo`Vr*$*+)$vuxk!JCAOIx$c;$HT$GBODtBqyzHr6N>Aj^Og!aXIv3d99U{88ZJ0{}094YU9N diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/RevokePharmaceuticalPrescription.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/RevokePharmaceuticalPrescription.sql index e8dec88..009f814 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/RevokePharmaceuticalPrescription.sql +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/RevokePharmaceuticalPrescription.sql @@ -2,7 +2,7 @@ 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], [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].[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) GO diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerScripts.Designer.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerScripts.Designer.cs index 723edec..0be59f3 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerScripts.Designer.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerScripts.Designer.cs @@ -100,7 +100,7 @@ internal static string CreateDatabase { ///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 { From 6542918e4cb2ff4e2ce664dbe66a6460f1baeb00 Mon Sep 17 00:00:00 2001 From: draphyz Date: Tue, 16 Jun 2020 21:06:49 +0200 Subject: [PATCH 025/111] Add ObjectName to validation result --- .../Validation/ValidationResult.cs | 16 +++++++++++++--- .../FluentValidatorAdapter.cs | 4 ++-- .../ValidationResultTranslator.cs | 9 ++++++--- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/Src/DDD.Core.Abstractions/Validation/ValidationResult.cs b/Src/DDD.Core.Abstractions/Validation/ValidationResult.cs index 05367cd..7611021 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 Conditions; +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(objectName, nameof(objectName)).IsNotNullOrWhiteSpace(); Condition.Requires(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.FluentValidation/FluentValidatorAdapter.cs b/Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs index 0b262e5..d4525b9 100644 --- a/Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs +++ b/Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs @@ -37,7 +37,7 @@ public FluentValidatorAdapter(IValidator fluentValidator) public DDD.Validation.ValidationResult Validate(T obj, string ruleSet = null) { var result = this.fluentValidator.Validate(obj, ruleSet: ruleSet); - return this.resultTranslator.Translate(result); + return this.resultTranslator.Translate(result, new { ObjectName = obj.GetType().Name }); } /// @@ -49,7 +49,7 @@ public DDD.Validation.ValidationResult Validate(T obj, string ruleSet = null) { await new SynchronizationContextRemover(); var result = await this.fluentValidator.ValidateAsync(obj, ruleSet: ruleSet); - return this.resultTranslator.Translate(result); + return this.resultTranslator.Translate(result, new { ObjectName = obj.GetType().Name }); } #endregion Methods diff --git a/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs b/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs index b77aff2..848d963 100644 --- a/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs +++ b/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs @@ -14,13 +14,16 @@ internal class ValidationResultTranslator #region Methods - public DDD.Validation.ValidationResult Translate(ValidationResult result, - IDictionary options) + public DDD.Validation.ValidationResult Translate(ValidationResult result, IDictionary options) { Condition.Requires(result, nameof(result)).IsNotNull(); + Condition.Requires(options, nameof(options)) + .IsNotNull() + .Evaluate(options.ContainsKey("ObjectName")); + var objectName = (string)options["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 DDD.Validation.ValidationFailure ToFailure(ValidationFailure failure) From 8170aae796e19c9cf3bed890d1c4277916a583af Mon Sep 17 00:00:00 2001 From: draphyz Date: Wed, 17 Jun 2020 21:59:32 +0200 Subject: [PATCH 026/111] Add decorators for error handling --- DDD.sln | 7 ++ .../AsyncPollyCommandHandler.cs | 47 ++++++++++++ Src/DDD.Core.Polly/AsyncPollyQueryHandler.cs | 47 ++++++++++++ Src/DDD.Core.Polly/DDD.Core.Polly.csproj | 71 +++++++++++++++++++ Src/DDD.Core.Polly/PollyCommandHandler.cs | 46 ++++++++++++ Src/DDD.Core.Polly/PollyQueryHandler.cs | 46 ++++++++++++ Src/DDD.Core.Polly/Properties/AssemblyInfo.cs | 7 ++ 7 files changed, 271 insertions(+) create mode 100644 Src/DDD.Core.Polly/AsyncPollyCommandHandler.cs create mode 100644 Src/DDD.Core.Polly/AsyncPollyQueryHandler.cs create mode 100644 Src/DDD.Core.Polly/DDD.Core.Polly.csproj create mode 100644 Src/DDD.Core.Polly/PollyCommandHandler.cs create mode 100644 Src/DDD.Core.Polly/PollyQueryHandler.cs create mode 100644 Src/DDD.Core.Polly/Properties/AssemblyInfo.cs diff --git a/DDD.sln b/DDD.sln index f558f62..c78dfed 100644 --- a/DDD.sln +++ b/DDD.sln @@ -61,6 +61,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Common.NHibernate", "Sr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.SimpleInjector", "Src\DDD.Core.SimpleInjector\DDD.Core.SimpleInjector.csproj", "{3EFAACD8-CF5E-4E31-884B-6B9F87F1E495}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.Polly", "Src\DDD.Core.Polly\DDD.Core.Polly.csproj", "{4878A4FB-2C1E-4C06-BB42-AFE1ACE2A882}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -147,6 +149,10 @@ Global {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -172,6 +178,7 @@ Global {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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {983C3AB4-301E-47A3-93FF-2E4BD8F2090F} diff --git a/Src/DDD.Core.Polly/AsyncPollyCommandHandler.cs b/Src/DDD.Core.Polly/AsyncPollyCommandHandler.cs new file mode 100644 index 0000000..a9e042b --- /dev/null +++ b/Src/DDD.Core.Polly/AsyncPollyCommandHandler.cs @@ -0,0 +1,47 @@ +using System.Threading.Tasks; +using Polly; +using Conditions; + +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 + +#pragma warning disable CS3001 // Argument type is not CLS-compliant + public AsyncPollyCommandHandler(IAsyncCommandHandler handler, IAsyncPolicy policy) +#pragma warning restore CS3001 // Argument type is not CLS-compliant + { + Condition.Requires(handler, nameof(handler)).IsNotNull(); + Condition.Requires(policy, nameof(policy)).IsNotNull(); + this.handler = handler; + this.policy = policy; + } + + #endregion Constructors + + #region Methods + + public async Task HandleAsync(TCommand command) + { + await policy.ExecuteAsync(() => this.handler.HandleAsync(command)); + } + + #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..f2ac0a9 --- /dev/null +++ b/Src/DDD.Core.Polly/AsyncPollyQueryHandler.cs @@ -0,0 +1,47 @@ +using Polly; +using Conditions; +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 + +#pragma warning disable CS3001 // Argument type is not CLS-compliant + public AsyncPollyQueryHandler(IAsyncQueryHandler handler, IAsyncPolicy policy) +#pragma warning restore CS3001 // Argument type is not CLS-compliant + { + Condition.Requires(handler, nameof(handler)).IsNotNull(); + Condition.Requires(policy, nameof(policy)).IsNotNull(); + this.handler = handler; + this.policy = policy; + } + + #endregion Constructors + + #region Methods + + public async Task HandleAsync(TQuery query) + { + return await policy.ExecuteAsync(() => this.handler.HandleAsync(query)); + } + + #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..4796e93 --- /dev/null +++ b/Src/DDD.Core.Polly/DDD.Core.Polly.csproj @@ -0,0 +1,71 @@ + + + + + Debug + AnyCPU + {4878A4FB-2C1E-4C06-BB42-AFE1ACE2A882} + Library + Properties + DDD.Core.Infrastructure.ErrorHandling + DDD.Core.Polly + v4.7.2 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + bin\Debug\DDD.Core.Polly.xml + 1591 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + bin\Release\DDD.Core.Polly.xml + 1591 + + + + + + + + + Properties\CommonAssemblyInfo.cs + + + + + + + + + + {2438b31a-3a39-4878-81fa-be5ae715eae5} + DDD.Core.Messages + + + {c6c3e419-b9aa-44ad-9dbf-789294687ae6} + DDD.Core + + + + + 2.1.0 + + + 7.2.1 + + + + \ No newline at end of file diff --git a/Src/DDD.Core.Polly/PollyCommandHandler.cs b/Src/DDD.Core.Polly/PollyCommandHandler.cs new file mode 100644 index 0000000..59db7ba --- /dev/null +++ b/Src/DDD.Core.Polly/PollyCommandHandler.cs @@ -0,0 +1,46 @@ +using Polly; +using Conditions; + +namespace DDD.Core.Infrastructure.ErrorHandling +{ + using Application; + + /// + /// A decorator that applies a resilience policy to the synchronous execution of a command. + /// + public class PollyCommandHandler : ICommandHandler + where TCommand : class, ICommand + { + + #region Fields + + private readonly ICommandHandler handler; + private readonly ISyncPolicy policy; + + #endregion Fields + + #region Constructors + +#pragma warning disable CS3001 // Argument type is not CLS-compliant + public PollyCommandHandler(ICommandHandler handler, ISyncPolicy policy) +#pragma warning restore CS3001 // Argument type is not CLS-compliant + { + Condition.Requires(handler, nameof(handler)).IsNotNull(); + Condition.Requires(policy, nameof(policy)).IsNotNull(); + this.handler = handler; + this.policy = policy; + } + + #endregion Constructors + + #region Methods + + public void Handle(TCommand command) + { + policy.Execute(() => this.handler.Handle(command)); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Polly/PollyQueryHandler.cs b/Src/DDD.Core.Polly/PollyQueryHandler.cs new file mode 100644 index 0000000..9858d5e --- /dev/null +++ b/Src/DDD.Core.Polly/PollyQueryHandler.cs @@ -0,0 +1,46 @@ +using Polly; +using Conditions; + +namespace DDD.Core.Infrastructure.ErrorHandling +{ + using Application; + + /// + /// A decorator that applies a resilience policy to the synchronous execution of a query. + /// + public class PollyQueryHandler : IQueryHandler + where TQuery : class, IQuery + { + + #region Fields + + private readonly IQueryHandler handler; + private readonly ISyncPolicy policy; + + #endregion Fields + + #region Constructors + +#pragma warning disable CS3001 // Argument type is not CLS-compliant + public PollyQueryHandler(IQueryHandler handler, ISyncPolicy policy) +#pragma warning restore CS3001 // Argument type is not CLS-compliant + { + Condition.Requires(handler, nameof(handler)).IsNotNull(); + Condition.Requires(policy, nameof(policy)).IsNotNull(); + this.handler = handler; + this.policy = policy; + } + + #endregion Constructors + + #region Methods + + public TResult Handle(TQuery query) + { + return policy.Execute(() => this.handler.Handle(query)); + } + + #endregion Methods + + } +} \ 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..5af017e --- /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("Useful decorators for error handling based on the Polly library.")] +[assembly: AssemblyProduct("DDD.Core.Polly")] +[assembly: Guid("4878a4fb-2c1e-4c06-bb42-afe1ace2a882")] \ No newline at end of file From bc4977ebbda1cd16c67175c1622686d14843618e Mon Sep 17 00:00:00 2001 From: draphyz Date: Wed, 17 Jun 2020 22:10:00 +0200 Subject: [PATCH 027/111] Migrate to PackageReference --- .../DDD.Common.NHibernate.csproj | 23 ++---- Src/DDD.Common.NHibernate/packages.config | 8 -- Src/DDD.Common/DDD.Common.csproj | 12 ++- Src/DDD.Common/packages.config | 4 - .../DDD.Core.Abstractions.csproj | 9 +-- Src/DDD.Core.Abstractions/packages.config | 4 - Src/DDD.Core.Dapper/DDD.Core.Dapper.csproj | 17 ++-- Src/DDD.Core.Dapper/packages.config | 6 -- .../DDD.Core.FluentValidation.csproj | 29 +++---- Src/DDD.Core.FluentValidation/packages.config | 9 --- .../DDD.Core.NHibernate.csproj | 29 ++----- Src/DDD.Core.NHibernate/packages.config | 9 --- .../DDD.Core.NServiceBus.csproj | 16 ++-- Src/DDD.Core.NServiceBus/packages.config | 5 -- .../DDD.Core.Newtonsoft.csproj | 17 ++-- Src/DDD.Core.Newtonsoft/packages.config | 5 -- .../DDD.Core.SimpleInjector.csproj | 13 ++-- Src/DDD.Core.SimpleInjector/packages.config | 5 -- Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj | 34 ++------ Src/DDD.Core.Xunit/packages.config | 8 -- Src/DDD.Core/DDD.Core.csproj | 18 ++--- Src/DDD.Core/packages.config | 5 -- .../DDD.HealthcareDelivery.Messages.csproj | 7 +- .../packages.config | 4 - Src/DDD.HealthcareDelivery/App.config | 31 ++------ .../DDD.HealthcareDelivery.csproj | 69 ++++++----------- Src/DDD.HealthcareDelivery/packages.config | 17 ---- .../DDD.Common.UnitTests.csproj | 51 ++++-------- Test/DDD.Common.UnitTests/packages.config | 13 ---- .../DDD.Core.Abstractions.UnitTests.csproj | 70 ++++++----------- .../packages.config | 17 ---- .../DDD.Core.UnitTests.csproj | 77 ++++++------------- Test/DDD.Core.UnitTests/packages.config | 18 ----- ...HealthcareDelivery.IntegrationTests.csproj | 73 +++++------------- .../packages.config | 19 ----- .../DDD.HealthcareDelivery.UnitTests.csproj | 58 ++++---------- .../packages.config | 17 ---- 37 files changed, 205 insertions(+), 621 deletions(-) delete mode 100644 Src/DDD.Common.NHibernate/packages.config delete mode 100644 Src/DDD.Common/packages.config delete mode 100644 Src/DDD.Core.Abstractions/packages.config delete mode 100644 Src/DDD.Core.Dapper/packages.config delete mode 100644 Src/DDD.Core.FluentValidation/packages.config delete mode 100644 Src/DDD.Core.NHibernate/packages.config delete mode 100644 Src/DDD.Core.NServiceBus/packages.config delete mode 100644 Src/DDD.Core.Newtonsoft/packages.config delete mode 100644 Src/DDD.Core.SimpleInjector/packages.config delete mode 100644 Src/DDD.Core.Xunit/packages.config delete mode 100644 Src/DDD.Core/packages.config delete mode 100644 Src/DDD.HealthcareDelivery.Messages/packages.config delete mode 100644 Src/DDD.HealthcareDelivery/packages.config delete mode 100644 Test/DDD.Common.UnitTests/packages.config delete mode 100644 Test/DDD.Core.Abstractions.UnitTests/packages.config delete mode 100644 Test/DDD.Core.UnitTests/packages.config delete mode 100644 Test/DDD.HealthcareDelivery.IntegrationTests/packages.config delete mode 100644 Test/DDD.HealthcareDelivery.UnitTests/packages.config diff --git a/Src/DDD.Common.NHibernate/DDD.Common.NHibernate.csproj b/Src/DDD.Common.NHibernate/DDD.Common.NHibernate.csproj index 91a3a14..252f2a9 100644 --- a/Src/DDD.Common.NHibernate/DDD.Common.NHibernate.csproj +++ b/Src/DDD.Common.NHibernate/DDD.Common.NHibernate.csproj @@ -33,21 +33,6 @@ 7.1 - - L:\Packages\Antlr3.Runtime.3.5.1\lib\net40-client\Antlr3.Runtime.dll - - - L:\Packages\Iesi.Collections.4.0.4\lib\net461\Iesi.Collections.dll - - - L:\Packages\NHibernate.5.2.5\lib\net461\NHibernate.dll - - - L:\Packages\Remotion.Linq.2.2.0\lib\net45\Remotion.Linq.dll - - - L:\Packages\Remotion.Linq.EagerFetching.2.2.0\lib\net45\Remotion.Linq.EagerFetching.dll - @@ -65,9 +50,6 @@ - - - {0b70b4fd-f5a0-4a6c-a3fd-90031e08c1c2} @@ -78,5 +60,10 @@ DDD.Core + + + 5.2.5 + + \ No newline at end of file diff --git a/Src/DDD.Common.NHibernate/packages.config b/Src/DDD.Common.NHibernate/packages.config deleted file mode 100644 index 23ea25b..0000000 --- a/Src/DDD.Common.NHibernate/packages.config +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/Src/DDD.Common/DDD.Common.csproj b/Src/DDD.Common/DDD.Common.csproj index 6d6ab6f..733b670 100644 --- a/Src/DDD.Common/DDD.Common.csproj +++ b/Src/DDD.Common/DDD.Common.csproj @@ -35,10 +35,6 @@ 1591 - - L:\Packages\Conditions.2.1.0\lib\portable-net45+win+wpa81+wp80+MonoAndroid10+MonoTouch10\Conditions.dll - True - @@ -66,9 +62,6 @@ - - - {596a8700-3d18-4a62-b200-1f78a9ea4617} @@ -79,6 +72,11 @@ DDD.Core + + + 2.1.0 + + -
-
+
- + - + - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj b/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj index 9457632..8eded95 100644 --- a/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj +++ b/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj @@ -35,47 +35,8 @@ 1591 - - L:\Packages\Antlr3.Runtime.3.5.1\lib\net40-client\Antlr3.Runtime.dll - - - 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\FluentValidation.8.1.3\lib\net45\FluentValidation.dll - - - L:\Packages\Iesi.Collections.4.0.4\lib\net461\Iesi.Collections.dll - - - L:\Packages\NHibernate.5.2.5\lib\net461\NHibernate.dll - - - L:\packages\Oracle.ManagedDataAccess.18.3.0\lib\net40\Oracle.ManagedDataAccess.dll - - - L:\Packages\Remotion.Linq.2.2.0\lib\net45\Remotion.Linq.dll - - - L:\Packages\Remotion.Linq.EagerFetching.2.2.0\lib\net45\Remotion.Linq.EagerFetching.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 - @@ -83,9 +44,6 @@ - - L:\packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll - @@ -160,9 +118,6 @@ - - Designer - @@ -206,7 +161,29 @@ DDD.HealthcareDelivery.Messages - + + + 2.1.0 + + + 1.60.1 + + + 8.1.3 + + + 5.2.5 + + + 18.3.0 + + + 1.5.0 + + + 4.5.0 + + diff --git a/Src/DDD.HealthcareDelivery/packages.config b/Src/DDD.HealthcareDelivery/packages.config deleted file mode 100644 index a30f14e..0000000 --- a/Src/DDD.HealthcareDelivery/packages.config +++ /dev/null @@ -1,17 +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..fa29e9a 100644 --- a/Test/DDD.Common.UnitTests/DDD.Common.UnitTests.csproj +++ b/Test/DDD.Common.UnitTests/DDD.Common.UnitTests.csproj @@ -1,7 +1,5 @@  - - Debug @@ -35,34 +33,14 @@ 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 - @@ -76,10 +54,6 @@ - - - - @@ -91,16 +65,23 @@ DDD.Core + + + 5.6.0 + + + 4.5.0 + + + 2.4.1 + + + 2.4.1 + 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}. - - - - - - {this.InnerException}"; + if (this.StackTrace != null) + s += $"{Environment.NewLine}{this.StackTrace}"; + return s; + } + + private static string DestinationTypeInfo(Type destinationType) + { + if (destinationType == null) + return "another type"; + return $"the type '{destinationType.Name}'"; + } + + private static string SourceTypeInfo(Type sourceType) + { + if (sourceType == null) + return "one type"; + return $"the type '{sourceType.Name}'"; + } + + #endregion Methods + } +} diff --git a/Src/DDD.Core.Abstractions/Serialization/SerializationException.cs b/Src/DDD.Core.Abstractions/Serialization/SerializationException.cs new file mode 100644 index 0000000..cdbfe18 --- /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}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.Newtonsoft/JsonSerializerWrapper.cs b/Src/DDD.Core.Newtonsoft/JsonSerializerWrapper.cs index 2ade331..d356147 100644 --- a/Src/DDD.Core.Newtonsoft/JsonSerializerWrapper.cs +++ b/Src/DDD.Core.Newtonsoft/JsonSerializerWrapper.cs @@ -59,7 +59,14 @@ public T Deserialize(Stream stream) using (var reader = new StreamReader(stream, this.Encoding)) using (var jsonReader = new JsonTextReader(reader)) { - return this.serializer.Deserialize(jsonReader); + try + { + return this.serializer.Deserialize(jsonReader); + } + catch(JsonException exception) + { + throw new SerializationException(typeof(T), exception); + } } } @@ -69,7 +76,14 @@ public void Serialize(Stream stream, object obj) using (StreamWriter writer = new StreamWriter(stream, this.Encoding)) using (JsonTextWriter jsonWriter = new JsonTextWriter(writer)) { - this.serializer.Serialize(jsonWriter, obj); + try + { + this.serializer.Serialize(jsonWriter, obj); + } + catch (JsonException exception) + { + throw new SerializationException(obj?.GetType(), exception); + } } } diff --git a/Src/DDD.Core/Infrastructure/Serialization/DataContractSerializerWrapper.cs b/Src/DDD.Core/Infrastructure/Serialization/DataContractSerializerWrapper.cs index 494ef30..3626afd 100644 --- a/Src/DDD.Core/Infrastructure/Serialization/DataContractSerializerWrapper.cs +++ b/Src/DDD.Core/Infrastructure/Serialization/DataContractSerializerWrapper.cs @@ -75,7 +75,14 @@ public T Deserialize(Stream stream) using (var reader = XmlReader.Create(stream, this.readerSettings)) { var serializer = new DataContractSerializer(typeof(T)); - return (T)serializer.ReadObject(reader); + try + { + return (T)serializer.ReadObject(reader); + } + catch(System.Runtime.Serialization.SerializationException exception) + { + throw new SerializationException(typeof(T), exception); + } } } @@ -86,7 +93,14 @@ public void Serialize(Stream stream, object obj) using (var writer = XmlWriter.Create(stream, this.writerSettings)) { var serializer = new DataContractSerializer(obj.GetType()); - serializer.WriteObject(writer, obj); + try + { + serializer.WriteObject(writer, obj); + } + catch (System.Runtime.Serialization.SerializationException exception) + { + throw new SerializationException(obj.GetType(), exception); + } } } From cff69fc41700dce5076811dbb65edafd5d0ea362 Mon Sep 17 00:00:00 2001 From: draphyz Date: Thu, 16 Jul 2020 19:14:08 +0200 Subject: [PATCH 029/111] Update README.md --- README.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index abab441..1c93823 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -### 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 .NET implementation of a medical prescription model using the approach "Domain-Driven Design" (DDD) and the architectural pattern "Command and Query Responsibility Segregation" (CQRS). **Points of interest** @@ -8,4 +8,57 @@ The goal of the project is to experiment some .NET implementations of core conce Only a lightweight version of CQRS (no data projection, one data store) has been considered. -See the [Wiki](https://github.com/draphyz/DDD/wiki) for more information. +**Model** + +The envisaged model reflects the lifecycle of a medical prescription and, in particular, of a pharmaceutical prescription. This lifecycle can be resumed by the following diagram. + +![Alt Prescription Lifecycle](https://github.com/draphyz/DDD/blob/entityframework/Doc/PrescriptionLifecycle.png) + +The current model only takes into account use cases related to the prescriber : 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 both 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 6 (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 main components on the command-side can be represented as follows : + +![Alt Command Components](https://github.com/draphyz/DDD/blob/entityframework/Doc/CommandComponents.png) + +_Query Model_ + +As mentioned above, 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) + +**Projects** + +Libraries are distributed by component (bounded context) : + +- The "Core" libraries include the core components (Entity, ValueObject, CommandHandler, ...) necessary to implement the approach DDD according to the architectural pattern CQRS. +- 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 project "DDD.HealthcareDelivery.IntegrationTests". + +**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 projects "DDD.Core.Polly" or "DDD.Core". + + From 3c4aa15f3503978354a4433da256fcadabf18080 Mon Sep 17 00:00:00 2001 From: draphyz Date: Wed, 12 Aug 2020 12:19:09 +0200 Subject: [PATCH 030/111] Migrate to SDK-Style projects --- .../DDD.Common.Messages.csproj | 35 +--- .../DDD.Common.NHibernate.csproj | 56 +---- Src/DDD.Common/DDD.Common.csproj | 80 +------ .../DDD.Core.Abstractions.csproj | 88 +------- Src/DDD.Core.Dapper/DDD.Core.Dapper.csproj | 70 +------ .../DDD.Core.FluentValidation.csproj | 90 ++------ .../DDD.Core.Messages.csproj | 41 +--- .../DDD.Core.NHibernate.csproj | 72 +------ .../DDD.Core.NServiceBus.csproj | 65 ++---- .../DDD.Core.Newtonsoft.csproj | 53 +---- Src/DDD.Core.Polly/DDD.Core.Polly.csproj | 63 +----- .../DDD.Core.SimpleInjector.csproj | 70 ++----- Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj | 67 +----- Src/DDD.Core/DDD.Core.csproj | 149 +------------ .../DDD.HealthcareDelivery.Messages.csproj | 63 +----- .../DDD.HealthcareDelivery.csproj | 196 +++--------------- .../DDD.Common.UnitTests.csproj | 83 ++------ .../Properties/AssemblyInfo.cs | 25 --- .../DDD.Core.Abstractions.UnitTests.csproj | 87 ++------ .../Properties/AssemblyInfo.cs | 25 --- .../DDD.Core.UnitTests.csproj | 125 ++--------- .../Properties/AssemblyInfo.cs | 25 --- ...HealthcareDelivery.IntegrationTests.csproj | 156 +++----------- .../Properties/AssemblyInfo.cs | 25 --- .../DDD.HealthcareDelivery.UnitTests.csproj | 119 ++--------- .../Properties/AssemblyInfo.cs | 25 --- 26 files changed, 255 insertions(+), 1698 deletions(-) diff --git a/Src/DDD.Common.Messages/DDD.Common.Messages.csproj b/Src/DDD.Common.Messages/DDD.Common.Messages.csproj index b2efab9..c0df122 100644 --- a/Src/DDD.Common.Messages/DDD.Common.Messages.csproj +++ b/Src/DDD.Common.Messages/DDD.Common.Messages.csproj @@ -1,45 +1,20 @@ - - - + - Debug - AnyCPU - {40A849C5-C8D7-4F76-856A-138AED73A6C3} - Library - Properties DDD.Common - DDD.Common.Messages - v4.7.2 - 512 - true + net472 + false + bin\$(Configuration)\ - 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.NHibernate/DDD.Common.NHibernate.csproj b/Src/DDD.Common.NHibernate/DDD.Common.NHibernate.csproj index 252f2a9..5058a97 100644 --- a/Src/DDD.Common.NHibernate/DDD.Common.NHibernate.csproj +++ b/Src/DDD.Common.NHibernate/DDD.Common.NHibernate.csproj @@ -1,69 +1,31 @@ - - - + - Debug - AnyCPU - {7466B831-1642-4B2B-9EA3-1C9299596C2A} - Library - Properties DDD.Common.Infrastructure.Data - DDD.Common.NHibernate - v4.7.2 - 512 - true + net472 + false + 7.1 + bin\$(Configuration)\ - true full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - 7.1 pdbonly - true - bin\Release\ - TRACE - prompt - 4 - 7.1 - - - - - Properties\CommonAssemblyInfo.cs - - - - - + - - {0b70b4fd-f5a0-4a6c-a3fd-90031e08c1c2} - DDD.Common - - - {C6C3E419-B9AA-44AD-9DBF-789294687AE6} - DDD.Core - + + - - 5.2.5 - + - \ No newline at end of file diff --git a/Src/DDD.Common/DDD.Common.csproj b/Src/DDD.Common/DDD.Common.csproj index 733b670..2cd6d09 100644 --- a/Src/DDD.Common/DDD.Common.csproj +++ b/Src/DDD.Common/DDD.Common.csproj @@ -1,88 +1,28 @@ - - - + - Debug - AnyCPU - {0B70B4FD-F5A0-4A6C-A3FD-90031E08C1C2} - Library - Properties - DDD.Common - DDD.Common - v4.7.2 - 512 - + net472 + false + bin\$(Configuration)\ + bin\$(Configuration)\DDD.Common.xml + 1591 - 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 - - - - Properties\CommonAssemblyInfo.cs - - - - - - - - - - - - - - - - - - - + - - {596a8700-3d18-4a62-b200-1f78a9ea4617} - DDD.Core.Abstractions - - - {c6c3e419-b9aa-44ad-9dbf-789294687ae6} - DDD.Core - + + - - 2.1.0 - + - - \ No newline at end of file diff --git a/Src/DDD.Core.Abstractions/DDD.Core.Abstractions.csproj b/Src/DDD.Core.Abstractions/DDD.Core.Abstractions.csproj index 92f7628..4bad687 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} - Library - Properties DDD - DDD.Core.Abstractions - v4.7.2 - 512 - + net472 + false + bin\$(Configuration)\ + bin\$(Configuration)\DDD.Core.Abstractions.xml + 1591 - 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 - - - - Properties\CommonAssemblyInfo.cs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - 2.1.0 - + - - \ 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 a097a50..9f0d765 100644 --- a/Src/DDD.Core.Dapper/DDD.Core.Dapper.csproj +++ b/Src/DDD.Core.Dapper/DDD.Core.Dapper.csproj @@ -1,79 +1,31 @@ - - - + - Debug - AnyCPU - {701DA58B-AE36-429F-8621-64109B8D29D7} - Library - Properties DDD.Core.Infrastructure.Data - DDD.Core.Dapper - v4.7.2 - 512 - + net472 + false + bin\$(Configuration)\ + bin\$(Configuration)\DDD.Core.Dapper.xml + 1591 - 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 - - - - - - - Properties\CommonAssemblyInfo.cs - - - + - - {596a8700-3d18-4a62-b200-1f78a9ea4617} - DDD.Core.Abstractions - - - {c6c3e419-b9aa-44ad-9dbf-789294687ae6} - DDD.Core - + + - - 2.1.0 - - - 1.60.1 - + + - - \ 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 e84da25..8c5b089 100644 --- a/Src/DDD.Core.FluentValidation/DDD.Core.FluentValidation.csproj +++ b/Src/DDD.Core.FluentValidation/DDD.Core.FluentValidation.csproj @@ -1,99 +1,35 @@ - - - + - Debug - AnyCPU - {5E3745FC-CA80-4D0F-8A25-20EE0F9CF163} - Library - Properties DDD.Core.Infrastructure.Validation - DDD.Core.FluentValidation - v4.7.2 - 512 - + net472 + false + bin\$(Configuration)\ + bin\$(Configuration)\DDD.Core.FluentValidation.xml + 1591 - 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 - - - - - - - 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 - + + + - - 2.1.0 - - - 8.1.3 - - - 4.5.0 - + + + - - \ 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..557ceb3 100644 --- a/Src/DDD.Core.Messages/DDD.Core.Messages.csproj +++ b/Src/DDD.Core.Messages/DDD.Core.Messages.csproj @@ -1,51 +1,20 @@ - - - + - Debug - AnyCPU - {2438B31A-3A39-4878-81FA-BE5AE715EAE5} - Library - Properties DDD.Core - DDD.Core.Messages - v4.7.2 - 512 - true + net472 + false + bin\$(Configuration)\ - 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.NHibernate/DDD.Core.NHibernate.csproj b/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj index 92804de..2595c55 100644 --- a/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj +++ b/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj @@ -1,83 +1,33 @@ - - - + - Debug - AnyCPU - {D4FC7E1B-CAE8-40F9-9FAF-0029D91CC6C4} - Library - Properties DDD.Core.Infrastructure.Data - DDD.Core.NHibernate - v4.7.2 - 512 - true + net472 + false + 7.2 + bin\$(Configuration)\ - true full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - 7.2 pdbonly - true - bin\Release\ - TRACE - prompt - 4 - 7.2 - - - - - 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 - + + + - - 2.1.0 - - - 5.2.5 - + + - \ No newline at end of file diff --git a/Src/DDD.Core.NServiceBus/DDD.Core.NServiceBus.csproj b/Src/DDD.Core.NServiceBus/DDD.Core.NServiceBus.csproj index f128036..a625a71 100644 --- a/Src/DDD.Core.NServiceBus/DDD.Core.NServiceBus.csproj +++ b/Src/DDD.Core.NServiceBus/DDD.Core.NServiceBus.csproj @@ -1,75 +1,34 @@ - - - + - Debug - AnyCPU - {436D869F-7566-4436-90A8-B655145C5BCA} - Library - Properties DDD.Core.Infrastructure.Messaging - DDD.Core.NServiceBus - v4.7.2 - 512 - true + net472 + false + bin\$(Configuration)\ + bin\$(Configuration)\DDD.Core.NServiceBus.xml + 1591 - 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 - - - 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 - + + + - - 2.1.0 - - - 7.1.6 - + + - \ No newline at end of file diff --git a/Src/DDD.Core.Newtonsoft/DDD.Core.Newtonsoft.csproj b/Src/DDD.Core.Newtonsoft/DDD.Core.Newtonsoft.csproj index b0e07c4..03bb9a3 100644 --- a/Src/DDD.Core.Newtonsoft/DDD.Core.Newtonsoft.csproj +++ b/Src/DDD.Core.Newtonsoft/DDD.Core.Newtonsoft.csproj @@ -1,66 +1,29 @@ - - - + - Debug - AnyCPU - {8BF8E0BD-B92C-4FC1-BE17-7C10036A2FE2} - Library - Properties DDD.Core.Infrastructure.Serialization - DDD.Core.Newtonsoft - v4.7.2 - 512 - true - + net472 + false + bin\$(Configuration)\ - true full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - Properties\CommonAssemblyInfo.cs - - - + - - {596a8700-3d18-4a62-b200-1f78a9ea4617} - DDD.Core.Abstractions - + - - 2.1.0 - - - 12.0.1 - + + - \ 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 index 4796e93..186d014 100644 --- a/Src/DDD.Core.Polly/DDD.Core.Polly.csproj +++ b/Src/DDD.Core.Polly/DDD.Core.Polly.csproj @@ -1,71 +1,30 @@ - - - + - Debug - AnyCPU - {4878A4FB-2C1E-4C06-BB42-AFE1ACE2A882} - Library - Properties DDD.Core.Infrastructure.ErrorHandling - DDD.Core.Polly - v4.7.2 - 512 - true + net472 + false + bin\$(Configuration)\ + bin\$(Configuration)\DDD.Core.Polly.xml + 1591 - true full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\DDD.Core.Polly.xml - 1591 pdbonly - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\DDD.Core.Polly.xml - 1591 - - - - Properties\CommonAssemblyInfo.cs - - - - - - + - - {2438b31a-3a39-4878-81fa-be5ae715eae5} - DDD.Core.Messages - - - {c6c3e419-b9aa-44ad-9dbf-789294687ae6} - DDD.Core - + + - - 2.1.0 - - - 7.2.1 - + + - \ No newline at end of file diff --git a/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj b/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj index 78772c6..428a71d 100644 --- a/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj +++ b/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj @@ -1,77 +1,31 @@ - - - + - Debug - AnyCPU - {3EFAACD8-CF5E-4E31-884B-6B9F87F1E495} - Library - Properties DDD.Core.Infrastructure.DependencyInjection - DDD.Core.SimpleInjector - v4.7.2 - 512 - true + net472 + false + bin\$(Configuration)\ + bin\$(Configuration)\DDD.Core.SimpleInjector.xml + 1591 - true full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\DDD.Core.SimpleInjector.xml - 1591 pdbonly - true - bin\Release\ - TRACE - prompt - 4 - 1591 - bin\Release\DDD.Core.SimpleInjector.xml - - - - 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 - + + + - - 2.1.0 - - - 4.8.1 - + + - \ No newline at end of file diff --git a/Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj b/Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj index 9855a61..232764d 100644 --- a/Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj +++ b/Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj @@ -1,76 +1,29 @@ - - - + - Debug - AnyCPU - {62B652DE-4CC9-4B85-8AE9-D66CEEA358DD} - Library - Properties DDD.Core.Infrastructure.Testing - DDD.Core.Xunit - v4.7.2 - 512 - - - + net472 + false + bin\$(Configuration)\ + bin\$(Configuration)\DDD.Core.Xunit.xml + 1591 - 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 - - - - - Properties\CommonAssemblyInfo.cs - - - - - + - - {c6c3e419-b9aa-44ad-9dbf-789294687ae6} - DDD.Core - + - - 2.1.0 - - - 2.4.1 - + + - - \ No newline at end of file diff --git a/Src/DDD.Core/DDD.Core.csproj b/Src/DDD.Core/DDD.Core.csproj index 0551aec..6d762b0 100644 --- a/Src/DDD.Core/DDD.Core.csproj +++ b/Src/DDD.Core/DDD.Core.csproj @@ -1,156 +1,29 @@ - - - + - Debug - AnyCPU - {C6C3E419-B9AA-44AD-9DBF-789294687AE6} - Library - Properties - DDD.Core - DDD.Core - v4.7.2 - 512 - + net472 + false + bin\$(Configuration)\ + bin\$(Configuration)\DDD.Core.xml + 1591 - 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 - - - - - - - Properties\CommonAssemblyInfo.cs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - {596a8700-3d18-4a62-b200-1f78a9ea4617} - DDD.Core.Abstractions - - - {2438b31a-3a39-4878-81fa-be5ae715eae5} - DDD.Core.Messages - + + - - 2.1.0 - - - 3.1.0 - + + - - \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery.Messages/DDD.HealthcareDelivery.Messages.csproj b/Src/DDD.HealthcareDelivery.Messages/DDD.HealthcareDelivery.Messages.csproj index 570a81f..3221dea 100644 --- a/Src/DDD.HealthcareDelivery.Messages/DDD.HealthcareDelivery.Messages.csproj +++ b/Src/DDD.HealthcareDelivery.Messages/DDD.HealthcareDelivery.Messages.csproj @@ -1,74 +1,27 @@ - - - + - Debug - AnyCPU - {B8BB212C-8AFC-4258-A023-EB1F6937F53D} - Library - Properties DDD.HealthcareDelivery - DDD.HealthcareDelivery.Messages - v4.7.2 - 512 - true + net472 + false + bin\$(Configuration)\ - true full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - Properties\CommonAssemblyInfo.cs - - - - - - - - - - - - - - - - + - - {40a849c5-c8d7-4f76-856a-138aed73a6c3} - DDD.Common.Messages - - - {2438b31a-3a39-4878-81fa-be5ae715eae5} - DDD.Core.Messages - + + - - 2.1.0 - + - \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj b/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj index 8eded95..c259a12 100644 --- a/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj +++ b/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj @@ -1,192 +1,56 @@ - - - + - Debug - AnyCPU - {5B8FFFD3-9A1C-4620-9DB3-CD76CD9E79BF} - Library - Properties - DDD.HealthcareDelivery - DDD.HealthcareDelivery - v4.7.2 - 512 - + net472 + false + bin\$(Configuration)\ + bin\$(Configuration)\DDD.HealthcareDelivery.xml + 1591 - 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 - 1591 - - - - - - - - Properties\CommonAssemblyInfo.cs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + SqlScripts.resx True True - - - - - - - - - {40A849C5-C8D7-4F76-856A-138AED73A6C3} - DDD.Common.Messages - - - {7466b831-1642-4b2b-9ea3-1c9299596c2a} - DDD.Common.NHibernate - - - {0b70b4fd-f5a0-4a6c-a3fd-90031e08c1c2} - DDD.Common - - - {701da58b-ae36-429f-8621-64109b8d29d7} - DDD.Core.Dapper - - - {5e3745fc-ca80-4d0f-8a25-20ee0f9cf163} - DDD.Core.FluentValidation - - - {2438B31A-3A39-4878-81FA-BE5AE715EAE5} - DDD.Core.Messages - - - {d4fc7e1b-cae8-40f9-9faf-0029d91cc6c4} - DDD.Core.NHibernate - - - {c6c3e419-b9aa-44ad-9dbf-789294687ae6} - DDD.Core - - - {596a8700-3d18-4a62-b200-1f78a9ea4617} - DDD.Core.Abstractions - - - {b8bb212c-8afc-4258-a023-eb1f6937f53d} - DDD.HealthcareDelivery.Messages - + + + + + + + + + + - - 2.1.0 - - - 1.60.1 - - - 8.1.3 - - - 5.2.5 - - - 18.3.0 - - - 1.5.0 - - - 4.5.0 - + + + + + + + - + ResXFileCodeGenerator SqlScripts.Designer.cs Designer @@ -195,12 +59,4 @@ - - \ 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 fa29e9a..098f9e6 100644 --- a/Test/DDD.Common.UnitTests/DDD.Common.UnitTests.csproj +++ b/Test/DDD.Common.UnitTests/DDD.Common.UnitTests.csproj @@ -1,92 +1,33 @@ - - - + - Debug - AnyCPU - {25F6B88A-4EFA-4516-BB7A-34ED68548636} - Library - Properties DDD.Common - DDD.Common.UnitTests - v4.7.2 - 512 - - - + net472 + DDD.Common.UnitTests + DDD.Common.UnitTests + Copyright © 2017 + bin\$(Configuration)\ - true full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - + + - - - - - - - - {0b70b4fd-f5a0-4a6c-a3fd-90031e08c1c2} - DDD.Common - - - {c6c3e419-b9aa-44ad-9dbf-789294687ae6} - DDD.Core - - - - - 5.6.0 - - - 4.5.0 - - - 2.4.1 - - - 2.4.1 - runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all - - \ No newline at end of file diff --git a/Test/DDD.Common.UnitTests/Properties/AssemblyInfo.cs b/Test/DDD.Common.UnitTests/Properties/AssemblyInfo.cs index 1c423aa..40057ec 100644 --- a/Test/DDD.Common.UnitTests/Properties/AssemblyInfo.cs +++ b/Test/DDD.Common.UnitTests/Properties/AssemblyInfo.cs @@ -3,18 +3,6 @@ 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.Common.UnitTests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("DDD.Common.UnitTests")] -[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 +10,4 @@ // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("25f6b88a-4efa-4516-bb7a-34ed68548636")] - -// 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", "Unit")] 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 81bc479..b5c8190 100644 --- a/Test/DDD.Core.Abstractions.UnitTests/DDD.Core.Abstractions.UnitTests.csproj +++ b/Test/DDD.Core.Abstractions.UnitTests/DDD.Core.Abstractions.UnitTests.csproj @@ -1,96 +1,35 @@ - - - + - Debug - AnyCPU - {9CC062C7-73EA-49B4-B694-31A5F48FD09D} - Library - Properties DDD - DDD.Core.Abstractions.UnitTests - v4.7.2 - 512 - - - + net472 + DDD.Core.Abstractions.UnitTests + DDD.Core.Abstractions.UnitTests + Copyright © 2017 + bin\$(Configuration)\ - true full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - + - - - - - - - - {596a8700-3d18-4a62-b200-1f78a9ea4617} - DDD.Core.Abstractions - - - - - 5.6.0 - - - 4.0.0 - - - 4.5.2 - - - 4.5.0 - - - 2.4.1 - - - 2.4.1 - runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all - - \ No newline at end of file diff --git a/Test/DDD.Core.Abstractions.UnitTests/Properties/AssemblyInfo.cs b/Test/DDD.Core.Abstractions.UnitTests/Properties/AssemblyInfo.cs index 8c569fc..dfaf3c5 100644 --- a/Test/DDD.Core.Abstractions.UnitTests/Properties/AssemblyInfo.cs +++ b/Test/DDD.Core.Abstractions.UnitTests/Properties/AssemblyInfo.cs @@ -2,18 +2,6 @@ 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.Core.Abstractions.UnitTests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("DDD.Core.Abstractions.UnitTests")] -[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. @@ -21,17 +9,4 @@ // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("9cc062c7-73ea-49b4-b694-31a5f48fd09d")] - -// 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", "Unit")] \ No newline at end of file diff --git a/Test/DDD.Core.UnitTests/DDD.Core.UnitTests.csproj b/Test/DDD.Core.UnitTests/DDD.Core.UnitTests.csproj index 0c4e881..78da70b 100644 --- a/Test/DDD.Core.UnitTests/DDD.Core.UnitTests.csproj +++ b/Test/DDD.Core.UnitTests/DDD.Core.UnitTests.csproj @@ -1,131 +1,38 @@ - - - + - Debug - AnyCPU - {63505329-BD97-4C75-B7D7-77FB42670E79} - Library - Properties DDD.Core - DDD.Core.UnitTests - v4.7.2 - 512 - - - + net472 + DDD.Core.UnitTests + DDD.Core.UnitTests + Copyright © 2017 + bin\$(Configuration)\ - true full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - - - {596a8700-3d18-4a62-b200-1f78a9ea4617} - DDD.Core.Abstractions - - - {2438b31a-3a39-4878-81fa-be5ae715eae5} - DDD.Core.Messages - - - {c6c3e419-b9aa-44ad-9dbf-789294687ae6} - DDD.Core - - - - - 5.6.0 - - - 4.0.0 - - - 18.3.0 - - - 4.5.2 - - - 4.5.0 - - - 2.4.1 - - - 2.4.1 - runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all - - \ No newline at end of file diff --git a/Test/DDD.Core.UnitTests/Properties/AssemblyInfo.cs b/Test/DDD.Core.UnitTests/Properties/AssemblyInfo.cs index 536081e..70c5526 100644 --- a/Test/DDD.Core.UnitTests/Properties/AssemblyInfo.cs +++ b/Test/DDD.Core.UnitTests/Properties/AssemblyInfo.cs @@ -2,18 +2,6 @@ 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.Core.UnitTests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("DDD.Core.UnitTests")] -[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. @@ -21,17 +9,4 @@ // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("63505329-bd97-4c75-b7d7-77fb42670e79")] - -// 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", "Unit")] \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj b/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj index ac72d32..60d1e43 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj @@ -1,143 +1,57 @@ - - - + - Debug - AnyCPU - {B53007C7-B314-40DF-B6E7-6C6576A5611C} - Library - Properties DDD.HealthcareDelivery - DDD.HealthcareDelivery.IntegrationTests - v4.7.2 - 512 - - - + net472 + DDD.HealthcareDelivery.IntegrationTests + DDD.HealthcareDelivery.IntegrationTests + Copyright © 2017 + bin\$(Configuration)\ - true full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - - - + True True OracleScripts.resx - - - - - - - + True True SqlServerScripts.resx - + + + + + + + + + + - - {40A849C5-C8D7-4F76-856A-138AED73A6C3} - DDD.Common.Messages - - - {0B70B4FD-F5A0-4A6C-A3FD-90031E08C1C2} - DDD.Common - - - {2438B31A-3A39-4878-81FA-BE5AE715EAE5} - DDD.Core.Messages - - - {d4fc7e1b-cae8-40f9-9faf-0029d91cc6c4} - DDD.Core.NHibernate - - - {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 - @@ -153,33 +67,13 @@ - - 5.6.0 - - - 5.2.5 - - - 18.3.0 - - - 4.5.0 - - - 2.4.1 - - - 2.4.1 - runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all - - \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Properties/AssemblyInfo.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Properties/AssemblyInfo.cs index 869e06e..7cf896c 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Properties/AssemblyInfo.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Properties/AssemblyInfo.cs @@ -3,18 +3,6 @@ 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 +10,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.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj b/Test/DDD.HealthcareDelivery.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj index 39ddbfa..55f681d 100644 --- a/Test/DDD.HealthcareDelivery.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj +++ b/Test/DDD.HealthcareDelivery.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj @@ -1,122 +1,41 @@ - - - + - Debug - AnyCPU - {CA376D7C-2A71-4518-B297-4C4A08DBF19D} - Library - Properties DDD.HealthcareDelivery - DDD.HealthcareDelivery.UnitTests - v4.7.2 - 512 - - - + net472 + DDD.HealthcareDelivery.UnitTests + DDD.HealthcareDelivery.UnitTests + Copyright © 2017 + bin\$(Configuration)\ - true full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - + + + + + + + + - - {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 - - - - - - - - - - - 5.6.0 - - - 8.1.3 - - - 4.5.0 - - - 2.4.1 - - - 2.4.1 - runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all - - \ 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..9319e4c 100644 --- a/Test/DDD.HealthcareDelivery.UnitTests/Properties/AssemblyInfo.cs +++ b/Test/DDD.HealthcareDelivery.UnitTests/Properties/AssemblyInfo.cs @@ -3,18 +3,6 @@ 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.UnitTests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("DDD.HealthcareDelivery.UnitTests")] -[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 +10,4 @@ // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("ca376d7c-2a71-4518-b297-4c4a08dbf19d")] - -// 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", "Unit")] From f19bffa4e7d7c09b50e86732cb5ca49c94bcee6c Mon Sep 17 00:00:00 2001 From: draphyz Date: Sat, 15 Aug 2020 22:23:21 +0200 Subject: [PATCH 031/111] Add support for .NET Core --- .../DDD.Common.Messages.csproj | 16 ++-- .../DDD.Common.NHibernate.csproj | 18 +--- Src/DDD.Common/DDD.Common.csproj | 22 ++--- .../DDD.Core.Abstractions.csproj | 22 ++--- Src/DDD.Core.Dapper/DDD.Core.Dapper.csproj | 27 +++--- .../DDD.Core.FluentValidation.csproj | 35 ++++---- .../DDD.Core.Messages.csproj | 16 ++-- Src/DDD.Core.NHibernate/CompositeUserType.cs | 4 +- .../DDD.Core.NHibernate.csproj | 21 ++--- .../NHibernateRepository.cs | 6 +- ...NHibernateRepositoryExceptionTranslator.cs | 8 +- .../OracleStoredEventMapping.cs | 2 +- .../SqlServerStoredEventMapping.cs | 2 +- Src/DDD.Core.NHibernate/StoredEventMapping.cs | 18 +--- .../DDD.Core.NServiceBus.csproj | 29 ++++--- .../DDD.Core.Newtonsoft.csproj | 27 +++--- Src/DDD.Core.Polly/DDD.Core.Polly.csproj | 26 +++--- .../DDD.Core.SimpleInjector.csproj | 26 +++--- Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj | 26 +++--- Src/DDD.Core.Xunit/DbFixture.cs | 6 +- Src/DDD.Core/DDD.Core.csproj | 26 +++--- .../Data/DbConnectionFactory.cs | 7 +- .../Data/IDbConnectionExtensions.cs | 1 + ...rExtensions.cs => SqlServerErrorHelper.cs} | 9 +- .../SqlServerToCommandExceptionTranslator.cs | 13 ++- .../SqlServerToQueryExceptionTranslator.cs | 13 ++- ...qlServerToRepositoryExceptionTranslator.cs | 13 ++- .../DDD.HealthcareDelivery.Messages.csproj | 20 ++--- Src/DDD.HealthcareDelivery/App.config | 15 ---- .../DDD.HealthcareDelivery.csproj | 29 ++----- ...nOracleHealthcareDeliveryConfiguration.cs} | 8 +- ...lServerHealthcareDeliveryConfiguration.cs} | 8 +- ....cs => HealthcareDeliveryConfiguration.cs} | 13 +-- ...> IHealthcareDeliveryConnectionFactory.cs} | 2 +- ... OracleHealthcareDeliveryConfiguration.cs} | 13 ++- .../OracleStoredEventMapping.cs | 15 ---- ...OraclePharmaceuticalPrescriptionMapping.cs | 13 --- .../OraclePrescribedMedicationMapping.cs | 2 +- .../OraclePrescriptionMapping.cs | 2 +- .../PharmaceuticalPrescriptionMapping.cs | 19 +---- ...rmaceuticalPrescriptionsByPatientFinder.cs | 2 +- .../PrescribedMedicationMapping.cs | 27 ++---- ...escribedMedicationsByPrescriptionFinder.cs | 2 +- .../PrescriptionIdentifierGenerator.cs | 2 +- .../Prescriptions/PrescriptionMapping.cs | 84 ++++++++----------- ...ServerPharmaceuticalPrescriptionMapping.cs | 13 --- .../SqlServerPrescribedMedicationMapping.cs | 2 +- .../SqlServerPrescriptionMapping.cs | 2 +- .../Infrastructure/SqlScripts.Designer.cs | 14 ++-- .../SqlServerHealthcareConfiguration.cs | 37 -------- ...qlServerHealthcareDeliveryConfiguration.cs | 35 ++++++++ .../SqlServerStoredEventMapping.cs | 15 ---- .../DDD.Common.UnitTests.csproj | 33 ++++---- Test/DDD.Common.UnitTests/app.config | 11 --- .../DDD.Core.Abstractions.UnitTests.csproj | 42 +++++----- .../app.config | 19 ----- .../DDD.Core.UnitTests.csproj | 43 +++++----- .../Data/IDbConnectionExtensionsTests.cs | 44 ---------- Test/DDD.Core.UnitTests/app.config | 40 --------- .../App.config | 43 ++-------- ...ePharmaceuticalPrescriptionCreatorTests.cs | 20 ----- ...ePharmaceuticalPrescriptionRevokerTests.cs | 20 ----- .../PharmaceuticalPrescriptionCreatorTests.cs | 26 +++--- .../PharmaceuticalPrescriptionRevokerTests.cs | 23 +++-- ...rPharmaceuticalPrescriptionCreatorTests.cs | 22 ----- ...rPharmaceuticalPrescriptionRevokerTests.cs | 20 ----- ...HealthcareDelivery.IntegrationTests.csproj | 40 ++++----- ...lgianOracleHealthcareConfigurationTests.cs | 25 ------ ...cleHealthcareDeliveryConfigurationTests.cs | 25 ++++++ ...anSqlServerHealthcareConfigurationTests.cs | 25 ------ ...verHealthcareDeliveryConfigurationTests.cs | 25 ++++++ ...> HealthcareDeliveryConfigurationTests.cs} | 40 ++++----- .../Infrastructure/IPersistenceFixture.cs | 21 +++++ .../{ => Infrastructure}/OracleCollection.cs | 2 +- .../Infrastructure/OracleConnectionFactory.cs | 38 +++++++++ .../{ => Infrastructure}/OracleFixture.cs | 32 ++++++- ...uticalPrescriptionsByPatientFinderTests.cs | 3 +- ...bedMedicationsByPrescriptionFinderTests.cs | 3 +- .../SqlServerCollection.cs | 2 +- .../SqlServerConnectionFactory.cs | 37 ++++++++ .../Infrastructure/SqlServerFixture.cs | 66 +++++++++++++++ .../OracleConnectionFactory.cs | 25 ------ .../SqlServerConnectionFactory.cs | 25 ------ .../SqlServerFixture.cs | 40 --------- .../DDD.HealthcareDelivery.UnitTests.csproj | 43 +++++----- .../app.config | 11 --- 86 files changed, 737 insertions(+), 1030 deletions(-) rename Src/DDD.Core/Infrastructure/Data/{SqlErrorExtensions.cs => SqlServerErrorHelper.cs} (95%) delete mode 100644 Src/DDD.HealthcareDelivery/App.config rename Src/DDD.HealthcareDelivery/Infrastructure/{BelgianOracleHealthcareConfiguration.cs => BelgianOracleHealthcareDeliveryConfiguration.cs} (72%) rename Src/DDD.HealthcareDelivery/Infrastructure/{BelgianSqlServerHealthcareConfiguration.cs => BelgianSqlServerHealthcareDeliveryConfiguration.cs} (72%) rename Src/DDD.HealthcareDelivery/Infrastructure/{HealthcareConfiguration.cs => HealthcareDeliveryConfiguration.cs} (69%) rename Src/DDD.HealthcareDelivery/Infrastructure/{IHealthcareConnectionFactory.cs => IHealthcareDeliveryConnectionFactory.cs} (72%) rename Src/DDD.HealthcareDelivery/Infrastructure/{OracleHealthcareConfiguration.cs => OracleHealthcareDeliveryConfiguration.cs} (58%) delete mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/OracleStoredEventMapping.cs delete mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePharmaceuticalPrescriptionMapping.cs delete mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPharmaceuticalPrescriptionMapping.cs delete mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareConfiguration.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareDeliveryConfiguration.cs delete mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/SqlServerStoredEventMapping.cs delete mode 100644 Test/DDD.Common.UnitTests/app.config delete mode 100644 Test/DDD.Core.Abstractions.UnitTests/app.config delete mode 100644 Test/DDD.Core.UnitTests/Infrastructure/Data/IDbConnectionExtensionsTests.cs delete mode 100644 Test/DDD.Core.UnitTests/app.config delete mode 100644 Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianOracleHealthcareConfigurationTests.cs create mode 100644 Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianOracleHealthcareDeliveryConfigurationTests.cs delete mode 100644 Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareConfigurationTests.cs create mode 100644 Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareDeliveryConfigurationTests.cs rename Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/{HealthcareConfigurationTests.cs => HealthcareDeliveryConfigurationTests.cs} (81%) create mode 100644 Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/IPersistenceFixture.cs rename Test/DDD.HealthcareDelivery.IntegrationTests/{ => Infrastructure}/OracleCollection.cs (74%) create mode 100644 Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleConnectionFactory.cs rename Test/DDD.HealthcareDelivery.IntegrationTests/{ => Infrastructure}/OracleFixture.cs (51%) rename Test/DDD.HealthcareDelivery.IntegrationTests/{ => Infrastructure}/SqlServerCollection.cs (75%) create mode 100644 Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerConnectionFactory.cs create mode 100644 Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerFixture.cs delete mode 100644 Test/DDD.HealthcareDelivery.IntegrationTests/OracleConnectionFactory.cs delete mode 100644 Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerConnectionFactory.cs delete mode 100644 Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerFixture.cs delete mode 100644 Test/DDD.HealthcareDelivery.UnitTests/app.config diff --git a/Src/DDD.Common.Messages/DDD.Common.Messages.csproj b/Src/DDD.Common.Messages/DDD.Common.Messages.csproj index c0df122..a54e40e 100644 --- a/Src/DDD.Common.Messages/DDD.Common.Messages.csproj +++ b/Src/DDD.Common.Messages/DDD.Common.Messages.csproj @@ -1,20 +1,16 @@  + net472;netstandard2.1 + Library DDD.Common - net472 false - bin\$(Configuration)\ - - - full - - - pdbonly - + + Properties\CommonAssemblyInfo.cs + - + \ 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 index 5058a97..3ff57db 100644 --- a/Src/DDD.Common.NHibernate/DDD.Common.NHibernate.csproj +++ b/Src/DDD.Common.NHibernate/DDD.Common.NHibernate.csproj @@ -1,23 +1,10 @@  DDD.Common.Infrastructure.Data - net472 + net472;netstandard2.1 false - 7.1 bin\$(Configuration)\ - - full - - - pdbonly - - - - - - - @@ -26,6 +13,7 @@ - + + \ No newline at end of file diff --git a/Src/DDD.Common/DDD.Common.csproj b/Src/DDD.Common/DDD.Common.csproj index 2cd6d09..45d3d6e 100644 --- a/Src/DDD.Common/DDD.Common.csproj +++ b/Src/DDD.Common/DDD.Common.csproj @@ -1,28 +1,28 @@  - net472 + net472;netstandard2.1 + Library false - bin\$(Configuration)\ - bin\$(Configuration)\DDD.Common.xml - 1591 - full + bin\Debug\DDD.Common.xml + 1591 - pdbonly + bin\Release\DDD.Common.xml + 1591 - - - - + + Properties\CommonAssemblyInfo.cs + - + + \ No newline at end of file diff --git a/Src/DDD.Core.Abstractions/DDD.Core.Abstractions.csproj b/Src/DDD.Core.Abstractions/DDD.Core.Abstractions.csproj index 4bad687..cd0388d 100644 --- a/Src/DDD.Core.Abstractions/DDD.Core.Abstractions.csproj +++ b/Src/DDD.Core.Abstractions/DDD.Core.Abstractions.csproj @@ -1,25 +1,25 @@  + net472;netstandard2.1 + Library DDD - net472 false - bin\$(Configuration)\ - bin\$(Configuration)\DDD.Core.Abstractions.xml - 1591 - full + bin\Debug\DDD.Core.Abstractions.xml + 1591 - pdbonly + bin\Release\DDD.Core.Abstractions.xml + 1591 - - - - + + Properties\CommonAssemblyInfo.cs + - + + \ 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 9f0d765..d3f29c4 100644 --- a/Src/DDD.Core.Dapper/DDD.Core.Dapper.csproj +++ b/Src/DDD.Core.Dapper/DDD.Core.Dapper.csproj @@ -1,31 +1,32 @@  + net472;netstandard2.1 + Library DDD.Core.Infrastructure.Data - net472 false - bin\$(Configuration)\ - bin\$(Configuration)\DDD.Core.Dapper.xml - 1591 - full + bin\Debug\DDD.Core.Dapper.xml + 1591 - pdbonly + bin\Release\DDD.Core.Dapper.xml + 1591 - - - - - + + Properties\CommonAssemblyInfo.cs + - - + + + 2.0.35 + + \ 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 8c5b089..3267ac6 100644 --- a/Src/DDD.Core.FluentValidation/DDD.Core.FluentValidation.csproj +++ b/Src/DDD.Core.FluentValidation/DDD.Core.FluentValidation.csproj @@ -1,26 +1,22 @@  + net472;netstandard2.1 + Library DDD.Core.Infrastructure.Validation - net472 false - bin\$(Configuration)\ - bin\$(Configuration)\DDD.Core.FluentValidation.xml - 1591 - full + bin\Debug\DDD.Core.FluentValidation.xml + 1591 - pdbonly + bin\Release\DDD.Core.FluentValidation.xml + 1591 - - - - - - - + + Properties\CommonAssemblyInfo.cs + @@ -28,8 +24,15 @@ - - - + + + 9.1.2 + + + + 4.5.0 + + + \ 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 557ceb3..f3f6e5e 100644 --- a/Src/DDD.Core.Messages/DDD.Core.Messages.csproj +++ b/Src/DDD.Core.Messages/DDD.Core.Messages.csproj @@ -1,20 +1,16 @@  + net472;netstandard2.1 + Library DDD.Core - net472 false - bin\$(Configuration)\ - - - full - - - pdbonly - + + Properties\CommonAssemblyInfo.cs + - + \ No newline at end of file diff --git a/Src/DDD.Core.NHibernate/CompositeUserType.cs b/Src/DDD.Core.NHibernate/CompositeUserType.cs index 616b462..e71c531 100644 --- a/Src/DDD.Core.NHibernate/CompositeUserType.cs +++ b/Src/DDD.Core.NHibernate/CompositeUserType.cs @@ -25,9 +25,9 @@ public abstract class CompositeUserType : ICompositeUserType public const string NotNullDiscriminatorValue = "not null"; public const string NullDiscriminatorValue = "null"; private readonly Type returnedClass; - private Dictionary classes; + private readonly Dictionary classes; private bool isMutable; - private List properties; + private readonly List properties; private string[] propertyNames; private IType[] propertyTypes; diff --git a/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj b/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj index 2595c55..0b3dbca 100644 --- a/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj +++ b/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj @@ -1,23 +1,11 @@  + net472;netstandard2.1 + Library DDD.Core.Infrastructure.Data - net472 false - 7.2 bin\$(Configuration)\ - - full - - - pdbonly - - - - - - - @@ -27,7 +15,8 @@ - - + + + \ No newline at end of file diff --git a/Src/DDD.Core.NHibernate/NHibernateRepository.cs b/Src/DDD.Core.NHibernate/NHibernateRepository.cs index 4da9444..67ddcb1 100644 --- a/Src/DDD.Core.NHibernate/NHibernateRepository.cs +++ b/Src/DDD.Core.NHibernate/NHibernateRepository.cs @@ -2,6 +2,8 @@ using NHibernate; using System; using System.Collections.Generic; +using System.Data; +using System.Data.Common; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -20,8 +22,8 @@ public class NHibernateRepository : IAsyncRepository eventTranslator; + private readonly IObjectTranslator exceptionTranslator = NHibernateRepositoryExceptionTranslator.Default; private readonly ISession session; - private readonly IObjectTranslator exceptionTranslator = NHibernateRepositoryExceptionTranslator.Default; #endregion Fields @@ -60,7 +62,7 @@ public async Task SaveAsync(TDomainEntity aggregate) try { await this.session.SaveOrUpdateAsync(aggregate); - foreach(var @event in events) + foreach (var @event in events) await this.session.SaveAsync(@event); } catch (HibernateException ex) diff --git a/Src/DDD.Core.NHibernate/NHibernateRepositoryExceptionTranslator.cs b/Src/DDD.Core.NHibernate/NHibernateRepositoryExceptionTranslator.cs index 26c2fe6..6461a9d 100644 --- a/Src/DDD.Core.NHibernate/NHibernateRepositoryExceptionTranslator.cs +++ b/Src/DDD.Core.NHibernate/NHibernateRepositoryExceptionTranslator.cs @@ -10,12 +10,12 @@ namespace DDD.Core.Infrastructure.Data using Mapping; using Core.Domain; - internal class NHibernateRepositoryExceptionTranslator : IObjectTranslator + internal class NHibernateRepositoryExceptionTranslator : IObjectTranslator { #region Fields - public static readonly IObjectTranslator Default = new NHibernateRepositoryExceptionTranslator(); + public static readonly IObjectTranslator Default = new NHibernateRepositoryExceptionTranslator(); private readonly IObjectTranslator dbExceptionTranslator = DbToRepositoryExceptionTranslator.Default; @@ -23,7 +23,7 @@ internal class NHibernateRepositoryExceptionTranslator : IObjectTranslator options = null) + public RepositoryException Translate(Exception exception, IDictionary options = null) { Condition.Requires(exception, nameof(exception)).IsNotNull(); Condition.Requires(options, nameof(options)) @@ -32,6 +32,8 @@ public RepositoryException Translate(HibernateException exception, IDictionary e.Id, m => m.Column(m1 => m1.SqlType("number(19,0)"))); diff --git a/Src/DDD.Core.NHibernate/SqlServerStoredEventMapping.cs b/Src/DDD.Core.NHibernate/SqlServerStoredEventMapping.cs index 96a8fc0..9092183 100644 --- a/Src/DDD.Core.NHibernate/SqlServerStoredEventMapping.cs +++ b/Src/DDD.Core.NHibernate/SqlServerStoredEventMapping.cs @@ -4,7 +4,7 @@ public class SqlServerStoredEventMapping : StoredEventMapping { #region Constructors - public SqlServerStoredEventMapping(bool useUpperCase) : base(useUpperCase) + public SqlServerStoredEventMapping() { // Fields this.Property(e => e.Body, m => m.Column(m1 => m1.SqlType("xml"))); diff --git a/Src/DDD.Core.NHibernate/StoredEventMapping.cs b/Src/DDD.Core.NHibernate/StoredEventMapping.cs index 99a2cc1..a4d42d5 100644 --- a/Src/DDD.Core.NHibernate/StoredEventMapping.cs +++ b/Src/DDD.Core.NHibernate/StoredEventMapping.cs @@ -7,22 +7,16 @@ namespace DDD.Core.Infrastructure.Data public abstract class StoredEventMapping : ClassMapping { - #region Fields - - private readonly bool useUpperCase; - - #endregion Fields #region Constructors - protected StoredEventMapping(bool useUpperCase) + protected StoredEventMapping() { - this.useUpperCase = useUpperCase; this.Lazy(false); // Table - this.Table(ToCasingConvention("Event")); + this.Table("Event"); // Keys - this.Id(e => e.Id, m1 => m1.Column(ToCasingConvention("EventId"))); + this.Id(e => e.Id, m1 => m1.Column("EventId")); // Fields this.Property(e => e.EventType, m => { @@ -49,11 +43,5 @@ protected StoredEventMapping(bool useUpperCase) #endregion Constructors - #region Methods - - protected string ToCasingConvention(string name) => this.useUpperCase ? name.ToUpperInvariant() : name; - - #endregion Methods - } } diff --git a/Src/DDD.Core.NServiceBus/DDD.Core.NServiceBus.csproj b/Src/DDD.Core.NServiceBus/DDD.Core.NServiceBus.csproj index a625a71..e118ff5 100644 --- a/Src/DDD.Core.NServiceBus/DDD.Core.NServiceBus.csproj +++ b/Src/DDD.Core.NServiceBus/DDD.Core.NServiceBus.csproj @@ -1,26 +1,22 @@  + net472;netstandard2.1 + Library DDD.Core.Infrastructure.Messaging - net472 false - bin\$(Configuration)\ - bin\$(Configuration)\DDD.Core.NServiceBus.xml - 1591 - full + 1591 + bin\Debug\DDD.Core.NServiceBus.xml - pdbonly + bin\Release\DDD.Core.NServiceBus.xml + 1591 - - - - - - - + + Properties\CommonAssemblyInfo.cs + @@ -28,7 +24,10 @@ - - + + + + 7.3.0 + \ No newline at end of file diff --git a/Src/DDD.Core.Newtonsoft/DDD.Core.Newtonsoft.csproj b/Src/DDD.Core.Newtonsoft/DDD.Core.Newtonsoft.csproj index 03bb9a3..d6b47f9 100644 --- a/Src/DDD.Core.Newtonsoft/DDD.Core.Newtonsoft.csproj +++ b/Src/DDD.Core.Newtonsoft/DDD.Core.Newtonsoft.csproj @@ -1,29 +1,24 @@  + net472;netstandard2.1 + Library DDD.Core.Infrastructure.Serialization - net472 false - bin\$(Configuration)\ - - full - - - pdbonly - - - - - - - + + Properties\CommonAssemblyInfo.cs + - - + + + + 12.0.3 + + \ 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 index 186d014..97d7a58 100644 --- a/Src/DDD.Core.Polly/DDD.Core.Polly.csproj +++ b/Src/DDD.Core.Polly/DDD.Core.Polly.csproj @@ -1,30 +1,32 @@  + net472;netstandard2.1 + Library DDD.Core.Infrastructure.ErrorHandling - net472 false - bin\$(Configuration)\ - bin\$(Configuration)\DDD.Core.Polly.xml - 1591 - full + bin\Debug\DDD.Core.Polly.xml + 1591 - pdbonly + bin\Release\DDD.Core.Polly.xml + 1591 - - - - + + Properties\CommonAssemblyInfo.cs + - - + + + + 7.2.1 + \ No newline at end of file diff --git a/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj b/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj index 428a71d..37f0971 100644 --- a/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj +++ b/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj @@ -1,23 +1,22 @@  + net472;netstandard2.1 + Library DDD.Core.Infrastructure.DependencyInjection - net472 false - bin\$(Configuration)\ - bin\$(Configuration)\DDD.Core.SimpleInjector.xml - 1591 - full + bin\Debug\DDD.Core.SimpleInjector.xml + 1591 - pdbonly + 1591 + bin\Release\DDD.Core.SimpleInjector.xml - - - - + + Properties\CommonAssemblyInfo.cs + @@ -25,7 +24,10 @@ - - + + + + 5.0.3 + \ No newline at end of file diff --git a/Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj b/Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj index 232764d..483c58a 100644 --- a/Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj +++ b/Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj @@ -1,29 +1,31 @@  + net472;netstandard2.1 + Library DDD.Core.Infrastructure.Testing - net472 false - bin\$(Configuration)\ - bin\$(Configuration)\DDD.Core.Xunit.xml - 1591 - full + bin\Debug\DDD.Core.Xunit.xml + 1591 - pdbonly + bin\Release\DDD.Core.Xunit.xml + 1591 - - - - + + Properties\CommonAssemblyInfo.cs + - - + + + + 2.4.1 + \ No newline at end of file diff --git a/Src/DDD.Core.Xunit/DbFixture.cs b/Src/DDD.Core.Xunit/DbFixture.cs index 9b697c9..57ecb5e 100644 --- a/Src/DDD.Core.Xunit/DbFixture.cs +++ b/Src/DDD.Core.Xunit/DbFixture.cs @@ -28,6 +28,7 @@ protected DbFixture(TConnectionFactory connectionFactory, string resourceFile) var resourceAssembly = Assembly.GetCallingAssembly(); var resourceType = resourceAssembly.GetTypes().Single(t => t.Name == resourceFile); this.resourceManager = new ResourceManager(resourceType); + this.RegisterDbProviderFactory(); this.CreateDatabase(); } @@ -61,7 +62,10 @@ public int[] ExecuteScriptFromResources(string scriptName) protected abstract int[] ExecuteScript(string script, IDbConnection connection); - #endregion Methods + protected virtual void RegisterDbProviderFactory() + { + } + #endregion Methods } } \ No newline at end of file diff --git a/Src/DDD.Core/DDD.Core.csproj b/Src/DDD.Core/DDD.Core.csproj index 6d762b0..54cf39c 100644 --- a/Src/DDD.Core/DDD.Core.csproj +++ b/Src/DDD.Core/DDD.Core.csproj @@ -1,29 +1,31 @@  - net472 + net472;netstandard2.1 + Library false - bin\$(Configuration)\ - bin\$(Configuration)\DDD.Core.xml - 1591 - full + bin\Debug\DDD.Core.xml + 1591 - pdbonly + bin\Release\DDD.Core.xml + 1591 - - - - + + Properties\CommonAssemblyInfo.cs + - - + + + + 3.1.7 + \ No newline at end of file diff --git a/Src/DDD.Core/Infrastructure/Data/DbConnectionFactory.cs b/Src/DDD.Core/Infrastructure/Data/DbConnectionFactory.cs index 4f4eaed..07fcb07 100644 --- a/Src/DDD.Core/Infrastructure/Data/DbConnectionFactory.cs +++ b/Src/DDD.Core/Infrastructure/Data/DbConnectionFactory.cs @@ -12,7 +12,7 @@ public abstract class DbConnectionFactory : IDbConnectionFactory #region Fields private readonly string connectionString; - private readonly DbProviderFactory providerFactory; + private readonly string providerName; #endregion Fields @@ -22,7 +22,7 @@ protected DbConnectionFactory(string providerName, string connectionString) { Condition.Requires(providerName, nameof(providerName)).IsNotNullOrWhiteSpace(); Condition.Requires(connectionString, nameof(connectionString)).IsNotNullOrWhiteSpace(); - this.providerFactory = DbProviderFactories.GetFactory(providerName); + this.providerName = providerName; this.connectionString = connectionString; } @@ -32,7 +32,8 @@ protected DbConnectionFactory(string providerName, string connectionString) public DbConnection CreateConnection() { - var connection = this.providerFactory.CreateConnection(); + var providerFactory = DbProviderFactories.GetFactory(providerName); + var connection = providerFactory.CreateConnection(); connection.ConnectionString = this.connectionString; return connection; } diff --git a/Src/DDD.Core/Infrastructure/Data/IDbConnectionExtensions.cs b/Src/DDD.Core/Infrastructure/Data/IDbConnectionExtensions.cs index 15ffd25..291bbba 100644 --- a/Src/DDD.Core/Infrastructure/Data/IDbConnectionExtensions.cs +++ b/Src/DDD.Core/Infrastructure/Data/IDbConnectionExtensions.cs @@ -15,6 +15,7 @@ public static IDbStandardExpressions Expressions(this IDbConnection connection) switch (connection.GetType().ToString()) { case "System.Data.SqlClient.SqlConnection": + case "Microsoft.Data.SqlClient.SqlConnection": return DbStandardExpressions.SqlServer2012; case "Oracle.ManagedDataAccess.Client.OracleConnection": diff --git a/Src/DDD.Core/Infrastructure/Data/SqlErrorExtensions.cs b/Src/DDD.Core/Infrastructure/Data/SqlServerErrorHelper.cs similarity index 95% rename from Src/DDD.Core/Infrastructure/Data/SqlErrorExtensions.cs rename to Src/DDD.Core/Infrastructure/Data/SqlServerErrorHelper.cs index b40f615..5a16f90 100644 --- a/Src/DDD.Core/Infrastructure/Data/SqlErrorExtensions.cs +++ b/Src/DDD.Core/Infrastructure/Data/SqlServerErrorHelper.cs @@ -1,14 +1,13 @@ using Conditions; -using System.Data.SqlClient; namespace DDD.Core.Infrastructure.Data { - internal static class SqlErrorExtensions + internal static class SqlServerErrorHelper { #region Methods - public static bool IsUnavailableError(this SqlError error) + public static bool IsUnavailableError(dynamic error) { Condition.Requires(error, nameof(error)).IsNotNull(); switch (error.Number) @@ -70,7 +69,7 @@ public static bool IsUnavailableError(this SqlError error) } } - public static bool IsUnauthorizedError(this SqlError error) + public static bool IsUnauthorizedError(dynamic error) { Condition.Requires(error, nameof(error)).IsNotNull(); switch (error.Number) @@ -105,7 +104,7 @@ public static bool IsUnauthorizedError(this SqlError error) } } - public static bool IsTimeoutError(this SqlError error) + public static bool IsTimeoutError(dynamic error) { Condition.Requires(error, nameof(error)).IsNotNull(); switch (error.Number) diff --git a/Src/DDD.Core/Infrastructure/Data/SqlServerToCommandExceptionTranslator.cs b/Src/DDD.Core/Infrastructure/Data/SqlServerToCommandExceptionTranslator.cs index 3327e54..92acddd 100644 --- a/Src/DDD.Core/Infrastructure/Data/SqlServerToCommandExceptionTranslator.cs +++ b/Src/DDD.Core/Infrastructure/Data/SqlServerToCommandExceptionTranslator.cs @@ -1,5 +1,4 @@ -using System.Data.SqlClient; -using System.Data.Common; +using System.Data.Common; using System.Collections.Generic; using Conditions; @@ -19,16 +18,16 @@ public CommandException Translate(DbException exception, IDictionary + net472;netstandard2.1 + Library DDD.HealthcareDelivery - net472 false - bin\$(Configuration)\ - - full - - - pdbonly - - - - - + + Properties\CommonAssemblyInfo.cs + - + + \ 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 53c41ea..0000000 --- a/Src/DDD.HealthcareDelivery/App.config +++ /dev/null @@ -1,15 +0,0 @@ - - - - -
- - - - - - - - \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj b/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj index c259a12..2da7d59 100644 --- a/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj +++ b/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj @@ -1,24 +1,11 @@  - net472 + net472;netstandard2.1 false bin\$(Configuration)\ bin\$(Configuration)\DDD.HealthcareDelivery.xml 1591 - - full - - - pdbonly - - - - - - - - @@ -40,13 +27,13 @@ - - - - - - - + + + + + + + diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/BelgianOracleHealthcareConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/BelgianOracleHealthcareDeliveryConfiguration.cs similarity index 72% rename from Src/DDD.HealthcareDelivery/Infrastructure/BelgianOracleHealthcareConfiguration.cs rename to Src/DDD.HealthcareDelivery/Infrastructure/BelgianOracleHealthcareDeliveryConfiguration.cs index 6462d6c..45fff3e 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/BelgianOracleHealthcareConfiguration.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/BelgianOracleHealthcareDeliveryConfiguration.cs @@ -8,12 +8,12 @@ namespace DDD.HealthcareDelivery.Infrastructure using Domain.Facilities; using Common.Domain; - public class BelgianOracleHealthcareConfiguration : OracleHealthcareConfiguration + public class BelgianOracleHealthcareDeliveryConfiguration : OracleHealthcareDeliveryConfiguration { #region Constructors - public BelgianOracleHealthcareConfiguration(string connectionString) : base(connectionString) + public BelgianOracleHealthcareDeliveryConfiguration(string connectionString) : base(connectionString) { } @@ -21,9 +21,9 @@ public BelgianOracleHealthcareConfiguration(string connectionString) : base(conn #region Methods - protected override void InitializeModel(ModelMapper modelMapper) + protected override void AddMappings(ModelMapper modelMapper) { - base.InitializeModel(modelMapper); + base.AddMappings(modelMapper); modelMapper.AddMapping { db.ConnectionString = connectionString; }); + var modelMapper = new ModelMapper(); + this.AddMappings(modelMapper); this.AddMapping(modelMapper.CompileMappingForAllExplicitlyAddedEntities()); } @@ -28,10 +27,12 @@ protected HealthcareConfiguration(string connectionString) #region Methods - protected virtual void InitializeModel(ModelMapper modelMapper) + 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/IHealthcareDeliveryConnectionFactory.cs similarity index 72% rename from Src/DDD.HealthcareDelivery/Infrastructure/IHealthcareConnectionFactory.cs rename to Src/DDD.HealthcareDelivery/Infrastructure/IHealthcareDeliveryConnectionFactory.cs index f295737..a101167 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/IHealthcareConnectionFactory.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/IHealthcareDeliveryConnectionFactory.cs @@ -5,7 +5,7 @@ /// /// Defines a connection factory for the context of healthcare delivery. /// - public interface IHealthcareConnectionFactory : IDbConnectionFactory + public interface IHealthcareDeliveryConnectionFactory : IDbConnectionFactory { } } diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareDeliveryConfiguration.cs similarity index 58% rename from Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareConfiguration.cs rename to Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareDeliveryConfiguration.cs index dbad3f7..060a87a 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareConfiguration.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareDeliveryConfiguration.cs @@ -1,19 +1,17 @@ -using NHibernate.Cfg; -using NHibernate.Dialect; +using NHibernate.Dialect; using NHibernate.Driver; using NHibernate.Mapping.ByCode; namespace DDD.HealthcareDelivery.Infrastructure { - using Prescriptions; using Core.Infrastructure.Data; - public abstract class OracleHealthcareConfiguration : HealthcareConfiguration + public abstract class OracleHealthcareDeliveryConfiguration : HealthcareDeliveryConfiguration { #region Constructors - protected OracleHealthcareConfiguration(string connectionString) : base(connectionString) + protected OracleHealthcareDeliveryConfiguration(string connectionString) : base(connectionString) { this.DataBaseIntegration(db => { @@ -27,10 +25,9 @@ protected OracleHealthcareConfiguration(string connectionString) : base(connecti #region Methods - protected override void InitializeModel(ModelMapper modelMapper) + protected override void AddMappings(ModelMapper modelMapper) { - base.InitializeModel(modelMapper); - modelMapper.AddMapping(); + base.AddMappings(modelMapper); modelMapper.AddMapping(); } diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/OracleStoredEventMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/OracleStoredEventMapping.cs deleted file mode 100644 index 8d7c3b8..0000000 --- a/Src/DDD.HealthcareDelivery/Infrastructure/OracleStoredEventMapping.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace DDD.HealthcareDelivery.Infrastructure -{ - using Data = Core.Infrastructure.Data; - - public class OracleStoredEventMapping : Data.OracleStoredEventMapping - { - #region Constructors - - public OracleStoredEventMapping() : base(true) - { - } - - #endregion Constructors - } -} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePharmaceuticalPrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePharmaceuticalPrescriptionMapping.cs deleted file mode 100644 index c80e3a7..0000000 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePharmaceuticalPrescriptionMapping.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions -{ - internal class OraclePharmaceuticalPrescriptionMapping : PharmaceuticalPrescriptionMapping - { - #region Constructors - - public OraclePharmaceuticalPrescriptionMapping() : base(true) - { - } - - #endregion Constructors - } -} \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescribedMedicationMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescribedMedicationMapping.cs index 5c64657..b2dbcfe 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescribedMedicationMapping.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescribedMedicationMapping.cs @@ -7,7 +7,7 @@ internal class OraclePrescribedMedicationMapping : PrescribedMe { #region Constructors - public OraclePrescribedMedicationMapping() : base(false) + public OraclePrescribedMedicationMapping() { // Fields this.Discriminator(m => m.Column(m1 => m1.SqlType("varchar2(20)"))); diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionMapping.cs index 17af862..730b884 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionMapping.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionMapping.cs @@ -13,7 +13,7 @@ internal class OraclePrescriptionMapping m.Column(m1 => m1.SqlType("varchar2(5)"))); diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionMapping.cs index 5b8eddb..252a358 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionMapping.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionMapping.cs @@ -5,20 +5,13 @@ namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions { using Domain.Prescriptions; - internal abstract class PharmaceuticalPrescriptionMapping : SubclassMapping + internal class PharmaceuticalPrescriptionMapping : SubclassMapping { - #region Fields - - private readonly bool useUpperCase; - - #endregion Fields - #region Constructors - protected PharmaceuticalPrescriptionMapping(bool useUpperCase) + public PharmaceuticalPrescriptionMapping() { - this.useUpperCase = useUpperCase; this.Lazy(false); // Fields this.DiscriminatorValue("PHARM"); @@ -28,7 +21,7 @@ protected PharmaceuticalPrescriptionMapping(bool useUpperCase) { m.Key(m1 => { - m1.Column(ToCasingConvention("PrescriptionId")); + m1.Column("PrescriptionId"); m1.NotNullable(true); }); m.Cascade(Cascade.All); @@ -38,11 +31,5 @@ protected PharmaceuticalPrescriptionMapping(bool useUpperCase) #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..3a2e4e4 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinder.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinder.cs @@ -14,7 +14,7 @@ public class PharmaceuticalPrescriptionsByPatientFinder #region Constructors - public PharmaceuticalPrescriptionsByPatientFinder(IHealthcareConnectionFactory connectionFactory) + public PharmaceuticalPrescriptionsByPatientFinder(IHealthcareDeliveryConnectionFactory connectionFactory) : base(connectionFactory) { } diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationMapping.cs index 45ea0ad..74d602c 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationMapping.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationMapping.cs @@ -10,36 +10,29 @@ internal abstract class PrescribedMedicationMapping : ClassMapp where TMedicationCode : MedicationCode { - #region Fields - - private readonly bool useUpperCase; - - #endregion Fields - #region Constructors - protected PrescribedMedicationMapping(bool useUpperCase) + protected PrescribedMedicationMapping() { - this.useUpperCase = useUpperCase; this.Lazy(false); // Table - this.Table(ToCasingConvention("PrescMedication")); + this.Table("PrescMedication"); // Keys this.Id("identifier", m => { - m.Column(ToCasingConvention("PrescMedicationId")); - m.Generator(Generators.Sequence, m1 => m1.Params(new { sequence = ToCasingConvention("PrescMedicationId") })); + m.Column("PrescMedicationId"); + m.Generator(Generators.Sequence, m1 => m1.Params(new { sequence = "PrescMedicationId" })); }); // Fields this.Discriminator(m => { - m.Column(ToCasingConvention("MedicationType")); + m.Column("MedicationType"); m.Length(20); m.NotNullable(true); }); this.Property(med => med.NameOrDescription, m => { - m.Column(ToCasingConvention("NameOrDesc")); + m.Column("NameOrDesc"); m.Type(NHibernateUtil.AnsiString); m.Length(1024); m.NotNullable(true); @@ -64,7 +57,7 @@ protected PrescribedMedicationMapping(bool useUpperCase) m.Class(); m.Property(c => c.Value, m1 => { - m1.Column(ToCasingConvention("Code")); + m1.Column("Code"); m1.Type(NHibernateUtil.AnsiString); m1.Length(20); }); @@ -73,11 +66,5 @@ protected PrescribedMedicationMapping(bool useUpperCase) #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..a9604d3 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinder.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinder.cs @@ -14,7 +14,7 @@ public class PrescribedMedicationsByPrescriptionFinder #region Constructors - public PrescribedMedicationsByPrescriptionFinder(IHealthcareConnectionFactory connectionFactory) + public PrescribedMedicationsByPrescriptionFinder(IHealthcareDeliveryConnectionFactory connectionFactory) : base(connectionFactory) { } diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionIdentifierGenerator.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionIdentifierGenerator.cs index 726c0fc..06a60ed 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionIdentifierGenerator.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionIdentifierGenerator.cs @@ -11,7 +11,7 @@ public class PrescriptionIdentifierGenerator : DbQueryHandler : ClassMapping where TPractitionerLicenseNumber : HealthcarePractitionerLicenseNumber @@ -17,30 +19,24 @@ internal abstract class PrescriptionMapping p.Identifier, m1 => m1.Property(i => i.Value, m2 => { - m2.Column(ToCasingConvention("PrescriptionId")); + m2.Column("PrescriptionId"); })); // Fields this.Discriminator(m => { - m.Column(ToCasingConvention("PrescriptionType")); + m.Column("PrescriptionType"); m.Length(5); m.NotNullable(true); }); @@ -56,7 +52,7 @@ protected PrescriptionMapping(bool useUpperCase) { m2.Column(m3 => { - m3.Name(ToCasingConvention("Language")); + m3.Name("Language"); m3.SqlType("char(2)"); }); m2.Type(NHibernateUtil.AnsiString); @@ -76,102 +72,102 @@ protected PrescriptionMapping(bool useUpperCase) m.Columns (m1 => { - m1.Name(ToCasingConvention("PrescriberId")); + m1.Name("PrescriberId"); m1.NotNullable(true); }, m1 => { - m1.Name(ToCasingConvention("PrescriberType")); + m1.Name("PrescriberType"); m1.Length(20); m1.NotNullable(true); }, m1 => { - m1.Name(ToCasingConvention("PrescriberLastName")); + m1.Name("PrescriberLastName"); m1.Length(50); m1.NotNullable(true); }, m1 => { - m1.Name(ToCasingConvention("PrescriberFirstName")); + m1.Name("PrescriberFirstName"); m1.Length(50); m1.NotNullable(true); }, m1 => { - m1.Name(ToCasingConvention("PrescriberDisplayName")); + m1.Name("PrescriberDisplayName"); m1.Length(100); m1.NotNullable(true); }, m1 => { - m1.Name(ToCasingConvention("PrescriberLicenseNum")); + m1.Name("PrescriberLicenseNum"); m1.Length(25); m1.NotNullable(true); }, m1 => { - m1.Name(ToCasingConvention("PrescriberSSN")); + m1.Name("PrescriberSSN"); m1.Length(25); }, m1 => { - m1.Name(ToCasingConvention("PrescriberSpeciality")); + m1.Name("PrescriberSpeciality"); m1.Length(50); }, m1 => { - m1.Name(ToCasingConvention("PrescriberPhone1")); + m1.Name("PrescriberPhone1"); m1.Length(20); }, m1 => { - m1.Name(ToCasingConvention("PrescriberPhone2")); + m1.Name("PrescriberPhone2"); m1.Length(20); }, m1 => { - m1.Name(ToCasingConvention("PrescriberEmail1")); + m1.Name("PrescriberEmail1"); m1.Length(50); }, m1 => { - m1.Name(ToCasingConvention("PrescriberEmail2")); + m1.Name("PrescriberEmail2"); m1.Length(50); }, m1 => { - m1.Name(ToCasingConvention("PrescriberWebSite")); + m1.Name("PrescriberWebSite"); m1.Length(255); }, m1 => { - m1.Name(ToCasingConvention("PrescriberStreet")); + m1.Name("PrescriberStreet"); m1.Length(50); }, m1 => { - m1.Name(ToCasingConvention("PrescriberHouseNum")); + m1.Name("PrescriberHouseNum"); m1.Length(10); }, m1 => { - m1.Name(ToCasingConvention("PrescriberBoxNum")); + m1.Name("PrescriberBoxNum"); m1.Length(10); }, m1 => { - m1.Name(ToCasingConvention("PrescriberPostCode")); + m1.Name("PrescriberPostCode"); m1.Length(10); }, m1 => { - m1.Name(ToCasingConvention("PrescriberCity")); + m1.Name("PrescriberCity"); m1.Length(50); }, m1 => { - m1.Name(ToCasingConvention("PrescriberCountry")); + m1.Name("PrescriberCountry"); m1.Length(2); m1.SqlType("char(2)"); }); @@ -181,21 +177,21 @@ protected PrescriptionMapping(bool useUpperCase) { m1.Property(p => p.Identifier, m2 => { - m2.Column(ToCasingConvention("PatientId")); + m2.Column("PatientId"); m2.NotNullable(true); }); m1.Component(p => p.FullName, m2 => { m2.Property(n => n.FirstName, m3 => { - m3.Column(ToCasingConvention("PatientFirstName")); + m3.Column("PatientFirstName"); m3.Type(NHibernateUtil.AnsiString); m3.Length(50); m3.NotNullable(true); }); m2.Property(n => n.LastName, m3 => { - m3.Column(ToCasingConvention("PatientLastName")); + m3.Column("PatientLastName"); m3.Type(NHibernateUtil.AnsiString); m3.Length(50); m3.NotNullable(true); @@ -203,7 +199,7 @@ protected PrescriptionMapping(bool useUpperCase) }); m1.Property(p => p.Sex, m3 => { - m3.Column(ToCasingConvention("PatientSex")); + m3.Column("PatientSex"); m3.Type(new EnumerationCodeType()); m3.Length(2); m3.NotNullable(true); @@ -213,14 +209,14 @@ protected PrescriptionMapping(bool useUpperCase) m2.Class(); m2.Property(n => n.Value, m3 => { - m3.Column(ToCasingConvention("PatientSSN")); + m3.Column("PatientSSN"); m3.Type(NHibernateUtil.AnsiString); m3.Length(25); }); }); m1.Property(p => p.Birthdate, m2 => { - m2.Column(ToCasingConvention("PatientBirthdate")); + m2.Column("PatientBirthdate"); m2.Type(NHibernateUtil.Date); }); }); @@ -231,24 +227,24 @@ protected PrescriptionMapping(bool useUpperCase) m.Columns (m1 => { - m1.Name(ToCasingConvention("FacilityId")); + m1.Name("FacilityId"); m1.NotNullable(true); }, m1 => { - m1.Name(ToCasingConvention("FacilityType")); + m1.Name("FacilityType"); m1.Length(20); m1.NotNullable(true); }, m1 => { - m1.Name(ToCasingConvention("FacilityName")); + m1.Name("FacilityName"); m1.Length(100); m1.NotNullable(true); }, m1 => { - m1.Name(ToCasingConvention("FacilityLicenseNum")); + m1.Name("FacilityLicenseNum"); m1.Length(25); }); }); @@ -256,11 +252,5 @@ protected PrescriptionMapping(bool useUpperCase) #endregion Constructors - #region Methods - - protected string ToCasingConvention(string name) => this.useUpperCase ? name.ToUpperInvariant() : name; - - #endregion Methods - } } diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPharmaceuticalPrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPharmaceuticalPrescriptionMapping.cs deleted file mode 100644 index 51948c2..0000000 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPharmaceuticalPrescriptionMapping.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions -{ - internal class SqlServerPharmaceuticalPrescriptionMapping : PharmaceuticalPrescriptionMapping - { - #region Constructors - - public SqlServerPharmaceuticalPrescriptionMapping() : base(false) - { - } - - #endregion Constructors - } -} \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescribedMedicationMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescribedMedicationMapping.cs index c0440b4..e9efe0d 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescribedMedicationMapping.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescribedMedicationMapping.cs @@ -7,7 +7,7 @@ internal class SqlServerPrescribedMedicationMapping : Prescribe { #region Constructors - public SqlServerPrescribedMedicationMapping() : base(false) + public SqlServerPrescribedMedicationMapping() { // Fields this.Discriminator(m => m.Column(m1 => m1.SqlType("varchar(20)"))); diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescriptionMapping.cs index 6f4e3f7..f2be6bf 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescriptionMapping.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescriptionMapping.cs @@ -13,7 +13,7 @@ internal class SqlServerPrescriptionMapping m.Column(m1 => m1.SqlType("varchar(5)"))); diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/SqlScripts.Designer.cs b/Src/DDD.HealthcareDelivery/Infrastructure/SqlScripts.Designer.cs index faf0bba..ffd8ada 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,12 +63,12 @@ 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, /// DeliverableAt, diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareConfiguration.cs deleted file mode 100644 index 012b14a..0000000 --- a/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareConfiguration.cs +++ /dev/null @@ -1,37 +0,0 @@ -using NHibernate.Cfg; -using NHibernate.Dialect; -using NHibernate.Driver; -using NHibernate.Mapping.ByCode; - -namespace DDD.HealthcareDelivery.Infrastructure -{ - using Prescriptions; - - public abstract class SqlServerHealthcareConfiguration : HealthcareConfiguration - { - - #region Constructors - - protected SqlServerHealthcareConfiguration(string connectionString) : base(connectionString) - { - this.DataBaseIntegration(db => - { - db.Dialect(); - db.Driver(); - }); - } - - #endregion Constructors - - #region Methods - - protected override void InitializeModel(ModelMapper modelMapper) - { - base.InitializeModel(modelMapper); - modelMapper.AddMapping(); - modelMapper.AddMapping(); - } - - #endregion Methods - } -} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareDeliveryConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareDeliveryConfiguration.cs new file mode 100644 index 0000000..de47068 --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareDeliveryConfiguration.cs @@ -0,0 +1,35 @@ +using NHibernate.Dialect; +using NHibernate.Driver; +using NHibernate.Mapping.ByCode; + +namespace DDD.HealthcareDelivery.Infrastructure +{ + using Core.Infrastructure.Data; + + public abstract class SqlServerHealthcareDeliveryConfiguration : HealthcareDeliveryConfiguration + { + + #region Constructors + + protected SqlServerHealthcareDeliveryConfiguration(string connectionString) : base(connectionString) + { + this.DataBaseIntegration(db => + { + db.Dialect(); + db.Driver(); + }); + } + + #endregion Constructors + + #region Methods + + protected override void AddMappings(ModelMapper modelMapper) + { + base.AddMappings(modelMapper); + modelMapper.AddMapping(); + } + + #endregion Methods + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerStoredEventMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerStoredEventMapping.cs deleted file mode 100644 index a92c422..0000000 --- a/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerStoredEventMapping.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace DDD.HealthcareDelivery.Infrastructure -{ - using Data = Core.Infrastructure.Data; - - public class SqlServerStoredEventMapping : Data.SqlServerStoredEventMapping - { - #region Constructors - - public SqlServerStoredEventMapping() : base(false) - { - } - - #endregion Constructors - } -} diff --git a/Test/DDD.Common.UnitTests/DDD.Common.UnitTests.csproj b/Test/DDD.Common.UnitTests/DDD.Common.UnitTests.csproj index 098f9e6..b9e4131 100644 --- a/Test/DDD.Common.UnitTests/DDD.Common.UnitTests.csproj +++ b/Test/DDD.Common.UnitTests/DDD.Common.UnitTests.csproj @@ -1,32 +1,31 @@  + net472;netcoreapp3.1 + Library DDD.Common - net472 - DDD.Common.UnitTests - DDD.Common.UnitTests - Copyright © 2017 - bin\$(Configuration)\ - - - full - - - pdbonly + false - - - + - - + + 5.10.3 + + + + + + 4.5.0 + - + + 2.4.3 + runtime; build; native; contentfiles; analyzers; buildtransitive all 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.Core.Abstractions.UnitTests/DDD.Core.Abstractions.UnitTests.csproj b/Test/DDD.Core.Abstractions.UnitTests/DDD.Core.Abstractions.UnitTests.csproj index b5c8190..7724202 100644 --- a/Test/DDD.Core.Abstractions.UnitTests/DDD.Core.Abstractions.UnitTests.csproj +++ b/Test/DDD.Core.Abstractions.UnitTests/DDD.Core.Abstractions.UnitTests.csproj @@ -1,34 +1,36 @@  + net472;netcoreapp3.1 + Library DDD - net472 - DDD.Core.Abstractions.UnitTests - DDD.Core.Abstractions.UnitTests - Copyright © 2017 - bin\$(Configuration)\ - - - full - - - pdbonly + false - - - - + - - - - + + 5.10.3 + + + + + 4.2.2 + + + + 4.5.4 + + + 4.5.0 + - + + 2.4.3 + runtime; build; native; contentfiles; analyzers; buildtransitive all 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.UnitTests/DDD.Core.UnitTests.csproj b/Test/DDD.Core.UnitTests/DDD.Core.UnitTests.csproj index 78da70b..9b9a037 100644 --- a/Test/DDD.Core.UnitTests/DDD.Core.UnitTests.csproj +++ b/Test/DDD.Core.UnitTests/DDD.Core.UnitTests.csproj @@ -1,23 +1,12 @@  + net472;netcoreapp3.1 + Library DDD.Core - net472 - DDD.Core.UnitTests - DDD.Core.UnitTests - Copyright © 2017 - bin\$(Configuration)\ - - - full - - - pdbonly + false - - - - + @@ -25,13 +14,25 @@ - - - - - + + 5.10.3 + + + + + 4.2.2 + + + + 4.5.4 + + + 4.5.0 + - + + 2.4.3 + runtime; build; native; contentfiles; analyzers; buildtransitive all 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/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.HealthcareDelivery.IntegrationTests/App.config b/Test/DDD.HealthcareDelivery.IntegrationTests/App.config index 71e615a..ea7fbec 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/App.config +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/App.config @@ -1,44 +1,17 @@  -
- -
+
- - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - \ 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 45aec04..73a0acb 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionCreatorTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionCreatorTests.cs @@ -1,13 +1,8 @@ using Xunit; -using System.Text; -using NHibernate; namespace DDD.HealthcareDelivery.Application.Prescriptions { - using Core.Domain; - using Core.Infrastructure.Serialization; using Infrastructure; - using Mapping; [Collection("Oracle")] public class OraclePharmaceuticalPrescriptionCreatorTests @@ -22,20 +17,5 @@ public OraclePharmaceuticalPrescriptionCreatorTests(OracleFixture fixture) : bas #endregion Constructors - #region Methods - - protected override IObjectTranslator CreateEventTranslator() - { - return new StoredEventTranslator(DataContractSerializerWrapper.Create(new UTF8Encoding(false))); - } - - protected override ISession CreateSession() - { - var configuration = new BelgianOracleHealthcareConfiguration(OracleConnectionFactory.ConnectionString); - return configuration.BuildSessionFactory().OpenSession(); - } - - #endregion Methods - } } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionRevokerTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionRevokerTests.cs index be4c92e..eae52e0 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionRevokerTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/OraclePharmaceuticalPrescriptionRevokerTests.cs @@ -1,13 +1,8 @@ using Xunit; -using System.Text; -using NHibernate; namespace DDD.HealthcareDelivery.Application.Prescriptions { - using Core.Domain; - using Core.Infrastructure.Serialization; using Infrastructure; - using Mapping; [Collection("Oracle")] public class OraclePharmaceuticalPrescriptionRevokerTests @@ -22,20 +17,5 @@ public OraclePharmaceuticalPrescriptionRevokerTests(OracleFixture fixture) : bas #endregion Constructors - #region Methods - - protected override IObjectTranslator CreateEventTranslator() - { - return new StoredEventTranslator(DataContractSerializerWrapper.Create(new UTF8Encoding(false))); - } - - protected override ISession CreateSession() - { - var configuration = new BelgianOracleHealthcareConfiguration(OracleConnectionFactory.ConnectionString); - return configuration.BuildSessionFactory().OpenSession(); - } - - #endregion Methods - } } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs index a8f64cc..c4d174e 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs @@ -1,5 +1,6 @@ using FluentAssertions; using System; +using System.Data.Common; using System.Security.Principal; using System.Threading; using System.Threading.Tasks; @@ -10,20 +11,21 @@ namespace DDD.HealthcareDelivery.Application.Prescriptions { using Common.Application; using Core.Domain; - using Core.Infrastructure.Testing; using Core.Infrastructure.Data; using Domain.Facilities; using Domain.Practitioners; using Domain.Prescriptions; using Infrastructure; - using Mapping; public abstract class PharmaceuticalPrescriptionCreatorTests : IDisposable - where TFixture : IDbFixture + where TFixture : IPersistenceFixture { + #region Fields + private ISessionFactory sessionFactory; private ISession session; + private DbConnection connection; #endregion Fields @@ -56,6 +58,8 @@ protected PharmaceuticalPrescriptionCreatorTests(TFixture fixture) public void Dispose() { this.session.Dispose(); + this.connection.Dispose(); + this.sessionFactory.Dispose(); } [Fact] @@ -74,11 +78,6 @@ public async Task HandleAsync_WhenCalled_CreatePharmaceuticalPrescription() prescription.PrescribedMedications().Should().NotBeNullOrEmpty(); } - - protected abstract IObjectTranslator CreateEventTranslator(); - - protected abstract ISession CreateSession(); - private static CreatePharmaceuticalPrescription CreateCommand() { return new CreatePharmaceuticalPrescription @@ -124,14 +123,21 @@ private static CreatePharmaceuticalPrescription CreateCommand() private IAsyncRepository CreateRepository() { - this.session = this.CreateSession(); + this.sessionFactory = this.Fixture.CreateSessionFactory(); + this.connection = this.Fixture.ConnectionFactory.CreateOpenConnection(); + this.session = this.sessionFactory + .WithOptions() + // To avoid transaction promotion from local to distributed + .Connection(this.connection) + .OpenSession(); return new NHibernateRepository ( this.session, - this.CreateEventTranslator() + 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 a57f3e0..fa37002 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs @@ -1,4 +1,5 @@ using Xunit; +using System.Data.Common; using System.Threading; using System.Threading.Tasks; using System.Security.Principal; @@ -11,17 +12,17 @@ namespace DDD.HealthcareDelivery.Application.Prescriptions using Core.Domain; using Core.Infrastructure.Data; using Domain.Prescriptions; - using Core.Infrastructure.Testing; using Infrastructure; - using Mapping; public abstract class PharmaceuticalPrescriptionRevokerTests : IDisposable - where TFixture : IDbFixture + where TFixture : IPersistenceFixture { #region Fields + private ISessionFactory sessionFactory; private ISession session; + private DbConnection connection; #endregion Fields @@ -53,6 +54,8 @@ protected PharmaceuticalPrescriptionRevokerTests(TFixture fixture) public void Dispose() { this.session.Dispose(); + this.connection.Dispose(); + this.sessionFactory.Dispose(); } [Fact] @@ -69,10 +72,6 @@ public async Task HandleAsync_WhenCalled_RevokePharmaceuticalPrescription() prescription.Status.Should().Be(Domain.Prescriptions.PrescriptionStatus.Revoked); } - protected abstract IObjectTranslator CreateEventTranslator(); - - protected abstract ISession CreateSession(); - private static RevokePharmaceuticalPrescription CreateCommand() { return new RevokePharmaceuticalPrescription @@ -84,11 +83,17 @@ private static RevokePharmaceuticalPrescription CreateCommand() private IAsyncRepository CreateRepository() { - this.session = this.CreateSession(); + this.sessionFactory = this.Fixture.CreateSessionFactory(); + this.connection = this.Fixture.ConnectionFactory.CreateOpenConnection(); + this.session = this.sessionFactory + .WithOptions() + // To avoid transaction promotion from local to distributed + .Connection(this.connection) + .OpenSession(); return new NHibernateRepository ( this.session, - this.CreateEventTranslator() + this.Fixture.CreateEventTranslator() ); } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionCreatorTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionCreatorTests.cs index a3c5ed1..6c5d554 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionCreatorTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionCreatorTests.cs @@ -1,15 +1,8 @@ using Xunit; -using System.Text; namespace DDD.HealthcareDelivery.Application.Prescriptions { - using Core.Domain; - using Core.Infrastructure.Serialization; - using Core.Infrastructure.Data; - using Domain.Prescriptions; using Infrastructure; - using DDD.Mapping; - using NHibernate; [Collection("SqlServer")] public class SqlServerPharmaceuticalPrescriptionCreatorTests @@ -24,20 +17,5 @@ public SqlServerPharmaceuticalPrescriptionCreatorTests(SqlServerFixture fixture) #endregion Constructors - #region Methods - - protected override IObjectTranslator CreateEventTranslator() - { - return new StoredEventTranslator(DataContractSerializerWrapper.Create(Encoding.Unicode)); - } - - protected override ISession CreateSession() - { - var configuration = new BelgianSqlServerHealthcareConfiguration(SqlServerConnectionFactory.ConnectionString); - return configuration.BuildSessionFactory().OpenSession(); - } - - #endregion Methods - } } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionRevokerTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/SqlServerPharmaceuticalPrescriptionRevokerTests.cs index 2bb7e74..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; -using NHibernate; namespace DDD.HealthcareDelivery.Application.Prescriptions { - using Core.Domain; - using Core.Infrastructure.Serialization; - using Mapping; using Infrastructure; [Collection("SqlServer")] @@ -22,20 +17,5 @@ public SqlServerPharmaceuticalPrescriptionRevokerTests(SqlServerFixture fixture) #endregion Constructors - #region Methods - - protected override IObjectTranslator CreateEventTranslator() - { - return new StoredEventTranslator(DataContractSerializerWrapper.Create(Encoding.Unicode)); - } - - protected override ISession CreateSession() - { - var configuration = new BelgianSqlServerHealthcareConfiguration(SqlServerConnectionFactory.ConnectionString); - return configuration.BuildSessionFactory().OpenSession(); - } - - #endregion Methods - } } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj b/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj index 60d1e43..b2fd684 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj @@ -1,27 +1,10 @@  + net472;netcoreapp3.1 + Library DDD.HealthcareDelivery - net472 - DDD.HealthcareDelivery.IntegrationTests - DDD.HealthcareDelivery.IntegrationTests - Copyright © 2017 - bin\$(Configuration)\ + false - - full - - - pdbonly - - - - - - - - - - True @@ -67,13 +50,22 @@ - - - + + 5.10.3 + + + + - + all + + + + + + \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianOracleHealthcareConfigurationTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianOracleHealthcareConfigurationTests.cs deleted file mode 100644 index d4906a1..0000000 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianOracleHealthcareConfigurationTests.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Xunit; - -namespace DDD.HealthcareDelivery.Infrastructure -{ - [Collection("Oracle")] - public class BelgianOracleHealthcareConfigurationTests : HealthcareConfigurationTests - { - #region Constructors - - public BelgianOracleHealthcareConfigurationTests(OracleFixture fixture) : base(fixture) - { - } - - #endregion Constructors - - #region Methods - - protected override HealthcareConfiguration CreateConfiguration() - { - return new BelgianOracleHealthcareConfiguration(OracleConnectionFactory.ConnectionString); - } - - #endregion Methods - } -} \ 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..376d070 --- /dev/null +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianOracleHealthcareDeliveryConfigurationTests.cs @@ -0,0 +1,25 @@ +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() + { + return new BelgianOracleHealthcareDeliveryConfiguration(OracleConnectionFactory.ConnectionString); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareConfigurationTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareConfigurationTests.cs deleted file mode 100644 index 220fd56..0000000 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareConfigurationTests.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Xunit; - -namespace DDD.HealthcareDelivery.Infrastructure -{ - [Collection("SqlServer")] - public class BelgianSqlServerHealthcareConfigurationTests : HealthcareConfigurationTests - { - #region Constructors - - public BelgianSqlServerHealthcareConfigurationTests(SqlServerFixture fixture) : base(fixture) - { - } - - #endregion Constructors - - #region Methods - - protected override HealthcareConfiguration CreateConfiguration() - { - return new BelgianSqlServerHealthcareConfiguration(SqlServerConnectionFactory.ConnectionString); - } - - #endregion Methods - } -} diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareDeliveryConfigurationTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareDeliveryConfigurationTests.cs new file mode 100644 index 0000000..60dd9e5 --- /dev/null +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareDeliveryConfigurationTests.cs @@ -0,0 +1,25 @@ +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() + { + return new BelgianSqlServerHealthcareDeliveryConfiguration(SqlServerConnectionFactory.ConnectionString); + } + + #endregion Methods + } +} diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareConfigurationTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareDeliveryConfigurationTests.cs similarity index 81% rename from Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareConfigurationTests.cs rename to Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareDeliveryConfigurationTests.cs index e155fba..3c73cea 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareConfigurationTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareDeliveryConfigurationTests.cs @@ -1,5 +1,6 @@ using FluentAssertions; using NHibernate; +using NHibernate.Tool.hbm2ddl; using System; using Xunit; @@ -7,26 +8,25 @@ namespace DDD.HealthcareDelivery.Infrastructure { using Common.Domain; using Core.Domain; - using Core.Infrastructure.Testing; using Domain.Facilities; using Domain.Patients; using Domain.Practitioners; using Domain.Prescriptions; - public abstract class HealthcareConfigurationTests : IDisposable - where TFixture : IDbFixture + public abstract class HealthcareDeliveryConfigurationTests : IDisposable + where TFixture : IPersistenceFixture { #region Fields - private readonly HealthcareConfiguration configuration; + private readonly HealthcareDeliveryConfiguration configuration; private readonly ISession session; #endregion Fields #region Constructors - protected HealthcareConfigurationTests(TFixture fixture) + protected HealthcareDeliveryConfigurationTests(TFixture fixture) { this.Fixture = fixture; this.configuration = this.CreateConfiguration(); @@ -42,20 +42,20 @@ protected HealthcareConfigurationTests(TFixture fixture) #endregion Properties - //[Fact] - //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(); - //} + [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 @@ -109,7 +109,7 @@ public void HealthcareConfiguration_WhenMappingValid_CanSaveAndRestorePrescripti // Assert prescription2.Should().BeEquivalentTo(prescription1); } - protected abstract HealthcareConfiguration CreateConfiguration(); + protected abstract HealthcareDeliveryConfiguration CreateConfiguration(); private static StoredEvent CreateEvent() { diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/IPersistenceFixture.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/IPersistenceFixture.cs new file mode 100644 index 0000000..25a9f17 --- /dev/null +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/IPersistenceFixture.cs @@ -0,0 +1,21 @@ +using NHibernate; + +namespace DDD.HealthcareDelivery.Infrastructure +{ + using Core.Domain; + using Core.Infrastructure.Testing; + using Mapping; + + public interface IPersistenceFixture : IDbFixture + where TConnectionFactory : class, IHealthcareDeliveryConnectionFactory + { + + #region Methods + + IObjectTranslator CreateEventTranslator(); + + ISessionFactory CreateSessionFactory(); + + #endregion Methods + } +} diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleCollection.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleCollection.cs similarity index 74% rename from Test/DDD.HealthcareDelivery.IntegrationTests/OracleCollection.cs rename to Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleCollection.cs index 29bb075..4afad64 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleCollection.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleCollection.cs @@ -1,6 +1,6 @@ using Xunit; -namespace DDD.HealthcareDelivery +namespace DDD.HealthcareDelivery.Infrastructure { [CollectionDefinition("Oracle")] public class OracleCollection : ICollectionFixture diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleConnectionFactory.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleConnectionFactory.cs new file mode 100644 index 0000000..55f9351 --- /dev/null +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleConnectionFactory.cs @@ -0,0 +1,38 @@ +namespace DDD.HealthcareDelivery.Infrastructure +{ + using Core.Infrastructure.Data; + + public class OracleConnectionFactory : DbConnectionFactory, IHealthcareDeliveryConnectionFactory + { + + #region Fields + + /// + /// Pooling=false is used to ensure that the System.Transactions infrastructure doesn't automatically escalates the transaction to be managed by the Microsoft Distributed Transaction Coordinator (MSDTC). + /// Do not use Pooling=false in production. + /// + public const string ConnectionString + = "Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=XE)));Persist Security Info=false;User Id=TEST;Password=dev;Pooling=false"; + + #endregion Fields + + #region Constructors + + private OracleConnectionFactory(string providerName, string connectionString) + : base(providerName, connectionString) + { + } + + #endregion Constructors + + #region Methods + + public static OracleConnectionFactory Create() + { + return new OracleConnectionFactory("Oracle.ManagedDataAccess.Client", ConnectionString); + } + + #endregion Methods + + } +} diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleFixture.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleFixture.cs similarity index 51% rename from Test/DDD.HealthcareDelivery.IntegrationTests/OracleFixture.cs rename to Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleFixture.cs index 7b7e976..5bb4a39 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleFixture.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleFixture.cs @@ -1,17 +1,25 @@ using System.Data; +using System.Text; +using NHibernate; using Oracle.ManagedDataAccess.Client; +#if NETCOREAPP3_1 +using System.Data.Common; +#endif -namespace DDD.HealthcareDelivery +namespace DDD.HealthcareDelivery.Infrastructure { using Core.Infrastructure.Testing; using Core.Infrastructure.Data; + using Core.Domain; + using Core.Infrastructure.Serialization; + using Mapping; - public class OracleFixture : DbFixture + public class OracleFixture : DbFixture, IPersistenceFixture { #region Constructors - public OracleFixture() : base(new OracleConnectionFactory(), "OracleScripts") + public OracleFixture() : base(OracleConnectionFactory.Create(), "OracleScripts") { } @@ -19,6 +27,17 @@ public OracleFixture() : base(new OracleConnectionFactory(), "OracleScripts") #region Methods + public IObjectTranslator CreateEventTranslator() + { + return new StoredEventTranslator(DataContractSerializerWrapper.Create(new UTF8Encoding(false))); + } + + public ISessionFactory CreateSessionFactory() + { + var configuration = new BelgianOracleHealthcareDeliveryConfiguration(OracleConnectionFactory.ConnectionString); + return configuration.BuildSessionFactory(); + } + protected override void CreateDatabase() { using (var connection = this.ConnectionFactory.CreateConnection()) @@ -36,6 +55,13 @@ protected override int[] ExecuteScript(string script, IDbConnection connection) return connection.ExecuteScript(script, batchSeparator: "/"); } + protected override void RegisterDbProviderFactory() + { +#if NETCOREAPP3_1 + DbProviderFactories.RegisterFactory("Oracle.ManagedDataAccess.Client", OracleClientFactory.Instance); +#endif + } + #endregion Methods } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinderTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinderTests.cs index 27fb953..da4650a 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinderTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinderTests.cs @@ -7,10 +7,9 @@ namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions { using Application.Prescriptions; - using Core.Infrastructure.Testing; public abstract class PharmaceuticalPrescriptionsByPatientFinderTests - where TFixture : IDbFixture + where TFixture : IPersistenceFixture { #region Fields diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinderTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinderTests.cs index 48a3d27..70a7324 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinderTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinderTests.cs @@ -6,10 +6,9 @@ namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions { using Application.Prescriptions; - using Core.Infrastructure.Testing; public abstract class PrescribedMedicationsByPrescriptionFinderTests - where TFixture : IDbFixture + where TFixture : IPersistenceFixture { #region Fields diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerCollection.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerCollection.cs similarity index 75% rename from Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerCollection.cs rename to Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerCollection.cs index dc5b994..86c6f8d 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerCollection.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerCollection.cs @@ -1,6 +1,6 @@ using Xunit; -namespace DDD.HealthcareDelivery +namespace DDD.HealthcareDelivery.Infrastructure { [CollectionDefinition("SqlServer")] public class SqlServerCollection : ICollectionFixture diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerConnectionFactory.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerConnectionFactory.cs new file mode 100644 index 0000000..27d973f --- /dev/null +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerConnectionFactory.cs @@ -0,0 +1,37 @@ +namespace DDD.HealthcareDelivery.Infrastructure +{ + using Core.Infrastructure.Data; + + public class SqlServerConnectionFactory : DbConnectionFactory, IHealthcareDeliveryConnectionFactory + { + + #region Fields + + /// + /// Pooling=false is used to ensure that the System.Transactions infrastructure doesn't automatically escalates the transaction to be managed by the Microsoft Distributed Transaction Coordinator (MSDTC). + /// Do not use Pooling=false in production. + /// + public const string ConnectionString + = @"Data Source=(local)\SQLEXPRESS;Database=Test;Integrated Security=False;User ID=sa;Password=dev;Pooling=false"; + + #endregion Fields + + #region Constructors + + private SqlServerConnectionFactory(string providerName, string connectionString) + : base(providerName, connectionString) + { + } + + #endregion Constructors + + #region Methods + + public static SqlServerConnectionFactory Create() + { + return new SqlServerConnectionFactory("Microsoft.Data.SqlClient", ConnectionString); + } + + #endregion Methods + } +} diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerFixture.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerFixture.cs new file mode 100644 index 0000000..d9a5d11 --- /dev/null +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerFixture.cs @@ -0,0 +1,66 @@ +using Microsoft.Data.SqlClient; +using NHibernate; +using System.Data; +using System.Text; +#if NETCOREAPP3_1 +using System.Data.Common; +#endif + +namespace DDD.HealthcareDelivery.Infrastructure +{ + using Core.Infrastructure.Testing; + using Core.Infrastructure.Data; + using Core.Domain; + using Core.Infrastructure.Serialization; + using Mapping; + + public class SqlServerFixture : DbFixture, IPersistenceFixture + { + + #region Constructors + + public SqlServerFixture() : base(SqlServerConnectionFactory.Create(), "SqlServerScripts") + { + } + + #endregion Constructors + + #region Methods + + public IObjectTranslator CreateEventTranslator() + { + return new StoredEventTranslator(DataContractSerializerWrapper.Create(Encoding.Unicode)); + } + + public ISessionFactory CreateSessionFactory() + { + var configuration = new BelgianSqlServerHealthcareDeliveryConfiguration(SqlServerConnectionFactory.ConnectionString); + return configuration.BuildSessionFactory(); + } + + 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); + } + + protected override void RegisterDbProviderFactory() + { +#if NETCOREAPP3_1 + DbProviderFactories.RegisterFactory("Microsoft.Data.SqlClient", SqlClientFactory.Instance); +#endif + } + + #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 c7bde6b..0000000 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleConnectionFactory.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace DDD.HealthcareDelivery -{ - using Core.Infrastructure.Data; - using Infrastructure; - - public class OracleConnectionFactory : DbConnectionFactory, IHealthcareConnectionFactory - { - - #region Fields - - public const string ConnectionString = @"Data Source=Local;Persist Security Info=true;User Id=TEST;Password=dev;Pooling=false"; - - #endregion Fields - - #region Constructors - - public OracleConnectionFactory() - : base("Oracle.ManagedDataAccess.Client", ConnectionString) - { - } - - #endregion Constructors - - } -} diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerConnectionFactory.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerConnectionFactory.cs deleted file mode 100644 index 7647897..0000000 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/SqlServerConnectionFactory.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace DDD.HealthcareDelivery -{ - using Core.Infrastructure.Data; - using Infrastructure; - - public class SqlServerConnectionFactory : DbConnectionFactory, IHealthcareConnectionFactory - { - - #region Fields - - public const string ConnectionString = @"Data Source=(local)\SQLEXPRESS;Database=Test;Integrated Security=False;User ID=sa;Password=dev;Pooling=false"; - - #endregion Fields - - #region Constructors - - public SqlServerConnectionFactory() - : base("System.Data.SqlClient", ConnectionString) - { - } - - #endregion Constructors - - } -} 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.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj b/Test/DDD.HealthcareDelivery.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj index 55f681d..56e2c82 100644 --- a/Test/DDD.HealthcareDelivery.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj +++ b/Test/DDD.HealthcareDelivery.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj @@ -1,24 +1,10 @@  + net472;netcoreapp3.0 + Library DDD.HealthcareDelivery - net472 - DDD.HealthcareDelivery.UnitTests - DDD.HealthcareDelivery.UnitTests - Copyright © 2017 - bin\$(Configuration)\ - - - full - - - pdbonly + false - - - - - - @@ -30,11 +16,26 @@ - - - + + + + + 5.10.3 + + + 9.1.2 + + + + + 4.5.0 + + + - + + 2.4.3 + runtime; build; native; contentfiles; analyzers; buildtransitive all 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 @@ - - - - - - - - - - - From a3d3c363a0ed3f38f919e83ff4656a33de13465a Mon Sep 17 00:00:00 2001 From: draphyz Date: Sun, 16 Aug 2020 09:44:40 +0200 Subject: [PATCH 032/111] Update Doc --- Doc/EntityFrameworkMapping.png | Bin 11693 -> 11754 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Doc/EntityFrameworkMapping.png b/Doc/EntityFrameworkMapping.png index 45be9e8ae1451cb1059fc85285b16359b3113057..43446ff982df3f9d6d86ab52d78488f8e6fce977 100644 GIT binary patch literal 11754 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 V?|M3e4RMJiEujFa5HtAxzW@m{=D`2} literal 11693 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 From cccd522103e7fc1fd0533477f77ca5b7a8eb5cdf Mon Sep 17 00:00:00 2001 From: draphyz Date: Thu, 3 Sep 2020 17:16:41 +0200 Subject: [PATCH 033/111] Declare Empty field as public --- Src/DDD.Common/Domain/BinaryContent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/DDD.Common/Domain/BinaryContent.cs b/Src/DDD.Common/Domain/BinaryContent.cs index 1d196b9..31ed42a 100644 --- a/Src/DDD.Common/Domain/BinaryContent.cs +++ b/Src/DDD.Common/Domain/BinaryContent.cs @@ -13,7 +13,7 @@ public class BinaryContent : ValueObject #region Fields - private static readonly BinaryContent Empty = new BinaryContent(new byte[0]); + public static readonly BinaryContent Empty = new BinaryContent(new byte[0]); #endregion Fields From ae369824b41ed71e4359c2d5407fee283bd59fad Mon Sep 17 00:00:00 2001 From: draphyz Date: Thu, 3 Sep 2020 18:01:04 +0200 Subject: [PATCH 034/111] Override method ToString() --- Src/DDD.Common/Domain/BinaryContent.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Src/DDD.Common/Domain/BinaryContent.cs b/Src/DDD.Common/Domain/BinaryContent.cs index 31ed42a..ca648fe 100644 --- a/Src/DDD.Common/Domain/BinaryContent.cs +++ b/Src/DDD.Common/Domain/BinaryContent.cs @@ -53,6 +53,8 @@ public override IEnumerable HashCodeComponents() yield return this.Data[i]; } + public override string ToString() => $"{this.GetType().Name} [data={this.Data}]"; + #endregion Methods } From 44e091c795706c910cd52d0a367d4f88bad2c915 Mon Sep 17 00:00:00 2001 From: draphyz Date: Tue, 1 Jun 2021 22:38:30 +0200 Subject: [PATCH 035/111] Add CommandInvalidException and DomainServiceInvalidException --- .../Application/CommandInvalidException.cs | 67 ++++++++++++++++++ .../DomainToCommandExceptionTranslator.cs | 2 + .../Domain/DomainServiceInvalidException.cs | 68 +++++++++++++++++++ 3 files changed, 137 insertions(+) create mode 100644 Src/DDD.Core/Application/CommandInvalidException.cs create mode 100644 Src/DDD.Core/Domain/DomainServiceInvalidException.cs diff --git a/Src/DDD.Core/Application/CommandInvalidException.cs b/Src/DDD.Core/Application/CommandInvalidException.cs new file mode 100644 index 0000000..92246ef --- /dev/null +++ b/Src/DDD.Core/Application/CommandInvalidException.cs @@ -0,0 +1,67 @@ +using System; +using System.Linq; +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 string ToString() + { + var s = $"{this.GetType()}: {this.Message} "; + s += $"{Environment.NewLine}IsTransient: {this.IsTransient}"; + if (this.Command != null) + s += $"{Environment.NewLine}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/DomainToCommandExceptionTranslator.cs b/Src/DDD.Core/Application/DomainToCommandExceptionTranslator.cs index 3bbfef6..cb15162 100644 --- a/Src/DDD.Core/Application/DomainToCommandExceptionTranslator.cs +++ b/Src/DDD.Core/Application/DomainToCommandExceptionTranslator.cs @@ -38,6 +38,8 @@ public CommandException Translate(DomainException exception, IDictionary + /// 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 string ToString() + { + var s = $"{this.GetType()}: {this.Message} "; + s += $"{Environment.NewLine}IsTransient: {this.IsTransient}"; + if (this.ServiceType != null) + s += $"{Environment.NewLine}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 + + } +} From 69347c0f38e90d462fc815d54f9ac0374edc4da0 Mon Sep 17 00:00:00 2001 From: draphyz Date: Wed, 18 Aug 2021 09:41:37 +0200 Subject: [PATCH 036/111] Update README.md --- README.md | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 1c93823..8549df2 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,72 @@ ### Domain-Driven Design example -This project is an example 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. +The goal of the project is to experiment with some .NET implementations 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. +Only a light version of CQRS (no data projection, one data store) has been considered. **Model** -The envisaged model reflects the lifecycle of a medical prescription and, in particular, of a pharmaceutical prescription. This lifecycle can be resumed by the following diagram. +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 use cases related to the prescriber : creation and revocation of a prescription. +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. +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 both used and some value types like enumerations has been implemented as value objects. +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 6 (branch entityframework). +- 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 main components on the command-side can be represented as follows : +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, 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. +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) **Projects** -Libraries are distributed by component (bounded context) : +The libraries are distributed by component (bounded context) : -- The "Core" libraries include the core components (Entity, ValueObject, CommandHandler, ...) necessary to implement the approach DDD according to the architectural pattern CQRS. +- 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 project "DDD.HealthcareDelivery.IntegrationTests". +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 projects "DDD.Core.Polly" or "DDD.Core". +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. From 2a11c0ff6b9f50bd61921e217bf61fa1fc74b18e Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 23 Aug 2021 21:13:24 +0200 Subject: [PATCH 037/111] Update the domain model --- .../PrescribedMedicationDescriptor.cs | 4 +--- .../PrescribedMedicationDetails.cs | 4 +--- ...ianPharmaceuticalPrescriptionTranslator.cs | 5 +---- .../Prescriptions/PrescribedMedication.cs | 17 +++++++---------- .../PrescribedPharmaceuticalCompounding.cs | 5 ++--- .../PrescribedPharmaceuticalProduct.cs | 5 ++--- .../PrescribedPharmaceuticalSubstance.cs | 5 ++--- .../PrescribedMedicationMapping.cs | 10 ---------- ...indPrescribedMedicationsByPrescription.sql | Bin 488 -> 448 bytes .../Infrastructure/SqlScripts.Designer.cs | 1 - .../PharmaceuticalPrescriptionCreatorTests.cs | 2 +- ...bedMedicationsByPrescriptionFinderTests.cs | 9 +++------ .../OracleScripts.Designer.cs | 8 ++++---- .../Scripts/Oracle/FillSchema.sql | 3 +-- ...indPrescribedMedicationsByPrescription.sql | Bin 11130 -> 10822 bytes .../RevokePharmaceuticalPrescription.sql | 4 ++-- .../Scripts/SqlServer/CreateDatabase.sql | Bin 16646 -> 16568 bytes ...indPrescribedMedicationsByPrescription.sql | Bin 12042 -> 11714 bytes .../RevokePharmaceuticalPrescription.sql | 4 ++-- .../DDD.HealthcareDelivery.UnitTests.csproj | 2 +- 20 files changed, 30 insertions(+), 58 deletions(-) diff --git a/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/PrescribedMedicationDescriptor.cs b/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/PrescribedMedicationDescriptor.cs index 2716314..a66194b 100644 --- a/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/PrescribedMedicationDescriptor.cs +++ b/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/PrescribedMedicationDescriptor.cs @@ -18,9 +18,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/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs b/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs index e47fd20..fb13591 100644 --- a/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs +++ b/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs @@ -41,8 +41,7 @@ private static PrescribedPharmaceuticalCompounding ToCompounding(PrescribedMedic ( medication.NameOrDescription, medication.Posology, - medication.Quantity, - medication.Duration + medication.Quantity ); } @@ -164,7 +163,6 @@ private static PrescribedPharmaceuticalProduct ToProduct(PrescribedMedicationDes medication.NameOrDescription, medication.Posology, medication.Quantity, - medication.Duration, BelgianMedicationCode.CreateIfNotEmpty(medication.Code) ); } @@ -175,7 +173,6 @@ private static PrescribedPharmaceuticalSubstance ToSubstance(PrescribedMedicatio medication.NameOrDescription, medication.Posology, medication.Quantity, - medication.Duration, BelgianMedicationCode.CreateIfNotEmpty(medication.Code) ); } diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedication.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedication.cs index b666513..afb0afd 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedication.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedication.cs @@ -20,8 +20,7 @@ protected PrescribedMedication() { } protected PrescribedMedication(string nameOrDescription, string posology = null, - string quantity = null, - string duration = null, + byte? quantity = null, MedicationCode code = null, int identifier = 0) { @@ -29,10 +28,11 @@ protected PrescribedMedication(string nameOrDescription, Condition.Requires(identifier, nameof(identifier)).IsGreaterOrEqual(0); this.NameOrDescription = nameOrDescription; this.Posology = posology; - if (!string.IsNullOrWhiteSpace(quantity)) + if (quantity.HasValue) + { + Condition.Requires(quantity, nameof(quantity)).IsGreaterOrEqual(1); this.Quantity = quantity; - if (!string.IsNullOrWhiteSpace(duration)) - this.Duration = duration; + } this.Code = code; this.identifier = identifier; } @@ -43,13 +43,11 @@ protected PrescribedMedication(string nameOrDescription, public MedicationCode Code { get; private set; } - public string Duration { get; private set; } - public string NameOrDescription { get; private set; } public string Posology { get; private set; } - public string Quantity { get; private set; } + public byte? Quantity { get; private set; } #endregion Properties @@ -60,13 +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 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/PrescribedPharmaceuticalCompounding.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalCompounding.cs index 7ab43f4..08728ce 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalCompounding.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalCompounding.cs @@ -12,10 +12,9 @@ protected PrescribedPharmaceuticalCompounding() { } public PrescribedPharmaceuticalCompounding(string nameOrDescription, string posology = null, - string quantity = null, - string duration = null, + byte? quantity = null, int identifier = 0) - : base(nameOrDescription, posology, quantity, duration, null, identifier) + : base(nameOrDescription, posology, quantity, null, identifier) { } diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalProduct.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalProduct.cs index 0b09031..65066fa 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalProduct.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalProduct.cs @@ -10,11 +10,10 @@ public class PrescribedPharmaceuticalProduct : PrescribedMedication public PrescribedPharmaceuticalProduct(string nameOrDescription, string posology = null, - string quantity = null, - string duration = null, + byte? quantity = null, MedicationCode code = null, int identifier = 0) - : base(nameOrDescription, posology, quantity, duration, code, identifier) + : base(nameOrDescription, posology, quantity, code, identifier) { } diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalSubstance.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalSubstance.cs index dd23cf0..5ca90f9 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalSubstance.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedPharmaceuticalSubstance.cs @@ -10,11 +10,10 @@ public class PrescribedPharmaceuticalSubstance : PrescribedMedication public PrescribedPharmaceuticalSubstance(string nameOrDescription, string posology = null, - string quantity = null, - string duration = null, + byte? quantity = null, MedicationCode code = null, int identifier = 0) - : base(nameOrDescription, posology, quantity, duration, code, identifier) + : base(nameOrDescription, posology, quantity, code, identifier) { } diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationMapping.cs index 74d602c..4a420bd 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationMapping.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationMapping.cs @@ -42,16 +42,6 @@ protected PrescribedMedicationMapping() m.Type(NHibernateUtil.AnsiString); m.Length(1024); }); - this.Property(med => med.Quantity, m => - { - m.Type(NHibernateUtil.AnsiString); - m.Length(100); - }); - this.Property(med => med.Duration, m => - { - m.Type(NHibernateUtil.AnsiString); - m.Length(100); - }); this.Component(med => med.Code, m => { m.Class(); diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Scripts/FindPrescribedMedicationsByPrescription.sql b/Src/DDD.HealthcareDelivery/Infrastructure/Scripts/FindPrescribedMedicationsByPrescription.sql index 091e56f1680e87839e92304f76d0407db7d6bf83..0b3877102f520de9838e05903fbee6b12b5fe9b9 100644 GIT binary patch delta 11 ScmaFCe1Lfa3*%%R#ytQTbOYc3 delta 26 hcmX@W{DOG{3nRY^Ln%WMLn1>7LncE$L*B&KR{>$~2nqlI diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/SqlScripts.Designer.cs b/Src/DDD.HealthcareDelivery/Infrastructure/SqlScripts.Designer.cs index ffd8ada..1950955 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/SqlScripts.Designer.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/SqlScripts.Designer.cs @@ -89,7 +89,6 @@ internal static string FindPharmaceuticalPrescriptionsByPatient { /// NameOrDesc AS NameOrDescription, /// Posology, /// Quantity, - /// Duration, /// Code ///FROM PrescMedication ///WHERE PrescriptionId = @PrescriptionId diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs index c4d174e..f4a1475 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs @@ -114,7 +114,7 @@ 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" } } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinderTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinderTests.cs index 70a7324..0af1988 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinderTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinderTests.cs @@ -44,8 +44,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 @@ -53,8 +52,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 } } @@ -69,8 +67,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" } } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.Designer.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.Designer.cs index f10606a..5e67a96 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", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class OracleScripts { @@ -114,7 +114,7 @@ internal static string FillSchema { /// SPCLEARSCHEMA(); ///END; //// - ///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, 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 +127,7 @@ internal static string FindPharmaceuticalPrescriptionsByPatient { /// SPCLEARSCHEMA(); ///END; //// - ///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, 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 +140,7 @@ internal static string FindPrescribedMedicationsByPrescription { /// SPCLEARSCHEMA(); ///END; //// - ///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, 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/Scripts/Oracle/FillSchema.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql index 0517530..e7b0dbc 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql @@ -73,8 +73,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 diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FindPrescribedMedicationsByPrescription.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FindPrescribedMedicationsByPrescription.sql index 73a5c347da10aa3612fe7a10c21f1571b6c5eebd..7edffa25f79e2e2a5f161e4ca8125a6e5dcba983 100644 GIT binary patch delta 89 zcmewrb}VGWKBdXq)MO^VQ<}zTxOuNKGb5P$L8Xb&aB`%!;^Zn0gGoyJfP(YT1!N{~ M(Lm9(O$(|C095E9&Hw-a delta 313 zcmX>W@+)k^J|%t^hEj$ihD3%EhD?ThhP=s*nu?QeDNWP%V^C)>WKdv80*bx^%BBL@ zDPYnV$Vy|#0qRKx>0l_Hyir_wa-H%wtY%G6X%dE+g`{h8p}6?wQ)+CCST)(Hk)i30 QI<;KA7?+DDpHhvSF delta 321 zcmX>U-4(atoGQNyLn%WMLn1>7LncE$L*C?AP4&rdRHteCF{m>bGAJ-40Y%>dWmAFd z6fkKFWTi3W0QDq;bTAZ8ekdS4xlh_;a*xI>tmf@eZxV)?hop1zLpJfv9vX6tSTwC9 WRfGCuA8qQoT1FSAt0#MCNC5zQGfr{< diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/RevokePharmaceuticalPrescription.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/RevokePharmaceuticalPrescription.sql index 009f814..bdaed57 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/RevokePharmaceuticalPrescription.sql +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/RevokePharmaceuticalPrescription.sql @@ -4,7 +4,7 @@ EXEC spClearDatabase 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], [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].[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.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj b/Test/DDD.HealthcareDelivery.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj index 56e2c82..0667970 100644 --- a/Test/DDD.HealthcareDelivery.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj +++ b/Test/DDD.HealthcareDelivery.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj @@ -1,6 +1,6 @@  - net472;netcoreapp3.0 + net472;netcoreapp3.1 Library DDD.HealthcareDelivery false From fa1541e424a816fb29572702dc322eae757d13d8 Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 23 Aug 2021 22:07:25 +0200 Subject: [PATCH 038/111] Move enums in application layer --- .../{Domain => Application}/Facilities/HealthFacilityType.cs | 2 +- .../Practitioners/HealthcarePractitionerType.cs | 2 +- .../Prescriptions/CreatePharmaceuticalPrescription.cs | 4 ++-- .../Prescriptions/PrescribedMedicationType.cs | 2 +- .../BelgianPharmaceuticalPrescriptionTranslator.cs | 2 ++ .../Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs | 4 ++-- 6 files changed, 9 insertions(+), 7 deletions(-) rename Src/DDD.HealthcareDelivery.Messages/{Domain => Application}/Facilities/HealthFacilityType.cs (60%) rename Src/DDD.HealthcareDelivery.Messages/{Domain => Application}/Practitioners/HealthcarePractitionerType.cs (55%) rename Src/DDD.HealthcareDelivery.Messages/{Domain => Application}/Prescriptions/PrescribedMedicationType.cs (64%) diff --git a/Src/DDD.HealthcareDelivery.Messages/Domain/Facilities/HealthFacilityType.cs b/Src/DDD.HealthcareDelivery.Messages/Application/Facilities/HealthFacilityType.cs similarity index 60% rename from Src/DDD.HealthcareDelivery.Messages/Domain/Facilities/HealthFacilityType.cs rename to Src/DDD.HealthcareDelivery.Messages/Application/Facilities/HealthFacilityType.cs index 1dd36ca..9fe6061 100644 --- a/Src/DDD.HealthcareDelivery.Messages/Domain/Facilities/HealthFacilityType.cs +++ b/Src/DDD.HealthcareDelivery.Messages/Application/Facilities/HealthFacilityType.cs @@ -1,4 +1,4 @@ -namespace DDD.HealthcareDelivery.Domain.Facilities +namespace DDD.HealthcareDelivery.Application.Facilities { public enum HealthFacilityType { 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 6441509..77a2231 100644 --- a/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/CreatePharmaceuticalPrescription.cs +++ b/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/CreatePharmaceuticalPrescription.cs @@ -5,8 +5,8 @@ namespace DDD.HealthcareDelivery.Application.Prescriptions { using Common.Application; using Core.Application; - using Domain.Facilities; - using Domain.Practitioners; + using Facilities; + using Practitioners; /// /// Encapsulates all information needed to create a pharmaceutical prescription. 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/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs b/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs index fb13591..be7bcfb 100644 --- a/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs +++ b/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs @@ -6,6 +6,8 @@ namespace DDD.HealthcareDelivery.Application.Prescriptions { using Common.Domain; + using Facilities; + using Practitioners; using Domain.Facilities; using Domain.Patients; using Domain.Practitioners; diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs index f4a1475..079a1ce 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs @@ -12,8 +12,8 @@ namespace DDD.HealthcareDelivery.Application.Prescriptions using Common.Application; using Core.Domain; using Core.Infrastructure.Data; - using Domain.Facilities; - using Domain.Practitioners; + using Facilities; + using Practitioners; using Domain.Prescriptions; using Infrastructure; From 64bc22be24a134f24acbf5c7863ea4d49b2b99b1 Mon Sep 17 00:00:00 2001 From: draphyz Date: Sun, 29 Aug 2021 18:30:00 +0200 Subject: [PATCH 039/111] Simplify the domain model --- .../NHibernateRepository.cs | 9 +- Src/DDD.Core.NHibernate/StoredEventMapping.cs | 16 ++-- .../Data}/StoredEvent.cs | 14 +-- .../Data}/StoredEventTranslator.cs | 22 ++++- .../Facilities/HealthFacilityType.cs | 8 -- .../CreatePharmaceuticalPrescription.cs | 13 +-- ...ianPharmaceuticalPrescriptionTranslator.cs | 39 +------- .../Domain/Encounters/EncounterIdentifier.cs | 34 +++++++ .../BelgianHealthFacilityLicenseNumber.cs | 56 ----------- .../Domain/Facilities/HealthFacility.cs | 58 ------------ .../Facilities/HealthFacilityLicenseNumber.cs | 19 ---- .../Domain/Facilities/Hospital.cs | 22 ----- .../Domain/Facilities/MedicalOffice.cs | 22 ----- .../PharmaceuticalPrescription.cs | 14 +-- .../Domain/Prescriptions/Prescription.cs | 11 +-- ...anOracleHealthcareDeliveryConfiguration.cs | 2 - ...qlServerHealthcareDeliveryConfiguration.cs | 2 - ...eatePharmaceuticalPrescriptionValidator.cs | 4 - .../HealthFacilityLicenseNumberType.cs | 22 ----- .../Prescriptions/HealthFacilityType.cs | 27 ------ .../OraclePrescriptionMapping.cs | 6 +- .../PrescribedMedicationMapping.cs | 1 + .../Prescriptions/PrescriptionMapping.cs | 36 ++----- .../SqlServerPrescriptionMapping.cs | 6 +- .../PharmaceuticalPrescriptionCreatorTests.cs | 5 +- .../HealthcareDeliveryConfigurationTests.cs | 12 +-- .../Infrastructure/IPersistenceFixture.cs | 1 + .../Scripts/Oracle/FillSchema.sql | 32 +++---- ...ndPharmaceuticalPrescriptionsByPatient.sql | 22 ++--- ...indPrescribedMedicationsByPrescription.sql | Bin 10822 -> 10852 bytes .../RevokePharmaceuticalPrescription.sql | 2 +- .../Scripts/SqlServer/CreateDatabase.sql | Bin 16568 -> 16370 bytes ...ndPharmaceuticalPrescriptionsByPatient.sql | 22 ++--- ...indPrescribedMedicationsByPrescription.sql | Bin 11714 -> 11790 bytes .../RevokePharmaceuticalPrescription.sql | 2 +- ...BelgianHealthFacilityLicenseNumberTests.cs | 55 ----------- .../PharmaceuticalPrescriptionTests.cs | 6 -- ...harmaceuticalPrescriptionValidatorTests.cs | 88 ------------------ 38 files changed, 146 insertions(+), 564 deletions(-) rename Src/DDD.Core/{Domain => Infrastructure/Data}/StoredEvent.cs (60%) rename Src/DDD.Core/{Domain => Infrastructure/Data}/StoredEventTranslator.cs (61%) delete mode 100644 Src/DDD.HealthcareDelivery.Messages/Application/Facilities/HealthFacilityType.cs create mode 100644 Src/DDD.HealthcareDelivery/Domain/Encounters/EncounterIdentifier.cs delete mode 100644 Src/DDD.HealthcareDelivery/Domain/Facilities/BelgianHealthFacilityLicenseNumber.cs delete mode 100644 Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacility.cs delete mode 100644 Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacilityLicenseNumber.cs delete mode 100644 Src/DDD.HealthcareDelivery/Domain/Facilities/Hospital.cs delete mode 100644 Src/DDD.HealthcareDelivery/Domain/Facilities/MedicalOffice.cs delete mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthFacilityLicenseNumberType.cs delete mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthFacilityType.cs delete mode 100644 Test/DDD.HealthcareDelivery.UnitTests/Domain/Facilities/BelgianHealthFacilityLicenseNumberTests.cs diff --git a/Src/DDD.Core.NHibernate/NHibernateRepository.cs b/Src/DDD.Core.NHibernate/NHibernateRepository.cs index 67ddcb1..136a490 100644 --- a/Src/DDD.Core.NHibernate/NHibernateRepository.cs +++ b/Src/DDD.Core.NHibernate/NHibernateRepository.cs @@ -72,15 +72,14 @@ public async Task SaveAsync(TDomainEntity aggregate) } private IEnumerable ToStoredEvents(TDomainEntity aggregate) - { - var commitId = Guid.NewGuid(); - var subject = Thread.CurrentPrincipal?.Identity?.Name; + { + var user = 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; + evt.UniqueId = Guid.NewGuid(); + evt.Username = user; return evt; }); } diff --git a/Src/DDD.Core.NHibernate/StoredEventMapping.cs b/Src/DDD.Core.NHibernate/StoredEventMapping.cs index a4d42d5..d4c8b2e 100644 --- a/Src/DDD.Core.NHibernate/StoredEventMapping.cs +++ b/Src/DDD.Core.NHibernate/StoredEventMapping.cs @@ -1,10 +1,9 @@ using NHibernate; using NHibernate.Mapping.ByCode.Conformist; +using NHibernate.Mapping.ByCode; namespace DDD.Core.Infrastructure.Data { - using Core.Domain; - public abstract class StoredEventMapping : ClassMapping { @@ -16,7 +15,11 @@ protected StoredEventMapping() // Table this.Table("Event"); // Keys - this.Id(e => e.Id, m1 => m1.Column("EventId")); + this.Id(e => e.Id, m1 => + { + m1.Column("EventId"); + m1.Generator(Generators.Sequence, m2 => m2.Params(new { sequence = "EventId" })); + }); // Fields this.Property(e => e.EventType, m => { @@ -24,21 +27,22 @@ protected StoredEventMapping() m.Length(50); m.NotNullable(true); }); + this.Property(e => e.Version); this.Property(e => e.StreamId, m => { m.Type(NHibernateUtil.AnsiString); m.Length(50); m.NotNullable(true); }); - this.Property(e => e.CommitId, m => m.NotNullable(true)); + this.Property(e => e.UniqueId, m => m.NotNullable(true)); this.Property(e => e.OccurredOn, m => m.Precision(2)); - this.Property(e => e.Subject, m => + this.Property(e => e.Username, m => { m.Type(NHibernateUtil.AnsiString); m.Length(100); }); this.Property(e => e.Body, m => m.NotNullable(true)); - this.Property(e => e.Dispatched); + this.Property(e => e.IsDispatched); } #endregion Constructors diff --git a/Src/DDD.Core/Domain/StoredEvent.cs b/Src/DDD.Core/Infrastructure/Data/StoredEvent.cs similarity index 60% rename from Src/DDD.Core/Domain/StoredEvent.cs rename to Src/DDD.Core/Infrastructure/Data/StoredEvent.cs index 2c9e365..4ff927c 100644 --- a/Src/DDD.Core/Domain/StoredEvent.cs +++ b/Src/DDD.Core/Infrastructure/Data/StoredEvent.cs @@ -1,6 +1,6 @@ using System; -namespace DDD.Core.Domain +namespace DDD.Core.Infrastructure.Data { public class StoredEvent { @@ -9,19 +9,21 @@ public class StoredEvent 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 bool IsDispatched { get; set; } = false; + public DateTime OccurredOn { get; set; } public string StreamId { get; set; } - public string Subject { get; set; } + public Guid UniqueId { get; set; } + + public string Username { get; set; } + + public byte Version { get; set; } = 1; #endregion Properties diff --git a/Src/DDD.Core/Domain/StoredEventTranslator.cs b/Src/DDD.Core/Infrastructure/Data/StoredEventTranslator.cs similarity index 61% rename from Src/DDD.Core/Domain/StoredEventTranslator.cs rename to Src/DDD.Core/Infrastructure/Data/StoredEventTranslator.cs index 036de56..26b9998 100644 --- a/Src/DDD.Core/Domain/StoredEventTranslator.cs +++ b/Src/DDD.Core/Infrastructure/Data/StoredEventTranslator.cs @@ -1,13 +1,16 @@ using Conditions; using System.Collections.Generic; +using System.Text.RegularExpressions; -namespace DDD.Core.Domain +namespace DDD.Core.Infrastructure.Data { + using Domain; using Mapping; - using Serialization; + using DDD.Serialization; public class StoredEventTranslator : IObjectTranslator { + #region Fields private readonly ITextSerializer eventSerializer; @@ -33,10 +36,25 @@ public StoredEvent Translate(IEvent @event, IDictionary options { OccurredOn = @event.OccurredOn, EventType = @event.GetType().Name, + Version = ToVersion(@event.GetType().FullName), Body = this.eventSerializer.SerializeToString(@event) }; } + private static byte ToVersion(string fullName) + { + byte version = 1; + var match = Regex.Match(fullName, @".(Version|V)\d+."); + if (match.Success) + { + var value = Regex.Replace(match.Value, "(Version|V)", string.Empty) + .Replace(".", string.Empty); + version = byte.Parse(value); + } + return version; + } + #endregion Methods + } } \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery.Messages/Application/Facilities/HealthFacilityType.cs b/Src/DDD.HealthcareDelivery.Messages/Application/Facilities/HealthFacilityType.cs deleted file mode 100644 index 9fe6061..0000000 --- a/Src/DDD.HealthcareDelivery.Messages/Application/Facilities/HealthFacilityType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace DDD.HealthcareDelivery.Application.Facilities -{ - public enum HealthFacilityType - { - MedicalOffice, - Hospital - } -} diff --git a/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/CreatePharmaceuticalPrescription.cs b/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/CreatePharmaceuticalPrescription.cs index 77a2231..7c2ded5 100644 --- a/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/CreatePharmaceuticalPrescription.cs +++ b/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/CreatePharmaceuticalPrescription.cs @@ -5,7 +5,6 @@ namespace DDD.HealthcareDelivery.Application.Prescriptions { using Common.Application; using Core.Application; - using Facilities; using Practitioners; /// @@ -20,13 +19,7 @@ public class CreatePharmaceuticalPrescription : ICommand 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,8 +84,8 @@ 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 diff --git a/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs b/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs index be7bcfb..393150b 100644 --- a/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs +++ b/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs @@ -6,12 +6,11 @@ namespace DDD.HealthcareDelivery.Application.Prescriptions { using Common.Domain; - using Facilities; using Practitioners; - using Domain.Facilities; using Domain.Patients; using Domain.Practitioners; using Domain.Prescriptions; + using Domain.Encounters; using Mapping; public class BelgianPharmaceuticalPrescriptionTranslator @@ -21,7 +20,7 @@ public class BelgianPharmaceuticalPrescriptionTranslator #region Methods public PharmaceuticalPrescription Translate(CreatePharmaceuticalPrescription command, - IDictionary options = null) + IDictionary options = null) { Condition.Requires(command, nameof(command)).IsNotNull(); return PharmaceuticalPrescription.Create @@ -29,10 +28,10 @@ public PharmaceuticalPrescription Translate(CreatePharmaceuticalPrescription com new PrescriptionIdentifier(command.PrescriptionIdentifier), ToPrescriber(command), ToPatient(command), - ToHealthFacility(command), command.Medications.Select(m => ToPrescribedMedication(m)), command.CreatedOn, new Alpha2LanguageCode(command.LanguageCode), + EncounterIdentifier.CreateIfNotEmpty(command.EncounterIdentifier), command.DeliverableAt ); } @@ -47,38 +46,6 @@ private static PrescribedPharmaceuticalCompounding ToCompounding(PrescribedMedic ); } - 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) - ); - } - - 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 diff --git a/Src/DDD.HealthcareDelivery/Domain/Encounters/EncounterIdentifier.cs b/Src/DDD.HealthcareDelivery/Domain/Encounters/EncounterIdentifier.cs new file mode 100644 index 0000000..e236ace --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Domain/Encounters/EncounterIdentifier.cs @@ -0,0 +1,34 @@ +using Conditions; + +namespace DDD.HealthcareDelivery.Domain.Encounters +{ + using Common.Domain; + + public class EncounterIdentifier : ArbitraryIdentifier + { + + #region Constructors + + public EncounterIdentifier(int value) : base(value) + { + Condition.Requires(value, nameof(value)).IsGreaterThan(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 94e067e..0000000 --- a/Src/DDD.HealthcareDelivery/Domain/Facilities/BelgianHealthFacilityLicenseNumber.cs +++ /dev/null @@ -1,56 +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)); - } - - protected BelgianHealthFacilityLicenseNumber() { } - - #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/HealthFacility.cs b/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacility.cs deleted file mode 100644 index 849b4d5..0000000 --- a/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacility.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Conditions; -using System.Collections.Generic; - -namespace DDD.HealthcareDelivery.Domain.Facilities -{ - using Core.Domain; - - /// - /// Represents any location where healthcare is provided. - /// - public abstract class HealthFacility : ValueObject - { - - #region Constructors - - protected HealthFacility() { } - - 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; private set; } - - public HealthFacilityLicenseNumber LicenseNumber { get; private set; } - - public string Name { get; private set; } - - #endregion Properties - - #region Methods - - public override IEnumerable EqualityComponents() - { - yield return this.Identifier; - yield return this.Name; - yield return this.LicenseNumber; - } - - 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 4a48c04..0000000 --- a/Src/DDD.HealthcareDelivery/Domain/Facilities/HealthFacilityLicenseNumber.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace DDD.HealthcareDelivery.Domain.Facilities -{ - using Common.Domain; - - public abstract class HealthFacilityLicenseNumber : IdentificationNumber - { - - #region Constructors - - protected HealthFacilityLicenseNumber() { } - - protected HealthFacilityLicenseNumber(string value) : base(value) - { - } - - #endregion Constructors - - } -} \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery/Domain/Facilities/Hospital.cs b/Src/DDD.HealthcareDelivery/Domain/Facilities/Hospital.cs deleted file mode 100644 index 61d14fc..0000000 --- a/Src/DDD.HealthcareDelivery/Domain/Facilities/Hospital.cs +++ /dev/null @@ -1,22 +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) - { - } - - protected Hospital() { } - - #endregion Constructors - } -} diff --git a/Src/DDD.HealthcareDelivery/Domain/Facilities/MedicalOffice.cs b/Src/DDD.HealthcareDelivery/Domain/Facilities/MedicalOffice.cs deleted file mode 100644 index 2905672..0000000 --- a/Src/DDD.HealthcareDelivery/Domain/Facilities/MedicalOffice.cs +++ /dev/null @@ -1,22 +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) - { - } - - protected MedicalOffice() { } - - #endregion Constructors - } -} diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs index 2534a55..11b706e 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs @@ -8,9 +8,9 @@ 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. @@ -29,14 +29,14 @@ public class PharmaceuticalPrescription : Prescription public PharmaceuticalPrescription(PrescriptionIdentifier identifier, HealthcarePractitioner prescriber, Patient patient, - HealthFacility healthFacility, IEnumerable prescribedMedications, Alpha2LanguageCode languageCode, PrescriptionStatus status, DateTime createdOn, + EncounterIdentifier encounterIdentifier = null, DateTime? delivrableAt = null, IEnumerable events = null) - : base(identifier, prescriber, patient, healthFacility, languageCode, status, createdOn, delivrableAt, events) + : base(identifier, prescriber, patient, languageCode, status, createdOn, encounterIdentifier, delivrableAt, events) { Condition.Requires(prescribedMedications, nameof(prescribedMedications)) .IsNotNull() @@ -54,10 +54,10 @@ protected PharmaceuticalPrescription() { } 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 +65,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,12 +79,12 @@ 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, DateTime.Now, languageCode, encounterIdentifier, delivrableAt); } public IEnumerable PrescribedMedications() => this.prescribedMedications.ToImmutableHashSet(); diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/Prescription.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/Prescription.cs index 70d84a9..446fb13 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/Prescription.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/Prescription.cs @@ -6,9 +6,9 @@ namespace DDD.HealthcareDelivery.Domain.Prescriptions { using Common.Domain; using Core.Domain; - using Facilities; using Patients; 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. @@ -23,10 +23,10 @@ protected Prescription() { } protected Prescription(PrescriptionIdentifier identifier, HealthcarePractitioner prescriber, Patient patient, - HealthFacility healthFacility, Alpha2LanguageCode languageCode, PrescriptionStatus status, DateTime createdOn, + EncounterIdentifier encounterIdentifier = null, DateTime? delivrableAt = null, IEnumerable events = null) : base(events) @@ -34,17 +34,16 @@ protected Prescription(PrescriptionIdentifier identifier, 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(); this.Identifier = identifier; this.Prescriber = prescriber; this.Patient = patient; - this.HealthFacility = healthFacility; + this.LanguageCode = languageCode; this.Status = status; this.CreatedOn = createdOn; + this.EncounterIdentifier = encounterIdentifier; this.DeliverableAt = delivrableAt; - this.LanguageCode = languageCode; } #endregion Constructors @@ -55,7 +54,7 @@ protected Prescription(PrescriptionIdentifier identifier, public DateTime? DeliverableAt { get; private set; } - public HealthFacility HealthFacility { get; private set; } + public EncounterIdentifier EncounterIdentifier { get; private set; } public PrescriptionIdentifier Identifier { get; private set; } diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/BelgianOracleHealthcareDeliveryConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/BelgianOracleHealthcareDeliveryConfiguration.cs index 45fff3e..caae2a5 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/BelgianOracleHealthcareDeliveryConfiguration.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/BelgianOracleHealthcareDeliveryConfiguration.cs @@ -5,7 +5,6 @@ namespace DDD.HealthcareDelivery.Infrastructure using Prescriptions; using Domain.Prescriptions; using Domain.Practitioners; - using Domain.Facilities; using Common.Domain; public class BelgianOracleHealthcareDeliveryConfiguration : OracleHealthcareDeliveryConfiguration @@ -25,7 +24,6 @@ protected override void AddMappings(ModelMapper modelMapper) { base.AddMappings(modelMapper); modelMapper.AddMapping>(); modelMapper.AddMapping>(); diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/BelgianSqlServerHealthcareDeliveryConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/BelgianSqlServerHealthcareDeliveryConfiguration.cs index 674f748..a903dc2 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/BelgianSqlServerHealthcareDeliveryConfiguration.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/BelgianSqlServerHealthcareDeliveryConfiguration.cs @@ -5,7 +5,6 @@ namespace DDD.HealthcareDelivery.Infrastructure using Prescriptions; using Domain.Prescriptions; using Domain.Practitioners; - using Domain.Facilities; using Common.Domain; public class BelgianSqlServerHealthcareDeliveryConfiguration : SqlServerHealthcareDeliveryConfiguration @@ -25,7 +24,6 @@ protected override void AddMappings(ModelMapper modelMapper) { base.AddMappings(modelMapper); modelMapper.AddMapping>(); modelMapper.AddMapping>(); 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/HealthFacilityLicenseNumberType.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthFacilityLicenseNumberType.cs deleted file mode 100644 index ed7dd2d..0000000 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthFacilityLicenseNumberType.cs +++ /dev/null @@ -1,22 +0,0 @@ -using NHibernate; - -namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions -{ - using Core.Infrastructure.Data; - using Domain.Facilities; - - internal class HealthFacilityLicenseNumberType : CompositeUserType - where T : HealthFacilityLicenseNumber - { - - #region Constructors - - public HealthFacilityLicenseNumberType() - { - this.Mutable(false); - this.Property(p => p.Value, NHibernateUtil.AnsiString); - } - - #endregion Constructors - } -} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthFacilityType.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthFacilityType.cs deleted file mode 100644 index 7e0ac0b..0000000 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/HealthFacilityType.cs +++ /dev/null @@ -1,27 +0,0 @@ -using NHibernate; - -namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions -{ - using Core.Infrastructure.Data; - using Domain.Facilities; - - internal class HealthFacilityType : CompositeUserType - where TFacilityLicenseNumber : HealthFacilityLicenseNumber - { - #region Constructors - - public HealthFacilityType() - { - this.Mutable(false); - this.Property(p => p.Identifier); - this.Discriminator("FacilityType"); - this.Property(p => p.Name, NHibernateUtil.AnsiString); - this.Property(p => p.LicenseNumber, NHibernateUtil.Custom(typeof(HealthFacilityLicenseNumberType))); - this.Subclass(); - this.Subclass(); - } - - #endregion Constructors - - } -} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionMapping.cs index 730b884..6f7292d 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionMapping.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/OraclePrescriptionMapping.cs @@ -2,12 +2,10 @@ { using Common.Domain; using Domain.Practitioners; - using Domain.Facilities; - internal class OraclePrescriptionMapping - : PrescriptionMapping + internal class OraclePrescriptionMapping + : PrescriptionMapping where TPractitionerLicenseNumber : HealthcarePractitionerLicenseNumber - where TFacilityLicenseNumber : HealthFacilityLicenseNumber where TSocialSecurityNumber : SocialSecurityNumber where TSex : Sex { diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationMapping.cs index 4a420bd..528e006 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationMapping.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationMapping.cs @@ -42,6 +42,7 @@ protected PrescribedMedicationMapping() m.Type(NHibernateUtil.AnsiString); m.Length(1024); }); + this.Property(med => med.Quantity); this.Component(med => med.Code, m => { m.Class(); diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs index b20393f..7d2b606 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionMapping.cs @@ -8,13 +8,11 @@ namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions using Common.Infrastructure.Data; using Domain.Prescriptions; using Domain.Practitioners; - using Domain.Facilities; - internal abstract class PrescriptionMapping + internal abstract class PrescriptionMapping : ClassMapping where TPractitionerLicenseNumber : HealthcarePractitionerLicenseNumber - where TFacilityLicenseNumber : HealthFacilityLicenseNumber where TSocialSecurityNumber : SocialSecurityNumber where TSex : Sex { @@ -220,34 +218,12 @@ protected PrescriptionMapping() m2.Type(NHibernateUtil.Date); }); }); - // Facility - this.Property(p => p.HealthFacility, m => + // Encounter + this.Component(p => p.EncounterIdentifier, m1 => + m1.Property(i => i.Value, m2 => { - m.Type>(); - m.Columns - (m1 => - { - m1.Name("FacilityId"); - m1.NotNullable(true); - }, - m1 => - { - m1.Name("FacilityType"); - m1.Length(20); - m1.NotNullable(true); - }, - m1 => - { - m1.Name("FacilityName"); - m1.Length(100); - m1.NotNullable(true); - }, - m1 => - { - m1.Name("FacilityLicenseNum"); - m1.Length(25); - }); - }); + m2.Column("EncounterId"); + })); } #endregion Constructors diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescriptionMapping.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescriptionMapping.cs index f2be6bf..338bacb 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescriptionMapping.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/SqlServerPrescriptionMapping.cs @@ -1,13 +1,11 @@ namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions { using Common.Domain; - using Domain.Facilities; using Domain.Practitioners; - internal class SqlServerPrescriptionMapping - : PrescriptionMapping + internal class SqlServerPrescriptionMapping + : PrescriptionMapping where TPractitionerLicenseNumber : HealthcarePractitionerLicenseNumber - where TFacilityLicenseNumber : HealthFacilityLicenseNumber where TSocialSecurityNumber : SocialSecurityNumber where TSex : Sex { diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs index 079a1ce..c01f56c 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs @@ -12,7 +12,6 @@ namespace DDD.HealthcareDelivery.Application.Prescriptions using Common.Application; using Core.Domain; using Core.Infrastructure.Data; - using Facilities; using Practitioners; using Domain.Prescriptions; using Infrastructure; @@ -101,11 +100,9 @@ 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), + EncounterIdentifier = 1, DeliverableAt = new DateTime(2018, 2, 1), Medications = new PrescribedMedicationDescriptor[] { diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareDeliveryConfigurationTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareDeliveryConfigurationTests.cs index 3c73cea..91ba8f7 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareDeliveryConfigurationTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareDeliveryConfigurationTests.cs @@ -7,8 +7,7 @@ namespace DDD.HealthcareDelivery.Infrastructure { using Common.Domain; - using Core.Domain; - using Domain.Facilities; + using Core.Infrastructure.Data; using Domain.Patients; using Domain.Practitioners; using Domain.Prescriptions; @@ -118,8 +117,8 @@ private static StoredEvent CreateEvent() Id = 1, Body = @"12018-01-01T10:06:00", StreamId = "1", - CommitId = Guid.NewGuid(), - Subject = "draphyz", + UniqueId = Guid.NewGuid(), + Username = "draphyz", EventType = "PharmaceuticalPrescriptionCreated", OccurredOn = new DateTime(2018, 1, 1) }; @@ -157,11 +156,6 @@ private static PharmaceuticalPrescription CreatePrescription() BelgianSex.Male, new BelgianSocialSecurityNumber("60207273601") ), - new MedicalOffice - ( - 1, - "Medical Office Donald Duck" - ), new PrescribedMedication[] { new PrescribedPharmaceuticalProduct diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/IPersistenceFixture.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/IPersistenceFixture.cs index 25a9f17..066107a 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/IPersistenceFixture.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/IPersistenceFixture.cs @@ -3,6 +3,7 @@ namespace DDD.HealthcareDelivery.Infrastructure { using Core.Domain; + using Core.Infrastructure.Data; using Core.Infrastructure.Testing; using Mapping; diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql index e7b0dbc..6e4eec7 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql @@ -46,12 +46,13 @@ END SPCLEARSCHEMA; CREATE TABLE TEST.EVENT ( EVENTID NUMBER(19,0), EVENTTYPE VARCHAR2(50 CHAR), - STREAMID VARCHAR2(50 CHAR), - COMMITID RAW(16), + VERSION NUMBER(3, 0), + STREAMID VARCHAR2(50 CHAR), + UNIQUEID RAW(16), OCCURREDON TIMESTAMP (6), - SUBJECT VARCHAR2(100 CHAR), + USERNAME VARCHAR2(100 CHAR), BODY XMLTYPE, - DISPATCHED NUMBER(1,0) + ISDISPATCHED 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 @@ -116,11 +117,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 @@ -183,17 +181,20 @@ END SPCLEARSCHEMA; ALTER TABLE TEST.EVENT MODIFY (EVENTTYPE NOT NULL ENABLE) / + + ALTER TABLE TEST.EVENT MODIFY (VERSION 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 (UNIQUEID 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 (ISDISPATCHED NOT NULL ENABLE) / ALTER TABLE TEST.EVENT MODIFY (BODY NOT NULL ENABLE) @@ -275,15 +276,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 845a71d..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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 7edffa25f79e2e2a5f161e4ca8125a6e5dcba983..48c1dc79f5432552b7df4e26a9f27286e702bd7a 100644 GIT binary patch delta 1018 zcmX>W@+4%#EEZ8$hCGI3hJ1!nAYHCHq+*rx1DcVGU2gJw zY0=FaSiUh$?vv+;^aUzP0a}y{)R4pA&yWU$AYLkiI@n-8AdRX<0Y#Mpg9}4GLmpUn z3eb@*45dKb*$nEF7izd}-XN$#7ynG&z#=kvoeJCJV{&sg-{Y-en%pDLHH`eW`Jbp9 X^KkRi0XbUxY4bu2dB)8@lycYsqqMTO diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/RevokePharmaceuticalPrescription.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/RevokePharmaceuticalPrescription.sql index fff569b..8995718 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/RevokePharmaceuticalPrescription.sql +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/RevokePharmaceuticalPrescription.sql @@ -2,7 +2,7 @@ BEGIN SPCLEARSCHEMA(); END; / -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, 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, Code) VALUES (1, 1, N'Product', N'Latansoc Mylan Coll. 2,5 ml X 3', N'1 goutte le soir', 1, NULL) / diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql index ca70c78257cdbaa4e46170e1bed3af9e2aa57bb7..5ab83d30a90643ec772162b4699d4bcb18943d4e 100644 GIT binary patch delta 108 zcmdnd$oQ##!xSanFoslyB8FmyOon`hyve`UlVh~F KHuG6LVFv(4xGG-& diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/FindPharmaceuticalPrescriptionsByPatient.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/FindPharmaceuticalPrescriptionsByPatient.sql index 2903bdf..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], [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], [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], [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], [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], [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], [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], [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], [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], [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], [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], [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], [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 5000c7e4f67cf0ef6d9e972082256c4614e14361..2d56e5b8c53302854e7c0e02a9647995b80fe482 100644 GIT binary patch delta 1098 zcmX>U-50Y#fmPI%A&()MA)lcXNS83AG88d*GNepye@o)Qlk h4O(C-HYFoy@ugXC;Qmnn2d>TJ9*rNH->6<=2LLU8Lk<7{ delta 855 zcmeB+ITXD?fz{KEA(0`OA(J5oh)WnM89W(M7-AW87!(+y8PMfIfbs 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("")] From 8d863afa9cd425399fa18ed1fd8c463fdd980d08 Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 27 Sep 2021 15:28:22 +0200 Subject: [PATCH 040/111] Update target frameworks and packages --- DDD.sln | 42 +++++++++---------- .../DDD.Common.Messages.csproj | 2 +- .../DDD.Common.NHibernate.csproj | 4 +- Src/DDD.Common/DDD.Common.csproj | 2 +- .../DDD.Core.Abstractions.csproj | 2 +- Src/DDD.Core.Dapper/DDD.Core.Dapper.csproj | 4 +- .../DDD.Core.FluentValidation.csproj | 6 +-- .../FluentValidatorAdapter.cs | 16 +++++-- .../FluentValidatorExtensions.cs | 14 +++---- .../ValidationResultTranslator.cs | 2 +- .../Validators/AlphaNumericValidator.cs | 21 ++++------ .../Validators/AlphabeticValidator.cs | 21 ++++------ .../Validators/CountValidator.cs | 37 ++++++++++------ .../Validators/ExactCountValidator.cs | 23 ++++++++-- .../Validators/MaximumCountValidator.cs | 23 ++++++++-- .../Validators/MinimumCountValidator.cs | 23 ++++++++-- .../Validators/NumericValidator.cs | 22 +++++----- .../DDD.Core.Messages.csproj | 2 +- .../DDD.Core.NHibernate.csproj | 4 +- .../DDD.Core.NServiceBus.csproj | 4 +- .../DDD.Core.Newtonsoft.csproj | 4 +- Src/DDD.Core.Polly/DDD.Core.Polly.csproj | 4 +- .../DDD.Core.SimpleInjector.csproj | 4 +- Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj | 2 +- Src/DDD.Core/DDD.Core.csproj | 4 +- .../DDD.HealthcareDelivery.Messages.csproj | 2 +- .../DDD.HealthcareDelivery.csproj | 12 +++--- .../DDD.Common.UnitTests.csproj | 6 +-- .../Domain/EnumerationTests.cs | 2 +- .../DDD.Core.Abstractions.UnitTests.csproj | 6 +-- .../runtimeconfig.template.json | 5 +++ .../DDD.Core.UnitTests.csproj | 6 +-- ...HealthcareDelivery.IntegrationTests.csproj | 11 ++--- .../Infrastructure/OracleFixture.cs | 4 +- .../Infrastructure/SqlServerFixture.cs | 4 +- .../DDD.HealthcareDelivery.UnitTests.csproj | 10 ++--- 36 files changed, 214 insertions(+), 146 deletions(-) create mode 100644 Test/DDD.Core.Abstractions.UnitTests/runtimeconfig.template.json diff --git a/DDD.sln b/DDD.sln index c78dfed..c523d6d 100644 --- a/DDD.sln +++ b/DDD.sln @@ -5,31 +5,31 @@ VisualStudioVersion = 16.0.29519.181 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.HealthcareDelivery.IntegrationTests", "Test\DDD.HealthcareDelivery.IntegrationTests\DDD.HealthcareDelivery.IntegrationTests.csproj", "{B53007C7-B314-40DF-B6E7-6C6576A5611C}" +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.Core.FluentValidation", "Src\DDD.Core.FluentValidation\DDD.Core.FluentValidation.csproj", "{5E3745FC-CA80-4D0F-8A25-20EE0F9CF163}" +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.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 @@ -40,28 +40,28 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Doc", "Doc", "{862B0E9C-B68 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("{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.HealthcareDelivery.Messages", "Src\DDD.HealthcareDelivery.Messages\DDD.HealthcareDelivery.Messages.csproj", "{B8BB212C-8AFC-4258-A023-EB1F6937F53D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Common.Messages", "Src\DDD.Common.Messages\DDD.Common.Messages.csproj", "{40A849C5-C8D7-4F76-856A-138AED73A6C3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Common.Messages", "Src\DDD.Common.Messages\DDD.Common.Messages.csproj", "{40A849C5-C8D7-4F76-856A-138AED73A6C3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.NServiceBus", "Src\DDD.Core.NServiceBus\DDD.Core.NServiceBus.csproj", "{436D869F-7566-4436-90A8-B655145C5BCA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.NServiceBus", "Src\DDD.Core.NServiceBus\DDD.Core.NServiceBus.csproj", "{436D869F-7566-4436-90A8-B655145C5BCA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.NHibernate", "Src\DDD.Core.NHibernate\DDD.Core.NHibernate.csproj", "{D4FC7E1B-CAE8-40F9-9FAF-0029D91CC6C4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.NHibernate", "Src\DDD.Core.NHibernate\DDD.Core.NHibernate.csproj", "{D4FC7E1B-CAE8-40F9-9FAF-0029D91CC6C4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Common.NHibernate", "Src\DDD.Common.NHibernate\DDD.Common.NHibernate.csproj", "{7466B831-1642-4B2B-9EA3-1C9299596C2A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Common.NHibernate", "Src\DDD.Common.NHibernate\DDD.Common.NHibernate.csproj", "{7466B831-1642-4B2B-9EA3-1C9299596C2A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.SimpleInjector", "Src\DDD.Core.SimpleInjector\DDD.Core.SimpleInjector.csproj", "{3EFAACD8-CF5E-4E31-884B-6B9F87F1E495}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.SimpleInjector", "Src\DDD.Core.SimpleInjector\DDD.Core.SimpleInjector.csproj", "{3EFAACD8-CF5E-4E31-884B-6B9F87F1E495}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.Core.Polly", "Src\DDD.Core.Polly\DDD.Core.Polly.csproj", "{4878A4FB-2C1E-4C06-BB42-AFE1ACE2A882}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.Polly", "Src\DDD.Core.Polly\DDD.Core.Polly.csproj", "{4878A4FB-2C1E-4C06-BB42-AFE1ACE2A882}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/Src/DDD.Common.Messages/DDD.Common.Messages.csproj b/Src/DDD.Common.Messages/DDD.Common.Messages.csproj index a54e40e..8d24737 100644 --- a/Src/DDD.Common.Messages/DDD.Common.Messages.csproj +++ b/Src/DDD.Common.Messages/DDD.Common.Messages.csproj @@ -1,6 +1,6 @@  - net472;netstandard2.1 + net48;netstandard2.1;net5.0 Library DDD.Common false diff --git a/Src/DDD.Common.NHibernate/DDD.Common.NHibernate.csproj b/Src/DDD.Common.NHibernate/DDD.Common.NHibernate.csproj index 3ff57db..54a48f0 100644 --- a/Src/DDD.Common.NHibernate/DDD.Common.NHibernate.csproj +++ b/Src/DDD.Common.NHibernate/DDD.Common.NHibernate.csproj @@ -1,7 +1,7 @@  DDD.Common.Infrastructure.Data - net472;netstandard2.1 + net48;netstandard2.1;net5.0 false bin\$(Configuration)\ @@ -13,7 +13,7 @@ - + \ No newline at end of file diff --git a/Src/DDD.Common/DDD.Common.csproj b/Src/DDD.Common/DDD.Common.csproj index 45d3d6e..3e15d80 100644 --- a/Src/DDD.Common/DDD.Common.csproj +++ b/Src/DDD.Common/DDD.Common.csproj @@ -1,6 +1,6 @@  - net472;netstandard2.1 + net48;netstandard2.1;net5.0 Library false diff --git a/Src/DDD.Core.Abstractions/DDD.Core.Abstractions.csproj b/Src/DDD.Core.Abstractions/DDD.Core.Abstractions.csproj index cd0388d..0100b0c 100644 --- a/Src/DDD.Core.Abstractions/DDD.Core.Abstractions.csproj +++ b/Src/DDD.Core.Abstractions/DDD.Core.Abstractions.csproj @@ -1,6 +1,6 @@  - net472;netstandard2.1 + net48;netstandard2.1;net5.0 Library DDD false diff --git a/Src/DDD.Core.Dapper/DDD.Core.Dapper.csproj b/Src/DDD.Core.Dapper/DDD.Core.Dapper.csproj index d3f29c4..f09ac92 100644 --- a/Src/DDD.Core.Dapper/DDD.Core.Dapper.csproj +++ b/Src/DDD.Core.Dapper/DDD.Core.Dapper.csproj @@ -1,6 +1,6 @@  - net472;netstandard2.1 + net48;netstandard2.1;net5.0 Library DDD.Core.Infrastructure.Data false @@ -25,7 +25,7 @@ - 2.0.35 + 2.0.78 diff --git a/Src/DDD.Core.FluentValidation/DDD.Core.FluentValidation.csproj b/Src/DDD.Core.FluentValidation/DDD.Core.FluentValidation.csproj index 3267ac6..0060991 100644 --- a/Src/DDD.Core.FluentValidation/DDD.Core.FluentValidation.csproj +++ b/Src/DDD.Core.FluentValidation/DDD.Core.FluentValidation.csproj @@ -1,6 +1,6 @@  - net472;netstandard2.1 + net48;netstandard2.1;net5.0 Library DDD.Core.Infrastructure.Validation false @@ -26,13 +26,13 @@ - 9.1.2 + 10.3.3 4.5.0 - + \ No newline at end of file diff --git a/Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs b/Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs index d4525b9..d337f76 100644 --- a/Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs +++ b/Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs @@ -8,7 +8,7 @@ namespace DDD.Core.Infrastructure.Validation using Mapping; using Threading; - public class FluentValidatorAdapter + public class FluentValidatorAdapter : DDD.Validation.IObjectValidator, DDD.Validation.IAsyncObjectValidator where T : class { @@ -36,7 +36,11 @@ public FluentValidatorAdapter(IValidator fluentValidator) /// The rule set. public DDD.Validation.ValidationResult Validate(T obj, string ruleSet = null) { - var result = this.fluentValidator.Validate(obj, ruleSet: ruleSet); + ValidationResult result; + if (string.IsNullOrWhiteSpace(ruleSet)) + result = this.fluentValidator.Validate(obj); + else + result = this.fluentValidator.Validate(obj, options => options.IncludeRuleSets(ruleSet.Split(','))); return this.resultTranslator.Translate(result, new { ObjectName = obj.GetType().Name }); } @@ -48,11 +52,15 @@ public DDD.Validation.ValidationResult Validate(T obj, string ruleSet = null) public async Task ValidateAsync(T obj, string ruleSet = null) { await new SynchronizationContextRemover(); - var result = await this.fluentValidator.ValidateAsync(obj, ruleSet: ruleSet); + ValidationResult result; + if (string.IsNullOrWhiteSpace(ruleSet)) + result = await this.fluentValidator.ValidateAsync(obj); + else + result = await this.fluentValidator.ValidateAsync(obj, options => options.IncludeRuleSets(ruleSet.Split(','))); return this.resultTranslator.Translate(result, new { ObjectName = obj.GetType().Name }); } #endregion Methods } -} +} \ No newline at end of file diff --git a/Src/DDD.Core.FluentValidation/FluentValidatorExtensions.cs b/Src/DDD.Core.FluentValidation/FluentValidatorExtensions.cs index 5d5fa79..da58341 100644 --- a/Src/DDD.Core.FluentValidation/FluentValidatorExtensions.cs +++ b/Src/DDD.Core.FluentValidation/FluentValidatorExtensions.cs @@ -15,7 +15,7 @@ public static class FluentValidatorExtensions /// public static IRuleBuilderOptions Alphabetic(this IRuleBuilder ruleBuilder) { - return ruleBuilder.SetValidator(new AlphabeticValidator()); + return ruleBuilder.SetValidator(new AlphabeticValidator()); } /// @@ -24,7 +24,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 +33,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 +42,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 +51,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 +60,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,7 +69,7 @@ public static IRuleBuilderOptions MinimumCount(this IRuleBuil /// public static IRuleBuilderOptions Numeric(this IRuleBuilder ruleBuilder) { - return ruleBuilder.SetValidator(new NumericValidator()); + return ruleBuilder.SetValidator(new NumericValidator()); } #endregion Methods diff --git a/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs b/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs index 848d963..f2205ba 100644 --- a/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs +++ b/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs @@ -42,4 +42,4 @@ private static DDD.Validation.ValidationFailure ToFailure(ValidationFailure fail #endregion Methods } -} +} \ No newline at end of file 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..0bdea0d 100644 --- a/Src/DDD.Core.FluentValidation/Validators/CountValidator.cs +++ b/Src/DDD.Core.FluentValidation/Validators/CountValidator.cs @@ -1,17 +1,17 @@ using System.Linq; using System.Collections; +using FluentValidation; using FluentValidation.Validators; using Conditions; 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); if (max.HasValue) @@ -20,17 +20,22 @@ public CountValidator(int min, int? max, string errorMessage) : base(errorMessag 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..f32302a 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..e21afe6 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.Messages/DDD.Core.Messages.csproj b/Src/DDD.Core.Messages/DDD.Core.Messages.csproj index f3f6e5e..f25553d 100644 --- a/Src/DDD.Core.Messages/DDD.Core.Messages.csproj +++ b/Src/DDD.Core.Messages/DDD.Core.Messages.csproj @@ -1,6 +1,6 @@  - net472;netstandard2.1 + net48;netstandard2.1;net5.0 Library DDD.Core false diff --git a/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj b/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj index 0b3dbca..a425f2f 100644 --- a/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj +++ b/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj @@ -1,6 +1,6 @@  - net472;netstandard2.1 + net48;netstandard2.1;net5.0 Library DDD.Core.Infrastructure.Data false @@ -17,6 +17,6 @@ - + \ No newline at end of file diff --git a/Src/DDD.Core.NServiceBus/DDD.Core.NServiceBus.csproj b/Src/DDD.Core.NServiceBus/DDD.Core.NServiceBus.csproj index e118ff5..7bee749 100644 --- a/Src/DDD.Core.NServiceBus/DDD.Core.NServiceBus.csproj +++ b/Src/DDD.Core.NServiceBus/DDD.Core.NServiceBus.csproj @@ -1,6 +1,6 @@  - net472;netstandard2.1 + net48;netstandard2.1;net5.0 Library DDD.Core.Infrastructure.Messaging false @@ -27,7 +27,7 @@ - 7.3.0 + 7.5.0 \ No newline at end of file diff --git a/Src/DDD.Core.Newtonsoft/DDD.Core.Newtonsoft.csproj b/Src/DDD.Core.Newtonsoft/DDD.Core.Newtonsoft.csproj index d6b47f9..8272470 100644 --- a/Src/DDD.Core.Newtonsoft/DDD.Core.Newtonsoft.csproj +++ b/Src/DDD.Core.Newtonsoft/DDD.Core.Newtonsoft.csproj @@ -1,6 +1,6 @@  - net472;netstandard2.1 + net48;netstandard2.1;net5.0 Library DDD.Core.Infrastructure.Serialization false @@ -17,7 +17,7 @@ - 12.0.3 + 13.0.1 diff --git a/Src/DDD.Core.Polly/DDD.Core.Polly.csproj b/Src/DDD.Core.Polly/DDD.Core.Polly.csproj index 97d7a58..2e336fe 100644 --- a/Src/DDD.Core.Polly/DDD.Core.Polly.csproj +++ b/Src/DDD.Core.Polly/DDD.Core.Polly.csproj @@ -1,6 +1,6 @@  - net472;netstandard2.1 + net48;netstandard2.1;net5.0 Library DDD.Core.Infrastructure.ErrorHandling false @@ -26,7 +26,7 @@ - 7.2.1 + 7.2.2 \ No newline at end of file diff --git a/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj b/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj index 37f0971..8b6b7f4 100644 --- a/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj +++ b/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj @@ -1,6 +1,6 @@  - net472;netstandard2.1 + net48;netstandard2.1;net5.0 Library DDD.Core.Infrastructure.DependencyInjection false @@ -27,7 +27,7 @@ - 5.0.3 + 5.3.2 \ No newline at end of file diff --git a/Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj b/Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj index 483c58a..2fe72bc 100644 --- a/Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj +++ b/Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj @@ -1,6 +1,6 @@  - net472;netstandard2.1 + net48;netstandard2.1;net5.0 Library DDD.Core.Infrastructure.Testing false diff --git a/Src/DDD.Core/DDD.Core.csproj b/Src/DDD.Core/DDD.Core.csproj index 54cf39c..dcb2f12 100644 --- a/Src/DDD.Core/DDD.Core.csproj +++ b/Src/DDD.Core/DDD.Core.csproj @@ -1,6 +1,6 @@  - net472;netstandard2.1 + net48;netstandard2.1;net5.0 Library false @@ -25,7 +25,7 @@ - 3.1.7 + 5.0.0 \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery.Messages/DDD.HealthcareDelivery.Messages.csproj b/Src/DDD.HealthcareDelivery.Messages/DDD.HealthcareDelivery.Messages.csproj index 40704af..7715044 100644 --- a/Src/DDD.HealthcareDelivery.Messages/DDD.HealthcareDelivery.Messages.csproj +++ b/Src/DDD.HealthcareDelivery.Messages/DDD.HealthcareDelivery.Messages.csproj @@ -1,6 +1,6 @@  - net472;netstandard2.1 + net48;netstandard2.1;net5.0 Library DDD.HealthcareDelivery false diff --git a/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj b/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj index 2da7d59..e771091 100644 --- a/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj +++ b/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj @@ -1,6 +1,6 @@  - net472;netstandard2.1 + net48;netstandard2.1;net5.0 false bin\$(Configuration)\ bin\$(Configuration)\DDD.HealthcareDelivery.xml @@ -28,12 +28,12 @@ - - + + - - - + + + diff --git a/Test/DDD.Common.UnitTests/DDD.Common.UnitTests.csproj b/Test/DDD.Common.UnitTests/DDD.Common.UnitTests.csproj index b9e4131..1c9a2bd 100644 --- a/Test/DDD.Common.UnitTests/DDD.Common.UnitTests.csproj +++ b/Test/DDD.Common.UnitTests/DDD.Common.UnitTests.csproj @@ -1,6 +1,6 @@  - net472;netcoreapp3.1 + net48;netcoreapp3.1;net5.0 Library DDD.Common false @@ -14,10 +14,10 @@ - 5.10.3 + 6.1.0 - + 4.5.0 diff --git a/Test/DDD.Common.UnitTests/Domain/EnumerationTests.cs b/Test/DDD.Common.UnitTests/Domain/EnumerationTests.cs index 36f590b..179b2e3 100644 --- a/Test/DDD.Common.UnitTests/Domain/EnumerationTests.cs +++ b/Test/DDD.Common.UnitTests/Domain/EnumerationTests.cs @@ -115,7 +115,7 @@ public void All_WhenCalled_ReturnsAllConstants() // Act var all = Enumeration.All(); // Assert - all.Should().BeEquivalentTo(FakeEnumeration.Fake1, FakeEnumeration.Fake2, FakeEnumeration.Fake3); + all.Should().BeEquivalentTo(new[] { FakeEnumeration.Fake1, FakeEnumeration.Fake2, FakeEnumeration.Fake3 }); } [Theory] 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 7724202..98f4d97 100644 --- a/Test/DDD.Core.Abstractions.UnitTests/DDD.Core.Abstractions.UnitTests.csproj +++ b/Test/DDD.Core.Abstractions.UnitTests/DDD.Core.Abstractions.UnitTests.csproj @@ -1,6 +1,6 @@  - net472;netcoreapp3.1 + net48;netcoreapp3.1;net5.0 Library DDD false @@ -13,10 +13,10 @@ - 5.10.3 + 6.1.0 - + 4.2.2 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.UnitTests/DDD.Core.UnitTests.csproj b/Test/DDD.Core.UnitTests/DDD.Core.UnitTests.csproj index 9b9a037..676d92a 100644 --- a/Test/DDD.Core.UnitTests/DDD.Core.UnitTests.csproj +++ b/Test/DDD.Core.UnitTests/DDD.Core.UnitTests.csproj @@ -1,6 +1,6 @@  - net472;netcoreapp3.1 + net48;netcoreapp3.1;net5.0 Library DDD.Core false @@ -15,10 +15,10 @@ - 5.10.3 + 6.1.0 - + 4.2.2 diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj b/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj index b2fd684..7ebbb34 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj @@ -1,6 +1,6 @@  - net472;netcoreapp3.1 + net48;netcoreapp3.1;net5.0 Library DDD.HealthcareDelivery false @@ -51,18 +51,19 @@ - 5.10.3 + 6.1.0 - - + + + all - + diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleFixture.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleFixture.cs index 5bb4a39..2848403 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleFixture.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleFixture.cs @@ -2,7 +2,7 @@ using System.Text; using NHibernate; using Oracle.ManagedDataAccess.Client; -#if NETCOREAPP3_1 +#if (NETCOREAPP3_1 || NET5_0) using System.Data.Common; #endif @@ -57,7 +57,7 @@ protected override int[] ExecuteScript(string script, IDbConnection connection) protected override void RegisterDbProviderFactory() { -#if NETCOREAPP3_1 +#if (NETCOREAPP3_1 || NET5_0) DbProviderFactories.RegisterFactory("Oracle.ManagedDataAccess.Client", OracleClientFactory.Instance); #endif } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerFixture.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerFixture.cs index d9a5d11..179d2d1 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerFixture.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerFixture.cs @@ -2,7 +2,7 @@ using NHibernate; using System.Data; using System.Text; -#if NETCOREAPP3_1 +#if (NETCOREAPP3_1 || NET5_0) using System.Data.Common; #endif @@ -55,7 +55,7 @@ protected override int[] ExecuteScript(string script, IDbConnection connection) protected override void RegisterDbProviderFactory() { -#if NETCOREAPP3_1 +#if (NETCOREAPP3_1 || NET5_0) DbProviderFactories.RegisterFactory("Microsoft.Data.SqlClient", SqlClientFactory.Instance); #endif } diff --git a/Test/DDD.HealthcareDelivery.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj b/Test/DDD.HealthcareDelivery.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj index 0667970..1e7bbbc 100644 --- a/Test/DDD.HealthcareDelivery.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj +++ b/Test/DDD.HealthcareDelivery.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj @@ -1,6 +1,6 @@  - net472;netcoreapp3.1 + net48;netcoreapp3.1;net5.0 Library DDD.HealthcareDelivery false @@ -20,17 +20,17 @@ - 5.10.3 + 6.1.0 - 9.1.2 + 10.3.3 - + 4.5.0 - + From 71404fea90b60b4713eec1a117aa19b81ff178dc Mon Sep 17 00:00:00 2001 From: draphyz Date: Tue, 28 Sep 2021 14:01:25 +0200 Subject: [PATCH 041/111] Use unique timestamps for events --- .../TimeStampProvider.cs | 79 ++++++++++++++++++ Src/DDD.Core.NHibernate/StoredEventMapping.cs | 2 +- .../CreatePharmaceuticalPrescription.cs | 2 +- .../PharmaceuticalPrescriptionCreated.cs | 6 +- .../PharmaceuticalPrescriptionRevoked.cs | 11 +-- .../PharmaceuticalPrescription.cs | 4 +- Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs | 2 +- Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs | 2 +- .../Scripts/Oracle/FillSchema.sql | 2 +- .../Scripts/SqlServer/CreateDatabase.sql | Bin 16370 -> 16370 bytes 10 files changed, 92 insertions(+), 18 deletions(-) create mode 100644 Src/DDD.Core.Abstractions/TimeStampProvider.cs diff --git a/Src/DDD.Core.Abstractions/TimeStampProvider.cs b/Src/DDD.Core.Abstractions/TimeStampProvider.cs new file mode 100644 index 0000000..c914cfa --- /dev/null +++ b/Src/DDD.Core.Abstractions/TimeStampProvider.cs @@ -0,0 +1,79 @@ +using System; + +namespace DDD +{ + /// + /// Provides unique timestamps based on the current date and time with a specified resolution in milliseconds. + /// + public class TimestampProvider + { + + #region Fields + + private readonly static TimestampProvider local = new TimestampProvider(isLocal: true); + private readonly static TimestampProvider universal = new TimestampProvider(isLocal: false); + private readonly object locker = new object(); + private readonly Func now; + private DateTime lastTimestamp = DateTime.MinValue; + + #endregion Fields + + #region Constructors + + public TimestampProvider(bool isLocal, double resolutionInMs = 1) + { + this.IsLocal = isLocal; + this.ResolutionInMs = resolutionInMs; + if (isLocal) + this.now = () => DateTime.Now; + else + this.now = () => DateTime.UtcNow; + } + + #endregion Constructors + + #region Properties + + /// + /// Indicates whether provided timestamps are local or universal. + /// + public bool IsLocal { get; } + + /// + /// Indicates the resolution in milliseconds of provided timestamps. + /// + public double ResolutionInMs { get; } + + #endregion Properties + + #region Methods + + /// + /// Provides a local timestamp with a resolution of one millisecond. + /// + public static DateTime LocalTimestamp() => local.Timestamp(); + + /// + /// Provides a universal timestamp with a resolution of one millisecond. + /// + public static DateTime UniversalTimestamp() => universal.Timestamp(); + + /// + /// Provides a timestamp. + /// + public DateTime Timestamp() + { + var now = this.now(); + lock (locker) + { + if ((now - lastTimestamp).TotalMilliseconds < ResolutionInMs) + now = lastTimestamp.AddMilliseconds(ResolutionInMs); + lastTimestamp = now; + } + return now; + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.NHibernate/StoredEventMapping.cs b/Src/DDD.Core.NHibernate/StoredEventMapping.cs index d4c8b2e..73db436 100644 --- a/Src/DDD.Core.NHibernate/StoredEventMapping.cs +++ b/Src/DDD.Core.NHibernate/StoredEventMapping.cs @@ -35,7 +35,7 @@ protected StoredEventMapping() m.NotNullable(true); }); this.Property(e => e.UniqueId, m => m.NotNullable(true)); - this.Property(e => e.OccurredOn, m => m.Precision(2)); + this.Property(e => e.OccurredOn, m => m.Precision(3)); // in milliseconds this.Property(e => e.Username, m => { m.Type(NHibernateUtil.AnsiString); diff --git a/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/CreatePharmaceuticalPrescription.cs b/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/CreatePharmaceuticalPrescription.cs index 7c2ded5..fed3278 100644 --- a/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/CreatePharmaceuticalPrescription.cs +++ b/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/CreatePharmaceuticalPrescription.cs @@ -15,7 +15,7 @@ public class CreatePharmaceuticalPrescription : ICommand #region Properties - public DateTime CreatedOn { get; set; } = DateTime.Now; + public DateTime CreatedOn { get; set; } public DateTime? DeliverableAt { get; set; } diff --git a/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionCreated.cs b/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionCreated.cs index fda94cb..9b91c59 100644 --- a/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionCreated.cs +++ b/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionCreated.cs @@ -6,17 +6,17 @@ namespace DDD.HealthcareDelivery.Domain.Prescriptions { using Core.Domain; - [DataContract(Namespace = "DDD.HealthcareDelivery.Domain.Prescriptions")] + [DataContract(Namespace = "DDD.HealthcareDelivery.Prescriptions")] 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); 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 f677359..6e5e2be 100644 --- a/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionRevoked.cs +++ b/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionRevoked.cs @@ -6,24 +6,19 @@ namespace DDD.HealthcareDelivery.Domain.Prescriptions { using Core.Domain; - [DataContract(Namespace = "DDD.HealthcareDelivery.Domain.Prescriptions")] + [DataContract(Namespace = "DDD.HealthcareDelivery.Prescriptions")] 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(); 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/Domain/Prescriptions/PharmaceuticalPrescription.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs index 11b706e..1ce02e4 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs @@ -84,14 +84,14 @@ public static PharmaceuticalPrescription Create(PrescriptionIdentifier identifie EncounterIdentifier encounterIdentifier = null, DateTime? delivrableAt = null) { - return Create(identifier, prescriber, patient, prescribedMedications, DateTime.Now, languageCode, encounterIdentifier, delivrableAt); + return Create(identifier, prescriber, patient, prescribedMedications, TimestampProvider.LocalTimestamp(), languageCode, encounterIdentifier, delivrableAt); } public IEnumerable PrescribedMedications() => this.prescribedMedications.ToImmutableHashSet(); protected override void AddPrescriptionRevokedEvent(string reason) { - this.AddEvent(new PharmaceuticalPrescriptionRevoked(this.Identifier.Value, reason)); + this.AddEvent(new PharmaceuticalPrescriptionRevoked(this.Identifier.Value, TimestampProvider.LocalTimestamp(), reason)); } #endregion Methods diff --git a/Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs b/Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs index f3acba1..c497fa3 100644 --- a/Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs +++ b/Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs @@ -6,7 +6,7 @@ public class FakeEvent1 : IEvent { #region Properties - public DateTime OccurredOn => DateTime.Now; + public DateTime OccurredOn => TimestampProvider.LocalTimestamp(); #endregion Properties } diff --git a/Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs b/Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs index 9c38fd7..5128310 100644 --- a/Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs +++ b/Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs @@ -6,7 +6,7 @@ public class FakeEvent3 : IEvent { #region Properties - public DateTime OccurredOn => DateTime.Now; + public DateTime OccurredOn => TimestampProvider.LocalTimestamp(); #endregion Properties } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql index 6e4eec7..5170f65 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql @@ -49,7 +49,7 @@ END SPCLEARSCHEMA; VERSION NUMBER(3, 0), STREAMID VARCHAR2(50 CHAR), UNIQUEID RAW(16), - OCCURREDON TIMESTAMP (6), + OCCURREDON TIMESTAMP (3), USERNAME VARCHAR2(100 CHAR), BODY XMLTYPE, ISDISPATCHED NUMBER(1,0) diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql index 5ab83d30a90643ec772162b4699d4bcb18943d4e..2b5ae42d4edf47739659a3aca6e0626df41ce391 100644 GIT binary patch delta 18 acmexV|EYe%B4tM7$qNnTHy=_qkO2T$O$YP< delta 18 acmexV|EYe%B4tLS$qNnTHy=_qkO2T$LkIH! From e2f5bc64cb1edc3f1ff06898d73ef70ffba88169 Mon Sep 17 00:00:00 2001 From: draphyz Date: Wed, 29 Sep 2021 15:08:32 +0200 Subject: [PATCH 042/111] Improve serializers --- DDD.sln | 7 +++ .../Serialization/JsonSerializationOptions.cs | 3 +- .../JsonSerializerWrapper.cs | 47 ++++++++------- .../DataContractSerializerWrapper.cs | 46 ++++++++------- .../Serialization/XmlSerializerWrapper.cs | 57 ++++++++++++------- .../DDD.Core.Newtonsoft.UnitTests.csproj | 19 +++++++ .../FakePerson.cs | 18 ++++++ .../JsonSerializerWrapperTests.cs | 47 +++++++++++++++ .../DataContractSerializerWrapperTests.cs | 47 +++++++++++++++ .../Serialization/FakePerson.cs | 18 ++++++ .../XmlSerializerWrapperTests.cs | 47 +++++++++++++++ 11 files changed, 291 insertions(+), 65 deletions(-) create mode 100644 Test/DDD.Core.Newtonsoft.UnitTests/DDD.Core.Newtonsoft.UnitTests.csproj create mode 100644 Test/DDD.Core.Newtonsoft.UnitTests/FakePerson.cs create mode 100644 Test/DDD.Core.Newtonsoft.UnitTests/JsonSerializerWrapperTests.cs create mode 100644 Test/DDD.Core.UnitTests/Infrastructure/Serialization/DataContractSerializerWrapperTests.cs create mode 100644 Test/DDD.Core.UnitTests/Infrastructure/Serialization/FakePerson.cs create mode 100644 Test/DDD.Core.UnitTests/Infrastructure/Serialization/XmlSerializerWrapperTests.cs diff --git a/DDD.sln b/DDD.sln index c523d6d..2177215 100644 --- a/DDD.sln +++ b/DDD.sln @@ -63,6 +63,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.SimpleInjector", " 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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -153,6 +155,10 @@ Global {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -179,6 +185,7 @@ Global {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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {983C3AB4-301E-47A3-93FF-2E4BD8F2090F} diff --git a/Src/DDD.Core.Abstractions/Serialization/JsonSerializationOptions.cs b/Src/DDD.Core.Abstractions/Serialization/JsonSerializationOptions.cs index 5b5e359..85a3b16 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; #endregion Properties + } } diff --git a/Src/DDD.Core.Newtonsoft/JsonSerializerWrapper.cs b/Src/DDD.Core.Newtonsoft/JsonSerializerWrapper.cs index d356147..bd1d105 100644 --- a/Src/DDD.Core.Newtonsoft/JsonSerializerWrapper.cs +++ b/Src/DDD.Core.Newtonsoft/JsonSerializerWrapper.cs @@ -14,31 +14,32 @@ 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(settings, nameof(settings)).IsNotNull(); Condition.Requires(encoding, nameof(encoding)).IsNotNull(); - this.serializer = serializer; + 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 bool Indent => this.settings.Formatting == Formatting.Indented; #endregion Properties @@ -47,23 +48,26 @@ 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); + var settings = DefaultSettings(); + settings.Formatting = indent ? Formatting.Indented : Formatting.None; + return new JsonSerializerWrapper(settings, encoding); } + public static JsonSerializerWrapper Create(bool indent = true) => Create(JsonSerializationOptions.Encoding, indent); + public T Deserialize(Stream stream) { Condition.Requires(stream, nameof(stream)).IsNotNull(); - using (var reader = new StreamReader(stream, this.Encoding)) - using (var jsonReader = new JsonTextReader(reader)) + using (var streamReader = new StreamReader(stream, this.Encoding, true, 1024, true)) + using (var jsonReader = new JsonTextReader(streamReader)) { + var serializer = JsonSerializer.Create(this.settings); try { - return this.serializer.Deserialize(jsonReader); + return serializer.Deserialize(jsonReader); } - catch(JsonException exception) + catch (JsonException exception) { throw new SerializationException(typeof(T), exception); } @@ -73,12 +77,13 @@ public T Deserialize(Stream stream) 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)) + using (var streamWriter = new StreamWriter(stream, this.Encoding, 1024, true)) + using (var jsonWriter = new JsonTextWriter(streamWriter)) { + var serializer = JsonSerializer.Create(this.settings); try { - this.serializer.Serialize(jsonWriter, obj); + serializer.Serialize(jsonWriter, obj); } catch (JsonException exception) { @@ -87,16 +92,16 @@ public void Serialize(Stream stream, object obj) } } - 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/Infrastructure/Serialization/DataContractSerializerWrapper.cs b/Src/DDD.Core/Infrastructure/Serialization/DataContractSerializerWrapper.cs index 3626afd..1946d86 100644 --- a/Src/DDD.Core/Infrastructure/Serialization/DataContractSerializerWrapper.cs +++ b/Src/DDD.Core/Infrastructure/Serialization/DataContractSerializerWrapper.cs @@ -1,8 +1,10 @@ using Conditions; +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,17 +23,12 @@ 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(); @@ -51,24 +48,18 @@ private DataContractSerializerWrapper(XmlWriterSettings writerSettings, #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(); + var writerSettings = DefaultWriterSettings(); + writerSettings.Encoding = encoding; + writerSettings.Indent = indent; + var readerSettings = DefaultReaderSettings(); return new DataContractSerializerWrapper(writerSettings, readerSettings); } + public static DataContractSerializerWrapper Create(bool indent = true) => Create(XmlSerializationOptions.Encoding, indent); + public T Deserialize(Stream stream) { Condition.Requires(stream, nameof(stream)).IsNotNull(); @@ -79,7 +70,7 @@ public T Deserialize(Stream stream) { return (T)serializer.ReadObject(reader); } - catch(System.Runtime.Serialization.SerializationException exception) + catch (RuntimeSerializationException exception) { throw new SerializationException(typeof(T), exception); } @@ -97,13 +88,24 @@ public void Serialize(Stream stream, object obj) { serializer.WriteObject(writer, obj); } - catch (System.Runtime.Serialization.SerializationException exception) + 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..460c6cb 100644 --- a/Src/DDD.Core/Infrastructure/Serialization/XmlSerializerWrapper.cs +++ b/Src/DDD.Core/Infrastructure/Serialization/XmlSerializerWrapper.cs @@ -1,4 +1,5 @@ using Conditions; +using System; using System.IO; using System.Text; using System.Xml; @@ -21,17 +22,12 @@ 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(); @@ -54,20 +50,14 @@ 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(); + 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) { @@ -75,7 +65,14 @@ public T Deserialize(Stream stream) using (var reader = XmlReader.Create(stream, this.readerSettings)) { var serializer = new XmlSerializer(typeof(T)); - return (T)serializer.Deserialize(reader); + try + { + return (T)serializer.Deserialize(reader); + } + catch (InvalidOperationException exception) + { + throw new SerializationException(typeof(T), exception); + } } } @@ -86,10 +83,28 @@ public void Serialize(Stream stream, object obj) 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/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..177aa11 --- /dev/null +++ b/Test/DDD.Core.Newtonsoft.UnitTests/DDD.Core.Newtonsoft.UnitTests.csproj @@ -0,0 +1,19 @@ + + + net48;netcoreapp3.1;net5.0 + DDD.Core.Infrastructure.Serialization + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + \ No newline at end of file diff --git a/Test/DDD.Core.Newtonsoft.UnitTests/FakePerson.cs b/Test/DDD.Core.Newtonsoft.UnitTests/FakePerson.cs new file mode 100644 index 0000000..3e38a76 --- /dev/null +++ b/Test/DDD.Core.Newtonsoft.UnitTests/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.Newtonsoft.UnitTests/JsonSerializerWrapperTests.cs b/Test/DDD.Core.Newtonsoft.UnitTests/JsonSerializerWrapperTests.cs new file mode 100644 index 0000000..375d025 --- /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); + // Assert + obj2.Should().BeEquivalentTo(obj1); + } + + #endregion Methods + + } +} 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..4ec1701 --- /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); + // 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..3f248d0 --- /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); + // Assert + obj2.Should().BeEquivalentTo(obj1); + } + + #endregion Methods + + } +} From 9639b9c2e9ac9365e00910d74fcb940b998f2bec Mon Sep 17 00:00:00 2001 From: draphyz Date: Wed, 29 Sep 2021 18:55:29 +0200 Subject: [PATCH 043/111] Store event bodies in json --- .../OracleStoredEventMapping.cs | 2 +- .../SqlServerStoredEventMapping.cs | 2 +- Src/DDD.Core.NHibernate/StoredEventMapping.cs | 7 +++++-- ...HealthcareDelivery.IntegrationTests.csproj | 1 + .../Infrastructure/OracleFixture.cs | 2 +- .../Infrastructure/SqlServerFixture.cs | 2 +- .../Scripts/Oracle/FillSchema.sql | 16 ++++------------ .../Scripts/SqlServer/CreateDatabase.sql | Bin 16370 -> 16370 bytes 8 files changed, 14 insertions(+), 18 deletions(-) diff --git a/Src/DDD.Core.NHibernate/OracleStoredEventMapping.cs b/Src/DDD.Core.NHibernate/OracleStoredEventMapping.cs index c10a2c3..8360c35 100644 --- a/Src/DDD.Core.NHibernate/OracleStoredEventMapping.cs +++ b/Src/DDD.Core.NHibernate/OracleStoredEventMapping.cs @@ -8,7 +8,7 @@ public OracleStoredEventMapping() { // Fields this.Id(e => e.Id, m => m.Column(m1 => m1.SqlType("number(19,0)"))); - this.Property(e => e.Body, m => m.Column(m1 => m1.SqlType("xmltype"))); + this.Property(e => e.Body, m => m.Column(m1 => m1.Length(4000))); } #endregion Constructors diff --git a/Src/DDD.Core.NHibernate/SqlServerStoredEventMapping.cs b/Src/DDD.Core.NHibernate/SqlServerStoredEventMapping.cs index 9092183..e32a7f5 100644 --- a/Src/DDD.Core.NHibernate/SqlServerStoredEventMapping.cs +++ b/Src/DDD.Core.NHibernate/SqlServerStoredEventMapping.cs @@ -7,7 +7,7 @@ public class SqlServerStoredEventMapping : StoredEventMapping public SqlServerStoredEventMapping() { // Fields - this.Property(e => e.Body, m => m.Column(m1 => m1.SqlType("xml"))); + this.Property(e => e.Body, m => m.Column(m1 => m1.Length(8000))); } #endregion Constructors diff --git a/Src/DDD.Core.NHibernate/StoredEventMapping.cs b/Src/DDD.Core.NHibernate/StoredEventMapping.cs index 73db436..fb65726 100644 --- a/Src/DDD.Core.NHibernate/StoredEventMapping.cs +++ b/Src/DDD.Core.NHibernate/StoredEventMapping.cs @@ -32,7 +32,6 @@ protected StoredEventMapping() { m.Type(NHibernateUtil.AnsiString); m.Length(50); - m.NotNullable(true); }); this.Property(e => e.UniqueId, m => m.NotNullable(true)); this.Property(e => e.OccurredOn, m => m.Precision(3)); // in milliseconds @@ -41,7 +40,11 @@ protected StoredEventMapping() m.Type(NHibernateUtil.AnsiString); m.Length(100); }); - this.Property(e => e.Body, m => m.NotNullable(true)); + this.Property(e => e.Body, m => + { + m.Type(NHibernateUtil.AnsiString); + m.NotNullable(true); + }); this.Property(e => e.IsDispatched); } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj b/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj index 7ebbb34..bc096c0 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj @@ -21,6 +21,7 @@ + diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleFixture.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleFixture.cs index 2848403..10adb84 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleFixture.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleFixture.cs @@ -29,7 +29,7 @@ public OracleFixture() : base(OracleConnectionFactory.Create(), "OracleScripts") public IObjectTranslator CreateEventTranslator() { - return new StoredEventTranslator(DataContractSerializerWrapper.Create(new UTF8Encoding(false))); + return new StoredEventTranslator(JsonSerializerWrapper.Create(false)); } public ISessionFactory CreateSessionFactory() diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerFixture.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerFixture.cs index 179d2d1..54fc16a 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerFixture.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerFixture.cs @@ -29,7 +29,7 @@ public SqlServerFixture() : base(SqlServerConnectionFactory.Create(), "SqlServer public IObjectTranslator CreateEventTranslator() { - return new StoredEventTranslator(DataContractSerializerWrapper.Create(Encoding.Unicode)); + return new StoredEventTranslator(JsonSerializerWrapper.Create(false)); } public ISessionFactory CreateSessionFactory() diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql index 5170f65..b5d7271 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql @@ -49,20 +49,15 @@ END SPCLEARSCHEMA; VERSION NUMBER(3, 0), STREAMID VARCHAR2(50 CHAR), UNIQUEID RAW(16), - OCCURREDON TIMESTAMP (3), - USERNAME VARCHAR2(100 CHAR), - BODY XMLTYPE, + OCCURREDON TIMESTAMP(3), + USERNAME VARCHAR2(100 CHAR), + BODY VARCHAR2(4000), ISDISPATCHED 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)) + TABLESPACE USERS / -------------------------------------------------------- -- DDL for Table PRESCMEDICATION @@ -185,9 +180,6 @@ END SPCLEARSCHEMA; ALTER TABLE TEST.EVENT MODIFY (VERSION NOT NULL ENABLE) / - ALTER TABLE TEST.EVENT MODIFY (STREAMID NOT NULL ENABLE) -/ - ALTER TABLE TEST.EVENT MODIFY (UNIQUEID NOT NULL ENABLE) / diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql index 2b5ae42d4edf47739659a3aca6e0626df41ce391..4200ae6bcdd98530d813c82de91a55c06f4c8bad 100644 GIT binary patch delta 30 mcmexV|EYchlk#E_WyZ-?MnbH)42cXClNTDwZ?;g`B@FOBw*lMhj&C From fde4777671325c252f15f24017f939212efda8ee Mon Sep 17 00:00:00 2001 From: draphyz Date: Thu, 30 Sep 2021 21:56:03 +0200 Subject: [PATCH 044/111] Change data contract namespaces --- .../Domain/Prescriptions/PharmaceuticalPrescriptionCreated.cs | 2 +- .../Domain/Prescriptions/PharmaceuticalPrescriptionRevoked.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionCreated.cs b/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionCreated.cs index 9b91c59..680d342 100644 --- a/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionCreated.cs +++ b/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionCreated.cs @@ -6,7 +6,7 @@ namespace DDD.HealthcareDelivery.Domain.Prescriptions { using Core.Domain; - [DataContract(Namespace = "DDD.HealthcareDelivery.Prescriptions")] + [DataContract(Namespace = "http://schemas.ddd.com/healthcare-delivery")] public class PharmaceuticalPrescriptionCreated : IDomainEvent { diff --git a/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionRevoked.cs b/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionRevoked.cs index 6e5e2be..92b5b35 100644 --- a/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionRevoked.cs +++ b/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionRevoked.cs @@ -6,7 +6,7 @@ namespace DDD.HealthcareDelivery.Domain.Prescriptions { using Core.Domain; - [DataContract(Namespace = "DDD.HealthcareDelivery.Prescriptions")] + [DataContract(Namespace = "http://schemas.ddd.com/healthcare-delivery")] public class PharmaceuticalPrescriptionRevoked : IDomainEvent { From c55d3d723f234692f077d55992bc29d557f30621 Mon Sep 17 00:00:00 2001 From: draphyz Date: Tue, 12 Oct 2021 20:25:20 +0200 Subject: [PATCH 045/111] Add EventTimestampProvider --- .../TimeStampProvider.cs | 12 ------- Src/DDD.Core/Domain/EventTimestampProvider.cs | 33 +++++++++++++++++++ Src/DDD.Core/Domain/IAsyncEventPublisher.cs | 3 -- .../PharmaceuticalPrescription.cs | 6 ++-- Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs | 2 +- Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs | 2 +- 6 files changed, 38 insertions(+), 20 deletions(-) create mode 100644 Src/DDD.Core/Domain/EventTimestampProvider.cs diff --git a/Src/DDD.Core.Abstractions/TimeStampProvider.cs b/Src/DDD.Core.Abstractions/TimeStampProvider.cs index c914cfa..67a68ef 100644 --- a/Src/DDD.Core.Abstractions/TimeStampProvider.cs +++ b/Src/DDD.Core.Abstractions/TimeStampProvider.cs @@ -10,8 +10,6 @@ public class TimestampProvider #region Fields - private readonly static TimestampProvider local = new TimestampProvider(isLocal: true); - private readonly static TimestampProvider universal = new TimestampProvider(isLocal: false); private readonly object locker = new object(); private readonly Func now; private DateTime lastTimestamp = DateTime.MinValue; @@ -48,16 +46,6 @@ public TimestampProvider(bool isLocal, double resolutionInMs = 1) #region Methods - /// - /// Provides a local timestamp with a resolution of one millisecond. - /// - public static DateTime LocalTimestamp() => local.Timestamp(); - - /// - /// Provides a universal timestamp with a resolution of one millisecond. - /// - public static DateTime UniversalTimestamp() => universal.Timestamp(); - /// /// Provides a timestamp. /// diff --git a/Src/DDD.Core/Domain/EventTimestampProvider.cs b/Src/DDD.Core/Domain/EventTimestampProvider.cs new file mode 100644 index 0000000..aaf93c3 --- /dev/null +++ b/Src/DDD.Core/Domain/EventTimestampProvider.cs @@ -0,0 +1,33 @@ +using System; + +namespace DDD.Core.Domain +{ + /// + /// Provides unique timestamps for events. + /// + public static class EventTimestampProvider + { + + #region Fields + + private readonly static TimestampProvider local = new TimestampProvider(isLocal: true); + private readonly static TimestampProvider universal = new TimestampProvider(isLocal: false); + + #endregion Fields + + #region Methods + + /// + /// Provides a local timestamp with a resolution of one millisecond. + /// + public static DateTime LocalTimestamp() => local.Timestamp(); + + /// + /// Provides a universal timestamp with a resolution of one millisecond. + /// + public static DateTime UniversalTimestamp() => universal.Timestamp(); + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Domain/IAsyncEventPublisher.cs b/Src/DDD.Core/Domain/IAsyncEventPublisher.cs index 9499d38..8b8a052 100644 --- a/Src/DDD.Core/Domain/IAsyncEventPublisher.cs +++ b/Src/DDD.Core/Domain/IAsyncEventPublisher.cs @@ -2,9 +2,6 @@ namespace DDD.Core.Domain { - /// - /// Publish asynchronously events outside the local bounded context (used to decouple bounded contexts). - /// public interface IAsyncEventPublisher { diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs index 1ce02e4..42c9b01 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs @@ -20,7 +20,7 @@ public class PharmaceuticalPrescription : Prescription #region Fields - private ISet prescribedMedications = new HashSet(); + private readonly ISet prescribedMedications = new HashSet(); #endregion Fields @@ -84,14 +84,14 @@ public static PharmaceuticalPrescription Create(PrescriptionIdentifier identifie EncounterIdentifier encounterIdentifier = null, DateTime? delivrableAt = null) { - return Create(identifier, prescriber, patient, prescribedMedications, TimestampProvider.LocalTimestamp(), languageCode, encounterIdentifier, delivrableAt); + return Create(identifier, prescriber, patient, prescribedMedications, EventTimestampProvider.LocalTimestamp(), languageCode, encounterIdentifier, delivrableAt); } public IEnumerable PrescribedMedications() => this.prescribedMedications.ToImmutableHashSet(); protected override void AddPrescriptionRevokedEvent(string reason) { - this.AddEvent(new PharmaceuticalPrescriptionRevoked(this.Identifier.Value, TimestampProvider.LocalTimestamp(), reason)); + this.AddEvent(new PharmaceuticalPrescriptionRevoked(this.Identifier.Value, EventTimestampProvider.LocalTimestamp(), reason)); } #endregion Methods diff --git a/Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs b/Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs index c497fa3..9adeb8a 100644 --- a/Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs +++ b/Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs @@ -6,7 +6,7 @@ public class FakeEvent1 : IEvent { #region Properties - public DateTime OccurredOn => TimestampProvider.LocalTimestamp(); + public DateTime OccurredOn => EventTimestampProvider.LocalTimestamp(); #endregion Properties } diff --git a/Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs b/Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs index 5128310..430e04d 100644 --- a/Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs +++ b/Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs @@ -6,7 +6,7 @@ public class FakeEvent3 : IEvent { #region Properties - public DateTime OccurredOn => TimestampProvider.LocalTimestamp(); + public DateTime OccurredOn => EventTimestampProvider.LocalTimestamp(); #endregion Properties } From 39a1fcc58d86b562f6e6e72990ce2cf5591a55e9 Mon Sep 17 00:00:00 2001 From: draphyz Date: Wed, 13 Oct 2021 11:17:10 +0200 Subject: [PATCH 046/111] Add StreamType To StoredEvent --- .../NHibernateRepository.cs | 1 + Src/DDD.Core.NHibernate/StoredEventMapping.cs | 7 ++++- .../Infrastructure/Data/StoredEvent.cs | 2 ++ .../Scripts/Oracle/FillSchema.sql | 25 +++++++++--------- .../Scripts/SqlServer/CreateDatabase.sql | Bin 16370 -> 16410 bytes 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/Src/DDD.Core.NHibernate/NHibernateRepository.cs b/Src/DDD.Core.NHibernate/NHibernateRepository.cs index 136a490..2c46f7d 100644 --- a/Src/DDD.Core.NHibernate/NHibernateRepository.cs +++ b/Src/DDD.Core.NHibernate/NHibernateRepository.cs @@ -77,6 +77,7 @@ private IEnumerable ToStoredEvents(TDomainEntity aggregate) return aggregate.AllEvents().Select(e => { var evt = this.eventTranslator.Translate(e); + evt.StreamType = aggregate.GetType().Name; evt.StreamId = aggregate.IdentityAsString(); evt.UniqueId = Guid.NewGuid(); evt.Username = user; diff --git a/Src/DDD.Core.NHibernate/StoredEventMapping.cs b/Src/DDD.Core.NHibernate/StoredEventMapping.cs index fb65726..b62911b 100644 --- a/Src/DDD.Core.NHibernate/StoredEventMapping.cs +++ b/Src/DDD.Core.NHibernate/StoredEventMapping.cs @@ -13,7 +13,7 @@ protected StoredEventMapping() { this.Lazy(false); // Table - this.Table("Event"); + this.Table("StoredEvent"); // Keys this.Id(e => e.Id, m1 => { @@ -28,6 +28,11 @@ protected StoredEventMapping() m.NotNullable(true); }); this.Property(e => e.Version); + this.Property(e => e.StreamType, m => + { + m.Type(NHibernateUtil.AnsiString); + m.Length(50); + }); this.Property(e => e.StreamId, m => { m.Type(NHibernateUtil.AnsiString); diff --git a/Src/DDD.Core/Infrastructure/Data/StoredEvent.cs b/Src/DDD.Core/Infrastructure/Data/StoredEvent.cs index 4ff927c..438f7d3 100644 --- a/Src/DDD.Core/Infrastructure/Data/StoredEvent.cs +++ b/Src/DDD.Core/Infrastructure/Data/StoredEvent.cs @@ -19,6 +19,8 @@ public class StoredEvent public string StreamId { get; set; } + public string StreamType { get; set; } + public Guid UniqueId { get; set; } public string Username { get; set; } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql index b5d7271..48720d0 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql @@ -40,13 +40,14 @@ BEGIN END SPCLEARSCHEMA; / -------------------------------------------------------- --- DDL for Table EVENT +-- DDL for Table STOREDEVENT -------------------------------------------------------- - CREATE TABLE TEST.EVENT + CREATE TABLE TEST.STOREDEVENT ( EVENTID NUMBER(19,0), EVENTTYPE VARCHAR2(50 CHAR), VERSION NUMBER(3, 0), + STREAMTYPE VARCHAR2(50 CHAR), STREAMID VARCHAR2(50 CHAR), UNIQUEID RAW(16), OCCURREDON TIMESTAMP(3), @@ -131,10 +132,10 @@ END SPCLEARSCHEMA; TABLESPACE USERS / -------------------------------------------------------- --- DDL for Index PK_EVENT +-- DDL for Index PK_STOREDEVENT -------------------------------------------------------- - CREATE UNIQUE INDEX TEST.PK_EVENT ON TEST.EVENT (EVENTID) + CREATE UNIQUE INDEX TEST.PK_STOREDEVENT ON TEST.STOREDEVENT (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) @@ -164,32 +165,32 @@ END SPCLEARSCHEMA; -- Constraints for Table EVENT -------------------------------------------------------- - ALTER TABLE TEST.EVENT ADD CONSTRAINT PK_EVENT PRIMARY KEY (EVENTID) + ALTER TABLE TEST.STOREDEVENT ADD CONSTRAINT PK_STOREDEVENT 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.STOREDEVENT MODIFY (EVENTID NOT NULL ENABLE) / - ALTER TABLE TEST.EVENT MODIFY (EVENTTYPE NOT NULL ENABLE) + ALTER TABLE TEST.STOREDEVENT MODIFY (EVENTTYPE NOT NULL ENABLE) / - ALTER TABLE TEST.EVENT MODIFY (VERSION NOT NULL ENABLE) + ALTER TABLE TEST.STOREDEVENT MODIFY (VERSION NOT NULL ENABLE) / - ALTER TABLE TEST.EVENT MODIFY (UNIQUEID NOT NULL ENABLE) + ALTER TABLE TEST.STOREDEVENT MODIFY (UNIQUEID NOT NULL ENABLE) / - ALTER TABLE TEST.EVENT MODIFY (OCCURREDON NOT NULL ENABLE) + ALTER TABLE TEST.STOREDEVENT MODIFY (OCCURREDON NOT NULL ENABLE) / - ALTER TABLE TEST.EVENT MODIFY (ISDISPATCHED NOT NULL ENABLE) + ALTER TABLE TEST.STOREDEVENT MODIFY (ISDISPATCHED NOT NULL ENABLE) / - ALTER TABLE TEST.EVENT MODIFY (BODY NOT NULL ENABLE) + ALTER TABLE TEST.STOREDEVENT MODIFY (BODY NOT NULL ENABLE) / -------------------------------------------------------- -- Constraints for Table PRESCMEDICATION diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql index 4200ae6bcdd98530d813c82de91a55c06f4c8bad..bf433adec221e7467a4d19e78f0e73792a32281d 100644 GIT binary patch delta 110 zcmexVKdXUp!wCiMV1^Qge1;;1RECtz#}#xLkp(7yR(v}-NJ(w;DWx+g5}W6$Y-gP; nWUa8dMyr8$@+2LZ%?cJqAQ8FAvuvd{&$7*6gmET&*{=Wq(9t9g delta 164 zcmbQ$!1$?t!wH4SHX>Y`1JsomL5#^_N{=?1C~sqAPhm)6$Y;>o{8eQ;D~vPQP+fWR zIjuTgxWGdl*~#a$TqaM@I@OsuNdCQq{O005MPG4ucc From 23e2cd050b502d81f87310da49e9ec16a33d9f4c Mon Sep 17 00:00:00 2001 From: draphyz Date: Fri, 15 Oct 2021 09:07:48 +0200 Subject: [PATCH 047/111] Move EventTimestampProvider --- .../Domain/EventTimestampProvider.cs | 4 ++-- Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs | 2 +- Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename Src/{DDD.Core => DDD.HealthcareDelivery}/Domain/EventTimestampProvider.cs (86%) diff --git a/Src/DDD.Core/Domain/EventTimestampProvider.cs b/Src/DDD.HealthcareDelivery/Domain/EventTimestampProvider.cs similarity index 86% rename from Src/DDD.Core/Domain/EventTimestampProvider.cs rename to Src/DDD.HealthcareDelivery/Domain/EventTimestampProvider.cs index aaf93c3..f2129fb 100644 --- a/Src/DDD.Core/Domain/EventTimestampProvider.cs +++ b/Src/DDD.HealthcareDelivery/Domain/EventTimestampProvider.cs @@ -1,9 +1,9 @@ using System; -namespace DDD.Core.Domain +namespace DDD.HealthcareDelivery.Domain { /// - /// Provides unique timestamps for events. + /// Provides unique timestamps for the events of the context of healthcare delivery. /// public static class EventTimestampProvider { diff --git a/Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs b/Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs index 9adeb8a..f3acba1 100644 --- a/Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs +++ b/Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs @@ -6,7 +6,7 @@ public class FakeEvent1 : IEvent { #region Properties - public DateTime OccurredOn => EventTimestampProvider.LocalTimestamp(); + public DateTime OccurredOn => DateTime.Now; #endregion Properties } diff --git a/Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs b/Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs index 430e04d..9c38fd7 100644 --- a/Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs +++ b/Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs @@ -6,7 +6,7 @@ public class FakeEvent3 : IEvent { #region Properties - public DateTime OccurredOn => EventTimestampProvider.LocalTimestamp(); + public DateTime OccurredOn => DateTime.Now; #endregion Properties } From 3048f98e33b8383e0746040dba825fde090e6fd6 Mon Sep 17 00:00:00 2001 From: draphyz Date: Fri, 22 Oct 2021 14:23:50 +0200 Subject: [PATCH 048/111] Add CancellationToken support --- .../Validation/IAsyncObjectValidator.cs | 5 +++-- Src/DDD.Core.Dapper/IDbConnectionExtensions.cs | 11 ++++++----- .../FluentValidatorAdapter.cs | 8 +++++--- Src/DDD.Core.NHibernate/NHibernateRepository.cs | 10 +++++----- .../NServiceBusEventPublisher.cs | 4 +++- Src/DDD.Core.Polly/AsyncPollyCommandHandler.cs | 9 ++++----- Src/DDD.Core.Polly/AsyncPollyQueryHandler.cs | 7 +++---- .../AsyncScopedCommandHandler.cs | 7 ++++--- .../AsyncScopedQueryHandler.cs | 7 ++++--- .../Application/AsyncCommandHandlerWithLogging.cs | 9 ++++----- .../Application/AsyncDomainCommandHandler.cs | 7 ++++--- .../Application/AsyncQueryHandlerWithLogging.cs | 9 ++++----- Src/DDD.Core/Application/CommandProcessor.cs | 9 +++++---- Src/DDD.Core/Application/IAsyncCommandHandler.cs | 5 +++-- Src/DDD.Core/Application/IAsyncQueryHandler.cs | 5 +++-- Src/DDD.Core/Application/ICommandProcessor.cs | 7 ++++--- Src/DDD.Core/Application/IQueryProcessor.cs | 7 ++++--- Src/DDD.Core/Application/QueryProcessor.cs | 9 +++++---- Src/DDD.Core/Domain/IAsyncEventPublisher.cs | 5 +++-- Src/DDD.Core/Domain/IAsyncRepository.cs | 7 ++++--- Src/DDD.Core/Infrastructure/Data/DbQueryHandler.cs | 7 ++++--- .../PharmaceuticalPrescriptionCreator.cs | 5 +++-- .../PharmaceuticalPrescriptionRevoker.cs | 5 +++-- .../PharmaceuticalPrescriptionsByPatientFinder.cs | 12 +++++++++--- .../PrescribedMedicationsByPrescriptionFinder.cs | 12 +++++++++--- .../Prescriptions/PrescriptionIdentifierGenerator.cs | 7 +++++-- 26 files changed, 113 insertions(+), 82 deletions(-) diff --git a/Src/DDD.Core.Abstractions/Validation/IAsyncObjectValidator.cs b/Src/DDD.Core.Abstractions/Validation/IAsyncObjectValidator.cs index a7a230a..f85b05d 100644 --- a/Src/DDD.Core.Abstractions/Validation/IAsyncObjectValidator.cs +++ b/Src/DDD.Core.Abstractions/Validation/IAsyncObjectValidator.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading; +using System.Threading.Tasks; namespace DDD.Validation { @@ -10,7 +11,7 @@ public interface IAsyncObjectValidator where T :class #region Methods - Task ValidateAsync(T obj, string ruleSet = null); + Task ValidateAsync(T obj, string ruleSet = null, CancellationToken cancellationToken = default); #endregion Methods diff --git a/Src/DDD.Core.Dapper/IDbConnectionExtensions.cs b/Src/DDD.Core.Dapper/IDbConnectionExtensions.cs index c0ed45e..c587902 100644 --- a/Src/DDD.Core.Dapper/IDbConnectionExtensions.cs +++ b/Src/DDD.Core.Dapper/IDbConnectionExtensions.cs @@ -1,9 +1,10 @@ -using System.Data; -using Dapper; +using Dapper; using Conditions; +using System; +using System.Data; +using System.Threading; using System.Threading.Tasks; using System.Collections.Generic; -using System; using System.Text.RegularExpressions; namespace DDD.Core.Infrastructure.Data @@ -59,13 +60,13 @@ public static TValue NextValue(this IDbConnection connection, string seq return connection.QuerySingle(sql); } - public static Task NextValueAsync(this IDbConnection connection, string sequence, string schema = null) + public static 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(); var expressions = connection.Expressions(); var sql = $"SELECT {expressions.NextValue(sequence, schema)} {expressions.FromDummy()}"; - return connection.QuerySingleAsync(sql); + return connection.QuerySingleAsync(new CommandDefinition(sql, cancellationToken: cancellationToken)); } #endregion Methods diff --git a/Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs b/Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs index d337f76..e144552 100644 --- a/Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs +++ b/Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs @@ -6,6 +6,7 @@ namespace DDD.Core.Infrastructure.Validation { using Mapping; + using System.Threading; using Threading; public class FluentValidatorAdapter @@ -49,14 +50,15 @@ public DDD.Validation.ValidationResult Validate(T obj, string ruleSet = null) /// /// The object to validate. /// The rule set. - public async Task ValidateAsync(T obj, string ruleSet = null) + /// An optional cancellation token. + public async Task ValidateAsync(T obj, string ruleSet = null, CancellationToken cancellationToken = default) { await new SynchronizationContextRemover(); ValidationResult result; if (string.IsNullOrWhiteSpace(ruleSet)) - result = await this.fluentValidator.ValidateAsync(obj); + result = await this.fluentValidator.ValidateAsync(obj, cancellationToken); else - result = await this.fluentValidator.ValidateAsync(obj, options => options.IncludeRuleSets(ruleSet.Split(','))); + result = await this.fluentValidator.ValidateAsync(obj, options => options.IncludeRuleSets(ruleSet.Split(',')), cancellationToken); return this.resultTranslator.Translate(result, new { ObjectName = obj.GetType().Name }); } diff --git a/Src/DDD.Core.NHibernate/NHibernateRepository.cs b/Src/DDD.Core.NHibernate/NHibernateRepository.cs index 2c46f7d..d03f79c 100644 --- a/Src/DDD.Core.NHibernate/NHibernateRepository.cs +++ b/Src/DDD.Core.NHibernate/NHibernateRepository.cs @@ -41,13 +41,13 @@ public NHibernateRepository(ISession session, IObjectTranslator FindAsync(TIdentity identity) + public async Task FindAsync(TIdentity identity, CancellationToken cancellationToken = default) { Condition.Requires(identity, nameof(identity)).IsNotNull(); await new SynchronizationContextRemover(); try { - return await this.session.GetAsync(identity); + return await this.session.GetAsync(identity, cancellationToken); } catch (HibernateException ex) { @@ -55,15 +55,15 @@ public async Task FindAsync(TIdentity identity) } } - public async Task SaveAsync(TDomainEntity aggregate) + public async Task SaveAsync(TDomainEntity aggregate, CancellationToken cancellationToken = default) { var events = ToStoredEvents(aggregate); await new SynchronizationContextRemover(); try { - await this.session.SaveOrUpdateAsync(aggregate); + await this.session.SaveOrUpdateAsync(aggregate, cancellationToken); foreach (var @event in events) - await this.session.SaveAsync(@event); + await this.session.SaveAsync(@event, cancellationToken); } catch (HibernateException ex) { diff --git a/Src/DDD.Core.NServiceBus/NServiceBusEventPublisher.cs b/Src/DDD.Core.NServiceBus/NServiceBusEventPublisher.cs index 257d576..3acc3fd 100644 --- a/Src/DDD.Core.NServiceBus/NServiceBusEventPublisher.cs +++ b/Src/DDD.Core.NServiceBus/NServiceBusEventPublisher.cs @@ -1,4 +1,5 @@ using NServiceBus; +using System.Threading; using System.Threading.Tasks; using Conditions; @@ -28,10 +29,11 @@ public NServiceBusEventPublisher(IMessageSession session) #region Methods - public async Task PublishAsync(IEvent @event) + public async Task PublishAsync(IEvent @event, CancellationToken cancellationToken = default) { Condition.Requires(@event, nameof(@event)).IsNotNull(); await new SynchronizationContextRemover(); + // Cancellation token support will be implemented in NServiceBus 8 (not yet released) await this.session.Publish(@event); } diff --git a/Src/DDD.Core.Polly/AsyncPollyCommandHandler.cs b/Src/DDD.Core.Polly/AsyncPollyCommandHandler.cs index a9e042b..6506fb3 100644 --- a/Src/DDD.Core.Polly/AsyncPollyCommandHandler.cs +++ b/Src/DDD.Core.Polly/AsyncPollyCommandHandler.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading; +using System.Threading.Tasks; using Polly; using Conditions; @@ -22,9 +23,7 @@ public class AsyncPollyCommandHandler : IAsyncCommandHandler #region Constructors -#pragma warning disable CS3001 // Argument type is not CLS-compliant public AsyncPollyCommandHandler(IAsyncCommandHandler handler, IAsyncPolicy policy) -#pragma warning restore CS3001 // Argument type is not CLS-compliant { Condition.Requires(handler, nameof(handler)).IsNotNull(); Condition.Requires(policy, nameof(policy)).IsNotNull(); @@ -36,9 +35,9 @@ public AsyncPollyCommandHandler(IAsyncCommandHandler handler, IAsyncPo #region Methods - public async Task HandleAsync(TCommand command) + public async Task HandleAsync(TCommand command, CancellationToken cancellationToken = default) { - await policy.ExecuteAsync(() => this.handler.HandleAsync(command)); + await policy.ExecuteAsync(() => this.handler.HandleAsync(command, cancellationToken)); } #endregion Methods diff --git a/Src/DDD.Core.Polly/AsyncPollyQueryHandler.cs b/Src/DDD.Core.Polly/AsyncPollyQueryHandler.cs index f2ac0a9..6ca0a3b 100644 --- a/Src/DDD.Core.Polly/AsyncPollyQueryHandler.cs +++ b/Src/DDD.Core.Polly/AsyncPollyQueryHandler.cs @@ -1,5 +1,6 @@ using Polly; using Conditions; +using System.Threading; using System.Threading.Tasks; namespace DDD.Core.Infrastructure.ErrorHandling @@ -22,9 +23,7 @@ public class AsyncPollyQueryHandler : IAsyncQueryHandler handler, IAsyncPolicy policy) -#pragma warning restore CS3001 // Argument type is not CLS-compliant { Condition.Requires(handler, nameof(handler)).IsNotNull(); Condition.Requires(policy, nameof(policy)).IsNotNull(); @@ -36,9 +35,9 @@ public AsyncPollyQueryHandler(IAsyncQueryHandler handler, IAsyn #region Methods - public async Task HandleAsync(TQuery query) + public async Task HandleAsync(TQuery query, CancellationToken cancellationToken = default) { - return await policy.ExecuteAsync(() => this.handler.HandleAsync(query)); + return await policy.ExecuteAsync(() => this.handler.HandleAsync(query, cancellationToken)); } #endregion Methods diff --git a/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs index c6c135a..3ad50bc 100644 --- a/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs @@ -1,6 +1,7 @@ using SimpleInjector; using SimpleInjector.Lifestyles; using Conditions; +using System.Threading; using System.Threading.Tasks; using System; @@ -36,16 +37,16 @@ public AsyncScopedCommandHandler(Func> handlerPro #region Methods - public async Task HandleAsync(TCommand command) + public async Task HandleAsync(TCommand command, CancellationToken cancellationToken = default) { using (AsyncScopedLifestyle.BeginScope(container)) { var handler = this.handlerProvider(); - await handler.HandleAsync(command); + await handler.HandleAsync(command, cancellationToken); } } #endregion Methods } -} \ No newline at end of file +} diff --git a/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs b/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs index 3cfa7fc..f4776ad 100644 --- a/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs @@ -1,6 +1,7 @@ using SimpleInjector; using SimpleInjector.Lifestyles; using Conditions; +using System.Threading; using System.Threading.Tasks; using System; @@ -36,16 +37,16 @@ public AsyncScopedQueryHandler(Func> handler #region Methods - public async Task HandleAsync(TQuery query) + public async Task HandleAsync(TQuery query, CancellationToken cancellationToken = default) { using (AsyncScopedLifestyle.BeginScope(container)) { var handler = this.handlerProvider(); - return await handler.HandleAsync(query); + return await handler.HandleAsync(query, cancellationToken); } } #endregion Methods } -} \ No newline at end of file +} diff --git a/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging.cs b/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging.cs index 43ec804..2354c1e 100644 --- a/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging.cs +++ b/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging.cs @@ -1,6 +1,7 @@ using Conditions; using Microsoft.Extensions.Logging; using System.Diagnostics; +using System.Threading; using System.Threading.Tasks; namespace DDD.Core.Application @@ -22,9 +23,7 @@ public class AsyncCommandHandlerWithLogging : IAsyncCommandHandler commandHandler, ILogger logger) -#pragma warning restore CS3001 // Argument type is not CLS-compliant { Condition.Requires(commandHandler, nameof(commandHandler)).IsNotNull(); Condition.Requires(logger, nameof(logger)).IsNotNull(); @@ -36,18 +35,18 @@ public AsyncCommandHandlerWithLogging(IAsyncCommandHandler commandHand #region Methods - public async Task HandleAsync(TCommand command) + public async Task HandleAsync(TCommand command, CancellationToken cancellationToken = default) { if (this.logger.IsEnabled(LogLevel.Information)) { this.logger.LogInformation("Executing command {Command}.", command); var stopWatch = Stopwatch.StartNew(); - await this.commandHandler.HandleAsync(command); + await this.commandHandler.HandleAsync(command, cancellationToken); stopWatch.Stop(); this.logger.LogInformation("Command executed in {CommandExecutionTime} ms.", stopWatch.ElapsedMilliseconds); } else - await this.commandHandler.HandleAsync(command); + await this.commandHandler.HandleAsync(command, cancellationToken); } #endregion Methods diff --git a/Src/DDD.Core/Application/AsyncDomainCommandHandler.cs b/Src/DDD.Core/Application/AsyncDomainCommandHandler.cs index 8fa5946..0269812 100644 --- a/Src/DDD.Core/Application/AsyncDomainCommandHandler.cs +++ b/Src/DDD.Core/Application/AsyncDomainCommandHandler.cs @@ -1,4 +1,5 @@ using Conditions; +using System.Threading; using System.Threading.Tasks; namespace DDD.Core.Application @@ -22,13 +23,13 @@ public abstract class AsyncDomainCommandHandler : IAsyncCommandHandler #region Methods - public async Task HandleAsync(TCommand command) + public async Task HandleAsync(TCommand command, CancellationToken cancellationToken = default) { Condition.Requires(command, nameof(command)).IsNotNull(); await new SynchronizationContextRemover(); try { - await this.ExecuteAsync(command); + await this.ExecuteAsync(command, cancellationToken); } catch (DomainException ex) { @@ -36,7 +37,7 @@ public async Task HandleAsync(TCommand command) } } - protected abstract Task ExecuteAsync(TCommand command); + protected abstract Task ExecuteAsync(TCommand command, CancellationToken cancellationToken = default); #endregion Methods diff --git a/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging.cs b/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging.cs index a7f3d69..b3ed5e9 100644 --- a/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging.cs +++ b/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging.cs @@ -1,6 +1,7 @@ using Conditions; using Microsoft.Extensions.Logging; using System.Diagnostics; +using System.Threading; using System.Threading.Tasks; namespace DDD.Core.Application @@ -16,9 +17,7 @@ public class AsyncQueryHandlerWithLogging : IAsyncQueryHandler< private readonly IAsyncQueryHandler queryHandler; private readonly ILogger logger; -#pragma warning disable CS3001 // Argument type is not CLS-compliant public AsyncQueryHandlerWithLogging(IAsyncQueryHandler queryHandler, ILogger logger) -#pragma warning restore CS3001 // Argument type is not CLS-compliant { Condition.Requires(queryHandler, nameof(queryHandler)).IsNotNull(); Condition.Requires(logger, nameof(logger)).IsNotNull(); @@ -30,19 +29,19 @@ public AsyncQueryHandlerWithLogging(IAsyncQueryHandler queryHan #region Methods - public async Task HandleAsync(TQuery query) + public async Task HandleAsync(TQuery query, CancellationToken cancellationToken = default) { if (this.logger.IsEnabled(LogLevel.Information)) { this.logger.LogInformation("Executing query {Query}.", query); var stopWatch = Stopwatch.StartNew(); - var result = await this.queryHandler.HandleAsync(query); + var result = await this.queryHandler.HandleAsync(query, cancellationToken); stopWatch.Stop(); this.logger.LogInformation("Query executed in {QueryExecutionTime} ms.", stopWatch.ElapsedMilliseconds); return result; } else - return await this.queryHandler.HandleAsync(query); + return await this.queryHandler.HandleAsync(query, cancellationToken); } #endregion Methods diff --git a/Src/DDD.Core/Application/CommandProcessor.cs b/Src/DDD.Core/Application/CommandProcessor.cs index c3e3166..4d9e5f2 100644 --- a/Src/DDD.Core/Application/CommandProcessor.cs +++ b/Src/DDD.Core/Application/CommandProcessor.cs @@ -1,5 +1,6 @@ using Conditions; using System; +using System.Threading; using System.Threading.Tasks; namespace DDD.Core.Application @@ -38,12 +39,12 @@ public void Process(TCommand command) where TCommand : class, ICommand handler.Handle(command); } - public Task ProcessAsync(TCommand command) where TCommand : class, ICommand + public Task ProcessAsync(TCommand command, CancellationToken cancellationToken = default) where TCommand : class, ICommand { 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."); - return handler.HandleAsync(command); + return handler.HandleAsync(command, cancellationToken); } public ValidationResult Validate(TCommand command, string ruleSet = null) where TCommand : class, ICommand @@ -54,12 +55,12 @@ public ValidationResult Validate(TCommand command, string ruleSet = nu return validator.Validate(command, ruleSet); } - public Task ValidateAsync(TCommand command, string ruleSet = null) where TCommand : class, ICommand + public Task ValidateAsync(TCommand command, string ruleSet = null, CancellationToken cancellationToken = default) 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); + return validator.ValidateAsync(command, ruleSet, cancellationToken); } #endregion Methods diff --git a/Src/DDD.Core/Application/IAsyncCommandHandler.cs b/Src/DDD.Core/Application/IAsyncCommandHandler.cs index 39e7a3c..2a117e4 100644 --- a/Src/DDD.Core/Application/IAsyncCommandHandler.cs +++ b/Src/DDD.Core/Application/IAsyncCommandHandler.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading; +using System.Threading.Tasks; namespace DDD.Core.Application { @@ -11,7 +12,7 @@ public interface IAsyncCommandHandler #region Methods - Task HandleAsync(TCommand command); + Task HandleAsync(TCommand command, CancellationToken cancellationToken = default); #endregion Methods diff --git a/Src/DDD.Core/Application/IAsyncQueryHandler.cs b/Src/DDD.Core/Application/IAsyncQueryHandler.cs index bd144b5..82f31cc 100644 --- a/Src/DDD.Core/Application/IAsyncQueryHandler.cs +++ b/Src/DDD.Core/Application/IAsyncQueryHandler.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading; +using System.Threading.Tasks; namespace DDD.Core.Application { @@ -12,7 +13,7 @@ public interface IAsyncQueryHandler { #region Methods - Task HandleAsync(TQuery query); + Task HandleAsync(TQuery query, CancellationToken cancellationToken = default); #endregion Methods } diff --git a/Src/DDD.Core/Application/ICommandProcessor.cs b/Src/DDD.Core/Application/ICommandProcessor.cs index b58bd1b..1a379e3 100644 --- a/Src/DDD.Core/Application/ICommandProcessor.cs +++ b/Src/DDD.Core/Application/ICommandProcessor.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading; +using System.Threading.Tasks; namespace DDD.Core.Application { @@ -14,11 +15,11 @@ public interface ICommandProcessor void Process(TCommand command) where TCommand : class, ICommand; - Task ProcessAsync(TCommand command) where TCommand : class, ICommand; + Task ProcessAsync(TCommand command, CancellationToken cancellationToken = default) where TCommand : class, ICommand; ValidationResult Validate(TCommand command, string ruleSet = null) where TCommand : class, ICommand; - Task ValidateAsync(TCommand command, string ruleSet = null) where TCommand : class, ICommand; + Task ValidateAsync(TCommand command, string ruleSet = null, CancellationToken cancellationToken = default) where TCommand : class, ICommand; #endregion Methods diff --git a/Src/DDD.Core/Application/IQueryProcessor.cs b/Src/DDD.Core/Application/IQueryProcessor.cs index 75441ff..b9041ac 100644 --- a/Src/DDD.Core/Application/IQueryProcessor.cs +++ b/Src/DDD.Core/Application/IQueryProcessor.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading; +using System.Threading.Tasks; namespace DDD.Core.Application { @@ -14,11 +15,11 @@ public interface IQueryProcessor TResult Process(IQuery query); - Task ProcessAsync(IQuery query); + Task ProcessAsync(IQuery query, CancellationToken cancellationToken = default); ValidationResult Validate(TQuery query, string ruleSet = null) where TQuery : class, IQuery; - Task ValidateAsync(TQuery query, string ruleSet = null) where TQuery : class, IQuery; + Task ValidateAsync(TQuery query, string ruleSet = null, CancellationToken cancellationToken = default) where TQuery : class, IQuery; #endregion Methods diff --git a/Src/DDD.Core/Application/QueryProcessor.cs b/Src/DDD.Core/Application/QueryProcessor.cs index c3fca73..d4e1127 100644 --- a/Src/DDD.Core/Application/QueryProcessor.cs +++ b/Src/DDD.Core/Application/QueryProcessor.cs @@ -1,5 +1,6 @@ using Conditions; using System; +using System.Threading; using System.Threading.Tasks; namespace DDD.Core.Application @@ -39,13 +40,13 @@ public TResult Process(IQuery query) return handler.Handle((dynamic)query); } - public Task ProcessAsync(IQuery query) + public Task ProcessAsync(IQuery query, CancellationToken cancellationToken = default) { Condition.Requires(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, cancellationToken); } public ValidationResult Validate(TQuery query, string ruleSet = null) where TQuery : class, IQuery @@ -56,12 +57,12 @@ public ValidationResult Validate(TQuery query, string ruleSet = null) wh return validator.Validate(query, ruleSet); } - public Task ValidateAsync(TQuery query, string ruleSet = null) where TQuery : class, IQuery + public Task ValidateAsync(TQuery query, string ruleSet = null, CancellationToken cancellationToken = default) 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.ValidateAsync(query, ruleSet); + return validator.ValidateAsync(query, ruleSet, cancellationToken); } #endregion Methods diff --git a/Src/DDD.Core/Domain/IAsyncEventPublisher.cs b/Src/DDD.Core/Domain/IAsyncEventPublisher.cs index 8b8a052..ff34360 100644 --- a/Src/DDD.Core/Domain/IAsyncEventPublisher.cs +++ b/Src/DDD.Core/Domain/IAsyncEventPublisher.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading; +using System.Threading.Tasks; namespace DDD.Core.Domain { @@ -7,7 +8,7 @@ public interface IAsyncEventPublisher #region Methods - Task PublishAsync(IEvent @event); + Task PublishAsync(IEvent @event, CancellationToken cancellationToken = default); #endregion Methods diff --git a/Src/DDD.Core/Domain/IAsyncRepository.cs b/Src/DDD.Core/Domain/IAsyncRepository.cs index b8ba09a..bf3a137 100644 --- a/Src/DDD.Core/Domain/IAsyncRepository.cs +++ b/Src/DDD.Core/Domain/IAsyncRepository.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading; +using System.Threading.Tasks; namespace DDD.Core.Domain { @@ -8,9 +9,9 @@ public interface IAsyncRepository { #region Methods - Task FindAsync(TIdentity identity); + Task FindAsync(TIdentity identity, CancellationToken cancellationToken = default); - Task SaveAsync(TDomainEntity aggregate); + Task SaveAsync(TDomainEntity aggregate, CancellationToken cancellationToken = default); #endregion Methods } diff --git a/Src/DDD.Core/Infrastructure/Data/DbQueryHandler.cs b/Src/DDD.Core/Infrastructure/Data/DbQueryHandler.cs index f2476b9..a2e4f22 100644 --- a/Src/DDD.Core/Infrastructure/Data/DbQueryHandler.cs +++ b/Src/DDD.Core/Infrastructure/Data/DbQueryHandler.cs @@ -1,6 +1,7 @@ using Conditions; using System.Data; using System.Data.Common; +using System.Threading; using System.Threading.Tasks; namespace DDD.Core.Infrastructure.Data @@ -47,7 +48,7 @@ protected DbQueryHandler(IDbConnectionFactory connectionFactory) #region Methods - public async Task HandleAsync(TQuery query) + public async Task HandleAsync(TQuery query, CancellationToken cancellationToken = default) { Condition.Requires(query, nameof(query)).IsNotNull(); await new SynchronizationContextRemover(); @@ -55,7 +56,7 @@ public async Task HandleAsync(TQuery query) { using (var connection = await this.ConnectionFactory.CreateOpenConnectionAsync()) { - return await this.ExecuteAsync(query, connection); + return await this.ExecuteAsync(query, connection, cancellationToken); } } catch(DbException ex) @@ -65,7 +66,7 @@ public async Task HandleAsync(TQuery query) } - protected abstract Task ExecuteAsync(TQuery query, IDbConnection connection); + protected abstract Task ExecuteAsync(TQuery query, IDbConnection connection, CancellationToken cancellationToken = default); #endregion Methods diff --git a/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionCreator.cs b/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionCreator.cs index 34d8bc9..735cdce 100644 --- a/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionCreator.cs +++ b/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionCreator.cs @@ -1,4 +1,5 @@ using Conditions; +using System.Threading; using System.Threading.Tasks; using System.Transactions; @@ -39,12 +40,12 @@ public PharmaceuticalPrescriptionCreator(IAsyncRepository> ExecuteAsync(FindPharmaceuticalPrescriptionsByPatient query, - IDbConnection connection) + IDbConnection connection, + CancellationToken cancellationToken = default) { var expressions = connection.Expressions(); return connection.QueryAsync ( - SqlScripts.FindPharmaceuticalPrescriptionsByPatient.Replace("@", expressions.ParameterPrefix()), - new { PatientId = query.PatientIdentifier } + new CommandDefinition + ( + SqlScripts.FindPharmaceuticalPrescriptionsByPatient.Replace("@", expressions.ParameterPrefix()), + new { PatientId = query.PatientIdentifier }, + cancellationToken: cancellationToken + ) ); } diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinder.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinder.cs index a9604d3..49a8f6c 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinder.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinder.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using Dapper; using System.Data; +using System.Threading; using System.Threading.Tasks; namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions @@ -24,13 +25,18 @@ public PrescribedMedicationsByPrescriptionFinder(IHealthcareDeliveryConnectionFa #region Methods protected override Task> ExecuteAsync(FindPrescribedMedicationsByPrescription query, - IDbConnection connection) + IDbConnection connection, + CancellationToken cancellationToken = default) { var expressions = connection.Expressions(); return connection.QueryAsync ( - SqlScripts.FindPrescribedMedicationsByPrescription.Replace("@", expressions.ParameterPrefix()), - new { PrescriptionId = query.PrescriptionIdentifier } + new CommandDefinition + ( + SqlScripts.FindPrescribedMedicationsByPrescription.Replace("@", expressions.ParameterPrefix()), + new { PrescriptionId = query.PrescriptionIdentifier }, + cancellationToken: cancellationToken + ) ); } diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionIdentifierGenerator.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionIdentifierGenerator.cs index 06a60ed..6532d70 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionIdentifierGenerator.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionIdentifierGenerator.cs @@ -1,4 +1,5 @@ using System.Data; +using System.Threading; using System.Threading.Tasks; namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions @@ -20,9 +21,11 @@ public PrescriptionIdentifierGenerator(IHealthcareDeliveryConnectionFactory conn #region Methods - protected override Task ExecuteAsync(GeneratePrescriptionIdentifier query, IDbConnection connection) + protected override Task ExecuteAsync(GeneratePrescriptionIdentifier query, + IDbConnection connection, + CancellationToken cancellationToken = default) { - return connection.NextValueAsync("PrescriptionId"); + return connection.NextValueAsync("PrescriptionId", cancellationToken: cancellationToken); } #endregion Methods From d3d7020445141ab8fb1e0c130e9f13e039452cf0 Mon Sep 17 00:00:00 2001 From: draphyz Date: Sun, 31 Oct 2021 23:06:04 +0100 Subject: [PATCH 049/111] Refactor timestamp providers --- .../DateTimeExtensions.cs | 16 +++- .../DelegatingTimestampProvider.cs | 36 ++++++++ .../ITimestampProvider.cs | 17 ++++ Src/DDD.Core.Abstractions/SystemTime.cs | 84 +++++++++++++++++++ .../TimeStampProvider.cs | 67 --------------- .../UniqueTimestampProvider.cs | 67 +++++++++++++++ .../Domain/EventTimestampProvider.cs | 33 -------- .../PharmaceuticalPrescription.cs | 4 +- Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs | 2 +- Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs | 2 +- 10 files changed, 220 insertions(+), 108 deletions(-) create mode 100644 Src/DDD.Core.Abstractions/DelegatingTimestampProvider.cs create mode 100644 Src/DDD.Core.Abstractions/ITimestampProvider.cs create mode 100644 Src/DDD.Core.Abstractions/SystemTime.cs delete mode 100644 Src/DDD.Core.Abstractions/TimeStampProvider.cs create mode 100644 Src/DDD.Core.Abstractions/UniqueTimestampProvider.cs delete mode 100644 Src/DDD.HealthcareDelivery/Domain/EventTimestampProvider.cs diff --git a/Src/DDD.Core.Abstractions/DateTimeExtensions.cs b/Src/DDD.Core.Abstractions/DateTimeExtensions.cs index bfa33a8..26d397e 100644 --- a/Src/DDD.Core.Abstractions/DateTimeExtensions.cs +++ b/Src/DDD.Core.Abstractions/DateTimeExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using Conditions; namespace DDD { @@ -12,11 +13,11 @@ 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. + /// Gets the next date and time. /// - public static string ToShortDateString(this DateTime instance, IFormatProvider provider) + public static DateTime Next(this DateTime instance) { - return instance.ToString("d", provider); + return instance.AddTicks(1); } /// @@ -27,7 +28,14 @@ public static string ToFrenchShortDateString(this DateTime instance) return instance.ToString("d", new CultureInfo("fr-FR")); } - #endregion Methods + /// + /// 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..02b1e55 --- /dev/null +++ b/Src/DDD.Core.Abstractions/DelegatingTimestampProvider.cs @@ -0,0 +1,36 @@ +using Conditions; +using System; + +namespace DDD +{ + public class DelegatingTimestampProvider : ITimestampProvider + { + + #region Fields + + private readonly Func timestamp; + + #endregion Fields + + #region Constructors + + public DelegatingTimestampProvider(Func timestamp) + { + Condition.Requires(timestamp, nameof(timestamp)).IsNotNull(); + this.timestamp = timestamp; + } + + #endregion Constructors + + #region Methods + + public static ITimestampProvider CreateLocal() => new DelegatingTimestampProvider(() => DateTime.Now); + + public static ITimestampProvider CreateUniversal() => new DelegatingTimestampProvider(() => DateTime.UtcNow); + + public DateTime GetTimestamp() => this.timestamp(); + + #endregion Methods + + } +} \ No newline at end of file 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/SystemTime.cs b/Src/DDD.Core.Abstractions/SystemTime.cs new file mode 100644 index 0000000..53396c4 --- /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 unit 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. + /// + public static DateTime Local() => localProvider.GetTimestamp(); + + /// + /// Resets the default timestamp providers (for unit testing). + /// + public static void Reset() + { + localProvider = DelegatingTimestampProvider.CreateLocal(); + universalProvider = DelegatingTimestampProvider.CreateUniversal(); + } + + /// + /// Replaces the default local timestamp provider (for unit testing). + /// + public static void SetLocalProvider(ITimestampProvider provider) + { + localProvider = provider; + } + + /// + /// Replaces the default local timestamp provider (for unit testing). + /// + public static void SetLocalProvider(Func timestamp) + { + localProvider = new DelegatingTimestampProvider(timestamp); + } + + /// + /// Replaces the default universal timestamp provider (for unit testing). + /// + public static void SetUniversalProvider(ITimestampProvider provider) + { + universalProvider = provider; + } + + /// + /// Replaces the default universal timestamp provider (for unit 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/TimeStampProvider.cs b/Src/DDD.Core.Abstractions/TimeStampProvider.cs deleted file mode 100644 index 67a68ef..0000000 --- a/Src/DDD.Core.Abstractions/TimeStampProvider.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; - -namespace DDD -{ - /// - /// Provides unique timestamps based on the current date and time with a specified resolution in milliseconds. - /// - public class TimestampProvider - { - - #region Fields - - private readonly object locker = new object(); - private readonly Func now; - private DateTime lastTimestamp = DateTime.MinValue; - - #endregion Fields - - #region Constructors - - public TimestampProvider(bool isLocal, double resolutionInMs = 1) - { - this.IsLocal = isLocal; - this.ResolutionInMs = resolutionInMs; - if (isLocal) - this.now = () => DateTime.Now; - else - this.now = () => DateTime.UtcNow; - } - - #endregion Constructors - - #region Properties - - /// - /// Indicates whether provided timestamps are local or universal. - /// - public bool IsLocal { get; } - - /// - /// Indicates the resolution in milliseconds of provided timestamps. - /// - public double ResolutionInMs { get; } - - #endregion Properties - - #region Methods - - /// - /// Provides a timestamp. - /// - public DateTime Timestamp() - { - var now = this.now(); - lock (locker) - { - if ((now - lastTimestamp).TotalMilliseconds < ResolutionInMs) - now = lastTimestamp.AddMilliseconds(ResolutionInMs); - lastTimestamp = now; - } - return now; - } - - #endregion Methods - - } -} diff --git a/Src/DDD.Core.Abstractions/UniqueTimestampProvider.cs b/Src/DDD.Core.Abstractions/UniqueTimestampProvider.cs new file mode 100644 index 0000000..0aab5d8 --- /dev/null +++ b/Src/DDD.Core.Abstractions/UniqueTimestampProvider.cs @@ -0,0 +1,67 @@ +using System; + +namespace DDD +{ + /// + /// Provides unique timestamps based on the current date and time on this computer with a specified resolution. + /// + public class UniqueTimestampProvider : ITimestampProvider + { + + #region Fields + + private readonly object locker = new object(); + private readonly Func timestamp; + private DateTime lastTimestamp = DateTime.MinValue; + + #endregion Fields + + #region Constructors + + public UniqueTimestampProvider(bool isLocal, TimeSpan resolution) + { + this.IsLocal = isLocal; + this.Resolution = resolution; + if (isLocal) + this.timestamp = () => DateTime.Now; + else + this.timestamp = () => DateTime.UtcNow; + } + + #endregion Constructors + + #region Properties + + /// + /// Indicates whether the provided timestamps are local or universal. + /// + public bool IsLocal { get; } + + /// + /// Indicates the resolution of the provided timestamps. + /// + public TimeSpan Resolution { get; } + + #endregion Properties + + #region Methods + + /// + /// Provides a unique timestamp on this computer. + /// + public DateTime GetTimestamp() + { + var timestamp = this.timestamp(); + 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.HealthcareDelivery/Domain/EventTimestampProvider.cs b/Src/DDD.HealthcareDelivery/Domain/EventTimestampProvider.cs deleted file mode 100644 index f2129fb..0000000 --- a/Src/DDD.HealthcareDelivery/Domain/EventTimestampProvider.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; - -namespace DDD.HealthcareDelivery.Domain -{ - /// - /// Provides unique timestamps for the events of the context of healthcare delivery. - /// - public static class EventTimestampProvider - { - - #region Fields - - private readonly static TimestampProvider local = new TimestampProvider(isLocal: true); - private readonly static TimestampProvider universal = new TimestampProvider(isLocal: false); - - #endregion Fields - - #region Methods - - /// - /// Provides a local timestamp with a resolution of one millisecond. - /// - public static DateTime LocalTimestamp() => local.Timestamp(); - - /// - /// Provides a universal timestamp with a resolution of one millisecond. - /// - public static DateTime UniversalTimestamp() => universal.Timestamp(); - - #endregion Methods - - } -} diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs index 42c9b01..fbf5c75 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs @@ -84,14 +84,14 @@ public static PharmaceuticalPrescription Create(PrescriptionIdentifier identifie EncounterIdentifier encounterIdentifier = null, DateTime? delivrableAt = null) { - return Create(identifier, prescriber, patient, prescribedMedications, EventTimestampProvider.LocalTimestamp(), languageCode, encounterIdentifier, delivrableAt); + return Create(identifier, prescriber, patient, prescribedMedications, SystemTime.Local(), languageCode, encounterIdentifier, delivrableAt); } public IEnumerable PrescribedMedications() => this.prescribedMedications.ToImmutableHashSet(); protected override void AddPrescriptionRevokedEvent(string reason) { - this.AddEvent(new PharmaceuticalPrescriptionRevoked(this.Identifier.Value, EventTimestampProvider.LocalTimestamp(), reason)); + this.AddEvent(new PharmaceuticalPrescriptionRevoked(this.Identifier.Value, SystemTime.Local(), reason)); } #endregion Methods diff --git a/Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs b/Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs index f3acba1..aeced88 100644 --- a/Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs +++ b/Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs @@ -6,7 +6,7 @@ public class FakeEvent1 : IEvent { #region Properties - public DateTime OccurredOn => DateTime.Now; + public DateTime OccurredOn => SystemTime.Local(); #endregion Properties } diff --git a/Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs b/Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs index 9c38fd7..108cc83 100644 --- a/Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs +++ b/Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs @@ -6,7 +6,7 @@ public class FakeEvent3 : IEvent { #region Properties - public DateTime OccurredOn => DateTime.Now; + public DateTime OccurredOn => SystemTime.Local(); #endregion Properties } From af5490135a5fa666f49959578ffb30515ae0465b Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 8 Nov 2021 15:04:46 +0100 Subject: [PATCH 050/111] Refactor event publishing --- DDD.sln | 7 - .../IServiceProviderExtensions.cs | 28 +++ .../DDD.Core.NServiceBus.csproj | 33 ---- .../EndpointConfigurationExtensions.cs | 46 ----- .../NServiceBusEventPublisher.cs | 43 ---- .../Properties/AssemblyInfo.cs | 7 - .../ContainerExtensions.cs | 13 ++ .../CustomMemberDataAttribute.cs | 1 - Src/DDD.Core/Domain/AsyncEventHandler.cs | 27 +++ Src/DDD.Core/Domain/EventHandler.cs | 5 +- Src/DDD.Core/Domain/EventPublisher.cs | 61 ++++-- Src/DDD.Core/Domain/IAsyncEventHandler.cs | 21 ++ Src/DDD.Core/Domain/IAsyncEventHandler`1.cs | 20 ++ Src/DDD.Core/Domain/IAsyncEventPublisher.cs | 16 -- Src/DDD.Core/Domain/IEventHandler`1.cs | 5 +- Src/DDD.Core/Domain/IEventPublisher.cs | 13 +- .../Domain/IEventPublisherExtensions.cs | 67 +------ .../PharmaceuticalPrescriptionCreator.cs | 5 - .../PharmaceuticalPrescriptionRevoker.cs | 7 +- .../DDD.Core.UnitTests.csproj | 1 + .../Domain/EventPublisherTests.cs | 187 +++++++++++------- .../PharmaceuticalPrescriptionCreatorTests.cs | 1 - .../PharmaceuticalPrescriptionRevokerTests.cs | 3 +- 23 files changed, 302 insertions(+), 315 deletions(-) delete mode 100644 Src/DDD.Core.NServiceBus/DDD.Core.NServiceBus.csproj delete mode 100644 Src/DDD.Core.NServiceBus/EndpointConfigurationExtensions.cs delete mode 100644 Src/DDD.Core.NServiceBus/NServiceBusEventPublisher.cs delete mode 100644 Src/DDD.Core.NServiceBus/Properties/AssemblyInfo.cs create mode 100644 Src/DDD.Core/Domain/AsyncEventHandler.cs create mode 100644 Src/DDD.Core/Domain/IAsyncEventHandler.cs create mode 100644 Src/DDD.Core/Domain/IAsyncEventHandler`1.cs delete mode 100644 Src/DDD.Core/Domain/IAsyncEventPublisher.cs diff --git a/DDD.sln b/DDD.sln index 2177215..5abd580 100644 --- a/DDD.sln +++ b/DDD.sln @@ -53,8 +53,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.HealthcareDelivery.Mess 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.NServiceBus", "Src\DDD.Core.NServiceBus\DDD.Core.NServiceBus.csproj", "{436D869F-7566-4436-90A8-B655145C5BCA}" -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}" @@ -135,10 +133,6 @@ 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 @@ -180,7 +174,6 @@ 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} diff --git a/Src/DDD.Core.Abstractions/IServiceProviderExtensions.cs b/Src/DDD.Core.Abstractions/IServiceProviderExtensions.cs index 5e016a9..bc2f956 100644 --- a/Src/DDD.Core.Abstractions/IServiceProviderExtensions.cs +++ b/Src/DDD.Core.Abstractions/IServiceProviderExtensions.cs @@ -1,5 +1,7 @@ using Conditions; using System; +using System.Collections.Generic; +using System.Linq; namespace DDD { @@ -23,6 +25,32 @@ public static T GetService(this IServiceProvider provider) 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) + { + Condition.Requires(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) + { + Condition.Requires(provider, nameof(provider)).IsNotNull(); + Condition.Requires(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.NServiceBus/DDD.Core.NServiceBus.csproj b/Src/DDD.Core.NServiceBus/DDD.Core.NServiceBus.csproj deleted file mode 100644 index 7bee749..0000000 --- a/Src/DDD.Core.NServiceBus/DDD.Core.NServiceBus.csproj +++ /dev/null @@ -1,33 +0,0 @@ - - - net48;netstandard2.1;net5.0 - Library - DDD.Core.Infrastructure.Messaging - false - - - 1591 - bin\Debug\DDD.Core.NServiceBus.xml - - - bin\Release\DDD.Core.NServiceBus.xml - 1591 - - - - Properties\CommonAssemblyInfo.cs - - - - - - - - - - - - 7.5.0 - - - \ 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 3acc3fd..0000000 --- a/Src/DDD.Core.NServiceBus/NServiceBusEventPublisher.cs +++ /dev/null @@ -1,43 +0,0 @@ -using NServiceBus; -using System.Threading; -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, CancellationToken cancellationToken = default) - { - Condition.Requires(@event, nameof(@event)).IsNotNull(); - await new SynchronizationContextRemover(); - // Cancellation token support will be implemented in NServiceBus 8 (not yet released) - 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.SimpleInjector/ContainerExtensions.cs b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs index 9f1ede7..2ae93cb 100644 --- a/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs +++ b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs @@ -8,6 +8,8 @@ namespace DDD.Core.Infrastructure.DependencyInjection { + using Core.Domain; + public static class ContainerExtensions { @@ -36,6 +38,17 @@ public static void RegisterConditional(this Container container, Type openGeneri container.Register(openGenericServiceType, implementationTypes); } + public static void RegisterEventHandlers(this Container container, params Assembly[] assemblies) + { + Condition.Requires(container, nameof(container)).IsNotNull(); + var handlerTypes = container.GetTypesToRegister(typeof(IEventHandler<>), assemblies, new TypesToRegisterOptions + { + IncludeGenericTypeDefinitions = true, + IncludeComposites = false, + }); + container.Collection.Register(typeof(IEventHandler<>), handlerTypes); + } + #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/Domain/AsyncEventHandler.cs b/Src/DDD.Core/Domain/AsyncEventHandler.cs new file mode 100644 index 0000000..76be362 --- /dev/null +++ b/Src/DDD.Core/Domain/AsyncEventHandler.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace DDD.Core.Domain +{ + /// + /// Base class for handling asynchronously events. + /// + public abstract class AsyncEventHandler : IAsyncEventHandler + where TEvent : class, IEvent + { + #region Properties + + Type IAsyncEventHandler.EventType => typeof(TEvent); + + #endregion Properties + + #region Methods + + public abstract Task HandleAsync(TEvent @event, CancellationToken cancellationToken = default); + + Task IAsyncEventHandler.HandleAsync(IEvent @event, CancellationToken cancellationToken) => this.HandleAsync((TEvent)@event, cancellationToken); + + #endregion Methods + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Domain/EventHandler.cs b/Src/DDD.Core/Domain/EventHandler.cs index 63dbe06..3cd7e76 100644 --- a/Src/DDD.Core/Domain/EventHandler.cs +++ b/Src/DDD.Core/Domain/EventHandler.cs @@ -2,8 +2,11 @@ namespace DDD.Core.Domain { + /// + /// Base class for handling events. + /// public abstract class EventHandler : IEventHandler - where TEvent : IEvent + where TEvent : class, IEvent { #region Properties diff --git a/Src/DDD.Core/Domain/EventPublisher.cs b/Src/DDD.Core/Domain/EventPublisher.cs index d3d5728..31f9181 100644 --- a/Src/DDD.Core/Domain/EventPublisher.cs +++ b/Src/DDD.Core/Domain/EventPublisher.cs @@ -1,41 +1,70 @@ using Conditions; -using System.Collections.Generic; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace DDD.Core.Domain { + using Threading; + public class EventPublisher : IEventPublisher { #region Fields - private List subscribers = new List(); + private readonly IServiceProvider serviceProvider; #endregion Fields + #region Constructors + + public EventPublisher(IServiceProvider serviceProvider) + { + Condition.Requires(serviceProvider, nameof(serviceProvider)).IsNotNull(); + this.serviceProvider = serviceProvider; + } + + #endregion Constructors + #region Methods - public void Publish(IEvent @event) + public void Publish(TEvent @event) where TEvent : class, IEvent { Condition.Requires(@event, nameof(@event)).IsNotNull(); - foreach (var subscriber in this.subscribers) + if (typeof(TEvent) == typeof(IEvent)) { - if (subscriber.EventType.IsAssignableFrom(@event.GetType())) - subscriber.Handle(@event); + var handlerType = typeof(IEventHandler<>).MakeGenericType(@event.GetType()); + var handlers = this.serviceProvider.GetServices(handlerType).Cast(); + foreach (var handler in handlers) + handler.Handle(@event); + } + else + { + var handlers = this.serviceProvider.GetServices>(); + foreach (var handler in handlers) + handler.Handle(@event); } } - public void Subscribe(IEventHandler subscriber) - { - Condition.Requires(subscriber, nameof(subscriber)).IsNotNull(); - this.subscribers.Add(subscriber); - } - - public void UnSubscribe(IEventHandler subscriber) + public async Task PublishAsync(TEvent @event, CancellationToken cancellationToken = default) where TEvent : class, IEvent { - Condition.Requires(subscriber, nameof(subscriber)).IsNotNull(); - this.subscribers.Remove(subscriber); + Condition.Requires(@event, nameof(@event)).IsNotNull(); + await new SynchronizationContextRemover(); + if (typeof(TEvent) == typeof(IEvent)) + { + var handlerType = typeof(IAsyncEventHandler<>).MakeGenericType(@event.GetType()); + var handlers = this.serviceProvider.GetServices(handlerType).Cast(); + foreach (var handler in handlers) + await handler.HandleAsync(@event, cancellationToken); + } + else + { + var handlers = this.serviceProvider.GetServices>(); + foreach (var handler in handlers) + await handler.HandleAsync(@event, cancellationToken); + } } #endregion Methods - } } \ No newline at end of file diff --git a/Src/DDD.Core/Domain/IAsyncEventHandler.cs b/Src/DDD.Core/Domain/IAsyncEventHandler.cs new file mode 100644 index 0000000..9de1670 --- /dev/null +++ b/Src/DDD.Core/Domain/IAsyncEventHandler.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace DDD.Core.Domain +{ + public interface IAsyncEventHandler + { + #region Properties + + Type EventType { get; } + + #endregion Properties + + #region Methods + + Task HandleAsync(IEvent @event, CancellationToken cancellationToken = default); + + #endregion Methods + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Domain/IAsyncEventHandler`1.cs b/Src/DDD.Core/Domain/IAsyncEventHandler`1.cs new file mode 100644 index 0000000..c68fdff --- /dev/null +++ b/Src/DDD.Core/Domain/IAsyncEventHandler`1.cs @@ -0,0 +1,20 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace DDD.Core.Domain +{ + /// + /// Defines a method that handles asynchronously an event of a specified type. + /// + public interface IAsyncEventHandler : IAsyncEventHandler + where TEvent : class, IEvent + { + + #region Methods + + Task HandleAsync(TEvent @event, CancellationToken cancellationToken = default); + + #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 ff34360..0000000 --- a/Src/DDD.Core/Domain/IAsyncEventPublisher.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace DDD.Core.Domain -{ - public interface IAsyncEventPublisher - { - - #region Methods - - Task PublishAsync(IEvent @event, CancellationToken cancellationToken = default); - - #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 index b7bbb9c..1af5371 100644 --- a/Src/DDD.Core/Domain/IEventHandler`1.cs +++ b/Src/DDD.Core/Domain/IEventHandler`1.cs @@ -1,7 +1,10 @@ namespace DDD.Core.Domain { + /// + /// Defines a method that handles an event of a specified type. + /// public interface IEventHandler : IEventHandler - where TEvent : IEvent + where TEvent : class, IEvent { #region Methods diff --git a/Src/DDD.Core/Domain/IEventPublisher.cs b/Src/DDD.Core/Domain/IEventPublisher.cs index 4e62274..d711072 100644 --- a/Src/DDD.Core/Domain/IEventPublisher.cs +++ b/Src/DDD.Core/Domain/IEventPublisher.cs @@ -1,18 +1,19 @@ -namespace DDD.Core.Domain +using System.Threading; +using System.Threading.Tasks; + +namespace DDD.Core.Domain { /// - /// Publish synchronously events inside the local bounded context (used to decouple layers). + /// Defines a component that publishes events of any type. /// public interface IEventPublisher { #region Methods - void Publish(IEvent @event); - - void Subscribe(IEventHandler subscriber); + void Publish(TEvent @event) where TEvent : class, IEvent; - void UnSubscribe(IEventHandler subscriber); + Task PublishAsync(TEvent @event, CancellationToken cancellationToken = default) where TEvent : class, IEvent; #endregion Methods diff --git a/Src/DDD.Core/Domain/IEventPublisherExtensions.cs b/Src/DDD.Core/Domain/IEventPublisherExtensions.cs index 4a411f0..0717b41 100644 --- a/Src/DDD.Core/Domain/IEventPublisherExtensions.cs +++ b/Src/DDD.Core/Domain/IEventPublisherExtensions.cs @@ -1,9 +1,11 @@ using Conditions; -using System; using System.Collections.Generic; namespace DDD.Core.Domain { + using System.Threading; + using Threading; + public static class IEventPublisherExtensions { @@ -19,69 +21,18 @@ public static void PublishAll(this IEventPublisher publisher, IEnumerable(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) + public async static void PublishAllAsync(this IEventPublisher publisher, IEnumerable events, CancellationToken cancellationToken = default) { Condition.Requires(publisher, nameof(publisher)).IsNotNull(); - Condition.Requires(subscribers, nameof(subscribers)) + Condition.Requires(events, nameof(events)) .IsNotNull() .DoesNotContain(null); - foreach (var subscriber in subscribers) - publisher.UnSubscribe(subscriber); + await new SynchronizationContextRemover(); + foreach (var @event in events) + await publisher.PublishAsync(@event, cancellationToken); } #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 - } -} +} \ 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 735cdce..9470073 100644 --- a/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionCreator.cs +++ b/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionCreator.cs @@ -16,7 +16,6 @@ public class PharmaceuticalPrescriptionCreator #region Fields - private readonly IEventPublisher publisher; private readonly IAsyncRepository repository; private readonly IObjectTranslator translator; @@ -25,14 +24,11 @@ public class PharmaceuticalPrescriptionCreator #region Constructors public PharmaceuticalPrescriptionCreator(IAsyncRepository repository, - IEventPublisher publisher, IObjectTranslator translator) { Condition.Requires(repository, nameof(repository)).IsNotNull(); - Condition.Requires(publisher, nameof(publisher)).IsNotNull(); Condition.Requires(translator, nameof(translator)).IsNotNull(); this.repository = repository; - this.publisher = publisher; this.translator = translator; } @@ -46,7 +42,6 @@ protected override async Task ExecuteAsync(CreatePharmaceuticalPrescription comm using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) { await this.repository.SaveAsync(prescription, cancellationToken); - this.publisher.PublishAll(prescription.AllEvents()); scope.Complete(); } } diff --git a/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionRevoker.cs b/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionRevoker.cs index 1c44e67..30ef5c0 100644 --- a/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionRevoker.cs +++ b/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionRevoker.cs @@ -15,20 +15,16 @@ public class PharmaceuticalPrescriptionRevoker #region Fields - private readonly IEventPublisher publisher; private readonly IAsyncRepository repository; #endregion Fields #region Constructors - public PharmaceuticalPrescriptionRevoker(IAsyncRepository repository, - IEventPublisher publisher) + public PharmaceuticalPrescriptionRevoker(IAsyncRepository repository) { Condition.Requires(repository, nameof(repository)).IsNotNull(); - Condition.Requires(publisher, nameof(publisher)).IsNotNull(); this.repository = repository; - this.publisher = publisher; } #endregion Constructors @@ -42,7 +38,6 @@ protected override async Task ExecuteAsync(RevokePharmaceuticalPrescription comm var prescription = await this.repository.FindAsync(new PrescriptionIdentifier(command.PrescriptionIdentifier)); prescription.Revoke(command.RevocationReason); await this.repository.SaveAsync(prescription, cancellationToken); - this.publisher.PublishAll(prescription.AllEvents()); scope.Complete(); } } diff --git a/Test/DDD.Core.UnitTests/DDD.Core.UnitTests.csproj b/Test/DDD.Core.UnitTests/DDD.Core.UnitTests.csproj index 676d92a..d249a91 100644 --- a/Test/DDD.Core.UnitTests/DDD.Core.UnitTests.csproj +++ b/Test/DDD.Core.UnitTests/DDD.Core.UnitTests.csproj @@ -22,6 +22,7 @@ 4.2.2 + 4.5.4 diff --git a/Test/DDD.Core.UnitTests/Domain/EventPublisherTests.cs b/Test/DDD.Core.UnitTests/Domain/EventPublisherTests.cs index 20ee298..575754a 100644 --- a/Test/DDD.Core.UnitTests/Domain/EventPublisherTests.cs +++ b/Test/DDD.Core.UnitTests/Domain/EventPublisherTests.cs @@ -1,161 +1,216 @@ using NSubstitute; using System.Collections.Generic; using Xunit; +using SimpleInjector; namespace DDD.Core.Domain { using Collections; + using System.Threading.Tasks; public class EventPublisherTests { + #region Methods - public static IEnumerable SubscribersToOtherEventsThanThisEvent() + public static IEnumerable AsyncHandlersOfOtherEventsThanThisEvent() + { + var fakeHandler1 = FakeAsyncHandler(); + var fakeHandler2 = FakeAsyncHandler(); + var fakeHandler3 = FakeAsyncHandler(); + var fakeHandler4 = FakeAsyncHandler(); + var container = new Container(); + container.Collection.Register(fakeHandler1); + container.Collection.Register(fakeHandler2); + container.Collection.Register(fakeHandler3, fakeHandler4); + var publisher = new EventPublisher(container); + yield return new object[] + { + publisher, + new FakeEvent1(), + new IAsyncEventHandler[] { fakeHandler2, fakeHandler3, fakeHandler4 } + }; + yield return new object[] + { + publisher, + new FakeEvent2(), + new IAsyncEventHandler[] { fakeHandler3, fakeHandler4 } + }; + yield return new object[] + { + publisher, + new FakeEvent3(), + new IAsyncEventHandler[] { fakeHandler1, fakeHandler2 } + }; + } + + public static IEnumerable AsyncHandlersOfThisEvent() { - 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); + var fakeHandler1 = FakeAsyncHandler(); + var fakeHandler2 = FakeAsyncHandler(); + var fakeHandler3 = FakeAsyncHandler(); + var fakeHandler4 = FakeAsyncHandler(); + var container = new Container(); + container.Collection.Register(fakeHandler1); + container.Collection.Register(fakeHandler2); + container.Collection.Register(fakeHandler3, fakeHandler4); + var publisher = new EventPublisher(container); yield return new object[] { publisher, new FakeEvent1(), - new IEventHandler[] { fakeSubscriber2, fakeSubscriber3, fakeSubscriber4 } + new IAsyncEventHandler[] { fakeHandler1 } }; yield return new object[] { publisher, new FakeEvent2(), - new IEventHandler[] { fakeSubscriber3, fakeSubscriber4 } + new IAsyncEventHandler[] { fakeHandler1, fakeHandler2 } }; yield return new object[] { publisher, new FakeEvent3(), - new IEventHandler[] { fakeSubscriber1, fakeSubscriber2 } + new IAsyncEventHandler[] { fakeHandler3, fakeHandler4 } }; } - public static IEnumerable SubscribersToThisEvent() + public static IEnumerable HandlersOfOtherEventsThanThisEvent() { - 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); + var fakeHandler1 = FakeHandler(); + var fakeHandler2 = FakeHandler(); + var fakeHandler3 = FakeHandler(); + var fakeHandler4 = FakeHandler(); + var container = new Container(); + container.Collection.Register(fakeHandler1); + container.Collection.Register(fakeHandler2); + container.Collection.Register(fakeHandler3, fakeHandler4); + var publisher = new EventPublisher(container); yield return new object[] { publisher, new FakeEvent1(), - new IEventHandler[] { fakeSubscriber1 } + new IEventHandler[] { fakeHandler2, fakeHandler3, fakeHandler4 } }; yield return new object[] { publisher, new FakeEvent2(), - new IEventHandler[] { fakeSubscriber1, fakeSubscriber2 } + new IEventHandler[] { fakeHandler3, fakeHandler4 } }; yield return new object[] { publisher, new FakeEvent3(), - new IEventHandler[] { fakeSubscriber3, fakeSubscriber4 } + new IEventHandler[] { fakeHandler1, fakeHandler2 } }; } - public static IEnumerable UnSubscribersToThisEvent() + public static IEnumerable HandlersOfThisEvent() { - 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); + var fakeHandler1 = FakeHandler(); + var fakeHandler2 = FakeHandler(); + var fakeHandler3 = FakeHandler(); + var fakeHandler4 = FakeHandler(); + var container = new Container(); + container.Collection.Register(fakeHandler1); + container.Collection.Register(fakeHandler2); + container.Collection.Register(fakeHandler3, fakeHandler4); + var publisher = new EventPublisher(container); yield return new object[] { publisher, new FakeEvent1(), - new IEventHandler[] { fakeSubscriber1 } + new IEventHandler[] { fakeHandler1 } }; yield return new object[] { publisher, new FakeEvent2(), - new IEventHandler[] { fakeSubscriber1, fakeSubscriber2 } + new IEventHandler[] { fakeHandler1, fakeHandler2 } }; yield return new object[] { publisher, new FakeEvent3(), - new IEventHandler[] { fakeSubscriber3, fakeSubscriber4 } + new IEventHandler[] { fakeHandler3, fakeHandler4 } }; } [Theory] - [MemberData(nameof(SubscribersToThisEvent))] - public void Publish_WhenCalled_CallsSubscribersToThisEvent(EventPublisher publisher, - IEvent @event, - IEnumerable subscribersToThisEvent) + [MemberData(nameof(HandlersOfThisEvent))] + public void Publish_WhenCalled_CallsHandlersOfThisEvent(EventPublisher publisher, + IEvent @event, + IEnumerable handlersOfThisEvent) { // Arrange - subscribersToThisEvent.ForEach(s => s.ClearReceivedCalls()); + handlersOfThisEvent.ForEach(s => s.ClearReceivedCalls()); // Act publisher.Publish(@event); // Assert - Assert.All(subscribersToThisEvent, s => s.Received(1).Handle(@event)); + Assert.All(handlersOfThisEvent, s => s.Received(1).Handle(@event)); } + [Theory] - [MemberData(nameof(UnSubscribersToThisEvent))] - public void Publish_WhenCalled_DoesNotCallUnSubscribersToThisEvent(EventPublisher publisher, - IEvent @event, - IEnumerable unSubscribersToThisEvent) + [MemberData(nameof(HandlersOfOtherEventsThanThisEvent))] + public void Publish_WhenCalled_DoesNotCallHandlersOfOtherEvents(EventPublisher publisher, + IEvent @event, + IEnumerable handlersOfOtherEvents) { // Arrange - unSubscribersToThisEvent.ForEach(s => s.ClearReceivedCalls()); + handlersOfOtherEvents.ForEach(s => s.ClearReceivedCalls()); // Act publisher.Publish(@event); // Assert - Assert.All(unSubscribersToThisEvent, s => s.DidNotReceive().Handle(Arg.Any())); + Assert.All(handlersOfOtherEvents, s => s.DidNotReceive().Handle(Arg.Any())); } [Theory] - [MemberData(nameof(SubscribersToOtherEventsThanThisEvent))] - public void Publish_WhenCalled_DoesNotCallSubscribersToOtherEvents(EventPublisher publisher, - IEvent @event, - IEnumerable subscribersToOtherEvents) + [MemberData(nameof(AsyncHandlersOfThisEvent))] + public async Task PublishAsync_WhenCalled_CallsHandlersOfThisEvent(EventPublisher publisher, + IEvent @event, + IEnumerable handlersOfThisEvent) { // Arrange - subscribersToOtherEvents.ForEach(s => s.ClearReceivedCalls()); + handlersOfThisEvent.ForEach(s => s.ClearReceivedCalls()); // Act - publisher.Publish(@event); + await publisher.PublishAsync(@event); // Assert - Assert.All(subscribersToOtherEvents, s => s.DidNotReceive().Handle(Arg.Any())); + Assert.All(handlersOfThisEvent, s => s.Received(1).HandleAsync(@event)); } - private static IEventHandler FakeSubscriber() where TEvent : IEvent + + [Theory] + [MemberData(nameof(AsyncHandlersOfOtherEventsThanThisEvent))] + public async Task PublishAsync_WhenCalled_DoesNotCallHandlersOfOtherEvents(EventPublisher publisher, + IEvent @event, + IEnumerable handlersOfOtherEvents) { - var fakeSubscriber = Substitute.For>(); - fakeSubscriber.EventType.Returns(typeof(TEvent)); - return fakeSubscriber; + // Arrange + handlersOfOtherEvents.ForEach(s => s.ClearReceivedCalls()); + // Act + await publisher.PublishAsync(@event); + // Assert + Assert.All(handlersOfOtherEvents, s => s.DidNotReceive().HandleAsync(Arg.Any())); + } + + private static IAsyncEventHandler FakeAsyncHandler() where TEvent : class, IEvent + { + var fakeHandler = Substitute.For>(); + fakeHandler.EventType.Returns(typeof(TEvent)); + return fakeHandler; + } + + private static IEventHandler FakeHandler() where TEvent : class, IEvent + { + var fakeHandler = Substitute.For>(); + fakeHandler.EventType.Returns(typeof(TEvent)); + return fakeHandler; } #endregion Methods + } + } \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs index c01f56c..9420f1b 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs @@ -37,7 +37,6 @@ protected PharmaceuticalPrescriptionCreatorTests(TFixture fixture) this.Handler = new PharmaceuticalPrescriptionCreator ( Repository, - new EventPublisher(), new BelgianPharmaceuticalPrescriptionTranslator() ); } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs index fa37002..5f6c0ba 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs @@ -34,8 +34,7 @@ protected PharmaceuticalPrescriptionRevokerTests(TFixture fixture) this.Repository = this.CreateRepository(); this.Handler = new PharmaceuticalPrescriptionRevoker ( - Repository, - new EventPublisher() + Repository ); } From 212296c7ca6ec4babb608c6e1a5441d34b0c810c Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 8 Nov 2021 17:06:04 +0100 Subject: [PATCH 051/111] Refactor event publishing --- Doc/CommandComponents.png | Bin 20300 -> 19372 bytes Doc/QueryComponents.png | Bin 15546 -> 15734 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/Doc/CommandComponents.png b/Doc/CommandComponents.png index b41532ccd44b5d7731eb8eac657ddc7bc72b5769..f093eb3e6cd544261178e7a6b80e1302e46ed331 100644 GIT binary patch literal 19372 zcmdVC2UJu`w=IlfM&zIZk`+XhEID(OoJDeM6p#!On~b7J&XSXoW0Qk~ZWSa-&Y@|^ zxyiAidDZASp6`D5-Fx2{@BhbmJrt#Ps$IL*thwfzwF90gNfTb7xPpg=M<^>J@e~ga zKMoJ?OzEYw;G1h_kD~DKZsW;HJW_K>T0y#dP!1&TZJ=(;=u8dU-DjA*o-N54KpcD4 z_uG}r5AI4je7z!^Jr&o}$Z`9~Ja^7rHZ zNHnM>eU`&B^8Bn~pEZYbk|^cp?_15m#v?V2b}a2&TdoL$x+gC>1stU2wGQUYgwxFu z?tvz~TKvQK3?81Ll=SimedqJDHzl9Vfp2}D+?>sMya%@6#a;4Y0sklj+$I43sM57O z_o=C^4HMZNe!K^6YItb!!blE8<)~+dcHo1witr;ICuke3iP|IOCyK5S6&)%AXB8@Yu>J@o;OQAyO{q1Zq{RKE^4W{ju@xTXE`aJk)F@yQVwH$6?aGP-VP*r-@1pvbTFH`;V(x z+q}&%dQa0*m*v7L4Va$mRtF!(1HLf;n$<#dEc!tw&qY2{ZP2N4I+rp7MxWxrljkCx z?BDOSFrYJKj$zUb4oo+gvEb5X-gurNQvrcs?l*J%&cv9nc7ma%L^>7n!&^9R`}o}@oQpr%#wbR2vaN~~WL zlB_L~nioF3`AvO*>H4ywe&2Vkc74&Tsu2wP6l{ZHz6W_j`8_GIin~!jV3R=w-@PVn zH}M40%@<3$hm>&}tkE-fqQgcUxIP2jsi0$g%6IGWL#+CR{SUzEhj`^=aQA=~_;^(|V8f zT+VD>HyL*bUW!}DYGWu*f>&sd%<}Nv1r{hH6NP*7EP5vT=Aq1z-3 zKyS?n7OmWyPA0co|M4XuA%UxtfBK@>{+Gr>d^|jkPn;J<8Y`kLQ|d5DN_lm(>-IezLF&xf4@hNje#+q2aZ4n(_++t^Q9ChfUSwlw*PN$E z&K8+GTq2eK-d`H@CA02m(-B1iU;jr}ZHr`FKqKoBKZfXuAdr4mk;xBqq zVlKQG<+S&INce((;jKcGkde#D@g8FT8+%Ml%)7^TBvtMW(t=2A%`{8Mx*zD3{F++w zST5Sp?9ST9pwahP=0gVVt7v3xl?_tRVeb25=tVrdKP4no-lr2FFDh6jWbtrw*KM{j zhlyeNACQ9hWv+xoNGerLxGf*=p-%c$l{=D!s=7VFc)9f@kj4J8?_(&^X{a+$P9WF{uQ3st+=apf*sQ^aO8g?+a$zPhE;T*2C*0r`?-CZ5)65jg$cn>_&+ixP| z397TNKlR*ip!GW1Xg)oUAeRpdb168CBlf%UE)E2<1pWh_fGkoh;1y($xZ$~M{H!ZaAJVP__wWg+27ho>d=b97ww7A!Txf-2 zbC_3pUUor|Qkr}kA!}xlRA`=#g)U_6uEjcZc?w;Bz{*OkNt>nN?R_$!S%5rP*%kN} z#pM%^iruI`o-T-V>YAi6IqMfU{JK<)yv+Ipg6Xnj^WI*>tZ=Av*3ll&z#A;YUSdAR+5P zU|I;**sRTRAhYp=@Tci(@eikqnU|@%%iojHrG>cJwVc(4%&>egmx`YWdg#R}bloqE`|8ooYGifJ z#`Euwhz3H&6$y_K<{~~>SqsF@PkmOpySrBalJKG4PfZ2%SILGCa0|FR(a;+`8Po-X zIeu6-nNeI^d`p?X1oH)1W~yj*j_Xd#9Dl7i%?5Jsn#ReNH0rR=rd^q&@8U=!(cw8a zPOL+DgU9Wb7tB@zJ-8E=FADj*$iCl}aa=kC_rzqBJNi5r)Ohhdd}<0=3q|Zrxcj2p z?qhaWnd9{Z10Ik{n>F=tnbHcy=ANbA`qFvJvn;Wa-t5eH#RrZH7cT6sj5NG$y7N#y zJn+EKG#Ebe(C6UKaMm~?(S5@c?6EKUyuzzoAGPlng^ad4LWz?c2d|WRSF6g|QXNct z+rFO`qid#Rrxh^}JyglHAt8=nH1-mIeaJU>k=EnM0-Hk2LRC!Raz6x4ax`9=E?T*g z6UHjEly?zIQmm|Ot5<3s){;V)FSf=%y%oykary`A+CX`JrB5Q>BhNU6CY5<*`x5I& zx#>`rtt@yE!dX!gv?rNc|`F5%I!AKJW9o<;U z(Le^bzxfxrN6xi%_W9!m45-m33n{~zs5gp;lTl^3eIzzp6|tERqR5eye z>G=J0|H<$@y3N50sMTc(b|-#Keh;Emx4dtYI*Tun)gP55CPvqNf7E#I3bxgUB0^S1 z?fj<8nVQW{aN8uDkGf0DqK!6~nx|;3ZxD}^P0I7J9y|?6Pis=yAe@E_E899Id1X#- zcNMzRxr?Fv*$!&Gwe9BymYoAv>YD8a$*4^|TkUp5q&|!sL*zy6LZ)wc&+j)v`Bq3r zTvMl$beaVq&K?IP`gC;cMMBV!>2aRWwR)D#&6+KkQxddr8p9ss?`Ui9QQEm7iu zwqL?tRxgDtP7=T{^X*i#u>$ZvMk{6E}~hlKbH2Miv94=BtXzbWZhy_U(O% z21QTB#FM*pVB~clHmyQLbtR1Fk)dTU=cFCUX6?OTZ9)p|2t|cpCuN3RrwV~i{bhdJ z!s)tHs~Zt8#Q~z@y^vI7xuQf3pHSzXP$y)b|0KIzrQcW z##?|fEt)RRYiyRRFzOi!IauEgqY7e&vcShn_Qh_{lA*LVGrI(p5WRMXd^fi4%)Erm zzV!A&MhVo`Y|%82K(Z1B<1^|CS3&}vjw#iF#a)u#N|;dLZ|ZJjQPB$}jWa>C5r zHRospL5}roUe^cT?`cPfP@>=O4pm-5)Ku&xdMb*Iq*tM9c{s;!EFNdQO&z7*JgRZ( z#kPqz6dlz`RWb^8yBj%8_4+KCdMK)A2et$ePV|~65tG;Q3klzWW zb<#bvMgWg9Yd;F&GetLn{(TlW)UeU20B||+8Ba-GXItd!opr`FMb0%5`|H= zf_U3QqxwK@59{z2C3!qJ-?Yix>h?o0&s||$@ zXvvC11&@}}>rpF~qwkZ5h=>rIPuBRh57xidEN6=B{&;C6-`4zXdcYK0IQ<>rl;m15 zy|#n#Ttjv+> zWJ4W=?9a)V=XxwxZQ4?XbmYd8mJgAX`&B-1ZX-vak~hB{>y8PcBH_rA{McKx=g8b) zTvybFSZ)y82{6NIx1-W^HwIZqi0zUEUkn{l+6ZNu_W*!apRIz&2pb=qC5})fiwW?r z+h`u1?-~ko8p+nU+m@lHv%U!pVGr#RZ3QD?B|zT^)>FkWuRm048hey0&6Xo7DIuYB zu-%!pC7Dt%sA*hOeGg<~5F@~7r2ha@hVoYfVzpV1F=`?pBqWJcjU`KCz`h86y z*=QsKo#}U|;&y%chw??@RS)??XN1R&WWZv{ObrTQt`HU^DWTWdSEOr)UC_u2UYsnb zwvjyC-VZiX30>UKc(+*cLod~_3UQ~_S$Ge-tq&0S=~ob+_?%})0zTQ1Iy8ivh|U~V-@?S-2BR(r zbXwi})h*lV)%RK7VueT{qOgNv;R8E6bIclcZT<8w%`I#Xm}u?45~7517gyK?It}zE zl9GrRV^hJmOFpV#HavOYJo_o&px9CEB5Is6Dm#CNWQ4h)E64lAcJr6&@(;(h%r@S_ zFXR2U>q(-_D&Tarr91HxRn7tadKwN9{{DK~d_&(*qdQHy4^iivWKhm=A>^Ajb`+&e6$JUWVBl0}7@? z`s9O#P1$P^45)BkBS~kKIZgs4N85uCuLAv|u<7U6^IJ$FQPXYf`7&s9mzesTdP~vR z&f1k(=rY^MBA2OGt|lbH6F?nCBZ-N&ezJM*nizC!b<+xizF||nhn_S=Kx>aG^_5PM2uymh|4-?R)JK$TQ>z-#njogdF*wi@+G(#Rr$A|SLLyiqroKF^EqD`Q-63Zd}|zc z3HTHdJ`@%t9qSpqS{9Ms~De~X_#6m}%HZP;O;NAH} zE!Y>+>msSox0}~W*9?#j<}VZd-zU#t-y*;b9(!We@imi}=t@~P%l-X@kMnEw7?W#$ z+=cEB1z2A7@{C>1P&L64CRSZVcr>r}B2%zrUwsbH7gKrKpd#L}mR?KUNp3v{C7qT2 zZcYu=MFG;!sPq@EDFo_o7KxOWm>7$z7aomC9jHvY8AB%4P$kUd1sF`tbq*R*DDC9j*C2P@XW(`|)+ zX__p^zr}PEs(+hva?#HExtrd-QfXr^osSN@Hb+kH7VUZt&_!Ni&Z^z}_e{SCh+cl5 zx^n}xFi&0rd)NN4j;g86_jI7$nM6cCPFdvn1(3>(>@IA> z`cgnrxPCHX18qtLajD)j<)J*mk~f>HH_fq+bJk(+Mzqgtv{O6LZ7%MnI7k>9EodGR zRksB04l~XyhJ-Fx1^IhmbsGro&ORNQpwvXN=-Q6#H<(hJE>Cad?vi!BN4iS|CY7eC zz z!^1;iG8QDcm*@4d8DhXQZG@%kHfw3!*LzZRyz;*{;vr%+R44Z)Mw?o3$#}{vX`RcxR{6=i_PHCB#%zBM$P$GZRI8nXY#2fcMZ z_6aO40|NuyQ#G~ih%gUhz2x}o$H&KTbqIEVKsn}nN)qw+Be;Sg%%tewbs-dX#+><$ z*a4Z4LiE)&X}I57@o3jmeoad0ac}odKlepa8q=16#N4@c=d_7k*Xs9x zw6XQ&BCW#n#2dRMVV)~Q-uvPo-6OuJNK^B8V~>nN1+u!STMczWewY#{aWNN8(fh93ry3_9mGUm{jTlyjWDYkF%q| z7J1A5Rcac875(aR`qFiou%{9dOv^e`hZj{$OfqFm^yTFx#^zmLi4%F z##&*xRMRzd3%B7p#Xqad@eXc=G1`nVN?_~bkt z)TTs`QyZ8x>Q+1R}g8k9< z^DP8CT4Q%Z>=gTguXp~LBi}c*;WExR>8zggx(W18HR6Z_2505gj?xUu=Q8Wpxe78ANCg!gOcYHZ~N0bzJu;U1 zASi-t77yQR)3Sm#_s|Ufh`nI%iHKC2HKK z^89mp46W;Hn=FqAT((c7B#sp$kEU|cm7dq<$#C~F9ir6pCS=~R(xDY9TUmba6Fcl|7&q?9xRqmPwwt5NO&krUjj>F`v?`A=+kdT=dZc0n%W$f_o z(9S-%=)ZB`>~b27A&R}-L8FvWgG|#Y=PM!-Dq*i2#!vf2f&5WlcLBEDU{JKyoV5!v z5s!6;MQzD<-S}EmO2eCVkvr>kNc{RL$FtZ%WvvKF8i9Pz>#bv~dy%mfv%{R~{f1@b zzE|wcww}BF8Fzs<2KBKEU+QJe(^J$gL#qZ9dOCyH-p#llxT9(8Dp5#i*TpmQdHWRy zVL|9d1<`U*3$C2tW>rFmt5(X>o&Ng1fY$ialcsmK9RIm1gx+$sOkC=!puHjPVj9i+ zqvwWB_b>P_suAE+xXV6VXnDPO<{5<+^|U4JAmSJ|Js0?m*@B7r^IwNAU(`-fH5LEo z1^jl3A}V%A?$BSL{u{lleJoGsbB+IEst04u?<7f(X93h%-xI1JrY`4^%SjzeeVGaB z_l#fmm*I}H4g;8GiW2x{pY#|eXKpJQNwIzZvNiz&D)ln8L36|92>jD=B-k`03T}ev zUn?b>6?mY5WT){%n7C6 z`WrFR*6Y5BWz9&v>NmH4cbwnOn-vKcN}cD5Qs1AxQ7nnMP~ZH;i&z$PWNP8=Qk^yRS-LNEdJ6Do*Vb&AJiJmsvb`uL7GRQzy-&=Ts=jc|y}UF`~y6<#*xmlsBv5^hR@c=UwM=~&nnbZV*W z4ED>S!T+07x&8n00x0O@zi)(V73J+Vjn66(N4SJVq(~;chViD}O(3yV)GyE^iD9tVpc3I!pRq|L;GwP?am-Dm(sUN`A9$!-+y43f1 z=@tZ2>=mw$@H&>r6TlBazjQDDBsyD*)iLLa;bJaRV}dfW!PMQ2{BmYlpqiC;*~kaV zVXykM(Y%pK`Hv$ZU!{*>hv*re+8&ww2w@^rqtfIb!8FqJ>9MwbU2MnDx$)V~tKXh} zb1yxLn*8G)|3dM_Q^0fzF7nh8Yf*};YSM?KRtsxD(hG+WWJRlX^2)+S$xr+imV+uL zsVZug^;1495oUVy!1mMWU{})La&^xUP-5~j#QUyyFsb)G#6p-Ujh<(RFYlhAXO%Mk zzG;0p;wru|Bl|#3RZ@7SLyhGC8>~Zf0hBT2d$<)GOwo%E+6x&sreg$q9 zVGW#uVqGaBnEJhm+V!slLoj{!775GJN6r1G_;d;xk4UpV-Vem4<>V}c2gE#*{=4TB zi|uhGM_MYXn-RVQWN_@^ax*nlPen!LqmXZajMVVM-W#lMLN|(M7-_DN^?e?_1xW<9 zi-pH~&o+1V_8|4nt2f4~y5fX1B|_u**0wX{QOj$PQjdc-dq`-HBw0PE?)swX?|td+ z4)=F-#(hXhNc7!W1Dk14qS?kUe~)eYtr#^n>RT%amyyj%#XZ23No&gDq+FaCz^r!q zkX}meMl!>O1#Bm3q2qVBUIbfFQzdQe6tSbKY{$C14XR8`im|f@6nnHhQ=-K(M7z^7A!92ph4Ysj4Au*L|Zw#jbYD=L^N9w zVMnfeXaR@0n`xGDA09s9dkn9P6D2wO*wlr>#D`~oFP(R4UM^N~9OG;Q*U zC0!Y-T8r!Lh4!fy_}% zFiyW95|BN&l;*{-)#sM(m2S8xxdgXT-j6op7^5NPS?Gd01kUpN>P7cX1m5LYAjjxU z1uHJBb-letgjbC3!xA3C)vYx|LEfs3qbWw{Y?!O~=zb~(Rmj`2RRFyrJQ|*#6DbM^ zF;fX_20iatv+hmM+P+~QPZB|&#ajuwO<+84Z5F^o{!pHf_nQty9UYyDcjatjgh`78 zx%XWYh(+N~W@J*wyL&5M$(pFvvaacwNG_&{Mu*Vt$jke=cYkWHT&Oyn!jL|IFbeLHsK@EmM_|?H$)tASD$UD z3rR{4^2++b6Zo$?H2WXs2XpfDGDYUb@b#-`j$9{lRlQ6p~;y{Zw1ZVAq{J(?ry4>eU-x%9!u`o#jyZ{Qm{0Dd)xwuY+s|M;EHY zSQNSf_afBaq|9x?E;9}Y2ONsj6B?1pXa5O<8f{E{+nVdpt#w|Fo3B5E$D%wbTKc(s z=4!dRL>96`wf^JT46O(=hvA`c);`4UsAY0;@({%P_%dTsF$mI*L_VUcR~5|n@bG-a z!_Q|inBF!Xz_if7>}KXae*F06B2g$;&t@|HFTet_h?aDy#25;N8p3_5yp9h+tb#os zLrHzh!EcYDXIvZiN1JME-vJ4fbr(&z~8qphaqxX8dv! znY~{PLjB?e47)*PopqmQW@e@@ng!3$=Td1gyqU)RyA@)^cjM_zhUwk3v^3xxC@L+r z??~VgLpq+6jBa#^Y{rr|f0rR0_BLD}3?Z}}(!+E&h^6Lrp2fRI{)qIIQ^uU;vFHV0 zXn@pX^Ytf@xeqiXUUV_R4=gD=^98WU&c+L0A`=pf(o>Hfm`?o`_{EAZk z+Va`is(LC#a~8 z8l6g+x1jYb)JnULBnS`RSisO{`|$S2D|o@cy;S-q9MB%8!ib2BTOs9xJQtEa<++IK zvzmAHxx+Sm_(srmZ_mom>y9hU3x9W`UHI`JikKh{# zrr5rFDDa}*3735i>raES5@4*S(3yIv^z+S^8koD|ut5KRP*$qvkGPkLINSPV@@2Ag zCZ0&^o5%}0J#fy58N8z&u)b*7XGu;?Y+fjrp|4qA`|t;qdWoXNu?k2aSwa3?enAs< zek?*1hp^GYrjaaJtZ=u=pIz};?O%y$Fx5!%rx=h__YLuV(^X~kgUHwMIF%TvOEl`k zXe-8(@0;rBa74Ztn9$-x)-2T;)#p`xA#(-}rT)sWW$uBAI6Epsw`t<6!KVGui;qa- zK<=L|G)`j$s${hr(pt?!4MHPZ?z(=T=vfv z0Q2?FWb@5f^59GUqh>Me#)xTZ2ZYgDoX{7hbW5O`=I?X zw2cBd4+CYMMB^~>$@)zKyy8mW;yYuwbax1n>b8^{#10>x-?APp&&|ll=sqqbHNAcF zkBBE^4@vRDP0wj-p?>`MG1rmUT7(z*E6%=To-c-T0dHP{C2XX|X&LO09wzG6!80`O zg;{N_uNt=xioCZ&^Y$i((`!IZuc2xGFy#IP7VGT>9QEQ@UghRWNZod?yl$n{;>wC% zrB!^_yl#aB33Dm$7k2kEuhbtLm6EM|sIGR|SsDlv-fW??t()9BsI(d>HT>e!&W`W^ z(*izz!N3_jLy+7K%gD-J4j_HCK2QeaJOBm0)R~N<@@$_nx|7dxIQ`XfeF8&Ew>15F zH>Zz6AeX;J`O`l5zqJOAP5wOs@eGB=P5S|l#@uc>TU$&21vy zU*zIr@h(yYp~7)M=LDG(q`OYIm1-R!{N+|-@569SNl8hQ z(+}NK4Gmr(8$aH=g!eMzQt82r^)1;zi{b}^y+25Z0}jhr{j&-jDNK$c-h1X&q+#<0 z(QdI~3Aq|+COiupF2M}LNz9q8fhjX|wx#4e3$oh|-`$pFX#01IYnzzqrV4hy(Vb?; z-seW@?EgOpJWwn-dv4@N?q3+;{|%@E^H%QT@D4814?Wa2M7%?ksJbAE?S8=dDs_Hy zzQjLS1Eh3_qFL%cHqpe2BU1tlPKg@p4t1Nv)n^)H+|qcnX)t3R|vSyB4bL?KvI7N8>sZ zF_5nr_56&`kCEt~g@N5?wYS~p;!O=4I(T%eZ1l@sC~_hu?Aux868dTJuPDr)B_Ly? z8W#)q{Qg?ycjh~cj;&8Q_{I*FBV@AbR)PeYO}Fy<{8ph(MPP-N>U3=VORay&_Xxd5 zK(*?>!MY_N0LHyr^|5+-3U)vZtXrWpj_SR!$`GsfbU_Jh{_)3(JEqX_9~k#a9Fz%P z)vy2kuyF82X2bDUrV2plUuuAc>>l%%er2&>+wRwYrU+nHut?*-GF2Co{Z4{U4=23$ zJHzoalI&f2(g_!j;lva?b3d`x)cLe>6|XssBUsG!U@Gn6B?Il3E`3 z%uMvpjv1dZGUwWu?{CBA050V`^YjUUAuX!3-j_IZL&MtaJ? zQFR=THl%NmeXLnRhP-xZv&_za>^4P5u5-)X|1x00jnxuqmCe>vOqj*~cQ?kKY9@p5 z*nIR`#%t{v8iF47W?nozEF3%{C5LZgF2UqI#){|&{bHd z%gV~S0c`tQzZv+c-vki~hWbZ~Zg4zPz_&E=uzsv^Q^UBUliV!dU!f|oQYx_l{;suc zu&91l$JBIw2c)Q9FEg@0;6GUmepEj%fa-@n1qTB7AI+h(BTt5mq2^|03qQWRxvK-z zUx{$v)(pl`KU%N-H$HZ5&JiJPyT6qG)r>#3fJeGsTwM1lic*P`xB^*egi`{oRb^?4YLyPM2 zZy3;{C+M4geTkn%kU?IComN}5|7*9wFu^~D0t=q|5bi-m6tW z{eESE;ocVA(_!~cZtgHY@TMrrLETUzQr89f+tS+_{)6CajE5oSCZ`3!vzz`Pn+54* z`xBfTcE}1=E?L!Oy1{44E7L)uJn#(4APPW?8*4b+kWC*30hXfe665SE;aU3BOhWY zmkY@<*YT^JAd#)J=&ZL)qrTUA_Rkc6mI?ua^l}yngDQ-4bht>3O1mF8mA9sJ(;*h< zW=XR)xaz8gX+epb<&soI_HI+;F*!|r0!Cf z^F+?8IrC5SC4b__`{557d-Gk{DB}(rC|zV$HnWOsDENY$swY<8vcbHT!dx0$bI!*;>{QQN(Mqnr3yidKGJbIKq#dR%rXy5GfU7|NKps{QP~&ECel3= zYqwTVTB_;1a-W$wamwIM5wPI^soL}K^$-`jeqqU1sFvI@@yYKXPpL0`|1F5D3vdiz zfymb=WP0%6fg==8ff$!woi`-M^{H_UvYZFUGQyv4EmaH|EU&E80oh%dfAZ~Yd#o*h z2t&gUxNL(2@PC#@@=J=fn3i`nS-P<-7Jo(ZXAQ7z#n)6*$KW3_lO_ZL^d>%TZncF@ zI`;jCRYb4|-$m+8iV>qA(f+Y9p-j)S9340RRe!%qNPV9vfrn$*d!uv~mn5*VvN~RT z7+-MHD*R2_eQB~9qCY3VLQ12f<@2&fgr*{aX~yFQz-ThzzNY@S;4Y>ig#9Fq<(!Tq zZ#|14x;jx)@@zH1lGbg1UUfhlFABFXv-o}gQ68$<5Zpxmkaq=hoemE zv;Wz$SM@L7$lf#na`vU7&ObF{QzX-|DnaG+?Fj&lF6TWsA549!(?nl&QXTX-_DWAK z(LE&`3nWdINgv(Z_VRRUm-U;L9}XlBz_l_y$i~n*;F3)|4&S8LY=tGwJc4d{U+yh- z51HvcSA3*i$Adnc%UN)(c=#h2=;3E|68jI+={9I`Uz!vomd@$Y7rI51=t=-rM2`6# zTb*|DE7?%?Uuro+y;0wt)~~#{XQA-WwxzoK&*g9??4Ss##yj0!o~_x?M17PjB@(KjQuYc^7BjAC6ZmpE;SBigRS%Dju=x~a5g@Yrp~ z18)0S>y-XML+=;oK*&9{3u?!6FNjq1zF=|HSW2uXWG(R*yN;8+_}h`utN{d;JcZ0Z z9Aj#C`r{0S)K%gyy$3~$Iab+4)@-uQ{`!Ieob4ig8m%_nJPx$C(J*lqr zLoOZ7W*`n$4l74#s70KG(pk_4xf>}pqB z43}D0zsMpC>gr?R)-2IKp_OcRVDoI<$c7fg4_L%G9K_X~dH&ib^A#yunZC!lwfUYU zb2%>c=1w6&dbLS_+tAH^ys8V5FBl7GC#me(gbc&{a44u)Kow&ZV*Vqj#FsliyIcSs zsi71OQttNE*FKF=%MWJ0(OWrV13mKP^5^yQ^n)ra-W0dT!?G+uj(v5NadMCtPd?FKqWXu4He#tJ!?1{a4WrmuBHsa{7`P zv{10VCBc=l>3~uUYG&W7rT$jA8R%u8~f8^^Y6ctLIf<4{~mTCG{Lp; zIFSD++;yg?CmC4I1;;zc&0V{C^$+91K%9*$tLV41{W$!*As^7H2sFv??rkpR7m?Nw z#KEh9+roWvBpT0<9Cy(a}{u-x27z|eFhvYFJCN`~LCymd$D~}W2TYz{WZ@FR@ z5}IOBEGxLpGYVMF6B65yTDSm{8;jLd{&Xx;`YYg!NN1Cg$^Rb!6b{MA=LLObV{jwF zMS}I!*^PI>@&x6l0<7I{|3j5QMiz|cI*>SlGm6>57FUdu2+*=^OS95Jx+quGj>nrZ zY>^V@7Q75FHWLt^+(t8GXaZ|kz>+E$7GQn}V|jJf|FBt)>IWMXctQ%x$>md`4`0}d z%d<>{HR39tV1>Jh{M1(+MJ_h!KZpVN!2;VPdO?wm%T`v-R-pc67TTl`%>oUZS#jxF z7*CWw9X-SmAY?%i3WTo*sANTrm-F4+^NrFZ|8KPW*Hhdlz%d+8LL2u8`hs@V|1%)@Ln0W76IU3 zaYWo}AvfD{)sw6DmjUI2yL5}X*YXda(IURms@2Z|OJ9T9tQI&hQ5)eU@`$wf&FkZ1 zz%1Y=Sd1vs@i7?N z!!H#=sYuam7j*4K)bUq!O=jr_Kxrjo*PWeiVr# z+$LLX4Ni*wQB13}^;YOB2l|lMM&k|lJ#;-_J+bT%QHME&<2?MhYq-eLl0NwXj~qEA zJ!`*7&mpb6oSaPg3u#=ORK}#3k+JHiEhs(HtCqqkGXOwF76P0>-sFqE)D#3@diQI6 z72an~+~kbMaVkz1hmMp5qR2fkKWfQs{t)8l<^cFRZr7~nSrpC)Zg}@==J{6@(cjth z?I!|vC~;IS%SH^S(_@cM905TbkNqr%n>2r=J+F=qzbHaay2iZo>XzO!zR~S{ z+nOZca0SVuzzH5YUhxO*X3`G9%7q0`6F8WOYHw@1f~1}~AB$sd|GpIeG_LWqE`UmA z{YzTypGf-87lHt-ij5U-<;84UwA>w!1#w!Y?Oa`2-0q;?KHJc^uRJXL;~I^y)`Du4 zD6Rqjc$vs=oys-)f=DY0e@mJGnfTU>BPYvN{SEd5lL~^duhZNYH#3BrS`>qXixP2x zvRaAr>X~x`S#1AAT)D=Bfa@g_5t1WUYnxPu0K+4;i>Z~~EBLD{2TYk< za`7Og1v5xj8ulUC#p?ECi*8kB+*+SZ_{doI^`L#w3gFHw8`mF+tS;8*3vMTtxVr`0 z80`2=z97faFZap)?!#vH8peZ=IOAW#7O#~2)~(?>*R9Rjb~1JY;Z{X=aXX_PRgfP3 zJFBzZ`kU3g?b#D-DYvY1Qgs5D_~ipi>ys1Tg(TqfVx|LjI3jA(aHb?Mg4ZJJ2jl4u$)(t;m)L#Oo=PQ=u z){fJFV2}6i@%@aG{~Q({dr=epl&FWw^@A00^dC>2d>G~3M+E!%vIqy1$Ud43R-+>z zNB-M<6&<~bLO|H#=@CqU_HU7Kzzs!Q`AYnhN5~%>7D5EP&~@`wSd*rQT#=M}w72S< zj2cFHhR_rB?#qLPKoYWrLZSAOyvYLWwiVQ9z)J86rawdcymr1uq3W;@Rh&yIMcrd- zh}Q~*jmt96*DUe^FYeeyBi-kdp=@A=1FF%X`b7?EaDVQT#1*e9c8JipHy{9;!6EE# zo56>Zl8lVE5|+-V&)?JdOmM`%AR>6x$lVAt%_0N+g;2vfz>$6x^@j-nhP*QQXQJoI z?XIyQ^v0IfgRg<``9(iaWWa<8p{sM(NH*W%qPig6*m*H>OQQhw2QW}@IAMSbY=ZPK zJ1CZQ9)3>aD^B&olRp4h=zTm`q-NkG*SgYsvWM_KUiT9#pvtV{THg&MVID)*7IoJ* z82{|e2l4uDLwocWv4Z_#)oWXjt)rDM7nK0mMMg&AOlUQhr(+w>@KISPYl4$iPmeUM zCs-I18y!tjBh*6hQ~JU!v>7G{%Q=tNeyYf$=?U;+o>kn|)zdpdkJ+A_IHPGZU*Enu z{gSkilXJf0>2+m8=DBmu2A`RS^9q~&Nog2i5!}SjSk7W!-@oKjSJu_Hs)kQ{74iH) zYMFIxTB?24($-z?P1E#7UvJx=KXeTOD&yC}D@aQ--#08e?yXpDADOt1o7GQ6}=vz`+&)-SABuF+ZcM$L3>dwL5eihQ3J%B-V>3yadwP$LcW zqm})*)7LP4Q6tD>58I=MsE)SJ%ar05>#m1zp`I=AULMnGu_@5V|Mq+XYwkY&$1TBi zt|E!C?@%+HYhaG27=9tN;{29w_yti|2Ip*|`)b)DlOrP_Nbw5Gg?2fR0e^b0OCE%Tfbjb4`Co^&KOCK$o_b#&#_s`t zDEhu=RQx72?ZPL(JJ%~Vt?JtBXe+~80^-8}NjL#nCf?*CdGe>PiCBEGRGU2r?d@TN zc&tXN@SjcUFHcbb7bp|q!Z$B0IF>N}$`)gi3xiS~jw{U|=MQ~zL{$fg+i1Bem^kpY zPAVvs*VSG3^X%8{l=Qrs!$&~Sk@Hpiv2|V@Mc_HD2wplG!;qqxiwpfuA_M)~KEfX%NpSZC^af3vj$Y!!oHnm`%+Ow#cDnua9fE1@56r(``}6 zv29gePa!g3T&qEcSB0$TMY**phR$#V%rE1X@}UJQ`||FvbJM=;*6y=B5qE=Om#f{% zSUNSk<30{|-jr`+K%WsH7hidrb%D?_<+m(@gd*_$E9oO##tK~$RfOs+i_Or&qZ~8Q z+}gYtADXks(ZbRS#WMO5hd(%!V~vH@&|7QB-(0|2C)#N!3qy7@=dnLFWRkr8NRcxa z$`MyHBqnG&TtLAdK3Q{^JM@&0-1}-~K|(^p3UYIn9(5nMx~v{AyhF{uysK^*;?Sp( zS6?a$nhZ*Q*)|ug9iD}^P?OLvFDtyGevfVA{8E!4v(B^#)lDbS)LgiRTR5Q`SJ*a+ zBj8hWS82R&c~aYbx5S29P`^ZF16i_GxQi(!CL2kh%IHMy;+GBgO{>u|kxID*d5o`% z&!bx@p=>}48~7x)W*U>(RN0{9BeKBD)zwv4xUI6`G(SBWm@@?6_KCKdJRWXV=^$1Y zo>H8zE-HTbY<-q2QPLvE^{k4d7Co4zqo7jk5u@4nCjfS9CfHyw+yQ}TjVHX_&Pu0AEaccjfi$@Ge8J5F7Vjrz()1I6Ci5AU+lS!kaIIlJHj%N70 zmciLj(9~F=xOS)~jb8;D&_cqGSzfFqqh+N6gI4!JemMq&!YvngQ%NF1psFd+0c0 zS;=>O&@#Y#+&h}1zye=H2&DGK1XB9?%Y5gvrNH^*Re^=YZcxm0mIuDs8rQI%1N!V` zc2dM<1Mp$gR$L^eBFF9V+}zyVnaWhpv#Iz!6ri22JymdQu3o&j%Jzn4|8>=o;ZoH# zy?Y1^0}dq^|5pwJRPA+bH&^FmoHjbWiG6rfDK2{2+S*y}y4-kV zxGjcLa;=<%t*v2?o1d?764F2Qm-^hePS00gqbM7r+_ez z83Sij1^=yUJipdnodTsakq9u(_N`uwzD4gw%2C;p6yO4Fq+n9+pcO*!0>iMS7dv z7~fRW`pV1|0#C0Y_nrCVW^T?gromng2FUc=!A40ZAzw~xG1CS4GVotYqb&>9yw2Wi z|0#WXy%0c*OOEdMh7P)4)r6kKb>Y_56n5Mxk<}oU6S2`N^z=-T97U*xn&k8+hSQ5M zbvqMerws}g0-qsoi?$bt&%4&$J;WjQw(diB`C!$L_4F6O(eA(a!4(@mn5TS00+vZM#U6G(H~F$N_UwWOG}e&{TwbMg^n zq9oH`HYm=tVZG4cgZ)_a zR$J#yB){$KqYl~fPHMd*)S=ek$IZ;Ivt&dN!K`SAWq##u!N@shwg$_V%*9-FAQYi|6%WNBo}8)B{MVjiLL_>I^Io|EWE+SziY1 z(mMv3k8I-KHKUukDCl(GEPDkwbL>;_V+G&kloILv zfI0{ayg!$qT6N{6ansXcwKe6}?RBYwIpuZ*$@g$FQ7TzCn1#*>)6NL5?4E+_T8$v1 zMqmqjx(Y6$il_NY?`9yUJ;k;RwI8ve%3Q_I{ZkRemv2u#cX+I0=nr3v@6)SY1}`Q! zPk2|)I*EL?>|eLA=QmC)9d$ZJA)obqntVR4qNRVeUN{N6q^+=TFyk@ah1UCK-qyfs z*6xuTY=;$u)rVQu@A^h3zg#W!Hd!GzlX`0krE?n4I+`3iI*0Up6C6IKs=fiQ4183X zd@d^`HNYg%wC_Q2Q0*rG>tP|gU5)Qt_gW`NKmR=RvLT&q@T$Ae?e-*Zo@yOw%P0Q2 ztb)B1Eo`c%!KP(xJz_X(egCpKI?otgdG%J4kwNJ7-{|1 z*?i~Of%C$5?}J@t)03n9TRXvc|IYD+8!ss$*80qRZ&ymE$7S6dS{T7ix^>scGwgIy zm3IB4GwSYO?~?OVDtGY0aP8T3xTW6qNC0FYaDVd1X1@pH9Cr9^o*o~mq1Le~)hgEe zDg`Z*e7nu%l_wr82;!}Hl73_Ax@CGo@&xpo!orcLc`iH~;UU*(Av)CdT=8#)ewCvNwp zAs5p*#om3e=>dM&kT3|eD-dv7e--)FzjFr0cx|xi%vj;+7QPz7l&Tbl z1$NGC^P$l~DqGi7^^SQPjI$S%J*x9B1ja>$g$&k{4+RYNr)fY(d(c9U(jD5=%4m;y zhp#1u{fW*T=oK`=G$$@bA3ZPCT2Ja}4$G+`F<^_ZV~Pe>KB}ftu7J*4x&OEV581 zxh^vEng8w|h9z#eFgs&M=A2*0h4(pio7{C`uA)>=Zn6mI*39UB#_2UTJ+Q=veR1u6{)MAK-|$sMAkaIO z>)Pf#=ipQ4&ia||f-BvrOR&aei_r`Q7!x!6e1dU-Q#rDUaOG_7g)D!q=x`mmClxGO z_)69ywPEsXvv7M^Q-GYIcxvCD^wWjr4lu9GyBi!_A?M!}YrT}uf}Dk4IQjhWX2JWK zX94uX)mEh2lUUD6iz$3ddx%M*MkP!-TI3D6&MAog;HR?o#aRJ z?nis>pgfARHz!{@=PM2N8CPPEN>-#@$!YAZqR)DFlgpEBr*}RC!HSN|(|{%RUUB1? zPqusMY+m5D;50ZDci-l8&o%eFLCt*6RYpiieRbHfKHM_j*n)aqdzyHD+TD66;R+_Y zGIuHvJAP}q>v_|4;5`3*V!-2Y7A9mpfxT!g`7_>GSX;ANULEZDgX6@+a3Pvzkq!5d z_A&qYkL-Cq4Ivn3>E5azdsKbh(e^O3L%N>Pa;ILBX6yFF5e&M|x9+;sG>=cVVOh;| za6=ZIA2YtbzwHT|pD&kpoS_}Ql;2p`c270msXtrc8)zVbo|o5qSJgUCiMy|iMVn`w zo;#nyUmXEVnFg0&B09pVz5(#IK|N zBSu&ZtYvv^fv`JyQdf)H?&Gd7-t$L|hTgeY&9$cWMUA~_# z?-c0Y37%M>%^zSpuXcmNp@VKYem!4KgOz(bo)m-bb0-a^btT&xke^lhJ%K%|tI^=t@mTh= zhLfQA+KeSXZ!FBqksL=x-lE0YJ)Tkg%D~Qf6~r@phJj9I0uABpK3wsu(6W{Iu-hZ4 zZ9-@6ih=zbd-wgD>_SuMM<4`vQ|7y;Y$>Hao*?Y?cZZpqTf3NF)MA-$vDryO%#g?+vJKBg*!-#dx7*zz@VewT@7p)~_QY<5 ze6C^somOfW$MXW$ymcaoq_^1%;wOAL3GbbYioFqcIjf{#Fy|ZP9~`S)1K=2?Sn&xr z%y8@2POtDOmF`OgVWF*5d~jXFGzGEubK>P&%`1E^t(Cl3AP{WY!Bh;x4NkA*D`>eb z_ip#*$no1`Gb%ssF7bQr(@yj*JkhW@^w>{!Kkv*I;#YAk_V;h`8b>Q-*di+r?EVY_ zg-%g$B*Ee{R#}v9U5us(adAPQSNl9r${^!FlUMG_;dRfC)@HsjY7@+iuilna!9rBy zmw42-RR*ML>~$4ak`zclCFgU4`27=CYuvZKSwJD5G89;tYd5AiLyUcZ=(BPJsrdBw}# zzGgvRs7aj`(2`)lsy!h2}fgZw+M2j*?g zXWZvU-=+keT=YXWe9^4;t1lnRE=yOC1tr;QUEuG1AZB*)!{NG8^z-lR!VOwbn|OZI z%b1L^-}PB`eq&yxpVQLGp~yKy&u?UApLBC{OyDI=q9>5IZ9gpI%&QU;GC125KFp(B zpDo^n0OQ)Qz2zF6qM`0IN6oY6s67m_wtCc6;od);#bMJwan{Ki9uK`z9g-sC%+~Pe zh|y36z7n5@*`Z>lKk=rzzjkrn`Mh)X1Cm6fz(haR%e*8i&@1<~oFk)dan|!(TW@5R z8_gYS2*a(N3WOq5HmEm-795>GwOTO-Oqs$64E`_c-qfh6@o!o+Xx{w(;@V z+{>|Cd#?I_K%_Y>eUB#?TOl5OHHDF+ zuzDQp+*^;yY#FEidTx?Lk*$++c6!#1CNh@T+n;$_vFsMO;b{uV>(J)n>d9Nn?VHtg z;~;+v%YZ)VedrBesDkkT?-=2Q;PtSN!0a-oVMVWMy{Y{^?e_kc{_)Xgs2+YvDIONpmsUni?L6xY)ndMmkA=wSuY6gz?Td{=ENWv5tN6>7*XhHz&DVo^ zdBQB@45YuY0_%eD zJisFMHYd_fTHPCDN!4=#sDJaAN2>O=w;PKGV_>c|WlT)2{jAP(a2$Qf;s>pcq^GsB zo~GMi1}wrM+!JAZR|DZgLTLc&Bx8f>$X}6X4!RPvUpG08D*^8%v8+tB;&v7ie76^BFnsrAb*cTo_ZFF&9tu0gv`p$m_&}*Q=i>gbAUd`Z3h|m z%sXENCRJ3)D33D2blGopi!zr^=%GF4uiSVj}3zNSs^*t4%?w}y>o^GCY zG=X~1YE?l2h$2j3rET|c8E|HLP4>@9xwYXNEXJ^dip^MPJx{_fZtP%}kyNt%4l1t-i;9YNeA>(>RLFP@ zcIB9*$ZnPo1}Z#`d9Of0!Jzi8cT$tzpOJLEa(RX2ANL6To}cG6ASmNXkKhOov09#3 z?k+&Qw;U(B_Pwa|FOWy%c$`iUq2B2+0%cy^bP9 zr`%*_Al?w;O-G3LrZW;%u#F-r@17t@u4y14-BXE=g4UM)XjwRtu!$y!_dz^M%!T)% z=XP+~P;ewO$#Zg3GwNN``tm5f0B`A)?*z7-zMcqSZYLr*K0~Q%gj_p0B6$8oqPja8 z1YSIZs7)IJ6?!6^=wZq(O9`Yw?H#A-(gCZ5`q6FeCnqO6Q>E)an?CA~{j{0$zP;&* zWgHkCKfZfm-zL2RG}f02uf_jbvW_ zP_&k9){b#CXn4&^n8u8;T4hiSvRmm4cV6&3Uk(#^+QRO)cQPV-+XIG0giB-C^fGt! z#xXBX>WB3?h)O*#O$U)42gf-4U=NgHlab=#rL<5q;3dg={N|yo2)~j(CFcV|vRgre z6a=)nxKHwc^AW7%7eVU2Q_N!2%jvK&C;&SQJ2bidespy7s$|ZfgT%h~5mk~-^2+k} zSNt@?%q=G39X{!u7R#-lTJ%App9!0FBc2$X8T7h5>wWS$0OJk3@$t`J3v&2=BZ;k^ z?Cma|YpT*K(|Lj#7EJ!Y=rxX1f5|<0oX=NnfWh+Y>PuVm@Q=%LT3`RgaNAFfsQx}( zYFw}XT!mb5ZSx+-eerv6AGlow-Ke;7)=o03_C4k!sK*=NhgKk9oo<il$X!%Oqs3qbau}(O`rSzsXe`qA$9Z z+SVF7w^;7XG~vGTh0ay98j$?JYUgvOvFgyJOp7PiK|exjBgHf z&B;!ZYA$!D;VV&%`wUYXSqr(bU9}8ijf!z2uFG5k{^ueoER-=i^CTd-IWsd8lsWI~ zE06h6fzv65Ds|gZdq^^1p`dx}3eRwT2dS===%O zvqYWn&%4li|L}#{p+0k&$V1<+jH+CAcMU*Z3`C3*ETUM+DGI70)~7N_=NTS*DX%&6 zO{sztk)kgl%QT!jM-4wpOeNbTfoHld`FYNUKZNSU+)}G?MANbopNHpuey+bi5xTe- z4L2+O@EYn56e9X?;Pa)E^XG3*RmLQ>ghp8FKh@V13yy%o#ahWfC@bxZIRf3}KfWO| ziUxgnqKxL3F`_o{~YIj9m((Cs;3!1dd^W%Tjn%7;U~w_oHVSiXm1|A~ zNUm-RWd~Il0~j@?>#!c@ftq3{13Xo8i$zdlPU=;JYOrQvR3F9m6iNUC7GR<-Dr%ps zuw*R=ZNJJz-N^TEUHC9;jqp+b&xWABJ3?gJ$ir|on-jEr!S+DB;}w4Bu|MWlTR}V& z1+m`F*8cK_<58`dXQx1Kkeq6zh^6!JqmAr)Kgt#Gk_htmuN|J5!VS1lQXgKD$H~m? z!8UaurrVq`D55x{%B`>IC$wpmUDbwGjtC;3i|~Q?WnR4pCUquU=i3D&Go+FP z>)^Lpuon&%UPgSNwcD}mO(xz~p-#WL7( z%{`N(4BP@!^@R{lIh^VJ>V-qJLC((MyVK87CuF@0Oj{ikbg{R9TenQ_)2O*P@q4gM zm>p(g(LULzYZE{aXlUbeVkGoaR^6W9C@f3os)}8x`_3!TMj$Dpc?2@Dx%yuxl>N@vL z*H>A_IG6Cc{;E*_2REDJ{@=a8&}eLnFIogYZJ(vm_{On=3zJBI5vxK5o zh{cFySf^d6ZTXu1Cbg?j_#_PgzIsCX;dcpjRzI`I(K59QIT{-J5=XnA<)O~Jq^HMi zdF5HFW8)wpDO_hMPHHuRlKnI=qaOR?Lu4FDetz*jpnO6yGASvD6#49N*aLMXOgWAF zO8^}CZxzO>O5mFQF-AN<{?K#PEndPDi#d1I%HlYNU5IE-{ByZc&JmI@NK1ane@sG3 zLxlGOO)RZkiFEhr{!(Iw{0M7)pW3oX07RYRf@WFkfVCs0^^DUIKiGkLyR0cYRpGo} zd+ytqBNr4b|3&`C{)PV}E$_D&=F||tHCGFua}rxJ{V`2sv!#MPnU^TJg)1zFBPyM3 zOV37fq}(pg<7{Xl!|VzC225#+1_m7EF`G^`3w+D~hs~T)|N5HrDe^<3Hmyyx&q|m9 zjCN5`l*;Xy8itdEo{?vI#tB+X@e!YLZSk4p!7 zlbW8zVw-GLlqoLD<7aJ9YlGt!_L2UE%1Ys!;Z)SFU$6E}u|aqNt2bkCQZ(7DU;3}5 z^4GjKDmI@eAPqB`i|$WIcqf?8A8!h*G3lM{gItDe2VvB0)(UY8qhcB+vHX4_zn;Px z;>ZTD3J_351GJm9=XM@#YG$snxrbD&119?P}vO*)j z4&WA$a!PaG3-w~{2MQ`SN3si5i}f~#Gm(&x1Wj4}L{QqF{ymQ^-bYQQtWUz$NB4kb zWG<{idP4HV^~?3S>k>N#Z%I=id!aB@xjRnE%c(KtaMX0fxno1jUUB>3?4v z7Vqt7?#dWG+(#BN82_PWU3b_XK$Q{~8ymYRqx%-EQx98yL`?+67sN47TuStu3Er_W zXzxM|im%T1Ax3S1*_GC*td6<TXZwsAWg8P1{G8sfYZb63+lc2UwUyOX zEw`>EQl1LgmvJ;8K0d#rBT8qoN$;cT7$W)eGIsAvycjC7v)5Y{ zWG*-ID_;nB|Ey{83%gmNx~*~Ehf~!dHYwJYZ9V-i^PRUoDJdxmMoGh%lR#y_hxFA0 zQ9s7-yr%ma!FWJX%4>$+w!N9k{mJI$FnI-4Os>5uafNIrXXi*@@tYkbsFuUM{_EyZ z9A%-AWkI@%-N$8snlewV+OMBYAsX3gI}{!8U2YG9h2MH7Pqy6 zBsc=s!t@AU2AH>GHA)0<27yT|u}X`Jb zzEB3sU@oLg#o3{C`1D>|L}*xjGq%}t0IbcQ1tGP6s&6JQ{v>(Ftr$SL5bbHbV}DPs zJ%l&lYV9of1fgKFYT}u?Otf$jXL;RxwOo%k;bO5-oO<*T70Jfg=Hb^* zZdbo_=l;hawTtkEC~4f6xiLd@ceEF;PooZ8`o76pkm)O^?a45vwHe)Hp0oIcHJbL& z@mo=@uOI--{$d%DvdO|bW2Ajg12|~+B&?}_1d4ftr)RJhzhao9Y3bicEW_s++3^V< zR;@_Z-kNWlwiqp>9{e+S5aa)k39sL(t+kF?2Avpt;n@bJ=z|IwnI=fI^khikYBfH==dw@K)#j|AQz3kRvo;8X3*P1I7hKaq-kt!sNuFl9tS7 zAi=n)MdW0!o|4WN^<*~mSg<1pr6;7|!Uq(J1y2d4um>liB@?sS5|z3$EjJ+K#5xAG zOp|?xP<4y&d)SpZ?(0^;FPtRgwj@CzR_S^H%Olo$1PEW2fnr*C>Q6tyNBGTz)ot7d z$uCW=r93hyPEP;nUi2T<2g;X;P{; zskE9rHVF=hs80VDO&IbPi0$wHh2LM&sC(NG)%vvOG>7#PuG-qGiCMlh2R@iU;w|6A zf=++9=1$8wcMvZOW2OopWz6 z4tNN52mmdlC3=wz6vlb85`~DQkByBDnUrN`Q;j5-GtdgCkMRCVYBljuP`I`ni#yhD zycca;-d;Um>FVjJJ&9=Jak5C2XJRWZx@v?4;WL=cU+4h!j1R9KB8X*CcT>HZLv9qR zu$*aM;H{RB!)aOl75D&Z_Wug|5gLa8*ept0Sz&XUfMYqa%G@O8WMWctVCSRAW9tUf z8>DC|&x=Jrg)FHqMZbFp_iX^iivl!`XRm4*k6zv3e9jqi3%imPxH%~??9tM;N@1Yyb#z}9~~=*TO3m1ViYIj!A$IL1>Nt`h4eAm1?zI3oY^ zTfoO*xubt}I9x@DEcr*1{+BTQCz3P0{B6+wJVyxA-?#PWIpX}boL@HR4EubQwY^mO z81W`mo9Phbnrur72ow6|sAg!>IWGhYVzXKT_<oR6G!QC)qlawFyG|1|3>W3gG_^%Pr_OtGy%o za{lfQ7Tbxl%z#`7|LD7IJmA8^(w`~&Y_|+O=*71q%#rT0W_0l2cjY?6v3tfirS^=ALLM*upxclqx zRXl7}(8b3b12bBR#y>`bcAvhA9)6*x*fTe|MsMl=LbmyAY5#)es= z1CtL?%*Jd?9+bbB9Cj!x8=h00SUo_}$CcDqCz;Fkt;8C5fFR<_AD(2 zVC1X0NK60)jp|JU0m6lcNO)H*8YOUkDGgY^JS(Z zxGI{=_G`U;iSWAo>=ZYek>qYP_*mdUXqp`F$y&B6sK!P9Jw3hxrPKG{6H*vyr@F4i z2QfBGE+1OVRuzB!`fTK;oBnL8-3-%o=b4`{k#Ym(-|P4TKQcPWWSJ>;7cBz9S_dxD z>(tT#{;O&8!Z`q70AlWSoVcI~FhS>8TT#7T`pVg~3^5pa9HN?*4#3!v(Vb8bo=F~T zGpKgTc{hwyoOZ>6yMc@ zWDhZ7b$y*tqhbf3{Vu>B1iq&OYki5v{YkEExGEOYWjVEpYTP<$YL`O^Yi|I>@{Y5i z;^5$X=|@D+F)}D_)R7;Vl4PiUF_H{ule6gv1PQ9#wH8w)QqF)6;m?E*CxjNi(ZhcM zl{GD?PhfP?%u0162`MnPe$ zcR0*2$uf6)g-lJ1_K9>oQ^p6;-5R*XAUa!4#;l6tt|P;M zDbnR<(C~;oHKX;B-y9j8c3sA06Ee#j55j8!0Q$bt($QV_Masej?wLwiA)~&8(++|9 zckkXY0gASiGa3RDz&{aAB7xMY%H`|#@6@Hq_Yg8#9PjqW{AQ*ATzkOrpIjSY*Z;+1 z0WAN$#GRn@=-lCVuFa#X2$4KQio|>18Q4=u6om>s@d?21+790K7aJtg(0^w>C__PL zSIas`AGkl)RGzSFH#HUDAr+xX^oiRcP|Gn<1~z1NN!%q zGy?!DI&+=!`oJ)~`O+ry%-Is0@clQUtY|V!lC9l75mClatvskQKdQETfD~fZrlEYp z5G$GLLFy37DmN4zH!sZ!B*TYZ=y9gI+aK>pw6M?)Zpu3pQ$S-PFgGa1?$TZif1O7I zGBbb8i|d3bcXK-I6qPghwNR5$;iWW4qMddy|4h&=Sz1l$7g4ADNFAAiz&GB zjyaBUZs+da(6CEJGg3Fjyb}bJ+x&6nNkvOA8C+)(+hMu8l09%Gnw0k3{CnA6Y#-`t zsx!1@K|CRFKFh&g`f^XA5ET*wK<>998qgA5sAc&iA!9aJZZP&i-r_q@x#Oq<0K-56 zVM865rdW<{$}wgmwtx<7X}EY6l&t#y0oWpti{kMN{l-v<`~W^KKeifG;IEt9&K@8g zs*Ln7XH0EbtU;!{Fh-uiQuMYdwzgQhTg!_rW{`5B@->8x-Kp)(LWM?a;;`5cjL?YI zlozs(?oYM-bBYL$unNnuO7}R(ick}Uq?GG4eKQ%g*q&@6j|BdX+X`As`Eg-w2}dwH%hy4gl2jCUMyVp!GCtJtsS*+acR77(Nnhg%UPejSA)wH~_s_iGU+4HWl+R zw3lT-)8nx1X_)bHdk7$x#P6wumZ)$j2Z`w;bHS$%aD-04oFY{T_YPa=o4m9;gv%H~_gvRaM9%hICp*1}pQI<=em zIO4yR4chA(S^)uXAg`C@J#0<1ZrD6@VYi;=1;Pz&L6siBy65JkdVp>+$ebO}!+!ku zv6wONo88CHC6k{<9nK;o8M+*@0gzxq#j6=n&ixH=5nA-ys#OsP9{&$%0Lc6IoBx^j z13UNriA)P4@xQ0j2mtW>-dq2ZW&V?M{`uR#NCWLZJn%*Sky8F+PaqP4zA`MO{~uDu zzvvXd+Kc`6v^MGm1B5$e<|xGYxCQH^`r2#>z9CEBm(D3Iid~3Rs}=h@QQ;36c)qVZ z(~*4`Za3kExb1Vd<*<{&pqLQA9srKyfW-MLBfs=E3gtMZ>@4NujmU&UZz`2DB;nEV z_oc&pCMZx!MM%&3aoYs4r+7X_Np}%xY)>XZ%whS)m097zCv`FXdkBq_&x4Ywli5OD zpzD9QB;@Ua;K_BFi1VNdgJrSlH2ceQBw;|fZp0Rsnyt|cK|6_etTkq$CL_4W5$26lbI7->F86#k$bw8Q!Jf;aPTk_X{>rKPx%s zQ~dJ4_j^|k573!DLKY}GIyMe?|o@`&t<6DEcd$9s^AE0(T z?8q3MfBg5XVj0lhu?QSc0O)&Co0Dx+YZjZZ5U}-5dcBckQhP2u?ncUuzDD$X8X(s3ZS8$ueCJ`BkvCGssiUpjfbtJ%E8#tWRwp^*cBbYIL4aU=`bb&$ zWh}dN-nN8h*w+i6Lljw3w*Hzev0-z-?WzHR9@3-Ew!$^IOyl>u{t!+dD#fGY#0eoPNY+$5+3HUg78b^y&FlSzTI4bc9bFGj zw*ZJq;`f9G;?l-2XsN$@_eK2~QAkWQ!{livvG3SAn8n;VI}@(oO#zAMnx6eHyt3dh zMk_#ZF{64`>~(WGF*7rxlCREYIb9|kga`c0sjYRt<_JnM{HL5&t-1oZRuQ-Hv9Zp8 z!ws-E3M1qAptd4|Q+Iu>A{(2N(I=p$7>^e&rVred)~H#w9;fKtrpm2baTGo4c*KxtUKz~57q6xkMIS>&YJ;G zIWo=_NV#2WG^~KDTZrNX%`yX|k&wmcOP$^i)qhBXt;rQS&u2|h5Z=!=CRYH%@g@3i ziSl1oxsJu|bUCmWX$t@D>d{-;s??}vYpL;o*fUa)CG%q9Z@!Z4s${u8&DoB(vd zm%66iP>6uBDZWYVDSVn=v2}hqhUsX#vq>6yPK?0B4BV~(=@@uSNzHP=fdgs}$0Fa< zvik@k0HQWlp+Ir8ES!{1Yc&~EEW|w!S>W`kp++0nhs;SkmsS~7*RLbUpUT+4zaeR6 z_(h3)r|6elNUHEpNgy~U>&ON1JFxrKAMQA@OOF61_LJN&@9jO)PvWLy_lItjSej6gm7_r)(NsalRIZdc{mKyKPQd6%1-ab`?} zIs{}l7g%S&ttFAxe^^VDH3>#z8{DV}aRdPMZjb%t7nQ4-X|@A!Gi=NZq^Uo10pJ9? zaHraJ&5Mo-{X3x0#woB8(bH-bZHIH9PV_^J#`k1IB$eOYaU(d&1aP?_{@9m?wG#0hd(o^Tfq=7FDpB+*y1N{d@mU80uumxgGaPF4L@d- z0)|eQNcxb8qEOn939#upiwC7Km_K|I9f7jMpew?LGig)1QAo^y>NTCXoc zaTeU6X_MiVdGMFAhW(oxqhkOEE9}Mc3GE&Se83>sD?>s6Yz#iqFO7enU!&H^GOHHB z;3?JbRXcFWED!E-_v4c&3Gzd@jpm55z6$hJeB@VIb3>Bi1ss<5Y=Exyo+_wmJA_IKJfo zR0fiu_!|5T(2Jt2n7=2o&TJoV?xJd@!ATCSvaU|g!!d>0yIgD#jPc#-7jMxegtyPU zNX6O2Q7aVYnhiik#pI5K;P?5$KtRhWWuPuLC@+|P3Sb7(-(bP?1316NOX*_J{aAmo z>{6K#qPds!#5gj$+WLu=WzUBDyCR#q0OmzzzOnFis}13wi2&Fp8s9SB|qlJUQ1P5%mVC{Q?W zVP8T`MhHxMam&KUxZfF04rF)422L^9O)GT7&xGaTfa%^ zY#_;W_@|+4e2)}Z`OkPPW-3BxAB&ha;vzjNmPC<|l#FEr!v@>^!X{+g76TyC+)nao zjZK2cRx|Hv^)64t_+T4Cj(D7p;r1sT4VP<(7CwC2_;M~W52AY{+}G8$gY|LbL%tqm zU

00oWlTTEYq;S#*LxU@EQ9?sxTIcspF2wOeb~*~r14Yyf>I zsO*(}_i>;yqR#>UJ!;kMhb#26hV`Q)_S*zCAV+uH8;)E9vup!k3R_^lO5os++nm>Z z1~lhz8z4c0*}x+WIAr;UMn+1l7x*#b+04huZcpC6d2^3r(r4-mP-^u40|4(>_!tMB ztZ~>XIKVBy%NzfTwcXJ*OtGIr@CpfdW(lN?48gAv{|AEpN>YK|Ss1zZDCrNlb@G?_ z{rlJdH5L8mn|F!nZ&$eM@dsF(n1AY;|KxVRdUP!lswIIH3zk;t#ovKYZz$*mVKF{r zOr6v`dYmqz2w{|F6nVBGYf8HzSsDAp9HQ`_wq+?#KN)fW>{zo&!jxJ$a|euk6C16N z5!%y_mOw2bJTvv1@+%{@c$7hRM+t-FE&Nrlk^_OhcuyD08kxXqJGB(lH<6VXg@&$5 z#<;+7ObD0;qwLDu=EGFsX6B7*o1vA7v#-Ij01I7JQF!dXPMA;Vz`+v`~krZ;sJs}FE(g_=zwYqw=R}KjUn)>t60judwJQD)=swZ=cbsf3q(&5B_Dcew*t=hK*`d=thqN_*VF)MSV`I|eW;%#oGAgSj3TsZ z0e_#q0gwdqXR2Pi-G}0BZjnI0y*Gr<(J!lj#S@@qGzgk+9ey1RSJoITeFfHr8TsIf zqCAVl7?>U>AGSt} zp548GN_9y_wL1NdGr%DgUP%w*Phd5bKmTxp&!{2cw*n}MHlF_WQBMe&$~*WqUc zS~+bD_;zwROh(a|B@B+pjlmI-6O&pfK%}wKbkrBsv9^0m3!;+hhg@cz;wz?{Ec%hb z#Xben6dgDpLZFODsH0Q{9!^vE3~~VQ%AE4MS{?cq!q@Y5&vAM@zblZw8 zn{=UV_!u4%mf1BLZx`lowuLZ-?~$ZPqvG(`vP9$Z)FD$o)^$Zp$NfMx_?WD*d%pXV zw{nHq&QNo|-`D21IuD#tid0dsp6Dr4+w8?(Heg|wB--z<;qy*)X?|+O5iYLcbUECb z8W=iE6%gR!bG?LKwjdw7rc9hp!vx%cyVbR@viw0;*w2@;Ialw}*c6uD(xkK5cjn|- znD>wd58+vG8a2O%hX4sW20D7b_vH5J^f8jgx&;oxb@Hv)=U!nkrId9;wHU+d@^YmZ zLlkWGo|1kSUi2HA>fGGD(<{i?dFke6-mQ}ZD<4ZK@aU9WQcTM4xw!%(x6r+-lhLse zJ6_(J+YHEgpXuV;je&Om*+JIoKLX5m-*YF+TtM3Jgo~L7r1(4&NWtp%{W3CHfBbj; z`keIi^z!oZ+dH3~F}2iXU;Nyz9f=dpF2wi{1Xj-rsIBAl$IS^RQcmTt;p+YKR z1Zv)mG0?ei#R{+~6k1`XEZu=RS zb7#usLo9vnZ+jG2mrdAV#=oNRKCr9eu=(dQU?ajQ2pHQ(76ZF79qzzVPN2UFXqp^0 q(*lQxLKYLb{?~P!3=9Rv`x#@N%nTFKxChL>3=E#GelF{r5}E*D3|ch+ diff --git a/Doc/QueryComponents.png b/Doc/QueryComponents.png index 6b7931ac31993b450e930ce62e892e59be166ec4..f3ea82a3d3b7254aeb1f3bd1d1dc1fb088de2087 100644 GIT binary patch literal 15734 zcmd73Wn5d^_U@myn|4=#gBNeHqQ$LPy9v@_#TvB1DPE*FgetDZwOA-sq2ygH1Lm>AcPoYoLoa5If-P|b$6VA`O^?pZL1#mw9UB6zjJd7+r_IB+L zo=QFjDMz9^w^J-X$0|I&HOXODj@i>*5x9vZtwLMp^hCCl5DPv$ePQPRM?nN9?1n*k zdXIqAT~>uL=C{wQaDN09PuYaU9-ea3YnB-|Zz-hqVh%?h_I1Sb$~=U~2bNFZIx?VL+d={!uN)%Uoyn?Z+B+oWjmcTm8+*dyLqpYO* zU{+Sv@p|>fEbX~7avXdyOdvuXHJrH=V3u3hv}{F19+nqt#GbuYZ@o280Ug+Ujzhmt zC22pe{3eJ!{fJTq_B8JsU)=r z7EZ1WzWX%vWdYXX(Mo!4k;>g={mb(4Jlsi2Xo@#}$S@5qzI#^E^v$V1_>_4hGKQbsBz@1t4VYI;&scE36OT??yu(uqj^D{~PSwJ~E z7{eY>!R;WM)4-c`#W3O-smOe#C^gGGQSgI87-w`93=u9Z*H>aSKoo7#F4Vo&BT02k z%n~RsnrbM)%W}3Ig$j>~nsVEy%@D$aaA6oubw(E~UPa1AOp5aj6K67)k3%hugOt6i z%|+=r?%Sy9I(@9xg2%mR@XQ}xR?E`*;U>dX)Wt2=eiNkH_A*b2KhTLNGf7TqU8i9=n48M{ z>2JF4ayvqqU#*r#2#4L?JJNC=D+~0z+Ao?uTgpMGFv`=_Kt@0S3j|klca=skJeXGE zrK+lZ6A8?HmU8OuhVNB~QHX#mjy7gf?XDY}=uwzMj^>n_GBYaO#$I`w& zDP~%V@H@h*>RD-$w*#toEsl@@+G_nJQj%$yOSBo>o4tO{!f=@#mQ&g?#%^@Xku1+G!&% zH`imv_W|ico#^E<2!zS#6^^!Yc!vN^KgQ;4!6wmt`zakcq{u(Cts~+Jq($h=!x#eT zc&Bz9@{L{g|85s~J_TD}TU*=MNZKnh)POIT7D6ImHurg*Hy{yu9EM>6ZXUt zy41ihF{e{kmzkTw`x9o_yW!{2gPHhTXM*X5Ob%W?uk3cey8pap^YM?Srlt&`%s1G* zJ+$5AQ7xyavf673di}W_w>HEN%YiHX@P_5#&owT)2-o}~4!7M(Z#x$T25yE%J8-Uu zm}OOvAf=R%^Kd)0ylTaVHP6?pl@>aL0}WzLjL-#?L?d6#Qt_g{{opHCvY&0a7Dg$> zrw~L#f+((;%n&;r7)a`4FHPC5+Uc%+{n-W+U#w`zaQ8jemd>*_UJiQeG@6hl@24~| zRUTK>6SiCC$Zwq4oR>Qe?VomQCmee|YF+I8<1XP4CE@O}{BCB-;#Nu8_P1}}gvoRn z@@tuJa57+h2AMf#b~6<^_6i%Ad@>Reg)Pla>1)@n?JVjJoPL&E>=3IB)hmq3Mz;EX zZq7#8)ngEk!sF=g``!-~O*Q`Xn@6nZ?CxG>3UmLSRX)8;kgV;NnM@I5XAC_KjyE3l zBbRL>WL(%4nII|_(ToEJcy=V;Vn=be%W$Qz2w~V=!l#ksgu7;aCHpK>^@%JV9v(I| zThe8yJ+3zazXf~?DM&P1RaJ4hUc1vJA`?v0>A)7^^5+N3EB)+B^AZ!BO-XW<)e2pR z@_0m@&g-`w)sM5Zq0l+A%CF**YoDFaE)P^Q1d=v&>

(C4r|#nE|Oo!-9_c0O=g{dqs}_MFvOr`d3? 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

0D= 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 From e6838cd06a145905caee7d39c749deb8d0551a15 Mon Sep 17 00:00:00 2001 From: draphyz Date: Wed, 10 Nov 2021 21:08:16 +0100 Subject: [PATCH 052/111] Move event handling and publishing abstractions to application layer --- Src/DDD.Core.SimpleInjector/ContainerExtensions.cs | 2 +- Src/DDD.Core/{Domain => Application}/AsyncEventHandler.cs | 4 +++- Src/DDD.Core/{Domain => Application}/EventHandler.cs | 4 +++- Src/DDD.Core/{Domain => Application}/EventPublisher.cs | 3 ++- Src/DDD.Core/{Domain => Application}/IAsyncEventHandler.cs | 4 +++- Src/DDD.Core/{Domain => Application}/IAsyncEventHandler`1.cs | 4 +++- Src/DDD.Core/{Domain => Application}/IEventHandler.cs | 4 +++- Src/DDD.Core/{Domain => Application}/IEventHandler`1.cs | 4 +++- Src/DDD.Core/{Domain => Application}/IEventPublisher.cs | 4 +++- .../{Domain => Application}/IEventPublisherExtensions.cs | 5 +++-- .../{Domain => Application}/EventPublisherTests.cs | 5 +++-- 11 files changed, 30 insertions(+), 13 deletions(-) rename Src/DDD.Core/{Domain => Application}/AsyncEventHandler.cs (93%) rename Src/DDD.Core/{Domain => Application}/EventHandler.cs (91%) rename Src/DDD.Core/{Domain => Application}/EventPublisher.cs (97%) rename Src/DDD.Core/{Domain => Application}/IAsyncEventHandler.cs (87%) rename Src/DDD.Core/{Domain => Application}/IAsyncEventHandler`1.cs (89%) rename Src/DDD.Core/{Domain => Application}/IEventHandler.cs (83%) rename Src/DDD.Core/{Domain => Application}/IEventHandler`1.cs (85%) rename Src/DDD.Core/{Domain => Application}/IEventPublisher.cs (90%) rename Src/DDD.Core/{Domain => Application}/IEventPublisherExtensions.cs (94%) rename Test/DDD.Core.UnitTests/{Domain => Application}/EventPublisherTests.cs (99%) diff --git a/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs index 2ae93cb..9e98d3e 100644 --- a/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs +++ b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs @@ -8,7 +8,7 @@ namespace DDD.Core.Infrastructure.DependencyInjection { - using Core.Domain; + using Core.Application; public static class ContainerExtensions { diff --git a/Src/DDD.Core/Domain/AsyncEventHandler.cs b/Src/DDD.Core/Application/AsyncEventHandler.cs similarity index 93% rename from Src/DDD.Core/Domain/AsyncEventHandler.cs rename to Src/DDD.Core/Application/AsyncEventHandler.cs index 76be362..52b1f2b 100644 --- a/Src/DDD.Core/Domain/AsyncEventHandler.cs +++ b/Src/DDD.Core/Application/AsyncEventHandler.cs @@ -2,8 +2,10 @@ using System.Threading; using System.Threading.Tasks; -namespace DDD.Core.Domain +namespace DDD.Core.Application { + using Domain; + ///

/// Base class for handling asynchronously events. /// diff --git a/Src/DDD.Core/Domain/EventHandler.cs b/Src/DDD.Core/Application/EventHandler.cs similarity index 91% rename from Src/DDD.Core/Domain/EventHandler.cs rename to Src/DDD.Core/Application/EventHandler.cs index 3cd7e76..75b8fa7 100644 --- a/Src/DDD.Core/Domain/EventHandler.cs +++ b/Src/DDD.Core/Application/EventHandler.cs @@ -1,7 +1,9 @@ using System; -namespace DDD.Core.Domain +namespace DDD.Core.Application { + using Domain; + /// /// Base class for handling events. /// diff --git a/Src/DDD.Core/Domain/EventPublisher.cs b/Src/DDD.Core/Application/EventPublisher.cs similarity index 97% rename from Src/DDD.Core/Domain/EventPublisher.cs rename to Src/DDD.Core/Application/EventPublisher.cs index 31f9181..a398a35 100644 --- a/Src/DDD.Core/Domain/EventPublisher.cs +++ b/Src/DDD.Core/Application/EventPublisher.cs @@ -4,8 +4,9 @@ using System.Threading; using System.Threading.Tasks; -namespace DDD.Core.Domain +namespace DDD.Core.Application { + using Domain; using Threading; public class EventPublisher : IEventPublisher diff --git a/Src/DDD.Core/Domain/IAsyncEventHandler.cs b/Src/DDD.Core/Application/IAsyncEventHandler.cs similarity index 87% rename from Src/DDD.Core/Domain/IAsyncEventHandler.cs rename to Src/DDD.Core/Application/IAsyncEventHandler.cs index 9de1670..370e179 100644 --- a/Src/DDD.Core/Domain/IAsyncEventHandler.cs +++ b/Src/DDD.Core/Application/IAsyncEventHandler.cs @@ -2,8 +2,10 @@ using System.Threading; using System.Threading.Tasks; -namespace DDD.Core.Domain +namespace DDD.Core.Application { + using Domain; + public interface IAsyncEventHandler { #region Properties diff --git a/Src/DDD.Core/Domain/IAsyncEventHandler`1.cs b/Src/DDD.Core/Application/IAsyncEventHandler`1.cs similarity index 89% rename from Src/DDD.Core/Domain/IAsyncEventHandler`1.cs rename to Src/DDD.Core/Application/IAsyncEventHandler`1.cs index c68fdff..adb6f92 100644 --- a/Src/DDD.Core/Domain/IAsyncEventHandler`1.cs +++ b/Src/DDD.Core/Application/IAsyncEventHandler`1.cs @@ -1,8 +1,10 @@ using System.Threading; using System.Threading.Tasks; -namespace DDD.Core.Domain +namespace DDD.Core.Application { + using Domain; + /// /// Defines a method that handles asynchronously an event of a specified type. /// diff --git a/Src/DDD.Core/Domain/IEventHandler.cs b/Src/DDD.Core/Application/IEventHandler.cs similarity index 83% rename from Src/DDD.Core/Domain/IEventHandler.cs rename to Src/DDD.Core/Application/IEventHandler.cs index 62407ae..3c356ac 100644 --- a/Src/DDD.Core/Domain/IEventHandler.cs +++ b/Src/DDD.Core/Application/IEventHandler.cs @@ -1,7 +1,9 @@ using System; -namespace DDD.Core.Domain +namespace DDD.Core.Application { + using Domain; + public interface IEventHandler { #region Properties diff --git a/Src/DDD.Core/Domain/IEventHandler`1.cs b/Src/DDD.Core/Application/IEventHandler`1.cs similarity index 85% rename from Src/DDD.Core/Domain/IEventHandler`1.cs rename to Src/DDD.Core/Application/IEventHandler`1.cs index 1af5371..5fc7ad6 100644 --- a/Src/DDD.Core/Domain/IEventHandler`1.cs +++ b/Src/DDD.Core/Application/IEventHandler`1.cs @@ -1,5 +1,7 @@ -namespace DDD.Core.Domain +namespace DDD.Core.Application { + using Domain; + /// /// Defines a method that handles an event of a specified type. /// diff --git a/Src/DDD.Core/Domain/IEventPublisher.cs b/Src/DDD.Core/Application/IEventPublisher.cs similarity index 90% rename from Src/DDD.Core/Domain/IEventPublisher.cs rename to Src/DDD.Core/Application/IEventPublisher.cs index d711072..eebaec9 100644 --- a/Src/DDD.Core/Domain/IEventPublisher.cs +++ b/Src/DDD.Core/Application/IEventPublisher.cs @@ -1,8 +1,10 @@ using System.Threading; using System.Threading.Tasks; -namespace DDD.Core.Domain +namespace DDD.Core.Application { + using Domain; + /// /// Defines a component that publishes events of any type. /// diff --git a/Src/DDD.Core/Domain/IEventPublisherExtensions.cs b/Src/DDD.Core/Application/IEventPublisherExtensions.cs similarity index 94% rename from Src/DDD.Core/Domain/IEventPublisherExtensions.cs rename to Src/DDD.Core/Application/IEventPublisherExtensions.cs index 0717b41..de1c0ef 100644 --- a/Src/DDD.Core/Domain/IEventPublisherExtensions.cs +++ b/Src/DDD.Core/Application/IEventPublisherExtensions.cs @@ -1,9 +1,10 @@ using Conditions; using System.Collections.Generic; +using System.Threading; -namespace DDD.Core.Domain +namespace DDD.Core.Application { - using System.Threading; + using Domain; using Threading; public static class IEventPublisherExtensions diff --git a/Test/DDD.Core.UnitTests/Domain/EventPublisherTests.cs b/Test/DDD.Core.UnitTests/Application/EventPublisherTests.cs similarity index 99% rename from Test/DDD.Core.UnitTests/Domain/EventPublisherTests.cs rename to Test/DDD.Core.UnitTests/Application/EventPublisherTests.cs index 575754a..db0235b 100644 --- a/Test/DDD.Core.UnitTests/Domain/EventPublisherTests.cs +++ b/Test/DDD.Core.UnitTests/Application/EventPublisherTests.cs @@ -2,11 +2,12 @@ using System.Collections.Generic; using Xunit; using SimpleInjector; +using System.Threading.Tasks; -namespace DDD.Core.Domain +namespace DDD.Core.Application { + using Domain; using Collections; - using System.Threading.Tasks; public class EventPublisherTests { From 28a1401e03c3f45d2a717b3d04c25919155f8703 Mon Sep 17 00:00:00 2001 From: draphyz Date: Sun, 14 Nov 2021 14:18:07 +0100 Subject: [PATCH 053/111] Set Indent to false by default --- .../Serialization/JsonSerializationOptions.cs | 2 +- .../Serialization/XmlSerializationOptions.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Src/DDD.Core.Abstractions/Serialization/JsonSerializationOptions.cs b/Src/DDD.Core.Abstractions/Serialization/JsonSerializationOptions.cs index 85a3b16..e01a375 100644 --- a/Src/DDD.Core.Abstractions/Serialization/JsonSerializationOptions.cs +++ b/Src/DDD.Core.Abstractions/Serialization/JsonSerializationOptions.cs @@ -9,7 +9,7 @@ public static class JsonSerializationOptions 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 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 } From 2585deab01800b72b2e893cb2e6d85de285f4f0b Mon Sep 17 00:00:00 2001 From: draphyz Date: Sun, 14 Nov 2021 20:32:13 +0100 Subject: [PATCH 054/111] Refactor stored event --- .../NHibernateRepository.cs | 5 +-- Src/DDD.Core.NHibernate/StoredEventMapping.cs | 22 ++++----- .../Infrastructure/Data/StoredEvent.cs | 6 +-- .../Data/StoredEventTranslator.cs | 5 ++- .../HealthcareDeliveryConfigurationTests.cs | 11 ++--- .../Scripts/Oracle/FillSchema.sql | 42 +++++++++--------- .../Scripts/SqlServer/CreateDatabase.sql | Bin 16410 -> 16262 bytes 7 files changed, 44 insertions(+), 47 deletions(-) diff --git a/Src/DDD.Core.NHibernate/NHibernateRepository.cs b/Src/DDD.Core.NHibernate/NHibernateRepository.cs index d03f79c..e837c73 100644 --- a/Src/DDD.Core.NHibernate/NHibernateRepository.cs +++ b/Src/DDD.Core.NHibernate/NHibernateRepository.cs @@ -73,14 +73,13 @@ public async Task SaveAsync(TDomainEntity aggregate, CancellationToken cancellat private IEnumerable ToStoredEvents(TDomainEntity aggregate) { - var user = Thread.CurrentPrincipal?.Identity?.Name; + var username = Thread.CurrentPrincipal?.Identity?.Name; return aggregate.AllEvents().Select(e => { var evt = this.eventTranslator.Translate(e); evt.StreamType = aggregate.GetType().Name; evt.StreamId = aggregate.IdentityAsString(); - evt.UniqueId = Guid.NewGuid(); - evt.Username = user; + evt.IssuedBy = username; return evt; }); } diff --git a/Src/DDD.Core.NHibernate/StoredEventMapping.cs b/Src/DDD.Core.NHibernate/StoredEventMapping.cs index b62911b..8d21dcc 100644 --- a/Src/DDD.Core.NHibernate/StoredEventMapping.cs +++ b/Src/DDD.Core.NHibernate/StoredEventMapping.cs @@ -13,7 +13,7 @@ protected StoredEventMapping() { this.Lazy(false); // Table - this.Table("StoredEvent"); + this.Table("Event"); // Keys this.Id(e => e.Id, m1 => { @@ -24,33 +24,33 @@ protected StoredEventMapping() this.Property(e => e.EventType, m => { m.Type(NHibernateUtil.AnsiString); - m.Length(50); + m.Length(250); m.NotNullable(true); }); this.Property(e => e.Version); - this.Property(e => e.StreamType, m => + this.Property(e => e.OccurredOn, m => m.Precision(3)); // in milliseconds + this.Property(e => e.Body, m => { m.Type(NHibernateUtil.AnsiString); - m.Length(50); + m.NotNullable(true); }); this.Property(e => e.StreamId, m => { m.Type(NHibernateUtil.AnsiString); m.Length(50); + m.NotNullable(true); }); - this.Property(e => e.UniqueId, m => m.NotNullable(true)); - this.Property(e => e.OccurredOn, m => m.Precision(3)); // in milliseconds - this.Property(e => e.Username, m => + this.Property(e => e.StreamType, m => { m.Type(NHibernateUtil.AnsiString); - m.Length(100); + m.Length(50); + m.NotNullable(true); }); - this.Property(e => e.Body, m => + this.Property(e => e.IssuedBy, m => { m.Type(NHibernateUtil.AnsiString); - m.NotNullable(true); + m.Length(100); }); - this.Property(e => e.IsDispatched); } #endregion Constructors diff --git a/Src/DDD.Core/Infrastructure/Data/StoredEvent.cs b/Src/DDD.Core/Infrastructure/Data/StoredEvent.cs index 438f7d3..5296805 100644 --- a/Src/DDD.Core/Infrastructure/Data/StoredEvent.cs +++ b/Src/DDD.Core/Infrastructure/Data/StoredEvent.cs @@ -13,17 +13,13 @@ public class StoredEvent public long Id { get; set; } - public bool IsDispatched { get; set; } = false; - public DateTime OccurredOn { get; set; } public string StreamId { get; set; } public string StreamType { get; set; } - public Guid UniqueId { get; set; } - - public string Username { get; set; } + public string IssuedBy { get; set; } public byte Version { get; set; } = 1; diff --git a/Src/DDD.Core/Infrastructure/Data/StoredEventTranslator.cs b/Src/DDD.Core/Infrastructure/Data/StoredEventTranslator.cs index 26b9998..e8020cc 100644 --- a/Src/DDD.Core/Infrastructure/Data/StoredEventTranslator.cs +++ b/Src/DDD.Core/Infrastructure/Data/StoredEventTranslator.cs @@ -32,11 +32,12 @@ public StoredEventTranslator(ITextSerializer eventSerializer) public StoredEvent Translate(IEvent @event, IDictionary options = null) { Condition.Requires(@event, nameof(@event)).IsNotNull(); + var eventType = @event.GetType(); return new StoredEvent() { OccurredOn = @event.OccurredOn, - EventType = @event.GetType().Name, - Version = ToVersion(@event.GetType().FullName), + EventType = $"{eventType.FullName}, {eventType.Assembly.GetName().Name}", + Version = ToVersion(eventType.FullName), Body = this.eventSerializer.SerializeToString(@event) }; } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareDeliveryConfigurationTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareDeliveryConfigurationTests.cs index 91ba8f7..16fa248 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareDeliveryConfigurationTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareDeliveryConfigurationTests.cs @@ -115,12 +115,13 @@ private static StoredEvent CreateEvent() return new StoredEvent { Id = 1, - Body = @"12018-01-01T10:06:00", + EventType = "DDD.HealthcareDelivery.Domain.Prescriptions.PharmaceuticalPrescriptionCreated, DDD.HealthcareDelivery.Messages", + Version = 1, + OccurredOn = new DateTime(2018, 1, 1), + Body = "{\"prescriptionId\":1,\"occurredOn\":\"2018 - 01 - 01T10:06:00\"}", StreamId = "1", - UniqueId = Guid.NewGuid(), - Username = "draphyz", - EventType = "PharmaceuticalPrescriptionCreated", - OccurredOn = new DateTime(2018, 1, 1) + StreamType = "PharmaceuticalPrescription", + IssuedBy = "draphyz" }; } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql index 48720d0..fb2374b 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql @@ -40,20 +40,18 @@ BEGIN END SPCLEARSCHEMA; / -------------------------------------------------------- --- DDL for Table STOREDEVENT +-- DDL for Table EVENT -------------------------------------------------------- - CREATE TABLE TEST.STOREDEVENT + CREATE TABLE TEST.EVENT ( EVENTID NUMBER(19,0), - EVENTTYPE VARCHAR2(50 CHAR), + EVENTTYPE VARCHAR2(250 CHAR), VERSION NUMBER(3, 0), - STREAMTYPE VARCHAR2(50 CHAR), - STREAMID VARCHAR2(50 CHAR), - UNIQUEID RAW(16), OCCURREDON TIMESTAMP(3), - USERNAME VARCHAR2(100 CHAR), - BODY VARCHAR2(4000), - ISDISPATCHED NUMBER(1,0) + BODY VARCHAR2(4000), + STREAMID VARCHAR2(50 CHAR), + STREAMTYPE VARCHAR2(50 CHAR), + ISSUEDBY VARCHAR2(100 CHAR) ) SEGMENT CREATION IMMEDIATE PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645 @@ -132,10 +130,10 @@ END SPCLEARSCHEMA; TABLESPACE USERS / -------------------------------------------------------- --- DDL for Index PK_STOREDEVENT +-- DDL for Index PK_EVENT -------------------------------------------------------- - CREATE UNIQUE INDEX TEST.PK_STOREDEVENT ON TEST.STOREDEVENT (EVENTID) + 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) @@ -165,33 +163,35 @@ END SPCLEARSCHEMA; -- Constraints for Table EVENT -------------------------------------------------------- - ALTER TABLE TEST.STOREDEVENT ADD CONSTRAINT PK_STOREDEVENT PRIMARY KEY (EVENTID) + 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.STOREDEVENT MODIFY (EVENTID NOT NULL ENABLE) + ALTER TABLE TEST.EVENT MODIFY (EVENTID NOT NULL ENABLE) / - ALTER TABLE TEST.STOREDEVENT MODIFY (EVENTTYPE NOT NULL ENABLE) + ALTER TABLE TEST.EVENT MODIFY (EVENTTYPE NOT NULL ENABLE) / - ALTER TABLE TEST.STOREDEVENT MODIFY (VERSION NOT NULL ENABLE) + ALTER TABLE TEST.EVENT MODIFY (VERSION NOT NULL ENABLE) / - ALTER TABLE TEST.STOREDEVENT MODIFY (UNIQUEID NOT NULL ENABLE) + ALTER TABLE TEST.EVENT MODIFY (OCCURREDON NOT NULL ENABLE) / - - ALTER TABLE TEST.STOREDEVENT MODIFY (OCCURREDON NOT NULL ENABLE) + + ALTER TABLE TEST.EVENT MODIFY (BODY NOT NULL ENABLE) / - ALTER TABLE TEST.STOREDEVENT MODIFY (ISDISPATCHED NOT NULL ENABLE) + ALTER TABLE TEST.EVENT MODIFY (STREAMID NOT NULL ENABLE) / - - ALTER TABLE TEST.STOREDEVENT MODIFY (BODY NOT NULL ENABLE) + + ALTER TABLE TEST.EVENT MODIFY (STREAMTYPE NOT NULL ENABLE) / + + -------------------------------------------------------- -- Constraints for Table PRESCMEDICATION -------------------------------------------------------- diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql index bf433adec221e7467a4d19e78f0e73792a32281d..79e50d0f90e62e8c049f92c3ee217e7fb498d31b 100644 GIT binary patch delta 98 zcmbQ$z}Qy5;e^6u8xgL}0qRPOAjV`dr4mM?$s5g-C$CX*n7lLopDRGNdx3FgQ(aG?JdYL6c>&fr`lF9xa8<-&9nj0a>^o*Z=?k delta 224 zcmZpxpVh#);e-NrFhdDLK0^^hDnrWV;|jWr$O4l;E7nhbpsX}`nv%ohEM=a_2b9$& zCn#Gu>M-y!a4~Q)L^Fgk|5P6b*P zJK4}ccJei)0M1Z`Vz5Zw Date: Sat, 20 Nov 2021 10:35:25 +0100 Subject: [PATCH 055/111] Update README.md --- README.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8549df2..ca21549 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,24 @@ This project is an example of a .NET implementation of a medical prescription mo **Points of interest** -The goal of the project is to experiment with some .NET implementations 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. +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. + +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 : + +- 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 -Only a light version of CQRS (no data projection, one data store) has been considered. **Model** From 02c301563c7cd0f680cff3a71edfd768ded789f3 Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 28 Feb 2022 14:05:53 +0100 Subject: [PATCH 056/111] CreateOpenConnectionAsync with CancellationToken --- Src/DDD.Core/Infrastructure/Data/DbQueryHandler.cs | 2 +- .../Infrastructure/Data/IDbConnectionFactoryExtensions.cs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Src/DDD.Core/Infrastructure/Data/DbQueryHandler.cs b/Src/DDD.Core/Infrastructure/Data/DbQueryHandler.cs index a2e4f22..112038a 100644 --- a/Src/DDD.Core/Infrastructure/Data/DbQueryHandler.cs +++ b/Src/DDD.Core/Infrastructure/Data/DbQueryHandler.cs @@ -54,7 +54,7 @@ public async Task HandleAsync(TQuery query, CancellationToken cancellat await new SynchronizationContextRemover(); try { - using (var connection = await this.ConnectionFactory.CreateOpenConnectionAsync()) + using (var connection = await this.ConnectionFactory.CreateOpenConnectionAsync(cancellationToken)) { return await this.ExecuteAsync(query, connection, cancellationToken); } diff --git a/Src/DDD.Core/Infrastructure/Data/IDbConnectionFactoryExtensions.cs b/Src/DDD.Core/Infrastructure/Data/IDbConnectionFactoryExtensions.cs index 6104045..bbb5c4f 100644 --- a/Src/DDD.Core/Infrastructure/Data/IDbConnectionFactoryExtensions.cs +++ b/Src/DDD.Core/Infrastructure/Data/IDbConnectionFactoryExtensions.cs @@ -1,4 +1,5 @@ using System.Data.Common; +using System.Threading; using System.Threading.Tasks; using Conditions; @@ -19,12 +20,12 @@ public static DbConnection CreateOpenConnection(this IDbConnectionFactory factor return connection; } - public static async Task CreateOpenConnectionAsync(this IDbConnectionFactory factory) + public static async Task CreateOpenConnectionAsync(this IDbConnectionFactory factory, CancellationToken cancellationToken = default) { Condition.Requires(factory, nameof(factory)).IsNotNull(); await new SynchronizationContextRemover(); var connection = factory.CreateConnection(); - await connection.OpenAsync(); + await connection.OpenAsync(cancellationToken); return connection; } From ad6d98762e92ebd84317672adfc6d1a47769f833 Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 28 Feb 2022 16:12:29 +0100 Subject: [PATCH 057/111] Add serialization format --- .../Serialization/ISerializer.cs | 7 +- .../Serialization/SerializationFormat.cs | 8 ++ .../EnumUpperStringType.cs | 77 ++++++++++++++++++ Src/DDD.Core.NHibernate/StoredEventMapping.cs | 6 ++ .../JsonSerializerWrapper.cs | 4 +- .../Infrastructure/Data/StoredEvent.cs | 7 +- .../Data/StoredEventTranslator.cs | 3 +- .../DataContractSerializerWrapper.cs | 2 + .../Serialization/XmlSerializerWrapper.cs | 2 + .../Scripts/Oracle/FillSchema.sql | 1 + .../Scripts/SqlServer/CreateDatabase.sql | Bin 16262 -> 16340 bytes 11 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 Src/DDD.Core.Abstractions/Serialization/SerializationFormat.cs create mode 100644 Src/DDD.Core.NHibernate/EnumUpperStringType.cs diff --git a/Src/DDD.Core.Abstractions/Serialization/ISerializer.cs b/Src/DDD.Core.Abstractions/Serialization/ISerializer.cs index 1fb21be..9254975 100644 --- a/Src/DDD.Core.Abstractions/Serialization/ISerializer.cs +++ b/Src/DDD.Core.Abstractions/Serialization/ISerializer.cs @@ -5,6 +5,12 @@ namespace DDD.Serialization public interface ISerializer { + #region Properties + + SerializationFormat Format { get; } + + #endregion Properties + #region Methods T Deserialize(Stream stream); @@ -12,6 +18,5 @@ public interface ISerializer void Serialize(Stream stream, object obj); #endregion Methods - } } \ No newline at end of file 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.NHibernate/EnumUpperStringType.cs b/Src/DDD.Core.NHibernate/EnumUpperStringType.cs new file mode 100644 index 0000000..ce96af6 --- /dev/null +++ b/Src/DDD.Core.NHibernate/EnumUpperStringType.cs @@ -0,0 +1,77 @@ +using NHibernate; +using NHibernate.Type; +using System; + +namespace DDD.Core.Infrastructure.Data +{ + public class EnumUpperStringType : EnumStringType + { + + #region Constructors + + public EnumUpperStringType(Type enumClass) : base(enumClass) + { + } + + public EnumUpperStringType(Type enumClass, int length) : base(enumClass, length) + { + } + + #endregion Constructors + + #region Methods + + public override object GetInstance(object code) + { + try + { + return Enum.Parse(this.ReturnedClass, code as string, true); + } + catch (ArgumentException ae) + { + throw new HibernateException(string.Format("Can't Parse {0} as {1}", code, ReturnedClass.Name), ae); + } + } + + public override object GetValue(object code) + { + var value = (string)base.GetValue(code); + return value?.ToUpper(); + } + + #endregion Methods + + } + + [Serializable] + public class EnumUpperStringType : EnumUpperStringType + { + + #region Fields + + private readonly string typeName; + + #endregion Fields + + #region Constructors + + public EnumUpperStringType() + : base(typeof(T)) + { + var type = GetType(); + typeName = type.FullName + ", " + type.Assembly.GetName().Name; + } + + #endregion Constructors + + #region Properties + + public override string Name + { + get { return typeName; } + } + + #endregion Properties + + } +} diff --git a/Src/DDD.Core.NHibernate/StoredEventMapping.cs b/Src/DDD.Core.NHibernate/StoredEventMapping.cs index 8d21dcc..0b20e92 100644 --- a/Src/DDD.Core.NHibernate/StoredEventMapping.cs +++ b/Src/DDD.Core.NHibernate/StoredEventMapping.cs @@ -1,6 +1,7 @@ using NHibernate; using NHibernate.Mapping.ByCode.Conformist; using NHibernate.Mapping.ByCode; +using DDD.Serialization; namespace DDD.Core.Infrastructure.Data { @@ -34,6 +35,11 @@ protected StoredEventMapping() m.Type(NHibernateUtil.AnsiString); m.NotNullable(true); }); + this.Property(e => e.BodyFormat, m => + { + m.Type(new EnumUpperStringType()); + m.NotNullable(true); + }); this.Property(e => e.StreamId, m => { m.Type(NHibernateUtil.AnsiString); diff --git a/Src/DDD.Core.Newtonsoft/JsonSerializerWrapper.cs b/Src/DDD.Core.Newtonsoft/JsonSerializerWrapper.cs index bd1d105..bf60341 100644 --- a/Src/DDD.Core.Newtonsoft/JsonSerializerWrapper.cs +++ b/Src/DDD.Core.Newtonsoft/JsonSerializerWrapper.cs @@ -39,6 +39,8 @@ public JsonSerializerWrapper(JsonSerializerSettings settings, Encoding encoding) public Encoding Encoding { get; } + public SerializationFormat Format => SerializationFormat.Json; + public bool Indent => this.settings.Formatting == Formatting.Indented; #endregion Properties @@ -107,4 +109,4 @@ private static JsonSerializerSettings DefaultSettings() #endregion Methods } -} +} \ No newline at end of file diff --git a/Src/DDD.Core/Infrastructure/Data/StoredEvent.cs b/Src/DDD.Core/Infrastructure/Data/StoredEvent.cs index 5296805..950ac25 100644 --- a/Src/DDD.Core/Infrastructure/Data/StoredEvent.cs +++ b/Src/DDD.Core/Infrastructure/Data/StoredEvent.cs @@ -1,4 +1,5 @@ using System; +using DDD.Serialization; namespace DDD.Core.Infrastructure.Data { @@ -9,18 +10,20 @@ public class StoredEvent public string Body { get; set; } + public SerializationFormat BodyFormat { get; set; } + public string EventType { get; set; } public long Id { get; set; } + public string IssuedBy { get; set; } + public DateTime OccurredOn { get; set; } public string StreamId { get; set; } public string StreamType { get; set; } - public string IssuedBy { get; set; } - public byte Version { get; set; } = 1; #endregion Properties diff --git a/Src/DDD.Core/Infrastructure/Data/StoredEventTranslator.cs b/Src/DDD.Core/Infrastructure/Data/StoredEventTranslator.cs index e8020cc..b8254c4 100644 --- a/Src/DDD.Core/Infrastructure/Data/StoredEventTranslator.cs +++ b/Src/DDD.Core/Infrastructure/Data/StoredEventTranslator.cs @@ -38,7 +38,8 @@ public StoredEvent Translate(IEvent @event, IDictionary options OccurredOn = @event.OccurredOn, EventType = $"{eventType.FullName}, {eventType.Assembly.GetName().Name}", Version = ToVersion(eventType.FullName), - Body = this.eventSerializer.SerializeToString(@event) + Body = this.eventSerializer.SerializeToString(@event), + BodyFormat = this.eventSerializer.Format }; } diff --git a/Src/DDD.Core/Infrastructure/Serialization/DataContractSerializerWrapper.cs b/Src/DDD.Core/Infrastructure/Serialization/DataContractSerializerWrapper.cs index 1946d86..0a4a81f 100644 --- a/Src/DDD.Core/Infrastructure/Serialization/DataContractSerializerWrapper.cs +++ b/Src/DDD.Core/Infrastructure/Serialization/DataContractSerializerWrapper.cs @@ -42,6 +42,8 @@ public DataContractSerializerWrapper(XmlWriterSettings writerSettings, public Encoding Encoding => this.writerSettings.Encoding; + public SerializationFormat Format => SerializationFormat.Xml; + public bool Indent => this.writerSettings.Indent; #endregion Properties diff --git a/Src/DDD.Core/Infrastructure/Serialization/XmlSerializerWrapper.cs b/Src/DDD.Core/Infrastructure/Serialization/XmlSerializerWrapper.cs index 460c6cb..994ceb2 100644 --- a/Src/DDD.Core/Infrastructure/Serialization/XmlSerializerWrapper.cs +++ b/Src/DDD.Core/Infrastructure/Serialization/XmlSerializerWrapper.cs @@ -41,6 +41,8 @@ public XmlSerializerWrapper(XmlWriterSettings writerSettings, public Encoding Encoding => this.writerSettings.Encoding; + public SerializationFormat Format => SerializationFormat.Xml; + public bool Indent => this.writerSettings.Indent; #endregion Properties diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql index fb2374b..37c0438 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/FillSchema.sql @@ -49,6 +49,7 @@ END SPCLEARSCHEMA; VERSION NUMBER(3, 0), OCCURREDON TIMESTAMP(3), BODY VARCHAR2(4000), + BODYFORMAT VARCHAR2(20), STREAMID VARCHAR2(50 CHAR), STREAMTYPE VARCHAR2(50 CHAR), ISSUEDBY VARCHAR2(100 CHAR) diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql index 79e50d0f90e62e8c049f92c3ee217e7fb498d31b..1e0bd78389838dc4834c938e503cd93bc957442b 100644 GIT binary patch delta 40 ucmZpxzf!*;OIg^7A)g_Ip_0K3NEb2WG9)sTOm;LU&DR>23 From 47ff170788bf3463b38641b329bf87418d2b20fb Mon Sep 17 00:00:00 2001 From: draphyz Date: Tue, 1 Mar 2022 13:13:45 +0100 Subject: [PATCH 058/111] Add serialization format --- Src/DDD.Core.NHibernate/StoredEventMapping.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Src/DDD.Core.NHibernate/StoredEventMapping.cs b/Src/DDD.Core.NHibernate/StoredEventMapping.cs index 0b20e92..4535e3e 100644 --- a/Src/DDD.Core.NHibernate/StoredEventMapping.cs +++ b/Src/DDD.Core.NHibernate/StoredEventMapping.cs @@ -38,6 +38,7 @@ protected StoredEventMapping() this.Property(e => e.BodyFormat, m => { m.Type(new EnumUpperStringType()); + m.Length(20); m.NotNullable(true); }); this.Property(e => e.StreamId, m => From f9f0ea6868076424d595f4084f430a953a9e2358 Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 7 Mar 2022 15:45:48 +0100 Subject: [PATCH 059/111] Store serialization format as string --- .../EnumUpperStringType.cs | 77 ------------------- Src/DDD.Core.NHibernate/StoredEventMapping.cs | 3 +- 2 files changed, 2 insertions(+), 78 deletions(-) delete mode 100644 Src/DDD.Core.NHibernate/EnumUpperStringType.cs diff --git a/Src/DDD.Core.NHibernate/EnumUpperStringType.cs b/Src/DDD.Core.NHibernate/EnumUpperStringType.cs deleted file mode 100644 index ce96af6..0000000 --- a/Src/DDD.Core.NHibernate/EnumUpperStringType.cs +++ /dev/null @@ -1,77 +0,0 @@ -using NHibernate; -using NHibernate.Type; -using System; - -namespace DDD.Core.Infrastructure.Data -{ - public class EnumUpperStringType : EnumStringType - { - - #region Constructors - - public EnumUpperStringType(Type enumClass) : base(enumClass) - { - } - - public EnumUpperStringType(Type enumClass, int length) : base(enumClass, length) - { - } - - #endregion Constructors - - #region Methods - - public override object GetInstance(object code) - { - try - { - return Enum.Parse(this.ReturnedClass, code as string, true); - } - catch (ArgumentException ae) - { - throw new HibernateException(string.Format("Can't Parse {0} as {1}", code, ReturnedClass.Name), ae); - } - } - - public override object GetValue(object code) - { - var value = (string)base.GetValue(code); - return value?.ToUpper(); - } - - #endregion Methods - - } - - [Serializable] - public class EnumUpperStringType : EnumUpperStringType - { - - #region Fields - - private readonly string typeName; - - #endregion Fields - - #region Constructors - - public EnumUpperStringType() - : base(typeof(T)) - { - var type = GetType(); - typeName = type.FullName + ", " + type.Assembly.GetName().Name; - } - - #endregion Constructors - - #region Properties - - public override string Name - { - get { return typeName; } - } - - #endregion Properties - - } -} diff --git a/Src/DDD.Core.NHibernate/StoredEventMapping.cs b/Src/DDD.Core.NHibernate/StoredEventMapping.cs index 4535e3e..1e09888 100644 --- a/Src/DDD.Core.NHibernate/StoredEventMapping.cs +++ b/Src/DDD.Core.NHibernate/StoredEventMapping.cs @@ -2,6 +2,7 @@ using NHibernate.Mapping.ByCode.Conformist; using NHibernate.Mapping.ByCode; using DDD.Serialization; +using NHibernate.Type; namespace DDD.Core.Infrastructure.Data { @@ -37,7 +38,7 @@ protected StoredEventMapping() }); this.Property(e => e.BodyFormat, m => { - m.Type(new EnumUpperStringType()); + m.Type(new EnumStringType()); m.Length(20); m.NotNullable(true); }); From 0fbe69fce06b9248b3c03bf97a28528fabd7da83 Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 7 Mar 2022 16:17:20 +0100 Subject: [PATCH 060/111] Add non generic method for deserialization --- .../Serialization/ISerializer.cs | 5 +++-- .../Serialization/ISerializerExtensions.cs | 20 +++++++++++++++++-- .../ITextSerializerExtensions.cs | 10 +++++++++- .../JsonSerializerWrapper.cs | 8 +++++--- .../DataContractSerializerWrapper.cs | 9 +++++---- .../Serialization/XmlSerializerWrapper.cs | 9 +++++---- .../JsonSerializerWrapperTests.cs | 2 +- .../DataContractSerializerWrapperTests.cs | 2 +- .../XmlSerializerWrapperTests.cs | 2 +- 9 files changed, 48 insertions(+), 19 deletions(-) diff --git a/Src/DDD.Core.Abstractions/Serialization/ISerializer.cs b/Src/DDD.Core.Abstractions/Serialization/ISerializer.cs index 9254975..035f02b 100644 --- a/Src/DDD.Core.Abstractions/Serialization/ISerializer.cs +++ b/Src/DDD.Core.Abstractions/Serialization/ISerializer.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; namespace DDD.Serialization { @@ -13,7 +14,7 @@ public interface ISerializer #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..9cd1f08 100644 --- a/Src/DDD.Core.Abstractions/Serialization/ISerializerExtensions.cs +++ b/Src/DDD.Core.Abstractions/Serialization/ISerializerExtensions.cs @@ -1,4 +1,5 @@ using Conditions; +using System; using System.IO; namespace DDD.Serialization @@ -8,16 +9,31 @@ public static class ISerializerExtensions #region Methods - public static T DeserializeFromFile(this ISerializer serializer, string filePath) + public static T Deserialize(this ISerializer serializer, Stream stream) + { + Condition.Requires(serializer, nameof(serializer)).IsNotNull(); + Condition.Requires(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(); + Condition.Requires(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) + { + Condition.Requires(serializer, nameof(serializer)).IsNotNull(); + Condition.Requires(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(); diff --git a/Src/DDD.Core.Abstractions/Serialization/ITextSerializerExtensions.cs b/Src/DDD.Core.Abstractions/Serialization/ITextSerializerExtensions.cs index c845dd5..6d40327 100644 --- a/Src/DDD.Core.Abstractions/Serialization/ITextSerializerExtensions.cs +++ b/Src/DDD.Core.Abstractions/Serialization/ITextSerializerExtensions.cs @@ -1,4 +1,5 @@ using Conditions; +using System; using System.IO; namespace DDD.Serialization @@ -11,10 +12,17 @@ public static class ITextSerializerExtensions public static T DeserializeFromString(this ITextSerializer serializer, string input) { Condition.Requires(serializer, nameof(serializer)).IsNotNull(); + return (T)serializer.DeserializeFromString(input, typeof(T)); + } + + public static object DeserializeFromString(this ITextSerializer serializer, string input, Type type) + { + Condition.Requires(serializer, nameof(serializer)).IsNotNull(); + Condition.Requires(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); } } diff --git a/Src/DDD.Core.Newtonsoft/JsonSerializerWrapper.cs b/Src/DDD.Core.Newtonsoft/JsonSerializerWrapper.cs index bf60341..2e94e30 100644 --- a/Src/DDD.Core.Newtonsoft/JsonSerializerWrapper.cs +++ b/Src/DDD.Core.Newtonsoft/JsonSerializerWrapper.cs @@ -2,6 +2,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; +using System; using System.IO; using System.Text; @@ -57,21 +58,22 @@ public static JsonSerializerWrapper Create(Encoding encoding, bool indent = true public static JsonSerializerWrapper Create(bool indent = true) => Create(JsonSerializationOptions.Encoding, indent); - public T Deserialize(Stream stream) + public object Deserialize(Stream stream, Type type) { Condition.Requires(stream, nameof(stream)).IsNotNull(); + Condition.Requires(type, nameof(type)).IsNotNull(); using (var streamReader = new StreamReader(stream, this.Encoding, true, 1024, true)) using (var jsonReader = new JsonTextReader(streamReader)) { var serializer = JsonSerializer.Create(this.settings); try { - return serializer.Deserialize(jsonReader); + return serializer.Deserialize(jsonReader, type); } catch (JsonException exception) { - throw new SerializationException(typeof(T), exception); + throw new SerializationException(type, exception); } } } diff --git a/Src/DDD.Core/Infrastructure/Serialization/DataContractSerializerWrapper.cs b/Src/DDD.Core/Infrastructure/Serialization/DataContractSerializerWrapper.cs index 0a4a81f..b332040 100644 --- a/Src/DDD.Core/Infrastructure/Serialization/DataContractSerializerWrapper.cs +++ b/Src/DDD.Core/Infrastructure/Serialization/DataContractSerializerWrapper.cs @@ -62,19 +62,20 @@ public static DataContractSerializerWrapper Create(Encoding encoding, bool inden public static DataContractSerializerWrapper 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(); + Condition.Requires(type, nameof(type)).IsNotNull(); using (var reader = XmlReader.Create(stream, this.readerSettings)) { - var serializer = new DataContractSerializer(typeof(T)); + var serializer = new DataContractSerializer(type); try { - return (T)serializer.ReadObject(reader); + return serializer.ReadObject(reader); } catch (RuntimeSerializationException exception) { - throw new SerializationException(typeof(T), exception); + throw new SerializationException(type, exception); } } } diff --git a/Src/DDD.Core/Infrastructure/Serialization/XmlSerializerWrapper.cs b/Src/DDD.Core/Infrastructure/Serialization/XmlSerializerWrapper.cs index 994ceb2..5618c6e 100644 --- a/Src/DDD.Core/Infrastructure/Serialization/XmlSerializerWrapper.cs +++ b/Src/DDD.Core/Infrastructure/Serialization/XmlSerializerWrapper.cs @@ -61,19 +61,20 @@ public static XmlSerializerWrapper Create(Encoding encoding, bool indent = true) 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(); + Condition.Requires(type, nameof(type)).IsNotNull(); using (var reader = XmlReader.Create(stream, this.readerSettings)) { - var serializer = new XmlSerializer(typeof(T)); + var serializer = new XmlSerializer(type); try { - return (T)serializer.Deserialize(reader); + return serializer.Deserialize(reader); } catch (InvalidOperationException exception) { - throw new SerializationException(typeof(T), exception); + throw new SerializationException(type, exception); } } } diff --git a/Test/DDD.Core.Newtonsoft.UnitTests/JsonSerializerWrapperTests.cs b/Test/DDD.Core.Newtonsoft.UnitTests/JsonSerializerWrapperTests.cs index 375d025..2936ea5 100644 --- a/Test/DDD.Core.Newtonsoft.UnitTests/JsonSerializerWrapperTests.cs +++ b/Test/DDD.Core.Newtonsoft.UnitTests/JsonSerializerWrapperTests.cs @@ -36,7 +36,7 @@ public void SerializeAndDeserialize_AreSymmetric() // Act serializer.Serialize(this.stream, obj1); this.stream.Position = 0; - var obj2 = serializer.Deserialize(this.stream); + var obj2 = serializer.Deserialize(this.stream, typeof(FakePerson)); // Assert obj2.Should().BeEquivalentTo(obj1); } diff --git a/Test/DDD.Core.UnitTests/Infrastructure/Serialization/DataContractSerializerWrapperTests.cs b/Test/DDD.Core.UnitTests/Infrastructure/Serialization/DataContractSerializerWrapperTests.cs index 4ec1701..69beff0 100644 --- a/Test/DDD.Core.UnitTests/Infrastructure/Serialization/DataContractSerializerWrapperTests.cs +++ b/Test/DDD.Core.UnitTests/Infrastructure/Serialization/DataContractSerializerWrapperTests.cs @@ -36,7 +36,7 @@ public void SerializeAndDeserialize_AreSymmetric() // Act serializer.Serialize(this.stream, obj1); this.stream.Position = 0; - var obj2 = serializer.Deserialize(this.stream); + var obj2 = serializer.Deserialize(this.stream, typeof(FakePerson)); // Assert obj2.Should().BeEquivalentTo(obj1); } diff --git a/Test/DDD.Core.UnitTests/Infrastructure/Serialization/XmlSerializerWrapperTests.cs b/Test/DDD.Core.UnitTests/Infrastructure/Serialization/XmlSerializerWrapperTests.cs index 3f248d0..ed10f3f 100644 --- a/Test/DDD.Core.UnitTests/Infrastructure/Serialization/XmlSerializerWrapperTests.cs +++ b/Test/DDD.Core.UnitTests/Infrastructure/Serialization/XmlSerializerWrapperTests.cs @@ -36,7 +36,7 @@ public void SerializeAndDeserialize_AreSymmetric() // Act serializer.Serialize(this.stream, obj1); this.stream.Position = 0; - var obj2 = serializer.Deserialize(this.stream); + var obj2 = serializer.Deserialize(this.stream, typeof(FakePerson)); // Assert obj2.Should().BeEquivalentTo(obj1); } From b72859f8e746598dc0ffaa255845ff30be8bb9d6 Mon Sep 17 00:00:00 2001 From: draphyz Date: Fri, 11 Mar 2022 11:31:51 +0100 Subject: [PATCH 061/111] Add QueryInvalidException --- .../Application/QueryInvalidException.cs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 Src/DDD.Core/Application/QueryInvalidException.cs diff --git a/Src/DDD.Core/Application/QueryInvalidException.cs b/Src/DDD.Core/Application/QueryInvalidException.cs new file mode 100644 index 0000000..6e081f0 --- /dev/null +++ b/Src/DDD.Core/Application/QueryInvalidException.cs @@ -0,0 +1,68 @@ +using System; +using System.Linq; + +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 string ToString() + { + var s = $"{this.GetType()}: {this.Message} "; + s += $"{Environment.NewLine}IsTransient: {this.IsTransient}"; + if (this.Query != null) + s += $"{Environment.NewLine}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 + + } +} From 13bc7902f7ec369226a81431bbd02ea85a41f522 Mon Sep 17 00:00:00 2001 From: draphyz Date: Fri, 11 Mar 2022 15:10:20 +0100 Subject: [PATCH 062/111] Refactor ValidationFailure --- .../Validation/ValidationFailure.cs | 45 +++++++++++++++++-- .../FluentValidatorExtensions.cs | 12 ++++- .../ValidationResultTranslator.cs | 18 ++++++-- 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/Src/DDD.Core.Abstractions/Validation/ValidationFailure.cs b/Src/DDD.Core.Abstractions/Validation/ValidationFailure.cs index 8a6207d..49853cd 100644 --- a/Src/DDD.Core.Abstractions/Validation/ValidationFailure.cs +++ b/Src/DDD.Core.Abstractions/Validation/ValidationFailure.cs @@ -1,10 +1,14 @@ using Conditions; +using System.Runtime.Serialization; +using System.Xml.Serialization; namespace DDD.Validation { /// /// Represents a validation failure. /// + [DataContract()] + [XmlType()] public class ValidationFailure { @@ -14,7 +18,8 @@ public ValidationFailure(string message, string code, FailureLevel level = FailureLevel.Warning, string propertyName = null, - object propertyValue = null) + string propertyValue = null, + string category = null) { Condition.Requires(message, nameof(message)).IsNotNullOrWhiteSpace(); Condition.Requires(code, nameof(code)).IsNotNullOrWhiteSpace(); @@ -24,6 +29,8 @@ public ValidationFailure(string message, if (!string.IsNullOrWhiteSpace(propertyName)) this.PropertyName = propertyName; this.PropertyValue = propertyValue; + if (!string.IsNullOrWhiteSpace(category)) + this.Category = category; } /// For serialization @@ -35,15 +42,47 @@ 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; } - public object PropertyValue { get; private set; } + /// + /// The property value that caused the failure. + /// + [DataMember(Name = "propertyValue")] + [XmlElement("propertyValue")] + public string PropertyValue { get; private set; } #endregion Properties @@ -51,7 +90,7 @@ private ValidationFailure() public override string ToString() { - return $"{this.GetType().Name} [message={this.Message}, code={this.Code}, level={this.Level}, propertyName={this.PropertyName}, propertyValue={this.PropertyValue}]"; + return $"{this.GetType().Name} [message={this.Message}, code={this.Code}, level={this.Level}, propertyName={this.PropertyName}, propertyValue={this.PropertyValue}, category={this.Category}]"; } #endregion Methods diff --git a/Src/DDD.Core.FluentValidation/FluentValidatorExtensions.cs b/Src/DDD.Core.FluentValidation/FluentValidatorExtensions.cs index da58341..ab85b2a 100644 --- a/Src/DDD.Core.FluentValidation/FluentValidatorExtensions.cs +++ b/Src/DDD.Core.FluentValidation/FluentValidatorExtensions.cs @@ -1,4 +1,5 @@ -using FluentValidation; +using Conditions; +using FluentValidation; using System.Collections; namespace DDD.Core.Infrastructure.Validation @@ -72,6 +73,15 @@ public static IRuleBuilderOptions Numeric(this IRuleBuilder()); } + /// + /// Specifies a custom category associated with the validation failure when validation fails for this rule. + /// + public static IRuleBuilderOptions WithCategory(this IRuleBuilderOptions rule, string category) + { + Condition.Requires(category, nameof(category)).IsNotNullOrWhiteSpace(); + return rule.WithState(x => $"category={category}"); + } + #endregion Methods } diff --git a/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs b/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs index f2205ba..7739c19 100644 --- a/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs +++ b/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs @@ -14,7 +14,7 @@ internal class ValidationResultTranslator #region Methods - public DDD.Validation.ValidationResult Translate(ValidationResult result, IDictionary options) + public DDD.Validation.ValidationResult Translate(ValidationResult result, IDictionary options = null) { Condition.Requires(result, nameof(result)).IsNotNull(); Condition.Requires(options, nameof(options)) @@ -26,6 +26,16 @@ public DDD.Validation.ValidationResult Translate(ValidationResult result, IDicti 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(); @@ -35,11 +45,11 @@ private static DDD.Validation.ValidationFailure ToFailure(ValidationFailure fail failure.ErrorCode, failure.Severity.ToString().ToEnum(), failure.PropertyName, - failure.AttemptedValue + failure.AttemptedValue?.ToString(), + GetCustomStateInfo(failure.CustomState, "category") ); } #endregion Methods - } -} \ No newline at end of file +} From 3afcbd3329de3218d1f0d2d8162be7f86a0b1388 Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 14 Mar 2022 15:33:32 +0100 Subject: [PATCH 063/111] Returns a Task from PublishAllAsync --- Src/DDD.Core/Application/IEventPublisherExtensions.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Src/DDD.Core/Application/IEventPublisherExtensions.cs b/Src/DDD.Core/Application/IEventPublisherExtensions.cs index de1c0ef..47cc543 100644 --- a/Src/DDD.Core/Application/IEventPublisherExtensions.cs +++ b/Src/DDD.Core/Application/IEventPublisherExtensions.cs @@ -1,6 +1,7 @@ using Conditions; using System.Collections.Generic; using System.Threading; +using System.Threading.Tasks; namespace DDD.Core.Application { @@ -12,6 +13,9 @@ public static class IEventPublisherExtensions #region Methods + /// + /// Publishes all events sequentially to handle them synchronously. + /// public static void PublishAll(this IEventPublisher publisher, IEnumerable events) { Condition.Requires(publisher, nameof(publisher)).IsNotNull(); @@ -22,7 +26,10 @@ public static void PublishAll(this IEventPublisher publisher, IEnumerable events, CancellationToken cancellationToken = default) + /// + /// Publishes all events sequentially to handle them asynchronously. + /// + public async static Task PublishAllAsync(this IEventPublisher publisher, IEnumerable events, CancellationToken cancellationToken = default) { Condition.Requires(publisher, nameof(publisher)).IsNotNull(); Condition.Requires(events, nameof(events)) From 3b14cb5eb484df8c2b981f3aafc62f5ac87e2759 Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 14 Mar 2022 17:14:23 +0100 Subject: [PATCH 064/111] Refactor event publishing --- Src/DDD.Core/Application/EventPublisher.cs | 43 ++++++++++--------- .../Application/IEventPublisherExtensions.cs | 4 +- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/Src/DDD.Core/Application/EventPublisher.cs b/Src/DDD.Core/Application/EventPublisher.cs index a398a35..46c2f97 100644 --- a/Src/DDD.Core/Application/EventPublisher.cs +++ b/Src/DDD.Core/Application/EventPublisher.cs @@ -1,5 +1,6 @@ using Conditions; using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -11,6 +12,7 @@ namespace DDD.Core.Application public class EventPublisher : IEventPublisher { + #region Fields private readonly IServiceProvider serviceProvider; @@ -32,40 +34,41 @@ public EventPublisher(IServiceProvider serviceProvider) public void Publish(TEvent @event) where TEvent : class, IEvent { Condition.Requires(@event, nameof(@event)).IsNotNull(); - if (typeof(TEvent) == typeof(IEvent)) - { - var handlerType = typeof(IEventHandler<>).MakeGenericType(@event.GetType()); - var handlers = this.serviceProvider.GetServices(handlerType).Cast(); - foreach (var handler in handlers) - handler.Handle(@event); - } - else - { - var handlers = this.serviceProvider.GetServices>(); - foreach (var handler in handlers) - handler.Handle(@event); - } + var handlers = this.GetEventHandlers(@event); + foreach (var handler in handlers) + handler.Handle(@event); } public async Task PublishAsync(TEvent @event, CancellationToken cancellationToken = default) where TEvent : class, IEvent { Condition.Requires(@event, nameof(@event)).IsNotNull(); await new SynchronizationContextRemover(); + var handlers = this.GetAsyncEventHandlers(@event); + foreach (var handler in handlers) + await handler.HandleAsync(@event, cancellationToken); + } + + private IEnumerable GetAsyncEventHandlers(TEvent @event) where TEvent : class, IEvent + { if (typeof(TEvent) == typeof(IEvent)) { var handlerType = typeof(IAsyncEventHandler<>).MakeGenericType(@event.GetType()); - var handlers = this.serviceProvider.GetServices(handlerType).Cast(); - foreach (var handler in handlers) - await handler.HandleAsync(@event, cancellationToken); + return this.serviceProvider.GetServices(handlerType).Cast(); } - else + return this.serviceProvider.GetServices>(); + } + + private IEnumerable GetEventHandlers(TEvent @event) where TEvent : class, IEvent + { + if (typeof(TEvent) == typeof(IEvent)) { - var handlers = this.serviceProvider.GetServices>(); - foreach (var handler in handlers) - await handler.HandleAsync(@event, cancellationToken); + var handlerType = typeof(IEventHandler<>).MakeGenericType(@event.GetType()); + return this.serviceProvider.GetServices(handlerType).Cast(); } + return this.serviceProvider.GetServices>(); } #endregion Methods + } } \ No newline at end of file diff --git a/Src/DDD.Core/Application/IEventPublisherExtensions.cs b/Src/DDD.Core/Application/IEventPublisherExtensions.cs index 47cc543..d6bb043 100644 --- a/Src/DDD.Core/Application/IEventPublisherExtensions.cs +++ b/Src/DDD.Core/Application/IEventPublisherExtensions.cs @@ -14,7 +14,7 @@ public static class IEventPublisherExtensions #region Methods /// - /// Publishes all events sequentially to handle them synchronously. + /// Publishes all events synchronously. /// public static void PublishAll(this IEventPublisher publisher, IEnumerable events) { @@ -27,7 +27,7 @@ public static void PublishAll(this IEventPublisher publisher, IEnumerable - /// Publishes all events sequentially to handle them asynchronously. + /// Publishes all events asynchronously and sequentially. /// public async static Task PublishAllAsync(this IEventPublisher publisher, IEnumerable events, CancellationToken cancellationToken = default) { From 8fbe668960c6467137cc0b4f618190c114b34891 Mon Sep 17 00:00:00 2001 From: draphyz Date: Thu, 19 May 2022 16:32:03 +0200 Subject: [PATCH 065/111] Implement universal and local timestamp providers --- .../DelegatingTimestampProvider.cs | 7 +++--- .../LocalTimestampProvider.cs | 16 +++++++++++++ Src/DDD.Core.Abstractions/SystemTime.cs | 18 +++++++-------- .../UniqueTimestampProvider.cs | 23 +++++++------------ .../UniversalTimestampProvider.cs | 18 +++++++++++++++ 5 files changed, 54 insertions(+), 28 deletions(-) create mode 100644 Src/DDD.Core.Abstractions/LocalTimestampProvider.cs create mode 100644 Src/DDD.Core.Abstractions/UniversalTimestampProvider.cs diff --git a/Src/DDD.Core.Abstractions/DelegatingTimestampProvider.cs b/Src/DDD.Core.Abstractions/DelegatingTimestampProvider.cs index 02b1e55..c0831bd 100644 --- a/Src/DDD.Core.Abstractions/DelegatingTimestampProvider.cs +++ b/Src/DDD.Core.Abstractions/DelegatingTimestampProvider.cs @@ -3,6 +3,9 @@ namespace DDD { + /// + /// Provides timestamps based on a delegate. + /// public class DelegatingTimestampProvider : ITimestampProvider { @@ -24,10 +27,6 @@ public DelegatingTimestampProvider(Func timestamp) #region Methods - public static ITimestampProvider CreateLocal() => new DelegatingTimestampProvider(() => DateTime.Now); - - public static ITimestampProvider CreateUniversal() => new DelegatingTimestampProvider(() => DateTime.UtcNow); - public DateTime GetTimestamp() => this.timestamp(); #endregion Methods 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/SystemTime.cs b/Src/DDD.Core.Abstractions/SystemTime.cs index 53396c4..b977ca6 100644 --- a/Src/DDD.Core.Abstractions/SystemTime.cs +++ b/Src/DDD.Core.Abstractions/SystemTime.cs @@ -4,7 +4,7 @@ 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 unit tests. + /// It allows changing and resetting the current time in tests. /// public static class SystemTime { @@ -28,21 +28,21 @@ static SystemTime() #region Methods /// - /// Gets the current date and time on this computer. + /// 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 (for unit testing). + /// Resets the default timestamp providers based on the current date and time on this computer (for testing). /// public static void Reset() { - localProvider = DelegatingTimestampProvider.CreateLocal(); - universalProvider = DelegatingTimestampProvider.CreateUniversal(); + localProvider = new LocalTimestampProvider(); + universalProvider = new UniversalTimestampProvider(); } /// - /// Replaces the default local timestamp provider (for unit testing). + /// Replaces the default local timestamp provider (for testing). /// public static void SetLocalProvider(ITimestampProvider provider) { @@ -50,7 +50,7 @@ public static void SetLocalProvider(ITimestampProvider provider) } /// - /// Replaces the default local timestamp provider (for unit testing). + /// Replaces the default local timestamp provider (for testing). /// public static void SetLocalProvider(Func timestamp) { @@ -58,7 +58,7 @@ public static void SetLocalProvider(Func timestamp) } /// - /// Replaces the default universal timestamp provider (for unit testing). + /// Replaces the default universal timestamp provider (for testing). /// public static void SetUniversalProvider(ITimestampProvider provider) { @@ -66,7 +66,7 @@ public static void SetUniversalProvider(ITimestampProvider provider) } /// - /// Replaces the default universal timestamp provider (for unit testing). + /// Replaces the default universal timestamp provider (for testing). /// public static void SetUniversalProvider(Func timestamp) { diff --git a/Src/DDD.Core.Abstractions/UniqueTimestampProvider.cs b/Src/DDD.Core.Abstractions/UniqueTimestampProvider.cs index 0aab5d8..91a33d9 100644 --- a/Src/DDD.Core.Abstractions/UniqueTimestampProvider.cs +++ b/Src/DDD.Core.Abstractions/UniqueTimestampProvider.cs @@ -1,9 +1,10 @@ using System; +using Conditions; namespace DDD { /// - /// Provides unique timestamps based on the current date and time on this computer with a specified resolution. + /// Provides unique timestamps with a specified resolution. /// public class UniqueTimestampProvider : ITimestampProvider { @@ -11,32 +12,24 @@ public class UniqueTimestampProvider : ITimestampProvider #region Fields private readonly object locker = new object(); - private readonly Func timestamp; + private readonly ITimestampProvider provider; private DateTime lastTimestamp = DateTime.MinValue; #endregion Fields #region Constructors - public UniqueTimestampProvider(bool isLocal, TimeSpan resolution) + public UniqueTimestampProvider(ITimestampProvider provider, TimeSpan resolution) { - this.IsLocal = isLocal; + Condition.Requires(provider, nameof(provider)).IsNotNull(); + this.provider = provider; this.Resolution = resolution; - if (isLocal) - this.timestamp = () => DateTime.Now; - else - this.timestamp = () => DateTime.UtcNow; } #endregion Constructors #region Properties - /// - /// Indicates whether the provided timestamps are local or universal. - /// - public bool IsLocal { get; } - /// /// Indicates the resolution of the provided timestamps. /// @@ -47,11 +40,11 @@ public UniqueTimestampProvider(bool isLocal, TimeSpan resolution) #region Methods /// - /// Provides a unique timestamp on this computer. + /// Provides a unique timestamp. /// public DateTime GetTimestamp() { - var timestamp = this.timestamp(); + var timestamp = this.provider.GetTimestamp(); lock (locker) { if ((timestamp - lastTimestamp) < Resolution) 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 From 34cf02fc6e48eefbdf2dbd3e4dedff61fe27a484 Mon Sep 17 00:00:00 2001 From: draphyz Date: Wed, 1 Feb 2023 13:53:47 +0100 Subject: [PATCH 066/111] Refactoring --- DDD.sln | 32 +- .../DDD.Common.Messages.csproj | 2 +- .../Properties/AssemblyInfo.cs | 2 +- .../DDD.Common.NHibernate.csproj | 2 +- Src/DDD.Common/DDD.Common.csproj | 4 +- Src/DDD.Common/Domain/Alpha2CountryCode.cs | 7 +- Src/DDD.Common/Domain/Alpha2LanguageCode.cs | 7 +- .../Domain/BelgianSocialSecurityNumber.cs | 9 +- Src/DDD.Common/Domain/BinaryContent.cs | 4 +- Src/DDD.Common/Domain/EmailAddress.cs | 16 +- Src/DDD.Common/Domain/Enumeration.cs | 45 +- Src/DDD.Common/Domain/FlagsEnumeration.cs | 174 +++++ Src/DDD.Common/Domain/FullName.cs | 10 +- Src/DDD.Common/Domain/IdentificationCode.cs | 4 +- Src/DDD.Common/Domain/IdentificationNumber.cs | 4 +- Src/DDD.Common/Domain/PostalAddress.cs | 6 +- .../Collections/ICollectionExtensions.cs | 6 +- .../Collections/IDictionaryExtensions.cs | 31 +- .../Collections/IEnumerableExtensions.cs | 15 +- .../IStructuralComparableExtensions.cs | 24 + .../IStructuralEquatableExtensions.cs | 22 + .../Collections/KeyEqualityComparer.cs | 4 +- .../DDD.Core.Abstractions.csproj | 4 +- .../Data/DbConnectionExtensions.cs | 34 + .../Data/DbConnectionHelper.cs | 27 + .../DateTimeExtensions.cs | 24 +- .../DelegatingTimestampProvider.cs | 6 +- .../ExceptionExtensions.cs | 61 ++ .../IServiceProviderExtensions.cs | 10 +- .../Linq/IEnumerableExtensions.cs | 51 +- .../Mapping/CompositeTranslator.cs | 62 ++ .../Mapping/DelegatingTranslator.cs | 21 +- .../Mapping/IMappingProcessor.cs | 6 +- .../Mapping/IMappingProcessorExtensions.cs | 44 +- .../Mapping/IObjectMapper.cs | 4 +- .../Mapping/IObjectMapperExtensions.cs | 8 +- .../Mapping/IObjectTranslator.cs | 22 +- .../Mapping/IObjectTranslatorExtensions.cs | 20 +- .../Mapping/IObjectTranslator`1.cs | 18 + .../Mapping/MappingException.cs | 42 +- .../Mapping/MappingProcessor.cs | 16 +- .../Mapping/ObjectTranslator.cs | 29 + .../Serialization/ISerializer.cs | 1 + .../Serialization/ISerializerExtensions.cs | 20 +- .../ITextSerializerExtensions.cs | 10 +- .../Serialization/JsonSerializationOptions.cs | 2 +- .../StringArgExtensions.cs | 56 ++ Src/DDD.Core.Abstractions/StringExtensions.cs | 53 +- .../StringParamExtensions.cs | 22 + .../TimestampedException.cs | 61 ++ Src/DDD.Core.Abstractions/TypeExtensions.cs | 18 + .../UniqueTimestampProvider.cs | 4 +- .../Validation/IAsyncObjectValidator.cs | 3 + .../Validation/IObjectValidator.cs | 14 +- .../Validation/ISyncObjectValidator.cs | 19 + .../Validation/ValidationFailure.cs | 10 +- .../Validation/ValidationResult.cs | 6 +- Src/DDD.Core.Cronos/CronosSchedule.cs | 46 ++ Src/DDD.Core.Cronos/CronosScheduleFactory.cs | 31 + Src/DDD.Core.Cronos/DDD.Core.Cronos.csproj | 17 + .../Properties/AssemblyInfo.cs | 7 + .../DDD.Core.Dapper.Oracle.csproj | 15 + .../OracleGuidTypeHandler.cs | 32 + .../Properties/AssemblyInfo.cs | 7 + Src/DDD.Core.Dapper/DDD.Core.Dapper.csproj | 33 +- .../EventStreamPositionUpdater.cs | 118 ++++ Src/DDD.Core.Dapper/EventStreamReader.cs | 121 ++++ Src/DDD.Core.Dapper/EventStreamSubcriber.cs | 122 ++++ Src/DDD.Core.Dapper/EventStreamsFinder.cs | 92 +++ .../FailedEventStreamExcluder.cs | 132 ++++ .../FailedEventStreamIncluder.cs | 104 +++ .../FailedEventStreamReader.cs | 119 ++++ .../FailedEventStreamUpdater.cs | 130 ++++ .../FailedEventStreamsFinder.cs | 94 +++ .../FailedRecurringCommandUpdater.cs | 124 ++++ .../IDbConnectionExtensions.cs | 104 ++- .../IncrementalDelaysTypeMapper.cs | 48 ++ Src/DDD.Core.Dapper/OracleScripts.Designer.cs | 111 ++++ Src/DDD.Core.Dapper/OracleScripts.resx | 127 ++++ .../Properties/AssemblyInfo.cs | 2 +- .../RecurringCommandIdGenerator.cs | 59 ++ .../RecurringCommandRegister.cs | 176 +++++ .../RecurringCommandsFinder.cs | 94 +++ .../Scripts/ExcludeFailedEventStream.sql | Bin 0 -> 1960 bytes .../Scripts/FindEventStreams.sql | Bin 0 -> 316 bytes .../Scripts/FindFailedEventStreams.sql | Bin 0 -> 628 bytes .../Scripts/FindRecurringCommandByType.sql | Bin 0 -> 298 bytes .../Scripts/FindRecurringCommands.sql | Bin 0 -> 270 bytes .../Scripts/IncludeFailedEventStream.sql | Bin 0 -> 220 bytes .../Scripts/InsertRecurringCommand.sql | Bin 0 -> 588 bytes .../Scripts/Oracle/ReadEventStream.sql | Bin 0 -> 800 bytes .../Scripts/Oracle/ReadFailedEventStream.sql | Bin 0 -> 808 bytes .../Scripts/ReadEventStream.sql | Bin 0 -> 630 bytes .../Scripts/ReadFailedEventStream.sql | Bin 0 -> 632 bytes .../Scripts/SubscribeToEventStream.sql | Bin 0 -> 630 bytes .../Scripts/UpdateEventStreamPosition.sql | Bin 0 -> 174 bytes .../Scripts/UpdateFailedEventStream.sql | Bin 0 -> 1274 bytes .../Scripts/UpdateRecurringCommand.sql | Bin 0 -> 286 bytes .../Scripts/UpdateRecurringCommandStatus.sql | Bin 0 -> 366 bytes Src/DDD.Core.Dapper/SqlScripts.Designer.cs | 316 +++++++++ Src/DDD.Core.Dapper/SqlScripts.resx | 163 +++++ .../SuccessfulRecurringCommandUpdater.cs | 124 ++++ .../AsyncFluentCommandValidatorAdapter.cs | 20 + .../AsyncFluentQueryValidatorAdapter.cs | 18 + ...pter.cs => AsyncFluentValidatorAdapter.cs} | 38 +- .../DDD.Core.FluentValidation.csproj | 10 +- .../FluentQueryValidatorAdapter.cs | 18 - .../FluentValidatorExtensions.cs | 5 +- .../Properties/AssemblyInfo.cs | 2 +- .../SyncFluentCommandValidatorAdapter.cs | 20 + ....cs => SyncFluentQueryValidatorAdapter.cs} | 8 +- .../SyncFluentValidatorAdapter.cs | 51 ++ .../ValidationResultTranslator.cs | 18 +- .../Validators/CountValidator.cs | 6 +- .../Validators/ExactCountValidator.cs | 2 +- .../Validators/MaximumCountValidator.cs | 2 +- .../Application/CommandExecutionStatus.cs | 8 + Src/DDD.Core.Messages/Application/Event.cs | 36 ++ .../Application/EventStream.cs | 33 + .../Application/ExcludeFailedEventStream.cs | 58 ++ .../Application/FailedEventStream.cs | 41 ++ .../Application/FindEventStreams.cs | 8 + .../Application/FindFailedEventStreams.cs | 8 + .../Application/FindRecurringCommands.cs | 8 + .../Application/GenerateRecurringCommandId.cs | 11 + .../Application/IncludeFailedEventStream.cs | 27 + .../Application/IncrementalDelay.cs | 15 + .../MarkRecurringCommandAsFailed.cs | 24 + .../MarkRecurringCommandAsSuccessful.cs | 24 + .../Application/ReadEventStream.cs | 26 + .../Application/ReadFailedEventStream.cs | 27 + .../Application/RecurringCommand.cs | 29 + .../Application/RecurringCommandDetail.cs | 35 + .../Application/RegisterRecurringCommand.cs | 28 + .../Application/SubscribeToEventStream.cs | 33 + .../Application/UpdateEventStreamPosition.cs | 26 + .../Application/UpdateFailedEventStream.cs | 46 ++ .../Application/WriteEvents.cs | 15 + .../DDD.Core.Messages.csproj | 3 +- .../Domain/BoundedContext.cs | 76 +++ Src/DDD.Core.Messages/Domain/IEvent.cs | 2 + .../Properties/AssemblyInfo.cs | 2 +- .../DDD.Core.NHibernate.csproj | 4 +- .../DelegatingSessionFactory.cs | 98 +++ ...{StoredEventMapping.cs => EventMapping.cs} | 12 +- Src/DDD.Core.NHibernate/ISessionFactory.cs | 35 + Src/DDD.Core.NHibernate/NHRepository.cs | 177 +++++ ....cs => NHRepositoryExceptionTranslator.cs} | 25 +- .../NHibernateRepository.cs | 90 --- ...dEventMapping.cs => OracleEventMapping.cs} | 5 +- ...entMapping.cs => SqlServerEventMapping.cs} | 4 +- .../DDD.Core.NSubstitute.csproj | 14 + .../Properties/AssemblyInfo.cs | 7 + .../SubstituteExtensions.cs | 40 ++ .../DDD.Core.Newtonsoft.csproj | 6 +- .../JsonSerializerWrapper.cs | 16 +- .../Properties/AssemblyInfo.cs | 2 +- .../AsyncPollyCommandHandler.cs | 13 +- .../AsyncPollyCommandHandler`1.cs | 53 ++ Src/DDD.Core.Polly/AsyncPollyQueryHandler.cs | 11 +- .../AsyncPollyQueryHandler`1.cs | 53 ++ Src/DDD.Core.Polly/DDD.Core.Polly.csproj | 6 +- Src/DDD.Core.Polly/PollyCommandHandler.cs | 46 -- Src/DDD.Core.Polly/Properties/AssemblyInfo.cs | 2 +- Src/DDD.Core.Polly/SyncPollyCommandHandler.cs | 44 ++ .../SyncPollyCommandHandler`1.cs | 52 ++ ...eryHandler.cs => SyncPollyQueryHandler.cs} | 18 +- Src/DDD.Core.Polly/SyncPollyQueryHandler`1.cs | 52 ++ .../AsyncScopedCommandHandler.cs | 15 +- .../AsyncScopedCommandHandler`1.cs | 63 ++ .../AsyncScopedQueryHandler.cs | 15 +- .../AsyncScopedQueryHandler`1.cs | 63 ++ .../ContainerExtensions.cs | 174 ++++- .../DDD.Core.SimpleInjector.csproj | 6 +- .../KeyedServiceProvider.cs | 12 +- .../Properties/AssemblyInfo.cs | 2 +- .../ThreadScopedCommandHandler.cs | 18 +- .../ThreadScopedCommandHandler`1.cs | 60 ++ .../ThreadScopedQueryHandler.cs | 18 +- .../ThreadScopedQueryHandler`1.cs | 60 ++ Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj | 13 +- Src/DDD.Core.Xunit/DbFixture.cs | 77 ++- Src/DDD.Core.Xunit/IDbFixture.cs | 18 +- Src/DDD.Core.Xunit/IDbFixtureExtensions.cs | 39 ++ .../Application/ApplicationException.cs | 29 +- .../AsyncCommandHandlerWithLogging.cs | 19 +- .../AsyncCommandHandlerWithLogging`1.cs | 61 ++ .../Application/AsyncDomainCommandHandler.cs | 45 -- Src/DDD.Core/Application/AsyncEventHandler.cs | 9 +- .../AsyncEventHandlerWithLogging.cs | 64 ++ .../AsyncQueryHandlerWithLogging.cs | 19 +- .../AsyncQueryHandlerWithLogging`1.cs | 62 ++ Src/DDD.Core/Application/CommandException.cs | 17 +- .../Application/CommandHandlerWithLogging.cs | 55 -- .../Application/CommandInvalidException.cs | 13 + Src/DDD.Core/Application/CommandProcessor.cs | 49 +- .../CompositeTranslatorExtensions.cs | 39 ++ .../Application/ContextualCommandProcessor.cs | 64 ++ .../Application/ContextualQueryProcessor.cs | 67 ++ .../Application/DomainCommandHandler.cs | 43 -- .../DomainToCommandExceptionTranslator.cs | 27 +- Src/DDD.Core/Application/EventConsumer.cs | 383 +++++++++++ .../Application/EventConsumerSettings.cs | 53 ++ Src/DDD.Core/Application/EventHandler.cs | 27 - Src/DDD.Core/Application/EventPublisher.cs | 34 +- Src/DDD.Core/Application/EventTranslator.cs | 62 ++ .../FailedEventStreamSummaryExtensions.cs | 65 ++ .../Application/IAsyncCommandHandler.cs | 8 +- .../IAsyncCommandHandlerExtensions.cs | 45 ++ .../Application/IAsyncCommandHandler`1.cs | 23 + .../Application/IAsyncEventHandler.cs | 6 +- .../Application/IAsyncEventHandler`1.cs | 5 +- .../Application/IAsyncQueryHandler.cs | 8 +- .../IAsyncQueryHandlerExtensions.cs | 24 + .../Application/IAsyncQueryHandler`1.cs | 26 + Src/DDD.Core/Application/ICommandHandler.cs | 11 +- Src/DDD.Core/Application/ICommandHandler`1.cs | 13 + Src/DDD.Core/Application/ICommandProcessor.cs | 33 +- .../ICommandProcessorExtensions.cs | 65 ++ Src/DDD.Core/Application/ICommandValidator.cs | 6 +- .../IContextualCommandProcessor.cs | 37 ++ .../IContextualCommandProcessor`1.cs | 25 + .../Application/IContextualQueryProcessor.cs | 39 ++ .../IContextualQueryProcessor`1.cs | 25 + Src/DDD.Core/Application/IEventConsumer.cs | 45 ++ Src/DDD.Core/Application/IEventConsumer`1.cs | 14 + Src/DDD.Core/Application/IEventHandler`1.cs | 17 - Src/DDD.Core/Application/IEventPublisher.cs | 12 +- .../Application/IEventPublisherExtensions.cs | 46 -- Src/DDD.Core/Application/IMessageContext.cs | 11 + .../Application/IMessageContextExtensions.cs | 73 +++ Src/DDD.Core/Application/IQueryHandler.cs | 11 +- Src/DDD.Core/Application/IQueryHandler`1.cs | 16 + Src/DDD.Core/Application/IQueryProcessor.cs | 32 +- .../Application/IQueryProcessorExtensions.cs | 38 ++ Src/DDD.Core/Application/IQueryValidator.cs | 8 +- .../Application/IRecurringCommandManager.cs | 48 ++ .../Application/IRecurringCommandManager`1.cs | 17 + .../Application/IRecurringSchedule.cs | 21 + .../Application/IRecurringScheduleFactory.cs | 23 + .../Application/ISyncCommandHandler.cs | 20 + .../ISyncCommandHandlerExtensions.cs | 25 + .../Application/ISyncCommandHandler`1.cs | 23 + .../Application/ISyncCommandValidator.cs | 12 + ...{IEventHandler.cs => ISyncEventHandler.cs} | 7 +- .../Application/ISyncEventHandler`1.cs | 19 + Src/DDD.Core/Application/ISyncQueryHandler.cs | 20 + .../ISyncQueryHandlerExtensions.cs | 23 + .../Application/ISyncQueryHandler`1.cs | 26 + .../Application/ISyncQueryValidator.cs | 12 + Src/DDD.Core/Application/MessageContext.cs | 52 ++ .../Application/MessageContextInfo.cs | 17 + .../Application/QueryConflictException.cs | 37 ++ Src/DDD.Core/Application/QueryException.cs | 9 + .../Application/QueryHandlerWithLogging.cs | 49 -- .../Application/QueryInvalidException.cs | 12 + Src/DDD.Core/Application/QueryProcessor.cs | 45 +- .../Application/RecurringCommandManager.cs | 263 ++++++++ .../RecurringCommandManagerSettings.cs | 43 ++ .../SyncCommandHandlerWithLogging.cs | 53 ++ .../SyncCommandHandlerWithLogging`1.cs | 60 ++ Src/DDD.Core/Application/SyncEventHandler.cs | 29 + .../SyncEventHandlerWithLogging.cs | 63 ++ .../SyncQueryHandlerWithLogging.cs | 47 ++ .../SyncQueryHandlerWithLogging`1.cs | 61 ++ Src/DDD.Core/DDD.Core.csproj | 7 +- .../Domain/CompositeTranslatorExtensions.cs | 39 ++ Src/DDD.Core/Domain/DomainEntity.cs | 29 +- Src/DDD.Core/Domain/DomainException.cs | 28 +- Src/DDD.Core/Domain/DomainServiceException.cs | 9 + .../Domain/DomainServiceInvalidException.cs | 12 + Src/DDD.Core/Domain/EntityState.cs | 10 + Src/DDD.Core/Domain/IAsyncRepository.cs | 8 +- Src/DDD.Core/Domain/IRepository.cs | 12 +- Src/DDD.Core/Domain/IStateEntity.cs | 14 + .../Domain/IStateObjectConvertible.cs | 14 + Src/DDD.Core/Domain/ISyncRepository.cs | 20 + Src/DDD.Core/Domain/RepositoryException.cs | 9 + Src/DDD.Core/Domain/ValueObject.cs | 7 +- .../Infrastructure/Data/DbCommandHander.cs | 69 -- .../Data/DbConnectionFactory.cs | 44 -- .../Infrastructure/Data/DbQueryHandler.cs | 74 --- .../Infrastructure/Data/DbScriptSplitter.cs | 10 +- .../Data/DbToCommandExceptionTranslator.cs | 24 +- .../Data/DbToQueryExceptionTranslator.cs | 26 +- .../Data/DbToRepositoryExceptionTranslator.cs | 28 +- .../Infrastructure/Data/GuidGenerator.cs | 69 ++ .../Data/IDbConnectionExtensions.cs | 38 +- .../Data/IDbConnectionFactory.cs | 16 - .../Data/IDbConnectionFactoryExtensions.cs | 35 - .../Data/IDbConnectionProvider.cs | 26 + .../Data/IDbConnectionProviderExtensions.cs | 63 ++ .../Data/IDbConnectionProvider`1.cs | 20 + .../Infrastructure/Data/IValueGenerator.cs | 18 + .../Data/LazyDbConnectionProvider.cs | 77 +++ .../Data/Oracle11Expressions.cs | 4 +- .../Infrastructure/Data/OracleErrorHelper.cs | 50 +- .../OracleToCommandExceptionTranslator.cs | 21 +- .../Data/OracleToQueryExceptionTranslator.cs | 21 +- .../OracleToRepositoryExceptionTranslator.cs | 23 +- .../Data/RandomGuidGenerator.cs | 15 + .../Data/SequentialBinaryGuidGenerator.cs | 82 +++ .../Data/SequentialSqlServerGuidGenerator.cs | 36 ++ .../Data/SequentialStringGuidGenerator.cs | 29 + .../Data/SqlServer2012Expressions.cs | 4 +- .../Data/SqlServerErrorHelper.cs | 18 +- .../SqlServerToCommandExceptionTranslator.cs | 18 +- .../SqlServerToQueryExceptionTranslator.cs | 18 +- ...qlServerToRepositoryExceptionTranslator.cs | 20 +- .../Infrastructure/Data/StoredEvent.cs | 32 - .../Data/StoredEventTranslator.cs | 62 -- .../DataContractSerializerWrapper.cs | 16 +- .../Serialization/XmlSerializerWrapper.cs | 16 +- .../CreatePharmaceuticalPrescription.cs | 2 +- .../DDD.HealthcareDelivery.Messages.csproj | 4 +- .../Domain/HealthcareDeliveryContext.cs | 15 + .../PharmaceuticalPrescriptionCreated.cs | 13 +- .../PharmaceuticalPrescriptionRevoked.cs | 16 +- ...ianPharmaceuticalPrescriptionTranslator.cs | 12 +- .../PharmaceuticalPrescriptionCreator.cs | 64 +- .../PharmaceuticalPrescriptionRevoker.cs | 61 +- .../DDD.HealthcareDelivery.csproj | 9 +- .../Domain/Encounters/EncounterIdentifier.cs | 4 +- .../Domain/Patients/Patient.cs | 8 +- ...gianHealthcarePractitionerLicenseNumber.cs | 9 +- .../Practitioners/HealthcarePractitioner.cs | 8 +- .../Prescriptions/BelgianMedicationCode.cs | 12 +- .../PharmaceuticalPrescription.cs | 11 +- .../Prescriptions/PrescribedMedication.cs | 8 +- .../Domain/Prescriptions/Prescription.cs | 17 +- .../Prescriptions/PrescriptionIdentifier.cs | 4 +- ...anOracleHealthcareDeliveryConfiguration.cs | 8 - ...qlServerHealthcareDeliveryConfiguration.cs | 8 - .../HealthcareDeliveryConfiguration.cs | 10 +- .../IHealthcareDeliveryConnectionFactory.cs | 11 - .../OracleHealthcareDeliveryConfiguration.cs | 13 +- .../PharmaceuticalPrescriptionRepository.cs | 27 + ...rmaceuticalPrescriptionsByPatientFinder.cs | 87 ++- ...escribedMedicationsByPrescriptionFinder.cs | 78 ++- .../PrescriptionIdentifierGenerator.cs | 59 +- ...qlServerHealthcareDeliveryConfiguration.cs | 19 +- .../DDD.Common.UnitTests.csproj | 10 +- .../Domain/EnumerationTests.cs | 15 +- .../Domain/FakeFlagsEnumeration.cs | 36 ++ .../Domain/FlagsEnumerationTests.cs | 97 +++ .../Properties/AssemblyInfo.cs | 25 + .../DDD.Core.Abstractions.UnitTests.csproj | 14 +- .../Linq/IEnumerableExtensionsTests.cs | 47 ++ .../Mapping/FakeObject3.cs | 6 + .../IMappingProcessorExtensionsTests.cs | 81 +++ .../Mapping/MappingProcessorTests.cs | 13 +- .../Properties/AssemblyInfo.cs | 25 + .../StringExtensionsTests.cs | 155 ++--- .../App.config | 22 + .../DDD.Core.Dapper.IntegrationTests.csproj | 55 ++ .../EventStreamPositionUpdaterTests.cs | 78 +++ .../EventStreamReaderTests.cs | 197 ++++++ .../EventStreamSubscriberTests.cs | 80 +++ .../EventStreamsFinderTests.cs | 100 +++ .../FailedEventStreamExcluderTests.cs | 97 +++ .../FailedEventStreamIncluderTests.cs | 78 +++ .../FailedEventStreamReaderTests.cs | 37 ++ .../FailedEventStreamUpdaterTests.cs | 95 +++ .../FailedEventStreamsFinderTests.cs | 92 +++ .../FailedRecurringCommandUpdaterTests.cs | 116 ++++ .../IDbConnectionExtensionsTests.cs | 88 +++ .../IPersistenceFixture.cs | 18 + .../OracleCollection.cs | 9 + .../OracleEventStreamPositionUpdaterTests.cs | 17 + .../OracleEventStreamReaderTests.cs | 80 +++ .../OracleEventStreamSubscriberTests.cs | 17 + .../OracleEventStreamsFinderTests.cs | 17 + .../OracleFailedEventStreamExcluderTests.cs | 17 + .../OracleFailedEventStreamIncluderTests.cs | 17 + .../OracleFailedEventStreamReaderTests.cs | 104 +++ .../OracleFailedEventStreamUpdaterTests.cs | 17 + .../OracleFailedEventStreamsFinderTests.cs | 17 + ...racleFailedRecurringCommandUpdaterTests.cs | 18 + .../OracleFixture.cs | 69 ++ .../OracleIDbConnectionExtensionsTests.cs | 17 + .../OracleRecurringCommandRegisterTests.cs | 18 + .../OracleRecurringCommandsFinderTests.cs | 45 ++ .../OracleScripts.Designer.cs | 318 +++++++++ .../OracleScripts.resx | 172 +++++ ...eSuccessfulRecurringCommandUpdaterTests.cs | 18 + .../Properties/AssemblyInfo.cs | 19 + .../RecurringCommandRegisterTests.cs | 104 +++ .../RecurringCommandsFinderTests.cs | 72 +++ .../Scripts/Oracle/CreateSchema.sql | 10 + .../Oracle/ExcludeFailedEventStream.sql | Bin 0 -> 70 bytes .../Scripts/Oracle/FillSchema.sql | 102 +++ .../Scripts/Oracle/FindEventStreams.sql | Bin 0 -> 886 bytes .../Scripts/Oracle/FindFailedEventStreams.sql | Bin 0 -> 1738 bytes .../Scripts/Oracle/FindRecurringCommands.sql | Bin 0 -> 1510 bytes .../Oracle/IncludeFailedEventStream.sql | Bin 0 -> 1738 bytes .../Oracle/MarkRecurringCommandAsFailed.sql | Bin 0 -> 1510 bytes .../MarkRecurringCommandAsSuccessful.sql | Bin 0 -> 1734 bytes .../Scripts/Oracle/NextId_ExistingRows.sql | Bin 0 -> 422 bytes .../Scripts/Oracle/NextId_NoRow.sql | Bin 0 -> 70 bytes .../Scripts/Oracle/ReadEventStream.sql | Bin 0 -> 13748 bytes .../Scripts/Oracle/ReadFailedEventStream.sql | Bin 0 -> 13748 bytes .../Oracle/RegisterRecurringCommand.sql | Bin 0 -> 70 bytes .../Scripts/Oracle/SubscribeToEventStream.sql | Bin 0 -> 70 bytes .../Oracle/UpdateEventStreamPosition.sql | Bin 0 -> 942 bytes .../Oracle/UpdateFailedEventStream.sql | Bin 0 -> 1738 bytes .../Scripts/SqlServer/CreateDatabase.sql | Bin 0 -> 15302 bytes .../SqlServer/ExcludeFailedEventStream.sql | Bin 0 -> 82 bytes .../Scripts/SqlServer/FindEventStreams.sql | Bin 0 -> 926 bytes .../SqlServer/FindFailedEventStreams.sql | Bin 0 -> 1940 bytes .../SqlServer/FindRecurringCommands.sql | Bin 0 -> 1618 bytes .../SqlServer/IncludeFailedEventStream.sql | Bin 0 -> 1940 bytes .../MarkRecurringCommandAsFailed.sql | Bin 0 -> 1618 bytes .../MarkRecurringCommandAsSuccessful.sql | Bin 0 -> 1812 bytes .../Scripts/SqlServer/NextId_ExistingRows.sql | Bin 0 -> 442 bytes .../Scripts/SqlServer/NextId_NoRow.sql | Bin 0 -> 124 bytes .../Scripts/SqlServer/ReadEventStream.sql | Bin 0 -> 13600 bytes .../SqlServer/ReadFailedEventStream.sql | Bin 0 -> 13600 bytes .../SqlServer/RegisterRecurringCommand.sql | Bin 0 -> 82 bytes .../SqlServer/SubscribeToEventStream.sql | Bin 0 -> 82 bytes .../SqlServer/UpdateEventStreamPosition.sql | Bin 0 -> 968 bytes .../SqlServer/UpdateFailedEventStream.sql | Bin 0 -> 1940 bytes .../SqlServerCollection.cs | 9 + ...qlServerEventStreamPositionUpdaterTests.cs | 17 + .../SqlServerEventStreamReaderTests.cs | 80 +++ .../SqlServerEventStreamSubscriberTests.cs | 17 + .../SqlServerEventStreamsFinderTests.cs | 18 + ...SqlServerFailedEventStreamExcluderTests.cs | 17 + ...SqlServerFailedEventStreamIncluderTests.cs | 17 + .../SqlServerFailedEventStreamReaderTests.cs | 104 +++ .../SqlServerFailedEventStreamUpdaterTests.cs | 17 + .../SqlServerFailedEventStreamsFinderTests.cs | 18 + ...erverFailedRecurringCommandUpdaterTests.cs | 18 + .../SqlServerFixture.cs | 67 ++ .../SqlServerIDbConnectionExtensionsTests.cs | 17 + .../SqlServerRecurringCommandRegisterTests.cs | 18 + .../SqlServerRecurringCommandsFinderTests.cs | 45 ++ .../SqlServerScripts.Designer.cs | 303 +++++++++ .../SqlServerScripts.resx | 169 +++++ ...rSuccessfulRecurringCommandUpdaterTests.cs | 18 + .../SuccessfulRecurringCommandUpdaterTests.cs | 115 ++++ .../TestContext.cs | 16 + .../testhost.dll.config | 12 + .../DDD.Core.Newtonsoft.UnitTests.csproj | 13 +- .../Application/CommandProcessorTests.cs | 24 +- .../Application/EventConsumerTests.cs | 603 ++++++++++++++++++ .../Application/EventPublisherTests.cs | 123 +--- .../Application/FakeSourceContext.cs | 15 + .../Application/QueryProcessorTests.cs | 24 +- .../RecurringCommandManagerTests.cs | 553 ++++++++++++++++ .../DDD.Core.UnitTests.csproj | 16 +- Test/DDD.Core.UnitTests/Domain/FakeContext.cs | 15 + Test/DDD.Core.UnitTests/Domain/FakeEvent1.cs | 13 +- Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs | 2 +- .../SequentialBinaryGuidGeneratorTests.cs | 31 + .../SequentialSqlServerGuidGeneratorTests.cs | 29 + .../SequentialStringGuidGeneratorTests.cs | 28 + .../Properties/AssemblyInfo.cs | 25 + .../App.config | 7 +- .../PharmaceuticalPrescriptionCreatorTests.cs | 46 +- .../PharmaceuticalPrescriptionRevokerTests.cs | 53 +- ...HealthcareDelivery.IntegrationTests.csproj | 27 +- ...cleHealthcareDeliveryConfigurationTests.cs | 12 +- ...verHealthcareDeliveryConfigurationTests.cs | 12 +- .../HealthcareDeliveryConfigurationTests.cs | 24 +- .../Infrastructure/IPersistenceFixture.cs | 16 +- .../Infrastructure/OracleConnectionFactory.cs | 38 -- .../Infrastructure/OracleFixture.cs | 64 +- ...uticalPrescriptionsByPatientFinderTests.cs | 34 +- ...bedMedicationsByPrescriptionFinderTests.cs | 33 +- .../SqlServerConnectionFactory.cs | 37 -- .../Infrastructure/SqlServerFixture.cs | 56 +- .../OracleScripts.Designer.cs | 14 +- .../OracleScripts.resx | 3 + .../CreatePharmaceuticalPrescription.sql | 4 + .../Scripts/Oracle/FillSchema.sql | 74 +-- .../Scripts/SqlServer/CreateDatabase.sql | Bin 16340 -> 16280 bytes .../CreatePharmaceuticalPrescription.sql | 4 + .../SqlServerScripts.resx | 3 + .../testhost.dll.config | 12 + .../DDD.HealthcareDelivery.UnitTests.csproj | 15 +- ...harmaceuticalPrescriptionValidatorTests.cs | 3 +- .../Properties/AssemblyInfo.cs | 25 + 482 files changed, 15234 insertions(+), 2391 deletions(-) create mode 100644 Src/DDD.Common/Domain/FlagsEnumeration.cs create mode 100644 Src/DDD.Core.Abstractions/Collections/IStructuralComparableExtensions.cs create mode 100644 Src/DDD.Core.Abstractions/Collections/IStructuralEquatableExtensions.cs create mode 100644 Src/DDD.Core.Abstractions/Data/DbConnectionExtensions.cs create mode 100644 Src/DDD.Core.Abstractions/Data/DbConnectionHelper.cs create mode 100644 Src/DDD.Core.Abstractions/ExceptionExtensions.cs create mode 100644 Src/DDD.Core.Abstractions/Mapping/CompositeTranslator.cs create mode 100644 Src/DDD.Core.Abstractions/Mapping/IObjectTranslator`1.cs create mode 100644 Src/DDD.Core.Abstractions/Mapping/ObjectTranslator.cs create mode 100644 Src/DDD.Core.Abstractions/StringArgExtensions.cs create mode 100644 Src/DDD.Core.Abstractions/StringParamExtensions.cs create mode 100644 Src/DDD.Core.Abstractions/TimestampedException.cs create mode 100644 Src/DDD.Core.Abstractions/TypeExtensions.cs create mode 100644 Src/DDD.Core.Abstractions/Validation/ISyncObjectValidator.cs create mode 100644 Src/DDD.Core.Cronos/CronosSchedule.cs create mode 100644 Src/DDD.Core.Cronos/CronosScheduleFactory.cs create mode 100644 Src/DDD.Core.Cronos/DDD.Core.Cronos.csproj create mode 100644 Src/DDD.Core.Cronos/Properties/AssemblyInfo.cs create mode 100644 Src/DDD.Core.Dapper.Oracle/DDD.Core.Dapper.Oracle.csproj create mode 100644 Src/DDD.Core.Dapper.Oracle/OracleGuidTypeHandler.cs create mode 100644 Src/DDD.Core.Dapper.Oracle/Properties/AssemblyInfo.cs create mode 100644 Src/DDD.Core.Dapper/EventStreamPositionUpdater.cs create mode 100644 Src/DDD.Core.Dapper/EventStreamReader.cs create mode 100644 Src/DDD.Core.Dapper/EventStreamSubcriber.cs create mode 100644 Src/DDD.Core.Dapper/EventStreamsFinder.cs create mode 100644 Src/DDD.Core.Dapper/FailedEventStreamExcluder.cs create mode 100644 Src/DDD.Core.Dapper/FailedEventStreamIncluder.cs create mode 100644 Src/DDD.Core.Dapper/FailedEventStreamReader.cs create mode 100644 Src/DDD.Core.Dapper/FailedEventStreamUpdater.cs create mode 100644 Src/DDD.Core.Dapper/FailedEventStreamsFinder.cs create mode 100644 Src/DDD.Core.Dapper/FailedRecurringCommandUpdater.cs create mode 100644 Src/DDD.Core.Dapper/IncrementalDelaysTypeMapper.cs create mode 100644 Src/DDD.Core.Dapper/OracleScripts.Designer.cs create mode 100644 Src/DDD.Core.Dapper/OracleScripts.resx create mode 100644 Src/DDD.Core.Dapper/RecurringCommandIdGenerator.cs create mode 100644 Src/DDD.Core.Dapper/RecurringCommandRegister.cs create mode 100644 Src/DDD.Core.Dapper/RecurringCommandsFinder.cs create mode 100644 Src/DDD.Core.Dapper/Scripts/ExcludeFailedEventStream.sql create mode 100644 Src/DDD.Core.Dapper/Scripts/FindEventStreams.sql create mode 100644 Src/DDD.Core.Dapper/Scripts/FindFailedEventStreams.sql create mode 100644 Src/DDD.Core.Dapper/Scripts/FindRecurringCommandByType.sql create mode 100644 Src/DDD.Core.Dapper/Scripts/FindRecurringCommands.sql create mode 100644 Src/DDD.Core.Dapper/Scripts/IncludeFailedEventStream.sql create mode 100644 Src/DDD.Core.Dapper/Scripts/InsertRecurringCommand.sql create mode 100644 Src/DDD.Core.Dapper/Scripts/Oracle/ReadEventStream.sql create mode 100644 Src/DDD.Core.Dapper/Scripts/Oracle/ReadFailedEventStream.sql create mode 100644 Src/DDD.Core.Dapper/Scripts/ReadEventStream.sql create mode 100644 Src/DDD.Core.Dapper/Scripts/ReadFailedEventStream.sql create mode 100644 Src/DDD.Core.Dapper/Scripts/SubscribeToEventStream.sql create mode 100644 Src/DDD.Core.Dapper/Scripts/UpdateEventStreamPosition.sql create mode 100644 Src/DDD.Core.Dapper/Scripts/UpdateFailedEventStream.sql create mode 100644 Src/DDD.Core.Dapper/Scripts/UpdateRecurringCommand.sql create mode 100644 Src/DDD.Core.Dapper/Scripts/UpdateRecurringCommandStatus.sql create mode 100644 Src/DDD.Core.Dapper/SqlScripts.Designer.cs create mode 100644 Src/DDD.Core.Dapper/SqlScripts.resx create mode 100644 Src/DDD.Core.Dapper/SuccessfulRecurringCommandUpdater.cs create mode 100644 Src/DDD.Core.FluentValidation/AsyncFluentCommandValidatorAdapter.cs create mode 100644 Src/DDD.Core.FluentValidation/AsyncFluentQueryValidatorAdapter.cs rename Src/DDD.Core.FluentValidation/{FluentValidatorAdapter.cs => AsyncFluentValidatorAdapter.cs} (55%) delete mode 100644 Src/DDD.Core.FluentValidation/FluentQueryValidatorAdapter.cs create mode 100644 Src/DDD.Core.FluentValidation/SyncFluentCommandValidatorAdapter.cs rename Src/DDD.Core.FluentValidation/{FluentCommandValidatorAdapter.cs => SyncFluentQueryValidatorAdapter.cs} (51%) create mode 100644 Src/DDD.Core.FluentValidation/SyncFluentValidatorAdapter.cs create mode 100644 Src/DDD.Core.Messages/Application/CommandExecutionStatus.cs create mode 100644 Src/DDD.Core.Messages/Application/Event.cs create mode 100644 Src/DDD.Core.Messages/Application/EventStream.cs create mode 100644 Src/DDD.Core.Messages/Application/ExcludeFailedEventStream.cs create mode 100644 Src/DDD.Core.Messages/Application/FailedEventStream.cs create mode 100644 Src/DDD.Core.Messages/Application/FindEventStreams.cs create mode 100644 Src/DDD.Core.Messages/Application/FindFailedEventStreams.cs create mode 100644 Src/DDD.Core.Messages/Application/FindRecurringCommands.cs create mode 100644 Src/DDD.Core.Messages/Application/GenerateRecurringCommandId.cs create mode 100644 Src/DDD.Core.Messages/Application/IncludeFailedEventStream.cs create mode 100644 Src/DDD.Core.Messages/Application/IncrementalDelay.cs create mode 100644 Src/DDD.Core.Messages/Application/MarkRecurringCommandAsFailed.cs create mode 100644 Src/DDD.Core.Messages/Application/MarkRecurringCommandAsSuccessful.cs create mode 100644 Src/DDD.Core.Messages/Application/ReadEventStream.cs create mode 100644 Src/DDD.Core.Messages/Application/ReadFailedEventStream.cs create mode 100644 Src/DDD.Core.Messages/Application/RecurringCommand.cs create mode 100644 Src/DDD.Core.Messages/Application/RecurringCommandDetail.cs create mode 100644 Src/DDD.Core.Messages/Application/RegisterRecurringCommand.cs create mode 100644 Src/DDD.Core.Messages/Application/SubscribeToEventStream.cs create mode 100644 Src/DDD.Core.Messages/Application/UpdateEventStreamPosition.cs create mode 100644 Src/DDD.Core.Messages/Application/UpdateFailedEventStream.cs create mode 100644 Src/DDD.Core.Messages/Application/WriteEvents.cs create mode 100644 Src/DDD.Core.Messages/Domain/BoundedContext.cs create mode 100644 Src/DDD.Core.NHibernate/DelegatingSessionFactory.cs rename Src/DDD.Core.NHibernate/{StoredEventMapping.cs => EventMapping.cs} (81%) create mode 100644 Src/DDD.Core.NHibernate/ISessionFactory.cs create mode 100644 Src/DDD.Core.NHibernate/NHRepository.cs rename Src/DDD.Core.NHibernate/{NHibernateRepositoryExceptionTranslator.cs => NHRepositoryExceptionTranslator.cs} (53%) delete mode 100644 Src/DDD.Core.NHibernate/NHibernateRepository.cs rename Src/DDD.Core.NHibernate/{OracleStoredEventMapping.cs => OracleEventMapping.cs} (56%) rename Src/DDD.Core.NHibernate/{SqlServerStoredEventMapping.cs => SqlServerEventMapping.cs} (68%) create mode 100644 Src/DDD.Core.NSubstitute/DDD.Core.NSubstitute.csproj create mode 100644 Src/DDD.Core.NSubstitute/Properties/AssemblyInfo.cs create mode 100644 Src/DDD.Core.NSubstitute/SubstituteExtensions.cs create mode 100644 Src/DDD.Core.Polly/AsyncPollyCommandHandler`1.cs create mode 100644 Src/DDD.Core.Polly/AsyncPollyQueryHandler`1.cs delete mode 100644 Src/DDD.Core.Polly/PollyCommandHandler.cs create mode 100644 Src/DDD.Core.Polly/SyncPollyCommandHandler.cs create mode 100644 Src/DDD.Core.Polly/SyncPollyCommandHandler`1.cs rename Src/DDD.Core.Polly/{PollyQueryHandler.cs => SyncPollyQueryHandler.cs} (53%) create mode 100644 Src/DDD.Core.Polly/SyncPollyQueryHandler`1.cs create mode 100644 Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler`1.cs create mode 100644 Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler`1.cs create mode 100644 Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler`1.cs create mode 100644 Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler`1.cs create mode 100644 Src/DDD.Core.Xunit/IDbFixtureExtensions.cs create mode 100644 Src/DDD.Core/Application/AsyncCommandHandlerWithLogging`1.cs delete mode 100644 Src/DDD.Core/Application/AsyncDomainCommandHandler.cs create mode 100644 Src/DDD.Core/Application/AsyncEventHandlerWithLogging.cs create mode 100644 Src/DDD.Core/Application/AsyncQueryHandlerWithLogging`1.cs delete mode 100644 Src/DDD.Core/Application/CommandHandlerWithLogging.cs create mode 100644 Src/DDD.Core/Application/CompositeTranslatorExtensions.cs create mode 100644 Src/DDD.Core/Application/ContextualCommandProcessor.cs create mode 100644 Src/DDD.Core/Application/ContextualQueryProcessor.cs delete mode 100644 Src/DDD.Core/Application/DomainCommandHandler.cs create mode 100644 Src/DDD.Core/Application/EventConsumer.cs create mode 100644 Src/DDD.Core/Application/EventConsumerSettings.cs delete mode 100644 Src/DDD.Core/Application/EventHandler.cs create mode 100644 Src/DDD.Core/Application/EventTranslator.cs create mode 100644 Src/DDD.Core/Application/FailedEventStreamSummaryExtensions.cs create mode 100644 Src/DDD.Core/Application/IAsyncCommandHandlerExtensions.cs create mode 100644 Src/DDD.Core/Application/IAsyncCommandHandler`1.cs create mode 100644 Src/DDD.Core/Application/IAsyncQueryHandlerExtensions.cs create mode 100644 Src/DDD.Core/Application/IAsyncQueryHandler`1.cs create mode 100644 Src/DDD.Core/Application/ICommandHandler`1.cs create mode 100644 Src/DDD.Core/Application/ICommandProcessorExtensions.cs create mode 100644 Src/DDD.Core/Application/IContextualCommandProcessor.cs create mode 100644 Src/DDD.Core/Application/IContextualCommandProcessor`1.cs create mode 100644 Src/DDD.Core/Application/IContextualQueryProcessor.cs create mode 100644 Src/DDD.Core/Application/IContextualQueryProcessor`1.cs create mode 100644 Src/DDD.Core/Application/IEventConsumer.cs create mode 100644 Src/DDD.Core/Application/IEventConsumer`1.cs delete mode 100644 Src/DDD.Core/Application/IEventHandler`1.cs delete mode 100644 Src/DDD.Core/Application/IEventPublisherExtensions.cs create mode 100644 Src/DDD.Core/Application/IMessageContext.cs create mode 100644 Src/DDD.Core/Application/IMessageContextExtensions.cs create mode 100644 Src/DDD.Core/Application/IQueryHandler`1.cs create mode 100644 Src/DDD.Core/Application/IQueryProcessorExtensions.cs create mode 100644 Src/DDD.Core/Application/IRecurringCommandManager.cs create mode 100644 Src/DDD.Core/Application/IRecurringCommandManager`1.cs create mode 100644 Src/DDD.Core/Application/IRecurringSchedule.cs create mode 100644 Src/DDD.Core/Application/IRecurringScheduleFactory.cs create mode 100644 Src/DDD.Core/Application/ISyncCommandHandler.cs create mode 100644 Src/DDD.Core/Application/ISyncCommandHandlerExtensions.cs create mode 100644 Src/DDD.Core/Application/ISyncCommandHandler`1.cs create mode 100644 Src/DDD.Core/Application/ISyncCommandValidator.cs rename Src/DDD.Core/Application/{IEventHandler.cs => ISyncEventHandler.cs} (50%) create mode 100644 Src/DDD.Core/Application/ISyncEventHandler`1.cs create mode 100644 Src/DDD.Core/Application/ISyncQueryHandler.cs create mode 100644 Src/DDD.Core/Application/ISyncQueryHandlerExtensions.cs create mode 100644 Src/DDD.Core/Application/ISyncQueryHandler`1.cs create mode 100644 Src/DDD.Core/Application/ISyncQueryValidator.cs create mode 100644 Src/DDD.Core/Application/MessageContext.cs create mode 100644 Src/DDD.Core/Application/MessageContextInfo.cs create mode 100644 Src/DDD.Core/Application/QueryConflictException.cs delete mode 100644 Src/DDD.Core/Application/QueryHandlerWithLogging.cs create mode 100644 Src/DDD.Core/Application/RecurringCommandManager.cs create mode 100644 Src/DDD.Core/Application/RecurringCommandManagerSettings.cs create mode 100644 Src/DDD.Core/Application/SyncCommandHandlerWithLogging.cs create mode 100644 Src/DDD.Core/Application/SyncCommandHandlerWithLogging`1.cs create mode 100644 Src/DDD.Core/Application/SyncEventHandler.cs create mode 100644 Src/DDD.Core/Application/SyncEventHandlerWithLogging.cs create mode 100644 Src/DDD.Core/Application/SyncQueryHandlerWithLogging.cs create mode 100644 Src/DDD.Core/Application/SyncQueryHandlerWithLogging`1.cs create mode 100644 Src/DDD.Core/Domain/CompositeTranslatorExtensions.cs create mode 100644 Src/DDD.Core/Domain/EntityState.cs create mode 100644 Src/DDD.Core/Domain/IStateEntity.cs create mode 100644 Src/DDD.Core/Domain/IStateObjectConvertible.cs create mode 100644 Src/DDD.Core/Domain/ISyncRepository.cs delete mode 100644 Src/DDD.Core/Infrastructure/Data/DbCommandHander.cs delete mode 100644 Src/DDD.Core/Infrastructure/Data/DbConnectionFactory.cs delete mode 100644 Src/DDD.Core/Infrastructure/Data/DbQueryHandler.cs create mode 100644 Src/DDD.Core/Infrastructure/Data/GuidGenerator.cs delete mode 100644 Src/DDD.Core/Infrastructure/Data/IDbConnectionFactory.cs delete mode 100644 Src/DDD.Core/Infrastructure/Data/IDbConnectionFactoryExtensions.cs create mode 100644 Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider.cs create mode 100644 Src/DDD.Core/Infrastructure/Data/IDbConnectionProviderExtensions.cs create mode 100644 Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider`1.cs create mode 100644 Src/DDD.Core/Infrastructure/Data/IValueGenerator.cs create mode 100644 Src/DDD.Core/Infrastructure/Data/LazyDbConnectionProvider.cs create mode 100644 Src/DDD.Core/Infrastructure/Data/RandomGuidGenerator.cs create mode 100644 Src/DDD.Core/Infrastructure/Data/SequentialBinaryGuidGenerator.cs create mode 100644 Src/DDD.Core/Infrastructure/Data/SequentialSqlServerGuidGenerator.cs create mode 100644 Src/DDD.Core/Infrastructure/Data/SequentialStringGuidGenerator.cs delete mode 100644 Src/DDD.Core/Infrastructure/Data/StoredEvent.cs delete mode 100644 Src/DDD.Core/Infrastructure/Data/StoredEventTranslator.cs create mode 100644 Src/DDD.HealthcareDelivery.Messages/Domain/HealthcareDeliveryContext.cs delete mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/IHealthcareDeliveryConnectionFactory.cs create mode 100644 Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionRepository.cs create mode 100644 Test/DDD.Common.UnitTests/Domain/FakeFlagsEnumeration.cs create mode 100644 Test/DDD.Common.UnitTests/Domain/FlagsEnumerationTests.cs create mode 100644 Test/DDD.Core.Abstractions.UnitTests/Linq/IEnumerableExtensionsTests.cs create mode 100644 Test/DDD.Core.Abstractions.UnitTests/Mapping/FakeObject3.cs create mode 100644 Test/DDD.Core.Abstractions.UnitTests/Mapping/IMappingProcessorExtensionsTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/App.config create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/DDD.Core.Dapper.IntegrationTests.csproj create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/EventStreamPositionUpdaterTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/EventStreamReaderTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/EventStreamSubscriberTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/EventStreamsFinderTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamExcluderTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamIncluderTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamReaderTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamUpdaterTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamsFinderTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/FailedRecurringCommandUpdaterTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/IDbConnectionExtensionsTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/IPersistenceFixture.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/OracleCollection.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/OracleEventStreamPositionUpdaterTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/OracleEventStreamReaderTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/OracleEventStreamSubscriberTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/OracleEventStreamsFinderTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/OracleFailedEventStreamExcluderTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/OracleFailedEventStreamIncluderTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/OracleFailedEventStreamReaderTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/OracleFailedEventStreamUpdaterTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/OracleFailedEventStreamsFinderTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/OracleFailedRecurringCommandUpdaterTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/OracleFixture.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/OracleIDbConnectionExtensionsTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/OracleRecurringCommandRegisterTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/OracleRecurringCommandsFinderTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/OracleScripts.Designer.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/OracleScripts.resx create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/OracleSuccessfulRecurringCommandUpdaterTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Properties/AssemblyInfo.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/RecurringCommandRegisterTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/RecurringCommandsFinderTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/CreateSchema.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/ExcludeFailedEventStream.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/FillSchema.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/FindEventStreams.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/FindFailedEventStreams.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/FindRecurringCommands.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/IncludeFailedEventStream.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/MarkRecurringCommandAsFailed.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/MarkRecurringCommandAsSuccessful.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/NextId_ExistingRows.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/NextId_NoRow.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/ReadEventStream.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/ReadFailedEventStream.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/RegisterRecurringCommand.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/SubscribeToEventStream.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/UpdateEventStreamPosition.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/UpdateFailedEventStream.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/ExcludeFailedEventStream.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/FindEventStreams.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/FindFailedEventStreams.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/FindRecurringCommands.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/IncludeFailedEventStream.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/MarkRecurringCommandAsFailed.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/MarkRecurringCommandAsSuccessful.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/NextId_ExistingRows.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/NextId_NoRow.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/ReadEventStream.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/ReadFailedEventStream.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/RegisterRecurringCommand.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/SubscribeToEventStream.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/UpdateEventStreamPosition.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/UpdateFailedEventStream.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/SqlServerCollection.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/SqlServerEventStreamPositionUpdaterTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/SqlServerEventStreamReaderTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/SqlServerEventStreamSubscriberTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/SqlServerEventStreamsFinderTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedEventStreamExcluderTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedEventStreamIncluderTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedEventStreamReaderTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedEventStreamUpdaterTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedEventStreamsFinderTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedRecurringCommandUpdaterTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/SqlServerFixture.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/SqlServerIDbConnectionExtensionsTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/SqlServerRecurringCommandRegisterTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/SqlServerRecurringCommandsFinderTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/SqlServerScripts.Designer.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/SqlServerScripts.resx create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/SqlServerSuccessfulRecurringCommandUpdaterTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/SuccessfulRecurringCommandUpdaterTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/TestContext.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/testhost.dll.config create mode 100644 Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs create mode 100644 Test/DDD.Core.UnitTests/Application/FakeSourceContext.cs create mode 100644 Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs create mode 100644 Test/DDD.Core.UnitTests/Domain/FakeContext.cs create mode 100644 Test/DDD.Core.UnitTests/Infrastructure/Data/SequentialBinaryGuidGeneratorTests.cs create mode 100644 Test/DDD.Core.UnitTests/Infrastructure/Data/SequentialSqlServerGuidGeneratorTests.cs create mode 100644 Test/DDD.Core.UnitTests/Infrastructure/Data/SequentialStringGuidGeneratorTests.cs delete mode 100644 Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleConnectionFactory.cs delete mode 100644 Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerConnectionFactory.cs create mode 100644 Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/CreatePharmaceuticalPrescription.sql create mode 100644 Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/CreatePharmaceuticalPrescription.sql create mode 100644 Test/DDD.HealthcareDelivery.IntegrationTests/testhost.dll.config diff --git a/DDD.sln b/DDD.sln index 5abd580..7bb3474 100644 --- a/DDD.sln +++ b/DDD.sln @@ -1,7 +1,7 @@  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 @@ -63,6 +63,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.Polly", "Src\DDD.C 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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -153,6 +161,22 @@ Global {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -179,6 +203,10 @@ Global {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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {983C3AB4-301E-47A3-93FF-2E4BD8F2090F} diff --git a/Src/DDD.Common.Messages/DDD.Common.Messages.csproj b/Src/DDD.Common.Messages/DDD.Common.Messages.csproj index 8d24737..51a7feb 100644 --- a/Src/DDD.Common.Messages/DDD.Common.Messages.csproj +++ b/Src/DDD.Common.Messages/DDD.Common.Messages.csproj @@ -1,6 +1,6 @@  - net48;netstandard2.1;net5.0 + net48;netstandard2.1 Library DDD.Common false 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 index 54a48f0..e9c5510 100644 --- a/Src/DDD.Common.NHibernate/DDD.Common.NHibernate.csproj +++ b/Src/DDD.Common.NHibernate/DDD.Common.NHibernate.csproj @@ -1,7 +1,7 @@  DDD.Common.Infrastructure.Data - net48;netstandard2.1;net5.0 + net48;netstandard2.1 false bin\$(Configuration)\ diff --git a/Src/DDD.Common/DDD.Common.csproj b/Src/DDD.Common/DDD.Common.csproj index 3e15d80..a3dabf1 100644 --- a/Src/DDD.Common/DDD.Common.csproj +++ b/Src/DDD.Common/DDD.Common.csproj @@ -1,6 +1,6 @@  - net48;netstandard2.1;net5.0 + net48;netstandard2.1 Library false @@ -22,7 +22,7 @@ - + \ No newline at end of file diff --git a/Src/DDD.Common/Domain/Alpha2CountryCode.cs b/Src/DDD.Common/Domain/Alpha2CountryCode.cs index 08f9203..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,9 +12,8 @@ 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() { } diff --git a/Src/DDD.Common/Domain/Alpha2LanguageCode.cs b/Src/DDD.Common/Domain/Alpha2LanguageCode.cs index 55f6f43..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,9 +12,8 @@ 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() { } diff --git a/Src/DDD.Common/Domain/BelgianSocialSecurityNumber.cs b/Src/DDD.Common/Domain/BelgianSocialSecurityNumber.cs index 294dc22..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,9 +13,8 @@ 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() { } @@ -39,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 ca648fe..72952dc 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,7 +21,7 @@ public class BinaryContent : ValueObject public BinaryContent(byte[] data) { - Condition.Requires(data, nameof(data)).IsNotNull(); + Ensure.That(data, nameof(data)).IsNotNull(); this.Data = data; } diff --git a/Src/DDD.Common/Domain/EmailAddress.cs b/Src/DDD.Common/Domain/EmailAddress.cs index 3f654f2..12960e9 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; namespace DDD.Common.Domain { @@ -15,9 +14,8 @@ 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; } @@ -41,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}"); } @@ -66,13 +64,13 @@ public override IEnumerable EqualityComponents() 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..ebafc15 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; @@ -113,19 +119,18 @@ public override string ToString() { return $"{this.GetType().Name} [value={this.Value}, code={this.Code}, name={this.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 46b7433..1d1626e 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; @@ -13,8 +13,8 @@ public class FullName : ComparableValueObject 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(); } @@ -54,13 +54,13 @@ public override string ToString() 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); } diff --git a/Src/DDD.Common/Domain/IdentificationCode.cs b/Src/DDD.Common/Domain/IdentificationCode.cs index acde891..3637db1 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; @@ -18,7 +18,7 @@ protected IdentificationCode() { } protected IdentificationCode(string value) { - Condition.Requires(value, nameof(value)).IsNotNullOrWhiteSpace(); + Ensure.That(value, nameof(value)).IsNotNullOrWhiteSpace(); this.Value = value.ToUpper(); } diff --git a/Src/DDD.Common/Domain/IdentificationNumber.cs b/Src/DDD.Common/Domain/IdentificationNumber.cs index b7609fc..b3d6bb9 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; @@ -18,7 +18,7 @@ protected IdentificationNumber() { } protected IdentificationNumber(string value) { - Condition.Requires(value, nameof(value)).IsNotNullOrWhiteSpace(); + Ensure.That(value, nameof(value)).IsNotNullOrWhiteSpace(); this.Value = value.ToUpper(); } diff --git a/Src/DDD.Common/Domain/PostalAddress.cs b/Src/DDD.Common/Domain/PostalAddress.cs index 6350380..15f327a 100644 --- a/Src/DDD.Common/Domain/PostalAddress.cs +++ b/Src/DDD.Common/Domain/PostalAddress.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System.Collections.Generic; namespace DDD.Common.Domain @@ -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)) 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 0100b0c..3c20567 100644 --- a/Src/DDD.Core.Abstractions/DDD.Core.Abstractions.csproj +++ b/Src/DDD.Core.Abstractions/DDD.Core.Abstractions.csproj @@ -1,6 +1,6 @@  - net48;netstandard2.1;net5.0 + net48;netstandard2.1 Library DDD false @@ -19,7 +19,7 @@ - + \ 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 26d397e..02c3359 100644 --- a/Src/DDD.Core.Abstractions/DateTimeExtensions.cs +++ b/Src/DDD.Core.Abstractions/DateTimeExtensions.cs @@ -1,6 +1,5 @@ using System; using System.Globalization; -using Conditions; namespace DDD { @@ -13,19 +12,29 @@ public static class DateTimeExtensions #region Methods /// - /// Gets the next date and time. + /// Converts the value of the current DateTime object to its equivalent short date string representation using the french culture-specific format information. /// - public static DateTime Next(this DateTime instance) + public static string ToFrenchShortDateString(this DateTime instance) { - return instance.AddTicks(1); + 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; } /// @@ -37,5 +46,6 @@ public static string ToShortDateString(this DateTime instance, IFormatProvider p } #endregion Methods + } } diff --git a/Src/DDD.Core.Abstractions/DelegatingTimestampProvider.cs b/Src/DDD.Core.Abstractions/DelegatingTimestampProvider.cs index c0831bd..684e5cd 100644 --- a/Src/DDD.Core.Abstractions/DelegatingTimestampProvider.cs +++ b/Src/DDD.Core.Abstractions/DelegatingTimestampProvider.cs @@ -1,10 +1,10 @@ -using Conditions; +using EnsureThat; using System; namespace DDD { /// - /// Provides timestamps based on a delegate. + /// Adapter that converts a delegate into an object that implements ITimestampProvider. /// public class DelegatingTimestampProvider : ITimestampProvider { @@ -19,7 +19,7 @@ public class DelegatingTimestampProvider : ITimestampProvider public DelegatingTimestampProvider(Func timestamp) { - Condition.Requires(timestamp, nameof(timestamp)).IsNotNull(); + Ensure.That(timestamp, nameof(timestamp)).IsNotNull(); this.timestamp = timestamp; } 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/IServiceProviderExtensions.cs b/Src/DDD.Core.Abstractions/IServiceProviderExtensions.cs index bc2f956..cb649ac 100644 --- a/Src/DDD.Core.Abstractions/IServiceProviderExtensions.cs +++ b/Src/DDD.Core.Abstractions/IServiceProviderExtensions.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System; using System.Collections.Generic; using System.Linq; @@ -21,7 +21,7 @@ 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)); } @@ -33,7 +33,7 @@ public static T GetService(this IServiceProvider provider) /// An enumeration of services of type . public static IEnumerable GetServices(this IServiceProvider provider) { - Condition.Requires(provider, nameof(provider)).IsNotNull(); + Ensure.That(provider, nameof(provider)).IsNotNull(); return provider.GetService>() ?? Enumerable.Empty(); } @@ -45,8 +45,8 @@ public static IEnumerable GetServices(this IServiceProvider provider) /// An enumeration of services of type . public static IEnumerable GetServices(this IServiceProvider provider, Type serviceType) { - Condition.Requires(provider, nameof(provider)).IsNotNull(); - Condition.Requires(serviceType, nameof(serviceType)).IsNotNull(); + 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(); } 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/Mapping/CompositeTranslator.cs b/Src/DDD.Core.Abstractions/Mapping/CompositeTranslator.cs new file mode 100644 index 0000000..dc50a4f --- /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, TDestination> translator) + where TDerivedSource : class, TSource + { + Ensure.That(translator, nameof(translator)).IsNotNull(); + this.translators.Add(new DelegatingTranslator(translator)); + } + + public override TDestination Translate(TSource source, IDictionary 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/DelegatingTranslator.cs b/Src/DDD.Core.Abstractions/Mapping/DelegatingTranslator.cs index ee06db2..ea908af 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, TDestination> translator; #endregion Fields #region Constructors - public DelegatingTranslator(Func, TDestination> toDestination) + public DelegatingTranslator(Func, TDestination> 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, IDictionary context = null) { - Condition.Requires(source).IsNotNull(); - return this.toDestination(source, options); + Ensure.That(source).IsNotNull(); + return this.translator(source, context); } #endregion Methods diff --git a/Src/DDD.Core.Abstractions/Mapping/IMappingProcessor.cs b/Src/DDD.Core.Abstractions/Mapping/IMappingProcessor.cs index 76e9632..4ef63f4 100644 --- a/Src/DDD.Core.Abstractions/Mapping/IMappingProcessor.cs +++ b/Src/DDD.Core.Abstractions/Mapping/IMappingProcessor.cs @@ -10,14 +10,14 @@ public interface IMappingProcessor #region Methods - void Map(TSource source, TDestination destination, IDictionary options = null) + void Map(TSource source, TDestination destination, IDictionary context = null) where TSource : class where TDestination : class; - TDestination Translate(object source, IDictionary options = null) + TDestination Translate(object source, IDictionary context = null) where TDestination : class; - TDestination Translate(TSource source, IDictionary options = null) + TDestination Translate(TSource source, IDictionary context = null) 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..46749c2 100644 --- a/Src/DDD.Core.Abstractions/Mapping/IMappingProcessorExtensions.cs +++ b/Src/DDD.Core.Abstractions/Mapping/IMappingProcessorExtensions.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System.Collections.Generic; namespace DDD.Mapping @@ -13,80 +13,80 @@ public static class IMappingProcessorExtensions 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(); + Ensure.That(processor, nameof(processor)).IsNotNull(); var dictionary = new Dictionary(); - dictionary.AddObject(options); + dictionary.AddObject(context); processor.Map(source, destination, dictionary); } public static TDestination Translate(this IMappingProcessor processor, object source, - object options) + object context) where TDestination : class { - Condition.Requires(processor, nameof(processor)).IsNotNull(); + Ensure.That(processor, nameof(processor)).IsNotNull(); var dictionary = new Dictionary(); - dictionary.AddObject(options); + dictionary.AddObject(context); return processor.Translate(source, dictionary); } public static TDestination Translate(this IMappingProcessor processor, TSource source, - object options) + object context) where TSource : class where TDestination : class { - Condition.Requires(processor, nameof(processor)).IsNotNull(); + Ensure.That(processor, nameof(processor)).IsNotNull(); var dictionary = new Dictionary(); - dictionary.AddObject(options); + dictionary.AddObject(context); return processor.Translate(source, dictionary); } public static IEnumerable TranslateCollection(this IMappingProcessor processor, IEnumerable source, - IDictionary options = null) + IDictionary context = null) where TSource : class where TDestination : class { - Condition.Requires(processor, nameof(processor)).IsNotNull(); + Ensure.That(processor, nameof(processor)).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) + IDictionary context = null) where TDestination : class { - Condition.Requires(processor, nameof(processor)).IsNotNull(); + Ensure.That(processor, nameof(processor)).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(); + Ensure.That(processor, nameof(processor)).IsNotNull(); var dictionary = new Dictionary(); - dictionary.AddObject(options); + dictionary.AddObject(context); return processor.TranslateCollection(source, dictionary); } public static IEnumerable TranslateCollection(this IMappingProcessor processor, IEnumerable source, - object options) + object context) where TDestination : class { - Condition.Requires(processor, nameof(processor)).IsNotNull(); + Ensure.That(processor, nameof(processor)).IsNotNull(); var dictionary = new Dictionary(); - dictionary.AddObject(options); + dictionary.AddObject(context); return processor.TranslateCollection(source, dictionary); } diff --git a/Src/DDD.Core.Abstractions/Mapping/IObjectMapper.cs b/Src/DDD.Core.Abstractions/Mapping/IObjectMapper.cs index 001b522..f17666b 100644 --- a/Src/DDD.Core.Abstractions/Mapping/IObjectMapper.cs +++ b/Src/DDD.Core.Abstractions/Mapping/IObjectMapper.cs @@ -3,7 +3,7 @@ namespace DDD.Mapping { /// - /// Defines a method that maps an input object of one type to an output object of another type. + /// Defines a method that maps an input object of one type to an output object of a different type. /// public interface IObjectMapper where TSource : class @@ -12,7 +12,7 @@ public interface IObjectMapper #region Methods - void Map(TSource source, TDestination destination, IDictionary options = null); + void Map(TSource source, TDestination destination, IDictionary context = null); #endregion Methods diff --git a/Src/DDD.Core.Abstractions/Mapping/IObjectMapperExtensions.cs b/Src/DDD.Core.Abstractions/Mapping/IObjectMapperExtensions.cs index edbfb6a..f931402 100644 --- a/Src/DDD.Core.Abstractions/Mapping/IObjectMapperExtensions.cs +++ b/Src/DDD.Core.Abstractions/Mapping/IObjectMapperExtensions.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System.Collections.Generic; namespace DDD.Mapping @@ -13,13 +13,13 @@ public static class IObjectMapperExtensions 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(); + Ensure.That(mapper, nameof(mapper)).IsNotNull(); var dictionary = new Dictionary(); - dictionary.AddObject(options); + dictionary.AddObject(context); mapper.Map(source, destination, dictionary); } diff --git a/Src/DDD.Core.Abstractions/Mapping/IObjectTranslator.cs b/Src/DDD.Core.Abstractions/Mapping/IObjectTranslator.cs index 1821c6d..4207139 100644 --- a/Src/DDD.Core.Abstractions/Mapping/IObjectTranslator.cs +++ b/Src/DDD.Core.Abstractions/Mapping/IObjectTranslator.cs @@ -1,18 +1,28 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace DDD.Mapping { /// - /// Defines a method that translates an input object of one type into an output object of another type. + /// 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, IDictionary context = null); #endregion Methods + } } diff --git a/Src/DDD.Core.Abstractions/Mapping/IObjectTranslatorExtensions.cs b/Src/DDD.Core.Abstractions/Mapping/IObjectTranslatorExtensions.cs index 6780c00..4375c2e 100644 --- a/Src/DDD.Core.Abstractions/Mapping/IObjectTranslatorExtensions.cs +++ b/Src/DDD.Core.Abstractions/Mapping/IObjectTranslatorExtensions.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System.Collections.Generic; namespace DDD.Mapping @@ -12,36 +12,36 @@ public static class IObjectTranslatorExtensions public static TDestination Translate(this IObjectTranslator translator, TSource source, - object options) + object context) where TSource : class where TDestination : class { - Condition.Requires(translator, nameof(translator)).IsNotNull(); + Ensure.That(translator, nameof(translator)).IsNotNull(); var dictionary = new Dictionary(); - dictionary.AddObject(options); + dictionary.AddObject(context); return translator.Translate(source, dictionary); } public static IEnumerable TranslateCollection(this IObjectTranslator translator, IEnumerable source, - IDictionary options = null) + IDictionary context = null) where TSource : class where TDestination : class { - Condition.Requires(translator, nameof(translator)).IsNotNull(); + Ensure.That(translator, nameof(translator)).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(); + Ensure.That(translator, nameof(translator)).IsNotNull(); var dictionary = new Dictionary(); - dictionary.AddObject(options); + dictionary.AddObject(context); return translator.TranslateCollection(source, dictionary); } 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..b54c0c5 --- /dev/null +++ b/Src/DDD.Core.Abstractions/Mapping/IObjectTranslator`1.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +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, IDictionary context = null); + + #endregion Methods + } +} diff --git a/Src/DDD.Core.Abstractions/Mapping/MappingException.cs b/Src/DDD.Core.Abstractions/Mapping/MappingException.cs index 6ecdd9c..4224e21 100644 --- a/Src/DDD.Core.Abstractions/Mapping/MappingException.cs +++ b/Src/DDD.Core.Abstractions/Mapping/MappingException.cs @@ -3,22 +3,34 @@ namespace DDD.Mapping { /// - /// Exception thrown when an error occurred while mapping (or translating) an input object of one type to an output object of another type. + /// Exception thrown by mappers or translators when a problem occurred while mapping objects. /// public class MappingException : Exception { #region Constructors - public MappingException(Type sourceType = null, Type destinationType = null, Exception innerException = null) - : base(DefaultMessage(sourceType, destinationType), innerException) + 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(string message, Type sourceType = null, Type destinationType = null, Exception innerException = null) - : base(message, innerException) + 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; @@ -36,11 +48,6 @@ public MappingException(string message, Type sourceType = null, Type destination #region Methods - public static string DefaultMessage(Type sourceType = null, Type destinationType = null) - { - return $"An error occurred while mapping an input object of {SourceTypeInfo(sourceType)} to an output object of {DestinationTypeInfo(destinationType)}."; - } - public override string ToString() { var s = $"{this.GetType()}: {this.Message} "; @@ -55,20 +62,7 @@ public override string ToString() return s; } - private static string DestinationTypeInfo(Type destinationType) - { - if (destinationType == null) - return "another type"; - return $"the type '{destinationType.Name}'"; - } - - private static string SourceTypeInfo(Type sourceType) - { - if (sourceType == null) - return "one type"; - return $"the type '{sourceType.Name}'"; - } - #endregion Methods + } } diff --git a/Src/DDD.Core.Abstractions/Mapping/MappingProcessor.cs b/Src/DDD.Core.Abstractions/Mapping/MappingProcessor.cs index a3b6f2d..7a77bba 100644 --- a/Src/DDD.Core.Abstractions/Mapping/MappingProcessor.cs +++ b/Src/DDD.Core.Abstractions/Mapping/MappingProcessor.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System; using System.Collections.Generic; @@ -20,7 +20,7 @@ public class MappingProcessor : IMappingProcessor public MappingProcessor(IServiceProvider serviceProvider) { - Condition.Requires(serviceProvider, nameof(serviceProvider)).IsNotNull(); + Ensure.That(serviceProvider, nameof(serviceProvider)).IsNotNull(); this.serviceProvider = serviceProvider; } @@ -28,32 +28,32 @@ public MappingProcessor(IServiceProvider serviceProvider) #region Methods - public void Map(TSource source, TDestination destination, IDictionary options = null) + public void Map(TSource source, TDestination destination, IDictionary context = null) 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); + mapper.Map(source, destination, context); } - public TDestination Translate(object source, IDictionary options = null) + public TDestination Translate(object source, IDictionary context = null) where TDestination : class { 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); + return translator.Translate((dynamic)source, context); } - public TDestination Translate(TSource source, IDictionary options = null) + public TDestination Translate(TSource source, IDictionary context = null) where TSource : class where TDestination : class { 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); + return translator.Translate(source, context); } #endregion Methods diff --git a/Src/DDD.Core.Abstractions/Mapping/ObjectTranslator.cs b/Src/DDD.Core.Abstractions/Mapping/ObjectTranslator.cs new file mode 100644 index 0000000..9f45bbb --- /dev/null +++ b/Src/DDD.Core.Abstractions/Mapping/ObjectTranslator.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; + +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, IDictionary context); + + object IObjectTranslator.Translate(object source, IDictionary context) => this.Translate((TSource)source, context); + + #endregion Methods + } +} diff --git a/Src/DDD.Core.Abstractions/Serialization/ISerializer.cs b/Src/DDD.Core.Abstractions/Serialization/ISerializer.cs index 035f02b..12079c2 100644 --- a/Src/DDD.Core.Abstractions/Serialization/ISerializer.cs +++ b/Src/DDD.Core.Abstractions/Serialization/ISerializer.cs @@ -19,5 +19,6 @@ public interface ISerializer void Serialize(Stream stream, object obj); #endregion Methods + } } \ No newline at end of file diff --git a/Src/DDD.Core.Abstractions/Serialization/ISerializerExtensions.cs b/Src/DDD.Core.Abstractions/Serialization/ISerializerExtensions.cs index 9cd1f08..fc38ba7 100644 --- a/Src/DDD.Core.Abstractions/Serialization/ISerializerExtensions.cs +++ b/Src/DDD.Core.Abstractions/Serialization/ISerializerExtensions.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System; using System.IO; @@ -11,16 +11,16 @@ public static class ISerializerExtensions public static T Deserialize(this ISerializer serializer, Stream stream) { - Condition.Requires(serializer, nameof(serializer)).IsNotNull(); - Condition.Requires(stream, nameof(stream)).IsNotNull(); + 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(); - Condition.Requires(type, nameof(type)).IsNotNull(); + 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, type); @@ -29,15 +29,15 @@ public static object DeserializeFromFile(this ISerializer serializer, string fil public static T DeserializeFromFile(this ISerializer serializer, string filePath) { - Condition.Requires(serializer, nameof(serializer)).IsNotNull(); - Condition.Requires(filePath, nameof(filePath)).IsNotNullOrWhiteSpace(); + 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 6d40327..f87b985 100644 --- a/Src/DDD.Core.Abstractions/Serialization/ITextSerializerExtensions.cs +++ b/Src/DDD.Core.Abstractions/Serialization/ITextSerializerExtensions.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System; using System.IO; @@ -11,14 +11,14 @@ 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) { - Condition.Requires(serializer, nameof(serializer)).IsNotNull(); - Condition.Requires(type, nameof(type)).IsNotNull(); + Ensure.That(serializer, nameof(serializer)).IsNotNull(); + Ensure.That(type, nameof(type)).IsNotNull(); var bytes = serializer.Encoding.GetBytes(input); using (var stream = new MemoryStream(bytes)) { @@ -28,7 +28,7 @@ public static object DeserializeFromString(this ITextSerializer serializer, stri 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 e01a375..0f41a64 100644 --- a/Src/DDD.Core.Abstractions/Serialization/JsonSerializationOptions.cs +++ b/Src/DDD.Core.Abstractions/Serialization/JsonSerializationOptions.cs @@ -14,4 +14,4 @@ public static class JsonSerializationOptions #endregion Properties } -} +} \ No newline at end of file diff --git a/Src/DDD.Core.Abstractions/StringArgExtensions.cs b/Src/DDD.Core.Abstractions/StringArgExtensions.cs new file mode 100644 index 0000000..59deb8c --- /dev/null +++ b/Src/DDD.Core.Abstractions/StringArgExtensions.cs @@ -0,0 +1,56 @@ +using EnsureThat; +using EnsureThat.Enforcers; + +namespace DDD +{ + public static partial class StringArgExtensions + { + + #region Methods + + 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; + } + + 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; + } + + 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; + } + + #endregion Methods + + + } +} 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..808ebb3 --- /dev/null +++ b/Src/DDD.Core.Abstractions/StringParamExtensions.cs @@ -0,0 +1,22 @@ +using EnsureThat; + +namespace DDD +{ + public static class StringParamExtensions + { + + #region Methods + + public static void IsAllLetters(this in StringParam param) + => Ensure.String.IsAllLetters(param.Value, param.Name, param.OptsFn); + + public static void IsAllDigits(this in StringParam param) + => Ensure.String.IsAllDigits(param.Value, param.Name, param.OptsFn); + + public static void HasMinLength(this in StringParam param, int minLength) + => Ensure.String.HasMinLength(param.Value, minLength, param.Name, param.OptsFn); + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Abstractions/TimestampedException.cs b/Src/DDD.Core.Abstractions/TimestampedException.cs new file mode 100644 index 0000000..7b92507 --- /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}Timestamp: {this.Timestamp}"; + s += $"{Environment.NewLine}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 index 91a33d9..1c03bac 100644 --- a/Src/DDD.Core.Abstractions/UniqueTimestampProvider.cs +++ b/Src/DDD.Core.Abstractions/UniqueTimestampProvider.cs @@ -1,5 +1,5 @@ using System; -using Conditions; +using EnsureThat; namespace DDD { @@ -21,7 +21,7 @@ public class UniqueTimestampProvider : ITimestampProvider public UniqueTimestampProvider(ITimestampProvider provider, TimeSpan resolution) { - Condition.Requires(provider, nameof(provider)).IsNotNull(); + Ensure.That(provider, nameof(provider)).IsNotNull(); this.provider = provider; this.Resolution = resolution; } diff --git a/Src/DDD.Core.Abstractions/Validation/IAsyncObjectValidator.cs b/Src/DDD.Core.Abstractions/Validation/IAsyncObjectValidator.cs index f85b05d..b861974 100644 --- a/Src/DDD.Core.Abstractions/Validation/IAsyncObjectValidator.cs +++ b/Src/DDD.Core.Abstractions/Validation/IAsyncObjectValidator.cs @@ -11,6 +11,9 @@ public interface IAsyncObjectValidator where T :class #region Methods + /// + /// Validates asynchronously an object of a specified type. + /// Task ValidateAsync(T obj, string ruleSet = null, CancellationToken cancellationToken = default); #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..469186d --- /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, string ruleSet = null); + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Abstractions/Validation/ValidationFailure.cs b/Src/DDD.Core.Abstractions/Validation/ValidationFailure.cs index 49853cd..40e39fe 100644 --- a/Src/DDD.Core.Abstractions/Validation/ValidationFailure.cs +++ b/Src/DDD.Core.Abstractions/Validation/ValidationFailure.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System.Runtime.Serialization; using System.Xml.Serialization; @@ -18,11 +18,11 @@ public ValidationFailure(string message, string code, FailureLevel level = FailureLevel.Warning, string propertyName = null, - string 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; @@ -82,7 +82,7 @@ private ValidationFailure() /// [DataMember(Name = "propertyValue")] [XmlElement("propertyValue")] - public string PropertyValue { get; private set; } + public object PropertyValue { get; private set; } #endregion Properties diff --git a/Src/DDD.Core.Abstractions/Validation/ValidationResult.cs b/Src/DDD.Core.Abstractions/Validation/ValidationResult.cs index 7611021..626169b 100644 --- a/Src/DDD.Core.Abstractions/Validation/ValidationResult.cs +++ b/Src/DDD.Core.Abstractions/Validation/ValidationResult.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System.Linq; namespace DDD.Validation @@ -13,8 +13,8 @@ public class ValidationResult public ValidationResult(bool isSuccessful, string objectName, ValidationFailure[] failures) { - Condition.Requires(objectName, nameof(objectName)).IsNotNullOrWhiteSpace(); - 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; 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..dd84fd9 --- /dev/null +++ b/Src/DDD.Core.Cronos/CronosScheduleFactory.cs @@ -0,0 +1,31 @@ +using Cronos; +using EnsureThat; +using System; + +namespace DDD.Core.Infrastructure +{ + using Application; + + public class CronosScheduleFactory : IRecurringScheduleFactory + { + + #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 f09ac92..9206e10 100644 --- a/Src/DDD.Core.Dapper/DDD.Core.Dapper.csproj +++ b/Src/DDD.Core.Dapper/DDD.Core.Dapper.csproj @@ -1,6 +1,6 @@  - net48;netstandard2.1;net5.0 + net48;netstandard2.1 Library DDD.Core.Infrastructure.Data false @@ -23,10 +23,33 @@ - - - 2.0.78 - + + + + + + + + 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..8b01b66 --- /dev/null +++ b/Src/DDD.Core.Dapper/EventStreamPositionUpdater.cs @@ -0,0 +1,118 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using System.Transactions; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using DDD.Core.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 = null) + { + 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 = null) + { + Ensure.That(command, nameof(command)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + var cancellationToken = context?.CancellationToken() ?? default; + 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..31aa9a9 --- /dev/null +++ b/Src/DDD.Core.Dapper/EventStreamReader.cs @@ -0,0 +1,121 @@ +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 DDD.Core.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 = null) + { + 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 = null) + { + Ensure.That(query, nameof(query)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + var cancellationToken = context?.CancellationToken() ?? default; + 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..faf1041 --- /dev/null +++ b/Src/DDD.Core.Dapper/EventStreamSubcriber.cs @@ -0,0 +1,122 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using System.Transactions; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using DDD.Core.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 = null) + { + 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 = null) + { + Ensure.That(command, nameof(command)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + var cancellationToken = context?.CancellationToken() ?? default; + 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..bec61e2 --- /dev/null +++ b/Src/DDD.Core.Dapper/EventStreamsFinder.cs @@ -0,0 +1,92 @@ +using System; +using System.Threading.Tasks; +using System.Collections.Generic; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using DDD.Core.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 = null) + { + 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 = null) + { + Ensure.That(query, nameof(query)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + var cancellationToken = context?.CancellationToken() ?? default; + 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..e6c3c05 --- /dev/null +++ b/Src/DDD.Core.Dapper/FailedEventStreamExcluder.cs @@ -0,0 +1,132 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using System.Transactions; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using DDD.Core.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 = null) + { + 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 = null) + { + Ensure.That(command, nameof(command)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + var cancellationToken = context?.CancellationToken() ?? default; + 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..d61d57f --- /dev/null +++ b/Src/DDD.Core.Dapper/FailedEventStreamIncluder.cs @@ -0,0 +1,104 @@ +using System; +using System.Threading.Tasks; +using System.Transactions; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using DDD.Core.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 = null) + { + 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 = null) + { + Ensure.That(command, nameof(command)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + var cancellationToken = context?.CancellationToken() ?? default; + 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/FailedEventStreamReader.cs b/Src/DDD.Core.Dapper/FailedEventStreamReader.cs new file mode 100644 index 0000000..842414f --- /dev/null +++ b/Src/DDD.Core.Dapper/FailedEventStreamReader.cs @@ -0,0 +1,119 @@ +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 DDD.Core.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 = null) + { + 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 = null) + { + Ensure.That(query, nameof(query)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + var cancellationToken = context?.CancellationToken() ?? default; + 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..4cd0a4e --- /dev/null +++ b/Src/DDD.Core.Dapper/FailedEventStreamUpdater.cs @@ -0,0 +1,130 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using System.Transactions; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using DDD.Core.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 = null) + { + 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 = null) + { + Ensure.That(command, nameof(command)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + var cancellationToken = context?.CancellationToken() ?? default; + 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..60ee3f5 --- /dev/null +++ b/Src/DDD.Core.Dapper/FailedEventStreamsFinder.cs @@ -0,0 +1,94 @@ +using System; +using System.Threading.Tasks; +using System.Collections.Generic; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using DDD.Core.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 = null) + { + 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 = null) + { + Ensure.That(query, nameof(query)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + var cancellationToken = context?.CancellationToken() ?? default; + 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..c11a3b4 --- /dev/null +++ b/Src/DDD.Core.Dapper/FailedRecurringCommandUpdater.cs @@ -0,0 +1,124 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using System.Transactions; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using DDD.Core.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 = null) + { + 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 = null) + { + Ensure.That(command, nameof(command)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + var cancellationToken = context?.CancellationToken() ?? default; + 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 c587902..36bdd73 100644 --- a/Src/DDD.Core.Dapper/IDbConnectionExtensions.cs +++ b/Src/DDD.Core.Dapper/IDbConnectionExtensions.cs @@ -1,14 +1,16 @@ -using Dapper; -using Conditions; -using System; -using System.Data; +using System.Data; +using Dapper; +using EnsureThat; using System.Threading; using System.Threading.Tasks; using System.Collections.Generic; +using System; using System.Text.RegularExpressions; namespace DDD.Core.Infrastructure.Data { + using Threading; + /// /// Adds extension method to the interface. /// @@ -21,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); @@ -39,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, CancellationToken cancellationToken = default) + 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(new CommandDefinition(sql, cancellationToken: cancellationToken)); + 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..9049789 --- /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 DDD.Core.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 = null) + { + 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..19cc233 --- /dev/null +++ b/Src/DDD.Core.Dapper/RecurringCommandRegister.cs @@ -0,0 +1,176 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using System.Transactions; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using DDD.Core.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 = null) + { + 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 = null) + { + Ensure.That(command, nameof(command)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + var cancellationToken = context?.CancellationToken() ?? default; + 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; + 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 + }; + 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..ef57c37 --- /dev/null +++ b/Src/DDD.Core.Dapper/RecurringCommandsFinder.cs @@ -0,0 +1,94 @@ +using System; +using System.Threading.Tasks; +using System.Collections.Generic; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using DDD.Core.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 = null) + { + 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 = null) + { + Ensure.That(query, nameof(query)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + var cancellationToken = context?.CancellationToken() ?? default; + 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 0000000000000000000000000000000000000000..a8e8ef32801c71fdf71eb705d1d1fb817807cd84 GIT binary patch literal 1960 zcmbVN+irqD6rATK{)3O2_z`H;ghZXH6`uyDVo7AwZbfb7nZR z{QiV^Lxd3)h|z|6iZSEe7zy?`VuLl`e)0N&oX8v(-X}Prg%{#%M@PejT%qScw>5N( z0%ul`Ga{t;WLNKAtT|a373MrsYID~s9cWflVr5!JxZZj{|MwEJr&FUE8&Be$$(yoP z#v{S*iK=q{&B`I2-_n1#`vZ2Cu``n|bGV-GqkO>*)btm4y-l3TgS+pNF{Og57(HaA zBN+u%yDJQcOF2CUW@wLZ^12U6TcM8$mh>)qcvDSo2Z5Ht<|)_lvhSdC^3dwqfFi9s0G?%X)dz;`8quR3H?;Pyy8Db9MQV$>;E&1At-yPXCd-wLane+of C!&B7& literal 0 HcmV?d00001 diff --git a/Src/DDD.Core.Dapper/Scripts/FindRecurringCommandByType.sql b/Src/DDD.Core.Dapper/Scripts/FindRecurringCommandByType.sql new file mode 100644 index 0000000000000000000000000000000000000000..38c5d6a3901c19d87fdc5dea5922cb2a4faa08fc GIT binary patch literal 298 zcmezWFPOoV!3T()8A2EofMh;HE)XU%^ zG4s7}7JFK1R?eI$6PaK9t14bxIVP2vezknc<;xXn;&Ahx)CIi{Jk&%QbDd24^kVe? dCK~tKA!R|JWHai~Hz?(DH-l`(ZlvkEegOJJC!qiU literal 0 HcmV?d00001 diff --git a/Src/DDD.Core.Dapper/Scripts/IncludeFailedEventStream.sql b/Src/DDD.Core.Dapper/Scripts/IncludeFailedEventStream.sql new file mode 100644 index 0000000000000000000000000000000000000000..84ae59ee57f6ae37a260fe96bf8a29d07fa57505 GIT binary patch literal 220 zcmaKm+X{j}6h+r_;6Kcd1O_Szrr-!Y57mR2N-%%B@iXG=7~09hem-2hA)}uW G=jaI(t4HAg literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fc82ee74b4b70659f454a08d74621574a70ebab8 GIT binary patch literal 800 zcmb7?-D<)>5QV>6g}%dHR`C&PY2yzKv_>>VdMWCKpjJ#R`tsJZ+oYTICMm;`Y;w-b zoH_GznKIy2r$>sR%^gjKOh|as-QG+d_k>s@);BA5d=ayx6QB#0e|mJvnZ1}*jD&^a z$U(7Eg+#TgI1!&ho@?WV!XMZ&mx`*~Q(!~9r#F(0vIFMNnT9;^ z^9(UFUi5FG6QXR#hDV(zIq8**HGzz^zO41OyQ%ohhs^4G|w=`8nBsTdumFk#V~|Q}6F?;~(-$ Isb;a~3IyL{^#A|> literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6533c1a3b83d9fb2f32f2e568cb5d8fdbef83f72 GIT binary patch literal 808 zcma))+iJo<5Qe|o3ckZ$R`C%m)Ko=58;FtMWw2iK!ULYrm$&`1x=mK43B&F(JO3R1 znfbX)=rPjyAjZ(7PK`bhA!F&8W^VF|k2Uf-vS7uYAz!pSwDZc}9369JBhNCAkh$c* zRkeuUX%0r2Bur v?*F2=?in5Yf;3i}F*Rx1|NhzjDR*j@g5>kHr#AyGy9;hi7F=T~E%Tje@ugb(| z#a+Z2NlhxgZ}YBjd)6$ZvIslT%-y&U?vs-c_svT}t~z=jk#xG$x{T#%@RFUI%;+i2 zvDSO8hK3St65c+NtDKc|uheZkdy65Rr|iQM&X*Cw`gcv#InJ1~691LX)GK{Gkq6AT eHxCi_3=H)*h4-0rXXUKp{TU+W!an{9r|TcVtX1Ry literal 0 HcmV?d00001 diff --git a/Src/DDD.Core.Dapper/Scripts/ReadFailedEventStream.sql b/Src/DDD.Core.Dapper/Scripts/ReadFailedEventStream.sql new file mode 100644 index 0000000000000000000000000000000000000000..360b612261bbc7e84561210477dbcfb81c1bf255 GIT binary patch literal 632 zcmaix$qK?i5Jc;$;6Kb!!Cw$!Tu?AZl7M&&nu7-yq7nUh^-UxgmncnVhOX+KuFCBi zYM=?(QzS$BnsGMeaNlXKmKv(7Mi^KpCEDt!u~us5h@t)(PgbN06Ou}%uO*QwB@!n3 z%&wa1vdxNl|L`HaR2#)?S;Y;(?29qHUrNfne_F;m5LvFnWl!tcPyw2}H}5Pm*9bQQ z^3zu%mwE(eKumGh;Ve7vX^m4xUC{nlJk~ki;K6wOy|Nms+NhKFzSr%3RegmE-+Bwo S_WNk-vvt4=t`>Y>72E;*231f1 literal 0 HcmV?d00001 diff --git a/Src/DDD.Core.Dapper/Scripts/SubscribeToEventStream.sql b/Src/DDD.Core.Dapper/Scripts/SubscribeToEventStream.sql new file mode 100644 index 0000000000000000000000000000000000000000..7eece799a7fdb24986c1be223afbbf247898001d GIT binary patch literal 630 zcmb7>%?g4*6otQa&^x#^=pm-aMbIB=?IT(Yl}cGOJ$!ZCiHMHO<_`Cqd(V9H`!3m# z5D}9SP~B3tsv=&T$tcw=xvM^MW6VgswiC+dD5RTqr~*yPd1~INSGkk&fz@`aD(jgW zYhIPpu~yQzw|XJpRSu_J-O1|Vw8_pP;L9;UY{Jl|?_A45l$TtFxtFHqIHn@ o0=U~ff$mQO`U0Ej*j0kUB%Hwm7!E;LLd+8wYA~}~s{jB1 literal 0 HcmV?d00001 diff --git a/Src/DDD.Core.Dapper/SqlScripts.Designer.cs b/Src/DDD.Core.Dapper/SqlScripts.Designer.cs new file mode 100644 index 0000000..2e71162 --- /dev/null +++ b/Src/DDD.Core.Dapper/SqlScripts.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 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 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..0baf519 --- /dev/null +++ b/Src/DDD.Core.Dapper/SqlScripts.resx @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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\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..c1c1a3d --- /dev/null +++ b/Src/DDD.Core.Dapper/SuccessfulRecurringCommandUpdater.cs @@ -0,0 +1,124 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using System.Transactions; +using Dapper; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using DDD.Core.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 = null) + { + 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 = null) + { + Ensure.That(command, nameof(command)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + var cancellationToken = context?.CancellationToken() ?? default; + 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.FluentValidation/AsyncFluentCommandValidatorAdapter.cs b/Src/DDD.Core.FluentValidation/AsyncFluentCommandValidatorAdapter.cs new file mode 100644 index 0000000..04cb323 --- /dev/null +++ b/Src/DDD.Core.FluentValidation/AsyncFluentCommandValidatorAdapter.cs @@ -0,0 +1,20 @@ +using FluentValidation; + +namespace DDD.Core.Infrastructure.Validation +{ + using Application; + + public class AsyncFluentCommandValidatorAdapter : AsyncFluentValidatorAdapter, IAsyncCommandValidator + where TCommand : class, ICommand + { + + #region Constructors + + public AsyncFluentCommandValidatorAdapter(IValidator fluentValidator) : base(fluentValidator) + { + } + + #endregion Constructors + + } +} diff --git a/Src/DDD.Core.FluentValidation/AsyncFluentQueryValidatorAdapter.cs b/Src/DDD.Core.FluentValidation/AsyncFluentQueryValidatorAdapter.cs new file mode 100644 index 0000000..58bbc4f --- /dev/null +++ b/Src/DDD.Core.FluentValidation/AsyncFluentQueryValidatorAdapter.cs @@ -0,0 +1,18 @@ +using FluentValidation; + +namespace DDD.Core.Infrastructure.Validation +{ + using Application; + + public class AsyncFluentQueryValidatorAdapter : AsyncFluentValidatorAdapter, IAsyncQueryValidator + where TQuery : class, IQuery + { + #region Constructors + + public AsyncFluentQueryValidatorAdapter(IValidator fluentValidator) : base(fluentValidator) + { + } + + #endregion Constructors + } +} diff --git a/Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs b/Src/DDD.Core.FluentValidation/AsyncFluentValidatorAdapter.cs similarity index 55% rename from Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs rename to Src/DDD.Core.FluentValidation/AsyncFluentValidatorAdapter.cs index e144552..dc7eafc 100644 --- a/Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs +++ b/Src/DDD.Core.FluentValidation/AsyncFluentValidatorAdapter.cs @@ -1,56 +1,44 @@ using FluentValidation; using FluentValidation.Results; -using Conditions; +using EnsureThat; +using System.Threading; using System.Threading.Tasks; namespace DDD.Core.Infrastructure.Validation { using Mapping; - using System.Threading; using Threading; - public class FluentValidatorAdapter - : DDD.Validation.IObjectValidator, DDD.Validation.IAsyncObjectValidator + public class AsyncFluentValidatorAdapter : DDD.Validation.IAsyncObjectValidator where T : class { #region Fields - private readonly IObjectTranslator resultTranslator; private readonly IValidator fluentValidator; + private readonly IObjectTranslator resultTranslator; #endregion Fields - public FluentValidatorAdapter(IValidator fluentValidator) + #region Constructors + + public AsyncFluentValidatorAdapter(IValidator fluentValidator) { - Condition.Requires(fluentValidator, nameof(fluentValidator)).IsNotNull(); + Ensure.That(fluentValidator, nameof(fluentValidator)).IsNotNull(); this.fluentValidator = fluentValidator; this.resultTranslator = new ValidationResultTranslator(); } - #region Methods + #endregion Constructors - /// - /// Validates the specified object. - /// - /// The object to validate. - /// The rule set. - public DDD.Validation.ValidationResult Validate(T obj, string ruleSet = null) - { - ValidationResult result; - if (string.IsNullOrWhiteSpace(ruleSet)) - result = this.fluentValidator.Validate(obj); - else - result = this.fluentValidator.Validate(obj, options => options.IncludeRuleSets(ruleSet.Split(','))); - return this.resultTranslator.Translate(result, new { ObjectName = obj.GetType().Name }); - } + #region Methods /// /// Validates asynchronously the specified object. /// /// The object to validate. /// The rule set. - /// An optional cancellation token. + /// A cancellation token. public async Task ValidateAsync(T obj, string ruleSet = null, CancellationToken cancellationToken = default) { await new SynchronizationContextRemover(); @@ -58,11 +46,11 @@ public DDD.Validation.ValidationResult Validate(T obj, string ruleSet = null) if (string.IsNullOrWhiteSpace(ruleSet)) result = await this.fluentValidator.ValidateAsync(obj, cancellationToken); else - result = await this.fluentValidator.ValidateAsync(obj, options => options.IncludeRuleSets(ruleSet.Split(',')), cancellationToken); + result = await this.fluentValidator.ValidateAsync(obj, context => context.IncludeRuleSets(ruleSet.Split(',')), cancellationToken); return this.resultTranslator.Translate(result, new { ObjectName = obj.GetType().Name }); } #endregion Methods } -} \ 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 0060991..e907b2a 100644 --- a/Src/DDD.Core.FluentValidation/DDD.Core.FluentValidation.csproj +++ b/Src/DDD.Core.FluentValidation/DDD.Core.FluentValidation.csproj @@ -1,6 +1,6 @@  - net48;netstandard2.1;net5.0 + net48;netstandard2.1 Library DDD.Core.Infrastructure.Validation false @@ -24,14 +24,12 @@ - + - 10.3.3 + 11.4.0 - - 4.5.0 - + 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/FluentValidatorExtensions.cs b/Src/DDD.Core.FluentValidation/FluentValidatorExtensions.cs index ab85b2a..bfd7b7e 100644 --- a/Src/DDD.Core.FluentValidation/FluentValidatorExtensions.cs +++ b/Src/DDD.Core.FluentValidation/FluentValidatorExtensions.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using FluentValidation; using System.Collections; @@ -78,11 +78,12 @@ public static IRuleBuilderOptions Numeric(this IRuleBuilder public static IRuleBuilderOptions WithCategory(this IRuleBuilderOptions rule, string category) { - Condition.Requires(category, nameof(category)).IsNotNullOrWhiteSpace(); + 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/SyncFluentCommandValidatorAdapter.cs b/Src/DDD.Core.FluentValidation/SyncFluentCommandValidatorAdapter.cs new file mode 100644 index 0000000..9ee98df --- /dev/null +++ b/Src/DDD.Core.FluentValidation/SyncFluentCommandValidatorAdapter.cs @@ -0,0 +1,20 @@ +using FluentValidation; + +namespace DDD.Core.Infrastructure.Validation +{ + using Application; + + public class SyncFluentCommandValidatorAdapter : SyncFluentValidatorAdapter, ISyncCommandValidator + where TCommand : class, ICommand + { + + #region Constructors + + public SyncFluentCommandValidatorAdapter(IValidator fluentValidator) : base(fluentValidator) + { + } + + #endregion Constructors + + } +} diff --git a/Src/DDD.Core.FluentValidation/FluentCommandValidatorAdapter.cs b/Src/DDD.Core.FluentValidation/SyncFluentQueryValidatorAdapter.cs similarity index 51% rename from Src/DDD.Core.FluentValidation/FluentCommandValidatorAdapter.cs rename to Src/DDD.Core.FluentValidation/SyncFluentQueryValidatorAdapter.cs index ac89be7..5dd2479 100644 --- a/Src/DDD.Core.FluentValidation/FluentCommandValidatorAdapter.cs +++ b/Src/DDD.Core.FluentValidation/SyncFluentQueryValidatorAdapter.cs @@ -4,17 +4,15 @@ namespace DDD.Core.Infrastructure.Validation { using Application; - public class FluentCommandValidatorAdapter : FluentValidatorAdapter, ICommandValidator - where TCommand : class, ICommand + public class SyncFluentQueryValidatorAdapter : SyncFluentValidatorAdapter, ISyncQueryValidator + where TQuery : class, IQuery { - #region Constructors - public FluentCommandValidatorAdapter(IValidator fluentValidator) : base(fluentValidator) + public SyncFluentQueryValidatorAdapter(IValidator fluentValidator) : base(fluentValidator) { } #endregion Constructors - } } diff --git a/Src/DDD.Core.FluentValidation/SyncFluentValidatorAdapter.cs b/Src/DDD.Core.FluentValidation/SyncFluentValidatorAdapter.cs new file mode 100644 index 0000000..d6831d0 --- /dev/null +++ b/Src/DDD.Core.FluentValidation/SyncFluentValidatorAdapter.cs @@ -0,0 +1,51 @@ +using FluentValidation; +using FluentValidation.Results; +using EnsureThat; + +namespace DDD.Core.Infrastructure.Validation +{ + using Mapping; + + public class SyncFluentValidatorAdapter : DDD.Validation.ISyncObjectValidator + where T : class + { + + #region Fields + + private readonly IValidator fluentValidator; + private readonly IObjectTranslator resultTranslator; + + #endregion Fields + + #region Constructors + + public SyncFluentValidatorAdapter(IValidator fluentValidator) + { + Ensure.That(fluentValidator, nameof(fluentValidator)).IsNotNull(); + this.fluentValidator = fluentValidator; + this.resultTranslator = new ValidationResultTranslator(); + } + + #endregion Constructors + + #region Methods + + /// + /// Validates synchronously the specified object. + /// + /// The object to validate. + /// The rule set. + public DDD.Validation.ValidationResult Validate(T obj, string ruleSet = null) + { + ValidationResult result; + if (string.IsNullOrWhiteSpace(ruleSet)) + result = this.fluentValidator.Validate(obj); + else + result = this.fluentValidator.Validate(obj, context => context.IncludeRuleSets(ruleSet.Split(','))); + return this.resultTranslator.Translate(result, new { ObjectName = obj.GetType().Name }); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs b/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs index 7739c19..8dd5066 100644 --- a/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs +++ b/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs @@ -1,6 +1,6 @@ using FluentValidation.Results; using FluentValidation; -using Conditions; +using EnsureThat; using System.Linq; using System.Collections.Generic; @@ -9,18 +9,16 @@ namespace DDD.Core.Infrastructure.Validation using Mapping; internal class ValidationResultTranslator - : IObjectTranslator + : ObjectTranslator { #region Methods - public DDD.Validation.ValidationResult Translate(ValidationResult result, IDictionary options = null) + public override DDD.Validation.ValidationResult Translate(ValidationResult result, IDictionary context = null) { - Condition.Requires(result, nameof(result)).IsNotNull(); - Condition.Requires(options, nameof(options)) - .IsNotNull() - .Evaluate(options.ContainsKey("ObjectName")); - var objectName = (string)options["ObjectName"]; + Ensure.That(result, nameof(result)).IsNotNull(); + Ensure.That(context, nameof(context)).ContainsKey("ObjectName"); + 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, objectName, failures); @@ -38,14 +36,14 @@ private static string GetCustomStateInfo(object customState, string infoType) 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?.ToString(), + failure.AttemptedValue, GetCustomStateInfo(failure.CustomState, "category") ); } diff --git a/Src/DDD.Core.FluentValidation/Validators/CountValidator.cs b/Src/DDD.Core.FluentValidation/Validators/CountValidator.cs index 0bdea0d..b18dc98 100644 --- a/Src/DDD.Core.FluentValidation/Validators/CountValidator.cs +++ b/Src/DDD.Core.FluentValidation/Validators/CountValidator.cs @@ -2,7 +2,7 @@ using System.Collections; using FluentValidation; using FluentValidation.Validators; -using Conditions; +using EnsureThat; namespace DDD.Core.Infrastructure.Validation.Validators { @@ -13,9 +13,9 @@ internal class CountValidator : PropertyValidator 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; } diff --git a/Src/DDD.Core.FluentValidation/Validators/ExactCountValidator.cs b/Src/DDD.Core.FluentValidation/Validators/ExactCountValidator.cs index f32302a..f4ac2e0 100644 --- a/Src/DDD.Core.FluentValidation/Validators/ExactCountValidator.cs +++ b/Src/DDD.Core.FluentValidation/Validators/ExactCountValidator.cs @@ -19,7 +19,7 @@ public ExactCountValidator(int count) : base(count, count) #region Methods - protected override string GetDefaultMessageTemplate(string errorCode) + 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 e21afe6..6699e32 100644 --- a/Src/DDD.Core.FluentValidation/Validators/MaximumCountValidator.cs +++ b/Src/DDD.Core.FluentValidation/Validators/MaximumCountValidator.cs @@ -19,7 +19,7 @@ public MaximumCountValidator(int max) : base(0, max) #region Methods - protected override string GetDefaultMessageTemplate(string errorCode) + 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.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..82b9cc7 --- /dev/null +++ b/Src/DDD.Core.Messages/Application/RecurringCommand.cs @@ -0,0 +1,29 @@ +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; } + + #endregion Properties + + #region Methods + + public override string ToString() + => $"{this.GetType().Name} [ {nameof(CommandId)}={this.CommandId}, {nameof(CommandType)}={this.CommandType}, {nameof(RecurringExpression)}={this.RecurringExpression}]"; + + #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..093abb9 --- /dev/null +++ b/Src/DDD.Core.Messages/Application/RecurringCommandDetail.cs @@ -0,0 +1,35 @@ +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 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}]"; + + #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..1e76dbd --- /dev/null +++ b/Src/DDD.Core.Messages/Application/RegisterRecurringCommand.cs @@ -0,0 +1,28 @@ +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; } + + #endregion Properties + + #region Methods + + public override string ToString() + => $"{this.GetType().Name} [ {nameof(CommandId)}={this.CommandId}, {nameof(CommandType)}={this.CommandType}, {nameof(RecurringExpression)}={this.RecurringExpression}]"; + + #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/WriteEvents.cs b/Src/DDD.Core.Messages/Application/WriteEvents.cs new file mode 100644 index 0000000..d606d18 --- /dev/null +++ b/Src/DDD.Core.Messages/Application/WriteEvents.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace DDD.Core.Application +{ + public class WriteEvents : ICommand + { + + #region Properties + + public ICollection Events { get; set; } = new List(); + + #endregion Properties + + } +} \ 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 f25553d..04a02fd 100644 --- a/Src/DDD.Core.Messages/DDD.Core.Messages.csproj +++ b/Src/DDD.Core.Messages/DDD.Core.Messages.csproj @@ -1,6 +1,6 @@  - net48;netstandard2.1;net5.0 + net48;netstandard2.1 Library DDD.Core false @@ -11,6 +11,7 @@ + \ 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..178b496 --- /dev/null +++ b/Src/DDD.Core.Messages/Domain/BoundedContext.cs @@ -0,0 +1,76 @@ +using EnsureThat; +using System; +using System.Collections.Generic; + +namespace DDD.Core.Domain +{ + /// + /// Represents a bounded context. + /// + public 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); + } + + 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/DDD.Core.NHibernate.csproj b/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj index a425f2f..5d1ff6d 100644 --- a/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj +++ b/Src/DDD.Core.NHibernate/DDD.Core.NHibernate.csproj @@ -1,6 +1,6 @@  - net48;netstandard2.1;net5.0 + net48;netstandard2.1 Library DDD.Core.Infrastructure.Data false @@ -15,7 +15,7 @@ - + diff --git a/Src/DDD.Core.NHibernate/DelegatingSessionFactory.cs b/Src/DDD.Core.NHibernate/DelegatingSessionFactory.cs new file mode 100644 index 0000000..72aae5f --- /dev/null +++ b/Src/DDD.Core.NHibernate/DelegatingSessionFactory.cs @@ -0,0 +1,98 @@ +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, new() + { + + #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(Configuration configuration, + Action options, + Func asyncOptions) + { + 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 = new TContext(); + } + + #endregion Constructors + + #region Properties + + public TContext Context { get; } + + private ISessionFactory SessionFactory => this.lazySessionFactory.Value; + + #endregion Properties + + #region Methods + + public static ISessionFactory Create(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(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/StoredEventMapping.cs b/Src/DDD.Core.NHibernate/EventMapping.cs similarity index 81% rename from Src/DDD.Core.NHibernate/StoredEventMapping.cs rename to Src/DDD.Core.NHibernate/EventMapping.cs index 1e09888..fc3bdb2 100644 --- a/Src/DDD.Core.NHibernate/StoredEventMapping.cs +++ b/Src/DDD.Core.NHibernate/EventMapping.cs @@ -6,21 +6,22 @@ namespace DDD.Core.Infrastructure.Data { - public abstract class StoredEventMapping : ClassMapping + using Application; + + public abstract class EventMapping : ClassMapping { #region Constructors - protected StoredEventMapping() + protected EventMapping() { this.Lazy(false); // Table this.Table("Event"); // Keys - this.Id(e => e.Id, m1 => + this.Id(e => e.EventId, m1 => { m1.Column("EventId"); - m1.Generator(Generators.Sequence, m2 => m2.Params(new { sequence = "EventId" })); }); // Fields this.Property(e => e.EventType, m => @@ -29,7 +30,6 @@ protected StoredEventMapping() m.Length(250); m.NotNullable(true); }); - this.Property(e => e.Version); this.Property(e => e.OccurredOn, m => m.Precision(3)); // in milliseconds this.Property(e => e.Body, m => { @@ -38,7 +38,7 @@ protected StoredEventMapping() }); this.Property(e => e.BodyFormat, m => { - m.Type(new EnumStringType()); + m.Type(NHibernateUtil.AnsiString); m.Length(20); m.NotNullable(true); }); 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/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/NHibernateRepositoryExceptionTranslator.cs b/Src/DDD.Core.NHibernate/NHRepositoryExceptionTranslator.cs similarity index 53% rename from Src/DDD.Core.NHibernate/NHibernateRepositoryExceptionTranslator.cs rename to Src/DDD.Core.NHibernate/NHRepositoryExceptionTranslator.cs index 6461a9d..4357b3a 100644 --- a/Src/DDD.Core.NHibernate/NHibernateRepositoryExceptionTranslator.cs +++ b/Src/DDD.Core.NHibernate/NHRepositoryExceptionTranslator.cs @@ -3,43 +3,40 @@ using NHibernate.Exceptions; using System.Collections.Generic; using System.Data.Common; -using Conditions; +using EnsureThat; namespace DDD.Core.Infrastructure.Data { using Mapping; + using Collections; using Core.Domain; - internal class NHibernateRepositoryExceptionTranslator : IObjectTranslator + internal class NHRepositoryExceptionTranslator : ObjectTranslator { #region Fields - public static readonly IObjectTranslator Default = new NHibernateRepositoryExceptionTranslator(); - - private readonly IObjectTranslator dbExceptionTranslator = DbToRepositoryExceptionTranslator.Default; + private readonly IObjectTranslator dbExceptionTranslator = new DbToRepositoryExceptionTranslator(); #endregion Fields #region Methods - public RepositoryException Translate(Exception exception, IDictionary options = null) + public override RepositoryException Translate(Exception exception, IDictionary context = null) { - Condition.Requires(exception, nameof(exception)).IsNotNull(); - Condition.Requires(options, nameof(options)) - .IsNotNull() - .Evaluate(options.ContainsKey("EntityType")); - var entityType = (Type)options["EntityType"]; + Ensure.That(exception, nameof(exception)).IsNotNull(); + Type entityType = null; + context?.TryGetValue("EntityType", out entityType); switch (exception) { case DbException dbEx: - return dbExceptionTranslator.Translate(dbEx, options); + return dbExceptionTranslator.Translate(dbEx, context); case ADOException _: var dbException = ADOExceptionHelper.ExtractDbException(exception); if (dbException != null) { - options.Add("OuterException", exception); - return dbExceptionTranslator.Translate(dbException, options); + context.Add("OuterException", exception); + return dbExceptionTranslator.Translate(dbException, context); } break; } diff --git a/Src/DDD.Core.NHibernate/NHibernateRepository.cs b/Src/DDD.Core.NHibernate/NHibernateRepository.cs deleted file mode 100644 index e837c73..0000000 --- a/Src/DDD.Core.NHibernate/NHibernateRepository.cs +++ /dev/null @@ -1,90 +0,0 @@ -using Conditions; -using NHibernate; -using System; -using System.Collections.Generic; -using System.Data; -using System.Data.Common; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace DDD.Core.Infrastructure.Data -{ - using Domain; - using Mapping; - using Threading; - - public class NHibernateRepository : IAsyncRepository - where TDomainEntity : DomainEntity - where TIdentity : ComparableValueObject - { - - #region Fields - - private readonly IObjectTranslator eventTranslator; - private readonly IObjectTranslator exceptionTranslator = NHibernateRepositoryExceptionTranslator.Default; - private readonly ISession session; - - #endregion Fields - - #region Constructors - - public NHibernateRepository(ISession session, IObjectTranslator eventTranslator) - { - Condition.Requires(session, nameof(session)).IsNotNull(); - Condition.Requires(eventTranslator, nameof(eventTranslator)).IsNotNull(); - this.session = session; - this.eventTranslator = eventTranslator; - } - - #endregion Constructors - - #region Methods - - public async Task FindAsync(TIdentity identity, CancellationToken cancellationToken = default) - { - Condition.Requires(identity, nameof(identity)).IsNotNull(); - await new SynchronizationContextRemover(); - try - { - return await this.session.GetAsync(identity, cancellationToken); - } - catch (HibernateException ex) - { - throw this.exceptionTranslator.Translate(ex, new { EntityType = typeof(TDomainEntity) }); - } - } - - public async Task SaveAsync(TDomainEntity aggregate, CancellationToken cancellationToken = default) - { - var events = ToStoredEvents(aggregate); - await new SynchronizationContextRemover(); - try - { - await this.session.SaveOrUpdateAsync(aggregate, cancellationToken); - foreach (var @event in events) - await this.session.SaveAsync(@event, cancellationToken); - } - catch (HibernateException ex) - { - throw this.exceptionTranslator.Translate(ex, new { EntityType = typeof(TDomainEntity) }); - } - } - - private IEnumerable ToStoredEvents(TDomainEntity aggregate) - { - var username = Thread.CurrentPrincipal?.Identity?.Name; - return aggregate.AllEvents().Select(e => - { - var evt = this.eventTranslator.Translate(e); - evt.StreamType = aggregate.GetType().Name; - evt.StreamId = aggregate.IdentityAsString(); - evt.IssuedBy = username; - return evt; - }); - } - - #endregion Methods - - } -} diff --git a/Src/DDD.Core.NHibernate/OracleStoredEventMapping.cs b/Src/DDD.Core.NHibernate/OracleEventMapping.cs similarity index 56% rename from Src/DDD.Core.NHibernate/OracleStoredEventMapping.cs rename to Src/DDD.Core.NHibernate/OracleEventMapping.cs index 8360c35..fd5a382 100644 --- a/Src/DDD.Core.NHibernate/OracleStoredEventMapping.cs +++ b/Src/DDD.Core.NHibernate/OracleEventMapping.cs @@ -1,13 +1,12 @@ namespace DDD.Core.Infrastructure.Data { - public class OracleStoredEventMapping : StoredEventMapping + public class OracleEventMapping : EventMapping { #region Constructors - public OracleStoredEventMapping() + public OracleEventMapping() { // Fields - this.Id(e => e.Id, m => m.Column(m1 => m1.SqlType("number(19,0)"))); this.Property(e => e.Body, m => m.Column(m1 => m1.Length(4000))); } diff --git a/Src/DDD.Core.NHibernate/SqlServerStoredEventMapping.cs b/Src/DDD.Core.NHibernate/SqlServerEventMapping.cs similarity index 68% rename from Src/DDD.Core.NHibernate/SqlServerStoredEventMapping.cs rename to Src/DDD.Core.NHibernate/SqlServerEventMapping.cs index e32a7f5..69d3a2b 100644 --- a/Src/DDD.Core.NHibernate/SqlServerStoredEventMapping.cs +++ b/Src/DDD.Core.NHibernate/SqlServerEventMapping.cs @@ -1,10 +1,10 @@ namespace DDD.Core.Infrastructure.Data { - public class SqlServerStoredEventMapping : StoredEventMapping + public class SqlServerEventMapping : EventMapping { #region Constructors - public SqlServerStoredEventMapping() + public SqlServerEventMapping() { // Fields this.Property(e => e.Body, m => m.Column(m1 => m1.Length(8000))); 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 8272470..e204863 100644 --- a/Src/DDD.Core.Newtonsoft/DDD.Core.Newtonsoft.csproj +++ b/Src/DDD.Core.Newtonsoft/DDD.Core.Newtonsoft.csproj @@ -1,6 +1,6 @@  - net48;netstandard2.1;net5.0 + net48;netstandard2.1 Library DDD.Core.Infrastructure.Serialization false @@ -14,10 +14,10 @@ - + - 13.0.1 + 13.0.2 diff --git a/Src/DDD.Core.Newtonsoft/JsonSerializerWrapper.cs b/Src/DDD.Core.Newtonsoft/JsonSerializerWrapper.cs index 2e94e30..29d11d2 100644 --- a/Src/DDD.Core.Newtonsoft/JsonSerializerWrapper.cs +++ b/Src/DDD.Core.Newtonsoft/JsonSerializerWrapper.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; @@ -28,8 +28,8 @@ public JsonSerializerWrapper() public JsonSerializerWrapper(JsonSerializerSettings settings, Encoding encoding) { - Condition.Requires(settings, nameof(settings)).IsNotNull(); - Condition.Requires(encoding, nameof(encoding)).IsNotNull(); + Ensure.That(settings, nameof(settings)).IsNotNull(); + Ensure.That(encoding, nameof(encoding)).IsNotNull(); this.settings = settings; this.Encoding = encoding; } @@ -50,7 +50,7 @@ public JsonSerializerWrapper(JsonSerializerSettings settings, Encoding encoding) public static JsonSerializerWrapper Create(Encoding encoding, bool indent = true) { - Condition.Requires(encoding, nameof(encoding)).IsNotNull(); + Ensure.That(encoding, nameof(encoding)).IsNotNull(); var settings = DefaultSettings(); settings.Formatting = indent ? Formatting.Indented : Formatting.None; return new JsonSerializerWrapper(settings, encoding); @@ -61,8 +61,8 @@ public static JsonSerializerWrapper Create(Encoding encoding, bool indent = true public object Deserialize(Stream stream, Type type) { - Condition.Requires(stream, nameof(stream)).IsNotNull(); - Condition.Requires(type, nameof(type)).IsNotNull(); + 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)) { @@ -80,7 +80,7 @@ public object Deserialize(Stream stream, Type type) public void Serialize(Stream stream, object obj) { - Condition.Requires(stream, nameof(stream)).IsNotNull(); + Ensure.That(stream, nameof(stream)).IsNotNull(); using (var streamWriter = new StreamWriter(stream, this.Encoding, 1024, true)) using (var jsonWriter = new JsonTextWriter(streamWriter)) { @@ -111,4 +111,4 @@ private static JsonSerializerSettings DefaultSettings() #endregion Methods } -} \ No newline at end of file +} 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.Polly/AsyncPollyCommandHandler.cs b/Src/DDD.Core.Polly/AsyncPollyCommandHandler.cs index 6506fb3..55dcaef 100644 --- a/Src/DDD.Core.Polly/AsyncPollyCommandHandler.cs +++ b/Src/DDD.Core.Polly/AsyncPollyCommandHandler.cs @@ -1,7 +1,6 @@ -using System.Threading; -using System.Threading.Tasks; +using System.Threading.Tasks; using Polly; -using Conditions; +using EnsureThat; namespace DDD.Core.Infrastructure.ErrorHandling { @@ -25,8 +24,8 @@ public class AsyncPollyCommandHandler : IAsyncCommandHandler public AsyncPollyCommandHandler(IAsyncCommandHandler handler, IAsyncPolicy policy) { - Condition.Requires(handler, nameof(handler)).IsNotNull(); - Condition.Requires(policy, nameof(policy)).IsNotNull(); + Ensure.That(handler, nameof(handler)).IsNotNull(); + Ensure.That(policy, nameof(policy)).IsNotNull(); this.handler = handler; this.policy = policy; } @@ -35,9 +34,9 @@ public AsyncPollyCommandHandler(IAsyncCommandHandler handler, IAsyncPo #region Methods - public async Task HandleAsync(TCommand command, CancellationToken cancellationToken = default) + public async Task HandleAsync(TCommand command, IMessageContext context = null) { - await policy.ExecuteAsync(() => this.handler.HandleAsync(command, cancellationToken)); + await policy.ExecuteAsync(() => this.handler.HandleAsync(command, context)); } #endregion Methods diff --git a/Src/DDD.Core.Polly/AsyncPollyCommandHandler`1.cs b/Src/DDD.Core.Polly/AsyncPollyCommandHandler`1.cs new file mode 100644 index 0000000..6639d9b --- /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 DDD.Core.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 = null) + { + 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 index 6ca0a3b..029569a 100644 --- a/Src/DDD.Core.Polly/AsyncPollyQueryHandler.cs +++ b/Src/DDD.Core.Polly/AsyncPollyQueryHandler.cs @@ -1,6 +1,5 @@ using Polly; -using Conditions; -using System.Threading; +using EnsureThat; using System.Threading.Tasks; namespace DDD.Core.Infrastructure.ErrorHandling @@ -25,8 +24,8 @@ public class AsyncPollyQueryHandler : IAsyncQueryHandler handler, IAsyncPolicy policy) { - Condition.Requires(handler, nameof(handler)).IsNotNull(); - Condition.Requires(policy, nameof(policy)).IsNotNull(); + Ensure.That(handler, nameof(handler)).IsNotNull(); + Ensure.That(policy, nameof(policy)).IsNotNull(); this.handler = handler; this.policy = policy; } @@ -35,9 +34,9 @@ public AsyncPollyQueryHandler(IAsyncQueryHandler handler, IAsyn #region Methods - public async Task HandleAsync(TQuery query, CancellationToken cancellationToken = default) + public async Task HandleAsync(TQuery query, IMessageContext context = null) { - return await policy.ExecuteAsync(() => this.handler.HandleAsync(query, cancellationToken)); + return await policy.ExecuteAsync(() => this.handler.HandleAsync(query, context)); } #endregion Methods diff --git a/Src/DDD.Core.Polly/AsyncPollyQueryHandler`1.cs b/Src/DDD.Core.Polly/AsyncPollyQueryHandler`1.cs new file mode 100644 index 0000000..991d091 --- /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 DDD.Core.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 = null) + { + 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 index 2e336fe..3a21cab 100644 --- a/Src/DDD.Core.Polly/DDD.Core.Polly.csproj +++ b/Src/DDD.Core.Polly/DDD.Core.Polly.csproj @@ -1,6 +1,6 @@  - net48;netstandard2.1;net5.0 + net48;netstandard2.1 Library DDD.Core.Infrastructure.ErrorHandling false @@ -23,10 +23,10 @@ - + - 7.2.2 + 7.2.3 \ No newline at end of file diff --git a/Src/DDD.Core.Polly/PollyCommandHandler.cs b/Src/DDD.Core.Polly/PollyCommandHandler.cs deleted file mode 100644 index 59db7ba..0000000 --- a/Src/DDD.Core.Polly/PollyCommandHandler.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Polly; -using Conditions; - -namespace DDD.Core.Infrastructure.ErrorHandling -{ - using Application; - - /// - /// A decorator that applies a resilience policy to the synchronous execution of a command. - /// - public class PollyCommandHandler : ICommandHandler - where TCommand : class, ICommand - { - - #region Fields - - private readonly ICommandHandler handler; - private readonly ISyncPolicy policy; - - #endregion Fields - - #region Constructors - -#pragma warning disable CS3001 // Argument type is not CLS-compliant - public PollyCommandHandler(ICommandHandler handler, ISyncPolicy policy) -#pragma warning restore CS3001 // Argument type is not CLS-compliant - { - Condition.Requires(handler, nameof(handler)).IsNotNull(); - Condition.Requires(policy, nameof(policy)).IsNotNull(); - this.handler = handler; - this.policy = policy; - } - - #endregion Constructors - - #region Methods - - public void Handle(TCommand command) - { - policy.Execute(() => this.handler.Handle(command)); - } - - #endregion Methods - - } -} \ No newline at end of file diff --git a/Src/DDD.Core.Polly/Properties/AssemblyInfo.cs b/Src/DDD.Core.Polly/Properties/AssemblyInfo.cs index 5af017e..f618d70 100644 --- a/Src/DDD.Core.Polly/Properties/AssemblyInfo.cs +++ b/Src/DDD.Core.Polly/Properties/AssemblyInfo.cs @@ -2,6 +2,6 @@ using System.Runtime.InteropServices; [assembly: AssemblyTitle("DDD.Core.Polly")] -[assembly: AssemblyDescription("Useful decorators for error handling based on the Polly library.")] +[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..e7d2fee --- /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 = null) + { + 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..b7a6940 --- /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 DDD.Core.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 = null) + { + policy.Execute(() => this.handler.Handle(command, context)); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.Polly/PollyQueryHandler.cs b/Src/DDD.Core.Polly/SyncPollyQueryHandler.cs similarity index 53% rename from Src/DDD.Core.Polly/PollyQueryHandler.cs rename to Src/DDD.Core.Polly/SyncPollyQueryHandler.cs index 9858d5e..34728c6 100644 --- a/Src/DDD.Core.Polly/PollyQueryHandler.cs +++ b/Src/DDD.Core.Polly/SyncPollyQueryHandler.cs @@ -1,5 +1,5 @@ using Polly; -using Conditions; +using EnsureThat; namespace DDD.Core.Infrastructure.ErrorHandling { @@ -8,25 +8,23 @@ namespace DDD.Core.Infrastructure.ErrorHandling /// /// A decorator that applies a resilience policy to the synchronous execution of a query. /// - public class PollyQueryHandler : IQueryHandler + public class SyncPollyQueryHandler : ISyncQueryHandler where TQuery : class, IQuery { #region Fields - private readonly IQueryHandler handler; + private readonly ISyncQueryHandler handler; private readonly ISyncPolicy policy; #endregion Fields #region Constructors -#pragma warning disable CS3001 // Argument type is not CLS-compliant - public PollyQueryHandler(IQueryHandler handler, ISyncPolicy policy) -#pragma warning restore CS3001 // Argument type is not CLS-compliant + public SyncPollyQueryHandler(ISyncQueryHandler handler, ISyncPolicy policy) { - Condition.Requires(handler, nameof(handler)).IsNotNull(); - Condition.Requires(policy, nameof(policy)).IsNotNull(); + Ensure.That(handler, nameof(handler)).IsNotNull(); + Ensure.That(policy, nameof(policy)).IsNotNull(); this.handler = handler; this.policy = policy; } @@ -35,9 +33,9 @@ public PollyQueryHandler(IQueryHandler handler, ISyncPolicy pol #region Methods - public TResult Handle(TQuery query) + public TResult Handle(TQuery query, IMessageContext context = null) { - return policy.Execute(() => this.handler.Handle(query)); + return policy.Execute(() => this.handler.Handle(query, context)); } #endregion Methods diff --git a/Src/DDD.Core.Polly/SyncPollyQueryHandler`1.cs b/Src/DDD.Core.Polly/SyncPollyQueryHandler`1.cs new file mode 100644 index 0000000..3dcf394 --- /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 DDD.Core.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 = null) + { + return policy.Execute(() => this.handler.Handle(query, context)); + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs index 3ad50bc..a94670c 100644 --- a/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs @@ -1,13 +1,13 @@ using SimpleInjector; using SimpleInjector.Lifestyles; -using Conditions; -using System.Threading; -using System.Threading.Tasks; +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 command. @@ -27,8 +27,8 @@ public class AsyncScopedCommandHandler : IAsyncCommandHandler> handlerProvider, Container container) { - Condition.Requires(handlerProvider, nameof(handlerProvider)).IsNotNull(); - Condition.Requires(container, nameof(container)).IsNotNull(); + Ensure.That(handlerProvider, nameof(handlerProvider)).IsNotNull(); + Ensure.That(container, nameof(container)).IsNotNull(); this.handlerProvider = handlerProvider; this.container = container; } @@ -37,12 +37,13 @@ public AsyncScopedCommandHandler(Func> handlerPro #region Methods - public async Task HandleAsync(TCommand command, CancellationToken cancellationToken = default) + public async Task HandleAsync(TCommand command, IMessageContext context = null) { using (AsyncScopedLifestyle.BeginScope(container)) { + await new SynchronizationContextRemover(); var handler = this.handlerProvider(); - await handler.HandleAsync(command, cancellationToken); + await handler.HandleAsync(command, context); } } diff --git a/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler`1.cs b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler`1.cs new file mode 100644 index 0000000..e5d3881 --- /dev/null +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler`1.cs @@ -0,0 +1,63 @@ +using SimpleInjector; +using SimpleInjector.Lifestyles; +using EnsureThat; +using System; +using System.Threading.Tasks; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Application; + using DDD.Core.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, new() + { + + #region Fields + + private readonly Container container; + private readonly TContext context; + 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; + this.context = new TContext(); + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.context; + + #endregion Properties + + #region Methods + + public async Task HandleAsync(TCommand command, IMessageContext context = null) + { + using (AsyncScopedLifestyle.BeginScope(container)) + { + await new SynchronizationContextRemover(); + 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 index f4776ad..2edad9f 100644 --- a/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs @@ -1,13 +1,13 @@ using SimpleInjector; using SimpleInjector.Lifestyles; -using Conditions; -using System.Threading; -using System.Threading.Tasks; +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. @@ -27,8 +27,8 @@ public class AsyncScopedQueryHandler : IAsyncQueryHandler> handlerProvider, Container container) { - Condition.Requires(handlerProvider, nameof(handlerProvider)).IsNotNull(); - Condition.Requires(container, nameof(container)).IsNotNull(); + Ensure.That(handlerProvider, nameof(handlerProvider)).IsNotNull(); + Ensure.That(container, nameof(container)).IsNotNull(); this.handlerProvider = handlerProvider; this.container = container; } @@ -37,12 +37,13 @@ public AsyncScopedQueryHandler(Func> handler #region Methods - public async Task HandleAsync(TQuery query, CancellationToken cancellationToken = default) + public async Task HandleAsync(TQuery query, IMessageContext context = null) { using (AsyncScopedLifestyle.BeginScope(container)) { + await new SynchronizationContextRemover(); var handler = this.handlerProvider(); - return await handler.HandleAsync(query, cancellationToken); + return await handler.HandleAsync(query, context); } } diff --git a/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler`1.cs b/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler`1.cs new file mode 100644 index 0000000..02acd6f --- /dev/null +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler`1.cs @@ -0,0 +1,63 @@ +using SimpleInjector; +using SimpleInjector.Lifestyles; +using EnsureThat; +using System; +using System.Threading.Tasks; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Application; + using DDD.Core.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, new() + { + + #region Fields + + private readonly Container container; + private readonly TContext context; + 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; + this.context = new TContext(); + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.context; + + #endregion Properties + + #region Methods + + public async Task HandleAsync(TQuery query, IMessageContext context = null) + { + using (AsyncScopedLifestyle.BeginScope(container)) + { + await new SynchronizationContextRemover(); + 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 index 9e98d3e..e5b1dc7 100644 --- a/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs +++ b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs @@ -3,12 +3,14 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using Conditions; +using EnsureThat; using DDD.DependencyInjection; namespace DDD.Core.Infrastructure.DependencyInjection { + using Mapping; using Core.Application; + using Core.Domain; public static class ContainerExtensions { @@ -17,39 +19,175 @@ public static class ContainerExtensions public static TService GetNamedInstance(this Container container, string name) where TService : class { - Condition.Requires(container, nameof(container)).IsNotNull(); + Ensure.That(container, nameof(container)).IsNotNull(); var provider = container.GetInstance>(); return provider.GetService(name); } + public static void RegisterCommandProcessors(this Container container) + { + Ensure.That(container, nameof(container)).IsNotNull(); + container.RegisterSingleton(typeof(IContextualCommandProcessor<>), typeof(ContextualCommandProcessor<>)); + container.RegisterSingleton(); + } + + public static void RegisterCommandHandlers(this Container container, IEnumerable assemblies, Func predicate) + { + Ensure.That(container, nameof(container)).IsNotNull(); + var context = new TypesToRegisterOptions { IncludeGenericTypeDefinitions = true }; + container.RegisterConditional(typeof(ISyncCommandHandler<,>), assemblies, predicate, context); + container.RegisterConditional(typeof(IAsyncCommandHandler<,>), assemblies, predicate, context); + container.RegisterDecorator(typeof(ISyncCommandHandler<,>), typeof(SyncCommandHandlerWithLogging<,>)); + container.RegisterDecorator(typeof(IAsyncCommandHandler<,>), typeof(AsyncCommandHandlerWithLogging<,>)); + container.RegisterDecorator(typeof(ISyncCommandHandler<,>), typeof(ThreadScopedCommandHandler<,>), Lifestyle.Singleton); + container.RegisterDecorator(typeof(IAsyncCommandHandler<,>), typeof(AsyncScopedCommandHandler<,>), Lifestyle.Singleton); + container.RegisterConditional(typeof(ISyncCommandHandler<>), assemblies, predicate); + container.RegisterConditional(typeof(IAsyncCommandHandler<>), assemblies, predicate); + container.RegisterDecorator(typeof(ISyncCommandHandler<>), typeof(SyncCommandHandlerWithLogging<>)); + container.RegisterDecorator(typeof(IAsyncCommandHandler<>), typeof(AsyncCommandHandlerWithLogging<>)); + container.RegisterDecorator(typeof(ISyncCommandHandler<>), typeof(ThreadScopedCommandHandler<>), Lifestyle.Singleton); + container.RegisterDecorator(typeof(IAsyncCommandHandler<>), typeof(AsyncScopedCommandHandler<>), Lifestyle.Singleton); + } + + public static void RegisterRecurringCommandManager(this Container container, RecurringCommandManagerSettings settings) + where TContext : BoundedContext, new() + { + container.RegisterInstance(settings); + container.RegisterSingleton, RecurringCommandManager>(); + container.Collection.Append>(Lifestyle.Singleton); + } + + public static void RegisterQueryProcessors(this Container container) + { + Ensure.That(container, nameof(container)).IsNotNull(); + container.RegisterSingleton(typeof(IContextualQueryProcessor<>), typeof(ContextualQueryProcessor<>)); + container.RegisterSingleton(); + } + + public static void RegisterQueryHandlers(this Container container, IEnumerable assemblies, Func predicate) + { + Ensure.That(container, nameof(container)).IsNotNull(); + var context = new TypesToRegisterOptions { IncludeGenericTypeDefinitions = true }; + container.RegisterConditional(typeof(ISyncQueryHandler<,,>), assemblies, predicate, context); + container.RegisterConditional(typeof(IAsyncQueryHandler<,,>), assemblies, predicate, context); + container.RegisterDecorator(typeof(ISyncQueryHandler<,,>), typeof(SyncQueryHandlerWithLogging<,,>)); + container.RegisterDecorator(typeof(IAsyncQueryHandler<,,>), typeof(AsyncQueryHandlerWithLogging<,,>)); + container.RegisterDecorator(typeof(ISyncQueryHandler<,,>), typeof(ThreadScopedQueryHandler<,,>), Lifestyle.Singleton); + container.RegisterDecorator(typeof(IAsyncQueryHandler<,,>), typeof(AsyncScopedQueryHandler<,,>), Lifestyle.Singleton); + container.RegisterConditional(typeof(ISyncQueryHandler<,>), assemblies, predicate); + container.RegisterConditional(typeof(IAsyncQueryHandler<,>), assemblies, predicate); + container.RegisterDecorator(typeof(ISyncQueryHandler<,>), typeof(SyncQueryHandlerWithLogging<,>)); + container.RegisterDecorator(typeof(IAsyncQueryHandler<,>), typeof(AsyncQueryHandlerWithLogging<,>)); + container.RegisterDecorator(typeof(ISyncQueryHandler<,>), typeof(ThreadScopedQueryHandler<,>), Lifestyle.Singleton); + container.RegisterDecorator(typeof(IAsyncQueryHandler<,>), typeof(AsyncScopedQueryHandler<,>), Lifestyle.Singleton); + } + + public static void RegisterEventPublisher(this Container container) + { + Ensure.That(container, nameof(container)).IsNotNull(); + container.RegisterSingleton(); + } + + public static void RegisterEventHandlers(this Container container, IEnumerable assemblies, Func predicate) + { + Ensure.That(container, nameof(container)).IsNotNull(); + var context = new TypesToRegisterOptions { IncludeGenericTypeDefinitions = true, IncludeComposites = false }; + var syncHandlerTypes = container.GetTypesToRegister(typeof(ISyncEventHandler<>), assemblies, context) + .Where(predicate); + container.Collection.Register(typeof(ISyncEventHandler<>), syncHandlerTypes); + var asyncHandlerTypes = container.GetTypesToRegister(typeof(IAsyncEventHandler<>), assemblies, context) + .Where(predicate); + container.Collection.Register(typeof(IAsyncEventHandler<>), asyncHandlerTypes); + container.RegisterDecorator(typeof(ISyncEventHandler<>), typeof(SyncEventHandlerWithLogging<>)); + container.RegisterDecorator(typeof(IAsyncEventHandler<>), typeof(AsyncEventHandlerWithLogging<>)); + } + + public static void RegisterEventConsumer(this Container container, EventConsumerSettings settings) + where TContext : BoundedContext, new() + { + container.RegisterInstance(settings); + container.RegisterSingleton, EventConsumer>(); + container.Collection.Append>(Lifestyle.Singleton); + } + + public static void RegisterMappingProcessor(this Container container) + { + Ensure.That(container, nameof(container)).IsNotNull(); + container.RegisterSingleton(); + } + + public static void RegisterMappersAndTranslators(this Container container, IEnumerable assemblies, Func predicate) + { + Ensure.That(container, nameof(container)).IsNotNull(); + container.RegisterConditional(typeof(IObjectMapper<,>), assemblies, predicate); + container.RegisterConditional(typeof(IObjectTranslator<,>), assemblies, predicate); + } + + public static void RegisterRepositories(this Container container, IEnumerable assemblies, Func predicate) + { + Ensure.That(container, nameof(container)).IsNotNull(); + container.RegisterConditional(typeof(ISyncRepository<,>), assemblies, predicate); + container.RegisterConditional(typeof(IAsyncRepository<,>), assemblies, predicate); + } + + 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 { - Condition.Requires(container, nameof(container)).IsNotNull(); - var registration = Lifestyle.Transient.CreateRegistration(instanceCreator, container); - container.RegisterConditional(registration, predicate); + Ensure.That(container, nameof(container)).IsNotNull(); + container.RegisterConditional(instanceCreator, Lifestyle.Transient, predicate); } - public static void RegisterConditional(this Container container, Type openGenericServiceType, IEnumerable assemblies, Func predicate) + public static void RegisterConditional(this Container container, + Type openGenericServiceType, + IEnumerable assemblies, + Lifestyle lifestyle, + Func predicate, + TypesToRegisterOptions context) { - Condition.Requires(container, nameof(container)).IsNotNull(); - var implementationTypes = container.GetTypesToRegister(openGenericServiceType, assemblies) + Ensure.That(container, nameof(container)).IsNotNull(); + var implementationTypes = container.GetTypesToRegister(openGenericServiceType, assemblies, context) .Where(predicate); - container.Register(openGenericServiceType, implementationTypes); + foreach (var implementationType in implementationTypes) + container.Register(openGenericServiceType, implementationType, lifestyle); } - public static void RegisterEventHandlers(this Container container, params Assembly[] assemblies) + public static void RegisterConditional(this Container container, + Type openGenericServiceType, + IEnumerable assemblies, + Lifestyle lifestyle, + Func predicate) { - Condition.Requires(container, nameof(container)).IsNotNull(); - var handlerTypes = container.GetTypesToRegister(typeof(IEventHandler<>), assemblies, new TypesToRegisterOptions - { - IncludeGenericTypeDefinitions = true, - IncludeComposites = false, - }); - container.Collection.Register(typeof(IEventHandler<>), handlerTypes); + Ensure.That(container, nameof(container)).IsNotNull(); + container.RegisterConditional(openGenericServiceType, assemblies, lifestyle, predicate, new TypesToRegisterOptions()); } - #endregion Methods + public static void RegisterConditional(this Container container, + Type openGenericServiceType, + IEnumerable assemblies, + Func predicate) + { + Ensure.That(container, nameof(container)).IsNotNull(); + container.RegisterConditional(openGenericServiceType, assemblies, Lifestyle.Transient, predicate); + } + public static void RegisterConditional(this Container container, + Type openGenericServiceType, + IEnumerable assemblies, + Func predicate, + TypesToRegisterOptions context) + { + Ensure.That(container, nameof(container)).IsNotNull(); + container.RegisterConditional(openGenericServiceType, assemblies, Lifestyle.Transient, predicate, context); + } + + #endregion Methods } } diff --git a/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj b/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj index 8b6b7f4..fec7f8b 100644 --- a/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj +++ b/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj @@ -1,6 +1,6 @@  - net48;netstandard2.1;net5.0 + net48;netstandard2.1 Library DDD.Core.Infrastructure.DependencyInjection false @@ -24,10 +24,10 @@ - + - 5.3.2 + 5.4.1 \ No newline at end of file diff --git a/Src/DDD.Core.SimpleInjector/KeyedServiceProvider.cs b/Src/DDD.Core.SimpleInjector/KeyedServiceProvider.cs index 67af283..0f9d4a6 100644 --- a/Src/DDD.Core.SimpleInjector/KeyedServiceProvider.cs +++ b/Src/DDD.Core.SimpleInjector/KeyedServiceProvider.cs @@ -1,6 +1,6 @@ using DDD.DependencyInjection; using SimpleInjector; -using Conditions; +using EnsureThat; using System.Collections.Generic; using System; @@ -21,7 +21,7 @@ public class KeyedServiceProvider : IKeyedServiceProvider keyComparer = null) { - Condition.Requires(container, nameof(container)).IsNotNull(); + Ensure.That(container, nameof(container)).IsNotNull(); this.container = container; this.producers = new Dictionary>(keyComparer); } @@ -40,16 +40,16 @@ public TService GetService(TKey key) public void Register(TKey key, Lifestyle lifestyle) where TImplementation : class, TService { - Condition.Requires(lifestyle, nameof(lifestyle)).IsNotNull(); + Ensure.That(lifestyle, nameof(lifestyle)).IsNotNull(); var producer = lifestyle.CreateProducer(container); this.producers.Add(key, producer); } public void Register(TKey key, Func instanceCreator, Lifestyle lifestyle) { - Condition.Requires(instanceCreator, nameof(instanceCreator)).IsNotNull(); - Condition.Requires(lifestyle, nameof(lifestyle)).IsNotNull(); - var producer = lifestyle.CreateProducer(instanceCreator, container); + Ensure.That(instanceCreator, nameof(instanceCreator)).IsNotNull(); + Ensure.That(lifestyle, nameof(lifestyle)).IsNotNull(); + var producer = lifestyle.CreateProducer(instanceCreator, container); this.producers.Add(key, producer); } diff --git a/Src/DDD.Core.SimpleInjector/Properties/AssemblyInfo.cs b/Src/DDD.Core.SimpleInjector/Properties/AssemblyInfo.cs index d89334f..043d4e5 100644 --- a/Src/DDD.Core.SimpleInjector/Properties/AssemblyInfo.cs +++ b/Src/DDD.Core.SimpleInjector/Properties/AssemblyInfo.cs @@ -2,6 +2,6 @@ using System.Runtime.InteropServices; [assembly: AssemblyTitle("DDD.Core.SimpleInjector")] -[assembly: AssemblyDescription("Useful decorators for dependency injection.")] +[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/ThreadScopedCommandHandler.cs b/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs index ae58ccf..bad7d15 100644 --- a/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs +++ b/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs @@ -1,6 +1,6 @@ using SimpleInjector; using SimpleInjector.Lifestyles; -using Conditions; +using EnsureThat; using System; namespace DDD.Core.Infrastructure.DependencyInjection @@ -10,23 +10,23 @@ namespace DDD.Core.Infrastructure.DependencyInjection /// /// A decorator that defines a scope around the synchronous execution of a command. /// - public class ThreadScopedCommandHandler : ICommandHandler + public class ThreadScopedCommandHandler : ISyncCommandHandler where TCommand : class, ICommand { #region Fields private readonly Container container; - private readonly Func> handlerProvider; + private readonly Func> handlerProvider; #endregion Fields #region Constructors - public ThreadScopedCommandHandler(Func> handlerProvider, Container container) + public ThreadScopedCommandHandler(Func> handlerProvider, Container container) { - Condition.Requires(handlerProvider, nameof(handlerProvider)).IsNotNull(); - Condition.Requires(container, nameof(container)).IsNotNull(); + Ensure.That(handlerProvider, nameof(handlerProvider)).IsNotNull(); + Ensure.That(container, nameof(container)).IsNotNull(); this.handlerProvider = handlerProvider; this.container = container; } @@ -35,16 +35,16 @@ public ThreadScopedCommandHandler(Func> handlerProvide #region Methods - public void Handle(TCommand command) + public void Handle(TCommand command, IMessageContext context = null) { using (ThreadScopedLifestyle.BeginScope(container)) { var handler = this.handlerProvider(); - handler.Handle(command); + handler.Handle(command, context); } } #endregion Methods } -} \ No newline at end of file +} diff --git a/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler`1.cs b/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler`1.cs new file mode 100644 index 0000000..f0845a6 --- /dev/null +++ b/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler`1.cs @@ -0,0 +1,60 @@ +using SimpleInjector; +using SimpleInjector.Lifestyles; +using EnsureThat; +using System; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Application; + using DDD.Core.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, new() + { + + #region Fields + + private readonly Container container; + private readonly TContext context; + 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; + this.context = new TContext(); + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.context; + + #endregion Properties + + #region Methods + + public void Handle(TCommand command, IMessageContext context = null) + { + 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 index 9e4bf08..0ec79e8 100644 --- a/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler.cs +++ b/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler.cs @@ -1,6 +1,6 @@ using SimpleInjector; using SimpleInjector.Lifestyles; -using Conditions; +using EnsureThat; using System; namespace DDD.Core.Infrastructure.DependencyInjection @@ -10,23 +10,23 @@ namespace DDD.Core.Infrastructure.DependencyInjection /// /// A decorator that defines a scope around the synchronous execution of a query. /// - public class ThreadScopedQueryHandler : IQueryHandler + public class ThreadScopedQueryHandler : ISyncQueryHandler where TQuery : class, IQuery { #region Fields private readonly Container container; - private readonly Func> handlerProvider; + private readonly Func> handlerProvider; #endregion Fields #region Constructors - public ThreadScopedQueryHandler(Func> handlerProvider, Container container) + public ThreadScopedQueryHandler(Func> handlerProvider, Container container) { - Condition.Requires(handlerProvider, nameof(handlerProvider)).IsNotNull(); - Condition.Requires(container, nameof(container)).IsNotNull(); + Ensure.That(handlerProvider, nameof(handlerProvider)).IsNotNull(); + Ensure.That(container, nameof(container)).IsNotNull(); this.handlerProvider = handlerProvider; this.container = container; } @@ -35,16 +35,16 @@ public ThreadScopedQueryHandler(Func> handlerProv #region Methods - public TResult Handle(TQuery query) + public TResult Handle(TQuery query, IMessageContext context = null) { using (ThreadScopedLifestyle.BeginScope(container)) { var handler = this.handlerProvider(); - return handler.Handle(query); + return handler.Handle(query, context); } } #endregion Methods } -} \ No newline at end of file +} diff --git a/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler`1.cs b/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler`1.cs new file mode 100644 index 0000000..61dc67f --- /dev/null +++ b/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler`1.cs @@ -0,0 +1,60 @@ +using SimpleInjector; +using SimpleInjector.Lifestyles; +using EnsureThat; +using System; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Application; + using DDD.Core.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, new() + { + + #region Fields + + private readonly Container container; + private readonly TContext context; + 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; + this.context = new TContext(); + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.context; + + #endregion Properties + + #region Methods + + public TResult Handle(TQuery query, IMessageContext context = null) + { + using (ThreadScopedLifestyle.BeginScope(container)) + { + var handler = this.handlerProvider(); + return handler.Handle(query, context); + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj b/Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj index 2fe72bc..91638eb 100644 --- a/Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj +++ b/Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj @@ -1,6 +1,6 @@  - net48;netstandard2.1;net5.0 + net48;netstandard2.1 Library DDD.Core.Infrastructure.Testing false @@ -22,10 +22,17 @@ - + + - 2.4.1 + 2.4.2 + + + + + + \ No newline at end of file diff --git a/Src/DDD.Core.Xunit/DbFixture.cs b/Src/DDD.Core.Xunit/DbFixture.cs index 57ecb5e..4d48c8b 100644 --- a/Src/DDD.Core.Xunit/DbFixture.cs +++ b/Src/DDD.Core.Xunit/DbFixture.cs @@ -1,71 +1,102 @@ -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 Application; + using DDD.Core.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.RegisterDbProviderFactory(); + 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); + return new LazyDbConnectionProvider(connectionSettings.ProviderName, connectionString); + } + + 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 virtual void RegisterDbProviderFactory() + 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 + } } \ No newline at end of file diff --git a/Src/DDD.Core.Xunit/IDbFixture.cs b/Src/DDD.Core.Xunit/IDbFixture.cs index 47c7855..1e3b7fe 100644 --- a/Src/DDD.Core.Xunit/IDbFixture.cs +++ b/Src/DDD.Core.Xunit/IDbFixture.cs @@ -1,18 +1,20 @@ -namespace DDD.Core.Infrastructure.Testing +using System.Data.Common; + +namespace DDD.Core.Infrastructure.Testing { using Data; + using Application; + using DDD.Core.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..899106c --- /dev/null +++ b/Src/DDD.Core.Xunit/IDbFixtureExtensions.cs @@ -0,0 +1,39 @@ +using EnsureThat; +using System.Data.Common; +using System.Threading.Tasks; +using System.Threading; + +namespace DDD.Core.Infrastructure.Testing +{ + using Application; + using DDD.Core.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/Application/ApplicationException.cs b/Src/DDD.Core/Application/ApplicationException.cs index ad82799..96e2c8b 100644 --- a/Src/DDD.Core/Application/ApplicationException.cs +++ b/Src/DDD.Core/Application/ApplicationException.cs @@ -5,49 +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(bool isTransient, Exception innerException = null) - : base(DefaultMessage(), innerException) + : base(isTransient, DefaultMessage(), innerException) { - this.IsTransient = isTransient; } protected ApplicationException(bool isTransient, string message, Exception innerException = null) - : base(message, innerException) + : base(isTransient, message, innerException) { - this.IsTransient = isTransient; } #endregion Constructors - #region Properties - - /// - /// Gets a value indicating whether the exception is transient. - /// - public bool IsTransient { get; } - - #endregion Properties - #region Methods public static string DefaultMessage() => "An error occurred in the application layer."; - public override string ToString() - { - var s = $"{this.GetType()}: {this.Message} "; - s += $"{Environment.NewLine}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/Application/AsyncCommandHandlerWithLogging.cs b/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging.cs index 2354c1e..e803e03 100644 --- a/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging.cs +++ b/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging.cs @@ -1,7 +1,6 @@ -using Conditions; +using EnsureThat; using Microsoft.Extensions.Logging; using System.Diagnostics; -using System.Threading; using System.Threading.Tasks; namespace DDD.Core.Application @@ -25,8 +24,8 @@ public class AsyncCommandHandlerWithLogging : IAsyncCommandHandler commandHandler, ILogger logger) { - Condition.Requires(commandHandler, nameof(commandHandler)).IsNotNull(); - Condition.Requires(logger, nameof(logger)).IsNotNull(); + Ensure.That(commandHandler, nameof(commandHandler)).IsNotNull(); + Ensure.That(logger, nameof(logger)).IsNotNull(); this.commandHandler = commandHandler; this.logger = logger; } @@ -35,18 +34,18 @@ public AsyncCommandHandlerWithLogging(IAsyncCommandHandler commandHand #region Methods - public async Task HandleAsync(TCommand command, CancellationToken cancellationToken = default) + public async Task HandleAsync(TCommand command, IMessageContext context = null) { - if (this.logger.IsEnabled(LogLevel.Information)) + if (this.logger.IsEnabled(LogLevel.Debug)) { - this.logger.LogInformation("Executing command {Command}.", command); + 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, cancellationToken); + await this.commandHandler.HandleAsync(command, context); stopWatch.Stop(); - this.logger.LogInformation("Command executed in {CommandExecutionTime} ms.", stopWatch.ElapsedMilliseconds); + 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, cancellationToken); + 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..c4d0768 --- /dev/null +++ b/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging`1.cs @@ -0,0 +1,61 @@ +using EnsureThat; +using DDD.Core.Domain; +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 + 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 = null) + { + 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 0269812..0000000 --- a/Src/DDD.Core/Application/AsyncDomainCommandHandler.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Conditions; -using System.Threading; -using System.Threading.Tasks; - -namespace DDD.Core.Application -{ - using Domain; - using Mapping; - 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 Fields - - private readonly IObjectTranslator exceptionTranslator = DomainToCommandExceptionTranslator.Default; - - #endregion Fields - - #region Methods - - public async Task HandleAsync(TCommand command, CancellationToken cancellationToken = default) - { - Condition.Requires(command, nameof(command)).IsNotNull(); - await new SynchronizationContextRemover(); - try - { - await this.ExecuteAsync(command, cancellationToken); - } - catch (DomainException ex) - { - throw this.exceptionTranslator.Translate(ex, new { Command = command }); - } - } - - protected abstract Task ExecuteAsync(TCommand command, CancellationToken cancellationToken = default); - - #endregion Methods - - } -} \ No newline at end of file diff --git a/Src/DDD.Core/Application/AsyncEventHandler.cs b/Src/DDD.Core/Application/AsyncEventHandler.cs index 52b1f2b..9fc0910 100644 --- a/Src/DDD.Core/Application/AsyncEventHandler.cs +++ b/Src/DDD.Core/Application/AsyncEventHandler.cs @@ -1,5 +1,4 @@ using System; -using System.Threading; using System.Threading.Tasks; namespace DDD.Core.Application @@ -10,8 +9,9 @@ namespace DDD.Core.Application /// Base class for handling asynchronously events. /// public abstract class AsyncEventHandler : IAsyncEventHandler - where TEvent : class, IEvent + where TEvent : class, IEvent { + #region Properties Type IAsyncEventHandler.EventType => typeof(TEvent); @@ -20,10 +20,11 @@ public abstract class AsyncEventHandler : IAsyncEventHandler #region Methods - public abstract Task HandleAsync(TEvent @event, CancellationToken cancellationToken = default); + public abstract Task HandleAsync(TEvent @event, IMessageContext context = null); - Task IAsyncEventHandler.HandleAsync(IEvent @event, CancellationToken cancellationToken) => this.HandleAsync((TEvent)@event, cancellationToken); + 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..c344700 --- /dev/null +++ b/Src/DDD.Core/Application/AsyncEventHandlerWithLogging.cs @@ -0,0 +1,64 @@ +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 + { + + #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 + + Type IAsyncEventHandler.EventType => this.eventHandler.EventType; + + #endregion Properties + + #region Methods + + public async Task HandleAsync(TEvent @event, IMessageContext context = null) + { + 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 index b3ed5e9..19c10d9 100644 --- a/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging.cs +++ b/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging.cs @@ -1,7 +1,6 @@ -using Conditions; +using EnsureThat; using Microsoft.Extensions.Logging; using System.Diagnostics; -using System.Threading; using System.Threading.Tasks; namespace DDD.Core.Application @@ -19,8 +18,8 @@ public class AsyncQueryHandlerWithLogging : IAsyncQueryHandler< public AsyncQueryHandlerWithLogging(IAsyncQueryHandler queryHandler, ILogger logger) { - Condition.Requires(queryHandler, nameof(queryHandler)).IsNotNull(); - Condition.Requires(logger, nameof(logger)).IsNotNull(); + Ensure.That(queryHandler, nameof(queryHandler)).IsNotNull(); + Ensure.That(logger, nameof(logger)).IsNotNull(); this.queryHandler = queryHandler; this.logger = logger; } @@ -29,19 +28,19 @@ public AsyncQueryHandlerWithLogging(IAsyncQueryHandler queryHan #region Methods - public async Task HandleAsync(TQuery query, CancellationToken cancellationToken = default) + public async Task HandleAsync(TQuery query, IMessageContext context = null) { - if (this.logger.IsEnabled(LogLevel.Information)) + if (this.logger.IsEnabled(LogLevel.Debug)) { - this.logger.LogInformation("Executing query {Query}.", query); + 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, cancellationToken); + var result = await this.queryHandler.HandleAsync(query, context); stopWatch.Stop(); - this.logger.LogInformation("Query executed in {QueryExecutionTime} ms.", stopWatch.ElapsedMilliseconds); + 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, cancellationToken); + 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..143a264 --- /dev/null +++ b/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging`1.cs @@ -0,0 +1,62 @@ +using EnsureThat; +using DDD.Core.Domain; +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 + 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 = null) + { + 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/CommandException.cs b/Src/DDD.Core/Application/CommandException.cs index d3acd29..643dc62 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 { @@ -32,16 +33,17 @@ public CommandException(bool isTransient, string message, ICommand command = nul #region Methods - public static string DefaultMessage(ICommand command = null) + public override void GetObjectData(SerializationInfo info, StreamingContext context) { - if (command == null) - return "A command failed."; - return $"The command '{command.GetType().Name}' failed."; + 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}Timestamp: {this.Timestamp}"; s += $"{Environment.NewLine}IsTransient: {this.IsTransient}"; if (this.Command != null) s += $"{Environment.NewLine}Command: {this.Command}"; @@ -52,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/CommandHandlerWithLogging.cs b/Src/DDD.Core/Application/CommandHandlerWithLogging.cs deleted file mode 100644 index ed608ab..0000000 --- a/Src/DDD.Core/Application/CommandHandlerWithLogging.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Conditions; -using Microsoft.Extensions.Logging; -using System.Diagnostics; - -namespace DDD.Core.Application -{ - /// - /// A decorator that logs information about commands. - /// - public class CommandHandlerWithLogging : ICommandHandler - where TCommand : class, ICommand - { - - #region Fields - - private readonly ICommandHandler commandHandler; - private readonly ILogger logger; - - #endregion Fields - - - #region Constructors - -#pragma warning disable CS3001 // Argument type is not CLS-compliant - public CommandHandlerWithLogging(ICommandHandler commandHandler, ILogger logger) -#pragma warning restore CS3001 // Argument type is not CLS-compliant - { - Condition.Requires(commandHandler, nameof(commandHandler)).IsNotNull(); - Condition.Requires(logger, nameof(logger)).IsNotNull(); - this.commandHandler = commandHandler; - this.logger = logger; - } - - #endregion Constructors - - #region Methods - - public void Handle(TCommand command) - { - if (this.logger.IsEnabled(LogLevel.Information)) - { - this.logger.LogInformation("Executing command {Command}.", command); - var stopWatch = Stopwatch.StartNew(); - this.commandHandler.Handle(command); - stopWatch.Stop(); - this.logger.LogInformation("Command executed in {CommandExecutionTime} ms.", stopWatch.ElapsedMilliseconds); - } - else - this.commandHandler.Handle(command); - } - - #endregion Methods - - } -} diff --git a/Src/DDD.Core/Application/CommandInvalidException.cs b/Src/DDD.Core/Application/CommandInvalidException.cs index 92246ef..4ec8c23 100644 --- a/Src/DDD.Core/Application/CommandInvalidException.cs +++ b/Src/DDD.Core/Application/CommandInvalidException.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Runtime.Serialization; using DDD.Validation; namespace DDD.Core.Application @@ -43,9 +44,21 @@ public CommandInvalidException(string message, ICommand command = null, Validati 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}Timestamp: {this.Timestamp}"; s += $"{Environment.NewLine}IsTransient: {this.IsTransient}"; if (this.Command != null) s += $"{Environment.NewLine}Command: {this.Command}"; diff --git a/Src/DDD.Core/Application/CommandProcessor.cs b/Src/DDD.Core/Application/CommandProcessor.cs index 4d9e5f2..399820f 100644 --- a/Src/DDD.Core/Application/CommandProcessor.cs +++ b/Src/DDD.Core/Application/CommandProcessor.cs @@ -1,14 +1,15 @@ -using Conditions; +using EnsureThat; using System; using System.Threading; using System.Threading.Tasks; namespace DDD.Core.Application { + using DDD.Core.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 { @@ -23,7 +24,7 @@ public class CommandProcessor : ICommandProcessor public CommandProcessor(IServiceProvider serviceProvider) { - Condition.Requires(serviceProvider, nameof(serviceProvider)).IsNotNull(); + Ensure.That(serviceProvider, nameof(serviceProvider)).IsNotNull(); this.serviceProvider = serviceProvider; } @@ -31,39 +32,51 @@ public CommandProcessor(IServiceProvider serviceProvider) #region Methods - public void Process(TCommand command) where TCommand : class, ICommand + public IContextualCommandProcessor In(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 new ContextualCommandProcessor(this.serviceProvider, context); } - public Task ProcessAsync(TCommand command, CancellationToken cancellationToken = default) where TCommand : class, ICommand + public IContextualCommandProcessor In(BoundedContext context) { - Condition.Requires(command, nameof(command)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); + var processorType = typeof(ContextualCommandProcessor<>).MakeGenericType(context.GetType()); + return (IContextualCommandProcessor)Activator.CreateInstance(processorType, this.serviceProvider, context); + } + + public void Process(TCommand command, IMessageContext context = null) 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 Task ProcessAsync(TCommand command, IMessageContext context = null) 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, cancellationToken); + 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 { - 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."); + Ensure.That(command, nameof(command)).IsNotNull(); + var validator = this.serviceProvider.GetService>(); + if (validator == null) throw new InvalidOperationException($"The command validator for type {typeof(ISyncCommandValidator)} could not be found."); return validator.Validate(command, ruleSet); } public Task ValidateAsync(TCommand command, string ruleSet = null, CancellationToken cancellationToken = default) where TCommand : class, ICommand { - Condition.Requires(command, nameof(command)).IsNotNull(); + Ensure.That(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."); + if (validator == null) throw new InvalidOperationException($"The command validator for type {typeof(IAsyncCommandValidator)} could not be found."); return validator.ValidateAsync(command, ruleSet, cancellationToken); } #endregion Methods - } } \ No newline at end of file diff --git a/Src/DDD.Core/Application/CompositeTranslatorExtensions.cs b/Src/DDD.Core/Application/CompositeTranslatorExtensions.cs new file mode 100644 index 0000000..4c1b56b --- /dev/null +++ b/Src/DDD.Core/Application/CompositeTranslatorExtensions.cs @@ -0,0 +1,39 @@ +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) => + { + ICommand command = null; + context?.TryGetValue("Command", out 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) => + { + IQuery query = null; + context?.TryGetValue("Query", out 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..117afa4 --- /dev/null +++ b/Src/DDD.Core/Application/ContextualCommandProcessor.cs @@ -0,0 +1,64 @@ +using EnsureThat; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + using DDD.Core.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 = null) 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 Task ProcessAsync(TCommand command, IMessageContext context = null) 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); + } + + #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..6f38862 --- /dev/null +++ b/Src/DDD.Core/Application/ContextualQueryProcessor.cs @@ -0,0 +1,67 @@ +using EnsureThat; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + using DDD.Core.Domain; + using Validation; + + /// + /// 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 = null) + { + 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 Task ProcessAsync(IQuery query, IMessageContext context = null) + { + 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); + } + + #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 fc0958c..0000000 --- a/Src/DDD.Core/Application/DomainCommandHandler.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Conditions; - -namespace DDD.Core.Application -{ - using Domain; - using Mapping; - - /// - /// 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 Fields - - private readonly IObjectTranslator exceptionTranslator = DomainToCommandExceptionTranslator.Default; - - #endregion Fields - - #region Methods - - public void Handle(TCommand command) - { - Condition.Requires(command, nameof(command)).IsNotNull(); - try - { - this.Execute(command); - } - catch (DomainException ex) - { - throw this.exceptionTranslator.Translate(ex, new { Command = command }); - } - } - - protected abstract void Execute(TCommand command); - - #endregion Methods - - } - -} diff --git a/Src/DDD.Core/Application/DomainToCommandExceptionTranslator.cs b/Src/DDD.Core/Application/DomainToCommandExceptionTranslator.cs index cb15162..790ffa7 100644 --- a/Src/DDD.Core/Application/DomainToCommandExceptionTranslator.cs +++ b/Src/DDD.Core/Application/DomainToCommandExceptionTranslator.cs @@ -1,29 +1,22 @@ -using System.Collections.Generic; -using Conditions; +using EnsureThat; +using System.Collections.Generic; namespace DDD.Core.Application { using Mapping; using Domain; + using Collections; - internal class DomainToCommandExceptionTranslator : IObjectTranslator + public class DomainToCommandExceptionTranslator : ObjectTranslator { - #region Fields - - public static readonly IObjectTranslator Default = new DomainToCommandExceptionTranslator(); - - #endregion Fields - #region Methods - public CommandException Translate(DomainException exception, IDictionary options = null) + public override CommandException Translate(DomainException exception, IDictionary context = null) { - Condition.Requires(exception, nameof(exception)).IsNotNull(); - Condition.Requires(options, nameof(options)) - .IsNotNull() - .Evaluate(options.ContainsKey("Command")); - var command = (ICommand)options["Command"]; + Ensure.That(exception).IsNotNull(); + ICommand command = null; + context?.TryGetValue("Command", out command); switch (exception) { case DomainServiceConflictException _: @@ -41,8 +34,8 @@ public CommandException Translate(DomainException exception, IDictionary : IEventConsumer, IDisposable + where TContext : BoundedContext, new() + { + + #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 IKeyedServiceProvider 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, + IKeyedServiceProvider 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.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; + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.settings.Context; + + public bool IsRunning { get; private set; } + + protected CancellationToken CancellationToken => this.cancellationTokenSource.Token; + + #endregion Properties + + #region Methods + + public void Start() + { + if (!this.IsRunning) + { + this.IsRunning = true; + this.consumeEvents = Task.Run(async () => await ConsumeEventsAsync()); + } + } + + public void Stop() + { + if (this.IsRunning) + { + 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 async Task ConsumeEventsAsync() + { + this.logger.LogInformation("Event handling in the context '{Context}' has started.", this.Context.Name); + this.logger.LogDebug("The configuration settings of event handling in the context '{Context}' are the following : {@Settings}", this.Context.Name, this.settings); + try + { + await new SynchronizationContextRemover(); + while (!ConsumationMaxReached()) + { + this.CancellationToken.ThrowIfCancellationRequested(); + this.logger.LogInformation("Event stream consumption in the context '{Context}' has started.", this.Context.Name); + var allStreams = await this.FindAllStreamsAsync(); + await this.ConsumeAllStreamsAsync(allStreams); + this.IncrementConsumationCountIfRequired(); + this.logger.LogInformation("Event stream consumption in the context '{Context}' has finished.", this.Context.Name); + await Task.Delay(TimeSpan.FromSeconds(this.settings.ConsumationDelay), this.CancellationToken); + } + if (this.CancellationToken.IsCancellationRequested) + this.logger.LogInformation("Event handling in the context '{Context}' has been stopped.", this.Context.Name); + else + this.logger.LogInformation("Event handling in the context '{Context}' has finished.", this.Context.Name); + } + catch (OperationCanceledException) + { + this.logger.LogInformation("Event handling in the context '{Context}' has been stopped.", this.Context.Name); + } + catch (Exception exception) + { + this.logger.LogError(default, exception, "An error occurred during event handling in the context '{Context}'.", this.Context.Name); + } + finally + { + this.IsRunning = false; + } + } + + private async Task<(IEnumerable, IEnumerable)> FindAllStreamsAsync() + { + var findStreams = this.queryProcessor.In().ProcessAsync(new FindEventStreams(), MessageContext.CancellableContext(this.CancellationToken)); + var findFailedStreams = this.queryProcessor.In().ProcessAsync(new FindFailedEventStreams(), MessageContext.CancellableContext(this.CancellationToken)); + await Task.WhenAll(findStreams, findFailedStreams); + return (findStreams.Result, findFailedStreams.Result); + } + + private async Task ConsumeAllStreamsAsync((IEnumerable Streams, IEnumerable FailedStreams) allStreams) + { + if (!allStreams.Streams.Any()) + this.logger.LogInformation("No event stream is set up for the context '{Context}'.", this.Context.Name); + var tasks = new List(); + foreach (var stream in allStreams.Streams) + { + var excludedStreams = allStreams.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("The 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.In(sourceContext).ProcessAsync(query, MessageContext.CancellableContext(this.CancellationToken)); + foreach (var notifiedEvent in notifiedEvents) + { + this.CancellationToken.ThrowIfCancellationRequested(); + try + { + this.logger.LogDebug("The handling of the 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("The handling of the 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 during the handling of the 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.In().ProcessAsync(command); + break; + } + } + this.logger.LogInformation("The 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("The 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.In(sourceContext).ProcessAsync(query, MessageContext.CancellableContext(this.CancellationToken)); + var success = true; + foreach (var notifiedEvent in notifiedEvents) + { + this.CancellationToken.ThrowIfCancellationRequested(); + try + { + this.logger.LogDebug("The handling of the 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("The handling of the 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 during the handling of the 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.In().ProcessAsync(command); + break; + } + } + if (success) + { + var command = new IncludeFailedEventStream + { + Id = excludedStream.StreamId, + Type = excludedStream.StreamType, + Source = excludedStream.StreamSource + }; + await this.commandProcessor.In().ProcessAsync(command); + excludedStreams.Remove(excludedStream); + } + this.logger.LogInformation("The consumption of the following failed event stream in the context '{Context}' has finished : {FailedStream}", this.Context.Name, excludedStream); + } + + private static bool IsTransientException(Exception exception) + { + if (exception is TimestampedException ex) + return ex.IsTransient; + return false; + } + + 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); + return context; + } + + private IMessageContext CreateContext(Event @event, EventStream stream) + { + var context = MessageContext.CancellableContext(this.CancellationToken); + context.AddEvent(@event); + context.AddStream(stream); + 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.GetService(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 + + } +} diff --git a/Src/DDD.Core/Application/EventConsumerSettings.cs b/Src/DDD.Core/Application/EventConsumerSettings.cs new file mode 100644 index 0000000..9edfebf --- /dev/null +++ b/Src/DDD.Core/Application/EventConsumerSettings.cs @@ -0,0 +1,53 @@ +using EnsureThat; +using DDD.Core.Domain; +using System.Runtime.Serialization; + +namespace DDD.Core.Application +{ + [DataContract()] + public class EventConsumerSettings + where TContext : BoundedContext, new() + { + #region Fields + + private readonly TContext context; + + #endregion Fields + + #region Constructors + + public EventConsumerSettings(short consumationDelay, + long? consumationMax = null) + { + Ensure.That(consumationDelay, nameof(consumationDelay)).IsGte((short)0); + if (consumationMax != null) + Ensure.That(consumationMax.Value, nameof(consumationMax)).IsGte(0); + this.ConsumationDelay = consumationDelay; + this.ConsumationMax = consumationMax; + this.context = new TContext(); + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the delay in seconds between two successive consumations. + /// + [DataMember(Order = 1)] + public short ConsumationDelay { get; } + + /// + /// Gets the maximum number of successive consumations. + /// + [DataMember(Order = 2)] + public long? ConsumationMax { get; } + + /// + /// Gets the associated context. + /// + public TContext Context => this.context; + + #endregion Properties + } +} diff --git a/Src/DDD.Core/Application/EventHandler.cs b/Src/DDD.Core/Application/EventHandler.cs deleted file mode 100644 index 75b8fa7..0000000 --- a/Src/DDD.Core/Application/EventHandler.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; - -namespace DDD.Core.Application -{ - using Domain; - - /// - /// Base class for handling events. - /// - public abstract class EventHandler : IEventHandler - where TEvent : class, 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/Application/EventPublisher.cs b/Src/DDD.Core/Application/EventPublisher.cs index 46c2f97..36f1205 100644 --- a/Src/DDD.Core/Application/EventPublisher.cs +++ b/Src/DDD.Core/Application/EventPublisher.cs @@ -1,8 +1,7 @@ -using Conditions; +using EnsureThat; using System; using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; namespace DDD.Core.Application @@ -23,7 +22,7 @@ public class EventPublisher : IEventPublisher public EventPublisher(IServiceProvider serviceProvider) { - Condition.Requires(serviceProvider, nameof(serviceProvider)).IsNotNull(); + Ensure.That(serviceProvider, nameof(serviceProvider)).IsNotNull(); this.serviceProvider = serviceProvider; } @@ -31,21 +30,16 @@ public EventPublisher(IServiceProvider serviceProvider) #region Methods - public void Publish(TEvent @event) where TEvent : class, IEvent + public async Task PublishAsync(TEvent @event, IMessageContext context = null) where TEvent : class, IEvent { - Condition.Requires(@event, nameof(@event)).IsNotNull(); - var handlers = this.GetEventHandlers(@event); - foreach (var handler in handlers) - handler.Handle(@event); - } - - public async Task PublishAsync(TEvent @event, CancellationToken cancellationToken = default) where TEvent : class, IEvent - { - Condition.Requires(@event, nameof(@event)).IsNotNull(); + Ensure.That(@event, nameof(@event)).IsNotNull(); await new SynchronizationContextRemover(); - var handlers = this.GetAsyncEventHandlers(@event); - foreach (var handler in handlers) - await handler.HandleAsync(@event, cancellationToken); + var syncHandlers = this.GetSyncEventHandlers(@event); + foreach (var handler in syncHandlers) + handler.Handle(@event, context); + var asyncHandlers = this.GetAsyncEventHandlers(@event); + foreach (var handler in asyncHandlers) + await handler.HandleAsync(@event, context); } private IEnumerable GetAsyncEventHandlers(TEvent @event) where TEvent : class, IEvent @@ -58,14 +52,14 @@ private IEnumerable GetAsyncEventHandlers(TEvent @ev return this.serviceProvider.GetServices>(); } - private IEnumerable GetEventHandlers(TEvent @event) where TEvent : class, IEvent + private IEnumerable GetSyncEventHandlers(TEvent @event) where TEvent : class, IEvent { if (typeof(TEvent) == typeof(IEvent)) { - var handlerType = typeof(IEventHandler<>).MakeGenericType(@event.GetType()); - return this.serviceProvider.GetServices(handlerType).Cast(); + var handlerType = typeof(ISyncEventHandler<>).MakeGenericType(@event.GetType()); + return this.serviceProvider.GetServices(handlerType).Cast(); } - return this.serviceProvider.GetServices>(); + return this.serviceProvider.GetServices>(); } #endregion Methods diff --git a/Src/DDD.Core/Application/EventTranslator.cs b/Src/DDD.Core/Application/EventTranslator.cs new file mode 100644 index 0000000..0c8d2ed --- /dev/null +++ b/Src/DDD.Core/Application/EventTranslator.cs @@ -0,0 +1,62 @@ +using EnsureThat; +using System; +using System.Collections.Generic; +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, IDictionary context = null) + { + Ensure.That(@event, nameof(@event)).IsNotNull(); + Guid eventId = default; + string streamId = null, streamType = null, issuedBy = null; + if (context != null) + { + context.TryGetValue("EventId", out eventId); + context.TryGetValue("StreamId", out streamId); + context.TryGetValue("StreamType", out streamType); + context.TryGetValue("IssuedBy", out 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 2a117e4..55e6e9d 100644 --- a/Src/DDD.Core/Application/IAsyncCommandHandler.cs +++ b/Src/DDD.Core/Application/IAsyncCommandHandler.cs @@ -1,5 +1,4 @@ -using System.Threading; -using System.Threading.Tasks; +using System.Threading.Tasks; namespace DDD.Core.Application { @@ -12,7 +11,10 @@ public interface IAsyncCommandHandler #region Methods - Task HandleAsync(TCommand command, CancellationToken cancellationToken = default); + /// + /// Handles asynchronously a command of a specified type. + /// + Task HandleAsync(TCommand command, IMessageContext context = null); #endregion Methods diff --git a/Src/DDD.Core/Application/IAsyncCommandHandlerExtensions.cs b/Src/DDD.Core/Application/IAsyncCommandHandlerExtensions.cs new file mode 100644 index 0000000..47f45ca --- /dev/null +++ b/Src/DDD.Core/Application/IAsyncCommandHandlerExtensions.cs @@ -0,0 +1,45 @@ +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, + object context) + where TCommand : class, ICommand + + { + Ensure.That(handler, nameof(handler)).IsNotNull(); + return handler.HandleAsync(command, MessageContext.FromObject(context)); + } + + public static async Task UpdateStreamPositionIfDefinedAsync(this IAsyncCommandHandler handler, IMessageContext context) + { + Ensure.That(handler, nameof(handler)).IsNotNull(); + if (context != null) + { + var @event = context.Event(); + var stream = context.Stream(); + if (@event != null && stream != null) + { + var command = new UpdateEventStreamPosition + { + Position = @event.EventId, + Type = stream.Type, + Source = stream.Source + }; + await handler.HandleAsync(command, context); + stream.Position = @event.EventId; + } + } + } + + #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..ceab794 --- /dev/null +++ b/Src/DDD.Core/Application/IAsyncCommandHandler`1.cs @@ -0,0 +1,23 @@ +using DDD.Core.Domain; + +namespace DDD.Core.Application +{ + /// + /// 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/IAsyncEventHandler.cs b/Src/DDD.Core/Application/IAsyncEventHandler.cs index 370e179..ce0f6df 100644 --- a/Src/DDD.Core/Application/IAsyncEventHandler.cs +++ b/Src/DDD.Core/Application/IAsyncEventHandler.cs @@ -1,11 +1,13 @@ using System; -using System.Threading; using System.Threading.Tasks; namespace DDD.Core.Application { using Domain; + /// + /// Defines a method that handles asynchronously an event of a specified type. + /// public interface IAsyncEventHandler { #region Properties @@ -16,7 +18,7 @@ public interface IAsyncEventHandler #region Methods - Task HandleAsync(IEvent @event, CancellationToken cancellationToken = default); + Task HandleAsync(IEvent @event, IMessageContext context = null); #endregion Methods } diff --git a/Src/DDD.Core/Application/IAsyncEventHandler`1.cs b/Src/DDD.Core/Application/IAsyncEventHandler`1.cs index adb6f92..e264d40 100644 --- a/Src/DDD.Core/Application/IAsyncEventHandler`1.cs +++ b/Src/DDD.Core/Application/IAsyncEventHandler`1.cs @@ -1,5 +1,4 @@ -using System.Threading; -using System.Threading.Tasks; +using System.Threading.Tasks; namespace DDD.Core.Application { @@ -14,7 +13,7 @@ public interface IAsyncEventHandler : IAsyncEventHandler #region Methods - Task HandleAsync(TEvent @event, CancellationToken cancellationToken = default); + Task HandleAsync(TEvent @event, IMessageContext context = null); #endregion Methods diff --git a/Src/DDD.Core/Application/IAsyncQueryHandler.cs b/Src/DDD.Core/Application/IAsyncQueryHandler.cs index 82f31cc..17fe0f6 100644 --- a/Src/DDD.Core/Application/IAsyncQueryHandler.cs +++ b/Src/DDD.Core/Application/IAsyncQueryHandler.cs @@ -1,5 +1,4 @@ -using System.Threading; -using System.Threading.Tasks; +using System.Threading.Tasks; namespace DDD.Core.Application { @@ -13,7 +12,10 @@ public interface IAsyncQueryHandler { #region Methods - Task HandleAsync(TQuery query, CancellationToken cancellationToken = default); + /// + /// Handles asynchronously a query of a specified type and provides a result of a specified type. + /// + Task HandleAsync(TQuery query, IMessageContext context = null); #endregion Methods } diff --git a/Src/DDD.Core/Application/IAsyncQueryHandlerExtensions.cs b/Src/DDD.Core/Application/IAsyncQueryHandlerExtensions.cs new file mode 100644 index 0000000..0bc59b0 --- /dev/null +++ b/Src/DDD.Core/Application/IAsyncQueryHandlerExtensions.cs @@ -0,0 +1,24 @@ +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, + 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..6b19f43 --- /dev/null +++ b/Src/DDD.Core/Application/IAsyncQueryHandler`1.cs @@ -0,0 +1,26 @@ +using DDD.Core.Domain; + +namespace DDD.Core.Application +{ + /// + /// 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/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..d0bc239 --- /dev/null +++ b/Src/DDD.Core/Application/ICommandHandler`1.cs @@ -0,0 +1,13 @@ +using DDD.Core.Domain; + +namespace DDD.Core.Application +{ + /// + /// 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 1a379e3..60f3a42 100644 --- a/Src/DDD.Core/Application/ICommandProcessor.cs +++ b/Src/DDD.Core/Application/ICommandProcessor.cs @@ -3,6 +3,8 @@ namespace DDD.Core.Application { + using Domain; + using System.Net; using Validation; /// @@ -13,15 +15,36 @@ public interface ICommandProcessor #region Methods - void Process(TCommand command) where TCommand : class, ICommand; - - Task ProcessAsync(TCommand command, CancellationToken cancellationToken = default) where TCommand : class, ICommand; - + /// + /// Specify the bounded context in which the command must be processed. + /// + IContextualCommandProcessor In(TContext context) where TContext : BoundedContext; + + /// + /// Specify the bounded context in which the command must be processed. + /// + IContextualCommandProcessor In(BoundedContext context); + + /// + /// Processes synchronously a command of a specified type. + /// + void Process(TCommand command, IMessageContext context = null) where TCommand : class, ICommand; + + /// + /// Processes asynchronously a command of a specified type. + /// + Task ProcessAsync(TCommand command, IMessageContext context = null) where TCommand : class, ICommand; + + /// + /// Validates synchronously a command of a specified type. + /// ValidationResult Validate(TCommand command, string ruleSet = null) where TCommand : class, ICommand; + /// + /// Validates asynchronously a command of a specified type. + /// Task ValidateAsync(TCommand command, string ruleSet = null, CancellationToken cancellationToken = default) where TCommand : class, ICommand; #endregion Methods - } } diff --git a/Src/DDD.Core/Application/ICommandProcessorExtensions.cs b/Src/DDD.Core/Application/ICommandProcessorExtensions.cs new file mode 100644 index 0000000..66f030c --- /dev/null +++ b/Src/DDD.Core/Application/ICommandProcessorExtensions.cs @@ -0,0 +1,65 @@ +using EnsureThat; +using System; +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + using Domain; + using Threading; + + public static class ICommandProcessorExtensions + { + + #region Methods + + public static IContextualCommandProcessor In(this ICommandProcessor processor) where TContext : BoundedContext, new() + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.In(new TContext()); + } + + 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 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 async Task ProcessWithDelayAsync(this ICommandProcessor processor, + TCommand command, + TimeSpan delay, + IMessageContext context = null) + where TCommand : class, ICommand + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + await new SynchronizationContextRemover(); + var cancellationToken = context?.CancellationToken() ?? default; + 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)); + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/ICommandValidator.cs b/Src/DDD.Core/Application/ICommandValidator.cs index 578d346..e9e081e 100644 --- a/Src/DDD.Core/Application/ICommandValidator.cs +++ b/Src/DDD.Core/Application/ICommandValidator.cs @@ -1,11 +1,9 @@ namespace DDD.Core.Application { - using Validation; - /// - /// Defines a method that validates a command of a specified type. + /// Defines methods that validate synchronously and asynchronously a command of a specified type. /// - public interface ICommandValidator : IObjectValidator + public interface ICommandValidator : ISyncCommandValidator, IAsyncCommandValidator where TCommand : class, ICommand { } diff --git a/Src/DDD.Core/Application/IContextualCommandProcessor.cs b/Src/DDD.Core/Application/IContextualCommandProcessor.cs new file mode 100644 index 0000000..04a53bd --- /dev/null +++ b/Src/DDD.Core/Application/IContextualCommandProcessor.cs @@ -0,0 +1,37 @@ +using DDD.Core.Domain; +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + /// + /// Defines a component that processes generic 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 = null) where TCommand : class, ICommand; + + /// + /// Processes asynchronously a command of a specified type in the specific bounded context. + /// + Task ProcessAsync(TCommand command, IMessageContext context = null) where TCommand : class, ICommand; + + #endregion Methods + } +} diff --git a/Src/DDD.Core/Application/IContextualCommandProcessor`1.cs b/Src/DDD.Core/Application/IContextualCommandProcessor`1.cs new file mode 100644 index 0000000..775c7c9 --- /dev/null +++ b/Src/DDD.Core/Application/IContextualCommandProcessor`1.cs @@ -0,0 +1,25 @@ +using DDD.Core.Domain; +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + /// + /// Defines a component that processes generic 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..95858a8 --- /dev/null +++ b/Src/DDD.Core/Application/IContextualQueryProcessor.cs @@ -0,0 +1,39 @@ +using DDD.Core.Domain; +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + /// + /// 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 = null); + + /// + /// Processes asynchronously a query of a specified type and provides a result of a specified type. + /// + Task ProcessAsync(IQuery query, IMessageContext context = null); + + #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..c009923 --- /dev/null +++ b/Src/DDD.Core/Application/IContextualQueryProcessor`1.cs @@ -0,0 +1,25 @@ +using DDD.Core.Domain; + +namespace DDD.Core.Application +{ + /// + /// 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..d8c4611 --- /dev/null +++ b/Src/DDD.Core/Application/IEventConsumer.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + /// + /// Defines a method that consumes events. + /// + /// + /// This component is used to read external event streams and to publish those events inside a bounded context. + /// + public interface IEventConsumer + { + + #region Properties + + /// + /// Determines whether the consumer is running. + /// + bool IsRunning { get; } + + #endregion Properties + + #region Methods + + /// + /// Starts consuming events. + /// + public void Start(); + + /// + /// Stops consuming events. + /// + 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..7b3b9d9 --- /dev/null +++ b/Src/DDD.Core/Application/IEventConsumer`1.cs @@ -0,0 +1,14 @@ +using DDD.Core.Domain; + +namespace DDD.Core.Application +{ + public interface IEventConsumer : IEventConsumer + where TContext : BoundedContext + { + #region Properties + + TContext Context { get; } + + #endregion Properties + } +} diff --git a/Src/DDD.Core/Application/IEventHandler`1.cs b/Src/DDD.Core/Application/IEventHandler`1.cs deleted file mode 100644 index 5fc7ad6..0000000 --- a/Src/DDD.Core/Application/IEventHandler`1.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace DDD.Core.Application -{ - using Domain; - - /// - /// Defines a method that handles an event of a specified type. - /// - public interface IEventHandler : IEventHandler - where TEvent : class, IEvent - { - #region Methods - - void Handle(TEvent @event); - - #endregion Methods - } -} \ No newline at end of file diff --git a/Src/DDD.Core/Application/IEventPublisher.cs b/Src/DDD.Core/Application/IEventPublisher.cs index eebaec9..92f696e 100644 --- a/Src/DDD.Core/Application/IEventPublisher.cs +++ b/Src/DDD.Core/Application/IEventPublisher.cs @@ -1,21 +1,21 @@ -using System.Threading; -using System.Threading.Tasks; +using System.Threading.Tasks; namespace DDD.Core.Application { using Domain; /// - /// Defines a component that publishes events of any type. + /// Defines a method that publishes events of any type. /// + /// + /// This component is used to publish events inside a bounded context. + /// public interface IEventPublisher { #region Methods - void Publish(TEvent @event) where TEvent : class, IEvent; - - Task PublishAsync(TEvent @event, CancellationToken cancellationToken = default) where TEvent : class, IEvent; + Task PublishAsync(TEvent @event, IMessageContext context = null) where TEvent : class, IEvent; #endregion Methods diff --git a/Src/DDD.Core/Application/IEventPublisherExtensions.cs b/Src/DDD.Core/Application/IEventPublisherExtensions.cs deleted file mode 100644 index d6bb043..0000000 --- a/Src/DDD.Core/Application/IEventPublisherExtensions.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Conditions; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace DDD.Core.Application -{ - using Domain; - using Threading; - - public static class IEventPublisherExtensions - { - - #region Methods - - /// - /// Publishes all events synchronously. - /// - 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); - } - - /// - /// Publishes all events asynchronously and sequentially. - /// - public async static Task PublishAllAsync(this IEventPublisher publisher, IEnumerable events, CancellationToken cancellationToken = default) - { - Condition.Requires(publisher, nameof(publisher)).IsNotNull(); - Condition.Requires(events, nameof(events)) - .IsNotNull() - .DoesNotContain(null); - await new SynchronizationContextRemover(); - foreach (var @event in events) - await publisher.PublishAsync(@event, cancellationToken); - } - - #endregion Methods - - } -} \ No newline at end of file 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..7f5f0de --- /dev/null +++ b/Src/DDD.Core/Application/IMessageContextExtensions.cs @@ -0,0 +1,73 @@ +using EnsureThat; +using System; +using System.Threading; + +namespace DDD.Core.Application +{ + using Domain; + using Collections; + + public static class IMessageContextExtensions + { + + #region Methods + + 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 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 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..385e235 --- /dev/null +++ b/Src/DDD.Core/Application/IQueryHandler`1.cs @@ -0,0 +1,16 @@ +using DDD.Core.Domain; + +namespace DDD.Core.Application +{ + /// + /// 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 b9041ac..ed594bb 100644 --- a/Src/DDD.Core/Application/IQueryProcessor.cs +++ b/Src/DDD.Core/Application/IQueryProcessor.cs @@ -3,6 +3,7 @@ namespace DDD.Core.Application { + using Domain; using Validation; /// @@ -13,15 +14,36 @@ public interface IQueryProcessor #region Methods - TResult Process(IQuery query); - - Task ProcessAsync(IQuery query, CancellationToken cancellationToken = default); - + /// + /// Specify the bounded context in which the query must be processed. + /// + IContextualQueryProcessor In(TContext context) where TContext : BoundedContext; + + /// + /// Specify the bounded context in which the query must be processed. + /// + IContextualQueryProcessor In(BoundedContext context); + + /// + /// Processes synchronously a query of a specified type and provides a result of a specified type. + /// + TResult Process(IQuery query, IMessageContext context = null); + + /// + /// Processes asynchronously a query of a specified type and provides a result of a specified type. + /// + Task ProcessAsync(IQuery query, IMessageContext context = null); + + /// + /// Validates synchronously a query of a specified type. + /// ValidationResult Validate(TQuery query, string ruleSet = null) where TQuery : class, IQuery; + /// + /// Validates asynchronously a query of a specified type. + /// Task ValidateAsync(TQuery query, string ruleSet = null, CancellationToken cancellationToken = default) where TQuery : class, IQuery; #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..38aa252 --- /dev/null +++ b/Src/DDD.Core/Application/IQueryProcessorExtensions.cs @@ -0,0 +1,38 @@ +using EnsureThat; +using System.Threading.Tasks; + +namespace DDD.Core.Application +{ + using Domain; + + public static class IQueryProcessorExtensions + { + + #region Methods + + public static IContextualQueryProcessor In(this IQueryProcessor processor) where TContext : BoundedContext, new() + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.In(new TContext()); + } + + 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 Task ProcessAsync(this IQueryProcessor processor, + IQuery query, + object context) + { + Ensure.That(processor, nameof(processor)).IsNotNull(); + return processor.ProcessAsync(query, MessageContext.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 index 0d4d46c..8fa542d 100644 --- a/Src/DDD.Core/Application/IQueryValidator.cs +++ b/Src/DDD.Core/Application/IQueryValidator.cs @@ -1,12 +1,10 @@ namespace DDD.Core.Application { - using Validation; - /// - /// Defines a method that validates a query of a specified type. + /// Defines methods that validate synchronously and asynchronously a query of a specified type. /// - public interface IQueryValidator : IObjectValidator + public interface IQueryValidator : ISyncQueryValidator, IAsyncQueryValidator 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..cb974cc --- /dev/null +++ b/Src/DDD.Core/Application/IRecurringCommandManager`1.cs @@ -0,0 +1,17 @@ +using DDD.Core.Domain; + +namespace DDD.Core.Application +{ + /// + /// 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..db0f09d --- /dev/null +++ b/Src/DDD.Core/Application/IRecurringScheduleFactory.cs @@ -0,0 +1,23 @@ +using System; + +namespace DDD.Core.Application +{ + /// + /// Defines a factory of recurring schedules. + /// + public interface IRecurringScheduleFactory + { + + #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..5157f8b --- /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 = null); + + #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..f32ff7c --- /dev/null +++ b/Src/DDD.Core/Application/ISyncCommandHandlerExtensions.cs @@ -0,0 +1,25 @@ +using EnsureThat; + +namespace DDD.Core.Application +{ + using Collections; + + public static class ISyncCommandHandlerExtensions + { + + #region Methods + + 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..55d8769 --- /dev/null +++ b/Src/DDD.Core/Application/ISyncCommandHandler`1.cs @@ -0,0 +1,23 @@ +using DDD.Core.Domain; + +namespace DDD.Core.Application +{ + /// + /// 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/ISyncCommandValidator.cs b/Src/DDD.Core/Application/ISyncCommandValidator.cs new file mode 100644 index 0000000..3efb86c --- /dev/null +++ b/Src/DDD.Core/Application/ISyncCommandValidator.cs @@ -0,0 +1,12 @@ +namespace DDD.Core.Application +{ + using Validation; + + /// + /// Defines a method that validates synchronously a command of a specified type. + /// + public interface ISyncCommandValidator : ISyncObjectValidator + where TCommand : class, ICommand + { + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/IEventHandler.cs b/Src/DDD.Core/Application/ISyncEventHandler.cs similarity index 50% rename from Src/DDD.Core/Application/IEventHandler.cs rename to Src/DDD.Core/Application/ISyncEventHandler.cs index 3c356ac..03b42e9 100644 --- a/Src/DDD.Core/Application/IEventHandler.cs +++ b/Src/DDD.Core/Application/ISyncEventHandler.cs @@ -4,7 +4,10 @@ namespace DDD.Core.Application { using Domain; - public interface IEventHandler + /// + /// Defines a method that handles synchronously an event of a specified type. + /// + public interface ISyncEventHandler { #region Properties @@ -14,7 +17,7 @@ public interface IEventHandler #region Methods - void Handle(IEvent @event); + void Handle(IEvent @event, IMessageContext context = null); #endregion Methods } diff --git a/Src/DDD.Core/Application/ISyncEventHandler`1.cs b/Src/DDD.Core/Application/ISyncEventHandler`1.cs new file mode 100644 index 0000000..2f72afb --- /dev/null +++ b/Src/DDD.Core/Application/ISyncEventHandler`1.cs @@ -0,0 +1,19 @@ +namespace DDD.Core.Application +{ + using Domain; + + /// + /// Defines a method that handles synchronously an event of a specified type. + /// + public interface ISyncEventHandler : ISyncEventHandler + where TEvent : class, IEvent + { + + #region Methods + + void Handle(TEvent @event, IMessageContext context = null); + + #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..3210afa --- /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 = null); + + #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..1527b40 --- /dev/null +++ b/Src/DDD.Core/Application/ISyncQueryHandlerExtensions.cs @@ -0,0 +1,23 @@ +using EnsureThat; + +namespace DDD.Core.Application +{ + public static class ISyncQueryHandlerExtensions + { + + #region Methods + + 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..3a62b4c --- /dev/null +++ b/Src/DDD.Core/Application/ISyncQueryHandler`1.cs @@ -0,0 +1,26 @@ +using DDD.Core.Domain; + +namespace DDD.Core.Application +{ + /// + /// 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/ISyncQueryValidator.cs b/Src/DDD.Core/Application/ISyncQueryValidator.cs new file mode 100644 index 0000000..5886865 --- /dev/null +++ b/Src/DDD.Core/Application/ISyncQueryValidator.cs @@ -0,0 +1,12 @@ +namespace DDD.Core.Application +{ + using Validation; + + /// + /// Defines a method that validates synchronously a query of a specified type. + /// + public interface ISyncQueryValidator : ISyncObjectValidator + where TQuery : class, IQuery + { + } +} \ 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..cac3209 --- /dev/null +++ b/Src/DDD.Core/Application/MessageContext.cs @@ -0,0 +1,52 @@ +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) + { + MessageContext messageContext; + if (context == null) + messageContext = null; + else + { + messageContext = new MessageContext(); + 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..dbbda33 --- /dev/null +++ b/Src/DDD.Core/Application/MessageContextInfo.cs @@ -0,0 +1,17 @@ +namespace DDD.Core.Application +{ + /// + /// Standard contextual information about the message. + /// + public class MessageContextInfo + { + #region Fields + + public const string Event = "Event", + CancellationToken = "CancellationToken", + Stream = "Stream", + FailedStream = "FailedStream"; + + #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 ea98619..975c486 100644 --- a/Src/DDD.Core/Application/QueryException.cs +++ b/Src/DDD.Core/Application/QueryException.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.Serialization; namespace DDD.Core.Application { @@ -39,9 +40,17 @@ public static string DefaultMessage(IQuery query = null) 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}Timestamp: {this.Timestamp}"; s += $"{Environment.NewLine}IsTransient: {this.IsTransient}"; if (this.Query != null) s += $"{Environment.NewLine}Query: {this.Query}"; diff --git a/Src/DDD.Core/Application/QueryHandlerWithLogging.cs b/Src/DDD.Core/Application/QueryHandlerWithLogging.cs deleted file mode 100644 index 1bce003..0000000 --- a/Src/DDD.Core/Application/QueryHandlerWithLogging.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Conditions; -using Microsoft.Extensions.Logging; -using System.Diagnostics; - -namespace DDD.Core.Application -{ - /// - /// A decorator that logs information about queries. - /// - public class QueryHandlerWithLogging : IQueryHandler - where TQuery : class, IQuery - { - #region Fields - - private readonly IQueryHandler queryHandler; - private readonly ILogger logger; - -#pragma warning disable CS3001 // Argument type is not CLS-compliant - public QueryHandlerWithLogging(IQueryHandler queryHandler, ILogger logger) -#pragma warning restore CS3001 // Argument type is not CLS-compliant - { - Condition.Requires(queryHandler, nameof(queryHandler)).IsNotNull(); - Condition.Requires(logger, nameof(logger)).IsNotNull(); - this.queryHandler = queryHandler; - this.logger = logger; - } - - #endregion Fields - - #region Methods - - public TResult Handle(TQuery query) - { - if (this.logger.IsEnabled(LogLevel.Information)) - { - this.logger.LogInformation("Executing query {Query}.", query); - var stopWatch = Stopwatch.StartNew(); - var result = this.queryHandler.Handle(query); - stopWatch.Stop(); - this.logger.LogInformation("Query executed in {QueryExecutionTime} ms.", stopWatch.ElapsedMilliseconds); - return result; - } - else - return this.queryHandler.Handle(query); - } - - #endregion Methods - } -} diff --git a/Src/DDD.Core/Application/QueryInvalidException.cs b/Src/DDD.Core/Application/QueryInvalidException.cs index 6e081f0..97a7ff7 100644 --- a/Src/DDD.Core/Application/QueryInvalidException.cs +++ b/Src/DDD.Core/Application/QueryInvalidException.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Runtime.Serialization; namespace DDD.Core.Application { @@ -44,9 +45,20 @@ public QueryInvalidException(string message, IQuery query = null, ValidationFail 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}Timestamp: {this.Timestamp}"; s += $"{Environment.NewLine}IsTransient: {this.IsTransient}"; if (this.Query != null) s += $"{Environment.NewLine}Query: {this.Query}"; diff --git a/Src/DDD.Core/Application/QueryProcessor.cs b/Src/DDD.Core/Application/QueryProcessor.cs index d4e1127..f8bc2a9 100644 --- a/Src/DDD.Core/Application/QueryProcessor.cs +++ b/Src/DDD.Core/Application/QueryProcessor.cs @@ -1,14 +1,15 @@ -using Conditions; +using EnsureThat; using System; using System.Threading; using System.Threading.Tasks; namespace DDD.Core.Application { + using Domain; using Validation; /// - /// 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 { @@ -23,7 +24,7 @@ public class QueryProcessor : IQueryProcessor public QueryProcessor(IServiceProvider serviceProvider) { - Condition.Requires(serviceProvider, nameof(serviceProvider)).IsNotNull(); + Ensure.That(serviceProvider, nameof(serviceProvider)).IsNotNull(); this.serviceProvider = serviceProvider; } @@ -31,41 +32,53 @@ public QueryProcessor(IServiceProvider serviceProvider) #region Methods - public TResult Process(IQuery query) + public IContextualQueryProcessor In(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 new ContextualQueryProcessor(this.serviceProvider, context); + } + + public IContextualQueryProcessor In(BoundedContext context) + { + Ensure.That(context, nameof(context)).IsNotNull(); + var processorType = typeof(ContextualQueryProcessor<>).MakeGenericType(context.GetType()); + return (IContextualQueryProcessor)Activator.CreateInstance(processorType, this.serviceProvider, context); + } + + public TResult Process(IQuery query, IMessageContext context = null) + { + 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); + return handler.Handle((dynamic)query, context); } - public Task ProcessAsync(IQuery query, CancellationToken cancellationToken = default) + public Task ProcessAsync(IQuery query, IMessageContext context = null) { - 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, cancellationToken); + return handler.HandleAsync((dynamic)query, context); } public ValidationResult Validate(TQuery query, string ruleSet = null) 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."); + Ensure.That(query, nameof(query)).IsNotNull(); + var validator = this.serviceProvider.GetService>(); + if (validator == null) throw new InvalidOperationException($"The query validator for type {typeof(ISyncQueryValidator)} could not be found."); return validator.Validate(query, ruleSet); } public Task ValidateAsync(TQuery query, string ruleSet = null, CancellationToken cancellationToken = default) where TQuery : class, IQuery { - Condition.Requires(query, nameof(query)).IsNotNull(); + Ensure.That(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."); + if (validator == null) throw new InvalidOperationException($"The query validator for type {typeof(IAsyncQueryValidator)} could not be found."); return validator.ValidateAsync(query, ruleSet, cancellationToken); } #endregion Methods - } } \ No newline at end of file diff --git a/Src/DDD.Core/Application/RecurringCommandManager.cs b/Src/DDD.Core/Application/RecurringCommandManager.cs new file mode 100644 index 0000000..91af71d --- /dev/null +++ b/Src/DDD.Core/Application/RecurringCommandManager.cs @@ -0,0 +1,263 @@ +using EnsureThat; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace DDD.Core.Application +{ + using DDD.Core.Domain; + using DependencyInjection; + using Serialization; + using Threading; + + public class RecurringCommandManager : IRecurringCommandManager, IDisposable + where TContext : BoundedContext, new() + { + + #region Fields + + private readonly IQueryProcessor queryProcessor; + private readonly ICommandProcessor commandProcessor; + private readonly IKeyedServiceProvider commandSerializers; + private readonly IRecurringScheduleFactory recurringScheduleFactory; + private readonly ILogger logger; + private readonly RecurringCommandManagerSettings settings; + private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + private Task manageCommands; + private bool disposed; + + #endregion Fields + + #region Constructors + + public RecurringCommandManager(ICommandProcessor commandProcessor, + IQueryProcessor queryProcessor, + IKeyedServiceProvider commandSerializers, + IRecurringScheduleFactory recurringScheduleFactory, + ILogger logger, + RecurringCommandManagerSettings settings) + { + Ensure.That(commandProcessor, nameof(commandProcessor)).IsNotNull(); + Ensure.That(queryProcessor, nameof(queryProcessor)).IsNotNull(); + Ensure.That(commandSerializers, nameof(commandSerializers)).IsNotNull(); + Ensure.That(recurringScheduleFactory, nameof(recurringScheduleFactory)).IsNotNull(); + Ensure.That(logger, nameof(logger)).IsNotNull(); + Ensure.That(settings, nameof(settings)).IsNotNull(); + this.commandProcessor = commandProcessor; + this.queryProcessor = queryProcessor; + this.commandSerializers = commandSerializers; + this.recurringScheduleFactory = recurringScheduleFactory; + this.logger = logger; + this.settings = settings; + } + + #endregion Constructors + + #region Properties + + public TContext Context => this.settings.Context; + + public bool IsRunning { get; private set; } + + protected CancellationToken CancellationToken => this.cancellationTokenSource.Token; + + #endregion Properties + + #region Methods + + public async Task RegisterAsync(ICommand command, string recurringExpression, CancellationToken cancellationToken = default) + { + Ensure.That(command, nameof(command)).IsNotNull(); + Ensure.That(recurringExpression, nameof(recurringExpression)).IsNotNull(); + this.ValidateRecurringExpression(recurringExpression); + await new SynchronizationContextRemover(); + var commandSerializer = this.commandSerializers.GetService(this.settings.CurrentSerializationFormat); + var commandId = this.queryProcessor.In().Process(new GenerateRecurringCommandId(), MessageContext.CancellableContext(cancellationToken)); + var registrationCommand = new RegisterRecurringCommand + { + CommandId = commandId, + CommandType = command.GetType().ShortAssemblyQualifiedName(), + Body = commandSerializer.SerializeToString(command), + BodyFormat = commandSerializer.Format.ToString().ToUpper(), + RecurringExpression = recurringExpression + }; + await this.commandProcessor.In().ProcessAsync(registrationCommand, MessageContext.CancellableContext(cancellationToken)); + } + + public void Start() + { + if (!this.IsRunning) + { + this.IsRunning = true; + this.manageCommands = Task.Run(async () => await ManageCommandsAsync()); + } + } + + public void Stop() + { + if (this.IsRunning) + { + 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(); + } + } + + 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 async Task ManageCommandsAsync() + { + try + { + this.logger.LogInformation("Recurring command management in the context '{Context}' has started.", this.Context.Name); + this.logger.LogDebug("The configuration settings of recurring command management in the context '{Context}' are the following : {@Settings}", this.Context.Name, this.settings); + 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); + if (this.CancellationToken.IsCancellationRequested) + this.logger.LogInformation("Recurring command management in the context '{Context}' has been stopped.", this.Context.Name); + else + this.logger.LogInformation("Recurring command management in the context '{Context}' has finished.", this.Context.Name); + } + catch (OperationCanceledException) + { + this.logger.LogInformation("Recurring command management in the context '{Context}' has been stopped.", this.Context.Name); + } + catch (Exception exception) + { + this.logger.LogError(default, exception, "An error occurred during recurring command management in the context '{Context}'.", this.Context.Name); + } + finally + { + this.IsRunning = false; + } + } + + private async Task> FindAllRecurringCommandsAsync() + { + var recurringCommands = await this.queryProcessor.In().ProcessAsync(new FindRecurringCommands(), MessageContext.CancellableContext(this.CancellationToken)); + var results = new List<(RecurringCommand, IRecurringSchedule)>(); + foreach (var recurringCommand in recurringCommands) + results.Add((recurringCommand, this.recurringScheduleFactory.Create(recurringCommand.RecurringExpression))); + return results; + } + + private async Task ManageCommandAsync(RecurringCommand recurringCommand, IRecurringSchedule recurringSchedule) + { + try + { + this.logger.LogInformation("The management of the 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("The next processing of the recurring command {CommandId} in the context '{Context}' has been 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 during the processing of the 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.In().ProcessAsync(failureCommand); + } + if (success) + { + this.logger.LogDebug("The processing of the 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.In().ProcessAsync(successCommand); + } + now = SystemTime.Local(); + nextOccurrence = recurringSchedule.GetNextOccurrence(now); + } + if (this.CancellationToken.IsCancellationRequested) + this.logger.LogInformation("The management of the recurring command {CommandId} in the context '{Context}' has been stopped.", recurringCommand.CommandId, this.Context.Name); + else + this.logger.LogInformation("The management of the recurring command {CommandId} in the context '{Context}' has finished.", recurringCommand.CommandId, this.Context.Name); + } + catch(OperationCanceledException) + { + this.logger.LogInformation("The management of the recurring command {CommandId} in the context '{Context}' has been stopped.", recurringCommand.CommandId, this.Context.Name); + throw; + } + catch (Exception exception) + { + this.logger.LogError(default, exception, "An error occurred during the management of the recurring command {CommandId} in the context '{Context}'.", recurringCommand.CommandId, this.Context.Name); + } + } + + 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.commandSerializers.GetService(format); + return (ICommand)serializer.DeserializeFromString(recurringCommand.Body, type); + } + + private void ValidateRecurringExpression(string recurringExpression) + { + try + { + this.recurringScheduleFactory.Create(recurringExpression); + } + catch (Exception ex) + { + throw new ArgumentException( + "The recurring expression is invalid. Please see the inner exception for details.", + nameof(recurringExpression), + ex); + } + } + + #endregion Methods + + } +} diff --git a/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs b/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs new file mode 100644 index 0000000..0761240 --- /dev/null +++ b/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs @@ -0,0 +1,43 @@ +using System.Runtime.Serialization; + +namespace DDD.Core.Application +{ + using DDD.Core.Domain; + using Serialization; + + [DataContract()] + public class RecurringCommandManagerSettings + where TContext : BoundedContext, new() + { + #region Fields + + private readonly TContext context; + + #endregion Fields + + #region Constructors + + public RecurringCommandManagerSettings(SerializationFormat currentSerializationFormat) + { + this.CurrentSerializationFormat = currentSerializationFormat; + this.context = new TContext(); + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the associated context. + /// + public TContext Context => this.context; + + /// + /// Gets the current serialization format of the recurring commands. + /// + [DataMember(Order = 1)] + public SerializationFormat CurrentSerializationFormat { get; } + + #endregion Properties + } +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/SyncCommandHandlerWithLogging.cs b/Src/DDD.Core/Application/SyncCommandHandlerWithLogging.cs new file mode 100644 index 0000000..9923d72 --- /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 = null) + { + 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..8f63a46 --- /dev/null +++ b/Src/DDD.Core/Application/SyncCommandHandlerWithLogging`1.cs @@ -0,0 +1,60 @@ +using EnsureThat; +using DDD.Core.Domain; +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 + 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 = null) + { + 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..1c0ef7e --- /dev/null +++ b/Src/DDD.Core/Application/SyncEventHandler.cs @@ -0,0 +1,29 @@ +using System; + +namespace DDD.Core.Application +{ + using Domain; + + /// + /// Base class for handling synchronously events. + /// + public abstract class SyncEventHandler : ISyncEventHandler + where TEvent : class, IEvent + { + + #region Properties + + Type ISyncEventHandler.EventType => typeof(TEvent); + + #endregion Properties + + #region Methods + + public abstract void Handle(TEvent @event, IMessageContext context = null); + + 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..9c0c578 --- /dev/null +++ b/Src/DDD.Core/Application/SyncEventHandlerWithLogging.cs @@ -0,0 +1,63 @@ +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 + { + + #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 + + Type ISyncEventHandler.EventType => this.eventHandler.EventType; + + #endregion Properties + + #region Methods + + public void Handle(TEvent @event, IMessageContext context = null) + { + 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..abfe0c5 --- /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 = null) + { + 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..2dd2181 --- /dev/null +++ b/Src/DDD.Core/Application/SyncQueryHandlerWithLogging`1.cs @@ -0,0 +1,61 @@ +using EnsureThat; +using DDD.Core.Domain; +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 + 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 = null) + { + 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 dcb2f12..a40bb3a 100644 --- a/Src/DDD.Core/DDD.Core.csproj +++ b/Src/DDD.Core/DDD.Core.csproj @@ -1,8 +1,9 @@  - net48;netstandard2.1;net5.0 + net48;netstandard2.1 Library false + 8.0 bin\Debug\DDD.Core.xml @@ -22,10 +23,10 @@ - + - 5.0.0 + 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..f996e88 --- /dev/null +++ b/Src/DDD.Core/Domain/CompositeTranslatorExtensions.cs @@ -0,0 +1,39 @@ +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) => + { + Type entityType = null; + context?.TryGetValue("EntityType", out 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) => + { + Type serviceType = null; + context?.TryGetValue("ServiceType", out 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 33cbd5d..75acf5d 100644 --- a/Src/DDD.Core/Domain/DomainEntity.cs +++ b/Src/DDD.Core/Domain/DomainEntity.cs @@ -1,15 +1,17 @@ -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. /// public abstract class DomainEntity : IEquatable { - #region Fields private readonly List events = new List(); @@ -18,14 +20,21 @@ public abstract class DomainEntity : IEquatable #region Constructors - protected DomainEntity(IEnumerable events = null) + protected DomainEntity(EntityState entityState = EntityState.Added, 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) @@ -59,19 +68,25 @@ public bool Equals(DomainEntity other) public override int GetHashCode() => this.Identity().GetHashCode(); - public abstract ComparableValueObject Identity(); - public virtual string IdentityAsString() { return string.Join("/", this.Identity().PrimitiveEqualityComponents()); } + + public abstract ComparableValueObject Identity(); + protected void AddEvent(IDomainEvent @event) { - Condition.Requires(@event, nameof(@event)).IsNotNull(); + Ensure.That(@event, nameof(@event)).IsNotNull(); this.events.Add(@event); } - #endregion Methods + 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 f25dab2..82969d0 100644 --- a/Src/DDD.Core/Domain/DomainException.cs +++ b/Src/DDD.Core/Domain/DomainException.cs @@ -5,49 +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(bool isTransient, Exception innerException = null) - : base(DefaultMessage(), innerException) + : base(isTransient, DefaultMessage(), innerException) { - this.IsTransient = isTransient; } protected DomainException(bool isTransient, string message, Exception innerException = null) - : base(message, innerException) + : base(isTransient, message, innerException) { - this.IsTransient = isTransient; } #endregion Constructors - #region Properties - - /// - /// Gets a value indicating whether the exception is transient. - /// - public bool IsTransient { get; } - - #endregion Properties - #region Methods public static string DefaultMessage() => "An error occurred in the domain layer."; - public override string ToString() - { - var s = $"{this.GetType()}: {this.Message} "; - s += $"{Environment.NewLine}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/Domain/DomainServiceException.cs b/Src/DDD.Core/Domain/DomainServiceException.cs index ec146e4..4a64e0e 100644 --- a/Src/DDD.Core/Domain/DomainServiceException.cs +++ b/Src/DDD.Core/Domain/DomainServiceException.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.Serialization; namespace DDD.Core.Domain { @@ -39,9 +40,17 @@ public static string DefaultMessage(Type serviceType = null) 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}Timestamp: {this.Timestamp}"; s += $"{Environment.NewLine}IsTransient: {this.IsTransient}"; if (this.ServiceType != null) s += $"{Environment.NewLine}ServiceType: {this.ServiceType}"; diff --git a/Src/DDD.Core/Domain/DomainServiceInvalidException.cs b/Src/DDD.Core/Domain/DomainServiceInvalidException.cs index 606897e..2220174 100644 --- a/Src/DDD.Core/Domain/DomainServiceInvalidException.cs +++ b/Src/DDD.Core/Domain/DomainServiceInvalidException.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Runtime.Serialization; namespace DDD.Core.Domain { @@ -44,9 +45,20 @@ public DomainServiceInvalidException(string message, Type serviceType = null, Va 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}Timestamp: {this.Timestamp}"; s += $"{Environment.NewLine}IsTransient: {this.IsTransient}"; if (this.ServiceType != null) s += $"{Environment.NewLine}ServiceType: {this.ServiceType}"; diff --git a/Src/DDD.Core/Domain/EntityState.cs b/Src/DDD.Core/Domain/EntityState.cs new file mode 100644 index 0000000..1d3d3f8 --- /dev/null +++ b/Src/DDD.Core/Domain/EntityState.cs @@ -0,0 +1,10 @@ +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/IAsyncRepository.cs b/Src/DDD.Core/Domain/IAsyncRepository.cs index bf3a137..3d44a3b 100644 --- a/Src/DDD.Core/Domain/IAsyncRepository.cs +++ b/Src/DDD.Core/Domain/IAsyncRepository.cs @@ -1,8 +1,12 @@ -using System.Threading; +using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace DDD.Core.Domain { + /// + /// Defines a component to find and save asynchronously an aggregate. + /// public interface IAsyncRepository where TDomainEntity : DomainEntity where TIdentity : ComparableValueObject @@ -15,4 +19,4 @@ public interface IAsyncRepository #endregion Methods } -} +} \ No newline at end of file diff --git a/Src/DDD.Core/Domain/IRepository.cs b/Src/DDD.Core/Domain/IRepository.cs index 6d0bbe1..89c0712 100644 --- a/Src/DDD.Core/Domain/IRepository.cs +++ b/Src/DDD.Core/Domain/IRepository.cs @@ -1,15 +1,9 @@ namespace DDD.Core.Domain { - public interface IRepository + public interface IRepository + : ISyncRepository, IAsyncRepository where TDomainEntity : DomainEntity where TIdentity : ComparableValueObject { - #region Methods - - TDomainEntity Find(TIdentity 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 new file mode 100644 index 0000000..303ab16 --- /dev/null +++ b/Src/DDD.Core/Domain/IStateEntity.cs @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000..25f91d6 --- /dev/null +++ b/Src/DDD.Core/Domain/IStateObjectConvertible.cs @@ -0,0 +1,14 @@ +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..0342db1 --- /dev/null +++ b/Src/DDD.Core/Domain/ISyncRepository.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +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/RepositoryException.cs b/Src/DDD.Core/Domain/RepositoryException.cs index 08d3cad..81f5691 100644 --- a/Src/DDD.Core/Domain/RepositoryException.cs +++ b/Src/DDD.Core/Domain/RepositoryException.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.Serialization; namespace DDD.Core.Domain { @@ -39,9 +40,17 @@ public static string DefaultMessage(Type entityType = null) 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}Timestamp: {this.Timestamp}"; s += $"{Environment.NewLine}IsTransient: {this.IsTransient}"; if (this.EntityType != null) s += $"{Environment.NewLine}EntityType: {this.EntityType}"; 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/DbCommandHander.cs b/Src/DDD.Core/Infrastructure/Data/DbCommandHander.cs deleted file mode 100644 index 6dcfda9..0000000 --- a/Src/DDD.Core/Infrastructure/Data/DbCommandHander.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Conditions; -using System.Data; -using System.Data.Common; -using System; - -namespace DDD.Core.Infrastructure.Data -{ - using Application; - using Mapping; - - /// - /// Base class for handling database commands. - /// - /// The type of the command. - /// - public abstract class DbCommandHandler : ICommandHandler - where TCommand : class, ICommand - { - - #region Fields - - private readonly IObjectTranslator exceptionTranslator = DbToCommandExceptionTranslator.Default; - - #endregion Fields - - #region Constructors - - /// - /// Initializes a new instance of the class. - /// - /// The database connection factory. - protected DbCommandHandler(IDbConnectionFactory connectionFactory) - { - Condition.Requires(connectionFactory, nameof(connectionFactory)).IsNotNull(); - this.ConnectionFactory = connectionFactory; - } - - #endregion Constructors - - #region Properties - - protected IDbConnectionFactory ConnectionFactory { get; } - - #endregion Properties - - #region Methods - - public void Handle(TCommand command) - { - Condition.Requires(command, nameof(command)).IsNotNull(); - try - { - using (var connection = this.ConnectionFactory.CreateOpenConnection()) - { - this.Execute(command, connection); - } - } - catch (DbException ex) - { - throw this.exceptionTranslator.Translate(ex, new { Command = command }); - } - } - - protected abstract void Execute(TCommand command, IDbConnection connection); - - #endregion Methods - - } -} \ No newline at end of file diff --git a/Src/DDD.Core/Infrastructure/Data/DbConnectionFactory.cs b/Src/DDD.Core/Infrastructure/Data/DbConnectionFactory.cs deleted file mode 100644 index 07fcb07..0000000 --- a/Src/DDD.Core/Infrastructure/Data/DbConnectionFactory.cs +++ /dev/null @@ -1,44 +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 string providerName; - - #endregion Fields - - #region Constructors - - protected DbConnectionFactory(string providerName, string connectionString) - { - Condition.Requires(providerName, nameof(providerName)).IsNotNullOrWhiteSpace(); - Condition.Requires(connectionString, nameof(connectionString)).IsNotNullOrWhiteSpace(); - this.providerName = providerName; - this.connectionString = connectionString; - } - - #endregion Constructors - - #region Methods - - public DbConnection CreateConnection() - { - var providerFactory = DbProviderFactories.GetFactory(providerName); - var connection = providerFactory.CreateConnection(); - connection.ConnectionString = this.connectionString; - return connection; - } - - #endregion Methods - - } -} diff --git a/Src/DDD.Core/Infrastructure/Data/DbQueryHandler.cs b/Src/DDD.Core/Infrastructure/Data/DbQueryHandler.cs deleted file mode 100644 index 112038a..0000000 --- a/Src/DDD.Core/Infrastructure/Data/DbQueryHandler.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Conditions; -using System.Data; -using System.Data.Common; -using System.Threading; -using System.Threading.Tasks; - -namespace DDD.Core.Infrastructure.Data -{ - using Application; - using Threading; - using Mapping; - - /// - /// 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 Fields - - private readonly IObjectTranslator exceptionTranslator = DbToQueryExceptionTranslator.Default; - - #endregion Fields - - #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, CancellationToken cancellationToken = default) - { - Condition.Requires(query, nameof(query)).IsNotNull(); - await new SynchronizationContextRemover(); - try - { - using (var connection = await this.ConnectionFactory.CreateOpenConnectionAsync(cancellationToken)) - { - return await this.ExecuteAsync(query, connection, cancellationToken); - } - } - catch(DbException ex) - { - throw this.exceptionTranslator.Translate(ex, new { Query = query }); - } - - } - - protected abstract Task ExecuteAsync(TQuery query, IDbConnection connection, CancellationToken cancellationToken = default); - - #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/DbToCommandExceptionTranslator.cs b/Src/DDD.Core/Infrastructure/Data/DbToCommandExceptionTranslator.cs index 896b78d..9531f41 100644 --- a/Src/DDD.Core/Infrastructure/Data/DbToCommandExceptionTranslator.cs +++ b/Src/DDD.Core/Infrastructure/Data/DbToCommandExceptionTranslator.cs @@ -1,19 +1,18 @@ using System.Data.Common; using System.Collections.Generic; -using Conditions; +using EnsureThat; namespace DDD.Core.Infrastructure.Data { using Mapping; + using Collections; using Application; - public class DbToCommandExceptionTranslator : IObjectTranslator + public class DbToCommandExceptionTranslator : ObjectTranslator { #region Fields - public static readonly IObjectTranslator Default = new DbToCommandExceptionTranslator(); - private readonly Dictionary> translators = new Dictionary>(); #endregion Fields @@ -23,25 +22,24 @@ public class DbToCommandExceptionTranslator : IObjectTranslator options = null) + public override CommandException Translate(DbException exception, IDictionary context = null) { - Condition.Requires(exception, nameof(exception)).IsNotNull(); - Condition.Requires(options, nameof(options)) - .IsNotNull() - .Evaluate(options.ContainsKey("Command")); + Ensure.That(exception, nameof(exception)).IsNotNull(); var exceptionType = exception.GetType().FullName; if (this.translators.TryGetValue(exceptionType, out var translator)) - return translator.Translate(exception, options); + return translator.Translate(exception, context); else { - var command = (ICommand)options["Command"]; + ICommand command = null; + context?.TryGetValue("Command", out command); return new CommandException(isTransient: false, command, exception); } } @@ -49,4 +47,4 @@ public CommandException Translate(DbException exception, IDictionary + public class DbToQueryExceptionTranslator : ObjectTranslator { #region Fields - public static readonly IObjectTranslator Default = new DbToQueryExceptionTranslator(); - private readonly Dictionary> translators = new Dictionary>(); #endregion Fields @@ -23,25 +22,24 @@ public class DbToQueryExceptionTranslator : IObjectTranslator options = null) + public override QueryException Translate(DbException exception, IDictionary context = null) { - Condition.Requires(exception, nameof(exception)).IsNotNull(); - Condition.Requires(options, nameof(options)) - .IsNotNull() - .Evaluate(options.ContainsKey("Query")); + Ensure.That(exception, nameof(exception)).IsNotNull(); var exceptionType = exception.GetType().FullName; if (this.translators.TryGetValue(exceptionType, out var translator)) - return translator.Translate(exception, options); - else + return translator.Translate(exception, context); + else { - var query = (IQuery)options["Query"]; + IQuery query = null; + context?.TryGetValue("Query", out query); return new QueryException(isTransient: false, query, exception); } } @@ -49,4 +47,4 @@ public QueryException Translate(DbException exception, IDictionary + public class DbToRepositoryExceptionTranslator : ObjectTranslator { #region Fields - public static readonly IObjectTranslator Default = new DbToRepositoryExceptionTranslator(); - private readonly Dictionary> translators = new Dictionary>(); #endregion Fields @@ -24,26 +23,25 @@ public class DbToRepositoryExceptionTranslator : IObjectTranslator options = null) + public override RepositoryException Translate(DbException exception, IDictionary context = null) { - Condition.Requires(exception, nameof(exception)).IsNotNull(); - Condition.Requires(options, nameof(options)) - .IsNotNull() - .Evaluate(options.ContainsKey("EntityType")); + Ensure.That(exception, nameof(exception)).IsNotNull(); var exceptionType = exception.GetType().FullName; if (this.translators.TryGetValue(exceptionType, out var translator)) - return translator.Translate(exception, options); - else + return translator.Translate(exception, context); + else { - var entityType = (Type)options["EntityType"]; - var outerException = options.ContainsKey("OuterException") ? (Exception)options["OuterException"] : exception; + Type entityType = null; + context?.TryGetValue("EntityType", out entityType); + var outerException = context.ContainsKey("OuterException") ? (Exception)context["OuterException"] : exception; return new RepositoryException(isTransient: false, entityType, outerException); } } @@ -51,4 +49,4 @@ public RepositoryException Translate(DbException exception, IDictionary + /// 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 291bbba..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,16 +9,17 @@ 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": case "Microsoft.Data.SqlClient.SqlConnection": return DbStandardExpressions.SqlServer2012; - case "Oracle.ManagedDataAccess.Client.OracleConnection": return DbStandardExpressions.Oracle11; @@ -26,6 +28,38 @@ public static IDbStandardExpressions Expressions(this IDbConnection 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 bbb5c4f..0000000 --- a/Src/DDD.Core/Infrastructure/Data/IDbConnectionFactoryExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Data.Common; -using System.Threading; -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, CancellationToken cancellationToken = default) - { - Condition.Requires(factory, nameof(factory)).IsNotNull(); - await new SynchronizationContextRemover(); - var connection = factory.CreateConnection(); - await connection.OpenAsync(cancellationToken); - 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..b5f4809 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider.cs @@ -0,0 +1,26 @@ +using System; +using System.Data.Common; + +namespace DDD.Core.Infrastructure.Data +{ + using Domain; + + /// + /// Provides and shares a database connection 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 + + 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..38fc8a4 --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider`1.cs @@ -0,0 +1,20 @@ +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using DDD.Core.Domain; + + /// + /// Provides and shares a database connection for a specific context between different components. + /// + public interface IDbConnectionProvider + : IDbConnectionProvider where TContext : BoundedContext + { + + #region Properties + + 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..bbf1f44 --- /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 Application; + using DDD.Core.Domain; + + public class LazyDbConnectionProvider + : IDbConnectionProvider where TContext : BoundedContext, new() + { + + #region Fields + + private readonly string connectionString; + private readonly Lazy lazyConnection; + private readonly string providerName; + private bool isDisposed; + + #endregion Fields + + #region Constructors + + public LazyDbConnectionProvider(string providerName, string connectionString) + { + Ensure.That(providerName, nameof(providerName)).IsNotNullOrWhiteSpace(); + Ensure.That(connectionString, nameof(connectionString)).IsNotNullOrWhiteSpace(); + this.providerName = providerName; + this.connectionString = connectionString; + this.Context = new TContext(); + this.lazyConnection = new Lazy(() => this.CreateConnection()); + } + + #endregion Constructors + + #region Properties + + public DbConnection Connection => this.lazyConnection.Value; + public TContext Context { get; } + + #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.providerName); + var connection = providerFactory.CreateConnection(); + connection.ConnectionString = this.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 index 4e0676d..15419db 100644 --- a/Src/DDD.Core/Infrastructure/Data/OracleErrorHelper.cs +++ b/Src/DDD.Core/Infrastructure/Data/OracleErrorHelper.cs @@ -1,9 +1,9 @@ -using Conditions; +using System; namespace DDD.Core.Infrastructure.Data { /// - /// To Improve. + /// To Improve. Use dynamic type to avoid to add a dependency on the Oracle library. /// internal class OracleErrorHelper { @@ -11,44 +11,48 @@ internal class OracleErrorHelper public static bool IsUnavailableError(dynamic error) { - Condition.Requires(error, nameof(error)).IsNotNull(); - switch (error.Number) + // 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 - case 3114: - return true; - default: - return false; - } + 3114 => true, + _ => false, + }; } public static bool IsUnauthorizedError(dynamic error) { - Condition.Requires(error, nameof(error)).IsNotNull(); - switch (error.Number) + if (error == null) throw new ArgumentNullException(nameof(error)); + return (dynamic)error.Number switch { // Oracle Error Code: 1017 // invalid username/password; logon denied - case 4060: - return true; - default: - return false; - } + 4060 => true, + _ => false, + }; } public static bool IsTimeoutError(dynamic error) { - Condition.Requires(error, nameof(error)).IsNotNull(); - switch (error.Number) + if (error == null) throw new ArgumentNullException(nameof(error)); + return (dynamic)error.Number switch { // Oracle Error Code: 1013 // user requested cancel of current operation - case 1013: - return true; - default: - return false; - } + 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 index 1809863..931a6b6 100644 --- a/Src/DDD.Core/Infrastructure/Data/OracleToCommandExceptionTranslator.cs +++ b/Src/DDD.Core/Infrastructure/Data/OracleToCommandExceptionTranslator.cs @@ -1,23 +1,25 @@ using System.Data.Common; using System.Collections.Generic; -using Conditions; +using EnsureThat; namespace DDD.Core.Infrastructure.Data { using Mapping; using Application; + using Collections; - internal class OracleToCommandExceptionTranslator : IObjectTranslator + /// + /// Use dynamic type to avoid to add a dependency on the Oracle library. + /// + internal class OracleToCommandExceptionTranslator : ObjectTranslator { #region Methods - public CommandException Translate(DbException exception, IDictionary options = null) + public override CommandException Translate(DbException exception, IDictionary context = null) { - Condition.Requires(exception, nameof(exception)).IsNotNull(); - Condition.Requires(options, nameof(options)) - .IsNotNull() - .Evaluate(options.ContainsKey("Command")); - var command = (ICommand)options["Command"]; + Ensure.That(exception, nameof(exception)).IsNotNull(); + ICommand command = null; + context?.TryGetValue("Command", out command); dynamic oracleException = exception; foreach (dynamic error in oracleException.Errors) { @@ -29,6 +31,9 @@ public CommandException Translate(DbException exception, IDictionary + /// + /// Use dynamic type to avoid to add a dependency on the Oracle library. + /// + internal class OracleToQueryExceptionTranslator : ObjectTranslator { #region Methods - public QueryException Translate(DbException exception, IDictionary options = null) + public override QueryException Translate(DbException exception, IDictionary context = null) { - Condition.Requires(exception, nameof(exception)).IsNotNull(); - Condition.Requires(options, nameof(options)) - .IsNotNull() - .Evaluate(options.ContainsKey("Query")); - var query = (IQuery)options["Query"]; + Ensure.That(exception, nameof(exception)).IsNotNull(); + IQuery query = null; + context?.TryGetValue("Query", out query); dynamic oracleException = exception; foreach (dynamic error in oracleException.Errors) { @@ -29,6 +31,9 @@ public QueryException Translate(DbException exception, IDictionary + /// + /// Use dynamic type to avoid to add a dependency on the Oracle library. + /// + internal class OracleToRepositoryExceptionTranslator : ObjectTranslator { #region Methods - public RepositoryException Translate(DbException exception, IDictionary options = null) + public override RepositoryException Translate(DbException exception, IDictionary context = null) { - Condition.Requires(exception, nameof(exception)).IsNotNull(); - Condition.Requires(options, nameof(options)) - .IsNotNull() - .Evaluate(options.ContainsKey("EntityType")); - var entityType = (Type)options["EntityType"]; - var outerException = options.ContainsKey("OuterException") ? (Exception)options["OuterException"] : exception; + Ensure.That(exception, nameof(exception)).IsNotNull(); + Type entityType = null; + context?.TryGetValue("EntityType", out entityType); + var outerException = context.ContainsKey("OuterException") ? (Exception)context["OuterException"] : exception; dynamic oracleException = exception; foreach (dynamic error in oracleException.Errors) { @@ -31,6 +33,9 @@ public RepositoryException Translate(DbException exception, IDictionary 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..c507aab --- /dev/null +++ b/Src/DDD.Core/Infrastructure/Data/SequentialSqlServerGuidGenerator.cs @@ -0,0 +1,36 @@ +using System; + +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 index 5a16f90..f652b48 100644 --- a/Src/DDD.Core/Infrastructure/Data/SqlServerErrorHelper.cs +++ b/Src/DDD.Core/Infrastructure/Data/SqlServerErrorHelper.cs @@ -1,4 +1,4 @@ -using Conditions; +using System; namespace DDD.Core.Infrastructure.Data { @@ -9,7 +9,8 @@ internal static class SqlServerErrorHelper public static bool IsUnavailableError(dynamic error) { - Condition.Requires(error, nameof(error)).IsNotNull(); + // 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 @@ -71,7 +72,7 @@ public static bool IsUnavailableError(dynamic error) public static bool IsUnauthorizedError(dynamic error) { - Condition.Requires(error, nameof(error)).IsNotNull(); + if (error == null) throw new ArgumentNullException(nameof(error)); switch (error.Number) { // SQL Error Code: 40532 @@ -106,7 +107,7 @@ public static bool IsUnauthorizedError(dynamic error) public static bool IsTimeoutError(dynamic error) { - Condition.Requires(error, nameof(error)).IsNotNull(); + if (error == null) throw new ArgumentNullException(nameof(error)); switch (error.Number) { // DBNETLIB Error Code: -2 @@ -118,6 +119,15 @@ public static bool IsTimeoutError(dynamic error) } } + 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 index 92acddd..ae30da4 100644 --- a/Src/DDD.Core/Infrastructure/Data/SqlServerToCommandExceptionTranslator.cs +++ b/Src/DDD.Core/Infrastructure/Data/SqlServerToCommandExceptionTranslator.cs @@ -1,23 +1,22 @@ using System.Data.Common; using System.Collections.Generic; -using Conditions; +using EnsureThat; namespace DDD.Core.Infrastructure.Data { using Mapping; using Application; + using Collections; - internal class SqlServerToCommandExceptionTranslator : IObjectTranslator + internal class SqlServerToCommandExceptionTranslator : ObjectTranslator { #region Methods - public CommandException Translate(DbException exception, IDictionary options = null) + public override CommandException Translate(DbException exception, IDictionary context = null) { - Condition.Requires(exception, nameof(exception)).IsNotNull(); - Condition.Requires(options, nameof(options)) - .IsNotNull() - .Evaluate(options.ContainsKey("Command")); - var command = (ICommand)options["Command"]; + Ensure.That(exception, nameof(exception)).IsNotNull(); + ICommand command = null; + context?.TryGetValue("Command", out command); dynamic sqlServerException = exception; foreach (dynamic error in sqlServerException.Errors) { @@ -29,6 +28,9 @@ public CommandException Translate(DbException exception, IDictionary + internal class SqlServerToQueryExceptionTranslator : ObjectTranslator { #region Methods - public QueryException Translate(DbException exception, IDictionary options = null) + public override QueryException Translate(DbException exception, IDictionary context = null) { - Condition.Requires(exception, nameof(exception)).IsNotNull(); - Condition.Requires(options, nameof(options)) - .IsNotNull() - .Evaluate(options.ContainsKey("Query")); - var query = (IQuery)options["Query"]; + Ensure.That(exception, nameof(exception)).IsNotNull(); + IQuery query = null; + context?.TryGetValue("Query", out query); dynamic sqlServerException = exception; foreach (dynamic error in sqlServerException.Errors) { @@ -29,6 +28,9 @@ public QueryException Translate(DbException exception, IDictionary + internal class SqlServerToRepositoryExceptionTranslator : ObjectTranslator { #region Methods - public RepositoryException Translate(DbException exception, IDictionary options = null) + public override RepositoryException Translate(DbException exception, IDictionary context = null) { - Condition.Requires(exception, nameof(exception)).IsNotNull(); - Condition.Requires(options, nameof(options)) - .IsNotNull() - .Evaluate(options.ContainsKey("EntityType")); - var entityType = (Type)options["EntityType"]; - var outerException = options.ContainsKey("OuterException") ? (Exception)options["OuterException"] : exception; + Ensure.That(exception, nameof(exception)).IsNotNull(); + Type entityType = null; + context?.TryGetValue("EntityType", out entityType); + var outerException = context.ContainsKey("OuterException") ? (Exception)context["OuterException"] : exception; dynamic sqlServerException = exception; foreach (dynamic error in sqlServerException.Errors) { @@ -31,6 +30,9 @@ public RepositoryException Translate(DbException exception, IDictionary - { - - #region Fields - - private readonly ITextSerializer eventSerializer; - - #endregion Fields - - #region Constructors - - public StoredEventTranslator(ITextSerializer eventSerializer) - { - Condition.Requires(eventSerializer, nameof(eventSerializer)).IsNotNull(); - this.eventSerializer = eventSerializer; - } - - #endregion Constructors - - #region Methods - - public StoredEvent Translate(IEvent @event, IDictionary options = null) - { - Condition.Requires(@event, nameof(@event)).IsNotNull(); - var eventType = @event.GetType(); - return new StoredEvent() - { - OccurredOn = @event.OccurredOn, - EventType = $"{eventType.FullName}, {eventType.Assembly.GetName().Name}", - Version = ToVersion(eventType.FullName), - Body = this.eventSerializer.SerializeToString(@event), - BodyFormat = this.eventSerializer.Format - }; - } - - private static byte ToVersion(string fullName) - { - byte version = 1; - var match = Regex.Match(fullName, @".(Version|V)\d+."); - if (match.Success) - { - var value = Regex.Replace(match.Value, "(Version|V)", string.Empty) - .Replace(".", string.Empty); - version = byte.Parse(value); - } - return version; - } - - #endregion Methods - - } -} \ No newline at end of file diff --git a/Src/DDD.Core/Infrastructure/Serialization/DataContractSerializerWrapper.cs b/Src/DDD.Core/Infrastructure/Serialization/DataContractSerializerWrapper.cs index b332040..0763039 100644 --- a/Src/DDD.Core/Infrastructure/Serialization/DataContractSerializerWrapper.cs +++ b/Src/DDD.Core/Infrastructure/Serialization/DataContractSerializerWrapper.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System; using System.IO; using System.Runtime.Serialization; @@ -30,8 +30,8 @@ public DataContractSerializerWrapper() 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; } @@ -52,7 +52,7 @@ public DataContractSerializerWrapper(XmlWriterSettings writerSettings, public static DataContractSerializerWrapper Create(Encoding encoding, bool indent = true) { - Condition.Requires(encoding, nameof(encoding)).IsNotNull(); + Ensure.That(encoding, nameof(encoding)).IsNotNull(); var writerSettings = DefaultWriterSettings(); writerSettings.Encoding = encoding; writerSettings.Indent = indent; @@ -64,8 +64,8 @@ public static DataContractSerializerWrapper Create(Encoding encoding, bool inden public object Deserialize(Stream stream, Type type) { - Condition.Requires(stream, nameof(stream)).IsNotNull(); - Condition.Requires(type, nameof(type)).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(type); @@ -82,8 +82,8 @@ public object Deserialize(Stream stream, Type type) 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()); diff --git a/Src/DDD.Core/Infrastructure/Serialization/XmlSerializerWrapper.cs b/Src/DDD.Core/Infrastructure/Serialization/XmlSerializerWrapper.cs index 5618c6e..a022011 100644 --- a/Src/DDD.Core/Infrastructure/Serialization/XmlSerializerWrapper.cs +++ b/Src/DDD.Core/Infrastructure/Serialization/XmlSerializerWrapper.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System; using System.IO; using System.Text; @@ -29,8 +29,8 @@ public XmlSerializerWrapper() 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; } @@ -51,7 +51,7 @@ public XmlSerializerWrapper(XmlWriterSettings writerSettings, public static XmlSerializerWrapper Create(Encoding encoding, bool indent = true) { - Condition.Requires(encoding, nameof(encoding)).IsNotNull(); + Ensure.That(encoding, nameof(encoding)).IsNotNull(); var writerSettings = DefaultWriterSettings(); writerSettings.Encoding = encoding; writerSettings.Indent = indent; @@ -63,8 +63,8 @@ public static XmlSerializerWrapper Create(Encoding encoding, bool indent = true) public object Deserialize(Stream stream, Type type) { - Condition.Requires(stream, nameof(stream)).IsNotNull(); - Condition.Requires(type, nameof(type)).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(type); @@ -81,8 +81,8 @@ public object Deserialize(Stream stream, Type type) 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()); diff --git a/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/CreatePharmaceuticalPrescription.cs b/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/CreatePharmaceuticalPrescription.cs index fed3278..e12644d 100644 --- a/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/CreatePharmaceuticalPrescription.cs +++ b/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/CreatePharmaceuticalPrescription.cs @@ -91,4 +91,4 @@ public override string ToString() #endregion Methods } -} +} \ No newline at end of file diff --git a/Src/DDD.HealthcareDelivery.Messages/DDD.HealthcareDelivery.Messages.csproj b/Src/DDD.HealthcareDelivery.Messages/DDD.HealthcareDelivery.Messages.csproj index 7715044..638359a 100644 --- a/Src/DDD.HealthcareDelivery.Messages/DDD.HealthcareDelivery.Messages.csproj +++ b/Src/DDD.HealthcareDelivery.Messages/DDD.HealthcareDelivery.Messages.csproj @@ -1,6 +1,6 @@  - net48;netstandard2.1;net5.0 + net48;netstandard2.1 Library DDD.HealthcareDelivery false @@ -15,7 +15,7 @@ - + \ No newline at end of file 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 680d342..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; @@ -14,7 +14,7 @@ public class PharmaceuticalPrescriptionCreated : IDomainEvent 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 = occurredOn; } @@ -26,12 +26,13 @@ private PharmaceuticalPrescriptionCreated() { } #region Properties - [DataMember(Name = "OccurredOn", Order = 2)] - public DateTime OccurredOn { get; private set; } - - [DataMember(Name = "PrescriptionId", Order = 1)] + [DataMember(Name = "PrescriptionId")] public int PrescriptionIdentifier { get; private set; } + [DataMember(Name = "OccurredOn")] + public DateTime OccurredOn { get; private set; } + #endregion Properties + } } diff --git a/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionRevoked.cs b/Src/DDD.HealthcareDelivery.Messages/Domain/Prescriptions/PharmaceuticalPrescriptionRevoked.cs index 92b5b35..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; @@ -14,8 +14,8 @@ public class PharmaceuticalPrescriptionRevoked : IDomainEvent 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 = occurredOn; @@ -28,13 +28,13 @@ private PharmaceuticalPrescriptionRevoked() { } #region Properties - [DataMember(Name = "OccurredOn", Order = 3)] - public DateTime OccurredOn { get; private set; } - - [DataMember(Name = "PrescriptionId", Order = 1)] + [DataMember(Name = "PrescriptionId")] public int PrescriptionIdentifier { get; private set; } - [DataMember(Name = "Reason", Order = 2)] + [DataMember(Name = "OccurredOn")] + public DateTime OccurredOn { get; private set; } + + [DataMember(Name = "Reason")] public string Reason { get; private set; } #endregion Properties diff --git a/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs b/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs index 393150b..d7aec34 100644 --- a/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs +++ b/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System; using System.Collections.Generic; using System.Linq; @@ -14,15 +14,15 @@ namespace DDD.HealthcareDelivery.Application.Prescriptions using Mapping; public class BelgianPharmaceuticalPrescriptionTranslator - : IObjectTranslator + : ObjectTranslator { #region Methods - public PharmaceuticalPrescription Translate(CreatePharmaceuticalPrescription command, - IDictionary options = null) + public override PharmaceuticalPrescription Translate(CreatePharmaceuticalPrescription command, + IDictionary context = null) { - Condition.Requires(command, nameof(command)).IsNotNull(); + Ensure.That(command, nameof(command)).IsNotNull(); return PharmaceuticalPrescription.Create ( new PrescriptionIdentifier(command.PrescriptionIdentifier), @@ -149,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 9470073..a6a53a3 100644 --- a/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionCreator.cs +++ b/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionCreator.cs @@ -1,5 +1,5 @@ -using Conditions; -using System.Threading; +using EnsureThat; +using System; using System.Threading.Tasks; using System.Transactions; @@ -9,40 +9,72 @@ 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 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, - IObjectTranslator translator) + public PharmaceuticalPrescriptionCreator(IRepository repository, + IObjectTranslator commandTranslator) { - Condition.Requires(repository, nameof(repository)).IsNotNull(); - Condition.Requires(translator, nameof(translator)).IsNotNull(); + Ensure.That(repository, nameof(repository)).IsNotNull(); + Ensure.That(commandTranslator, nameof(commandTranslator)).IsNotNull(); this.repository = repository; - 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, CancellationToken cancellationToken = default) + public void Handle(CreatePharmaceuticalPrescription command, IMessageContext context = null) { - 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, cancellationToken); - 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 = null) + { + Ensure.That(command, nameof(command)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + var cancellationToken = context?.CancellationToken() ?? default; + 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 30ef5c0..952166d 100644 --- a/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionRevoker.cs +++ b/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionRevoker.cs @@ -1,5 +1,5 @@ -using Conditions; -using System.Threading; +using EnsureThat; +using System; using System.Threading.Tasks; using System.Transactions; @@ -8,37 +8,72 @@ 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 IAsyncRepository repository; + private readonly IRepository repository; + private readonly CompositeTranslator exceptionTranslator; #endregion Fields #region Constructors - public PharmaceuticalPrescriptionRevoker(IAsyncRepository repository) + public PharmaceuticalPrescriptionRevoker(IRepository repository) { - Condition.Requires(repository, nameof(repository)).IsNotNull(); + Ensure.That(repository, nameof(repository)).IsNotNull(); this.repository = repository; + this.exceptionTranslator = new CompositeTranslator(); + this.exceptionTranslator.Register(new DomainToCommandExceptionTranslator()); + this.exceptionTranslator.RegisterFallback(); } #endregion Constructors #region Methods - protected override async Task ExecuteAsync(RevokePharmaceuticalPrescription command, CancellationToken cancellationToken = default) + public void Handle(RevokePharmaceuticalPrescription command, IMessageContext context = null) + { + Ensure.That(command, nameof(command)).IsNotNull(); + try + { + 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 = null) { - using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + Ensure.That(command, nameof(command)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + var cancellationToken = context?.CancellationToken() ?? default; + 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()) { - var prescription = await this.repository.FindAsync(new PrescriptionIdentifier(command.PrescriptionIdentifier)); - prescription.Revoke(command.RevocationReason); - await this.repository.SaveAsync(prescription, cancellationToken); - scope.Complete(); + 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 e771091..11c3a97 100644 --- a/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj +++ b/Src/DDD.HealthcareDelivery/DDD.HealthcareDelivery.csproj @@ -1,6 +1,6 @@  - net48;netstandard2.1;net5.0 + net48;netstandard2.1 false bin\$(Configuration)\ bin\$(Configuration)\DDD.HealthcareDelivery.xml @@ -27,12 +27,11 @@ - - - + + - + diff --git a/Src/DDD.HealthcareDelivery/Domain/Encounters/EncounterIdentifier.cs b/Src/DDD.HealthcareDelivery/Domain/Encounters/EncounterIdentifier.cs index e236ace..f8f9a21 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Encounters/EncounterIdentifier.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Encounters/EncounterIdentifier.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; namespace DDD.HealthcareDelivery.Domain.Encounters { @@ -11,7 +11,7 @@ public class EncounterIdentifier : ArbitraryIdentifier public EncounterIdentifier(int value) : base(value) { - Condition.Requires(value, nameof(value)).IsGreaterThan(0); + Ensure.That(value, nameof(value)).IsGt(0); } protected EncounterIdentifier() diff --git a/Src/DDD.HealthcareDelivery/Domain/Patients/Patient.cs b/Src/DDD.HealthcareDelivery/Domain/Patients/Patient.cs index e7cff33..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; @@ -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; diff --git a/Src/DDD.HealthcareDelivery/Domain/Practitioners/BelgianHealthcarePractitionerLicenseNumber.cs b/Src/DDD.HealthcareDelivery/Domain/Practitioners/BelgianHealthcarePractitionerLicenseNumber.cs index d91802e..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,9 +12,8 @@ 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() { } @@ -64,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/HealthcarePractitioner.cs b/Src/DDD.HealthcareDelivery/Domain/Practitioners/HealthcarePractitioner.cs index 3c91d97..5299ae4 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Practitioners/HealthcarePractitioner.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Practitioners/HealthcarePractitioner.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System.Collections.Generic; namespace DDD.HealthcareDelivery.Domain.Practitioners @@ -25,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; diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/BelgianMedicationCode.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/BelgianMedicationCode.cs index 1916132..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,9 +12,8 @@ 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() { } @@ -28,9 +27,8 @@ protected BelgianMedicationCode() { } /// 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/PharmaceuticalPrescription.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs index fbf5c75..54070fd 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -35,13 +35,12 @@ public PharmaceuticalPrescription(PrescriptionIdentifier identifier, DateTime createdOn, EncounterIdentifier encounterIdentifier = null, DateTime? delivrableAt = null, + EntityState entityState = EntityState.Added, IEnumerable events = null) - : base(identifier, prescriber, patient, languageCode, status, createdOn, encounterIdentifier, delivrableAt, events) + : base(identifier, prescriber, patient, languageCode, status, createdOn, encounterIdentifier, delivrableAt, entityState, events) { - Condition.Requires(prescribedMedications, nameof(prescribedMedications)) - .IsNotNull() - .IsNotEmpty() - .DoesNotContain(null); + Ensure.That(prescribedMedications, nameof(prescribedMedications)).IsNotNull(); + Ensure.Enumerable.HasItems(prescribedMedications, nameof(prescribedMedications)); this.prescribedMedications.AddRange(prescribedMedications); } diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedication.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedication.cs index afb0afd..5f8921e 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedication.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescribedMedication.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System.Collections.Generic; namespace DDD.HealthcareDelivery.Domain.Prescriptions @@ -24,13 +24,13 @@ protected PrescribedMedication(string nameOrDescription, MedicationCode code = null, 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 (quantity.HasValue) { - Condition.Requires(quantity, nameof(quantity)).IsGreaterOrEqual(1); + Ensure.That(quantity.Value, nameof(quantity)).IsGte((byte)1); this.Quantity = quantity; } this.Code = code; diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/Prescription.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/Prescription.cs index 446fb13..f0ae3b6 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/Prescription.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/Prescription.cs @@ -1,4 +1,4 @@ -using Conditions; +using EnsureThat; using System; using System.Collections.Generic; @@ -28,14 +28,15 @@ protected Prescription(PrescriptionIdentifier identifier, DateTime createdOn, EncounterIdentifier encounterIdentifier = null, DateTime? delivrableAt = null, + EntityState entityState = EntityState.Added, IEnumerable events = null) - : base(events) + : base(entityState, events) { - Condition.Requires(identifier, nameof(identifier)).IsNotNull(); - Condition.Requires(prescriber, nameof(prescriber)).IsNotNull(); - Condition.Requires(patient, nameof(patient)).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; @@ -74,7 +75,7 @@ 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; diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescriptionIdentifier.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PrescriptionIdentifier.cs index 6baa80a..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,7 @@ 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() diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/BelgianOracleHealthcareDeliveryConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/BelgianOracleHealthcareDeliveryConfiguration.cs index caae2a5..6e72116 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/BelgianOracleHealthcareDeliveryConfiguration.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/BelgianOracleHealthcareDeliveryConfiguration.cs @@ -10,14 +10,6 @@ namespace DDD.HealthcareDelivery.Infrastructure public class BelgianOracleHealthcareDeliveryConfiguration : OracleHealthcareDeliveryConfiguration { - #region Constructors - - public BelgianOracleHealthcareDeliveryConfiguration(string connectionString) : base(connectionString) - { - } - - #endregion Constructors - #region Methods protected override void AddMappings(ModelMapper modelMapper) diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/BelgianSqlServerHealthcareDeliveryConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/BelgianSqlServerHealthcareDeliveryConfiguration.cs index a903dc2..88e2d06 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/BelgianSqlServerHealthcareDeliveryConfiguration.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/BelgianSqlServerHealthcareDeliveryConfiguration.cs @@ -10,14 +10,6 @@ namespace DDD.HealthcareDelivery.Infrastructure public class BelgianSqlServerHealthcareDeliveryConfiguration : SqlServerHealthcareDeliveryConfiguration { - #region Constructors - - public BelgianSqlServerHealthcareDeliveryConfiguration(string connectionString) : base(connectionString) - { - } - - #endregion Constructors - #region Methods protected override void AddMappings(ModelMapper modelMapper) diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/HealthcareDeliveryConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/HealthcareDeliveryConfiguration.cs index 09dd603..fec263c 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/HealthcareDeliveryConfiguration.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/HealthcareDeliveryConfiguration.cs @@ -1,5 +1,4 @@ -using Conditions; -using NHibernate.Cfg; +using NHibernate.Cfg; using NHibernate.Mapping.ByCode; namespace DDD.HealthcareDelivery.Infrastructure @@ -11,13 +10,8 @@ public abstract class HealthcareDeliveryConfiguration : Configuration #region Constructors - protected HealthcareDeliveryConfiguration(string connectionString) + protected HealthcareDeliveryConfiguration() { - Condition.Requires(connectionString, nameof(connectionString)).IsNotNullOrWhiteSpace(); - this.DataBaseIntegration(db => - { - db.ConnectionString = connectionString; - }); var modelMapper = new ModelMapper(); this.AddMappings(modelMapper); this.AddMapping(modelMapper.CompileMappingForAllExplicitlyAddedEntities()); diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/IHealthcareDeliveryConnectionFactory.cs b/Src/DDD.HealthcareDelivery/Infrastructure/IHealthcareDeliveryConnectionFactory.cs deleted file mode 100644 index a101167..0000000 --- a/Src/DDD.HealthcareDelivery/Infrastructure/IHealthcareDeliveryConnectionFactory.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 IHealthcareDeliveryConnectionFactory : IDbConnectionFactory - { - } -} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareDeliveryConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareDeliveryConfiguration.cs index 060a87a..bbc2653 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareDeliveryConfiguration.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/OracleHealthcareDeliveryConfiguration.cs @@ -1,6 +1,4 @@ -using NHibernate.Dialect; -using NHibernate.Driver; -using NHibernate.Mapping.ByCode; +using NHibernate.Mapping.ByCode; namespace DDD.HealthcareDelivery.Infrastructure { @@ -11,13 +9,8 @@ public abstract class OracleHealthcareDeliveryConfiguration : HealthcareDelivery #region Constructors - protected OracleHealthcareDeliveryConfiguration(string connectionString) : base(connectionString) + protected OracleHealthcareDeliveryConfiguration() { - this.DataBaseIntegration(db => - { - db.Dialect(); - db.Driver(); - }); this.SetNamingStrategy(UpperCaseNamingStrategy.Instance); } @@ -28,7 +21,7 @@ protected OracleHealthcareDeliveryConfiguration(string connectionString) : base( protected override void AddMappings(ModelMapper modelMapper) { base.AddMappings(modelMapper); - modelMapper.AddMapping(); + modelMapper.AddMapping(); } #endregion Methods diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionRepository.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionRepository.cs new file mode 100644 index 0000000..738432e --- /dev/null +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionRepository.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; + +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 + : NHRepository + { + #region Constructors + + public PharmaceuticalPrescriptionRepository(ISessionFactory sessionFactory, + IObjectTranslator eventTranslator) + : base(sessionFactory, eventTranslator) + { + } + + #endregion Constructors + } +} diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinder.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinder.cs index 71b2c73..f804999 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinder.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinder.cs @@ -1,42 +1,87 @@ -using System.Collections.Generic; -using Dapper; -using System.Data; -using System.Threading; +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(IHealthcareDeliveryConnectionFactory 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 = null) { + 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, - CancellationToken cancellationToken = default) + public async Task> HandleAsync(FindPharmaceuticalPrescriptionsByPatient query, IMessageContext context = null) { - var expressions = connection.Expressions(); - return connection.QueryAsync - ( - new CommandDefinition - ( - SqlScripts.FindPharmaceuticalPrescriptionsByPatient.Replace("@", expressions.ParameterPrefix()), - new { PatientId = query.PatientIdentifier }, - cancellationToken: cancellationToken - ) - ); + Ensure.That(query, nameof(query)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + var cancellationToken = context?.CancellationToken() ?? default; + 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/PrescribedMedicationsByPrescriptionFinder.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinder.cs index 49a8f6c..ef10d68 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinder.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinder.cs @@ -1,43 +1,83 @@ -using System.Collections.Generic; -using Dapper; -using System.Data; -using System.Threading; +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(IHealthcareDeliveryConnectionFactory 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, - CancellationToken cancellationToken = default) + public IEnumerable Handle(FindPrescribedMedicationsByPrescription query, IMessageContext context = null) + { + 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 = null) { - var expressions = connection.Expressions(); - return connection.QueryAsync - ( - new CommandDefinition + Ensure.That(query, nameof(query)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + var cancellationToken = context?.CancellationToken() ?? default; + var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); + var expressions = connection.Expressions(); + return await connection.QueryAsync ( - SqlScripts.FindPrescribedMedicationsByPrescription.Replace("@", expressions.ParameterPrefix()), - new { PrescriptionId = query.PrescriptionIdentifier }, - cancellationToken: cancellationToken - ) - ); + 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/PrescriptionIdentifierGenerator.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionIdentifierGenerator.cs index 6532d70..515b260 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionIdentifierGenerator.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionIdentifierGenerator.cs @@ -1,31 +1,70 @@ -using System.Data; -using System.Threading; +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(IHealthcareDeliveryConnectionFactory 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, - CancellationToken cancellationToken = default) + public int Handle(GeneratePrescriptionIdentifier query, IMessageContext context = null) + { + 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 = null) { - return connection.NextValueAsync("PrescriptionId", cancellationToken: cancellationToken); + Ensure.That(query, nameof(query)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + var cancellationToken = context?.CancellationToken() ?? default; + 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/SqlServerHealthcareDeliveryConfiguration.cs b/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareDeliveryConfiguration.cs index de47068..4fbf6ee 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareDeliveryConfiguration.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/SqlServerHealthcareDeliveryConfiguration.cs @@ -1,6 +1,4 @@ -using NHibernate.Dialect; -using NHibernate.Driver; -using NHibernate.Mapping.ByCode; +using NHibernate.Mapping.ByCode; namespace DDD.HealthcareDelivery.Infrastructure { @@ -9,25 +7,12 @@ namespace DDD.HealthcareDelivery.Infrastructure public abstract class SqlServerHealthcareDeliveryConfiguration : HealthcareDeliveryConfiguration { - #region Constructors - - protected SqlServerHealthcareDeliveryConfiguration(string connectionString) : base(connectionString) - { - this.DataBaseIntegration(db => - { - db.Dialect(); - db.Driver(); - }); - } - - #endregion Constructors - #region Methods protected override void AddMappings(ModelMapper modelMapper) { base.AddMappings(modelMapper); - modelMapper.AddMapping(); + modelMapper.AddMapping(); } #endregion Methods diff --git a/Test/DDD.Common.UnitTests/DDD.Common.UnitTests.csproj b/Test/DDD.Common.UnitTests/DDD.Common.UnitTests.csproj index 1c9a2bd..eb5e50e 100644 --- a/Test/DDD.Common.UnitTests/DDD.Common.UnitTests.csproj +++ b/Test/DDD.Common.UnitTests/DDD.Common.UnitTests.csproj @@ -1,6 +1,6 @@  - net48;netcoreapp3.1;net5.0 + net48;net6.0 Library DDD.Common false @@ -14,17 +14,17 @@ - 6.1.0 + 6.9.0 - + 4.5.0 - + - 2.4.3 + 2.4.5 runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Test/DDD.Common.UnitTests/Domain/EnumerationTests.cs b/Test/DDD.Common.UnitTests/Domain/EnumerationTests.cs index 179b2e3..9b34aef 100644 --- a/Test/DDD.Common.UnitTests/Domain/EnumerationTests.cs +++ b/Test/DDD.Common.UnitTests/Domain/EnumerationTests.cs @@ -1,4 +1,5 @@ -using FluentAssertions; +using DDD.Common.Domain; +using FluentAssertions; using System; using System.Collections.Generic; using Xunit; @@ -110,10 +111,10 @@ 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(new[] { FakeEnumeration.Fake1, FakeEnumeration.Fake2, FakeEnumeration.Fake3 }); } @@ -239,7 +240,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 +262,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 +273,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 +294,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 40057ec..1c423aa 100644 --- a/Test/DDD.Common.UnitTests/Properties/AssemblyInfo.cs +++ b/Test/DDD.Common.UnitTests/Properties/AssemblyInfo.cs @@ -3,6 +3,18 @@ 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.Common.UnitTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("DDD.Common.UnitTests")] +[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. @@ -10,4 +22,17 @@ // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("25f6b88a-4efa-4516-bb7a-34ed68548636")] + +// 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", "Unit")] 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 98f4d97..8d061ce 100644 --- a/Test/DDD.Core.Abstractions.UnitTests/DDD.Core.Abstractions.UnitTests.csproj +++ b/Test/DDD.Core.Abstractions.UnitTests/DDD.Core.Abstractions.UnitTests.csproj @@ -1,6 +1,6 @@  - net48;netcoreapp3.1;net5.0 + net48;net6.0 Library DDD false @@ -13,13 +13,11 @@ - 6.1.0 + 6.9.0 - - - 4.2.2 - + + 4.5.4 @@ -27,9 +25,9 @@ 4.5.0 - + - 2.4.3 + 2.4.5 runtime; build; native; contentfiles; analyzers; buildtransitive all 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..8e4935f --- /dev/null +++ b/Test/DDD.Core.Abstractions.UnitTests/Mapping/IMappingProcessorExtensionsTests.cs @@ -0,0 +1,81 @@ +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); + } + + #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) + { + // Act + var destination = IMappingProcessorExtensions.TranslateCollection(this.processor, source) + .ToList(); + // Assert + this.translator1To2.Received(source.Count()).Translate(Arg.Is(x => source.Contains(x))); + } + + [Theory] + [MemberData(nameof(Sources))] + public void TranslateCollection_OfSpecifiedType_CallsExpectedTranslator(IEnumerable source) + { + // Act + var destination = IMappingProcessorExtensions.TranslateCollection(this.processor, source) + .ToList(); + // Assert + this.translator1To2.Received(source.Count()).Translate(Arg.Is(x => source.Contains(x))); + } + + #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..28cf920 100644 --- a/Test/DDD.Core.Abstractions.UnitTests/Mapping/MappingProcessorTests.cs +++ b/Test/DDD.Core.Abstractions.UnitTests/Mapping/MappingProcessorTests.cs @@ -53,7 +53,18 @@ public void Map_WhenCalled_CallsExpectedMapper() } [Fact] - public void Translate_WhenCalled_CallsExpectedTranslator() + public void Translate_SpecifiedType_CallsExpectedTranslator() + { + // Arrange + var source = new FakeObject1(); + // Act + var destination = this.processor.Translate(source); + // Assert + this.translator1To2.Received(1).Translate(source); + } + + [Fact] + public void Translate_Object_CallsExpectedTranslator() { // Arrange var source = new FakeObject1(); diff --git a/Test/DDD.Core.Abstractions.UnitTests/Properties/AssemblyInfo.cs b/Test/DDD.Core.Abstractions.UnitTests/Properties/AssemblyInfo.cs index dfaf3c5..8c569fc 100644 --- a/Test/DDD.Core.Abstractions.UnitTests/Properties/AssemblyInfo.cs +++ b/Test/DDD.Core.Abstractions.UnitTests/Properties/AssemblyInfo.cs @@ -2,6 +2,18 @@ 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.Core.Abstractions.UnitTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("DDD.Core.Abstractions.UnitTests")] +[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. @@ -9,4 +21,17 @@ // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("9cc062c7-73ea-49b4-b694-31a5f48fd09d")] + +// 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", "Unit")] \ No newline at end of file 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.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..653f85b --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/DDD.Core.Dapper.IntegrationTests.csproj @@ -0,0 +1,55 @@ + + + net48;net6.0 + Library + DDD.Core.Infrastructure.Data + + + + + + + + + 2.4.5 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + 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/EventStreamPositionUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/EventStreamPositionUpdaterTests.cs new file mode 100644 index 0000000..d28ea1d --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/EventStreamPositionUpdaterTests.cs @@ -0,0 +1,78 @@ +using FluentAssertions; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + + 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/EventStreamReaderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/EventStreamReaderTests.cs new file mode 100644 index 0000000..240fb3f --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/EventStreamReaderTests.cs @@ -0,0 +1,197 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + + public abstract class EventStreamReaderTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected EventStreamReaderTests(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 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 + this.Fixture.ExecuteScriptFromResources("ReadEventStream"); + var handler = new EventStreamReader(this.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 + this.Fixture.ExecuteScriptFromResources("ReadEventStream"); + var handler = new EventStreamReader(this.ConnectionProvider); + // Act + var results = await handler.HandleAsync(query); + // Assert + results.Should().BeEquivalentTo(expectedResults, context => context.WithStrictOrdering() + .Excluding(e => e.EventId)); + } + + public void Dispose() + { + this.ConnectionProvider.Dispose(); + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/EventStreamSubscriberTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/EventStreamSubscriberTests.cs new file mode 100644 index 0000000..62baa29 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/EventStreamSubscriberTests.cs @@ -0,0 +1,80 @@ +using FluentAssertions; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + + public abstract class EventStreamSubscriberTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected EventStreamSubscriberTests(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 + + [Fact] + public void Handle_WhenCalled_DoesNotThrowException() + { + // Arrange + this.Fixture.ExecuteScriptFromResources("SubscribeToEventStream"); + var handler = new EventStreamSubscriber(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("SubscribeToEventStream"); + var handler = new EventStreamSubscriber(this.ConnectionProvider); + var command = CreateCommand(); + // Act + Func handle = async () => await handler.HandleAsync(command); + // Assert + await handle.Should().NotThrowAsync(); + } + + public void Dispose() + { + this.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/EventStreamsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/EventStreamsFinderTests.cs new file mode 100644 index 0000000..df4a604 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/EventStreamsFinderTests.cs @@ -0,0 +1,100 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + + public abstract class EventStreamsFinderTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected EventStreamsFinderTests(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 + + [Fact] + public void Handle_WhenCalled_ReturnsExpectedResults() + { + // Arrange + var query = new FindEventStreams(); + var expectedResults = ExpectedResults(); + this.Fixture.ExecuteScriptFromResources("FindEventStreams"); + var handler = new EventStreamsFinder(this.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(); + this.Fixture.ExecuteScriptFromResources("FindEventStreams"); + var handler = new EventStreamsFinder(this.ConnectionProvider); + // Act + var results = await handler.HandleAsync(query); + // Assert + results.Should().BeEquivalentTo(expectedResults, context => context.WithStrictOrdering()); + } + + public void Dispose() + { + this.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/FailedEventStreamExcluderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamExcluderTests.cs new file mode 100644 index 0000000..d740baa --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamExcluderTests.cs @@ -0,0 +1,97 @@ +using FluentAssertions; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + + public abstract class FailedEventStreamExcluderTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected FailedEventStreamExcluderTests(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 + + [Fact] + public void Handle_WhenCalled_DoesNotThrowException() + { + // Arrange + this.Fixture.ExecuteScriptFromResources("ExcludeFailedEventStream"); + var handler = new FailedEventStreamCreator(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("ExcludeFailedEventStream"); + var handler = new FailedEventStreamCreator(this.ConnectionProvider); + var command = CreateCommand(); + // Act + Func handle = async () => await handler.HandleAsync(command); + // Assert + await handle.Should().NotThrowAsync(); + } + + public void Dispose() + { + this.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/FailedEventStreamIncluderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamIncluderTests.cs new file mode 100644 index 0000000..e69d3fa --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamIncluderTests.cs @@ -0,0 +1,78 @@ +using FluentAssertions; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + + public abstract class FailedEventStreamIncluderTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected FailedEventStreamIncluderTests(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 + + [Fact] + public void Handle_WhenCalled_DoesNotThrowException() + { + // Arrange + this.Fixture.ExecuteScriptFromResources("IncludeFailedEventStream"); + var handler = new FailedEventStreamDeleter(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("IncludeFailedEventStream"); + var handler = new FailedEventStreamDeleter(this.ConnectionProvider); + var command = CreateCommand(); + // Act + Func handle = async () => await handler.HandleAsync(command); + // Assert + await handle.Should().NotThrowAsync(); + } + + public void Dispose() + { + this.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/FailedEventStreamReaderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamReaderTests.cs new file mode 100644 index 0000000..061d682 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamReaderTests.cs @@ -0,0 +1,37 @@ +using System; + +namespace DDD.Core.Infrastructure.Data +{ + public abstract class EventsByStreamIdFinderTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected EventsByStreamIdFinderTests(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(); + } + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamUpdaterTests.cs new file mode 100644 index 0000000..996a812 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamUpdaterTests.cs @@ -0,0 +1,95 @@ +using FluentAssertions; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + + public abstract class FailedEventStreamUpdaterTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected FailedEventStreamUpdaterTests(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 + + [Fact] + public void Handle_WhenCalled_DoesNotThrowException() + { + // Arrange + this.Fixture.ExecuteScriptFromResources("UpdateFailedEventStream"); + var handler = new FailedEventStreamUpdater(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("UpdateFailedEventStream"); + var handler = new FailedEventStreamUpdater(this.ConnectionProvider); + var command = CreateCommand(); + // Act + Func handle = async () => await handler.HandleAsync(command); + // Assert + await handle.Should().NotThrowAsync(); + } + + public void Dispose() + { + this.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/FailedEventStreamsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamsFinderTests.cs new file mode 100644 index 0000000..5394d3b --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamsFinderTests.cs @@ -0,0 +1,92 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + + public abstract class FailedEventStreamsFinderTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected FailedEventStreamsFinderTests(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 + + [Fact] + public void Handle_WhenCalled_ReturnsExpectedResults() + { + // Arrange + var query = new FindFailedEventStreams(); + var expectedResults = ExpectedResults(); + this.Fixture.ExecuteScriptFromResources("FindFailedEventStreams"); + var handler = new FailedEventStreamsFinder(this.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(); + this.Fixture.ExecuteScriptFromResources("FindFailedEventStreams"); + var handler = new FailedEventStreamsFinder(this.ConnectionProvider); + // Act + var results = await handler.HandleAsync(query); + // Assert + results.Should().BeEquivalentTo(expectedResults, context => context.WithStrictOrdering()); + } + + public void Dispose() + { + this.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/FailedRecurringCommandUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/FailedRecurringCommandUpdaterTests.cs new file mode 100644 index 0000000..34d76d0 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/FailedRecurringCommandUpdaterTests.cs @@ -0,0 +1,116 @@ +using FluentAssertions; +using Dapper; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using System.Data.Common; + + public abstract class FailedRecurringCommandUpdaterTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected FailedRecurringCommandUpdaterTests(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 + + [Fact] + public void Handle_WhenCalled_UpdatesCommand() + { + // Arrange + this.Fixture.ExecuteScriptFromResources("MarkRecurringCommandAsFailed"); + var handler = new FailedRecurringCommandUpdater(this.ConnectionProvider); + var command = CreateCommand(); + var expectedCommand = ExpectedCommand(); + // Act + handler.Handle(command); + // Assert + UpdatedCommand().Should().BeEquivalentTo(expectedCommand); + } + + [Fact] + public async Task HandleAsync_WhenCalled_UpdatesCommand() + { + // Arrange + this.Fixture.ExecuteScriptFromResources("MarkRecurringCommandAsFailed"); + var handler = new FailedRecurringCommandUpdater(this.ConnectionProvider); + var command = CreateCommand(); + var expectedCommand = ExpectedCommand(); + // Act + await handler.HandleAsync(command); + // Assert + UpdatedCommand().Should().BeEquivalentTo(expectedCommand); + + } + + public void Dispose() + { + this.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 = this.Fixture.CreateConnection()) + { + connection.Open(); + var sql = "SELECT CommandId, CommandType, Body, BodyFormat, RecurringExpression, 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 = "* * * * *", + 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/IDbConnectionExtensionsTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/IDbConnectionExtensionsTests.cs new file mode 100644 index 0000000..ef3352b --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/IDbConnectionExtensionsTests.cs @@ -0,0 +1,88 @@ +using FluentAssertions; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + public abstract class IDbConnectionExtensionsTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected IDbConnectionExtensionsTests(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 NextId_WhenExistingRows_ReturnsExpectedId() + { + // Arrange + this.Fixture.ExecuteScriptFromResources("NextId_ExistingRows"); + var connection = this.ConnectionProvider.GetOpenConnection(); + // Act + var id = connection.NextId("TableWithId", "Id"); + // Assert + id.Should().Be(5); + } + + [Fact] + public void NextId_WhenNoRow_ReturnsExpectedId() + { + // Arrange + this.Fixture.ExecuteScriptFromResources("NextId_NoRow"); + var connection = this.ConnectionProvider.GetOpenConnection(); + // Act + var id = connection.NextId("TableWithId", "Id"); + // Assert + id.Should().Be(1); + } + + [Fact] + public async Task NextIdAsync_WhenExistingRows_ReturnsExpectedId() + { + // Arrange + this.Fixture.ExecuteScriptFromResources("NextId_ExistingRows"); + var connection = await this.ConnectionProvider.GetOpenConnectionAsync(); + // Act + var id = await connection.NextIdAsync("TableWithId", "Id"); + // Assert + id.Should().Be(5); + } + + [Fact] + public async Task NextIdAsync_WhenNoRow_ReturnsExpectedId() + { + // Arrange + this.Fixture.ExecuteScriptFromResources("NextId_NoRow"); + var connection = await this.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/IPersistenceFixture.cs b/Test/DDD.Core.Dapper.IntegrationTests/IPersistenceFixture.cs new file mode 100644 index 0000000..ff05683 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/IPersistenceFixture.cs @@ -0,0 +1,18 @@ +namespace DDD.Core.Infrastructure.Data +{ + using Core.Application; + using Core.Domain; + using Core.Infrastructure.Testing; + using Mapping; + + public interface IPersistenceFixture : IDbFixture + { + + #region Methods + + IObjectTranslator CreateEventTranslator(); + + #endregion Methods + + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/OracleCollection.cs b/Test/DDD.Core.Dapper.IntegrationTests/OracleCollection.cs new file mode 100644 index 0000000..d84423a --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/OracleCollection.cs @@ -0,0 +1,9 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [CollectionDefinition("Oracle")] + public class OracleCollection : ICollectionFixture + { + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/OracleEventStreamPositionUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/OracleEventStreamPositionUpdaterTests.cs new file mode 100644 index 0000000..cdd3a69 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/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/OracleEventStreamReaderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/OracleEventStreamReaderTests.cs new file mode 100644 index 0000000..a36d22a --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/OracleEventStreamReaderTests.cs @@ -0,0 +1,80 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + + [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 + this.Fixture.ExecuteScriptFromResources("ReadEventStream"); + var handler = new EventStreamReader(this.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 + this.Fixture.ExecuteScriptFromResources("ReadEventStream"); + var handler = new EventStreamReader(this.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/OracleEventStreamSubscriberTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/OracleEventStreamSubscriberTests.cs new file mode 100644 index 0000000..90dacc8 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/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/OracleEventStreamsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/OracleEventStreamsFinderTests.cs new file mode 100644 index 0000000..3ef7345 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/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/OracleFailedEventStreamExcluderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/OracleFailedEventStreamExcluderTests.cs new file mode 100644 index 0000000..4f985fb --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/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/OracleFailedEventStreamIncluderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/OracleFailedEventStreamIncluderTests.cs new file mode 100644 index 0000000..13b2cc0 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/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/OracleFailedEventStreamReaderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/OracleFailedEventStreamReaderTests.cs new file mode 100644 index 0000000..3f90098 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/OracleFailedEventStreamReaderTests.cs @@ -0,0 +1,104 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + + [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 + this.Fixture.ExecuteScriptFromResources("ReadFailedEventStream"); + var handler = new FailedEventStreamReader(this.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 + this.Fixture.ExecuteScriptFromResources("ReadFailedEventStream"); + var handler = new FailedEventStreamReader(this.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/OracleFailedEventStreamUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/OracleFailedEventStreamUpdaterTests.cs new file mode 100644 index 0000000..e7a06eb --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/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/OracleFailedEventStreamsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/OracleFailedEventStreamsFinderTests.cs new file mode 100644 index 0000000..f3f14ab --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/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/OracleFailedRecurringCommandUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/OracleFailedRecurringCommandUpdaterTests.cs new file mode 100644 index 0000000..f34afdd --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/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/OracleFixture.cs b/Test/DDD.Core.Dapper.IntegrationTests/OracleFixture.cs new file mode 100644 index 0000000..5693c65 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/OracleFixture.cs @@ -0,0 +1,69 @@ +using Dapper; +using Oracle.ManagedDataAccess.Client; +using System.Data; +using System.Data.Common; +using System.Configuration; + +namespace DDD.Core.Infrastructure.Data +{ + using Core.Application; + using Core.Domain; + using Core.Infrastructure.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 = 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.Core.Dapper.IntegrationTests/OracleIDbConnectionExtensionsTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/OracleIDbConnectionExtensionsTests.cs new file mode 100644 index 0000000..90be6bb --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/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/OracleRecurringCommandRegisterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/OracleRecurringCommandRegisterTests.cs new file mode 100644 index 0000000..f3ffd0a --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/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/OracleRecurringCommandsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/OracleRecurringCommandsFinderTests.cs new file mode 100644 index 0000000..a4818d2 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/OracleRecurringCommandsFinderTests.cs @@ -0,0 +1,45 @@ +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 = "* * * * *" + }; + 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 * *" + }; + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/OracleScripts.Designer.cs b/Test/DDD.Core.Dapper.IntegrationTests/OracleScripts.Designer.cs new file mode 100644 index 0000000..c6a39e1 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/OracleScripts.Designer.cs @@ -0,0 +1,318 @@ +//------------------------------------------------------------------------------ +// +// 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); + } + } + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/OracleScripts.resx b/Test/DDD.Core.Dapper.IntegrationTests/OracleScripts.resx new file mode 100644 index 0000000..129ada1 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/OracleScripts.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\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 + + \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/OracleSuccessfulRecurringCommandUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/OracleSuccessfulRecurringCommandUpdaterTests.cs new file mode 100644 index 0000000..d3c8fc5 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/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/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/RecurringCommandRegisterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/RecurringCommandRegisterTests.cs new file mode 100644 index 0000000..3fdc61f --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/RecurringCommandRegisterTests.cs @@ -0,0 +1,104 @@ +using FluentAssertions; +using Dapper; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + + public abstract class RecurringCommandRegisterTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected RecurringCommandRegisterTests(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 + + [Fact] + public void Handle_WhenCalled_RegistersCommand() + { + // Arrange + this.Fixture.ExecuteScriptFromResources("RegisterRecurringCommand"); + var handler = new RecurringCommandRegister(this.ConnectionProvider); + var command = CreateCommand(); + var expectedCommands = ExpectedCommands(); + // Act + handler.Handle(command); + // Assert + RegistredCommands().Should().BeEquivalentTo(expectedCommands); + } + + [Fact] + public async Task HandleAsync_WhenCalled_RegistersCommand() + { + // Arrange + this.Fixture.ExecuteScriptFromResources("RegisterRecurringCommand"); + var handler = new RecurringCommandRegister(this.ConnectionProvider); + var command = CreateCommand(); + var expectedCommands = ExpectedCommands(); + // Act + await handler.HandleAsync(command); + // Assert + RegistredCommands().Should().BeEquivalentTo(expectedCommands); + } + + public void Dispose() + { + this.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 = "* * * * *" + }; + } + + private IEnumerable RegistredCommands() + { + using (var connection = this.Fixture.CreateConnection()) + { + connection.Open(); + return connection.Query("SELECT CommandId, CommandType, Body, BodyFormat, RecurringExpression 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 = "* * * * *" + }; + } + + #endregion Methods + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/RecurringCommandsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/RecurringCommandsFinderTests.cs new file mode 100644 index 0000000..406a07a --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/RecurringCommandsFinderTests.cs @@ -0,0 +1,72 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + + public abstract class RecurringCommandsFinderTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected RecurringCommandsFinderTests(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 + + [Fact] + public void Handle_WhenCalled_ReturnsExpectedResults() + { + // Arrange + this.Fixture.ExecuteScriptFromResources("FindRecurringCommands"); + var query = new FindRecurringCommands(); + var expectedResults = ExpectedResults(); + var handler = new RecurringCommandsFinder(this.ConnectionProvider); + // Act + var results = handler.Handle(query); + // Assert + results.Should().BeEquivalentTo(expectedResults, context => context.WithStrictOrdering()); + } + + [Fact] + public async Task HandleAsync_WhenCalled_ReturnsExpectedResults() + { + // Arrange + this.Fixture.ExecuteScriptFromResources("FindRecurringCommands"); + var query = new FindRecurringCommands(); + var expectedResults = ExpectedResults(); + var handler = new RecurringCommandsFinder(this.ConnectionProvider); + // Act + var results = await handler.HandleAsync(query); + // Assert + results.Should().BeEquivalentTo(expectedResults, context => context.WithStrictOrdering()); + } + + public void Dispose() + { + this.ConnectionProvider.Dispose(); + } + + protected abstract IEnumerable ExpectedResults(); + + #endregion Methods + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/CreateSchema.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/CreateSchema.sql new file mode 100644 index 0000000..8b7bb14 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/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/Scripts/Oracle/ExcludeFailedEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/ExcludeFailedEventStream.sql new file mode 100644 index 0000000000000000000000000000000000000000..f809473f6c0bcdfe32b7daf92769211a354e1317 GIT binary patch literal 70 zcmezW&xyg6!JWaA!H1qXfjxX)wu%I JyFl6c3;>?N32*=a literal 0 HcmV?d00001 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/FillSchema.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/FillSchema.sql new file mode 100644 index 0000000..997d6fd --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/FillSchema.sql @@ -0,0 +1,102 @@ +-------------------------------------------------------- +-- 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, + 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/Scripts/Oracle/FindEventStreams.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/FindEventStreams.sql new file mode 100644 index 0000000000000000000000000000000000000000..c372bd0f02a1f67a2ce6f588b96c08bc4232d6dc GIT binary patch literal 886 zcmds#OHaZ;6otYUhk+6=FR6y-$^om0O?i C&TSX~ literal 0 HcmV?d00001 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/FindFailedEventStreams.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/FindFailedEventStreams.sql new file mode 100644 index 0000000000000000000000000000000000000000..edb0b10206ed22ab0d71f4fa2b811442da824377 GIT binary patch literal 1738 zcma)+TW{Jx5QXQtQvZYBXe7#|xizg+35;p1LV_@jLSL!`Xobopib>S``L^F#+uR%) zw6b@1X7#~!C zHnZ49c5e$X#_Se0XVv3u#J^-s5JHFBKz7Alz+BJgo0Sd=xh%J?j%)uqBP` z(!PS?=QJHO5ng+Im-s7Uk$FzgSNj!qVAl?-vplEsZHNAxE{iFz)0v1-qsEFlZ?PGB zUN2^v#9p*IC=m#Gn^okyZMGv(%~kn6nW}o?qUe}Nq~iN}Ln78%Nc%*gXj+`}?F+lm zzM(sSzel^Vi9OH4pWpPbQ(ekID706bRqyRL#e-jl39eqXI{|X zb2_{P;@qZ4)bFLO9OIlM$=U~F_R=W^wA|IoBB7z&Nk{+0*n)M3oV_$;RJBH#JxZE=~Hf!`;wS#4x=Pr^V zD0;IL!xdR3*{tjI<|R43RILwS?5>MvZ{7HW)jmi~S9A52-qRU7%`taNxvLMw+YsKe zcRTao!Q{-bGPfU`QE$#%mf~rfV_`1vGO%CRE?j@B3o^9*a^Kx4`{$8(s-)FEPUkAO Uw8-7oUpGC|ORn{S%fD>@03d%0X#fBK literal 0 HcmV?d00001 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/FindRecurringCommands.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/FindRecurringCommands.sql new file mode 100644 index 0000000000000000000000000000000000000000..1909b94eb140e5fcfdbf1b9861622b0ec02aa80a GIT binary patch literal 1510 zcmeH`%}?r35XH~h#Q%Y?!Ov$Ypy*pcDT)C!r5IfjH7~~JM^G2W_^+$KnbH_U z=BBqZXU?47JDqucYAV*YI_hg)b1JX~+E=75ctZ(Zq#f)Y-hvkO*LgAg*2G?O#1sbb z5~XOEmue3y1skycL1zyY)3MwFYSTyubOn4GO?AY1VvjUYXyF?xx7%QetEP@8a3c-D z5iSx!2M+Pep4-}#tjxecBnDsPz)+55z@uUR$hoQqzm z1G2hg*?FuAeU%ib&doMqwfr5x*~E&$n_BmHpinjJI$jfIO&h$kXvt4tN|xX9YF}xr zQ>=!rS^ssWV~upn`%0JKZM<_B5&i{h1bU*fE6u9(VBWAs`sY^m#gZOWS``L^F#+uR%) zw6b@1X7#~!C zHnZ49c5e$X#_Se0XVv3u#J^-s5JHFBKz7Alz+BJgo0Sd=xh%J?j%)uqBP` z(!PS?=QJHO5ng+Im-s7Uk$FzgSNj!qVAl?-vplEsZHNAxE{iFz)0v1-qsEFlZ?PGB zUN2^v#9p*IC=m#Gn^okyZMGv(%~kn6nW}o?qUe}Nq~iN}Ln78%Nc%*gXj+`}?F+lm zzM(sSzel^Vi9OH4pWpPbQ(ekID706bRqyRL#e-jl39eqXI{|X zb2_{P;@qZ4)bFLO9OIlM$=U~F_R=W^wA|IoBB7z&Nk{+0*n)M3oV_$;RJBH#JxZE=~Hf!`;wS#4x=Pr^V zD0;IL!xdR3*{tjI<|R43RILwS?5>MvZ{7HW)jmi~S9A52-qRU7%`taNxvLMw+YsKe zcRTao!Q{-bGPfU`QE$#%mf~rfV_`1vGO%CRE?j@B3o^9*a^Kx4`{$8(s-)FEPUkAO Uw8-7oUpGC|ORn{S%fD>@03d%0X#fBK literal 0 HcmV?d00001 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/MarkRecurringCommandAsFailed.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/MarkRecurringCommandAsFailed.sql new file mode 100644 index 0000000000000000000000000000000000000000..6b3db7b32fb5070541f025ab439ffd32c517a76d GIT binary patch literal 1510 zcmeH{NlyYn5QX2_#Q#9J;4Ta>xSWtp42u~Cqepba7#9rc!5II#`c=cI2*v{kFQ(Ex z)vu~vS9e#<+v}bxs;RD)7PX)R($S$xYJe9NuqtUEy~%1x%i48*75vL%ebXVv(7`WI z2#2jgwb4Sb4)X>)ZCH%QViT}UB6ZL;@JTe(3Hele?SY0c zY_QuMa;;095&gh-pj+@Nt1CRr^9JdGM(k>5nPzF?d5`pT;k40?DLpI6Nr!OUfe+z- zf+MLNMx;l_{Di(X!_RYm8P4TYSH`y4UeOe#g_kVrPU!M_g zl&1zGDzQ)H`PAJO-vV#$e|)WiC+pM4%icGBuleiK`;+>dRlQkPy-E5q&G59&Np1G% z$Phh859GA%<{bTIeVt+LgRT;j!D79f{H6NSRDS}$1QBr`#P>N}^VYpB{w~%Ras9so D;Casa literal 0 HcmV?d00001 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/MarkRecurringCommandAsSuccessful.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/MarkRecurringCommandAsSuccessful.sql new file mode 100644 index 0000000000000000000000000000000000000000..4c51799a37a220b4c6ef5ab804cd2e23b88c12f4 GIT binary patch literal 1734 zcmeH{TW=FF5QWb(68~Y9m$VR)Y?>wzp4wd6R7qN8Hwd24mfb=Hp;eknREWP$`DQj3 z(iS0v#1pb?kH<5|5n^=dj|nTJbsxV89_5b^M3CX^RLWs-{OP z@~Y-zlFbqKzaplLNXk^eIdevw*XX+X!W(2nxO{~camD(8E?8x7{YC*h324FUj;Wq4 zu*!b2arQs|{+@p+HLwfEQkZrIvl{T~IdhYjX{=^r=qCRfSXuq9YxO>J{)~S$s_5VI z?;&>XI6Di}eU)CL@A5#4A^8f$< literal 0 HcmV?d00001 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/NextId_ExistingRows.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/NextId_ExistingRows.sql new file mode 100644 index 0000000000000000000000000000000000000000..0fd406e272a49593511c40cffff8d6fe1910a10e GIT binary patch literal 422 zcmcJJF%E)26hvQb;vFcZGJ>6zAW;%f!>)S&i=b~e#uG@k*qO=9%YV-e9cOy3tXWa` z0(V*l^COe4<)R*S8%hqj*V#XP^)@Go5bRAv8OcR%s_3rCw3lg=)w?WYrN2}?Z45lz VhMezA(=GqzRts_Kg}D14_XE>AGb#W8 literal 0 HcmV?d00001 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/NextId_NoRow.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/NextId_NoRow.sql new file mode 100644 index 0000000000000000000000000000000000000000..f809473f6c0bcdfe32b7daf92769211a354e1317 GIT binary patch literal 70 zcmezW&xyg6!JWaA!H1qXfjxX)wu%I JyFl6c3;>?N32*=a literal 0 HcmV?d00001 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/ReadEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/ReadEventStream.sql new file mode 100644 index 0000000000000000000000000000000000000000..35f2a2155a813250e7c128ad2f29a9af492bb7a1 GIT binary patch literal 13748 zcmeI3TW{M&5QXQtK>vf`7sqI#N~EYu`eag4?IMm1OD)i+LWzwwfb9f!9KUD%mT`G(4jx$3PcdFfZOdD-vm*(Jx7 z`$kN?+r0l9ZGL6Hx)w<0*S2L%>(TPQjX3Mtq)eaGko?#>?s?M!i%9plizyjdm#cre zW5{%l&|R$`Shm7@DI*=e94dF4ynUgUj(!-u$H~x=g&RfX_5PqUyE~KA+3nGsxSQE zYu>WoXU7X_+@}snxxrl9rF5Hj_xb8e#&ApxBaSXIZGY*#zird?*Jn4fo9J9qAV<|m1TuzFl=B)i?VCgKUNu(`>nihB~f+d+rrkNBf% zCMt@ZMNE`R-G!-&F;R`Ba+oLqD{c5E<`+6UZooVmV9zD6&<V2d6?9@s~c3t0LNpmzJ-2h2i zIeH|e%v_znuDz69mzP?0-D0Ubw4jLZJG-Cpbvv&{)BRot*>n-Mopy}0L#zEFc^>42 zNVs+?MbjAHxAwZL-!sb3$T^N8$4MP<)N~r;Y2fIl)Y66h8bvFQXh=Cp)=a?sX@MBU zifB(sUIyj$KvK0<-n#t_if0gRymgn_cqx+nwQBEOv=lGpqc9%kZ-_FQvF4#~jR{H- zq#Z}UcNc;mtsA=h#7XktX+O3~t8Vo^hnsxlR~;Tj@>!QhPmEEWWPBQJFXr|0EzsJk=+q5g9R~b;X|Rt_f;~6?-uJ z+Utb*rnvj{q0{@;Bhab%tEa+ud8xe@Lopvk{DC!^%0!dmUj44GEJa08NYjh$v{$8> zb$cDQ>#V&~Fz^@AtWy^Up7%*yGcWV;NQbV}CFL*RwGUtP^(7?_sYkWwKONHdSMI55 zKIPek)AKjqTkMx#PP&I$prPCI3h{*KzaHA{l0QTdT@5smtcddY^236@NcX?Q-10pw p6P@n)E^WnRK`R1UBAcgJTvIB?eCB-12+!Xh(C!Upvf`7sqI#N~EYu`eag4?IMm1OD)i+LWzwwfb9f!9KUD%mT`G(4jx$3PcdFfZOdD-vm*(Jx7 z`$kN?+r0l9ZGL6Hx)w<0*S2L%>(TPQjX3Mtq)eaGko?#>?s?M!i%9plizyjdm#cre zW5{%l&|R$`Shm7@DI*=e94dF4ynUgUj(!-u$H~x=g&RfX_5PqUyE~KA+3nGsxSQE zYu>WoXU7X_+@}snxxrl9rF5Hj_xb8e#&ApxBaSXIZGY*#zird?*Jn4fo9J9qAV<|m1TuzFl=B)i?VCgKUNu(`>nihB~f+d+rrkNBf% zCMt@ZMNE`R-G!-&F;R`Ba+oLqD{c5E<`+6UZooVmV9zD6&<V2d6?9@s~c3t0LNpmzJ-2h2i zIeH|e%v_znuDz69mzP?0-D0Ubw4jLZJG-Cpbvv&{)BRot*>n-Mopy}0L#zEFc^>42 zNVs+?MbjAHxAwZL-!sb3$T^N8$4MP<)N~r;Y2fIl)Y66h8bvFQXh=Cp)=a?sX@MBU zifB(sUIyj$KvK0<-n#t_if0gRymgn_cqx+nwQBEOv=lGpqc9%kZ-_FQvF4#~jR{H- zq#Z}UcNc;mtsA=h#7XktX+O3~t8Vo^hnsxlR~;Tj@>!QhPmEEWWPBQJFXr|0EzsJk=+q5g9R~b;X|Rt_f;~6?-uJ z+Utb*rnvj{q0{@;Bhab%tEa+ud8xe@Lopvk{DC!^%0!dmUj44GEJa08NYjh$v{$8> zb$cDQ>#V&~Fz^@AtWy^Up7%*yGcWV;NQbV}CFL*RwGUtP^(7?_sYkWwKONHdSMI55 zKIPek)AKjqTkMx#PP&I$prPCI3h{*KzaHA{l0QTdT@5smtcddY^236@NcX?Q-10pw p6P@n)E^WnRK`R1UBAcgJTvIB?eCB-12+!Xh(C!Up1qXfjxX)wu%I JyFl6c3;>?N32*=a literal 0 HcmV?d00001 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/SubscribeToEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/SubscribeToEventStream.sql new file mode 100644 index 0000000000000000000000000000000000000000..f809473f6c0bcdfe32b7daf92769211a354e1317 GIT binary patch literal 70 zcmezW&xyg6!JWaA!H1qXfjxX)wu%I JyFl6c3;>?N32*=a literal 0 HcmV?d00001 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/UpdateEventStreamPosition.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/UpdateEventStreamPosition.sql new file mode 100644 index 0000000000000000000000000000000000000000..82705d854d9bdf4208e6204bc66b958054fb77bf GIT binary patch literal 942 zcmd^;OHaZ;6ot+UlMZ7L5o7(1<@?{cc+^afNYdZrZu$ z-ZN)rdZr)mq58VktwuW5ksKn?L_H0tcNK$rx*;EePE^-r!Ry11s=TS%L1EfXG22#4 z(2SK*F_dadmQqW2pEAB^uBEo%t=8mIJ+gkPot{7q*ojtprk7QC0^TTxpOY;MPE60{ z@7VuTuRm%L-39d>^Gq=Wtoea zWS%;xMY;4*7dZ<|hq) BZdm{T literal 0 HcmV?d00001 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/UpdateFailedEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/UpdateFailedEventStream.sql new file mode 100644 index 0000000000000000000000000000000000000000..edb0b10206ed22ab0d71f4fa2b811442da824377 GIT binary patch literal 1738 zcma)+TW{Jx5QXQtQvZYBXe7#|xizg+35;p1LV_@jLSL!`Xobopib>S``L^F#+uR%) zw6b@1X7#~!C zHnZ49c5e$X#_Se0XVv3u#J^-s5JHFBKz7Alz+BJgo0Sd=xh%J?j%)uqBP` z(!PS?=QJHO5ng+Im-s7Uk$FzgSNj!qVAl?-vplEsZHNAxE{iFz)0v1-qsEFlZ?PGB zUN2^v#9p*IC=m#Gn^okyZMGv(%~kn6nW}o?qUe}Nq~iN}Ln78%Nc%*gXj+`}?F+lm zzM(sSzel^Vi9OH4pWpPbQ(ekID706bRqyRL#e-jl39eqXI{|X zb2_{P;@qZ4)bFLO9OIlM$=U~F_R=W^wA|IoBB7z&Nk{+0*n)M3oV_$;RJBH#JxZE=~Hf!`;wS#4x=Pr^V zD0;IL!xdR3*{tjI<|R43RILwS?5>MvZ{7HW)jmi~S9A52-qRU7%`taNxvLMw+YsKe zcRTao!Q{-bGPfU`QE$#%mf~rfV_`1vGO%CRE?j@B3o^9*a^Kx4`{$8(s-)FEPUkAO Uw8-7oUpGC|ORn{S%fD>@03d%0X#fBK literal 0 HcmV?d00001 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql new file mode 100644 index 0000000000000000000000000000000000000000..e16fa3556c88ddab1967e0da61fcdbd277204d07 GIT binary patch literal 15302 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 nJ~aFPs+0pcVeE(VIh^Z)>YP7U|~ literal 0 HcmV?d00001 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/FindEventStreams.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/FindEventStreams.sql new file mode 100644 index 0000000000000000000000000000000000000000..b8fea7adfcdf69710088b9bf794c1675c8846dd3 GIT binary patch literal 926 zcmds#K~KU!5QX1W6aND@AfZ}BTCXbAkcbc}#>99)3JI}-Y0+4JJo@bxY7fR3k7l#G zGqdl_%4&woaYeH#*U!MsyqE)l}7hvjnE8S+)ID)#Vko z_DnwaoT<%SBe`;POvFpXY0DDws%vGqW~z$bJCs z3+1X_y!?)SGb-GtVmbd&r5>*wq2z?ujnOPYuZTZ1;csk}>g!y?!ZR9_hf{bzTV;;1 R^X#pSZbq>@Vif$VN}qq-eAfT~ literal 0 HcmV?d00001 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/FindFailedEventStreams.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/FindFailedEventStreams.sql new file mode 100644 index 0000000000000000000000000000000000000000..575084196dd614e735b0e8e447a30c5f57969601 GIT binary patch literal 1940 zcma)-+iu!G5QgWvQs2RM0VzfXa=xfk4r!5+G>S=7smcWbw?gG4Oe4s{xBX^}@d75G z)vkAUX8$>l|NVWGP@3Y&4{1vzvCQOIKFSCABH!%Wl<&~%aKtjd&RkU2HvA$*<`;B@a=U_8sQp}F^MS)f&v zOAC?t2h{Ek`aSd*U1rQNTBP`yR<6|Sc2j0k<|bpZLTZCJbfo5Stn0naam)Ld(UD*8 zBIQ}Rjn=}xpB96}hqYzYwmCPg+s&D;;RD)gEEnX8`d_1)dN!loB7dH9-{AEC+5*ed zS2b?Nc=$IN(U&*#Mv+W-lFxD@k7QA38FME`Qemovx?k6?2FBc>*2(W1=NusQ+(y9K z5g&2i3CKkE0f)pLw_j&FLoZbHM$Wa}i;5r29+WfEaobh7B zNxv~YLG9PPP4cZ|{|*mYD#0VFBa6p^>dik8K7 mpVseZ&f)eX5(A6N=_+rv8ywRkbk&X#EF+`$0dY$Ip#K4TK|sm? literal 0 HcmV?d00001 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/FindRecurringCommands.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/FindRecurringCommands.sql new file mode 100644 index 0000000000000000000000000000000000000000..a5b8c2251ce37937b5e43d71d1d18c08c506d104 GIT binary patch literal 1618 zcmeH{TW`}q5QWb(68|B}OMo_!?Suq)gI-WjNKxaUN+=JxIH;nRD0Zb*h`&zz%{Zn> zor(vLctNXO&(5Bi*_l1#+n+-XbfAj9C|06WnMQi5Cwi|V{~qXs^F0u$7JSb@jkVN1 zYsQ=~Q*`S#UG?4TF+MA@U5q!2F#~@|ga+~+(m7+S3-lw-pOv&-KiPHLO?<2y?vr4b zpT5J}4Bf;1A^N39vCMMpF?Lg3p*yvc-5e?6{&i4eP6M?V9p+0kw${8R=5gOg!{5a!(ID!AamKBtSQocz=#bqv5SuR& zx~H<2k07n8W9n>D9Q-uKV>9Q_kMZ)1DY*S=7smcWbw?gG4Oe4s{xBX^}@d75G z)vkAUX8$>l|NVWGP@3Y&4{1vzvCQOIKFSCABH!%Wl<&~%aKtjd&RkU2HvA$*<`;B@a=U_8sQp}F^MS)f&v zOAC?t2h{Ek`aSd*U1rQNTBP`yR<6|Sc2j0k<|bpZLTZCJbfo5Stn0naam)Ld(UD*8 zBIQ}Rjn=}xpB96}hqYzYwmCPg+s&D;;RD)gEEnX8`d_1)dN!loB7dH9-{AEC+5*ed zS2b?Nc=$IN(U&*#Mv+W-lFxD@k7QA38FME`Qemovx?k6?2FBc>*2(W1=NusQ+(y9K z5g&2i3CKkE0f)pLw_j&FLoZbHM$Wa}i;5r29+WfEaobh7B zNxv~YLG9PPP4cZ|{|*mYD#0VFBa6p^>dik8K7 mpVseZ&f)eX5(A6N=_+rv8ywRkbk&X#EF+`$0dY$Ip#K4TK|sm? literal 0 HcmV?d00001 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/MarkRecurringCommandAsFailed.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/MarkRecurringCommandAsFailed.sql new file mode 100644 index 0000000000000000000000000000000000000000..a5b8c2251ce37937b5e43d71d1d18c08c506d104 GIT binary patch literal 1618 zcmeH{TW`}q5QWb(68|B}OMo_!?Suq)gI-WjNKxaUN+=JxIH;nRD0Zb*h`&zz%{Zn> zor(vLctNXO&(5Bi*_l1#+n+-XbfAj9C|06WnMQi5Cwi|V{~qXs^F0u$7JSb@jkVN1 zYsQ=~Q*`S#UG?4TF+MA@U5q!2F#~@|ga+~+(m7+S3-lw-pOv&-KiPHLO?<2y?vr4b zpT5J}4Bf;1A^N39vCMMpF?Lg3p*yvc-5e?6{&i4eP6M?V9p+0kw${8R=5gOg!{5a!(ID!AamKBtSQocz=#bqv5SuR& zx~H<2k07n8W9n>D9Q-uKV>9Q_kMZ)1DY*fgc5QgWB#DB1INrBpmosd9pkTxhNZBgwYB$Puw9H?lUD0Zb*h`&yGX4Z}q z5`>VDxS-XpcW2-E+-Hl;)6zJ#0_ ztUj_yA)B*{h~Eb_=C&a%Mu+(viLQ04iCR3i5!kzEB?7!I8COI#MZ1hHkRGQmAhus5 zG)ucyPC;r_$JAMO&SPD^N=@ET^94RqzB#WKB0FNvsFe1`J^RBwxX5rJ`Fz{R;4Sqz zr`xKWo>H&v>F&DZ7SojnhK{fkF?_ySKV1{n2C-CYV0%5gQOCAg@d_@a=(v7}csK1d zjik2`O-w$@u$0V#HF`m=uDiCo;0Ozed&tyEbcvb&OP^=-?yeD z#n4jGV_E3fvf)DRHKo!1tb}Efj!He5Nz@nBOJ(J)Z?0sXT8L?IuSztd?(&ZCV9@nP fySMzE*GSWy6E|x8SbBd?6o`BXMvjA#k1O&YPmn#% literal 0 HcmV?d00001 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/NextId_NoRow.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/NextId_NoRow.sql new file mode 100644 index 0000000000000000000000000000000000000000..1cd8752bd1377ea5a9bfb8f58eeac5c373663134 GIT binary patch literal 124 zcmezWFO(se!IeRQA(|nCA(f$+p@bopftP`c!JWY$OuI5f0Og&5DvB8jfOHN}RU$(X agA0%>0pcVeE(VIh^l&l|h6+H_6c_;BZVvhY literal 0 HcmV?d00001 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/ReadEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/ReadEventStream.sql new file mode 100644 index 0000000000000000000000000000000000000000..05a33175f9768fbff07efbb3f204c43dc5e13327 GIT binary patch literal 13600 zcmeI3UvJws6vg>GVBaCgOIoZpEZMTHy)8|)WmuP@NrwSNAMDuP0_0C&Cq;u{Uwzx} zTw1Z@*ol-#ZVL#8WSOFFGD8)0f|UKwW3NTkqYq_dLhi-`x-HSNE2>9r5cKwcl}V*QM5Dj<)NOyWs+UwKz9O z8_v@vT!mQBAlHDqE!W}Gy44uU zVrXJ0nb1Fm^vo70>66mMGY!u2(U>cpg|)jF>XKi*E#2ops_3uufQ*byitbgmu=(YiZxrtn)DI_&94~$mxL%&30nrA)!Zz zQlvIC9Ia!W7(}Jpy==^U39QqlO*9YjpQo};QN*lcoy4l1jd(SJZrtYe0{K8Fl3L&q zbYdJM+{C1}EN)X;$L2rFdV%IT6LMCGpv}&80%~0ST&G=-byizd1T*cFkLpP6DUq6H z{Q*%}0KaH#6T^su6qSXfj}0j!P|+iGb)+^SuB#F=Ujpk$Pb`mhiX&$ATt`*~c&1CN z92J`D)ChWq_P9{5A3xV=88$|EqQjNAM!K`VlIAwSLqb&q|4763tRt*-z>VfY@?Eua zoz1A!8Z*2)ugFz%^NwlCA!kKvP3kI+EgriKO=VBCwS4HwYmL{a+a;lAwMwm6NUII! zd~4OER;ur=_6+D9irV`05swt9)FWe^E+|sE^f&1WQ3gkSQiUohbHhrRsOoi|MV}Sd zC~}JpLBdhQb<^5LBS|^4pH8mGt%y}!EG!bqbyEJiKa#64^st5?bSP2_IU*1t4Iu;_ zitDt-BTY^GBxSC*0z+C;)oNpv(cBSsXzjZEHJ)|-G&26_f}R%jD^Ib08hVzkQuGkl z*nuIn`(X3xf%^U!o>4p?K9OeN_Yf#%)3wqR-zaWN$8oXEnZ!^+U6t6`w0fXfhnU94 zZ%GuFuunN06y@R|#n9;;5X)8-;heOOYR>VycOF0g(d<*Yvv^~`p&bT&ehDpo?s(lm zyK+3L7W1DpZoYQidYOCt+{gFQHf^EEO*8cbJ5SZE^Pwi6RhpA&$5dE|eQFf>U3TTD z2yMfUuqZ!hpUwa-nwVnH1v%2M#W`AEkXO?vIwAjrBO*_g>?0jcibeI2TY?_#ZD&+PKfNE9Ni8FKg4XEbo3mC0)AWr_{83sq~g4bB0-gwT*aA-&G; zNY%V+nboupPyU-!ic3GI8q+JQ*}J{2w|Z4P0+ZtQ@-)4tMTS|cye6TQubSIF7q@9$ zNecJKa3l-~Pg21ZBWhM5G{wej3td8cRQ)QH^o;gOUHoTy{D5_dEB6=W>mMm&c3r3U zV&Bj^v-?tCTdo=Tr*PPZ+p&B`&exQqQuOZ$aly6SRmo@EQ|Wh~Td%0+vo1S-zwbX3 z9*17TX`^dszN&vuNZrtW-zH;JOiLB##t&=imaJFFLlHjAvm-p8l_8dD)>d?XtG8nj ULRf_m(Mj_pNRH57)Ht*#H0l literal 0 HcmV?d00001 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/ReadFailedEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/ReadFailedEventStream.sql new file mode 100644 index 0000000000000000000000000000000000000000..05a33175f9768fbff07efbb3f204c43dc5e13327 GIT binary patch literal 13600 zcmeI3UvJws6vg>GVBaCgOIoZpEZMTHy)8|)WmuP@NrwSNAMDuP0_0C&Cq;u{Uwzx} zTw1Z@*ol-#ZVL#8WSOFFGD8)0f|UKwW3NTkqYq_dLhi-`x-HSNE2>9r5cKwcl}V*QM5Dj<)NOyWs+UwKz9O z8_v@vT!mQBAlHDqE!W}Gy44uU zVrXJ0nb1Fm^vo70>66mMGY!u2(U>cpg|)jF>XKi*E#2ops_3uufQ*byitbgmu=(YiZxrtn)DI_&94~$mxL%&30nrA)!Zz zQlvIC9Ia!W7(}Jpy==^U39QqlO*9YjpQo};QN*lcoy4l1jd(SJZrtYe0{K8Fl3L&q zbYdJM+{C1}EN)X;$L2rFdV%IT6LMCGpv}&80%~0ST&G=-byizd1T*cFkLpP6DUq6H z{Q*%}0KaH#6T^su6qSXfj}0j!P|+iGb)+^SuB#F=Ujpk$Pb`mhiX&$ATt`*~c&1CN z92J`D)ChWq_P9{5A3xV=88$|EqQjNAM!K`VlIAwSLqb&q|4763tRt*-z>VfY@?Eua zoz1A!8Z*2)ugFz%^NwlCA!kKvP3kI+EgriKO=VBCwS4HwYmL{a+a;lAwMwm6NUII! zd~4OER;ur=_6+D9irV`05swt9)FWe^E+|sE^f&1WQ3gkSQiUohbHhrRsOoi|MV}Sd zC~}JpLBdhQb<^5LBS|^4pH8mGt%y}!EG!bqbyEJiKa#64^st5?bSP2_IU*1t4Iu;_ zitDt-BTY^GBxSC*0z+C;)oNpv(cBSsXzjZEHJ)|-G&26_f}R%jD^Ib08hVzkQuGkl z*nuIn`(X3xf%^U!o>4p?K9OeN_Yf#%)3wqR-zaWN$8oXEnZ!^+U6t6`w0fXfhnU94 zZ%GuFuunN06y@R|#n9;;5X)8-;heOOYR>VycOF0g(d<*Yvv^~`p&bT&ehDpo?s(lm zyK+3L7W1DpZoYQidYOCt+{gFQHf^EEO*8cbJ5SZE^Pwi6RhpA&$5dE|eQFf>U3TTD z2yMfUuqZ!hpUwa-nwVnH1v%2M#W`AEkXO?vIwAjrBO*_g>?0jcibeI2TY?_#ZD&+PKfNE9Ni8FKg4XEbo3mC0)AWr_{83sq~g4bB0-gwT*aA-&G; zNY%V+nboupPyU-!ic3GI8q+JQ*}J{2w|Z4P0+ZtQ@-)4tMTS|cye6TQubSIF7q@9$ zNecJKa3l-~Pg21ZBWhM5G{wej3td8cRQ)QH^o;gOUHoTy{D5_dEB6=W>mMm&c3r3U zV&Bj^v-?tCTdo=Tr*PPZ+p&B`&exQqQuOZ$aly6SRmo@EQ|Wh~Td%0+vo1S-zwbX3 z9*17TX`^dszN&vuNZrtW-zH;JOiLB##t&=imaJFFLlHjAvm-p8l_8dD)>d?XtG8nj ULRf_m(Mj_pNRH57)Ht*#H0l literal 0 HcmV?d00001 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/RegisterRecurringCommand.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/RegisterRecurringCommand.sql new file mode 100644 index 0000000000000000000000000000000000000000..c969f406c7b50fd59630da581ab80245b4305aaa GIT binary patch literal 82 zcmezWFO(se!IeRQA(|nCA(f$+p@bopftP`c!JWY$OuI5f0Og&5DvB8jfOHN}RU$(X SgA0%>0pcVeE(VIh^Z)>YP7U|~ literal 0 HcmV?d00001 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/SubscribeToEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/SubscribeToEventStream.sql new file mode 100644 index 0000000000000000000000000000000000000000..c969f406c7b50fd59630da581ab80245b4305aaa GIT binary patch literal 82 zcmezWFO(se!IeRQA(|nCA(f$+p@bopftP`c!JWY$OuI5f0Og&5DvB8jfOHN}RU$(X SgA0%>0pcVeE(VIh^Z)>YP7U|~ literal 0 HcmV?d00001 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/UpdateEventStreamPosition.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/UpdateEventStreamPosition.sql new file mode 100644 index 0000000000000000000000000000000000000000..560253d1d3e5e29fd36c27387d1985a55190d540 GIT binary patch literal 968 zcmds#OHaZ;6otGXk3H9bou!kP#g}O7kL`Z7;J`$_iS(i*R!m8W6h^%KY@s_WTFVlqF z3EFIf4Z)atOLdw3^$UL+#FW^Q{G!7No;md&m}a6k+owkFYaQ!C1E!6zs;a0@EG5(Q ztl0S~>hNBLK92q=k;3a4#ra_ zX7+{-IQPMQqFmK}#Zigd{AZTBymA7QF|Rv?vlPA({!q-{D46Q$OyR~d9BeOVV|}k} U=NLV&-dyu$7|T6|!N0Qf0WQ&f*#H0l literal 0 HcmV?d00001 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/UpdateFailedEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/UpdateFailedEventStream.sql new file mode 100644 index 0000000000000000000000000000000000000000..575084196dd614e735b0e8e447a30c5f57969601 GIT binary patch literal 1940 zcma)-+iu!G5QgWvQs2RM0VzfXa=xfk4r!5+G>S=7smcWbw?gG4Oe4s{xBX^}@d75G z)vkAUX8$>l|NVWGP@3Y&4{1vzvCQOIKFSCABH!%Wl<&~%aKtjd&RkU2HvA$*<`;B@a=U_8sQp}F^MS)f&v zOAC?t2h{Ek`aSd*U1rQNTBP`yR<6|Sc2j0k<|bpZLTZCJbfo5Stn0naam)Ld(UD*8 zBIQ}Rjn=}xpB96}hqYzYwmCPg+s&D;;RD)gEEnX8`d_1)dN!loB7dH9-{AEC+5*ed zS2b?Nc=$IN(U&*#Mv+W-lFxD@k7QA38FME`Qemovx?k6?2FBc>*2(W1=NusQ+(y9K z5g&2i3CKkE0f)pLw_j&FLoZbHM$Wa}i;5r29+WfEaobh7B zNxv~YLG9PPP4cZ|{|*mYD#0VFBa6p^>dik8K7 mpVseZ&f)eX5(A6N=_+rv8ywRkbk&X#EF+`$0dY$Ip#K4TK|sm? literal 0 HcmV?d00001 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerCollection.cs b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerCollection.cs new file mode 100644 index 0000000..2123526 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerCollection.cs @@ -0,0 +1,9 @@ +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + [CollectionDefinition("SqlServer")] + public class SqlServerCollection : ICollectionFixture + { + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerEventStreamPositionUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerEventStreamPositionUpdaterTests.cs new file mode 100644 index 0000000..5e28a28 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/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/SqlServerEventStreamReaderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerEventStreamReaderTests.cs new file mode 100644 index 0000000..1489948 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerEventStreamReaderTests.cs @@ -0,0 +1,80 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + + [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 + this.Fixture.ExecuteScriptFromResources("ReadEventStream"); + var handler = new EventStreamReader(this.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 + this.Fixture.ExecuteScriptFromResources("ReadEventStream"); + var handler = new EventStreamReader(this.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/SqlServerEventStreamSubscriberTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerEventStreamSubscriberTests.cs new file mode 100644 index 0000000..4a496d8 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/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/SqlServerEventStreamsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerEventStreamsFinderTests.cs new file mode 100644 index 0000000..eb05829 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/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/SqlServerFailedEventStreamExcluderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedEventStreamExcluderTests.cs new file mode 100644 index 0000000..9e69843 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/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/SqlServerFailedEventStreamIncluderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedEventStreamIncluderTests.cs new file mode 100644 index 0000000..1d5bad0 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/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/SqlServerFailedEventStreamReaderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedEventStreamReaderTests.cs new file mode 100644 index 0000000..decce7e --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedEventStreamReaderTests.cs @@ -0,0 +1,104 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + + [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 + this.Fixture.ExecuteScriptFromResources("ReadFailedEventStream"); + var handler = new FailedEventStreamReader(this.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 + this.Fixture.ExecuteScriptFromResources("ReadFailedEventStream"); + var handler = new FailedEventStreamReader(this.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/SqlServerFailedEventStreamUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedEventStreamUpdaterTests.cs new file mode 100644 index 0000000..95846ce --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/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/SqlServerFailedEventStreamsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedEventStreamsFinderTests.cs new file mode 100644 index 0000000..6a70691 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/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/SqlServerFailedRecurringCommandUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedRecurringCommandUpdaterTests.cs new file mode 100644 index 0000000..3ac5f14 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/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/SqlServerFixture.cs b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerFixture.cs new file mode 100644 index 0000000..8ad33a0 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerFixture.cs @@ -0,0 +1,67 @@ +using Dapper; +using System.Data; +using System.Data.Common; +using Microsoft.Data.SqlClient; +using System.Configuration; + +namespace DDD.Core.Infrastructure.Data +{ + using Core.Application; + using Core.Domain; + using Core.Infrastructure.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 = 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.Core.Dapper.IntegrationTests/SqlServerIDbConnectionExtensionsTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerIDbConnectionExtensionsTests.cs new file mode 100644 index 0000000..c700e67 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/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/SqlServerRecurringCommandRegisterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerRecurringCommandRegisterTests.cs new file mode 100644 index 0000000..28c25a7 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/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/SqlServerRecurringCommandsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerRecurringCommandsFinderTests.cs new file mode 100644 index 0000000..a42debe --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerRecurringCommandsFinderTests.cs @@ -0,0 +1,45 @@ +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 = "* * * * *" + }; + 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 * *" + }; + } + + #endregion Methods + + } +} \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerScripts.Designer.cs b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerScripts.Designer.cs new file mode 100644 index 0000000..0bdf6ba --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerScripts.Designer.cs @@ -0,0 +1,303 @@ +//------------------------------------------------------------------------------ +// +// 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); + } + } + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerScripts.resx b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerScripts.resx new file mode 100644 index 0000000..c95b5db --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerScripts.resx @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + \ No newline at end of file diff --git a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerSuccessfulRecurringCommandUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/SqlServerSuccessfulRecurringCommandUpdaterTests.cs new file mode 100644 index 0000000..3355106 --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/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/SuccessfulRecurringCommandUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/SuccessfulRecurringCommandUpdaterTests.cs new file mode 100644 index 0000000..19f8e5b --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/SuccessfulRecurringCommandUpdaterTests.cs @@ -0,0 +1,115 @@ +using FluentAssertions; +using Dapper; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace DDD.Core.Infrastructure.Data +{ + using Application; + using System.Data.Common; + + public abstract class SuccessfulRecurringCommandUpdaterTests : IDisposable + where TFixture : IPersistenceFixture + { + + #region Constructors + + protected SuccessfulRecurringCommandUpdaterTests(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 + + [Fact] + public void Handle_WhenCalled_UpdatesCommand() + { + // Arrange + this.Fixture.ExecuteScriptFromResources("MarkRecurringCommandAsSuccessful"); + var handler = new SuccessfulRecurringCommandUpdater(this.ConnectionProvider); + var command = CreateCommand(); + var expectedCommand = ExpectedCommand(); + // Act + handler.Handle(command); + // Assert + UpdatedCommand().Should().BeEquivalentTo(expectedCommand); + } + + [Fact] + public async Task HandleAsync_WhenCalled_UpdatesCommand() + { + // Arrange + this.Fixture.ExecuteScriptFromResources("MarkRecurringCommandAsSuccessful"); + var handler = new SuccessfulRecurringCommandUpdater(this.ConnectionProvider); + var command = CreateCommand(); + var expectedCommand = ExpectedCommand(); + // Act + await handler.HandleAsync(command); + // Assert + UpdatedCommand().Should().BeEquivalentTo(expectedCommand); + + } + + public void Dispose() + { + this.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 = this.Fixture.CreateConnection()) + { + connection.Open(); + var sql = "SELECT CommandId, CommandType, Body, BodyFormat, RecurringExpression, 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 = "* * * * *", + LastExecutionTime = new DateTime(2022, 2, 1), + LastExecutionStatus = CommandExecutionStatus.Successful, + LastExceptionInfo = null + }; + } + + #endregion Methods + } +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/TestContext.cs b/Test/DDD.Core.Dapper.IntegrationTests/TestContext.cs new file mode 100644 index 0000000..a989ebe --- /dev/null +++ b/Test/DDD.Core.Dapper.IntegrationTests/TestContext.cs @@ -0,0 +1,16 @@ +namespace DDD.Core.Infrastructure.Data +{ + using Domain; + + public class TestContext : BoundedContext + { + #region Constructors + + public TestContext() : base("TST", "Test") + { + } + + #endregion Constructors + + } +} 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 index 177aa11..3f442f3 100644 --- a/Test/DDD.Core.Newtonsoft.UnitTests/DDD.Core.Newtonsoft.UnitTests.csproj +++ b/Test/DDD.Core.Newtonsoft.UnitTests/DDD.Core.Newtonsoft.UnitTests.csproj @@ -1,16 +1,17 @@  - net48;netcoreapp3.1;net5.0 + net48;net6.0 DDD.Core.Infrastructure.Serialization - - + + - - - all + + + 2.4.5 runtime; build; native; contentfiles; analyzers; buildtransitive + all diff --git a/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs b/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs index 3bdcf24..9cd3d2e 100644 --- a/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs +++ b/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs @@ -8,10 +8,10 @@ public class CommandProcessorTests { #region Fields - private readonly ICommandHandler handlerOfCommand1; - private readonly ICommandHandler handlerOfCommand2; - private readonly ICommandValidator validatorOfCommand1; - private readonly ICommandValidator validatorOfCommand2; + private readonly ISyncCommandHandler handlerOfCommand1; + private readonly ISyncCommandHandler handlerOfCommand2; + private readonly ISyncCommandValidator validatorOfCommand1; + private readonly ISyncCommandValidator validatorOfCommand2; private readonly CommandProcessor processor; #endregion Fields @@ -20,18 +20,18 @@ public class CommandProcessorTests public CommandProcessorTests() { - this.handlerOfCommand1 = Substitute.For>(); - this.handlerOfCommand2 = Substitute.For>(); - this.validatorOfCommand1 = Substitute.For>(); - this.validatorOfCommand2 = Substitute.For>(); + this.handlerOfCommand1 = Substitute.For>(); + this.handlerOfCommand2 = Substitute.For>(); + this.validatorOfCommand1 = Substitute.For>(); + this.validatorOfCommand2 = Substitute.For>(); var serviceProvider = Substitute.For(); - serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ICommandHandler)))) + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncCommandHandler)))) .Returns(this.handlerOfCommand1); - serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ICommandHandler)))) + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncCommandHandler)))) .Returns(this.handlerOfCommand2); - serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ICommandValidator)))) + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncCommandValidator)))) .Returns(this.validatorOfCommand1); - serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ICommandValidator)))) + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncCommandValidator)))) .Returns(this.validatorOfCommand2); processor = new CommandProcessor(serviceProvider); } diff --git a/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs b/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs new file mode 100644 index 0000000..a9e0f8f --- /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 DependencyInjection; + 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 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.In() + .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 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.In() + .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.Error, 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.Error, 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.Error, 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.Error, 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 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.In() + .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 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.In() + .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.Error, 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.Error, 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.Error, exception)); + } + + [Fact] + public async Task StartAndWait_WhenFailedEventStreamSuccessfullyProcessed_DeletesFailedEventStream() + { + // Arrange + var @event = FakeEvent(); + var commandProcessor = FakeCommandProcessor(); + 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 + await commandProcessor.In().Received(1) + .ProcessAsync(Arg.Any(), Arg.Any()); + } + private static ICommandProcessor FakeCommandProcessor() + { + var contextualProcessor = Substitute.For>(); + var commandProcessor = Substitute.For(); + commandProcessor.In().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.In().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.In().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.In().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 IKeyedServiceProvider FakeEventSerializers() + { + var jsonSerializer = Substitute.For(); + jsonSerializer.Encoding.Returns(JsonSerializationOptions.Encoding); + jsonSerializer.Deserialize(Arg.Any(), Arg.Any()).Returns(Substitute.For()); + var provider = Substitute.For>(); + provider.GetService(Arg.Is(f => f == SerializationFormat.Json)).Returns(jsonSerializer); + return provider; + } + + private static IKeyedServiceProvider FakeEventSerializersThrowingException(Exception exception) + { + var jsonSerializer = Substitute.For(); + jsonSerializer.Encoding.Returns(JsonSerializationOptions.Encoding); + jsonSerializer.When(s => s.Deserialize(Arg.Any(), Arg.Any())).Throw(exception); + var provider = Substitute.For>(); + provider.GetService(Arg.Is(f => f == SerializationFormat.Json)).Returns(jsonSerializer); + return provider; + } + + 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.In().Returns(contextualProcessorForFakeContext); + queryProcessor.In(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.In().Returns(contextualProcessorForFakeContext); + queryProcessor.In(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.In().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.In().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.In().Returns(contextualProcessorForFakeContext); + queryProcessor.In(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.In().Returns(contextualProcessorForFakeContext); + queryProcessor.In(Arg.Any()).Returns(contextualProcessorForFakeSourceContext); + return queryProcessor; + } + + private static EventConsumerSettings FakeSettings() => new EventConsumerSettings(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 index db0235b..389c5fa 100644 --- a/Test/DDD.Core.UnitTests/Application/EventPublisherTests.cs +++ b/Test/DDD.Core.UnitTests/Application/EventPublisherTests.cs @@ -14,68 +14,6 @@ public class EventPublisherTests #region Methods - public static IEnumerable AsyncHandlersOfOtherEventsThanThisEvent() - { - var fakeHandler1 = FakeAsyncHandler(); - var fakeHandler2 = FakeAsyncHandler(); - var fakeHandler3 = FakeAsyncHandler(); - var fakeHandler4 = FakeAsyncHandler(); - var container = new Container(); - container.Collection.Register(fakeHandler1); - container.Collection.Register(fakeHandler2); - container.Collection.Register(fakeHandler3, fakeHandler4); - var publisher = new EventPublisher(container); - yield return new object[] - { - publisher, - new FakeEvent1(), - new IAsyncEventHandler[] { fakeHandler2, fakeHandler3, fakeHandler4 } - }; - yield return new object[] - { - publisher, - new FakeEvent2(), - new IAsyncEventHandler[] { fakeHandler3, fakeHandler4 } - }; - yield return new object[] - { - publisher, - new FakeEvent3(), - new IAsyncEventHandler[] { fakeHandler1, fakeHandler2 } - }; - } - - public static IEnumerable AsyncHandlersOfThisEvent() - { - var fakeHandler1 = FakeAsyncHandler(); - var fakeHandler2 = FakeAsyncHandler(); - var fakeHandler3 = FakeAsyncHandler(); - var fakeHandler4 = FakeAsyncHandler(); - var container = new Container(); - container.Collection.Register(fakeHandler1); - container.Collection.Register(fakeHandler2); - container.Collection.Register(fakeHandler3, fakeHandler4); - var publisher = new EventPublisher(container); - yield return new object[] - { - publisher, - new FakeEvent1(), - new IAsyncEventHandler[] { fakeHandler1 } - }; - yield return new object[] - { - publisher, - new FakeEvent2(), - new IAsyncEventHandler[] { fakeHandler1, fakeHandler2 } - }; - yield return new object[] - { - publisher, - new FakeEvent3(), - new IAsyncEventHandler[] { fakeHandler3, fakeHandler4 } - }; - } - public static IEnumerable HandlersOfOtherEventsThanThisEvent() { var fakeHandler1 = FakeHandler(); @@ -91,19 +29,19 @@ public static IEnumerable HandlersOfOtherEventsThanThisEvent() { publisher, new FakeEvent1(), - new IEventHandler[] { fakeHandler2, fakeHandler3, fakeHandler4 } + new IAsyncEventHandler[] { fakeHandler2, fakeHandler3, fakeHandler4 } }; yield return new object[] { publisher, new FakeEvent2(), - new IEventHandler[] { fakeHandler3, fakeHandler4 } + new IAsyncEventHandler[] { fakeHandler3, fakeHandler4 } }; yield return new object[] { publisher, new FakeEvent3(), - new IEventHandler[] { fakeHandler1, fakeHandler2 } + new IAsyncEventHandler[] { fakeHandler1, fakeHandler2 } }; } @@ -122,56 +60,28 @@ public static IEnumerable HandlersOfThisEvent() { publisher, new FakeEvent1(), - new IEventHandler[] { fakeHandler1 } + new IAsyncEventHandler[] { fakeHandler1 } }; yield return new object[] { publisher, new FakeEvent2(), - new IEventHandler[] { fakeHandler1, fakeHandler2 } + new IAsyncEventHandler[] { fakeHandler1, fakeHandler2 } }; yield return new object[] { publisher, new FakeEvent3(), - new IEventHandler[] { fakeHandler3, fakeHandler4 } + new IAsyncEventHandler[] { fakeHandler3, fakeHandler4 } }; } - [Theory] - [MemberData(nameof(HandlersOfThisEvent))] - public void Publish_WhenCalled_CallsHandlersOfThisEvent(EventPublisher publisher, - IEvent @event, - IEnumerable handlersOfThisEvent) - { - // Arrange - handlersOfThisEvent.ForEach(s => s.ClearReceivedCalls()); - // Act - publisher.Publish(@event); - // Assert - Assert.All(handlersOfThisEvent, s => s.Received(1).Handle(@event)); - } - - - [Theory] - [MemberData(nameof(HandlersOfOtherEventsThanThisEvent))] - public void Publish_WhenCalled_DoesNotCallHandlersOfOtherEvents(EventPublisher publisher, - IEvent @event, - IEnumerable handlersOfOtherEvents) - { - // Arrange - handlersOfOtherEvents.ForEach(s => s.ClearReceivedCalls()); - // Act - publisher.Publish(@event); - // Assert - Assert.All(handlersOfOtherEvents, s => s.DidNotReceive().Handle(Arg.Any())); - } [Theory] - [MemberData(nameof(AsyncHandlersOfThisEvent))] + [MemberData(nameof(HandlersOfThisEvent))] public async Task PublishAsync_WhenCalled_CallsHandlersOfThisEvent(EventPublisher publisher, - IEvent @event, - IEnumerable handlersOfThisEvent) + IEvent @event, + IEnumerable handlersOfThisEvent) { // Arrange handlersOfThisEvent.ForEach(s => s.ClearReceivedCalls()); @@ -183,10 +93,10 @@ public async Task PublishAsync_WhenCalled_CallsHandlersOfThisEvent(EventPublishe [Theory] - [MemberData(nameof(AsyncHandlersOfOtherEventsThanThisEvent))] + [MemberData(nameof(HandlersOfOtherEventsThanThisEvent))] public async Task PublishAsync_WhenCalled_DoesNotCallHandlersOfOtherEvents(EventPublisher publisher, - IEvent @event, - IEnumerable handlersOfOtherEvents) + IEvent @event, + IEnumerable handlersOfOtherEvents) { // Arrange handlersOfOtherEvents.ForEach(s => s.ClearReceivedCalls()); @@ -196,20 +106,13 @@ public async Task PublishAsync_WhenCalled_DoesNotCallHandlersOfOtherEvents(Event Assert.All(handlersOfOtherEvents, s => s.DidNotReceive().HandleAsync(Arg.Any())); } - private static IAsyncEventHandler FakeAsyncHandler() where TEvent : class, IEvent + private static IAsyncEventHandler FakeHandler() where TEvent : class, IEvent { var fakeHandler = Substitute.For>(); fakeHandler.EventType.Returns(typeof(TEvent)); return fakeHandler; } - private static IEventHandler FakeHandler() where TEvent : class, IEvent - { - var fakeHandler = Substitute.For>(); - fakeHandler.EventType.Returns(typeof(TEvent)); - return fakeHandler; - } - #endregion Methods } diff --git a/Test/DDD.Core.UnitTests/Application/FakeSourceContext.cs b/Test/DDD.Core.UnitTests/Application/FakeSourceContext.cs new file mode 100644 index 0000000..08d61eb --- /dev/null +++ b/Test/DDD.Core.UnitTests/Application/FakeSourceContext.cs @@ -0,0 +1,15 @@ +namespace DDD.Core.Application +{ + using Domain; + + public class FakeSourceContext : BoundedContext + { + #region Constructors + + public FakeSourceContext() : base("FKS", "FakeSource") { } + + #endregion Constructors + + + } +} diff --git a/Test/DDD.Core.UnitTests/Application/QueryProcessorTests.cs b/Test/DDD.Core.UnitTests/Application/QueryProcessorTests.cs index d0eaad6..ff91113 100644 --- a/Test/DDD.Core.UnitTests/Application/QueryProcessorTests.cs +++ b/Test/DDD.Core.UnitTests/Application/QueryProcessorTests.cs @@ -8,10 +8,10 @@ public class QueryProcessorTests { #region Fields - private readonly IQueryHandler handlerOfQuery1; - private readonly IQueryHandler handlerOfQuery2; - private readonly IQueryValidator validatorOfQuery1; - private readonly IQueryValidator validatorOfQuery2; + private readonly ISyncQueryHandler handlerOfQuery1; + private readonly ISyncQueryHandler handlerOfQuery2; + private readonly ISyncQueryValidator validatorOfQuery1; + private readonly ISyncQueryValidator validatorOfQuery2; private readonly QueryProcessor processor; #endregion Fields @@ -20,18 +20,18 @@ public class QueryProcessorTests public QueryProcessorTests() { - this.handlerOfQuery1 = Substitute.For>(); - this.handlerOfQuery2 = Substitute.For>(); - this.validatorOfQuery1 = Substitute.For>(); - this.validatorOfQuery2 = Substitute.For>(); + this.handlerOfQuery1 = Substitute.For>(); + this.handlerOfQuery2 = Substitute.For>(); + this.validatorOfQuery1 = Substitute.For>(); + this.validatorOfQuery2 = Substitute.For>(); var serviceProvider = Substitute.For(); - serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(IQueryHandler)))) + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncQueryHandler)))) .Returns(this.handlerOfQuery1); - serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(IQueryHandler)))) + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncQueryHandler)))) .Returns(this.handlerOfQuery2); - serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(IQueryValidator)))) + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncQueryValidator)))) .Returns(this.validatorOfQuery1); - serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(IQueryValidator)))) + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncQueryValidator)))) .Returns(this.validatorOfQuery2); processor = new QueryProcessor(serviceProvider); } diff --git a/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs b/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs new file mode 100644 index 0000000..7ed73b4 --- /dev/null +++ b/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs @@ -0,0 +1,553 @@ +using Microsoft.Extensions.Logging; +using FluentAssertions; +using NSubstitute; +using NSubstitute.Core; +using System.Threading.Tasks; +using System; +using System.IO; +using Xunit; + +namespace DDD.Core.Application +{ + using DependencyInjection; + using Serialization; + using Infrastructure.Testing; + using DDD.Core.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 commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, 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 commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(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 commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); + var commandSerializers = FakeCommandSerializersThrowingException(exception); + var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(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 commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorThrowingExceptionWhenFindingRecurringCommands(exception); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(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_SetsIsRunningToFalse() + { + // Arrange + var exception = FakeException(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorThrowingExceptionWhenFindingRecurringCommands(exception); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(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 commandProcessor = FakeCommandProcessorThrowingExceptionWhenMarkingRecurringCommandAsFailed(exception); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(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 commandProcessor = FakeCommandProcessorThrowingExceptionWhenMarkingRecurringCommandAsFailed(exception); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(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 commandProcessor = FakeCommandProcessorThrowingExceptionWhenMarkingRecurringCommandAsSuccessful(exception); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(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 commandProcessor = FakeCommandProcessorThrowingExceptionWhenMarkingRecurringCommandAsSuccessful(exception); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(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 commandProcessor = FakeCommandProcessorThrowingExceptionWhenProcessingRecurringCommand(exception); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(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 commandProcessor = FakeCommandProcessorThrowingExceptionWhenProcessingRecurringCommand(exception); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + // Act + manager.Start(); + manager.Wait(TimeSpan.FromSeconds(5)); + // Assert + await commandProcessor.In() + .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 commandProcessor = FakeCommandProcessorThrowingExceptionWhenProcessingRecurringCommand(exception); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(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 commandProcessor = FakeCommandProcessorThrowingExceptionWhenProcessingFakeCommand1(exception); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand1, recurringCommand2); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(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_WhenRecurringCommandSuccessfullyProcessed_MarksRecurringCommandAsSuccessful() + { + // Arrange + var recurringCommand = FakeCommand1ForSingleExecution(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + // Act + manager.Start(); + manager.Wait(TimeSpan.FromSeconds(5)); + // Assert + await commandProcessor.In() + .Received(1) + .ProcessAsync(Arg.Is(c => c.CommandId == recurringCommand.CommandId), Arg.Any()); + } + + [Fact] + public void Stop_WhenIsRunning_SetsIsRunningToFalse() + { + // Arrange + var recurringCommand = FakeCommand1ForRecurringExecution(); + var commandProcessor = FakeCommandProcessor(); + var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand);; + var commandSerializers = FakeCommandSerializers(); + var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var logger = FakeLogger(); + var settings = FakeSettings(); + manager = new RecurringCommandManager(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() + }; + } + + 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() + }; + } + + 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() + }; + } + + 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() + }; + } + private static Guid FakeCommandId() => new Guid("f7df5bd0-8763-677e-7e6b-3a0044746810"); + + private static IKeyedServiceProvider 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()); + var provider = Substitute.For>(); + provider.GetService(Arg.Is(f => f == SerializationFormat.Json)).Returns(jsonSerializer); + return provider; + } + + private static IKeyedServiceProvider FakeCommandSerializersThrowingException(Exception exception) + { + var jsonSerializer = Substitute.For(); + jsonSerializer.Encoding.Returns(JsonSerializationOptions.Encoding); + jsonSerializer.When(s => s.Deserialize(Arg.Any(), Arg.Any())).Throw(exception); + var provider = Substitute.For>(); + provider.GetService(Arg.Is(f => f == SerializationFormat.Json)).Returns(jsonSerializer); + return provider; + } + + private static ICommandProcessor FakeCommandProcessor() + { + var contextualProcessor = Substitute.For>(); + var commandProcessor = Substitute.For(); + commandProcessor.In().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.In().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.In().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.In().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.In().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.In().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.In().Returns(contextualProcessor); + return queryProcessor; + } + + private static IRecurringScheduleFactory FakeRecurringScheduleFactory() + { + var recurringScheduleForSingleExecution1 = FakeRecurringScheduleForSingleExecution(); + var recurringScheduleForDoubleExecution1 = FakeRecurringScheduleForDoubleExecution(); + var recurringScheduleForRecurringExecution1 = FakeRecurringScheduleForRecurringExecution(); + var recurringScheduleForSingleExecution2 = FakeRecurringScheduleForSingleExecution(); + var recurringScheduleForDoubleExecution2 = FakeRecurringScheduleForDoubleExecution(); + var recurringScheduleForRecurringExecution2 = FakeRecurringScheduleForRecurringExecution(); + var recurringSchedulefactory = Substitute.For(); + 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(); + 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); + + 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 d249a91..dce894f 100644 --- a/Test/DDD.Core.UnitTests/DDD.Core.UnitTests.csproj +++ b/Test/DDD.Core.UnitTests/DDD.Core.UnitTests.csproj @@ -1,6 +1,6 @@  - net48;netcoreapp3.1;net5.0 + net48;net6.0 Library DDD.Core false @@ -11,18 +11,16 @@ + - 6.1.0 + 6.9.0 - - - 4.2.2 - - + + 4.5.4 @@ -30,9 +28,9 @@ 4.5.0 - + - 2.4.3 + 2.4.5 runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Test/DDD.Core.UnitTests/Domain/FakeContext.cs b/Test/DDD.Core.UnitTests/Domain/FakeContext.cs new file mode 100644 index 0000000..909565f --- /dev/null +++ b/Test/DDD.Core.UnitTests/Domain/FakeContext.cs @@ -0,0 +1,15 @@ +namespace DDD.Core.Domain +{ + using 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 aeced88..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 => SystemTime.Local(); + public DateTime OccurredOn { get; private set; } #endregion Properties + } } \ No newline at end of file diff --git a/Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs b/Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs index 108cc83..9c38fd7 100644 --- a/Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs +++ b/Test/DDD.Core.UnitTests/Domain/FakeEvent3.cs @@ -6,7 +6,7 @@ public class FakeEvent3 : IEvent { #region Properties - public DateTime OccurredOn => SystemTime.Local(); + public DateTime OccurredOn => DateTime.Now; #endregion Properties } 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/Properties/AssemblyInfo.cs b/Test/DDD.Core.UnitTests/Properties/AssemblyInfo.cs index 70c5526..536081e 100644 --- a/Test/DDD.Core.UnitTests/Properties/AssemblyInfo.cs +++ b/Test/DDD.Core.UnitTests/Properties/AssemblyInfo.cs @@ -2,6 +2,18 @@ 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.Core.UnitTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("DDD.Core.UnitTests")] +[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. @@ -9,4 +21,17 @@ // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("63505329-bd97-4c75-b7d7-77fb42670e79")] + +// 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", "Unit")] \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/App.config b/Test/DDD.HealthcareDelivery.IntegrationTests/App.config index ea7fbec..34c9916 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/App.config +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/App.config @@ -1,13 +1,18 @@  +
+ + + + - : IDisposable - where TFixture : IPersistenceFixture + where TFixture : IPersistenceFixture { - #region Fields - - private ISessionFactory sessionFactory; - private ISession session; - private DbConnection connection; - - #endregion Fields - #region Constructors 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 ( @@ -45,9 +38,11 @@ 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 @@ -55,16 +50,16 @@ protected PharmaceuticalPrescriptionCreatorTests(TFixture fixture) public void Dispose() { - this.session.Dispose(); - this.connection.Dispose(); - this.sessionFactory.Dispose(); + this.ConnectionProvider.Dispose(); + this.Repository.Dispose(); + this.SessionFactory.Dispose(); } [Fact] public async Task HandleAsync_WhenCalled_CreatePharmaceuticalPrescription() { // Arrange - this.Fixture.ExecuteScriptFromResources("ClearDatabase"); + this.Fixture.ExecuteScriptFromResources("CreatePharmaceuticalPrescription"); Thread.CurrentPrincipal = new GenericPrincipal(new GenericIdentity("d.duck"), new string[] { "User" }); var command = CreateCommand(); // Act @@ -117,18 +112,11 @@ private static CreatePharmaceuticalPrescription CreateCommand() }; } - private IAsyncRepository CreateRepository() + private PharmaceuticalPrescriptionRepository CreateRepository() { - this.sessionFactory = this.Fixture.CreateSessionFactory(); - this.connection = this.Fixture.ConnectionFactory.CreateOpenConnection(); - this.session = this.sessionFactory - .WithOptions() - // To avoid transaction promotion from local to distributed - .Connection(this.connection) - .OpenSession(); - return new NHibernateRepository - ( - this.session, + return new PharmaceuticalPrescriptionRepository + ( + this.SessionFactory, this.Fixture.CreateEventTranslator() ); } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs index 5f6c0ba..dd7d6d0 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs @@ -1,41 +1,31 @@ using Xunit; -using System.Data.Common; using System.Threading; using System.Threading.Tasks; using System.Security.Principal; -using FluentAssertions; -using NHibernate; using System; +using FluentAssertions; namespace DDD.HealthcareDelivery.Application.Prescriptions { - using Core.Domain; + using Domain; using Core.Infrastructure.Data; using Domain.Prescriptions; using Infrastructure; + using Infrastructure.Prescriptions; public abstract class PharmaceuticalPrescriptionRevokerTests : IDisposable - where TFixture : IPersistenceFixture + where TFixture : IPersistenceFixture { - #region Fields - - private ISessionFactory sessionFactory; - private ISession session; - private DbConnection connection; - - #endregion Fields - #region Constructors 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 - ); + this.Handler = new PharmaceuticalPrescriptionRevoker(Repository); } #endregion Constructors @@ -43,8 +33,14 @@ 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 @@ -52,9 +48,9 @@ protected PharmaceuticalPrescriptionRevokerTests(TFixture fixture) public void Dispose() { - this.session.Dispose(); - this.connection.Dispose(); - this.sessionFactory.Dispose(); + this.ConnectionProvider.Dispose(); + this.Repository.Dispose(); + this.SessionFactory.Dispose(); } [Fact] @@ -80,18 +76,11 @@ private static RevokePharmaceuticalPrescription CreateCommand() }; } - private IAsyncRepository CreateRepository() + private PharmaceuticalPrescriptionRepository CreateRepository() { - this.sessionFactory = this.Fixture.CreateSessionFactory(); - this.connection = this.Fixture.ConnectionFactory.CreateOpenConnection(); - this.session = this.sessionFactory - .WithOptions() - // To avoid transaction promotion from local to distributed - .Connection(this.connection) - .OpenSession(); - return new NHibernateRepository - ( - this.session, + return new PharmaceuticalPrescriptionRepository + ( + this.SessionFactory, this.Fixture.CreateEventTranslator() ); } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj b/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj index bc096c0..97f9679 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj @@ -1,10 +1,16 @@  - net48;netcoreapp3.1;net5.0 + net48;net6.0 Library DDD.HealthcareDelivery false + + + + + + True @@ -40,9 +46,7 @@ - - @@ -52,22 +56,21 @@ - 6.1.0 + 6.9.0 - - - + + + - - + + + 2.4.5 + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianOracleHealthcareDeliveryConfigurationTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianOracleHealthcareDeliveryConfigurationTests.cs index 376d070..66c42db 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianOracleHealthcareDeliveryConfigurationTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianOracleHealthcareDeliveryConfigurationTests.cs @@ -1,4 +1,6 @@ -using Xunit; +using NHibernate.Dialect; +using NHibernate.Driver; +using Xunit; namespace DDD.HealthcareDelivery.Infrastructure { @@ -17,7 +19,13 @@ public BelgianOracleHealthcareDeliveryConfigurationTests(OracleFixture fixture) protected override HealthcareDeliveryConfiguration CreateConfiguration() { - return new BelgianOracleHealthcareDeliveryConfiguration(OracleConnectionFactory.ConnectionString); + var configuration = new BelgianOracleHealthcareDeliveryConfiguration().DataBaseIntegration(db => + { + db.Dialect(); + db.Driver(); + db.ConnectionStringName = "Oracle"; + }); + return (HealthcareDeliveryConfiguration)configuration; } #endregion Methods diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareDeliveryConfigurationTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareDeliveryConfigurationTests.cs index 60dd9e5..17652e8 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareDeliveryConfigurationTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/BelgianSqlServerHealthcareDeliveryConfigurationTests.cs @@ -1,4 +1,6 @@ -using Xunit; +using NHibernate.Dialect; +using NHibernate.Driver; +using Xunit; namespace DDD.HealthcareDelivery.Infrastructure { @@ -17,7 +19,13 @@ public BelgianSqlServerHealthcareDeliveryConfigurationTests(SqlServerFixture fix protected override HealthcareDeliveryConfiguration CreateConfiguration() { - return new BelgianSqlServerHealthcareDeliveryConfiguration(SqlServerConnectionFactory.ConnectionString); + 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 index 16fa248..d5a1f7f 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareDeliveryConfigurationTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/HealthcareDeliveryConfigurationTests.cs @@ -6,19 +6,20 @@ namespace DDD.HealthcareDelivery.Infrastructure { + using Core.Application; using Common.Domain; - using Core.Infrastructure.Data; using Domain.Patients; using Domain.Practitioners; using Domain.Prescriptions; public abstract class HealthcareDeliveryConfigurationTests : IDisposable - where TFixture : IPersistenceFixture + where TFixture : IPersistenceFixture { #region Fields private readonly HealthcareDeliveryConfiguration configuration; + private readonly ISessionFactory sessionFactory; private readonly ISession session; #endregion Fields @@ -29,8 +30,8 @@ protected HealthcareDeliveryConfigurationTests(TFixture fixture) { this.Fixture = fixture; this.configuration = this.CreateConfiguration(); - var sessionfactory = this.configuration.BuildSessionFactory(); - this.session = sessionfactory.OpenSession(); + this.sessionFactory = this.configuration.BuildSessionFactory(); + this.session = this.sessionFactory.OpenSession(); } #endregion Constructors @@ -60,7 +61,8 @@ public void HealthcareConfiguration_WhenMappingValid_CanExportSchema() public void Dispose() { - this.session?.Dispose(); + this.session.Dispose(); + this.sessionFactory.Dispose(); } [Fact] @@ -76,10 +78,10 @@ public void HealthcareConfiguration_WhenMappingValid_CanSaveAndRestoreEvents() transaction.Commit(); } this.session.Clear(); - StoredEvent event2; + Event event2; using (var transaction = this.session.BeginTransaction()) { - event2 = this.session.Get(event1.Id); + event2 = this.session.Get(event1.EventId); transaction.Commit(); } // Assert @@ -110,15 +112,15 @@ public void HealthcareConfiguration_WhenMappingValid_CanSaveAndRestorePrescripti } protected abstract HealthcareDeliveryConfiguration CreateConfiguration(); - private static StoredEvent CreateEvent() + private static Event CreateEvent() { - return new StoredEvent + return new Event { - Id = 1, + EventId = new Guid("d9fdd908-9e0a-c80f-e72d-e94a0f7d4902"), EventType = "DDD.HealthcareDelivery.Domain.Prescriptions.PharmaceuticalPrescriptionCreated, DDD.HealthcareDelivery.Messages", - Version = 1, OccurredOn = new DateTime(2018, 1, 1), Body = "{\"prescriptionId\":1,\"occurredOn\":\"2018 - 01 - 01T10:06:00\"}", + BodyFormat = "JSON", StreamId = "1", StreamType = "PharmaceuticalPrescription", IssuedBy = "draphyz" diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/IPersistenceFixture.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/IPersistenceFixture.cs index 066107a..019978e 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/IPersistenceFixture.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/IPersistenceFixture.cs @@ -1,22 +1,22 @@ -using NHibernate; - -namespace DDD.HealthcareDelivery.Infrastructure +namespace DDD.HealthcareDelivery.Infrastructure { + using Domain; using Core.Domain; - using Core.Infrastructure.Data; + using Core.Application; using Core.Infrastructure.Testing; + using Core.Infrastructure.Data; using Mapping; - public interface IPersistenceFixture : IDbFixture - where TConnectionFactory : class, IHealthcareDeliveryConnectionFactory + public interface IPersistenceFixture : IDbFixture { #region Methods - IObjectTranslator CreateEventTranslator(); + IObjectTranslator CreateEventTranslator(); - ISessionFactory CreateSessionFactory(); + DelegatingSessionFactory CreateSessionFactory(IDbConnectionProvider connectionProvider); #endregion Methods + } } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleConnectionFactory.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleConnectionFactory.cs deleted file mode 100644 index 55f9351..0000000 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleConnectionFactory.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace DDD.HealthcareDelivery.Infrastructure -{ - using Core.Infrastructure.Data; - - public class OracleConnectionFactory : DbConnectionFactory, IHealthcareDeliveryConnectionFactory - { - - #region Fields - - /// - /// Pooling=false is used to ensure that the System.Transactions infrastructure doesn't automatically escalates the transaction to be managed by the Microsoft Distributed Transaction Coordinator (MSDTC). - /// Do not use Pooling=false in production. - /// - public const string ConnectionString - = "Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=XE)));Persist Security Info=false;User Id=TEST;Password=dev;Pooling=false"; - - #endregion Fields - - #region Constructors - - private OracleConnectionFactory(string providerName, string connectionString) - : base(providerName, connectionString) - { - } - - #endregion Constructors - - #region Methods - - public static OracleConnectionFactory Create() - { - return new OracleConnectionFactory("Oracle.ManagedDataAccess.Client", ConnectionString); - } - - #endregion Methods - - } -} diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleFixture.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleFixture.cs index 10adb84..9362a17 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleFixture.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleFixture.cs @@ -1,46 +1,68 @@ using System.Data; -using System.Text; -using NHibernate; -using Oracle.ManagedDataAccess.Client; -#if (NETCOREAPP3_1 || NET5_0) +#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 + public class OracleFixture : DbFixture, IPersistenceFixture { - #region Constructors +#region Constructors - public OracleFixture() : base(OracleConnectionFactory.Create(), "OracleScripts") + public OracleFixture() : base("OracleScripts", ConfigurationManager.ConnectionStrings["Oracle"]) { } - #endregion Constructors +#endregion Constructors - #region Methods +#region Methods - public IObjectTranslator CreateEventTranslator() + public IObjectTranslator CreateEventTranslator() { - return new StoredEventTranslator(JsonSerializerWrapper.Create(false)); + return new EventTranslator(JsonSerializerWrapper.Create(false)); } - public ISessionFactory CreateSessionFactory() + public DelegatingSessionFactory CreateSessionFactory(IDbConnectionProvider connectionProvider) { - var configuration = new BelgianOracleHealthcareDeliveryConfiguration(OracleConnectionFactory.ConnectionString); - return configuration.BuildSessionFactory(); + var configuration = new BelgianOracleHealthcareDeliveryConfiguration().DataBaseIntegration(db => + { + db.Dialect(); + db.Driver(); + db.KeywordsAutoImport = Hbm2DDLKeyWords.None; + }); + return new DelegatingSessionFactory + ( + 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.ConnectionFactory.CreateConnection()) + using (var connection = this.CreateConnection()) { var builder = new OracleConnectionStringBuilder(connection.ConnectionString) { UserID = "SYS", DBAPrivilege = "SYSDBA" }; connection.ConnectionString = builder.ConnectionString; @@ -55,14 +77,20 @@ protected override int[] ExecuteScript(string script, IDbConnection connection) return connection.ExecuteScript(script, batchSeparator: "/"); } - protected override void RegisterDbProviderFactory() + protected override void LoadConfiguration() { -#if (NETCOREAPP3_1 || NET5_0) +#if (NET6_0) DbProviderFactories.RegisterFactory("Oracle.ManagedDataAccess.Client", OracleClientFactory.Instance); #endif } - #endregion Methods + 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 da4650a..4738ba7 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinderTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinderTests.cs @@ -6,26 +6,32 @@ namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions { + using Domain; using Application.Prescriptions; + using Core.Infrastructure.Testing; + using Core.Infrastructure.Data; - public abstract class PharmaceuticalPrescriptionsByPatientFinderTests - where TFixture : IPersistenceFixture + 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() @@ -105,14 +111,20 @@ 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); // Act var results = await handler.HandleAsync(query); // Assert - results.Should().BeEquivalentTo(expectedResults, options => options.WithStrictOrdering()); + results.Should().BeEquivalentTo(expectedResults, context => context.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 0af1988..c3607ac 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinderTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinderTests.cs @@ -1,30 +1,38 @@ using FluentAssertions; +using System; using System.Collections.Generic; using System.Threading.Tasks; using Xunit; namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions { + using Domain; using Application.Prescriptions; + using Core.Infrastructure.Testing; + using Core.Infrastructure.Data; - public abstract class PrescribedMedicationsByPrescriptionFinderTests - where TFixture : IPersistenceFixture + 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() @@ -79,14 +87,19 @@ 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); // Act var results = await handler.HandleAsync(query); // 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/SqlServerConnectionFactory.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerConnectionFactory.cs deleted file mode 100644 index 27d973f..0000000 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerConnectionFactory.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace DDD.HealthcareDelivery.Infrastructure -{ - using Core.Infrastructure.Data; - - public class SqlServerConnectionFactory : DbConnectionFactory, IHealthcareDeliveryConnectionFactory - { - - #region Fields - - /// - /// Pooling=false is used to ensure that the System.Transactions infrastructure doesn't automatically escalates the transaction to be managed by the Microsoft Distributed Transaction Coordinator (MSDTC). - /// Do not use Pooling=false in production. - /// - public const string ConnectionString - = @"Data Source=(local)\SQLEXPRESS;Database=Test;Integrated Security=False;User ID=sa;Password=dev;Pooling=false"; - - #endregion Fields - - #region Constructors - - private SqlServerConnectionFactory(string providerName, string connectionString) - : base(providerName, connectionString) - { - } - - #endregion Constructors - - #region Methods - - public static SqlServerConnectionFactory Create() - { - return new SqlServerConnectionFactory("Microsoft.Data.SqlClient", ConnectionString); - } - - #endregion Methods - } -} diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerFixture.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerFixture.cs index 54fc16a..84c79a8 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerFixture.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerFixture.cs @@ -1,25 +1,29 @@ using Microsoft.Data.SqlClient; -using NHibernate; using System.Data; -using System.Text; -#if (NETCOREAPP3_1 || NET5_0) +#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 + public class SqlServerFixture : DbFixture, IPersistenceFixture { #region Constructors - public SqlServerFixture() : base(SqlServerConnectionFactory.Create(), "SqlServerScripts") + public SqlServerFixture() : base("SqlServerScripts", ConfigurationManager.ConnectionStrings["SqlServer"]) { } @@ -27,20 +31,38 @@ public SqlServerFixture() : base(SqlServerConnectionFactory.Create(), "SqlServer #region Methods - public IObjectTranslator CreateEventTranslator() + public IObjectTranslator CreateEventTranslator() { - return new StoredEventTranslator(JsonSerializerWrapper.Create(false)); + return new EventTranslator(JsonSerializerWrapper.Create(false)); } - public ISessionFactory CreateSessionFactory() + public DelegatingSessionFactory CreateSessionFactory(IDbConnectionProvider connectionProvider) { - var configuration = new BelgianSqlServerHealthcareDeliveryConfiguration(SqlServerConnectionFactory.ConnectionString); - return configuration.BuildSessionFactory(); + var configuration = new BelgianSqlServerHealthcareDeliveryConfiguration().DataBaseIntegration(db => + { + db.Dialect(); + db.Driver(); + db.KeywordsAutoImport = Hbm2DDLKeyWords.None; + }); + return new DelegatingSessionFactory + ( + 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.ConnectionFactory.CreateConnection()) + using (var connection = this.CreateConnection()) { var builder = new SqlConnectionStringBuilder(connection.ConnectionString) { InitialCatalog = "master" }; connection.ConnectionString = builder.ConnectionString; @@ -48,18 +70,26 @@ protected override void CreateDatabase() this.ExecuteScript(SqlServerScripts.CreateDatabase, connection); } } + protected override int[] ExecuteScript(string script, IDbConnection connection) { return connection.ExecuteScript(script); } - protected override void RegisterDbProviderFactory() + protected override void LoadConfiguration() { -#if (NETCOREAPP3_1 || NET5_0) +#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 } diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.Designer.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.Designer.cs index 5e67a96..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", "16.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 { @@ -72,6 +72,18 @@ internal static string ClearDatabase { } } + /// + /// Looks up a localized string similar to BEGIN + /// SPCLEARSCHEMA(); + ///END; + ////. + /// + internal static string CreatePharmaceuticalPrescription { + get { + return ResourceManager.GetString("CreatePharmaceuticalPrescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to DECLARE /// usercount NUMBER; diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.resx b/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.resx index 182091b..ca655d9 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.resx +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/OracleScripts.resx @@ -121,6 +121,9 @@ 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 + scripts\oracle\createschema.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/CreatePharmaceuticalPrescription.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/CreatePharmaceuticalPrescription.sql new file mode 100644 index 0000000..e14a84a --- /dev/null +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/Oracle/CreatePharmaceuticalPrescription.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 37c0438..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,20 +39,15 @@ END SPCLEARSCHEMA; -------------------------------------------------------- CREATE TABLE TEST.EVENT - ( EVENTID NUMBER(19,0), - EVENTTYPE VARCHAR2(250 CHAR), - VERSION NUMBER(3, 0), - OCCURREDON TIMESTAMP(3), - BODY VARCHAR2(4000), - BODYFORMAT VARCHAR2(20), - STREAMID VARCHAR2(50 CHAR), - STREAMTYPE VARCHAR2(50 CHAR), - ISSUEDBY VARCHAR2(100 CHAR) - ) 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 + (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 @@ -69,7 +59,7 @@ END SPCLEARSCHEMA; MEDICATIONTYPE VARCHAR2(20 CHAR), NAMEORDESC VARCHAR2(1024 CHAR), POSOLOGY VARCHAR2(1024 CHAR), - QUANTITY NUMBER(3, 0), + QUANTITY NUMBER(3,0), CODE VARCHAR2(20 CHAR) ) SEGMENT CREATION IMMEDIATE PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING @@ -131,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 -------------------------------------------------------- @@ -160,39 +140,6 @@ END SPCLEARSCHEMA; PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT) 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 (VERSION NOT NULL ENABLE) -/ - - ALTER TABLE TEST.EVENT MODIFY (OCCURREDON NOT NULL ENABLE) -/ - - ALTER TABLE TEST.EVENT MODIFY (BODY NOT NULL ENABLE) -/ - - ALTER TABLE TEST.EVENT MODIFY (STREAMID NOT NULL ENABLE) -/ - - ALTER TABLE TEST.EVENT MODIFY (STREAMTYPE NOT NULL ENABLE) -/ - - -------------------------------------------------------- -- Constraints for Table PRESCMEDICATION -------------------------------------------------------- @@ -270,7 +217,6 @@ END SPCLEARSCHEMA; ALTER TABLE TEST.PRESCRIPTION MODIFY (PATIENTSEX NOT NULL ENABLE) / - -------------------------------------------------------- -- Ref Constraints for Table PRESCMEDICATION -------------------------------------------------------- diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql b/Test/DDD.HealthcareDelivery.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql index 1e0bd78389838dc4834c938e503cd93bc957442b..91d9d5a663db92a42690753a9cfe5c8f6fefda2c 100644 GIT binary patch delta 155 zcmcaoKcjwwoRWMgLmopWLm@*cLn@F?0g`zPB|ugh5T`N}O;*&CpUkDiH91JhW%D(q zTa4@}3`q?640@Z_s~lvUe87fda)PkZ scripts\sqlserver\createdatabase.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + scripts\sqlserver\createpharmaceuticalprescription.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 + scripts\sqlserver\findpharmaceuticalprescriptionsbypatient.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 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 1e7bbbc..86c8cca 100644 --- a/Test/DDD.HealthcareDelivery.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj +++ b/Test/DDD.HealthcareDelivery.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj @@ -1,6 +1,6 @@  - net48;netcoreapp3.1;net5.0 + net48;net6.0 Library DDD.HealthcareDelivery false @@ -20,21 +20,18 @@ - 6.1.0 + 6.9.0 - 10.3.3 + 11.4.0 - - - 4.5.0 - + - + - 2.4.3 + 2.4.5 runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Test/DDD.HealthcareDelivery.UnitTests/Infrastructure/Prescriptions/BelgianCreatePharmaceuticalPrescriptionValidatorTests.cs b/Test/DDD.HealthcareDelivery.UnitTests/Infrastructure/Prescriptions/BelgianCreatePharmaceuticalPrescriptionValidatorTests.cs index 9650f3b..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 { @@ -571,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 9319e4c..fa89750 100644 --- a/Test/DDD.HealthcareDelivery.UnitTests/Properties/AssemblyInfo.cs +++ b/Test/DDD.HealthcareDelivery.UnitTests/Properties/AssemblyInfo.cs @@ -3,6 +3,18 @@ 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.UnitTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("DDD.HealthcareDelivery.UnitTests")] +[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. @@ -10,4 +22,17 @@ // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("ca376d7c-2a71-4518-b297-4c4a08dbf19d")] + +// 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", "Unit")] From 226175c2dd9660de0bbde21738f1f6c980256bd7 Mon Sep 17 00:00:00 2001 From: draphyz Date: Fri, 3 Feb 2023 09:16:00 +0100 Subject: [PATCH 067/111] Refactoring --- Src/DDD.Core.Dapper/EventStreamPositionUpdater.cs | 2 +- Src/DDD.Core.Dapper/EventStreamReader.cs | 2 +- Src/DDD.Core.Dapper/EventStreamSubcriber.cs | 2 +- Src/DDD.Core.Dapper/EventStreamsFinder.cs | 2 +- Src/DDD.Core.Dapper/FailedEventStreamExcluder.cs | 2 +- Src/DDD.Core.Dapper/FailedEventStreamIncluder.cs | 2 +- Src/DDD.Core.Dapper/FailedEventStreamReader.cs | 2 +- Src/DDD.Core.Dapper/FailedEventStreamUpdater.cs | 2 +- Src/DDD.Core.Dapper/FailedEventStreamsFinder.cs | 2 +- Src/DDD.Core.Dapper/FailedRecurringCommandUpdater.cs | 2 +- Src/DDD.Core.Dapper/RecurringCommandIdGenerator.cs | 2 +- Src/DDD.Core.Dapper/RecurringCommandRegister.cs | 2 +- Src/DDD.Core.Dapper/RecurringCommandsFinder.cs | 2 +- Src/DDD.Core.Dapper/SuccessfulRecurringCommandUpdater.cs | 2 +- Src/DDD.Core.NHibernate/EventMapping.cs | 3 --- Src/DDD.Core.NHibernate/MemberInfoExtensions.cs | 1 - Src/DDD.Core.Polly/AsyncPollyCommandHandler`1.cs | 2 +- Src/DDD.Core.Polly/AsyncPollyQueryHandler`1.cs | 2 +- Src/DDD.Core.Polly/SyncPollyCommandHandler`1.cs | 2 +- Src/DDD.Core.Polly/SyncPollyQueryHandler`1.cs | 2 +- Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler`1.cs | 2 +- Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler`1.cs | 2 +- .../ThreadScopedCommandHandler`1.cs | 2 +- Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler`1.cs | 2 +- Src/DDD.Core.Xunit/DbFixture.cs | 3 +-- Src/DDD.Core.Xunit/IDbFixture.cs | 3 +-- Src/DDD.Core.Xunit/IDbFixtureExtensions.cs | 3 +-- .../Application/AsyncCommandHandlerWithLogging`1.cs | 3 ++- Src/DDD.Core/Application/AsyncQueryHandlerWithLogging`1.cs | 3 ++- Src/DDD.Core/Application/CommandProcessor.cs | 2 +- Src/DDD.Core/Application/ContextualCommandProcessor.cs | 3 +-- Src/DDD.Core/Application/ContextualQueryProcessor.cs | 4 +--- Src/DDD.Core/Application/EventConsumerSettings.cs | 3 ++- Src/DDD.Core/Application/IAsyncCommandHandler`1.cs | 6 +++--- Src/DDD.Core/Application/IAsyncQueryHandler`1.cs | 6 +++--- Src/DDD.Core/Application/ICommandHandler`1.cs | 6 +++--- Src/DDD.Core/Application/ICommandProcessor.cs | 1 - Src/DDD.Core/Application/IContextualCommandProcessor.cs | 5 +++-- Src/DDD.Core/Application/IContextualCommandProcessor`1.cs | 7 +++---- Src/DDD.Core/Application/IContextualQueryProcessor.cs | 5 +++-- Src/DDD.Core/Application/IContextualQueryProcessor`1.cs | 6 +++--- Src/DDD.Core/Application/IEventConsumer.cs | 2 -- Src/DDD.Core/Application/IEventConsumer`1.cs | 6 +++--- Src/DDD.Core/Application/IMessageContextExtensions.cs | 2 -- Src/DDD.Core/Application/IQueryHandler`1.cs | 6 +++--- Src/DDD.Core/Application/IRecurringCommandManager`1.cs | 6 +++--- Src/DDD.Core/Application/ISyncCommandHandlerExtensions.cs | 2 -- Src/DDD.Core/Application/ISyncCommandHandler`1.cs | 6 +++--- Src/DDD.Core/Application/ISyncQueryHandler`1.cs | 6 +++--- Src/DDD.Core/Application/RecurringCommandManager.cs | 2 +- .../Application/RecurringCommandManagerSettings.cs | 2 +- .../Application/SyncCommandHandlerWithLogging`1.cs | 3 ++- Src/DDD.Core/Application/SyncQueryHandlerWithLogging`1.cs | 3 ++- Src/DDD.Core/Domain/DomainEntity.cs | 3 --- Src/DDD.Core/Domain/IAsyncRepository.cs | 3 +-- Src/DDD.Core/Domain/ISyncRepository.cs | 4 +--- Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider.cs | 2 -- .../Infrastructure/Data/IDbConnectionProvider`1.cs | 3 +-- .../Infrastructure/Data/LazyDbConnectionProvider.cs | 3 +-- .../Data/SequentialSqlServerGuidGenerator.cs | 4 +--- .../Prescriptions/PrescribedMedicationDescriptor.cs | 2 -- .../Prescriptions/PharmaceuticalPrescriptionRepository.cs | 6 +----- Test/DDD.Common.UnitTests/Domain/EnumerationTests.cs | 3 +-- Test/DDD.Common.UnitTests/Properties/AssemblyInfo.cs | 1 - .../Application/RecurringCommandManagerTests.cs | 2 +- Test/DDD.Core.UnitTests/Domain/FakeContext.cs | 2 -- .../DDD.HealthcareDelivery.IntegrationTests.csproj | 5 +++++ .../Properties/AssemblyInfo.cs | 4 +--- .../Properties/AssemblyInfo.cs | 1 - 69 files changed, 90 insertions(+), 121 deletions(-) diff --git a/Src/DDD.Core.Dapper/EventStreamPositionUpdater.cs b/Src/DDD.Core.Dapper/EventStreamPositionUpdater.cs index 8b01b66..a3b5cd6 100644 --- a/Src/DDD.Core.Dapper/EventStreamPositionUpdater.cs +++ b/Src/DDD.Core.Dapper/EventStreamPositionUpdater.cs @@ -8,7 +8,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; - using DDD.Core.Domain; + using Domain; using Mapping; using Threading; diff --git a/Src/DDD.Core.Dapper/EventStreamReader.cs b/Src/DDD.Core.Dapper/EventStreamReader.cs index 31aa9a9..eedcb1c 100644 --- a/Src/DDD.Core.Dapper/EventStreamReader.cs +++ b/Src/DDD.Core.Dapper/EventStreamReader.cs @@ -9,7 +9,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; - using DDD.Core.Domain; + using Domain; using Mapping; using Threading; diff --git a/Src/DDD.Core.Dapper/EventStreamSubcriber.cs b/Src/DDD.Core.Dapper/EventStreamSubcriber.cs index faf1041..ff60c55 100644 --- a/Src/DDD.Core.Dapper/EventStreamSubcriber.cs +++ b/Src/DDD.Core.Dapper/EventStreamSubcriber.cs @@ -8,7 +8,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; - using DDD.Core.Domain; + using Domain; using Mapping; using Threading; diff --git a/Src/DDD.Core.Dapper/EventStreamsFinder.cs b/Src/DDD.Core.Dapper/EventStreamsFinder.cs index bec61e2..f8002c2 100644 --- a/Src/DDD.Core.Dapper/EventStreamsFinder.cs +++ b/Src/DDD.Core.Dapper/EventStreamsFinder.cs @@ -7,7 +7,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; - using DDD.Core.Domain; + using Domain; using Mapping; using Threading; diff --git a/Src/DDD.Core.Dapper/FailedEventStreamExcluder.cs b/Src/DDD.Core.Dapper/FailedEventStreamExcluder.cs index e6c3c05..3b635d4 100644 --- a/Src/DDD.Core.Dapper/FailedEventStreamExcluder.cs +++ b/Src/DDD.Core.Dapper/FailedEventStreamExcluder.cs @@ -8,7 +8,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; - using DDD.Core.Domain; + using Domain; using Mapping; using Threading; diff --git a/Src/DDD.Core.Dapper/FailedEventStreamIncluder.cs b/Src/DDD.Core.Dapper/FailedEventStreamIncluder.cs index d61d57f..0118bf3 100644 --- a/Src/DDD.Core.Dapper/FailedEventStreamIncluder.cs +++ b/Src/DDD.Core.Dapper/FailedEventStreamIncluder.cs @@ -7,7 +7,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; - using DDD.Core.Domain; + using Domain; using Mapping; using Threading; diff --git a/Src/DDD.Core.Dapper/FailedEventStreamReader.cs b/Src/DDD.Core.Dapper/FailedEventStreamReader.cs index 842414f..9333790 100644 --- a/Src/DDD.Core.Dapper/FailedEventStreamReader.cs +++ b/Src/DDD.Core.Dapper/FailedEventStreamReader.cs @@ -8,7 +8,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; - using DDD.Core.Domain; + using Domain; using Mapping; using Threading; diff --git a/Src/DDD.Core.Dapper/FailedEventStreamUpdater.cs b/Src/DDD.Core.Dapper/FailedEventStreamUpdater.cs index 4cd0a4e..6c956ee 100644 --- a/Src/DDD.Core.Dapper/FailedEventStreamUpdater.cs +++ b/Src/DDD.Core.Dapper/FailedEventStreamUpdater.cs @@ -8,7 +8,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; - using DDD.Core.Domain; + using Domain; using Mapping; using Threading; diff --git a/Src/DDD.Core.Dapper/FailedEventStreamsFinder.cs b/Src/DDD.Core.Dapper/FailedEventStreamsFinder.cs index 60ee3f5..494a0a8 100644 --- a/Src/DDD.Core.Dapper/FailedEventStreamsFinder.cs +++ b/Src/DDD.Core.Dapper/FailedEventStreamsFinder.cs @@ -7,7 +7,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; - using DDD.Core.Domain; + using Domain; using Mapping; using Threading; diff --git a/Src/DDD.Core.Dapper/FailedRecurringCommandUpdater.cs b/Src/DDD.Core.Dapper/FailedRecurringCommandUpdater.cs index c11a3b4..f2c728d 100644 --- a/Src/DDD.Core.Dapper/FailedRecurringCommandUpdater.cs +++ b/Src/DDD.Core.Dapper/FailedRecurringCommandUpdater.cs @@ -8,7 +8,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; - using DDD.Core.Domain; + using Domain; using Mapping; using Threading; diff --git a/Src/DDD.Core.Dapper/RecurringCommandIdGenerator.cs b/Src/DDD.Core.Dapper/RecurringCommandIdGenerator.cs index 9049789..c4e4c3a 100644 --- a/Src/DDD.Core.Dapper/RecurringCommandIdGenerator.cs +++ b/Src/DDD.Core.Dapper/RecurringCommandIdGenerator.cs @@ -4,7 +4,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; - using DDD.Core.Domain; + using Domain; using Mapping; public class RecurringCommandIdGenerator : ISyncQueryHandler diff --git a/Src/DDD.Core.Dapper/RecurringCommandRegister.cs b/Src/DDD.Core.Dapper/RecurringCommandRegister.cs index 19cc233..37cc247 100644 --- a/Src/DDD.Core.Dapper/RecurringCommandRegister.cs +++ b/Src/DDD.Core.Dapper/RecurringCommandRegister.cs @@ -8,7 +8,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; - using DDD.Core.Domain; + using Domain; using Mapping; using Threading; diff --git a/Src/DDD.Core.Dapper/RecurringCommandsFinder.cs b/Src/DDD.Core.Dapper/RecurringCommandsFinder.cs index ef57c37..28c556c 100644 --- a/Src/DDD.Core.Dapper/RecurringCommandsFinder.cs +++ b/Src/DDD.Core.Dapper/RecurringCommandsFinder.cs @@ -7,7 +7,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; - using DDD.Core.Domain; + using Domain; using Mapping; using Threading; diff --git a/Src/DDD.Core.Dapper/SuccessfulRecurringCommandUpdater.cs b/Src/DDD.Core.Dapper/SuccessfulRecurringCommandUpdater.cs index c1c1a3d..ddcfc8c 100644 --- a/Src/DDD.Core.Dapper/SuccessfulRecurringCommandUpdater.cs +++ b/Src/DDD.Core.Dapper/SuccessfulRecurringCommandUpdater.cs @@ -8,7 +8,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; - using DDD.Core.Domain; + using Domain; using Mapping; using Threading; diff --git a/Src/DDD.Core.NHibernate/EventMapping.cs b/Src/DDD.Core.NHibernate/EventMapping.cs index fc3bdb2..0fe1282 100644 --- a/Src/DDD.Core.NHibernate/EventMapping.cs +++ b/Src/DDD.Core.NHibernate/EventMapping.cs @@ -1,8 +1,5 @@ using NHibernate; using NHibernate.Mapping.ByCode.Conformist; -using NHibernate.Mapping.ByCode; -using DDD.Serialization; -using NHibernate.Type; namespace DDD.Core.Infrastructure.Data { diff --git a/Src/DDD.Core.NHibernate/MemberInfoExtensions.cs b/Src/DDD.Core.NHibernate/MemberInfoExtensions.cs index 5a97065..f7f131c 100644 --- a/Src/DDD.Core.NHibernate/MemberInfoExtensions.cs +++ b/Src/DDD.Core.NHibernate/MemberInfoExtensions.cs @@ -1,7 +1,6 @@ using System; using System.Reflection; using NHibernate; -using System.Linq; namespace DDD.Core.Infrastructure.Data { diff --git a/Src/DDD.Core.Polly/AsyncPollyCommandHandler`1.cs b/Src/DDD.Core.Polly/AsyncPollyCommandHandler`1.cs index 6639d9b..bb1a0fa 100644 --- a/Src/DDD.Core.Polly/AsyncPollyCommandHandler`1.cs +++ b/Src/DDD.Core.Polly/AsyncPollyCommandHandler`1.cs @@ -5,7 +5,7 @@ namespace DDD.Core.Infrastructure.ErrorHandling { using Application; - using DDD.Core.Domain; + using Domain; /// /// A decorator that applies a resilience policy to the asynchronous execution of a command. diff --git a/Src/DDD.Core.Polly/AsyncPollyQueryHandler`1.cs b/Src/DDD.Core.Polly/AsyncPollyQueryHandler`1.cs index 991d091..d0ce34c 100644 --- a/Src/DDD.Core.Polly/AsyncPollyQueryHandler`1.cs +++ b/Src/DDD.Core.Polly/AsyncPollyQueryHandler`1.cs @@ -5,7 +5,7 @@ namespace DDD.Core.Infrastructure.ErrorHandling { using Application; - using DDD.Core.Domain; + using Domain; /// /// A decorator that applies a resilience policy to the asynchronous execution of a query. diff --git a/Src/DDD.Core.Polly/SyncPollyCommandHandler`1.cs b/Src/DDD.Core.Polly/SyncPollyCommandHandler`1.cs index b7a6940..bf9f315 100644 --- a/Src/DDD.Core.Polly/SyncPollyCommandHandler`1.cs +++ b/Src/DDD.Core.Polly/SyncPollyCommandHandler`1.cs @@ -4,7 +4,7 @@ namespace DDD.Core.Infrastructure.ErrorHandling { using Application; - using DDD.Core.Domain; + using Domain; /// /// A decorator that applies a resilience policy to the synchronous execution of a command. diff --git a/Src/DDD.Core.Polly/SyncPollyQueryHandler`1.cs b/Src/DDD.Core.Polly/SyncPollyQueryHandler`1.cs index 3dcf394..43b8ce4 100644 --- a/Src/DDD.Core.Polly/SyncPollyQueryHandler`1.cs +++ b/Src/DDD.Core.Polly/SyncPollyQueryHandler`1.cs @@ -4,7 +4,7 @@ namespace DDD.Core.Infrastructure.ErrorHandling { using Application; - using DDD.Core.Domain; + using Domain; /// /// A decorator that applies a resilience policy to the synchronous execution of a query. diff --git a/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler`1.cs b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler`1.cs index e5d3881..9c46d56 100644 --- a/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler`1.cs +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler`1.cs @@ -7,7 +7,7 @@ namespace DDD.Core.Infrastructure.DependencyInjection { using Application; - using DDD.Core.Domain; + using Domain; using Threading; /// diff --git a/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler`1.cs b/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler`1.cs index 02acd6f..31693d3 100644 --- a/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler`1.cs +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler`1.cs @@ -7,7 +7,7 @@ namespace DDD.Core.Infrastructure.DependencyInjection { using Application; - using DDD.Core.Domain; + using Domain; using Threading; /// diff --git a/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler`1.cs b/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler`1.cs index f0845a6..5cad331 100644 --- a/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler`1.cs +++ b/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler`1.cs @@ -6,7 +6,7 @@ namespace DDD.Core.Infrastructure.DependencyInjection { using Application; - using DDD.Core.Domain; + using Domain; /// /// A decorator that defines a scope around the synchronous execution of a command. diff --git a/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler`1.cs b/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler`1.cs index 61dc67f..3eaf327 100644 --- a/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler`1.cs +++ b/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler`1.cs @@ -6,7 +6,7 @@ namespace DDD.Core.Infrastructure.DependencyInjection { using Application; - using DDD.Core.Domain; + using Domain; /// /// A decorator that defines a scope around the synchronous execution of a query. diff --git a/Src/DDD.Core.Xunit/DbFixture.cs b/Src/DDD.Core.Xunit/DbFixture.cs index 4d48c8b..069bb86 100644 --- a/Src/DDD.Core.Xunit/DbFixture.cs +++ b/Src/DDD.Core.Xunit/DbFixture.cs @@ -10,8 +10,7 @@ namespace DDD.Core.Infrastructure.Testing { using Data; - using Application; - using DDD.Core.Domain; + using Domain; public abstract class DbFixture : IDbFixture where TContext : BoundedContext, new() diff --git a/Src/DDD.Core.Xunit/IDbFixture.cs b/Src/DDD.Core.Xunit/IDbFixture.cs index 1e3b7fe..c01b83f 100644 --- a/Src/DDD.Core.Xunit/IDbFixture.cs +++ b/Src/DDD.Core.Xunit/IDbFixture.cs @@ -3,8 +3,7 @@ namespace DDD.Core.Infrastructure.Testing { using Data; - using Application; - using DDD.Core.Domain; + using Domain; public interface IDbFixture where TContext : BoundedContext diff --git a/Src/DDD.Core.Xunit/IDbFixtureExtensions.cs b/Src/DDD.Core.Xunit/IDbFixtureExtensions.cs index 899106c..8d52e69 100644 --- a/Src/DDD.Core.Xunit/IDbFixtureExtensions.cs +++ b/Src/DDD.Core.Xunit/IDbFixtureExtensions.cs @@ -5,8 +5,7 @@ namespace DDD.Core.Infrastructure.Testing { - using Application; - using DDD.Core.Domain; + using Domain; public static class IDbFixtureExtensions { diff --git a/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging`1.cs b/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging`1.cs index c4d0768..1efee46 100644 --- a/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging`1.cs +++ b/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging`1.cs @@ -1,11 +1,12 @@ using EnsureThat; -using DDD.Core.Domain; using Microsoft.Extensions.Logging; using System.Diagnostics; using System.Threading.Tasks; namespace DDD.Core.Application { + using Domain; + /// /// A decorator that logs information about commands. /// diff --git a/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging`1.cs b/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging`1.cs index 143a264..5f56d60 100644 --- a/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging`1.cs +++ b/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging`1.cs @@ -1,11 +1,12 @@ using EnsureThat; -using DDD.Core.Domain; using Microsoft.Extensions.Logging; using System.Diagnostics; using System.Threading.Tasks; namespace DDD.Core.Application { + using Domain; + /// /// A decorator that logs information about queries. /// diff --git a/Src/DDD.Core/Application/CommandProcessor.cs b/Src/DDD.Core/Application/CommandProcessor.cs index 399820f..4b16dc0 100644 --- a/Src/DDD.Core/Application/CommandProcessor.cs +++ b/Src/DDD.Core/Application/CommandProcessor.cs @@ -5,7 +5,7 @@ namespace DDD.Core.Application { - using DDD.Core.Domain; + using Domain; using Validation; /// diff --git a/Src/DDD.Core/Application/ContextualCommandProcessor.cs b/Src/DDD.Core/Application/ContextualCommandProcessor.cs index 117afa4..25b6b0c 100644 --- a/Src/DDD.Core/Application/ContextualCommandProcessor.cs +++ b/Src/DDD.Core/Application/ContextualCommandProcessor.cs @@ -1,11 +1,10 @@ using EnsureThat; using System; -using System.Threading; using System.Threading.Tasks; namespace DDD.Core.Application { - using DDD.Core.Domain; + using Domain; /// /// The command processor for processing generic commands in a specific bounded context. diff --git a/Src/DDD.Core/Application/ContextualQueryProcessor.cs b/Src/DDD.Core/Application/ContextualQueryProcessor.cs index 6f38862..bc60c1c 100644 --- a/Src/DDD.Core/Application/ContextualQueryProcessor.cs +++ b/Src/DDD.Core/Application/ContextualQueryProcessor.cs @@ -1,12 +1,10 @@ using EnsureThat; using System; -using System.Threading; using System.Threading.Tasks; namespace DDD.Core.Application { - using DDD.Core.Domain; - using Validation; + using Domain; /// /// The query processor for processing and validating generic queries in a specific bounded context. diff --git a/Src/DDD.Core/Application/EventConsumerSettings.cs b/Src/DDD.Core/Application/EventConsumerSettings.cs index 9edfebf..db08068 100644 --- a/Src/DDD.Core/Application/EventConsumerSettings.cs +++ b/Src/DDD.Core/Application/EventConsumerSettings.cs @@ -1,9 +1,10 @@ using EnsureThat; -using DDD.Core.Domain; using System.Runtime.Serialization; namespace DDD.Core.Application { + using Domain; + [DataContract()] public class EventConsumerSettings where TContext : BoundedContext, new() diff --git a/Src/DDD.Core/Application/IAsyncCommandHandler`1.cs b/Src/DDD.Core/Application/IAsyncCommandHandler`1.cs index ceab794..6e4218a 100644 --- a/Src/DDD.Core/Application/IAsyncCommandHandler`1.cs +++ b/Src/DDD.Core/Application/IAsyncCommandHandler`1.cs @@ -1,7 +1,7 @@ -using DDD.Core.Domain; - -namespace DDD.Core.Application +namespace DDD.Core.Application { + using Domain; + /// /// Defines a method that handles asynchronously a command of a specified type in a specific bounded context. /// diff --git a/Src/DDD.Core/Application/IAsyncQueryHandler`1.cs b/Src/DDD.Core/Application/IAsyncQueryHandler`1.cs index 6b19f43..fb2dd22 100644 --- a/Src/DDD.Core/Application/IAsyncQueryHandler`1.cs +++ b/Src/DDD.Core/Application/IAsyncQueryHandler`1.cs @@ -1,7 +1,7 @@ -using DDD.Core.Domain; - -namespace DDD.Core.Application +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. /// diff --git a/Src/DDD.Core/Application/ICommandHandler`1.cs b/Src/DDD.Core/Application/ICommandHandler`1.cs index d0bc239..fe9d650 100644 --- a/Src/DDD.Core/Application/ICommandHandler`1.cs +++ b/Src/DDD.Core/Application/ICommandHandler`1.cs @@ -1,7 +1,7 @@ -using DDD.Core.Domain; - -namespace DDD.Core.Application +namespace DDD.Core.Application { + using Domain; + /// /// Defines methods that handle synchronously and asynchronously a command of a specified type in a specific bounded context. /// diff --git a/Src/DDD.Core/Application/ICommandProcessor.cs b/Src/DDD.Core/Application/ICommandProcessor.cs index 60f3a42..84783a1 100644 --- a/Src/DDD.Core/Application/ICommandProcessor.cs +++ b/Src/DDD.Core/Application/ICommandProcessor.cs @@ -4,7 +4,6 @@ namespace DDD.Core.Application { using Domain; - using System.Net; using Validation; /// diff --git a/Src/DDD.Core/Application/IContextualCommandProcessor.cs b/Src/DDD.Core/Application/IContextualCommandProcessor.cs index 04a53bd..9dfd825 100644 --- a/Src/DDD.Core/Application/IContextualCommandProcessor.cs +++ b/Src/DDD.Core/Application/IContextualCommandProcessor.cs @@ -1,8 +1,9 @@ -using DDD.Core.Domain; -using System.Threading.Tasks; +using System.Threading.Tasks; namespace DDD.Core.Application { + using Domain; + /// /// Defines a component that processes generic commands in a specific bounded context. /// diff --git a/Src/DDD.Core/Application/IContextualCommandProcessor`1.cs b/Src/DDD.Core/Application/IContextualCommandProcessor`1.cs index 775c7c9..4c647b5 100644 --- a/Src/DDD.Core/Application/IContextualCommandProcessor`1.cs +++ b/Src/DDD.Core/Application/IContextualCommandProcessor`1.cs @@ -1,8 +1,7 @@ -using DDD.Core.Domain; -using System.Threading.Tasks; - -namespace DDD.Core.Application +namespace DDD.Core.Application { + using Domain; + /// /// Defines a component that processes generic commands in a specific bounded context. /// diff --git a/Src/DDD.Core/Application/IContextualQueryProcessor.cs b/Src/DDD.Core/Application/IContextualQueryProcessor.cs index 95858a8..bc3bb66 100644 --- a/Src/DDD.Core/Application/IContextualQueryProcessor.cs +++ b/Src/DDD.Core/Application/IContextualQueryProcessor.cs @@ -1,8 +1,9 @@ -using DDD.Core.Domain; -using System.Threading.Tasks; +using System.Threading.Tasks; namespace DDD.Core.Application { + using Domain; + /// /// Defines a component that processes generic queries in a specific bounded context. /// diff --git a/Src/DDD.Core/Application/IContextualQueryProcessor`1.cs b/Src/DDD.Core/Application/IContextualQueryProcessor`1.cs index c009923..2c95061 100644 --- a/Src/DDD.Core/Application/IContextualQueryProcessor`1.cs +++ b/Src/DDD.Core/Application/IContextualQueryProcessor`1.cs @@ -1,7 +1,7 @@ -using DDD.Core.Domain; - -namespace DDD.Core.Application +namespace DDD.Core.Application { + using Domain; + /// /// Defines a component that processes generic queries in a specific bounded context. /// diff --git a/Src/DDD.Core/Application/IEventConsumer.cs b/Src/DDD.Core/Application/IEventConsumer.cs index d8c4611..11a5287 100644 --- a/Src/DDD.Core/Application/IEventConsumer.cs +++ b/Src/DDD.Core/Application/IEventConsumer.cs @@ -1,6 +1,4 @@ using System; -using System.Threading; -using System.Threading.Tasks; namespace DDD.Core.Application { diff --git a/Src/DDD.Core/Application/IEventConsumer`1.cs b/Src/DDD.Core/Application/IEventConsumer`1.cs index 7b3b9d9..efe8c57 100644 --- a/Src/DDD.Core/Application/IEventConsumer`1.cs +++ b/Src/DDD.Core/Application/IEventConsumer`1.cs @@ -1,7 +1,7 @@ -using DDD.Core.Domain; - -namespace DDD.Core.Application +namespace DDD.Core.Application { + using Domain; + public interface IEventConsumer : IEventConsumer where TContext : BoundedContext { diff --git a/Src/DDD.Core/Application/IMessageContextExtensions.cs b/Src/DDD.Core/Application/IMessageContextExtensions.cs index 7f5f0de..c5645bf 100644 --- a/Src/DDD.Core/Application/IMessageContextExtensions.cs +++ b/Src/DDD.Core/Application/IMessageContextExtensions.cs @@ -1,10 +1,8 @@ using EnsureThat; -using System; using System.Threading; namespace DDD.Core.Application { - using Domain; using Collections; public static class IMessageContextExtensions diff --git a/Src/DDD.Core/Application/IQueryHandler`1.cs b/Src/DDD.Core/Application/IQueryHandler`1.cs index 385e235..ca18295 100644 --- a/Src/DDD.Core/Application/IQueryHandler`1.cs +++ b/Src/DDD.Core/Application/IQueryHandler`1.cs @@ -1,7 +1,7 @@ -using DDD.Core.Domain; - -namespace DDD.Core.Application +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. /// diff --git a/Src/DDD.Core/Application/IRecurringCommandManager`1.cs b/Src/DDD.Core/Application/IRecurringCommandManager`1.cs index cb974cc..4691021 100644 --- a/Src/DDD.Core/Application/IRecurringCommandManager`1.cs +++ b/Src/DDD.Core/Application/IRecurringCommandManager`1.cs @@ -1,7 +1,7 @@ -using DDD.Core.Domain; - -namespace DDD.Core.Application +namespace DDD.Core.Application { + using Domain; + /// /// Represents a component that registers, schedules and processes recurring commands in a specific bounded context. /// diff --git a/Src/DDD.Core/Application/ISyncCommandHandlerExtensions.cs b/Src/DDD.Core/Application/ISyncCommandHandlerExtensions.cs index f32ff7c..55b695a 100644 --- a/Src/DDD.Core/Application/ISyncCommandHandlerExtensions.cs +++ b/Src/DDD.Core/Application/ISyncCommandHandlerExtensions.cs @@ -2,8 +2,6 @@ namespace DDD.Core.Application { - using Collections; - public static class ISyncCommandHandlerExtensions { diff --git a/Src/DDD.Core/Application/ISyncCommandHandler`1.cs b/Src/DDD.Core/Application/ISyncCommandHandler`1.cs index 55d8769..3e58695 100644 --- a/Src/DDD.Core/Application/ISyncCommandHandler`1.cs +++ b/Src/DDD.Core/Application/ISyncCommandHandler`1.cs @@ -1,7 +1,7 @@ -using DDD.Core.Domain; - -namespace DDD.Core.Application +namespace DDD.Core.Application { + using Domain; + /// /// Defines a method that handles synchronously a command of a specified type in a specific bounded context. /// diff --git a/Src/DDD.Core/Application/ISyncQueryHandler`1.cs b/Src/DDD.Core/Application/ISyncQueryHandler`1.cs index 3a62b4c..b2c9146 100644 --- a/Src/DDD.Core/Application/ISyncQueryHandler`1.cs +++ b/Src/DDD.Core/Application/ISyncQueryHandler`1.cs @@ -1,7 +1,7 @@ -using DDD.Core.Domain; - -namespace DDD.Core.Application +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. /// diff --git a/Src/DDD.Core/Application/RecurringCommandManager.cs b/Src/DDD.Core/Application/RecurringCommandManager.cs index 91af71d..496477d 100644 --- a/Src/DDD.Core/Application/RecurringCommandManager.cs +++ b/Src/DDD.Core/Application/RecurringCommandManager.cs @@ -7,7 +7,7 @@ namespace DDD.Core.Application { - using DDD.Core.Domain; + using Domain; using DependencyInjection; using Serialization; using Threading; diff --git a/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs b/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs index 0761240..cb0473f 100644 --- a/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs +++ b/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs @@ -2,7 +2,7 @@ namespace DDD.Core.Application { - using DDD.Core.Domain; + using Domain; using Serialization; [DataContract()] diff --git a/Src/DDD.Core/Application/SyncCommandHandlerWithLogging`1.cs b/Src/DDD.Core/Application/SyncCommandHandlerWithLogging`1.cs index 8f63a46..7f2298b 100644 --- a/Src/DDD.Core/Application/SyncCommandHandlerWithLogging`1.cs +++ b/Src/DDD.Core/Application/SyncCommandHandlerWithLogging`1.cs @@ -1,10 +1,11 @@ using EnsureThat; -using DDD.Core.Domain; using Microsoft.Extensions.Logging; using System.Diagnostics; namespace DDD.Core.Application { + using Domain; + /// /// A decorator that logs information about commands. /// diff --git a/Src/DDD.Core/Application/SyncQueryHandlerWithLogging`1.cs b/Src/DDD.Core/Application/SyncQueryHandlerWithLogging`1.cs index 2dd2181..84b09c6 100644 --- a/Src/DDD.Core/Application/SyncQueryHandlerWithLogging`1.cs +++ b/Src/DDD.Core/Application/SyncQueryHandlerWithLogging`1.cs @@ -1,10 +1,11 @@ using EnsureThat; -using DDD.Core.Domain; using Microsoft.Extensions.Logging; using System.Diagnostics; namespace DDD.Core.Application { + using Domain; + /// /// A decorator that logs information about queries. /// diff --git a/Src/DDD.Core/Domain/DomainEntity.cs b/Src/DDD.Core/Domain/DomainEntity.cs index 75acf5d..99c5112 100644 --- a/Src/DDD.Core/Domain/DomainEntity.cs +++ b/Src/DDD.Core/Domain/DomainEntity.cs @@ -1,12 +1,9 @@ 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. /// diff --git a/Src/DDD.Core/Domain/IAsyncRepository.cs b/Src/DDD.Core/Domain/IAsyncRepository.cs index 3d44a3b..a34379c 100644 --- a/Src/DDD.Core/Domain/IAsyncRepository.cs +++ b/Src/DDD.Core/Domain/IAsyncRepository.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace DDD.Core.Domain diff --git a/Src/DDD.Core/Domain/ISyncRepository.cs b/Src/DDD.Core/Domain/ISyncRepository.cs index 0342db1..b5f1f1d 100644 --- a/Src/DDD.Core/Domain/ISyncRepository.cs +++ b/Src/DDD.Core/Domain/ISyncRepository.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace DDD.Core.Domain +namespace DDD.Core.Domain { /// /// Defines a component to find and save synchronously an aggregate. diff --git a/Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider.cs b/Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider.cs index b5f4809..db3cba5 100644 --- a/Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider.cs +++ b/Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider.cs @@ -3,8 +3,6 @@ namespace DDD.Core.Infrastructure.Data { - using Domain; - /// /// Provides and shares a database connection between different components. /// This component owns the connection and is responsible for disposing the connection. diff --git a/Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider`1.cs b/Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider`1.cs index 38fc8a4..457e489 100644 --- a/Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider`1.cs +++ b/Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider`1.cs @@ -1,7 +1,6 @@ namespace DDD.Core.Infrastructure.Data { - using Application; - using DDD.Core.Domain; + using Domain; /// /// Provides and shares a database connection for a specific context between different components. diff --git a/Src/DDD.Core/Infrastructure/Data/LazyDbConnectionProvider.cs b/Src/DDD.Core/Infrastructure/Data/LazyDbConnectionProvider.cs index bbf1f44..405900b 100644 --- a/Src/DDD.Core/Infrastructure/Data/LazyDbConnectionProvider.cs +++ b/Src/DDD.Core/Infrastructure/Data/LazyDbConnectionProvider.cs @@ -5,8 +5,7 @@ namespace DDD.Core.Infrastructure.Data { - using Application; - using DDD.Core.Domain; + using Domain; public class LazyDbConnectionProvider : IDbConnectionProvider where TContext : BoundedContext, new() diff --git a/Src/DDD.Core/Infrastructure/Data/SequentialSqlServerGuidGenerator.cs b/Src/DDD.Core/Infrastructure/Data/SequentialSqlServerGuidGenerator.cs index c507aab..91bbaca 100644 --- a/Src/DDD.Core/Infrastructure/Data/SequentialSqlServerGuidGenerator.cs +++ b/Src/DDD.Core/Infrastructure/Data/SequentialSqlServerGuidGenerator.cs @@ -1,6 +1,4 @@ -using System; - -namespace DDD.Core.Infrastructure.Data +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. diff --git a/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/PrescribedMedicationDescriptor.cs b/Src/DDD.HealthcareDelivery.Messages/Application/Prescriptions/PrescribedMedicationDescriptor.cs index a66194b..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. /// diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionRepository.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionRepository.cs index 738432e..b3f183d 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionRepository.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PharmaceuticalPrescriptionRepository.cs @@ -1,8 +1,4 @@ -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; diff --git a/Test/DDD.Common.UnitTests/Domain/EnumerationTests.cs b/Test/DDD.Common.UnitTests/Domain/EnumerationTests.cs index 9b34aef..530dc70 100644 --- a/Test/DDD.Common.UnitTests/Domain/EnumerationTests.cs +++ b/Test/DDD.Common.UnitTests/Domain/EnumerationTests.cs @@ -1,5 +1,4 @@ -using DDD.Common.Domain; -using FluentAssertions; +using FluentAssertions; using System; using System.Collections.Generic; using Xunit; 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.Core.UnitTests/Application/RecurringCommandManagerTests.cs b/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs index 7ed73b4..5267ab4 100644 --- a/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs +++ b/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs @@ -12,7 +12,7 @@ namespace DDD.Core.Application using DependencyInjection; using Serialization; using Infrastructure.Testing; - using DDD.Core.Domain; + using Domain; public class RecurringCommandManagerTests : IDisposable { diff --git a/Test/DDD.Core.UnitTests/Domain/FakeContext.cs b/Test/DDD.Core.UnitTests/Domain/FakeContext.cs index 909565f..0d4d3a2 100644 --- a/Test/DDD.Core.UnitTests/Domain/FakeContext.cs +++ b/Test/DDD.Core.UnitTests/Domain/FakeContext.cs @@ -1,7 +1,5 @@ namespace DDD.Core.Domain { - using Domain; - public class FakeContext : BoundedContext { #region Constructors diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj b/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj index 97f9679..b2dee0c 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj @@ -73,4 +73,9 @@ + + + PreserveNewest + + \ No newline at end of file diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Properties/AssemblyInfo.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Properties/AssemblyInfo.cs index 7cf896c..61ffb3e 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Properties/AssemblyInfo.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Properties/AssemblyInfo.cs @@ -1,6 +1,4 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; using Xunit; // Setting ComVisible to false makes the types in this assembly not visible 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; From 9dfef9ded045d9449fe75ef0a581a6d2a339ff10 Mon Sep 17 00:00:00 2001 From: draphyz Date: Fri, 3 Feb 2023 13:45:34 +0100 Subject: [PATCH 068/111] Refactoring --- .../ICommandProcessorExtensions.cs | 2 +- .../Application/IQueryProcessorExtensions.cs | 2 +- .../DDD.Core.Dapper.IntegrationTests.csproj | 15 +++++------ .../{ => Domain}/TestContext.cs | 4 +-- .../Data}/EventStreamPositionUpdaterTests.cs | 1 + .../Data}/EventStreamReaderTests.cs | 15 ++++++----- .../Data}/EventStreamSubscriberTests.cs | 19 +++++++------- .../Data}/EventStreamsFinderTests.cs | 15 ++++++----- .../Data}/FailedEventStreamExcluderTests.cs | 19 +++++++------- .../Data}/FailedEventStreamIncluderTests.cs | 15 ++++++----- .../Data}/FailedEventStreamReaderTests.cs | 8 +++--- .../Data}/FailedEventStreamUpdaterTests.cs | 17 +++++++------ .../Data}/FailedEventStreamsFinderTests.cs | 15 ++++++----- .../FailedRecurringCommandUpdaterTests.cs | 21 +++++++-------- .../Data}/IDbConnectionExtensionsTests.cs | 24 ++++++++++-------- .../Data}/IPersistenceFixture.cs | 1 + .../Data}/OracleCollection.cs | 0 .../OracleEventStreamPositionUpdaterTests.cs | 0 .../Data}/OracleEventStreamReaderTests.cs | 9 ++++--- .../Data}/OracleEventStreamSubscriberTests.cs | 0 .../Data}/OracleEventStreamsFinderTests.cs | 0 .../OracleFailedEventStreamExcluderTests.cs | 0 .../OracleFailedEventStreamIncluderTests.cs | 0 .../OracleFailedEventStreamReaderTests.cs | 9 ++++--- .../OracleFailedEventStreamUpdaterTests.cs | 0 .../OracleFailedEventStreamsFinderTests.cs | 0 ...racleFailedRecurringCommandUpdaterTests.cs | 0 .../Data}/OracleFixture.cs | 14 +++++----- .../OracleIDbConnectionExtensionsTests.cs | 0 .../OracleRecurringCommandRegisterTests.cs | 0 .../OracleRecurringCommandsFinderTests.cs | 0 .../Data}/OracleScripts.Designer.cs | 0 .../Data}/OracleScripts.resx | 0 ...eSuccessfulRecurringCommandUpdaterTests.cs | 0 .../Data}/RecurringCommandRegisterTests.cs | 17 +++++++------ .../Data}/RecurringCommandsFinderTests.cs | 15 ++++++----- .../Data}/Scripts/Oracle/CreateSchema.sql | 0 .../Oracle/ExcludeFailedEventStream.sql | Bin .../Data}/Scripts/Oracle/FillSchema.sql | 0 .../Data}/Scripts/Oracle/FindEventStreams.sql | Bin .../Scripts/Oracle/FindFailedEventStreams.sql | Bin .../Scripts/Oracle/FindRecurringCommands.sql | Bin .../Oracle/IncludeFailedEventStream.sql | Bin .../Oracle/MarkRecurringCommandAsFailed.sql | Bin .../MarkRecurringCommandAsSuccessful.sql | Bin .../Scripts/Oracle/NextId_ExistingRows.sql | Bin .../Data}/Scripts/Oracle/NextId_NoRow.sql | Bin .../Data}/Scripts/Oracle/ReadEventStream.sql | Bin .../Scripts/Oracle/ReadFailedEventStream.sql | Bin .../Oracle/RegisterRecurringCommand.sql | Bin .../Scripts/Oracle/SubscribeToEventStream.sql | Bin .../Oracle/UpdateEventStreamPosition.sql | Bin .../Oracle/UpdateFailedEventStream.sql | Bin .../Scripts/SqlServer/CreateDatabase.sql | Bin .../SqlServer/ExcludeFailedEventStream.sql | Bin .../Scripts/SqlServer/FindEventStreams.sql | Bin .../SqlServer/FindFailedEventStreams.sql | Bin .../SqlServer/FindRecurringCommands.sql | Bin .../SqlServer/IncludeFailedEventStream.sql | Bin .../MarkRecurringCommandAsFailed.sql | Bin .../MarkRecurringCommandAsSuccessful.sql | Bin .../Scripts/SqlServer/NextId_ExistingRows.sql | Bin .../Data}/Scripts/SqlServer/NextId_NoRow.sql | Bin .../Scripts/SqlServer/ReadEventStream.sql | Bin .../SqlServer/ReadFailedEventStream.sql | Bin .../SqlServer/RegisterRecurringCommand.sql | Bin .../SqlServer/SubscribeToEventStream.sql | Bin .../SqlServer/UpdateEventStreamPosition.sql | Bin .../SqlServer/UpdateFailedEventStream.sql | Bin .../Data}/SqlServerCollection.cs | 0 ...qlServerEventStreamPositionUpdaterTests.cs | 0 .../Data}/SqlServerEventStreamReaderTests.cs | 9 ++++--- .../SqlServerEventStreamSubscriberTests.cs | 0 .../Data}/SqlServerEventStreamsFinderTests.cs | 0 ...SqlServerFailedEventStreamExcluderTests.cs | 0 ...SqlServerFailedEventStreamIncluderTests.cs | 0 .../SqlServerFailedEventStreamReaderTests.cs | 11 ++++---- .../SqlServerFailedEventStreamUpdaterTests.cs | 0 .../SqlServerFailedEventStreamsFinderTests.cs | 0 ...erverFailedRecurringCommandUpdaterTests.cs | 0 .../Data}/SqlServerFixture.cs | 12 +++++---- .../SqlServerIDbConnectionExtensionsTests.cs | 0 .../SqlServerRecurringCommandRegisterTests.cs | 0 .../SqlServerRecurringCommandsFinderTests.cs | 0 .../Data}/SqlServerScripts.Designer.cs | 0 .../Data}/SqlServerScripts.resx | 0 ...rSuccessfulRecurringCommandUpdaterTests.cs | 0 .../SuccessfulRecurringCommandUpdaterTests.cs | 21 +++++++-------- .../FakeSourceContext.cs | 5 +--- 89 files changed, 167 insertions(+), 148 deletions(-) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Domain}/TestContext.cs (76%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/EventStreamPositionUpdaterTests.cs (99%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/EventStreamReaderTests.cs (94%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/EventStreamSubscriberTests.cs (78%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/EventStreamsFinderTests.cs (84%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/FailedEventStreamExcluderTests.cs (84%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/FailedEventStreamIncluderTests.cs (82%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/FailedEventStreamReaderTests.cs (79%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/FailedEventStreamUpdaterTests.cs (87%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/FailedEventStreamsFinderTests.cs (87%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/FailedRecurringCommandUpdaterTests.cs (86%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/IDbConnectionExtensionsTests.cs (69%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/IPersistenceFixture.cs (95%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/OracleCollection.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/OracleEventStreamPositionUpdaterTests.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/OracleEventStreamReaderTests.cs (88%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/OracleEventStreamSubscriberTests.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/OracleEventStreamsFinderTests.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/OracleFailedEventStreamExcluderTests.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/OracleFailedEventStreamIncluderTests.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/OracleFailedEventStreamReaderTests.cs (95%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/OracleFailedEventStreamUpdaterTests.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/OracleFailedEventStreamsFinderTests.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/OracleFailedRecurringCommandUpdaterTests.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/OracleFixture.cs (86%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/OracleIDbConnectionExtensionsTests.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/OracleRecurringCommandRegisterTests.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/OracleRecurringCommandsFinderTests.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/OracleScripts.Designer.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/OracleScripts.resx (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/OracleSuccessfulRecurringCommandUpdaterTests.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/RecurringCommandRegisterTests.cs (86%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/RecurringCommandsFinderTests.cs (83%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/Oracle/CreateSchema.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/Oracle/ExcludeFailedEventStream.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/Oracle/FillSchema.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/Oracle/FindEventStreams.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/Oracle/FindFailedEventStreams.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/Oracle/FindRecurringCommands.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/Oracle/IncludeFailedEventStream.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/Oracle/MarkRecurringCommandAsFailed.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/Oracle/MarkRecurringCommandAsSuccessful.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/Oracle/NextId_ExistingRows.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/Oracle/NextId_NoRow.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/Oracle/ReadEventStream.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/Oracle/ReadFailedEventStream.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/Oracle/RegisterRecurringCommand.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/Oracle/SubscribeToEventStream.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/Oracle/UpdateEventStreamPosition.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/Oracle/UpdateFailedEventStream.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/SqlServer/CreateDatabase.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/SqlServer/ExcludeFailedEventStream.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/SqlServer/FindEventStreams.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/SqlServer/FindFailedEventStreams.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/SqlServer/FindRecurringCommands.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/SqlServer/IncludeFailedEventStream.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/SqlServer/MarkRecurringCommandAsFailed.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/SqlServer/MarkRecurringCommandAsSuccessful.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/SqlServer/NextId_ExistingRows.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/SqlServer/NextId_NoRow.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/SqlServer/ReadEventStream.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/SqlServer/ReadFailedEventStream.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/SqlServer/RegisterRecurringCommand.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/SqlServer/SubscribeToEventStream.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/SqlServer/UpdateEventStreamPosition.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/Scripts/SqlServer/UpdateFailedEventStream.sql (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/SqlServerCollection.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/SqlServerEventStreamPositionUpdaterTests.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/SqlServerEventStreamReaderTests.cs (88%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/SqlServerEventStreamSubscriberTests.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/SqlServerEventStreamsFinderTests.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/SqlServerFailedEventStreamExcluderTests.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/SqlServerFailedEventStreamIncluderTests.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/SqlServerFailedEventStreamReaderTests.cs (94%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/SqlServerFailedEventStreamUpdaterTests.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/SqlServerFailedEventStreamsFinderTests.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/SqlServerFailedRecurringCommandUpdaterTests.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/SqlServerFixture.cs (88%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/SqlServerIDbConnectionExtensionsTests.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/SqlServerRecurringCommandRegisterTests.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/SqlServerRecurringCommandsFinderTests.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/SqlServerScripts.Designer.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/SqlServerScripts.resx (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/SqlServerSuccessfulRecurringCommandUpdaterTests.cs (100%) rename Test/DDD.Core.Dapper.IntegrationTests/{ => Infrastructure/Data}/SuccessfulRecurringCommandUpdaterTests.cs (86%) rename Test/DDD.Core.UnitTests/{Application => Domain}/FakeSourceContext.cs (78%) diff --git a/Src/DDD.Core/Application/ICommandProcessorExtensions.cs b/Src/DDD.Core/Application/ICommandProcessorExtensions.cs index 66f030c..77624fe 100644 --- a/Src/DDD.Core/Application/ICommandProcessorExtensions.cs +++ b/Src/DDD.Core/Application/ICommandProcessorExtensions.cs @@ -15,7 +15,7 @@ public static class ICommandProcessorExtensions public static IContextualCommandProcessor In(this ICommandProcessor processor) where TContext : BoundedContext, new() { Ensure.That(processor, nameof(processor)).IsNotNull(); - return processor.In(new TContext()); + return processor.In(new TContext()); } public static void Process(this ICommandProcessor processor, diff --git a/Src/DDD.Core/Application/IQueryProcessorExtensions.cs b/Src/DDD.Core/Application/IQueryProcessorExtensions.cs index 38aa252..94b1f79 100644 --- a/Src/DDD.Core/Application/IQueryProcessorExtensions.cs +++ b/Src/DDD.Core/Application/IQueryProcessorExtensions.cs @@ -13,7 +13,7 @@ public static class IQueryProcessorExtensions public static IContextualQueryProcessor In(this IQueryProcessor processor) where TContext : BoundedContext, new() { Ensure.That(processor, nameof(processor)).IsNotNull(); - return processor.In(new TContext()); + return processor.In(new TContext()); } public static TResult Process(this IQueryProcessor processor, diff --git a/Test/DDD.Core.Dapper.IntegrationTests/DDD.Core.Dapper.IntegrationTests.csproj b/Test/DDD.Core.Dapper.IntegrationTests/DDD.Core.Dapper.IntegrationTests.csproj index 653f85b..79ba4f1 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/DDD.Core.Dapper.IntegrationTests.csproj +++ b/Test/DDD.Core.Dapper.IntegrationTests/DDD.Core.Dapper.IntegrationTests.csproj @@ -2,7 +2,7 @@ net48;net6.0 Library - DDD.Core.Infrastructure.Data + DDD.Core @@ -10,10 +10,9 @@ - - 2.4.5 - runtime; build; native; contentfiles; analyzers; buildtransitive + all + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -26,23 +25,23 @@ - + True True OracleScripts.resx - + True True SqlServerScripts.resx - + ResXFileCodeGenerator OracleScripts.Designer.cs - + ResXFileCodeGenerator SqlServerScripts.Designer.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/TestContext.cs b/Test/DDD.Core.Dapper.IntegrationTests/Domain/TestContext.cs similarity index 76% rename from Test/DDD.Core.Dapper.IntegrationTests/TestContext.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Domain/TestContext.cs index a989ebe..f0e5443 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/TestContext.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Domain/TestContext.cs @@ -1,7 +1,5 @@ -namespace DDD.Core.Infrastructure.Data +namespace DDD.Core.Domain { - using Domain; - public class TestContext : BoundedContext { #region Constructors diff --git a/Test/DDD.Core.Dapper.IntegrationTests/EventStreamPositionUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/EventStreamPositionUpdaterTests.cs similarity index 99% rename from Test/DDD.Core.Dapper.IntegrationTests/EventStreamPositionUpdaterTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/EventStreamPositionUpdaterTests.cs index d28ea1d..cdc76db 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/EventStreamPositionUpdaterTests.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/EventStreamPositionUpdaterTests.cs @@ -6,6 +6,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; + using Domain; public abstract class EventStreamPositionUpdaterTests : IDisposable where TFixture : IPersistenceFixture diff --git a/Test/DDD.Core.Dapper.IntegrationTests/EventStreamReaderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/EventStreamReaderTests.cs similarity index 94% rename from Test/DDD.Core.Dapper.IntegrationTests/EventStreamReaderTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/EventStreamReaderTests.cs index 240fb3f..0de320a 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/EventStreamReaderTests.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/EventStreamReaderTests.cs @@ -7,6 +7,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; + using Domain; public abstract class EventStreamReaderTests : IDisposable where TFixture : IPersistenceFixture @@ -16,8 +17,8 @@ public abstract class EventStreamReaderTests : IDisposable protected EventStreamReaderTests(TFixture fixture) { - this.Fixture = fixture; - this.ConnectionProvider = fixture.CreateConnectionProvider(); + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); } #endregion Constructors @@ -163,8 +164,8 @@ public static IEnumerable QueriesAndResults_1() public void Handle_WhenCalled_ReturnsExpectedResults_1(ReadEventStream query, IEnumerable expectedResults) { // Arrange - this.Fixture.ExecuteScriptFromResources("ReadEventStream"); - var handler = new EventStreamReader(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("ReadEventStream"); + var handler = new EventStreamReader(ConnectionProvider); // Act var results = handler.Handle(query); // Assert @@ -177,8 +178,8 @@ public void Handle_WhenCalled_ReturnsExpectedResults_1(ReadEventStream query, IE public async Task HandleAsync_WhenCalled_ReturnsExpectedResults_1(ReadEventStream query, IEnumerable expectedResults) { // Arrange - this.Fixture.ExecuteScriptFromResources("ReadEventStream"); - var handler = new EventStreamReader(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("ReadEventStream"); + var handler = new EventStreamReader(ConnectionProvider); // Act var results = await handler.HandleAsync(query); // Assert @@ -188,7 +189,7 @@ public async Task HandleAsync_WhenCalled_ReturnsExpectedResults_1(ReadEventStrea public void Dispose() { - this.ConnectionProvider.Dispose(); + ConnectionProvider.Dispose(); } #endregion Methods diff --git a/Test/DDD.Core.Dapper.IntegrationTests/EventStreamSubscriberTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/EventStreamSubscriberTests.cs similarity index 78% rename from Test/DDD.Core.Dapper.IntegrationTests/EventStreamSubscriberTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/EventStreamSubscriberTests.cs index 62baa29..3a69772 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/EventStreamSubscriberTests.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/EventStreamSubscriberTests.cs @@ -6,6 +6,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; + using Domain; public abstract class EventStreamSubscriberTests : IDisposable where TFixture : IPersistenceFixture @@ -15,8 +16,8 @@ public abstract class EventStreamSubscriberTests : IDisposable protected EventStreamSubscriberTests(TFixture fixture) { - this.Fixture = fixture; - this.ConnectionProvider = fixture.CreateConnectionProvider(); + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); } #endregion Constructors @@ -35,8 +36,8 @@ protected EventStreamSubscriberTests(TFixture fixture) public void Handle_WhenCalled_DoesNotThrowException() { // Arrange - this.Fixture.ExecuteScriptFromResources("SubscribeToEventStream"); - var handler = new EventStreamSubscriber(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("SubscribeToEventStream"); + var handler = new EventStreamSubscriber(ConnectionProvider); var command = CreateCommand(); // Act Action handle = () => handler.Handle(command); @@ -48,8 +49,8 @@ public void Handle_WhenCalled_DoesNotThrowException() public async Task HandleAsync_WhenCalled_DoesNotThrowException() { // Arrange - this.Fixture.ExecuteScriptFromResources("SubscribeToEventStream"); - var handler = new EventStreamSubscriber(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("SubscribeToEventStream"); + var handler = new EventStreamSubscriber(ConnectionProvider); var command = CreateCommand(); // Act Func handle = async () => await handler.HandleAsync(command); @@ -59,7 +60,7 @@ public async Task HandleAsync_WhenCalled_DoesNotThrowException() public void Dispose() { - this.ConnectionProvider.Dispose(); + ConnectionProvider.Dispose(); } private static SubscribeToEventStream CreateCommand() @@ -68,9 +69,9 @@ private static SubscribeToEventStream CreateCommand() { Type = "Message", Source = "COL", - Position = Guid.Empty, + Position = Guid.Empty, RetryMax = 3, - RetryDelays = new [] { new IncrementalDelay { Delay = 60, Increment = 30 } }, + RetryDelays = new[] { new IncrementalDelay { Delay = 60, Increment = 30 } }, BlockSize = 100 }; } diff --git a/Test/DDD.Core.Dapper.IntegrationTests/EventStreamsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/EventStreamsFinderTests.cs similarity index 84% rename from Test/DDD.Core.Dapper.IntegrationTests/EventStreamsFinderTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/EventStreamsFinderTests.cs index df4a604..e6b9842 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/EventStreamsFinderTests.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/EventStreamsFinderTests.cs @@ -7,6 +7,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; + using Domain; public abstract class EventStreamsFinderTests : IDisposable where TFixture : IPersistenceFixture @@ -16,8 +17,8 @@ public abstract class EventStreamsFinderTests : IDisposable protected EventStreamsFinderTests(TFixture fixture) { - this.Fixture = fixture; - this.ConnectionProvider = fixture.CreateConnectionProvider(); + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); } #endregion Constructors @@ -38,8 +39,8 @@ public void Handle_WhenCalled_ReturnsExpectedResults() // Arrange var query = new FindEventStreams(); var expectedResults = ExpectedResults(); - this.Fixture.ExecuteScriptFromResources("FindEventStreams"); - var handler = new EventStreamsFinder(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("FindEventStreams"); + var handler = new EventStreamsFinder(ConnectionProvider); // Act var results = handler.Handle(query); // Assert @@ -52,8 +53,8 @@ public async Task HandleAsync_WhenCalled_ReturnsExpectedResults() // Arrange var query = new FindEventStreams(); var expectedResults = ExpectedResults(); - this.Fixture.ExecuteScriptFromResources("FindEventStreams"); - var handler = new EventStreamsFinder(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("FindEventStreams"); + var handler = new EventStreamsFinder(ConnectionProvider); // Act var results = await handler.HandleAsync(query); // Assert @@ -62,7 +63,7 @@ public async Task HandleAsync_WhenCalled_ReturnsExpectedResults() public void Dispose() { - this.ConnectionProvider.Dispose(); + ConnectionProvider.Dispose(); } private static IEnumerable ExpectedResults() diff --git a/Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamExcluderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamExcluderTests.cs similarity index 84% rename from Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamExcluderTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamExcluderTests.cs index d740baa..a9407ad 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamExcluderTests.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamExcluderTests.cs @@ -6,6 +6,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; + using Domain; public abstract class FailedEventStreamExcluderTests : IDisposable where TFixture : IPersistenceFixture @@ -15,8 +16,8 @@ public abstract class FailedEventStreamExcluderTests : IDisposable protected FailedEventStreamExcluderTests(TFixture fixture) { - this.Fixture = fixture; - this.ConnectionProvider = fixture.CreateConnectionProvider(); + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); } #endregion Constructors @@ -35,8 +36,8 @@ protected FailedEventStreamExcluderTests(TFixture fixture) public void Handle_WhenCalled_DoesNotThrowException() { // Arrange - this.Fixture.ExecuteScriptFromResources("ExcludeFailedEventStream"); - var handler = new FailedEventStreamCreator(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("ExcludeFailedEventStream"); + var handler = new FailedEventStreamCreator(ConnectionProvider); var command = CreateCommand(); // Act Action handle = () => handler.Handle(command); @@ -48,8 +49,8 @@ public void Handle_WhenCalled_DoesNotThrowException() public async Task HandleAsync_WhenCalled_DoesNotThrowException() { // Arrange - this.Fixture.ExecuteScriptFromResources("ExcludeFailedEventStream"); - var handler = new FailedEventStreamCreator(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("ExcludeFailedEventStream"); + var handler = new FailedEventStreamCreator(ConnectionProvider); var command = CreateCommand(); // Act Func handle = async () => await handler.HandleAsync(command); @@ -59,7 +60,7 @@ public async Task HandleAsync_WhenCalled_DoesNotThrowException() public void Dispose() { - this.ConnectionProvider.Dispose(); + ConnectionProvider.Dispose(); } private static ExcludeFailedEventStream CreateCommand() @@ -68,7 +69,7 @@ private static ExcludeFailedEventStream CreateCommand() { StreamId = "2", StreamType = "MessageBox", - StreamSource = "COL", + 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", @@ -81,7 +82,7 @@ private static ExcludeFailedEventStream CreateCommand() BaseExceptionMessage = "Format not supported.", RetryCount = 0, RetryMax = 5, - RetryDelays = new [] + RetryDelays = new[] { new IncrementalDelay{ Delay = 10 }, new IncrementalDelay{ Delay = 60 }, diff --git a/Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamIncluderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamIncluderTests.cs similarity index 82% rename from Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamIncluderTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamIncluderTests.cs index e69d3fa..32e70d2 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamIncluderTests.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamIncluderTests.cs @@ -6,6 +6,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; + using Domain; public abstract class FailedEventStreamIncluderTests : IDisposable where TFixture : IPersistenceFixture @@ -15,8 +16,8 @@ public abstract class FailedEventStreamIncluderTests : IDisposable protected FailedEventStreamIncluderTests(TFixture fixture) { - this.Fixture = fixture; - this.ConnectionProvider = fixture.CreateConnectionProvider(); + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); } #endregion Constructors @@ -35,8 +36,8 @@ protected FailedEventStreamIncluderTests(TFixture fixture) public void Handle_WhenCalled_DoesNotThrowException() { // Arrange - this.Fixture.ExecuteScriptFromResources("IncludeFailedEventStream"); - var handler = new FailedEventStreamDeleter(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("IncludeFailedEventStream"); + var handler = new FailedEventStreamDeleter(ConnectionProvider); var command = CreateCommand(); // Act Action handle = () => handler.Handle(command); @@ -48,8 +49,8 @@ public void Handle_WhenCalled_DoesNotThrowException() public async Task HandleAsync_WhenCalled_DoesNotThrowException() { // Arrange - this.Fixture.ExecuteScriptFromResources("IncludeFailedEventStream"); - var handler = new FailedEventStreamDeleter(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("IncludeFailedEventStream"); + var handler = new FailedEventStreamDeleter(ConnectionProvider); var command = CreateCommand(); // Act Func handle = async () => await handler.HandleAsync(command); @@ -59,7 +60,7 @@ public async Task HandleAsync_WhenCalled_DoesNotThrowException() public void Dispose() { - this.ConnectionProvider.Dispose(); + ConnectionProvider.Dispose(); } private static IncludeFailedEventStream CreateCommand() diff --git a/Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamReaderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamReaderTests.cs similarity index 79% rename from Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamReaderTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamReaderTests.cs index 061d682..6c9b15b 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamReaderTests.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamReaderTests.cs @@ -2,6 +2,8 @@ namespace DDD.Core.Infrastructure.Data { + using Domain; + public abstract class EventsByStreamIdFinderTests : IDisposable where TFixture : IPersistenceFixture { @@ -10,8 +12,8 @@ public abstract class EventsByStreamIdFinderTests : IDisposable protected EventsByStreamIdFinderTests(TFixture fixture) { - this.Fixture = fixture; - this.ConnectionProvider = fixture.CreateConnectionProvider(); + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); } #endregion Constructors @@ -28,7 +30,7 @@ protected EventsByStreamIdFinderTests(TFixture fixture) public void Dispose() { - this.ConnectionProvider.Dispose(); + ConnectionProvider.Dispose(); } #endregion Methods diff --git a/Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamUpdaterTests.cs similarity index 87% rename from Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamUpdaterTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamUpdaterTests.cs index 996a812..cbd18dc 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamUpdaterTests.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamUpdaterTests.cs @@ -6,6 +6,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; + using Domain; public abstract class FailedEventStreamUpdaterTests : IDisposable where TFixture : IPersistenceFixture @@ -15,8 +16,8 @@ public abstract class FailedEventStreamUpdaterTests : IDisposable protected FailedEventStreamUpdaterTests(TFixture fixture) { - this.Fixture = fixture; - this.ConnectionProvider = fixture.CreateConnectionProvider(); + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); } #endregion Constructors @@ -35,8 +36,8 @@ protected FailedEventStreamUpdaterTests(TFixture fixture) public void Handle_WhenCalled_DoesNotThrowException() { // Arrange - this.Fixture.ExecuteScriptFromResources("UpdateFailedEventStream"); - var handler = new FailedEventStreamUpdater(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("UpdateFailedEventStream"); + var handler = new FailedEventStreamUpdater(ConnectionProvider); var command = CreateCommand(); // Act Action handle = () => handler.Handle(command); @@ -48,8 +49,8 @@ public void Handle_WhenCalled_DoesNotThrowException() public async Task HandleAsync_WhenCalled_DoesNotThrowException() { // Arrange - this.Fixture.ExecuteScriptFromResources("UpdateFailedEventStream"); - var handler = new FailedEventStreamUpdater(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("UpdateFailedEventStream"); + var handler = new FailedEventStreamUpdater(ConnectionProvider); var command = CreateCommand(); // Act Func handle = async () => await handler.HandleAsync(command); @@ -59,7 +60,7 @@ public async Task HandleAsync_WhenCalled_DoesNotThrowException() public void Dispose() { - this.ConnectionProvider.Dispose(); + ConnectionProvider.Dispose(); } private static UpdateFailedEventStream CreateCommand() @@ -81,7 +82,7 @@ private static UpdateFailedEventStream CreateCommand() BaseExceptionMessage = "The method or operation is not implemented.", RetryCount = 1, RetryMax = 5, - RetryDelays = new [] + RetryDelays = new[] { new IncrementalDelay{ Delay = 10 }, new IncrementalDelay{ Delay = 60 }, diff --git a/Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamsFinderTests.cs similarity index 87% rename from Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamsFinderTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamsFinderTests.cs index 5394d3b..060497b 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/FailedEventStreamsFinderTests.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamsFinderTests.cs @@ -7,6 +7,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; + using Domain; public abstract class FailedEventStreamsFinderTests : IDisposable where TFixture : IPersistenceFixture @@ -16,8 +17,8 @@ public abstract class FailedEventStreamsFinderTests : IDisposable protected FailedEventStreamsFinderTests(TFixture fixture) { - this.Fixture = fixture; - this.ConnectionProvider = fixture.CreateConnectionProvider(); + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); } #endregion Constructors @@ -38,8 +39,8 @@ public void Handle_WhenCalled_ReturnsExpectedResults() // Arrange var query = new FindFailedEventStreams(); var expectedResults = ExpectedResults(); - this.Fixture.ExecuteScriptFromResources("FindFailedEventStreams"); - var handler = new FailedEventStreamsFinder(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("FindFailedEventStreams"); + var handler = new FailedEventStreamsFinder(ConnectionProvider); // Act var results = handler.Handle(query); // Assert @@ -52,8 +53,8 @@ public async Task HandleAsync_WhenCalled_ReturnsExpectedResults() // Arrange var query = new FindFailedEventStreams(); var expectedResults = ExpectedResults(); - this.Fixture.ExecuteScriptFromResources("FindFailedEventStreams"); - var handler = new FailedEventStreamsFinder(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("FindFailedEventStreams"); + var handler = new FailedEventStreamsFinder(ConnectionProvider); // Act var results = await handler.HandleAsync(query); // Assert @@ -62,7 +63,7 @@ public async Task HandleAsync_WhenCalled_ReturnsExpectedResults() public void Dispose() { - this.ConnectionProvider.Dispose(); + ConnectionProvider.Dispose(); } private static IEnumerable ExpectedResults() diff --git a/Test/DDD.Core.Dapper.IntegrationTests/FailedRecurringCommandUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedRecurringCommandUpdaterTests.cs similarity index 86% rename from Test/DDD.Core.Dapper.IntegrationTests/FailedRecurringCommandUpdaterTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedRecurringCommandUpdaterTests.cs index 34d76d0..c031738 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/FailedRecurringCommandUpdaterTests.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedRecurringCommandUpdaterTests.cs @@ -2,12 +2,13 @@ using Dapper; using System; using System.Threading.Tasks; +using System.Data.Common; using Xunit; namespace DDD.Core.Infrastructure.Data { using Application; - using System.Data.Common; + using Domain; public abstract class FailedRecurringCommandUpdaterTests : IDisposable where TFixture : IPersistenceFixture @@ -17,8 +18,8 @@ public abstract class FailedRecurringCommandUpdaterTests : IDisposable protected FailedRecurringCommandUpdaterTests(TFixture fixture) { - this.Fixture = fixture; - this.ConnectionProvider = fixture.CreateConnectionProvider(); + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); } #endregion Constructors @@ -37,8 +38,8 @@ protected FailedRecurringCommandUpdaterTests(TFixture fixture) public void Handle_WhenCalled_UpdatesCommand() { // Arrange - this.Fixture.ExecuteScriptFromResources("MarkRecurringCommandAsFailed"); - var handler = new FailedRecurringCommandUpdater(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("MarkRecurringCommandAsFailed"); + var handler = new FailedRecurringCommandUpdater(ConnectionProvider); var command = CreateCommand(); var expectedCommand = ExpectedCommand(); // Act @@ -51,8 +52,8 @@ public void Handle_WhenCalled_UpdatesCommand() public async Task HandleAsync_WhenCalled_UpdatesCommand() { // Arrange - this.Fixture.ExecuteScriptFromResources("MarkRecurringCommandAsFailed"); - var handler = new FailedRecurringCommandUpdater(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("MarkRecurringCommandAsFailed"); + var handler = new FailedRecurringCommandUpdater(ConnectionProvider); var command = CreateCommand(); var expectedCommand = ExpectedCommand(); // Act @@ -64,7 +65,7 @@ public async Task HandleAsync_WhenCalled_UpdatesCommand() public void Dispose() { - this.ConnectionProvider.Dispose(); + ConnectionProvider.Dispose(); } private static MarkRecurringCommandAsFailed CreateCommand() @@ -79,7 +80,7 @@ private static MarkRecurringCommandAsFailed CreateCommand() private RecurringCommandDetail UpdatedCommand() { - using (var connection = this.Fixture.CreateConnection()) + using (var connection = Fixture.CreateConnection()) { connection.Open(); var sql = "SELECT CommandId, CommandType, Body, BodyFormat, RecurringExpression, LastExecutionTime, CASE LastExecutionStatus WHEN 'F' THEN 'Failed' WHEN 'S' THEN 'Successful' END LastExecutionStatus, LastExceptionInfo FROM Command WHERE CommandId = @CommandId"; @@ -104,7 +105,7 @@ private static RecurringCommandDetail ExpectedCommand() CommandType = "DDD.Core.Application.FakeCommand1, DDD.Core.Messages", Body = "{\"Property1\":\"dummy\",\"Property2\":10}", BodyFormat = "JSON", - RecurringExpression = "* * * * *", + RecurringExpression = "* * * * *", LastExecutionTime = new DateTime(2022, 1, 1), LastExecutionStatus = CommandExecutionStatus.Failed, LastExceptionInfo = "System.TimeoutException: The operation has timed-out." diff --git a/Test/DDD.Core.Dapper.IntegrationTests/IDbConnectionExtensionsTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/IDbConnectionExtensionsTests.cs similarity index 69% rename from Test/DDD.Core.Dapper.IntegrationTests/IDbConnectionExtensionsTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/IDbConnectionExtensionsTests.cs index ef3352b..b6e158d 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/IDbConnectionExtensionsTests.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/IDbConnectionExtensionsTests.cs @@ -5,6 +5,8 @@ namespace DDD.Core.Infrastructure.Data { + using Domain; + public abstract class IDbConnectionExtensionsTests : IDisposable where TFixture : IPersistenceFixture { @@ -13,8 +15,8 @@ public abstract class IDbConnectionExtensionsTests : IDisposable protected IDbConnectionExtensionsTests(TFixture fixture) { - this.Fixture = fixture; - this.ConnectionProvider = fixture.CreateConnectionProvider(); + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); } #endregion Constructors @@ -31,15 +33,15 @@ protected IDbConnectionExtensionsTests(TFixture fixture) public void Dispose() { - this.ConnectionProvider.Dispose(); + ConnectionProvider.Dispose(); } [Fact] public void NextId_WhenExistingRows_ReturnsExpectedId() { // Arrange - this.Fixture.ExecuteScriptFromResources("NextId_ExistingRows"); - var connection = this.ConnectionProvider.GetOpenConnection(); + Fixture.ExecuteScriptFromResources("NextId_ExistingRows"); + var connection = ConnectionProvider.GetOpenConnection(); // Act var id = connection.NextId("TableWithId", "Id"); // Assert @@ -50,8 +52,8 @@ public void NextId_WhenExistingRows_ReturnsExpectedId() public void NextId_WhenNoRow_ReturnsExpectedId() { // Arrange - this.Fixture.ExecuteScriptFromResources("NextId_NoRow"); - var connection = this.ConnectionProvider.GetOpenConnection(); + Fixture.ExecuteScriptFromResources("NextId_NoRow"); + var connection = ConnectionProvider.GetOpenConnection(); // Act var id = connection.NextId("TableWithId", "Id"); // Assert @@ -62,8 +64,8 @@ public void NextId_WhenNoRow_ReturnsExpectedId() public async Task NextIdAsync_WhenExistingRows_ReturnsExpectedId() { // Arrange - this.Fixture.ExecuteScriptFromResources("NextId_ExistingRows"); - var connection = await this.ConnectionProvider.GetOpenConnectionAsync(); + Fixture.ExecuteScriptFromResources("NextId_ExistingRows"); + var connection = await ConnectionProvider.GetOpenConnectionAsync(); // Act var id = await connection.NextIdAsync("TableWithId", "Id"); // Assert @@ -74,8 +76,8 @@ public async Task NextIdAsync_WhenExistingRows_ReturnsExpectedId() public async Task NextIdAsync_WhenNoRow_ReturnsExpectedId() { // Arrange - this.Fixture.ExecuteScriptFromResources("NextId_NoRow"); - var connection = await this.ConnectionProvider.GetOpenConnectionAsync(); + Fixture.ExecuteScriptFromResources("NextId_NoRow"); + var connection = await ConnectionProvider.GetOpenConnectionAsync(); // Act var id = await connection.NextIdAsync("TableWithId", "Id"); // Assert diff --git a/Test/DDD.Core.Dapper.IntegrationTests/IPersistenceFixture.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/IPersistenceFixture.cs similarity index 95% rename from Test/DDD.Core.Dapper.IntegrationTests/IPersistenceFixture.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/IPersistenceFixture.cs index ff05683..ff28d8b 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/IPersistenceFixture.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/IPersistenceFixture.cs @@ -3,6 +3,7 @@ using Core.Application; using Core.Domain; using Core.Infrastructure.Testing; + using Domain; using Mapping; public interface IPersistenceFixture : IDbFixture diff --git a/Test/DDD.Core.Dapper.IntegrationTests/OracleCollection.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleCollection.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/OracleCollection.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleCollection.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/OracleEventStreamPositionUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleEventStreamPositionUpdaterTests.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/OracleEventStreamPositionUpdaterTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleEventStreamPositionUpdaterTests.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/OracleEventStreamReaderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleEventStreamReaderTests.cs similarity index 88% rename from Test/DDD.Core.Dapper.IntegrationTests/OracleEventStreamReaderTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleEventStreamReaderTests.cs index a36d22a..40033a5 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/OracleEventStreamReaderTests.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleEventStreamReaderTests.cs @@ -7,6 +7,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; + using Domain; [Collection("Oracle")] public class OracleEventStreamReaderTests : EventStreamReaderTests @@ -53,8 +54,8 @@ public static IEnumerable QueriesAndResults_2() public void Handle_WhenCalled_ReturnsExpectedResults_2(ReadEventStream query, IEnumerable expectedResults) { // Arrange - this.Fixture.ExecuteScriptFromResources("ReadEventStream"); - var handler = new EventStreamReader(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("ReadEventStream"); + var handler = new EventStreamReader(ConnectionProvider); // Act var results = handler.Handle(query); // Assert @@ -66,8 +67,8 @@ public void Handle_WhenCalled_ReturnsExpectedResults_2(ReadEventStream query, IE public async Task HandleAsync_WhenCalled_ReturnsExpectedResults_2(ReadEventStream query, IEnumerable expectedResults) { // Arrange - this.Fixture.ExecuteScriptFromResources("ReadEventStream"); - var handler = new EventStreamReader(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("ReadEventStream"); + var handler = new EventStreamReader(ConnectionProvider); // Act var results = await handler.HandleAsync(query); // Assert diff --git a/Test/DDD.Core.Dapper.IntegrationTests/OracleEventStreamSubscriberTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleEventStreamSubscriberTests.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/OracleEventStreamSubscriberTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleEventStreamSubscriberTests.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/OracleEventStreamsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleEventStreamsFinderTests.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/OracleEventStreamsFinderTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleEventStreamsFinderTests.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/OracleFailedEventStreamExcluderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamExcluderTests.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/OracleFailedEventStreamExcluderTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamExcluderTests.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/OracleFailedEventStreamIncluderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamIncluderTests.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/OracleFailedEventStreamIncluderTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamIncluderTests.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/OracleFailedEventStreamReaderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamReaderTests.cs similarity index 95% rename from Test/DDD.Core.Dapper.IntegrationTests/OracleFailedEventStreamReaderTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamReaderTests.cs index 3f90098..7f22b3c 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/OracleFailedEventStreamReaderTests.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamReaderTests.cs @@ -7,6 +7,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; + using Domain; [Collection("Oracle")] public class OracleFailedEventStreamReaderTests : EventsByStreamIdFinderTests @@ -77,8 +78,8 @@ public static IEnumerable QueriesAndResults_2() public void Handle_WhenCalled_ReturnsExpectedResults_2(ReadFailedEventStream query, IEnumerable expectedResults) { // Arrange - this.Fixture.ExecuteScriptFromResources("ReadFailedEventStream"); - var handler = new FailedEventStreamReader(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("ReadFailedEventStream"); + var handler = new FailedEventStreamReader(ConnectionProvider); // Act var results = handler.Handle(query); // Assert @@ -90,8 +91,8 @@ public void Handle_WhenCalled_ReturnsExpectedResults_2(ReadFailedEventStream que public async Task HandleAsync_WhenCalled_ReturnsExpectedResults_2(ReadFailedEventStream query, IEnumerable expectedResults) { // Arrange - this.Fixture.ExecuteScriptFromResources("ReadFailedEventStream"); - var handler = new FailedEventStreamReader(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("ReadFailedEventStream"); + var handler = new FailedEventStreamReader(ConnectionProvider); // Act var results = await handler.HandleAsync(query); // Assert diff --git a/Test/DDD.Core.Dapper.IntegrationTests/OracleFailedEventStreamUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamUpdaterTests.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/OracleFailedEventStreamUpdaterTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamUpdaterTests.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/OracleFailedEventStreamsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamsFinderTests.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/OracleFailedEventStreamsFinderTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamsFinderTests.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/OracleFailedRecurringCommandUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedRecurringCommandUpdaterTests.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/OracleFailedRecurringCommandUpdaterTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedRecurringCommandUpdaterTests.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/OracleFixture.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFixture.cs similarity index 86% rename from Test/DDD.Core.Dapper.IntegrationTests/OracleFixture.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFixture.cs index 5693c65..899a0c1 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/OracleFixture.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFixture.cs @@ -1,14 +1,16 @@ 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 Core.Application; - using Core.Domain; - using Core.Infrastructure.Serialization; + using Application; + using Domain; + using Serialization; using Mapping; using Testing; @@ -35,14 +37,14 @@ public IObjectTranslator CreateEventTranslator() protected override void CreateDatabase() { - using (var connection = this.CreateConnection()) + using (var connection = CreateConnection()) { var builder = new OracleConnectionStringBuilder(connection.ConnectionString) { UserID = "SYS", DBAPrivilege = "SYSDBA" }; connection.ConnectionString = builder.ConnectionString; connection.Open(); - this.ExecuteScript(OracleScripts.CreateSchema, connection); + ExecuteScript(OracleScripts.CreateSchema, connection); } - this.ExecuteScript(OracleScripts.FillSchema); + ExecuteScript(OracleScripts.FillSchema); } protected override int[] ExecuteScript(string script, IDbConnection connection) diff --git a/Test/DDD.Core.Dapper.IntegrationTests/OracleIDbConnectionExtensionsTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleIDbConnectionExtensionsTests.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/OracleIDbConnectionExtensionsTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleIDbConnectionExtensionsTests.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/OracleRecurringCommandRegisterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleRecurringCommandRegisterTests.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/OracleRecurringCommandRegisterTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleRecurringCommandRegisterTests.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/OracleRecurringCommandsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleRecurringCommandsFinderTests.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/OracleRecurringCommandsFinderTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleRecurringCommandsFinderTests.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/OracleScripts.Designer.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleScripts.Designer.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/OracleScripts.Designer.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleScripts.Designer.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/OracleScripts.resx b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleScripts.resx similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/OracleScripts.resx rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleScripts.resx diff --git a/Test/DDD.Core.Dapper.IntegrationTests/OracleSuccessfulRecurringCommandUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleSuccessfulRecurringCommandUpdaterTests.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/OracleSuccessfulRecurringCommandUpdaterTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleSuccessfulRecurringCommandUpdaterTests.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/RecurringCommandRegisterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/RecurringCommandRegisterTests.cs similarity index 86% rename from Test/DDD.Core.Dapper.IntegrationTests/RecurringCommandRegisterTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/RecurringCommandRegisterTests.cs index 3fdc61f..14832b0 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/RecurringCommandRegisterTests.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/RecurringCommandRegisterTests.cs @@ -8,6 +8,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; + using Domain; public abstract class RecurringCommandRegisterTests : IDisposable where TFixture : IPersistenceFixture @@ -17,8 +18,8 @@ public abstract class RecurringCommandRegisterTests : IDisposable protected RecurringCommandRegisterTests(TFixture fixture) { - this.Fixture = fixture; - this.ConnectionProvider = fixture.CreateConnectionProvider(); + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); } #endregion Constructors @@ -37,8 +38,8 @@ protected RecurringCommandRegisterTests(TFixture fixture) public void Handle_WhenCalled_RegistersCommand() { // Arrange - this.Fixture.ExecuteScriptFromResources("RegisterRecurringCommand"); - var handler = new RecurringCommandRegister(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("RegisterRecurringCommand"); + var handler = new RecurringCommandRegister(ConnectionProvider); var command = CreateCommand(); var expectedCommands = ExpectedCommands(); // Act @@ -51,8 +52,8 @@ public void Handle_WhenCalled_RegistersCommand() public async Task HandleAsync_WhenCalled_RegistersCommand() { // Arrange - this.Fixture.ExecuteScriptFromResources("RegisterRecurringCommand"); - var handler = new RecurringCommandRegister(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("RegisterRecurringCommand"); + var handler = new RecurringCommandRegister(ConnectionProvider); var command = CreateCommand(); var expectedCommands = ExpectedCommands(); // Act @@ -63,7 +64,7 @@ public async Task HandleAsync_WhenCalled_RegistersCommand() public void Dispose() { - this.ConnectionProvider.Dispose(); + ConnectionProvider.Dispose(); } private static RegisterRecurringCommand CreateCommand() @@ -80,7 +81,7 @@ private static RegisterRecurringCommand CreateCommand() private IEnumerable RegistredCommands() { - using (var connection = this.Fixture.CreateConnection()) + using (var connection = Fixture.CreateConnection()) { connection.Open(); return connection.Query("SELECT CommandId, CommandType, Body, BodyFormat, RecurringExpression FROM Command"); diff --git a/Test/DDD.Core.Dapper.IntegrationTests/RecurringCommandsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/RecurringCommandsFinderTests.cs similarity index 83% rename from Test/DDD.Core.Dapper.IntegrationTests/RecurringCommandsFinderTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/RecurringCommandsFinderTests.cs index 406a07a..8e97021 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/RecurringCommandsFinderTests.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/RecurringCommandsFinderTests.cs @@ -7,6 +7,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; + using Domain; public abstract class RecurringCommandsFinderTests : IDisposable where TFixture : IPersistenceFixture @@ -16,8 +17,8 @@ public abstract class RecurringCommandsFinderTests : IDisposable protected RecurringCommandsFinderTests(TFixture fixture) { - this.Fixture = fixture; - this.ConnectionProvider = fixture.CreateConnectionProvider(); + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); } #endregion Constructors @@ -36,10 +37,10 @@ protected RecurringCommandsFinderTests(TFixture fixture) public void Handle_WhenCalled_ReturnsExpectedResults() { // Arrange - this.Fixture.ExecuteScriptFromResources("FindRecurringCommands"); + Fixture.ExecuteScriptFromResources("FindRecurringCommands"); var query = new FindRecurringCommands(); var expectedResults = ExpectedResults(); - var handler = new RecurringCommandsFinder(this.ConnectionProvider); + var handler = new RecurringCommandsFinder(ConnectionProvider); // Act var results = handler.Handle(query); // Assert @@ -50,10 +51,10 @@ public void Handle_WhenCalled_ReturnsExpectedResults() public async Task HandleAsync_WhenCalled_ReturnsExpectedResults() { // Arrange - this.Fixture.ExecuteScriptFromResources("FindRecurringCommands"); + Fixture.ExecuteScriptFromResources("FindRecurringCommands"); var query = new FindRecurringCommands(); var expectedResults = ExpectedResults(); - var handler = new RecurringCommandsFinder(this.ConnectionProvider); + var handler = new RecurringCommandsFinder(ConnectionProvider); // Act var results = await handler.HandleAsync(query); // Assert @@ -62,7 +63,7 @@ public async Task HandleAsync_WhenCalled_ReturnsExpectedResults() public void Dispose() { - this.ConnectionProvider.Dispose(); + ConnectionProvider.Dispose(); } protected abstract IEnumerable ExpectedResults(); diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/CreateSchema.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/CreateSchema.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/CreateSchema.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/CreateSchema.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/ExcludeFailedEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/ExcludeFailedEventStream.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/ExcludeFailedEventStream.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/ExcludeFailedEventStream.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/FillSchema.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/FillSchema.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/FillSchema.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/FillSchema.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/FindEventStreams.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/FindEventStreams.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/FindEventStreams.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/FindEventStreams.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/FindFailedEventStreams.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/FindFailedEventStreams.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/FindFailedEventStreams.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/FindFailedEventStreams.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/FindRecurringCommands.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/FindRecurringCommands.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/FindRecurringCommands.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/FindRecurringCommands.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/IncludeFailedEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/IncludeFailedEventStream.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/IncludeFailedEventStream.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/IncludeFailedEventStream.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/MarkRecurringCommandAsFailed.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/MarkRecurringCommandAsFailed.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/MarkRecurringCommandAsFailed.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/MarkRecurringCommandAsFailed.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/MarkRecurringCommandAsSuccessful.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/MarkRecurringCommandAsSuccessful.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/MarkRecurringCommandAsSuccessful.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/MarkRecurringCommandAsSuccessful.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/NextId_ExistingRows.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/NextId_ExistingRows.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/NextId_ExistingRows.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/NextId_ExistingRows.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/NextId_NoRow.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/NextId_NoRow.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/NextId_NoRow.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/NextId_NoRow.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/ReadEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/ReadEventStream.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/ReadEventStream.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/ReadEventStream.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/ReadFailedEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/ReadFailedEventStream.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/ReadFailedEventStream.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/ReadFailedEventStream.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/RegisterRecurringCommand.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/RegisterRecurringCommand.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/RegisterRecurringCommand.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/RegisterRecurringCommand.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/SubscribeToEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/SubscribeToEventStream.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/SubscribeToEventStream.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/SubscribeToEventStream.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/UpdateEventStreamPosition.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/UpdateEventStreamPosition.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/UpdateEventStreamPosition.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/UpdateEventStreamPosition.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/UpdateFailedEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/UpdateFailedEventStream.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/Oracle/UpdateFailedEventStream.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/UpdateFailedEventStream.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/CreateDatabase.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/CreateDatabase.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/CreateDatabase.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/ExcludeFailedEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/ExcludeFailedEventStream.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/ExcludeFailedEventStream.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/ExcludeFailedEventStream.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/FindEventStreams.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/FindEventStreams.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/FindEventStreams.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/FindEventStreams.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/FindFailedEventStreams.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/FindFailedEventStreams.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/FindFailedEventStreams.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/FindFailedEventStreams.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/FindRecurringCommands.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/FindRecurringCommands.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/FindRecurringCommands.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/FindRecurringCommands.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/IncludeFailedEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/IncludeFailedEventStream.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/IncludeFailedEventStream.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/IncludeFailedEventStream.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/MarkRecurringCommandAsFailed.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/MarkRecurringCommandAsFailed.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/MarkRecurringCommandAsFailed.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/MarkRecurringCommandAsFailed.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/MarkRecurringCommandAsSuccessful.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/MarkRecurringCommandAsSuccessful.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/MarkRecurringCommandAsSuccessful.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/MarkRecurringCommandAsSuccessful.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/NextId_ExistingRows.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/NextId_ExistingRows.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/NextId_ExistingRows.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/NextId_ExistingRows.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/NextId_NoRow.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/NextId_NoRow.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/NextId_NoRow.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/NextId_NoRow.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/ReadEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/ReadEventStream.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/ReadEventStream.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/ReadEventStream.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/ReadFailedEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/ReadFailedEventStream.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/ReadFailedEventStream.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/ReadFailedEventStream.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/RegisterRecurringCommand.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/RegisterRecurringCommand.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/RegisterRecurringCommand.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/RegisterRecurringCommand.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/SubscribeToEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/SubscribeToEventStream.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/SubscribeToEventStream.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/SubscribeToEventStream.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/UpdateEventStreamPosition.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/UpdateEventStreamPosition.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/UpdateEventStreamPosition.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/UpdateEventStreamPosition.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/UpdateFailedEventStream.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/UpdateFailedEventStream.sql similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/Scripts/SqlServer/UpdateFailedEventStream.sql rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/UpdateFailedEventStream.sql diff --git a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerCollection.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerCollection.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/SqlServerCollection.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerCollection.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerEventStreamPositionUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerEventStreamPositionUpdaterTests.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/SqlServerEventStreamPositionUpdaterTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerEventStreamPositionUpdaterTests.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerEventStreamReaderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerEventStreamReaderTests.cs similarity index 88% rename from Test/DDD.Core.Dapper.IntegrationTests/SqlServerEventStreamReaderTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerEventStreamReaderTests.cs index 1489948..7c4cc62 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerEventStreamReaderTests.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerEventStreamReaderTests.cs @@ -7,6 +7,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; + using Domain; [Collection("SqlServer")] public class SqlServerEventStreamReaderTests : EventStreamReaderTests @@ -53,8 +54,8 @@ public static IEnumerable QueriesAndResults_2() public void Handle_WhenCalled_ReturnsExpectedResults_2(ReadEventStream query, IEnumerable expectedResults) { // Arrange - this.Fixture.ExecuteScriptFromResources("ReadEventStream"); - var handler = new EventStreamReader(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("ReadEventStream"); + var handler = new EventStreamReader(ConnectionProvider); // Act var results = handler.Handle(query); // Assert @@ -66,8 +67,8 @@ public void Handle_WhenCalled_ReturnsExpectedResults_2(ReadEventStream query, IE public async Task HandleAsync_WhenCalled_ReturnsExpectedResults_2(ReadEventStream query, IEnumerable expectedResults) { // Arrange - this.Fixture.ExecuteScriptFromResources("ReadEventStream"); - var handler = new EventStreamReader(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("ReadEventStream"); + var handler = new EventStreamReader(ConnectionProvider); // Act var results = await handler.HandleAsync(query); // Assert diff --git a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerEventStreamSubscriberTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerEventStreamSubscriberTests.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/SqlServerEventStreamSubscriberTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerEventStreamSubscriberTests.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerEventStreamsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerEventStreamsFinderTests.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/SqlServerEventStreamsFinderTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerEventStreamsFinderTests.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedEventStreamExcluderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamExcluderTests.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedEventStreamExcluderTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamExcluderTests.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedEventStreamIncluderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamIncluderTests.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedEventStreamIncluderTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamIncluderTests.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedEventStreamReaderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamReaderTests.cs similarity index 94% rename from Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedEventStreamReaderTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamReaderTests.cs index decce7e..0e8ff9a 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedEventStreamReaderTests.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamReaderTests.cs @@ -7,6 +7,7 @@ namespace DDD.Core.Infrastructure.Data { using Application; + using Domain; [Collection("SqlServer")] public class SqlServerFailedEventStreamReaderTests : EventsByStreamIdFinderTests @@ -42,7 +43,7 @@ public static IEnumerable QueriesAndResults() 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", + StreamId = "5", StreamType = "Message", IssuedBy = "Dr Folamour" }, @@ -77,8 +78,8 @@ public static IEnumerable QueriesAndResults() public void Handle_WhenCalled_ReturnsExpectedResults(ReadFailedEventStream query, IEnumerable expectedResults) { // Arrange - this.Fixture.ExecuteScriptFromResources("ReadFailedEventStream"); - var handler = new FailedEventStreamReader(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("ReadFailedEventStream"); + var handler = new FailedEventStreamReader(ConnectionProvider); // Act var results = handler.Handle(query); // Assert @@ -90,8 +91,8 @@ public void Handle_WhenCalled_ReturnsExpectedResults(ReadFailedEventStream query public async Task HandleAsync_WhenCalled_ReturnsExpectedResults(ReadFailedEventStream query, IEnumerable expectedResults) { // Arrange - this.Fixture.ExecuteScriptFromResources("ReadFailedEventStream"); - var handler = new FailedEventStreamReader(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("ReadFailedEventStream"); + var handler = new FailedEventStreamReader(ConnectionProvider); // Act var results = await handler.HandleAsync(query); // Assert diff --git a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedEventStreamUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamUpdaterTests.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedEventStreamUpdaterTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamUpdaterTests.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedEventStreamsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamsFinderTests.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedEventStreamsFinderTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamsFinderTests.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedRecurringCommandUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedRecurringCommandUpdaterTests.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/SqlServerFailedRecurringCommandUpdaterTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedRecurringCommandUpdaterTests.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerFixture.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFixture.cs similarity index 88% rename from Test/DDD.Core.Dapper.IntegrationTests/SqlServerFixture.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFixture.cs index 8ad33a0..389311b 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerFixture.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFixture.cs @@ -1,14 +1,16 @@ 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 Core.Application; - using Core.Domain; - using Core.Infrastructure.Serialization; + using Application; + using Domain; + using Serialization; using Mapping; using Testing; @@ -34,12 +36,12 @@ public IObjectTranslator CreateEventTranslator() protected override void CreateDatabase() { - using (var connection = this.CreateConnection()) + using (var connection = CreateConnection()) { var builder = new SqlConnectionStringBuilder(connection.ConnectionString) { InitialCatalog = "master" }; connection.ConnectionString = builder.ConnectionString; connection.Open(); - this.ExecuteScript(SqlServerScripts.CreateDatabase, connection); + ExecuteScript(SqlServerScripts.CreateDatabase, connection); } } diff --git a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerIDbConnectionExtensionsTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerIDbConnectionExtensionsTests.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/SqlServerIDbConnectionExtensionsTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerIDbConnectionExtensionsTests.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerRecurringCommandRegisterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerRecurringCommandRegisterTests.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/SqlServerRecurringCommandRegisterTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerRecurringCommandRegisterTests.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerRecurringCommandsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerRecurringCommandsFinderTests.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/SqlServerRecurringCommandsFinderTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerRecurringCommandsFinderTests.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerScripts.Designer.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerScripts.Designer.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/SqlServerScripts.Designer.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerScripts.Designer.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerScripts.resx b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerScripts.resx similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/SqlServerScripts.resx rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerScripts.resx diff --git a/Test/DDD.Core.Dapper.IntegrationTests/SqlServerSuccessfulRecurringCommandUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerSuccessfulRecurringCommandUpdaterTests.cs similarity index 100% rename from Test/DDD.Core.Dapper.IntegrationTests/SqlServerSuccessfulRecurringCommandUpdaterTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerSuccessfulRecurringCommandUpdaterTests.cs diff --git a/Test/DDD.Core.Dapper.IntegrationTests/SuccessfulRecurringCommandUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SuccessfulRecurringCommandUpdaterTests.cs similarity index 86% rename from Test/DDD.Core.Dapper.IntegrationTests/SuccessfulRecurringCommandUpdaterTests.cs rename to Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SuccessfulRecurringCommandUpdaterTests.cs index 19f8e5b..1d1cdb8 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/SuccessfulRecurringCommandUpdaterTests.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SuccessfulRecurringCommandUpdaterTests.cs @@ -2,12 +2,13 @@ using Dapper; using System; using System.Threading.Tasks; +using System.Data.Common; using Xunit; namespace DDD.Core.Infrastructure.Data { using Application; - using System.Data.Common; + using Domain; public abstract class SuccessfulRecurringCommandUpdaterTests : IDisposable where TFixture : IPersistenceFixture @@ -17,8 +18,8 @@ public abstract class SuccessfulRecurringCommandUpdaterTests : IDispos protected SuccessfulRecurringCommandUpdaterTests(TFixture fixture) { - this.Fixture = fixture; - this.ConnectionProvider = fixture.CreateConnectionProvider(); + Fixture = fixture; + ConnectionProvider = fixture.CreateConnectionProvider(); } #endregion Constructors @@ -37,8 +38,8 @@ protected SuccessfulRecurringCommandUpdaterTests(TFixture fixture) public void Handle_WhenCalled_UpdatesCommand() { // Arrange - this.Fixture.ExecuteScriptFromResources("MarkRecurringCommandAsSuccessful"); - var handler = new SuccessfulRecurringCommandUpdater(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("MarkRecurringCommandAsSuccessful"); + var handler = new SuccessfulRecurringCommandUpdater(ConnectionProvider); var command = CreateCommand(); var expectedCommand = ExpectedCommand(); // Act @@ -51,8 +52,8 @@ public void Handle_WhenCalled_UpdatesCommand() public async Task HandleAsync_WhenCalled_UpdatesCommand() { // Arrange - this.Fixture.ExecuteScriptFromResources("MarkRecurringCommandAsSuccessful"); - var handler = new SuccessfulRecurringCommandUpdater(this.ConnectionProvider); + Fixture.ExecuteScriptFromResources("MarkRecurringCommandAsSuccessful"); + var handler = new SuccessfulRecurringCommandUpdater(ConnectionProvider); var command = CreateCommand(); var expectedCommand = ExpectedCommand(); // Act @@ -64,7 +65,7 @@ public async Task HandleAsync_WhenCalled_UpdatesCommand() public void Dispose() { - this.ConnectionProvider.Dispose(); + ConnectionProvider.Dispose(); } private static MarkRecurringCommandAsSuccessful CreateCommand() @@ -78,7 +79,7 @@ private static MarkRecurringCommandAsSuccessful CreateCommand() private RecurringCommandDetail UpdatedCommand() { - using (var connection = this.Fixture.CreateConnection()) + using (var connection = Fixture.CreateConnection()) { connection.Open(); var sql = "SELECT CommandId, CommandType, Body, BodyFormat, RecurringExpression, LastExecutionTime, CASE LastExecutionStatus WHEN 'F' THEN 'Successful' WHEN 'S' THEN 'Successful' END LastExecutionStatus, LastExceptionInfo FROM Command WHERE CommandId = @CommandId"; @@ -103,7 +104,7 @@ private static RecurringCommandDetail ExpectedCommand() CommandType = "DDD.Core.Application.FakeCommand1, DDD.Core.Messages", Body = "{\"Property1\":\"dummy\",\"Property2\":10}", BodyFormat = "JSON", - RecurringExpression = "* * * * *", + RecurringExpression = "* * * * *", LastExecutionTime = new DateTime(2022, 2, 1), LastExecutionStatus = CommandExecutionStatus.Successful, LastExceptionInfo = null diff --git a/Test/DDD.Core.UnitTests/Application/FakeSourceContext.cs b/Test/DDD.Core.UnitTests/Domain/FakeSourceContext.cs similarity index 78% rename from Test/DDD.Core.UnitTests/Application/FakeSourceContext.cs rename to Test/DDD.Core.UnitTests/Domain/FakeSourceContext.cs index 08d61eb..49035f9 100644 --- a/Test/DDD.Core.UnitTests/Application/FakeSourceContext.cs +++ b/Test/DDD.Core.UnitTests/Domain/FakeSourceContext.cs @@ -1,7 +1,5 @@ -namespace DDD.Core.Application +namespace DDD.Core.Domain { - using Domain; - public class FakeSourceContext : BoundedContext { #region Constructors @@ -10,6 +8,5 @@ public FakeSourceContext() : base("FKS", "FakeSource") { } #endregion Constructors - } } From ca8ee150c21b9ebc1e9e428aa25fba614b8d7797 Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 6 Feb 2023 13:04:49 +0100 Subject: [PATCH 069/111] Refactoring --- Src/DDD.Core.Abstractions/AnyArgExtensions.cs | 22 ++++ .../EnumerableArgExtensions.cs | 28 ++++ Src/DDD.Core.Abstractions/ParamExtensions.cs | 20 +++ .../StringArgExtensions.cs | 11 +- .../StringParamExtensions.cs | 9 ++ .../AnyArgExtensionsTests.cs | 52 ++++++++ .../EnumerableArgExtensionsTests.cs | 59 +++++++++ .../ParamExtensionsTests.cs | 52 ++++++++ .../StringArgExtensionsTests.cs | 121 +++++++++++++++++ .../StringParamExtensionsTests.cs | 122 ++++++++++++++++++ 10 files changed, 495 insertions(+), 1 deletion(-) create mode 100644 Src/DDD.Core.Abstractions/AnyArgExtensions.cs create mode 100644 Src/DDD.Core.Abstractions/EnumerableArgExtensions.cs create mode 100644 Src/DDD.Core.Abstractions/ParamExtensions.cs create mode 100644 Test/DDD.Core.Abstractions.UnitTests/AnyArgExtensionsTests.cs create mode 100644 Test/DDD.Core.Abstractions.UnitTests/EnumerableArgExtensionsTests.cs create mode 100644 Test/DDD.Core.Abstractions.UnitTests/ParamExtensionsTests.cs create mode 100644 Test/DDD.Core.Abstractions.UnitTests/StringArgExtensionsTests.cs create mode 100644 Test/DDD.Core.Abstractions.UnitTests/StringParamExtensionsTests.cs 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/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/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/StringArgExtensions.cs b/Src/DDD.Core.Abstractions/StringArgExtensions.cs index 59deb8c..85f1e5c 100644 --- a/Src/DDD.Core.Abstractions/StringArgExtensions.cs +++ b/Src/DDD.Core.Abstractions/StringArgExtensions.cs @@ -3,11 +3,14 @@ namespace DDD { - public static partial class StringArgExtensions + 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); @@ -21,6 +24,9 @@ public static string HasMinLength(this StringArg _, string value, int minLength, 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); @@ -35,6 +41,9 @@ public static string IsAllDigits(this StringArg _, string value, string paramNam 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); diff --git a/Src/DDD.Core.Abstractions/StringParamExtensions.cs b/Src/DDD.Core.Abstractions/StringParamExtensions.cs index 808ebb3..1e7d456 100644 --- a/Src/DDD.Core.Abstractions/StringParamExtensions.cs +++ b/Src/DDD.Core.Abstractions/StringParamExtensions.cs @@ -7,12 +7,21 @@ public static class StringParamExtensions #region Methods + /// + /// 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 contains only digits. + /// public static void IsAllDigits(this in StringParam param) => Ensure.String.IsAllDigits(param.Value, param.Name, param.OptsFn); + /// + /// 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); 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/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/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..77e35f0 --- /dev/null +++ b/Test/DDD.Core.Abstractions.UnitTests/StringArgExtensionsTests.cs @@ -0,0 +1,121 @@ +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(); + } + + #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..be5fd98 --- /dev/null +++ b/Test/DDD.Core.Abstractions.UnitTests/StringParamExtensionsTests.cs @@ -0,0 +1,122 @@ +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(); + } + + #endregion Methods + + } +} From 3cfd51a1ba26d0bcf5d1b21cbb31443aea0f10b3 Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 6 Feb 2023 13:52:58 +0100 Subject: [PATCH 070/111] Refactoring --- .../Domain/Prescriptions/PharmaceuticalPrescription.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs index 54070fd..69997d5 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs @@ -41,6 +41,7 @@ public PharmaceuticalPrescription(PrescriptionIdentifier identifier, { Ensure.That(prescribedMedications, nameof(prescribedMedications)).IsNotNull(); Ensure.Enumerable.HasItems(prescribedMedications, nameof(prescribedMedications)); + Ensure.Enumerable.HasNoNull(prescribedMedications, nameof(prescribedMedications)); this.prescribedMedications.AddRange(prescribedMedications); } From 7bfb5ad02b272ce9d8daca57e0e029ea1cc8c6e1 Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 6 Feb 2023 15:26:33 +0100 Subject: [PATCH 071/111] Refactoring --- .../StringArgExtensions.cs | 13 ++++++- .../StringParamExtensions.cs | 23 +++++++---- .../StringArgExtensionsTests.cs | 38 ++++++++++++++++++- .../StringParamExtensionsTests.cs | 38 ++++++++++++++++++- 4 files changed, 100 insertions(+), 12 deletions(-) diff --git a/Src/DDD.Core.Abstractions/StringArgExtensions.cs b/Src/DDD.Core.Abstractions/StringArgExtensions.cs index 85f1e5c..ce48425 100644 --- a/Src/DDD.Core.Abstractions/StringArgExtensions.cs +++ b/Src/DDD.Core.Abstractions/StringArgExtensions.cs @@ -1,5 +1,6 @@ using EnsureThat; using EnsureThat.Enforcers; +using System; namespace DDD { @@ -58,8 +59,16 @@ public static string IsAllLetters(this StringArg _, string value, string paramNa return value; } - #endregion Methods + /// + /// 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/StringParamExtensions.cs b/Src/DDD.Core.Abstractions/StringParamExtensions.cs index 1e7d456..4b86b69 100644 --- a/Src/DDD.Core.Abstractions/StringParamExtensions.cs +++ b/Src/DDD.Core.Abstractions/StringParamExtensions.cs @@ -1,4 +1,5 @@ -using EnsureThat; +using System; +using EnsureThat; namespace DDD { @@ -8,10 +9,10 @@ public static class StringParamExtensions #region Methods /// - /// Ensures that the string contains only letters. + /// Ensures that the string has a minimum length. /// - public static void IsAllLetters(this in StringParam param) - => Ensure.String.IsAllLetters(param.Value, param.Name, param.OptsFn); + 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. @@ -20,12 +21,18 @@ public static void IsAllDigits(this in StringParam param) => Ensure.String.IsAllDigits(param.Value, param.Name, param.OptsFn); /// - /// Ensures that the string has a minimum length. + /// Ensures that the string contains only letters. /// - public static void HasMinLength(this in StringParam param, int minLength) - => Ensure.String.HasMinLength(param.Value, minLength, param.Name, param.OptsFn); + 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/Test/DDD.Core.Abstractions.UnitTests/StringArgExtensionsTests.cs b/Test/DDD.Core.Abstractions.UnitTests/StringArgExtensionsTests.cs index 77e35f0..cdaa7a2 100644 --- a/Test/DDD.Core.Abstractions.UnitTests/StringArgExtensionsTests.cs +++ b/Test/DDD.Core.Abstractions.UnitTests/StringArgExtensionsTests.cs @@ -115,7 +115,43 @@ public void IsAllLetters_WhenValidCharacters_DoesNotThrow(string value) 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 } -} +} \ No newline at end of file diff --git a/Test/DDD.Core.Abstractions.UnitTests/StringParamExtensionsTests.cs b/Test/DDD.Core.Abstractions.UnitTests/StringParamExtensionsTests.cs index be5fd98..5ac35eb 100644 --- a/Test/DDD.Core.Abstractions.UnitTests/StringParamExtensionsTests.cs +++ b/Test/DDD.Core.Abstractions.UnitTests/StringParamExtensionsTests.cs @@ -116,7 +116,43 @@ public void IsAllLetters_WhenValidCharacters_DoesNotThrow(string value) 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 } -} +} \ No newline at end of file From e69596905a1c922f4be7777a2b8e16727ce65586 Mon Sep 17 00:00:00 2001 From: draphyz Date: Tue, 7 Feb 2023 16:47:26 +0100 Subject: [PATCH 072/111] Refactoring --- Src/DDD.Core.SimpleInjector/ContainerExtensions.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs index e5b1dc7..2c1351c 100644 --- a/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs +++ b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs @@ -24,6 +24,11 @@ public static TService GetNamedInstance(this Container container, stri return provider.GetService(name); } + public static void RegisterBoundedContexts(this Container container, IEnumerable assemblies) + { + container.Collection.Register(assemblies, Lifestyle.Singleton); + } + public static void RegisterCommandProcessors(this Container container) { Ensure.That(container, nameof(container)).IsNotNull(); @@ -126,8 +131,8 @@ public static void RegisterMappersAndTranslators(this Container container, IEnum public static void RegisterRepositories(this Container container, IEnumerable assemblies, Func predicate) { Ensure.That(container, nameof(container)).IsNotNull(); - container.RegisterConditional(typeof(ISyncRepository<,>), assemblies, predicate); - container.RegisterConditional(typeof(IAsyncRepository<,>), assemblies, predicate); + container.RegisterConditional(typeof(ISyncRepository<,>), assemblies, Lifestyle.Scoped, predicate); + container.RegisterConditional(typeof(IAsyncRepository<,>), assemblies, Lifestyle.Scoped, predicate); } public static void RegisterConditional(this Container container, Func instanceCreator, Lifestyle lifestyle, Predicate predicate) From 9e00e71901af8d5dc81ad9a8e86d5ccbfbe5709e Mon Sep 17 00:00:00 2001 From: draphyz Date: Tue, 7 Feb 2023 18:13:36 +0100 Subject: [PATCH 073/111] Refactoring --- Src/DDD.Core.Messages/Application/WriteEvents.cs | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 Src/DDD.Core.Messages/Application/WriteEvents.cs diff --git a/Src/DDD.Core.Messages/Application/WriteEvents.cs b/Src/DDD.Core.Messages/Application/WriteEvents.cs deleted file mode 100644 index d606d18..0000000 --- a/Src/DDD.Core.Messages/Application/WriteEvents.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; - -namespace DDD.Core.Application -{ - public class WriteEvents : ICommand - { - - #region Properties - - public ICollection Events { get; set; } = new List(); - - #endregion Properties - - } -} \ No newline at end of file From ed7a2c2a6a45709ed92823c29fcf064e8c4e1996 Mon Sep 17 00:00:00 2001 From: draphyz Date: Thu, 16 Feb 2023 11:22:12 +0100 Subject: [PATCH 074/111] Refactoring --- DDD.sln | 7 + .../FailedEventStreamPositionUpdater.cs | 119 ++++++++ .../UpdateFailedEventStreamPosition.sql | Bin 0 -> 282 bytes Src/DDD.Core.Dapper/SqlScripts.Designer.cs | 13 + Src/DDD.Core.Dapper/SqlScripts.resx | 3 + .../UpdateFailedEventStreamPosition.cs | 28 ++ .../ContainerExtensions.cs | 38 +++ ...ore.SimpleInjector.FluentValidation.csproj | 25 ++ .../Properties/AssemblyInfo.cs | 7 + .../AsyncScopedCommandHandler.cs | 50 +++- .../ContainerExtensions.cs | 259 ++++++++++-------- .../DDD.Core.SimpleInjector.csproj | 3 + .../ThreadScopedCommandHandler.cs | 52 +++- Src/DDD.Core/Application/AsyncEventHandler.cs | 7 +- .../AsyncEventHandlerWithLogging.cs | 11 +- Src/DDD.Core/Application/EventConsumer.cs | 6 +- Src/DDD.Core/Application/EventPublisher.cs | 45 +-- .../IAsyncCommandHandlerExtensions.cs | 21 -- .../Application/IAsyncEventHandler.cs | 13 +- .../Application/IAsyncEventHandler`1.cs | 17 +- Src/DDD.Core/Application/IEventConsumer.cs | 15 +- Src/DDD.Core/Application/IEventConsumer`1.cs | 11 +- Src/DDD.Core/Application/IEventPublisher.cs | 17 +- Src/DDD.Core/Application/IEventPublisher`1.cs | 20 ++ .../Application/IMessageContextExtensions.cs | 22 +- Src/DDD.Core/Application/ISyncEventHandler.cs | 15 +- .../Application/ISyncEventHandler`1.cs | 16 +- .../Application/MessageContextInfo.cs | 9 +- Src/DDD.Core/Application/SyncEventHandler.cs | 7 +- .../SyncEventHandlerWithLogging.cs | 11 +- .../Data/IDbConnectionProvider.cs | 12 +- .../Data/IDbConnectionProvider`1.cs | 12 +- .../Data/LazyDbConnectionProvider.cs | 3 + .../FailedEventStreamPositionUpdaterTests.cs | 80 ++++++ ...leFailedEventStreamPositionUpdaterTests.cs | 17 ++ .../Data/OracleScripts.Designer.cs | 13 + .../Infrastructure/Data/OracleScripts.resx | 3 + .../UpdateFailedEventStreamPosition.sql | Bin 0 -> 1778 bytes .../UpdateFailedEventStreamPosition.sql | Bin 0 -> 1980 bytes ...erFailedEventStreamPositionUpdaterTests.cs | 17 ++ .../Data/SqlServerScripts.Designer.cs | 13 + .../Infrastructure/Data/SqlServerScripts.resx | 3 + .../Application/EventConsumerTests.cs | 8 +- .../Application/EventPublisherTests.cs | 73 ++--- 44 files changed, 883 insertions(+), 238 deletions(-) create mode 100644 Src/DDD.Core.Dapper/FailedEventStreamPositionUpdater.cs create mode 100644 Src/DDD.Core.Dapper/Scripts/UpdateFailedEventStreamPosition.sql create mode 100644 Src/DDD.Core.Messages/Application/UpdateFailedEventStreamPosition.cs create mode 100644 Src/DDD.Core.SimpleInjector.FluentValidation/ContainerExtensions.cs create mode 100644 Src/DDD.Core.SimpleInjector.FluentValidation/DDD.Core.SimpleInjector.FluentValidation.csproj create mode 100644 Src/DDD.Core.SimpleInjector.FluentValidation/Properties/AssemblyInfo.cs create mode 100644 Src/DDD.Core/Application/IEventPublisher`1.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedEventStreamPositionUpdaterTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleFailedEventStreamPositionUpdaterTests.cs create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/UpdateFailedEventStreamPosition.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/SqlServer/UpdateFailedEventStreamPosition.sql create mode 100644 Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerFailedEventStreamPositionUpdaterTests.cs diff --git a/DDD.sln b/DDD.sln index 7bb3474..b123ea7 100644 --- a/DDD.sln +++ b/DDD.sln @@ -71,6 +71,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.Dapper.Integration 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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -177,6 +179,10 @@ Global {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -207,6 +213,7 @@ Global {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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {983C3AB4-301E-47A3-93FF-2E4BD8F2090F} diff --git a/Src/DDD.Core.Dapper/FailedEventStreamPositionUpdater.cs b/Src/DDD.Core.Dapper/FailedEventStreamPositionUpdater.cs new file mode 100644 index 0000000..5f7af28 --- /dev/null +++ b/Src/DDD.Core.Dapper/FailedEventStreamPositionUpdater.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 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 = null) + { + 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 = null) + { + Ensure.That(command, nameof(command)).IsNotNull(); + try + { + await new SynchronizationContextRemover(); + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + var cancellationToken = context?.CancellationToken() ?? default; + 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/Scripts/UpdateFailedEventStreamPosition.sql b/Src/DDD.Core.Dapper/Scripts/UpdateFailedEventStreamPosition.sql new file mode 100644 index 0000000000000000000000000000000000000000..baff45df0bcb044b58453b032840c13ec2509d29 GIT binary patch literal 282 zcmaKn(F%e<6hzOn&_C>t1PLnjB9eyBVm%0{Q5N{~+N`*+Lc}ucox68t?#|PNjEIne z7(+ru!<`#f@sI9~fn0d1u4vg2h{u{Y>+xSRdR}rEgkD<1UN|UY+qW(yNBNwWT(ikI iDcvb;ZKp$0?fEOSVC13goWk+{s$3hr^{d)b`n@l3mMx0_ literal 0 HcmV?d00001 diff --git a/Src/DDD.Core.Dapper/SqlScripts.Designer.cs b/Src/DDD.Core.Dapper/SqlScripts.Designer.cs index 2e71162..19c8c1d 100644 --- a/Src/DDD.Core.Dapper/SqlScripts.Designer.cs +++ b/Src/DDD.Core.Dapper/SqlScripts.Designer.cs @@ -287,6 +287,19 @@ internal static string UpdateFailedEventStream { } } + /// + /// 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, diff --git a/Src/DDD.Core.Dapper/SqlScripts.resx b/Src/DDD.Core.Dapper/SqlScripts.resx index 0baf519..6eaad29 100644 --- a/Src/DDD.Core.Dapper/SqlScripts.resx +++ b/Src/DDD.Core.Dapper/SqlScripts.resx @@ -154,6 +154,9 @@ 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 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.SimpleInjector.FluentValidation/ContainerExtensions.cs b/Src/DDD.Core.SimpleInjector.FluentValidation/ContainerExtensions.cs new file mode 100644 index 0000000..2486996 --- /dev/null +++ b/Src/DDD.Core.SimpleInjector.FluentValidation/ContainerExtensions.cs @@ -0,0 +1,38 @@ +using FluentValidation; +using SimpleInjector; +using System.Collections.Generic; +using System.Reflection; +using System; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Validation; + using Application; + using EnsureThat; + + public static class ContainerExtensions + { + + #region Methods + + /// + /// Registers command and queries validators. + /// + /// The container that registers validators. + /// The assemblies that contain validators. + /// A predicate that determines which validators should be registered. + public static void RegisterValidators(this Container container, IEnumerable assemblies, Func predicate = null) + { + Ensure.That(container, nameof(container)).IsNotNull(); + var notNullPredicate = predicate ?? (t => true); + container.RegisterConditional(typeof(IValidator<>), assemblies, notNullPredicate); + container.Register(typeof(ISyncCommandValidator<>), typeof(SyncFluentCommandValidatorAdapter<>)); + container.Register(typeof(IAsyncCommandValidator<>), typeof(AsyncFluentCommandValidatorAdapter<>)); + container.Register(typeof(ISyncQueryValidator<>), typeof(SyncFluentQueryValidatorAdapter<>)); + container.Register(typeof(IAsyncQueryValidator<>), typeof(AsyncFluentQueryValidatorAdapter<>)); + } + + #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/AsyncScopedCommandHandler.cs b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs index a94670c..ba18d48 100644 --- a/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs @@ -2,6 +2,7 @@ using SimpleInjector.Lifestyles; using EnsureThat; using System; +using System.Transactions; using System.Threading.Tasks; namespace DDD.Core.Infrastructure.DependencyInjection @@ -11,6 +12,7 @@ namespace DDD.Core.Infrastructure.DependencyInjection /// /// 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 @@ -25,7 +27,8 @@ public class AsyncScopedCommandHandler : IAsyncCommandHandler> handlerProvider, Container container) + public AsyncScopedCommandHandler(Func> handlerProvider, + Container container) { Ensure.That(handlerProvider, nameof(handlerProvider)).IsNotNull(); Ensure.That(container, nameof(container)).IsNotNull(); @@ -39,11 +42,52 @@ public AsyncScopedCommandHandler(Func> handlerPro public async Task HandleAsync(TCommand command, IMessageContext context = null) { + await new SynchronizationContextRemover(); using (AsyncScopedLifestyle.BeginScope(container)) { - await new SynchronizationContextRemover(); - var handler = this.handlerProvider(); + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + var handler = this.handlerProvider(); + await handler.HandleAsync(command, context); + if (context?.IsEventHandling() == true) // Exception to the rule "One transaction per command" to avoid to handle the same event more than once + await UpdateEventStreamPositionAsync(context); + scope.Complete(); + } + } + } + + 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; } } diff --git a/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs index 2c1351c..eed69d7 100644 --- a/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs +++ b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs @@ -1,196 +1,213 @@ -using SimpleInjector; +using EnsureThat; +using Microsoft.Extensions.Logging; +using SimpleInjector; +using SimpleInjector.Lifestyles; using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; -using EnsureThat; +using System.Linq; using DDD.DependencyInjection; namespace DDD.Core.Infrastructure.DependencyInjection { using Mapping; - using Core.Application; - using Core.Domain; + using Application; + using Domain; public static class ContainerExtensions { #region Methods - - public static TService GetNamedInstance(this Container container, string name) where TService : class + public static void RegisterConditional(this Container container, + Func instanceCreator, + Lifestyle lifestyle, + Predicate predicate) + where TService : class { Ensure.That(container, nameof(container)).IsNotNull(); - var provider = container.GetInstance>(); - return provider.GetService(name); - } - - public static void RegisterBoundedContexts(this Container container, IEnumerable assemblies) - { - container.Collection.Register(assemblies, Lifestyle.Singleton); + var registration = lifestyle.CreateRegistration(instanceCreator, container); + container.RegisterConditional(registration, predicate); } - public static void RegisterCommandProcessors(this Container container) + public static void RegisterConditional(this Container container, Func instanceCreator, Predicate predicate) + where TService : class { Ensure.That(container, nameof(container)).IsNotNull(); - container.RegisterSingleton(typeof(IContextualCommandProcessor<>), typeof(ContextualCommandProcessor<>)); - container.RegisterSingleton(); + container.RegisterConditional(instanceCreator, Lifestyle.Transient, predicate); } - public static void RegisterCommandHandlers(this Container container, IEnumerable assemblies, Func 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 context = new TypesToRegisterOptions { IncludeGenericTypeDefinitions = true }; - container.RegisterConditional(typeof(ISyncCommandHandler<,>), assemblies, predicate, context); - container.RegisterConditional(typeof(IAsyncCommandHandler<,>), assemblies, predicate, context); - container.RegisterDecorator(typeof(ISyncCommandHandler<,>), typeof(SyncCommandHandlerWithLogging<,>)); - container.RegisterDecorator(typeof(IAsyncCommandHandler<,>), typeof(AsyncCommandHandlerWithLogging<,>)); - container.RegisterDecorator(typeof(ISyncCommandHandler<,>), typeof(ThreadScopedCommandHandler<,>), Lifestyle.Singleton); - container.RegisterDecorator(typeof(IAsyncCommandHandler<,>), typeof(AsyncScopedCommandHandler<,>), Lifestyle.Singleton); - container.RegisterConditional(typeof(ISyncCommandHandler<>), assemblies, predicate); - container.RegisterConditional(typeof(IAsyncCommandHandler<>), assemblies, predicate); - container.RegisterDecorator(typeof(ISyncCommandHandler<>), typeof(SyncCommandHandlerWithLogging<>)); - container.RegisterDecorator(typeof(IAsyncCommandHandler<>), typeof(AsyncCommandHandlerWithLogging<>)); - container.RegisterDecorator(typeof(ISyncCommandHandler<>), typeof(ThreadScopedCommandHandler<>), Lifestyle.Singleton); - container.RegisterDecorator(typeof(IAsyncCommandHandler<>), typeof(AsyncScopedCommandHandler<>), Lifestyle.Singleton); + 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 RegisterRecurringCommandManager(this Container container, RecurringCommandManagerSettings settings) - where TContext : BoundedContext, new() + public static void RegisterConditional(this Container container, + Type serviceType, + IEnumerable assemblies, + Lifestyle lifestyle, + Func predicate) { - container.RegisterInstance(settings); - container.RegisterSingleton, RecurringCommandManager>(); - container.Collection.Append>(Lifestyle.Singleton); + Ensure.That(container, nameof(container)).IsNotNull(); + container.RegisterConditional(serviceType, assemblies, lifestyle, predicate, new TypesToRegisterOptions()); } - public static void RegisterQueryProcessors(this Container container) + public static void RegisterConditional(this Container container, + Type serviceType, + IEnumerable assemblies, + Func predicate) { Ensure.That(container, nameof(container)).IsNotNull(); - container.RegisterSingleton(typeof(IContextualQueryProcessor<>), typeof(ContextualQueryProcessor<>)); - container.RegisterSingleton(); + container.RegisterConditional(serviceType, assemblies, Lifestyle.Transient, predicate); } - public static void RegisterQueryHandlers(this Container container, IEnumerable assemblies, Func predicate) + public static void RegisterConditional(this Container container, + Type serviceType, + IEnumerable assemblies, + Func predicate, + TypesToRegisterOptions options) { Ensure.That(container, nameof(container)).IsNotNull(); - var context = new TypesToRegisterOptions { IncludeGenericTypeDefinitions = true }; - container.RegisterConditional(typeof(ISyncQueryHandler<,,>), assemblies, predicate, context); - container.RegisterConditional(typeof(IAsyncQueryHandler<,,>), assemblies, predicate, context); - container.RegisterDecorator(typeof(ISyncQueryHandler<,,>), typeof(SyncQueryHandlerWithLogging<,,>)); - container.RegisterDecorator(typeof(IAsyncQueryHandler<,,>), typeof(AsyncQueryHandlerWithLogging<,,>)); - container.RegisterDecorator(typeof(ISyncQueryHandler<,,>), typeof(ThreadScopedQueryHandler<,,>), Lifestyle.Singleton); - container.RegisterDecorator(typeof(IAsyncQueryHandler<,,>), typeof(AsyncScopedQueryHandler<,,>), Lifestyle.Singleton); - container.RegisterConditional(typeof(ISyncQueryHandler<,>), assemblies, predicate); - container.RegisterConditional(typeof(IAsyncQueryHandler<,>), assemblies, predicate); - container.RegisterDecorator(typeof(ISyncQueryHandler<,>), typeof(SyncQueryHandlerWithLogging<,>)); - container.RegisterDecorator(typeof(IAsyncQueryHandler<,>), typeof(AsyncQueryHandlerWithLogging<,>)); - container.RegisterDecorator(typeof(ISyncQueryHandler<,>), typeof(ThreadScopedQueryHandler<,>), Lifestyle.Singleton); - container.RegisterDecorator(typeof(IAsyncQueryHandler<,>), typeof(AsyncScopedQueryHandler<,>), Lifestyle.Singleton); + container.RegisterConditional(serviceType, assemblies, Lifestyle.Transient, predicate, options); } - public static void RegisterEventPublisher(this Container container) + /// + /// Gets a named instance of a service. + /// + public static TService GetNamedInstance(this Container container, string name) where TService : class { Ensure.That(container, nameof(container)).IsNotNull(); - container.RegisterSingleton(); + var provider = container.GetInstance>(); + return provider.GetService(name); } - public static void RegisterEventHandlers(this Container container, IEnumerable assemblies, Func predicate) + /// + /// Registers base components for handling commands, queries and events. + /// + /// The container that registers base components. + /// The assemblies that contain base components. + /// A predicate that determines which components should be registered. + public static void RegisterBaseComponents(this Container container, IEnumerable assemblies, Func predicate = null) { Ensure.That(container, nameof(container)).IsNotNull(); - var context = new TypesToRegisterOptions { IncludeGenericTypeDefinitions = true, IncludeComposites = false }; - var syncHandlerTypes = container.GetTypesToRegister(typeof(ISyncEventHandler<>), assemblies, context) - .Where(predicate); - container.Collection.Register(typeof(ISyncEventHandler<>), syncHandlerTypes); - var asyncHandlerTypes = container.GetTypesToRegister(typeof(IAsyncEventHandler<>), assemblies, context) - .Where(predicate); - container.Collection.Register(typeof(IAsyncEventHandler<>), asyncHandlerTypes); - container.RegisterDecorator(typeof(ISyncEventHandler<>), typeof(SyncEventHandlerWithLogging<>)); - container.RegisterDecorator(typeof(IAsyncEventHandler<>), typeof(AsyncEventHandlerWithLogging<>)); + Ensure.That(assemblies, nameof(assemblies)).IsNotNull(); ; + var notNullPredicate = predicate ?? (t => true); + container.Options.DefaultScopedLifestyle = Lifestyle.CreateHybrid(new ThreadScopedLifestyle(), new AsyncScopedLifestyle()); + container.RegisterInstance(container); + container.Collection.Register(assemblies, Lifestyle.Singleton); + container.RegisterSingleton(); + container.RegisterCommandHandlers(assemblies, notNullPredicate); + container.RegisterSingleton(); + container.RegisterQueryHandlers(assemblies, notNullPredicate); + container.RegisterSingleton(typeof(IEventPublisher<>), typeof(EventPublisher<>)); + container.RegisterEventHandlers(assemblies, notNullPredicate); + container.RegisterSingleton(); + container.RegisterMappersAndTranslators(assemblies, notNullPredicate); + container.RegisterRepositories(assemblies, notNullPredicate); } + /// + /// Registers an event consumer for a specific bounded context. + /// public static void RegisterEventConsumer(this Container container, EventConsumerSettings settings) where TContext : BoundedContext, new() { + Ensure.That(container, nameof(container)).IsNotNull(); + Ensure.That(settings, nameof(settings)).IsNotNull(); container.RegisterInstance(settings); container.RegisterSingleton, EventConsumer>(); container.Collection.Append>(Lifestyle.Singleton); } - public static void RegisterMappingProcessor(this Container container) - { - Ensure.That(container, nameof(container)).IsNotNull(); - container.RegisterSingleton(); - } - - public static void RegisterMappersAndTranslators(this Container container, IEnumerable assemblies, Func predicate) + /// + /// Registers a recurring command manager for a specific bounded context. + /// + public static void RegisterRecurringCommandManager(this Container container, RecurringCommandManagerSettings settings) + where TContext : BoundedContext, new() { Ensure.That(container, nameof(container)).IsNotNull(); - container.RegisterConditional(typeof(IObjectMapper<,>), assemblies, predicate); - container.RegisterConditional(typeof(IObjectTranslator<,>), assemblies, predicate); + Ensure.That(settings, nameof(settings)).IsNotNull(); + container.RegisterInstance(settings); + container.RegisterSingleton, RecurringCommandManager>(); + container.Collection.Append>(Lifestyle.Singleton); } - public static void RegisterRepositories(this Container container, IEnumerable assemblies, Func predicate) + /// + /// Registers loggers. + /// + public static void RegisterLoggers(this Container container, ILoggerFactory loggerFactory) { - Ensure.That(container, nameof(container)).IsNotNull(); - container.RegisterConditional(typeof(ISyncRepository<,>), assemblies, Lifestyle.Scoped, predicate); - container.RegisterConditional(typeof(IAsyncRepository<,>), assemblies, Lifestyle.Scoped, predicate); + container.RegisterInstance(loggerFactory); + container.RegisterConditional(typeof(ILogger), + c => typeof(Logger<>).MakeGenericType(c.Consumer.ImplementationType), + Lifestyle.Singleton, + _ => true); + container.RegisterSingleton(typeof(ILogger<>), typeof(Logger<>)); } - public static void RegisterConditional(this Container container, Func instanceCreator, Lifestyle lifestyle, Predicate predicate) - where TService : class + private static void RegisterCommandHandlers(this Container container, IEnumerable assemblies, Func predicate) { - Ensure.That(container, nameof(container)).IsNotNull(); - var registration = lifestyle.CreateRegistration(instanceCreator, container); - container.RegisterConditional(registration, predicate); + var options = new TypesToRegisterOptions { IncludeGenericTypeDefinitions = true }; + container.RegisterConditional(typeof(ISyncCommandHandler<,>), assemblies, predicate, options); + container.RegisterDecorator(typeof(ISyncCommandHandler<,>), typeof(SyncCommandHandlerWithLogging<,>)); + container.RegisterDecorator(typeof(ISyncCommandHandler<,>), typeof(ThreadScopedCommandHandler<,>), Lifestyle.Singleton); + container.RegisterConditional(typeof(IAsyncCommandHandler<,>), assemblies, predicate, options); + container.RegisterDecorator(typeof(IAsyncCommandHandler<,>), typeof(AsyncCommandHandlerWithLogging<,>)); + container.RegisterDecorator(typeof(IAsyncCommandHandler<,>), typeof(AsyncScopedCommandHandler<,>), Lifestyle.Singleton); + container.RegisterConditional(typeof(ISyncCommandHandler<>), assemblies, predicate); + container.RegisterDecorator(typeof(ISyncCommandHandler<>), typeof(SyncCommandHandlerWithLogging<>)); + container.RegisterDecorator(typeof(ISyncCommandHandler<>), typeof(ThreadScopedCommandHandler<>), Lifestyle.Singleton); + container.RegisterConditional(typeof(IAsyncCommandHandler<>), assemblies, predicate); + container.RegisterDecorator(typeof(IAsyncCommandHandler<>), typeof(AsyncCommandHandlerWithLogging<>)); + container.RegisterDecorator(typeof(IAsyncCommandHandler<>), typeof(AsyncScopedCommandHandler<>), Lifestyle.Singleton); } - public static void RegisterConditional(this Container container, Func instanceCreator, Predicate predicate) - where TService : class + private static void RegisterQueryHandlers(this Container container, IEnumerable assemblies, Func predicate) { - Ensure.That(container, nameof(container)).IsNotNull(); - container.RegisterConditional(instanceCreator, Lifestyle.Transient, predicate); - } - - public static void RegisterConditional(this Container container, - Type openGenericServiceType, - IEnumerable assemblies, - Lifestyle lifestyle, - Func predicate, - TypesToRegisterOptions context) - { - Ensure.That(container, nameof(container)).IsNotNull(); - var implementationTypes = container.GetTypesToRegister(openGenericServiceType, assemblies, context) - .Where(predicate); - foreach (var implementationType in implementationTypes) - container.Register(openGenericServiceType, implementationType, lifestyle); + var options = new TypesToRegisterOptions { IncludeGenericTypeDefinitions = true }; + container.RegisterConditional(typeof(ISyncQueryHandler<,,>), assemblies, predicate, options); + container.RegisterDecorator(typeof(ISyncQueryHandler<,,>), typeof(SyncQueryHandlerWithLogging<,,>)); + container.RegisterDecorator(typeof(ISyncQueryHandler<,,>), typeof(ThreadScopedQueryHandler<,,>), Lifestyle.Singleton); + container.RegisterConditional(typeof(IAsyncQueryHandler<,,>), assemblies, predicate, options); + container.RegisterDecorator(typeof(IAsyncQueryHandler<,,>), typeof(AsyncQueryHandlerWithLogging<,,>)); + container.RegisterDecorator(typeof(IAsyncQueryHandler<,,>), typeof(AsyncScopedQueryHandler<,,>), Lifestyle.Singleton); + container.RegisterConditional(typeof(ISyncQueryHandler<,>), assemblies, predicate); + container.RegisterDecorator(typeof(ISyncQueryHandler<,>), typeof(SyncQueryHandlerWithLogging<,>)); + container.RegisterDecorator(typeof(ISyncQueryHandler<,>), typeof(ThreadScopedQueryHandler<,>), Lifestyle.Singleton); + container.RegisterConditional(typeof(IAsyncQueryHandler<,>), assemblies, predicate); + container.RegisterDecorator(typeof(IAsyncQueryHandler<,>), typeof(AsyncQueryHandlerWithLogging<,>)); + container.RegisterDecorator(typeof(IAsyncQueryHandler<,>), typeof(AsyncScopedQueryHandler<,>), Lifestyle.Singleton); } - public static void RegisterConditional(this Container container, - Type openGenericServiceType, - IEnumerable assemblies, - Lifestyle lifestyle, - Func predicate) + private static void RegisterEventHandlers(this Container container, IEnumerable assemblies, Func predicate) { - Ensure.That(container, nameof(container)).IsNotNull(); - container.RegisterConditional(openGenericServiceType, assemblies, lifestyle, predicate, new TypesToRegisterOptions()); + container.RegisterConditional(typeof(ISyncEventHandler<,>), assemblies, predicate); + container.RegisterConditional(typeof(IAsyncEventHandler<,>), assemblies, predicate); + container.RegisterDecorator(typeof(ISyncEventHandler<,>), typeof(SyncEventHandlerWithLogging<,>)); + container.RegisterDecorator(typeof(IAsyncEventHandler<,>), typeof(AsyncEventHandlerWithLogging<,>)); } - public static void RegisterConditional(this Container container, - Type openGenericServiceType, - IEnumerable assemblies, - Func predicate) + private static void RegisterMappersAndTranslators(this Container container, IEnumerable assemblies, Func predicate) { - Ensure.That(container, nameof(container)).IsNotNull(); - container.RegisterConditional(openGenericServiceType, assemblies, Lifestyle.Transient, predicate); + container.RegisterConditional(typeof(IObjectMapper<,>), assemblies, predicate); + container.RegisterConditional(typeof(IObjectTranslator<,>), assemblies, predicate); } - public static void RegisterConditional(this Container container, - Type openGenericServiceType, - IEnumerable assemblies, - Func predicate, - TypesToRegisterOptions context) + private static void RegisterRepositories(this Container container, IEnumerable assemblies, Func predicate) { - Ensure.That(container, nameof(container)).IsNotNull(); - container.RegisterConditional(openGenericServiceType, assemblies, Lifestyle.Transient, predicate, context); + container.RegisterConditional(typeof(ISyncRepository<,>), assemblies, Lifestyle.Scoped, predicate); + container.RegisterConditional(typeof(IAsyncRepository<,>), assemblies, Lifestyle.Scoped, predicate); } #endregion Methods diff --git a/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj b/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj index fec7f8b..d69a2be 100644 --- a/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj +++ b/Src/DDD.Core.SimpleInjector/DDD.Core.SimpleInjector.csproj @@ -30,4 +30,7 @@ 5.4.1 + + + \ No newline at end of file diff --git a/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs b/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs index bad7d15..2d6e986 100644 --- a/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs +++ b/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs @@ -2,6 +2,7 @@ using SimpleInjector.Lifestyles; using EnsureThat; using System; +using System.Transactions; namespace DDD.Core.Infrastructure.DependencyInjection { @@ -9,6 +10,7 @@ namespace DDD.Core.Infrastructure.DependencyInjection /// /// 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 @@ -23,7 +25,8 @@ public class ThreadScopedCommandHandler : ISyncCommandHandler> handlerProvider, Container container) + public ThreadScopedCommandHandler(Func> handlerProvider, + Container container) { Ensure.That(handlerProvider, nameof(handlerProvider)).IsNotNull(); Ensure.That(container, nameof(container)).IsNotNull(); @@ -31,16 +34,57 @@ public ThreadScopedCommandHandler(Func> handlerPro this.container = container; } - #endregion Constructors + #endregion Constructors - #region Methods + #region Methods< public void Handle(TCommand command, IMessageContext context = null) { using (ThreadScopedLifestyle.BeginScope(container)) { - var handler = this.handlerProvider(); + using (var scope = new TransactionScope()) + { + var handler = this.handlerProvider(); + handler.Handle(command, context); + if (context?.IsEventHandling() == true) // Exception to the rule "One transaction per command" to avoid to handle the same event more than once + UpdateEventStreamPosition(context); + scope.Complete(); + } + } + } + + 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; } } diff --git a/Src/DDD.Core/Application/AsyncEventHandler.cs b/Src/DDD.Core/Application/AsyncEventHandler.cs index 9fc0910..3313552 100644 --- a/Src/DDD.Core/Application/AsyncEventHandler.cs +++ b/Src/DDD.Core/Application/AsyncEventHandler.cs @@ -8,12 +8,17 @@ namespace DDD.Core.Application /// /// Base class for handling asynchronously events. /// - public abstract class AsyncEventHandler : IAsyncEventHandler + public abstract class AsyncEventHandler : IAsyncEventHandler where TEvent : class, IEvent + where TContext : BoundedContext, new() { #region Properties + public TContext Context { get; } = new TContext(); + + BoundedContext IAsyncEventHandler.Context => this.Context; + Type IAsyncEventHandler.EventType => typeof(TEvent); #endregion Properties diff --git a/Src/DDD.Core/Application/AsyncEventHandlerWithLogging.cs b/Src/DDD.Core/Application/AsyncEventHandlerWithLogging.cs index c344700..51fd8f3 100644 --- a/Src/DDD.Core/Application/AsyncEventHandlerWithLogging.cs +++ b/Src/DDD.Core/Application/AsyncEventHandlerWithLogging.cs @@ -11,20 +11,21 @@ namespace DDD.Core.Application /// /// A decorator that logs information about events. /// - public class AsyncEventHandlerWithLogging : IAsyncEventHandler + public class AsyncEventHandlerWithLogging : IAsyncEventHandler where TEvent : class, IEvent + where TContext : BoundedContext { #region Fields - private readonly IAsyncEventHandler eventHandler; + private readonly IAsyncEventHandler eventHandler; private readonly ILogger logger; #endregion Fields #region Constructors - public AsyncEventHandlerWithLogging(IAsyncEventHandler eventHandler, ILogger logger) + public AsyncEventHandlerWithLogging(IAsyncEventHandler eventHandler, ILogger logger) { Ensure.That(eventHandler, nameof(eventHandler)).IsNotNull(); Ensure.That(logger, nameof(logger)).IsNotNull(); @@ -36,6 +37,10 @@ public AsyncEventHandlerWithLogging(IAsyncEventHandler eventHandler, ILo #region Properties + public TContext Context => eventHandler.Context; + + BoundedContext IAsyncEventHandler.Context => this.Context; + Type IAsyncEventHandler.EventType => this.eventHandler.EventType; #endregion Properties diff --git a/Src/DDD.Core/Application/EventConsumer.cs b/Src/DDD.Core/Application/EventConsumer.cs index 18b1436..e3f2fa2 100644 --- a/Src/DDD.Core/Application/EventConsumer.cs +++ b/Src/DDD.Core/Application/EventConsumer.cs @@ -22,7 +22,7 @@ public class EventConsumer : IEventConsumer, IDisposable private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); private readonly ICommandProcessor commandProcessor; private readonly IQueryProcessor queryProcessor; - private readonly IEventPublisher eventPublisher; + private readonly IEventPublisher eventPublisher; private readonly IEnumerable boundedContexts; private readonly IKeyedServiceProvider eventSerializers; private readonly ILogger logger; @@ -37,7 +37,7 @@ public class EventConsumer : IEventConsumer, IDisposable public EventConsumer(ICommandProcessor commandProcessor, IQueryProcessor queryProcessor, - IEventPublisher eventPublisher, + IEventPublisher eventPublisher, IEnumerable boundedContexts, IKeyedServiceProvider eventSerializers, ILogger logger, @@ -70,6 +70,8 @@ public EventConsumer(ICommandProcessor commandProcessor, protected CancellationToken CancellationToken => this.cancellationTokenSource.Token; + BoundedContext IEventConsumer.Context => this.Context; + #endregion Properties #region Methods diff --git a/Src/DDD.Core/Application/EventPublisher.cs b/Src/DDD.Core/Application/EventPublisher.cs index 36f1205..9a3684b 100644 --- a/Src/DDD.Core/Application/EventPublisher.cs +++ b/Src/DDD.Core/Application/EventPublisher.cs @@ -1,7 +1,5 @@ using EnsureThat; using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; namespace DDD.Core.Application @@ -9,7 +7,8 @@ namespace DDD.Core.Application using Domain; using Threading; - public class EventPublisher : IEventPublisher + public class EventPublisher : IEventPublisher + where TContext : BoundedContext, new() { #region Fields @@ -24,42 +23,54 @@ public EventPublisher(IServiceProvider serviceProvider) { Ensure.That(serviceProvider, nameof(serviceProvider)).IsNotNull(); this.serviceProvider = serviceProvider; + this.Context = new TContext(); } #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 = null) where TEvent : class, IEvent { Ensure.That(@event, nameof(@event)).IsNotNull(); await new SynchronizationContextRemover(); - var syncHandlers = this.GetSyncEventHandlers(@event); - foreach (var handler in syncHandlers) - handler.Handle(@event, context); - var asyncHandlers = this.GetAsyncEventHandlers(@event); - foreach (var handler in asyncHandlers) - await handler.HandleAsync(@event, context); + 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 IEnumerable GetAsyncEventHandlers(TEvent @event) where TEvent : class, IEvent + private IAsyncEventHandler GetAsyncEventHandler(TEvent @event) where TEvent : class, IEvent { if (typeof(TEvent) == typeof(IEvent)) { - var handlerType = typeof(IAsyncEventHandler<>).MakeGenericType(@event.GetType()); - return this.serviceProvider.GetServices(handlerType).Cast(); + var handlerType = typeof(IAsyncEventHandler<,>).MakeGenericType(@event.GetType(), typeof(TContext)); + return (IAsyncEventHandler)this.serviceProvider.GetService(handlerType); } - return this.serviceProvider.GetServices>(); + return this.serviceProvider.GetService>(); } - private IEnumerable GetSyncEventHandlers(TEvent @event) where TEvent : class, IEvent + private ISyncEventHandler GetSyncEventHandler(TEvent @event) where TEvent : class, IEvent { if (typeof(TEvent) == typeof(IEvent)) { - var handlerType = typeof(ISyncEventHandler<>).MakeGenericType(@event.GetType()); - return this.serviceProvider.GetServices(handlerType).Cast(); + var handlerType = typeof(ISyncEventHandler<,>).MakeGenericType(@event.GetType(), typeof(TContext)); + return (ISyncEventHandler)this.serviceProvider.GetService(handlerType); } - return this.serviceProvider.GetServices>(); + return this.serviceProvider.GetService>(); } #endregion Methods diff --git a/Src/DDD.Core/Application/IAsyncCommandHandlerExtensions.cs b/Src/DDD.Core/Application/IAsyncCommandHandlerExtensions.cs index 47f45ca..098a569 100644 --- a/Src/DDD.Core/Application/IAsyncCommandHandlerExtensions.cs +++ b/Src/DDD.Core/Application/IAsyncCommandHandlerExtensions.cs @@ -18,27 +18,6 @@ public static Task HandleAsync(this IAsyncCommandHandler han return handler.HandleAsync(command, MessageContext.FromObject(context)); } - public static async Task UpdateStreamPositionIfDefinedAsync(this IAsyncCommandHandler handler, IMessageContext context) - { - Ensure.That(handler, nameof(handler)).IsNotNull(); - if (context != null) - { - var @event = context.Event(); - var stream = context.Stream(); - if (@event != null && stream != null) - { - var command = new UpdateEventStreamPosition - { - Position = @event.EventId, - Type = stream.Type, - Source = stream.Source - }; - await handler.HandleAsync(command, context); - stream.Position = @event.EventId; - } - } - } - #endregion Methods } diff --git a/Src/DDD.Core/Application/IAsyncEventHandler.cs b/Src/DDD.Core/Application/IAsyncEventHandler.cs index ce0f6df..536d5ef 100644 --- a/Src/DDD.Core/Application/IAsyncEventHandler.cs +++ b/Src/DDD.Core/Application/IAsyncEventHandler.cs @@ -6,18 +6,29 @@ namespace DDD.Core.Application using Domain; /// - /// Defines a method that handles asynchronously an event of a specified type. + /// 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 = null); #endregion Methods diff --git a/Src/DDD.Core/Application/IAsyncEventHandler`1.cs b/Src/DDD.Core/Application/IAsyncEventHandler`1.cs index e264d40..bbf43a6 100644 --- a/Src/DDD.Core/Application/IAsyncEventHandler`1.cs +++ b/Src/DDD.Core/Application/IAsyncEventHandler`1.cs @@ -5,14 +5,27 @@ namespace DDD.Core.Application using Domain; /// - /// Defines a method that handles asynchronously an event of a specified type. + /// Defines a method that handles asynchronously an event of a specified type in a specific bounded context. /// - public interface IAsyncEventHandler : IAsyncEventHandler + 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 = null); #endregion Methods diff --git a/Src/DDD.Core/Application/IEventConsumer.cs b/Src/DDD.Core/Application/IEventConsumer.cs index 11a5287..1abf623 100644 --- a/Src/DDD.Core/Application/IEventConsumer.cs +++ b/Src/DDD.Core/Application/IEventConsumer.cs @@ -2,17 +2,24 @@ namespace DDD.Core.Application { + using Domain; + /// - /// Defines a method that consumes events. + /// Defines methods for consuming events in a specific bounded context. /// /// - /// This component is used to read external event streams and to publish those events inside a 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. /// @@ -23,12 +30,12 @@ public interface IEventConsumer #region Methods /// - /// Starts consuming events. + /// Starts consuming events in a specific bounded context. /// public void Start(); /// - /// Stops consuming events. + /// Stops consuming events in a specific bounded context. /// public void Stop(); diff --git a/Src/DDD.Core/Application/IEventConsumer`1.cs b/Src/DDD.Core/Application/IEventConsumer`1.cs index efe8c57..2c39cf4 100644 --- a/Src/DDD.Core/Application/IEventConsumer`1.cs +++ b/Src/DDD.Core/Application/IEventConsumer`1.cs @@ -2,12 +2,21 @@ { 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 - TContext Context { get; } + /// + /// 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 index 92f696e..27a983f 100644 --- a/Src/DDD.Core/Application/IEventPublisher.cs +++ b/Src/DDD.Core/Application/IEventPublisher.cs @@ -5,16 +5,25 @@ namespace DDD.Core.Application using Domain; /// - /// Defines a method that publishes events of any type. + /// Defines a method that publishes events of any type in a specific bounded context. /// - /// - /// This component is used to publish events inside a 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 = null) where TEvent : class, IEvent; #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/IMessageContextExtensions.cs b/Src/DDD.Core/Application/IMessageContextExtensions.cs index c5645bf..580f9ce 100644 --- a/Src/DDD.Core/Application/IMessageContextExtensions.cs +++ b/Src/DDD.Core/Application/IMessageContextExtensions.cs @@ -4,12 +4,20 @@ 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(); @@ -37,6 +45,13 @@ public static void AddStream(this IMessageContext context, EventStream stream) 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(); @@ -58,6 +73,12 @@ public static FailedEventStream FailedStream(this IMessageContext context) 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(); @@ -66,6 +87,5 @@ public static EventStream Stream(this IMessageContext context) } #endregion Methods - } } diff --git a/Src/DDD.Core/Application/ISyncEventHandler.cs b/Src/DDD.Core/Application/ISyncEventHandler.cs index 03b42e9..36c0b45 100644 --- a/Src/DDD.Core/Application/ISyncEventHandler.cs +++ b/Src/DDD.Core/Application/ISyncEventHandler.cs @@ -5,20 +5,33 @@ namespace DDD.Core.Application using Domain; /// - /// Defines a method that handles synchronously an event of a specified type. + /// 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 = null); #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 index 2f72afb..6b78ce1 100644 --- a/Src/DDD.Core/Application/ISyncEventHandler`1.cs +++ b/Src/DDD.Core/Application/ISyncEventHandler`1.cs @@ -3,14 +3,26 @@ using Domain; /// - /// Defines a method that handles synchronously an event of a specified type. + /// Defines a method that handles synchronously an event of a specified type in a specific bounded context. /// - public interface ISyncEventHandler : ISyncEventHandler + 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 = null); #endregion Methods diff --git a/Src/DDD.Core/Application/MessageContextInfo.cs b/Src/DDD.Core/Application/MessageContextInfo.cs index dbbda33..1ce400f 100644 --- a/Src/DDD.Core/Application/MessageContextInfo.cs +++ b/Src/DDD.Core/Application/MessageContextInfo.cs @@ -7,10 +7,11 @@ public class MessageContextInfo { #region Fields - public const string Event = "Event", - CancellationToken = "CancellationToken", - Stream = "Stream", - FailedStream = "FailedStream"; + 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/SyncEventHandler.cs b/Src/DDD.Core/Application/SyncEventHandler.cs index 1c0ef7e..1586077 100644 --- a/Src/DDD.Core/Application/SyncEventHandler.cs +++ b/Src/DDD.Core/Application/SyncEventHandler.cs @@ -7,12 +7,17 @@ namespace DDD.Core.Application /// /// Base class for handling synchronously events. /// - public abstract class SyncEventHandler : ISyncEventHandler + public abstract class SyncEventHandler : ISyncEventHandler where TEvent : class, IEvent + where TContext : BoundedContext, new() { #region Properties + public TContext Context { get; } = new TContext(); + + BoundedContext ISyncEventHandler.Context => this.Context; + Type ISyncEventHandler.EventType => typeof(TEvent); #endregion Properties diff --git a/Src/DDD.Core/Application/SyncEventHandlerWithLogging.cs b/Src/DDD.Core/Application/SyncEventHandlerWithLogging.cs index 9c0c578..36a8105 100644 --- a/Src/DDD.Core/Application/SyncEventHandlerWithLogging.cs +++ b/Src/DDD.Core/Application/SyncEventHandlerWithLogging.cs @@ -10,20 +10,21 @@ namespace DDD.Core.Application /// /// A decorator that logs information about events. /// - public class SyncEventHandlerWithLogging : ISyncEventHandler + public class SyncEventHandlerWithLogging : ISyncEventHandler where TEvent : class, IEvent + where TContext : BoundedContext { #region Fields - private readonly ISyncEventHandler eventHandler; + private readonly ISyncEventHandler eventHandler; private readonly ILogger logger; #endregion Fields #region Constructors - public SyncEventHandlerWithLogging(ISyncEventHandler eventHandler, ILogger logger) + public SyncEventHandlerWithLogging(ISyncEventHandler eventHandler, ILogger logger) { Ensure.That(eventHandler, nameof(eventHandler)).IsNotNull(); Ensure.That(logger, nameof(logger)).IsNotNull(); @@ -35,6 +36,10 @@ public SyncEventHandlerWithLogging(ISyncEventHandler eventHandler, ILogg #region Properties + public TContext Context => eventHandler.Context; + + BoundedContext ISyncEventHandler.Context => this.Context; + Type ISyncEventHandler.EventType => this.eventHandler.EventType; #endregion Properties diff --git a/Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider.cs b/Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider.cs index db3cba5..dde13b8 100644 --- a/Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider.cs +++ b/Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider.cs @@ -3,8 +3,10 @@ namespace DDD.Core.Infrastructure.Data { + using Domain; + /// - /// Provides and shares a database connection between different components. + /// 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. /// /// @@ -16,6 +18,14 @@ 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/IDbConnectionProvider`1.cs b/Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider`1.cs index 457e489..23859b3 100644 --- a/Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider`1.cs +++ b/Src/DDD.Core/Infrastructure/Data/IDbConnectionProvider`1.cs @@ -3,15 +3,23 @@ using Domain; /// - /// Provides and shares a database connection for a specific context between different components. + /// 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 - TContext Context { get; } + /// + /// The bounded context associated with the database connection. + /// + new TContext Context { get; } #endregion Properties diff --git a/Src/DDD.Core/Infrastructure/Data/LazyDbConnectionProvider.cs b/Src/DDD.Core/Infrastructure/Data/LazyDbConnectionProvider.cs index 405900b..4335e39 100644 --- a/Src/DDD.Core/Infrastructure/Data/LazyDbConnectionProvider.cs +++ b/Src/DDD.Core/Infrastructure/Data/LazyDbConnectionProvider.cs @@ -37,8 +37,11 @@ public LazyDbConnectionProvider(string providerName, string connectionString) #region Properties public DbConnection Connection => this.lazyConnection.Value; + public TContext Context { get; } + BoundedContext IDbConnectionProvider.Context => this.Context; + #endregion Properties #region 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/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/OracleScripts.Designer.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleScripts.Designer.cs index c6a39e1..569d1d6 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleScripts.Designer.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleScripts.Designer.cs @@ -314,5 +314,18 @@ internal static string UpdateFailedEventStream { 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 index 129ada1..732bcd1 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleScripts.resx +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleScripts.resx @@ -169,4 +169,7 @@ 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/Scripts/Oracle/UpdateFailedEventStreamPosition.sql b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/UpdateFailedEventStreamPosition.sql new file mode 100644 index 0000000000000000000000000000000000000000..9a7d65a1b93d50630086d5f97801bccaa822a2a3 GIT binary patch literal 1778 zcmb7^TW=Ck6ouEbiT{B&Btb6rW;HQ^VXzaSB@8y;i=lu@!Si)>!n^O!wS`srv~zh+MGOHv2SvErJUY7J0JU6ye~RQpqZ8$ z>0T=^#++7~v+Lj+@-C2*Y#R7^`ot;F7j!xJ zdr)5!{bCN5j?JvzD2sKL?wM?f{yV!Wv++CDSo}u3zX1P2B_4ZMbjsILnN`&#G2XIH zkeB$@+}G!P&gqJbdsHl{M&M`|9_=7&(-YhIA=I$NwkhHs;b*Z-3oa&@8tR|5KLQFI99M zD8fcL?veGUNLEeWZB`ckjHghK)M~y{vi4zVz<#l=)UupnF>oE2Q14xgXg5H_|Csn0 zydHS$9*8C&U0Z-%WHH9-kzD%NM-wuQHT%1AjAsG%7c9$h;QEnpwcWR97PqD#hJC Yi@#<5<~(h($ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..076e93e8a272acae8ed48eff367d58df78afa9af GIT binary patch literal 1980 zcmb7_Z*ST_495L>(!N996{La!v|Za5lLn<*rM8r>OY(MrnfB)P|AZ_vFt8^rkNape+Z{>}Al+X5V%Omm@8j(zS57Fw%OfGoN zna|9KJ@?F&+#2c~n2Ov-YXWYS^(FcPL+OM2!pSi>0dr=^BlJ8eq3P~jf$=1VhUV^f zWQkW{p)A1WFHn117|3s9qf8|{$OpNWd#Wg~in)~osWUY~UFZF4g0XO8B0UXKRcPy+wo#H24uwpCGAUDHEr^s-68H?+C!G`*##%QucJg# z%-}vJt3*F#sINUxv%KaxF3MZht3H`eeNaL&tKK}oKpdqjD@obmV;7s};B4p2qn35N zM{6IXOUsGoc+QD2U;2*d9U8w@bzF^A`^R|5QVSlL9$UsHbT^N<#te;AbGY7>6;uCM y+DSZCdRz40kfLR|-sSb{)wF*+u|?nVa=fZ#?HY&l5`AiC@srHxzE7UgKkYwFl}h#i literal 0 HcmV?d00001 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/SqlServerScripts.Designer.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerScripts.Designer.cs index 0bdf6ba..7095a4c 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerScripts.Designer.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerScripts.Designer.cs @@ -299,5 +299,18 @@ internal static string UpdateFailedEventStream { 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 index c95b5db..0d2a903 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerScripts.resx +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerScripts.resx @@ -166,4 +166,7 @@ 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.UnitTests/Application/EventConsumerTests.cs b/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs index a9e0f8f..3dadf10 100644 --- a/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs +++ b/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs @@ -385,7 +385,7 @@ public void StartAndWait_WhenExceptionInUpdatingFailedEventStream_LogsException( var eventSerializers = FakeEventSerializers(); var logger = FakeLogger(); var settings = FakeSettings(); - consumer = new EventConsumer(commandProcessor, queryProcessor, eventPublisher,boundedContexts, eventSerializers, logger, settings); + consumer = new EventConsumer(commandProcessor, queryProcessor, eventPublisher, boundedContexts, eventSerializers, logger, settings); // Act consumer.Start(); consumer.Wait(TimeSpan.FromSeconds(5)); @@ -466,11 +466,11 @@ private static Event FakeEvent() }; } - private static IEventPublisher FakeEventPublisher() => Substitute.For(); + private static IEventPublisher FakeEventPublisher() => Substitute.For>(); - private static IEventPublisher FakeEventPublisherThrowingException(Exception exception) + private static IEventPublisher FakeEventPublisherThrowingException(Exception exception) { - var eventPublisher = Substitute.For(); + var eventPublisher = Substitute.For>(); eventPublisher.When(p => p.PublishAsync(Arg.Any(), Arg.Any())) .Throw(exception); return eventPublisher; diff --git a/Test/DDD.Core.UnitTests/Application/EventPublisherTests.cs b/Test/DDD.Core.UnitTests/Application/EventPublisherTests.cs index 389c5fa..e98c06d 100644 --- a/Test/DDD.Core.UnitTests/Application/EventPublisherTests.cs +++ b/Test/DDD.Core.UnitTests/Application/EventPublisherTests.cs @@ -14,17 +14,17 @@ public class EventPublisherTests #region Methods - public static IEnumerable HandlersOfOtherEventsThanThisEvent() + public static IEnumerable HandlersOfOtherEventsOrContexts() { - var fakeHandler1 = FakeHandler(); - var fakeHandler2 = FakeHandler(); - var fakeHandler3 = FakeHandler(); - var fakeHandler4 = FakeHandler(); + var fakeHandler1 = FakeHandler(); + var fakeHandler2 = FakeHandler(); + var fakeHandler3 = FakeHandler(); + var fakeHandler4 = FakeHandler(); var container = new Container(); - container.Collection.Register(fakeHandler1); - container.Collection.Register(fakeHandler2); - container.Collection.Register(fakeHandler3, fakeHandler4); - var publisher = new EventPublisher(container); + container.RegisterInstance(fakeHandler1); + container.RegisterInstance(fakeHandler2); + container.RegisterInstance(fakeHandler3); + var publisher = new EventPublisher(container); yield return new object[] { publisher, @@ -35,68 +35,68 @@ public static IEnumerable HandlersOfOtherEventsThanThisEvent() { publisher, new FakeEvent2(), - new IAsyncEventHandler[] { fakeHandler3, fakeHandler4 } + new IAsyncEventHandler[] { fakeHandler1, fakeHandler3, fakeHandler4 } }; yield return new object[] { publisher, new FakeEvent3(), - new IAsyncEventHandler[] { fakeHandler1, fakeHandler2 } + new IAsyncEventHandler[] { fakeHandler1, fakeHandler2, fakeHandler4 } }; } - public static IEnumerable HandlersOfThisEvent() + public static IEnumerable HandlerOfThisEventAndThisContext() { - var fakeHandler1 = FakeHandler(); - var fakeHandler2 = FakeHandler(); - var fakeHandler3 = FakeHandler(); - var fakeHandler4 = FakeHandler(); + var fakeHandler1 = FakeHandler(); + var fakeHandler2 = FakeHandler(); + var fakeHandler3 = FakeHandler(); + var fakeHandler4 = FakeHandler(); var container = new Container(); - container.Collection.Register(fakeHandler1); - container.Collection.Register(fakeHandler2); - container.Collection.Register(fakeHandler3, fakeHandler4); - var publisher = new EventPublisher(container); + container.RegisterInstance(fakeHandler1); + container.RegisterInstance(fakeHandler2); + container.RegisterInstance(fakeHandler3); + var publisher = new EventPublisher(container); yield return new object[] { publisher, new FakeEvent1(), - new IAsyncEventHandler[] { fakeHandler1 } + fakeHandler1 }; yield return new object[] { publisher, new FakeEvent2(), - new IAsyncEventHandler[] { fakeHandler1, fakeHandler2 } + fakeHandler2 }; yield return new object[] { publisher, new FakeEvent3(), - new IAsyncEventHandler[] { fakeHandler3, fakeHandler4 } + fakeHandler3 }; } [Theory] - [MemberData(nameof(HandlersOfThisEvent))] - public async Task PublishAsync_WhenCalled_CallsHandlersOfThisEvent(EventPublisher publisher, - IEvent @event, - IEnumerable handlersOfThisEvent) + [MemberData(nameof(HandlerOfThisEventAndThisContext))] + public async Task PublishAsync_WhenCalled_CallsHandlerOfThisEventAndThisContext(EventPublisher publisher, + IEvent @event, + IAsyncEventHandler handlerOfThisEvent) { // Arrange - handlersOfThisEvent.ForEach(s => s.ClearReceivedCalls()); + handlerOfThisEvent.ClearReceivedCalls(); // Act await publisher.PublishAsync(@event); // Assert - Assert.All(handlersOfThisEvent, s => s.Received(1).HandleAsync(@event)); + await handlerOfThisEvent.Received(1).HandleAsync(@event); } [Theory] - [MemberData(nameof(HandlersOfOtherEventsThanThisEvent))] - public async Task PublishAsync_WhenCalled_DoesNotCallHandlersOfOtherEvents(EventPublisher publisher, - IEvent @event, - IEnumerable handlersOfOtherEvents) + [MemberData(nameof(HandlersOfOtherEventsOrContexts))] + public async Task PublishAsync_WhenCalled_DoesNotCallHandlersOfOtherEventsOrContexts(EventPublisher publisher, + IEvent @event, + IEnumerable handlersOfOtherEvents) { // Arrange handlersOfOtherEvents.ForEach(s => s.ClearReceivedCalls()); @@ -106,10 +106,13 @@ public async Task PublishAsync_WhenCalled_DoesNotCallHandlersOfOtherEvents(Event Assert.All(handlersOfOtherEvents, s => s.DidNotReceive().HandleAsync(Arg.Any())); } - private static IAsyncEventHandler FakeHandler() where TEvent : class, IEvent + private static IAsyncEventHandler FakeHandler() + where TEvent : class, IEvent + where TContext : BoundedContext, new() { - var fakeHandler = Substitute.For>(); + var fakeHandler = Substitute.For>(); fakeHandler.EventType.Returns(typeof(TEvent)); + fakeHandler.Context.Returns(new TContext()); return fakeHandler; } From c4d2d0e0995cffd0ea211dc18954e0f0ddd44f97 Mon Sep 17 00:00:00 2001 From: draphyz Date: Tue, 21 Feb 2023 16:25:27 +0100 Subject: [PATCH 075/111] Add diagrams --- Doc/CommandSide.png | Bin 0 -> 40988 bytes Doc/QuerySide.png | Bin 0 -> 39952 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 Doc/CommandSide.png create mode 100644 Doc/QuerySide.png diff --git a/Doc/CommandSide.png b/Doc/CommandSide.png new file mode 100644 index 0000000000000000000000000000000000000000..ffc6d536d60620336e4d1d33781ad982b546863f GIT binary patch literal 40988 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

v+qZ9#9PD!&6dGgT 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 a#z7&ZWV2Y{wf+zOs41N!Jvm_(_hf(s>`= z?|$Qsd&m6)KF2x3qlf*AwVs%BK6CA`M`{nT?~>iUapMNIq5@L$#*Lew;a~aNH{owI z-xoH)Zz!&s4`pwZeWzH3KismFQI)xIqcR%v+yV{$eCN4>f$NPMI33sjqI5eITHm;l zny-kI(e^Oi{C&q*_2dlWxdD?6r)x|409p*Yx;%TG0WwtLr~8_br9Za?@sq^xLSK$|@?6)zz-U{JojU zw+PI6adC0+n1ddO_YV%@cJ$h^H{T;eC^;y;<6letpztrL;Og>emV649-?7@e!SK-d zyX@$6HVmcFAJo#><|#zV%vzs0IGkRj7;$+WZiI;#85(|1Ms)P|_m7V1I7s=O3(N9b z#*fg+EFo^)!&AdxGFEUT3HY#eMKZ(1Q=~v?m_Ft7z zTxU$j4X%Il{whsRPrJFfNc1m;f~L#UvYz$j39IW zib}$3aeG_JW9LtF7*BvZIRypBgDbt^sW#g$)6>6yA8q|+|9WwhenZkDmQKD?m`;UI)(loo$}!cnsyp zE620YmY!(@XAynryG*^fH{0REBZT!jBt(f5nJ24{xW`KASPKtvqu9D6SzasW@j%KP zUTz+bY806O{zD9gcR0E+`x}#5H4gR`yxPnrpI;_Cn4Fx%rV{sXpu-v|Gcj^d$65C) zl8jl7J12=4nIo91ZSUxa^GjB^RHHcw#6jk1!(Hq}FH+8UbQy;MD{mH zeO)Pna&vP@cDIL6huLOlXRn5GA8LL^o(|2OrBlqdVIc@sh0$LR$KEUIV3zf?u(sas zpl6dloqQDjS&pZ(i_8pXqt+FJgYPT52DCuYiep#{Nm?@x)J9)ni#{qc(ZA( zF-%%oTH;UEMv8{h^7YhTmJo6JDOQyf1Eo<>T{Q{>jMo~G!{MS`Ie70q{wN&zIt z7L6Sxf8I>$rf#i^XtCa4s*uxsXISgyX>o3@)#sO+DS}T8Tm3E^MnCElK0d^wBh*=C^N6TYOGG02&plaJey14Hd66ANv z(B~CK#a)_tp_hczk zwDMrR&h`1xU8)_m9lxEjotYh+UCG^NyBYaH>M3`~^(KBPItj%bRxp*e#?n@LhP;lf z;i%1{%Wq1d=$nT{&yHYH4Lb5XKXx`a{P=i-pomKst4cOWcJ17vUQSou>C6K6fxrAo z*c~1Y)iLOu@uH$G>|QfM-n@A_!b&KtDwK`e!}-dUwMoaAh7NDUd~hRFvt@(dv4ub_ zSLBP$+h3q&l~z#biKTy-$YH?iw({tlmQCm5YaN$`uTdJjITSuumlA>(X9w$l*Vfjy zx35-zesDTOd`lP8`_=s;_aUx5HR{;XLh{K-)m13|q3ZL* z=xEKp2`S?+6$q~V6c(mvZvJPylNznuto3bD|3tRXEJb%fbU;1xR}EcAM=_5bn!5g> zTu7G$A*UJLQo~aH%9{pv{0H#pADDASNUe0d!S(g=seAVO^g%gA8Mca(jSVB^ ze8Ahc?Q{nRT#2{rsmU7CN9Z<96n^b@-x^-GJc%{bq*D!Jo=<0%N~B}-=z_QE`NS8r zczbHesLqj8!i&G|_rbwICGA_4@K!X@QFyrAoScfs5-&F5rdDR(4px^D^+Og?9ELj8iLXnm&@}o{MlN3KKAr%$X?1n~+Urq;>d>ZeF z^w7{y3CVpLIy%d%%ZoTBRfm}-Pe_oBjg7Zd5)O8D!k&B9j_49z`?rne{w)v4`5Jss zqdZ6J`Z+=@NiWGNxeaY%cXAi|k9S*r;P$OqtQKk?<>RuQs5$SBdghD=AJNynrKF^+ z8St(!4b)CW1oC~4C-!BJr18J#S$XkXFJq z;+?2!UzQ9lu-*~s%KT(In9=hhXSG#DRW-H`9hWL{7)3&+rf=h{DFu1DDSVdiKgzeRAKP zSsN{;q@+|%;`&|p)QFmz8WS_(_h)yj&$o<*S9l5+{f<9mJN+?gcMo>Lnk8?M@Og90 zwCg)>)a4%GL^8|Q@)RM!ihbUTJpC~&`=GX}s@=gT@(kzlZIAba+n2jW{ER`80ft`y zBu}dIGoD`dJUq5wyJRY7lCgZ~CB)0C)OP~7e4_5@6t2*Y<`r_5`XsrmV=y6eY`om8 ziw4hzY%i(4{-_99Sy`!yZ(wAkSE8?>lD!|Ztj)st`t|E9H{m!1BcsorIYgX>E32zv ztf2~Oz$$vbK;v%4H)-;ywCPVA_A2%|ShK8A(kKn5ZGd*JRQszpj+va_E?`rmQ0v26 z5>@2;j_^R11qfxY#hzHJ?za}}b&wGHQne1FY_@8nmzC--*h3iZ+_}T7mL`qjetx_= zF)<WZ7&*|9t* z#uF$ec?Z}y>w8cfvpUp~O05rSFJHd=_3M`r z{zc0>4rU03u*k^i^p&qs7r3oP>|ld{pHJ-X78h|pC8ool@fq8V}C71#x09_?3j1IrR3z`u(Yy@rW9M=+>9Cx z3Q5AewSu~GX9ahKaD{w@c7=I`(~>{7U|te^Sa^H36)5KcJlw(l{^`;7Y`v42qfUf# zr!Us*m-nxk9OQ?rfKrm zFT+Usci5*`UO1BnE77_xNl8c-pUVX zRqCOWl=K4_T2@x};K2jHYMUcPaU?Q%Ey-jGPoGX!*}y-ZdyAxkj{1O&k?asCa*d8t zbx*s(37wstq493RYTGetK2*WZ3mGdj`E1>b4~UXs_vPlVFx_|9>>*gfZY%fteuBgV zzz2tfN4f^Th`0bF-qbSAd%v$|&5)=*)0x^A4 z@Yd%i|FE&JAO^cH?!NC1m*G;&5N|O5@~Wq&XLq3+ikAsoo)<1}@i3YyozE8Mbm#RQ z0*|IwO@NJaVL1j;g>cPQ^AzJCN7)U-vOi8q>UZ1=oyfiK2Sz z_jZQs!97-XU97x3s9V|%E=y3sAbYS$d6Czfy8m?l`M&G^%Y9F{U_3-#yxOBjKVb2I zQiy;$!=#!Ls;hbbk}Z}K(w-0EDn~xz`+L6bZ{JjE@L37AtZ0^Av1Nm%G&VN&cQDho zK{^GsnbMa^Vs?6(LOBq^3`8vjgX7avLw$YYW-kF@VGpQeu;X!Af1&1)E2AW$ap1w9 zMPNzudF+Q7#KgpGZT|vG)P}lWgKtl@@6UmzPgH?4Z@5>AR0<$OwW)^eAOEPsE-o(4 z!cwT7DG8siwlHYJ4_s=8`}T+0G4|<-cZO9F2xbd9PE^Coy z65tVRoAI$8P#r6)YBk_^L>Q=ra@UtnO7R6uP2h8L=vQ{uM2G89cMB`Pdz zXK(L&d2Ueik(fhOzo5d};-p=Z$uO)6pB=qV7I>?(tLtaDHjqns0u3VXoj)jJS*RE| z92z;DkdLSPtN)H>{Ue+mFs|>(Vbj#g&UynriVr}zuBIk3DyrG%#3fzy1(IEZ=nTju z1UM9(sT%vAKs%^}pSOXk+tt-IGUB#7--SRREJX!gEnzB?CJJyk>u+6u3dG#KdherW zn3$N}-e*wGm%jB%J_skcfB!Wb3ZpD&YI3r&(8}weGebyOrF+400E$q?crot`?F2Ym zny|B_wRPUU@n~>9RzF#yFo(PTDUv<536UC?1_F>PEC(bCsfbIS9i3)qax#gY-uUuB zD*Wp1?sl5~gx3M7VQ6TGcK3coRaK++QCZFGI&5zt6=bBfogE3t!WrfB6`d9h9m8*I z9`WP#60)PeBVV+8`0$~bnVG-;71SY!H+fIE)xW=kXZx#hz~$T8+CZQI<(8m>PB|JkFhbof6LgqWrb{yR3b?bHxpj(y9op=;V4rl%5a3~Q^~W(ra_{ew1pUUgkS?> zQBM=rsIpds^p|F0VgmGAtX+h^76kF@zB-)mI9WAZ2t~E4vy&g{hq5v|EiKAsV`C%8 z0SF@evBP*b-Vyg%4>gp{F;bEM^4Hp6s%yd^z76Ov(e%QEC>O?p{J*3 zJrhMP^p*r63xCJ1jWv)Wz^-3G6VmE)0tqe1$H!M93Gz&%|CQee_B9sTffWJBA`aaX z8a%a>pRrfjueQC=eEfc<#79HOc##qk7A+`mi_6Q)>+4fxCSpU#Je3*fK>Mp&%*@R3 zc<}LQKpI&xK@fZHP?7-^d3t%7HoDzG)hX8X{kN>@=2jzmaUT6&7v=Z>)r7#q>`vb& zNHlqQc?;AsDEv8L+L>O{dh^~aXi%g>0zo2J!0Qkd6(wL+i^quqR?)xRdSzOm@_sN= z%IX!A(xQQ|FNLqY_E)mY%cBQbQbKuOLk81BNuJD+$7GnMfB3e-m?mKd_9&c?9fyqn zHe)-W6K>O{WH%->m>Pf_#qNJ`K&)GQjqw{?Ka7;2SG=|q=^75VAyO{0V^orYlJ-nS zMn*M--+rQE4hVrfLG19)u`wVKfI)4bGJgh{P15&N!B(@gvvXsnd1_#QlYxOjQgUWu z;uB~~w%^k;pSHIjebh}ex22+#3#<{KtVkrD2aPNJNgk_)-IIT~{xmM{3df%Rr zDCm+4I2(BF7xs9={fl;?mMmsEJ&^P38f9 zz$i+oahtIn5M*|ho3pb)g*l26h6({Wxfr1MeC4E%b|%3~#L9?78;+rzFa<&AkW5h7 zQU#xq>6URC)!kQRTc519eS=G#A>#TN<}?+f6R`7+PP+yEDT>O<#jyOa4r*#@!!L3p z2-(L#T8Lx=jt>xznA`Lmy4bZk=H&sb9kAo*XZP)xm>9s-%1LiD<@LiWIjq9IxGYB-6zj~k5N&BKn3zW-kF2IuYdL% ze^5_VKd};N)$nx~o?sY;D#HPJ4NkKykPonFp-egigoFa1D~Di{1Khl>ulm$Yeivk^ z1^}gYcXmiXeS|W|teTSNmvpzlZD?#PL&%Bh^p3+rYU)g8^^E)6rWE;d9v;VV1s+|% z-2MH^P)7mqfpGGL?C9HjzWH9Z+sgcjPdI+-e}(4s5xSiNV9qxqVI20>)(k1yswyhq zAT2Nd4N764qobptWm~d9LxyXps;a^oM_Q=-1Cqd^h3$~T-UTEF|063is}EwKNWYR!^c&PV4bh3IsZ1$E! zpk722P>7f|c{u<5PHAOjUo)U|oF1%0yBB}Cp>Jnbn4N6_ z!IJ-(2+!QNw}$2fa6W|P-TN#%5Iny>Iir%#LuDw0=HsOYVLMdxNZPy#*6mxSloZe< z;HmF23SH*s=b@CCWy_-ZNDm?RDYvY$s7XZK?DX|X>eAwyjHL^%ckJ)?^oVa&fYgQC1%f=Eav_!9?zquzCM%3;#FK6K|k6l?ftv*;rGAL zhrsQ^EJg}IegM8xSzBAGQ=G_8D)^(aq6S$1T^+ncH=kP^2l|Kd1Virnh#&P3XV(%s ztRwqXy2m4y=~jP#P!wLQjmU&*OF|Wd6|&v|fIBVOzPbJ3nRc7-V{C?HQKDo}#kAxfYJo&)TFzWC(HlUZ1Bc~oAS zJ+ZYQ7`Wh22>x*nl-2a~^klSH$Nhr?SjMS*WnuszmltQs-QQk;YydX~1&xN2 zcW!RZ|MJ8Z%7XUpSkm!r{lk1VV}3lw4r-3oE9liYMt@`5Oegckz_62B^v7doqQczM3LOXye2M?=TtUJ)zX$>Q#LaODZDlH5-2v54yOb*9YIJ0z{b!C<_SQ%+5*o0xQiwiXnhj9 z9!@k}fp+%xK)P5x44hzDAVa;*4xhqx9}$5*;=cLoTC+%If~qMWK_u^f4d$VIA`qf^ z`))QNHW(r(?*orAvKSg3p+(3HD?qbtd)x_??;V$3R$2?w3@#l2QuPe+9-E$aKr>0D zrB5N+c!)?(U{he}p1?bUPRoO+dIg{W}m~(RV^EieiV?A?J^84#^|ZVq$hq{@G&robKCyOGZF5d-yxx7i0wi zJUDEvs!xcfoS$@}blE+GJ$M6B)}duhTs~OFuN)4Ij-G{<1E0yiS9C23LQ|B;{kN@yxu?1(Ew5tqvZdDWxbB9Gz*ewk_Qp|;lpSGC?8P(=5iSOpYJ|| zOx^X(NmN}cyZza1wF*$Qz1Pwk+*ihdk+hb>+UL%Ca%gC1FE)RTc68)|xSFGAZ=U*X zaBv$kf(3J?*_+wK=34O0jd|(5F`)s%9JC94L&KE!?;(J&ZC>wpLd*C!bC2}j;ieHW zaT4?hauO1nPWq6cS1;{vzB2)O+#N*@f{~P)o?A}>yIzhBbkFN9M;THRVJM@nUJg+- zynl`9Vd}}7DG|sK2#*)oM4O_(rx=WCAK%lXQ1T?U9a-YTW&G(;o#P~wx><1V2n$aD z1Dl#6Cx3d6i0BZ&{B=Ca%LB@6hCZ1rEgr>t*KA{DC|7=myC%!zR{R}6ULZ-1L!=`j zai(E90!TKBDOUyUflAWB)aMdlp&BU@|y$11?zl#d;raA zLTD-_ut6H#O>OG=IDwq1u#kgN9#egOae**^0V`yRgQX>1h9YSdfPTQ;S0{fn=`M*4 zA4OS|gt8J`UY>13SpmV&2E@f-Y@Yl>sC)}%uF+AF~_?LdZmUhfEWO6ge%TTtONgFTXV3t@A~=` zKtHZXJh5*TMxxUFj1^rwFOJ~j0j zTnPXKiJbd*unRi(>6ko)Uv7}}-PG=dO7869(q1Q)h*qQQ-kt_b1HIxtSl_%nJQQ-{ zF?jFn-5G2h<%d1Hsr&Wx^iqgS&bphO+*AbW4HQ{TINXBrcOYb_)eGHz?gi9Qrf|o*Yfk zb9F*Yf@a%Zg_XS1UWsG+_Rhj&CL?rH8f^iTwEXt#9e%H|Bv$mPpiP*-hQb!?k^;6A$bT6 zC7a0|)PyfUK{V*BSnXgeU1P?~#DuO~xzDi_h(=(57V#0kMW~Oigk;1F#ER=Nkxo@; z!UF*Eu=NdqL>N@D05~a&i6HI@bV9MfB;sIERon~SrWl>Lt|=VkD*mg(jN*Eo47i0@ zkM2QNM&8wsIN$%N4%LntNDH7S4FsrH1+cY!o>~Uhgo{MhBzSSZl&616J3ImF%r*Cu zIgIr))(3G!@F7ww2O0wHf-&%6vab(x^!1bZ!>bfvI;4bvu^rwAjQ=<)V4p#>__;IJ zakMc>9}HL?rV=P9D4K<7F%~hlNNIuKTcX%v2z>(QT#K| zRB$NtI*FT9S@%NaGyU{D8y*t4QiK1Mq^32@8oKQIZoUi*cie0k7L zpqK+yypQOef`{D^x{HlnYVujI#o_7GrwCaCXyp)=le4p}PVX74vDNmBlo5Jp%O!W9sZ7iUMrxp6YMK2S2@*WL$t9k`50$p-4pn{3*J zG%38dp76T5x|R%+2{U?8qG#Znx2GHZ6 zjhR+j2Dh~Mfi?wX>-DgVN_=EuqAv6SeJ+*&<@w*NdU#+@PrFQ9TTB@ja~;IKYaKLN(6Kls3aWeY-4}|EW5%fg`WqU)l^~X?yxK@ zzR(w24r-{h%UY}w`WJJv!t#-jz6CBBza3PgUR)mSJjiH}JI8u@Sa^9|01bhV%B7Km zggUlU2LFQyAD^y(9!(beO{=n(`Ue%tE3kY6V z13;v8pqSs*peQk{y;~dw;~GGm>#M5?oJK@Z`VsNabT^5Jh+vubc6NS@j>>nLKYjYU zP=kKPRr8D2(bm%^PtH0z)cEcY(l&Mid!wMBKoZzDfkXuAUT=RtW;4j@jejK34+00g zAk#|P<wz6525jvqDQtzzDEIK=u0*G=xew3sq0sy zaTp(267e z#@(F51p{DiKtKRTbIo^X+e1e%1O~PxF@z+;C6t31z>gznAMyl6V6JnZi{Sd`3bX`N zWoQXZSF9)vH!FP*#dU3_(?!O%0RvBof~QNhpu^Bgaedk;7!e)<+`jwSIX*5hE`LV76VA~Fu^16y$El5z44lY(Nfb{ zd{1NPx+#G^YOrr*0gx5SuIo}CA*l32@r96#EI4_yG@xk&41ekCTWb%-1Z*-Fyj1-g zm~P%rR!l-RhLkEQQ)SUf{BkZRBDCDJVyttehpbk|#jhQ%*@D|HDxNLMehly}aET57 z{%rzt^%^GAB71l^$Qy+jg}yA%0iXn@i@BGD65giV1K$C}3Nb=sep3?!UkO-gytqD? zaTC4-f>BaYecCAqBPl3Aoq+YAh8ML&Mn|`L?9vI+Ku|N+#=Lv};)Oikd(Nes=A{98 z-QM@$TC6QS+C6%7Orr;b>I8Wy;HFxgo1=Zj@p5g174&v-CI$wmK#yUD0+}XmEi(yS zijr^!9vqoQdGqGY9dPv_RrU|qc@r8M8g47t?CRao5wIW5+u3;@OnC>gD%!>m#66(3 zFdA6_g$8;UJU-`xI2XI=Pl!u?u1VjZLg<~)kI%086qAy8>ers0&{ow#eqAiK-tqDA z@87v#00?vV97S3H2te*2s;_-ml&pq|A}a5B(flbH8wBEmg5-csfCY}xa@Yfeavnau zDWKSH&ib12B_!JKf`fzM3<8jFqzc*gM5Pt916(XD?dN#!@k^+_4GaL<9xqTOhwO0z zNCFf8rs1Tu`Pk@>wm|V=49`K#(hD>{|0iogp7D+UcJ5s#n*nA6CaFqip2MeTnXX%C zXcj*=0o#HKe*e18g@W}Ue&^+*;Xv+*s{080rkI#}T3SDWc%;66AI79f2@j_NLmAMQ zp?PRry$<&Nl5H~gFq+>I8HnVnGaYl5u)feEbjt{P|(0; zfI;nnxF0pn7W;17wOw#23<)UJefit_3<+J z)HCbq>#w&UY~4=6?|fo<+6THVU_1iVyh)%G065)%{6dY2VAAvw|EwuszTgPY#6hfp zPfX05u2#gB^tri$+@R2BXn;E=q`-2<#Ks6$AlFk3ygau6VL_a8S%508{ic?z)rk21kRT-m4(P6bI~$#l3F?E>v~SU^0?pKWRY(*d|STp5z< z%=94VUJzU zX>^Knq(b#KftDPAyaNE52w`6Vq57JiJU&nQMI+>$&xSw z)ZYw-sV!3+H$h(VQ?Rtg~uaxs7* zJ?sQvXjZu2@M=7Oc{(*?BWg(>64ng6|T7Q;}mH35F~9?==|e>g!K{IVSIceeM+q zKM)Qu)>b6H0E7_xYv?xYPJDs-J0EJ)$HRc6oC-Qw>@xyTUMFMew1e9Wu<^zD`MYf^ z1xbt=W!JVg&?FE8LYuQ4%wP_tldKI@45G!LBM1+Ozr&8sSOtSRU=^T@geCKjLW=`I z@!(=L{<<<$Rq-MaUmoc1`5uEq6+}2uklH~zii(T`<0oTj7fh3Up*lo|!9k@~ZZd{s zkG^g671(ZL_4fLUt;wAj3P za2}y)p25j49u7hlsT$BQeH?sV;>8un;vkujlJ^m;*t%Fs)-d*fsqB3e`;Uc%EURvP z(E1|^kox16`7o+80kdUEP^IoyFT=?G*oFF8KX%Q$+vuPor_^BNfO=X;#nZfhUrMTV zQC-nx^H(hfE>+F5-((%h!vh1@#r`9Vkb;^90S~mfBq>k8_kp7Q_U-ix#P)Gf^XTC#SvqHjg9Sj3l-c- zZrd{#NoIa{yRBu$4Z{G9S1V9Ue0M{!FFFw-`}_OhM4W8cR?^DK%7%#b^>t>aq?b1o zEL>QKD_X9_Y-e7@R)OndWBeDuM+;+P@$O=>c0QToRP0!Nr5-i-9hL0%`+Fz%3^Y3P zDD)955wMz&>&Cb8ih$My9~=R8K%R4#G{j1PWSQb29zdkq3n^{ad%hdZ8tZ`86tkb& zeyyp7a)f~S`^IcgsG=4v_1@k2K8SkF377%_K=Qyw$99K7yrZeVt7ty*SNhK0l5$BC zBgn)5j*gQ2d_&*9Rr}>hqe{PN7#bd~G{coZrkQLE#GvBcB>=-vf;qyWw-3N+F%-`W z;RXkJ^7i0dLOlbv?Tr=`AKa#*@DMpkq%QIqY&8mOwFTfXK$Qe2_G~3g#GHl<7i(8h zFxr8GhDs0F6nNeP4j$f(Ry-pId*%E6g)V3mc$PPEd1RoMyc?vw^Ei0^4}`v^FdrWJ zyT047>m&XNcE1}1m}Wk^bBBN>G5EuWeZ#7@voA+&vrM-Q@ESC3<=xUpShvFP#quoD zJ@&`*>oF@9794`?9|0H$806o8BLDDEXd!Ce!YxPxw-+yJYifQZeFe~oRQ+r` zcnBqLOrByW85X)%=YMv$DIR4>4|V3!@vMRPJj{hXf&$J40RaJ3zmE)LcuKA@pncg4 zBH4B%?t_VJHY`+;o|ZjFh)ym%xr%(7J@x{aQ-wwkC!6Z>($Xm8W?P$d$%1^`w+hwh z(SJ^@7a@dAc1o{BBOnNJt3APr0CT+tCeF1I`922)+uqBH7}eFJ3IE zCTnVd21qLx0%jET&O8gSrC7sB()q3k;1v-}ec)rO`Uz%*o2aN@RNw%C6V~*4Gzebh zL8&W_loc^J{|E}4ytK451c^L>JqZZxaNHFtXGC(<9rln7?OF#Nczw{3@Zv&%^?zxM zsW(&l2}9#YdY+!1Q0V}eM^{(>1Ctq2W%6}5!a5c3!rJO;rST7aD6k`AV~-nXFt1JY zdBw;WblD7K-J(ff6(y`(Diql|r`e3w?+yjqb<15_{hFq+Q>{ z7i%&XG;zSN#iq@Tp^8=>9v&dz5tH{pD&ng$RKvImAQYI95OI3hOge7R`2oMe6alo_ z=aa?=kF8(6oAdd*Y$>{O9%9dT6sW@%n)G*5H*4)!6v!_A4T*9csWg!9B}>4 zJAWRfZAZ0IQfz1l-RIDMC(3$Oc9scD-ee`W!TLt2Es-SMq&B-jdsWW>iacpeA-lt!6>-%gB z)&mMj_7aVZ377#EHCdEah=FjzvSiapEpD*YfQ5#Gn12A~UZ7L7fJG5cYxz%V=TVpw zBqM2zNUT)ctbKZxM*mB}^s~1le3vuL_lDvH?pMxu!ECubI6};AV{d7)I{hkDc&Nqo z1YfG0PbBgD13mPjNIMbhmbD3&>+FS0%M^Y9Stu3rU_I*GKm6mV`yLy zrjOQpq!*TFkS^xg^JFlD(f@mSnt+tLwNBT)`)>WNZ~k`m>rs@DXB+n~mZeLC~3VuXLl=5X-5}|z1IeSR)_{~S`(+$2DwX)-1bsLtPhds}e zxHD{h#eL5|Y9pLimLGk7$%&91B_Sa>K0DJZ%)&WSRv?1BHo7|7B6>N7w!as~^sXMG zzGGotO33}sg_J-SFj+9g)9IY(*OwHN(5#1}ds1~jzkGESI+wc)W2Y?ZUewmB@`u$- zF08~i{y962?f==nDmJTHoHlU1KDCHYd$BRw+6ur5LPH5;d@$I{OB-BIW-UYcZ&b0k zux=w_-*sA%okmjd&6PDp+sCMP?To*#{vPzE#IPOTD<_WwBwsTcVO z4RolrVfi2~ZFyw8e2j9ehP?D_Z%z(vb*mD^k?35*Z$3TQS3wCj%3YWG&-V-iK9tWU zrHN2Biq+KFGa4ok3i5-bvVXj51zZQnlf@;E(9{;^NilNF^SKzs`^avD9gGEgz;4s( z`R{=|_i0jos}h5V`(D;~hN{cCC*8ss$sx7)NVE4~$LGWSrq7L5AXnu#unF3cQkuCck^R7QXYTx`Niq>eYG1*{m+H4Fw6D}9AN!`*_dzA(e+XM=L(PRE*kM$@92f0W0$6m^R`lvoel;Q zrRYDgowaya>Fj)!=I1%~p`dcND4;Fk?^1Qb}2;;TP}IyXfE=D+G^6R5ye~a^bet)gyA!S1tV1`*LK^M zzU!PV7HJB>-emddlh3Y{P>}G#!r9TR=I=+3^zF5i8hrPBy z7*{4zT0?mKJpi?}2iG2Am9R&Ml2f^hZ3egF8fx~ClAv%IIWIi+knM)gpU;ku3+JCQ zE$SoA5sm6TK54f#3IEMCm#NJJdBgE9KbJxh26Z!eMWge>v8W0nZk?J!5?VcV)0WQS zjXe?O%~2MEMJch9ggQ$W{fa+;sj#rI;f~-7TduVHVGR>IFe5+{Ago-AvFh5HU$sIo z1kJt?d;FM_wchNqVvlOHVIfwkVBPMC*mnaS@6UfPKHyoViJL!5W?qgwSO!<eUFj{JCKlXA^qgeNB7KdnI_|<&;?me< zzE1!#Z)U1jmQ86TW^ftFifAUu?)wq_)X@F^XetfTF4LE!ARwJ=~8ozF73!37Mo%cu;ABv{#%xN6Msr%FsUHN7V;FM zDSXYAEq3#tsH@ki4_;x?WBRYr)YDqGNax)T9t^%+yXt^M8G$AE-rL(dGh^Dxn+i1q z0PPF>>+eoe0~p3_MF-ZaCaN9YIXhD*l=`k z5WSwD{q(n2=z1=+p|P}QgN&hVy>x0J)5ChdfvTTyIs8rY)*68FwQGMC1;xhlGSmEi zXjs?@5FVlERcvYruh$O|-JOjfwc%uBu$r1^$g`twaVF{P&Cuk+#0;!6#<$>mD+FTf zW1Zw<)W9YUo;+4KiVlbx#Pkzz_X#~2`vx;WfPTHbcgX#qlcxYq1SGZ#2Bk82ynAC_0Ci@TR3j(jfxQwwLKw=2&*A{ASb? zskDqygc%wKCnq?x^uc-p0-FyUjfhm3y!PMbDH7n_!_&}}ZIK@%47>?blqWEd1A)mD zeiTLnU_;ktS$FDAHsQ~)`~-pk$eQ4zasJa52m?F7rdyC^a4NzN1X_3l7+)sU$gvW# z+=qLkTAqRW~3SF1zC_q4ewLfaJe%l0)aeX91%FhQ>b}f+~AftA}`L^qwOo(xceIcKv4;rSW!f$(I z7Y~S$FhhF^tN|DZoUGhAIB14s0?O0g-kt&TEa;RlJcOPLD(N41^W+;KjyAZ!d4uck zb7=@@%$5m4FT};i$M2+YT|9;n#`X86q>0eaU){yQ`5wm{ZFu=s9+~(0`qr`D@@e+| zR~PjpIsRZOocoaJ$7cMg0_>71Wq-VPm zPYfbH!`YVr1kE z12{MX!)n^}32Gjwk+l^SB;pp@LT| zVzl44g)5$jp>vQ4JbP@#F5nkL7{t~H!ys^bz*LOh^1X!wn2W*OeDa<^#Wa`)&AhFeacKwh+>qc)0Y5# zhuyV-#{v%7&RaEMx1WklT74&x4)nI3Ez2WnA~Ic?$o8)@c7OUihQFR4Qda}MeyO5B z2o48UI>M;6621k+F(y>lSrVIEP^Ia{el$1cMF)?)^fyKuHbVp)$2O9T9S4tj~5tS$bUniv{rXNnJ>*etpZr6jKpv0TKl-H|rzem=~iO{+=oB2jfU z?8(Pt+ZMndIP%JVhwX_jM|l{s%XT>cz?iVNEWFG;XXYmN`iR#;>+*^);m3j7A+qKWINF*rks|R29wv_VaSoM1dz(_xy>ZO;}2pAIDRzA zOGp-RFKT5_UwqQ3ud*on@a^;bb`rP0Ul4vseqUm-ZuQ}$);w3Qdr;6v?P86gJhqSl zhY?xzm|weZ9wfz#G3NItmOt6OI;yDHw04@zLqq3bFD~HJ_^!11E=H_=heh?qrl?rf zQBJv~zIy9zI2ot57d)LB7>6!l$dcpyDK$y&LlrO#ULUtU-9I@PH-q!1|5~pu zoIof83mq62ra*p#ldo^VXa!~Ey3WDu8k{FUYObAIU|fKcMev<4p5Ro0FDL+MW0i3P zl^u2-+*aVmlb7D>FjjwmpPbyAZ^t1*a5K-;!n9s8TwXhe{_RjbiMZF z^rajh-770)neVHLxcKl%^l90f#V>kxYTYm2BpXjS)ShdIrg|Oj9B&ij9UBNe4-9SM zU_*hY0jmU*!)pUQf-W7tS_*an22h6Jr4bL;t!)dJxrDl?2~&N%m^7Yot>tW@PP3OI zG|a2q5h(lNip-12L)oEz)5a zxImMHLm=R731vD>dZ33zOG^t554xDRtgI}2MkV;#AY>;4^S6U|&pQrOX*JBt+6c zN;-BgA)%p57Pw(wR37o94e=>9q zYT6a(t=`$O#<@X2v6-R7=~b9fY+mckQJw>T78m52wO!+lWV5L^&N@flXSKf*b68R5 zQoV={Kb^qVu*-egy<)*gKg=yGzI^^X3eg12FL0_Jow%?sd((yu$LsmdH~$`X9XEML zcx~!5kb2~6g}seSw4%yTL4TIMz5a_lvQ|dn{R(}PEnIS*^H@RUwad`I9=W){0qA|zRvu!#tai_P8ez{?=yyMI7(FuvM>kr*r2Nv74&T<;v_D>1P zm_ELCY1(Y!PLl>%^F~xqbclwESC(qKgBi|r98j6twKOF`L{th~@oVKOLci?hmpvKC zMUWb}cNAn%M4bw3Bfl5569gwcn5BmQ!GS>)z9TE0B1O13v4y$Zy=tkveE#NM`yXYm zO#~u!SRC9}23N`&x)Wm>eu<=LMXqtVxJ=ZTR9jVY`a*Wud6i1O=%SnF)pGl8#;&Xp z4(jG-TY5HZd6g3S)wryv>U+Q$KEtYy8=DsrLwZ)v; z?hw_DP6(>h6v^{daXWLr#i}ws;skMHqQCz)fX{@%MuRk!HOk7BApBr#`h$e`&T#10 zo8o;p83H)9BejICR8_6esxT&|%qYWXYtvAZGfxhe>E9g<-11U4H@`40#YWL~-LMc{ zyX^vlNmjM2P6I(~>IGHRMdQ$+q=W>rhDNBF=p^Hjrg7Ac7_x#C*cdOJ+IADIQQ*cv zUiyI;2LvZl>Nof|G$7Ga^W3^M5>vUO$?|XN>$5V{PM0qVRl6PA?~5SZ4r9 z0R+|v*kGV-0XIOldW$puPjc3sJ2r^Bz}x6!Ooj9h=)p{+V|WDP|K?hU!RGY*Q3q(R zgjwEkq*-V|B8A#d4Msw*>5Xx!@Pv?R=EkIs&%)MYS7SF%?JmgQpcx=LX++}`!|LyzTm!ZWrO)fR*+CI$#l4;{+r01|=gkm|wTvGxyep=LQbeBdZ58HdRZylE&R5G{Y{q5e7UBE^JIV{Wx^<~BsxASH#^K?3>= zlv_-+z2y|T}ZKiTR#NCJ0<1w z7stPRD9gWf)hDBsfx(_^(P~Umy5To8)cb5OC-^0+QAq8a= zm^HdF-t~N_Ofv?bbl=&3PJXwXXI_D<&TrGJvf{yM>$fVuo42m;Z@8OWd9~8NifN~0 z^hX@4A=>PbL}9Kpd$Zl<%hJ&oUaoSZO*TE=+i>{2K-4Sl6+<1_>|8basZjo#H3)M6 z=3Y4rthAF>Jzr?#YQc1BdCiARBhj|aK4Qy%E>g0MnA*p$`z|bW2Tax7wKQ#G#2}nm zAoggsVHQ{nw5PAS^O-D8 z`Nk1D%Rt*iQCOcr3B75sLS5seI(TPH<5vnvsyAeRBy*4<1Z_k?%q#U7VW(|{j8cc| z0r{s3onF6udqY`C>d%~Y#ps{@NA`8q&=`!IUP(Cd`cuS1L#gLTJQ3oA*Ak77m6%k9 zs1kw#O1_kg{b~R1&3RmeFfD%u(Xj;x=Y6S9Nv$&Z_zIa~C$v zrB+`)^j9&?_SxH=A2eAWQNeiml$SYtc50Zl(Ux!6CzcXw#4EFBZl%d!_I9M>X{R## z{sM#Emw$qlC8DHxc@9VHyvkk_CTi4#%pYwP;#%TI)@jj!?#1&9*MDp1W-de}MyG11 zB*vwj+})mQI`>Xv)4;prh=zsV#((x|LPdmOhH*sBqiZU!U6t?`J34i1xm_vS+Oq#$ zI!fkG?SpY#*-wAAw_MsfzTiE-RB$5r)vChg;Ts0_MB=S0&EZQDYnzHjb2;1ox#)=e zPeUa$314~IrgAyCmS=KnM3bMZkbziKqB*G%$ZZ8*%=Yi94Lw_B7mZ8|B}WM~;6yAx zKk-&MQH(Kg-_rcA!>pbcMZO(<_ciNyV{_v>Vw$c)J>PnCYMts;hV53rSH23osUiY_ z6+XukeJ2U}_fYWB%`{>hM7iA72pDZKv(VVJ=kn=gWeOIwM&xPHmvwdf_V3q;xd3iN zOtwhddfW`-%rl{7o}e|?#4Kbr1LFaj(g~Daa>=Z|HxsfLkmLKM8fr!>^I)@X7CqUD8%fjRzd87Ib1Ea*$g$;ha^1{4?q!CAjU#BSd3+p#k8Gc$vI z_vLL74=?rgqSJ>d*BO-C0OxUU-`;!F93=wUBRUR~CHsAbji5mTu=3>GE$%h7=s>FM zzjwIw%y+eHiRFh$FS~5nBNPTPBzWA z|Jf)naRZpqqZEk%W(-rxp1Qhq<~(<^vqgC@+*ufSvpsXMPHV*=QItE3&&ax7*Zbo@hpaOE zqP3Oz)P^Oe;+tc|vr|(q80jP*Y|o<)r41MazOc+mg>|=xY3JtMvA(qE^oU((%;5Py zYO)W<%b2kWEb0euHko0TICM19{s#_R!}=&JvCzGS!e3TO^pfDq{FH3{?6H)0KDz7s z_9Hf*jtSGYnInsv2<5U-1P08U+SiDDLuVz7=&+<3FDHNhzV$E&-y}d5SjWJiog562 zCF8p?e>(^YsfvgqDnFC;~q zp03hf|DD~!ddKpPtGY=b#j)S%4a#QG@tm`5+wW<8IV6ExRJ$|w;o9G-t+zOT9gTQR z|NJ3$;3Se_v_7;B@@nq(14ANP=i7w%&LxqE1^$^hlq=YJEVaIIWn~wV*q+Z->TnCs ztDQI$z`lki<)J`{i1_&F6Z@KM?zZ)yW|8Ii#tcEDU4TsAdv_q3^p0ldqcw|$lo?6F zx}0`sIz!WSO>m2hW8*efE@#!&_yCUh6FN;>15Yh^PTo&d^l_aM*tk2KJzM8^_d&FX zWzQ2a)-V~C^!-%o+50}bjl?48QRK}j>& zvvr({<3}EUDe`D5y?))vVbg2jh~#3|&bLoOj;*%z3QJp$U+nulnU@zhd)tZd7+o!@ zYLkc?@%Z<_jd3&T(;;OY=RC|+q*E!kn4i|3Xigr^_udrTP#SLfc)!)b(w#SSPn?dw zap?oZG{U*%7};#JADuJYite1c^#kfIGvvp&XAUg4oA-ppOL4L8kvNu5wd3lw56=2c zWf#}aJ7d$0qie8cc>lP!TXV>qhwWjFO0<}VGEPR0hjc&N$vfAPuDUGzDWWgH`!H?e zKfZ=8-=sdn0{^WCoN;B6F9jIsv(G-TZBV-&5D8%%(Hh=z&1m97pI;7jg(vr)A5%W_ z^NhxaVY%VG@{dj%>`pl!8{s!A`14DBa{Rk0if`-Pl+*EVQ>cwYZx1gSt8GCsgq{)R zHpVQ=iVI(tpH+V@YB+<&@z|G6+w$SuuaNerI`8f6P?k3PH(I34*rVVne^FfMKi3#( z(L4BrOUd)=yEk9%m010Al{qV^XlzvGbmDa1b-Q;x%dKDE`g|Pz{4TZrr!GqJ^{BvJ z-4c)AKr!lx`PD#5^}VK$t@Jmfi(AJXfl#9!*`af2scigl)E-eq{w- z<;NcS6`g7MpH3_0S`>N?d6~8(@grA9@3@B7jpM3jg$$xr+GWhOb{mX%mpX4QMDlC4x>Lr=& zEpQV7%p1M6rK$c+VOXvHgHxmhpBc)e`rCyR!epS_Q@4V%RgYwBXGwI258MoX&L{=E zGDu!TlLG<`N181kpy^VdlG68GaaZoA!HzxmL-#4OT9elGltJ08pmZy*MZ0{i^<6cW ztLX>(Urj4WjeGXJJ~cPNTg{%Z%eE?7AS5}-i2bNO{m*FV!#&te21iNzw)ovv3_rE? z#eoDm?6TDsy}i6I*vQ`I#-)o_uRl!TjE#QIH_6zjZJa zdV1Afzf)hg-wXROOS*Pl`DbU~`>=XyB$Yeq

ghKcF$=BYb+7BPB4unNYx2*pvY+!m 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 Q!T+>0j*y?I9l!Gb0GOkl(f|Me literal 0 HcmV?d00001 From 734ab9f7c37c7d1ee7138af79c12444a68ca5d0f Mon Sep 17 00:00:00 2001 From: draphyz Date: Tue, 21 Feb 2023 16:32:56 +0100 Subject: [PATCH 076/111] Update README.md --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ca21549..4091e00 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,18 @@ The main components are developed from technologies commonly used by .NET develo - 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) **Model** @@ -62,7 +74,7 @@ As mentioned above, the command and query data stores are not differentiated but ![Alt Query Components](https://github.com/draphyz/DDD/blob/entityframework/Doc/QueryComponents.png) -**Projects** +**Organization of code** The libraries are distributed by component (bounded context) : From 7be577754ff2dcb80bc6dbb290b094ef61aa68a1 Mon Sep 17 00:00:00 2001 From: draphyz Date: Sun, 2 Apr 2023 17:15:55 +0200 Subject: [PATCH 077/111] Update README.md --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 4091e00..af847ae 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,16 @@ The architecture of the project is based on the Hexagonal Architecture. The main ![Alt Architecture on query side](https://github.com/draphyz/DDD/blob/entityframework/Doc/QuerySide.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 a change in the state of the application (more precisely the change of an aggregate) and occur during the processing of a command. It is important to transactionally record this change and the associated event(s). A simple way is to record them in the same database. + **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. From c24ca71570fccc6af073dd8494aeb2d55fc12186 Mon Sep 17 00:00:00 2001 From: draphyz Date: Sun, 2 Apr 2023 20:29:40 +0200 Subject: [PATCH 078/111] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index af847ae..d9701d4 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ In a message-based application, 3 types of messages can be handled : - 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 a change in the state of the application (more precisely the change of an aggregate) and occur during the processing of a command. It is important to transactionally record this change and the associated event(s). A simple way is to record them in the same database. +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 a change in the state of the application (more precisely a change in the state of an aggregate) and occur during the processing of a command. It is important to transactionally record this change and the associated event(s). A simple way is to record them in the same database. **Model** From f5fb4ef1594226a41bddc054e778db226d1899d6 Mon Sep 17 00:00:00 2001 From: draphyz Date: Sun, 2 Apr 2023 20:40:53 +0200 Subject: [PATCH 079/111] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d9701d4..ad09b2d 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ In a message-based application, 3 types of messages can be handled : - 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 a change in the state of the application (more precisely a change in the state of an aggregate) and occur during the processing of a command. It is important to transactionally record this change and the associated event(s). A simple way is to record them in the same database. +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 a change in the state of the application (more precisely a change in the state of an aggregate) and occur during the processing of a command. It is important to transactionally record this change and the associated event(s). A simple way 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. **Model** From 3c28ec2cf4d33c0226f0fbe4b28589e4c897325d Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 3 Apr 2023 10:36:51 +0200 Subject: [PATCH 080/111] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ad09b2d..33a27e3 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ In a message-based application, 3 types of messages can be handled : - 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 a change in the state of the application (more precisely a change in the state of an aggregate) and occur during the processing of a command. It is important to transactionally record this change and the associated event(s). A simple way 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. +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 a change in the state of the application (more precisely a change in the state of an aggregate) and 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. **Model** From 4ba82779c6343188630e4791aef25fc48ebae547 Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 3 Apr 2023 15:07:35 +0200 Subject: [PATCH 081/111] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 33a27e3..d97efa4 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ 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 +- an event : a message that carries data about something that has 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 a change in the state of the application (more precisely a change in the state of an aggregate) and 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. +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. **Model** From b736a71da50ced108a280686aae202d185b693be Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 3 Apr 2023 15:40:37 +0200 Subject: [PATCH 082/111] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d97efa4..e184693 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ In a message-based application, 3 types of messages can be handled : - 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 has 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. +Commands and queries are usually handled synchronously and events asynchronously. Some events called "domain events" capture an occurrence of something that has 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. **Model** From e620773a60b272d7d6d1017cc3bcde1f9e9e3ef3 Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 3 Apr 2023 17:29:49 +0200 Subject: [PATCH 083/111] Update README.md --- README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e184693..c87bb4a 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,19 @@ 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 has happened in 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 has 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. +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 plan to 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. **Model** From d3e23a55e980d21157573bf3127ca0e306c37752 Mon Sep 17 00:00:00 2001 From: draphyz Date: Tue, 4 Apr 2023 11:17:45 +0200 Subject: [PATCH 084/111] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c87bb4a..83e82b3 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ In a message-based application, 3 types of messages can be handled : 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 plan to set up a delivery mechanism of events, you must take into account various concerns : +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 @@ -55,6 +55,8 @@ Domain events are mainly used to decouple the different parts (bounded contexts) 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 the subscriptions to these streams, reading the streams to which they have subscribed, handling the associated events and updating the current position within these streams. + **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. From d304a6bb095f52cfbdcd4e37fb64aaf42c575481 Mon Sep 17 00:00:00 2001 From: draphyz Date: Wed, 5 Apr 2023 09:45:57 +0200 Subject: [PATCH 085/111] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 83e82b3..d6d89af 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Domain events are mainly used to decouple the different parts (bounded contexts) 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 the subscriptions to these streams, reading the streams to which they have subscribed, handling the associated events and updating the current position within these streams. +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 subscriptions to event streams, reading the streams to which they have subscribed, handling the associated events and updating the current position within these streams. **Model** From 8f9f1b03e1739badcebebb8fb460a4a2ef3f5c11 Mon Sep 17 00:00:00 2001 From: draphyz Date: Wed, 5 Apr 2023 10:38:20 +0200 Subject: [PATCH 086/111] Update documentation --- Doc/CommandComponents.png | Bin 19372 -> 29689 bytes Doc/QueryComponents.png | Bin 15734 -> 26967 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/Doc/CommandComponents.png b/Doc/CommandComponents.png index f093eb3e6cd544261178e7a6b80e1302e46ed331..75b774cbca97ade2949d895a856dda706139f2cf 100644 GIT binary patch literal 29689 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

aoGl)6@anSF-nw|Mq*{X+Z~@2d8#gR# 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⋘YSt8^&_Q7^5m1T%D5 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 Hi>Lnw>10c& literal 19372 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

(C4r|#nE|Oo!-9_c0O=g{dqs}_MFvOr`d3? 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

0D= zL+NF1-S(4_0isK6aW&-+>8oDs-#Al4s2|qu_8GTFP9n}vZR!pe`jsSVO*&(5o|%)& z7WLf->CwzrdTMOs4SVVGk?RMs=Eh1u!ONtu|1mZGl_l@NEHAavH@@P_p>`j!p5wPL zSeAdvrT1VOHl;+D0_$;J0eX;JzgCN256KuJyi#Wh_p&X& zimO`R_YKG(dln!>@+4H8qeIdVK#=FXBL!arB`=ZPYCSYJw%q>pytn;#yVqf zW~?|dDaOXX3u4YY0{K%_xdv@yq4F&K84bB2%xWc-Vn~!=8Lx~|(5f!);pu6EPlF20 zm)tSxn*{A%J|S)2h)1#}Qx=2AXo#Ik6`*Q}REMSh#^er~`>VM;L7AbXp$_RfZ}F6* z?Ad=l6V|YZ(6Z5B7^0uw+rIl4z=1TNu-pC-E^IfRLuEixK(=8XG6E-GJAbW z)L48yLceB7(+BZrAG!7=SU_zFGJ7N^e{XoLI;oKyFJxzFh9R@X2_Uh|mCTCwd=tdcC_s88F=@`Co`*9{%`;miZi|tTbtkLE>FMdk6RaI|&Y~|h3lA!3 zsb#D8SF}l2*UCC%pPzOii=aW4P7nw+_eC#2o<8mVr`oPpr!6c?AI!5NhUWGQnbe!Z z(_Ms!Z2v9Y2JqoV>-wc|!LAnt{r5bZT646Losf{=G-2CZ+hC=Ko1yDbie+*SZCbn@~}Li8eYbSil$0!^w&qHSH5951(p#YK3rd(&qnab-2< zJRf3`c$j8kMLvLNVaB=Rw5$&wb~zZn4V5*X^J}I>Ot4Pvf$~ENvswA#;$o7hOHoRS z!DQ7NiHWn#>XIG!#9@@MY3Ea#*Si|$V&f~r`PFNt!6y!76u0GxGw*!6rP82r{$dZ5 z$V}G{Od6YqGL-0aKz7j zknZ%5f2B7_%ZnVn3(+}LJ;U@*Dea$?A{g&w*+e3E!7@Da?|{aNTYrrP@81#tZ`5CW z`c3}+f8-sber(oPg>?Um+Vu2LidC5vU~T%)9@zWSh!U!X8qNWNh^%`%tx&*OX7K*!1XM>R7rdxAt7P8|>pkYmNxJPFCcXU?d`lD*W_&l`~lfey# zM}e(d74{=WPfYEO5^|)mB1CIc+(b0y@5g8yK+6Wya&TSNN8`F8QRbbFt3WTlTg&qK zy7t5tHV%bi1)nvuAhYysB5dUYOMKGU=5IMTv&I@`ex6HK(ehSf3>%otisGT5>r;vF zv35&H1ycD%fx3CE{!MSXPHu(%64zsA)#YS?OI7LK(a2*$!8QV6`e&sm&Fq-LnJ!IF z$IQ_gm)1Z&! z)l#8bgKPaQ5Az(eSmc{SOM{&^>52t`AMm}keu$RZ`uH@u{%{_L=*g$?aLMWL2US%y z%CsG=R(9nv`=t!*38g)VER+8tx!Y=C^*UZ<^3C2rw~O~P7WF>c6;7|ptJEsUC4(=?BqWJOb1d&l)SmBhmaSVX*Ts{9FwqTP@X=qOh`9zd zaApqMPl~EgZdG!^7E;Xwcq)q@FoTuZ`T6;|xmMVfK}JSK5y#cn)ssqMST!VN#F}N@ z&~^ws;m*onqUu;?!tlL5=kn9v6bMtc?_=78l#MhA~=~yA5WkcSLXm z*tp%tFuRZ6u1x*%MGqWN#A#jBdGpKo8oJO>=IPVcmBH-sv9Y70Xyq$hI4b1A83hF) z&ewD5UD-y`cQr>0Fu&>nvRZpCqLVIUANP&24ZDz z_Ce4T<-zwp#`X)~H7KCR4)S-q>*n#4$fJV?8OLGZUwn<_xGlboPp0e9{tkaA{`|a4 zsITfYS-u>v{~Ne;o$=dV#w2895=SFlKw^T%0H2n3IJfbrgq~|?XsD@$FC+C`Ezvcj z2KDl+*9t05PEO_w_|F~a<%398$-%t$6O1pqzp5VZ$=?~OpKq(TExl5=YYb&r9o|o4 z;kQmzk*}Dzi>p}n1gN&t73WSIh&&`yRMN8Ux{};0=DqL9q)P9*1zr-s9^bBD(n1Ik z$3s~Nl)O!R_+=F9KY znSQ_?g{vh#1vG(vWZfoJ^DUGeC$8i4> z_8rO5fJQ%`0%m)(aRKuzL67n#EnaDWi}~z72z;>tTV+>TMEy@cTsLR}(%dxiLUTnO z$Z95uUC=e<@G%q_nO;50Bx#LMk^*2BL=kmIq%m8`K^?`b>|(bfk}_uWxdsiD3{WXE zcVd9D(~oQX<+{oHbWN2Q^2L^{v7^wm@!l_HS70OBjMPVbRgzgX`UIEGHjKz3gV*J8 z_JC^xA)eH3rck0`6 zX0BbsAdeZ0qzH5lILxgu7Y+T&T1aCH!!(b$>*iXAMzYfPVyfts2Z2rmBwE>goDo|_ z5h&DV*?@5vbnYvc4V0Evi?dXr`_Q29#y3+CqIW%{4Qa?`U&4S)29PtggK5w1W*KFY zsCO-BaSV;I1~xU8)FM)^I@V4b@Usf!K&CXkFYod(1C=>o=0HO<(Q#v7nP@IHto1y2Cq9$ z4Q0P1Vn#=3eX9y%>ViC3$<7BJ3vJs>4Pp-{or^*R96q%wa7DBG&k(ERDSDs6oawZ) zv!j$KTKTFsWo|X5lE=#uNRLr-J-wd?TeF$Pi!wAWa|ur9R)=bc-m|U_hifJiZ4qp# zy|0kkZ)144xbD$`tOGXsg?>oLn;Z$4=gC$xzY(hNND!2gSBtw}mRXJhYq`*UJU&XL z)$(Yl`Vl?wjYCSt8Y(;i8>sU$!72Bh<&31dvm;Em(EF``tv{!(76OOzMGB%d`{HtiE1OkLpU2NaDJ#X9cRn4Q%-}VarUAZvnIRLjRI1{#@h!Q#vXB11kao zthNC5v{F!6KfgaeF{@-{U-1ymNsVdb0&d~Tw~h-2q%f0oHAx`P)u~{4cS=g{hbB;L zX*^!?x7DrWR&+C1%0B5HP)G$a!YQoo4N;Z2p79w~7=no5cuGQ>*p6Bq@> zAAx^HsNq6r8^lCTPMz*;L>4Ilf%0i!Uow#UO|kTWzETEG*>zID>&T918&j#$b2P9~ zjir}f$OJ-6sNM0VZtAl@Rh`Rde*4&?nC5$H_=8Ap5uo{soBBgfGK57$>F5l0N+ShX z0qF)I)2JWB6X)ZT+i{;R1$Y_5Jy7|%ldTje9rn~F9}@&s=cnkE3syCcpP^Gp_O2rM zK8>tu9H5LQ0lzW;NFab^7TQRbg9{^swE~`IQ3M7|sP|IIF?sxAGSjhj;dM&?L}bz` z(ZA1w${&O`NbPzE_XTwMn*gAmy9E?LDo_ApgFS{yd46seWI8pnH~o_$DPi~BCe`sv z;8H#2vl8Aa$n~E1{|=~_Fw9;3wd|)Yn2ulqhMW@i_KUE-HOdbaN~0a4bR9=OlD(|( zJU#L~f1e9h*`mJSRAMRpygT2XT^Nv_aD;Xa5Un76DnMN-SeEi0gqeX&>cJETt8`#FJ5547jn%xPqe~fh9~!mWjr5g2PUupFYqnk0+i^ufcWQi za!0Z`GxLW@&Ba&ZP)bx|-}5h`PWQ&8q@-fZH%l&7M<$*}0iUabsDxECp1XHVyz5Qx zO0;)Wo~{Hp` zyQ(Uuii{rxMKiU&qoJ(@Q~6Bhdq?Uw-4c9py<7l~>1gXuCO7B|{gG+>N{D#C=3neZ zv@nPeOTs;h&!&dRxQY#2|F52=2G7I{n5cf0lxy{WE!ZIVF4-*?xeB?LW~mzEQ2yGR zpzBEr46C_>hF2$9OVI@%BtsXXk!D)89yOUIf7QM&BEGsVRO8c4kTE*iH0l+9%K}R+ zL+aSeVQSwjkd%6W?^hWYPi7r_)u?O|H)?WC6+D{5B@FwL+`NwK529W5)0K^&~31t}S7h}VRL<@lnT>f`VP^Mm;enA)r3A?{O zyL@oQoHHshy6(w^5s~TeGK-H{z7B~Q{ucmnzrXXXj}H(6Zm$T{U-%}i??&Zf&>3h{iYz!B(@`5U!Wr* z)IN-NR?KaWUAJoBQc~9z6ma#geYY$;yedv`(;>8#ix$L23JQhF%?;%0GWJV(6&tzy z0y3+QLs@As{rw~06_da$AQ0yO;#Hb-Qiz2KT8*Uz6q)rSjh^8Bgy3I zk;qrENTZ$O=Ko`=>#*u+IaTKY;KtJGs!oxS@A%rwQgagf`S`Fw1m+%*bIitC*Sf^P z1+v zp=~X?sk1JJ+w(Ej#3nu9sUzOW-+mGw;0fq8?!@w2@J#-rBWJ*d@XA8tKQV>Jd3rdM zzk&p$u&zG?@b4{Dqsd>s{9LOm4@wdecZHolhQkpXppqtdoE!TR1R#J9WMpvrPQU7n z9crDX~f zo9#u8rNx1j!3pxn2%L z!O8+oPTV~Tt12sn`>kS3E{pGfRovroFC{pxQdVSvpfb2qP$eBF=8Q~7#L$c zQzys{0ERCM)+grmi1r{5*888NE-T`6biZ`>wa3WNHqDfD)aCu{A^Wa!rwa5-yW z1utDA%2ElGrZO&zu@Zp%bO>o1ZTiIt=?vKH)s|EZ75e#d+f9k$^5yzVG>U&4S10g= zV&AMimpiJmG{(F1<__I$*C3F9_i9}Nd5^u-gio_*dNQZl1;k$r!78ii z2c3ZL_$`J2iQM^F{~i!UVBP`#1b4&wqS9?>jsxK$z+~8|=cCxGBSi^lb^5?ng!G;9jPS5GwRTlmgEQ@d#J7L7XNTg z#j!Z(OKgz-4oKw&uDkv+p)@EahhXiE2|znx?beWXb)w5c$BY4iFaR!q&??nnhnipH zo%A}Pv~_gim#-T{?(Xdk<<*wjck_P4X`}w5A%8wO9-Th02Bxy(Z637W&Wo@-4Psnuer?r>=g3HvLvwq z_3^8Z=-&hjsk|{p*9-S^!=J56pucA`N*Mw%qW2`n`Kz1gBaZ5*E16!ye{FHn+Oapr zcrfDs{ZOwj+N1=E7C_qfVwd{wBIv)Usecnp|F4ZjCYxy? Date: Wed, 5 Apr 2023 14:41:00 +0200 Subject: [PATCH 087/111] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d6d89af..6493cdc 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Domain events are mainly used to decouple the different parts (bounded contexts) 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 subscriptions to event streams, reading the streams to which they have subscribed, handling the associated events and updating the current position within these streams. +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 they have subscribed, handling the associated events and updating the current position within these streams. **Model** From ee0776b213fd3fc2b7165ad987728f90bbfd1f53 Mon Sep 17 00:00:00 2001 From: draphyz Date: Wed, 5 Apr 2023 14:44:52 +0200 Subject: [PATCH 088/111] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6493cdc..47d2df0 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Domain events are mainly used to decouple the different parts (bounded contexts) 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 they have subscribed, handling the associated events and updating the current position within these streams. +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. **Model** From f6d52007cbee9933532a39012ab79ef3d77cdd37 Mon Sep 17 00:00:00 2001 From: draphyz Date: Wed, 5 Apr 2023 19:36:35 +0200 Subject: [PATCH 089/111] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 47d2df0..91ac136 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Domain events are mainly used to decouple the different parts (bounded contexts) 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. +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** From f69d8da2cfc98d86a677add46416ccb39ac48590 Mon Sep 17 00:00:00 2001 From: draphyz Date: Tue, 30 May 2023 17:24:29 +0200 Subject: [PATCH 090/111] Remove unnecessary code --- Build/CommonAssemblyInfo.cs | 4 +--- Src/DDD.Core/Domain/DomainEntity.cs | 15 +-------------- Src/DDD.Core/Domain/EntityState.cs | 10 ---------- Src/DDD.Core/Domain/IStateEntity.cs | 14 -------------- Src/DDD.Core/Domain/IStateObjectConvertible.cs | 14 -------------- .../Prescriptions/PharmaceuticalPrescription.cs | 3 +-- .../Domain/Prescriptions/Prescription.cs | 3 +-- 7 files changed, 4 insertions(+), 59 deletions(-) delete mode 100644 Src/DDD.Core/Domain/EntityState.cs delete mode 100644 Src/DDD.Core/Domain/IStateEntity.cs delete mode 100644 Src/DDD.Core/Domain/IStateObjectConvertible.cs 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/Src/DDD.Core/Domain/DomainEntity.cs b/Src/DDD.Core/Domain/DomainEntity.cs index 99c5112..579eef8 100644 --- a/Src/DDD.Core/Domain/DomainEntity.cs +++ b/Src/DDD.Core/Domain/DomainEntity.cs @@ -17,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) @@ -78,12 +71,6 @@ protected void AddEvent(IDomainEvent @event) 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/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/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.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs index 69997d5..6016ea6 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/PharmaceuticalPrescription.cs @@ -35,9 +35,8 @@ public PharmaceuticalPrescription(PrescriptionIdentifier identifier, DateTime createdOn, EncounterIdentifier encounterIdentifier = null, DateTime? delivrableAt = null, - EntityState entityState = EntityState.Added, IEnumerable events = null) - : base(identifier, prescriber, patient, languageCode, status, createdOn, encounterIdentifier, delivrableAt, entityState, events) + : base(identifier, prescriber, patient, languageCode, status, createdOn, encounterIdentifier, delivrableAt, events) { Ensure.That(prescribedMedications, nameof(prescribedMedications)).IsNotNull(); Ensure.Enumerable.HasItems(prescribedMedications, nameof(prescribedMedications)); diff --git a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/Prescription.cs b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/Prescription.cs index f0ae3b6..779b150 100644 --- a/Src/DDD.HealthcareDelivery/Domain/Prescriptions/Prescription.cs +++ b/Src/DDD.HealthcareDelivery/Domain/Prescriptions/Prescription.cs @@ -28,9 +28,8 @@ protected Prescription(PrescriptionIdentifier identifier, DateTime createdOn, EncounterIdentifier encounterIdentifier = null, DateTime? delivrableAt = null, - EntityState entityState = EntityState.Added, IEnumerable events = null) - : base(entityState, events) + : base(events) { Ensure.That(identifier, nameof(identifier)).IsNotNull(); Ensure.That(prescriber, nameof(prescriber)).IsNotNull(); From d996e6db1a8a975f72c9d4b5649cc868cfe87f94 Mon Sep 17 00:00:00 2001 From: draphyz Date: Thu, 1 Jun 2023 12:17:43 +0200 Subject: [PATCH 091/111] Remove unnecessary code --- .../AsyncFluentCommandValidatorAdapter.cs | 20 -------- .../AsyncFluentQueryValidatorAdapter.cs | 18 ------- ...orAdapter.cs => FluentValidatorAdapter.cs} | 21 ++++++-- .../SyncFluentCommandValidatorAdapter.cs | 20 -------- .../SyncFluentQueryValidatorAdapter.cs | 18 ------- .../SyncFluentValidatorAdapter.cs | 51 ------------------- .../ContainerExtensions.cs | 10 ++-- Src/DDD.Core/Application/CommandProcessor.cs | 8 +-- .../Application/IAsyncCommandValidator.cs | 12 ----- .../Application/IAsyncQueryValidator.cs | 12 ----- Src/DDD.Core/Application/ICommandValidator.cs | 10 ---- Src/DDD.Core/Application/IQueryValidator.cs | 10 ---- .../Application/ISyncCommandValidator.cs | 12 ----- .../Application/ISyncQueryValidator.cs | 12 ----- Src/DDD.Core/Application/QueryProcessor.cs | 8 +-- .../Application/CommandProcessorTests.cs | 13 ++--- .../Application/QueryProcessorTests.cs | 13 ++--- 17 files changed, 44 insertions(+), 224 deletions(-) delete mode 100644 Src/DDD.Core.FluentValidation/AsyncFluentCommandValidatorAdapter.cs delete mode 100644 Src/DDD.Core.FluentValidation/AsyncFluentQueryValidatorAdapter.cs rename Src/DDD.Core.FluentValidation/{AsyncFluentValidatorAdapter.cs => FluentValidatorAdapter.cs} (67%) delete mode 100644 Src/DDD.Core.FluentValidation/SyncFluentCommandValidatorAdapter.cs delete mode 100644 Src/DDD.Core.FluentValidation/SyncFluentQueryValidatorAdapter.cs delete mode 100644 Src/DDD.Core.FluentValidation/SyncFluentValidatorAdapter.cs delete mode 100644 Src/DDD.Core/Application/IAsyncCommandValidator.cs delete mode 100644 Src/DDD.Core/Application/IAsyncQueryValidator.cs delete mode 100644 Src/DDD.Core/Application/ICommandValidator.cs delete mode 100644 Src/DDD.Core/Application/IQueryValidator.cs delete mode 100644 Src/DDD.Core/Application/ISyncCommandValidator.cs delete mode 100644 Src/DDD.Core/Application/ISyncQueryValidator.cs diff --git a/Src/DDD.Core.FluentValidation/AsyncFluentCommandValidatorAdapter.cs b/Src/DDD.Core.FluentValidation/AsyncFluentCommandValidatorAdapter.cs deleted file mode 100644 index 04cb323..0000000 --- a/Src/DDD.Core.FluentValidation/AsyncFluentCommandValidatorAdapter.cs +++ /dev/null @@ -1,20 +0,0 @@ -using FluentValidation; - -namespace DDD.Core.Infrastructure.Validation -{ - using Application; - - public class AsyncFluentCommandValidatorAdapter : AsyncFluentValidatorAdapter, IAsyncCommandValidator - where TCommand : class, ICommand - { - - #region Constructors - - public AsyncFluentCommandValidatorAdapter(IValidator fluentValidator) : base(fluentValidator) - { - } - - #endregion Constructors - - } -} diff --git a/Src/DDD.Core.FluentValidation/AsyncFluentQueryValidatorAdapter.cs b/Src/DDD.Core.FluentValidation/AsyncFluentQueryValidatorAdapter.cs deleted file mode 100644 index 58bbc4f..0000000 --- a/Src/DDD.Core.FluentValidation/AsyncFluentQueryValidatorAdapter.cs +++ /dev/null @@ -1,18 +0,0 @@ -using FluentValidation; - -namespace DDD.Core.Infrastructure.Validation -{ - using Application; - - public class AsyncFluentQueryValidatorAdapter : AsyncFluentValidatorAdapter, IAsyncQueryValidator - where TQuery : class, IQuery - { - #region Constructors - - public AsyncFluentQueryValidatorAdapter(IValidator fluentValidator) : base(fluentValidator) - { - } - - #endregion Constructors - } -} diff --git a/Src/DDD.Core.FluentValidation/AsyncFluentValidatorAdapter.cs b/Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs similarity index 67% rename from Src/DDD.Core.FluentValidation/AsyncFluentValidatorAdapter.cs rename to Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs index dc7eafc..7910db3 100644 --- a/Src/DDD.Core.FluentValidation/AsyncFluentValidatorAdapter.cs +++ b/Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs @@ -1,15 +1,15 @@ using FluentValidation; using FluentValidation.Results; using EnsureThat; -using System.Threading; using System.Threading.Tasks; +using System.Threading; namespace DDD.Core.Infrastructure.Validation { using Mapping; using Threading; - public class AsyncFluentValidatorAdapter : DDD.Validation.IAsyncObjectValidator + public class FluentValidatorAdapter : DDD.Validation.IObjectValidator where T : class { @@ -22,7 +22,7 @@ public class AsyncFluentValidatorAdapter : DDD.Validation.IAsyncObjectValidat #region Constructors - public AsyncFluentValidatorAdapter(IValidator fluentValidator) + public FluentValidatorAdapter(IValidator fluentValidator) { Ensure.That(fluentValidator, nameof(fluentValidator)).IsNotNull(); this.fluentValidator = fluentValidator; @@ -33,6 +33,21 @@ public AsyncFluentValidatorAdapter(IValidator fluentValidator) #region Methods + /// + /// Validates synchronously the specified object. + /// + /// The object to validate. + /// The rule set. + public DDD.Validation.ValidationResult Validate(T obj, string ruleSet = null) + { + ValidationResult result; + if (string.IsNullOrWhiteSpace(ruleSet)) + result = this.fluentValidator.Validate(obj); + else + result = this.fluentValidator.Validate(obj, context => context.IncludeRuleSets(ruleSet.Split(','))); + return this.resultTranslator.Translate(result, new { ObjectName = obj.GetType().Name }); + } + /// /// Validates asynchronously the specified object. /// diff --git a/Src/DDD.Core.FluentValidation/SyncFluentCommandValidatorAdapter.cs b/Src/DDD.Core.FluentValidation/SyncFluentCommandValidatorAdapter.cs deleted file mode 100644 index 9ee98df..0000000 --- a/Src/DDD.Core.FluentValidation/SyncFluentCommandValidatorAdapter.cs +++ /dev/null @@ -1,20 +0,0 @@ -using FluentValidation; - -namespace DDD.Core.Infrastructure.Validation -{ - using Application; - - public class SyncFluentCommandValidatorAdapter : SyncFluentValidatorAdapter, ISyncCommandValidator - where TCommand : class, ICommand - { - - #region Constructors - - public SyncFluentCommandValidatorAdapter(IValidator fluentValidator) : base(fluentValidator) - { - } - - #endregion Constructors - - } -} diff --git a/Src/DDD.Core.FluentValidation/SyncFluentQueryValidatorAdapter.cs b/Src/DDD.Core.FluentValidation/SyncFluentQueryValidatorAdapter.cs deleted file mode 100644 index 5dd2479..0000000 --- a/Src/DDD.Core.FluentValidation/SyncFluentQueryValidatorAdapter.cs +++ /dev/null @@ -1,18 +0,0 @@ -using FluentValidation; - -namespace DDD.Core.Infrastructure.Validation -{ - using Application; - - public class SyncFluentQueryValidatorAdapter : SyncFluentValidatorAdapter, ISyncQueryValidator - where TQuery : class, IQuery - { - #region Constructors - - public SyncFluentQueryValidatorAdapter(IValidator fluentValidator) : base(fluentValidator) - { - } - - #endregion Constructors - } -} diff --git a/Src/DDD.Core.FluentValidation/SyncFluentValidatorAdapter.cs b/Src/DDD.Core.FluentValidation/SyncFluentValidatorAdapter.cs deleted file mode 100644 index d6831d0..0000000 --- a/Src/DDD.Core.FluentValidation/SyncFluentValidatorAdapter.cs +++ /dev/null @@ -1,51 +0,0 @@ -using FluentValidation; -using FluentValidation.Results; -using EnsureThat; - -namespace DDD.Core.Infrastructure.Validation -{ - using Mapping; - - public class SyncFluentValidatorAdapter : DDD.Validation.ISyncObjectValidator - where T : class - { - - #region Fields - - private readonly IValidator fluentValidator; - private readonly IObjectTranslator resultTranslator; - - #endregion Fields - - #region Constructors - - public SyncFluentValidatorAdapter(IValidator fluentValidator) - { - Ensure.That(fluentValidator, nameof(fluentValidator)).IsNotNull(); - this.fluentValidator = fluentValidator; - this.resultTranslator = new ValidationResultTranslator(); - } - - #endregion Constructors - - #region Methods - - /// - /// Validates synchronously the specified object. - /// - /// The object to validate. - /// The rule set. - public DDD.Validation.ValidationResult Validate(T obj, string ruleSet = null) - { - ValidationResult result; - if (string.IsNullOrWhiteSpace(ruleSet)) - result = this.fluentValidator.Validate(obj); - else - result = this.fluentValidator.Validate(obj, context => context.IncludeRuleSets(ruleSet.Split(','))); - return this.resultTranslator.Translate(result, new { ObjectName = obj.GetType().Name }); - } - - #endregion Methods - - } -} diff --git a/Src/DDD.Core.SimpleInjector.FluentValidation/ContainerExtensions.cs b/Src/DDD.Core.SimpleInjector.FluentValidation/ContainerExtensions.cs index 2486996..6989d35 100644 --- a/Src/DDD.Core.SimpleInjector.FluentValidation/ContainerExtensions.cs +++ b/Src/DDD.Core.SimpleInjector.FluentValidation/ContainerExtensions.cs @@ -3,11 +3,11 @@ using System.Collections.Generic; using System.Reflection; using System; +using DDD.Validation; namespace DDD.Core.Infrastructure.DependencyInjection { using Validation; - using Application; using EnsureThat; public static class ContainerExtensions @@ -16,7 +16,7 @@ public static class ContainerExtensions #region Methods /// - /// Registers command and queries validators. + /// Registers object validators. /// /// The container that registers validators. /// The assemblies that contain validators. @@ -26,10 +26,8 @@ public static void RegisterValidators(this Container container, IEnumerable true); container.RegisterConditional(typeof(IValidator<>), assemblies, notNullPredicate); - container.Register(typeof(ISyncCommandValidator<>), typeof(SyncFluentCommandValidatorAdapter<>)); - container.Register(typeof(IAsyncCommandValidator<>), typeof(AsyncFluentCommandValidatorAdapter<>)); - container.Register(typeof(ISyncQueryValidator<>), typeof(SyncFluentQueryValidatorAdapter<>)); - container.Register(typeof(IAsyncQueryValidator<>), typeof(AsyncFluentQueryValidatorAdapter<>)); + container.Register(typeof(ISyncObjectValidator<>), typeof(FluentValidatorAdapter<>)); + container.Register(typeof(IAsyncObjectValidator<>), typeof(FluentValidatorAdapter<>)); } #endregion Methods diff --git a/Src/DDD.Core/Application/CommandProcessor.cs b/Src/DDD.Core/Application/CommandProcessor.cs index 4b16dc0..d8d0504 100644 --- a/Src/DDD.Core/Application/CommandProcessor.cs +++ b/Src/DDD.Core/Application/CommandProcessor.cs @@ -64,16 +64,16 @@ public Task ProcessAsync(TCommand command, IMessageContext context = n public ValidationResult Validate(TCommand command, string ruleSet = null) where TCommand : class, ICommand { Ensure.That(command, nameof(command)).IsNotNull(); - var validator = this.serviceProvider.GetService>(); - if (validator == null) throw new InvalidOperationException($"The command validator for type {typeof(ISyncCommandValidator)} could not be found."); + var validator = this.serviceProvider.GetService>(); + if (validator == null) throw new InvalidOperationException($"The command validator for type {typeof(ISyncObjectValidator)} could not be found."); return validator.Validate(command, ruleSet); } public Task ValidateAsync(TCommand command, string ruleSet = null, CancellationToken cancellationToken = default) where TCommand : class, ICommand { Ensure.That(command, nameof(command)).IsNotNull(); - var validator = this.serviceProvider.GetService>(); - if (validator == null) throw new InvalidOperationException($"The command validator for type {typeof(IAsyncCommandValidator)} could not be found."); + var validator = this.serviceProvider.GetService>(); + if (validator == null) throw new InvalidOperationException($"The command validator for type {typeof(IAsyncObjectValidator)} could not be found."); return validator.ValidateAsync(command, ruleSet, cancellationToken); } 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/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/ICommandValidator.cs b/Src/DDD.Core/Application/ICommandValidator.cs deleted file mode 100644 index e9e081e..0000000 --- a/Src/DDD.Core/Application/ICommandValidator.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace DDD.Core.Application -{ - /// - /// Defines methods that validate synchronously and asynchronously a command of a specified type. - /// - public interface ICommandValidator : ISyncCommandValidator, IAsyncCommandValidator - where TCommand : class, ICommand - { - } -} \ 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 8fa542d..0000000 --- a/Src/DDD.Core/Application/IQueryValidator.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace DDD.Core.Application -{ - /// - /// Defines methods that validate synchronously and asynchronously a query of a specified type. - /// - public interface IQueryValidator : ISyncQueryValidator, IAsyncQueryValidator - where TQuery : class, IQuery - { - } -} diff --git a/Src/DDD.Core/Application/ISyncCommandValidator.cs b/Src/DDD.Core/Application/ISyncCommandValidator.cs deleted file mode 100644 index 3efb86c..0000000 --- a/Src/DDD.Core/Application/ISyncCommandValidator.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace DDD.Core.Application -{ - using Validation; - - /// - /// Defines a method that validates synchronously a command of a specified type. - /// - public interface ISyncCommandValidator : ISyncObjectValidator - where TCommand : class, ICommand - { - } -} \ No newline at end of file diff --git a/Src/DDD.Core/Application/ISyncQueryValidator.cs b/Src/DDD.Core/Application/ISyncQueryValidator.cs deleted file mode 100644 index 5886865..0000000 --- a/Src/DDD.Core/Application/ISyncQueryValidator.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace DDD.Core.Application -{ - using Validation; - - /// - /// Defines a method that validates synchronously a query of a specified type. - /// - public interface ISyncQueryValidator : ISyncObjectValidator - where TQuery : class, IQuery - { - } -} \ No newline at end of file diff --git a/Src/DDD.Core/Application/QueryProcessor.cs b/Src/DDD.Core/Application/QueryProcessor.cs index f8bc2a9..6bcf926 100644 --- a/Src/DDD.Core/Application/QueryProcessor.cs +++ b/Src/DDD.Core/Application/QueryProcessor.cs @@ -66,16 +66,16 @@ public Task ProcessAsync(IQuery query, IMessageContex public ValidationResult Validate(TQuery query, string ruleSet = null) where TQuery : class, IQuery { Ensure.That(query, nameof(query)).IsNotNull(); - var validator = this.serviceProvider.GetService>(); - if (validator == null) throw new InvalidOperationException($"The query validator for type {typeof(ISyncQueryValidator)} could not be found."); + var validator = this.serviceProvider.GetService>(); + if (validator == null) throw new InvalidOperationException($"The query validator for type {typeof(ISyncObjectValidator)} could not be found."); return validator.Validate(query, ruleSet); } public Task ValidateAsync(TQuery query, string ruleSet = null, CancellationToken cancellationToken = default) where TQuery : class, IQuery { Ensure.That(query, nameof(query)).IsNotNull(); - var validator = this.serviceProvider.GetService>(); - if (validator == null) throw new InvalidOperationException($"The query validator for type {typeof(IAsyncQueryValidator)} could not be found."); + var validator = this.serviceProvider.GetService>(); + if (validator == null) throw new InvalidOperationException($"The query validator for type {typeof(IAsyncObjectValidator)} could not be found."); return validator.ValidateAsync(query, ruleSet, cancellationToken); } diff --git a/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs b/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs index 9cd3d2e..17a473c 100644 --- a/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs +++ b/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs @@ -1,6 +1,7 @@ using NSubstitute; using System; using Xunit; +using DDD.Validation; namespace DDD.Core.Application { @@ -10,8 +11,8 @@ public class CommandProcessorTests private readonly ISyncCommandHandler handlerOfCommand1; private readonly ISyncCommandHandler handlerOfCommand2; - private readonly ISyncCommandValidator validatorOfCommand1; - private readonly ISyncCommandValidator validatorOfCommand2; + private readonly ISyncObjectValidator validatorOfCommand1; + private readonly ISyncObjectValidator validatorOfCommand2; private readonly CommandProcessor processor; #endregion Fields @@ -22,16 +23,16 @@ public CommandProcessorTests() { this.handlerOfCommand1 = Substitute.For>(); this.handlerOfCommand2 = Substitute.For>(); - this.validatorOfCommand1 = Substitute.For>(); - this.validatorOfCommand2 = Substitute.For>(); + this.validatorOfCommand1 = Substitute.For>(); + this.validatorOfCommand2 = Substitute.For>(); var serviceProvider = Substitute.For(); serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncCommandHandler)))) .Returns(this.handlerOfCommand1); serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncCommandHandler)))) .Returns(this.handlerOfCommand2); - serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncCommandValidator)))) + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncObjectValidator)))) .Returns(this.validatorOfCommand1); - serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncCommandValidator)))) + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncObjectValidator)))) .Returns(this.validatorOfCommand2); processor = new CommandProcessor(serviceProvider); } diff --git a/Test/DDD.Core.UnitTests/Application/QueryProcessorTests.cs b/Test/DDD.Core.UnitTests/Application/QueryProcessorTests.cs index ff91113..422c5ce 100644 --- a/Test/DDD.Core.UnitTests/Application/QueryProcessorTests.cs +++ b/Test/DDD.Core.UnitTests/Application/QueryProcessorTests.cs @@ -1,6 +1,7 @@ using NSubstitute; using System; using Xunit; +using DDD.Validation; namespace DDD.Core.Application { @@ -10,8 +11,8 @@ public class QueryProcessorTests private readonly ISyncQueryHandler handlerOfQuery1; private readonly ISyncQueryHandler handlerOfQuery2; - private readonly ISyncQueryValidator validatorOfQuery1; - private readonly ISyncQueryValidator validatorOfQuery2; + private readonly ISyncObjectValidator validatorOfQuery1; + private readonly ISyncObjectValidator validatorOfQuery2; private readonly QueryProcessor processor; #endregion Fields @@ -22,16 +23,16 @@ public QueryProcessorTests() { this.handlerOfQuery1 = Substitute.For>(); this.handlerOfQuery2 = Substitute.For>(); - this.validatorOfQuery1 = Substitute.For>(); - this.validatorOfQuery2 = Substitute.For>(); + this.validatorOfQuery1 = Substitute.For>(); + this.validatorOfQuery2 = Substitute.For>(); var serviceProvider = Substitute.For(); serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncQueryHandler)))) .Returns(this.handlerOfQuery1); serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncQueryHandler)))) .Returns(this.handlerOfQuery2); - serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncQueryValidator)))) + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncObjectValidator)))) .Returns(this.validatorOfQuery1); - serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncQueryValidator)))) + serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncObjectValidator)))) .Returns(this.validatorOfQuery2); processor = new QueryProcessor(serviceProvider); } From 074d3303589ef914056cbb741bc0193dc6ce84c1 Mon Sep 17 00:00:00 2001 From: draphyz Date: Sun, 4 Jun 2023 16:27:36 +0200 Subject: [PATCH 092/111] Uniformize interfaces --- .../Mapping/CompositeTranslator.cs | 4 +- .../Mapping/DelegatingTranslator.cs | 8 +- .../Mapping/IMappingContext.cs | 11 +++ .../Mapping/IMappingProcessor.cs | 10 +-- .../Mapping/IMappingProcessorExtensions.cs | 71 +++++++++++----- .../Mapping/IObjectMapper.cs | 6 +- .../Mapping/IObjectMapperExtensions.cs | 17 ++-- .../Mapping/IObjectTranslator.cs | 3 +- .../Mapping/IObjectTranslatorExtensions.cs | 31 +++++-- .../Mapping/IObjectTranslator`1.cs | 6 +- .../Mapping/MappingContext.cs | 35 ++++++++ .../Mapping/MappingProcessor.cs | 7 +- .../Mapping/ObjectTranslator.cs | 5 +- .../Validation/IAsyncObjectValidator.cs | 5 +- .../IAsyncObjectValidatorExtensions.cs | 31 +++++++ .../Validation/ISyncObjectValidator.cs | 2 +- .../ISyncObjectValidatorExtensions.cs | 30 +++++++ .../Validation/IValidationContext.cs | 11 +++ .../IValidationContextExtensions.cs | 44 ++++++++++ .../Validation/ValidationContext.cs | 50 +++++++++++ .../Validation/ValidationContextInfo.cs | 15 ++++ .../EventStreamPositionUpdater.cs | 7 +- Src/DDD.Core.Dapper/EventStreamReader.cs | 7 +- Src/DDD.Core.Dapper/EventStreamSubcriber.cs | 7 +- Src/DDD.Core.Dapper/EventStreamsFinder.cs | 7 +- .../FailedEventStreamExcluder.cs | 7 +- .../FailedEventStreamIncluder.cs | 7 +- .../FailedEventStreamPositionUpdater.cs | 7 +- .../FailedEventStreamReader.cs | 7 +- .../FailedEventStreamUpdater.cs | 7 +- .../FailedEventStreamsFinder.cs | 7 +- .../FailedRecurringCommandUpdater.cs | 7 +- .../RecurringCommandIdGenerator.cs | 2 +- .../RecurringCommandRegister.cs | 7 +- .../RecurringCommandsFinder.cs | 7 +- .../SuccessfulRecurringCommandUpdater.cs | 7 +- .../DDD.Core.FluentValidation.csproj | 1 - .../FluentValidatorAdapter.cs | 37 ++++---- .../ValidationResultTranslator.cs | 6 +- .../Domain/BoundedContext.cs | 2 +- .../NHRepositoryExceptionTranslator.cs | 6 +- .../AsyncPollyCommandHandler.cs | 2 +- .../AsyncPollyCommandHandler`1.cs | 2 +- Src/DDD.Core.Polly/AsyncPollyQueryHandler.cs | 2 +- .../AsyncPollyQueryHandler`1.cs | 2 +- Src/DDD.Core.Polly/SyncPollyCommandHandler.cs | 2 +- .../SyncPollyCommandHandler`1.cs | 2 +- Src/DDD.Core.Polly/SyncPollyQueryHandler.cs | 2 +- Src/DDD.Core.Polly/SyncPollyQueryHandler`1.cs | 2 +- .../AsyncScopedCommandHandler.cs | 5 +- .../AsyncScopedCommandHandler`1.cs | 8 +- .../AsyncScopedQueryHandler.cs | 2 +- .../AsyncScopedQueryHandler`1.cs | 8 +- .../ContainerExtensions.cs | 15 +++- .../ThreadScopedCommandHandler.cs | 5 +- .../ThreadScopedCommandHandler`1.cs | 8 +- .../ThreadScopedQueryHandler.cs | 2 +- .../ThreadScopedQueryHandler`1.cs | 8 +- Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj | 1 - Src/DDD.Core.Xunit/DbFixture.cs | 2 +- Src/DDD.Core.Xunit/IDbFixture.cs | 2 +- Src/DDD.Core.Xunit/IDbFixtureExtensions.cs | 4 +- .../AsyncCommandHandlerWithLogging.cs | 2 +- .../AsyncCommandHandlerWithLogging`1.cs | 2 +- Src/DDD.Core/Application/AsyncEventHandler.cs | 17 +++- .../AsyncEventHandlerWithLogging.cs | 2 +- .../AsyncQueryHandlerWithLogging.cs | 2 +- .../AsyncQueryHandlerWithLogging`1.cs | 2 +- Src/DDD.Core/Application/CommandProcessor.cs | 30 ++++--- .../CompositeTranslatorExtensions.cs | 6 +- .../Application/ContextualCommandProcessor.cs | 4 +- .../Application/ContextualQueryProcessor.cs | 4 +- .../DomainToCommandExceptionTranslator.cs | 7 +- Src/DDD.Core/Application/EventConsumer.cs | 16 ++-- .../Application/EventConsumerSettings.cs | 16 ++-- Src/DDD.Core/Application/EventPublisher.cs | 9 +- Src/DDD.Core/Application/EventTranslator.cs | 4 +- .../Application/IAsyncCommandHandler.cs | 2 +- .../IAsyncCommandHandlerExtensions.cs | 9 ++ .../Application/IAsyncEventHandler.cs | 2 +- .../Application/IAsyncEventHandler`1.cs | 2 +- .../Application/IAsyncQueryHandler.cs | 2 +- .../IAsyncQueryHandlerExtensions.cs | 9 ++ Src/DDD.Core/Application/ICommandProcessor.cs | 17 ++-- .../ICommandProcessorExtensions.cs | 65 ++++++++++++-- .../IContextualCommandProcessor.cs | 6 +- .../IContextualCommandProcessorExtensions.cs | 84 +++++++++++++++++++ .../IContextualCommandProcessor`1.cs | 2 +- .../Application/IContextualQueryProcessor.cs | 4 +- .../IContextualQueryProcessorExtensions.cs | 44 ++++++++++ Src/DDD.Core/Application/IEventPublisher.cs | 2 +- .../Application/IEventPublisherExtensions.cs | 33 ++++++++ Src/DDD.Core/Application/IQueryProcessor.cs | 17 ++-- .../Application/IQueryProcessorExtensions.cs | 48 ++++++++++- .../Application/ISyncCommandHandler.cs | 2 +- .../ISyncCommandHandlerExtensions.cs | 9 ++ Src/DDD.Core/Application/ISyncEventHandler.cs | 2 +- .../Application/ISyncEventHandler`1.cs | 2 +- Src/DDD.Core/Application/ISyncQueryHandler.cs | 2 +- .../ISyncQueryHandlerExtensions.cs | 9 ++ Src/DDD.Core/Application/MessageContext.cs | 12 +-- Src/DDD.Core/Application/QueryProcessor.cs | 31 ++++--- .../Application/RecurringCommandManager.cs | 12 +-- .../RecurringCommandManagerSettings.cs | 16 ++-- .../SyncCommandHandlerWithLogging.cs | 2 +- .../SyncCommandHandlerWithLogging`1.cs | 2 +- Src/DDD.Core/Application/SyncEventHandler.cs | 17 +++- .../SyncEventHandlerWithLogging.cs | 2 +- .../SyncQueryHandlerWithLogging.cs | 2 +- .../SyncQueryHandlerWithLogging`1.cs | 2 +- .../Domain/CompositeTranslatorExtensions.cs | 6 +- .../Data/DbToCommandExceptionTranslator.cs | 6 +- .../Data/DbToQueryExceptionTranslator.cs | 6 +- .../Data/DbToRepositoryExceptionTranslator.cs | 6 +- .../Data/LazyDbConnectionProvider.cs | 7 +- .../OracleToCommandExceptionTranslator.cs | 7 +- .../Data/OracleToQueryExceptionTranslator.cs | 7 +- .../OracleToRepositoryExceptionTranslator.cs | 7 +- .../SqlServerToCommandExceptionTranslator.cs | 7 +- .../SqlServerToQueryExceptionTranslator.cs | 7 +- ...qlServerToRepositoryExceptionTranslator.cs | 7 +- ...ianPharmaceuticalPrescriptionTranslator.cs | 4 +- .../PharmaceuticalPrescriptionCreator.cs | 7 +- .../PharmaceuticalPrescriptionRevoker.cs | 8 +- ...rmaceuticalPrescriptionsByPatientFinder.cs | 7 +- ...escribedMedicationsByPrescriptionFinder.cs | 7 +- .../PrescriptionIdentifierGenerator.cs | 7 +- .../IMappingProcessorExtensionsTests.cs | 16 ++-- .../Mapping/MappingProcessorTests.cs | 15 ++-- .../StringArgExtensionsTests.cs | 2 +- .../StringParamExtensionsTests.cs | 2 +- .../Data/IPersistenceFixture.cs | 5 +- .../Application/CommandProcessorTests.cs | 11 ++- .../Application/EventConsumerTests.cs | 45 +++++----- .../Application/EventPublisherTests.cs | 15 ++-- .../Application/QueryProcessorTests.cs | 10 ++- .../RecurringCommandManagerTests.cs | 22 ++--- .../PharmaceuticalPrescriptionCreatorTests.cs | 4 +- .../PharmaceuticalPrescriptionRevokerTests.cs | 4 +- ...uticalPrescriptionsByPatientFinderTests.cs | 6 +- ...bedMedicationsByPrescriptionFinderTests.cs | 4 +- 141 files changed, 1069 insertions(+), 440 deletions(-) create mode 100644 Src/DDD.Core.Abstractions/Mapping/IMappingContext.cs create mode 100644 Src/DDD.Core.Abstractions/Mapping/MappingContext.cs create mode 100644 Src/DDD.Core.Abstractions/Validation/IAsyncObjectValidatorExtensions.cs create mode 100644 Src/DDD.Core.Abstractions/Validation/ISyncObjectValidatorExtensions.cs create mode 100644 Src/DDD.Core.Abstractions/Validation/IValidationContext.cs create mode 100644 Src/DDD.Core.Abstractions/Validation/IValidationContextExtensions.cs create mode 100644 Src/DDD.Core.Abstractions/Validation/ValidationContext.cs create mode 100644 Src/DDD.Core.Abstractions/Validation/ValidationContextInfo.cs create mode 100644 Src/DDD.Core/Application/IContextualCommandProcessorExtensions.cs create mode 100644 Src/DDD.Core/Application/IContextualQueryProcessorExtensions.cs create mode 100644 Src/DDD.Core/Application/IEventPublisherExtensions.cs diff --git a/Src/DDD.Core.Abstractions/Mapping/CompositeTranslator.cs b/Src/DDD.Core.Abstractions/Mapping/CompositeTranslator.cs index dc50a4f..79c09f3 100644 --- a/Src/DDD.Core.Abstractions/Mapping/CompositeTranslator.cs +++ b/Src/DDD.Core.Abstractions/Mapping/CompositeTranslator.cs @@ -40,14 +40,14 @@ public void Register(IObjectTranslator /// 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, TDestination> translator) + 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, IDictionary context) + public override TDestination Translate(TSource source, IMappingContext context) { if (source == null) return null; var translator = this.translators.FirstOrDefault(t => t.SourceType.IsAssignableFrom(source.GetType())); diff --git a/Src/DDD.Core.Abstractions/Mapping/DelegatingTranslator.cs b/Src/DDD.Core.Abstractions/Mapping/DelegatingTranslator.cs index ea908af..7fb5851 100644 --- a/Src/DDD.Core.Abstractions/Mapping/DelegatingTranslator.cs +++ b/Src/DDD.Core.Abstractions/Mapping/DelegatingTranslator.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using EnsureThat; namespace DDD.Mapping @@ -14,13 +13,13 @@ public class DelegatingTranslator : ObjectTranslator, TDestination> translator; + private readonly Func translator; #endregion Fields #region Constructors - public DelegatingTranslator(Func, TDestination> translator) + public DelegatingTranslator(Func translator) { Ensure.That(translator).IsNotNull(); this.translator = translator; @@ -30,9 +29,10 @@ public DelegatingTranslator(Func, TDestinat #region Methods - public override TDestination Translate(TSource source, IDictionary context = null) + public override TDestination Translate(TSource source, IMappingContext context) { Ensure.That(source).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); return this.translator(source, context); } 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/IMappingProcessor.cs b/Src/DDD.Core.Abstractions/Mapping/IMappingProcessor.cs index 4ef63f4..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 context = null) + void Map(TSource source, TDestination destination, IMappingContext context) where TSource : class where TDestination : class; - TDestination Translate(object source, IDictionary context = null) + TDestination Translate(object source, IMappingContext context) where TDestination : class; - TDestination Translate(TSource source, IDictionary context = 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 46749c2..b2349b9 100644 --- a/Src/DDD.Core.Abstractions/Mapping/IMappingProcessorExtensions.cs +++ b/Src/DDD.Core.Abstractions/Mapping/IMappingProcessorExtensions.cs @@ -3,12 +3,36 @@ 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, @@ -18,9 +42,7 @@ public static void Map(this IMappingProcessor processor, where TDestination : class { Ensure.That(processor, nameof(processor)).IsNotNull(); - var dictionary = new Dictionary(); - dictionary.AddObject(context); - processor.Map(source, destination, dictionary); + processor.Map(source, destination, MappingContext.FromObject(context)); } public static TDestination Translate(this IMappingProcessor processor, @@ -29,9 +51,7 @@ public static TDestination Translate(this IMappingProcessor proces where TDestination : class { Ensure.That(processor, nameof(processor)).IsNotNull(); - var dictionary = new Dictionary(); - dictionary.AddObject(context); - return processor.Translate(source, dictionary); + return processor.Translate(source, MappingContext.FromObject(context)); } public static TDestination Translate(this IMappingProcessor processor, @@ -41,28 +61,45 @@ public static TDestination Translate(this IMappingProcess where TDestination : class { Ensure.That(processor, nameof(processor)).IsNotNull(); - var dictionary = new Dictionary(); - dictionary.AddObject(context); - return processor.Translate(source, dictionary); + return processor.Translate(source, MappingContext.FromObject(context)); + } + + public static IEnumerable TranslateCollection(this IMappingProcessor processor, + IEnumerable source) + where TSource : class + where TDestination : class + { + 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 context = null) + IMappingContext context) where TSource : class where TDestination : class { Ensure.That(processor, nameof(processor)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); foreach (var item in source) yield return processor.Translate(item, context); } public static IEnumerable TranslateCollection(this IMappingProcessor processor, IEnumerable source, - IDictionary context = null) + IMappingContext context) where TDestination : class { Ensure.That(processor, nameof(processor)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); foreach (var item in source) yield return processor.Translate(item, context); } @@ -74,9 +111,7 @@ public static IEnumerable TranslateCollection(); - dictionary.AddObject(context); - return processor.TranslateCollection(source, dictionary); + return processor.TranslateCollection(source, MappingContext.FromObject(context)); } public static IEnumerable TranslateCollection(this IMappingProcessor processor, @@ -85,9 +120,7 @@ public static IEnumerable TranslateCollection(this I where TDestination : class { Ensure.That(processor, nameof(processor)).IsNotNull(); - var dictionary = new Dictionary(); - dictionary.AddObject(context); - return processor.TranslateCollection(source, dictionary); + 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 f17666b..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 context = 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 f931402..6446fe1 100644 --- a/Src/DDD.Core.Abstractions/Mapping/IObjectMapperExtensions.cs +++ b/Src/DDD.Core.Abstractions/Mapping/IObjectMapperExtensions.cs @@ -1,15 +1,22 @@ using EnsureThat; -using System.Collections.Generic; 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, @@ -18,9 +25,7 @@ public static void Map(this IObjectMapper(); - dictionary.AddObject(context); - mapper.Map(source, destination, dictionary); + 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 4207139..f851524 100644 --- a/Src/DDD.Core.Abstractions/Mapping/IObjectTranslator.cs +++ b/Src/DDD.Core.Abstractions/Mapping/IObjectTranslator.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; namespace DDD.Mapping { @@ -20,7 +19,7 @@ public interface IObjectTranslator #region Methods - object Translate(object source, IDictionary context = 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 4375c2e..83f3c67 100644 --- a/Src/DDD.Core.Abstractions/Mapping/IObjectTranslatorExtensions.cs +++ b/Src/DDD.Core.Abstractions/Mapping/IObjectTranslatorExtensions.cs @@ -3,13 +3,20 @@ 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 context) @@ -17,18 +24,26 @@ public static TDestination Translate(this IObjectTranslat where TDestination : class { Ensure.That(translator, nameof(translator)).IsNotNull(); - var dictionary = new Dictionary(); - dictionary.AddObject(context); - return translator.Translate(source, dictionary); + return translator.Translate(source, MappingContext.FromObject(context)); + } + + public static IEnumerable TranslateCollection(this IObjectTranslator translator, + IEnumerable source) + where TSource : class + where TDestination : class + { + Ensure.That(translator, nameof(translator)).IsNotNull(); + return translator.TranslateCollection(source, new MappingContext()); } public static IEnumerable TranslateCollection(this IObjectTranslator translator, IEnumerable source, - IDictionary context = null) + IMappingContext context) where TSource : class where TDestination : class { Ensure.That(translator, nameof(translator)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); foreach (var item in source) yield return translator.Translate(item, context); } @@ -40,9 +55,7 @@ public static IEnumerable TranslateCollection(); - dictionary.AddObject(context); - return translator.TranslateCollection(source, dictionary); + 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 index b54c0c5..e813960 100644 --- a/Src/DDD.Core.Abstractions/Mapping/IObjectTranslator`1.cs +++ b/Src/DDD.Core.Abstractions/Mapping/IObjectTranslator`1.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace DDD.Mapping +namespace DDD.Mapping { /// /// Defines a method that translates an input object of one type into an output object of a different type. @@ -11,7 +9,7 @@ public interface IObjectTranslator : IObjectTransl { #region Methods - TDestination Translate(TSource source, IDictionary context = null); + 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/MappingProcessor.cs b/Src/DDD.Core.Abstractions/Mapping/MappingProcessor.cs index 7a77bba..08f1262 100644 --- a/Src/DDD.Core.Abstractions/Mapping/MappingProcessor.cs +++ b/Src/DDD.Core.Abstractions/Mapping/MappingProcessor.cs @@ -1,6 +1,5 @@ using EnsureThat; using System; -using System.Collections.Generic; namespace DDD.Mapping { @@ -28,7 +27,7 @@ public MappingProcessor(IServiceProvider serviceProvider) #region Methods - public void Map(TSource source, TDestination destination, IDictionary context = null) + public void Map(TSource source, TDestination destination, IMappingContext context) where TSource : class where TDestination : class { @@ -37,7 +36,7 @@ public void Map(TSource source, TDestination destination, mapper.Map(source, destination, context); } - public TDestination Translate(object source, IDictionary context = null) + public TDestination Translate(object source, IMappingContext context) where TDestination : class { if (source == null) return null; @@ -47,7 +46,7 @@ public TDestination Translate(object source, IDictionary(TSource source, IDictionary context = null) + public TDestination Translate(TSource source, IMappingContext context) where TSource : class where TDestination : class { diff --git a/Src/DDD.Core.Abstractions/Mapping/ObjectTranslator.cs b/Src/DDD.Core.Abstractions/Mapping/ObjectTranslator.cs index 9f45bbb..7da8f1c 100644 --- a/Src/DDD.Core.Abstractions/Mapping/ObjectTranslator.cs +++ b/Src/DDD.Core.Abstractions/Mapping/ObjectTranslator.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; namespace DDD.Mapping { @@ -20,9 +19,9 @@ public abstract class ObjectTranslator : IObjectTranslato #region Methods - public abstract TDestination Translate(TSource source, IDictionary context); + public abstract TDestination Translate(TSource source, IMappingContext context); - object IObjectTranslator.Translate(object source, IDictionary context) => this.Translate((TSource)source, context); + object IObjectTranslator.Translate(object source, IMappingContext context) => this.Translate((TSource)source, context); #endregion Methods } diff --git a/Src/DDD.Core.Abstractions/Validation/IAsyncObjectValidator.cs b/Src/DDD.Core.Abstractions/Validation/IAsyncObjectValidator.cs index b861974..1d3525e 100644 --- a/Src/DDD.Core.Abstractions/Validation/IAsyncObjectValidator.cs +++ b/Src/DDD.Core.Abstractions/Validation/IAsyncObjectValidator.cs @@ -1,5 +1,4 @@ -using System.Threading; -using System.Threading.Tasks; +using System.Threading.Tasks; namespace DDD.Validation { @@ -14,7 +13,7 @@ public interface IAsyncObjectValidator where T :class /// /// Validates asynchronously an object of a specified type. /// - Task ValidateAsync(T obj, string ruleSet = null, CancellationToken cancellationToken = default); + 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/ISyncObjectValidator.cs b/Src/DDD.Core.Abstractions/Validation/ISyncObjectValidator.cs index 469186d..b559dc3 100644 --- a/Src/DDD.Core.Abstractions/Validation/ISyncObjectValidator.cs +++ b/Src/DDD.Core.Abstractions/Validation/ISyncObjectValidator.cs @@ -11,7 +11,7 @@ public interface ISyncObjectValidator where T :class /// /// Validates synchronously an object of a specified type. /// - ValidationResult Validate(T obj, string ruleSet = null); + 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.Dapper/EventStreamPositionUpdater.cs b/Src/DDD.Core.Dapper/EventStreamPositionUpdater.cs index a3b5cd6..89ac13f 100644 --- a/Src/DDD.Core.Dapper/EventStreamPositionUpdater.cs +++ b/Src/DDD.Core.Dapper/EventStreamPositionUpdater.cs @@ -44,7 +44,7 @@ public EventStreamPositionUpdater(IDbConnectionProvider connectionProv #region Methods - public void Handle(UpdateEventStreamPosition command, IMessageContext context = null) + public void Handle(UpdateEventStreamPosition command, IMessageContext context) { Ensure.That(command, nameof(command)).IsNotNull(); try @@ -70,15 +70,16 @@ public void Handle(UpdateEventStreamPosition command, IMessageContext context = } } - public async Task HandleAsync(UpdateEventStreamPosition command, IMessageContext context = null) + 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() ?? default; + var cancellationToken = context.CancellationToken(); var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); var expressions = connection.Expressions(); await connection.ExecuteAsync diff --git a/Src/DDD.Core.Dapper/EventStreamReader.cs b/Src/DDD.Core.Dapper/EventStreamReader.cs index eedcb1c..dd7d0fe 100644 --- a/Src/DDD.Core.Dapper/EventStreamReader.cs +++ b/Src/DDD.Core.Dapper/EventStreamReader.cs @@ -45,7 +45,7 @@ public EventStreamReader(IDbConnectionProvider connectionProvider) #region Methods - public IEnumerable Handle(ReadEventStream query, IMessageContext context = null) + public IEnumerable Handle(ReadEventStream query, IMessageContext context) { Ensure.That(query, nameof(query)).IsNotNull(); try @@ -66,13 +66,14 @@ public IEnumerable Handle(ReadEventStream query, IMessageContext context } } - public async Task> HandleAsync(ReadEventStream query, IMessageContext context = null) + 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() ?? default; + var cancellationToken = context.CancellationToken(); var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); return await connection.QueryAsync ( diff --git a/Src/DDD.Core.Dapper/EventStreamSubcriber.cs b/Src/DDD.Core.Dapper/EventStreamSubcriber.cs index ff60c55..237d6e6 100644 --- a/Src/DDD.Core.Dapper/EventStreamSubcriber.cs +++ b/Src/DDD.Core.Dapper/EventStreamSubcriber.cs @@ -44,7 +44,7 @@ public EventStreamSubscriber(IDbConnectionProvider connectionProvider) #region Methods - public void Handle(SubscribeToEventStream command, IMessageContext context = null) + public void Handle(SubscribeToEventStream command, IMessageContext context) { Ensure.That(command, nameof(command)).IsNotNull(); try @@ -70,15 +70,16 @@ public void Handle(SubscribeToEventStream command, IMessageContext context = nul } } - public async Task HandleAsync(SubscribeToEventStream command, IMessageContext context = null) + 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() ?? default; + var cancellationToken = context.CancellationToken(); var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); var expressions = connection.Expressions(); await connection.ExecuteAsync diff --git a/Src/DDD.Core.Dapper/EventStreamsFinder.cs b/Src/DDD.Core.Dapper/EventStreamsFinder.cs index f8002c2..d1648d3 100644 --- a/Src/DDD.Core.Dapper/EventStreamsFinder.cs +++ b/Src/DDD.Core.Dapper/EventStreamsFinder.cs @@ -43,7 +43,7 @@ public EventStreamsFinder(IDbConnectionProvider connectionProvider) #region Methods - public IEnumerable Handle(FindEventStreams query, IMessageContext context = null) + public IEnumerable Handle(FindEventStreams query, IMessageContext context) { Ensure.That(query, nameof(query)).IsNotNull(); try @@ -63,13 +63,14 @@ public IEnumerable Handle(FindEventStreams query, IMessageContext c } } - public async Task> HandleAsync(FindEventStreams query, IMessageContext context = null) + 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() ?? default; + var cancellationToken = context.CancellationToken(); var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); return await connection.QueryAsync ( diff --git a/Src/DDD.Core.Dapper/FailedEventStreamExcluder.cs b/Src/DDD.Core.Dapper/FailedEventStreamExcluder.cs index 3b635d4..2dcbc0e 100644 --- a/Src/DDD.Core.Dapper/FailedEventStreamExcluder.cs +++ b/Src/DDD.Core.Dapper/FailedEventStreamExcluder.cs @@ -45,7 +45,7 @@ public FailedEventStreamCreator(IDbConnectionProvider connectionProvid #region Methods - public void Handle(ExcludeFailedEventStream command, IMessageContext context = null) + public void Handle(ExcludeFailedEventStream command, IMessageContext context) { Ensure.That(command, nameof(command)).IsNotNull(); try @@ -71,15 +71,16 @@ public void Handle(ExcludeFailedEventStream command, IMessageContext context = n } } - public async Task HandleAsync(ExcludeFailedEventStream command, IMessageContext context = null) + 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() ?? default; + var cancellationToken = context.CancellationToken(); var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); var expressions = connection.Expressions(); await connection.ExecuteAsync diff --git a/Src/DDD.Core.Dapper/FailedEventStreamIncluder.cs b/Src/DDD.Core.Dapper/FailedEventStreamIncluder.cs index 0118bf3..5f4229a 100644 --- a/Src/DDD.Core.Dapper/FailedEventStreamIncluder.cs +++ b/Src/DDD.Core.Dapper/FailedEventStreamIncluder.cs @@ -43,7 +43,7 @@ public FailedEventStreamDeleter(IDbConnectionProvider connectionProvid #region Methods - public void Handle(IncludeFailedEventStream command, IMessageContext context = null) + public void Handle(IncludeFailedEventStream command, IMessageContext context) { Ensure.That(command, nameof(command)).IsNotNull(); try @@ -69,15 +69,16 @@ public void Handle(IncludeFailedEventStream command, IMessageContext context = n } } - public async Task HandleAsync(IncludeFailedEventStream command, IMessageContext context = null) + 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() ?? default; + var cancellationToken = context.CancellationToken(); var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); var expressions = connection.Expressions(); await connection.ExecuteAsync diff --git a/Src/DDD.Core.Dapper/FailedEventStreamPositionUpdater.cs b/Src/DDD.Core.Dapper/FailedEventStreamPositionUpdater.cs index 5f7af28..3582a24 100644 --- a/Src/DDD.Core.Dapper/FailedEventStreamPositionUpdater.cs +++ b/Src/DDD.Core.Dapper/FailedEventStreamPositionUpdater.cs @@ -44,7 +44,7 @@ public FailedEventStreamPositionUpdater(IDbConnectionProvider connecti #region Methods - public void Handle(UpdateFailedEventStreamPosition command, IMessageContext context = null) + public void Handle(UpdateFailedEventStreamPosition command, IMessageContext context) { Ensure.That(command, nameof(command)).IsNotNull(); try @@ -70,15 +70,16 @@ public void Handle(UpdateFailedEventStreamPosition command, IMessageContext cont } } - public async Task HandleAsync(UpdateFailedEventStreamPosition command, IMessageContext context = null) + 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() ?? default; + var cancellationToken = context.CancellationToken(); var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); var expressions = connection.Expressions(); await connection.ExecuteAsync diff --git a/Src/DDD.Core.Dapper/FailedEventStreamReader.cs b/Src/DDD.Core.Dapper/FailedEventStreamReader.cs index 9333790..9990424 100644 --- a/Src/DDD.Core.Dapper/FailedEventStreamReader.cs +++ b/Src/DDD.Core.Dapper/FailedEventStreamReader.cs @@ -44,7 +44,7 @@ public FailedEventStreamReader(IDbConnectionProvider connectionProvide #region Methods - public IEnumerable Handle(ReadFailedEventStream query, IMessageContext context = null) + public IEnumerable Handle(ReadFailedEventStream query, IMessageContext context) { Ensure.That(query, nameof(query)).IsNotNull(); try @@ -65,13 +65,14 @@ public IEnumerable Handle(ReadFailedEventStream query, IMessageContext co } } - public async Task> HandleAsync(ReadFailedEventStream query, IMessageContext context = null) + 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() ?? default; + var cancellationToken = context.CancellationToken(); var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); return await connection.QueryAsync ( diff --git a/Src/DDD.Core.Dapper/FailedEventStreamUpdater.cs b/Src/DDD.Core.Dapper/FailedEventStreamUpdater.cs index 6c956ee..a259fe5 100644 --- a/Src/DDD.Core.Dapper/FailedEventStreamUpdater.cs +++ b/Src/DDD.Core.Dapper/FailedEventStreamUpdater.cs @@ -44,7 +44,7 @@ public FailedEventStreamUpdater(IDbConnectionProvider connectionProvid #region Methods - public void Handle(UpdateFailedEventStream command, IMessageContext context = null) + public void Handle(UpdateFailedEventStream command, IMessageContext context) { Ensure.That(command, nameof(command)).IsNotNull(); try @@ -70,15 +70,16 @@ public void Handle(UpdateFailedEventStream command, IMessageContext context = nu } } - public async Task HandleAsync(UpdateFailedEventStream command, IMessageContext context = null) + 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() ?? default; + var cancellationToken = context.CancellationToken(); var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); var expressions = connection.Expressions(); await connection.ExecuteAsync diff --git a/Src/DDD.Core.Dapper/FailedEventStreamsFinder.cs b/Src/DDD.Core.Dapper/FailedEventStreamsFinder.cs index 494a0a8..a137697 100644 --- a/Src/DDD.Core.Dapper/FailedEventStreamsFinder.cs +++ b/Src/DDD.Core.Dapper/FailedEventStreamsFinder.cs @@ -43,7 +43,7 @@ public FailedEventStreamsFinder(IDbConnectionProvider connectionProvid #region Methods - public IEnumerable Handle(FindFailedEventStreams query, IMessageContext context = null) + public IEnumerable Handle(FindFailedEventStreams query, IMessageContext context) { Ensure.That(query, nameof(query)).IsNotNull(); try @@ -64,13 +64,14 @@ public IEnumerable Handle(FindFailedEventStreams query, IMess } } - public async Task> HandleAsync(FindFailedEventStreams query, IMessageContext context = null) + 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() ?? default; + var cancellationToken = context.CancellationToken(); var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); var expressions = connection.Expressions(); return await connection.QueryAsync diff --git a/Src/DDD.Core.Dapper/FailedRecurringCommandUpdater.cs b/Src/DDD.Core.Dapper/FailedRecurringCommandUpdater.cs index f2c728d..572f329 100644 --- a/Src/DDD.Core.Dapper/FailedRecurringCommandUpdater.cs +++ b/Src/DDD.Core.Dapper/FailedRecurringCommandUpdater.cs @@ -44,7 +44,7 @@ public FailedRecurringCommandUpdater(IDbConnectionProvider connectionP #region Methods - public void Handle(MarkRecurringCommandAsFailed command, IMessageContext context = null) + public void Handle(MarkRecurringCommandAsFailed command, IMessageContext context) { Ensure.That(command, nameof(command)).IsNotNull(); try @@ -70,15 +70,16 @@ public void Handle(MarkRecurringCommandAsFailed command, IMessageContext context } } - public async Task HandleAsync(MarkRecurringCommandAsFailed command, IMessageContext context = null) + 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() ?? default; + var cancellationToken = context.CancellationToken(); var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); var expressions = connection.Expressions(); await connection.ExecuteAsync diff --git a/Src/DDD.Core.Dapper/RecurringCommandIdGenerator.cs b/Src/DDD.Core.Dapper/RecurringCommandIdGenerator.cs index c4e4c3a..beba2ff 100644 --- a/Src/DDD.Core.Dapper/RecurringCommandIdGenerator.cs +++ b/Src/DDD.Core.Dapper/RecurringCommandIdGenerator.cs @@ -39,7 +39,7 @@ public RecurringCommandIdGenerator(IDbConnectionProvider connectionPro #region Methods - public Guid Handle(GenerateRecurringCommandId query, IMessageContext context = null) + public Guid Handle(GenerateRecurringCommandId query, IMessageContext context) { Ensure.That(query, nameof(query)).IsNotNull(); try diff --git a/Src/DDD.Core.Dapper/RecurringCommandRegister.cs b/Src/DDD.Core.Dapper/RecurringCommandRegister.cs index 37cc247..e488363 100644 --- a/Src/DDD.Core.Dapper/RecurringCommandRegister.cs +++ b/Src/DDD.Core.Dapper/RecurringCommandRegister.cs @@ -44,7 +44,7 @@ public RecurringCommandRegister(IDbConnectionProvider connectionProvid #region Methods - public void Handle(RegisterRecurringCommand command, IMessageContext context = null) + public void Handle(RegisterRecurringCommand command, IMessageContext context) { Ensure.That(command, nameof(command)).IsNotNull(); try @@ -93,15 +93,16 @@ public void Handle(RegisterRecurringCommand command, IMessageContext context = n } } - public async Task HandleAsync(RegisterRecurringCommand command, IMessageContext context = null) + 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() ?? default; + var cancellationToken = context.CancellationToken(); var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); var parameterPrefix = connection.Expressions().ParameterPrefix(); var parameters = ToParameters(command, connection); diff --git a/Src/DDD.Core.Dapper/RecurringCommandsFinder.cs b/Src/DDD.Core.Dapper/RecurringCommandsFinder.cs index 28c556c..a3f7be4 100644 --- a/Src/DDD.Core.Dapper/RecurringCommandsFinder.cs +++ b/Src/DDD.Core.Dapper/RecurringCommandsFinder.cs @@ -43,7 +43,7 @@ public RecurringCommandsFinder(IDbConnectionProvider connectionProvide #region Methods - public IEnumerable Handle(FindRecurringCommands query, IMessageContext context = null) + public IEnumerable Handle(FindRecurringCommands query, IMessageContext context) { Ensure.That(query, nameof(query)).IsNotNull(); try @@ -64,13 +64,14 @@ public IEnumerable Handle(FindRecurringCommands query, IMessag } } - public async Task> HandleAsync(FindRecurringCommands query, IMessageContext context = null) + 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() ?? default; + var cancellationToken = context.CancellationToken(); var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); var expressions = connection.Expressions(); return await connection.QueryAsync diff --git a/Src/DDD.Core.Dapper/SuccessfulRecurringCommandUpdater.cs b/Src/DDD.Core.Dapper/SuccessfulRecurringCommandUpdater.cs index ddcfc8c..06ada29 100644 --- a/Src/DDD.Core.Dapper/SuccessfulRecurringCommandUpdater.cs +++ b/Src/DDD.Core.Dapper/SuccessfulRecurringCommandUpdater.cs @@ -44,7 +44,7 @@ public SuccessfulRecurringCommandUpdater(IDbConnectionProvider connect #region Methods - public void Handle(MarkRecurringCommandAsSuccessful command, IMessageContext context = null) + public void Handle(MarkRecurringCommandAsSuccessful command, IMessageContext context) { Ensure.That(command, nameof(command)).IsNotNull(); try @@ -70,15 +70,16 @@ public void Handle(MarkRecurringCommandAsSuccessful command, IMessageContext con } } - public async Task HandleAsync(MarkRecurringCommandAsSuccessful command, IMessageContext context = null) + 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() ?? default; + var cancellationToken = context.CancellationToken(); var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); var expressions = connection.Expressions(); await connection.ExecuteAsync diff --git a/Src/DDD.Core.FluentValidation/DDD.Core.FluentValidation.csproj b/Src/DDD.Core.FluentValidation/DDD.Core.FluentValidation.csproj index e907b2a..f25a4af 100644 --- a/Src/DDD.Core.FluentValidation/DDD.Core.FluentValidation.csproj +++ b/Src/DDD.Core.FluentValidation/DDD.Core.FluentValidation.csproj @@ -30,7 +30,6 @@ - \ No newline at end of file diff --git a/Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs b/Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs index 7910db3..bf87f77 100644 --- a/Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs +++ b/Src/DDD.Core.FluentValidation/FluentValidatorAdapter.cs @@ -1,22 +1,21 @@ -using FluentValidation; -using FluentValidation.Results; +using System.Threading.Tasks; using EnsureThat; -using System.Threading.Tasks; -using System.Threading; +using FluentValidation; namespace DDD.Core.Infrastructure.Validation { using Mapping; using Threading; + using DDD.Validation; - public class FluentValidatorAdapter : DDD.Validation.IObjectValidator + public class FluentValidatorAdapter : IObjectValidator where T : class { #region Fields private readonly IValidator fluentValidator; - private readonly IObjectTranslator resultTranslator; + private readonly IObjectTranslator resultTranslator; #endregion Fields @@ -37,14 +36,16 @@ public FluentValidatorAdapter(IValidator fluentValidator) /// 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) { - ValidationResult result; - if (string.IsNullOrWhiteSpace(ruleSet)) + 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, context => context.IncludeRuleSets(ruleSet.Split(','))); + result = this.fluentValidator.Validate(obj, c => c.IncludeRuleSets(ruleSets)); return this.resultTranslator.Translate(result, new { ObjectName = obj.GetType().Name }); } @@ -52,16 +53,18 @@ public DDD.Validation.ValidationResult Validate(T obj, string ruleSet = null) /// Validates asynchronously the specified object. /// /// The object to validate. - /// The rule set. - /// A cancellation token. - public async Task ValidateAsync(T obj, string ruleSet = null, CancellationToken cancellationToken = default) + /// The validation context. + public async Task ValidateAsync(T obj, IValidationContext context) { + Ensure.That(context, nameof(context)).IsNotNull(); await new SynchronizationContextRemover(); - ValidationResult result; - if (string.IsNullOrWhiteSpace(ruleSet)) + 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, context => context.IncludeRuleSets(ruleSet.Split(',')), cancellationToken); + result = await this.fluentValidator.ValidateAsync(obj, c => c.IncludeRuleSets(ruleSets), cancellationToken); return this.resultTranslator.Translate(result, new { ObjectName = obj.GetType().Name }); } diff --git a/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs b/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs index 8dd5066..a217f16 100644 --- a/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs +++ b/Src/DDD.Core.FluentValidation/ValidationResultTranslator.cs @@ -2,7 +2,6 @@ using FluentValidation; using EnsureThat; using System.Linq; -using System.Collections.Generic; namespace DDD.Core.Infrastructure.Validation { @@ -14,10 +13,11 @@ internal class ValidationResultTranslator #region Methods - public override DDD.Validation.ValidationResult Translate(ValidationResult result, IDictionary context = null) + public override DDD.Validation.ValidationResult Translate(ValidationResult result, IMappingContext context) { Ensure.That(result, nameof(result)).IsNotNull(); - Ensure.That(context, nameof(context)).ContainsKey("ObjectName"); + 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(); diff --git a/Src/DDD.Core.Messages/Domain/BoundedContext.cs b/Src/DDD.Core.Messages/Domain/BoundedContext.cs index 178b496..7deb61f 100644 --- a/Src/DDD.Core.Messages/Domain/BoundedContext.cs +++ b/Src/DDD.Core.Messages/Domain/BoundedContext.cs @@ -7,7 +7,7 @@ namespace DDD.Core.Domain /// /// Represents a bounded context. /// - public class BoundedContext : IEquatable + public abstract class BoundedContext : IEquatable { #region Constructors diff --git a/Src/DDD.Core.NHibernate/NHRepositoryExceptionTranslator.cs b/Src/DDD.Core.NHibernate/NHRepositoryExceptionTranslator.cs index 4357b3a..dcf842e 100644 --- a/Src/DDD.Core.NHibernate/NHRepositoryExceptionTranslator.cs +++ b/Src/DDD.Core.NHibernate/NHRepositoryExceptionTranslator.cs @@ -22,11 +22,11 @@ internal class NHRepositoryExceptionTranslator : ObjectTranslator context = null) + public override RepositoryException Translate(Exception exception, IMappingContext context) { Ensure.That(exception, nameof(exception)).IsNotNull(); - Type entityType = null; - context?.TryGetValue("EntityType", out entityType); + Ensure.That(context, nameof(context)).IsNotNull(); + context.TryGetValue("EntityType", out Type entityType); switch (exception) { case DbException dbEx: diff --git a/Src/DDD.Core.Polly/AsyncPollyCommandHandler.cs b/Src/DDD.Core.Polly/AsyncPollyCommandHandler.cs index 55dcaef..37ed459 100644 --- a/Src/DDD.Core.Polly/AsyncPollyCommandHandler.cs +++ b/Src/DDD.Core.Polly/AsyncPollyCommandHandler.cs @@ -34,7 +34,7 @@ public AsyncPollyCommandHandler(IAsyncCommandHandler handler, IAsyncPo #region Methods - public async Task HandleAsync(TCommand command, IMessageContext context = null) + public async Task HandleAsync(TCommand command, IMessageContext context) { await policy.ExecuteAsync(() => this.handler.HandleAsync(command, context)); } diff --git a/Src/DDD.Core.Polly/AsyncPollyCommandHandler`1.cs b/Src/DDD.Core.Polly/AsyncPollyCommandHandler`1.cs index bb1a0fa..15fc25a 100644 --- a/Src/DDD.Core.Polly/AsyncPollyCommandHandler`1.cs +++ b/Src/DDD.Core.Polly/AsyncPollyCommandHandler`1.cs @@ -42,7 +42,7 @@ public AsyncPollyCommandHandler(IAsyncCommandHandler handler #region Methods - public async Task HandleAsync(TCommand command, IMessageContext context = null) + public async Task HandleAsync(TCommand command, IMessageContext context) { await policy.ExecuteAsync(() => this.handler.HandleAsync(command, context)); } diff --git a/Src/DDD.Core.Polly/AsyncPollyQueryHandler.cs b/Src/DDD.Core.Polly/AsyncPollyQueryHandler.cs index 029569a..02f1449 100644 --- a/Src/DDD.Core.Polly/AsyncPollyQueryHandler.cs +++ b/Src/DDD.Core.Polly/AsyncPollyQueryHandler.cs @@ -34,7 +34,7 @@ public AsyncPollyQueryHandler(IAsyncQueryHandler handler, IAsyn #region Methods - public async Task HandleAsync(TQuery query, IMessageContext context = null) + public async Task HandleAsync(TQuery query, IMessageContext context) { return await policy.ExecuteAsync(() => this.handler.HandleAsync(query, context)); } diff --git a/Src/DDD.Core.Polly/AsyncPollyQueryHandler`1.cs b/Src/DDD.Core.Polly/AsyncPollyQueryHandler`1.cs index d0ce34c..ceb32f2 100644 --- a/Src/DDD.Core.Polly/AsyncPollyQueryHandler`1.cs +++ b/Src/DDD.Core.Polly/AsyncPollyQueryHandler`1.cs @@ -42,7 +42,7 @@ public AsyncPollyQueryHandler(IAsyncQueryHandler hand #region Methods - public async Task HandleAsync(TQuery query, IMessageContext context = null) + public async Task HandleAsync(TQuery query, IMessageContext context) { return await policy.ExecuteAsync(() => this.handler.HandleAsync(query, context)); } diff --git a/Src/DDD.Core.Polly/SyncPollyCommandHandler.cs b/Src/DDD.Core.Polly/SyncPollyCommandHandler.cs index e7d2fee..f5ef48c 100644 --- a/Src/DDD.Core.Polly/SyncPollyCommandHandler.cs +++ b/Src/DDD.Core.Polly/SyncPollyCommandHandler.cs @@ -33,7 +33,7 @@ public SyncPollyCommandHandler(ISyncCommandHandler handler, ISyncPolic #region Methods - public void Handle(TCommand command, IMessageContext context = null) + public void Handle(TCommand command, IMessageContext context) { policy.Execute(() => this.handler.Handle(command, context)); } diff --git a/Src/DDD.Core.Polly/SyncPollyCommandHandler`1.cs b/Src/DDD.Core.Polly/SyncPollyCommandHandler`1.cs index bf9f315..3bda959 100644 --- a/Src/DDD.Core.Polly/SyncPollyCommandHandler`1.cs +++ b/Src/DDD.Core.Polly/SyncPollyCommandHandler`1.cs @@ -41,7 +41,7 @@ public SyncPollyCommandHandler(ISyncCommandHandler handler, #region Methods - public void Handle(TCommand command, IMessageContext context = null) + public void Handle(TCommand command, IMessageContext context) { policy.Execute(() => this.handler.Handle(command, context)); } diff --git a/Src/DDD.Core.Polly/SyncPollyQueryHandler.cs b/Src/DDD.Core.Polly/SyncPollyQueryHandler.cs index 34728c6..17ab25f 100644 --- a/Src/DDD.Core.Polly/SyncPollyQueryHandler.cs +++ b/Src/DDD.Core.Polly/SyncPollyQueryHandler.cs @@ -33,7 +33,7 @@ public SyncPollyQueryHandler(ISyncQueryHandler handler, ISyncPo #region Methods - public TResult Handle(TQuery query, IMessageContext context = null) + public TResult Handle(TQuery query, IMessageContext context) { return policy.Execute(() => this.handler.Handle(query, context)); } diff --git a/Src/DDD.Core.Polly/SyncPollyQueryHandler`1.cs b/Src/DDD.Core.Polly/SyncPollyQueryHandler`1.cs index 43b8ce4..d5d2061 100644 --- a/Src/DDD.Core.Polly/SyncPollyQueryHandler`1.cs +++ b/Src/DDD.Core.Polly/SyncPollyQueryHandler`1.cs @@ -41,7 +41,7 @@ public SyncPollyQueryHandler(IQueryHandler handler, I #region Methods - public TResult Handle(TQuery query, IMessageContext context = null) + public TResult Handle(TQuery query, IMessageContext context) { return policy.Execute(() => this.handler.Handle(query, context)); } diff --git a/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs index ba18d48..c480ce6 100644 --- a/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs @@ -40,8 +40,9 @@ public AsyncScopedCommandHandler(Func> handlerPro #region Methods - public async Task HandleAsync(TCommand command, IMessageContext context = null) + public async Task HandleAsync(TCommand command, IMessageContext context) { + Ensure.That(context, nameof(context)).IsNotNull(); await new SynchronizationContextRemover(); using (AsyncScopedLifestyle.BeginScope(container)) { @@ -49,7 +50,7 @@ public async Task HandleAsync(TCommand command, IMessageContext context = null) { var handler = this.handlerProvider(); await handler.HandleAsync(command, context); - if (context?.IsEventHandling() == true) // Exception to the rule "One transaction per command" to avoid to handle the same event more than once + if (context.IsEventHandling()) // Exception to the rule "One transaction per command" to avoid to handle the same event more than once await UpdateEventStreamPositionAsync(context); scope.Complete(); } diff --git a/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler`1.cs b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler`1.cs index 9c46d56..737e629 100644 --- a/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler`1.cs +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler`1.cs @@ -15,13 +15,12 @@ namespace DDD.Core.Infrastructure.DependencyInjection /// public class AsyncScopedCommandHandler : IAsyncCommandHandler where TCommand : class, ICommand - where TContext : BoundedContext, new() + where TContext : BoundedContext { #region Fields private readonly Container container; - private readonly TContext context; private readonly Func> handlerProvider; #endregion Fields @@ -34,20 +33,19 @@ public AsyncScopedCommandHandler(Func> Ensure.That(container, nameof(container)).IsNotNull(); this.handlerProvider = handlerProvider; this.container = container; - this.context = new TContext(); } #endregion Constructors #region Properties - public TContext Context => this.context; + public TContext Context => this.handlerProvider().Context; #endregion Properties #region Methods - public async Task HandleAsync(TCommand command, IMessageContext context = null) + public async Task HandleAsync(TCommand command, IMessageContext context) { using (AsyncScopedLifestyle.BeginScope(container)) { diff --git a/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs b/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs index 2edad9f..21d5161 100644 --- a/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs @@ -37,7 +37,7 @@ public AsyncScopedQueryHandler(Func> handler #region Methods - public async Task HandleAsync(TQuery query, IMessageContext context = null) + public async Task HandleAsync(TQuery query, IMessageContext context) { using (AsyncScopedLifestyle.BeginScope(container)) { diff --git a/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler`1.cs b/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler`1.cs index 31693d3..9e4fc8f 100644 --- a/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler`1.cs +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler`1.cs @@ -15,13 +15,12 @@ namespace DDD.Core.Infrastructure.DependencyInjection /// public class AsyncScopedQueryHandler : IAsyncQueryHandler where TQuery : class, IQuery - where TContext : BoundedContext, new() + where TContext : BoundedContext { #region Fields private readonly Container container; - private readonly TContext context; private readonly Func> handlerProvider; #endregion Fields @@ -34,20 +33,19 @@ public AsyncScopedQueryHandler(Func this.context; + public TContext Context => this.handlerProvider().Context; #endregion Properties #region Methods - public async Task HandleAsync(TQuery query, IMessageContext context = null) + public async Task HandleAsync(TQuery query, IMessageContext context) { using (AsyncScopedLifestyle.BeginScope(container)) { diff --git a/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs index eed69d7..9e7d619 100644 --- a/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs +++ b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs @@ -105,7 +105,7 @@ public static void RegisterBaseComponents(this Container container, IEnumerable< var notNullPredicate = predicate ?? (t => true); container.Options.DefaultScopedLifestyle = Lifestyle.CreateHybrid(new ThreadScopedLifestyle(), new AsyncScopedLifestyle()); container.RegisterInstance(container); - container.Collection.Register(assemblies, Lifestyle.Singleton); + container.RegisterBoundedContexts(assemblies, notNullPredicate); container.RegisterSingleton(); container.RegisterCommandHandlers(assemblies, notNullPredicate); container.RegisterSingleton(); @@ -121,7 +121,7 @@ public static void RegisterBaseComponents(this Container container, IEnumerable< /// Registers an event consumer for a specific bounded context. /// public static void RegisterEventConsumer(this Container container, EventConsumerSettings settings) - where TContext : BoundedContext, new() + where TContext : BoundedContext { Ensure.That(container, nameof(container)).IsNotNull(); Ensure.That(settings, nameof(settings)).IsNotNull(); @@ -134,7 +134,7 @@ public static void RegisterEventConsumer(this Container container, Eve /// Registers a recurring command manager for a specific bounded context. /// public static void RegisterRecurringCommandManager(this Container container, RecurringCommandManagerSettings settings) - where TContext : BoundedContext, new() + where TContext : BoundedContext { Ensure.That(container, nameof(container)).IsNotNull(); Ensure.That(settings, nameof(settings)).IsNotNull(); @@ -210,6 +210,15 @@ private static void RegisterRepositories(this Container container, IEnumerable), assemblies, Lifestyle.Scoped, predicate); } + private static void RegisterBoundedContexts(this Container container, IEnumerable assemblies, Func predicate) + { + var contextTypes = container.GetTypesToRegister(assemblies) + .Where(predicate); + foreach(var contextType in contextTypes) + container.RegisterSingleton(contextType, contextType); + container.Collection.Register(assemblies, Lifestyle.Singleton); + } + #endregion Methods } } diff --git a/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs b/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs index 2d6e986..9de2a65 100644 --- a/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs +++ b/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs @@ -38,15 +38,16 @@ public ThreadScopedCommandHandler(Func> handlerPro #region Methods< - public void Handle(TCommand command, IMessageContext context = null) + public void Handle(TCommand command, IMessageContext context) { + Ensure.That(context, nameof(context)).IsNotNull(); using (ThreadScopedLifestyle.BeginScope(container)) { using (var scope = new TransactionScope()) { var handler = this.handlerProvider(); handler.Handle(command, context); - if (context?.IsEventHandling() == true) // Exception to the rule "One transaction per command" to avoid to handle the same event more than once + if (context.IsEventHandling()) // Exception to the rule "One transaction per command" to avoid to handle the same event more than once UpdateEventStreamPosition(context); scope.Complete(); } diff --git a/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler`1.cs b/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler`1.cs index 5cad331..528e8f7 100644 --- a/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler`1.cs +++ b/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler`1.cs @@ -13,13 +13,12 @@ namespace DDD.Core.Infrastructure.DependencyInjection /// public class ThreadScopedCommandHandler : ISyncCommandHandler where TCommand : class, ICommand - where TContext : BoundedContext, new() + where TContext : BoundedContext { #region Fields private readonly Container container; - private readonly TContext context; private readonly Func> handlerProvider; #endregion Fields @@ -32,20 +31,19 @@ public ThreadScopedCommandHandler(Func> Ensure.That(container, nameof(container)).IsNotNull(); this.handlerProvider = handlerProvider; this.container = container; - this.context = new TContext(); } #endregion Constructors #region Properties - public TContext Context => this.context; + public TContext Context => this.handlerProvider().Context; #endregion Properties #region Methods - public void Handle(TCommand command, IMessageContext context = null) + public void Handle(TCommand command, IMessageContext context) { using (ThreadScopedLifestyle.BeginScope(container)) { diff --git a/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler.cs b/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler.cs index 0ec79e8..9aa6f85 100644 --- a/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler.cs +++ b/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler.cs @@ -35,7 +35,7 @@ public ThreadScopedQueryHandler(Func> handler #region Methods - public TResult Handle(TQuery query, IMessageContext context = null) + public TResult Handle(TQuery query, IMessageContext context) { using (ThreadScopedLifestyle.BeginScope(container)) { diff --git a/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler`1.cs b/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler`1.cs index 3eaf327..dac4e18 100644 --- a/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler`1.cs +++ b/Src/DDD.Core.SimpleInjector/ThreadScopedQueryHandler`1.cs @@ -13,13 +13,12 @@ namespace DDD.Core.Infrastructure.DependencyInjection /// public class ThreadScopedQueryHandler : ISyncQueryHandler where TQuery : class, IQuery - where TContext : BoundedContext, new() + where TContext : BoundedContext { #region Fields private readonly Container container; - private readonly TContext context; private readonly Func> handlerProvider; #endregion Fields @@ -32,20 +31,19 @@ public ThreadScopedQueryHandler(Func this.context; + public TContext Context => this.handlerProvider().Context; #endregion Properties #region Methods - public TResult Handle(TQuery query, IMessageContext context = null) + public TResult Handle(TQuery query, IMessageContext context) { using (ThreadScopedLifestyle.BeginScope(container)) { diff --git a/Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj b/Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj index 91638eb..e009531 100644 --- a/Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj +++ b/Src/DDD.Core.Xunit/DDD.Core.Xunit.csproj @@ -24,7 +24,6 @@ - 2.4.2 diff --git a/Src/DDD.Core.Xunit/DbFixture.cs b/Src/DDD.Core.Xunit/DbFixture.cs index 069bb86..9a96612 100644 --- a/Src/DDD.Core.Xunit/DbFixture.cs +++ b/Src/DDD.Core.Xunit/DbFixture.cs @@ -59,7 +59,7 @@ public int[] ExecuteScriptFromResources(string scriptName) public IDbConnectionProvider CreateConnectionProvider(bool pooling = true) { var connectionString = this.SetPooling(connectionSettings.ConnectionString, pooling); - return new LazyDbConnectionProvider(connectionSettings.ProviderName, connectionString); + return new LazyDbConnectionProvider(new TContext(), connectionSettings.ProviderName, connectionString); } public DbConnection CreateConnection(bool pooling = true) diff --git a/Src/DDD.Core.Xunit/IDbFixture.cs b/Src/DDD.Core.Xunit/IDbFixture.cs index c01b83f..34154fa 100644 --- a/Src/DDD.Core.Xunit/IDbFixture.cs +++ b/Src/DDD.Core.Xunit/IDbFixture.cs @@ -6,7 +6,7 @@ namespace DDD.Core.Infrastructure.Testing using Domain; public interface IDbFixture - where TContext : BoundedContext + where TContext : BoundedContext, new() { #region Methods diff --git a/Src/DDD.Core.Xunit/IDbFixtureExtensions.cs b/Src/DDD.Core.Xunit/IDbFixtureExtensions.cs index 8d52e69..4ae3885 100644 --- a/Src/DDD.Core.Xunit/IDbFixtureExtensions.cs +++ b/Src/DDD.Core.Xunit/IDbFixtureExtensions.cs @@ -13,7 +13,7 @@ public static class IDbFixtureExtensions #region Methods public static DbConnection CreateOpenConnection(this IDbFixture fixture, bool pooling = true) - where TContext : BoundedContext + where TContext : BoundedContext, new() { Ensure.That(fixture, nameof(fixture)).IsNotNull(); var connection = fixture.CreateConnection(pooling); @@ -24,7 +24,7 @@ public static DbConnection CreateOpenConnection(this IDbFixture CreateOpenConnectionAsync(this IDbFixture fixture, CancellationToken cancellationToken = default, bool pooling = true) - where TContext : BoundedContext + where TContext : BoundedContext, new() { Ensure.That(fixture, nameof(fixture)).IsNotNull(); var connection = fixture.CreateConnection(pooling); diff --git a/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging.cs b/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging.cs index e803e03..51db00e 100644 --- a/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging.cs +++ b/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging.cs @@ -34,7 +34,7 @@ public AsyncCommandHandlerWithLogging(IAsyncCommandHandler commandHand #region Methods - public async Task HandleAsync(TCommand command, IMessageContext context = null) + public async Task HandleAsync(TCommand command, IMessageContext context) { if (this.logger.IsEnabled(LogLevel.Debug)) { diff --git a/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging`1.cs b/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging`1.cs index 1efee46..dedcbf4 100644 --- a/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging`1.cs +++ b/Src/DDD.Core/Application/AsyncCommandHandlerWithLogging`1.cs @@ -42,7 +42,7 @@ public AsyncCommandHandlerWithLogging(IAsyncCommandHandler c #region Methods - public async Task HandleAsync(TCommand command, IMessageContext context = null) + public async Task HandleAsync(TCommand command, IMessageContext context) { if (this.logger.IsEnabled(LogLevel.Debug)) { diff --git a/Src/DDD.Core/Application/AsyncEventHandler.cs b/Src/DDD.Core/Application/AsyncEventHandler.cs index 3313552..8b62ed4 100644 --- a/Src/DDD.Core/Application/AsyncEventHandler.cs +++ b/Src/DDD.Core/Application/AsyncEventHandler.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using EnsureThat; namespace DDD.Core.Application { @@ -10,12 +11,22 @@ namespace DDD.Core.Application /// public abstract class AsyncEventHandler : IAsyncEventHandler where TEvent : class, IEvent - where TContext : BoundedContext, new() + 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; } = new TContext(); + public TContext Context { get; } BoundedContext IAsyncEventHandler.Context => this.Context; @@ -25,7 +36,7 @@ public abstract class AsyncEventHandler : IAsyncEventHandler this.HandleAsync((TEvent)@event, context); diff --git a/Src/DDD.Core/Application/AsyncEventHandlerWithLogging.cs b/Src/DDD.Core/Application/AsyncEventHandlerWithLogging.cs index 51fd8f3..da738f0 100644 --- a/Src/DDD.Core/Application/AsyncEventHandlerWithLogging.cs +++ b/Src/DDD.Core/Application/AsyncEventHandlerWithLogging.cs @@ -47,7 +47,7 @@ public AsyncEventHandlerWithLogging(IAsyncEventHandler eventHa #region Methods - public async Task HandleAsync(TEvent @event, IMessageContext context = null) + public async Task HandleAsync(TEvent @event, IMessageContext context) { if (this.logger.IsEnabled(LogLevel.Debug)) { diff --git a/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging.cs b/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging.cs index 19c10d9..da199cd 100644 --- a/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging.cs +++ b/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging.cs @@ -28,7 +28,7 @@ public AsyncQueryHandlerWithLogging(IAsyncQueryHandler queryHan #region Methods - public async Task HandleAsync(TQuery query, IMessageContext context = null) + public async Task HandleAsync(TQuery query, IMessageContext context) { if (this.logger.IsEnabled(LogLevel.Debug)) { diff --git a/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging`1.cs b/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging`1.cs index 5f56d60..8a158ab 100644 --- a/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging`1.cs +++ b/Src/DDD.Core/Application/AsyncQueryHandlerWithLogging`1.cs @@ -42,7 +42,7 @@ public AsyncQueryHandlerWithLogging(IAsyncQueryHandler HandleAsync(TQuery query, IMessageContext context = null) + public async Task HandleAsync(TQuery query, IMessageContext context) { if (this.logger.IsEnabled(LogLevel.Debug)) { diff --git a/Src/DDD.Core/Application/CommandProcessor.cs b/Src/DDD.Core/Application/CommandProcessor.cs index d8d0504..a9316f0 100644 --- a/Src/DDD.Core/Application/CommandProcessor.cs +++ b/Src/DDD.Core/Application/CommandProcessor.cs @@ -1,11 +1,11 @@ using EnsureThat; using System; -using System.Threading; using System.Threading.Tasks; namespace DDD.Core.Application { using Domain; + using System.Collections.Concurrent; using Validation; /// @@ -17,6 +17,8 @@ public class CommandProcessor : ICommandProcessor #region Fields private readonly IServiceProvider serviceProvider; + private static readonly ConcurrentDictionary contextualProcessors + = new ConcurrentDictionary(); #endregion Fields @@ -32,20 +34,24 @@ public CommandProcessor(IServiceProvider serviceProvider) #region Methods - public IContextualCommandProcessor In(TContext context) where TContext : BoundedContext + public IContextualCommandProcessor InGeneric(TContext context) where TContext : BoundedContext { Ensure.That(context, nameof(context)).IsNotNull(); - return new ContextualCommandProcessor(this.serviceProvider, context); + return (IContextualCommandProcessor)contextualProcessors.GetOrAdd(context, _ => + new ContextualCommandProcessor(this.serviceProvider, context)); } - public IContextualCommandProcessor In(BoundedContext context) + public IContextualCommandProcessor InSpecific(BoundedContext context) { Ensure.That(context, nameof(context)).IsNotNull(); - var processorType = typeof(ContextualCommandProcessor<>).MakeGenericType(context.GetType()); - return (IContextualCommandProcessor)Activator.CreateInstance(processorType, this.serviceProvider, context); + 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 = null) where TCommand : class, ICommand + public void Process(TCommand command, IMessageContext context) where TCommand : class, ICommand { Ensure.That(command, nameof(command)).IsNotNull(); var handler = this.serviceProvider.GetService>(); @@ -53,7 +59,7 @@ public void Process(TCommand command, IMessageContext context = null) handler.Handle(command, context); } - public Task ProcessAsync(TCommand command, IMessageContext context = null) where TCommand : class, ICommand + public Task ProcessAsync(TCommand command, IMessageContext context) where TCommand : class, ICommand { Ensure.That(command, nameof(command)).IsNotNull(); var handler = this.serviceProvider.GetService>(); @@ -61,20 +67,20 @@ public Task ProcessAsync(TCommand command, IMessageContext context = n return handler.HandleAsync(command, context); } - public ValidationResult Validate(TCommand command, string ruleSet = null) where TCommand : class, ICommand + public ValidationResult Validate(TCommand command, IValidationContext context) where TCommand : class, ICommand { Ensure.That(command, nameof(command)).IsNotNull(); var validator = this.serviceProvider.GetService>(); if (validator == null) throw new InvalidOperationException($"The command validator for type {typeof(ISyncObjectValidator)} could not be found."); - return validator.Validate(command, ruleSet); + return validator.Validate(command, context); } - public Task ValidateAsync(TCommand command, string ruleSet = null, CancellationToken cancellationToken = default) where TCommand : class, ICommand + 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) throw new InvalidOperationException($"The command validator for type {typeof(IAsyncObjectValidator)} could not be found."); - return validator.ValidateAsync(command, ruleSet, cancellationToken); + return validator.ValidateAsync(command, context); } #endregion Methods diff --git a/Src/DDD.Core/Application/CompositeTranslatorExtensions.cs b/Src/DDD.Core/Application/CompositeTranslatorExtensions.cs index 4c1b56b..f210fcf 100644 --- a/Src/DDD.Core/Application/CompositeTranslatorExtensions.cs +++ b/Src/DDD.Core/Application/CompositeTranslatorExtensions.cs @@ -16,8 +16,7 @@ public static void RegisterFallback(this CompositeTranslator((exception, context) => { - ICommand command = null; - context?.TryGetValue("Command", out command); + context.TryGetValue("Command", out ICommand command); return new CommandException(isTransient: false, command, exception); }); } @@ -27,8 +26,7 @@ public static void RegisterFallback(this CompositeTranslator((exception, context) => { - IQuery query = null; - context?.TryGetValue("Query", out query); + context.TryGetValue("Query", out IQuery query); return new QueryException(isTransient: false, query, exception); }); } diff --git a/Src/DDD.Core/Application/ContextualCommandProcessor.cs b/Src/DDD.Core/Application/ContextualCommandProcessor.cs index 25b6b0c..d414297 100644 --- a/Src/DDD.Core/Application/ContextualCommandProcessor.cs +++ b/Src/DDD.Core/Application/ContextualCommandProcessor.cs @@ -41,7 +41,7 @@ public ContextualCommandProcessor(IServiceProvider serviceProvider, TContext con #region Methods - public void Process(TCommand command, IMessageContext context = null) where TCommand : class, ICommand + public void Process(TCommand command, IMessageContext context) where TCommand : class, ICommand { Ensure.That(command, nameof(command)).IsNotNull(); var handler = this.serviceProvider.GetService>(); @@ -49,7 +49,7 @@ public void Process(TCommand command, IMessageContext context = null) handler.Handle(command, context); } - public Task ProcessAsync(TCommand command, IMessageContext context = null) where TCommand : class, ICommand + public Task ProcessAsync(TCommand command, IMessageContext context) where TCommand : class, ICommand { Ensure.That(command, nameof(command)).IsNotNull(); var handler = this.serviceProvider.GetService>(); diff --git a/Src/DDD.Core/Application/ContextualQueryProcessor.cs b/Src/DDD.Core/Application/ContextualQueryProcessor.cs index bc60c1c..edacf7a 100644 --- a/Src/DDD.Core/Application/ContextualQueryProcessor.cs +++ b/Src/DDD.Core/Application/ContextualQueryProcessor.cs @@ -41,7 +41,7 @@ public ContextualQueryProcessor(IServiceProvider serviceProvider, TContext conte #region Methods - public TResult Process(IQuery query, IMessageContext context = null) + public TResult Process(IQuery query, IMessageContext context) { Ensure.That(query, nameof(query)).IsNotNull(); var handlerType = typeof(ISyncQueryHandler<,,>).MakeGenericType(query.GetType(), typeof(TResult), typeof(TContext)); @@ -50,7 +50,7 @@ public TResult Process(IQuery query, IMessageContext context = return handler.Handle((dynamic)query, context); } - public Task ProcessAsync(IQuery query, IMessageContext context = null) + public Task ProcessAsync(IQuery query, IMessageContext context) { Ensure.That(query, nameof(query)).IsNotNull(); var handlerType = typeof(IAsyncQueryHandler<,,>).MakeGenericType(query.GetType(), typeof(TResult), typeof(TContext)); diff --git a/Src/DDD.Core/Application/DomainToCommandExceptionTranslator.cs b/Src/DDD.Core/Application/DomainToCommandExceptionTranslator.cs index 790ffa7..df9e28f 100644 --- a/Src/DDD.Core/Application/DomainToCommandExceptionTranslator.cs +++ b/Src/DDD.Core/Application/DomainToCommandExceptionTranslator.cs @@ -1,5 +1,4 @@ using EnsureThat; -using System.Collections.Generic; namespace DDD.Core.Application { @@ -12,11 +11,11 @@ public class DomainToCommandExceptionTranslator : ObjectTranslator context = null) + public override CommandException Translate(DomainException exception, IMappingContext context) { Ensure.That(exception).IsNotNull(); - ICommand command = null; - context?.TryGetValue("Command", out command); + Ensure.That(context, nameof(context)).IsNotNull(); + context.TryGetValue("Command", out ICommand command); switch (exception) { case DomainServiceConflictException _: diff --git a/Src/DDD.Core/Application/EventConsumer.cs b/Src/DDD.Core/Application/EventConsumer.cs index e3f2fa2..7e44e72 100644 --- a/Src/DDD.Core/Application/EventConsumer.cs +++ b/Src/DDD.Core/Application/EventConsumer.cs @@ -14,7 +14,7 @@ namespace DDD.Core.Application using Threading; public class EventConsumer : IEventConsumer, IDisposable - where TContext : BoundedContext, new() + where TContext : BoundedContext { #region Fields @@ -162,8 +162,8 @@ private async Task ConsumeEventsAsync() private async Task<(IEnumerable, IEnumerable)> FindAllStreamsAsync() { - var findStreams = this.queryProcessor.In().ProcessAsync(new FindEventStreams(), MessageContext.CancellableContext(this.CancellationToken)); - var findFailedStreams = this.queryProcessor.In().ProcessAsync(new FindFailedEventStreams(), MessageContext.CancellableContext(this.CancellationToken)); + 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 (findStreams.Result, findFailedStreams.Result); } @@ -209,7 +209,7 @@ private async Task ConsumeStreamWithoutExcludedStreamsAsync(EventStream stream, ExcludedStreamIds = excludedStreams.Select(s => s.StreamId).ToArray() }; var sourceContext = this.boundedContexts.Single(c => c.Code == stream.Source); - var notifiedEvents = await this.queryProcessor.In(sourceContext).ProcessAsync(query, MessageContext.CancellableContext(this.CancellationToken)); + var notifiedEvents = await this.queryProcessor.InSpecific(sourceContext).ProcessAsync(query, MessageContext.CancellableContext(this.CancellationToken)); foreach (var notifiedEvent in notifiedEvents) { this.CancellationToken.ThrowIfCancellationRequested(); @@ -246,7 +246,7 @@ private async Task ConsumeStreamWithoutExcludedStreamsAsync(EventStream stream, RetryDelays = this.GetRetryDelays(isTransientException, stream), BlockSize = stream.BlockSize }; - await this.commandProcessor.In().ProcessAsync(command); + await this.commandProcessor.InGeneric(Context).ProcessAsync(command); break; } } @@ -265,7 +265,7 @@ private async Task ConsumeExcludedStreamAsync(EventStream stream, FailedEventStr StreamId = excludedStream.StreamId }; var sourceContext = this.boundedContexts.Single(c => c.Code == excludedStream.StreamSource); - var notifiedEvents = await this.queryProcessor.In(sourceContext).ProcessAsync(query, MessageContext.CancellableContext(this.CancellationToken)); + var notifiedEvents = await this.queryProcessor.InSpecific(sourceContext).ProcessAsync(query, MessageContext.CancellableContext(this.CancellationToken)); var success = true; foreach (var notifiedEvent in notifiedEvents) { @@ -304,7 +304,7 @@ private async Task ConsumeExcludedStreamAsync(EventStream stream, FailedEventStr RetryMax = isNewEvent ? this.GetRetryMax(isTransientException, stream) : excludedStream.RetryMax, RetryDelays = isNewEvent ? this.GetRetryDelays(isTransientException, stream) : excludedStream.RetryDelays }; - await this.commandProcessor.In().ProcessAsync(command); + await this.commandProcessor.InGeneric(Context).ProcessAsync(command); break; } } @@ -316,7 +316,7 @@ private async Task ConsumeExcludedStreamAsync(EventStream stream, FailedEventStr Type = excludedStream.StreamType, Source = excludedStream.StreamSource }; - await this.commandProcessor.In().ProcessAsync(command); + await this.commandProcessor.InGeneric(Context).ProcessAsync(command); excludedStreams.Remove(excludedStream); } this.logger.LogInformation("The consumption of the following failed event stream in the context '{Context}' has finished : {FailedStream}", this.Context.Name, excludedStream); diff --git a/Src/DDD.Core/Application/EventConsumerSettings.cs b/Src/DDD.Core/Application/EventConsumerSettings.cs index db08068..a8df2ea 100644 --- a/Src/DDD.Core/Application/EventConsumerSettings.cs +++ b/Src/DDD.Core/Application/EventConsumerSettings.cs @@ -7,25 +7,22 @@ namespace DDD.Core.Application [DataContract()] public class EventConsumerSettings - where TContext : BoundedContext, new() + where TContext : BoundedContext { - #region Fields - - private readonly TContext context; - - #endregion Fields #region Constructors - public EventConsumerSettings(short consumationDelay, + public EventConsumerSettings(TContext context, + short consumationDelay, long? consumationMax = null) { + Ensure.That(context, nameof(context)).IsNotNull(); Ensure.That(consumationDelay, nameof(consumationDelay)).IsGte((short)0); if (consumationMax != null) Ensure.That(consumationMax.Value, nameof(consumationMax)).IsGte(0); + this.Context = context; this.ConsumationDelay = consumationDelay; this.ConsumationMax = consumationMax; - this.context = new TContext(); } #endregion Constructors @@ -47,8 +44,9 @@ public EventConsumerSettings(short consumationDelay, /// /// Gets the associated context. /// - public TContext Context => this.context; + public TContext Context { get; } #endregion Properties + } } diff --git a/Src/DDD.Core/Application/EventPublisher.cs b/Src/DDD.Core/Application/EventPublisher.cs index 9a3684b..ea163a1 100644 --- a/Src/DDD.Core/Application/EventPublisher.cs +++ b/Src/DDD.Core/Application/EventPublisher.cs @@ -8,7 +8,7 @@ namespace DDD.Core.Application using Threading; public class EventPublisher : IEventPublisher - where TContext : BoundedContext, new() + where TContext : BoundedContext { #region Fields @@ -19,11 +19,12 @@ public class EventPublisher : IEventPublisher #region Constructors - public EventPublisher(IServiceProvider serviceProvider) + public EventPublisher(TContext context, IServiceProvider serviceProvider) { + Ensure.That(context, nameof(context)).IsNotNull(); Ensure.That(serviceProvider, nameof(serviceProvider)).IsNotNull(); + this.Context = context; this.serviceProvider = serviceProvider; - this.Context = new TContext(); } #endregion Constructors @@ -38,7 +39,7 @@ public EventPublisher(IServiceProvider serviceProvider) #region Methods - public async Task PublishAsync(TEvent @event, IMessageContext context = null) where TEvent : class, IEvent + public async Task PublishAsync(TEvent @event, IMessageContext context) where TEvent : class, IEvent { Ensure.That(@event, nameof(@event)).IsNotNull(); await new SynchronizationContextRemover(); diff --git a/Src/DDD.Core/Application/EventTranslator.cs b/Src/DDD.Core/Application/EventTranslator.cs index 0c8d2ed..1730d5a 100644 --- a/Src/DDD.Core/Application/EventTranslator.cs +++ b/Src/DDD.Core/Application/EventTranslator.cs @@ -1,6 +1,5 @@ using EnsureThat; using System; -using System.Collections.Generic; using DDD.Serialization; namespace DDD.Core.Application @@ -30,9 +29,10 @@ public EventTranslator(ITextSerializer eventSerializer) #region Methods - public override Event Translate(IEvent @event, IDictionary context = null) + public override Event Translate(IEvent @event, IMappingContext context) { Ensure.That(@event, nameof(@event)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); Guid eventId = default; string streamId = null, streamType = null, issuedBy = null; if (context != null) diff --git a/Src/DDD.Core/Application/IAsyncCommandHandler.cs b/Src/DDD.Core/Application/IAsyncCommandHandler.cs index 55e6e9d..5805bff 100644 --- a/Src/DDD.Core/Application/IAsyncCommandHandler.cs +++ b/Src/DDD.Core/Application/IAsyncCommandHandler.cs @@ -14,7 +14,7 @@ public interface IAsyncCommandHandler /// /// Handles asynchronously a command of a specified type. /// - Task HandleAsync(TCommand command, IMessageContext context = null); + Task HandleAsync(TCommand command, IMessageContext context); #endregion Methods diff --git a/Src/DDD.Core/Application/IAsyncCommandHandlerExtensions.cs b/Src/DDD.Core/Application/IAsyncCommandHandlerExtensions.cs index 098a569..5b8c3ad 100644 --- a/Src/DDD.Core/Application/IAsyncCommandHandlerExtensions.cs +++ b/Src/DDD.Core/Application/IAsyncCommandHandlerExtensions.cs @@ -8,6 +8,15 @@ 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) diff --git a/Src/DDD.Core/Application/IAsyncEventHandler.cs b/Src/DDD.Core/Application/IAsyncEventHandler.cs index 536d5ef..325badc 100644 --- a/Src/DDD.Core/Application/IAsyncEventHandler.cs +++ b/Src/DDD.Core/Application/IAsyncEventHandler.cs @@ -29,7 +29,7 @@ public interface IAsyncEventHandler /// /// Handles asynchronously an event of a specified type in a specific bounded context. /// - Task HandleAsync(IEvent @event, IMessageContext context = null); + Task HandleAsync(IEvent @event, IMessageContext context); #endregion Methods } diff --git a/Src/DDD.Core/Application/IAsyncEventHandler`1.cs b/Src/DDD.Core/Application/IAsyncEventHandler`1.cs index bbf43a6..b78b985 100644 --- a/Src/DDD.Core/Application/IAsyncEventHandler`1.cs +++ b/Src/DDD.Core/Application/IAsyncEventHandler`1.cs @@ -26,7 +26,7 @@ public interface IAsyncEventHandler : IAsyncEventHandle /// /// Handles asynchronously an event of a specified type in a specific bounded context. /// - Task HandleAsync(TEvent @event, IMessageContext context = null); + Task HandleAsync(TEvent @event, IMessageContext context); #endregion Methods diff --git a/Src/DDD.Core/Application/IAsyncQueryHandler.cs b/Src/DDD.Core/Application/IAsyncQueryHandler.cs index 17fe0f6..19747ea 100644 --- a/Src/DDD.Core/Application/IAsyncQueryHandler.cs +++ b/Src/DDD.Core/Application/IAsyncQueryHandler.cs @@ -15,7 +15,7 @@ public interface IAsyncQueryHandler /// /// Handles asynchronously a query of a specified type and provides a result of a specified type. /// - Task HandleAsync(TQuery query, IMessageContext context = null); + Task HandleAsync(TQuery query, IMessageContext context); #endregion Methods } diff --git a/Src/DDD.Core/Application/IAsyncQueryHandlerExtensions.cs b/Src/DDD.Core/Application/IAsyncQueryHandlerExtensions.cs index 0bc59b0..ee5c20d 100644 --- a/Src/DDD.Core/Application/IAsyncQueryHandlerExtensions.cs +++ b/Src/DDD.Core/Application/IAsyncQueryHandlerExtensions.cs @@ -8,6 +8,15 @@ 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) diff --git a/Src/DDD.Core/Application/ICommandProcessor.cs b/Src/DDD.Core/Application/ICommandProcessor.cs index 84783a1..7bf7f58 100644 --- a/Src/DDD.Core/Application/ICommandProcessor.cs +++ b/Src/DDD.Core/Application/ICommandProcessor.cs @@ -1,10 +1,9 @@ -using System.Threading; -using System.Threading.Tasks; +using System.Threading.Tasks; namespace DDD.Core.Application { - using Domain; using Validation; + using Domain; /// /// Defines a component that validates and processes commands of any type. @@ -17,32 +16,32 @@ public interface ICommandProcessor /// /// Specify the bounded context in which the command must be processed. /// - IContextualCommandProcessor In(TContext context) where TContext : BoundedContext; + IContextualCommandProcessor InGeneric(TContext context) where TContext : BoundedContext; /// /// Specify the bounded context in which the command must be processed. /// - IContextualCommandProcessor In(BoundedContext context); + IContextualCommandProcessor InSpecific(BoundedContext context); /// /// Processes synchronously a command of a specified type. /// - void Process(TCommand command, IMessageContext context = null) where TCommand : class, ICommand; + void Process(TCommand command, IMessageContext context) where TCommand : class, ICommand; /// /// Processes asynchronously a command of a specified type. /// - Task ProcessAsync(TCommand command, IMessageContext context = null) where TCommand : class, ICommand; + Task ProcessAsync(TCommand command, IMessageContext context) where TCommand : class, ICommand; /// /// Validates synchronously a command of a specified type. /// - ValidationResult Validate(TCommand command, string ruleSet = null) where TCommand : class, ICommand; + ValidationResult Validate(TCommand command, IValidationContext context) where TCommand : class, ICommand; /// /// Validates asynchronously a command of a specified type. /// - Task ValidateAsync(TCommand command, string ruleSet = null, CancellationToken cancellationToken = default) where TCommand : class, ICommand; + Task ValidateAsync(TCommand command, IValidationContext context) where TCommand : class, ICommand; #endregion Methods } diff --git a/Src/DDD.Core/Application/ICommandProcessorExtensions.cs b/Src/DDD.Core/Application/ICommandProcessorExtensions.cs index 77624fe..89c4b74 100644 --- a/Src/DDD.Core/Application/ICommandProcessorExtensions.cs +++ b/Src/DDD.Core/Application/ICommandProcessorExtensions.cs @@ -1,10 +1,10 @@ using EnsureThat; using System; using System.Threading.Tasks; +using DDD.Validation; namespace DDD.Core.Application { - using Domain; using Threading; public static class ICommandProcessorExtensions @@ -12,10 +12,12 @@ public static class ICommandProcessorExtensions #region Methods - public static IContextualCommandProcessor In(this ICommandProcessor processor) where TContext : BoundedContext, new() + public static void Process(this ICommandProcessor processor, + TCommand command) + where TCommand : class, ICommand { Ensure.That(processor, nameof(processor)).IsNotNull(); - return processor.In(new TContext()); + processor.Process(command, new MessageContext()); } public static void Process(this ICommandProcessor processor, @@ -27,6 +29,14 @@ public static void Process(this ICommandProcessor processor, 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) @@ -36,15 +46,25 @@ public static Task ProcessAsync(this ICommandProcessor processor, 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 = null) + 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() ?? default; + var cancellationToken = context.CancellationToken(); await Task.Delay(delay, cancellationToken); await processor.ProcessAsync(command, context); } @@ -59,6 +79,41 @@ public static Task ProcessWithDelayAsync(this ICommandProcessor proces 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 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)); + } + + #endregion Methods } diff --git a/Src/DDD.Core/Application/IContextualCommandProcessor.cs b/Src/DDD.Core/Application/IContextualCommandProcessor.cs index 9dfd825..9696701 100644 --- a/Src/DDD.Core/Application/IContextualCommandProcessor.cs +++ b/Src/DDD.Core/Application/IContextualCommandProcessor.cs @@ -5,7 +5,7 @@ namespace DDD.Core.Application using Domain; /// - /// Defines a component that processes generic commands in a specific bounded context. + /// 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. @@ -26,12 +26,12 @@ public interface IContextualCommandProcessor /// /// Processes synchronously a command of a specified type in the specific bounded context. /// - void Process(TCommand command, IMessageContext context = null) where TCommand : class, ICommand; + void Process(TCommand command, IMessageContext context) where TCommand : class, ICommand; /// /// Processes asynchronously a command of a specified type in the specific bounded context. /// - Task ProcessAsync(TCommand command, IMessageContext context = null) where TCommand : class, ICommand; + Task ProcessAsync(TCommand command, IMessageContext context) where TCommand : class, ICommand; #endregion Methods } diff --git a/Src/DDD.Core/Application/IContextualCommandProcessorExtensions.cs b/Src/DDD.Core/Application/IContextualCommandProcessorExtensions.cs new file mode 100644 index 0000000..b674718 --- /dev/null +++ b/Src/DDD.Core/Application/IContextualCommandProcessorExtensions.cs @@ -0,0 +1,84 @@ +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 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 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)); + } + + #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 index 4c647b5..120d001 100644 --- a/Src/DDD.Core/Application/IContextualCommandProcessor`1.cs +++ b/Src/DDD.Core/Application/IContextualCommandProcessor`1.cs @@ -3,7 +3,7 @@ using Domain; /// - /// Defines a component that processes generic commands in a specific bounded context. + /// 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. diff --git a/Src/DDD.Core/Application/IContextualQueryProcessor.cs b/Src/DDD.Core/Application/IContextualQueryProcessor.cs index bc3bb66..2d9de55 100644 --- a/Src/DDD.Core/Application/IContextualQueryProcessor.cs +++ b/Src/DDD.Core/Application/IContextualQueryProcessor.cs @@ -27,12 +27,12 @@ public interface IContextualQueryProcessor /// /// Processes synchronously a query of a specified type and provides a result of a specified type. /// - TResult Process(IQuery query, IMessageContext context = null); + TResult 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 = null); + 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..9524745 --- /dev/null +++ b/Src/DDD.Core/Application/IContextualQueryProcessorExtensions.cs @@ -0,0 +1,44 @@ +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 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/IEventPublisher.cs b/Src/DDD.Core/Application/IEventPublisher.cs index 27a983f..ae2fae2 100644 --- a/Src/DDD.Core/Application/IEventPublisher.cs +++ b/Src/DDD.Core/Application/IEventPublisher.cs @@ -24,7 +24,7 @@ public interface IEventPublisher /// /// Publishes an event in a specific bounded context. /// - Task PublishAsync(TEvent @event, IMessageContext context = null) where TEvent : class, IEvent; + Task PublishAsync(TEvent @event, IMessageContext context) where TEvent : class, IEvent; #endregion Methods 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/IQueryProcessor.cs b/Src/DDD.Core/Application/IQueryProcessor.cs index ed594bb..85eb432 100644 --- a/Src/DDD.Core/Application/IQueryProcessor.cs +++ b/Src/DDD.Core/Application/IQueryProcessor.cs @@ -1,10 +1,9 @@ -using System.Threading; -using System.Threading.Tasks; +using System.Threading.Tasks; namespace DDD.Core.Application { - using Domain; using Validation; + using Domain; /// /// Defines a component that validates and processes queries of any type. @@ -17,32 +16,32 @@ public interface IQueryProcessor /// /// Specify the bounded context in which the query must be processed. /// - IContextualQueryProcessor In(TContext context) where TContext : BoundedContext; + IContextualQueryProcessor InGeneric(TContext context) where TContext : BoundedContext; /// /// Specify the bounded context in which the query must be processed. /// - IContextualQueryProcessor In(BoundedContext context); + IContextualQueryProcessor InSpecific(BoundedContext context); /// /// Processes synchronously a query of a specified type and provides a result of a specified type. /// - TResult Process(IQuery query, IMessageContext context = null); + TResult 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 = null); + Task ProcessAsync(IQuery query, IMessageContext context); /// /// Validates synchronously a query of a specified type. /// - ValidationResult Validate(TQuery query, string ruleSet = null) where TQuery : class, IQuery; + ValidationResult Validate(TQuery query, IValidationContext context) where TQuery : class, IQuery; /// /// Validates asynchronously a query of a specified type. /// - Task ValidateAsync(TQuery query, string ruleSet = null, CancellationToken cancellationToken = default) where TQuery : class, IQuery; + Task ValidateAsync(TQuery query, IValidationContext context) where TQuery : class, IQuery; #endregion Methods } diff --git a/Src/DDD.Core/Application/IQueryProcessorExtensions.cs b/Src/DDD.Core/Application/IQueryProcessorExtensions.cs index 94b1f79..4b5c070 100644 --- a/Src/DDD.Core/Application/IQueryProcessorExtensions.cs +++ b/Src/DDD.Core/Application/IQueryProcessorExtensions.cs @@ -1,19 +1,20 @@ using EnsureThat; using System.Threading.Tasks; +using DDD.Validation; namespace DDD.Core.Application { - using Domain; public static class IQueryProcessorExtensions { #region Methods - public static IContextualQueryProcessor In(this IQueryProcessor processor) where TContext : BoundedContext, new() + public static TResult Process(this IQueryProcessor processor, + IQuery query) { Ensure.That(processor, nameof(processor)).IsNotNull(); - return processor.In(new TContext()); + return processor.Process(query, new MessageContext()); } public static TResult Process(this IQueryProcessor processor, @@ -24,6 +25,13 @@ public static TResult Process(this IQueryProcessor processor, 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) @@ -32,6 +40,40 @@ public static Task ProcessAsync(this IQueryProcessor processor 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 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)); + } + #endregion Methods } diff --git a/Src/DDD.Core/Application/ISyncCommandHandler.cs b/Src/DDD.Core/Application/ISyncCommandHandler.cs index 5157f8b..512b0bc 100644 --- a/Src/DDD.Core/Application/ISyncCommandHandler.cs +++ b/Src/DDD.Core/Application/ISyncCommandHandler.cs @@ -12,7 +12,7 @@ public interface ISyncCommandHandler /// /// Handles synchronously a command of a specified type. /// - void Handle(TCommand command, IMessageContext context = null); + void Handle(TCommand command, IMessageContext context); #endregion Methods diff --git a/Src/DDD.Core/Application/ISyncCommandHandlerExtensions.cs b/Src/DDD.Core/Application/ISyncCommandHandlerExtensions.cs index 55b695a..adaa00e 100644 --- a/Src/DDD.Core/Application/ISyncCommandHandlerExtensions.cs +++ b/Src/DDD.Core/Application/ISyncCommandHandlerExtensions.cs @@ -7,6 +7,15 @@ 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) diff --git a/Src/DDD.Core/Application/ISyncEventHandler.cs b/Src/DDD.Core/Application/ISyncEventHandler.cs index 36c0b45..50386b2 100644 --- a/Src/DDD.Core/Application/ISyncEventHandler.cs +++ b/Src/DDD.Core/Application/ISyncEventHandler.cs @@ -29,7 +29,7 @@ public interface ISyncEventHandler /// /// Handles synchronously an event of a specified type in a specific bounded context. /// - void Handle(IEvent @event, IMessageContext context = null); + void Handle(IEvent @event, IMessageContext context); #endregion Methods diff --git a/Src/DDD.Core/Application/ISyncEventHandler`1.cs b/Src/DDD.Core/Application/ISyncEventHandler`1.cs index 6b78ce1..3829fd2 100644 --- a/Src/DDD.Core/Application/ISyncEventHandler`1.cs +++ b/Src/DDD.Core/Application/ISyncEventHandler`1.cs @@ -23,7 +23,7 @@ public interface ISyncEventHandler : ISyncEventHandler /// /// Handles synchronously an event of a specified type in a specific bounded context. /// - void Handle(TEvent @event, IMessageContext context = null); + void Handle(TEvent @event, IMessageContext context); #endregion Methods diff --git a/Src/DDD.Core/Application/ISyncQueryHandler.cs b/Src/DDD.Core/Application/ISyncQueryHandler.cs index 3210afa..21f29c8 100644 --- a/Src/DDD.Core/Application/ISyncQueryHandler.cs +++ b/Src/DDD.Core/Application/ISyncQueryHandler.cs @@ -13,7 +13,7 @@ public interface ISyncQueryHandler /// /// Handles synchronously a query of a specified type and provides a result of a specified type. /// - TResult Handle(TQuery query, IMessageContext context = null); + TResult Handle(TQuery query, IMessageContext context); #endregion Methods } diff --git a/Src/DDD.Core/Application/ISyncQueryHandlerExtensions.cs b/Src/DDD.Core/Application/ISyncQueryHandlerExtensions.cs index 1527b40..9f702f7 100644 --- a/Src/DDD.Core/Application/ISyncQueryHandlerExtensions.cs +++ b/Src/DDD.Core/Application/ISyncQueryHandlerExtensions.cs @@ -7,6 +7,15 @@ 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) diff --git a/Src/DDD.Core/Application/MessageContext.cs b/Src/DDD.Core/Application/MessageContext.cs index cac3209..49f7a27 100644 --- a/Src/DDD.Core/Application/MessageContext.cs +++ b/Src/DDD.Core/Application/MessageContext.cs @@ -8,8 +8,7 @@ namespace DDD.Core.Application /// /// Provides access to the execution context of a message. /// - public class MessageContext - : Dictionary, IMessageContext + public class MessageContext : Dictionary, IMessageContext { #region Constructors @@ -35,14 +34,9 @@ public static IMessageContext CancellableContext(CancellationToken cancellationT public static IMessageContext FromObject(object context) { - MessageContext messageContext; - if (context == null) - messageContext = null; - else - { - messageContext = new MessageContext(); + var messageContext = new MessageContext(); + if (context != null) messageContext.AddObject(context); - } return messageContext; } diff --git a/Src/DDD.Core/Application/QueryProcessor.cs b/Src/DDD.Core/Application/QueryProcessor.cs index 6bcf926..1fdfccd 100644 --- a/Src/DDD.Core/Application/QueryProcessor.cs +++ b/Src/DDD.Core/Application/QueryProcessor.cs @@ -6,6 +6,7 @@ namespace DDD.Core.Application { using Domain; + using System.Collections.Concurrent; using Validation; /// @@ -16,6 +17,9 @@ public class QueryProcessor : IQueryProcessor #region Fields + private static readonly ConcurrentDictionary contextualProcessors + = new ConcurrentDictionary(); + private readonly IServiceProvider serviceProvider; #endregion Fields @@ -32,20 +36,24 @@ public QueryProcessor(IServiceProvider serviceProvider) #region Methods - public IContextualQueryProcessor In(TContext context) where TContext : BoundedContext + public IContextualQueryProcessor InGeneric(TContext context) where TContext : BoundedContext { Ensure.That(context, nameof(context)).IsNotNull(); - return new ContextualQueryProcessor(this.serviceProvider, context); + return (IContextualQueryProcessor)contextualProcessors.GetOrAdd(context, _ => + new ContextualQueryProcessor(this.serviceProvider, context)); } - public IContextualQueryProcessor In(BoundedContext context) + public IContextualQueryProcessor InSpecific(BoundedContext context) { Ensure.That(context, nameof(context)).IsNotNull(); - var processorType = typeof(ContextualQueryProcessor<>).MakeGenericType(context.GetType()); - return (IContextualQueryProcessor)Activator.CreateInstance(processorType, this.serviceProvider, context); + 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 = null) + public TResult Process(IQuery query, IMessageContext context) { Ensure.That(query, nameof(query)).IsNotNull(); var handlerType = typeof(ISyncQueryHandler<,>).MakeGenericType(query.GetType(), typeof(TResult)); @@ -54,7 +62,7 @@ public TResult Process(IQuery query, IMessageContext context = return handler.Handle((dynamic)query, context); } - public Task ProcessAsync(IQuery query, IMessageContext context = null) + public Task ProcessAsync(IQuery query, IMessageContext context) { Ensure.That(query, nameof(query)).IsNotNull(); var handlerType = typeof(IAsyncQueryHandler<,>).MakeGenericType(query.GetType(), typeof(TResult)); @@ -63,22 +71,23 @@ public Task ProcessAsync(IQuery query, IMessageContex return handler.HandleAsync((dynamic)query, context); } - public ValidationResult Validate(TQuery query, string ruleSet = null) where TQuery : class, IQuery + 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) throw new InvalidOperationException($"The query validator for type {typeof(ISyncObjectValidator)} could not be found."); - return validator.Validate(query, ruleSet); + return validator.Validate(query, context); } - public Task ValidateAsync(TQuery query, string ruleSet = null, CancellationToken cancellationToken = default) where TQuery : class, IQuery + public Task ValidateAsync(TQuery query, IValidationContext context) where TQuery : class, IQuery { Ensure.That(query, nameof(query)).IsNotNull(); var validator = this.serviceProvider.GetService>(); if (validator == null) throw new InvalidOperationException($"The query validator for type {typeof(IAsyncObjectValidator)} could not be found."); - return validator.ValidateAsync(query, ruleSet, cancellationToken); + return validator.ValidateAsync(query, context); } #endregion Methods + } } \ No newline at end of file diff --git a/Src/DDD.Core/Application/RecurringCommandManager.cs b/Src/DDD.Core/Application/RecurringCommandManager.cs index 496477d..d1641ad 100644 --- a/Src/DDD.Core/Application/RecurringCommandManager.cs +++ b/Src/DDD.Core/Application/RecurringCommandManager.cs @@ -13,7 +13,7 @@ namespace DDD.Core.Application using Threading; public class RecurringCommandManager : IRecurringCommandManager, IDisposable - where TContext : BoundedContext, new() + where TContext : BoundedContext { #region Fields @@ -74,7 +74,7 @@ public async Task RegisterAsync(ICommand command, string recurringExpression, Ca this.ValidateRecurringExpression(recurringExpression); await new SynchronizationContextRemover(); var commandSerializer = this.commandSerializers.GetService(this.settings.CurrentSerializationFormat); - var commandId = this.queryProcessor.In().Process(new GenerateRecurringCommandId(), MessageContext.CancellableContext(cancellationToken)); + var commandId = this.queryProcessor.InGeneric(Context).Process(new GenerateRecurringCommandId(), MessageContext.CancellableContext(cancellationToken)); var registrationCommand = new RegisterRecurringCommand { CommandId = commandId, @@ -83,7 +83,7 @@ public async Task RegisterAsync(ICommand command, string recurringExpression, Ca BodyFormat = commandSerializer.Format.ToString().ToUpper(), RecurringExpression = recurringExpression }; - await this.commandProcessor.In().ProcessAsync(registrationCommand, MessageContext.CancellableContext(cancellationToken)); + await this.commandProcessor.InGeneric(Context).ProcessAsync(registrationCommand, MessageContext.CancellableContext(cancellationToken)); } public void Start() @@ -168,7 +168,7 @@ private async Task ManageCommandsAsync() private async Task> FindAllRecurringCommandsAsync() { - var recurringCommands = await this.queryProcessor.In().ProcessAsync(new FindRecurringCommands(), MessageContext.CancellableContext(this.CancellationToken)); + 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) results.Add((recurringCommand, this.recurringScheduleFactory.Create(recurringCommand.RecurringExpression))); @@ -203,7 +203,7 @@ private async Task ManageCommandAsync(RecurringCommand recurringCommand, IRecurr ExecutionTime = nextOccurrence.Value, ExceptionInfo = exception.ToString() }; - await this.commandProcessor.In().ProcessAsync(failureCommand); + await this.commandProcessor.InGeneric(Context).ProcessAsync(failureCommand); } if (success) { @@ -213,7 +213,7 @@ private async Task ManageCommandAsync(RecurringCommand recurringCommand, IRecurr CommandId = recurringCommand.CommandId, ExecutionTime = nextOccurrence.Value }; - await this.commandProcessor.In().ProcessAsync(successCommand); + await this.commandProcessor.InGeneric(Context).ProcessAsync(successCommand); } now = SystemTime.Local(); nextOccurrence = recurringSchedule.GetNextOccurrence(now); diff --git a/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs b/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs index cb0473f..97ab003 100644 --- a/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs +++ b/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs @@ -1,4 +1,5 @@ using System.Runtime.Serialization; +using EnsureThat; namespace DDD.Core.Application { @@ -7,20 +8,17 @@ namespace DDD.Core.Application [DataContract()] public class RecurringCommandManagerSettings - where TContext : BoundedContext, new() + where TContext : BoundedContext { - #region Fields - - private readonly TContext context; - - #endregion Fields #region Constructors - public RecurringCommandManagerSettings(SerializationFormat currentSerializationFormat) + public RecurringCommandManagerSettings(TContext context, + SerializationFormat currentSerializationFormat) { + Ensure.That(context, nameof(context)).IsNotNull(); + this.Context= context; this.CurrentSerializationFormat = currentSerializationFormat; - this.context = new TContext(); } #endregion Constructors @@ -30,7 +28,7 @@ public RecurringCommandManagerSettings(SerializationFormat currentSerializationF /// /// Gets the associated context. /// - public TContext Context => this.context; + public TContext Context { get; } /// /// Gets the current serialization format of the recurring commands. diff --git a/Src/DDD.Core/Application/SyncCommandHandlerWithLogging.cs b/Src/DDD.Core/Application/SyncCommandHandlerWithLogging.cs index 9923d72..fe76b47 100644 --- a/Src/DDD.Core/Application/SyncCommandHandlerWithLogging.cs +++ b/Src/DDD.Core/Application/SyncCommandHandlerWithLogging.cs @@ -33,7 +33,7 @@ public SyncCommandHandlerWithLogging(ISyncCommandHandler commandHandle #region Methods - public void Handle(TCommand command, IMessageContext context = null) + public void Handle(TCommand command, IMessageContext context) { if (this.logger.IsEnabled(LogLevel.Debug)) { diff --git a/Src/DDD.Core/Application/SyncCommandHandlerWithLogging`1.cs b/Src/DDD.Core/Application/SyncCommandHandlerWithLogging`1.cs index 7f2298b..0f67796 100644 --- a/Src/DDD.Core/Application/SyncCommandHandlerWithLogging`1.cs +++ b/Src/DDD.Core/Application/SyncCommandHandlerWithLogging`1.cs @@ -41,7 +41,7 @@ public SyncCommandHandlerWithLogging(ISyncCommandHandler com #region Methods - public void Handle(TCommand command, IMessageContext context = null) + public void Handle(TCommand command, IMessageContext context) { if (this.logger.IsEnabled(LogLevel.Debug)) { diff --git a/Src/DDD.Core/Application/SyncEventHandler.cs b/Src/DDD.Core/Application/SyncEventHandler.cs index 1586077..d3ce296 100644 --- a/Src/DDD.Core/Application/SyncEventHandler.cs +++ b/Src/DDD.Core/Application/SyncEventHandler.cs @@ -1,4 +1,5 @@ using System; +using EnsureThat; namespace DDD.Core.Application { @@ -9,12 +10,22 @@ namespace DDD.Core.Application /// public abstract class SyncEventHandler : ISyncEventHandler where TEvent : class, IEvent - where TContext : BoundedContext, new() + 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; } = new TContext(); + public TContext Context { get; } BoundedContext ISyncEventHandler.Context => this.Context; @@ -24,7 +35,7 @@ public abstract class SyncEventHandler : ISyncEventHandler this.Handle((TEvent)@event, context); diff --git a/Src/DDD.Core/Application/SyncEventHandlerWithLogging.cs b/Src/DDD.Core/Application/SyncEventHandlerWithLogging.cs index 36a8105..2cb88b0 100644 --- a/Src/DDD.Core/Application/SyncEventHandlerWithLogging.cs +++ b/Src/DDD.Core/Application/SyncEventHandlerWithLogging.cs @@ -46,7 +46,7 @@ public SyncEventHandlerWithLogging(ISyncEventHandler eventHand #region Methods - public void Handle(TEvent @event, IMessageContext context = null) + public void Handle(TEvent @event, IMessageContext context) { if (this.logger.IsEnabled(LogLevel.Debug)) { diff --git a/Src/DDD.Core/Application/SyncQueryHandlerWithLogging.cs b/Src/DDD.Core/Application/SyncQueryHandlerWithLogging.cs index abfe0c5..0f0fa85 100644 --- a/Src/DDD.Core/Application/SyncQueryHandlerWithLogging.cs +++ b/Src/DDD.Core/Application/SyncQueryHandlerWithLogging.cs @@ -27,7 +27,7 @@ public SyncQueryHandlerWithLogging(ISyncQueryHandler queryHandl #region Methods - public TResult Handle(TQuery query, IMessageContext context = null) + public TResult Handle(TQuery query, IMessageContext context) { if (this.logger.IsEnabled(LogLevel.Debug)) { diff --git a/Src/DDD.Core/Application/SyncQueryHandlerWithLogging`1.cs b/Src/DDD.Core/Application/SyncQueryHandlerWithLogging`1.cs index 84b09c6..51496c1 100644 --- a/Src/DDD.Core/Application/SyncQueryHandlerWithLogging`1.cs +++ b/Src/DDD.Core/Application/SyncQueryHandlerWithLogging`1.cs @@ -41,7 +41,7 @@ public SyncQueryHandlerWithLogging(ISyncQueryHandler #region Methods - public TResult Handle(TQuery query, IMessageContext context = null) + public TResult Handle(TQuery query, IMessageContext context) { if (this.logger.IsEnabled(LogLevel.Debug)) { diff --git a/Src/DDD.Core/Domain/CompositeTranslatorExtensions.cs b/Src/DDD.Core/Domain/CompositeTranslatorExtensions.cs index f996e88..375c99d 100644 --- a/Src/DDD.Core/Domain/CompositeTranslatorExtensions.cs +++ b/Src/DDD.Core/Domain/CompositeTranslatorExtensions.cs @@ -16,8 +16,7 @@ public static void RegisterFallback(this CompositeTranslator((exception, context) => { - Type entityType = null; - context?.TryGetValue("EntityType", out entityType); + context.TryGetValue("EntityType", out Type entityType); return new RepositoryException(isTransient: false, entityType, exception); }); } @@ -27,8 +26,7 @@ public static void RegisterFallback(this CompositeTranslator((exception, context) => { - Type serviceType = null; - context?.TryGetValue("ServiceType", out serviceType); + context.TryGetValue("ServiceType", out Type serviceType); return new DomainServiceException(isTransient: false, serviceType, exception); }); } diff --git a/Src/DDD.Core/Infrastructure/Data/DbToCommandExceptionTranslator.cs b/Src/DDD.Core/Infrastructure/Data/DbToCommandExceptionTranslator.cs index 9531f41..344cee3 100644 --- a/Src/DDD.Core/Infrastructure/Data/DbToCommandExceptionTranslator.cs +++ b/Src/DDD.Core/Infrastructure/Data/DbToCommandExceptionTranslator.cs @@ -30,16 +30,16 @@ public DbToCommandExceptionTranslator() #region Methods - public override CommandException Translate(DbException exception, IDictionary context = null) + 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 { - ICommand command = null; - context?.TryGetValue("Command", out command); + context.TryGetValue("Command", out ICommand command); return new CommandException(isTransient: false, command, exception); } } diff --git a/Src/DDD.Core/Infrastructure/Data/DbToQueryExceptionTranslator.cs b/Src/DDD.Core/Infrastructure/Data/DbToQueryExceptionTranslator.cs index 378041f..fa3509c 100644 --- a/Src/DDD.Core/Infrastructure/Data/DbToQueryExceptionTranslator.cs +++ b/Src/DDD.Core/Infrastructure/Data/DbToQueryExceptionTranslator.cs @@ -30,16 +30,16 @@ public DbToQueryExceptionTranslator() #region Methods - public override QueryException Translate(DbException exception, IDictionary context = null) + 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 { - IQuery query = null; - context?.TryGetValue("Query", out query); + context.TryGetValue("Query", out IQuery query); return new QueryException(isTransient: false, query, exception); } } diff --git a/Src/DDD.Core/Infrastructure/Data/DbToRepositoryExceptionTranslator.cs b/Src/DDD.Core/Infrastructure/Data/DbToRepositoryExceptionTranslator.cs index ecf4baf..4d2883e 100644 --- a/Src/DDD.Core/Infrastructure/Data/DbToRepositoryExceptionTranslator.cs +++ b/Src/DDD.Core/Infrastructure/Data/DbToRepositoryExceptionTranslator.cs @@ -31,16 +31,16 @@ public DbToRepositoryExceptionTranslator() #region Methods - public override RepositoryException Translate(DbException exception, IDictionary context = null) + 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 { - Type entityType = null; - context?.TryGetValue("EntityType", out entityType); + context.TryGetValue("EntityType", out Type entityType); var outerException = context.ContainsKey("OuterException") ? (Exception)context["OuterException"] : exception; return new RepositoryException(isTransient: false, entityType, outerException); } diff --git a/Src/DDD.Core/Infrastructure/Data/LazyDbConnectionProvider.cs b/Src/DDD.Core/Infrastructure/Data/LazyDbConnectionProvider.cs index 4335e39..baf1454 100644 --- a/Src/DDD.Core/Infrastructure/Data/LazyDbConnectionProvider.cs +++ b/Src/DDD.Core/Infrastructure/Data/LazyDbConnectionProvider.cs @@ -8,7 +8,7 @@ namespace DDD.Core.Infrastructure.Data using Domain; public class LazyDbConnectionProvider - : IDbConnectionProvider where TContext : BoundedContext, new() + : IDbConnectionProvider where TContext : BoundedContext { #region Fields @@ -22,13 +22,14 @@ public class LazyDbConnectionProvider #region Constructors - public LazyDbConnectionProvider(string providerName, string connectionString) + public LazyDbConnectionProvider(TContext context, string providerName, string connectionString) { + Ensure.That(context, nameof(context)).IsNotNull(); Ensure.That(providerName, nameof(providerName)).IsNotNullOrWhiteSpace(); Ensure.That(connectionString, nameof(connectionString)).IsNotNullOrWhiteSpace(); + this.Context = context; this.providerName = providerName; this.connectionString = connectionString; - this.Context = new TContext(); this.lazyConnection = new Lazy(() => this.CreateConnection()); } diff --git a/Src/DDD.Core/Infrastructure/Data/OracleToCommandExceptionTranslator.cs b/Src/DDD.Core/Infrastructure/Data/OracleToCommandExceptionTranslator.cs index 931a6b6..79d4ce5 100644 --- a/Src/DDD.Core/Infrastructure/Data/OracleToCommandExceptionTranslator.cs +++ b/Src/DDD.Core/Infrastructure/Data/OracleToCommandExceptionTranslator.cs @@ -1,5 +1,4 @@ using System.Data.Common; -using System.Collections.Generic; using EnsureThat; namespace DDD.Core.Infrastructure.Data @@ -15,11 +14,11 @@ internal class OracleToCommandExceptionTranslator : ObjectTranslator context = null) + public override CommandException Translate(DbException exception, IMappingContext context) { Ensure.That(exception, nameof(exception)).IsNotNull(); - ICommand command = null; - context?.TryGetValue("Command", out command); + Ensure.That(context, nameof(context)).IsNotNull(); + context.TryGetValue("Command", out ICommand command); dynamic oracleException = exception; foreach (dynamic error in oracleException.Errors) { diff --git a/Src/DDD.Core/Infrastructure/Data/OracleToQueryExceptionTranslator.cs b/Src/DDD.Core/Infrastructure/Data/OracleToQueryExceptionTranslator.cs index 5b48eac..e2e17a9 100644 --- a/Src/DDD.Core/Infrastructure/Data/OracleToQueryExceptionTranslator.cs +++ b/Src/DDD.Core/Infrastructure/Data/OracleToQueryExceptionTranslator.cs @@ -1,5 +1,4 @@ using System.Data.Common; -using System.Collections.Generic; using EnsureThat; namespace DDD.Core.Infrastructure.Data @@ -15,11 +14,11 @@ internal class OracleToQueryExceptionTranslator : ObjectTranslator context = null) + public override QueryException Translate(DbException exception, IMappingContext context) { Ensure.That(exception, nameof(exception)).IsNotNull(); - IQuery query = null; - context?.TryGetValue("Query", out query); + Ensure.That(context, nameof(context)).IsNotNull(); + context.TryGetValue("Query", out IQuery query); dynamic oracleException = exception; foreach (dynamic error in oracleException.Errors) { diff --git a/Src/DDD.Core/Infrastructure/Data/OracleToRepositoryExceptionTranslator.cs b/Src/DDD.Core/Infrastructure/Data/OracleToRepositoryExceptionTranslator.cs index c6910e3..2f6ecf2 100644 --- a/Src/DDD.Core/Infrastructure/Data/OracleToRepositoryExceptionTranslator.cs +++ b/Src/DDD.Core/Infrastructure/Data/OracleToRepositoryExceptionTranslator.cs @@ -1,5 +1,4 @@ using System.Data.Common; -using System.Collections.Generic; using System; using EnsureThat; @@ -16,11 +15,11 @@ internal class OracleToRepositoryExceptionTranslator : ObjectTranslator context = null) + public override RepositoryException Translate(DbException exception, IMappingContext context) { Ensure.That(exception, nameof(exception)).IsNotNull(); - Type entityType = null; - context?.TryGetValue("EntityType", out entityType); + 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) diff --git a/Src/DDD.Core/Infrastructure/Data/SqlServerToCommandExceptionTranslator.cs b/Src/DDD.Core/Infrastructure/Data/SqlServerToCommandExceptionTranslator.cs index ae30da4..2afbdea 100644 --- a/Src/DDD.Core/Infrastructure/Data/SqlServerToCommandExceptionTranslator.cs +++ b/Src/DDD.Core/Infrastructure/Data/SqlServerToCommandExceptionTranslator.cs @@ -1,5 +1,4 @@ using System.Data.Common; -using System.Collections.Generic; using EnsureThat; namespace DDD.Core.Infrastructure.Data @@ -12,11 +11,11 @@ internal class SqlServerToCommandExceptionTranslator : ObjectTranslator context = null) + public override CommandException Translate(DbException exception, IMappingContext context) { Ensure.That(exception, nameof(exception)).IsNotNull(); - ICommand command = null; - context?.TryGetValue("Command", out command); + Ensure.That(context, nameof(context)).IsNotNull(); + context.TryGetValue("Command", out ICommand command); dynamic sqlServerException = exception; foreach (dynamic error in sqlServerException.Errors) { diff --git a/Src/DDD.Core/Infrastructure/Data/SqlServerToQueryExceptionTranslator.cs b/Src/DDD.Core/Infrastructure/Data/SqlServerToQueryExceptionTranslator.cs index cba297a..0255e2b 100644 --- a/Src/DDD.Core/Infrastructure/Data/SqlServerToQueryExceptionTranslator.cs +++ b/Src/DDD.Core/Infrastructure/Data/SqlServerToQueryExceptionTranslator.cs @@ -1,5 +1,4 @@ using System.Data.Common; -using System.Collections.Generic; using EnsureThat; namespace DDD.Core.Infrastructure.Data @@ -12,11 +11,11 @@ internal class SqlServerToQueryExceptionTranslator : ObjectTranslator context = null) + public override QueryException Translate(DbException exception, IMappingContext context) { Ensure.That(exception, nameof(exception)).IsNotNull(); - IQuery query = null; - context?.TryGetValue("Query", out query); + Ensure.That(context, nameof(context)).IsNotNull(); + context.TryGetValue("Query", out IQuery query); dynamic sqlServerException = exception; foreach (dynamic error in sqlServerException.Errors) { diff --git a/Src/DDD.Core/Infrastructure/Data/SqlServerToRepositoryExceptionTranslator.cs b/Src/DDD.Core/Infrastructure/Data/SqlServerToRepositoryExceptionTranslator.cs index cfb73dc..d1322c3 100644 --- a/Src/DDD.Core/Infrastructure/Data/SqlServerToRepositoryExceptionTranslator.cs +++ b/Src/DDD.Core/Infrastructure/Data/SqlServerToRepositoryExceptionTranslator.cs @@ -1,5 +1,4 @@ using System.Data.Common; -using System.Collections.Generic; using EnsureThat; using System; @@ -13,11 +12,11 @@ internal class SqlServerToRepositoryExceptionTranslator : ObjectTranslator context = null) + public override RepositoryException Translate(DbException exception, IMappingContext context) { Ensure.That(exception, nameof(exception)).IsNotNull(); - Type entityType = null; - context?.TryGetValue("EntityType", out entityType); + 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) diff --git a/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs b/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs index d7aec34..747070f 100644 --- a/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs +++ b/Src/DDD.HealthcareDelivery/Application/Prescriptions/BelgianPharmaceuticalPrescriptionTranslator.cs @@ -1,6 +1,5 @@ using EnsureThat; using System; -using System.Collections.Generic; using System.Linq; namespace DDD.HealthcareDelivery.Application.Prescriptions @@ -20,9 +19,10 @@ public class BelgianPharmaceuticalPrescriptionTranslator #region Methods public override PharmaceuticalPrescription Translate(CreatePharmaceuticalPrescription command, - IDictionary context = null) + IMappingContext context) { Ensure.That(command, nameof(command)).IsNotNull(); + Ensure.That(context, nameof(context)); return PharmaceuticalPrescription.Create ( new PrescriptionIdentifier(command.PrescriptionIdentifier), diff --git a/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionCreator.cs b/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionCreator.cs index a6a53a3..0ddcfb7 100644 --- a/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionCreator.cs +++ b/Src/DDD.HealthcareDelivery/Application/Prescriptions/PharmaceuticalPrescriptionCreator.cs @@ -40,7 +40,7 @@ public PharmaceuticalPrescriptionCreator(IRepository { @@ -37,7 +38,7 @@ public PharmaceuticalPrescriptionRevoker(IRepository Handle(FindPharmaceuticalPrescriptionsByPatient query, IMessageContext context = null) + public IEnumerable Handle(FindPharmaceuticalPrescriptionsByPatient query, IMessageContext context) { Ensure.That(query, nameof(query)).IsNotNull(); try @@ -55,13 +55,14 @@ public IEnumerable Handle(FindPharmaceuticalP } } - public async Task> HandleAsync(FindPharmaceuticalPrescriptionsByPatient query, IMessageContext context = null) + public async Task> HandleAsync(FindPharmaceuticalPrescriptionsByPatient query, IMessageContext context) { Ensure.That(query, nameof(query)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); try { await new SynchronizationContextRemover(); - var cancellationToken = context?.CancellationToken() ?? default; + var cancellationToken = context.CancellationToken(); var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); var expressions = connection.Expressions(); return await connection.QueryAsync diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinder.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinder.cs index ef10d68..88195ea 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinder.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinder.cs @@ -39,7 +39,7 @@ public PrescribedMedicationsByPrescriptionFinder(IDbConnectionProvider Handle(FindPrescribedMedicationsByPrescription query, IMessageContext context = null) + public IEnumerable Handle(FindPrescribedMedicationsByPrescription query, IMessageContext context) { Ensure.That(query, nameof(query)).IsNotNull(); try @@ -55,13 +55,14 @@ public IEnumerable Handle(FindPrescribedMedications } } - public async Task> HandleAsync(FindPrescribedMedicationsByPrescription query, IMessageContext context = null) + public async Task> HandleAsync(FindPrescribedMedicationsByPrescription query, IMessageContext context) { Ensure.That(query, nameof(query)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); try { await new SynchronizationContextRemover(); - var cancellationToken = context?.CancellationToken() ?? default; + var cancellationToken = context.CancellationToken(); var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); var expressions = connection.Expressions(); return await connection.QueryAsync diff --git a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionIdentifierGenerator.cs b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionIdentifierGenerator.cs index 515b260..a1c9b02 100644 --- a/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionIdentifierGenerator.cs +++ b/Src/DDD.HealthcareDelivery/Infrastructure/Prescriptions/PrescriptionIdentifierGenerator.cs @@ -37,7 +37,7 @@ public PrescriptionIdentifierGenerator(IDbConnectionProvider HandleAsync(GeneratePrescriptionIdentifier query, IMessageContext context = null) + public async Task HandleAsync(GeneratePrescriptionIdentifier query, IMessageContext context) { Ensure.That(query, nameof(query)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNull(); try { await new SynchronizationContextRemover(); - var cancellationToken = context?.CancellationToken() ?? default; + var cancellationToken = context.CancellationToken(); var connection = await this.connectionProvider.GetOpenConnectionAsync(cancellationToken); return await connection.NextValueAsync("PrescriptionId", null, cancellationToken); } diff --git a/Test/DDD.Core.Abstractions.UnitTests/Mapping/IMappingProcessorExtensionsTests.cs b/Test/DDD.Core.Abstractions.UnitTests/Mapping/IMappingProcessorExtensionsTests.cs index 8e4935f..61a0410 100644 --- a/Test/DDD.Core.Abstractions.UnitTests/Mapping/IMappingProcessorExtensionsTests.cs +++ b/Test/DDD.Core.Abstractions.UnitTests/Mapping/IMappingProcessorExtensionsTests.cs @@ -57,22 +57,26 @@ public static IEnumerable Sources() [MemberData(nameof(Sources))] public void TranslateCollection_OfObject_CallsExpectedTranslator(IEnumerable source) { + // Arrange + var context = new MappingContext(); // Act - var destination = IMappingProcessorExtensions.TranslateCollection(this.processor, source) - .ToList(); + IMappingProcessorExtensions.TranslateCollection(this.processor, source, context) + .ToList(); // Assert - this.translator1To2.Received(source.Count()).Translate(Arg.Is(x => source.Contains(x))); + 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 - var destination = IMappingProcessorExtensions.TranslateCollection(this.processor, source) - .ToList(); + IMappingProcessorExtensions.TranslateCollection(this.processor, source, context) + .ToList(); // Assert - this.translator1To2.Received(source.Count()).Translate(Arg.Is(x => source.Contains(x))); + this.translator1To2.Received(source.Count()).Translate(Arg.Is(x => source.Contains(x)), context); } #endregion Methods diff --git a/Test/DDD.Core.Abstractions.UnitTests/Mapping/MappingProcessorTests.cs b/Test/DDD.Core.Abstractions.UnitTests/Mapping/MappingProcessorTests.cs index 28cf920..98e3b6c 100644 --- a/Test/DDD.Core.Abstractions.UnitTests/Mapping/MappingProcessorTests.cs +++ b/Test/DDD.Core.Abstractions.UnitTests/Mapping/MappingProcessorTests.cs @@ -46,10 +46,11 @@ 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] @@ -57,10 +58,11 @@ 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] @@ -68,10 +70,11 @@ public void Translate_Object_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); } #endregion Methods diff --git a/Test/DDD.Core.Abstractions.UnitTests/StringArgExtensionsTests.cs b/Test/DDD.Core.Abstractions.UnitTests/StringArgExtensionsTests.cs index cdaa7a2..87d9f96 100644 --- a/Test/DDD.Core.Abstractions.UnitTests/StringArgExtensionsTests.cs +++ b/Test/DDD.Core.Abstractions.UnitTests/StringArgExtensionsTests.cs @@ -154,4 +154,4 @@ public void Satisfy_WhenConditionSatisfied_DoesNotThrow() #endregion Methods } -} \ No newline at end of file +} diff --git a/Test/DDD.Core.Abstractions.UnitTests/StringParamExtensionsTests.cs b/Test/DDD.Core.Abstractions.UnitTests/StringParamExtensionsTests.cs index 5ac35eb..c8db901 100644 --- a/Test/DDD.Core.Abstractions.UnitTests/StringParamExtensionsTests.cs +++ b/Test/DDD.Core.Abstractions.UnitTests/StringParamExtensionsTests.cs @@ -155,4 +155,4 @@ public void Satisfy_WhenConditionSatisfied_DoesNotThrow() #endregion Methods } -} \ No newline at end of file +} diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/IPersistenceFixture.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/IPersistenceFixture.cs index ff28d8b..236a423 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/IPersistenceFixture.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/IPersistenceFixture.cs @@ -1,9 +1,8 @@ namespace DDD.Core.Infrastructure.Data { - using Core.Application; - using Core.Domain; - using Core.Infrastructure.Testing; + using Application; using Domain; + using Testing; using Mapping; public interface IPersistenceFixture : IDbFixture diff --git a/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs b/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs index 17a473c..ed2120c 100644 --- a/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs +++ b/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs @@ -1,5 +1,6 @@ using NSubstitute; using System; +using System.Threading.Tasks; using Xunit; using DDD.Validation; @@ -46,10 +47,11 @@ public void Process_WhenCommandDefined_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.handlerOfCommand1.Received(1).Handle(command, context); } [Fact] @@ -57,10 +59,11 @@ public void Validate_WhenCommandDefined_CallsExpectedValidator() { // Arrange var command = new FakeCommand1(); + var context = new ValidationContext(); // Act - this.processor.Validate(command); + this.processor.Validate(command, context); // Assert - this.validatorOfCommand1.Received(1).Validate(command); + this.validatorOfCommand1.Received(1).Validate(command, context); } #endregion Methods diff --git a/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs b/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs index 3dadf10..d682e09 100644 --- a/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs +++ b/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs @@ -79,6 +79,7 @@ public async Task StartAndWait_WhenExceptionInDeserializingEvent_ExcludesFailedE var commandProcessor = FakeCommandProcessor(); var queryProcessor = FakeQueryProcessorReadingEventStream(@event); var eventPublisher = FakeEventPublisher(); + var fakeContext = new FakeContext(); var boundedContexts = FakeBoundedContexts(); var eventSerializers = FakeEventSerializersThrowingException(exception); var logger = FakeLogger(); @@ -88,7 +89,7 @@ public async Task StartAndWait_WhenExceptionInDeserializingEvent_ExcludesFailedE consumer.Start(); consumer.Wait(TimeSpan.FromSeconds(5)); // Assert - await commandProcessor.In() + await commandProcessor.InGeneric(fakeContext) .Received(1) .ProcessAsync(Arg.Is(c => c.EventId == @event.EventId && c.ExceptionMessage == exception.Message), Arg.Any()); } @@ -144,6 +145,7 @@ public async Task StartAndWait_WhenExceptionInDeserializingFailedEvent_UpdatesFa var commandProcessor = FakeCommandProcessor(); var queryProcessor = FakeQueryProcessorReadingFailedEventStream(@event); var eventPublisher = FakeEventPublisher(); + var fakeContext = new FakeContext(); var boundedContexts = FakeBoundedContexts(); var eventSerializers = FakeEventSerializersThrowingException(exception); var logger = FakeLogger(); @@ -153,7 +155,7 @@ public async Task StartAndWait_WhenExceptionInDeserializingFailedEvent_UpdatesFa consumer.Start(); consumer.Wait(TimeSpan.FromSeconds(5)); // Assert - await commandProcessor.In() + await commandProcessor.InGeneric(fakeContext) .Received(1) .ProcessAsync(Arg.Is(c => c.EventId == @event.EventId && c.ExceptionMessage == exception.Message), Arg.Any()); } @@ -251,6 +253,7 @@ public async Task StartAndWait_WhenExceptionInPublishingEvent_ExcludesFailedEven var commandProcessor = FakeCommandProcessor(); var queryProcessor = FakeQueryProcessorReadingEventStream(@event); var eventPublisher = FakeEventPublisherThrowingException(exception); + var fakeContext = new FakeContext(); var boundedContexts = FakeBoundedContexts(); var eventSerializers = FakeEventSerializers(); var logger = FakeLogger(); @@ -260,7 +263,7 @@ public async Task StartAndWait_WhenExceptionInPublishingEvent_ExcludesFailedEven consumer.Start(); consumer.Wait(TimeSpan.FromSeconds(5)); // Assert - await commandProcessor.In() + await commandProcessor.InGeneric(fakeContext) .Received(1) .ProcessAsync(Arg.Is(c => c.EventId == @event.EventId && c.ExceptionMessage == exception.Message), Arg.Any()); } @@ -316,6 +319,7 @@ public async Task StartAndWait_WhenExceptionInPublishingFailedEvent_UpdatesFaile var commandProcessor = FakeCommandProcessor(); var queryProcessor = FakeQueryProcessorReadingFailedEventStream(@event); var eventPublisher = FakeEventPublisherThrowingException(exception); + var fakeContext = new FakeContext(); var boundedContexts = FakeBoundedContexts(); var eventSerializers = FakeEventSerializers(); var logger = FakeLogger(); @@ -325,7 +329,7 @@ public async Task StartAndWait_WhenExceptionInPublishingFailedEvent_UpdatesFaile consumer.Start(); consumer.Wait(TimeSpan.FromSeconds(5)); // Assert - await commandProcessor.In() + await commandProcessor.InGeneric(fakeContext) .Received(1) .ProcessAsync(Arg.Is(c => c.EventId == @event.EventId && c.ExceptionMessage == exception.Message), Arg.Any()); } @@ -401,6 +405,7 @@ public async Task StartAndWait_WhenFailedEventStreamSuccessfullyProcessed_Delete var commandProcessor = FakeCommandProcessor(); var queryProcessor = FakeQueryProcessorReadingFailedEventStream(@event); var eventPublisher = FakeEventPublisher(); + var fakeContext = new FakeContext(); var boundedContexts = FakeBoundedContexts(); var eventSerializers = FakeEventSerializers(); var logger = FakeLogger(); @@ -410,14 +415,14 @@ public async Task StartAndWait_WhenFailedEventStreamSuccessfullyProcessed_Delete consumer.Start(); consumer.Wait(TimeSpan.FromSeconds(5)); // Assert - await commandProcessor.In().Received(1) + await commandProcessor.InGeneric(fakeContext).Received(1) .ProcessAsync(Arg.Any(), Arg.Any()); } private static ICommandProcessor FakeCommandProcessor() { var contextualProcessor = Substitute.For>(); var commandProcessor = Substitute.For(); - commandProcessor.In().Returns(contextualProcessor); + commandProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); return commandProcessor; } @@ -427,7 +432,7 @@ private static ICommandProcessor FakeCommandProcessorThrowingExceptionWhenExclud contextualProcessor.When(p => p.ProcessAsync(Arg.Any(), Arg.Any())) .Throw(exception); var commandProcessor = Substitute.For(); - commandProcessor.In().Returns(contextualProcessor); + commandProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); return commandProcessor; } @@ -437,7 +442,7 @@ private static ICommandProcessor FakeCommandProcessorThrowingExceptionWhenInclud contextualProcessor.When(p => p.ProcessAsync(Arg.Any(), Arg.Any())) .Throw(exception); var commandProcessor = Substitute.For(); - commandProcessor.In().Returns(contextualProcessor); + commandProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); return commandProcessor; } @@ -447,7 +452,7 @@ private static ICommandProcessor FakeCommandProcessorThrowingExceptionWhenUpdati contextualProcessor.When(p => p.ProcessAsync(Arg.Any(), Arg.Any())) .Throw(exception); var commandProcessor = Substitute.For(); - commandProcessor.In().Returns(contextualProcessor); + commandProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); return commandProcessor; } @@ -509,8 +514,8 @@ private static IQueryProcessor FakeQueryProcessorReadingEventStream(params Event contextualProcessorForFakeSourceContext.ProcessAsync(Arg.Any(), Arg.Any()) .Returns(events); var queryProcessor = Substitute.For(); - queryProcessor.In().Returns(contextualProcessorForFakeContext); - queryProcessor.In(Arg.Any()).Returns(contextualProcessorForFakeSourceContext); + queryProcessor.InGeneric(Arg.Any()).Returns(contextualProcessorForFakeContext); + queryProcessor.InSpecific(Arg.Any()).Returns(contextualProcessorForFakeSourceContext); return queryProcessor; } @@ -525,8 +530,8 @@ private static IQueryProcessor FakeQueryProcessorReadingFailedEventStream(params contextualProcessorForFakeSourceContext.ProcessAsync(Arg.Any(), Arg.Any()) .Returns(events); var queryProcessor = Substitute.For(); - queryProcessor.In().Returns(contextualProcessorForFakeContext); - queryProcessor.In(Arg.Any()).Returns(contextualProcessorForFakeSourceContext); + queryProcessor.InGeneric(Arg.Any()).Returns(contextualProcessorForFakeContext); + queryProcessor.InSpecific(Arg.Any()).Returns(contextualProcessorForFakeSourceContext); return queryProcessor; } @@ -536,7 +541,7 @@ private static IQueryProcessor FakeQueryProcessorThrowingExceptionWhenFindingEve contextualProcessor.When(p => p.ProcessAsync(Arg.Any(), Arg.Any())) .Throw(exception); var queryProcessor = Substitute.For(); - queryProcessor.In().Returns(contextualProcessor); + queryProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); return queryProcessor; } @@ -546,7 +551,7 @@ private static IQueryProcessor FakeQueryProcessorThrowingExceptionWhenFindingFai contextualProcessor.When(p => p.ProcessAsync(Arg.Any(), Arg.Any())) .Throw(exception); var queryProcessor = Substitute.For(); - queryProcessor.In().Returns(contextualProcessor); + queryProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); return queryProcessor; } @@ -559,8 +564,8 @@ private static IQueryProcessor FakeQueryProcessorThrowingExceptionWhenReadingEve contextualProcessorForFakeSourceContext.When(p => p.ProcessAsync(Arg.Any(), Arg.Any())) .Throw(exception); var queryProcessor = Substitute.For(); - queryProcessor.In().Returns(contextualProcessorForFakeContext); - queryProcessor.In(Arg.Any()).Returns(contextualProcessorForFakeSourceContext); + queryProcessor.InGeneric(Arg.Any()).Returns(contextualProcessorForFakeContext); + queryProcessor.InSpecific(Arg.Any()).Returns(contextualProcessorForFakeSourceContext); return queryProcessor; } @@ -575,12 +580,12 @@ private static IQueryProcessor FakeQueryProcessorThrowingExceptionWhenReadingFai contextualProcessorForFakeSourceContext.When(p => p.ProcessAsync(Arg.Any(), Arg.Any())) .Throw(exception); var queryProcessor = Substitute.For(); - queryProcessor.In().Returns(contextualProcessorForFakeContext); - queryProcessor.In(Arg.Any()).Returns(contextualProcessorForFakeSourceContext); + queryProcessor.InGeneric(Arg.Any()).Returns(contextualProcessorForFakeContext); + queryProcessor.InSpecific(Arg.Any()).Returns(contextualProcessorForFakeSourceContext); return queryProcessor; } - private static EventConsumerSettings FakeSettings() => new EventConsumerSettings(1, 1); + private static EventConsumerSettings FakeSettings() => new EventConsumerSettings(new FakeContext(), 1, 1); private static IEnumerable FakeBoundedContexts() { diff --git a/Test/DDD.Core.UnitTests/Application/EventPublisherTests.cs b/Test/DDD.Core.UnitTests/Application/EventPublisherTests.cs index e98c06d..b793947 100644 --- a/Test/DDD.Core.UnitTests/Application/EventPublisherTests.cs +++ b/Test/DDD.Core.UnitTests/Application/EventPublisherTests.cs @@ -24,7 +24,7 @@ public static IEnumerable HandlersOfOtherEventsOrContexts() container.RegisterInstance(fakeHandler1); container.RegisterInstance(fakeHandler2); container.RegisterInstance(fakeHandler3); - var publisher = new EventPublisher(container); + var publisher = new EventPublisher(new FakeContext(), container); yield return new object[] { publisher, @@ -50,12 +50,11 @@ public static IEnumerable HandlerOfThisEventAndThisContext() 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(container); + var publisher = new EventPublisher(new FakeContext(), container); yield return new object[] { publisher, @@ -84,11 +83,12 @@ public async Task PublishAsync_WhenCalled_CallsHandlerOfThisEventAndThisContext( IAsyncEventHandler handlerOfThisEvent) { // Arrange + var context = new MessageContext(); handlerOfThisEvent.ClearReceivedCalls(); // Act - await publisher.PublishAsync(@event); + await publisher.PublishAsync(@event, context); // Assert - await handlerOfThisEvent.Received(1).HandleAsync(@event); + await handlerOfThisEvent.Received(1).HandleAsync(@event, context); } @@ -99,11 +99,12 @@ public async Task PublishAsync_WhenCalled_DoesNotCallHandlersOfOtherEventsOrCont IEnumerable handlersOfOtherEvents) { // Arrange + var context = new MessageContext(); handlersOfOtherEvents.ForEach(s => s.ClearReceivedCalls()); // Act - await publisher.PublishAsync(@event); + await publisher.PublishAsync(@event, context); // Assert - Assert.All(handlersOfOtherEvents, s => s.DidNotReceive().HandleAsync(Arg.Any())); + Assert.All(handlersOfOtherEvents, s => s.DidNotReceive().HandleAsync(Arg.Any(), context)); } private static IAsyncEventHandler FakeHandler() diff --git a/Test/DDD.Core.UnitTests/Application/QueryProcessorTests.cs b/Test/DDD.Core.UnitTests/Application/QueryProcessorTests.cs index 422c5ce..9d2c5d8 100644 --- a/Test/DDD.Core.UnitTests/Application/QueryProcessorTests.cs +++ b/Test/DDD.Core.UnitTests/Application/QueryProcessorTests.cs @@ -46,10 +46,11 @@ public void Process_WhenQueryDefined_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.handlerOfQuery1.Received(1).Handle(query, context); } [Fact] @@ -57,10 +58,11 @@ public void Validate_WhenQueryDefined_CallsExpectedValidator() { // Arrange var query = new FakeQuery1(); + var context = new ValidationContext(); // Act - this.processor.Validate(query); + this.processor.Validate(query, context); // Assert - this.validatorOfQuery1.Received(1).Validate(query); + this.validatorOfQuery1.Received(1).Validate(query, context); } #endregion Methods diff --git a/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs b/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs index 5267ab4..caf270b 100644 --- a/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs +++ b/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs @@ -238,12 +238,13 @@ public async Task StartAndWait_WhenExceptionInProcessingRecurringCommand_MarksRe var recurringScheduleFactory = FakeRecurringScheduleFactory(); var logger = FakeLogger(); var settings = FakeSettings(); + var fakeContext = new FakeContext(); manager = new RecurringCommandManager(commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); // Act manager.Start(); manager.Wait(TimeSpan.FromSeconds(5)); // Assert - await commandProcessor.In() + await commandProcessor.InGeneric(fakeContext) .Received(1) .ProcessAsync(Arg.Is(c => c.CommandId == recurringCommand.CommandId && c.ExceptionInfo == exception.ToString()), Arg.Any()); } @@ -303,12 +304,13 @@ public async Task StartAndWait_WhenRecurringCommandSuccessfullyProcessed_MarksRe var recurringScheduleFactory = FakeRecurringScheduleFactory(); var logger = FakeLogger(); var settings = FakeSettings(); + var fakeContext = new FakeContext(); manager = new RecurringCommandManager(commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); // Act manager.Start(); manager.Wait(TimeSpan.FromSeconds(5)); // Assert - await commandProcessor.In() + await commandProcessor.InGeneric(fakeContext) .Received(1) .ProcessAsync(Arg.Is(c => c.CommandId == recurringCommand.CommandId), Arg.Any()); } @@ -416,7 +418,7 @@ private static ICommandProcessor FakeCommandProcessor() { var contextualProcessor = Substitute.For>(); var commandProcessor = Substitute.For(); - commandProcessor.In().Returns(contextualProcessor); + commandProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); return commandProcessor; } @@ -428,7 +430,7 @@ private static ICommandProcessor FakeCommandProcessorThrowingExceptionWhenMarkin var commandProcessor = Substitute.For(); commandProcessor.When(p => p.ProcessAsync(Arg.Any(), Arg.Any())) .Throw(FakeException()); - commandProcessor.In().Returns(contextualProcessor); + commandProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); return commandProcessor; } @@ -438,7 +440,7 @@ private static ICommandProcessor FakeCommandProcessorThrowingExceptionWhenMarkin contextualProcessor.When(p => p.ProcessAsync(Arg.Any(), Arg.Any())) .Throw(exception); var commandProcessor = Substitute.For(); - commandProcessor.In().Returns(contextualProcessor); + commandProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); return commandProcessor; } @@ -448,7 +450,7 @@ private static ICommandProcessor FakeCommandProcessorThrowingExceptionWhenProces var commandProcessor = Substitute.For(); commandProcessor.When(p => p.ProcessAsync(Arg.Any(), Arg.Any())) .Throw(exception); - commandProcessor.In().Returns(contextualProcessor); + commandProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); return commandProcessor; } @@ -458,7 +460,7 @@ private static ICommandProcessor FakeCommandProcessorThrowingExceptionWhenProces var commandProcessor = Substitute.For(); commandProcessor.When(p => p.ProcessAsync(Arg.Any(), Arg.Any())) .Throw(exception); - commandProcessor.In().Returns(contextualProcessor); + commandProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); return commandProcessor; } @@ -470,7 +472,7 @@ private static IQueryProcessor FakeQueryProcessorFindingRecurringCommands(params contextualProcessor.ProcessAsync(Arg.Any(), Arg.Any()) .Returns(FakeCommandId()); var queryProcessor = Substitute.For(); - queryProcessor.In().Returns(contextualProcessor); + queryProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); return queryProcessor; } @@ -482,7 +484,7 @@ private static IQueryProcessor FakeQueryProcessorThrowingExceptionWhenFindingRec contextualProcessor.ProcessAsync(Arg.Any(), Arg.Any()) .Returns(FakeCommandId()); var queryProcessor = Substitute.For(); - queryProcessor.In().Returns(contextualProcessor); + queryProcessor.InGeneric(Arg.Any()).Returns(contextualProcessor); return queryProcessor; } @@ -534,7 +536,7 @@ private static IRecurringSchedule FakeRecurringScheduleForSingleExecution() private static ILogger FakeLogger() => Substitute.For(); - private static RecurringCommandManagerSettings FakeSettings() => new RecurringCommandManagerSettings(SerializationFormat.Json); + private static RecurringCommandManagerSettings FakeSettings() => new RecurringCommandManagerSettings(new FakeContext(), SerializationFormat.Json); private static string InvalidRecurringExpression() => "* * * * * * * *"; diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs index c0bc8ae..3a947d8 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionCreatorTests.cs @@ -7,6 +7,7 @@ namespace DDD.HealthcareDelivery.Application.Prescriptions { + using Core.Application; using Common.Application; using Practitioners; using Domain; @@ -62,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(); diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs index dd7d6d0..5fa79e3 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Application/Prescriptions/PharmaceuticalPrescriptionRevokerTests.cs @@ -7,6 +7,7 @@ namespace DDD.HealthcareDelivery.Application.Prescriptions { + using Core.Application; using Domain; using Core.Infrastructure.Data; using Domain.Prescriptions; @@ -60,8 +61,9 @@ 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); diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinderTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinderTests.cs index 4738ba7..80bfcde 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinderTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PharmaceuticalPrescriptionsByPatientFinderTests.cs @@ -6,6 +6,7 @@ namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions { + using Core.Application; using Domain; using Application.Prescriptions; using Core.Infrastructure.Testing; @@ -113,10 +114,11 @@ public async Task HandleAsync_WhenCalled_ReturnsValidResults(FindPharmaceuticalP // Arrange 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, context => context.WithStrictOrdering()); + results.Should().BeEquivalentTo(expectedResults, c => c.WithStrictOrdering()); } public void Dispose() diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinderTests.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinderTests.cs index c3607ac..45ce4a0 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinderTests.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/Prescriptions/PrescribedMedicationsByPrescriptionFinderTests.cs @@ -6,6 +6,7 @@ namespace DDD.HealthcareDelivery.Infrastructure.Prescriptions { + using Core.Application; using Domain; using Application.Prescriptions; using Core.Infrastructure.Testing; @@ -89,8 +90,9 @@ public async Task HandleAsync_WhenCalled_ReturnsValidResults(FindPrescribedMedic // Arrange 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); } From 3995bd7e640a8c09e5bdf7433f87a9f6dcf143f6 Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 5 Jun 2023 15:22:04 +0200 Subject: [PATCH 093/111] Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 91ac136..e013ec9 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ The main components are developed from technologies commonly used by .NET develo **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 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) @@ -35,6 +35,10 @@ The architecture of the project is based on the Hexagonal Architecture. The main ![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. + **Message handling** In a message-based application, 3 types of messages can be handled : From a93b0e7edfbc4b325d90c4ac104e1df69d9c1e0d Mon Sep 17 00:00:00 2001 From: draphyz Date: Wed, 21 Jun 2023 15:22:31 +0200 Subject: [PATCH 094/111] Simplify code --- Src/DDD.Core/Application/EventTranslator.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Src/DDD.Core/Application/EventTranslator.cs b/Src/DDD.Core/Application/EventTranslator.cs index 1730d5a..c501ea7 100644 --- a/Src/DDD.Core/Application/EventTranslator.cs +++ b/Src/DDD.Core/Application/EventTranslator.cs @@ -33,15 +33,10 @@ public override Event Translate(IEvent @event, IMappingContext context) { Ensure.That(@event, nameof(@event)).IsNotNull(); Ensure.That(context, nameof(context)).IsNotNull(); - Guid eventId = default; - string streamId = null, streamType = null, issuedBy = null; - if (context != null) - { - context.TryGetValue("EventId", out eventId); - context.TryGetValue("StreamId", out streamId); - context.TryGetValue("StreamType", out streamType); - context.TryGetValue("IssuedBy", out issuedBy); - } + 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() { From 7d42296370b6bacc7b30bf10512601a2e3d1fc85 Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 3 Jul 2023 14:11:30 +0200 Subject: [PATCH 095/111] Improve logging --- Src/DDD.Core/Application/EventConsumer.cs | 111 +++++++++++------- .../Application/RecurringCommandManager.cs | 36 +++--- .../Application/EventConsumerTests.cs | 18 +-- .../RecurringCommandManagerTests.cs | 6 +- 4 files changed, 94 insertions(+), 77 deletions(-) diff --git a/Src/DDD.Core/Application/EventConsumer.cs b/Src/DDD.Core/Application/EventConsumer.cs index 7e44e72..fd73965 100644 --- a/Src/DDD.Core/Application/EventConsumer.cs +++ b/Src/DDD.Core/Application/EventConsumer.cs @@ -1,4 +1,4 @@ -using EnsureThat; +using EnsureThat; using System; using System.Collections.Generic; using System.Linq; @@ -68,9 +68,8 @@ public EventConsumer(ICommandProcessor commandProcessor, public bool IsRunning { get; private set; } - protected CancellationToken CancellationToken => this.cancellationTokenSource.Token; - BoundedContext IEventConsumer.Context => this.Context; + protected CancellationToken CancellationToken => this.cancellationTokenSource.Token; #endregion Properties @@ -80,8 +79,11 @@ 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); } } @@ -89,6 +91,7 @@ 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(); } @@ -124,35 +127,38 @@ protected virtual void Dispose(bool disposing) } } + private static bool IsTransientException(Exception exception) + { + if (exception is TimestampedException ex) + return ex.IsTransient; + return false; + } + private async Task ConsumeEventsAsync() { - this.logger.LogInformation("Event handling in the context '{Context}' has started.", this.Context.Name); - this.logger.LogDebug("The configuration settings of event handling in the context '{Context}' are the following : {@Settings}", this.Context.Name, this.settings); try { await new SynchronizationContextRemover(); while (!ConsumationMaxReached()) { this.CancellationToken.ThrowIfCancellationRequested(); - this.logger.LogInformation("Event stream consumption in the context '{Context}' has started.", this.Context.Name); - var allStreams = await this.FindAllStreamsAsync(); - await this.ConsumeAllStreamsAsync(allStreams); + 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("Event stream consumption in the context '{Context}' has finished.", this.Context.Name); + this.logger.LogInformation("Consumption of event streams in the context '{Context}' has finished.", this.Context.Name); await Task.Delay(TimeSpan.FromSeconds(this.settings.ConsumationDelay), this.CancellationToken); } - if (this.CancellationToken.IsCancellationRequested) - this.logger.LogInformation("Event handling in the context '{Context}' has been stopped.", this.Context.Name); - else - this.logger.LogInformation("Event handling in the context '{Context}' has finished.", this.Context.Name); } - catch (OperationCanceledException) + catch(OperationCanceledException) { - this.logger.LogInformation("Event handling in the context '{Context}' has been stopped.", this.Context.Name); + this.logger.LogInformation("EventConsumer for the context '{Context}' has stopped.", this.Context.Name); } catch (Exception exception) { - this.logger.LogError(default, exception, "An error occurred during event handling in the context '{Context}'.", this.Context.Name); + this.logger.LogCritical(default, exception, "An error occurred while consuming event streams in the context '{Context}'.", this.Context.Name); } finally { @@ -160,24 +166,22 @@ private async Task ConsumeEventsAsync() } } - private async Task<(IEnumerable, IEnumerable)> FindAllStreamsAsync() + 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 (findStreams.Result, findFailedStreams.Result); + return new StreamsInfo(findStreams.Result, findFailedStreams.Result); } - private async Task ConsumeAllStreamsAsync((IEnumerable Streams, IEnumerable FailedStreams) allStreams) + private async Task ConsumeAllStreamsAsync(StreamsInfo streamsInfo) { - if (!allStreams.Streams.Any()) - this.logger.LogInformation("No event stream is set up for the context '{Context}'.", this.Context.Name); var tasks = new List(); - foreach (var stream in allStreams.Streams) + foreach (var stream in streamsInfo.Streams) { - var excludedStreams = allStreams.FailedStreams - .Where(s => s.StreamSource == stream.Source && s.StreamType == stream.Type) - .ToList(); + var excludedStreams = streamsInfo.FailedStreams + .Where(s => s.StreamSource == stream.Source && s.StreamType == stream.Type) + .ToList(); tasks.Add(ConsumeStreamAsync(stream, excludedStreams)); } await Task.WhenAll(tasks); @@ -200,7 +204,7 @@ private async Task ConsumeExcludedStreamsAsync(EventStream stream, List excludedStreams) { - this.logger.LogInformation("The consumption of the following event stream in the context '{Context}' has started : {Stream}", this.Context.Name, stream); + 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, @@ -215,15 +219,15 @@ private async Task ConsumeStreamWithoutExcludedStreamsAsync(EventStream stream, this.CancellationToken.ThrowIfCancellationRequested(); try { - this.logger.LogDebug("The handling of the event {EventId} in the context '{Context}' has started.", notifiedEvent.EventId, this.Context.Name); + 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("The handling of the event {EventId} in the context '{Context}' has finished.", notifiedEvent.EventId, this.Context.Name); + 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 during the handling of the event {EventId} in the context '{Context}'.", notifiedEvent.EventId, this.Context.Name); + 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 @@ -250,12 +254,12 @@ private async Task ConsumeStreamWithoutExcludedStreamsAsync(EventStream stream, break; } } - this.logger.LogInformation("The consumption of the following event stream in the context '{Context}' has finished : {Stream}", this.Context.Name, stream); + 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("The consumption of the following failed event stream in the context '{Context}' has started : {FailedStream}", this.Context.Name, excludedStream); + 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, @@ -272,16 +276,16 @@ private async Task ConsumeExcludedStreamAsync(EventStream stream, FailedEventStr this.CancellationToken.ThrowIfCancellationRequested(); try { - this.logger.LogDebug("The handling of the event {EventId} in the context '{Context}' has started.", notifiedEvent.EventId, this.Context.Name); + 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("The handling of the event {EventId} in the context '{Context}' has finished.", notifiedEvent.EventId, this.Context.Name); + 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 during the handling of the event {EventId} in the context '{Context}'.", notifiedEvent.EventId, this.Context.Name); + 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(); @@ -319,16 +323,8 @@ private async Task ConsumeExcludedStreamAsync(EventStream stream, FailedEventStr await this.commandProcessor.InGeneric(Context).ProcessAsync(command); excludedStreams.Remove(excludedStream); } - this.logger.LogInformation("The consumption of the following failed event stream in the context '{Context}' has finished : {FailedStream}", this.Context.Name, excludedStream); - } - - private static bool IsTransientException(Exception exception) - { - if (exception is TimestampedException ex) - return ex.IsTransient; - return false; + 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; @@ -381,5 +377,32 @@ private void IncrementConsumationCountIfRequired() #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 + } -} +} \ No newline at end of file diff --git a/Src/DDD.Core/Application/RecurringCommandManager.cs b/Src/DDD.Core/Application/RecurringCommandManager.cs index d1641ad..15d2a58 100644 --- a/Src/DDD.Core/Application/RecurringCommandManager.cs +++ b/Src/DDD.Core/Application/RecurringCommandManager.cs @@ -1,4 +1,4 @@ -using EnsureThat; +using EnsureThat; using System; using System.Collections.Generic; using System.Threading; @@ -90,8 +90,11 @@ 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); } } @@ -99,6 +102,7 @@ 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(); } @@ -138,8 +142,6 @@ private async Task ManageCommandsAsync() { try { - this.logger.LogInformation("Recurring command management in the context '{Context}' has started.", this.Context.Name); - this.logger.LogDebug("The configuration settings of recurring command management in the context '{Context}' are the following : {@Settings}", this.Context.Name, this.settings); this.CancellationToken.ThrowIfCancellationRequested(); await new SynchronizationContextRemover(); var commandInfos = await this.FindAllRecurringCommandsAsync(); @@ -147,18 +149,14 @@ private async Task ManageCommandsAsync() foreach (var commandInfo in commandInfos) tasks.Add(ManageCommandAsync(commandInfo.RecurringCommand, commandInfo.RecurringSchedule)); await Task.WhenAll(tasks); - if (this.CancellationToken.IsCancellationRequested) - this.logger.LogInformation("Recurring command management in the context '{Context}' has been stopped.", this.Context.Name); - else - this.logger.LogInformation("Recurring command management in the context '{Context}' has finished.", this.Context.Name); } - catch (OperationCanceledException) + catch(OperationCanceledException) { - this.logger.LogInformation("Recurring command management in the context '{Context}' has been stopped.", this.Context.Name); + this.logger.LogInformation("RecurringCommandManager for the context '{Context}' has stopped.", this.Context.Name); } catch (Exception exception) { - this.logger.LogError(default, exception, "An error occurred during recurring command management in the context '{Context}'.", this.Context.Name); + this.logger.LogCritical(default, exception, "An error occurred while managing recurring commands in the context '{Context}'.", this.Context.Name); } finally { @@ -179,14 +177,14 @@ private async Task ManageCommandAsync(RecurringCommand recurringCommand, IRecurr { try { - this.logger.LogInformation("The management of the recurring command {CommandId} in the context '{Context}' has started.", recurringCommand.CommandId, this.Context.Name); + 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("The next processing of the recurring command {CommandId} in the context '{Context}' has been scheduled for {CommandExecutionTime}.", recurringCommand.CommandId, this.Context.Name, nextOccurrence); + 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 { @@ -196,7 +194,7 @@ private async Task ManageCommandAsync(RecurringCommand recurringCommand, IRecurr catch (Exception exception) when (!(exception is OperationCanceledException)) { success = false; - this.logger.LogError(default, exception, "An error occurred during the processing of the recurring command {CommandId} in the context '{Context}'.", recurringCommand.CommandId, this.Context.Name); + 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, @@ -207,7 +205,7 @@ private async Task ManageCommandAsync(RecurringCommand recurringCommand, IRecurr } if (success) { - this.logger.LogDebug("The processing of the recurring command {CommandId} in the context '{Context}' has successfully finished.", recurringCommand.CommandId, this.Context.Name); + 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, @@ -218,19 +216,15 @@ private async Task ManageCommandAsync(RecurringCommand recurringCommand, IRecurr now = SystemTime.Local(); nextOccurrence = recurringSchedule.GetNextOccurrence(now); } - if (this.CancellationToken.IsCancellationRequested) - this.logger.LogInformation("The management of the recurring command {CommandId} in the context '{Context}' has been stopped.", recurringCommand.CommandId, this.Context.Name); - else - this.logger.LogInformation("The management of the recurring command {CommandId} in the context '{Context}' has finished.", recurringCommand.CommandId, this.Context.Name); } catch(OperationCanceledException) { - this.logger.LogInformation("The management of the recurring command {CommandId} in the context '{Context}' has been stopped.", recurringCommand.CommandId, this.Context.Name); + 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 during the management of the recurring command {CommandId} in the context '{Context}'.", recurringCommand.CommandId, this.Context.Name); + this.logger.LogError(default, exception, "An error occurred while managing recurring command {CommandId} in the context '{Context}'.", recurringCommand.CommandId, this.Context.Name); } } @@ -260,4 +254,4 @@ private void ValidateRecurringExpression(string recurringExpression) #endregion Methods } -} +} \ No newline at end of file diff --git a/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs b/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs index d682e09..b41bef6 100644 --- a/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs +++ b/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using NSubstitute; using NSubstitute.Core; using System.Threading.Tasks; @@ -178,7 +178,7 @@ public void StartAndWait_WhenExceptionInExcludingFailedEventStream_LogsException consumer.Start(); consumer.Wait(TimeSpan.FromSeconds(5)); // Assert - logger.Received(c => IsExpectedLogCall(c, LogLevel.Error, exception)); + logger.Received(c => IsExpectedLogCall(c, LogLevel.Critical, exception)); } [Fact] @@ -199,7 +199,7 @@ public void StartAndWait_WhenExceptionInFindingEventStreams_LogsException() consumer.Start(); consumer.Wait(TimeSpan.FromSeconds(5)); // Assert - logger.Received(c => IsExpectedLogCall(c, LogLevel.Error, exception)); + logger.Received(c => IsExpectedLogCall(c, LogLevel.Critical, exception)); } [Fact] @@ -220,7 +220,7 @@ public void StartAndWait_WhenExceptionInFindingFailedEventStreams_LogsException( consumer.Start(); consumer.Wait(TimeSpan.FromSeconds(5)); // Assert - logger.Received(c => IsExpectedLogCall(c, LogLevel.Error, exception)); + logger.Received(c => IsExpectedLogCall(c, LogLevel.Critical, exception)); } [Fact] @@ -241,7 +241,7 @@ public void StartAndWait_WhenExceptionInIncludingFailedEventStream_LogsException consumer.Start(); consumer.Wait(TimeSpan.FromSeconds(5)); // Assert - logger.Received(c => IsExpectedLogCall(c, LogLevel.Error, exception)); + logger.Received(c => IsExpectedLogCall(c, LogLevel.Critical, exception)); } [Fact] @@ -352,7 +352,7 @@ public void StartAndWait_WhenExceptionInReadingEventStream_LogsException() consumer.Start(); consumer.Wait(TimeSpan.FromSeconds(5)); // Assert - logger.Received(c => IsExpectedLogCall(c, LogLevel.Error, exception)); + logger.Received(c => IsExpectedLogCall(c, LogLevel.Critical, exception)); } [Fact] @@ -373,7 +373,7 @@ public void StartAndWait_WhenExceptionInReadingFailedEventStream_LogsException() consumer.Start(); consumer.Wait(TimeSpan.FromSeconds(5)); // Assert - logger.Received(c => IsExpectedLogCall(c, LogLevel.Error, exception)); + logger.Received(c => IsExpectedLogCall(c, LogLevel.Critical, exception)); } [Fact] @@ -394,7 +394,7 @@ public void StartAndWait_WhenExceptionInUpdatingFailedEventStream_LogsException( consumer.Start(); consumer.Wait(TimeSpan.FromSeconds(5)); // Assert - logger.Received(c => IsExpectedLogCall(c, LogLevel.Error, exception)); + logger.Received(c => IsExpectedLogCall(c, LogLevel.Critical, exception)); } [Fact] @@ -605,4 +605,4 @@ private static bool IsExpectedLogCall(ICall call, LogLevel level, Exception exce #endregion Methods } -} +} \ No newline at end of file diff --git a/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs b/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs index caf270b..a0b0e7d 100644 --- a/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs +++ b/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using FluentAssertions; using NSubstitute; using NSubstitute.Core; @@ -104,7 +104,7 @@ public void StartAndWait_WhenExceptionInFindingRecurringCommands_LogsException() manager.Start(); manager.Wait(TimeSpan.FromSeconds(5)); // Assert - logger.Received(c => IsExpectedLogCall(c, LogLevel.Error, exception)); + logger.Received(c => IsExpectedLogCall(c, LogLevel.Critical, exception)); } [Fact] @@ -552,4 +552,4 @@ private static bool IsExpectedLogCall(ICall call, LogLevel level, Exception exce #endregion Methods } -} +} \ No newline at end of file From 39537538f8f4ac616ac0c96a704c03ed2831a51e Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 3 Jul 2023 14:24:35 +0200 Subject: [PATCH 096/111] Add non generic classes for settings --- .../ContainerExtensions.cs | 14 +++-- Src/DDD.Core/Application/EventConsumer.cs | 6 +-- .../Application/EventConsumerSettings.cs | 32 +++++++----- .../Application/EventConsumerSettings`1.cs | 52 +++++++++++++++++++ .../RecurringCommandManagerSettings.cs | 25 +++++---- .../RecurringCommandManagerSettings`1.cs | 41 +++++++++++++++ .../Application/EventConsumerTests.cs | 6 +-- 7 files changed, 145 insertions(+), 31 deletions(-) create mode 100644 Src/DDD.Core/Application/EventConsumerSettings`1.cs create mode 100644 Src/DDD.Core/Application/RecurringCommandManagerSettings`1.cs diff --git a/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs index 9e7d619..a9fff4f 100644 --- a/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs +++ b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs @@ -120,12 +120,16 @@ public static void RegisterBaseComponents(this Container container, IEnumerable< /// /// Registers an event consumer for a specific bounded context. /// - public static void RegisterEventConsumer(this Container container, EventConsumerSettings settings) + public static void RegisterEventConsumer(this Container container, EventConsumerSettings settings) where TContext : BoundedContext { Ensure.That(container, nameof(container)).IsNotNull(); Ensure.That(settings, nameof(settings)).IsNotNull(); - container.RegisterInstance(settings); + var context = container.GetAllInstances().FirstOrDefault(c => c.Name == settings.Context); + if (context == null) + throw new InvalidOperationException($"No bounded context with the name '{settings.Context}' is registered."); + var consumerSettings = new EventConsumerSettings((TContext)context, TimeSpan.FromSeconds(settings.ConsumationDelay), settings.ConsumationMax); + container.RegisterInstance(consumerSettings); container.RegisterSingleton, EventConsumer>(); container.Collection.Append>(Lifestyle.Singleton); } @@ -133,11 +137,15 @@ public static void RegisterEventConsumer(this Container container, Eve /// /// Registers a recurring command manager for a specific bounded context. /// - public static void RegisterRecurringCommandManager(this Container container, RecurringCommandManagerSettings settings) + public static void RegisterRecurringCommandManager(this Container container, RecurringCommandManagerSettings settings) where TContext : BoundedContext { Ensure.That(container, nameof(container)).IsNotNull(); Ensure.That(settings, nameof(settings)).IsNotNull(); + var context = container.GetAllInstances().FirstOrDefault(c => c.Name == settings.Context); + if (context == null) + throw new InvalidOperationException($"No bounded context with the name '{settings.Context}' is registered."); + var managerSettings = new RecurringCommandManagerSettings((TContext)context, settings.CurrentSerializationFormat); container.RegisterInstance(settings); container.RegisterSingleton, RecurringCommandManager>(); container.Collection.Append>(Lifestyle.Singleton); diff --git a/Src/DDD.Core/Application/EventConsumer.cs b/Src/DDD.Core/Application/EventConsumer.cs index fd73965..2397d0f 100644 --- a/Src/DDD.Core/Application/EventConsumer.cs +++ b/Src/DDD.Core/Application/EventConsumer.cs @@ -1,4 +1,4 @@ -using EnsureThat; +using EnsureThat; using System; using System.Collections.Generic; using System.Linq; @@ -149,7 +149,7 @@ private async Task ConsumeEventsAsync() 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(TimeSpan.FromSeconds(this.settings.ConsumationDelay), this.CancellationToken); + await Task.Delay(this.settings.ConsumationDelay, this.CancellationToken); } } catch(OperationCanceledException) @@ -405,4 +405,4 @@ public StreamsInfo(IEnumerable streams, IEnumerable - where TContext : BoundedContext + /// + /// Used for serialization + /// + [DataContract] + public class EventConsumerSettings { #region Constructors - public EventConsumerSettings(TContext context, + public EventConsumerSettings(string context, short consumationDelay, long? consumationMax = null) { - Ensure.That(context, nameof(context)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNullOrWhiteSpace(); Ensure.That(consumationDelay, nameof(consumationDelay)).IsGte((short)0); if (consumationMax != null) Ensure.That(consumationMax.Value, nameof(consumationMax)).IsGte(0); @@ -25,26 +25,32 @@ public EventConsumerSettings(TContext context, this.ConsumationMax = consumationMax; } + /// + /// For serialization + /// + private EventConsumerSettings() { } + #endregion Constructors #region Properties /// - /// Gets the delay in seconds between two successive consumations. + /// Gets the associated context. /// [DataMember(Order = 1)] - public short ConsumationDelay { get; } + public string Context { get; private set; } /// - /// Gets the maximum number of successive consumations. + /// Gets the delay in seconds between two successive consumations. /// [DataMember(Order = 2)] - public long? ConsumationMax { get; } + public short ConsumationDelay { get; private set; } /// - /// Gets the associated context. + /// Gets the maximum number of successive consumations. /// - public TContext Context { get; } + [DataMember(Order = 3)] + public long? ConsumationMax { get; private set; } #endregion Properties diff --git a/Src/DDD.Core/Application/EventConsumerSettings`1.cs b/Src/DDD.Core/Application/EventConsumerSettings`1.cs new file mode 100644 index 0000000..f9af666 --- /dev/null +++ b/Src/DDD.Core/Application/EventConsumerSettings`1.cs @@ -0,0 +1,52 @@ +using EnsureThat; +using System; + +namespace DDD.Core.Application +{ + using Domain; + + /// + /// Used for dependency injection + /// + public class EventConsumerSettings + where TContext : BoundedContext + { + + #region Constructors + + public EventConsumerSettings(TContext context, + TimeSpan consumationDelay, + long? consumationMax = null) + { + Ensure.That(context, nameof(context)).IsNotNull(); + Ensure.That(consumationDelay, nameof(consumationDelay)).IsGte(TimeSpan.Zero); + if (consumationMax != null) + Ensure.That(consumationMax.Value, nameof(consumationMax)).IsGte(0); + this.Context = context; + this.ConsumationDelay = consumationDelay; + this.ConsumationMax = consumationMax; + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the associated context. + /// + public TContext Context { 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 + + } +} diff --git a/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs b/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs index 97ab003..a56102f 100644 --- a/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs +++ b/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs @@ -3,24 +3,30 @@ namespace DDD.Core.Application { - using Domain; using Serialization; - [DataContract()] - public class RecurringCommandManagerSettings - where TContext : BoundedContext + /// + /// Used for serialization + /// + [DataContract] + public class RecurringCommandManagerSettings { #region Constructors - public RecurringCommandManagerSettings(TContext context, + public RecurringCommandManagerSettings(string context, SerializationFormat currentSerializationFormat) { - Ensure.That(context, nameof(context)).IsNotNull(); + Ensure.That(context, nameof(context)).IsNotNullOrWhiteSpace(); this.Context= context; this.CurrentSerializationFormat = currentSerializationFormat; } + /// + /// For serialization + /// + private RecurringCommandManagerSettings() { } + #endregion Constructors #region Properties @@ -28,13 +34,14 @@ public RecurringCommandManagerSettings(TContext context, /// /// Gets the associated context. /// - public TContext Context { get; } + [DataMember(Order = 1)] + public string Context { get; private set; } /// /// Gets the current serialization format of the recurring commands. /// - [DataMember(Order = 1)] - public SerializationFormat CurrentSerializationFormat { get; } + [DataMember(Order = 2)] + public SerializationFormat CurrentSerializationFormat { get; private set; } #endregion Properties } diff --git a/Src/DDD.Core/Application/RecurringCommandManagerSettings`1.cs b/Src/DDD.Core/Application/RecurringCommandManagerSettings`1.cs new file mode 100644 index 0000000..a367518 --- /dev/null +++ b/Src/DDD.Core/Application/RecurringCommandManagerSettings`1.cs @@ -0,0 +1,41 @@ +using EnsureThat; + +namespace DDD.Core.Application +{ + using Domain; + using Serialization; + + /// + /// Used for dependency injection + /// + public class RecurringCommandManagerSettings + where TContext : BoundedContext + { + + #region Constructors + + public RecurringCommandManagerSettings(TContext context, + SerializationFormat currentSerializationFormat) + { + Ensure.That(context, nameof(context)).IsNotNull(); + this.Context= context; + this.CurrentSerializationFormat = currentSerializationFormat; + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the associated context. + /// + public TContext Context { get; } + + /// + /// Gets the current serialization format of the recurring commands. + /// + public SerializationFormat CurrentSerializationFormat { get; } + + #endregion Properties + } +} \ No newline at end of file diff --git a/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs b/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs index b41bef6..76e7519 100644 --- a/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs +++ b/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using NSubstitute; using NSubstitute.Core; using System.Threading.Tasks; @@ -585,7 +585,7 @@ private static IQueryProcessor FakeQueryProcessorThrowingExceptionWhenReadingFai return queryProcessor; } - private static EventConsumerSettings FakeSettings() => new EventConsumerSettings(new FakeContext(), 1, 1); + private static EventConsumerSettings FakeSettings() => new EventConsumerSettings(new FakeContext(), TimeSpan.FromSeconds(1), 1); private static IEnumerable FakeBoundedContexts() { @@ -605,4 +605,4 @@ private static bool IsExpectedLogCall(ICall call, LogLevel level, Exception exce #endregion Methods } -} \ No newline at end of file +} From 5b469db734b110e5f8b8cb0aba993436e8b1a7a6 Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 3 Jul 2023 14:34:32 +0200 Subject: [PATCH 097/111] Revert "Add non generic classes for settings" This reverts commit 39537538f8f4ac616ac0c96a704c03ed2831a51e. --- .../ContainerExtensions.cs | 14 ++--- Src/DDD.Core/Application/EventConsumer.cs | 6 +-- .../Application/EventConsumerSettings.cs | 32 +++++------- .../Application/EventConsumerSettings`1.cs | 52 ------------------- .../RecurringCommandManagerSettings.cs | 25 ++++----- .../RecurringCommandManagerSettings`1.cs | 41 --------------- .../Application/EventConsumerTests.cs | 6 +-- 7 files changed, 31 insertions(+), 145 deletions(-) delete mode 100644 Src/DDD.Core/Application/EventConsumerSettings`1.cs delete mode 100644 Src/DDD.Core/Application/RecurringCommandManagerSettings`1.cs diff --git a/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs index a9fff4f..9e7d619 100644 --- a/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs +++ b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs @@ -120,16 +120,12 @@ public static void RegisterBaseComponents(this Container container, IEnumerable< /// /// Registers an event consumer for a specific bounded context. /// - public static void RegisterEventConsumer(this Container container, EventConsumerSettings settings) + public static void RegisterEventConsumer(this Container container, EventConsumerSettings settings) where TContext : BoundedContext { Ensure.That(container, nameof(container)).IsNotNull(); Ensure.That(settings, nameof(settings)).IsNotNull(); - var context = container.GetAllInstances().FirstOrDefault(c => c.Name == settings.Context); - if (context == null) - throw new InvalidOperationException($"No bounded context with the name '{settings.Context}' is registered."); - var consumerSettings = new EventConsumerSettings((TContext)context, TimeSpan.FromSeconds(settings.ConsumationDelay), settings.ConsumationMax); - container.RegisterInstance(consumerSettings); + container.RegisterInstance(settings); container.RegisterSingleton, EventConsumer>(); container.Collection.Append>(Lifestyle.Singleton); } @@ -137,15 +133,11 @@ public static void RegisterEventConsumer(this Container container, Eve /// /// Registers a recurring command manager for a specific bounded context. /// - public static void RegisterRecurringCommandManager(this Container container, RecurringCommandManagerSettings settings) + public static void RegisterRecurringCommandManager(this Container container, RecurringCommandManagerSettings settings) where TContext : BoundedContext { Ensure.That(container, nameof(container)).IsNotNull(); Ensure.That(settings, nameof(settings)).IsNotNull(); - var context = container.GetAllInstances().FirstOrDefault(c => c.Name == settings.Context); - if (context == null) - throw new InvalidOperationException($"No bounded context with the name '{settings.Context}' is registered."); - var managerSettings = new RecurringCommandManagerSettings((TContext)context, settings.CurrentSerializationFormat); container.RegisterInstance(settings); container.RegisterSingleton, RecurringCommandManager>(); container.Collection.Append>(Lifestyle.Singleton); diff --git a/Src/DDD.Core/Application/EventConsumer.cs b/Src/DDD.Core/Application/EventConsumer.cs index 2397d0f..fd73965 100644 --- a/Src/DDD.Core/Application/EventConsumer.cs +++ b/Src/DDD.Core/Application/EventConsumer.cs @@ -1,4 +1,4 @@ -using EnsureThat; +using EnsureThat; using System; using System.Collections.Generic; using System.Linq; @@ -149,7 +149,7 @@ private async Task ConsumeEventsAsync() 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); + await Task.Delay(TimeSpan.FromSeconds(this.settings.ConsumationDelay), this.CancellationToken); } } catch(OperationCanceledException) @@ -405,4 +405,4 @@ public StreamsInfo(IEnumerable streams, IEnumerable - /// Used for serialization - /// - [DataContract] - public class EventConsumerSettings + using Domain; + + [DataContract()] + public class EventConsumerSettings + where TContext : BoundedContext { #region Constructors - public EventConsumerSettings(string context, + public EventConsumerSettings(TContext context, short consumationDelay, long? consumationMax = null) { - Ensure.That(context, nameof(context)).IsNotNullOrWhiteSpace(); + Ensure.That(context, nameof(context)).IsNotNull(); Ensure.That(consumationDelay, nameof(consumationDelay)).IsGte((short)0); if (consumationMax != null) Ensure.That(consumationMax.Value, nameof(consumationMax)).IsGte(0); @@ -25,32 +25,26 @@ public EventConsumerSettings(string context, this.ConsumationMax = consumationMax; } - /// - /// For serialization - /// - private EventConsumerSettings() { } - #endregion Constructors #region Properties /// - /// Gets the associated context. + /// Gets the delay in seconds between two successive consumations. /// [DataMember(Order = 1)] - public string Context { get; private set; } + public short ConsumationDelay { get; } /// - /// Gets the delay in seconds between two successive consumations. + /// Gets the maximum number of successive consumations. /// [DataMember(Order = 2)] - public short ConsumationDelay { get; private set; } + public long? ConsumationMax { get; } /// - /// Gets the maximum number of successive consumations. + /// Gets the associated context. /// - [DataMember(Order = 3)] - public long? ConsumationMax { get; private set; } + public TContext Context { get; } #endregion Properties diff --git a/Src/DDD.Core/Application/EventConsumerSettings`1.cs b/Src/DDD.Core/Application/EventConsumerSettings`1.cs deleted file mode 100644 index f9af666..0000000 --- a/Src/DDD.Core/Application/EventConsumerSettings`1.cs +++ /dev/null @@ -1,52 +0,0 @@ -using EnsureThat; -using System; - -namespace DDD.Core.Application -{ - using Domain; - - /// - /// Used for dependency injection - /// - public class EventConsumerSettings - where TContext : BoundedContext - { - - #region Constructors - - public EventConsumerSettings(TContext context, - TimeSpan consumationDelay, - long? consumationMax = null) - { - Ensure.That(context, nameof(context)).IsNotNull(); - Ensure.That(consumationDelay, nameof(consumationDelay)).IsGte(TimeSpan.Zero); - if (consumationMax != null) - Ensure.That(consumationMax.Value, nameof(consumationMax)).IsGte(0); - this.Context = context; - this.ConsumationDelay = consumationDelay; - this.ConsumationMax = consumationMax; - } - - #endregion Constructors - - #region Properties - - /// - /// Gets the associated context. - /// - public TContext Context { 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 - - } -} diff --git a/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs b/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs index a56102f..97ab003 100644 --- a/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs +++ b/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs @@ -3,30 +3,24 @@ namespace DDD.Core.Application { + using Domain; using Serialization; - /// - /// Used for serialization - /// - [DataContract] - public class RecurringCommandManagerSettings + [DataContract()] + public class RecurringCommandManagerSettings + where TContext : BoundedContext { #region Constructors - public RecurringCommandManagerSettings(string context, + public RecurringCommandManagerSettings(TContext context, SerializationFormat currentSerializationFormat) { - Ensure.That(context, nameof(context)).IsNotNullOrWhiteSpace(); + Ensure.That(context, nameof(context)).IsNotNull(); this.Context= context; this.CurrentSerializationFormat = currentSerializationFormat; } - /// - /// For serialization - /// - private RecurringCommandManagerSettings() { } - #endregion Constructors #region Properties @@ -34,14 +28,13 @@ private RecurringCommandManagerSettings() { } /// /// Gets the associated context. /// - [DataMember(Order = 1)] - public string Context { get; private set; } + public TContext Context { get; } /// /// Gets the current serialization format of the recurring commands. /// - [DataMember(Order = 2)] - public SerializationFormat CurrentSerializationFormat { get; private set; } + [DataMember(Order = 1)] + public SerializationFormat CurrentSerializationFormat { get; } #endregion Properties } diff --git a/Src/DDD.Core/Application/RecurringCommandManagerSettings`1.cs b/Src/DDD.Core/Application/RecurringCommandManagerSettings`1.cs deleted file mode 100644 index a367518..0000000 --- a/Src/DDD.Core/Application/RecurringCommandManagerSettings`1.cs +++ /dev/null @@ -1,41 +0,0 @@ -using EnsureThat; - -namespace DDD.Core.Application -{ - using Domain; - using Serialization; - - /// - /// Used for dependency injection - /// - public class RecurringCommandManagerSettings - where TContext : BoundedContext - { - - #region Constructors - - public RecurringCommandManagerSettings(TContext context, - SerializationFormat currentSerializationFormat) - { - Ensure.That(context, nameof(context)).IsNotNull(); - this.Context= context; - this.CurrentSerializationFormat = currentSerializationFormat; - } - - #endregion Constructors - - #region Properties - - /// - /// Gets the associated context. - /// - public TContext Context { get; } - - /// - /// Gets the current serialization format of the recurring commands. - /// - public SerializationFormat CurrentSerializationFormat { get; } - - #endregion Properties - } -} \ No newline at end of file diff --git a/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs b/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs index 76e7519..b41bef6 100644 --- a/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs +++ b/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using NSubstitute; using NSubstitute.Core; using System.Threading.Tasks; @@ -585,7 +585,7 @@ private static IQueryProcessor FakeQueryProcessorThrowingExceptionWhenReadingFai return queryProcessor; } - private static EventConsumerSettings FakeSettings() => new EventConsumerSettings(new FakeContext(), TimeSpan.FromSeconds(1), 1); + private static EventConsumerSettings FakeSettings() => new EventConsumerSettings(new FakeContext(), 1, 1); private static IEnumerable FakeBoundedContexts() { @@ -605,4 +605,4 @@ private static bool IsExpectedLogCall(ICall call, LogLevel level, Exception exce #endregion Methods } -} +} \ No newline at end of file From 43aadb06e61af6536fad7d549f276302ecc42c36 Mon Sep 17 00:00:00 2001 From: draphyz Date: Tue, 18 Jul 2023 14:06:56 +0200 Subject: [PATCH 098/111] Add API for dependency injection --- DDD.sln | 1 + Doc/DeploymentEvolution.png | Bin 0 -> 126547 bytes Src/DDD.Common/Domain/ArbitraryIdentifier.cs | 2 +- Src/DDD.Common/Domain/BinaryContent.cs | 2 +- Src/DDD.Common/Domain/ContactInformation.cs | 5 +- Src/DDD.Common/Domain/EmailAddress.cs | 2 +- Src/DDD.Common/Domain/Enumeration.cs | 3 +- Src/DDD.Common/Domain/FullName.cs | 2 +- Src/DDD.Common/Domain/IdentificationCode.cs | 2 +- Src/DDD.Common/Domain/IdentificationNumber.cs | 2 +- Src/DDD.Common/Domain/PostalAddress.cs | 5 +- .../IKeyedServiceProvider.cs | 14 - Src/DDD.Core.Abstractions/IFluentInterface.cs | 44 ++ Src/DDD.Core.Abstractions/IObjectBuilder.cs | 15 + .../Mapping/DelegatingMapper.cs | 43 ++ .../Mapping/IMappingContextExtensions.cs | 30 + .../Mapping/MappingContextInfo.cs | 16 + .../Mapping/MappingException.cs | 4 +- .../Mapping/MappingProcessor.cs | 33 +- .../Mapping/MappingProcessorSettings.cs | 26 + .../Serialization/SerializationException.cs | 2 +- .../TimestampedException.cs | 4 +- .../Validation/DelegatingValidator.cs | 59 ++ .../Validation/ValidationFailure.cs | 6 +- Src/DDD.Core.Cronos/CronosScheduleFactory.cs | 5 + .../RecurringCommandRegister.cs | 4 +- .../Scripts/FindRecurringCommandByType.sql | Bin 298 -> 368 bytes .../Scripts/FindRecurringCommands.sql | Bin 270 -> 340 bytes .../Scripts/InsertRecurringCommand.sql | Bin 588 -> 750 bytes .../Scripts/UpdateRecurringCommand.sql | Bin 286 -> 408 bytes .../Application/RecurringCommand.cs | 4 +- .../Application/RecurringCommandDetail.cs | 4 +- .../Application/RegisterRecurringCommand.cs | 4 +- .../Domain/BoundedContext.cs | 3 + .../ContainerExtensions.cs | 284 +++++--- .../KeyedServiceProvider.cs | 59 -- Src/DDD.Core.SimpleInjector/RegisteredType.cs | 31 + Src/DDD.Core.Xunit/DbFixture.cs | 3 +- Src/DDD.Core/Application/CommandException.cs | 6 +- .../Application/CommandInvalidException.cs | 6 +- Src/DDD.Core/Application/CommandProcessor.cs | 19 +- .../Application/CommandProcessorSettings.cs | 24 + Src/DDD.Core/Application/EventConsumer.cs | 20 +- .../Application/EventConsumerSettings.cs | 32 +- .../Application/IRecurringScheduleFactory.cs | 7 +- Src/DDD.Core/Application/QueryException.cs | 6 +- .../Application/QueryInvalidException.cs | 6 +- Src/DDD.Core/Application/QueryProcessor.cs | 21 +- .../Application/QueryProcessorSettings.cs | 24 + .../Application/RecurringCommandManager.cs | 139 ++-- .../RecurringCommandManagerSettings.cs | 29 +- .../Application/RecurringExpressionFormat.cs | 7 + Src/DDD.Core/Domain/DomainServiceException.cs | 6 +- .../Domain/DomainServiceInvalidException.cs | 6 +- Src/DDD.Core/Domain/RepositoryException.cs | 6 +- .../Data/DbConnectionSettings.cs | 45 ++ .../Data/LazyDbConnectionProvider.cs | 15 +- .../AppRegistrationOptions.cs | 191 +++++ .../CommandsRegistrationOptions.cs | 113 +++ .../DbConnectionOptions.cs | 94 +++ .../EventConsumerOptions.cs | 98 +++ .../EventsRegistrationOptions.cs | 94 +++ .../MappingRegistrationOptions.cs | 61 ++ .../QueriesRegistrationOptions.cs | 60 ++ .../RecurringCommandManagerOptions.cs | 89 +++ .../DDD.Common.UnitTests.csproj | 2 +- .../DDD.Core.Abstractions.UnitTests.csproj | 2 +- .../IMappingProcessorExtensionsTests.cs | 2 +- .../Mapping/MappingProcessorTests.cs | 2 +- .../DDD.Core.Dapper.IntegrationTests.csproj | 2 +- .../FailedRecurringCommandUpdaterTests.cs | 3 +- .../OracleRecurringCommandsFinderTests.cs | 6 +- .../Data/RecurringCommandRegisterTests.cs | 8 +- .../Data/Scripts/Oracle/FillSchema.sql | 1 + .../Scripts/Oracle/FindRecurringCommands.sql | Bin 1510 -> 1654 bytes .../Oracle/MarkRecurringCommandAsFailed.sql | Bin 1510 -> 1654 bytes .../MarkRecurringCommandAsSuccessful.sql | Bin 1734 -> 1878 bytes .../Data/Scripts/SqlServer/CreateDatabase.sql | Bin 15302 -> 15410 bytes .../SqlServer/FindRecurringCommands.sql | Bin 1618 -> 1770 bytes .../MarkRecurringCommandAsFailed.sql | Bin 1618 -> 1770 bytes .../MarkRecurringCommandAsSuccessful.sql | Bin 1812 -> 1964 bytes .../SqlServerRecurringCommandsFinderTests.cs | 6 +- .../SuccessfulRecurringCommandUpdaterTests.cs | 3 +- .../DDD.Core.Newtonsoft.UnitTests.csproj | 2 +- .../ContainerExtensionsTests.cs | 674 ++++++++++++++++++ .../DDD.Core.SimpleInjector.UnitTests.csproj | 19 + .../Decorated.cs | 15 + .../DummyContext.cs | 15 + .../FakeCommand.cs | 8 + .../FakeCommandHandler.cs | 21 + .../FakeContext.cs | 15 + .../FakeEvent.cs | 17 + .../FakeEventHandler.cs | 49 ++ .../FakeMapper.cs | 15 + .../FakeQuery.cs | 8 + .../FakeQueryHandler.cs | 22 + .../FakeTranslator.cs | 32 + .../FirstDecorator.cs | 30 + .../FirstDelegatingDecorator.cs | 32 + .../IDoSomething.cs | 13 + .../SecondDecorator.cs | 31 + .../SecondDelegatingDecorator.cs | 32 + .../Application/CommandProcessorTests.cs | 2 +- .../Application/EventConsumerTests.cs | 19 +- .../Application/QueryProcessorTests.cs | 2 +- .../RecurringCommandManagerTests.cs | 121 ++-- .../DDD.Core.UnitTests.csproj | 2 +- ...HealthcareDelivery.IntegrationTests.csproj | 2 +- .../DDD.HealthcareDelivery.UnitTests.csproj | 2 +- 109 files changed, 2768 insertions(+), 416 deletions(-) create mode 100644 Doc/DeploymentEvolution.png delete mode 100644 Src/DDD.Core.Abstractions/DependencyInjection/IKeyedServiceProvider.cs create mode 100644 Src/DDD.Core.Abstractions/IFluentInterface.cs create mode 100644 Src/DDD.Core.Abstractions/IObjectBuilder.cs create mode 100644 Src/DDD.Core.Abstractions/Mapping/DelegatingMapper.cs create mode 100644 Src/DDD.Core.Abstractions/Mapping/IMappingContextExtensions.cs create mode 100644 Src/DDD.Core.Abstractions/Mapping/MappingContextInfo.cs create mode 100644 Src/DDD.Core.Abstractions/Mapping/MappingProcessorSettings.cs create mode 100644 Src/DDD.Core.Abstractions/Validation/DelegatingValidator.cs delete mode 100644 Src/DDD.Core.SimpleInjector/KeyedServiceProvider.cs create mode 100644 Src/DDD.Core.SimpleInjector/RegisteredType.cs create mode 100644 Src/DDD.Core/Application/CommandProcessorSettings.cs create mode 100644 Src/DDD.Core/Application/QueryProcessorSettings.cs create mode 100644 Src/DDD.Core/Application/RecurringExpressionFormat.cs create mode 100644 Src/DDD.Core/Infrastructure/Data/DbConnectionSettings.cs create mode 100644 Src/DDD.Core/Infrastructure/DependencyInjection/AppRegistrationOptions.cs create mode 100644 Src/DDD.Core/Infrastructure/DependencyInjection/CommandsRegistrationOptions.cs create mode 100644 Src/DDD.Core/Infrastructure/DependencyInjection/DbConnectionOptions.cs create mode 100644 Src/DDD.Core/Infrastructure/DependencyInjection/EventConsumerOptions.cs create mode 100644 Src/DDD.Core/Infrastructure/DependencyInjection/EventsRegistrationOptions.cs create mode 100644 Src/DDD.Core/Infrastructure/DependencyInjection/MappingRegistrationOptions.cs create mode 100644 Src/DDD.Core/Infrastructure/DependencyInjection/QueriesRegistrationOptions.cs create mode 100644 Src/DDD.Core/Infrastructure/DependencyInjection/RecurringCommandManagerOptions.cs create mode 100644 Test/DDD.Core.SimpleInjector.UnitTests/ContainerExtensionsTests.cs create mode 100644 Test/DDD.Core.SimpleInjector.UnitTests/DDD.Core.SimpleInjector.UnitTests.csproj create mode 100644 Test/DDD.Core.SimpleInjector.UnitTests/Decorated.cs create mode 100644 Test/DDD.Core.SimpleInjector.UnitTests/DummyContext.cs create mode 100644 Test/DDD.Core.SimpleInjector.UnitTests/FakeCommand.cs create mode 100644 Test/DDD.Core.SimpleInjector.UnitTests/FakeCommandHandler.cs create mode 100644 Test/DDD.Core.SimpleInjector.UnitTests/FakeContext.cs create mode 100644 Test/DDD.Core.SimpleInjector.UnitTests/FakeEvent.cs create mode 100644 Test/DDD.Core.SimpleInjector.UnitTests/FakeEventHandler.cs create mode 100644 Test/DDD.Core.SimpleInjector.UnitTests/FakeMapper.cs create mode 100644 Test/DDD.Core.SimpleInjector.UnitTests/FakeQuery.cs create mode 100644 Test/DDD.Core.SimpleInjector.UnitTests/FakeQueryHandler.cs create mode 100644 Test/DDD.Core.SimpleInjector.UnitTests/FakeTranslator.cs create mode 100644 Test/DDD.Core.SimpleInjector.UnitTests/FirstDecorator.cs create mode 100644 Test/DDD.Core.SimpleInjector.UnitTests/FirstDelegatingDecorator.cs create mode 100644 Test/DDD.Core.SimpleInjector.UnitTests/IDoSomething.cs create mode 100644 Test/DDD.Core.SimpleInjector.UnitTests/SecondDecorator.cs create mode 100644 Test/DDD.Core.SimpleInjector.UnitTests/SecondDelegatingDecorator.cs diff --git a/DDD.sln b/DDD.sln index b123ea7..9e76a58 100644 --- a/DDD.sln +++ b/DDD.sln @@ -34,6 +34,7 @@ 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 diff --git a/Doc/DeploymentEvolution.png b/Doc/DeploymentEvolution.png new file mode 100644 index 0000000000000000000000000000000000000000..536424743f479321a4eac0573ebe8249a546df66 GIT binary patch literal 126547 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 PfWJ$M*DhvWFnaud@V`=6 literal 0 HcmV?d00001 diff --git a/Src/DDD.Common/Domain/ArbitraryIdentifier.cs b/Src/DDD.Common/Domain/ArbitraryIdentifier.cs index 6561727..26aaf1e 100644 --- a/Src/DDD.Common/Domain/ArbitraryIdentifier.cs +++ b/Src/DDD.Common/Domain/ArbitraryIdentifier.cs @@ -41,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/BinaryContent.cs b/Src/DDD.Common/Domain/BinaryContent.cs index 72952dc..f5dc352 100644 --- a/Src/DDD.Common/Domain/BinaryContent.cs +++ b/Src/DDD.Common/Domain/BinaryContent.cs @@ -53,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 e3b8a92..d54f95a 100644 --- a/Src/DDD.Common/Domain/ContactInformation.cs +++ b/Src/DDD.Common/Domain/ContactInformation.cs @@ -100,8 +100,9 @@ public override IEnumerable EqualityComponents() 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/EmailAddress.cs b/Src/DDD.Common/Domain/EmailAddress.cs index 12960e9..950f84e 100644 --- a/Src/DDD.Common/Domain/EmailAddress.cs +++ b/Src/DDD.Common/Domain/EmailAddress.cs @@ -58,7 +58,7 @@ 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(); diff --git a/Src/DDD.Common/Domain/Enumeration.cs b/Src/DDD.Common/Domain/Enumeration.cs index ebafc15..6981586 100644 --- a/Src/DDD.Common/Domain/Enumeration.cs +++ b/Src/DDD.Common/Domain/Enumeration.cs @@ -117,8 +117,9 @@ 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 IEnumerable GetAllInstances() where TEnum : Enumeration { var enumType = typeof(TEnum); diff --git a/Src/DDD.Common/Domain/FullName.cs b/Src/DDD.Common/Domain/FullName.cs index 1d1626e..d265395 100644 --- a/Src/DDD.Common/Domain/FullName.cs +++ b/Src/DDD.Common/Domain/FullName.cs @@ -49,7 +49,7 @@ public override IEnumerable EqualityComponents() 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) diff --git a/Src/DDD.Common/Domain/IdentificationCode.cs b/Src/DDD.Common/Domain/IdentificationCode.cs index 3637db1..8c70639 100644 --- a/Src/DDD.Common/Domain/IdentificationCode.cs +++ b/Src/DDD.Common/Domain/IdentificationCode.cs @@ -42,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 b3d6bb9..471e6f1 100644 --- a/Src/DDD.Common/Domain/IdentificationNumber.cs +++ b/Src/DDD.Common/Domain/IdentificationNumber.cs @@ -42,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/PostalAddress.cs b/Src/DDD.Common/Domain/PostalAddress.cs index 15f327a..28a1035 100644 --- a/Src/DDD.Common/Domain/PostalAddress.cs +++ b/Src/DDD.Common/Domain/PostalAddress.cs @@ -83,10 +83,7 @@ public override IEnumerable EqualityComponents() } 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.Core.Abstractions/DependencyInjection/IKeyedServiceProvider.cs b/Src/DDD.Core.Abstractions/DependencyInjection/IKeyedServiceProvider.cs deleted file mode 100644 index c8d1b88..0000000 --- a/Src/DDD.Core.Abstractions/DependencyInjection/IKeyedServiceProvider.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace DDD.DependencyInjection -{ - public interface IKeyedServiceProvider - where TService : class - { - - #region Methods - - TService GetService(TKey key); - - #endregion Methods - - } -} diff --git a/Src/DDD.Core.Abstractions/IFluentInterface.cs b/Src/DDD.Core.Abstractions/IFluentInterface.cs new file mode 100644 index 0000000..eaabf8f --- /dev/null +++ b/Src/DDD.Core.Abstractions/IFluentInterface.cs @@ -0,0 +1,44 @@ +using System; +using System.ComponentModel; + +namespace DDD +{ + /// + /// Interface that is used to build fluent interfaces and hides methods declared by from IntelliSense. + /// + /// // + /// Code that consumes implementations of this interface should expect one of two things: + /// + /// When referencing the interface from within the same solution (project reference), you will still see the methods this interface is meant to hide. + /// When referencing the interface through the compiled output assembly (external reference), the standard Object methods will be hidden as intended. + /// When using Resharper, be sure to configure it to respect the attribute: Options, go to Environment | IntelliSense | Completion Appearance and check "Filter members by [EditorBrowsable] attribute". + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public interface IFluentInterface + { + /// + /// Redeclaration that hides the method from IntelliSense. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + Type GetType(); + + /// + /// Redeclaration that hides the method from IntelliSense. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + int GetHashCode(); + + /// + /// Redeclaration that hides the method from IntelliSense. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + string ToString(); + + /// + /// Redeclaration that hides the method from IntelliSense. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + bool Equals(object obj); + } +} diff --git a/Src/DDD.Core.Abstractions/IObjectBuilder.cs b/Src/DDD.Core.Abstractions/IObjectBuilder.cs new file mode 100644 index 0000000..8814920 --- /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 : IFluentInterface + where T : class + { + #region Methods + + T Build(); + + #endregion Methods + } +} \ No newline at end of file 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/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/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 index 4224e21..9ebe507 100644 --- a/Src/DDD.Core.Abstractions/Mapping/MappingException.cs +++ b/Src/DDD.Core.Abstractions/Mapping/MappingException.cs @@ -52,9 +52,9 @@ public override string ToString() { var s = $"{this.GetType()}: {this.Message} "; if (this.SourceType != null) - s += $"{Environment.NewLine}SourceType: {this.SourceType}"; + s += $"{Environment.NewLine}{nameof(SourceType)}: {this.SourceType}"; if (this.DestinationType != null) - s += $"{Environment.NewLine}DestinationType: {this.DestinationType}"; + s += $"{Environment.NewLine}{nameof(DestinationType)}: {this.DestinationType}"; if (this.InnerException != null) s += $" ---> {this.InnerException}"; if (this.StackTrace != null) diff --git a/Src/DDD.Core.Abstractions/Mapping/MappingProcessor.cs b/Src/DDD.Core.Abstractions/Mapping/MappingProcessor.cs index 08f1262..e1f01ef 100644 --- a/Src/DDD.Core.Abstractions/Mapping/MappingProcessor.cs +++ b/Src/DDD.Core.Abstractions/Mapping/MappingProcessor.cs @@ -12,15 +12,18 @@ 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) { Ensure.That(serviceProvider, nameof(serviceProvider)).IsNotNull(); + Ensure.That(settings, nameof(settings)).IsNotNull(); this.serviceProvider = serviceProvider; + this.settings = settings; } #endregion Constructors @@ -32,17 +35,30 @@ public void Map(TSource source, TDestination destination, 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, context); + 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, 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."); + 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); } @@ -50,8 +66,15 @@ public TDestination Translate(TSource source, IMappingCon 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."); + 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); } 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/Serialization/SerializationException.cs b/Src/DDD.Core.Abstractions/Serialization/SerializationException.cs index cdbfe18..46ced8d 100644 --- a/Src/DDD.Core.Abstractions/Serialization/SerializationException.cs +++ b/Src/DDD.Core.Abstractions/Serialization/SerializationException.cs @@ -43,7 +43,7 @@ public override string ToString() { var s = $"{this.GetType()}: {this.Message} "; if (this.ObjectType != null) - s += $"{Environment.NewLine}ObjectType: {this.ObjectType}"; + s += $"{Environment.NewLine}{nameof(ObjectType)}: {this.ObjectType}"; if (this.InnerException != null) s += $" ---> {this.InnerException}"; if (this.StackTrace != null) diff --git a/Src/DDD.Core.Abstractions/TimestampedException.cs b/Src/DDD.Core.Abstractions/TimestampedException.cs index 7b92507..752cd05 100644 --- a/Src/DDD.Core.Abstractions/TimestampedException.cs +++ b/Src/DDD.Core.Abstractions/TimestampedException.cs @@ -46,8 +46,8 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont public override string ToString() { var s = $"{this.GetType()}: {this.Message} "; - s += $"{Environment.NewLine}Timestamp: {this.Timestamp}"; - s += $"{Environment.NewLine}IsTransient: {this.IsTransient}"; + 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) 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/ValidationFailure.cs b/Src/DDD.Core.Abstractions/Validation/ValidationFailure.cs index 40e39fe..5995014 100644 --- a/Src/DDD.Core.Abstractions/Validation/ValidationFailure.cs +++ b/Src/DDD.Core.Abstractions/Validation/ValidationFailure.cs @@ -88,10 +88,8 @@ private ValidationFailure() #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}, category={this.Category}]"; - } + 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.Cronos/CronosScheduleFactory.cs b/Src/DDD.Core.Cronos/CronosScheduleFactory.cs index dd84fd9..1c0edb2 100644 --- a/Src/DDD.Core.Cronos/CronosScheduleFactory.cs +++ b/Src/DDD.Core.Cronos/CronosScheduleFactory.cs @@ -8,6 +8,11 @@ namespace DDD.Core.Infrastructure public class CronosScheduleFactory : IRecurringScheduleFactory { + #region Properties + + public RecurringExpressionFormat Format { get; } = RecurringExpressionFormat.Cron; + + #endregion Properties #region Methods diff --git a/Src/DDD.Core.Dapper/RecurringCommandRegister.cs b/Src/DDD.Core.Dapper/RecurringCommandRegister.cs index e488363..263cece 100644 --- a/Src/DDD.Core.Dapper/RecurringCommandRegister.cs +++ b/Src/DDD.Core.Dapper/RecurringCommandRegister.cs @@ -153,6 +153,7 @@ private static bool HasChanges(RegisterRecurringCommand command, RecurringComman 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; } @@ -166,7 +167,8 @@ private object ToParameters(RegisterRecurringCommand command, IDbConnection conn command.CommandType, command.Body, command.BodyFormat, - command.RecurringExpression + command.RecurringExpression, + command.RecurringExpressionFormat }; return command; } diff --git a/Src/DDD.Core.Dapper/Scripts/FindRecurringCommandByType.sql b/Src/DDD.Core.Dapper/Scripts/FindRecurringCommandByType.sql index 38c5d6a3901c19d87fdc5dea5922cb2a4faa08fc..f37a220d681f2ea3b9990522a62e1c1a9fe71928 100644 GIT binary patch delta 27 jcmZ3*^nq!@sfkJ=6PKuQyD{W56fxv7Br=ptJeddpi0uhV delta 10 Rcmeysw2EoMsmTJ22>=-i1N{I1 diff --git a/Src/DDD.Core.Dapper/Scripts/FindRecurringCommands.sql b/Src/DDD.Core.Dapper/Scripts/FindRecurringCommands.sql index 023500e3c1f872a7f73d191faed2999ee7e0944b..0ea50a6b81f529e96d734d6fdc28fdf3372b3fe9 100644 GIT binary patch delta 27 jcmeBUy23Q!)I=qbiA&VD-5By2iWqVk5*bP+o^%BOgCPj^ delta 10 Rcmcb@)Wte0046t3~B%X delta 15 WcmaFIdWL0#8sp|NMioXTO$Gof&;%6# diff --git a/Src/DDD.Core.Dapper/Scripts/UpdateRecurringCommand.sql b/Src/DDD.Core.Dapper/Scripts/UpdateRecurringCommand.sql index 34bd9f2b1f9b159c174f80665fd8d6a22ac00a31..e0461c8c4a0cbe5a75375df6eeb19c50bc0cae8b 100644 GIT binary patch delta 47 scmbQoG=q7 $"{this.GetType().Name} [ {nameof(CommandId)}={this.CommandId}, {nameof(CommandType)}={this.CommandType}, {nameof(RecurringExpression)}={this.RecurringExpression}]"; + => $"{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 index 093abb9..ef2bfa5 100644 --- a/Src/DDD.Core.Messages/Application/RecurringCommandDetail.cs +++ b/Src/DDD.Core.Messages/Application/RecurringCommandDetail.cs @@ -16,6 +16,8 @@ public class RecurringCommandDetail public string RecurringExpression { get; set; } + public string RecurringExpressionFormat { get; set; } + public DateTime? LastExecutionTime { get; set; } public CommandExecutionStatus? LastExecutionStatus { get; set; } @@ -27,7 +29,7 @@ public class RecurringCommandDetail #region Methods public override string ToString() - => $"{this.GetType().Name} [ {nameof(CommandId)}={this.CommandId}, {nameof(CommandType)}={this.CommandType}, {nameof(RecurringExpression)}={this.RecurringExpression}]"; + => $"{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 index 1e76dbd..496514a 100644 --- a/Src/DDD.Core.Messages/Application/RegisterRecurringCommand.cs +++ b/Src/DDD.Core.Messages/Application/RegisterRecurringCommand.cs @@ -16,12 +16,14 @@ public class RegisterRecurringCommand : ICommand 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}]"; + => $"{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/Domain/BoundedContext.cs b/Src/DDD.Core.Messages/Domain/BoundedContext.cs index 7deb61f..f910879 100644 --- a/Src/DDD.Core.Messages/Domain/BoundedContext.cs +++ b/Src/DDD.Core.Messages/Domain/BoundedContext.cs @@ -58,6 +58,9 @@ 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(); diff --git a/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs index 9e7d619..78d7fac 100644 --- a/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs +++ b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs @@ -6,23 +6,116 @@ using System.Collections.Generic; using System.Reflection; using System.Linq; -using DDD.DependencyInjection; +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(); + configureOptions(builder); + var appOptions = ((IObjectBuilder)builder).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); + } + } + public static void RegisterConditional(this Container container, Func instanceCreator, Lifestyle lifestyle, Predicate predicate) - where TService : class + where TService : class { Ensure.That(container, nameof(container)).IsNotNull(); var registration = lifestyle.CreateRegistration(instanceCreator, container); @@ -83,72 +176,90 @@ public static void RegisterConditional(this Container container, } /// - /// Gets a named instance of a service. + /// Tries to get an instance of the specified service. /// - public static TService GetNamedInstance(this Container container, string name) where TService : class + public static bool TryGetInstance(this Container container, out TService instance) + where TService : class { Ensure.That(container, nameof(container)).IsNotNull(); - var provider = container.GetInstance>(); - return provider.GetService(name); + IServiceProvider provider = container; + instance = (TService)provider.GetService(typeof(TService)); + return instance != null; } - /// - /// Registers base components for handling commands, queries and events. - /// - /// The container that registers base components. - /// The assemblies that contain base components. - /// A predicate that determines which components should be registered. - public static void RegisterBaseComponents(this Container container, IEnumerable assemblies, Func predicate = null) + private static void RegisterBoundedContexts(this Container container, AppRegistrationOptions options) { - Ensure.That(container, nameof(container)).IsNotNull(); - Ensure.That(assemblies, nameof(assemblies)).IsNotNull(); ; - var notNullPredicate = predicate ?? (t => true); - container.Options.DefaultScopedLifestyle = Lifestyle.CreateHybrid(new ThreadScopedLifestyle(), new AsyncScopedLifestyle()); - container.RegisterInstance(container); - container.RegisterBoundedContexts(assemblies, notNullPredicate); - container.RegisterSingleton(); - container.RegisterCommandHandlers(assemblies, notNullPredicate); - container.RegisterSingleton(); - container.RegisterQueryHandlers(assemblies, notNullPredicate); - container.RegisterSingleton(typeof(IEventPublisher<>), typeof(EventPublisher<>)); - container.RegisterEventHandlers(assemblies, notNullPredicate); - container.RegisterSingleton(); - container.RegisterMappersAndTranslators(assemblies, notNullPredicate); - container.RegisterRepositories(assemblies, notNullPredicate); + 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); + } } - /// - /// Registers an event consumer for a specific bounded context. - /// - public static void RegisterEventConsumer(this Container container, EventConsumerSettings settings) - where TContext : BoundedContext + private static void RegisterCommandHandlers(this Container container, AppRegistrationOptions options) { - Ensure.That(container, nameof(container)).IsNotNull(); - Ensure.That(settings, nameof(settings)).IsNotNull(); - container.RegisterInstance(settings); - container.RegisterSingleton, EventConsumer>(); - container.Collection.Append>(Lifestyle.Singleton); + 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); + container.RegisterConditional(typeof(IAsyncCommandHandler<,>), options.AssembliesToScan, options.TypeFilter, registerOptions); + container.RegisterDecorator(typeof(IAsyncCommandHandler<,>), typeof(AsyncCommandHandlerWithLogging<,>)); + container.RegisterDecorator(typeof(IAsyncCommandHandler<,>), typeof(AsyncScopedCommandHandler<,>), Lifestyle.Singleton); + 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); } - /// - /// Registers a recurring command manager for a specific bounded context. - /// - public static void RegisterRecurringCommandManager(this Container container, RecurringCommandManagerSettings settings) - where TContext : BoundedContext + private static void RegisterEventConsumer(this Container container, EventConsumerOptions options) { - Ensure.That(container, nameof(container)).IsNotNull(); - Ensure.That(settings, nameof(settings)).IsNotNull(); - container.RegisterInstance(settings); - container.RegisterSingleton, RecurringCommandManager>(); - container.Collection.Append>(Lifestyle.Singleton); + 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); } - /// - /// Registers loggers. - /// - public static void RegisterLoggers(this Container container, ILoggerFactory loggerFactory) + 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) { - container.RegisterInstance(loggerFactory); + 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, @@ -156,69 +267,62 @@ public static void RegisterLoggers(this Container container, ILoggerFactory logg container.RegisterSingleton(typeof(ILogger<>), typeof(Logger<>)); } - private static void RegisterCommandHandlers(this Container container, IEnumerable assemblies, Func predicate) + private static void RegisterMappersAndTranslators(this Container container, AppRegistrationOptions options) { - var options = new TypesToRegisterOptions { IncludeGenericTypeDefinitions = true }; - container.RegisterConditional(typeof(ISyncCommandHandler<,>), assemblies, predicate, options); - container.RegisterDecorator(typeof(ISyncCommandHandler<,>), typeof(SyncCommandHandlerWithLogging<,>)); - container.RegisterDecorator(typeof(ISyncCommandHandler<,>), typeof(ThreadScopedCommandHandler<,>), Lifestyle.Singleton); - container.RegisterConditional(typeof(IAsyncCommandHandler<,>), assemblies, predicate, options); - container.RegisterDecorator(typeof(IAsyncCommandHandler<,>), typeof(AsyncCommandHandlerWithLogging<,>)); - container.RegisterDecorator(typeof(IAsyncCommandHandler<,>), typeof(AsyncScopedCommandHandler<,>), Lifestyle.Singleton); - container.RegisterConditional(typeof(ISyncCommandHandler<>), assemblies, predicate); - container.RegisterDecorator(typeof(ISyncCommandHandler<>), typeof(SyncCommandHandlerWithLogging<>)); - container.RegisterDecorator(typeof(ISyncCommandHandler<>), typeof(ThreadScopedCommandHandler<>), Lifestyle.Singleton); - container.RegisterConditional(typeof(IAsyncCommandHandler<>), assemblies, predicate); - container.RegisterDecorator(typeof(IAsyncCommandHandler<>), typeof(AsyncCommandHandlerWithLogging<>)); - container.RegisterDecorator(typeof(IAsyncCommandHandler<>), typeof(AsyncScopedCommandHandler<>), Lifestyle.Singleton); + container.RegisterConditional(typeof(IObjectMapper<,>), options.AssembliesToScan, options.TypeFilter); + container.RegisterConditional(typeof(IObjectTranslator<,>), options.AssembliesToScan, options.TypeFilter); } - private static void RegisterQueryHandlers(this Container container, IEnumerable assemblies, Func predicate) + private static void RegisterQueryHandlers(this Container container, AppRegistrationOptions options) { - var options = new TypesToRegisterOptions { IncludeGenericTypeDefinitions = true }; - container.RegisterConditional(typeof(ISyncQueryHandler<,,>), assemblies, predicate, 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<,,>), assemblies, predicate, options); + 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<,>), assemblies, predicate); + 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<,>), assemblies, predicate); + 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 RegisterEventHandlers(this Container container, IEnumerable assemblies, Func predicate) + private static void RegisterRecurringCommandManager(this Container container, RecurringCommandManagerOptions options) { - container.RegisterConditional(typeof(ISyncEventHandler<,>), assemblies, predicate); - container.RegisterConditional(typeof(IAsyncEventHandler<,>), assemblies, predicate); - container.RegisterDecorator(typeof(ISyncEventHandler<,>), typeof(SyncEventHandlerWithLogging<,>)); - container.RegisterDecorator(typeof(IAsyncEventHandler<,>), typeof(AsyncEventHandlerWithLogging<,>)); + 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 RegisterMappersAndTranslators(this Container container, IEnumerable assemblies, Func predicate) + private static void RegisterRepositories(this Container container, AppRegistrationOptions options) { - container.RegisterConditional(typeof(IObjectMapper<,>), assemblies, predicate); - container.RegisterConditional(typeof(IObjectTranslator<,>), assemblies, predicate); + container.RegisterConditional(typeof(ISyncRepository<,>), options.AssembliesToScan, Lifestyle.Scoped, options.TypeFilter); + container.RegisterConditional(typeof(IAsyncRepository<,>), options.AssembliesToScan, Lifestyle.Scoped, options.TypeFilter); } - private static void RegisterRepositories(this Container container, IEnumerable assemblies, Func predicate) + private static void RegisterSerializers(this Container container, AppRegistrationOptions options) { - container.RegisterConditional(typeof(ISyncRepository<,>), assemblies, Lifestyle.Scoped, predicate); - container.RegisterConditional(typeof(IAsyncRepository<,>), assemblies, Lifestyle.Scoped, predicate); + foreach (var serializer in options.Serializers) + container.Collection.Append(serializer, Lifestyle.Singleton); } - private static void RegisterBoundedContexts(this Container container, IEnumerable assemblies, Func predicate) + private static void RegisterScheduleFactories(this Container container, CommandsRegistrationOptions options) { - var contextTypes = container.GetTypesToRegister(assemblies) - .Where(predicate); - foreach(var contextType in contextTypes) - container.RegisterSingleton(contextType, contextType); - container.Collection.Register(assemblies, Lifestyle.Singleton); + foreach (var scheduleFactory in options.SchedulesFactories) + container.Collection.Append(scheduleFactory, Lifestyle.Singleton); } #endregion Methods + } } diff --git a/Src/DDD.Core.SimpleInjector/KeyedServiceProvider.cs b/Src/DDD.Core.SimpleInjector/KeyedServiceProvider.cs deleted file mode 100644 index 0f9d4a6..0000000 --- a/Src/DDD.Core.SimpleInjector/KeyedServiceProvider.cs +++ /dev/null @@ -1,59 +0,0 @@ -using DDD.DependencyInjection; -using SimpleInjector; -using EnsureThat; -using System.Collections.Generic; -using System; - -namespace DDD.Core.Infrastructure.DependencyInjection -{ - public class KeyedServiceProvider : IKeyedServiceProvider - where TService : class - { - - #region Fields - - private readonly Container container; - private readonly Dictionary> producers; - - #endregion Fields - - #region Constructors - - public KeyedServiceProvider(Container container, IEqualityComparer keyComparer = null) - { - Ensure.That(container, nameof(container)).IsNotNull(); - this.container = container; - this.producers = new Dictionary>(keyComparer); - } - - #endregion Constructors - - #region Methods - - public TService GetService(TKey key) - { - if (this.producers.TryGetValue(key, out var producer)) - return producer.GetInstance(); - throw new InvalidOperationException($"No registration for key {key}."); - } - - public void Register(TKey key, Lifestyle lifestyle) - where TImplementation : class, TService - { - Ensure.That(lifestyle, nameof(lifestyle)).IsNotNull(); - var producer = lifestyle.CreateProducer(container); - this.producers.Add(key, producer); - } - - public void Register(TKey key, Func instanceCreator, Lifestyle lifestyle) - { - Ensure.That(instanceCreator, nameof(instanceCreator)).IsNotNull(); - Ensure.That(lifestyle, nameof(lifestyle)).IsNotNull(); - var producer = lifestyle.CreateProducer(instanceCreator, container); - this.producers.Add(key, producer); - } - - #endregion Methods - - } -} \ 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.Xunit/DbFixture.cs b/Src/DDD.Core.Xunit/DbFixture.cs index 9a96612..60a13e3 100644 --- a/Src/DDD.Core.Xunit/DbFixture.cs +++ b/Src/DDD.Core.Xunit/DbFixture.cs @@ -59,7 +59,8 @@ public int[] ExecuteScriptFromResources(string scriptName) public IDbConnectionProvider CreateConnectionProvider(bool pooling = true) { var connectionString = this.SetPooling(connectionSettings.ConnectionString, pooling); - return new LazyDbConnectionProvider(new TContext(), connectionSettings.ProviderName, connectionString); + var settings = new DbConnectionSettings(connectionSettings.ProviderName, connectionString); + return new LazyDbConnectionProvider(new TContext(), settings); } public DbConnection CreateConnection(bool pooling = true) diff --git a/Src/DDD.Core/Application/CommandException.cs b/Src/DDD.Core/Application/CommandException.cs index 643dc62..d820b27 100644 --- a/Src/DDD.Core/Application/CommandException.cs +++ b/Src/DDD.Core/Application/CommandException.cs @@ -43,10 +43,10 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont public override string ToString() { var s = $"{this.GetType()}: {this.Message} "; - s += $"{Environment.NewLine}Timestamp: {this.Timestamp}"; - s += $"{Environment.NewLine}IsTransient: {this.IsTransient}"; + 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) diff --git a/Src/DDD.Core/Application/CommandInvalidException.cs b/Src/DDD.Core/Application/CommandInvalidException.cs index 4ec8c23..3dba528 100644 --- a/Src/DDD.Core/Application/CommandInvalidException.cs +++ b/Src/DDD.Core/Application/CommandInvalidException.cs @@ -58,10 +58,10 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont public override string ToString() { var s = $"{this.GetType()}: {this.Message} "; - s += $"{Environment.NewLine}Timestamp: {this.Timestamp}"; - s += $"{Environment.NewLine}IsTransient: {this.IsTransient}"; + 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.Failures != null) { for (var i = 0; i < this.Failures.Length; i++) diff --git a/Src/DDD.Core/Application/CommandProcessor.cs b/Src/DDD.Core/Application/CommandProcessor.cs index a9316f0..071d927 100644 --- a/Src/DDD.Core/Application/CommandProcessor.cs +++ b/Src/DDD.Core/Application/CommandProcessor.cs @@ -17,6 +17,7 @@ public class CommandProcessor : ICommandProcessor #region Fields private readonly IServiceProvider serviceProvider; + private readonly CommandProcessorSettings settings; private static readonly ConcurrentDictionary contextualProcessors = new ConcurrentDictionary(); @@ -24,10 +25,12 @@ private static readonly ConcurrentDictionary(TCommand command, IValidationContext { Ensure.That(command, nameof(command)).IsNotNull(); var validator = this.serviceProvider.GetService>(); - if (validator == null) throw new InvalidOperationException($"The command validator for type {typeof(ISyncObjectValidator)} could not be found."); + 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); } @@ -79,7 +87,12 @@ public Task ValidateAsync(TCommand command, IValidat { Ensure.That(command, nameof(command)).IsNotNull(); var validator = this.serviceProvider.GetService>(); - if (validator == null) throw new InvalidOperationException($"The command validator for type {typeof(IAsyncObjectValidator)} could not be found."); + 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); } 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/EventConsumer.cs b/Src/DDD.Core/Application/EventConsumer.cs index fd73965..6336b48 100644 --- a/Src/DDD.Core/Application/EventConsumer.cs +++ b/Src/DDD.Core/Application/EventConsumer.cs @@ -1,4 +1,4 @@ -using EnsureThat; +using EnsureThat; using System; using System.Collections.Generic; using System.Linq; @@ -8,10 +8,10 @@ namespace DDD.Core.Application { - using DependencyInjection; using Serialization; using Domain; using Threading; + using DDD; public class EventConsumer : IEventConsumer, IDisposable where TContext : BoundedContext @@ -24,7 +24,7 @@ public class EventConsumer : IEventConsumer, IDisposable private readonly IQueryProcessor queryProcessor; private readonly IEventPublisher eventPublisher; private readonly IEnumerable boundedContexts; - private readonly IKeyedServiceProvider eventSerializers; + private readonly IEnumerable eventSerializers; private readonly ILogger logger; private readonly EventConsumerSettings settings; private long consumationCount; @@ -39,7 +39,7 @@ public EventConsumer(ICommandProcessor commandProcessor, IQueryProcessor queryProcessor, IEventPublisher eventPublisher, IEnumerable boundedContexts, - IKeyedServiceProvider eventSerializers, + IEnumerable eventSerializers, ILogger logger, EventConsumerSettings settings) { @@ -48,6 +48,7 @@ public EventConsumer(ICommandProcessor commandProcessor, 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(); @@ -58,13 +59,14 @@ public EventConsumer(ICommandProcessor commandProcessor, 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 => this.settings.Context; + public TContext Context { get; } public bool IsRunning { get; private set; } @@ -80,7 +82,7 @@ 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.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); @@ -149,7 +151,7 @@ private async Task ConsumeEventsAsync() 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(TimeSpan.FromSeconds(this.settings.ConsumationDelay), this.CancellationToken); + await Task.Delay(this.settings.ConsumationDelay, this.CancellationToken); } } catch(OperationCanceledException) @@ -351,7 +353,7 @@ 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.GetService(format); + var serializer = this.eventSerializers.First(s => s.Format == format); return (IEvent)serializer.DeserializeFromString(notifiedEvent.Body, type); } @@ -405,4 +407,4 @@ public StreamsInfo(IEnumerable streams, IEnumerable where TContext : BoundedContext { #region Constructors - public EventConsumerSettings(TContext context, - short consumationDelay, + public EventConsumerSettings(TimeSpan consumationDelay, long? consumationMax = null) { - Ensure.That(context, nameof(context)).IsNotNull(); - Ensure.That(consumationDelay, nameof(consumationDelay)).IsGte((short)0); + Ensure.That(consumationDelay, nameof(consumationDelay)).IsGte(TimeSpan.Zero); if (consumationMax != null) Ensure.That(consumationMax.Value, nameof(consumationMax)).IsGte(0); - this.Context = context; + this.ContextType = typeof(TContext); this.ConsumationDelay = consumationDelay; this.ConsumationMax = consumationMax; } @@ -30,23 +27,28 @@ public EventConsumerSettings(TContext context, #region Properties /// - /// Gets the delay in seconds between two successive consumations. + /// Gets the type of the associated context. /// - [DataMember(Order = 1)] - public short ConsumationDelay { get; } + public Type ContextType { get; } /// - /// Gets the maximum number of successive consumations. + /// Gets the delay between two successive consumations. /// - [DataMember(Order = 2)] - public long? ConsumationMax { get; } + public TimeSpan ConsumationDelay { get; } /// - /// Gets the associated context. + /// Gets the maximum number of successive consumations. /// - public TContext Context { get; } + 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/IRecurringScheduleFactory.cs b/Src/DDD.Core/Application/IRecurringScheduleFactory.cs index db0f09d..bd0ff38 100644 --- a/Src/DDD.Core/Application/IRecurringScheduleFactory.cs +++ b/Src/DDD.Core/Application/IRecurringScheduleFactory.cs @@ -8,6 +8,12 @@ namespace DDD.Core.Application public interface IRecurringScheduleFactory { + #region Properties + + RecurringExpressionFormat Format { get; } + + #endregion Properties + #region Methods /// @@ -18,6 +24,5 @@ public interface IRecurringScheduleFactory IRecurringSchedule Create(string recurringExpression); #endregion Methods - } } diff --git a/Src/DDD.Core/Application/QueryException.cs b/Src/DDD.Core/Application/QueryException.cs index 975c486..89b81a5 100644 --- a/Src/DDD.Core/Application/QueryException.cs +++ b/Src/DDD.Core/Application/QueryException.cs @@ -50,10 +50,10 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont public override string ToString() { var s = $"{this.GetType()}: {this.Message} "; - s += $"{Environment.NewLine}Timestamp: {this.Timestamp}"; - s += $"{Environment.NewLine}IsTransient: {this.IsTransient}"; + 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 index 97a7ff7..cdba158 100644 --- a/Src/DDD.Core/Application/QueryInvalidException.cs +++ b/Src/DDD.Core/Application/QueryInvalidException.cs @@ -58,10 +58,10 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont public override string ToString() { var s = $"{this.GetType()}: {this.Message} "; - s += $"{Environment.NewLine}Timestamp: {this.Timestamp}"; - s += $"{Environment.NewLine}IsTransient: {this.IsTransient}"; + 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.Failures != null) { for (var i = 0; i < this.Failures.Length; i++) diff --git a/Src/DDD.Core/Application/QueryProcessor.cs b/Src/DDD.Core/Application/QueryProcessor.cs index 1fdfccd..21c3ad0 100644 --- a/Src/DDD.Core/Application/QueryProcessor.cs +++ b/Src/DDD.Core/Application/QueryProcessor.cs @@ -1,6 +1,5 @@ using EnsureThat; using System; -using System.Threading; using System.Threading.Tasks; namespace DDD.Core.Application @@ -19,17 +18,19 @@ public class QueryProcessor : IQueryProcessor 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) { Ensure.That(serviceProvider, nameof(serviceProvider)).IsNotNull(); + Ensure.That(settings, nameof(settings)).IsNotNull(); this.serviceProvider = serviceProvider; + this.settings = settings; } #endregion Constructors @@ -75,7 +76,12 @@ public ValidationResult Validate(TQuery query, IValidationContext contex { Ensure.That(query, nameof(query)).IsNotNull(); var validator = this.serviceProvider.GetService>(); - if (validator == null) throw new InvalidOperationException($"The query validator for type {typeof(ISyncObjectValidator)} could not be found."); + 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); } @@ -83,7 +89,12 @@ public Task ValidateAsync(TQuery query, IValidationCon { Ensure.That(query, nameof(query)).IsNotNull(); var validator = this.serviceProvider.GetService>(); - if (validator == null) throw new InvalidOperationException($"The query validator for type {typeof(IAsyncObjectValidator)} could not be found."); + 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); } 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/RecurringCommandManager.cs b/Src/DDD.Core/Application/RecurringCommandManager.cs index 15d2a58..dfa84e4 100644 --- a/Src/DDD.Core/Application/RecurringCommandManager.cs +++ b/Src/DDD.Core/Application/RecurringCommandManager.cs @@ -1,5 +1,6 @@ -using EnsureThat; +using EnsureThat; using System; +using System.Linq; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -8,9 +9,9 @@ namespace DDD.Core.Application { using Domain; - using DependencyInjection; using Serialization; using Threading; + using DDD; public class RecurringCommandManager : IRecurringCommandManager, IDisposable where TContext : BoundedContext @@ -18,37 +19,40 @@ public class RecurringCommandManager : IRecurringCommandManager commandSerializers; - private readonly IRecurringScheduleFactory recurringScheduleFactory; + private readonly IEnumerable commandSerializers; private readonly ILogger logger; + private readonly IQueryProcessor queryProcessor; + private readonly IEnumerable recurringScheduleFactories; private readonly RecurringCommandManagerSettings settings; - private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); - private Task manageCommands; private bool disposed; + private Task manageCommands; #endregion Fields #region Constructors - public RecurringCommandManager(ICommandProcessor commandProcessor, + public RecurringCommandManager(TContext context, + ICommandProcessor commandProcessor, IQueryProcessor queryProcessor, - IKeyedServiceProvider commandSerializers, - IRecurringScheduleFactory recurringScheduleFactory, + 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(recurringScheduleFactory, nameof(recurringScheduleFactory)).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.recurringScheduleFactory = recurringScheduleFactory; + this.recurringScheduleFactories = recurringScheduleFactories; this.logger = logger; this.settings = settings; } @@ -57,7 +61,7 @@ public RecurringCommandManager(ICommandProcessor commandProcessor, #region Properties - public TContext Context => this.settings.Context; + public TContext Context { get; } public bool IsRunning { get; private set; } @@ -67,21 +71,29 @@ public RecurringCommandManager(ICommandProcessor commandProcessor, #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(); - this.ValidateRecurringExpression(recurringExpression); + var recurringScheduleFactory = this.GetScheduleFactory(this.settings.CurrentExpressionFormat); + ValidateRecurringExpression(recurringScheduleFactory, recurringExpression); await new SynchronizationContextRemover(); - var commandSerializer = this.commandSerializers.GetService(this.settings.CurrentSerializationFormat); + 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 = commandSerializer.SerializeToString(command), - BodyFormat = commandSerializer.Format.ToString().ToUpper(), - RecurringExpression = recurringExpression + 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)); } @@ -91,7 +103,7 @@ 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.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); @@ -118,13 +130,6 @@ public void Wait(TimeSpan? timeout = null) this.manageCommands.Wait(); } } - - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - protected virtual void Dispose(bool disposing) { if (!disposed) @@ -138,41 +143,46 @@ protected virtual void Dispose(bool disposing) } } - private async Task ManageCommandsAsync() + private static void ValidateRecurringExpression(IRecurringScheduleFactory recurringScheduleFactory, string recurringExpression) { 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); + recurringScheduleFactory.Create(recurringExpression); } - catch (Exception exception) - { - this.logger.LogCritical(default, exception, "An error occurred while managing recurring commands in the context '{Context}'.", this.Context.Name); - } - finally + catch (Exception ex) { - this.IsRunning = false; + 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) - results.Add((recurringCommand, this.recurringScheduleFactory.Create(recurringCommand.RecurringExpression))); + { + 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 @@ -217,41 +227,44 @@ private async Task ManageCommandAsync(RecurringCommand recurringCommand, IRecurr nextOccurrence = recurringSchedule.GetNextOccurrence(now); } } - catch(OperationCanceledException) + 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) + 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 ICommand DeserializeCommand(RecurringCommand recurringCommand) - { - var format = (SerializationFormat)Enum.Parse(typeof(SerializationFormat), recurringCommand.BodyFormat, ignoreCase: true); - var type = Type.GetType(recurringCommand.CommandType); - var serializer = this.commandSerializers.GetService(format); - return (ICommand)serializer.DeserializeFromString(recurringCommand.Body, type); - } - - private void ValidateRecurringExpression(string recurringExpression) + private async Task ManageCommandsAsync() { try { - this.recurringScheduleFactory.Create(recurringExpression); + 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 (Exception ex) + catch(OperationCanceledException) { - throw new ArgumentException( - "The recurring expression is invalid. Please see the inner exception for details.", - nameof(recurringExpression), - ex); + 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 } -} \ No newline at end of file +} diff --git a/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs b/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs index 97ab003..b57c3ac 100644 --- a/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs +++ b/Src/DDD.Core/Application/RecurringCommandManagerSettings.cs @@ -1,24 +1,22 @@ -using System.Runtime.Serialization; -using EnsureThat; +using System; namespace DDD.Core.Application { using Domain; using Serialization; - [DataContract()] public class RecurringCommandManagerSettings where TContext : BoundedContext { #region Constructors - public RecurringCommandManagerSettings(TContext context, - SerializationFormat currentSerializationFormat) + public RecurringCommandManagerSettings(SerializationFormat currentSerializationFormat, + RecurringExpressionFormat currentExpressionFormat) { - Ensure.That(context, nameof(context)).IsNotNull(); - this.Context= context; + this.ContextType= typeof(TContext); this.CurrentSerializationFormat = currentSerializationFormat; + this.CurrentExpressionFormat = currentExpressionFormat; } #endregion Constructors @@ -26,16 +24,27 @@ public RecurringCommandManagerSettings(TContext context, #region Properties /// - /// Gets the associated context. + /// Gets the type of the associated context. /// - public TContext Context { get; } + public Type ContextType { get; } /// /// Gets the current serialization format of the recurring commands. /// - [DataMember(Order = 1)] 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/Domain/DomainServiceException.cs b/Src/DDD.Core/Domain/DomainServiceException.cs index 4a64e0e..b1b5bb9 100644 --- a/Src/DDD.Core/Domain/DomainServiceException.cs +++ b/Src/DDD.Core/Domain/DomainServiceException.cs @@ -50,10 +50,10 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont public override string ToString() { var s = $"{this.GetType()}: {this.Message} "; - s += $"{Environment.NewLine}Timestamp: {this.Timestamp}"; - s += $"{Environment.NewLine}IsTransient: {this.IsTransient}"; + 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) diff --git a/Src/DDD.Core/Domain/DomainServiceInvalidException.cs b/Src/DDD.Core/Domain/DomainServiceInvalidException.cs index 2220174..349f2aa 100644 --- a/Src/DDD.Core/Domain/DomainServiceInvalidException.cs +++ b/Src/DDD.Core/Domain/DomainServiceInvalidException.cs @@ -58,10 +58,10 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont public override string ToString() { var s = $"{this.GetType()}: {this.Message} "; - s += $"{Environment.NewLine}Timestamp: {this.Timestamp}"; - s += $"{Environment.NewLine}IsTransient: {this.IsTransient}"; + 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.Failures != null) { for (var i = 0; i < this.Failures.Length; i++) diff --git a/Src/DDD.Core/Domain/RepositoryException.cs b/Src/DDD.Core/Domain/RepositoryException.cs index 81f5691..62409eb 100644 --- a/Src/DDD.Core/Domain/RepositoryException.cs +++ b/Src/DDD.Core/Domain/RepositoryException.cs @@ -50,10 +50,10 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont public override string ToString() { var s = $"{this.GetType()}: {this.Message} "; - s += $"{Environment.NewLine}Timestamp: {this.Timestamp}"; - s += $"{Environment.NewLine}IsTransient: {this.IsTransient}"; + s += $"{Environment.NewLine}{nameof(Timestamp)}: {this.Timestamp}"; + s += $"{Environment.NewLine}{nameof(IsTransient)}: {this.IsTransient}"; if (this.EntityType != null) - s += $"{Environment.NewLine}EntityType: {this.EntityType}"; + s += $"{Environment.NewLine}{nameof(EntityType)}: {this.EntityType}"; if (this.InnerException != null) s += $" ---> {this.InnerException}"; if (this.StackTrace != null) 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/LazyDbConnectionProvider.cs b/Src/DDD.Core/Infrastructure/Data/LazyDbConnectionProvider.cs index baf1454..a0da117 100644 --- a/Src/DDD.Core/Infrastructure/Data/LazyDbConnectionProvider.cs +++ b/Src/DDD.Core/Infrastructure/Data/LazyDbConnectionProvider.cs @@ -13,23 +13,20 @@ public class LazyDbConnectionProvider #region Fields - private readonly string connectionString; + private readonly DbConnectionSettings settings; private readonly Lazy lazyConnection; - private readonly string providerName; private bool isDisposed; #endregion Fields #region Constructors - public LazyDbConnectionProvider(TContext context, string providerName, string connectionString) + public LazyDbConnectionProvider(TContext context, DbConnectionSettings settings) { Ensure.That(context, nameof(context)).IsNotNull(); - Ensure.That(providerName, nameof(providerName)).IsNotNullOrWhiteSpace(); - Ensure.That(connectionString, nameof(connectionString)).IsNotNullOrWhiteSpace(); + Ensure.That(settings, nameof(settings)).IsNotNull(); this.Context = context; - this.providerName = providerName; - this.connectionString = connectionString; + this.settings = settings; this.lazyConnection = new Lazy(() => this.CreateConnection()); } @@ -68,9 +65,9 @@ protected virtual void Dispose(bool isDisposing) private DbConnection CreateConnection() { - var providerFactory = DbProviderFactories.GetFactory(this.providerName); + var providerFactory = DbProviderFactories.GetFactory(this.settings.ProviderName); var connection = providerFactory.CreateConnection(); - connection.ConnectionString = this.connectionString; + connection.ConnectionString = this.settings.ConnectionString; return connection; } diff --git a/Src/DDD.Core/Infrastructure/DependencyInjection/AppRegistrationOptions.cs b/Src/DDD.Core/Infrastructure/DependencyInjection/AppRegistrationOptions.cs new file mode 100644 index 0000000..ec9e44e --- /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 : IObjectBuilder + { + + #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; + } + + AppRegistrationOptions IObjectBuilder.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..ad08359 --- /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 : IObjectBuilder + { + + #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; + } + CommandsRegistrationOptions IObjectBuilder.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..87a4734 --- /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 : IObjectBuilder + 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; + } + + DbConnectionOptions IObjectBuilder.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..0f003f7 --- /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 : IObjectBuilder + 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; + } + + EventConsumerOptions IObjectBuilder.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..f96afa5 --- /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 : IObjectBuilder + { + + #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; + } + + EventsRegistrationOptions IObjectBuilder.Build() => this.options; + + #endregion Methods + + } + + #endregion Classes + + } +} diff --git a/Src/DDD.Core/Infrastructure/DependencyInjection/MappingRegistrationOptions.cs b/Src/DDD.Core/Infrastructure/DependencyInjection/MappingRegistrationOptions.cs new file mode 100644 index 0000000..3c32dfa --- /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 : IObjectBuilder + { + + #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; + } + + MappingRegistrationOptions IObjectBuilder.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..1a5e30d --- /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 : IObjectBuilder + { + + #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); + } + + QueriesRegistrationOptions IObjectBuilder.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..03610a9 --- /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 : IObjectBuilder + 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; + } + + RecurringCommandManagerOptions IObjectBuilder.Build() => this.options; + + #endregion Methods + + } + + #endregion Classes + + } +} diff --git a/Test/DDD.Common.UnitTests/DDD.Common.UnitTests.csproj b/Test/DDD.Common.UnitTests/DDD.Common.UnitTests.csproj index eb5e50e..ea8fd5d 100644 --- a/Test/DDD.Common.UnitTests/DDD.Common.UnitTests.csproj +++ b/Test/DDD.Common.UnitTests/DDD.Common.UnitTests.csproj @@ -17,7 +17,7 @@ 6.9.0 - + 4.5.0 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 8d061ce..5c5de54 100644 --- a/Test/DDD.Core.Abstractions.UnitTests/DDD.Core.Abstractions.UnitTests.csproj +++ b/Test/DDD.Core.Abstractions.UnitTests/DDD.Core.Abstractions.UnitTests.csproj @@ -16,7 +16,7 @@ 6.9.0 - + diff --git a/Test/DDD.Core.Abstractions.UnitTests/Mapping/IMappingProcessorExtensionsTests.cs b/Test/DDD.Core.Abstractions.UnitTests/Mapping/IMappingProcessorExtensionsTests.cs index 61a0410..06ae1f3 100644 --- a/Test/DDD.Core.Abstractions.UnitTests/Mapping/IMappingProcessorExtensionsTests.cs +++ b/Test/DDD.Core.Abstractions.UnitTests/Mapping/IMappingProcessorExtensionsTests.cs @@ -28,7 +28,7 @@ public IMappingProcessorExtensionsTests() .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 diff --git a/Test/DDD.Core.Abstractions.UnitTests/Mapping/MappingProcessorTests.cs b/Test/DDD.Core.Abstractions.UnitTests/Mapping/MappingProcessorTests.cs index 98e3b6c..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 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/DDD.Core.Dapper.IntegrationTests.csproj b/Test/DDD.Core.Dapper.IntegrationTests/DDD.Core.Dapper.IntegrationTests.csproj index 79ba4f1..28a1945 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/DDD.Core.Dapper.IntegrationTests.csproj +++ b/Test/DDD.Core.Dapper.IntegrationTests/DDD.Core.Dapper.IntegrationTests.csproj @@ -7,7 +7,7 @@ - + diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedRecurringCommandUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedRecurringCommandUpdaterTests.cs index c031738..d075c2b 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedRecurringCommandUpdaterTests.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/FailedRecurringCommandUpdaterTests.cs @@ -83,7 +83,7 @@ private RecurringCommandDetail UpdatedCommand() using (var connection = Fixture.CreateConnection()) { connection.Open(); - var sql = "SELECT CommandId, CommandType, Body, BodyFormat, RecurringExpression, LastExecutionTime, CASE LastExecutionStatus WHEN 'F' THEN 'Failed' WHEN 'S' THEN 'Successful' END LastExecutionStatus, LastExceptionInfo FROM Command WHERE CommandId = @CommandId"; + 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)); } @@ -106,6 +106,7 @@ private static RecurringCommandDetail ExpectedCommand() 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." diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleRecurringCommandsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleRecurringCommandsFinderTests.cs index a4818d2..6a940c0 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleRecurringCommandsFinderTests.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/OracleRecurringCommandsFinderTests.cs @@ -27,7 +27,8 @@ protected override IEnumerable ExpectedResults() CommandType = "DDD.Core.Application.FakeCommand1, DDD.Core.Messages", Body = "{\"Property1\":\"dummy\",\"Property2\":10}", BodyFormat = "JSON", - RecurringExpression = "* * * * *" + RecurringExpression = "* * * * *", + RecurringExpressionFormat = "CRON" }; yield return new RecurringCommand { @@ -35,7 +36,8 @@ protected override IEnumerable ExpectedResults() 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 * *" + RecurringExpression = "0 0 1 * *", + RecurringExpressionFormat = "CRON" }; } diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/RecurringCommandRegisterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/RecurringCommandRegisterTests.cs index 14832b0..0d61814 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/RecurringCommandRegisterTests.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/RecurringCommandRegisterTests.cs @@ -75,7 +75,8 @@ private static RegisterRecurringCommand CreateCommand() CommandType = "DDD.Core.Application.FakeCommand1, DDD.Core.Messages", Body = "{\"Property1\":\"dummy\",\"Property2\":10}", BodyFormat = "JSON", - RecurringExpression = "* * * * *" + RecurringExpression = "* * * * *", + RecurringExpressionFormat = "CRON" }; } @@ -84,7 +85,7 @@ private IEnumerable RegistredCommands() using (var connection = Fixture.CreateConnection()) { connection.Open(); - return connection.Query("SELECT CommandId, CommandType, Body, BodyFormat, RecurringExpression FROM Command"); + return connection.Query("SELECT CommandId, CommandType, Body, BodyFormat, RecurringExpression, RecurringExpressionFormat FROM Command"); } } @@ -96,7 +97,8 @@ private static IEnumerable ExpectedCommands() CommandType = "DDD.Core.Application.FakeCommand1, DDD.Core.Messages", Body = "{\"Property1\":\"dummy\",\"Property2\":10}", BodyFormat = "JSON", - RecurringExpression = "* * * * *" + RecurringExpression = "* * * * *", + RecurringExpressionFormat = "CRON" }; } 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 index 997d6fd..c65d3d9 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/FillSchema.sql +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/Scripts/Oracle/FillSchema.sql @@ -30,6 +30,7 @@ CREATE TABLE TEST.COMMAND 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, 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 index 1909b94eb140e5fcfdbf1b9861622b0ec02aa80a..93f9d691f0f9e7732f1a0f79b9f0991795279ccd 100644 GIT binary patch delta 82 zcmaFH{f%eBpNW^$xZN218G;ym85|ixCd)F)Zx&-*#mK47;0zS>XYiZ6o#`nyRoj@k Jm{3&d0|3iu6*m9? delta 28 icmeyy^Nf4KpUp;$M;Iq_FyEPM#JFhl9+oB$M-2e8mkP%K 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 index 6b3db7b32fb5070541f025ab439ffd32c517a76d..34e5adc16d3005b9c8cd988bf3df09a399815eff 100644 GIT binary patch delta 82 zcmaFH{f%eBpNW^$xZN218G;ym85|ixCd)F)Zx&-*#mK47;0zS>XYiZ6o#`nyRoj@k Jm{3&d0|3iu6*m9? delta 28 icmeyy^Nf4KpUp;$M;Iq_FyEPM#MrZW8Osb1M-2e7j|#B> 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 index 4c51799a37a220b4c6ef5ab804cd2e23b88c12f4..0f44de7362273dbc9ac9c74b0272859786ce3829 100644 GIT binary patch delta 85 zcmX@cdyQ|xpNW^$xZN218G;ym85|ixCd)F)Zx&-*!N~2$pw8e76!vHE+q|9WC?ht7 Q(^$BeIN{1BPiNBy0Oc?h4*&oF delta 29 lcmcb{cZ_$!pUp;$hZr}rG4ErXY{b~Jxs7!O<75MNH2|_{3V{Fs 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 index e16fa3556c88ddab1967e0da61fcdbd277204d07..2b70632eadd05daf9c8601bf14be04490797f88f 100644 GIT binary patch delta 28 kcmX?BzNuovE!oK*Ojst@$SO@Xl69MGASbo?i|i#?0KNbV?f?J) delta 16 Ycmdl~ajbm9E!oL)lD8;LH%j;LqT<`6ClEOy&!-852Tg@<%2^09Z;A A$p8QV delta 29 kcmaFGdx>X*3ghG|mT8;6Fgh?!c4Pjvxr)V%3CPd@0Iov{egFUf 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 index a5b8c2251ce37937b5e43d71d1d18c08c506d104..83cbb3371d514890c3b53f4ad3c9f7bee4ee4835 100644 GIT binary patch delta 58 zcmcb_^NM$a3gg6YY7-xcY*t~s#>lD8;LH%j;LqT<`6ClEOy&!-852Tg@<%2^09Z;A A$p8QV delta 29 kcmaFGdx>X*3ghG|mT8;6Fgh?!c4Pjvxr)V%3CPd@0Iov{egFUf 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 index 7ce1befb00e45993c8d9b5eccb08d5fb567217c4..192da130267d5814620a4be52036bff84084eaec 100644 GIT binary patch delta 72 zcmbQjw}yX%3ghGs7PX0wL?&-znYMWsqa7o+AA>rBGeZ!AKZD=qk4#5l3N~+JF=OI} QOEIWV{>Uad`5&7Y07?cG`2YX_ delta 38 rcmZ3(KZS3D3ghGsMybsejJp^&yD;wq^Cnj?)@;7Sx`1(V3%dpY0uT*2 diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerRecurringCommandsFinderTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerRecurringCommandsFinderTests.cs index a42debe..ce92573 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerRecurringCommandsFinderTests.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SqlServerRecurringCommandsFinderTests.cs @@ -27,7 +27,8 @@ protected override IEnumerable ExpectedResults() CommandType = "DDD.Core.Application.FakeCommand1, DDD.Core.Messages", Body = "{\"Property1\":\"dummy\",\"Property2\":10}", BodyFormat = "JSON", - RecurringExpression = "* * * * *" + RecurringExpression = "* * * * *", + RecurringExpressionFormat = "CRON" }; yield return new RecurringCommand { @@ -35,7 +36,8 @@ protected override IEnumerable ExpectedResults() 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 * *" + RecurringExpression = "0 0 1 * *", + RecurringExpressionFormat = "CRON" }; } diff --git a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SuccessfulRecurringCommandUpdaterTests.cs b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SuccessfulRecurringCommandUpdaterTests.cs index 1d1cdb8..c250017 100644 --- a/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SuccessfulRecurringCommandUpdaterTests.cs +++ b/Test/DDD.Core.Dapper.IntegrationTests/Infrastructure/Data/SuccessfulRecurringCommandUpdaterTests.cs @@ -82,7 +82,7 @@ private RecurringCommandDetail UpdatedCommand() using (var connection = Fixture.CreateConnection()) { connection.Open(); - var sql = "SELECT CommandId, CommandType, Body, BodyFormat, RecurringExpression, LastExecutionTime, CASE LastExecutionStatus WHEN 'F' THEN 'Successful' WHEN 'S' THEN 'Successful' END LastExecutionStatus, LastExceptionInfo FROM Command WHERE CommandId = @CommandId"; + 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)); } @@ -105,6 +105,7 @@ private static RecurringCommandDetail ExpectedCommand() Body = "{\"Property1\":\"dummy\",\"Property2\":10}", BodyFormat = "JSON", RecurringExpression = "* * * * *", + RecurringExpressionFormat = "CRON", LastExecutionTime = new DateTime(2022, 2, 1), LastExecutionStatus = CommandExecutionStatus.Successful, LastExceptionInfo = null diff --git a/Test/DDD.Core.Newtonsoft.UnitTests/DDD.Core.Newtonsoft.UnitTests.csproj b/Test/DDD.Core.Newtonsoft.UnitTests/DDD.Core.Newtonsoft.UnitTests.csproj index 3f442f3..ec20d98 100644 --- a/Test/DDD.Core.Newtonsoft.UnitTests/DDD.Core.Newtonsoft.UnitTests.csproj +++ b/Test/DDD.Core.Newtonsoft.UnitTests/DDD.Core.Newtonsoft.UnitTests.csproj @@ -5,7 +5,7 @@ - + diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/ContainerExtensionsTests.cs b/Test/DDD.Core.SimpleInjector.UnitTests/ContainerExtensionsTests.cs new file mode 100644 index 0000000..74b839f --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/ContainerExtensionsTests.cs @@ -0,0 +1,674 @@ +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/DDD.Core.SimpleInjector.UnitTests.csproj b/Test/DDD.Core.SimpleInjector.UnitTests/DDD.Core.SimpleInjector.UnitTests.csproj new file mode 100644 index 0000000..2f8eacc --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/DDD.Core.SimpleInjector.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.UnitTests/Decorated.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Decorated.cs new file mode 100644 index 0000000..6ee1a71 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/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/DummyContext.cs b/Test/DDD.Core.SimpleInjector.UnitTests/DummyContext.cs new file mode 100644 index 0000000..475b994 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/DummyContext.cs @@ -0,0 +1,15 @@ +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Domain; + + public class DummyContext : BoundedContext + { + #region Constructors + + public DummyContext() : base("DUM", "Dummy") + { + } + + #endregion Constructors + } +} diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/FakeCommand.cs b/Test/DDD.Core.SimpleInjector.UnitTests/FakeCommand.cs new file mode 100644 index 0000000..4693dba --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/FakeCommand.cs @@ -0,0 +1,8 @@ +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Application; + + public class FakeCommand : ICommand + { + } +} \ No newline at end of file diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/FakeCommandHandler.cs b/Test/DDD.Core.SimpleInjector.UnitTests/FakeCommandHandler.cs new file mode 100644 index 0000000..fdc155c --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/FakeCommandHandler.cs @@ -0,0 +1,21 @@ +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Application; + using System.Threading.Tasks; + + 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/FakeContext.cs b/Test/DDD.Core.SimpleInjector.UnitTests/FakeContext.cs new file mode 100644 index 0000000..e20fb35 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/FakeContext.cs @@ -0,0 +1,15 @@ +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Domain; + + public class FakeContext : BoundedContext + { + #region Constructors + + public FakeContext() : base("FK", "Fake") + { + } + + #endregion Constructors + } +} diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/FakeEvent.cs b/Test/DDD.Core.SimpleInjector.UnitTests/FakeEvent.cs new file mode 100644 index 0000000..13d4014 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/FakeEvent.cs @@ -0,0 +1,17 @@ +using System; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Domain; + + public class FakeEvent : IEvent + { + + #region Properties + + public DateTime OccurredOn { get; } + + #endregion Properties + + } +} diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/FakeEventHandler.cs b/Test/DDD.Core.SimpleInjector.UnitTests/FakeEventHandler.cs new file mode 100644 index 0000000..b76711c --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/FakeEventHandler.cs @@ -0,0 +1,49 @@ +using System; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Application; + using DDD.Core.Domain; + using System.Threading.Tasks; + + public class FakeEventHandler + : ISyncEventHandler, + IAsyncEventHandler + { + + #region Properties + + public FakeContext Context { get; } = new FakeContext(); + + public Type EventType => typeof(FakeEvent); + + BoundedContext ISyncEventHandler.Context => this.Context; + + BoundedContext IAsyncEventHandler.Context => this.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/FakeMapper.cs b/Test/DDD.Core.SimpleInjector.UnitTests/FakeMapper.cs new file mode 100644 index 0000000..7e40ede --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/FakeMapper.cs @@ -0,0 +1,15 @@ +namespace DDD.Core.Infrastructure.DependencyInjection +{ + 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/FakeQuery.cs b/Test/DDD.Core.SimpleInjector.UnitTests/FakeQuery.cs new file mode 100644 index 0000000..6d9e3a6 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/FakeQuery.cs @@ -0,0 +1,8 @@ +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Application; + + public class FakeQuery : IQuery + { + } +} diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/FakeQueryHandler.cs b/Test/DDD.Core.SimpleInjector.UnitTests/FakeQueryHandler.cs new file mode 100644 index 0000000..74c5be0 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/FakeQueryHandler.cs @@ -0,0 +1,22 @@ +namespace DDD.Core.Infrastructure.DependencyInjection +{ + using Application; + using System.Threading.Tasks; + + 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/FakeTranslator.cs b/Test/DDD.Core.SimpleInjector.UnitTests/FakeTranslator.cs new file mode 100644 index 0000000..b6c4c15 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/FakeTranslator.cs @@ -0,0 +1,32 @@ +using System; + +namespace DDD.Core.Infrastructure.DependencyInjection +{ + 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/FirstDecorator.cs b/Test/DDD.Core.SimpleInjector.UnitTests/FirstDecorator.cs new file mode 100644 index 0000000..72fd617 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/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/FirstDelegatingDecorator.cs b/Test/DDD.Core.SimpleInjector.UnitTests/FirstDelegatingDecorator.cs new file mode 100644 index 0000000..a78b562 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/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/IDoSomething.cs b/Test/DDD.Core.SimpleInjector.UnitTests/IDoSomething.cs new file mode 100644 index 0000000..fa9e2f2 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/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/SecondDecorator.cs b/Test/DDD.Core.SimpleInjector.UnitTests/SecondDecorator.cs new file mode 100644 index 0000000..0388fb2 --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/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/SecondDelegatingDecorator.cs b/Test/DDD.Core.SimpleInjector.UnitTests/SecondDelegatingDecorator.cs new file mode 100644 index 0000000..891b9af --- /dev/null +++ b/Test/DDD.Core.SimpleInjector.UnitTests/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 ed2120c..5808c8b 100644 --- a/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs +++ b/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs @@ -35,7 +35,7 @@ public CommandProcessorTests() .Returns(this.validatorOfCommand1); serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncObjectValidator)))) .Returns(this.validatorOfCommand2); - processor = new CommandProcessor(serviceProvider); + processor = new CommandProcessor(serviceProvider, new CommandProcessorSettings()); } #endregion Constructors diff --git a/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs b/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs index b41bef6..a995f46 100644 --- a/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs +++ b/Test/DDD.Core.UnitTests/Application/EventConsumerTests.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using NSubstitute; using NSubstitute.Core; using System.Threading.Tasks; @@ -10,7 +10,6 @@ namespace DDD.Core.Application { - using DependencyInjection; using Serialization; using Domain; using Infrastructure.Testing; @@ -481,24 +480,20 @@ private static IEventPublisher FakeEventPublisherThrowingException( return eventPublisher; } - private static IKeyedServiceProvider FakeEventSerializers() + private static IEnumerable FakeEventSerializers() { var jsonSerializer = Substitute.For(); jsonSerializer.Encoding.Returns(JsonSerializationOptions.Encoding); jsonSerializer.Deserialize(Arg.Any(), Arg.Any()).Returns(Substitute.For()); - var provider = Substitute.For>(); - provider.GetService(Arg.Is(f => f == SerializationFormat.Json)).Returns(jsonSerializer); - return provider; + yield return jsonSerializer; } - private static IKeyedServiceProvider FakeEventSerializersThrowingException(Exception exception) + 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); - var provider = Substitute.For>(); - provider.GetService(Arg.Is(f => f == SerializationFormat.Json)).Returns(jsonSerializer); - return provider; + yield return jsonSerializer; } private static Exception FakeException() => new Exception("Fake exception."); @@ -585,7 +580,7 @@ private static IQueryProcessor FakeQueryProcessorThrowingExceptionWhenReadingFai return queryProcessor; } - private static EventConsumerSettings FakeSettings() => new EventConsumerSettings(new FakeContext(), 1, 1); + private static EventConsumerSettings FakeSettings() => new EventConsumerSettings(TimeSpan.FromSeconds(1), 1); private static IEnumerable FakeBoundedContexts() { @@ -605,4 +600,4 @@ private static bool IsExpectedLogCall(ICall call, LogLevel level, Exception exce #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 9d2c5d8..76714d6 100644 --- a/Test/DDD.Core.UnitTests/Application/QueryProcessorTests.cs +++ b/Test/DDD.Core.UnitTests/Application/QueryProcessorTests.cs @@ -34,7 +34,7 @@ public QueryProcessorTests() .Returns(this.validatorOfQuery1); serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncObjectValidator)))) .Returns(this.validatorOfQuery2); - processor = new QueryProcessor(serviceProvider); + processor = new QueryProcessor(serviceProvider, new QueryProcessorSettings()); } #endregion Constructors diff --git a/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs b/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs index a0b0e7d..4c838b1 100644 --- a/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs +++ b/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs @@ -1,15 +1,15 @@ -using Microsoft.Extensions.Logging; +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 DependencyInjection; using Serialization; using Infrastructure.Testing; using Domain; @@ -37,13 +37,14 @@ public async Task RegisterAsync_WhenRecurringExpressionIsInvalid_ThrowsArgumentE var command = FakeCommand1(); var recurringExpression = InvalidRecurringExpression(); var recurringCommand = FakeCommand1ForSingleExecution(); + var context = new FakeContext(); var commandProcessor = FakeCommandProcessor(); var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); var commandSerializers = FakeCommandSerializers(); - var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var recurringScheduleFactories = FakeRecurringScheduleFactories(); var logger = FakeLogger(); var settings = FakeSettings(); - manager = new RecurringCommandManager(commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactories, logger, settings); // Act Func registerAsync = async () => await manager.RegisterAsync(command, recurringExpression); // Assert @@ -55,13 +56,14 @@ 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 = FakeRecurringScheduleFactory(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); var logger = FakeLogger(); var settings = FakeSettings(); - manager = new RecurringCommandManager(commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); // Act manager.Start(); // Assert @@ -74,13 +76,14 @@ public void StartAndWait_WhenExceptionInDeserializingRecurringCommand_LogsExcept // Arrange var exception = FakeException(); var recurringCommand = FakeCommand1ForSingleExecution(); + var context = new FakeContext(); var commandProcessor = FakeCommandProcessor(); var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); var commandSerializers = FakeCommandSerializersThrowingException(exception); - var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); var logger = FakeLogger(); var settings = FakeSettings(); - manager = new RecurringCommandManager(commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); // Act manager.Start(); manager.Wait(TimeSpan.FromSeconds(5)); @@ -93,13 +96,14 @@ 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 = FakeRecurringScheduleFactory(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); var logger = FakeLogger(); var settings = FakeSettings(); - manager = new RecurringCommandManager(commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); // Act manager.Start(); manager.Wait(TimeSpan.FromSeconds(5)); @@ -112,13 +116,14 @@ public void StartAndWait_WhenExceptionInFindingRecurringCommands_SetsIsRunningTo { // Arrange var exception = FakeException(); + var context = new FakeContext(); var commandProcessor = FakeCommandProcessor(); var queryProcessor = FakeQueryProcessorThrowingExceptionWhenFindingRecurringCommands(exception); var commandSerializers = FakeCommandSerializers(); - var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); var logger = FakeLogger(); var settings = FakeSettings(); - manager = new RecurringCommandManager(commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); // Act manager.Start(); manager.Wait(TimeSpan.FromSeconds(5)); @@ -132,13 +137,14 @@ public async Task StartAndWait_WhenExceptionInMarkingRecurringCommandAsFailed_Do // Arrange var exception = FakeException(); var recurringCommand = FakeCommand1ForRecurringExecution(); + var context = new FakeContext(); var commandProcessor = FakeCommandProcessorThrowingExceptionWhenMarkingRecurringCommandAsFailed(exception); var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); var commandSerializers = FakeCommandSerializers(); - var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); var logger = FakeLogger(); var settings = FakeSettings(); - manager = new RecurringCommandManager(commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); // Act manager.Start(); manager.Wait(TimeSpan.FromSeconds(5)); @@ -153,13 +159,14 @@ public void StartAndWait_WhenExceptionInMarkingRecurringCommandAsFailed_LogsExce // Arrange var exception = FakeException(); var recurringCommand = FakeCommand1ForSingleExecution(); + var context = new FakeContext(); var commandProcessor = FakeCommandProcessorThrowingExceptionWhenMarkingRecurringCommandAsFailed(exception); var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); var commandSerializers = FakeCommandSerializers(); - var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); var logger = FakeLogger(); var settings = FakeSettings(); - manager = new RecurringCommandManager(commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); // Act manager.Start(); manager.Wait(TimeSpan.FromSeconds(5)); @@ -173,13 +180,14 @@ public async Task StartAndWait_WhenExceptionInMarkingRecurringCommandAsSuccessfu // Arrange var exception = FakeException(); var recurringCommand = FakeCommand1ForRecurringExecution(); + var context = new FakeContext(); var commandProcessor = FakeCommandProcessorThrowingExceptionWhenMarkingRecurringCommandAsSuccessful(exception); var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); var commandSerializers = FakeCommandSerializers(); - var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); var logger = FakeLogger(); var settings = FakeSettings(); - manager = new RecurringCommandManager(commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); // Act manager.Start(); manager.Wait(TimeSpan.FromSeconds(5)); @@ -194,13 +202,14 @@ public void StartAndWait_WhenExceptionInMarkingRecurringCommandAsSuccessful_Logs // Arrange var exception = FakeException(); var recurringCommand = FakeCommand1ForSingleExecution(); + var context = new FakeContext(); var commandProcessor = FakeCommandProcessorThrowingExceptionWhenMarkingRecurringCommandAsSuccessful(exception); var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); var commandSerializers = FakeCommandSerializers(); - var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); var logger = FakeLogger(); var settings = FakeSettings(); - manager = new RecurringCommandManager(commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); // Act manager.Start(); manager.Wait(TimeSpan.FromSeconds(5)); @@ -213,13 +222,14 @@ 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 = FakeRecurringScheduleFactory(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); var logger = FakeLogger(); var settings = FakeSettings(); - manager = new RecurringCommandManager(commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); // Act manager.Start(); manager.Wait(TimeSpan.FromSeconds(5)); @@ -232,19 +242,19 @@ public async Task StartAndWait_WhenExceptionInProcessingRecurringCommand_MarksRe // Arrange var exception = FakeException(); var recurringCommand = FakeCommand1ForSingleExecution(); + var context = new FakeContext(); var commandProcessor = FakeCommandProcessorThrowingExceptionWhenProcessingRecurringCommand(exception); var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); var commandSerializers = FakeCommandSerializers(); - var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); var logger = FakeLogger(); var settings = FakeSettings(); - var fakeContext = new FakeContext(); - manager = new RecurringCommandManager(commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); // Act manager.Start(); manager.Wait(TimeSpan.FromSeconds(5)); // Assert - await commandProcessor.InGeneric(fakeContext) + await commandProcessor.InGeneric(context) .Received(1) .ProcessAsync(Arg.Is(c => c.CommandId == recurringCommand.CommandId && c.ExceptionInfo == exception.ToString()), Arg.Any()); } @@ -255,13 +265,14 @@ public async Task StartAndWait_WhenExceptionInProcessingRecurringCommand_Retries // Arrange var exception = FakeException(); var recurringCommand = FakeCommand1ForDoubleExecution(); + var context = new FakeContext(); var commandProcessor = FakeCommandProcessorThrowingExceptionWhenProcessingRecurringCommand(exception); var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); var commandSerializers = FakeCommandSerializers(); - var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); var logger = FakeLogger(); var settings = FakeSettings(); - manager = new RecurringCommandManager(commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); // Act manager.Start(); manager.Wait(TimeSpan.FromSeconds(5)); @@ -277,13 +288,14 @@ public async Task StartAndWait_WhenExceptionInProcessingSpecificRecurringCommand 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 = FakeRecurringScheduleFactory(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); var logger = FakeLogger(); var settings = FakeSettings(); - manager = new RecurringCommandManager(commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); // Act manager.Start(); manager.Wait(TimeSpan.FromSeconds(5)); @@ -298,19 +310,19 @@ public async Task StartAndWait_WhenRecurringCommandSuccessfullyProcessed_MarksRe { // Arrange var recurringCommand = FakeCommand1ForSingleExecution(); + var context = new FakeContext(); var commandProcessor = FakeCommandProcessor(); var queryProcessor = FakeQueryProcessorFindingRecurringCommands(recurringCommand); var commandSerializers = FakeCommandSerializers(); - var recurringScheduleFactory = FakeRecurringScheduleFactory(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); var logger = FakeLogger(); var settings = FakeSettings(); - var fakeContext = new FakeContext(); - manager = new RecurringCommandManager(commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); // Act manager.Start(); manager.Wait(TimeSpan.FromSeconds(5)); // Assert - await commandProcessor.InGeneric(fakeContext) + await commandProcessor.InGeneric(context) .Received(1) .ProcessAsync(Arg.Is(c => c.CommandId == recurringCommand.CommandId), Arg.Any()); } @@ -320,13 +332,14 @@ 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 = FakeRecurringScheduleFactory(); + var recurringScheduleFactory = FakeRecurringScheduleFactories(); var logger = FakeLogger(); var settings = FakeSettings(); - manager = new RecurringCommandManager(commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); + manager = new RecurringCommandManager(context, commandProcessor, queryProcessor, commandSerializers, recurringScheduleFactory, logger, settings); manager.Start(); // Act manager.Stop(); @@ -350,7 +363,8 @@ private static RecurringCommand FakeCommand1ForDoubleExecution() CommandType = "DDD.Core.Application.FakeCommand1, DDD.Core.UnitTests", Body = "{\"Property1\":\"dummy\",\"Property2\":10}", BodyFormat = "JSON", - RecurringExpression = RecurringExpressionForDoubleExecution() + RecurringExpression = RecurringExpressionForDoubleExecution(), + RecurringExpressionFormat = "CRON" }; } @@ -362,7 +376,8 @@ private static RecurringCommand FakeCommand1ForRecurringExecution() CommandType = "DDD.Core.Application.FakeCommand1, DDD.Core.UnitTests", Body = "{\"Property1\":\"dummy\",\"Property2\":10}", BodyFormat = "JSON", - RecurringExpression = RecurringExpressionForRecurringExecution() + RecurringExpression = RecurringExpressionForRecurringExecution(), + RecurringExpressionFormat = "CRON" }; } @@ -374,7 +389,8 @@ private static RecurringCommand FakeCommand1ForSingleExecution() CommandType = "DDD.Core.Application.FakeCommand1, DDD.Core.UnitTests", Body = "{\"Property1\":\"dummy\",\"Property2\":10}", BodyFormat = "JSON", - RecurringExpression = RecurringExpressionForSingleExecution() + RecurringExpression = RecurringExpressionForSingleExecution(), + RecurringExpressionFormat = "CRON" }; } @@ -388,30 +404,27 @@ private static RecurringCommand FakeCommand2ForDoubleExecution() CommandType = "DDD.Core.Application.FakeCommand2, DDD.Core.UnitTests", Body = "{\"Property1\":\"dummy\",\"Property2\":10}", BodyFormat = "JSON", - RecurringExpression = RecurringExpressionForDoubleExecution() + RecurringExpression = RecurringExpressionForDoubleExecution(), + RecurringExpressionFormat = "CRON" }; } private static Guid FakeCommandId() => new Guid("f7df5bd0-8763-677e-7e6b-3a0044746810"); - private static IKeyedServiceProvider FakeCommandSerializers() + 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()); - var provider = Substitute.For>(); - provider.GetService(Arg.Is(f => f == SerializationFormat.Json)).Returns(jsonSerializer); - return provider; + yield return jsonSerializer; } - private static IKeyedServiceProvider FakeCommandSerializersThrowingException(Exception exception) + 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); - var provider = Substitute.For>(); - provider.GetService(Arg.Is(f => f == SerializationFormat.Json)).Returns(jsonSerializer); - return provider; + yield return jsonSerializer; } private static ICommandProcessor FakeCommandProcessor() @@ -488,7 +501,7 @@ private static IQueryProcessor FakeQueryProcessorThrowingExceptionWhenFindingRec return queryProcessor; } - private static IRecurringScheduleFactory FakeRecurringScheduleFactory() + private static IEnumerable FakeRecurringScheduleFactories() { var recurringScheduleForSingleExecution1 = FakeRecurringScheduleForSingleExecution(); var recurringScheduleForDoubleExecution1 = FakeRecurringScheduleForDoubleExecution(); @@ -497,6 +510,7 @@ private static IRecurringScheduleFactory FakeRecurringScheduleFactory() var recurringScheduleForDoubleExecution2 = FakeRecurringScheduleForDoubleExecution(); var recurringScheduleForRecurringExecution2 = FakeRecurringScheduleForRecurringExecution(); var recurringSchedulefactory = Substitute.For(); + recurringSchedulefactory.Format.Returns(RecurringExpressionFormat.Cron); recurringSchedulefactory.Create(RecurringExpressionForSingleExecution()) .Returns(recurringScheduleForSingleExecution1, recurringScheduleForSingleExecution2); recurringSchedulefactory.Create(RecurringExpressionForDoubleExecution()) @@ -505,7 +519,7 @@ private static IRecurringScheduleFactory FakeRecurringScheduleFactory() .Returns(recurringScheduleForRecurringExecution1, recurringScheduleForRecurringExecution2); recurringSchedulefactory.When(f => f.Create(InvalidRecurringExpression())) .Throw(); - return recurringSchedulefactory; + yield return recurringSchedulefactory; } private static IRecurringSchedule FakeRecurringScheduleForDoubleExecution() @@ -528,7 +542,7 @@ private static IRecurringSchedule FakeRecurringScheduleForSingleExecution() { var recurringSchedule = Substitute.For(); recurringSchedule.GetNextOccurrence(Arg.Any()) - .Returns(x => ((DateTime)x[0]).AddSeconds(1), x => null); + .Returns(x => ((DateTime)x[0]).AddSeconds(1), x => null); return recurringSchedule; } @@ -536,7 +550,8 @@ private static IRecurringSchedule FakeRecurringScheduleForSingleExecution() private static ILogger FakeLogger() => Substitute.For(); - private static RecurringCommandManagerSettings FakeSettings() => new RecurringCommandManagerSettings(new FakeContext(), SerializationFormat.Json); + private static RecurringCommandManagerSettings FakeSettings() + => new RecurringCommandManagerSettings(SerializationFormat.Json, RecurringExpressionFormat.Cron); private static string InvalidRecurringExpression() => "* * * * * * * *"; @@ -552,4 +567,4 @@ private static bool IsExpectedLogCall(ICall call, LogLevel level, Exception exce #endregion Methods } -} \ No newline at end of file +} diff --git a/Test/DDD.Core.UnitTests/DDD.Core.UnitTests.csproj b/Test/DDD.Core.UnitTests/DDD.Core.UnitTests.csproj index dce894f..56d9bee 100644 --- a/Test/DDD.Core.UnitTests/DDD.Core.UnitTests.csproj +++ b/Test/DDD.Core.UnitTests/DDD.Core.UnitTests.csproj @@ -19,7 +19,7 @@ 6.9.0 - + diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj b/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj index b2dee0c..f5c7af9 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/DDD.HealthcareDelivery.IntegrationTests.csproj @@ -60,7 +60,7 @@ - + diff --git a/Test/DDD.HealthcareDelivery.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj b/Test/DDD.HealthcareDelivery.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj index 86c8cca..88925ce 100644 --- a/Test/DDD.HealthcareDelivery.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj +++ b/Test/DDD.HealthcareDelivery.UnitTests/DDD.HealthcareDelivery.UnitTests.csproj @@ -26,7 +26,7 @@ 11.4.0 - + From 1ecc77cbb6676fa92c6fa4c98fc81e27be954259 Mon Sep 17 00:00:00 2001 From: draphyz Date: Tue, 18 Jul 2023 14:08:01 +0200 Subject: [PATCH 099/111] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e013ec9..4339b41 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ The architecture of the project is based on the Hexagonal Architecture. The main 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 : From a39216d2a982d4334bde91dd5b08c5980a270ade Mon Sep 17 00:00:00 2001 From: draphyz Date: Tue, 18 Jul 2023 16:17:10 +0200 Subject: [PATCH 100/111] Add API for dependency injection --- Src/DDD.Core.Abstractions/FluentBuilder.cs | 40 +++++++++++++++++ Src/DDD.Core.Abstractions/IFluentInterface.cs | 44 ------------------- Src/DDD.Core.Abstractions/IObjectBuilder.cs | 2 +- .../AppRegistrationOptions.cs | 4 +- .../CommandsRegistrationOptions.cs | 4 +- .../DbConnectionOptions.cs | 4 +- .../EventConsumerOptions.cs | 4 +- .../EventsRegistrationOptions.cs | 4 +- .../MappingRegistrationOptions.cs | 4 +- .../QueriesRegistrationOptions.cs | 4 +- .../RecurringCommandManagerOptions.cs | 4 +- 11 files changed, 57 insertions(+), 61 deletions(-) create mode 100644 Src/DDD.Core.Abstractions/FluentBuilder.cs delete mode 100644 Src/DDD.Core.Abstractions/IFluentInterface.cs 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/IFluentInterface.cs b/Src/DDD.Core.Abstractions/IFluentInterface.cs deleted file mode 100644 index eaabf8f..0000000 --- a/Src/DDD.Core.Abstractions/IFluentInterface.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.ComponentModel; - -namespace DDD -{ - /// - /// Interface that is used to build fluent interfaces and hides methods declared by from IntelliSense. - /// - /// // - /// Code that consumes implementations of this interface should expect one of two things: - /// - /// When referencing the interface from within the same solution (project reference), you will still see the methods this interface is meant to hide. - /// When referencing the interface through the compiled output assembly (external reference), the standard Object methods will be hidden as intended. - /// When using Resharper, be sure to configure it to respect the attribute: Options, go to Environment | IntelliSense | Completion Appearance and check "Filter members by [EditorBrowsable] attribute". - /// - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public interface IFluentInterface - { - /// - /// Redeclaration that hides the method from IntelliSense. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - Type GetType(); - - /// - /// Redeclaration that hides the method from IntelliSense. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - int GetHashCode(); - - /// - /// Redeclaration that hides the method from IntelliSense. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - string ToString(); - - /// - /// Redeclaration that hides the method from IntelliSense. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - bool Equals(object obj); - } -} diff --git a/Src/DDD.Core.Abstractions/IObjectBuilder.cs b/Src/DDD.Core.Abstractions/IObjectBuilder.cs index 8814920..f6d1827 100644 --- a/Src/DDD.Core.Abstractions/IObjectBuilder.cs +++ b/Src/DDD.Core.Abstractions/IObjectBuilder.cs @@ -3,7 +3,7 @@ /// /// Defines a method that builds an object of a specified type. /// - public interface IObjectBuilder : IFluentInterface + public interface IObjectBuilder where T : class { #region Methods diff --git a/Src/DDD.Core/Infrastructure/DependencyInjection/AppRegistrationOptions.cs b/Src/DDD.Core/Infrastructure/DependencyInjection/AppRegistrationOptions.cs index ec9e44e..9a9420d 100644 --- a/Src/DDD.Core/Infrastructure/DependencyInjection/AppRegistrationOptions.cs +++ b/Src/DDD.Core/Infrastructure/DependencyInjection/AppRegistrationOptions.cs @@ -58,7 +58,7 @@ private AppRegistrationOptions() #region Classes - public class Builder : IObjectBuilder + public class Builder : FluentBuilder { #region Fields @@ -179,7 +179,7 @@ public Builder ConfigureLogging(Func loggerFactoryCreator) return this; } - AppRegistrationOptions IObjectBuilder.Build() => this.options; + protected override AppRegistrationOptions Build() => this.options; #endregion Methods diff --git a/Src/DDD.Core/Infrastructure/DependencyInjection/CommandsRegistrationOptions.cs b/Src/DDD.Core/Infrastructure/DependencyInjection/CommandsRegistrationOptions.cs index ad08359..673c36b 100644 --- a/Src/DDD.Core/Infrastructure/DependencyInjection/CommandsRegistrationOptions.cs +++ b/Src/DDD.Core/Infrastructure/DependencyInjection/CommandsRegistrationOptions.cs @@ -40,7 +40,7 @@ private CommandsRegistrationOptions() #region Classes - public class Builder : IObjectBuilder + public class Builder : FluentBuilder { #region Fields @@ -101,7 +101,7 @@ public Builder RegisterDefaultValidator(Func.Create(validator); return this; } - CommandsRegistrationOptions IObjectBuilder.Build() => this.options; + protected override CommandsRegistrationOptions Build() => this.options; #endregion Methods diff --git a/Src/DDD.Core/Infrastructure/DependencyInjection/DbConnectionOptions.cs b/Src/DDD.Core/Infrastructure/DependencyInjection/DbConnectionOptions.cs index 87a4734..b4144da 100644 --- a/Src/DDD.Core/Infrastructure/DependencyInjection/DbConnectionOptions.cs +++ b/Src/DDD.Core/Infrastructure/DependencyInjection/DbConnectionOptions.cs @@ -47,7 +47,7 @@ private DbConnectionOptions() { } #region Classes - public class Builder : IObjectBuilder + public class Builder : FluentBuilder where TContext : BoundedContext { @@ -82,7 +82,7 @@ public Builder SetConnectionString(string connectionString) return this; } - DbConnectionOptions IObjectBuilder.Build() => this.options; + protected override DbConnectionOptions Build() => this.options; #endregion Methods diff --git a/Src/DDD.Core/Infrastructure/DependencyInjection/EventConsumerOptions.cs b/Src/DDD.Core/Infrastructure/DependencyInjection/EventConsumerOptions.cs index 0f003f7..cbbdb88 100644 --- a/Src/DDD.Core/Infrastructure/DependencyInjection/EventConsumerOptions.cs +++ b/Src/DDD.Core/Infrastructure/DependencyInjection/EventConsumerOptions.cs @@ -48,7 +48,7 @@ private EventConsumerOptions() { } #region Classes - public class Builder : IObjectBuilder + public class Builder : FluentBuilder where TContext : BoundedContext { @@ -86,7 +86,7 @@ public Builder SetConsumationMax(long consumationMax) return this; } - EventConsumerOptions IObjectBuilder.Build() => this.options; + protected override EventConsumerOptions Build() => this.options; #endregion Methods diff --git a/Src/DDD.Core/Infrastructure/DependencyInjection/EventsRegistrationOptions.cs b/Src/DDD.Core/Infrastructure/DependencyInjection/EventsRegistrationOptions.cs index f96afa5..b835b38 100644 --- a/Src/DDD.Core/Infrastructure/DependencyInjection/EventsRegistrationOptions.cs +++ b/Src/DDD.Core/Infrastructure/DependencyInjection/EventsRegistrationOptions.cs @@ -35,7 +35,7 @@ private EventsRegistrationOptions() { } #region Classes - public class Builder : IObjectBuilder + public class Builder : FluentBuilder { #region Fields @@ -82,7 +82,7 @@ public Builder SetCurrentSerializationFormat(SerializationFormat format) return this; } - EventsRegistrationOptions IObjectBuilder.Build() => this.options; + protected override EventsRegistrationOptions Build() => this.options; #endregion Methods diff --git a/Src/DDD.Core/Infrastructure/DependencyInjection/MappingRegistrationOptions.cs b/Src/DDD.Core/Infrastructure/DependencyInjection/MappingRegistrationOptions.cs index 3c32dfa..31b72a2 100644 --- a/Src/DDD.Core/Infrastructure/DependencyInjection/MappingRegistrationOptions.cs +++ b/Src/DDD.Core/Infrastructure/DependencyInjection/MappingRegistrationOptions.cs @@ -24,7 +24,7 @@ private MappingRegistrationOptions() { } #region Classes - public class Builder : IObjectBuilder + public class Builder : FluentBuilder { #region Fields @@ -49,7 +49,7 @@ public Builder RegisterDefaultMapper(Action map return this; } - MappingRegistrationOptions IObjectBuilder.Build() => this.options; + protected override MappingRegistrationOptions Build() => this.options; #endregion Methods diff --git a/Src/DDD.Core/Infrastructure/DependencyInjection/QueriesRegistrationOptions.cs b/Src/DDD.Core/Infrastructure/DependencyInjection/QueriesRegistrationOptions.cs index 1a5e30d..0783615 100644 --- a/Src/DDD.Core/Infrastructure/DependencyInjection/QueriesRegistrationOptions.cs +++ b/Src/DDD.Core/Infrastructure/DependencyInjection/QueriesRegistrationOptions.cs @@ -23,7 +23,7 @@ private QueriesRegistrationOptions() { } #region Classes - public class Builder : IObjectBuilder + public class Builder : FluentBuilder { #region Fields @@ -48,7 +48,7 @@ public Builder RegisterDefaultSuccessfullyValidator() return this.RegisterDefaultValidator(validator); } - QueriesRegistrationOptions IObjectBuilder.Build() => this.options; + protected override QueriesRegistrationOptions Build() => this.options; #endregion Methods diff --git a/Src/DDD.Core/Infrastructure/DependencyInjection/RecurringCommandManagerOptions.cs b/Src/DDD.Core/Infrastructure/DependencyInjection/RecurringCommandManagerOptions.cs index 03610a9..01fe5da 100644 --- a/Src/DDD.Core/Infrastructure/DependencyInjection/RecurringCommandManagerOptions.cs +++ b/Src/DDD.Core/Infrastructure/DependencyInjection/RecurringCommandManagerOptions.cs @@ -48,7 +48,7 @@ private RecurringCommandManagerOptions() { } #region Classes - public class Builder : IObjectBuilder + public class Builder : FluentBuilder where TContext : BoundedContext { @@ -77,7 +77,7 @@ public Builder SetCurrentExpressionFormat(RecurringExpressionFormat fo return this; } - RecurringCommandManagerOptions IObjectBuilder.Build() => this.options; + protected override RecurringCommandManagerOptions Build() => this.options; #endregion Methods From bb9022cfaf42d8cb43b181154e41c31e166a8840 Mon Sep 17 00:00:00 2001 From: draphyz Date: Wed, 19 Jul 2023 11:42:12 +0200 Subject: [PATCH 101/111] Update schema --- Doc/DeploymentEvolution.png | Bin 126547 -> 123645 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Doc/DeploymentEvolution.png b/Doc/DeploymentEvolution.png index 536424743f479321a4eac0573ebe8249a546df66..33be40607da6b256eb0fc52c2af81a9fce49cef5 100644 GIT binary patch literal 123645 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

HKfOtHnBt&mr=;j7i?2?THf2D4DE_ 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 literal 126547 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 PfWJ$M*DhvWFnaud@V`=6 From 2dc85f212c5c8d3ebd0e9502076250e89e800a34 Mon Sep 17 00:00:00 2001 From: draphyz Date: Wed, 19 Jul 2023 11:55:10 +0200 Subject: [PATCH 102/111] Update schema --- Doc/DeploymentEvolution.png | Bin 123645 -> 118262 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Doc/DeploymentEvolution.png b/Doc/DeploymentEvolution.png index 33be40607da6b256eb0fc52c2af81a9fce49cef5..66fa07dbdebf7299092cab2acf16443e09f07c5c 100644 GIT binary patch literal 118262 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

3)@{hb0zAk%$bH@Pzf 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�evbHN~oQ1_KE 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 literal 123645 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

HKfOtHnBt&mr=;j7i?2?THf2D4DE_ z@5l!`3Uk|%&$ zb<*OlIPTey-@`rqsmN_Qa$##=W1`0@ou)*8#k z>U18tmaI-MnY8Pkj=bYn7&n_j@Fwnc$ zWj^`dQCJU-H*(<2Vk=BJXl=eSznOlU->||Dp$V7?hYIr_to7Z;$T>Z$Q?|^`&H^b{ zjcNjE*6lC4mu%nU=ZfZqGi!?d^sujAV-drCoAp zQHpQ@P_}j=x%h&eLYM%Xbvfoor#6>!zcwk-b($<>Hp1*p$^~!whD!iO>+3(auMb!} zinqumxz~1(T1x5l)SD%jn>TaD%S>V9>=s!(Kk`m9bvgD&G)~iwechFzI|c2F4`m2C zLeSkpC11PY%$xHEz_PpVHOui*Uidcjt<9V z%FBC<_Sg)Q@%DSCHXE~Bzs`u_p&oqC@6X%tC09O$x=r)Q*<)p}+IZ%Y3igb#QG(A_ z{JneIuqi3=dsrmTT3T{@Sse@$vpRUao?eFiB?nBZaH&@rf1KOI;NtGZ=vVXdcTEm- z7wzJO60f`+lRHZ3UgmR&1va<(X46_x+AEv9WuT+GcYBxI(OUGTd&k{}#L2)TG&tBi zC9kZktR6f@EQ;B;uu*r@!iusMxLgF%$KOSZtK(mR73edrMvj#r=xeo)e&0T_PzDAD zrNWyduYJ%7d!CiG#v|HVK=ssl)*>TKUAx1xw-(0AEFpYK6-&!F;D^$!Rn=v+EIFgw ztVYV}_?4yrqKu@N7!TULdk1pbtYZ@+ov_a*Sv%~)Aiyi=2);ITYLfucPT@g-FL*E2 zIGy+aJIH`HZ|VzOW5;(vN1m|BMjE><+A~Sr`l~8T+*k3*0ewL#QzHGadMX{ERm9HT z-oBy0{>l|z_hwS|NIH%j*Y2&PJP=Bt&IaO7fr=9QRc<>yqM8`+-V;n^s8zZ&cV*}; z6u}xw-u{`6tI8DgPe@EO9945r;WZ_qChLMu0baVDl=W6ONvNz_$<#J?PSs$M;)AC> z4#w_-+q`%E&P5X+Yt%!z+TKiJo2HhwgC=@ zOHN3*ujs?ni(x+NXt#|WS9_ftg)J&-FGwB)3*p4d%E|=YduOO2Jnn*6trr2H%3#ys zXY0`_^^@=C5)~!3USfEpnRH%RS-Ei^sHpiA*$WO7n4<9MkIEcBU)n=5b|yFDQdwyI z3-fl`=kyvu5V0Y;=*@|CK}^Q#VLup%T2OiG#zs#)ia(lHF&Is}?v42t3R^#()B zM>oQV1o~gYJsGg)utHPi&IsM6+pnZvO8xc7T|dPih*JKL-!yOa^eL(`F(ZP9ISsTj zxD&6GTwr}`emk}*<_hcSwvs-y2Ea*u2anv#BWM#2!_nEQ+4L=)J=D`R?H zudE1&5e5!E%?fD-hv#D4R)AxK*?<2fh~^g??^SQvB*25id`=mXuPf$q=&FM@=qqZn z*3T3^E;h=cJ1)1g-~DZ4bgjFC#BN|dTpmVbs`q6!uf0(!x=v7aur+>STnB(4oQ$$# zMdZF~?u+vhhfYVFNX@MZ+eKF3UhOLN!X@ELs1MY8rB#eV@Z zaO}zsAYn8`IadE`6~AZkYjh%_RCM$Tp~mU4&{N%Tb@73rrOTg>}=AODQuAvnqnS8(ag zlNII=fWi>!hbna98=twh|Me(%eq|D+{MU^5!?>*rm3FLwfvY1dkJiQOMen8>zt6sw z+Fw;`>6SIP=zZd%5PVFU6Wy6BqV}Q9DlDVg;{)Fg`W6Ynz95v_KJ#r-lBH-+}~Bt=PW;5#cGuF!s*)5s#kqm z2#lBWo`THv)W=1_aqGMqy(4!Yi&Qi)C*#)+kwbR^XIbI@<5qYqcMgepE|Qk(5E+H4gBKeN=FBgbAf4;wfg`eg;gk zEYCKilyv9OR3*JleT;lu1fOMF$B2KuFvO`G*dAdh^p~@JJ6iIyXE$PvXJ%?zE~0Iy z&w=|GBACY25)>3P31;`&H*el7UP`t&{Dhh3C>968L84g@ZWn>4jQ6VL#z`ikAz4s< zD2;+d+%!8b?w)z&vy%`-$AHJqw$QzUGrYQ~Szl4NZ``bTCd4XlvW@);m#e_JD|mA7{=@=eMPTFf>kGUU_G6FU8(mEtZ;9>Z2>+(wDnU8}A#|kN}y_?>t%Rn0-r7aMXeTj>GBsE?^eeD!lVW~Sv zeXJ0=6K_XDlsnLViRbM(d?5J;is15Ymt+lpzN1HXmJj+LQW{iF3s5>bOVPBChQYc9 zbIr~4nK*i1871m4u>qDJMri5mqkry18&l>3Hj`3bC{Z&?W5=EYFI*Sr^m!BCy?b}3 zYkRKlRgRsUxN3T>d1u3`5eMh1#Jd^{Gwd5j+i!L+xcu0+ZNBhY+m?H0cArGMyxISR zStzq0h3$C9rnsu?NqizL$9MVuY2D0D<7V6HtyS!A;fzK zSDmuMU(YiDl^ZGJ{zWm4#%p9|T{aWGPO$toQg>$}CEG6T{swOH%;QJ!XU!rs>C!pJ z%4C<0d4(ko2dO7YDYlYzw=*z%p+IN7mN*5;v}`H!Idh1%4XD#&IsEfBV!sFA8$$#F zQ)aw}?xA-)1K#w!q~bc;W%+@c#ZG9r_OmQi7u>Z;r)GP5K3+q?;`+FI7oQ%>$Mz)+Z2A6k4jUk`YF=ekh}_{<`B{AKVAathwD5{5@7IR}nC;Vz zQobk5--UxeAa{sQ!mljNEr8o@S{lFoZlmzb++7EG$d!5^I|U`%I>*Ucr2#POYb2ij zYIS5AtJ}i$P)U1Cd8a(ODs)@z0r5&qM(N7wq>K=;zob+H-qyo$ha&e%xGmkLt+`gz z27)wsKZD#3B0uNF+Yho<%uFpb;bEE8o#W`FM=WM3cXkCuXrU>A?tF*wiRMCn&f1Rs}R z>f^HVXkRv2)(l7at0-5U#>*HZ4~AB>TVq^8f)>0(vB?omPJKf-GkKO@-?9h%Ye7fp z6dJYHCq@PlTc4xQIHUmc)3qQbf-Ay)sIhi&rb{#G_V})#kPyRz;Ni&|3YOAU;S$=q zd;N2tQv=-OK*j_^pf|9#LlQ-||C->@8j|t`?@oT}T3*WKOtG}IY&4e7cmW^w)jT*uMBm0>~Rbcu__(4Z;RNHxUhCL_`ap&-<&NQR3&lI+KO zW}8o7H2Fhh3)N#$QzHK4)76mnA0?rHJXvT+md6v=upp7txdq3Xk@{3pr z>syK9oi@qqXK2m8&(I&=co+@7hrGM~#^t>bBfwe-#z?&2jZEB?(#C?A>vgpXtd3@o zLz!9Ro4`Ik!}!q@ne|#a=#{dxEeKurAHQ1xE?ja6Ji>CwLR+Qw{^Pfhnm%_r6chyK zz0Yak3RvprJjk^{-tYwy4=$FKtps?7`@!?Di(QM#P$Ofs3``c+(qF^*l}NcKT(nKI z2?URQo#H-A&)^ATXOa1u59+BEnF8JTSyBX ztYc?0M`QAL2dfaN^n*C96dl9uD);Zmin$K3aHjcA*tdKhtPkVm;{>N!R8#%)%Tc%_ zP9I%pzFMQy=gS#Ng0bdO2)B!QPJc8uOwM+`u%YUNmg@oWgEZbFGuWED)>*8OBVi2* z1q<0B$v=Laocs1>#-d}#Xst)QuP@iWCg%d$>b zrw`Jw6$;RNSy*gv9!>n&pe4bBn z>vAfS&a$5f(bH!URjI8MDrAC}Vr9$vzGLO3NRgKUJpIkBZyw(h@G{E{hbDh!rfn0z z?Y?t+h$y=wr!(67%(@m}*YRbm-Imt<)*?f9I2(69Od)HLVBsgBgaRf7`|;(!6da4CaXp2j6F0Y`wG><~b`22TQGF`v;)~55EoByS^9`66Dfg7dxg{dI z4j8i=xp|lGCZ&WKT@?yBXy$vQj3np$>f(HY7t6UUs)v5_;f3Dk;r8quAK+xAE!pc? zoc-dMzEww#)!ne<&;qV~yg2V87a!wZDRaGl>7S~+?H)c=FC#`dW9N+rFY~T{v4DO` z-*uiC#P5V-M&}(3`Ns|XQ1BzR`@-lsO7ptr3IS945M~Y>kMF^x-P`20Y`dtr0?Q)x z4h$JjQQ|oy$8#CW%XxB0-l7plbY;+=#Xt~N3k`ipKk)MM4uFMr0uiSxZt>|cqp$^Z zIA|Odhd>&_!z`4NqT>Ch&#ClL9yCGjTXwp;i>+iD{z#^Ai4zx-U)v5?SAA=9B+Ag$ z?FMn`&WVMH-Y&yg#aL(a2s4GYw^ir!Ua2fb5}!V$MF9HVuV2sV>oayhBp(P$wJKvn zv|!;d!HJgl;yt|gM=N=-j`v<_K1aC|Zuj#cLK9vX^k7xh&-N5;kHvY|yd*CPv>DaT zW;@9_{Ys7d_*Tok=I+F2}ez2`L05npJuJdhV0*aYp-sr#dhtVE9qTBl=f z>qI*CF#*ADy@AZ)J zcDA1qLB>Yp@<#PE#cD~wsfo;Yb`yP7l;n^1AA4O;t8)8oqkA_*dRu{E`|UnPiw`ye zg!_FA8h^bTeb?v?9^8=RK;@SBA~hu``xerwU}X3|4XqWRh>FNwzZCTIVc~29+k9PV zUTsCm068zNNaCRd^6nN-HjNxoYBPI=fMEUg08l$B%b)(ZkkS0vCtIQSMEPACKXU7m z=8rta9J4RCQt466!}CP3CQ&03__pQ??*5bXXAzJPMa^{(ax2s0`YxlzGiTJkO${~G z)9ao;30~yng6Ga=RkA(W$s+42RlGDeBy#J=g-~%DRv$1`Gxp&zLs=>rVJX0dY!@GP zJB;z2{WN~kR9H;#I(A9I0OXsa2$X9EH>8y=g{r8kCPO-A_w&c!# zpi8l9wM--ipv$r29P*wpGVq@O?FOd41>mi^VB*`@wiGBA&{9CoaS@2N3Wib(ZzqDNWC!d-g0t#5gx4p)LN^-Tb9|> z_}|6+Ew--E;UpQr&k(umc1dEKYXOS)-?5U&xNjk8f{n=N6m!YN)pD@+6)PbR&~=dh zBM=fb4UD^cibf{0w_EJD;0O&6l~{R*v(otR!S+Z3JbnN7IG zv;}0$;-fSxQ=?#|?k?xIlADCCs7#Ek@YKsYTS?ABs%r5gefb*QNXVAcPQg4jbYEjR zaO0KKB*@?$p;;e1a|9a{dWs*%P_gk(l+mg)%S#uiNEI!EonX4LPiyuST^y64%)8}M zz;n4dIpb>wp@!&_5+n6ab-4v2Zw8C*OMC8I|MI@JIpb25BR^sw?k$ z>{Jfln2Vw!Sq{mM1nHPdr(fhtFCSOb3kxUHH4S}S9VAzd<<(o)Mq zSMKFrPzenr#k1_#4*X4Eg57b%&Z!h{z+iHmi2D#QO&7>Db zgD6W#DobWn7dlgwuI^6H-P3&i_`r`o7+iprn3$_;&0k$VgkZ8<$c+)A7}7<@APCo` zLS2K1?{i4DnN%>~(&e>AiW4s?G<1$Ksh~&bYtD*amar*D@@GC}dbauH{{DVL!)@VD z3!t&x6*$;YjxZrvJJCQ~%`2!D(Ti|~oWE#AvXis^mG~tu zA+Tf5)Y@jl^77BOg8DD@M@;6R6kI8hnZ)7Ad7@d3I9+Yh;=IG6?%^dzyK`;qXbFrM zX1OG0g2!kB!}30d*&VW+>^~KG`^i&^i(k%C=s8azAJXc_GyZV9TNA9c8Ib$>Q1)A7 z0$j$Q+#Glp|Ng#hG2bd_JaSn-TUQx@QmR8pn`tdg-skpe1u{5mzWb8;b};!CYjoZu zE8o+cPYB2BVIzA(MM_KM_VDP)EknO|%NpUwYkBq-JwTtq=Czqf1=P7pp^~_$nqz$e z_SkeDk_2N{T*l`ER{IW!$8gez-B^dCF9~N+X2OVmT@=7cNjk-P;_o{sU2Igf_7uC~|T|NS@2}Fu@w9>AA9mX%UpU+Ayju%JFS8Mp%S7;{9ic2|KFf2%Rt-& zoXmzcHLrJUYd=hF8F>=j5hrNk5clQ&ZJ&qT)DVkkM0V@_OaBC%mqVQ+9L4yur+Aan z$kez}JHvDAL_aXI%K4qKppI>R@E6870 zC^R0Bh=K!b9q5tlMX4OacHIGm9}gd2+~lMkqBvkW18f@JYm2_<6)DIvbw9!ezhR$K z5Hk{>0h=&><@j~3tE;mBGg7-IBZmPD>$uonq{|G#oM{0AO#E`e5+XKH@Hg7XUe+tt z0rYpNqY0V2g24=m9ugG|;^E=36Qy(vW4v>Af}fk)3yTB97t;vY>l&P~>YRd1xK+|X z1e7=cxN?!;@lID{AU|Ld{3BY`oQVZoRC3pa8f>v;hp)0#_P(G!c$BoH-C-pFLYg0P|$au!1*)KU?c__7LbMRU6}7C zHo?KY?KC>j1&(QaGj!w&c>jb&^@PKByzXUXF!4~Bh|WYPpAW`-tTRpp|OYG zxajz&U>|1jmGgYWAm=J0<^I3e59gdE@Q{1A3!7KI0Q+N4pCHB^g@ER%NB0>&MuL3L z|Ae3cYrg?v9E!s6{g|mK5dLI@3gnRQ)Q^yHk?gRmS8p7=nlvsi35TuquJ56uS4as3 zV0i2_H;|a}afXqR@o+`UGK`8t@#t%>?5e-Q@!q2f&cFuH=}8BFW=(~M_{BgJ(tZa4 zH=?8pd;L2_)9#1z?+UJh2hpqAm0e)@D51ElAzzEYfx97T#*Od;8I;%P z0!*`cC3E0b2$VZ|<=76^ewmtug$2dYPAYrH&U|`yadGkV?Q;0aigIkFjP}*7e$UmH zcz1)$DPgnB2~p7@fKwir9kL#x=L@TLiGO9aCpV-a^#nh^4_I;UZ5HIogmR>Pcdk`l zyBkz_b=*Y%7k02-uD2>NTca-!QAbfNw}eEC(;nQ!m}d3pqeqY4$efdFzr{KIT5`0< zf&9h~O0>q9^o_3%QE7{I?q|iVC1yX@qSoKkY}D+xY9>2{P3%lL+hF{bY9(zip;yjl zAf{xj3P!JR`LC!ZsP-B5yXueaMENnC?Z!e#WI&0B3c7 z(q`j@@F(*ZN&w3SaZlK^3Uf_S36w|YO-PShKNDqM=bt6O2V#wX{xcx1$YeH%(%%QW z23&8Q$p0grhMa#2X7(pRA3;X_KXAA8u@>4IN8ke{9x-V8KVo!##4{qX3#?6rEh+Pw?R55NG@L2-04|7WW0xgu^C4?hCr4AeO5qp&CNNqJAYA6Al z>N~REHr3WOQ31fezfs-g*!joIroVL0|IMMV?|}1fm?XFUCRX@wq8WJ34Tqlp6bK-L z(fCI#+8_DujnUZpYc&3o8ULPp1Q_(qvc?th!@sF+&?L)zA!D>#nJLUGUmqE=KfKzC zhy(!O!KL*3pn%wr8UB}2;+)x)--`x8nDl3Xz<)7&{~@eccmJ7y)?O=xKL2Zk=Tmk? zK#k`ASt4);Ur>{Gl590|M~qzlQv|Z^H&-BwM-&jNu7v+XDWTKC^ed5HJsSQ)|F9d>N$;@#GR&%y{}nixq8J=)`b1qyo~c2L`s7A$jJ!N))pOwo&@8~}hHb4F6(SW4 z>R8bVY+geJw?R$cR0-=?Wqf{SG_k$?yyW$sd#Dj;ziOJ2`o63Tr*^j>6p=vdc%Au! z<|Pb=N2AeoZ>VI|fVSWBBI>WdN!1}W3X0ZSliGeSo-w{s6EoAr+41c5)%HAp>*6VN zsFb6S7xSt1xpojVPC}>Gd-zVDAe5ob4oare0C{ALx0dc*?&6QOyK9nnQ+A?Kc=8>C zi5_W~zkf8Js40ro3`%MNo9Hk=sjI8U1Ng0K0C}n^l-&d)bqds{0Wq?Fb$0_xToJ`_ zvLSSs^pBTYv&^fQydOP^g*t-guFx5_pzf`Hrg`lpUui|70=sC0ZiJ?Rx`MvU1fH*d zgC_a#4Tq6dQ9r?-Gp}%I)kKwZ(?Ee|UPQvRAsU&@Vcg57i#8>*37r*KTTY#JyUbf- zJ#vO`-3~)T3eHcdYw))GI92XNW|eXj1v@}qJbH?LU+P0^GRl9hP!0%lrB= zG0m0Jg!`b}%ae$KmEO_A{qOa^yvw1o=odZ3Hk(Dtt+&EKi z0*Xw$i;KJgK$0rFiSR0iZ-9P71X^IGhb*4@5UH0!$CiPT%W{CY1A;$seOzXA#qNRg zCAaglwRqolb&{p8MSojykul+mqaB3~P4fcbdXH}AamM+8l@{u|=HHJ@S<*l*HTH<3MBiF>7V823t`}=FTS!G(9J6zw6NPl4^9(1 zT<1p`y-@A1h8L98)E*By96X|<-#@xMom;B!pcDdxX1-yrwYs#^Wcu`^mulk0?HFhf zr$W<0;GPqZiAIZC)n?5+lhjpFQ2Q}>^#@O)UmG;`)fc+r#`S$C13to#diY7pxZ_G# zsZ5`7@v6s6eQTYxWnEl4Z< zw>$ti{l@sVvfur^2E6x;XV`SfAT=xW^z{DJ?DL1vE~y2BJVVvJ1qB68vKMQNL`wIX z9SoxH3JDGdDO3GYcx77#NfJ9XM*`8?YB@g`I9M00o_}F)!lyBn2=BLFjBSgqk!~*o6I@lAs z6;KMpU$DG1pQ746eaZeUYaN5h?OI}=*J4Ib_3ey`!#S)E!_K-+#ebtR35OX@r*u~u z@0VSch;Un)J-o!gMtxQRsW9mcOxZ!;QRUmHH%FN1@v%hNMX&D#Ljtt(eDC6~h|64b z>ua+qRfjHp$w8+chE9CBkH25|s=_fgfckPqYZzdk&AfvPC1B|~ym$LsA6@y$2#2u~ z@j^ezcoTnm(_3XQG`dR4#S!b@1M-z4bUaGv#l-~(1ic~A6E?c_*|X8d5&Se)p&rh` zTF8ocj-BD~Ue#NKv9saODKWLNN)JwV-^Y8Ojy5KzUSMDBmX7OD6rRtI{|xrNPVZzc z^b2d;)s?%;*2Bh^Q@u#&`LKPO+$zuUAo`q%(?I;ord!E7g`$kSD(#2Tzc}@AO_$D&^O@@4*C)Pf8(JytnV=W1@LK+{(={@EU_^1g@(5BM z;C%4kwJPq2YBL%sqN$7_ri(TiJEu%}_(18=``>C@HshUZ2R%~fv{re9FkexN!6It1$80nqMtn7s>%jTbq|hK6+y8;eX_SwwimSG9^t1bp#`VU;JM4foJfJrkAG&a>-AqOVpsPgfW`eiEEp&4S{bTU&ICeX z>=oBd@!zJxtOZbgYa(RCMD>Z9)mtD_i)O|CZo^w?zQUa14S`h{bDT?>HJgaKLS z)c5?d1!z<@irX59{QpQEHdTemLRHk<8@466tz;C}Vx1aV)K*GBivB5g1a&#YP6IkM z$G=cGjlE+Sn5@?nFHW|IdazM@B>>0cOP8k78cN;V#IZ^dRe=bh zdwRv;aZ@nAxw@@2%|zA-N)87CwKa~d-Umsstp8>6knwyn(zx|M9OhEw_iZT(phMs)se(% zFQyF;IOW!~&4Oq|UswFRX>#_WBoR=wI>4cl04v>8*0x%7x8qj=y19bR;d}e@CFohK zZEc9#;+S{KQc>heb4WAPkK&+*a`qVjB9FXX_e>nQtw(7DII2B9j<(l`DHM_ zp~dWYFH(sYDrUu06)L*>rJOr*(CK^af8~cFle)ZKMfJd9nU*?c95FXyDOxu-8sT$l zbeDrbIk~7P918Z62!yAg`27Z~ozD65YA0?Evya`|f>g#rCK=uIRu4)3)DNUD-$UeP z)FYbUagz-ZstyWeX8)Ij#B&&%dqkAm9*1&cexrgwq+>6wEe9Z36%`dnIXLbh-KguK zPO0tjnHM~Lk>}{|;uSr)uX4b-!xposB!|Ze=o8*PtrxRu)nnn~ z;UU}ucgwrQaOsI_(%9tWj6V0`?H8a;_^pF-6O67+&e-+hp>;HZs&?rhI~r9ZIm9>g z-G(C`j(CFPai>X(P~ppadV70oAx6${Aey}Cw6J;WwfJfWTO6=(WY&3C6g!l3jSj>9 zC?FPFi`|@@KAdK|1F+-EFmDgdBh{k|O}3Lq=}5*-y8{*OAL+K_SW#KI?J)IO&>dna z%qHoPJk`hbv3$^~H&E9$JC>#eb(GsEMn!c19IbSWuetM3u@PLc-cQbgIUZITt(Z+u zucM1n08b0MnA9g42V^zq7?i-Yln%TCcJUjWl~*C>%)Y9uY&pepxZ-cE)i5zj7ns5bjm_-5H)^yrkF%^9&ppa5@mw^T3jV^~_mD$kQ2?03M4qjcA;&!#=2G#1T;Z%nM?Y)l-zdc2Z8 zO7x$-5;Zc<*P0(NzO*Q)VnOuQDEaPGF>I@1%iLpLZ(>x&u)+WuW$s@0;Dl?HA81%q zfBm@A%6V*i>#{FZQrVB)+=a`@<{}mG=;8Su+t%47ropj#-sG|%SzGSiSfa9A)0o1F z6A{I_@^@o~erhWmj8z)I#j74YCh)|`_kfvnf0o2;u-de2JAq45K<|qflbzYd8cjSf z*Lh$rY3S#{b$&kp5a!qwGje#UZ<49AVc7GdzZ+FQJD;wpV&gW$f=$N73SaK(4`7XE z_E?=JkT~W4vMcP`X1Zr^=}2^%LzHZmJ0iGI5-$C0(T2_CmdO;2a4Jiiq$CsKEG=c z;1#8GVT2`DyGym~lX8Bw=Y%mR2;C|XU{58(T$F=tfl^Be|xQz>9)w- z{Z@?Sn5;PY5a@@oa-EeePqd)ZMbc(Z#BsFZ)+6oIMsmX3*<E&-xl~FO|uj zeKb8{NzHmG-tq9y0nkK8rC5r}p~K#%oTOW$CLUP0tpo=D@*C>@gWu57VBVeL*|`s9 zI(dIfQ|8-Z@1)c>#kon39nWSFd8QF2rC@T%Pk$GfNYFmM5riN^LClHWb@1S_r{{fN zRz4RVfQG0@AwCANzk3Dc?N~fMu5h#@%ylBH>t|X$+4Q%z(nZg4bdf@$i7h|qvZKZ% zzyzvfZpb8QC@<;)v-=&eY*Sv(9?ffOb~emwa?#{GK-YhNx)2>RUk@k9K%qa zwM$g%SUKjfypJ1VH~}U-yTD5a>QT?CtHa~R#T`Z>9yIFG@pgIi(4OH$!qy#0X6SuW zc|T(In~Mt)Q>i)=$KO2Q=I44qdqWcVZRUdm`#^VZ*Xy(GUz`H$yobBb8_`R@#bSob zT7rohmm?HR7JCI73Y;AVU0?lZ7JuvhxNv!lgQcbTkw&4pRBzfc1p)K&S(1gvnppZ+ z-RWN*Vx`-LiKyCB@anGNR{A4icKeBeA@#w%Hd+@kJX4I)_i4dRz~!!pDIS*`ZDq3@ zf%(XrG^#0(7#GZMSyPY=@$aS2iYib=c*sq}ULGAAO{W0!n9h%hn{;J3+>qfwtcC8b zSPK#NQ)O$TM;cBv;S=tLiJ$X*B#L+v2hB3h7{^uixW|Y z(WrOW8(}Y+u=41z>c*@y>x@~!5t6l7{DFlHO%gYMJXp7#b9_aDmAYL%m&_w6PgGJ( zn$}|^qIJ$1p@KNcxdsQny&d}k-o}kA&0}6(jlKQ%0_lcsT-7p=%q}mF`zmI2l#}<7 zR$F#rL-+vvsORNr!0`++doJ&OBy#hQl6rwFqm_~{yrIVviQxH7x_R920R3r;zDOw^M$KpRhgf+GQZlY*1Hef=80*k{`nB?Mz| z5QY#S8mUO0)004j1Zko48BqXr(JOf;Ep(s6JdZdv&{0sUT&$7sIKWwZNG9=YcoJ1q z(yr4_QH3%8_TJ_il#<*XOn=}7eBgk*uU!OnL)EiwGoxJ=0qi(QB{eH+K=z;O{|i5O zxU}4Mg5d}FPo8tjr;(@4S0Vkp?=d%xcY$wn~|6FwlA5 z)*O>PVmZ{%IG~VH_PjP(n*Q%6FFYEI>E4o;E3ltoCn3Fe?|ivcCP)@ z*)y#n;xz$@_ZQu|avKX9HM;O%)^mB^ol{}rAC|JO6paQ)*bA91Y)i(c23HK8(>jOZ z+`5BGL-j5+!Q@goZNS7FZ&qV1QFJAQLfQ=b__$Th<7f7vI(^M4LppNmI_SmL6z7?W z&Z*Igk{K~4x2qIUb_9prMpbvz<|K7;-?UYH%CdhndDG#eSE5>wf4ydqxBsENm?MNx z>I8gVd64)(g94ggozs*^ahQO4?`tu$&0z3md1RBW@|VG;_!miM?`uja-zPSj2gW5? zZCBCS^Szlvy{$9kHObbvx!C?QQ%W_#-WgxoPgTC$_pJhxzWc$@Q2fARO6PUW6dxa< zTf=&z+Ua$RN~%h>nYyL%fBPnBMq!q^w~qQIL}%|c`>7o8^|oDoxTtlG~w;WaBX zIGq3e0%Xn5C-r7xVeF|;2N(7S4x_>7DUPi^K6SQ}Nfb;n=gIc4^rhr*6|kJ1n~Td$ zEHk7*xw+dp+&tsp{J<`oWh}?TE+^}Z(gerA@mptGLqm(Ct5r03u}U9y#VUR5K6WYo zR=3=t)^aRO$zyUwGz-N8-f!jDmJ5fF=hTqBW5jD5T+mg_&KVysK3fcPON|@_dkTiX)z=L48PU4Z+_- zD<{vqFfxe>NsyDis;fii*qnMxE;+YJU;3K(i=x*7#+;>Qryi)Zq<_}ZLYqhM5il&t znfFqQloPG1Q}6{$R;I+qQporGHc8W5`rWE!uCiY1=)o`5Qo{XD_#}TU&1=n+iN_@s zo!2NCWMUNR4V-*c^-?7{kVr9X{aRffy;m`4jA8IhY*qC)WP<$q9vW14)HRp1ri-Sr zYEq*M;j$F2Gp(ZIOWwUgV{S%A_6p8lb=q?I0==o|+zWOb<8l3ft52VZ9MM0Bd8W}3 z9)OCdZiDuQq0j-j>L*Qv0qNjEy+g_JLplb!im$T_j%6ynZ$b+Aiya44wS+F-t1Rlx zDW@@*{p3*UTh2oW9h&G^o3d5%>r?hy1I;&#RZ#AC!^5YbN$kz7-IZY>{6VRa ztj`xm3Wf86k}a7C-uMJ_b2AMzi=`bAV@jlT+;j!KiBDUFlJIJniG@3{>Vo|KuQ^Wj zHiVTvm275w#;C7=7QSX+U`0#7&|kjz;rvM@^j_wurBo>0gO^|;Jpuh%-=lcV-d`Sf zs4@2&4l7+?kQf-p71`=f^zMegwslD@96u6KAOSo$MvDVy{?#8P?0P!!{>zFmo51AV z0@HhL^sB~Mel$;>|7c=b-80%txyO`VziR43>LDL7{eOIf9L?^R_ooO0US(#}qD?KD zk?$Cn7c$Gt`mDMF_a4!x>@hyvX$OCo{L^lE#kxH|Dy^n>8zIe!m7XwFC$lOKW&`_LXOE zyicXeAN|~ZDtkKM^7Z$*QLb9|-M;cYpUmeaE)2Y^M!p|JTczl*fG*fckLtC`=Nus(2e5iVPn$2Iz zVt!kvQ@621fkRs5(gMRUS5433E3UJ<8UMNw7aqw~ruQy@m~Lv_d5){8eT;wh6-$@e z85w^fpR4fKGxh$c9a{&W<1$99xg5(V8#A)^=a8@4QFWVQZri1qrj?7?EJU%D|ERC= zTOR98GrulKYg3Sl$5z@D6>G3AwAA$KQgsTxA)^Ymz@?YwEz9IjzTPd9C5c-asn{Mh z*;BJTKXk}v;b*vn|9OnrR6Gh-)U;&Z;^wr>Z2SS0J?b(Xb!g0C`Ax{zYQfI##o>>W zC&I*?W#>f~AEA~6`HDWyUw{Ag!+67~<--A7k7f7l=f!!|M5~q7A(Or;KM188K&mea z{YXQ{4z_^uvs;1GW4v~wcLQ8JOuf^P#JISa=rWfz@na|Fh z>hkv#?>Kx(3FR#R|CDy+flzi)-&U5aMY4p*PRde|EZGSeW*BQ_D6&Mznyn;Dc0!^u zWX3w#l1L)EEW;aR$(Aj$jCJ_#h*9tReSdxae9UvVbME>5&biM$N6ve+A@sD!3E!n& zeQ(|$)juZ;UaXfqtU3~-nlL^0)BHzXr^xVA`^{%Xy_9fR^^ZGGv99fFi9jo#aRp zdWN;(L&Ti?B8RY$Sp13C`Wi6n2K!304eis_MV>{zbGZJfIPA@?OVqE{7i?rIZ^*2A zpH~pdyauPUG`bt@5Jbt691kjdGn&lq&8^bmXMo!JxXXnLTB`rP8sFdfnP-eoz2W1cM4{Ema7ia0n+L_SgAy!QEX3#{4IG%d;Iaw4 z+01YP)L;9)bMHpk0(NQwx4zyWl2@V7r(#^wzc$70k5-x=w02~m(rcZEkuI}?yCqBG zkzGc{1DS+5OAkgvrJ{G0YTl+~Jfb?LT45uCHZM)6Wa8P4>MKVPU$^?uuD3VN0&yi^ zrdz9d(7Z8zuYQ%1zJh97L7dBHx&~&~7;w@{Z!zrgMpmb5MbSlw<+5gDPai$JU%tKf z0cZS1mw34ZtntTWSC(2IvtyLKX9`%1`Fh>LM_@6oYEJ`kBt`?=+}omzzJ*!nJl(TZ z^JcR!Ui#ZAObHT_HokObfaY zHyfGF_l71%@#!3?DK8YRvQ#1G21^;7$Uz;jE!3^~Krw%vnlIUDZ><9(6-DX!XKlCB=4*ebu z?nRlZ^?pk)CEYIN;lI;MaeXba@Yot=cmab7wxkKxUgAclIL98=ht+b^FwT}|4~gGh zu8<3c<|SIX|02^D3BEK{@Oa%TArnRU)c28j={mzguejOh?OV4-$*F@hj32C2RISEw zH~WiOamhyV_=>@9528{$hOGeW~`R^SQ=*j*1&5dvUnCW)^ z#8_n;h|~tHtjm^7y&uOpZG6f(Bs$Nc-glHw(tbSm6?Rb<^1U@!n%+P?p{dL( z+F)M%oVo_PV`9(mZQ+&r8JO#o-6ha8g5dJ4B(%-@>7~8Q$kp{y1z4b197lXmL|!Bv z=5FmlgR)IZI*y;zQw@_{*082?;PV`=%&VS~ayC`1!4X`vwe@cZK-Pd7=Ghnqt!e_M z!QFwQ{-LcsJ@5{57ylMB2Uw4Zxmc}W+RV30+>-Z#@bzp6d80CwZdgcGo>^wuTQfa* zz-6D_Lm5SICSU3Cjbr}i70vjOvs`F!T$!F0UTr}jf*_5Q4gn|kHrDBw_}b;=&ZRBb zTzPg+y*it`NA8NSNdop|<_nbeL3%ES>KX${59xD0%VzAj(DkL9aR!C?DYSKYNV>F^ z-m9DrCygqtutxdMA*pJlYiAiSH*XfFhUMDxNip081-fN8F6>U`pV;`HkLm3OmIFBl z-kF)M%*6D(`-??Bx@BjBjE5O%HfFdApW+azLz2IvEbpdqyk-!isa#V;k6YGq){iRV zMR5g8{hW#ZiKCyizrdT8m)H5FlUMu71$wUN(kuaC_S5gV`CI(GtjilTee^`)3FC~7e`*&VvRK>-mKwwRsmyqBTX z$H28T3eg(x;LCf_b_crf#jm*Qh$%nP@8ZSI-rfoK!i)?hny@hWM^Ii<`Hb#3Gxx2@ zfCUE3^dkk2!DCF$(fwQanb5MSnD%KY0T!H z*=W8O?JT`jAxWzdw z5{5EICHcq*Zp~nkz%I?`Ah26cki6F4!mE>dBByOSA8_latv`|7 z*r>g^%U4E6nMtZ^%#y0bSPZk9SHl$(JWeqe>%8A1E^k$yc%DVbxWGE{cSjchc-7JS z&aLWkkyc;*)6Kny54djBMm4mj`wZMFUoR77_3aE-+@33lavLAHGp^d!VI+!{$TJ;# zhbw5_>e?8b&pwuO=iGld75B;Qw((YO)TCAJg8#K)i-D*jgwP-i#^7Kgk^0Qj+}7$& z8eS{0j;&XI`Bt(kK2p-5Z%ORu0bOGXx6BTKGX&el58P3rWjo1-u6bx#{}_e4@3zzs z!*tE)(&~?b^d(r`W-p$_lpkf~^W#Dci_{Kz61rpCy7b*Id8zK~rm(uU(cQUk9A5;6 z!V3=f36~tHG#80#nwY@((>tFDE#a0NL_k|}6V43^8M2y-33f*mDRy8^j|{I{U|br* zCFcaAZd{wTbA1x0JsQ9g$oAwxob!kt3Y7dBv$3e;yv$ zL(ZQu)trQ#e})~J9$w9e4zZE(boAFxKz}Wfkr@bco)_h|ai8eq?<}`Rwyd_?nAI;9 zYguip-cB`YO%ohBWg7h;FT1h9{$L9!LWca{iAMrFXe}kalSm4OCi{6@rGNP9PpMfF z683Q|Rb``#m6)l!0={ntsuK!eieplz;OK1z(J>af%~ z3~If4{5fT2Zti7)whQNwi>JEcoUPVUic89xXD(EwP3j{3s1M&B)1^M!{x%{3dffC( zzKzXZY+{2VHU>K{f|STPXO6R*!);Rfu&-Od>9kQ2Plgk)CYbg9G11D4*;@+6Y9~FX z2L+kGvZ4`7OLkqnj@qNv7ZzP?pheR=zM|Dh74=@t*{@zWL36;(s_c=RsDh-fIn$H; zEG+QP*HfNQfypyzClWpzDIa_D}IY=?xLOGLn)$b3bJn>8k`of2_=^Qeri7Xr@bV5%=Vv{)tdrK*Dx~|2x!jDr=Ie9#<%T#hMs; zORRrsv;fh5s{rugwj|VKc3-+LTYQlV0Pk6>F{1m;L}UCd#dKxym%^n~KQv$FrBuX5 z{L8q=!WpHr{?XM= zUw9vynek~@9*D}PnREC1C8(IVZBixg%C5X>k~>rd#Xg)bG9I=r+Pue${-il$wNmQf zH*P1)9OH81nXrmrcVGJbJ+3yX-zmx)mtSeB-tu&;P$piF9|$YKZKHAaFQa~Ytx;_U zMlkEw5|=@A=6JP_vbMI4%~A{8^?P>??&^hxV&@U{FYkUjPEO6l|MK^F6!C@!&mA(9 z(eGtJ^twRI;%VBSV218U=0$hBnep;VnDO>X=FBNbLgjQJMvLu0{Laz;dKQoME$iT! zZ-vV2=bWM!I%FirM;2|mW$zeXc@&?lNwA8AAd%k9y7HrcI7H=0PI&tD<| z6r7C^oM=7BL4+KPy&Rg}ap2@eZ~m_;Gmjpx52kPnaUyBbYd2gz-rCyP_m7Y`olhq6 zxpMp~*kcX}KE2dgKZa!ax?=Rl8)rxfAboBE2D4~o{TuqQMOlv4@4lP9=VL!5b44WUV_j z9XIfdYjXDIQTJSMzOF(MCzn z&PK`6_#6nt(&@F&Xw!8vLX0C-&OPRD#t2Xyy779dmhG*n>1e~xZ=F5=@EqS*@fu8{ zq;Kk|ZSC~SgjV$TBeP$39qFtBW0;1vkB2ieAJtYih!Mo7A(0Ssm_Xea%fMPTvNojY zJ(HmKU@!`W^j*3s4AS4Z8#koa5n#ji-2VmR*sWa2mVbKhiUn4Guz&LSHkAYgWq_ls~AY@Kl##`Rs z^{;y|Q``#$3S#e*47cNu<1HzZe)6br2?X|lyZt2|cZz6ichT&**MGz*fme*o4N;_* z`R4+E(V$7CN!8trSxK)Co*T!b;+WvN7hGvB@gOPo+$i12(0WY}uOrYt0;dSjfxC-m z7Tx4U<0jbIP7K8ou*o4?&*EQE8uO+p4(Eb6N`Ks)HEi%obD?xvU9yleLdF1MP8m4E zlL5RRAA(B#2hsv^)o+C5_NCuK+z-$pu2l$d>Gz!4Aqect!K?oAjZ~yms)C@U{A0X- zlR)qb>x);%(y21^$ggCRWp0EgVBL;nb5THaNZ`8SgRj02c+UPVIb{P~7U=1Kj%Y;1 z)|Gt|u|Xnn7r1^iKj!;N+SKK{!Ml`BrpOr1{SZuG1*^YA9!GC;<{?E}MXw+>JhXdz zyPhM4s+XIl(0#2La>l(PqI%Vk8W+^eLlSsdfH3im=l^m0$T@nNvjn-4zVw>kQ1m)_ z!+f#3z@};WTvMT4*Xwdt>EmRK^VGl+>)QV}kmqJkm0P){o?J0>UG}-{WqTR~yr*{g zEn*~=)n-7Ss~wZrWlp7(p-5cJEu!Lq0Aw3dC({M1o%}1gJu6Eo(Z+9La9bBO-WKP= zD&Fw>Rh}*c^oDBm4jQZN1?KgR2g)#9dz}+V1!>!&6FX?Bl-?f&yQvszIM$I9{wJKJagn7y+eFTI z%|eqs2i71m`0gcUcO&G#0CDaWOZ)>83W%-}f#0ivSbHN%Z^H2sP$jS-2!{aNf%xfx z#JqNS@dvz;KU<`1e%$X{V$oMY245&`Zd1f-d8;8h6O5ZflV5jUNjMB+ZrrBJsmV z^kc7;ln_p@Rw54-4_*02FI(vMR@%|W%G<{qxju78UOBI&(8HElnw>n5Q($PO# zfc^SyQPSWP{||8DfBn9L9cbC?jtw4hoBGQR+&C4O&!;i^*@pw^HGR}>00TFjn##<6 zt#UB(yg^e4ZA@oVll34A3q(aci5wOdI^R=Mr7$@=88h(H z$>5W=JMo+-rmY7y>0tM(s-NL_Y$<*t^OakM(PM6jVEt!e!GdXGDtgHx{9|Luhr$z; zNyzL77V0d=|3cqyum*OH*j|%Fc`S*vkILe-+)iw^<|RU&PqRO zX&H56QH_S?5IB!(IP&F1Cq(#^Xx#S&nz_xc&TA?*L!n3B%_&2%5C!kzcq5oC>oLFN(qV(x1mXAtV$$B&1X) zRe-$=Nx2rWgCOrg@P9Lu_^&qgr6$YX%b@a{O> zgl-uFlq7*Fok`xr*Zu3rg*a=lTyc$2x#}l|cDUw)9Z!rnDWZ<+O~kQvZ(Rp5jgjvF3%={*YwG&s=g99j}5wI@n&JSQmi3J-4Q?z)(NO{_00cj|MPi};=y z-xqa*y<|77^>6A)|F;(G9ZCvFT{%lwj5c{pNBr-ibeBtS$-uD&@12MKH8DF; z3`#|`z67k-Z|{>RY94gs6{(2dj_LWowSY)eT9m$og5?m}!6o8y>?rS`Rm5+;#*dbG zS5ylr-MqZmD0>7^13%K)=e2ALFTQDDdJ96ktCU(4-P;TUciJ!7IYKO(0pNAXi7*4p zL9h>mJu^@BW7QO#I#0PhTO<4Zh_FSYzX0|LvJIM25}z%kfNj}Zt3R>3%fWVAr~dJk zx?L8otLP+h>9=JdwEKgR;2yb%6FUh|GNm3Z0evXsy}Wehw*YsB5EbyaVC?HzlC(A{ ztGy9{?j=(}S6@a{riss#ERQI`7QI&|<%x}IDw3vM7?u@8Pkz!>`=ia{AQ(n3o)4gY z?`=)dz{*6hIkMf2gbott!!yM6`B%x){(~7^UgL*|KMqi`1zkz`_})BDeAaM_tij%r zK{4$ivC&Dvj5~tz69^f6c@AK*JLU)cGXt1h?DM*k-_qWBZpRweoRxhHzF`c zc`~`G#5?^YZgTCufy8DR8yrAawowdu`!e#So>1t-VpJqFK@5$6*SprQ1s*VTz5KyM;hg4iqxSu&xk znP1P{iO+ZnK=v4~i<@p-#6~mI1KQH&pT=RAMzVPSXp^240eyJ17bt=--)&F?Ov}&j zojfR{0)fFi%+ovzD9OQx<|nQ2Q^sQt6*r$p96NKVY9XPZ;e}c_qA6bZu&*wjb9#C* zJ3SGKhAsDJc^37W-)VJ8`UDyH6Th`IRKi&Ae_ejg>mC#oP;0#;J{c z9vCCZ~0yeWl0!>68Y0lLlWVH255zH@Ct5 z`b##QfFsTCp@9AA5JfoW8vTkc(3d9zQG!IOiHM4hji)+4&V4G}Q4a1U$Wwst*>DK= ziMF&D5a)3>szXbZH2r8snbsKaz*QLs!E6%nZ{ z=kdV`ZZj|>@bz(XKLYVxPwY6zrZw37vTD3Nog41HJn{Z+^@fGlOwlKKkE!XL>}LzG zmn>1>bIXnLa$nDa4h^bNNq~V#8c9{k@6V80WeMm#6#}W~LK{+I9}7P7yd zmHNYy+a>)@Z^fr9o31)kvKUywSYFQGYn%579op)-8hI1_Znz>W{BRqn)N|!$&t+Ci~mfPFLiD@0_f>!6K{srT@uCMosEn+a`QM8v=$WOjgAy9OwhG(!I+g z2|h@-;G|>ak6&H@TBiQ7BP%z=MoI`)lY-LSavZrz8C>=(c>7ssE=oZpaLGA=as)4a zum)7@0!L#N8$o7Mc(hvBhS(B32{R3}oS@+|t; zwld~&9Cpa#@jNahQ$Ru)7<_GD420`+T)~hG$Z8Vg{biOTq}t|7NnTkQ+1p5gTDo!= zcIHA)yf%U(pih*2&hB0+X6jIOU%2-+ex;e5L4IhKPEQ?XoucS@Lm#=X`;&Ij!wkpk$B$N4l0r;+z1zaFgPUoC{(*3Yw7 zdBI?^X3c)@st*3`<6IPFVRx|8mWn05M7EqJ51erc)SO-4d|4uE0H>RSV`Zzcbw0r< zV3EHA==}(H;Rk4<#Zwt{Z0^zQP>0sk58e{($%3P}71eigrJ9c7R9qIwEwfoO^GaYS zznf?nuR(GjdqAv;+WhFaI+#_5xzm_8%jQdJZf!ECT=9X_YG_V!T8#CE3)og*P#@(j zQ>`0O?M2|m_8|FIZ7#HGD8^C_gSy}6T)=L%U2}H(yeKZuz6$<^lZ@>$fQ6x&=LCmd zfizA3b!WSiAg|c3EO-JTYwm}phlih(KJX-<;3LM*Z;m{>gGr&{$p^6Jnxv!vjvbf- zz8;5r1MD3=OfV4YaaG`_rW6ZWu-Xmp5+#>21Q>bv8>;4RM+ogwDFBlTiA0JpF-!Y= zWFMGx0NBALuSg#u0@ElTXq;OkO<|Y!?zH(EDCI`=?1&&u5=a5wrB?cWc>fXUx!vAv zJzvof@q8h5zxNgC=PUcYFM^$rtf*ehxZk@t=-v6@v&Z|rpWB7$3S~X$epi92;N3^D z-{t;zRD5Iym3;d&LV}2$kdVC?xg}dhrrg_Zx`r1v*p7bx)3g zIP^|^r3pRHwnqDP7eglEAeEFJf?6QF2M!!yvi0-xi`gC@jCvkG&gp5znM!^;fV`Sv z&97mO^jtM1xTAIw6kXLf|72q$bBgyAffep@#@lKjR!xX+?mu$@Q*vQ)AtnBX5E}uh zHW#}8;9;R4eis=YdT6g5@VYq@NS+##eQN(bC38T4%vH^I`yzBP6h?xX3X^yLgXw%f ztOOh3{Z6*ODx}8XEZ2B62l&o=WQ;NCy+7$83G(i#rCBi&@TaAwr<#B1YS8}x&S<7I From 8d7a76718cfbac293ae92e50fe5bd25690c4cd63 Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 24 Jul 2023 18:01:06 +0200 Subject: [PATCH 103/111] Add API for dependency injection --- DDD.sln | 14 +++ ...AppRegistrationOptionsBuilderExtensions.cs | 34 ++++++++ .../ContainerExtensions.cs | 36 -------- .../ContainerExtensions.cs | 8 +- .../AppRegistrationOptions.cs | 34 ++++---- .../ExtendableRegistrationOptionsBuilder.cs | 34 ++++++++ .../IExtendableRegistrationOptionsBuilder.cs | 19 ++++ .../ContainerExtensionsTests.cs | 87 +++++++++++++++++++ ...Injector.FluentValidation.UnitTests.csproj | 19 ++++ .../FakeObject.cs} | 6 +- .../FakeObjectValidator.cs | 8 ++ .../Application/FakeCommand.cs | 6 ++ .../{ => Application}/FakeCommandHandler.cs | 7 +- .../{ => Application}/FakeEventHandler.cs | 17 ++-- .../{ => Application}/FakeMapper.cs | 3 +- .../Application/FakeQuery.cs | 6 ++ .../{ => Application}/FakeQueryHandler.cs | 7 +- .../{ => Application}/FakeTranslator.cs | 3 +- .../DDD.Core.SimpleInjector.UnitTests.csproj | 2 +- .../{ => Domain}/DummyContext.cs | 4 +- .../{ => Domain}/FakeContext.cs | 4 +- .../{ => Domain}/FakeEvent.cs | 4 +- .../FakeQuery.cs | 8 -- .../ContainerExtensionsTests.cs | 1 + .../DependencyInjection}/Decorated.cs | 0 .../DependencyInjection}/FirstDecorator.cs | 0 .../FirstDelegatingDecorator.cs | 0 .../DependencyInjection}/IDoSomething.cs | 0 .../DependencyInjection}/SecondDecorator.cs | 0 .../SecondDelegatingDecorator.cs | 0 30 files changed, 274 insertions(+), 97 deletions(-) create mode 100644 Src/DDD.Core.SimpleInjector.FluentValidation/AppRegistrationOptionsBuilderExtensions.cs delete mode 100644 Src/DDD.Core.SimpleInjector.FluentValidation/ContainerExtensions.cs create mode 100644 Src/DDD.Core/Infrastructure/DependencyInjection/ExtendableRegistrationOptionsBuilder.cs create mode 100644 Src/DDD.Core/Infrastructure/DependencyInjection/IExtendableRegistrationOptionsBuilder.cs create mode 100644 Test/DDD.Core.SimpleInjector.FluentValidation.UnitTests/ContainerExtensionsTests.cs create mode 100644 Test/DDD.Core.SimpleInjector.FluentValidation.UnitTests/DDD.Core.SimpleInjector.FluentValidation.UnitTests.csproj rename Test/{DDD.Core.SimpleInjector.UnitTests/FakeCommand.cs => DDD.Core.SimpleInjector.FluentValidation.UnitTests/FakeObject.cs} (52%) create mode 100644 Test/DDD.Core.SimpleInjector.FluentValidation.UnitTests/FakeObjectValidator.cs create mode 100644 Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeCommand.cs rename Test/DDD.Core.SimpleInjector.UnitTests/{ => Application}/FakeCommandHandler.cs (76%) rename Test/DDD.Core.SimpleInjector.UnitTests/{ => Application}/FakeEventHandler.cs (67%) rename Test/DDD.Core.SimpleInjector.UnitTests/{ => Application}/FakeMapper.cs (82%) create mode 100644 Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeQuery.cs rename Test/DDD.Core.SimpleInjector.UnitTests/{ => Application}/FakeQueryHandler.cs (78%) rename Test/DDD.Core.SimpleInjector.UnitTests/{ => Application}/FakeTranslator.cs (92%) rename Test/DDD.Core.SimpleInjector.UnitTests/{ => Domain}/DummyContext.cs (72%) rename Test/DDD.Core.SimpleInjector.UnitTests/{ => Domain}/FakeContext.cs (71%) rename Test/DDD.Core.SimpleInjector.UnitTests/{ => Domain}/FakeEvent.cs (70%) delete mode 100644 Test/DDD.Core.SimpleInjector.UnitTests/FakeQuery.cs rename Test/DDD.Core.SimpleInjector.UnitTests/{ => Infrastructure/DependencyInjection}/ContainerExtensionsTests.cs (99%) rename Test/DDD.Core.SimpleInjector.UnitTests/{ => Infrastructure/DependencyInjection}/Decorated.cs (100%) rename Test/DDD.Core.SimpleInjector.UnitTests/{ => Infrastructure/DependencyInjection}/FirstDecorator.cs (100%) rename Test/DDD.Core.SimpleInjector.UnitTests/{ => Infrastructure/DependencyInjection}/FirstDelegatingDecorator.cs (100%) rename Test/DDD.Core.SimpleInjector.UnitTests/{ => Infrastructure/DependencyInjection}/IDoSomething.cs (100%) rename Test/DDD.Core.SimpleInjector.UnitTests/{ => Infrastructure/DependencyInjection}/SecondDecorator.cs (100%) rename Test/DDD.Core.SimpleInjector.UnitTests/{ => Infrastructure/DependencyInjection}/SecondDelegatingDecorator.cs (100%) diff --git a/DDD.sln b/DDD.sln index 9e76a58..1c3d99c 100644 --- a/DDD.sln +++ b/DDD.sln @@ -74,6 +74,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.NSubstitute", "Src 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("{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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -184,6 +188,14 @@ Global {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -215,6 +227,8 @@ Global {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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {983C3AB4-301E-47A3-93FF-2E4BD8F2090F} 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/ContainerExtensions.cs b/Src/DDD.Core.SimpleInjector.FluentValidation/ContainerExtensions.cs deleted file mode 100644 index 6989d35..0000000 --- a/Src/DDD.Core.SimpleInjector.FluentValidation/ContainerExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -using FluentValidation; -using SimpleInjector; -using System.Collections.Generic; -using System.Reflection; -using System; -using DDD.Validation; - -namespace DDD.Core.Infrastructure.DependencyInjection -{ - using Validation; - using EnsureThat; - - public static class ContainerExtensions - { - - #region Methods - - ///

- /// Registers object validators. - /// - /// The container that registers validators. - /// The assemblies that contain validators. - /// A predicate that determines which validators should be registered. - public static void RegisterValidators(this Container container, IEnumerable assemblies, Func predicate = null) - { - Ensure.That(container, nameof(container)).IsNotNull(); - var notNullPredicate = predicate ?? (t => true); - container.RegisterConditional(typeof(IValidator<>), assemblies, notNullPredicate); - container.Register(typeof(ISyncObjectValidator<>), typeof(FluentValidatorAdapter<>)); - container.Register(typeof(IAsyncObjectValidator<>), typeof(FluentValidatorAdapter<>)); - } - - #endregion Methods - - } -} diff --git a/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs index 78d7fac..5c1a05a 100644 --- a/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs +++ b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs @@ -58,13 +58,14 @@ public static IEnumerable GetDecoratorChainOf(this Con /// /// Configures application core components. /// - public static void ConfigureApp(this Container container, Action configureOptions) + 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 builder = new AppRegistrationOptions.Builder(); + var extendableBuilder = (IExtendableRegistrationOptionsBuilder)builder; configureOptions(builder); - var appOptions = ((IObjectBuilder)builder).Build(); + var appOptions = extendableBuilder.Build(); container.Options.DefaultScopedLifestyle = Lifestyle.CreateHybrid(new ThreadScopedLifestyle(), new AsyncScopedLifestyle()); container.RegisterInstance(container); container.RegisterLogger(appOptions); @@ -109,6 +110,7 @@ public static void ConfigureApp(this Container container, Action(); container.RegisterMappersAndTranslators(appOptions); } + extendableBuilder.ApplyExtensions(container); } public static void RegisterConditional(this Container container, diff --git a/Src/DDD.Core/Infrastructure/DependencyInjection/AppRegistrationOptions.cs b/Src/DDD.Core/Infrastructure/DependencyInjection/AppRegistrationOptions.cs index 9a9420d..9d0b043 100644 --- a/Src/DDD.Core/Infrastructure/DependencyInjection/AppRegistrationOptions.cs +++ b/Src/DDD.Core/Infrastructure/DependencyInjection/AppRegistrationOptions.cs @@ -58,7 +58,8 @@ private AppRegistrationOptions() #region Classes - public class Builder : FluentBuilder + public class Builder : ExtendableRegistrationOptionsBuilder + where TContainer : class { #region Fields @@ -69,35 +70,35 @@ public class Builder : FluentBuilder #region Methods - public Builder RegisterTypesFrom(IEnumerable assemblies) + public Builder RegisterTypesFrom(IEnumerable assemblies) { Ensure.That(assemblies, nameof(assemblies)).IsNotNull(); this.options.assembliesToScan.AddRange(assemblies); return this; } - public Builder RegisterTypesFrom(params Assembly[] assemblies) + public Builder RegisterTypesFrom(params Assembly[] assemblies) { Ensure.That(assemblies, nameof(assemblies)).IsNotNull(); this.options.assembliesToScan.AddRange(assemblies); return this; } - public Builder FilterTypesWith(Func filter) + public Builder FilterTypesWith(Func filter) { Ensure.That(filter, nameof(filter)).IsNotNull(); this.options.TypeFilter = filter; return this; } - public Builder RegisterSerializer(SerializationFormat format, Func serializerCreator) + 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) + public Builder ConfigureDbConnectionFor(Action> configureOptions) where TContext : BoundedContext { Ensure.That(configureOptions, nameof(configureOptions)).IsNotNull(); @@ -107,26 +108,26 @@ public Builder ConfigureDbConnectionFor(Action optionsCollection) + public Builder ConfigureDbConnections(IEnumerable optionsCollection) { Ensure.That(optionsCollection, nameof(optionsCollection)).IsNotNull(); options.dbConnectionOptionsCollection.AddRange(optionsCollection); return this; } - public Builder ConfigureDbConnections(params DbConnectionOptions[] optionsCollection) + public Builder ConfigureDbConnections(params DbConnectionOptions[] optionsCollection) { Ensure.That(optionsCollection, nameof(optionsCollection)).IsNotNull(); options.dbConnectionOptionsCollection.AddRange(optionsCollection); return this; } - public Builder ConfigureCommands() + public Builder ConfigureCommands() { return this.ConfigureCommands(_ => { }); } - public Builder ConfigureCommands(Action configureOptions) + public Builder ConfigureCommands(Action configureOptions) { Ensure.That(configureOptions, nameof(configureOptions)).IsNotNull(); var builder = new CommandsRegistrationOptions.Builder(); @@ -135,12 +136,12 @@ public Builder ConfigureCommands(Action con return this; } - public Builder ConfigureQueries() + public Builder ConfigureQueries() { return this.ConfigureQueries(_ => { }); } - public Builder ConfigureQueries(Action configureOptions) + public Builder ConfigureQueries(Action configureOptions) { Ensure.That(configureOptions, nameof(configureOptions)).IsNotNull(); var builder = new QueriesRegistrationOptions.Builder(); @@ -149,7 +150,7 @@ public Builder ConfigureQueries(Action confi return this; } - public Builder ConfigureEvents(Action configureOptions) + public Builder ConfigureEvents(Action configureOptions) { Ensure.That(configureOptions, nameof(configureOptions)).IsNotNull(); var builder = new EventsRegistrationOptions.Builder(); @@ -158,12 +159,12 @@ public Builder ConfigureEvents(Action configu return this; } - public Builder ConfigureMapping() + public Builder ConfigureMapping() { return this.ConfigureMapping(_ => { }); } - public Builder ConfigureMapping(Action configureOptions) + public Builder ConfigureMapping(Action configureOptions) { Ensure.That(configureOptions, nameof(configureOptions)).IsNotNull(); var builder = new MappingRegistrationOptions.Builder(); @@ -172,7 +173,7 @@ public Builder ConfigureMapping(Action confi return this; } - public Builder ConfigureLogging(Func loggerFactoryCreator) + public Builder ConfigureLogging(Func loggerFactoryCreator) { Ensure.That(loggerFactoryCreator, nameof(loggerFactoryCreator)).IsNotNull(); this.options.loggerFactoryCreator = loggerFactoryCreator; @@ -182,7 +183,6 @@ public Builder ConfigureLogging(Func loggerFactoryCreator) protected override AppRegistrationOptions 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/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.UnitTests/FakeCommand.cs b/Test/DDD.Core.SimpleInjector.FluentValidation.UnitTests/FakeObject.cs similarity index 52% rename from Test/DDD.Core.SimpleInjector.UnitTests/FakeCommand.cs rename to Test/DDD.Core.SimpleInjector.FluentValidation.UnitTests/FakeObject.cs index 4693dba..b6a176f 100644 --- a/Test/DDD.Core.SimpleInjector.UnitTests/FakeCommand.cs +++ b/Test/DDD.Core.SimpleInjector.FluentValidation.UnitTests/FakeObject.cs @@ -1,8 +1,6 @@ namespace DDD.Core.Infrastructure.DependencyInjection { - using Application; - - public class FakeCommand : ICommand + public class FakeObject { } -} \ No newline at end of file +} 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.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/FakeCommandHandler.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeCommandHandler.cs similarity index 76% rename from Test/DDD.Core.SimpleInjector.UnitTests/FakeCommandHandler.cs rename to Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeCommandHandler.cs index fdc155c..2d805c8 100644 --- a/Test/DDD.Core.SimpleInjector.UnitTests/FakeCommandHandler.cs +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeCommandHandler.cs @@ -1,8 +1,7 @@ -namespace DDD.Core.Infrastructure.DependencyInjection -{ - using Application; - using System.Threading.Tasks; +using System.Threading.Tasks; +namespace DDD.Core.Application +{ public class FakeCommandHandler : ICommandHandler { #region Methods diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/FakeEventHandler.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeEventHandler.cs similarity index 67% rename from Test/DDD.Core.SimpleInjector.UnitTests/FakeEventHandler.cs rename to Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeEventHandler.cs index b76711c..2c1e1aa 100644 --- a/Test/DDD.Core.SimpleInjector.UnitTests/FakeEventHandler.cs +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeEventHandler.cs @@ -1,13 +1,12 @@ using System; +using System.Threading.Tasks; -namespace DDD.Core.Infrastructure.DependencyInjection +namespace DDD.Core.Application { - using Application; - using DDD.Core.Domain; - using System.Threading.Tasks; - - public class FakeEventHandler - : ISyncEventHandler, + using Domain; + + public class FakeEventHandler + : ISyncEventHandler, IAsyncEventHandler { @@ -17,9 +16,9 @@ public class FakeEventHandler public Type EventType => typeof(FakeEvent); - BoundedContext ISyncEventHandler.Context => this.Context; + BoundedContext ISyncEventHandler.Context => Context; - BoundedContext IAsyncEventHandler.Context => this.Context; + BoundedContext IAsyncEventHandler.Context => Context; #endregion Properties diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/FakeMapper.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeMapper.cs similarity index 82% rename from Test/DDD.Core.SimpleInjector.UnitTests/FakeMapper.cs rename to Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeMapper.cs index 7e40ede..d6e1a6a 100644 --- a/Test/DDD.Core.SimpleInjector.UnitTests/FakeMapper.cs +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeMapper.cs @@ -1,5 +1,6 @@ -namespace DDD.Core.Infrastructure.DependencyInjection +namespace DDD.Core.Application { + using Domain; using Mapping; public class FakeMapper : IObjectMapper 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/FakeQueryHandler.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeQueryHandler.cs similarity index 78% rename from Test/DDD.Core.SimpleInjector.UnitTests/FakeQueryHandler.cs rename to Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeQueryHandler.cs index 74c5be0..818405e 100644 --- a/Test/DDD.Core.SimpleInjector.UnitTests/FakeQueryHandler.cs +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeQueryHandler.cs @@ -1,8 +1,7 @@ -namespace DDD.Core.Infrastructure.DependencyInjection -{ - using Application; - using System.Threading.Tasks; +using System.Threading.Tasks; +namespace DDD.Core.Application +{ public class FakeQueryHandler : IQueryHandler { #region Methods diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/FakeTranslator.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeTranslator.cs similarity index 92% rename from Test/DDD.Core.SimpleInjector.UnitTests/FakeTranslator.cs rename to Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeTranslator.cs index b6c4c15..ffd46b7 100644 --- a/Test/DDD.Core.SimpleInjector.UnitTests/FakeTranslator.cs +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Application/FakeTranslator.cs @@ -1,7 +1,8 @@ using System; -namespace DDD.Core.Infrastructure.DependencyInjection +namespace DDD.Core.Application { + using Domain; using Mapping; public class FakeTranslator : IObjectTranslator diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/DDD.Core.SimpleInjector.UnitTests.csproj b/Test/DDD.Core.SimpleInjector.UnitTests/DDD.Core.SimpleInjector.UnitTests.csproj index 2f8eacc..18157c0 100644 --- a/Test/DDD.Core.SimpleInjector.UnitTests/DDD.Core.SimpleInjector.UnitTests.csproj +++ b/Test/DDD.Core.SimpleInjector.UnitTests/DDD.Core.SimpleInjector.UnitTests.csproj @@ -1,7 +1,7 @@  net48;net6.0 - DDD.Core.Infrastructure.DependencyInjection + DDD.Core diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/DummyContext.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Domain/DummyContext.cs similarity index 72% rename from Test/DDD.Core.SimpleInjector.UnitTests/DummyContext.cs rename to Test/DDD.Core.SimpleInjector.UnitTests/Domain/DummyContext.cs index 475b994..434861b 100644 --- a/Test/DDD.Core.SimpleInjector.UnitTests/DummyContext.cs +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Domain/DummyContext.cs @@ -1,7 +1,5 @@ -namespace DDD.Core.Infrastructure.DependencyInjection +namespace DDD.Core.Domain { - using Domain; - public class DummyContext : BoundedContext { #region Constructors diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/FakeContext.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Domain/FakeContext.cs similarity index 71% rename from Test/DDD.Core.SimpleInjector.UnitTests/FakeContext.cs rename to Test/DDD.Core.SimpleInjector.UnitTests/Domain/FakeContext.cs index e20fb35..e0c004c 100644 --- a/Test/DDD.Core.SimpleInjector.UnitTests/FakeContext.cs +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Domain/FakeContext.cs @@ -1,7 +1,5 @@ -namespace DDD.Core.Infrastructure.DependencyInjection +namespace DDD.Core.Domain { - using Domain; - public class FakeContext : BoundedContext { #region Constructors diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/FakeEvent.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Domain/FakeEvent.cs similarity index 70% rename from Test/DDD.Core.SimpleInjector.UnitTests/FakeEvent.cs rename to Test/DDD.Core.SimpleInjector.UnitTests/Domain/FakeEvent.cs index 13d4014..7557110 100644 --- a/Test/DDD.Core.SimpleInjector.UnitTests/FakeEvent.cs +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Domain/FakeEvent.cs @@ -1,9 +1,7 @@ using System; -namespace DDD.Core.Infrastructure.DependencyInjection +namespace DDD.Core.Domain { - using Domain; - public class FakeEvent : IEvent { diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/FakeQuery.cs b/Test/DDD.Core.SimpleInjector.UnitTests/FakeQuery.cs deleted file mode 100644 index 6d9e3a6..0000000 --- a/Test/DDD.Core.SimpleInjector.UnitTests/FakeQuery.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace DDD.Core.Infrastructure.DependencyInjection -{ - using Application; - - public class FakeQuery : IQuery - { - } -} diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/ContainerExtensionsTests.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/ContainerExtensionsTests.cs similarity index 99% rename from Test/DDD.Core.SimpleInjector.UnitTests/ContainerExtensionsTests.cs rename to Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/ContainerExtensionsTests.cs index 74b839f..f11ed62 100644 --- a/Test/DDD.Core.SimpleInjector.UnitTests/ContainerExtensionsTests.cs +++ b/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/ContainerExtensionsTests.cs @@ -401,6 +401,7 @@ public void ConfigureApp_WithRegisterTypesFrom_RegistersBoundedContextsInCollect var contexts = registration.GetInstance(); contexts.Should().BeEquivalentTo(expectedContexts); } + [Fact] public void ConfigureApp_WithRegisterTypesFrom_RegistersSameInstancesOfBoundedContextsAsSingletonAndInCollection() { diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/Decorated.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/Decorated.cs similarity index 100% rename from Test/DDD.Core.SimpleInjector.UnitTests/Decorated.cs rename to Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/Decorated.cs diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/FirstDecorator.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/FirstDecorator.cs similarity index 100% rename from Test/DDD.Core.SimpleInjector.UnitTests/FirstDecorator.cs rename to Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/FirstDecorator.cs diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/FirstDelegatingDecorator.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/FirstDelegatingDecorator.cs similarity index 100% rename from Test/DDD.Core.SimpleInjector.UnitTests/FirstDelegatingDecorator.cs rename to Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/FirstDelegatingDecorator.cs diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/IDoSomething.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/IDoSomething.cs similarity index 100% rename from Test/DDD.Core.SimpleInjector.UnitTests/IDoSomething.cs rename to Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/IDoSomething.cs diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/SecondDecorator.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/SecondDecorator.cs similarity index 100% rename from Test/DDD.Core.SimpleInjector.UnitTests/SecondDecorator.cs rename to Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/SecondDecorator.cs diff --git a/Test/DDD.Core.SimpleInjector.UnitTests/SecondDelegatingDecorator.cs b/Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/SecondDelegatingDecorator.cs similarity index 100% rename from Test/DDD.Core.SimpleInjector.UnitTests/SecondDelegatingDecorator.cs rename to Test/DDD.Core.SimpleInjector.UnitTests/Infrastructure/DependencyInjection/SecondDelegatingDecorator.cs From aa7f3f85f48f2e6a70eb7d14ee85ec079a9c86e5 Mon Sep 17 00:00:00 2001 From: draphyz Date: Tue, 25 Jul 2023 11:37:08 +0200 Subject: [PATCH 104/111] Add API for dependency injection --- DDD.sln | 14 ++++ .../DelegatingSessionFactory.cs | 12 ++-- ...AppRegistrationOptionsBuilderExtensions.cs | 64 +++++++++++++++++++ .../DDD.Core.SimpleInjector.NHibernate.csproj | 12 ++++ .../Properties/AssemblyInfo.cs | 7 ++ Src/DDD.Core.Xunit/IDbFixture.cs | 2 +- Src/DDD.Core.Xunit/IDbFixtureExtensions.cs | 4 +- ...SimpleInjector.NHibernate.UnitTests.csproj | 18 ++++++ .../Domain/FakeContext.cs | 13 ++++ .../ContainerExtensionsTests.cs | 58 +++++++++++++++++ .../Infrastructure/OracleFixture.cs | 1 + .../Infrastructure/SqlServerFixture.cs | 3 +- 12 files changed, 199 insertions(+), 9 deletions(-) create mode 100644 Src/DDD.Core.SimpleInjector.NHibernate/AppRegistrationOptionsBuilderExtensions.cs create mode 100644 Src/DDD.Core.SimpleInjector.NHibernate/DDD.Core.SimpleInjector.NHibernate.csproj create mode 100644 Src/DDD.Core.SimpleInjector.NHibernate/Properties/AssemblyInfo.cs create mode 100644 Test/DDD.Core.SimpleInjector.NHibernate.UnitTests/DDD.Core.SimpleInjector.NHibernate.UnitTests.csproj create mode 100644 Test/DDD.Core.SimpleInjector.NHibernate.UnitTests/Domain/FakeContext.cs create mode 100644 Test/DDD.Core.SimpleInjector.NHibernate.UnitTests/Infrastructure/DependencyInjection/ContainerExtensionsTests.cs diff --git a/DDD.sln b/DDD.sln index 1c3d99c..a637b7b 100644 --- a/DDD.sln +++ b/DDD.sln @@ -78,6 +78,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.Core.SimpleInjector.Flu EndProject 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.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.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 Debug|Any CPU = Debug|Any CPU @@ -196,6 +200,14 @@ Global {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 @@ -229,6 +241,8 @@ Global {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/Src/DDD.Core.NHibernate/DelegatingSessionFactory.cs b/Src/DDD.Core.NHibernate/DelegatingSessionFactory.cs index 72aae5f..b8e7e4a 100644 --- a/Src/DDD.Core.NHibernate/DelegatingSessionFactory.cs +++ b/Src/DDD.Core.NHibernate/DelegatingSessionFactory.cs @@ -10,7 +10,7 @@ namespace DDD.Core.Infrastructure.Data using Domain; public class DelegatingSessionFactory : ISessionFactory, IDisposable - where TContext : BoundedContext, new() + where TContext : BoundedContext { #region Fields @@ -25,10 +25,12 @@ public class DelegatingSessionFactory : ISessionFactory, IDi #region Constructors - public DelegatingSessionFactory(Configuration configuration, + 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(); @@ -36,7 +38,7 @@ public DelegatingSessionFactory(Configuration configuration, this.options = options; this.asyncOptions = asyncOptions; this.lazySessionFactory = new Lazy(() => this.configuration.BuildSessionFactory()); - this.Context = new TContext(); + this.Context = context; } #endregion Constructors @@ -51,12 +53,12 @@ public DelegatingSessionFactory(Configuration configuration, #region Methods - public static ISessionFactory Create(Configuration configuration, Action options) + 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(configuration, options, asyncOptions); + return new DelegatingSessionFactory(context, configuration, options, asyncOptions); } public ISession CreateSession() 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..e860c3e --- /dev/null +++ b/Src/DDD.Core.SimpleInjector.NHibernate/DDD.Core.SimpleInjector.NHibernate.csproj @@ -0,0 +1,12 @@ + + + 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..9d72888 --- /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 validation 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.Xunit/IDbFixture.cs b/Src/DDD.Core.Xunit/IDbFixture.cs index 34154fa..c01b83f 100644 --- a/Src/DDD.Core.Xunit/IDbFixture.cs +++ b/Src/DDD.Core.Xunit/IDbFixture.cs @@ -6,7 +6,7 @@ namespace DDD.Core.Infrastructure.Testing using Domain; public interface IDbFixture - where TContext : BoundedContext, new() + where TContext : BoundedContext { #region Methods diff --git a/Src/DDD.Core.Xunit/IDbFixtureExtensions.cs b/Src/DDD.Core.Xunit/IDbFixtureExtensions.cs index 4ae3885..8d52e69 100644 --- a/Src/DDD.Core.Xunit/IDbFixtureExtensions.cs +++ b/Src/DDD.Core.Xunit/IDbFixtureExtensions.cs @@ -13,7 +13,7 @@ public static class IDbFixtureExtensions #region Methods public static DbConnection CreateOpenConnection(this IDbFixture fixture, bool pooling = true) - where TContext : BoundedContext, new() + where TContext : BoundedContext { Ensure.That(fixture, nameof(fixture)).IsNotNull(); var connection = fixture.CreateConnection(pooling); @@ -24,7 +24,7 @@ public static DbConnection CreateOpenConnection(this IDbFixture CreateOpenConnectionAsync(this IDbFixture fixture, CancellationToken cancellationToken = default, bool pooling = true) - where TContext : BoundedContext, new() + where TContext : BoundedContext { Ensure.That(fixture, nameof(fixture)).IsNotNull(); var connection = fixture.CreateConnection(pooling); 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.HealthcareDelivery.IntegrationTests/Infrastructure/OracleFixture.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleFixture.cs index 9362a17..d58dd69 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleFixture.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/OracleFixture.cs @@ -46,6 +46,7 @@ public DelegatingSessionFactory CreateSessionFactory( }); return new DelegatingSessionFactory ( + new HealthcareDeliveryContext(), configuration, options => { diff --git a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerFixture.cs b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerFixture.cs index 84c79a8..7e5da39 100644 --- a/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerFixture.cs +++ b/Test/DDD.HealthcareDelivery.IntegrationTests/Infrastructure/SqlServerFixture.cs @@ -46,6 +46,7 @@ public DelegatingSessionFactory CreateSessionFactory( }); return new DelegatingSessionFactory ( + new HealthcareDeliveryContext(), configuration, options => { @@ -57,7 +58,7 @@ public DelegatingSessionFactory CreateSessionFactory( var connection = await connectionProvider.GetOpenConnectionAsync(cancellationToken); options.Connection(connection); } - ); + ) ; } protected override void CreateDatabase() From fb9ecaee3898a72923b210fc2fd256f20e941635 Mon Sep 17 00:00:00 2001 From: draphyz Date: Tue, 25 Jul 2023 12:01:09 +0200 Subject: [PATCH 105/111] Add API for dependency injection --- .../DDD.Core.SimpleInjector.NHibernate.csproj | 3 +++ .../Properties/AssemblyInfo.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Src/DDD.Core.SimpleInjector.NHibernate/DDD.Core.SimpleInjector.NHibernate.csproj b/Src/DDD.Core.SimpleInjector.NHibernate/DDD.Core.SimpleInjector.NHibernate.csproj index e860c3e..c01d0bd 100644 --- a/Src/DDD.Core.SimpleInjector.NHibernate/DDD.Core.SimpleInjector.NHibernate.csproj +++ b/Src/DDD.Core.SimpleInjector.NHibernate/DDD.Core.SimpleInjector.NHibernate.csproj @@ -5,6 +5,9 @@ DDD.Core.Infrastructure.DependencyInjection false + + + diff --git a/Src/DDD.Core.SimpleInjector.NHibernate/Properties/AssemblyInfo.cs b/Src/DDD.Core.SimpleInjector.NHibernate/Properties/AssemblyInfo.cs index 9d72888..0da677b 100644 --- a/Src/DDD.Core.SimpleInjector.NHibernate/Properties/AssemblyInfo.cs +++ b/Src/DDD.Core.SimpleInjector.NHibernate/Properties/AssemblyInfo.cs @@ -2,6 +2,6 @@ using System.Runtime.InteropServices; [assembly: AssemblyTitle("DDD.Core.SimpleInjector.NHibernate")] -[assembly: AssemblyDescription("Extensions for SimpleInjector to register validation components based on 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 From 5940535a9b48af8255e3a9db8d0ce6d9adf0b777 Mon Sep 17 00:00:00 2001 From: draphyz Date: Tue, 1 Aug 2023 12:05:14 +0200 Subject: [PATCH 106/111] Add methods to command processor --- Src/DDD.Core/Application/CommandProcessor.cs | 48 +++++++++- .../Application/ContextualCommandProcessor.cs | 18 ++++ Src/DDD.Core/Application/ICommandProcessor.cs | 20 +++++ .../ICommandProcessorExtensions.cs | 89 +++++++++++++++++++ .../IContextualCommandProcessor.cs | 10 +++ .../IContextualCommandProcessorExtensions.cs | 60 +++++++++++++ .../Application/CommandProcessorTests.cs | 28 +++++- .../RecurringCommandManagerTests.cs | 2 +- 8 files changed, 271 insertions(+), 4 deletions(-) diff --git a/Src/DDD.Core/Application/CommandProcessor.cs b/Src/DDD.Core/Application/CommandProcessor.cs index 071d927..b57c1b0 100644 --- a/Src/DDD.Core/Application/CommandProcessor.cs +++ b/Src/DDD.Core/Application/CommandProcessor.cs @@ -1,11 +1,11 @@ using EnsureThat; using System; using System.Threading.Tasks; +using System.Collections.Concurrent; namespace DDD.Core.Application { using Domain; - using System.Collections.Concurrent; using Validation; /// @@ -62,6 +62,15 @@ public void Process(TCommand command, IMessageContext context) where T 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(); @@ -70,6 +79,15 @@ public Task ProcessAsync(TCommand command, IMessageContext context) wh 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()); + 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 ValidationResult Validate(TCommand command, IValidationContext context) where TCommand : class, ICommand { Ensure.That(command, nameof(command)).IsNotNull(); @@ -83,6 +101,20 @@ public ValidationResult Validate(TCommand command, IValidationContext return validator.Validate(command, context); } + 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(); @@ -96,6 +128,20 @@ public Task ValidateAsync(TCommand command, IValidat 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/ContextualCommandProcessor.cs b/Src/DDD.Core/Application/ContextualCommandProcessor.cs index d414297..c993514 100644 --- a/Src/DDD.Core/Application/ContextualCommandProcessor.cs +++ b/Src/DDD.Core/Application/ContextualCommandProcessor.cs @@ -49,6 +49,15 @@ public void Process(TCommand command, IMessageContext context) where T 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(); @@ -57,6 +66,15 @@ public Task ProcessAsync(TCommand command, IMessageContext context) wh 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 } diff --git a/Src/DDD.Core/Application/ICommandProcessor.cs b/Src/DDD.Core/Application/ICommandProcessor.cs index 7bf7f58..26f5dc4 100644 --- a/Src/DDD.Core/Application/ICommandProcessor.cs +++ b/Src/DDD.Core/Application/ICommandProcessor.cs @@ -28,21 +28,41 @@ public interface ICommandProcessor /// void Process(TCommand command, IMessageContext context) where TCommand : class, ICommand; + /// + /// Processes synchronously a command. + /// + void Process(ICommand command, IMessageContext context); + /// /// 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 index 89c4b74..3dc18ab 100644 --- a/Src/DDD.Core/Application/ICommandProcessorExtensions.cs +++ b/Src/DDD.Core/Application/ICommandProcessorExtensions.cs @@ -29,6 +29,21 @@ public static void Process(this ICommandProcessor processor, 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 @@ -46,6 +61,21 @@ public static Task ProcessAsync(this ICommandProcessor processor, 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) @@ -79,6 +109,36 @@ public static Task ProcessWithDelayAsync(this ICommandProcessor proces 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 @@ -96,6 +156,21 @@ public static ValidationResult Validate(this ICommandProcessor process 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 @@ -113,6 +188,20 @@ public static Task ValidateAsync(this ICommandProces 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/IContextualCommandProcessor.cs b/Src/DDD.Core/Application/IContextualCommandProcessor.cs index 9696701..2bc24fb 100644 --- a/Src/DDD.Core/Application/IContextualCommandProcessor.cs +++ b/Src/DDD.Core/Application/IContextualCommandProcessor.cs @@ -28,11 +28,21 @@ public interface IContextualCommandProcessor ///
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 index b674718..c51368c 100644 --- a/Src/DDD.Core/Application/IContextualCommandProcessorExtensions.cs +++ b/Src/DDD.Core/Application/IContextualCommandProcessorExtensions.cs @@ -28,6 +28,21 @@ public static void Process(this IContextualCommandProcessor processor, 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 @@ -45,6 +60,21 @@ public static Task ProcessAsync(this IContextualCommandProcessor proce 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) @@ -78,6 +108,36 @@ public static Task ProcessWithDelayAsync(this IContextualCommandProces 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 } diff --git a/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs b/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs index 5808c8b..7672db7 100644 --- a/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs +++ b/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs @@ -43,7 +43,7 @@ public CommandProcessorTests() #region Methods [Fact] - public void Process_WhenCommandDefined_CallsExpectedHandler() + public void Process_WhenConcreteCommand_CallsExpectedHandler() { // Arrange var command = new FakeCommand1(); @@ -55,7 +55,19 @@ public void Process_WhenCommandDefined_CallsExpectedHandler() } [Fact] - public void Validate_WhenCommandDefined_CallsExpectedValidator() + public void Process_WhenAbstractCommand_CallsExpectedHandler() + { + // Arrange + var command = new FakeCommand1(); + var context = new MessageContext(); + // Act + this.processor.Process((ICommand)command, context); + // Assert + this.handlerOfCommand1.Received(1).Handle(command, context); + } + + [Fact] + public void Validate_WhenConcreteCommand_CallsExpectedValidator() { // Arrange var command = new FakeCommand1(); @@ -66,6 +78,18 @@ public void Validate_WhenCommandDefined_CallsExpectedValidator() this.validatorOfCommand1.Received(1).Validate(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.validatorOfCommand1.Received(1).Validate(command, context); + } + #endregion Methods } } \ No newline at end of file diff --git a/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs b/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs index 4c838b1..0221170 100644 --- a/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs +++ b/Test/DDD.Core.UnitTests/Application/RecurringCommandManagerTests.cs @@ -301,7 +301,7 @@ public async Task StartAndWait_WhenExceptionInProcessingSpecificRecurringCommand manager.Wait(TimeSpan.FromSeconds(5)); // Assert await commandProcessor.Received(2) - .ProcessAsync(Arg.Any(), Arg.Any()); + .ProcessAsync(Arg.Is(c => c is FakeCommand2), Arg.Any()); } From c82803423ccbb2c73383f04892a1c809f03f012b Mon Sep 17 00:00:00 2001 From: draphyz Date: Wed, 2 Aug 2023 10:53:30 +0200 Subject: [PATCH 107/111] Add unit tests for command processor --- .../Application/CommandProcessorTests.cs | 96 +++++++++++++++---- 1 file changed, 80 insertions(+), 16 deletions(-) diff --git a/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs b/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs index 7672db7..177a566 100644 --- a/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs +++ b/Test/DDD.Core.UnitTests/Application/CommandProcessorTests.cs @@ -10,10 +10,14 @@ public class CommandProcessorTests { #region Fields - private readonly ISyncCommandHandler handlerOfCommand1; - private readonly ISyncCommandHandler handlerOfCommand2; - private readonly ISyncObjectValidator validatorOfCommand1; - private readonly ISyncObjectValidator 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 @@ -22,19 +26,31 @@ 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(ISyncCommandHandler)))) - .Returns(this.handlerOfCommand1); + .Returns(this.syncHandlerOfCommand1); serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncCommandHandler)))) - .Returns(this.handlerOfCommand2); + .Returns(this.syncHandlerOfCommand2); serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncObjectValidator)))) - .Returns(this.validatorOfCommand1); + .Returns(this.syncValidatorOfCommand1); serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncObjectValidator)))) - .Returns(this.validatorOfCommand2); + .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()); } @@ -51,7 +67,19 @@ public void Process_WhenConcreteCommand_CallsExpectedHandler() // Act this.processor.Process(command, context); // Assert - this.handlerOfCommand1.Received(1).Handle(command, context); + this.syncHandlerOfCommand1.Received(1).Handle(command, context); + } + + [Fact] + public async Task ProcessAsync_WhenConcreteCommand_CallsExpectedHandler() + { + // Arrange + var command = new FakeCommand1(); + var context = new MessageContext(); + // Act + await this.processor.ProcessAsync(command, context); + // Assert + await this.asyncHandlerOfCommand1.Received(1).HandleAsync(command, context); } [Fact] @@ -63,7 +91,19 @@ public void Process_WhenAbstractCommand_CallsExpectedHandler() // Act this.processor.Process((ICommand)command, context); // Assert - this.handlerOfCommand1.Received(1).Handle(command, context); + 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] @@ -75,7 +115,19 @@ public void Validate_WhenConcreteCommand_CallsExpectedValidator() // Act this.processor.Validate(command, context); // Assert - this.validatorOfCommand1.Received(1).Validate(command, context); + 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] @@ -87,7 +139,19 @@ public void Validate_WhenAbstractCommand_CallsExpectedValidator() // Act this.processor.Validate((ICommand)command, context); // Assert - this.validatorOfCommand1.Received(1).Validate(command, context); + 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 From b892a8deccb39692835be7019d105269fe3ae865 Mon Sep 17 00:00:00 2001 From: draphyz Date: Wed, 2 Aug 2023 10:53:52 +0200 Subject: [PATCH 108/111] Add methods to query processor --- .../Application/ContextualQueryProcessor.cs | 29 +++++ .../Application/IContextualQueryProcessor.cs | 10 ++ .../IContextualQueryProcessorExtensions.cs | 30 +++++ Src/DDD.Core/Application/IQueryProcessor.cs | 20 +++ .../Application/IQueryProcessorExtensions.cs | 60 +++++++++ Src/DDD.Core/Application/QueryProcessor.cs | 57 +++++++++ .../Application/QueryProcessorTests.cs | 121 +++++++++++++++--- 7 files changed, 311 insertions(+), 16 deletions(-) diff --git a/Src/DDD.Core/Application/ContextualQueryProcessor.cs b/Src/DDD.Core/Application/ContextualQueryProcessor.cs index edacf7a..fa9f29e 100644 --- a/Src/DDD.Core/Application/ContextualQueryProcessor.cs +++ b/Src/DDD.Core/Application/ContextualQueryProcessor.cs @@ -1,10 +1,12 @@ 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. @@ -50,6 +52,19 @@ public TResult Process(IQuery query, IMessageContext context) 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(); @@ -59,6 +74,20 @@ public Task ProcessAsync(IQuery query, IMessageContex 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 } diff --git a/Src/DDD.Core/Application/IContextualQueryProcessor.cs b/Src/DDD.Core/Application/IContextualQueryProcessor.cs index 2d9de55..0a5cd13 100644 --- a/Src/DDD.Core/Application/IContextualQueryProcessor.cs +++ b/Src/DDD.Core/Application/IContextualQueryProcessor.cs @@ -29,11 +29,21 @@ public interface IContextualQueryProcessor /// 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 index 9524745..4f83269 100644 --- a/Src/DDD.Core/Application/IContextualQueryProcessorExtensions.cs +++ b/Src/DDD.Core/Application/IContextualQueryProcessorExtensions.cs @@ -23,6 +23,21 @@ public static TResult Process(this IContextualQueryProcessor processor, 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) { @@ -38,6 +53,21 @@ public static Task ProcessAsync(this IContextualQueryProcessor 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/IQueryProcessor.cs b/Src/DDD.Core/Application/IQueryProcessor.cs index 85eb432..772f807 100644 --- a/Src/DDD.Core/Application/IQueryProcessor.cs +++ b/Src/DDD.Core/Application/IQueryProcessor.cs @@ -28,21 +28,41 @@ public interface IQueryProcessor /// 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); + /// /// 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 index 4b5c070..784d358 100644 --- a/Src/DDD.Core/Application/IQueryProcessorExtensions.cs +++ b/Src/DDD.Core/Application/IQueryProcessorExtensions.cs @@ -25,6 +25,21 @@ public static TResult Process(this IQueryProcessor processor, 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) { @@ -40,6 +55,21 @@ public static Task ProcessAsync(this IQueryProcessor processor 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 @@ -57,6 +87,21 @@ public static ValidationResult Validate(this IQueryProcessor processor, 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 @@ -74,6 +119,21 @@ public static Task ValidateAsync(this IQueryProcessor 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 } diff --git a/Src/DDD.Core/Application/QueryProcessor.cs b/Src/DDD.Core/Application/QueryProcessor.cs index 21c3ad0..dd0751d 100644 --- a/Src/DDD.Core/Application/QueryProcessor.cs +++ b/Src/DDD.Core/Application/QueryProcessor.cs @@ -1,5 +1,6 @@ using EnsureThat; using System; +using System.Linq; using System.Threading.Tasks; namespace DDD.Core.Application @@ -7,6 +8,7 @@ namespace DDD.Core.Application using Domain; using System.Collections.Concurrent; using Validation; + using Threading; /// /// The default query processor for processing and validating queries of any type. @@ -63,6 +65,19 @@ public TResult Process(IQuery query, IMessageContext context) 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, context); + } + public Task ProcessAsync(IQuery query, IMessageContext context) { Ensure.That(query, nameof(query)).IsNotNull(); @@ -72,6 +87,20 @@ public Task ProcessAsync(IQuery query, IMessageContex 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(); @@ -85,6 +114,20 @@ public ValidationResult Validate(TQuery query, IValidationContext contex 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 Task ValidateAsync(TQuery query, IValidationContext context) where TQuery : class, IQuery { Ensure.That(query, nameof(query)).IsNotNull(); @@ -98,6 +141,20 @@ public Task ValidateAsync(TQuery query, IValidationCon return validator.ValidateAsync(query, context); } + public Task ValidateAsync(IQuery query, IValidationContext context) + { + 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/Test/DDD.Core.UnitTests/Application/QueryProcessorTests.cs b/Test/DDD.Core.UnitTests/Application/QueryProcessorTests.cs index 76714d6..268a091 100644 --- a/Test/DDD.Core.UnitTests/Application/QueryProcessorTests.cs +++ b/Test/DDD.Core.UnitTests/Application/QueryProcessorTests.cs @@ -1,5 +1,6 @@ using NSubstitute; using System; +using System.Threading.Tasks; using Xunit; using DDD.Validation; @@ -9,10 +10,14 @@ public class QueryProcessorTests { #region Fields - private readonly ISyncQueryHandler handlerOfQuery1; - private readonly ISyncQueryHandler handlerOfQuery2; - private readonly ISyncObjectValidator validatorOfQuery1; - private readonly ISyncObjectValidator 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 @@ -21,19 +26,31 @@ 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(ISyncQueryHandler)))) - .Returns(this.handlerOfQuery1); + .Returns(this.syncHandlerOfQuery1); serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncQueryHandler)))) - .Returns(this.handlerOfQuery2); + .Returns(this.syncHandlerOfQuery2); serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncObjectValidator)))) - .Returns(this.validatorOfQuery1); + .Returns(this.syncValidatorOfQuery1); serviceProvider.GetService(Arg.Is(t => t.IsAssignableFrom(typeof(ISyncObjectValidator)))) - .Returns(this.validatorOfQuery2); + .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()); } @@ -42,7 +59,7 @@ public QueryProcessorTests() #region Methods [Fact] - public void Process_WhenQueryDefined_CallsExpectedHandler() + public void Process_WhenConcreteQuery_CallsExpectedHandler() { // Arrange var query = new FakeQuery1(); @@ -50,11 +67,47 @@ public void Process_WhenQueryDefined_CallsExpectedHandler() // Act this.processor.Process(query, context); // Assert - this.handlerOfQuery1.Received(1).Handle(query, context); + 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 + await this.processor.ProcessAsync(query, context); + // Assert + 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(); @@ -62,7 +115,43 @@ public void Validate_WhenQueryDefined_CallsExpectedValidator() // Act this.processor.Validate(query, context); // Assert - this.validatorOfQuery1.Received(1).Validate(query, context); + 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 From fbe476209ce5826fc927cdc7bd301acded7f7fb7 Mon Sep 17 00:00:00 2001 From: draphyz Date: Fri, 12 Apr 2024 18:33:47 +0200 Subject: [PATCH 109/111] Fix bug bounded context null in AsyncScopedCommandHandler.UpdateEventStreamPositionASync --- Src/DDD.Core/Application/EventConsumer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Src/DDD.Core/Application/EventConsumer.cs b/Src/DDD.Core/Application/EventConsumer.cs index 6336b48..1e4f349 100644 --- a/Src/DDD.Core/Application/EventConsumer.cs +++ b/Src/DDD.Core/Application/EventConsumer.cs @@ -338,6 +338,7 @@ 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; } @@ -346,6 +347,7 @@ 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; } From 00ed0f9b0f4b2dc97b6339ce990f4aa99c0aedc6 Mon Sep 17 00:00:00 2001 From: draphyz Date: Mon, 22 Apr 2024 13:41:49 +0200 Subject: [PATCH 110/111] Fix bug with MSDTC --- Src/DDD.Core.SimpleInjector/ContainerExtensions.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs index 5c1a05a..b26ac37 100644 --- a/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs +++ b/Src/DDD.Core.SimpleInjector/ContainerExtensions.cs @@ -205,10 +205,10 @@ private static void RegisterCommandHandlers(this Container container, AppRegistr 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); + 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); + 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); @@ -324,6 +324,16 @@ private static void RegisterScheduleFactories(this Container container, Commands 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 } From 124c824d66eef5854278907fe0d925745c21006f Mon Sep 17 00:00:00 2001 From: draphyz Date: Wed, 24 Apr 2024 19:17:06 +0200 Subject: [PATCH 111/111] Check IsEventHandling before creating outer transaction scope --- .../AsyncScopedCommandHandler.cs | 14 +++++++++----- .../AsyncScopedCommandHandler`1.cs | 2 +- .../AsyncScopedQueryHandler.cs | 2 +- .../AsyncScopedQueryHandler`1.cs | 2 +- .../ThreadScopedCommandHandler.cs | 14 +++++++++----- 5 files changed, 21 insertions(+), 13 deletions(-) diff --git a/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs index c480ce6..7eec107 100644 --- a/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler.cs @@ -46,14 +46,18 @@ public async Task HandleAsync(TCommand command, IMessageContext context) await new SynchronizationContextRemover(); using (AsyncScopedLifestyle.BeginScope(container)) { - using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + 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 { - var handler = this.handlerProvider(); - await handler.HandleAsync(command, context); - 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(); + scope.Complete(); + } } + else + await handler.HandleAsync(command, context); } } diff --git a/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler`1.cs b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler`1.cs index 737e629..65f08e6 100644 --- a/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler`1.cs +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedCommandHandler`1.cs @@ -47,9 +47,9 @@ public AsyncScopedCommandHandler(Func> public async Task HandleAsync(TCommand command, IMessageContext context) { + await new SynchronizationContextRemover(); using (AsyncScopedLifestyle.BeginScope(container)) { - await new SynchronizationContextRemover(); var handler = this.handlerProvider(); await handler.HandleAsync(command, context); } diff --git a/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs b/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs index 21d5161..66365c4 100644 --- a/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler.cs @@ -39,9 +39,9 @@ public AsyncScopedQueryHandler(Func> handler public async Task HandleAsync(TQuery query, IMessageContext context) { + await new SynchronizationContextRemover(); using (AsyncScopedLifestyle.BeginScope(container)) { - await new SynchronizationContextRemover(); var handler = this.handlerProvider(); return await handler.HandleAsync(query, context); } diff --git a/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler`1.cs b/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler`1.cs index 9e4fc8f..8044e3e 100644 --- a/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler`1.cs +++ b/Src/DDD.Core.SimpleInjector/AsyncScopedQueryHandler`1.cs @@ -47,9 +47,9 @@ public AsyncScopedQueryHandler(Func HandleAsync(TQuery query, IMessageContext context) { + await new SynchronizationContextRemover(); using (AsyncScopedLifestyle.BeginScope(container)) { - await new SynchronizationContextRemover(); var handler = this.handlerProvider(); return await handler.HandleAsync(query, context); } diff --git a/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs b/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs index 9de2a65..f73215d 100644 --- a/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs +++ b/Src/DDD.Core.SimpleInjector/ThreadScopedCommandHandler.cs @@ -43,14 +43,18 @@ public void Handle(TCommand command, IMessageContext context) Ensure.That(context, nameof(context)).IsNotNull(); using (ThreadScopedLifestyle.BeginScope(container)) { - using (var scope = new TransactionScope()) + 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 { - var handler = this.handlerProvider(); - handler.Handle(command, context); - 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(); + scope.Complete(); + } } + else + handler.Handle(command, context); } }