diff --git a/Backend.Fx.sln b/Backend.Fx.sln index 471ddf96..c2035662 100644 --- a/Backend.Fx.sln +++ b/Backend.Fx.sln @@ -45,7 +45,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Backend.Fx.InMemoryPersiste EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Backend.Fx.Log4NetLogging", "src\implementations\Backend.Fx.Log4NetLogging\Backend.Fx.Log4NetLogging.csproj", "{C27BA4CE-882B-405F-906E-4DFA6E9F1216}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{DFD5E4B8-2479-4D29-9857-9199B94E412A}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backend.Fx.RabbitMq.Tests", "tests\Backend.Fx.RabbitMq.Tests\Backend.Fx.RabbitMq.Tests.csproj", "{6D0A5E9D-2FA5-4CC9-96B0-C2C871335E3A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backend.Fx.SerilogLogging", "src\implementations\Backend.Fx.SerilogLogging\Backend.Fx.SerilogLogging.csproj", "{33F7D896-2276-4DD7-A4DA-8FD5C47F7735}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -108,6 +110,14 @@ Global {C27BA4CE-882B-405F-906E-4DFA6E9F1216}.Debug|Any CPU.Build.0 = Debug|Any CPU {C27BA4CE-882B-405F-906E-4DFA6E9F1216}.Release|Any CPU.ActiveCfg = Release|Any CPU {C27BA4CE-882B-405F-906E-4DFA6E9F1216}.Release|Any CPU.Build.0 = Release|Any CPU + {6D0A5E9D-2FA5-4CC9-96B0-C2C871335E3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6D0A5E9D-2FA5-4CC9-96B0-C2C871335E3A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D0A5E9D-2FA5-4CC9-96B0-C2C871335E3A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6D0A5E9D-2FA5-4CC9-96B0-C2C871335E3A}.Release|Any CPU.Build.0 = Release|Any CPU + {33F7D896-2276-4DD7-A4DA-8FD5C47F7735}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {33F7D896-2276-4DD7-A4DA-8FD5C47F7735}.Debug|Any CPU.Build.0 = Debug|Any CPU + {33F7D896-2276-4DD7-A4DA-8FD5C47F7735}.Release|Any CPU.ActiveCfg = Release|Any CPU + {33F7D896-2276-4DD7-A4DA-8FD5C47F7735}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -129,6 +139,8 @@ Global {45EC5987-1C85-4940-8E5E-3B4F0FA90AF8} = {56ACAE69-F7F0-4FF2-BEE6-4B079481CF9A} {0B8F13CA-1347-4655-9D41-AED21B1AFAC4} = {739A7296-579F-4D9A-BC73-DCECD260D7A0} {C27BA4CE-882B-405F-906E-4DFA6E9F1216} = {739A7296-579F-4D9A-BC73-DCECD260D7A0} + {6D0A5E9D-2FA5-4CC9-96B0-C2C871335E3A} = {C7885592-A4B8-4BA8-8D3A-1EDA4025D17A} + {33F7D896-2276-4DD7-A4DA-8FD5C47F7735} = {739A7296-579F-4D9A-BC73-DCECD260D7A0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {45648557-C751-44AD-9C87-0F12EB673969} diff --git a/README.md b/README.md index 9b0a04ae..6deeb34f 100644 --- a/README.md +++ b/README.md @@ -24,5 +24,5 @@ Vendor | Library | .NET Standard | NuGet [Entity Framework Core 2.1](https://github.com/aspnet/EntityFramework) as persistence mechanism | Backend.Fx.EfCorePersistence | 2.0 | [![NuGet](https://img.shields.io/nuget/v/Backend.Fx.EfCorePersistence.svg)](https://www.nuget.org/packages/Backend.Fx.EfCorePersistence) InMemory Persistence implementation | Backend.Fx.InMemoryPersistence | 1.3 | [![NuGet](https://img.shields.io/nuget/v/Backend.Fx.InMemoryPersistence.svg)](https://www.nuget.org/packages/Backend.Fx.InMemoryPersistence) [NLog](https://github.com/NLog/NLog) logging | Backend.Fx.NLogLogging | 1.6 | [![NuGet](https://img.shields.io/nuget/v/Backend.Fx.NLogLogging.svg)](https://www.nuget.org/packages/Backend.Fx.NLogLogging) -[RabbitMq](https://www.rabbitmq.com/) Event Bus | Backend.Fx.RabbitMq | 1.5 | [![NuGet](https://img.shields.io/nuget/v/Backend.Fx.RabbitMq.svg)](https://www.nuget.org/packages/Backend.Fx.RabbitMq) +[RabbitMq](https://www.rabbitmq.com/) Message Bus | Backend.Fx.RabbitMq | 1.5 | [![NuGet](https://img.shields.io/nuget/v/Backend.Fx.RabbitMq.svg)](https://www.nuget.org/packages/Backend.Fx.RabbitMq) [Simple Injector](https://github.com/simpleinjector/SimpleInjector) as DI container | Backend.Fx.SimpleInjectorDependencyInjection | 1.3 | [![NuGet](https://img.shields.io/nuget/v/Backend.Fx.SimpleInjectorDependencyInjection.svg)](https://www.nuget.org/packages/Backend.Fx.SimpleInjectorDependencyInjection) diff --git a/src/abstractions/Backend.Fx/Backend.Fx.csproj b/src/abstractions/Backend.Fx/Backend.Fx.csproj index 5d9d5a45..3f24de27 100644 --- a/src/abstractions/Backend.Fx/Backend.Fx.csproj +++ b/src/abstractions/Backend.Fx/Backend.Fx.csproj @@ -30,14 +30,14 @@ - + - + \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/BuildingBlocks/AggregateRoot.cs b/src/abstractions/Backend.Fx/BuildingBlocks/AggregateRoot.cs index e06e7fdd..e22101f1 100644 --- a/src/abstractions/Backend.Fx/BuildingBlocks/AggregateRoot.cs +++ b/src/abstractions/Backend.Fx/BuildingBlocks/AggregateRoot.cs @@ -7,10 +7,13 @@ public abstract class AggregateRoot : Entity { protected AggregateRoot() - { } + { + } - protected AggregateRoot(int id) : base(id) { } + protected AggregateRoot(int id) : base(id) + { + } public int TenantId { get; set; } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/BuildingBlocks/Entity.cs b/src/abstractions/Backend.Fx/BuildingBlocks/Entity.cs index 9b9175a6..5ddc4d22 100644 --- a/src/abstractions/Backend.Fx/BuildingBlocks/Entity.cs +++ b/src/abstractions/Backend.Fx/BuildingBlocks/Entity.cs @@ -1,39 +1,32 @@ -using Backend.Fx.Extensions; +using System; +using System.ComponentModel.DataAnnotations; +using Backend.Fx.Extensions; +using JetBrains.Annotations; namespace Backend.Fx.BuildingBlocks { - using System; - using System.ComponentModel.DataAnnotations; - using System.Diagnostics; - using JetBrains.Annotations; - /// /// An object that is not defined by its attributes, but rather by a thread of continuity and its identity. /// https://en.wikipedia.org/wiki/Domain-driven_design#Building_blocks /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + "}")] public abstract class Entity : Identified { protected Entity() - { } + { + } protected Entity(int id) { Id = id; } - [UsedImplicitly] - public string DebuggerDisplay => $"{GetType().Name}[{Id}]"; - public DateTime CreatedOn { get; protected set; } - [StringLength(100)] - public string CreatedBy { get; protected set; } + [StringLength(100)] public string CreatedBy { get; protected set; } public DateTime? ChangedOn { get; protected set; } - [StringLength(100)] - public string ChangedBy { get; protected set; } + [StringLength(100)] public string ChangedBy { get; protected set; } public void SetCreatedProperties([NotNull] string createdBy, DateTime createdOn) { @@ -41,10 +34,12 @@ public void SetCreatedProperties([NotNull] string createdBy, DateTime createdOn) { throw new ArgumentNullException(nameof(createdBy)); } + if (createdBy == string.Empty) { throw new ArgumentException(nameof(createdBy)); } + CreatedBy = createdBy.Cut(100); CreatedOn = createdOn; } @@ -55,12 +50,14 @@ public void SetModifiedProperties([NotNull] string changedBy, DateTime changedOn { throw new ArgumentNullException(nameof(changedBy)); } + if (changedBy == string.Empty) { throw new ArgumentException(nameof(changedBy)); } + ChangedBy = changedBy.Cut(100); ChangedOn = changedOn; } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/BuildingBlocks/IApplicationService.cs b/src/abstractions/Backend.Fx/BuildingBlocks/IApplicationService.cs index d0169f7f..832d8777 100644 --- a/src/abstractions/Backend.Fx/BuildingBlocks/IApplicationService.cs +++ b/src/abstractions/Backend.Fx/BuildingBlocks/IApplicationService.cs @@ -4,5 +4,6 @@ /// A marker interface to identify application services to be auto registered in the container on boot /// public interface IApplicationService - { } -} + { + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/BuildingBlocks/IDomainService.cs b/src/abstractions/Backend.Fx/BuildingBlocks/IDomainService.cs index 9a054a57..ddcc120d 100644 --- a/src/abstractions/Backend.Fx/BuildingBlocks/IDomainService.cs +++ b/src/abstractions/Backend.Fx/BuildingBlocks/IDomainService.cs @@ -4,5 +4,6 @@ /// A marker interface to domain application services to be auto registered in the container on boot /// public interface IDomainService - { } -} + { + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/BuildingBlocks/IFullTextSearchService.cs b/src/abstractions/Backend.Fx/BuildingBlocks/IFullTextSearchService.cs deleted file mode 100644 index 7fb552f3..00000000 --- a/src/abstractions/Backend.Fx/BuildingBlocks/IFullTextSearchService.cs +++ /dev/null @@ -1,10 +0,0 @@ - -namespace Backend.Fx.BuildingBlocks -{ - using System.Linq; - - public interface IFullTextSearchService where TAggregateRoot : AggregateRoot - { - IQueryable Search(string searchQuery); - } -} diff --git a/src/abstractions/Backend.Fx/BuildingBlocks/IRepository.cs b/src/abstractions/Backend.Fx/BuildingBlocks/IRepository.cs index 2ed24ff7..19ba3187 100644 --- a/src/abstractions/Backend.Fx/BuildingBlocks/IRepository.cs +++ b/src/abstractions/Backend.Fx/BuildingBlocks/IRepository.cs @@ -1,8 +1,8 @@ -namespace Backend.Fx.BuildingBlocks -{ - using System.Collections.Generic; - using System.Linq; +using System.Collections.Generic; +using System.Linq; +namespace Backend.Fx.BuildingBlocks +{ /// /// Encapsulates methods for retrieving domain objects /// See https://en.wikipedia.org/wiki/Domain-driven_design#Building_blocks @@ -19,6 +19,5 @@ public interface IRepository where TAggregateRoot : AggregateRoo bool Any(); TAggregateRoot[] Resolve(IEnumerable ids); IQueryable AggregateQueryable { get; } - } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/BuildingBlocks/IView.cs b/src/abstractions/Backend.Fx/BuildingBlocks/IView.cs index 1772c19c..0121addb 100644 --- a/src/abstractions/Backend.Fx/BuildingBlocks/IView.cs +++ b/src/abstractions/Backend.Fx/BuildingBlocks/IView.cs @@ -1,13 +1,14 @@ -namespace Backend.Fx.BuildingBlocks -{ - using System; - using System.Collections; - using System.Collections.Generic; - using System.Linq; - using System.Linq.Expressions; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +namespace Backend.Fx.BuildingBlocks +{ public interface IView : IQueryable - {} + { + } public abstract class View : IView { @@ -34,4 +35,4 @@ IEnumerator IEnumerable.GetEnumerator() public IQueryProvider Provider => _viewImplementation.Provider; } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/BuildingBlocks/Identified.cs b/src/abstractions/Backend.Fx/BuildingBlocks/Identified.cs index 1c1aa61c..f37a3bde 100644 --- a/src/abstractions/Backend.Fx/BuildingBlocks/Identified.cs +++ b/src/abstractions/Backend.Fx/BuildingBlocks/Identified.cs @@ -1,13 +1,16 @@ using System; using System.ComponentModel.DataAnnotations; +using System.Diagnostics; +using JetBrains.Annotations; namespace Backend.Fx.BuildingBlocks { + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + "}")] public abstract class Identified : IEquatable { - [Key] - public int Id { get; set; } + [Key] public int Id { get; set; } + [UsedImplicitly] public string DebuggerDisplay => $"{GetType().Name}[{Id}]"; public bool Equals(Identified other) { @@ -21,7 +24,7 @@ public bool Equals(Identified other) public override bool Equals(object obj) { - Identified other = obj as Identified; + var other = obj as Identified; if (other == null) { return false; @@ -40,7 +43,7 @@ public override int GetHashCode() // ReSharper enable NonReadonlyMemberInGetHashCode // ReSharper disable once BaseObjectGetHashCodeCallInGetHashCode - return base.GetHashCode(); + return base.GetHashCode(); } public static bool operator ==(Identified x, Identified y) @@ -53,4 +56,4 @@ public override int GetHashCode() return !(x == y); } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/BuildingBlocks/Repository.cs b/src/abstractions/Backend.Fx/BuildingBlocks/Repository.cs index 591640d9..82783e43 100644 --- a/src/abstractions/Backend.Fx/BuildingBlocks/Repository.cs +++ b/src/abstractions/Backend.Fx/BuildingBlocks/Repository.cs @@ -1,16 +1,16 @@ -namespace Backend.Fx.BuildingBlocks +using System; +using System.Collections.Generic; +using System.Linq; +using Backend.Fx.Environment.MultiTenancy; +using Backend.Fx.Exceptions; +using Backend.Fx.Extensions; +using Backend.Fx.Logging; +using Backend.Fx.Patterns.Authorization; +using Backend.Fx.Patterns.DependencyInjection; +using JetBrains.Annotations; + +namespace Backend.Fx.BuildingBlocks { - using System; - using System.Collections.Generic; - using System.Linq; - using Environment.MultiTenancy; - using Exceptions; - using Extensions; - using JetBrains.Annotations; - using Logging; - using Patterns.Authorization; - using Patterns.DependencyInjection; - public abstract class Repository : IRepository where TAggregateRoot : AggregateRoot { private static readonly ILogger Logger = LogManager.Create>(); @@ -45,7 +45,7 @@ public IQueryable AggregateQueryable public TAggregateRoot Single(int id) { Logger.Debug($"Getting single {AggregateTypeName}[{id}]"); - var aggregateRoot = AggregateQueryable.FirstOrDefault(aggr => aggr.Id.Equals(id)); + TAggregateRoot aggregateRoot = AggregateQueryable.FirstOrDefault(aggr => aggr.Id.Equals(id)); if (aggregateRoot == null) { throw new NotFoundException(id); diff --git a/src/abstractions/Backend.Fx/BuildingBlocks/ValueObject.cs b/src/abstractions/Backend.Fx/BuildingBlocks/ValueObject.cs index e3c1fc76..f4d5a5f3 100644 --- a/src/abstractions/Backend.Fx/BuildingBlocks/ValueObject.cs +++ b/src/abstractions/Backend.Fx/BuildingBlocks/ValueObject.cs @@ -1,9 +1,9 @@ -namespace Backend.Fx.BuildingBlocks -{ - using System; - using System.Collections.Generic; - using System.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +namespace Backend.Fx.BuildingBlocks +{ /// /// An object that contains attributes but has no conceptual identity. /// https://en.wikipedia.org/wiki/Domain-driven_design#Building_blocks @@ -21,7 +21,7 @@ public override bool Equals(object obj) if (ReferenceEquals(this, obj)) return true; if (ReferenceEquals(null, obj)) return false; if (GetType() != obj.GetType()) return false; - return GetEqualityComponents().SequenceEqual(((ValueObject)obj).GetEqualityComponents()); + return GetEqualityComponents().SequenceEqual(((ValueObject) obj).GetEqualityComponents()); } public override int GetHashCode() @@ -29,7 +29,10 @@ public override int GetHashCode() unchecked { var hash = 17; - foreach (var obj in GetEqualityComponents()) hash = hash * 23 + (obj != null ? obj.GetHashCode() : 0); + foreach (object obj in GetEqualityComponents()) + { + hash = hash * 23 + (obj != null ? obj.GetHashCode() : 0); + } return hash; } @@ -40,24 +43,38 @@ public abstract class ComparableValueObject : ValueObject, IComparable { public int CompareTo(object obj) { - if (ReferenceEquals(this, obj)) return 0; - if (ReferenceEquals(null, obj)) return 1; + if (ReferenceEquals(this, obj)) + { + return 0; + } + + if (ReferenceEquals(null, obj)) + { + return 1; + } if (GetType() != obj.GetType()) + { throw new InvalidOperationException(); + } return CompareTo(obj as ComparableValueObject); } protected abstract IEnumerable GetComparableComponents(); - protected IComparable AsNonGenericComparable(IComparable comparable) - { - return new NonGenericComparable(comparable); - } - protected int CompareTo(ComparableValueObject other) { + if (ReferenceEquals(this, other)) + { + return 0; + } + + if (ReferenceEquals(null, other)) + { + return 1; + } + using (var thisComponents = GetComparableComponents().GetEnumerator()) using (var otherComponents = other.GetComparableComponents().GetEnumerator()) { @@ -66,12 +83,17 @@ protected int CompareTo(ComparableValueObject other) var x = thisComponents.MoveNext(); var y = otherComponents.MoveNext(); if (x != y) + { throw new InvalidOperationException(); + } + if (x) { - var c = thisComponents.Current.CompareTo(otherComponents.Current); + var c = thisComponents.Current?.CompareTo(otherComponents.Current) ?? 0; if (c != 0) + { return c; + } } else { @@ -82,24 +104,6 @@ protected int CompareTo(ComparableValueObject other) return 0; } } - - private class NonGenericComparable : IComparable - { - private readonly IComparable _comparable; - - public NonGenericComparable(IComparable comparable) - { - _comparable = comparable; - } - - public int CompareTo(object obj) - { - if (ReferenceEquals(_comparable, obj)) return 0; - if (ReferenceEquals(null, obj)) - throw new ArgumentNullException(); - return _comparable.CompareTo((T) obj); - } - } } public abstract class ComparableValueObject : ComparableValueObject, IComparable diff --git a/src/abstractions/Backend.Fx/ConfigurationSettings/ISettingSerializer.cs b/src/abstractions/Backend.Fx/ConfigurationSettings/ISettingSerializer.cs index d8738665..4fc9868d 100644 --- a/src/abstractions/Backend.Fx/ConfigurationSettings/ISettingSerializer.cs +++ b/src/abstractions/Backend.Fx/ConfigurationSettings/ISettingSerializer.cs @@ -1,11 +1,12 @@ +using System; +using System.Globalization; +using JetBrains.Annotations; + namespace Backend.Fx.ConfigurationSettings { - using System; - using System.Globalization; - using JetBrains.Annotations; - public interface ISettingSerializer - { } + { + } public interface ISettingSerializer : ISettingSerializer { @@ -37,7 +38,7 @@ public string Serialize(int? setting) public int? Deserialize(string value) { - return string.IsNullOrWhiteSpace(value) ? (int?)null : int.Parse(value, CultureInfo.InvariantCulture); + return string.IsNullOrWhiteSpace(value) ? (int?) null : int.Parse(value, CultureInfo.InvariantCulture); } } @@ -51,7 +52,7 @@ public string Serialize(double? setting) public double? Deserialize(string value) { - return string.IsNullOrWhiteSpace(value) ? (double?)null : double.Parse(value, CultureInfo.InvariantCulture); + return string.IsNullOrWhiteSpace(value) ? (double?) null : double.Parse(value, CultureInfo.InvariantCulture); } } @@ -65,7 +66,7 @@ public string Serialize(bool? setting) public bool? Deserialize(string value) { - return string.IsNullOrWhiteSpace(value) ? (bool?)null : bool.Parse(value); + return string.IsNullOrWhiteSpace(value) ? (bool?) null : bool.Parse(value); } } @@ -79,7 +80,7 @@ public string Serialize(DateTime? setting) public DateTime? Deserialize(string value) { - return string.IsNullOrWhiteSpace(value) ? (DateTime?)null : DateTime.Parse(value, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); + return string.IsNullOrWhiteSpace(value) ? (DateTime?) null : DateTime.Parse(value, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); } } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/ConfigurationSettings/Setting.cs b/src/abstractions/Backend.Fx/ConfigurationSettings/Setting.cs index 214d4605..c1e55a92 100644 --- a/src/abstractions/Backend.Fx/ConfigurationSettings/Setting.cs +++ b/src/abstractions/Backend.Fx/ConfigurationSettings/Setting.cs @@ -1,12 +1,14 @@ +using Backend.Fx.BuildingBlocks; +using JetBrains.Annotations; + namespace Backend.Fx.ConfigurationSettings { - using BuildingBlocks; - using JetBrains.Annotations; - public class Setting : AggregateRoot { [UsedImplicitly] - private Setting() { } + private Setting() + { + } public Setting(int id, string key) : base(id) { diff --git a/src/abstractions/Backend.Fx/ConfigurationSettings/SettingSerializerFactory.cs b/src/abstractions/Backend.Fx/ConfigurationSettings/SettingSerializerFactory.cs index 6acec89e..a270989c 100644 --- a/src/abstractions/Backend.Fx/ConfigurationSettings/SettingSerializerFactory.cs +++ b/src/abstractions/Backend.Fx/ConfigurationSettings/SettingSerializerFactory.cs @@ -1,11 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using JetBrains.Annotations; + namespace Backend.Fx.ConfigurationSettings { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - using JetBrains.Annotations; - public interface ISettingSerializerFactory { ISettingSerializer GetSerializer(); @@ -18,16 +18,16 @@ public class SettingSerializerFactory : ISettingSerializerFactory public SettingSerializerFactory() { Serializers = typeof(ISettingSerializer) - .GetTypeInfo() - .Assembly - .ExportedTypes - .Select(t => t.GetTypeInfo()) - .Where(t => !t.IsAbstract && t.IsClass && typeof(ISettingSerializer).GetTypeInfo().IsAssignableFrom(t)) - .ToDictionary( - t => t.ImplementedInterfaces - .Single(i => i.GetTypeInfo().IsGenericType && i.GetGenericTypeDefinition() == typeof(ISettingSerializer<>)) - .GenericTypeArguments.Single(), - t => (ISettingSerializer) Activator.CreateInstance(t.AsType())); + .GetTypeInfo() + .Assembly + .ExportedTypes + .Select(t => t.GetTypeInfo()) + .Where(t => !t.IsAbstract && t.IsClass && typeof(ISettingSerializer).GetTypeInfo().IsAssignableFrom(t)) + .ToDictionary( + t => t.ImplementedInterfaces + .Single(i => i.GetTypeInfo().IsGenericType && i.GetGenericTypeDefinition() == typeof(ISettingSerializer<>)) + .GenericTypeArguments.Single(), + t => (ISettingSerializer) Activator.CreateInstance(t.AsType())); } [NotNull] @@ -39,7 +39,7 @@ public ISettingSerializer GetSerializer() } throw new ArgumentOutOfRangeException(nameof(T), - $"No Serializer for Setting Type {typeof(T).Name} available"); + $"No Serializer for Setting Type {typeof(T).Name} available"); } } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/ConfigurationSettings/SettingsService.cs b/src/abstractions/Backend.Fx/ConfigurationSettings/SettingsService.cs index 59ad3bad..26476479 100644 --- a/src/abstractions/Backend.Fx/ConfigurationSettings/SettingsService.cs +++ b/src/abstractions/Backend.Fx/ConfigurationSettings/SettingsService.cs @@ -1,9 +1,9 @@ +using System.Linq; +using Backend.Fx.BuildingBlocks; +using Backend.Fx.Patterns.IdGeneration; + namespace Backend.Fx.ConfigurationSettings { - using System.Linq; - using BuildingBlocks; - using Patterns.IdGeneration; - public abstract class SettingsService { private readonly string _category; @@ -27,6 +27,7 @@ protected T ReadSetting(string key) { return default(T); } + var serializer = _settingSerializerFactory.GetSerializer(); return setting.GetValue(serializer); } @@ -40,6 +41,7 @@ protected void WriteSetting(string key, T value) setting = new Setting(_idGenerator.NextId(), categoryKey); _settingRepository.Add(setting); } + var serializer = _settingSerializerFactory.GetSerializer(); setting.SetValue(serializer, value); } diff --git a/src/abstractions/Backend.Fx/Environment/Authentication/AnonymousIdentity.cs b/src/abstractions/Backend.Fx/Environment/Authentication/AnonymousIdentity.cs index b564bbc0..3ef29a26 100644 --- a/src/abstractions/Backend.Fx/Environment/Authentication/AnonymousIdentity.cs +++ b/src/abstractions/Backend.Fx/Environment/Authentication/AnonymousIdentity.cs @@ -1,7 +1,7 @@ -namespace Backend.Fx.Environment.Authentication -{ - using System.Security.Principal; +using System.Security.Principal; +namespace Backend.Fx.Environment.Authentication +{ public class AnonymousIdentity : IIdentity { public string Name => "ANONYMOUS"; diff --git a/src/abstractions/Backend.Fx/Environment/Authentication/CurrentIdentityHolder.cs b/src/abstractions/Backend.Fx/Environment/Authentication/CurrentIdentityHolder.cs index e93eea55..7c7cadb6 100644 --- a/src/abstractions/Backend.Fx/Environment/Authentication/CurrentIdentityHolder.cs +++ b/src/abstractions/Backend.Fx/Environment/Authentication/CurrentIdentityHolder.cs @@ -1,8 +1,8 @@ -namespace Backend.Fx.Environment.Authentication -{ - using System.Security.Principal; - using Patterns.DependencyInjection; +using System.Security.Principal; +using Backend.Fx.Patterns.DependencyInjection; +namespace Backend.Fx.Environment.Authentication +{ public class CurrentIdentityHolder : CurrentTHolder { public override IIdentity ProvideInstance() @@ -28,4 +28,4 @@ public static ICurrentTHolder CreateSystem() return currentIdentityHolder; } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/Authentication/SystemIdentity.cs b/src/abstractions/Backend.Fx/Environment/Authentication/SystemIdentity.cs index 525fc8e9..cb388295 100644 --- a/src/abstractions/Backend.Fx/Environment/Authentication/SystemIdentity.cs +++ b/src/abstractions/Backend.Fx/Environment/Authentication/SystemIdentity.cs @@ -1,7 +1,7 @@ -namespace Backend.Fx.Environment.Authentication -{ - using System.Security.Principal; +using System.Security.Principal; +namespace Backend.Fx.Environment.Authentication +{ public class SystemIdentity : IIdentity { public string Name => "SYSTEM"; @@ -10,4 +10,4 @@ public class SystemIdentity : IIdentity public bool IsAuthenticated => true; } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/DateAndTime/AdjustableClock.cs b/src/abstractions/Backend.Fx/Environment/DateAndTime/AdjustableClock.cs new file mode 100644 index 00000000..9af417ae --- /dev/null +++ b/src/abstractions/Backend.Fx/Environment/DateAndTime/AdjustableClock.cs @@ -0,0 +1,34 @@ +using System; +using Backend.Fx.Logging; + +namespace Backend.Fx.Environment.DateAndTime +{ + public class AdjustableClock : IClock + { + private static readonly ILogger Logger = LogManager.Create(); + + private readonly IClock _clockImplementation; + private DateTime? _overriddenUtcNow; + + public AdjustableClock(IClock clockImplementation) + { + _clockImplementation = clockImplementation; + } + + public DateTime UtcNow => _overriddenUtcNow ?? _clockImplementation.UtcNow; + + public void OverrideUtcNow(DateTime utcNow) + { + Logger.Trace($"Adjusting clock to {utcNow}"); + _overriddenUtcNow = utcNow; + } + + public DateTime Advance(TimeSpan timespan) + { + _overriddenUtcNow = _overriddenUtcNow ?? _clockImplementation.UtcNow; + Logger.Trace($"Advancing clock by {timespan}"); + _overriddenUtcNow = _overriddenUtcNow.Value.Add(timespan); + return _overriddenUtcNow.Value; + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/DateAndTime/Clock.cs b/src/abstractions/Backend.Fx/Environment/DateAndTime/Clock.cs deleted file mode 100644 index 2183439a..00000000 --- a/src/abstractions/Backend.Fx/Environment/DateAndTime/Clock.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Backend.Fx.Environment.DateAndTime -{ - using System; - using Logging; - - public abstract class Clock : IClock - { - private static readonly ILogger Logger = LogManager.Create(); - - private DateTime? _utcNow; - - public DateTime UtcNow => _utcNow ?? DateTime.UtcNow; - - public void OverrideUtcNow(DateTime overriddenUtcNow) - { - Logger.Debug("Freezing clock at {0:O}", overriddenUtcNow); - _utcNow = overriddenUtcNow; - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/DateAndTime/FrozenClock.cs b/src/abstractions/Backend.Fx/Environment/DateAndTime/FrozenClock.cs index b3ff7a44..2bcf206f 100644 --- a/src/abstractions/Backend.Fx/Environment/DateAndTime/FrozenClock.cs +++ b/src/abstractions/Backend.Fx/Environment/DateAndTime/FrozenClock.cs @@ -1,23 +1,22 @@ -namespace Backend.Fx.Environment.DateAndTime -{ - using System; +using System; +using Backend.Fx.Logging; +namespace Backend.Fx.Environment.DateAndTime +{ /// /// Best practice for web (service) applications: time does not advance during a single request /// - public class FrozenClock : Clock + public class FrozenClock : IClock { - public FrozenClock() : this(DateTime.UtcNow) - { } - - private FrozenClock(DateTime utcNow) + private static readonly ILogger Logger = LogManager.Create(); + + // ReSharper disable once UnusedParameter.Local + public FrozenClock(IClock clock) { - OverrideUtcNow(utcNow); + UtcNow = DateTime.UtcNow; + Logger.Trace($"Freezing clock at {UtcNow}"); } - public static IClock WithFrozenUtcNow(DateTime utcNow) - { - return new FrozenClock(utcNow); - } + public DateTime UtcNow { get; } } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/DateAndTime/IClock.cs b/src/abstractions/Backend.Fx/Environment/DateAndTime/IClock.cs index 51cd1822..dcd4a886 100644 --- a/src/abstractions/Backend.Fx/Environment/DateAndTime/IClock.cs +++ b/src/abstractions/Backend.Fx/Environment/DateAndTime/IClock.cs @@ -1,7 +1,7 @@ -namespace Backend.Fx.Environment.DateAndTime -{ - using System; +using System; +namespace Backend.Fx.Environment.DateAndTime +{ /// /// Wraps access to DateTime.UtcNow. By means of this interface the current time can be mocked. /// the database should only store universal date and time values, that could be translated into user's time by applying a UtcOffset @@ -9,7 +9,5 @@ public interface IClock { DateTime UtcNow { get; } - - void OverrideUtcNow(DateTime utcNow); } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/DateAndTime/WallClock.cs b/src/abstractions/Backend.Fx/Environment/DateAndTime/WallClock.cs index 3de01b89..5ca43a75 100644 --- a/src/abstractions/Backend.Fx/Environment/DateAndTime/WallClock.cs +++ b/src/abstractions/Backend.Fx/Environment/DateAndTime/WallClock.cs @@ -1,8 +1,12 @@ -namespace Backend.Fx.Environment.DateAndTime +using System; + +namespace Backend.Fx.Environment.DateAndTime { /// /// The real system clock /// - public class WallClock : Clock - { } -} + public class WallClock : IClock + { + public DateTime UtcNow => DateTime.UtcNow; + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/AllTenantBackendFxApplicationInvoker.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/AllTenantBackendFxApplicationInvoker.cs new file mode 100644 index 00000000..4d89d2c2 --- /dev/null +++ b/src/abstractions/Backend.Fx/Environment/MultiTenancy/AllTenantBackendFxApplicationInvoker.cs @@ -0,0 +1,32 @@ +using System; +using System.Linq; +using Backend.Fx.Environment.Authentication; +using Backend.Fx.Logging; +using Backend.Fx.Patterns.DependencyInjection; + +namespace Backend.Fx.Environment.MultiTenancy +{ + public class AllTenantBackendFxApplicationInvoker + { + private static readonly ILogger Logger = LogManager.Create(); + private readonly ITenantIdProvider _tenantIdProvider; + private readonly IBackendFxApplicationInvoker _invoker; + + public AllTenantBackendFxApplicationInvoker(ITenantIdProvider tenantIdProvider, IBackendFxApplicationInvoker invoker) + { + _tenantIdProvider = tenantIdProvider; + _invoker = invoker; + } + + public void Invoke(Action action) + { + var correlationId = Guid.NewGuid(); + TenantId[] tenantIds = _tenantIdProvider.GetActiveDemonstrationTenantIds().Concat(_tenantIdProvider.GetActiveProductionTenantIds()).ToArray(); + Logger.Debug($"Action will be called in tenants: {string.Join(",", tenantIds.Select(t => t.ToString()))}"); + foreach (TenantId tenantId in tenantIds) + { + _invoker.Invoke(action, new SystemIdentity(), tenantId, correlationId); + } + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/BackendFxApplicationTenantExtensions.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/BackendFxApplicationTenantExtensions.cs deleted file mode 100644 index 100c94a1..00000000 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/BackendFxApplicationTenantExtensions.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using Backend.Fx.Environment.Authentication; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DataGeneration; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.Patterns.EventAggregation.Integration; - -namespace Backend.Fx.Environment.MultiTenancy -{ - public static class BackendFxApplicationTenantExtensions - { - private static readonly ILogger Logger = LogManager.Create(typeof(BackendFxApplicationTenantExtensions)); - - public static void RegisterSeedActionForNewlyCreatedTenants(this IBackendFxApplication application, ITenantService tenantService) - { - application.CompositionRoot - .GetInstance() - .Subscribe(new DelegateIntegrationEventHandler(tenantCreated => - { - Logger.Info($"Seeding data for recently created {(tenantCreated.IsDemoTenant?"demo ":"")}tenant {tenantCreated.TenantId}"); - try - { - var tenantId = new TenantId(tenantCreated.TenantId); - application.SeedDataForTenant(tenantId, tenantCreated.IsDemoTenant); - tenantService.ActivateTenant(tenantId); - } - catch (Exception ex) - { - Logger.Error(ex, $"Seeding data for recently created {(tenantCreated.IsDemoTenant ? "demo " : "")}tenant {tenantCreated.TenantId} failed."); - } - })); - } - - public static void SeedDataForAllActiveTenants(this IBackendFxApplication application) - { - using (Logger.InfoDuration("Seeding data")) - { - var prodTenantIds = application.TenantIdService.GetActiveProductionTenantIds(); - foreach (var prodTenantId in prodTenantIds) - { - application.SeedDataForTenant(prodTenantId, false); - } - - var demoTenantIds = application.TenantIdService.GetActiveDemonstrationTenantIds(); - foreach (var demoTenantId in demoTenantIds) - { - application.SeedDataForTenant(demoTenantId, true); - } - } - } - - private static void SeedDataForTenant(this IBackendFxApplication application, TenantId tenantId, bool isDemoTenant) - { - using (Logger.InfoDuration($"Seeding data for tenant {tenantId.Value}")) - { - Type[] dataGeneratorTypesToRun; - - using (application.BeginScope()) - { - var dataGenerators = application.CompositionRoot.GetInstances() - .OrderBy(dg => dg.Priority) - .Select(dg => dg.GetType()); - - if (!isDemoTenant) - { - dataGenerators = dataGenerators.Where(dg => !typeof(IDemoDataGenerator).IsAssignableFrom(dg)); - } - - dataGeneratorTypesToRun = dataGenerators.ToArray(); - } - - foreach (var dataGeneratorTypeToRun in dataGeneratorTypesToRun) - { - application.Invoke(() => - { - IDataGenerator dataGenerator = application - .CompositionRoot - .GetInstances() - .Single(dg => dg.GetType() == dataGeneratorTypeToRun); - dataGenerator.Generate(); - }, new SystemIdentity(), tenantId); - } - } - } - } -} diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/CurrentTenantIdHolder.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/CurrentTenantIdHolder.cs index 7898b24a..ede076ae 100644 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/CurrentTenantIdHolder.cs +++ b/src/abstractions/Backend.Fx/Environment/MultiTenancy/CurrentTenantIdHolder.cs @@ -1,7 +1,7 @@ -namespace Backend.Fx.Environment.MultiTenancy -{ - using Patterns.DependencyInjection; +using Backend.Fx.Patterns.DependencyInjection; +namespace Backend.Fx.Environment.MultiTenancy +{ public class CurrentTenantIdHolder : CurrentTHolder { public static CurrentTenantIdHolder Create(int tenantId) @@ -34,7 +34,7 @@ protected override string Describe(TenantId instance) { return $"TenantId: {instance.Value}"; } - + return "TenantId: null"; } } diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/ITenantIdService.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/ITenantIdService.cs deleted file mode 100644 index 6d42c8fd..00000000 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/ITenantIdService.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Linq; - -namespace Backend.Fx.Environment.MultiTenancy -{ - public interface ITenantIdService - { - TenantId[] GetActiveTenantIds(); - TenantId[] GetActiveDemonstrationTenantIds(); - TenantId[] GetActiveProductionTenantIds(); - } - - public class TenantIdService : ITenantIdService - { - private readonly ITenantRepository _tenantRepository; - - public TenantIdService(ITenantRepository tenantRepository) - { - _tenantRepository = tenantRepository; - } - - - public TenantId[] GetActiveTenantIds() - { - return _tenantRepository - .GetTenants() - .Where(t => t.State == TenantState.Active) - .Select(t => new TenantId(t.Id)) - .ToArray(); - } - - public TenantId[] GetActiveDemonstrationTenantIds() - { - return _tenantRepository - .GetTenants() - .Where(t => t.State == TenantState.Active && t.IsDemoTenant) - .Select(t => new TenantId(t.Id)) - .ToArray(); - } - - public TenantId[] GetActiveProductionTenantIds() - { - return _tenantRepository - .GetTenants() - .Where(t => t.State == TenantState.Active && !t.IsDemoTenant) - .Select(t => new TenantId(t.Id)) - .ToArray(); - } - } - -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/ITenantService.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/ITenantService.cs deleted file mode 100644 index f165eb2f..00000000 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/ITenantService.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Linq; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.EventAggregation.Integration; -using JetBrains.Annotations; - -namespace Backend.Fx.Environment.MultiTenancy -{ - /// - /// Encapsulates the management of tenants - /// Note that this should not use repositories and other building blocks, but access the persistence layer directly - /// - public interface ITenantService - { - TenantId CreateDemonstrationTenant(string name, string description, string defaultCultureName); - TenantId CreateProductionTenant(string name, string description, string defaultCultureName); - void ActivateTenant(TenantId tenantId); - void DeactivateTenant(TenantId tenantId); - } - - public class TenantService : ITenantService - { - private readonly IEventBus _eventBus; - private readonly ITenantRepository _tenantRepository; - private static readonly ILogger Logger = LogManager.Create(); - - public TenantService(IEventBus eventBus, ITenantRepository tenantRepository) - { - _eventBus = eventBus; - _tenantRepository = tenantRepository; - } - - public TenantId CreateDemonstrationTenant(string name, string description, string defaultCultureName) - { - Logger.Info($"Creating demonstration tenant: {name}"); - return CreateTenant(name, description, true, defaultCultureName); - } - - public TenantId CreateProductionTenant(string name, string description, string defaultCultureName) - { - Logger.Info($"Creating production tenant: {name}"); - return CreateTenant(name, description, false, defaultCultureName); - } - - public void ActivateTenant(TenantId tenantId) - { - var tenant = _tenantRepository.GetTenant(tenantId); - tenant.State = TenantState.Active; - _tenantRepository.SaveTenant(tenant); - _eventBus.Publish(new TenantActivated(tenant.Id, tenant.Name, tenant.Description, tenant.IsDemoTenant, tenant.DefaultCultureName)); - } - - public void DeactivateTenant(TenantId tenantId) - { - var tenant = _tenantRepository.GetTenant(tenantId); - tenant.State = TenantState.Inactive; - _tenantRepository.SaveTenant(tenant); - _eventBus.Publish(new TenantDeactivated(tenant.Id, tenant.Name, tenant.Description, tenant.IsDemoTenant, tenant.DefaultCultureName)); - } - - protected virtual TenantId CreateTenant([NotNull] string name, string description, bool isDemo, string defaultCultureName) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); - } - - if (_tenantRepository.GetTenants().Any(t => t.Name != null && t.Name.ToLowerInvariant() == name.ToLowerInvariant())) - { - throw new ArgumentException($"There is already a tenant named {name}"); - } - - Tenant tenant = new Tenant(name, description, isDemo, defaultCultureName); - _tenantRepository.SaveTenant(tenant); - var tenantId = new TenantId(tenant.Id); - _eventBus.Publish(new TenantCreated(tenant.Id, tenant.Name, tenant.Description, tenant.IsDemoTenant, tenant.DefaultCultureName)); - return tenantId; - } - } -} diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/MultiTenantApplication.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/MultiTenantApplication.cs new file mode 100644 index 00000000..7d1de723 --- /dev/null +++ b/src/abstractions/Backend.Fx/Environment/MultiTenancy/MultiTenantApplication.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Patterns.DependencyInjection; +using Backend.Fx.Patterns.EventAggregation.Integration; + +namespace Backend.Fx.Environment.MultiTenancy +{ + public class MultiTenantApplication : TenantApplication, IBackendFxApplication + { + private readonly IBackendFxApplication _application; + + public MultiTenantApplication(IBackendFxApplication application) : base(application) + { + _application = application; + } + + public void Dispose() + { + _application.Dispose(); + } + + public IBackendFxApplicationAsyncInvoker AsyncInvoker => _application.AsyncInvoker; + + public ICompositionRoot CompositionRoot => _application.CompositionRoot; + + public IBackendFxApplicationInvoker Invoker => _application.Invoker; + + public IMessageBus MessageBus => _application.MessageBus; + + public bool WaitForBoot(int timeoutMilliSeconds = Int32.MaxValue, CancellationToken cancellationToken = default) + { + return _application.WaitForBoot(timeoutMilliSeconds, cancellationToken); + } + + public async Task Boot(CancellationToken cancellationToken = default) + { + EnableDataGenerationForNewTenants(); + + await _application.Boot(cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/SingleTenantApplication.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/SingleTenantApplication.cs new file mode 100644 index 00000000..dc5fa344 --- /dev/null +++ b/src/abstractions/Backend.Fx/Environment/MultiTenancy/SingleTenantApplication.cs @@ -0,0 +1,56 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Logging; +using Backend.Fx.Patterns.DependencyInjection; +using Backend.Fx.Patterns.EventAggregation.Integration; + +namespace Backend.Fx.Environment.MultiTenancy +{ + public class SingleTenantApplication : TenantApplication, IBackendFxApplication + { + private static readonly ILogger Logger = LogManager.Create(); + private readonly ITenantService _tenantService; + private readonly IBackendFxApplication _application; + private readonly TenantCreationParameters _tenantCreationParameters; + + public SingleTenantApplication(TenantCreationParameters tenantCreationParameters, ITenantService tenantService, IBackendFxApplication application) : base(application) + { + _tenantService = tenantService; + _application = application; + _tenantCreationParameters = tenantCreationParameters; + } + + public void Dispose() + { + _application.Dispose(); + } + + public IBackendFxApplicationAsyncInvoker AsyncInvoker => _application.AsyncInvoker; + + public ICompositionRoot CompositionRoot => _application.CompositionRoot; + + public IBackendFxApplicationInvoker Invoker => _application.Invoker; + + public IMessageBus MessageBus => _application.MessageBus; + + public TenantId TenantId { get; private set; } + + public bool WaitForBoot(int timeoutMilliSeconds = Int32.MaxValue, CancellationToken cancellationToken = default) + { + return _application.WaitForBoot(timeoutMilliSeconds, cancellationToken); + } + + public async Task Boot(CancellationToken cancellationToken = default) + { + EnableDataGenerationForNewTenants(); + + await _application.Boot(cancellationToken); + + Logger.Info($"Ensuring existence of single tenant {_tenantCreationParameters.Name}"); + TenantId = _tenantService.GetActiveTenantIds().SingleOrDefault() + ?? _tenantService.CreateTenant(_tenantCreationParameters); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/Tenant.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/Tenant.cs index 28417a63..f038f099 100644 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/Tenant.cs +++ b/src/abstractions/Backend.Fx/Environment/MultiTenancy/Tenant.cs @@ -1,11 +1,9 @@ -namespace Backend.Fx.Environment.MultiTenancy -{ - using System; - using System.ComponentModel.DataAnnotations; - using System.ComponentModel.DataAnnotations.Schema; - using System.Globalization; - using JetBrains.Annotations; +using System; +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; +namespace Backend.Fx.Environment.MultiTenancy +{ /// /// Represents a tenant in the application /// @@ -13,44 +11,32 @@ public class Tenant { [UsedImplicitly] private Tenant() - { } + { + } - public Tenant([NotNull] string name, string description, bool isDemoTenant, string defaultCultureName) + public Tenant([NotNull] string name, string description, bool isDemoTenant) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); Name = name; Description = description; IsDemoTenant = isDemoTenant; - DefaultCultureName = defaultCultureName; - State = TenantState.Created; + State = TenantState.Active; } - - [Key] - public int Id { get; set; } - [Required] - public string Name { get; set; } + [Key] public int Id { get; set; } + + [Required] public string Name { get; set; } public string Description { get; set; } public bool IsDemoTenant { get; set; } - - public TenantState State { get; set; } - - public string DefaultCultureName { get; set; } - [NotMapped] - public CultureInfo DefaultCulture - { - get => new CultureInfo(DefaultCultureName); - set => DefaultCultureName = value.Name; - } + public TenantState State { get; set; } } public enum TenantState { - Created = 0, Active = 2, Inactive = -1 } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantActivated.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantActivated.cs index 26f7dbc8..d4bc387d 100644 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantActivated.cs +++ b/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantActivated.cs @@ -2,8 +2,8 @@ { public class TenantActivated : TenantStatusChanged { - public TenantActivated(int tenantId, string name, string description, bool isDemoTenant, string defaultCultureName) - : base(tenantId, name, description, isDemoTenant, defaultCultureName) + public TenantActivated(int tenantId, string name, string description, bool isDemoTenant) + : base(tenantId, name, description, isDemoTenant) { } } diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantApplication.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantApplication.cs new file mode 100644 index 00000000..32ce9ac8 --- /dev/null +++ b/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantApplication.cs @@ -0,0 +1,39 @@ +using System; +using Backend.Fx.Logging; +using Backend.Fx.Patterns.DataGeneration; +using Backend.Fx.Patterns.DependencyInjection; +using Backend.Fx.Patterns.EventAggregation.Integration; + +namespace Backend.Fx.Environment.MultiTenancy +{ + public abstract class TenantApplication + { + private static readonly ILogger Logger = LogManager.Create(); + + private readonly IBackendFxApplication _application; + private readonly DataGenerationContext _dataGenerationContext; + + + protected TenantApplication(IBackendFxApplication application) + { + _application = application; + _dataGenerationContext = new DataGenerationContext(_application.CompositionRoot, _application.Invoker); + } + + protected void EnableDataGenerationForNewTenants() + { + _application.MessageBus.Subscribe(new DelegateIntegrationMessageHandler(tenantCreated => + { + Logger.Info($"Seeding data for recently activated {(tenantCreated.IsDemoTenant ? "demo " : "")}tenant {tenantCreated.TenantId}"); + try + { + _dataGenerationContext.SeedDataForTenant(new TenantId(tenantCreated.TenantId), tenantCreated.IsDemoTenant); + } + catch (Exception ex) + { + Logger.Error(ex, $"Seeding data for recently activated {(tenantCreated.IsDemoTenant ? "demo " : "")}tenant {tenantCreated.TenantId} failed."); + } + })); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantCreated.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantCreated.cs deleted file mode 100644 index cd026d2a..00000000 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantCreated.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Backend.Fx.Environment.MultiTenancy -{ - public class TenantCreated : TenantStatusChanged - { - public TenantCreated(int tenantId, string name, string description, bool isDemoTenant, string defaultCultureName) - : base(tenantId, name, description, isDemoTenant, defaultCultureName) - { - } - } -} diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantCreationParameters.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantCreationParameters.cs new file mode 100644 index 00000000..850646fa --- /dev/null +++ b/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantCreationParameters.cs @@ -0,0 +1,20 @@ +namespace Backend.Fx.Environment.MultiTenancy +{ + public class TenantCreationParameters + { + public TenantCreationParameters() + { + } + + public TenantCreationParameters(string name, string description, bool isDemonstrationTenant) + { + Name = name; + Description = description; + IsDemonstrationTenant = isDemonstrationTenant; + } + + public bool IsDemonstrationTenant { get; set; } + public string Name { get; set; } = "Tenant"; + public string Description { get; set; } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantDeactivated.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantDeactivated.cs index 818a716f..eb2399d9 100644 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantDeactivated.cs +++ b/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantDeactivated.cs @@ -1,23 +1,10 @@ -using Backend.Fx.Patterns.EventAggregation.Integration; - -namespace Backend.Fx.Environment.MultiTenancy +namespace Backend.Fx.Environment.MultiTenancy { - public class TenantDeactivated : IntegrationEvent + public class TenantDeactivated : TenantStatusChanged { - public TenantDeactivated(int tenantId, string name, string description, bool isDemoTenant, string defaultCultureName) : base(tenantId) + public TenantDeactivated(int tenantId, string name, string description, bool isDemoTenant) + : base(tenantId, name, description, isDemoTenant) { - Name = name; - Description = description; - IsDemoTenant = isDemoTenant; - DefaultCultureName = defaultCultureName; } - - public string Name { get; } - - public string Description { get; } - - public bool IsDemoTenant { get; } - - public string DefaultCultureName { get; } } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantId.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantId.cs index 48cd1c17..46b43d2c 100644 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantId.cs +++ b/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantId.cs @@ -1,11 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Backend.Fx.BuildingBlocks; namespace Backend.Fx.Environment.MultiTenancy { - using System; - using System.Diagnostics; - using BuildingBlocks; - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + "}")] public class TenantId : ValueObject { @@ -38,7 +37,6 @@ protected string DebuggerDisplay { get { - if (HasValue) { return $"TenantId: {Value}"; @@ -48,6 +46,11 @@ protected string DebuggerDisplay } } + public override string ToString() + { + return _id?.ToString() ?? "NULL"; + } + protected override IEnumerable GetEqualityComponents() { yield return _id; diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantService.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantService.cs new file mode 100644 index 00000000..54750aa2 --- /dev/null +++ b/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantService.cs @@ -0,0 +1,119 @@ +using System; +using System.Linq; +using Backend.Fx.Logging; +using Backend.Fx.Patterns.EventAggregation.Integration; +using JetBrains.Annotations; + +namespace Backend.Fx.Environment.MultiTenancy +{ + /// + /// Encapsulates the management of tenants + /// Note that this should not use repositories and other building blocks, but access the persistence layer directly + /// + public interface ITenantService + { + TenantId CreateTenant(TenantCreationParameters param); + void ActivateTenant(TenantId tenantId); + void DeactivateTenant(TenantId tenantId); + TenantId[] GetActiveTenantIds(); + TenantId[] GetActiveDemonstrationTenantIds(); + TenantId[] GetActiveProductionTenantIds(); + } + + public interface ITenantIdProvider + { + TenantId[] GetActiveDemonstrationTenantIds(); + TenantId[] GetActiveProductionTenantIds(); + } + + public class TenantService : ITenantService, ITenantIdProvider + { + private static readonly ILogger Logger = LogManager.Create(); + private readonly IMessageBus _messageBus; + private readonly ITenantRepository _tenantRepository; + + public TenantService(IMessageBus messageBus, ITenantRepository tenantRepository) + { + _messageBus = messageBus; + _tenantRepository = tenantRepository; + } + + public TenantId CreateTenant(TenantCreationParameters param) + { + Logger.Info($"Creating tenant: {param.Name}"); + return CreateTenant(param.Name, param.Description, param.IsDemonstrationTenant); + } + + public void ActivateTenant(TenantId tenantId) + { + Logger.Info($"Activating tenant: {tenantId}"); + Tenant tenant = _tenantRepository.GetTenant(tenantId); + tenant.State = TenantState.Active; + _tenantRepository.SaveTenant(tenant); + _messageBus.Publish(new TenantActivated(tenant.Id, tenant.Name, tenant.Description, tenant.IsDemoTenant)); + } + + public void DeactivateTenant(TenantId tenantId) + { + Logger.Info($"Deactivating tenant: {tenantId}"); + Tenant tenant = _tenantRepository.GetTenant(tenantId); + tenant.State = TenantState.Inactive; + _tenantRepository.SaveTenant(tenant); + _messageBus.Publish(new TenantDeactivated(tenant.Id, tenant.Name, tenant.Description, tenant.IsDemoTenant)); + } + + public TenantId[] GetActiveTenantIds() + { + var activeTenantIds = _tenantRepository + .GetTenants() + .Where(t => t.State == TenantState.Active) + .Select(t => new TenantId(t.Id)) + .ToArray(); + Logger.Trace($"Active TenantIds: {string.Join(",",activeTenantIds.Select(t => t.ToString()))}"); + return activeTenantIds; + } + + public TenantId[] GetActiveDemonstrationTenantIds() + { + var activeDemonstrationTenantIds = _tenantRepository + .GetTenants() + .Where(t => t.State == TenantState.Active && t.IsDemoTenant) + .Select(t => new TenantId(t.Id)) + .ToArray(); + Logger.Trace($"Active Demonstration TenantIds: {string.Join(",",activeDemonstrationTenantIds.Select(t => t.ToString()))}"); + return activeDemonstrationTenantIds; + } + + public TenantId[] GetActiveProductionTenantIds() + { + var activeProductionTenantIds = _tenantRepository + .GetTenants() + .Where(t => t.State == TenantState.Active && !t.IsDemoTenant) + .Select(t => new TenantId(t.Id)) + .ToArray(); + Logger.Trace($"Active Production TenantIds: {string.Join(",",activeProductionTenantIds.Select(t => t.ToString()))}"); + return activeProductionTenantIds; + } + + protected virtual TenantId CreateTenant([NotNull] string name, string description, bool isDemo) + { + Logger.Info($"Creating Tenant {name}"); + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); + } + + if (_tenantRepository.GetTenants().Any(t => t.Name != null && t.Name.ToLowerInvariant() == name.ToLowerInvariant())) + { + throw new ArgumentException($"There is already a tenant named {name}"); + } + + var tenant = new Tenant(name, description, isDemo); + _tenantRepository.SaveTenant(tenant); + var tenantId = new TenantId(tenant.Id); + _messageBus.Publish(new TenantActivated(tenant.Id, tenant.Name, tenant.Description, tenant.IsDemoTenant)); + return tenantId; + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantStatusChanged.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantStatusChanged.cs index c923089c..571ee46e 100644 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantStatusChanged.cs +++ b/src/abstractions/Backend.Fx/Environment/MultiTenancy/TenantStatusChanged.cs @@ -4,12 +4,11 @@ namespace Backend.Fx.Environment.MultiTenancy { public abstract class TenantStatusChanged : IntegrationEvent { - protected TenantStatusChanged(int tenantId, string name, string description, bool isDemoTenant, string defaultCultureName) : base(tenantId) + protected TenantStatusChanged(int tenantId, string name, string description, bool isDemoTenant) : base(tenantId) { Name = name; Description = description; IsDemoTenant = isDemoTenant; - DefaultCultureName = defaultCultureName; } public string Name { get; } @@ -17,7 +16,5 @@ protected TenantStatusChanged(int tenantId, string name, string description, boo public string Description { get; } public bool IsDemoTenant { get; } - - public string DefaultCultureName { get; } } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/Persistence/BackendFxDbApplication.cs b/src/abstractions/Backend.Fx/Environment/Persistence/BackendFxDbApplication.cs new file mode 100644 index 00000000..954ea561 --- /dev/null +++ b/src/abstractions/Backend.Fx/Environment/Persistence/BackendFxDbApplication.cs @@ -0,0 +1,57 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Logging; +using Backend.Fx.Patterns.DependencyInjection; +using Backend.Fx.Patterns.EventAggregation.Domain; +using Backend.Fx.Patterns.EventAggregation.Integration; + +namespace Backend.Fx.Environment.Persistence +{ + public class BackendFxDbApplication : IBackendFxApplication + { + private static readonly ILogger Logger = LogManager.Create(); + + private readonly IDatabaseBootstrapper _databaseBootstrapper; + private readonly IDatabaseAvailabilityAwaiter _databaseAvailabilityAwaiter; + private readonly IBackendFxApplication _backendFxApplication; + + public BackendFxDbApplication(IDatabaseBootstrapper databaseBootstrapper, + IDatabaseAvailabilityAwaiter databaseAvailabilityAwaiter, + IBackendFxApplication backendFxApplication) + { + _databaseBootstrapper = databaseBootstrapper; + _databaseAvailabilityAwaiter = databaseAvailabilityAwaiter; + _backendFxApplication = backendFxApplication; + } + + public void Dispose() + { + Logger.Trace("Disposing..."); + _databaseBootstrapper.Dispose(); + _backendFxApplication.Dispose(); + } + + public IBackendFxApplicationAsyncInvoker AsyncInvoker => _backendFxApplication.AsyncInvoker; + + public ICompositionRoot CompositionRoot => _backendFxApplication.CompositionRoot; + + public IBackendFxApplicationInvoker Invoker => _backendFxApplication.Invoker; + + public IMessageBus MessageBus => _backendFxApplication.MessageBus; + + public bool WaitForBoot(int timeoutMilliSeconds = Int32.MaxValue, CancellationToken cancellationToken = default) + { + Logger.Trace("Waiting for boot..."); + return _backendFxApplication.WaitForBoot(timeoutMilliSeconds, cancellationToken); + } + + public async Task Boot(CancellationToken cancellationToken = default) + { + Logger.Trace("Booting..."); + await _databaseAvailabilityAwaiter.WaitForDatabase(cancellationToken); + _databaseBootstrapper.EnsureDatabaseExistence(); + await _backendFxApplication.Boot(cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/Persistence/DbConnectionOperationDecorator.cs b/src/abstractions/Backend.Fx/Environment/Persistence/DbConnectionOperationDecorator.cs new file mode 100644 index 00000000..6355e974 --- /dev/null +++ b/src/abstractions/Backend.Fx/Environment/Persistence/DbConnectionOperationDecorator.cs @@ -0,0 +1,43 @@ +using System; +using System.Data; +using Backend.Fx.Logging; +using Backend.Fx.Patterns.DependencyInjection; + +namespace Backend.Fx.Environment.Persistence +{ + public class DbConnectionOperationDecorator : IOperation + { + private static readonly ILogger Logger = LogManager.Create(); + private IDisposable _connectionLifetimeLogger; + public DbConnectionOperationDecorator(IDbConnection dbConnection, IOperation operation) + { + DbConnection = dbConnection; + Operation = operation; + } + + public IOperation Operation { get; } + + public IDbConnection DbConnection { get; } + + public void Begin() + { + Logger.Debug("Opening database connection"); + DbConnection.Open(); + _connectionLifetimeLogger = Logger.DebugDuration("Database connection open", "Database connection closed"); + Operation.Begin(); + } + + public void Complete() + { + Operation.Complete(); + Logger.Debug("Closing database connection"); + DbConnection.Close(); + _connectionLifetimeLogger?.Dispose(); + } + + public void Cancel() + { + Operation.Cancel(); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/Persistence/DbTransactionOperationDecorator.cs b/src/abstractions/Backend.Fx/Environment/Persistence/DbTransactionOperationDecorator.cs new file mode 100644 index 00000000..51b79d16 --- /dev/null +++ b/src/abstractions/Backend.Fx/Environment/Persistence/DbTransactionOperationDecorator.cs @@ -0,0 +1,131 @@ +using System; +using System.Data; +using Backend.Fx.Logging; +using Backend.Fx.Patterns.DependencyInjection; + +namespace Backend.Fx.Environment.Persistence +{ + /// + /// Enriches the operation to use a database transaction during lifetime. The transaction gets started, before IOperation.Begin() + /// is being called and gets committed after IOperation.Complete() is being called. + /// + public class DbTransactionOperationDecorator : IOperation + { + private static readonly ILogger Logger = LogManager.Create(); + private readonly IDbConnection _dbConnection; + private readonly IOperation _operation; + private bool _shouldHandleConnectionState; + private IsolationLevel _isolationLevel = IsolationLevel.Unspecified; + private IDisposable _transactionLifetimeLogger; + private TxState _state = TxState.NotStarted; + + public DbTransactionOperationDecorator(IDbConnection dbConnection, IOperation operation) + { + _dbConnection = dbConnection; + _operation = operation; + } + + + public virtual void Begin() + { + if (_state != TxState.NotStarted) + { + throw new InvalidOperationException("A Transaction has been started by this operation before."); + } + + _shouldHandleConnectionState = ShouldHandleConnectionState(); + if (_shouldHandleConnectionState) + { + Logger.Debug("Opening connection"); + _dbConnection.Open(); + } + + Logger.Debug("Beginning transaction"); + CurrentTransaction = _dbConnection.BeginTransaction(_isolationLevel); + _transactionLifetimeLogger = Logger.DebugDuration("Transaction open", "Transaction terminated"); + _state = TxState.Active; + _operation.Begin(); + } + + public IDbTransaction CurrentTransaction { get; private set; } + + public void Complete() + { + if (_state != TxState.Active) + { + throw new InvalidOperationException($"A transaction cannot be committed when it is {_state}."); + } + + _operation.Complete(); + + Logger.Debug("Committing transaction"); + CurrentTransaction.Commit(); + CurrentTransaction.Dispose(); + CurrentTransaction = null; + _transactionLifetimeLogger?.Dispose(); + _transactionLifetimeLogger = null; + if (_shouldHandleConnectionState) + { + Logger.Debug("Closing connection"); + _dbConnection.Close(); + } + + _state = TxState.Committed; + } + + public void Cancel() + { + Logger.Debug("rolling back transaction"); + if (_state != TxState.Active) + { + throw new InvalidOperationException($"Cannot roll back a transaction that is {_state}"); + } + + _operation.Cancel(); + + CurrentTransaction.Rollback(); + CurrentTransaction.Dispose(); + CurrentTransaction = null; + + _transactionLifetimeLogger?.Dispose(); + _transactionLifetimeLogger = null; + if (_shouldHandleConnectionState) + { + _dbConnection.Close(); + } + + _state = TxState.RolledBack; + } + + public void SetIsolationLevel(IsolationLevel isolationLevel) + { + if (_state != TxState.NotStarted) + { + throw new InvalidOperationException("Isolation level cannot be changed after the transaction has been started"); + } + + _isolationLevel = isolationLevel; + } + + private bool ShouldHandleConnectionState() + { + switch (_dbConnection.State) + { + case ConnectionState.Closed: + return true; + case ConnectionState.Open: + return false; + default: + throw new InvalidOperationException($"A connection provided to the operation must either be closed or open, but must not be {_dbConnection.State}"); + } + } + + private enum TxState + { + NotStarted, + Active, + Committed, + RolledBack + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/Persistence/FlushDomainEventAggregatorDecorator.cs b/src/abstractions/Backend.Fx/Environment/Persistence/FlushDomainEventAggregatorDecorator.cs new file mode 100644 index 00000000..791bc42e --- /dev/null +++ b/src/abstractions/Backend.Fx/Environment/Persistence/FlushDomainEventAggregatorDecorator.cs @@ -0,0 +1,31 @@ +using Backend.Fx.Logging; +using Backend.Fx.Patterns.EventAggregation.Domain; + +namespace Backend.Fx.Environment.Persistence +{ + public class FlushDomainEventAggregatorDecorator : IDomainEventAggregator + { + private static readonly ILogger Logger = LogManager.Create(); + + private readonly ICanFlush _canFlush; + private readonly IDomainEventAggregator _domainEventAggregatorImplementation; + + public FlushDomainEventAggregatorDecorator(ICanFlush canFlush, IDomainEventAggregator domainEventAggregatorImplementation) + { + _canFlush = canFlush; + _domainEventAggregatorImplementation = domainEventAggregatorImplementation; + } + + public void PublishDomainEvent(TDomainEvent domainEvent) where TDomainEvent : IDomainEvent + { + _domainEventAggregatorImplementation.PublishDomainEvent(domainEvent); + } + + public void RaiseEvents() + { + Logger.Debug("Flushing before raising domain events"); + _canFlush.Flush(); + _domainEventAggregatorImplementation.RaiseEvents(); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/Persistence/FlushOperationDecorator.cs b/src/abstractions/Backend.Fx/Environment/Persistence/FlushOperationDecorator.cs new file mode 100644 index 00000000..3ee242f0 --- /dev/null +++ b/src/abstractions/Backend.Fx/Environment/Persistence/FlushOperationDecorator.cs @@ -0,0 +1,35 @@ +using Backend.Fx.Logging; +using Backend.Fx.Patterns.DependencyInjection; + +namespace Backend.Fx.Environment.Persistence +{ + public class FlushOperationDecorator : IOperation + { + private static readonly ILogger Logger = LogManager.Create(); + private readonly IOperation _operationImplementation; + private readonly ICanFlush _canFlush; + + public FlushOperationDecorator(ICanFlush canFlush, IOperation operationImplementation) + { + _operationImplementation = operationImplementation; + _canFlush = canFlush; + } + + public void Begin() + { + _operationImplementation.Begin(); + } + + public void Complete() + { + Logger.Debug("Flushing before completion of operation"); + _canFlush.Flush(); + _operationImplementation.Complete(); + } + + public void Cancel() + { + _operationImplementation.Cancel(); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/UnitOfWork/ICanFlush.cs b/src/abstractions/Backend.Fx/Environment/Persistence/ICanFlush.cs similarity index 58% rename from src/abstractions/Backend.Fx/Patterns/UnitOfWork/ICanFlush.cs rename to src/abstractions/Backend.Fx/Environment/Persistence/ICanFlush.cs index 4b8cc4cb..7e27683e 100644 --- a/src/abstractions/Backend.Fx/Patterns/UnitOfWork/ICanFlush.cs +++ b/src/abstractions/Backend.Fx/Environment/Persistence/ICanFlush.cs @@ -1,4 +1,4 @@ -namespace Backend.Fx.Patterns.UnitOfWork +namespace Backend.Fx.Environment.Persistence { public interface ICanFlush { diff --git a/src/abstractions/Backend.Fx/Environment/Persistence/IDatabaseAvailabilityAwaiter.cs b/src/abstractions/Backend.Fx/Environment/Persistence/IDatabaseAvailabilityAwaiter.cs new file mode 100644 index 00000000..7b21926e --- /dev/null +++ b/src/abstractions/Backend.Fx/Environment/Persistence/IDatabaseAvailabilityAwaiter.cs @@ -0,0 +1,10 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Backend.Fx.Environment.Persistence +{ + public interface IDatabaseAvailabilityAwaiter + { + Task WaitForDatabase(CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/Persistence/IDatabaseBootstrapper.cs b/src/abstractions/Backend.Fx/Environment/Persistence/IDatabaseBootstrapper.cs index 0115d696..65f1049a 100644 --- a/src/abstractions/Backend.Fx/Environment/Persistence/IDatabaseBootstrapper.cs +++ b/src/abstractions/Backend.Fx/Environment/Persistence/IDatabaseBootstrapper.cs @@ -9,4 +9,4 @@ public interface IDatabaseBootstrapper : IDisposable { void EnsureDatabaseExistence(); } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/Persistence/IDatabaseUtil.cs b/src/abstractions/Backend.Fx/Environment/Persistence/IDatabaseUtil.cs deleted file mode 100644 index c8ccc169..00000000 --- a/src/abstractions/Backend.Fx/Environment/Persistence/IDatabaseUtil.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Backend.Fx.Environment.Persistence -{ - public interface IDatabaseUtil - { - bool WaitUntilAvailable(int retries, Func sleepDurationProvider); - Task WaitUntilAvailableAsync(int retries, Func sleepDurationProvider, CancellationToken cancellationToken = default(CancellationToken)); - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/Persistence/ReadonlyDbTransactionOperationDecorator.cs b/src/abstractions/Backend.Fx/Environment/Persistence/ReadonlyDbTransactionOperationDecorator.cs new file mode 100644 index 00000000..fd26b42e --- /dev/null +++ b/src/abstractions/Backend.Fx/Environment/Persistence/ReadonlyDbTransactionOperationDecorator.cs @@ -0,0 +1,32 @@ +using Backend.Fx.Logging; +using Backend.Fx.Patterns.DependencyInjection; + +namespace Backend.Fx.Environment.Persistence +{ + public class ReadonlyDbTransactionOperationDecorator : IOperation + { + private static readonly ILogger Logger = LogManager.Create(); + private readonly IOperation _operationImplementation; + + public ReadonlyDbTransactionOperationDecorator(IOperation operationImplementation) + { + _operationImplementation = operationImplementation; + } + + public void Begin() + { + _operationImplementation.Begin(); + } + + public void Complete() + { + Logger.Debug("Canceling operation instead of completing it due to classification as readonly operation"); + _operationImplementation.Cancel(); + } + + public void Cancel() + { + _operationImplementation.Cancel(); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Exceptions/ClientException.cs b/src/abstractions/Backend.Fx/Exceptions/ClientException.cs index 055d0df0..0278b926 100644 --- a/src/abstractions/Backend.Fx/Exceptions/ClientException.cs +++ b/src/abstractions/Backend.Fx/Exceptions/ClientException.cs @@ -1,24 +1,26 @@ -namespace Backend.Fx.Exceptions -{ - using System; - using System.Collections.Generic; - using System.Linq; - using JetBrains.Annotations; - +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +namespace Backend.Fx.Exceptions +{ public class ClientException : Exception { public ClientException() - : base("Bad request.") - { } + : base("Bad request.") + { + } public ClientException(string message) - : base(message) - { } + : base(message) + { + } public ClientException(string message, Exception innerException) - : base(message, innerException) - { } + : base(message, innerException) + { + } public Errors Errors { get; } = new Errors(); @@ -33,18 +35,18 @@ public override string ToString() string exceptionType = GetType().ToString(); string message = string.IsNullOrEmpty(Message) - ? exceptionType - : exceptionType + ": " + Message; + ? exceptionType + : exceptionType + ": " + Message; string innerException = InnerException != null - ? " ---> " - + InnerException - + Environment.NewLine - + " End of inner exception stack trace" - : null; + ? " ---> " + + InnerException + + System.Environment.NewLine + + " End of inner exception stack trace" + : null; - return string.Join(Environment.NewLine, - new[] { message, Errors.ToString(), innerException, StackTrace }.Where(s => s != null)); + return string.Join(System.Environment.NewLine, + new[] {message, Errors.ToString(), innerException, StackTrace}.Where(s => s != null)); } } @@ -75,4 +77,4 @@ public static TEx AddErrors(this TEx clientException, string key, [Localiza return clientException; } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Exceptions/ConflictedException.cs b/src/abstractions/Backend.Fx/Exceptions/ConflictedException.cs index b88e57a9..471a3ea4 100644 --- a/src/abstractions/Backend.Fx/Exceptions/ConflictedException.cs +++ b/src/abstractions/Backend.Fx/Exceptions/ConflictedException.cs @@ -1,20 +1,23 @@ -namespace Backend.Fx.Exceptions -{ - using System; +using System; +namespace Backend.Fx.Exceptions +{ public class ConflictedException : ClientException { public ConflictedException() - : base("Conflicted") - { } + : base("Conflicted") + { + } public ConflictedException(string message) - : base(message) - { } + : base(message) + { + } public ConflictedException(string message, Exception innerException) - : base(message, innerException) - { } + : base(message, innerException) + { + } public static IExceptionBuilder UseBuilder() { diff --git a/src/abstractions/Backend.Fx/Exceptions/Errors.cs b/src/abstractions/Backend.Fx/Exceptions/Errors.cs index 1e2155af..1aeae363 100644 --- a/src/abstractions/Backend.Fx/Exceptions/Errors.cs +++ b/src/abstractions/Backend.Fx/Exceptions/Errors.cs @@ -1,10 +1,10 @@ -namespace Backend.Fx.Exceptions -{ - using System.Collections; - using System.Collections.Generic; - using System.Linq; - using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +namespace Backend.Fx.Exceptions +{ public class Errors : IReadOnlyDictionary { private const string GenericErrorKey = ""; @@ -88,7 +88,7 @@ IEnumerator IEnumerable.GetEnumerator() public override string ToString() { - StringBuilder b = new StringBuilder(); + var b = new StringBuilder(); b.Append("Errors: "); b.Append(Count.ToString()); b.AppendLine(); diff --git a/src/abstractions/Backend.Fx/Exceptions/ExceptionBuilder.cs b/src/abstractions/Backend.Fx/Exceptions/ExceptionBuilder.cs index 5947e227..79084a40 100644 --- a/src/abstractions/Backend.Fx/Exceptions/ExceptionBuilder.cs +++ b/src/abstractions/Backend.Fx/Exceptions/ExceptionBuilder.cs @@ -1,7 +1,7 @@ -namespace Backend.Fx.Exceptions -{ - using System; +using System; +namespace Backend.Fx.Exceptions +{ public interface IExceptionBuilder : IDisposable { void Add(string error); diff --git a/src/abstractions/Backend.Fx/Exceptions/ForbiddenException.cs b/src/abstractions/Backend.Fx/Exceptions/ForbiddenException.cs index 304496ed..f02ecebe 100644 --- a/src/abstractions/Backend.Fx/Exceptions/ForbiddenException.cs +++ b/src/abstractions/Backend.Fx/Exceptions/ForbiddenException.cs @@ -1,24 +1,27 @@ -namespace Backend.Fx.Exceptions -{ - using System; +using System; +namespace Backend.Fx.Exceptions +{ public class ForbiddenException : ClientException { - public ForbiddenException() - : base("Unauthorized") - { } + public ForbiddenException() + : base("Unauthorized") + { + } - public ForbiddenException(string message) - : base(message) - { } + public ForbiddenException(string message) + : base(message) + { + } public ForbiddenException(string message, Exception innerException) - : base(message, innerException) - { } - + : base(message, innerException) + { + } + public static IExceptionBuilder UseBuilder() { return new ExceptionBuilder(); } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Exceptions/NotFoundException.cs b/src/abstractions/Backend.Fx/Exceptions/NotFoundException.cs index 695791d0..74266c2c 100644 --- a/src/abstractions/Backend.Fx/Exceptions/NotFoundException.cs +++ b/src/abstractions/Backend.Fx/Exceptions/NotFoundException.cs @@ -1,12 +1,5 @@ namespace Backend.Fx.Exceptions { - public class NotFoundException : NotFoundException - { - public NotFoundException(object id) - : base(typeof(TEntity).Name, id) - {} - } - public class NotFoundException : ClientException { public string EntityName { get; } @@ -14,11 +7,12 @@ public class NotFoundException : ClientException public object Id { get; } public NotFoundException() - : base("Not found.") - {} - + : base("Not found.") + { + } + public NotFoundException(string entityName, object id) - : base($"No {entityName}[{id}] found.") + : base($"No {entityName}[{id}] found.") { EntityName = entityName; Id = id; @@ -29,4 +23,12 @@ public static IExceptionBuilder UseBuilder() return new ExceptionBuilder(); } } + + public class NotFoundException : NotFoundException + { + public NotFoundException(object id) + : base(typeof(TEntity).Name, id) + { + } + } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Exceptions/TooManyRequestsException.cs b/src/abstractions/Backend.Fx/Exceptions/TooManyRequestsException.cs index 8248853f..77a6766b 100644 --- a/src/abstractions/Backend.Fx/Exceptions/TooManyRequestsException.cs +++ b/src/abstractions/Backend.Fx/Exceptions/TooManyRequestsException.cs @@ -22,4 +22,4 @@ public TooManyRequestsException(int retryAfter, string message, Exception innerE public int RetryAfter { get; } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Exceptions/UnauthorizedException.cs b/src/abstractions/Backend.Fx/Exceptions/UnauthorizedException.cs index 892c7137..28fe1829 100644 --- a/src/abstractions/Backend.Fx/Exceptions/UnauthorizedException.cs +++ b/src/abstractions/Backend.Fx/Exceptions/UnauthorizedException.cs @@ -6,14 +6,17 @@ public class UnauthorizedException : ClientException { public UnauthorizedException() : base("Unauthorized") - { } - + { + } + public UnauthorizedException(string message) : base(message) - { } + { + } public UnauthorizedException(string message, Exception innerException) : base(message, innerException) - { } + { + } } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Exceptions/UnprocessableException.cs b/src/abstractions/Backend.Fx/Exceptions/UnprocessableException.cs index c6520d69..ce231520 100644 --- a/src/abstractions/Backend.Fx/Exceptions/UnprocessableException.cs +++ b/src/abstractions/Backend.Fx/Exceptions/UnprocessableException.cs @@ -1,20 +1,23 @@ -namespace Backend.Fx.Exceptions -{ - using System; +using System; +namespace Backend.Fx.Exceptions +{ public class UnprocessableException : ClientException { - public UnprocessableException() - : base("The provided arguments could not be processed.") - {} + public UnprocessableException() + : base("The provided arguments could not be processed.") + { + } - public UnprocessableException(string message) - : base(message) - { } + public UnprocessableException(string message) + : base(message) + { + } - public UnprocessableException(string message, Exception innerException) - : base(message, innerException) - { } + public UnprocessableException(string message, Exception innerException) + : base(message, innerException) + { + } public static IExceptionBuilder UseBuilder() { diff --git a/src/abstractions/Backend.Fx/Extensions/AsyncHelper.cs b/src/abstractions/Backend.Fx/Extensions/AsyncHelper.cs index d39025e0..46bf8915 100644 --- a/src/abstractions/Backend.Fx/Extensions/AsyncHelper.cs +++ b/src/abstractions/Backend.Fx/Extensions/AsyncHelper.cs @@ -1,10 +1,10 @@ -namespace Backend.Fx.Extensions -{ - using System; - using System.Collections.Generic; - using System.Threading; - using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +namespace Backend.Fx.Extensions +{ public static class AsyncHelper { /// @@ -17,27 +17,27 @@ public static class AsyncHelper /// public static void RunSync(Func task) { - var oldContext = SynchronizationContext.Current; + SynchronizationContext oldContext = SynchronizationContext.Current; try { var synch = new ExclusiveSynchronizationContext(); SynchronizationContext.SetSynchronizationContext(synch); synch.Post(async _ => - { - try - { - await task(); - } - catch (Exception e) - { - synch.InnerException = e; - throw; - } - finally - { - synch.EndMessageLoop(); - } - }, null); + { + try + { + await task(); + } + catch (Exception e) + { + synch.InnerException = e; + throw; + } + finally + { + synch.EndMessageLoop(); + } + }, null); synch.BeginMessageLoop(); } finally @@ -54,34 +54,35 @@ public static void RunSync(Func task) /// public static T RunSync(Func> task) { - T ret = default(T); - var oldContext = SynchronizationContext.Current; + var ret = default(T); + SynchronizationContext oldContext = SynchronizationContext.Current; try { var exclusiveSynchronizationContext = new ExclusiveSynchronizationContext(); SynchronizationContext.SetSynchronizationContext(exclusiveSynchronizationContext); exclusiveSynchronizationContext.Post(async _ => - { - try - { - ret = await task(); - } - catch (Exception e) - { - exclusiveSynchronizationContext.InnerException = e; - throw; - } - finally - { - exclusiveSynchronizationContext.EndMessageLoop(); - } - }, null); + { + try + { + ret = await task(); + } + catch (Exception e) + { + exclusiveSynchronizationContext.InnerException = e; + throw; + } + finally + { + exclusiveSynchronizationContext.EndMessageLoop(); + } + }, null); exclusiveSynchronizationContext.BeginMessageLoop(); } finally { SynchronizationContext.SetSynchronizationContext(oldContext); } + return ret; } @@ -89,8 +90,8 @@ private class ExclusiveSynchronizationContext : SynchronizationContext { private bool _done; public Exception InnerException { private get; set; } - readonly AutoResetEvent _workItemsWaiting = new AutoResetEvent(false); - readonly Queue> _items = new Queue>(); + private readonly AutoResetEvent _workItemsWaiting = new AutoResetEvent(false); + private readonly Queue> _items = new Queue>(); public override void Send(SendOrPostCallback d, object state) { @@ -103,6 +104,7 @@ public override void Post(SendOrPostCallback d, object state) { _items.Enqueue(Tuple.Create(d, state)); } + _workItemsWaiting.Set(); } @@ -123,6 +125,7 @@ public void BeginMessageLoop() task = _items.Dequeue(); } } + if (task != null) { task.Item1(task.Item2); diff --git a/src/abstractions/Backend.Fx/Extensions/DateTimeEx.cs b/src/abstractions/Backend.Fx/Extensions/DateTimeEx.cs index b015003f..0d7ad38d 100644 --- a/src/abstractions/Backend.Fx/Extensions/DateTimeEx.cs +++ b/src/abstractions/Backend.Fx/Extensions/DateTimeEx.cs @@ -17,13 +17,14 @@ public static DateTime StartOfWeek(this DateTime dt, DayOfWeek startOfWeek = Day { diff += 7; } + return dt.AddDays(-1 * diff).Date; } public static DateTime GetWeekDay(this DateTime dt, DayOfWeek dayOfWeek, DayOfWeek startOfWeek = DayOfWeek.Monday) { dt = dt.StartOfWeek(startOfWeek); - while(dt.DayOfWeek != dayOfWeek) + while (dt.DayOfWeek != dayOfWeek) { dt = dt.AddDays(1); } @@ -33,7 +34,7 @@ public static DateTime GetWeekDay(this DateTime dt, DayOfWeek dayOfWeek, DayOfWe public static long ToUnixEpochDate(this DateTime utcDate) { - return (long)Math.Round((utcDate - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds); + return (long) Math.Round((utcDate - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds); } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Extensions/EnumerableEx.cs b/src/abstractions/Backend.Fx/Extensions/EnumerableEx.cs index b4b8bf56..97dfb7c6 100644 --- a/src/abstractions/Backend.Fx/Extensions/EnumerableEx.cs +++ b/src/abstractions/Backend.Fx/Extensions/EnumerableEx.cs @@ -7,10 +7,10 @@ public static class EnumerableEx { public static void ForAll(this IEnumerable enumerable, Action action) { - foreach (var item in enumerable) + foreach (T item in enumerable) { action(item); } } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Extensions/MultipleDisposable.cs b/src/abstractions/Backend.Fx/Extensions/MultipleDisposable.cs index 0190f36a..df5b80d5 100644 --- a/src/abstractions/Backend.Fx/Extensions/MultipleDisposable.cs +++ b/src/abstractions/Backend.Fx/Extensions/MultipleDisposable.cs @@ -13,10 +13,10 @@ public MultipleDisposable(params IDisposable[] disposables) public void Dispose() { - foreach (var disposable in _disposables) + foreach (IDisposable disposable in _disposables) { disposable?.Dispose(); } } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Extensions/ReaderWriterLockSlimExtensions.cs b/src/abstractions/Backend.Fx/Extensions/ReaderWriterLockSlimExtensions.cs index d0464798..7ad0645d 100644 --- a/src/abstractions/Backend.Fx/Extensions/ReaderWriterLockSlimExtensions.cs +++ b/src/abstractions/Backend.Fx/Extensions/ReaderWriterLockSlimExtensions.cs @@ -8,11 +8,13 @@ public static class ReaderWriterLockSlimExtensions private sealed class ReadLockToken : IDisposable { private ReaderWriterLockSlim _sync; + public ReadLockToken(ReaderWriterLockSlim sync) { _sync = sync; sync.EnterReadLock(); } + public void Dispose() { if (_sync != null) @@ -22,14 +24,17 @@ public void Dispose() } } } + private sealed class WriteLockToken : IDisposable { private ReaderWriterLockSlim _sync; + public WriteLockToken(ReaderWriterLockSlim sync) { _sync = sync; sync.EnterWriteLock(); } + public void Dispose() { if (_sync != null) @@ -44,9 +49,10 @@ public static IDisposable Read(this ReaderWriterLockSlim obj) { return new ReadLockToken(obj); } + public static IDisposable Write(this ReaderWriterLockSlim obj) { return new WriteLockToken(obj); } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Extensions/ReflectionEx.cs b/src/abstractions/Backend.Fx/Extensions/ReflectionEx.cs index 34065952..1d357fa8 100644 --- a/src/abstractions/Backend.Fx/Extensions/ReflectionEx.cs +++ b/src/abstractions/Backend.Fx/Extensions/ReflectionEx.cs @@ -1,10 +1,9 @@ using System; +using System.Linq; +using System.Reflection; namespace Backend.Fx.Extensions { - using System.Linq; - using System.Reflection; - public static class ReflectionEx { public static bool IsImplementationOfOpenGenericInterface(this Type t, Type openGenericInterface) @@ -25,4 +24,4 @@ public static string GetDetailedTypeName(this Type t) return detailedTypeName; } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Extensions/StringEnumUtil.cs b/src/abstractions/Backend.Fx/Extensions/StringEnumUtil.cs index 9966ebb9..de594405 100644 --- a/src/abstractions/Backend.Fx/Extensions/StringEnumUtil.cs +++ b/src/abstractions/Backend.Fx/Extensions/StringEnumUtil.cs @@ -1,13 +1,13 @@ -namespace Backend.Fx.Extensions -{ - using System; - using System.Linq; +using System; +using System.Linq; +namespace Backend.Fx.Extensions +{ public static class StringEnumUtil { public static TEnum Parse(this string value) where TEnum : struct { - if(Enum.TryParse(value, true, out TEnum enumValue)) + if (Enum.TryParse(value, true, out TEnum enumValue)) { return enumValue; } @@ -17,4 +17,4 @@ public static TEnum Parse(this string value) where TEnum : struct throw new ArgumentException($"The string [{value}] is not a valid value for the enum type {typeof(TEnum).Name}. Valid string values are: [{validValuesString}]"); } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Extensions/StringEx.cs b/src/abstractions/Backend.Fx/Extensions/StringEx.cs index dbf24d23..54091267 100644 --- a/src/abstractions/Backend.Fx/Extensions/StringEx.cs +++ b/src/abstractions/Backend.Fx/Extensions/StringEx.cs @@ -1,7 +1,7 @@ -namespace Backend.Fx.Extensions -{ - using System.Text.RegularExpressions; +using System.Text.RegularExpressions; +namespace Backend.Fx.Extensions +{ public static class StringEx { public static string Cut(this string s, int length) @@ -29,4 +29,4 @@ public static string ToMacintoshLineEnding(this string s) return Regex.Replace(s, @"\r?\n", "\r"); } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Extensions/TolerantDateTimeComparer.cs b/src/abstractions/Backend.Fx/Extensions/TolerantDateTimeComparer.cs index b759448d..cb401099 100644 --- a/src/abstractions/Backend.Fx/Extensions/TolerantDateTimeComparer.cs +++ b/src/abstractions/Backend.Fx/Extensions/TolerantDateTimeComparer.cs @@ -50,4 +50,4 @@ public int GetHashCode(DateTime? obj) return obj?.GetHashCode() ?? 0; } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Hacking/PrivateSetterCaller.cs b/src/abstractions/Backend.Fx/Hacking/PrivateSetterCaller.cs index 32a365fd..db95c07b 100644 --- a/src/abstractions/Backend.Fx/Hacking/PrivateSetterCaller.cs +++ b/src/abstractions/Backend.Fx/Hacking/PrivateSetterCaller.cs @@ -16,7 +16,7 @@ private static string GetName(Expression> exp) { if (!(exp.Body is MemberExpression body)) { - UnaryExpression ubody = (UnaryExpression)exp.Body; + var ubody = (UnaryExpression) exp.Body; body = ubody.Operand as MemberExpression; } @@ -24,4 +24,4 @@ private static string GetName(Expression> exp) return body.Member.Name; } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Hacking/PrivateUtil.cs b/src/abstractions/Backend.Fx/Hacking/PrivateUtil.cs index 23ab30de..a2cb535c 100644 --- a/src/abstractions/Backend.Fx/Hacking/PrivateUtil.cs +++ b/src/abstractions/Backend.Fx/Hacking/PrivateUtil.cs @@ -8,12 +8,13 @@ public static class PrivateUtil { public static T CreateInstanceFromPrivateDefaultConstructor() { - var constructor = typeof(T).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance).SingleOrDefault(ci => ci.GetParameters().Length == 0); + ConstructorInfo constructor = typeof(T).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance).SingleOrDefault(ci => ci.GetParameters().Length == 0); if (constructor == null) { throw new InvalidOperationException($"No private default constructor found in {typeof(T).Name}"); } - T instance = (T)constructor.Invoke(null); + + var instance = (T) constructor.Invoke(null); return instance; } } diff --git a/src/abstractions/Backend.Fx/Logging/DebugExceptionLogger.cs b/src/abstractions/Backend.Fx/Logging/DebugExceptionLogger.cs index 67da033e..8db9eee5 100644 --- a/src/abstractions/Backend.Fx/Logging/DebugExceptionLogger.cs +++ b/src/abstractions/Backend.Fx/Logging/DebugExceptionLogger.cs @@ -1,17 +1,16 @@ -using Backend.Fx.Exceptions; +using System; +using System.Diagnostics; +using Backend.Fx.Exceptions; namespace Backend.Fx.Logging { - using System; - using System.Diagnostics; - public class DebugExceptionLogger : IExceptionLogger { public void LogException(Exception exception) { if (exception is ClientException cex) { - Debug.WriteLine(cex + Environment.NewLine + cex.Errors); + Debug.WriteLine(cex + System.Environment.NewLine + cex.Errors); } else { diff --git a/src/abstractions/Backend.Fx/Logging/DebugLogger.cs b/src/abstractions/Backend.Fx/Logging/DebugLogger.cs index 2c1d4fa1..7e2b408b 100644 --- a/src/abstractions/Backend.Fx/Logging/DebugLogger.cs +++ b/src/abstractions/Backend.Fx/Logging/DebugLogger.cs @@ -1,8 +1,8 @@ -namespace Backend.Fx.Logging -{ - using System; - using System.Diagnostics; +using System; +using System.Diagnostics; +namespace Backend.Fx.Logging +{ [DebuggerStepThrough] public class DebugLogger : ILogger { @@ -169,6 +169,5 @@ private void PrintToDebug(string format, params object[] args) { System.Diagnostics.Debug.WriteLine(_type + format, args); } - } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Logging/DebugLoggerFactory.cs b/src/abstractions/Backend.Fx/Logging/DebugLoggerFactory.cs index b1f38845..5dd6a59a 100644 --- a/src/abstractions/Backend.Fx/Logging/DebugLoggerFactory.cs +++ b/src/abstractions/Backend.Fx/Logging/DebugLoggerFactory.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; namespace Backend.Fx.Logging { @@ -10,12 +11,30 @@ public ILogger Create(string s) return new DebugLogger(s); } + public ILogger Create(Type t) + { + string s = t.FullName; + var indexOf = s?.IndexOf('[') ?? 0; + if (indexOf > 0) + { + s = s?.Substring(0, indexOf); + } + + return Create(s); + } + + public ILogger Create() + { + return Create(typeof(T)); + } + public void BeginActivity(int activityIndex) { Debug.WriteLine($"Beginning activity {activityIndex}"); } public void Shutdown() - {} + { + } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Logging/DurationLogger.cs b/src/abstractions/Backend.Fx/Logging/DurationLogger.cs index b8b78fcb..d8c36e38 100644 --- a/src/abstractions/Backend.Fx/Logging/DurationLogger.cs +++ b/src/abstractions/Backend.Fx/Logging/DurationLogger.cs @@ -1,8 +1,8 @@ -namespace Backend.Fx.Logging -{ - using System; - using System.Diagnostics; +using System; +using System.Diagnostics; +namespace Backend.Fx.Logging +{ [DebuggerStepThrough] public class DurationLogger : IDisposable { @@ -12,7 +12,9 @@ public class DurationLogger : IDisposable private readonly Stopwatch _stopwatch = new Stopwatch(); public DurationLogger(Action logAction, string activity) - : this(logAction, activity, activity) {} + : this(logAction, activity, activity) + { + } public DurationLogger(Action logAction, string beginMessage, string endMessage) { diff --git a/src/abstractions/Backend.Fx/Logging/ExceptionExtensions.cs b/src/abstractions/Backend.Fx/Logging/ExceptionExtensions.cs index f2130f64..2d4d1859 100644 --- a/src/abstractions/Backend.Fx/Logging/ExceptionExtensions.cs +++ b/src/abstractions/Backend.Fx/Logging/ExceptionExtensions.cs @@ -1,10 +1,10 @@ -namespace Backend.Fx.Logging -{ - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Linq; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +namespace Backend.Fx.Logging +{ [DebuggerStepThrough] public static class ExceptionExtensions { diff --git a/src/abstractions/Backend.Fx/Logging/ExceptionLoggers.cs b/src/abstractions/Backend.Fx/Logging/ExceptionLoggers.cs index a4d4fd5a..aec1ef44 100644 --- a/src/abstractions/Backend.Fx/Logging/ExceptionLoggers.cs +++ b/src/abstractions/Backend.Fx/Logging/ExceptionLoggers.cs @@ -1,17 +1,28 @@ -namespace Backend.Fx.Logging -{ - using System; - using System.Collections; - using System.Collections.Generic; +using System; +using System.Collections; +using System.Collections.Generic; +namespace Backend.Fx.Logging +{ public class ExceptionLoggers : ICollection, IExceptionLogger { private static readonly ILogger Logger = LogManager.Create(); private readonly ICollection _collectionImplementation = new List(); + public ExceptionLoggers() + { } + + public ExceptionLoggers(params IExceptionLogger[] exceptionLoggers) + { + foreach (IExceptionLogger exceptionLogger in exceptionLoggers) + { + _collectionImplementation.Add(exceptionLogger); + } + } + public void LogException(Exception ex) { - foreach (var exceptionLogger in _collectionImplementation) + foreach (IExceptionLogger exceptionLogger in _collectionImplementation) { try { @@ -19,8 +30,7 @@ public void LogException(Exception ex) } catch (Exception ex2) { - Logger.Error(ex, - $"{exceptionLogger.GetType().Name} failed to log the {ex2.GetType()} mith message {ex.Message}"); + Logger.Error(ex, $"{exceptionLogger.GetType().Name} failed to log the {ex2.GetType()} with message {ex.Message}"); } } } @@ -32,7 +42,7 @@ public IEnumerator GetEnumerator() IEnumerator IEnumerable.GetEnumerator() { - return ((IEnumerable)_collectionImplementation).GetEnumerator(); + return ((IEnumerable) _collectionImplementation).GetEnumerator(); } public void Add(IExceptionLogger item) diff --git a/src/abstractions/Backend.Fx/Logging/IExceptionLogger.cs b/src/abstractions/Backend.Fx/Logging/IExceptionLogger.cs index bacbb910..d36959c7 100644 --- a/src/abstractions/Backend.Fx/Logging/IExceptionLogger.cs +++ b/src/abstractions/Backend.Fx/Logging/IExceptionLogger.cs @@ -1,8 +1,8 @@ -namespace Backend.Fx.Logging -{ - using System; - using Exceptions; +using System; +using Backend.Fx.Exceptions; +namespace Backend.Fx.Logging +{ public interface IExceptionLogger { void LogException(Exception exception); @@ -29,4 +29,4 @@ public void LogException(Exception exception) } } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Logging/ILogger.cs b/src/abstractions/Backend.Fx/Logging/ILogger.cs index c1a3f1f0..f7e5c24b 100644 --- a/src/abstractions/Backend.Fx/Logging/ILogger.cs +++ b/src/abstractions/Backend.Fx/Logging/ILogger.cs @@ -1,13 +1,13 @@ // ReSharper disable UnusedMethodReturnValue.Global +using System; +using JetBrains.Annotations; namespace Backend.Fx.Logging { - using System; - using JetBrains.Annotations; - public interface ILogger { #region fatal + Exception Fatal(Exception exception); [StringFormatMethod("format")] @@ -15,9 +15,11 @@ public interface ILogger [StringFormatMethod("format")] Exception Fatal(Exception exception, string format, params object[] args); + #endregion #region error + Exception Error(Exception exception); [StringFormatMethod("format")] @@ -25,9 +27,11 @@ public interface ILogger [StringFormatMethod("format")] Exception Error(Exception exception, string format, params object[] args); + #endregion #region warn + Exception Warn(Exception exception); [StringFormatMethod("format")] @@ -35,9 +39,11 @@ public interface ILogger [StringFormatMethod("format")] Exception Warn(Exception exception, string format, params object[] args); + #endregion #region info + Exception Info(Exception exception); IDisposable InfoDuration(string activity); @@ -49,9 +55,11 @@ public interface ILogger [StringFormatMethod("format")] Exception Info(Exception exception, string format, params object[] args); + #endregion #region debug + bool IsDebugEnabled(); Exception Debug(Exception exception); @@ -65,9 +73,11 @@ public interface ILogger [StringFormatMethod("format")] Exception Debug(Exception exception, string format, params object[] args); + #endregion #region Trace + bool IsTraceEnabled(); Exception Trace(Exception exception); @@ -81,6 +91,7 @@ public interface ILogger [StringFormatMethod("format")] Exception Trace(Exception exception, string format, params object[] args); + #endregion } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Logging/ILoggerFactory.cs b/src/abstractions/Backend.Fx/Logging/ILoggerFactory.cs index d4832e18..2aa76862 100644 --- a/src/abstractions/Backend.Fx/Logging/ILoggerFactory.cs +++ b/src/abstractions/Backend.Fx/Logging/ILoggerFactory.cs @@ -1,8 +1,13 @@ -namespace Backend.Fx.Logging +using System; + +namespace Backend.Fx.Logging { public interface ILoggerFactory { ILogger Create(string s); + ILogger Create(Type t); + ILogger Create(); + void BeginActivity(int activityIndex); void Shutdown(); } diff --git a/src/abstractions/Backend.Fx/Logging/LogManager.cs b/src/abstractions/Backend.Fx/Logging/LogManager.cs index aa5dcafe..8319f65a 100644 --- a/src/abstractions/Backend.Fx/Logging/LogManager.cs +++ b/src/abstractions/Backend.Fx/Logging/LogManager.cs @@ -1,13 +1,13 @@ -namespace Backend.Fx.Logging -{ - using System; - using System.Diagnostics; - using System.Threading; +using System; +using System.Diagnostics; +using System.Threading; +namespace Backend.Fx.Logging +{ [DebuggerStepThrough] public abstract class LogManager { - private static int _activityIndex=1; + private static int _activityIndex = 1; private static ILoggerFactory _factory = new DebugLoggerFactory(); public static void Initialize(ILoggerFactory theFactory) @@ -17,18 +17,12 @@ public static void Initialize(ILoggerFactory theFactory) public static ILogger Create() { - return Create(typeof(T)); + return _factory.Create(); } public static ILogger Create(Type t) { - string s = t.FullName; - var indexOf = s?.IndexOf('[') ?? 0; - if (indexOf > 0) - { - s = s?.Substring(0, indexOf); - } - return Create(s); + return _factory.Create(t); } public static ILogger Create(string s) diff --git a/src/abstractions/Backend.Fx/Patterns/Authorization/AggregateAuthorization.cs b/src/abstractions/Backend.Fx/Patterns/Authorization/AggregateAuthorization.cs index 4d554036..759cea10 100644 --- a/src/abstractions/Backend.Fx/Patterns/Authorization/AggregateAuthorization.cs +++ b/src/abstractions/Backend.Fx/Patterns/Authorization/AggregateAuthorization.cs @@ -1,17 +1,17 @@ -namespace Backend.Fx.Patterns.Authorization -{ - using System; - using System.Linq; - using System.Linq.Expressions; - using BuildingBlocks; +using System; +using System.Linq; +using System.Linq.Expressions; +using Backend.Fx.BuildingBlocks; +using Backend.Fx.Logging; +namespace Backend.Fx.Patterns.Authorization +{ public abstract class AggregateAuthorization : IAggregateAuthorization where TAggregateRoot : AggregateRoot { + private static readonly ILogger Logger = LogManager.Create>(); + /// > - public virtual Expression> HasAccessExpression - { - get { return agg => true; } - } + public abstract Expression> HasAccessExpression { get; } /// > public virtual IQueryable Filter(IQueryable queryable) @@ -20,10 +20,7 @@ public virtual IQueryable Filter(IQueryable quer } /// > - public virtual bool CanCreate(TAggregateRoot t) - { - return true; - } + public abstract bool CanCreate(TAggregateRoot t); /// /// Implement a guard that might disallow modifying an existing aggregate. @@ -32,13 +29,17 @@ public virtual bool CanCreate(TAggregateRoot t) /// public virtual bool CanModify(TAggregateRoot t) { - return CanCreate(t); + var canCreate = CanCreate(t); + Logger.Trace($"CanCreate({t.DebuggerDisplay}): {canCreate}"); + return canCreate; } /// > public virtual bool CanDelete(TAggregateRoot t) { - return CanModify(t); + var canModify = CanModify(t); + Logger.Trace($"CanModify({t.DebuggerDisplay}): {canModify}"); + return canModify; } } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/Authorization/AllowAll.cs b/src/abstractions/Backend.Fx/Patterns/Authorization/AllowAll.cs index 12b4fa51..6b416d79 100644 --- a/src/abstractions/Backend.Fx/Patterns/Authorization/AllowAll.cs +++ b/src/abstractions/Backend.Fx/Patterns/Authorization/AllowAll.cs @@ -1,7 +1,19 @@ -namespace Backend.Fx.Patterns.Authorization -{ - using BuildingBlocks; +using System; +using System.Linq.Expressions; +using Backend.Fx.BuildingBlocks; +namespace Backend.Fx.Patterns.Authorization +{ public class AllowAll : AggregateAuthorization where TAggregateRoot : AggregateRoot - {} + { + public override Expression> HasAccessExpression + { + get { return agg => true; } + } + + public override bool CanCreate(TAggregateRoot t) + { + return true; + } + } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/Authorization/DenyAll.cs b/src/abstractions/Backend.Fx/Patterns/Authorization/DenyAll.cs index 1567eea4..3e99e961 100644 --- a/src/abstractions/Backend.Fx/Patterns/Authorization/DenyAll.cs +++ b/src/abstractions/Backend.Fx/Patterns/Authorization/DenyAll.cs @@ -1,9 +1,9 @@ -namespace Backend.Fx.Patterns.Authorization -{ - using System; - using System.Linq.Expressions; - using BuildingBlocks; +using System; +using System.Linq.Expressions; +using Backend.Fx.BuildingBlocks; +namespace Backend.Fx.Patterns.Authorization +{ public class DenyAll : AggregateAuthorization where TAggregateRoot : AggregateRoot { public override Expression> HasAccessExpression @@ -15,10 +15,5 @@ public override bool CanCreate(TAggregateRoot t) { return false; } - - public override bool CanModify(TAggregateRoot t) - { - return false; - } } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/Authorization/IAggregateAuthorization.cs b/src/abstractions/Backend.Fx/Patterns/Authorization/IAggregateAuthorization.cs index c66d73e5..990b8583 100644 --- a/src/abstractions/Backend.Fx/Patterns/Authorization/IAggregateAuthorization.cs +++ b/src/abstractions/Backend.Fx/Patterns/Authorization/IAggregateAuthorization.cs @@ -34,4 +34,4 @@ public interface IAggregateAuthorization where TAggregateRoot : /// bool CanDelete(TAggregateRoot t); } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DataGeneration/DataGenerationContext.cs b/src/abstractions/Backend.Fx/Patterns/DataGeneration/DataGenerationContext.cs new file mode 100644 index 00000000..ec544dbc --- /dev/null +++ b/src/abstractions/Backend.Fx/Patterns/DataGeneration/DataGenerationContext.cs @@ -0,0 +1,66 @@ +using System; +using System.Linq; +using System.Reflection; +using Backend.Fx.Environment.Authentication; +using Backend.Fx.Environment.MultiTenancy; +using Backend.Fx.Logging; +using Backend.Fx.Patterns.DependencyInjection; + +namespace Backend.Fx.Patterns.DataGeneration +{ + public interface IDataGenerationContext + { + void SeedDataForTenant(TenantId tenantId, bool isDemoTenant); + } + + public class DataGenerationContext : IDataGenerationContext + { + private static readonly ILogger Logger = LogManager.Create(); + + private readonly ICompositionRoot _compositionRoot; + private readonly IBackendFxApplicationInvoker _invoker; + + public DataGenerationContext(ICompositionRoot compositionRoot, IBackendFxApplicationInvoker invoker) + { + _compositionRoot = compositionRoot; + _invoker = invoker; + } + + public void SeedDataForTenant(TenantId tenantId, bool isDemoTenant) + { + using (Logger.InfoDuration($"Seeding data for tenant {tenantId.Value}")) + { + Type[] dataGeneratorTypesToRun = GetDataGeneratorTypes(_compositionRoot, isDemoTenant); + foreach (Type dataGeneratorTypeToRun in dataGeneratorTypesToRun) + { + _invoker.Invoke(instanceProvider => + { + IDataGenerator dataGenerator = instanceProvider + .GetInstances() + .Single(dg => dg.GetType() == dataGeneratorTypeToRun); + dataGenerator.Generate(); + }, new SystemIdentity(), tenantId); + } + } + } + + private static Type[] GetDataGeneratorTypes(ICompositionRoot compositionRoot, bool includeDemoDataGenerators) + { + using (IInjectionScope scope = compositionRoot.BeginScope()) + { + var dataGenerators = scope + .InstanceProvider + .GetInstances() + .OrderBy(dg => dg.Priority) + .Select(dg => dg.GetType()); + + if (!includeDemoDataGenerators) + { + dataGenerators = dataGenerators.Where(dg => !typeof(IDemoDataGenerator).IsAssignableFrom(dg)); + } + + return dataGenerators.ToArray(); + } + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DataGeneration/GenerateDataOnBoot.cs b/src/abstractions/Backend.Fx/Patterns/DataGeneration/GenerateDataOnBoot.cs new file mode 100644 index 00000000..a2b36f36 --- /dev/null +++ b/src/abstractions/Backend.Fx/Patterns/DataGeneration/GenerateDataOnBoot.cs @@ -0,0 +1,72 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Environment.MultiTenancy; +using Backend.Fx.Logging; +using Backend.Fx.Patterns.DependencyInjection; +using Backend.Fx.Patterns.EventAggregation.Integration; +using JetBrains.Annotations; + +namespace Backend.Fx.Patterns.DataGeneration +{ + public class GenerateDataOnBoot : IBackendFxApplication + { + private static readonly ILogger Logger = LogManager.Create(); + private readonly ITenantIdProvider _tenantIdProvider; + private readonly IBackendFxApplication _application; + private readonly IModule _dataGenerationModule; + public IDataGenerationContext DataGenerationContext { get; [UsedImplicitly] private set; } + + public GenerateDataOnBoot(ITenantIdProvider tenantIdProvider, IModule dataGenerationModule, IBackendFxApplication application) + { + _tenantIdProvider = tenantIdProvider; + _application = application; + _dataGenerationModule = dataGenerationModule; + DataGenerationContext = new DataGenerationContext(_application.CompositionRoot, + _application.Invoker); + } + + public void Dispose() + { + _application.Dispose(); + } + + public IBackendFxApplicationAsyncInvoker AsyncInvoker => _application.AsyncInvoker; + public ICompositionRoot CompositionRoot => _application.CompositionRoot; + public IBackendFxApplicationInvoker Invoker => _application.Invoker; + + public IMessageBus MessageBus => _application.MessageBus; + + public bool WaitForBoot(int timeoutMilliSeconds = Int32.MaxValue, CancellationToken cancellationToken = default) + { + return _application.WaitForBoot(timeoutMilliSeconds, cancellationToken); + } + + public async Task Boot(CancellationToken cancellationToken = default) + { + _application.CompositionRoot.RegisterModules(_dataGenerationModule); + await _application.Boot(cancellationToken); + + + SeedDataForAllActiveTenants(); + } + + public void SeedDataForAllActiveTenants() + { + using (Logger.InfoDuration("Seeding data")) + { + var prodTenantIds = _tenantIdProvider.GetActiveProductionTenantIds(); + foreach (TenantId prodTenantId in prodTenantIds) + { + DataGenerationContext.SeedDataForTenant(prodTenantId, false); + } + + var demoTenantIds = _tenantIdProvider.GetActiveDemonstrationTenantIds(); + foreach (TenantId demoTenantId in demoTenantIds) + { + DataGenerationContext.SeedDataForTenant(demoTenantId, true); + } + } + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DataGeneration/IDemoDataGenerator.cs b/src/abstractions/Backend.Fx/Patterns/DataGeneration/IDemoDataGenerator.cs index b5201b90..6908aba5 100644 --- a/src/abstractions/Backend.Fx/Patterns/DataGeneration/IDemoDataGenerator.cs +++ b/src/abstractions/Backend.Fx/Patterns/DataGeneration/IDemoDataGenerator.cs @@ -3,5 +3,7 @@ /// /// Marks an as active in development environments only /// - public interface IDemoDataGenerator : IDataGenerator { } + public interface IDemoDataGenerator : IDataGenerator + { + } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DataGeneration/IProductiveDataGenerator.cs b/src/abstractions/Backend.Fx/Patterns/DataGeneration/IProductiveDataGenerator.cs index 076fd726..17733651 100644 --- a/src/abstractions/Backend.Fx/Patterns/DataGeneration/IProductiveDataGenerator.cs +++ b/src/abstractions/Backend.Fx/Patterns/DataGeneration/IProductiveDataGenerator.cs @@ -3,5 +3,7 @@ /// /// Marks an as active in all environments /// - public interface IProductiveDataGenerator : IDataGenerator { } + public interface IProductiveDataGenerator : IDataGenerator + { + } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/BackendFxApplication.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/BackendFxApplication.cs index 6b2dab37..449f64b0 100644 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/BackendFxApplication.cs +++ b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/BackendFxApplication.cs @@ -1,55 +1,100 @@ using System; -using System.Reflection; using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using Backend.Fx.Environment.Authentication; +using Backend.Fx.Environment.DateAndTime; using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Extensions; using Backend.Fx.Logging; -using Backend.Fx.Patterns.Jobs; -using Backend.Fx.Patterns.UnitOfWork; +using Backend.Fx.Patterns.EventAggregation.Domain; +using Backend.Fx.Patterns.EventAggregation.Integration; namespace Backend.Fx.Patterns.DependencyInjection { /// /// The root object of the whole backend fx application framework /// - public abstract class BackendFxApplication : IBackendFxApplication + public interface IBackendFxApplication : IDisposable + { + /// + /// The async invoker runs a given action asynchronously in an application scope with injection facilities + /// + IBackendFxApplicationAsyncInvoker AsyncInvoker { get; } + + /// + /// The composition root of the dependency injection framework + /// + ICompositionRoot CompositionRoot { get; } + + /// + /// The invoker runs a given action in an application scope with injection facilities + /// + IBackendFxApplicationInvoker Invoker { get; } + + /// + /// The message bus to send and receive event messages + /// + IMessageBus MessageBus { get; } + + /// + /// allows synchronously awaiting application startup + /// + bool WaitForBoot(int timeoutMilliSeconds = int.MaxValue, CancellationToken cancellationToken = default); + + /// + /// Initializes ans starts the application (async) + /// + /// + Task Boot(CancellationToken cancellationToken = default); + } + + + public class BackendFxApplication : IBackendFxApplication { private static readonly ILogger Logger = LogManager.Create(); private readonly ManualResetEventSlim _isBooted = new ManualResetEventSlim(false); - private int _scopeIndex = 1; /// /// Initializes the application's runtime instance /// /// The composition root of the dependency injection framework - /// This service provides all known tenant ids - /// The exception logger used for job execution and integration event handling - protected BackendFxApplication(ICompositionRoot compositionRoot, ITenantIdService tenantIdService, IExceptionLogger exceptionLogger) + /// The message bus implementation used by this application instance + /// + public BackendFxApplication(ICompositionRoot compositionRoot, IMessageBus messageBus, IExceptionLogger exceptionLogger) { + var invoker = new BackendFxApplicationInvoker(compositionRoot); + AsyncInvoker = new ExceptionLoggingAsyncInvoker(exceptionLogger, invoker); + Invoker = new ExceptionLoggingInvoker(exceptionLogger, invoker); + MessageBus = messageBus; + MessageBus.ProvideInvoker(new SequentializingBackendFxApplicationInvoker( + new WaitForBootInvoker(this, + new ExceptionLoggingAndHandlingInvoker(exceptionLogger, Invoker)))); CompositionRoot = compositionRoot; - TenantIdService = tenantIdService; - ExceptionLogger = exceptionLogger; + CompositionRoot.InfrastructureModule.RegisterScoped(); + CompositionRoot.InfrastructureModule.RegisterScoped, CurrentCorrelationHolder>(); + CompositionRoot.InfrastructureModule.RegisterScoped, CurrentIdentityHolder>(); + CompositionRoot.InfrastructureModule.RegisterScoped, CurrentTenantIdHolder>(); + CompositionRoot.InfrastructureModule.RegisterScoped(); + CompositionRoot.InfrastructureModule.RegisterScoped(() => new DomainEventAggregator(compositionRoot)); + CompositionRoot.InfrastructureModule.RegisterScoped( + () => new MessageBusScope(MessageBus, compositionRoot.InstanceProvider.GetInstance>())); } - public IExceptionLogger ExceptionLogger { get; } + public IBackendFxApplicationAsyncInvoker AsyncInvoker { get; } - /// public ICompositionRoot CompositionRoot { get; } - public ITenantIdService TenantIdService { get; } + public IBackendFxApplicationInvoker Invoker { get; } + + public IMessageBus MessageBus { get; } - /// - public async Task Boot(CancellationToken cancellationToken = default) + public Task Boot(CancellationToken cancellationToken = default) { Logger.Info("Booting application"); - await OnBoot(cancellationToken); CompositionRoot.Verify(); - - await OnBooted(cancellationToken); + MessageBus.Connect(); _isBooted.Set(); + return Task.CompletedTask; } public bool WaitForBoot(int timeoutMilliSeconds = int.MaxValue, CancellationToken cancellationToken = default) @@ -57,105 +102,7 @@ public bool WaitForBoot(int timeoutMilliSeconds = int.MaxValue, CancellationToke return _isBooted.Wait(timeoutMilliSeconds, cancellationToken); } - public IDisposable BeginScope(IIdentity identity = null, TenantId tenantId = null) - { - var scopeIndex = _scopeIndex++; - tenantId = tenantId ?? new TenantId(null); - identity = identity ?? new AnonymousIdentity(); - - IDisposable scopeDurationLogger = Logger.InfoDuration( - $"Starting scope {scopeIndex} for {identity.Name} in tenant {(tenantId.HasValue ? tenantId.Value.ToString() : "null")}", - $"Ended scope {scopeIndex} for {identity.Name} in tenant {(tenantId.HasValue ? tenantId.Value.ToString() : "null")}"); - IDisposable scope = CompositionRoot.BeginScope(); - CompositionRoot.GetInstance>().ReplaceCurrent(tenantId); - CompositionRoot.GetInstance>().ReplaceCurrent(identity); - - return new MultipleDisposable(scope, scopeDurationLogger); - } - - public void Run() where TJob : class, IJob - { - var tenantIds = TenantIdService.GetActiveTenantIds(); - foreach (TenantId tenantId in tenantIds) - { - Invoke(() => CompositionRoot.GetInstance().Run(), new SystemIdentity(), tenantId); - } - } - - public void Run(TenantId tenantId) where TJob : class, IJob - { - Invoke(() => CompositionRoot.GetInstance().Run(), new SystemIdentity(), tenantId); - } - - public void Invoke(Action action, IIdentity identity, TenantId tenantId, Action configureScope = null) - { - using (BeginScope(identity, tenantId)) - { - configureScope?.Invoke(CompositionRoot); - var unitOfWork = CompositionRoot.GetInstance(); - try - { - unitOfWork.Begin(); - action.Invoke(); - unitOfWork.Complete(); - } - catch (TargetInvocationException ex) - { - ExceptionLogger.LogException(ex.InnerException ?? ex); - } - catch (Exception ex) - { - Logger.Info(ex); - ExceptionLogger.LogException(ex); - } - } - } - - public async Task InvokeAsync(Func awaitableAsyncAction, IIdentity identity, TenantId tenantId, Action configureScope = null) - { - using (BeginScope(identity, tenantId)) - { - configureScope?.Invoke(CompositionRoot); - var unitOfWork = CompositionRoot.GetInstance(); - try - { - unitOfWork.Begin(); - await awaitableAsyncAction.Invoke(); - unitOfWork.Complete(); - } - catch (TargetInvocationException ex) - { - ExceptionLogger.LogException(ex.InnerException ?? ex); - } - catch (Exception ex) - { - Logger.Info(ex); - ExceptionLogger.LogException(ex); - } - } - } - - /// - /// Extension point to do additional initialization before composition root is initialized - /// - /// - /// - protected virtual async Task OnBoot(CancellationToken cancellationToken) - { - await Task.CompletedTask; - } - - /// - /// Extension point to do additional initialization after composition root is initialized - /// - /// - /// - protected virtual async Task OnBooted(CancellationToken cancellationToken) - { - await Task.CompletedTask; - } - - protected virtual void Dispose(bool disposing) + protected void Dispose(bool disposing) { if (disposing) { diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/BackendFxApplicationInvoker.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/BackendFxApplicationInvoker.cs new file mode 100644 index 00000000..f96e6da0 --- /dev/null +++ b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/BackendFxApplicationInvoker.cs @@ -0,0 +1,126 @@ +using System; +using System.Security.Principal; +using System.Threading.Tasks; +using Backend.Fx.Environment.Authentication; +using Backend.Fx.Environment.MultiTenancy; +using Backend.Fx.Extensions; +using Backend.Fx.Logging; +using Backend.Fx.Patterns.EventAggregation.Domain; +using Backend.Fx.Patterns.EventAggregation.Integration; + +namespace Backend.Fx.Patterns.DependencyInjection +{ + public interface IBackendFxApplicationAsyncInvoker + { + /// The async action to be invoked by the application + /// The acting identity + /// The targeted tenant id + /// The correlation id, when it was continued + Task InvokeAsync(Func awaitableAsyncAction, IIdentity identity, TenantId tenantId, Guid? correlationId = null); + } + + + public interface IBackendFxApplicationInvoker + { + /// The action to be invoked by the application + /// The acting identity + /// The targeted tenant id + /// The correlation id, when it was continued + void Invoke(Action action, IIdentity identity, TenantId tenantId, Guid? correlationId = null); + } + + + public class BackendFxApplicationInvoker : IBackendFxApplicationInvoker, IBackendFxApplicationAsyncInvoker + { + private readonly ICompositionRoot _compositionRoot; + private static readonly ILogger Logger = LogManager.Create(); + + public BackendFxApplicationInvoker(ICompositionRoot compositionRoot) + { + _compositionRoot = compositionRoot; + } + + + public void Invoke(Action action, IIdentity identity, TenantId tenantId, Guid? correlationId = null) + { + Logger.Info($"Invoking synchronous action as {identity.Name} in {tenantId}"); + using (IInjectionScope injectionScope = BeginScope(identity, tenantId, correlationId)) + { + using (UseDurationLogger(injectionScope)) + { + var operation = injectionScope.InstanceProvider.GetInstance(); + try + { + operation.Begin(); + action.Invoke(injectionScope.InstanceProvider); + injectionScope.InstanceProvider.GetInstance().RaiseEvents(); + operation.Complete(); + } + catch + { + operation.Cancel(); + throw; + } + + var messageBusScope = injectionScope.InstanceProvider.GetInstance(); + AsyncHelper.RunSync(() => messageBusScope.RaiseEvents()); + } + } + } + + public async Task InvokeAsync(Func awaitableAsyncAction, IIdentity identity, TenantId tenantId, Guid? correlationId = null) + { + Logger.Info($"Invoking asynchronous action as {identity.Name} in {tenantId}"); + using (IInjectionScope injectionScope = BeginScope(identity, tenantId, correlationId)) + { + using (UseDurationLogger(injectionScope)) + { + var operation = injectionScope.InstanceProvider.GetInstance(); + try + { + operation.Begin(); + await awaitableAsyncAction.Invoke(injectionScope.InstanceProvider); + injectionScope.InstanceProvider.GetInstance().RaiseEvents(); + operation.Complete(); + } + catch + { + operation.Cancel(); + throw; + } + + await injectionScope.InstanceProvider.GetInstance().RaiseEvents(); + } + } + } + + + private IInjectionScope BeginScope(IIdentity identity, TenantId tenantId, Guid? correlationId) + { + IInjectionScope injectionScope = _compositionRoot.BeginScope(); + tenantId = tenantId ?? new TenantId(null); + injectionScope.InstanceProvider.GetInstance>().ReplaceCurrent(tenantId); + + identity = identity ?? new AnonymousIdentity(); + injectionScope.InstanceProvider.GetInstance>().ReplaceCurrent(identity); + + if (correlationId.HasValue) + { + injectionScope.InstanceProvider.GetInstance>().Current.Resume(correlationId.Value); + } + + return injectionScope; + } + + + private static IDisposable UseDurationLogger(IInjectionScope injectionScope) + { + IIdentity identity = injectionScope.InstanceProvider.GetInstance>().Current; + TenantId tenantId = injectionScope.InstanceProvider.GetInstance>().Current; + Correlation correlation = injectionScope.InstanceProvider.GetInstance>().Current; + return Logger.InfoDuration( + $"Starting scope {injectionScope.SequenceNumber} (correlation [{correlation.Id}]) for {identity.Name} in tenant {(tenantId.HasValue ? tenantId.Value.ToString() : "null")}", + $"Ended scope {injectionScope.SequenceNumber} (correlation [{correlation.Id}]) for {identity.Name} in tenant {(tenantId.HasValue ? tenantId.Value.ToString() : "null")}"); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/BackendFxDbApplication.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/BackendFxDbApplication.cs deleted file mode 100644 index f167df1c..00000000 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/BackendFxDbApplication.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Environment.Persistence; -using Backend.Fx.Logging; - -namespace Backend.Fx.Patterns.DependencyInjection -{ - public abstract class BackendFxDbApplication : BackendFxApplication - { - /// - /// Initializes the application's runtime instance - /// - /// The composition root of the dependency injection framework - /// The database manager for the current application - /// - /// The exception logger used by jobs and integration event handling - protected BackendFxDbApplication( - ICompositionRoot compositionRoot, - ITenantIdService tenantIdService, - IExceptionLogger exceptionLogger, - IDatabaseBootstrapper databaseBootstrapper) - : base(compositionRoot, tenantIdService, exceptionLogger) - { - DatabaseBootstrapper = databaseBootstrapper; - } - - /// - /// The utility instance for database bootstrapping - /// - public IDatabaseBootstrapper DatabaseBootstrapper { get; } - - protected sealed override async Task OnBoot(CancellationToken cancellationToken) - { - await OnDatabaseBoot(cancellationToken); - await WaitForDatabase(cancellationToken); - DatabaseBootstrapper.EnsureDatabaseExistence(); - await OnDatabaseBooted(cancellationToken); - } - - protected virtual async Task WaitForDatabase(CancellationToken cancellationToken) - { - await Task.CompletedTask; - } - - /// - /// Extension point to do additional initialization before existence of database is ensured - /// - /// - protected virtual async Task OnDatabaseBoot(CancellationToken cancellationToken) - { - await Task.CompletedTask; - } - - /// - /// Extension point to do additional initialization after existence of database is ensured - /// - /// - protected virtual async Task OnDatabaseBooted(CancellationToken cancellationToken) - { - await Task.CompletedTask; - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (disposing) - { - DatabaseBootstrapper?.Dispose(); - } - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/Correlation.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/Correlation.cs index 1e2b62ff..eee99ba4 100644 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/Correlation.cs +++ b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/Correlation.cs @@ -6,7 +6,7 @@ namespace Backend.Fx.Patterns.DependencyInjection public class Correlation { private static readonly ILogger Logger = LogManager.Create(); - + public Guid Id { get; private set; } = Guid.NewGuid(); public void Resume(Guid correlationId) diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/CurrentCorrelationHolder.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/CurrentCorrelationHolder.cs index 7e778737..70a9f67e 100644 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/CurrentCorrelationHolder.cs +++ b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/CurrentCorrelationHolder.cs @@ -9,12 +9,7 @@ public override Correlation ProvideInstance() protected override string Describe(Correlation instance) { - if (instance == null) - { - return ""; - } - - return $"Correlation: {instance.Id}"; + return $"Correlation: {instance?.Id.ToString() ?? "NULL"}"; } } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ICurrentTHolder.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/CurrentTHolder.cs similarity index 94% rename from src/abstractions/Backend.Fx/Patterns/DependencyInjection/ICurrentTHolder.cs rename to src/abstractions/Backend.Fx/Patterns/DependencyInjection/CurrentTHolder.cs index 31bca04a..aa50a518 100644 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ICurrentTHolder.cs +++ b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/CurrentTHolder.cs @@ -1,7 +1,7 @@ -namespace Backend.Fx.Patterns.DependencyInjection -{ - using Logging; +using Backend.Fx.Logging; +namespace Backend.Fx.Patterns.DependencyInjection +{ /// /// Holds a current instance of T that might be replaced during the scope /// @@ -30,6 +30,7 @@ public T Current _current = ProvideInstance(); Logger.Debug($"Initial instance of {typeof(T).Name} is: {Describe(_current)}"); } + return _current; } } @@ -46,4 +47,4 @@ public void ReplaceCurrent(T newCurrentInstance) protected abstract string Describe(T instance); } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ExceptionLoggingAndHandlingInvoker.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ExceptionLoggingAndHandlingInvoker.cs new file mode 100644 index 00000000..2616f0aa --- /dev/null +++ b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ExceptionLoggingAndHandlingInvoker.cs @@ -0,0 +1,31 @@ +using System; +using System.Security.Principal; +using Backend.Fx.Environment.MultiTenancy; +using Backend.Fx.Logging; + +namespace Backend.Fx.Patterns.DependencyInjection +{ + public class ExceptionLoggingAndHandlingInvoker : IBackendFxApplicationInvoker + { + private readonly IExceptionLogger _exceptionLogger; + private readonly IBackendFxApplicationInvoker _invoker; + + public ExceptionLoggingAndHandlingInvoker(IExceptionLogger exceptionLogger, IBackendFxApplicationInvoker invoker) + { + _exceptionLogger = exceptionLogger; + _invoker = invoker; + } + + public void Invoke(Action action, IIdentity identity, TenantId tenantId, Guid? correlationId = null) + { + try + { + _invoker.Invoke(action, identity, tenantId, correlationId); + } + catch (Exception ex) + { + _exceptionLogger.LogException(ex); + } + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ExceptionLoggingAsyncInvoker.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ExceptionLoggingAsyncInvoker.cs new file mode 100644 index 00000000..f87ede34 --- /dev/null +++ b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ExceptionLoggingAsyncInvoker.cs @@ -0,0 +1,33 @@ +using System; +using System.Security.Principal; +using System.Threading.Tasks; +using Backend.Fx.Environment.MultiTenancy; +using Backend.Fx.Logging; + +namespace Backend.Fx.Patterns.DependencyInjection +{ + public class ExceptionLoggingAsyncInvoker : IBackendFxApplicationAsyncInvoker + { + private readonly IExceptionLogger _exceptionLogger; + private readonly IBackendFxApplicationAsyncInvoker _invoker; + + public ExceptionLoggingAsyncInvoker(IExceptionLogger exceptionLogger, IBackendFxApplicationAsyncInvoker invoker) + { + _exceptionLogger = exceptionLogger; + _invoker = invoker; + } + + public async Task InvokeAsync(Func awaitableAsyncAction, IIdentity identity, TenantId tenantId, Guid? correlationId = null) + { + try + { + await _invoker.InvokeAsync(awaitableAsyncAction, identity, tenantId, correlationId); + } + catch (Exception ex) + { + _exceptionLogger.LogException(ex); + throw; + } + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ExceptionLoggingInvoker.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ExceptionLoggingInvoker.cs new file mode 100644 index 00000000..04097ea4 --- /dev/null +++ b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ExceptionLoggingInvoker.cs @@ -0,0 +1,32 @@ +using System; +using System.Security.Principal; +using Backend.Fx.Environment.MultiTenancy; +using Backend.Fx.Logging; + +namespace Backend.Fx.Patterns.DependencyInjection +{ + public class ExceptionLoggingInvoker : IBackendFxApplicationInvoker + { + private readonly IExceptionLogger _exceptionLogger; + private readonly IBackendFxApplicationInvoker _invoker; + + public ExceptionLoggingInvoker(IExceptionLogger exceptionLogger, IBackendFxApplicationInvoker invoker) + { + _exceptionLogger = exceptionLogger; + _invoker = invoker; + } + + public void Invoke(Action action, IIdentity identity, TenantId tenantId, Guid? correlationId = null) + { + try + { + _invoker.Invoke(action, identity, tenantId, correlationId); + } + catch (Exception ex) + { + _exceptionLogger.LogException(ex); + throw; + } + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/IBackendFxApplication.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/IBackendFxApplication.cs deleted file mode 100644 index 30315c75..00000000 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/IBackendFxApplication.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Security.Principal; -using System.Threading; -using System.Threading.Tasks; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.Jobs; - -namespace Backend.Fx.Patterns.DependencyInjection -{ - public interface IBackendFxApplication : IDisposable - { - /// - /// The composition root of the dependency injection framework - /// - ICompositionRoot CompositionRoot { get; } - - /// - /// Used to log exceptions - /// - IExceptionLogger ExceptionLogger { get; } - - /// - /// Provides application wide tenant ids - /// - ITenantIdService TenantIdService { get; } - - /// - /// allows synchronously awaiting application startup - /// - bool WaitForBoot(int timeoutMilliSeconds = int.MaxValue, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Initializes ans starts the application (async) - /// - /// - Task Boot(CancellationToken cancellationToken = default(CancellationToken)); - - IDisposable BeginScope(IIdentity identity = null, TenantId tenantId = null); - - /// The action to be invoked by the application - /// The acting identity - /// The targeted tenant id - /// An optional action that is used to configure the scope before beginning the unit of work - void Invoke(Action action, IIdentity identity, TenantId tenantId, Action configureScope = null); - - /// The async action to be invoked by the application - /// The acting identity - /// The targeted tenant id - /// An optional action that is used to configure the scope before beginning the unit of work - Task InvokeAsync(Func awaitableAsyncAction, IIdentity identity, TenantId tenantId, Action configureScope = null); - - void Run() where TJob : class, IJob; - - void Run(TenantId tenantId) where TJob : class, IJob; - } -} diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ICompositionRoot.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ICompositionRoot.cs index bc946b73..f015d298 100644 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ICompositionRoot.cs +++ b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ICompositionRoot.cs @@ -1,10 +1,8 @@ -namespace Backend.Fx.Patterns.DependencyInjection -{ - using System; - using System.Collections; - using System.Collections.Generic; - using EventAggregation.Domain; +using System; +using Backend.Fx.Patterns.EventAggregation.Domain; +namespace Backend.Fx.Patterns.DependencyInjection +{ /// /// Encapsulates the injection framework of choice. The implementation follows the Register/Resolve/Release pattern. /// Usage of this interface is only allowed for framework integration (or tests). NEVER (!) access the injector from @@ -17,41 +15,9 @@ public interface ICompositionRoot : IDisposable, IDomainEventHandlerProvider void RegisterModules(params IModule[] modules); - /// - /// Gets a service instance by providing its type - /// - /// - /// - object GetInstance(Type serviceType); - - /// - /// Gets all service instances by providing their type - /// - /// - /// - IEnumerable GetInstances(Type serviceType); - - /// - /// Gets a service instance by providing its type via generic type parameter - /// - /// - /// - T GetInstance() where T : class; - - /// - /// Gets all service instances by providing their type via generic type parameter - /// - /// - /// - IEnumerable GetInstances() where T : class; - - IDisposable BeginScope(); + IInjectionScope BeginScope(); + IInstanceProvider InstanceProvider { get; } - /// - /// Gets the current correlation, when inside a scope, otherwise this method will return false - /// - /// - /// - bool TryGetCurrentCorrelation(out Correlation correlation); + IInfrastructureModule InfrastructureModule { get; } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/IInstanceProvider.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/IInstanceProvider.cs new file mode 100644 index 00000000..f5af5265 --- /dev/null +++ b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/IInstanceProvider.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Backend.Fx.Patterns.DependencyInjection +{ + public interface IInstanceProvider + { + /// + /// Gets a service instance valid for the scope by providing its type + /// + /// + /// + object GetInstance(Type serviceType); + + /// + /// Gets all service instances valid for the scope by providing their type + /// + /// + /// + IEnumerable GetInstances(Type serviceType); + + /// + /// Gets a service instance valid for the scope by providing its type via generic type parameter + /// + /// + /// + T GetInstance() where T : class; + + /// + /// Gets all service instances valid for the scope by providing their type via generic type parameter + /// + /// + /// + IEnumerable GetInstances() where T : class; + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/IModule.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/IModule.cs index c1a95641..efec59f4 100644 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/IModule.cs +++ b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/IModule.cs @@ -4,4 +4,4 @@ public interface IModule { void Register(ICompositionRoot compositionRoot); } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/InfrastructureModule.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/InfrastructureModule.cs new file mode 100644 index 00000000..8dc57a87 --- /dev/null +++ b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/InfrastructureModule.cs @@ -0,0 +1,23 @@ +using System; +using System.Reflection; + +namespace Backend.Fx.Patterns.DependencyInjection +{ + public interface IInfrastructureModule + { + void RegisterScoped() + where TImpl : class, TService + where TService : class; + + void RegisterScoped(Func factory) + where TService : class; + + void RegisterScoped(Type serviceType, Type implementationType); + void RegisterScoped(Type serviceType, Assembly[] assembliesToScan); + + void RegisterDecorator() where TService : class where TImpl : class, TService; + + void RegisterSingleton() where TService : class where TImpl : class, TService; + void RegisterInstance(TService instance) where TService : class; + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/InjectionScope.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/InjectionScope.cs new file mode 100644 index 00000000..4174373d --- /dev/null +++ b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/InjectionScope.cs @@ -0,0 +1,26 @@ +using System; + +namespace Backend.Fx.Patterns.DependencyInjection +{ + public interface IInjectionScope : IDisposable + { + int SequenceNumber { get; } + + IInstanceProvider InstanceProvider { get; } + } + + + public abstract class InjectionScope : IInjectionScope + { + protected InjectionScope(int sequenceNumber) + { + SequenceNumber = sequenceNumber; + } + + public int SequenceNumber { get; } + + public abstract IInstanceProvider InstanceProvider { get; } + + public abstract void Dispose(); + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/Operation.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/Operation.cs new file mode 100644 index 00000000..384a74d3 --- /dev/null +++ b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/Operation.cs @@ -0,0 +1,59 @@ +using System; +using Backend.Fx.Logging; + +namespace Backend.Fx.Patterns.DependencyInjection +{ + public interface IOperation + { + void Begin(); + + void Complete(); + + void Cancel(); + } + + public class Operation : IOperation + { + private static readonly ILogger Logger = LogManager.Create(); + private static int _index; + private readonly int _instanceId = _index++; + private bool? _isActive; + private IDisposable _lifetimeLogger; + + public virtual void Begin() + { + if (_isActive != null) + { + throw new InvalidOperationException($"Cannot begin an operation that is {(_isActive.Value ? "active" : "terminated")}"); + } + + _lifetimeLogger = Logger.DebugDuration($"Beginning operation #{_instanceId}", $"Terminating operation #{_instanceId}"); + _isActive = true; + } + + public virtual void Complete() + { + Logger.Info($"Completing operation #{_instanceId}."); + if (_isActive != true) + { + throw new InvalidOperationException($"Cannot begin an operation that is {(_isActive == false ? "terminated" : "not active")}"); + } + + _isActive = false; + _lifetimeLogger?.Dispose(); + _lifetimeLogger = null; + } + + public void Cancel() + { + Logger.Info($"Canceling operation #{_instanceId}."); + if (_isActive != true) + { + throw new InvalidOperationException($"Cannot cancel an operation that is {(_isActive == false ? "terminated" : "not active")}"); + } + _isActive = false; + _lifetimeLogger?.Dispose(); + _lifetimeLogger = null; + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/SequentializingBackendFxApplicationInvoker.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/SequentializingBackendFxApplicationInvoker.cs new file mode 100644 index 00000000..b145d0f9 --- /dev/null +++ b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/SequentializingBackendFxApplicationInvoker.cs @@ -0,0 +1,26 @@ +using System; +using System.Security.Principal; +using Backend.Fx.Environment.MultiTenancy; + +namespace Backend.Fx.Patterns.DependencyInjection +{ + public class SequentializingBackendFxApplicationInvoker : IBackendFxApplicationInvoker + { + private readonly object _syncLock = new object(); + private readonly IBackendFxApplicationInvoker _backendFxApplicationInvokerImplementation; + + public SequentializingBackendFxApplicationInvoker(IBackendFxApplicationInvoker backendFxApplicationInvokerImplementation) + { + _backendFxApplicationInvokerImplementation = backendFxApplicationInvokerImplementation; + } + + + public void Invoke(Action action, IIdentity identity, TenantId tenantId, Guid? correlationId = null) + { + lock (_syncLock) + { + _backendFxApplicationInvokerImplementation.Invoke(action, identity, tenantId, correlationId); + } + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/WaitForBootInvoker.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/WaitForBootInvoker.cs new file mode 100644 index 00000000..3dcecb9f --- /dev/null +++ b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/WaitForBootInvoker.cs @@ -0,0 +1,24 @@ +using System; +using System.Security.Principal; +using Backend.Fx.Environment.MultiTenancy; + +namespace Backend.Fx.Patterns.DependencyInjection +{ + public class WaitForBootInvoker : IBackendFxApplicationInvoker + { + private readonly IBackendFxApplication _application; + private readonly IBackendFxApplicationInvoker _invoker; + + public WaitForBootInvoker(IBackendFxApplication application, IBackendFxApplicationInvoker invoker) + { + _application = application; + _invoker = invoker; + } + + public void Invoke(Action action, IIdentity identity, TenantId tenantId, Guid? correlationId = null) + { + _application.WaitForBoot(); + _invoker.Invoke(action, identity, tenantId, correlationId); + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Domain/DomainEventAggregator.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Domain/DomainEventAggregator.cs index a515d5dc..a196c478 100644 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Domain/DomainEventAggregator.cs +++ b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Domain/DomainEventAggregator.cs @@ -1,9 +1,9 @@ -namespace Backend.Fx.Patterns.EventAggregation.Domain -{ - using System; - using System.Collections.Concurrent; - using Logging; +using System; +using System.Collections.Concurrent; +using Backend.Fx.Logging; +namespace Backend.Fx.Patterns.EventAggregation.Domain +{ public class DomainEventAggregator : IDomainEventAggregator { private class HandleAction @@ -39,19 +39,19 @@ public void PublishDomainEvent(TDomainEvent domainEvent) where TDo { foreach (var injectedHandler in _domainEventHandlerProvider.GetAllEventHandlers()) { - HandleAction handleAction = new HandleAction ( - typeof(TDomainEvent).Name, - injectedHandler.GetType().Name, - () => injectedHandler.Handle(domainEvent)); + var handleAction = new HandleAction( + typeof(TDomainEvent).Name, + injectedHandler.GetType().Name, + () => injectedHandler.Handle(domainEvent)); _handleActions.Enqueue(handleAction); - Logger.Debug($"Invocation of {injectedHandler.GetType().Name} for domain event {typeof(TDomainEvent).Name} registered. It will be executed on completion of unit of work"); + Logger.Debug($"Invocation of {injectedHandler.GetType().Name} for domain event {typeof(TDomainEvent).Name} registered. It will be executed on completion of operation"); } } public void RaiseEvents() { - while (_handleActions.TryDequeue(out var handleAction)) + while (_handleActions.TryDequeue(out HandleAction handleAction)) { try { @@ -65,4 +65,4 @@ public void RaiseEvents() } } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Domain/IDomainEvent.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Domain/IDomainEvent.cs index 66634d65..8f9af72b 100644 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Domain/IDomainEvent.cs +++ b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Domain/IDomainEvent.cs @@ -4,5 +4,7 @@ /// Marker interface for domain events that must be handled in the same scope and transaction of the publishing logic. /// Handlers are called through dependency injection /// - public interface IDomainEvent { } -} + public interface IDomainEvent + { + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/DelegateIntegrationEventHandler.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/DelegateIntegrationMessageHandler.cs similarity index 56% rename from src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/DelegateIntegrationEventHandler.cs rename to src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/DelegateIntegrationMessageHandler.cs index 11e3a932..043bec4e 100644 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/DelegateIntegrationEventHandler.cs +++ b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/DelegateIntegrationMessageHandler.cs @@ -2,12 +2,12 @@ namespace Backend.Fx.Patterns.EventAggregation.Integration { - public class DelegateIntegrationEventHandler - : IIntegrationEventHandler where TIntegrationEvent : IIntegrationEvent + public class DelegateIntegrationMessageHandler + : IIntegrationMessageHandler where TIntegrationEvent : IIntegrationEvent { private readonly Action _handleAction; - public DelegateIntegrationEventHandler(Action handleAction) + public DelegateIntegrationMessageHandler(Action handleAction) { _handleAction = handleAction; } @@ -17,4 +17,4 @@ public void Handle(TIntegrationEvent eventData) _handleAction.Invoke(eventData); } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/DynamicSubscription.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/DynamicSubscription.cs index d081facd..800fa2a9 100644 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/DynamicSubscription.cs +++ b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/DynamicSubscription.cs @@ -8,22 +8,20 @@ namespace Backend.Fx.Patterns.EventAggregation.Integration public class DynamicSubscription : ISubscription { private static readonly ILogger Logger = LogManager.Create(); - private readonly IBackendFxApplication _application; private readonly Type _handlerType; - public DynamicSubscription(IBackendFxApplication application, Type handlerType) + public DynamicSubscription(Type handlerType) { - _application = application; _handlerType = handlerType; } - public void Process(string eventName, EventProcessingContext context) + public void Process(IInstanceProvider instanceProvider, EventProcessingContext context) { Logger.Info($"Getting subscribed handler instance of type {_handlerType.Name}"); - object handlerInstance = _application.CompositionRoot.GetInstance(_handlerType); + object handlerInstance = instanceProvider.GetInstance(_handlerType); using (Logger.InfoDuration($"Invoking subscribed handler {_handlerType.GetDetailedTypeName()}")) { - ((IIntegrationEventHandler)handlerInstance).Handle(context.DynamicEvent); + ((IIntegrationMessageHandler) handlerInstance).Handle(context.DynamicEvent); } } diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/EventBus.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/EventBus.cs deleted file mode 100644 index cd015460..00000000 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/EventBus.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Threading.Tasks; -using Backend.Fx.Environment.Authentication; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; - -namespace Backend.Fx.Patterns.EventAggregation.Integration -{ - public abstract class EventBus : IEventBus - { - private static readonly ILogger Logger = LogManager.Create(); - private readonly IBackendFxApplication _application; - - /// - /// Holds the registered handlers. - /// Each event type name (key) matches to various subscriptions - /// - private readonly ConcurrentDictionary> _subscriptions = new ConcurrentDictionary>(); - - protected EventBus(IBackendFxApplication application) - { - Logger.Debug($"Instantiating {GetType()}"); - _application = application; - } - - public abstract void Connect(); - - public Task Publish(IIntegrationEvent integrationEvent) - { - Guid correlationId = _application.CompositionRoot.TryGetCurrentCorrelation(out Correlation correlation) ? correlation.Id : Guid.NewGuid(); - ((IntegrationEvent) integrationEvent).SetCorrelationId(correlationId); - return PublishOnEventBus(integrationEvent); - } - - protected abstract Task PublishOnEventBus(IIntegrationEvent integrationEvent); - - - /// - public void Subscribe(string eventName) where THandler : IIntegrationEventHandler - { - Logger.Info($"Subscribing to {eventName}"); - var subscription = new DynamicSubscription(_application, typeof(THandler)); - _subscriptions.AddOrUpdate(eventName, - s => new List {subscription}, - (s, list) => - { - list.Add(subscription); - return list; - }); - Subscribe(eventName); - } - - /// - public void Subscribe() where THandler : IIntegrationEventHandler where TEvent : IIntegrationEvent - { - string eventName = typeof(TEvent).FullName ?? typeof(TEvent).Name; - - Logger.Info($"Subscribing to {eventName}"); - var subscription = new TypedSubscription(_application, typeof(THandler)); - _subscriptions.AddOrUpdate(eventName, - s => new List {subscription}, - (s, list) => - { - list.Add(subscription); - return list; - }); - Subscribe(eventName); - } - - public void Subscribe(IIntegrationEventHandler handler) - where TEvent : IIntegrationEvent - { - string eventName = typeof(TEvent).FullName ?? typeof(TEvent).Name; - - Logger.Info($"Subscribing to {eventName}"); - var subscription = new SingletonSubscription(handler); - _subscriptions.AddOrUpdate(eventName, - s => new List {subscription}, - (s, list) => - { - list.Add(subscription); - return list; - }); - Subscribe(eventName); - } - - public void Unsubscribe(string eventName) where THandler : IIntegrationEventHandler - { - Logger.Info($"Unsubscribing from {eventName}"); - if (_subscriptions.TryGetValue(eventName, out var handlers)) - { - handlers.RemoveAll(t => t.Matches(typeof(THandler))); - } - - Unsubscribe(eventName); - } - - public void Unsubscribe() where THandler : IIntegrationEventHandler where TEvent : IIntegrationEvent - { - string eventName = typeof(TEvent).FullName; - Debug.Assert(eventName != null, nameof(eventName) + " != null"); - - Logger.Info($"Unsubscribing from {eventName}"); - if (_subscriptions.TryGetValue(eventName, out var handlers)) - { - handlers.RemoveAll(t => t.Matches(typeof(THandler))); - } - - Unsubscribe(eventName); - } - - public void Unsubscribe(IIntegrationEventHandler handler) where TEvent : IIntegrationEvent - { - string eventName = typeof(TEvent).FullName; - Debug.Assert(eventName != null, nameof(eventName) + " != null"); - - Logger.Info($"Unsubscribing from {eventName}"); - if (_subscriptions.TryGetValue(eventName, out var handlers)) - { - handlers.RemoveAll(t => t.Matches(handler)); - } - - Unsubscribe(eventName); - } - - protected abstract void Subscribe(string eventName); - protected abstract void Unsubscribe(string eventName); - - protected void Process(string eventName, EventProcessingContext context) - { - Logger.Info($"Processing a {eventName} event"); - if (_subscriptions.TryGetValue(eventName, out List subscriptions)) - { - foreach (ISubscription subscription in subscriptions) - { - _application.Invoke( - () => subscription.Process(eventName, context), - new SystemIdentity(), - context.TenantId, - compositionRoot => ConfigureProcessingScope(compositionRoot, context)); - } - } - else - { - Logger.Info($"No handler registered. Ignoring {eventName} event"); - } - } - - protected virtual void ConfigureProcessingScope(ICompositionRoot compositionRoot, EventProcessingContext context) - { - compositionRoot.GetInstance>().Current.Resume(context.CorrelationId); - } - - protected virtual void Dispose(bool disposing) - { - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IIntegrationEventHandler.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IIntegrationMessageHandler.cs similarity index 54% rename from src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IIntegrationEventHandler.cs rename to src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IIntegrationMessageHandler.cs index d3c816b3..2c2a0e1c 100644 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IIntegrationEventHandler.cs +++ b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IIntegrationMessageHandler.cs @@ -1,12 +1,12 @@ namespace Backend.Fx.Patterns.EventAggregation.Integration { - public interface IIntegrationEventHandler + public interface IIntegrationMessageHandler { void Handle(dynamic eventData); } - public interface IIntegrationEventHandler where TEvent : IIntegrationEvent + public interface IIntegrationMessageHandler where TEvent : IIntegrationEvent { void Handle(TEvent eventData); } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IEventBus.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IMessageBus.cs similarity index 53% rename from src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IEventBus.cs rename to src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IMessageBus.cs index 9133b55b..bce92202 100644 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IEventBus.cs +++ b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IMessageBus.cs @@ -1,17 +1,24 @@ using System; using System.Threading.Tasks; +using Backend.Fx.Patterns.DependencyInjection; namespace Backend.Fx.Patterns.EventAggregation.Integration { - public interface IEventBus : IDisposable + public interface IMessageBus : IDisposable { + /// + /// This instance is used to determine a message name from an integration event. The default implementation just + /// returns event.GetType().Name + /// + IMessageNameProvider MessageNameProvider { get; } + void Connect(); /// - /// Directly publishes an event on the event bus without delay. + /// Directly publishes an event on the message bus without delay. /// In most cases you want to publish an event when the cause is considered as safely done, e.g. when the - /// wrapping transaction is committed. Use to let the framework raise all events - /// after committing the unit of work. + /// wrapping transaction is committed. Use to let the framework raise all events + /// after completing the operation. /// /// /// @@ -21,9 +28,9 @@ public interface IEventBus : IDisposable /// Subscribes to an integration event with a dynamic event handler /// /// The handler type - /// Th event name to subscribe to. (Should be Type.FullName to avoid namespace collisions) - void Subscribe(string eventName) - where THandler : IIntegrationEventHandler; + /// The event name to subscribe to. + void Subscribe(string messageName) + where THandler : IIntegrationMessageHandler; /// /// Subscribes to an integration event with a generically typed event handler @@ -31,24 +38,26 @@ void Subscribe(string eventName) /// The handler type /// The event type to subscribe to void Subscribe() - where THandler : IIntegrationEventHandler + where THandler : IIntegrationMessageHandler where TEvent : IIntegrationEvent; /// /// Subscribes to an integration event with a singleton instance event handler /// /// The event type to subscribe to - void Subscribe(IIntegrationEventHandler handler) + void Subscribe(IIntegrationMessageHandler handler) where TEvent : IIntegrationEvent; - void Unsubscribe(string eventName) - where THandler : IIntegrationEventHandler; + void Unsubscribe(string messageName) + where THandler : IIntegrationMessageHandler; void Unsubscribe() - where THandler : IIntegrationEventHandler + where THandler : IIntegrationMessageHandler where TEvent : IIntegrationEvent; - void Unsubscribe(IIntegrationEventHandler handler) + void Unsubscribe(IIntegrationMessageHandler handler) where TEvent : IIntegrationEvent; + + void ProvideInvoker(IBackendFxApplicationInvoker invoker); } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IMessageNameProvider.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IMessageNameProvider.cs new file mode 100644 index 00000000..cd7883a0 --- /dev/null +++ b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IMessageNameProvider.cs @@ -0,0 +1,15 @@ +using System; +using JetBrains.Annotations; + +namespace Backend.Fx.Patterns.EventAggregation.Integration +{ + public interface IMessageNameProvider + { + [NotNull] + string GetMessageName(); + [NotNull] + string GetMessageName(Type t); + [NotNull] + string GetMessageName(IIntegrationEvent integrationEvent); + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/ISubscription.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/ISubscription.cs index 8534fcfa..91ca9a39 100644 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/ISubscription.cs +++ b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/ISubscription.cs @@ -1,8 +1,10 @@ -namespace Backend.Fx.Patterns.EventAggregation.Integration +using Backend.Fx.Patterns.DependencyInjection; + +namespace Backend.Fx.Patterns.EventAggregation.Integration { public interface ISubscription { - void Process(string eventName, EventProcessingContext context); + void Process(IInstanceProvider instanceProvider, EventProcessingContext context); bool Matches(object handler); } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/InMemoryEventBus.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/InMemoryMessageBus.cs similarity index 61% rename from src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/InMemoryEventBus.cs rename to src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/InMemoryMessageBus.cs index 6b104259..13f558dc 100644 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/InMemoryEventBus.cs +++ b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/InMemoryMessageBus.cs @@ -1,34 +1,30 @@ -using Backend.Fx.Patterns.DependencyInjection; - -namespace Backend.Fx.Patterns.EventAggregation.Integration +namespace Backend.Fx.Patterns.EventAggregation.Integration { using System; using System.Threading.Tasks; using Environment.MultiTenancy; - public class InMemoryEventBus : EventBus + public class InMemoryMessageBus : MessageBus { - public InMemoryEventBus(IBackendFxApplication application) - : base(application) + public override void Connect() { } - public override void Connect() - { } - - protected override Task PublishOnEventBus(IIntegrationEvent integrationEvent) + protected override Task PublishOnMessageBus(IIntegrationEvent integrationEvent) { - Task.Run(() => Process(integrationEvent.GetType().FullName, new InMemoryProcessingContext(integrationEvent))); - + Task.Run(() => Process(MessageNameProvider.GetMessageName(integrationEvent), new InMemoryProcessingContext(integrationEvent))); + // the returning Task is about publishing the event, not processing! return Task.CompletedTask; } - protected override void Subscribe(string eventName) - { } + protected override void Subscribe(string messageName) + { + } - protected override void Unsubscribe(string eventName) - { } + protected override void Unsubscribe(string messageName) + { + } private class InMemoryProcessingContext : EventProcessingContext { @@ -50,4 +46,4 @@ public override IIntegrationEvent GetTypedEvent(Type eventType) } } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IntegrationEvent.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IntegrationEvent.cs index 5e25bb03..91d6a721 100644 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IntegrationEvent.cs +++ b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/IntegrationEvent.cs @@ -1,5 +1,4 @@ using System; -using Backend.Fx.Patterns.DependencyInjection; namespace Backend.Fx.Patterns.EventAggregation.Integration { @@ -12,7 +11,7 @@ public interface IIntegrationEvent } /// - /// Events that should be handled in a separate context. Might be persisted as well using an external event bus. + /// Events that should be handled in a separate context. Might be persisted as well using an external message bus. /// See https://blogs.msdn.microsoft.com/cesardelatorre/2017/02/07/domain-events-vs-integration-events-in-domain-driven-design-and-microservices-architectures/ /// public abstract class IntegrationEvent : IIntegrationEvent @@ -22,9 +21,9 @@ public abstract class IntegrationEvent : IIntegrationEvent public DateTime CreationDate { get; } = DateTime.UtcNow; public int TenantId { get; } - + public Guid CorrelationId { get; private set; } - + internal void SetCorrelationId(Guid correlationId) { CorrelationId = correlationId; @@ -35,4 +34,4 @@ protected IntegrationEvent(int tenantId) TenantId = tenantId; } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/MessageBus.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/MessageBus.cs new file mode 100644 index 00000000..d2dd3cc6 --- /dev/null +++ b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/MessageBus.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using Backend.Fx.Environment.Authentication; +using Backend.Fx.Logging; +using Backend.Fx.Patterns.DependencyInjection; + +namespace Backend.Fx.Patterns.EventAggregation.Integration +{ + public abstract class MessageBus : IMessageBus + { + private static readonly ILogger Logger = LogManager.Create(); + + /// + /// Holds the registered handlers. + /// Each event type name (key) matches to various subscriptions + /// + private readonly ConcurrentDictionary> _subscriptions = new ConcurrentDictionary>(); + + private IBackendFxApplicationInvoker _invoker; + + public IMessageNameProvider MessageNameProvider { get; } = new DefaultMessageNameProvider(); + public abstract void Connect(); + + public void ProvideInvoker(IBackendFxApplicationInvoker invoker) + { + _invoker = invoker; + } + + public Task Publish(IIntegrationEvent integrationEvent) + { + return PublishOnMessageBus(integrationEvent); + } + + protected abstract Task PublishOnMessageBus(IIntegrationEvent integrationEvent); + + + /// + public void Subscribe(string messageName) where THandler : IIntegrationMessageHandler + { + Logger.Info($"Subscribing to {messageName}"); + EnsureInvoker(); + var subscription = new DynamicSubscription(typeof(THandler)); + _subscriptions.AddOrUpdate(messageName, + s => new List {subscription}, + (s, list) => + { + list.Add(subscription); + return list; + }); + Subscribe(messageName); + } + + /// + public void Subscribe() where THandler : IIntegrationMessageHandler where TEvent : IIntegrationEvent + { + string eventName = MessageNameProvider.GetMessageName(); + Logger.Info($"Subscribing to {eventName}"); + EnsureInvoker(); + var subscription = new TypedSubscription(typeof(THandler)); + _subscriptions.AddOrUpdate(eventName, + s => new List {subscription}, + (s, list) => + { + list.Add(subscription); + return list; + }); + Subscribe(eventName); + } + + public void Subscribe(IIntegrationMessageHandler handler) + where TEvent : IIntegrationEvent + { + string eventName = MessageNameProvider.GetMessageName(); + Logger.Info($"Subscribing to {eventName}"); + EnsureInvoker(); + var subscription = new SingletonSubscription(handler); + _subscriptions.AddOrUpdate(eventName, + s => new List {subscription}, + (s, list) => + { + list.Add(subscription); + return list; + }); + Subscribe(eventName); + } + + public void Unsubscribe(string messageName) where THandler : IIntegrationMessageHandler + { + Logger.Info($"Unsubscribing from {messageName}"); + if (_subscriptions.TryGetValue(messageName, out var handlers)) + { + handlers.RemoveAll(t => t.Matches(typeof(THandler))); + } + + Unsubscribe(messageName); + } + + public void Unsubscribe() where THandler : IIntegrationMessageHandler where TEvent : IIntegrationEvent + { + string eventName = MessageNameProvider.GetMessageName(); + Logger.Info($"Unsubscribing from {eventName}"); + if (_subscriptions.TryGetValue(eventName, out var handlers)) + { + handlers.RemoveAll(t => t.Matches(typeof(THandler))); + } + + Unsubscribe(eventName); + } + + public void Unsubscribe(IIntegrationMessageHandler handler) where TEvent : IIntegrationEvent + { + string eventName = MessageNameProvider.GetMessageName(); + Logger.Info($"Unsubscribing from {eventName}"); + if (_subscriptions.TryGetValue(eventName, out var handlers)) + { + handlers.RemoveAll(t => t.Matches(handler)); + } + + Unsubscribe(eventName); + } + + protected abstract void Subscribe(string messageName); + protected abstract void Unsubscribe(string messageName); + + protected void Process(string messageName, EventProcessingContext context) + { + Logger.Info($"Processing a {messageName} message"); + EnsureInvoker(); + + if (_subscriptions.TryGetValue(messageName, out List subscriptions)) + { + foreach (ISubscription subscription in subscriptions) + { + try + { + _invoker.Invoke( + instanceProvider => subscription.Process(instanceProvider, context), + new SystemIdentity(), + context.TenantId, + context.CorrelationId); + } + catch (Exception ex) + { + Logger.Warn(ex, $"Processing a {messageName} message failed"); + throw; + } + } + } + else + { + Logger.Info($"No handler registered. Ignoring {messageName} event"); + } + } + + private void EnsureInvoker() + { + if (_invoker == null) + { + throw new InvalidOperationException("Before using the message bus you have to provide the application invoker by calling ProvideInvoker()"); + } + } + + protected virtual void Dispose(bool disposing) + { + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private class DefaultMessageNameProvider : IMessageNameProvider + { + public string GetMessageName() + { + return GetMessageName(typeof(T)); + } + + public string GetMessageName(Type t) + { + var messageName = t.Name ?? throw new ArgumentException("Type name is null!"); + return messageName; + } + + public string GetMessageName(IIntegrationEvent integrationEvent) + { + return GetMessageName(integrationEvent.GetType()); + } + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/EventBusScope.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/MessageBusScope.cs similarity index 50% rename from src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/EventBusScope.cs rename to src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/MessageBusScope.cs index ac981196..70880bef 100644 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/EventBusScope.cs +++ b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/MessageBusScope.cs @@ -1,32 +1,37 @@ -namespace Backend.Fx.Patterns.EventAggregation.Integration +using Backend.Fx.Patterns.DependencyInjection; + +namespace Backend.Fx.Patterns.EventAggregation.Integration { using System.Collections.Concurrent; using System.Threading.Tasks; - - public interface IEventBusScope - { + + public interface IMessageBusScope + { /// /// Enqueue an event to be raised later. - /// Intention is to let events bubble up after an operation has terminated, e.g. when a wrapping - /// unit of work has completed. + /// Intention is to let events bubble up after an operation has terminated /// /// void Publish(IIntegrationEvent integrationEvent); + Task RaiseEvents(); } - public class EventBusScope : IEventBusScope + public class MessageBusScope : IMessageBusScope { private readonly ConcurrentQueue _integrationEvents = new ConcurrentQueue(); - private readonly IEventBus _eventBus; + private readonly IMessageBus _messageBus; + private readonly ICurrentTHolder _correlationHolder; - public EventBusScope(IEventBus eventBus) + public MessageBusScope(IMessageBus messageBus, ICurrentTHolder correlationHolder) { - _eventBus = eventBus; + _messageBus = messageBus; + _correlationHolder = correlationHolder; } - - void IEventBusScope.Publish(IIntegrationEvent integrationEvent) + + void IMessageBusScope.Publish(IIntegrationEvent integrationEvent) { + ((IntegrationEvent) integrationEvent).SetCorrelationId(_correlationHolder.Current.Id); _integrationEvents.Enqueue(integrationEvent); } @@ -34,9 +39,8 @@ public async Task RaiseEvents() { while (_integrationEvents.TryDequeue(out IIntegrationEvent integrationEvent)) { - await _eventBus.Publish(integrationEvent); + await _messageBus.Publish(integrationEvent); } } } -} - +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/SingletonSubscription.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/SingletonSubscription.cs index 0757432f..424a605e 100644 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/SingletonSubscription.cs +++ b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/SingletonSubscription.cs @@ -1,22 +1,23 @@ using Backend.Fx.Logging; +using Backend.Fx.Patterns.DependencyInjection; namespace Backend.Fx.Patterns.EventAggregation.Integration { - public class SingletonSubscription: ISubscription where TEvent : IIntegrationEvent + public class SingletonSubscription : ISubscription where TEvent : IIntegrationEvent { private static readonly ILogger Logger = LogManager.Create>(); - private readonly IIntegrationEventHandler _handler; + private readonly IIntegrationMessageHandler _handler; - public SingletonSubscription(IIntegrationEventHandler handler) + public SingletonSubscription(IIntegrationMessageHandler handler) { _handler = handler; } - public void Process(string eventName, EventProcessingContext context) + public void Process(IInstanceProvider instanceProvider, EventProcessingContext context) { using (Logger.InfoDuration($"Invoking subscribed handler {_handler.GetType().Name}")) { - _handler.Handle((TEvent)context.GetTypedEvent(typeof(TEvent))); + _handler.Handle((TEvent) context.GetTypedEvent(typeof(TEvent))); } } diff --git a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/TypedSubscription.cs b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/TypedSubscription.cs index a39fb885..c644da9c 100644 --- a/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/TypedSubscription.cs +++ b/src/abstractions/Backend.Fx/Patterns/EventAggregation/Integration/TypedSubscription.cs @@ -11,35 +11,33 @@ namespace Backend.Fx.Patterns.EventAggregation.Integration public class TypedSubscription : ISubscription { private static readonly ILogger Logger = LogManager.Create(); - private readonly IBackendFxApplication _application; private readonly Type _handlerType; - public TypedSubscription(IBackendFxApplication application, Type handlerType) + public TypedSubscription(Type handlerType) { - _application = application; _handlerType = handlerType; } - public void Process(string eventName, EventProcessingContext context) + public void Process(IInstanceProvider instanceProvider, EventProcessingContext context) { - Type interfaceType = _handlerType.GetInterfaces().First(x => x.GetTypeInfo().IsGenericType && x.GetGenericTypeDefinition() == typeof(IIntegrationEventHandler<>)); - var eventType = interfaceType.GetGenericArguments().Single(t => typeof(IIntegrationEvent).IsAssignableFrom(t)); - var integrationEvent = context.GetTypedEvent(eventType); - MethodInfo handleMethod = _handlerType.GetRuntimeMethod("Handle", new[] { eventType }); - Debug.Assert(handleMethod != null, $"No method with signature `Handle({eventName} event)` found on {_handlerType.Name}"); + Type interfaceType = _handlerType.GetInterfaces().First(x => x.GetTypeInfo().IsGenericType && x.GetGenericTypeDefinition() == typeof(IIntegrationMessageHandler<>)); + Type eventType = interfaceType.GetGenericArguments().Single(t => typeof(IIntegrationEvent).IsAssignableFrom(t)); + IIntegrationEvent integrationEvent = context.GetTypedEvent(eventType); + MethodInfo handleMethod = _handlerType.GetRuntimeMethod("Handle", new[] {eventType}); + Debug.Assert(handleMethod != null, $"No method with signature `Handle({eventType.Name} event)` found on {_handlerType.Name}"); Logger.Info($"Getting subscribed handler instance of type {_handlerType.Name}"); - object handlerInstance = _application.CompositionRoot.GetInstance(_handlerType); + object handlerInstance = instanceProvider.GetInstance(_handlerType); using (Logger.InfoDuration($"Invoking subscribed handler {_handlerType.GetDetailedTypeName()}")) { - handleMethod.Invoke(handlerInstance, new object[] { integrationEvent }); + handleMethod.Invoke(handlerInstance, new object[] {integrationEvent}); } } public bool Matches(object handler) { - return (Type)handler == _handlerType; + return (Type) handler == _handlerType; } } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/IdGeneration/HiLoIdGenerator.cs b/src/abstractions/Backend.Fx/Patterns/IdGeneration/HiLoIdGenerator.cs index 37cd55a8..8c2b76bd 100644 --- a/src/abstractions/Backend.Fx/Patterns/IdGeneration/HiLoIdGenerator.cs +++ b/src/abstractions/Backend.Fx/Patterns/IdGeneration/HiLoIdGenerator.cs @@ -1,8 +1,8 @@ +using System.Threading; +using Backend.Fx.Logging; + namespace Backend.Fx.Patterns.IdGeneration { - using System.Threading; - using Logging; - public abstract class HiLoIdGenerator : IIdGenerator { private static readonly ILogger Logger = LogManager.Create(); diff --git a/src/abstractions/Backend.Fx/Patterns/IdGeneration/IIdGenerator.cs b/src/abstractions/Backend.Fx/Patterns/IdGeneration/IIdGenerator.cs index ba1063a4..5c59c6d2 100644 --- a/src/abstractions/Backend.Fx/Patterns/IdGeneration/IIdGenerator.cs +++ b/src/abstractions/Backend.Fx/Patterns/IdGeneration/IIdGenerator.cs @@ -4,4 +4,4 @@ public interface IIdGenerator { int NextId(); } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/IdGeneration/ISequence.cs b/src/abstractions/Backend.Fx/Patterns/IdGeneration/ISequence.cs index b2f977a5..02a4516b 100644 --- a/src/abstractions/Backend.Fx/Patterns/IdGeneration/ISequence.cs +++ b/src/abstractions/Backend.Fx/Patterns/IdGeneration/ISequence.cs @@ -6,4 +6,4 @@ public interface ISequence int GetNextValue(); int Increment { get; } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/IdGeneration/SequenceHiLoIdGenerator.cs b/src/abstractions/Backend.Fx/Patterns/IdGeneration/SequenceHiLoIdGenerator.cs index c9cd0510..693bccbc 100644 --- a/src/abstractions/Backend.Fx/Patterns/IdGeneration/SequenceHiLoIdGenerator.cs +++ b/src/abstractions/Backend.Fx/Patterns/IdGeneration/SequenceHiLoIdGenerator.cs @@ -3,7 +3,7 @@ public abstract class SequenceHiLoIdGenerator : HiLoIdGenerator, IEntityIdGenerator { private readonly ISequence _sequence; - + protected SequenceHiLoIdGenerator(ISequence sequence) { _sequence = sequence; diff --git a/src/abstractions/Backend.Fx/Patterns/Jobs/IJob.cs b/src/abstractions/Backend.Fx/Patterns/Jobs/IJob.cs index afbfbb91..552e21eb 100644 --- a/src/abstractions/Backend.Fx/Patterns/Jobs/IJob.cs +++ b/src/abstractions/Backend.Fx/Patterns/Jobs/IJob.cs @@ -7,4 +7,4 @@ public interface IJob { void Run(); } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/Transactions/ReadonlyDecorator.cs b/src/abstractions/Backend.Fx/Patterns/Transactions/ReadonlyDecorator.cs deleted file mode 100644 index 0c69a3ee..00000000 --- a/src/abstractions/Backend.Fx/Patterns/Transactions/ReadonlyDecorator.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Data; -using Backend.Fx.Logging; - -namespace Backend.Fx.Patterns.Transactions -{ - public class ReadonlyDecorator : ITransactionContext - { - private static readonly ILogger Logger = LogManager.Create(); - - private readonly ITransactionContext _transactionContext; - - public ReadonlyDecorator(ITransactionContext transactionContext) - { - Logger.Debug("Making this transaction context readonly"); - _transactionContext = transactionContext; - } - - public void BeginTransaction() => _transactionContext.BeginTransaction(); - - public void CommitTransaction() - { - Logger.Debug("Committing transaction is intercepted and replaced with rollback transaction to ensure readonly behavior"); - RollbackTransaction(); - } - - public void RollbackTransaction() => _transactionContext.RollbackTransaction(); - - public void SetIsolationLevel(IsolationLevel isolationLevel) - { - _transactionContext.SetIsolationLevel(isolationLevel); - } - - - public IDbTransaction CurrentTransaction => _transactionContext.CurrentTransaction; - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/Transactions/TransactionContext.cs b/src/abstractions/Backend.Fx/Patterns/Transactions/TransactionContext.cs deleted file mode 100644 index 4b79ef78..00000000 --- a/src/abstractions/Backend.Fx/Patterns/Transactions/TransactionContext.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System; -using System.Data; -using Backend.Fx.Logging; - -namespace Backend.Fx.Patterns.Transactions -{ - /// - /// wraps an underlying database transaction. In combination with a injection container, access to - /// the current transaction can be gained by means of this interface. - /// - public interface ITransactionContext - { - IDbTransaction CurrentTransaction { get; } - void BeginTransaction(); - void CommitTransaction(); - void RollbackTransaction(); - void SetIsolationLevel(IsolationLevel isolationLevel); - } - - public class TransactionContext : ITransactionContext, IDisposable - { - private static readonly ILogger Logger = LogManager.Create(); - private readonly bool _shouldHandleConnectionState; - private IsolationLevel _isolationLevel = IsolationLevel.Unspecified; - private IDisposable _transactionLifetimeLogger; - - public TransactionContext(IDbConnection connection) - { - Connection = connection; - ConnectionState state = Connection.State; - switch (state) - { - case ConnectionState.Closed: - _shouldHandleConnectionState = true; - break; - case ConnectionState.Open: - _shouldHandleConnectionState = false; - break; - default: - throw new InvalidOperationException($"A connection provided to the TransactionContext must either be closed or open, but must not be {state}"); - } - } - - public IDbConnection Connection { get; } - - public IDbTransaction CurrentTransaction { get; private set; } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - public void BeginTransaction() - { - if (_shouldHandleConnectionState) - { - Logger.Debug("Opening connection"); - Connection.Open(); - } - - Logger.Debug("Beginning transaction"); - CurrentTransaction = Connection.BeginTransaction(_isolationLevel); - _transactionLifetimeLogger = Logger.DebugDuration("Transaction open"); - } - - public void CommitTransaction() - { - Logger.Debug("Committing transaction"); - CurrentTransaction.Commit(); - CurrentTransaction.Dispose(); - CurrentTransaction = null; - _transactionLifetimeLogger?.Dispose(); - _transactionLifetimeLogger = null; - if (_shouldHandleConnectionState) - { - Logger.Debug("Closing connection"); - Connection.Close(); - } - } - - public void RollbackTransaction() - { - Logger.Debug("rolling back transaction"); - CurrentTransaction.Rollback(); - CurrentTransaction.Dispose(); - CurrentTransaction = null; - _transactionLifetimeLogger?.Dispose(); - _transactionLifetimeLogger = null; - if (_shouldHandleConnectionState) - { - Connection.Close(); - } - } - - public void SetIsolationLevel(IsolationLevel isolationLevel) - { - if (CurrentTransaction != null) - { - throw new InvalidOperationException("Isolation level cannot be changed after the transaction has been started"); - } - - _isolationLevel = isolationLevel; - } - - private void Dispose(bool disposing) - { - if (disposing) - { - try - { - CurrentTransaction?.Dispose(); - CurrentTransaction = null; - if (_shouldHandleConnectionState && Connection.State == ConnectionState.Open) - { - Connection.Close(); - } - } - catch (Exception ex) - { - Logger.Error(ex, "Dispose failed"); - } - - _transactionLifetimeLogger?.Dispose(); - CurrentTransaction?.Dispose(); - } - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/UnitOfWork/DbConnectionDecorator.cs b/src/abstractions/Backend.Fx/Patterns/UnitOfWork/DbConnectionDecorator.cs deleted file mode 100644 index a07eb772..00000000 --- a/src/abstractions/Backend.Fx/Patterns/UnitOfWork/DbConnectionDecorator.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Data; -using System.Security.Principal; -using Backend.Fx.Patterns.DependencyInjection; - -namespace Backend.Fx.Patterns.UnitOfWork -{ - /// - /// Enriches the unit of work to open and close a database connection during lifetime - /// - public class DbConnectionDecorator : IUnitOfWork - { - public DbConnectionDecorator(IDbConnection dbConnection, IUnitOfWork unitOfWork) - { - DbConnection = dbConnection; - UnitOfWork = unitOfWork; - } - - public IUnitOfWork UnitOfWork { get; } - - public IDbConnection DbConnection { get; } - - public ICurrentTHolder IdentityHolder => UnitOfWork.IdentityHolder; - - public void Begin() - { - DbConnection.Open(); - UnitOfWork.Begin(); - } - - public void Complete() - { - UnitOfWork.Complete(); - DbConnection.Close(); - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/UnitOfWork/DbTransactionDecorator.cs b/src/abstractions/Backend.Fx/Patterns/UnitOfWork/DbTransactionDecorator.cs deleted file mode 100644 index 4c80beb4..00000000 --- a/src/abstractions/Backend.Fx/Patterns/UnitOfWork/DbTransactionDecorator.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Security.Principal; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.Patterns.Transactions; - -namespace Backend.Fx.Patterns.UnitOfWork -{ - /// - /// Enriches the unit of work to use a database transaction during lifetime. The transaction gets started, before IUnitOfWork.Begin() - /// is being called and gets committed after IUnitOfWork.Complete() is being called. - /// - public class DbTransactionDecorator : IUnitOfWork - { - public DbTransactionDecorator(ITransactionContext transactionContext, IUnitOfWork unitOfWork) - { - TransactionContext = transactionContext; - UnitOfWork = unitOfWork; - } - - public IUnitOfWork UnitOfWork { get; } - - public ITransactionContext TransactionContext { get; } - - public ICurrentTHolder IdentityHolder => UnitOfWork.IdentityHolder; - - public virtual void Begin() - { - TransactionContext.BeginTransaction(); - UnitOfWork.Begin(); - } - - public virtual void Complete() - { - UnitOfWork.Complete(); - TransactionContext.CommitTransaction(); - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/UnitOfWork/IUnitOfWork.cs b/src/abstractions/Backend.Fx/Patterns/UnitOfWork/IUnitOfWork.cs deleted file mode 100644 index 962422f6..00000000 --- a/src/abstractions/Backend.Fx/Patterns/UnitOfWork/IUnitOfWork.cs +++ /dev/null @@ -1,87 +0,0 @@ -namespace Backend.Fx.Patterns.UnitOfWork -{ - using System; - using System.Security.Principal; - using DependencyInjection; - using Environment.DateAndTime; - using EventAggregation.Domain; - using EventAggregation.Integration; - using Extensions; - using Logging; - - /// - /// Maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems. - /// - public interface IUnitOfWork - { - void Begin(); - void Complete(); - ICurrentTHolder IdentityHolder { get; } - } - - public abstract class UnitOfWork : IUnitOfWork, ICanFlush - { - private static readonly ILogger Logger = LogManager.Create(); - private static int _index; - private readonly int _instanceId = _index++; - private readonly IClock _clock; - private readonly IDomainEventAggregator _eventAggregator; - private readonly IEventBusScope _eventBusScope; - private bool? _isCompleted; - private IDisposable _lifetimeLogger; - - protected UnitOfWork(IClock clock, ICurrentTHolder identityHolder, - IDomainEventAggregator eventAggregator, IEventBusScope eventBusScope) - { - _clock = clock; - _eventAggregator = eventAggregator; - _eventBusScope = eventBusScope; - IdentityHolder = identityHolder; - } - - public ICurrentTHolder IdentityHolder { get; } - - public virtual void Flush() - { - Logger.Debug("Flushing unit of work #" + _instanceId); - UpdateTrackingProperties(IdentityHolder.Current.Name, _clock.UtcNow); - } - - public virtual void Begin() - { - _lifetimeLogger = Logger.DebugDuration($"Beginning unit of work #{_instanceId}", $"Disposing unit of work #{_instanceId}"); - _isCompleted = false; - } - - public virtual void Complete() - { - Logger.Debug("Completing unit of work #" + _instanceId); - Flush(); // we have to flush before raising events, therefore the handlers find the latest changes in the DB - _eventAggregator.RaiseEvents(); - Flush(); // event handlers change the DB state, so we have to flush again - AsyncHelper.RunSync(()=>_eventBusScope.RaiseEvents()); - _isCompleted = true; - } - - protected abstract void UpdateTrackingProperties(string userId, DateTime utcNow); - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - if (_isCompleted == false) - { - Logger.Info($"Canceling unit of work #{_instanceId}."); - } - _lifetimeLogger?.Dispose(); - _lifetimeLogger = null; - } - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - } -} diff --git a/src/abstractions/Backend.Fx/RandomData/Generator.cs b/src/abstractions/Backend.Fx/RandomData/Generator.cs new file mode 100644 index 00000000..77595ae6 --- /dev/null +++ b/src/abstractions/Backend.Fx/RandomData/Generator.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Backend.Fx.RandomData +{ + public abstract class Generator : IEnumerable + { + private readonly HashSet _identicalPreventionMemory = new HashSet(); + + public IEnumerator GetEnumerator() + { + const int maxRetries = 1000; + int retries = 0; + while (true) + { + T next; + int hashCode; + do + { + next = Next(); + hashCode = next.GetHashCode(); + + if (retries++ > maxRetries) + { + throw new Exception($"Tried {maxRetries} times to generate a unique {typeof(T).Name} but did not succeed, aborting now."); + } + } while (_identicalPreventionMemory.Contains(hashCode)); + + _identicalPreventionMemory.Add(hashCode); + yield return next; + } + // ReSharper disable once IteratorNeverReturns + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + protected abstract T Next(); + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/LandLineGenerator.cs b/src/abstractions/Backend.Fx/RandomData/LandLineGenerator.cs new file mode 100644 index 00000000..996f4f02 --- /dev/null +++ b/src/abstractions/Backend.Fx/RandomData/LandLineGenerator.cs @@ -0,0 +1,19 @@ +using System.Linq; + +namespace Backend.Fx.RandomData +{ + public class LandLineGenerator : Generator + { + public static string Generate() + { + return new LandLineGenerator().Take(1).Single(); + } + + protected override string Next() + { + var generated = Numbers.LandLineNetworks.Random(); + while (generated.Length < TestRandom.Instance.Next(8, 11)) generated += Numbers.Ciphers.Random(); + return generated; + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/Letters.cs b/src/abstractions/Backend.Fx/RandomData/Letters.cs index 2d49fd04..4d18a3d5 100644 --- a/src/abstractions/Backend.Fx/RandomData/Letters.cs +++ b/src/abstractions/Backend.Fx/RandomData/Letters.cs @@ -1,38 +1,29 @@ namespace Backend.Fx.RandomData { - public class Letters + public static class Letters { public static string UpperCase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; public static string LowerCase = "abcdefghijklmnopqrstuvwxyz"; public static string RandomUpperCase(int length) { - string random = string.Empty; - for (int i = 0; i < length; i++) - { - random += UpperCase.Random(); - } + var random = string.Empty; + for (var i = 0; i < length; i++) random += UpperCase.Random(); return random; } public static string RandomLowerCase(int length) { - string random = string.Empty; - for (int i = 0; i < length; i++) - { - random += LowerCase.Random(); - } + var random = string.Empty; + for (var i = 0; i < length; i++) random += LowerCase.Random(); return random; } public static string RandomNormalCase(int length) { - string random = string.Empty; - for (int i = 0; i < length; i++) - { - random += i == 0 ? UpperCase.Random() : LowerCase.Random(); - } + var random = string.Empty; + for (var i = 0; i < length; i++) random += i == 0 ? UpperCase.Random() : LowerCase.Random(); return random; } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/LinqExtensions.cs b/src/abstractions/Backend.Fx/RandomData/LinqExtensions.cs index 4b6595ae..8fcce937 100644 --- a/src/abstractions/Backend.Fx/RandomData/LinqExtensions.cs +++ b/src/abstractions/Backend.Fx/RandomData/LinqExtensions.cs @@ -1,33 +1,30 @@ -namespace Backend.Fx.RandomData -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Linq.Dynamic.Core; - using System.Reflection; - using BuildingBlocks; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Reflection; +using Backend.Fx.BuildingBlocks; +namespace Backend.Fx.RandomData +{ public static class LinqExtensions { /// - /// https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle + /// https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle /// /// /// /// public static IEnumerable Shuffle(this IEnumerable source) { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } + if (source == null) throw new ArgumentNullException(nameof(source)); - T[] sourceAsArray = source as T[] ?? source.ToArray(); + var sourceAsArray = source as T[] ?? source.ToArray(); - int n = sourceAsArray.Length; + var n = sourceAsArray.Length; while (n > 1) { - int k = TestRandom.Instance.Next(n--); + var k = TestRandom.Instance.Next(n--); T temp = sourceAsArray[n]; sourceAsArray[n] = sourceAsArray[k]; sourceAsArray[k] = temp; @@ -38,57 +35,41 @@ public static IEnumerable Shuffle(this IEnumerable source) public static T Random(this IEnumerable source) { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } + if (source == null) throw new ArgumentNullException(nameof(source)); // ReSharper disable once PossibleMultipleEnumeration - if (TryAsQueryable(source, out IQueryable sourceQueryable, out int count)) + if (TryAsQueryable(source, out var sourceQueryable, out var count)) { if (count == 0) - { throw new ArgumentException( $"The enumerable of {typeof(T).Name} does not contain any items, therefore no random item can be returned.", nameof(source)); - } - + return sourceQueryable.Skip(TestRandom.Next(count - 1)).First(); } // ReSharper disable once PossibleMultipleEnumeration var sourceArray = source as T[] ?? source.ToArray(); if (sourceArray.Length == 0) - { throw new ArgumentException( $"The enumerable of {typeof(T).Name} does not contain any items, therefore no random item can be returned.", nameof(source)); - } return sourceArray.ElementAt(TestRandom.Next(sourceArray.Length)); } public static T RandomOrDefault(this IEnumerable source) { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } + if (source == null) throw new ArgumentNullException(nameof(source)); // ReSharper disable once PossibleMultipleEnumeration - if (TryAsQueryable(source, out IQueryable sourceQueryable, out int count)) + if (TryAsQueryable(source, out var sourceQueryable, out var count)) { - if (count == 0) - { - return default(T); - } - + if (count == 0) return default; + return sourceQueryable.Skip(TestRandom.Next(count - 1)).FirstOrDefault(); } // ReSharper disable once PossibleMultipleEnumeration var sourceArray = source as T[] ?? source.ToArray(); - if (sourceArray.Length == 0) - { - return default(T); - } + if (sourceArray.Length == 0) return default; return sourceArray.ElementAt(TestRandom.Next(sourceArray.Length)); } @@ -105,12 +86,9 @@ private static bool TryAsQueryable(this IEnumerable source, out IQueryable outQueryable = new T[0].AsQueryable(); return true; } - - var idProperty = typeof(T).GetProperty(nameof(Identified.Id), BindingFlags.Instance | BindingFlags.Public); - if (idProperty != null) - { - sourceQueryable = sourceQueryable.OrderBy(nameof(Identified.Id)); - } + + PropertyInfo idProperty = typeof(T).GetProperty(nameof(Identified.Id), BindingFlags.Instance | BindingFlags.Public); + if (idProperty != null) sourceQueryable = sourceQueryable.OrderBy(nameof(Identified.Id)); outQueryable = sourceQueryable; return true; diff --git a/src/abstractions/Backend.Fx/RandomData/LoremIpsum.cs b/src/abstractions/Backend.Fx/RandomData/LoremIpsum.cs index 045ea049..f4546be8 100644 --- a/src/abstractions/Backend.Fx/RandomData/LoremIpsum.cs +++ b/src/abstractions/Backend.Fx/RandomData/LoremIpsum.cs @@ -1,31 +1,38 @@ -namespace Backend.Fx.RandomData +using System.Linq; + +namespace Backend.Fx.RandomData { - public class LoremIpsum + public class LoremIpsumGenerator : Generator { - public static string Generate(int minWords, int maxWords, bool asSentence) + private static string[] _words = new[] { - var words = new[] { - "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit", "proin", "eget", "iaculis", "quam", "pellentesque", "elementum", "gravida", "nulla", "at", "tincidunt", "donec", "vulputate", "velit", "sapien", "a", "auctor", "justo", "id", "nunc", "et", "consequat", "magna", "in", "blandit", "ut", "eros", "tempus", "condimentum", "sem", "ac", "feugiat", "tellus", "curabitur", "aliquet", "ultrices", "arcu", "eu", "lacinia", "aliquam", "integer", "non", "venenatis", "sed", "accumsan", "massa", "nibh", "vestibulum", "nec", "porta", "libero", "vel", "ex", "molestie", "pretium", "dignissim", "ligula", "maximus", "placerat", "nisl", "felis", "fringilla", "efficitur", "mi", "nam", "vitae", "orci", "suscipit", "porttitor", "leo", "posuere", "sollicitudin", "dictum", "tristique", "dui", "urna", "quis", "quisque", "semper", "diam", "pulvinar", "erat", "ornare", "maecenas", "euismod", "odio", "tortor", "cursus", "convallis", "enim", "sodales", "facilisis", "faucibus", "fusce", "scelerisque", "purus", "praesent", "interdum", "turpis", "mauris", "duis", "finibus", "augue", "nullam", "mollis", "lacus", "egestas", "metus", "mattis", "morbi", "laoreet", "bibendum", "phasellus", "risus", "neque", "volutpat", "lobortis", "malesuada", "sagittis", "rhoncus", "est", "imperdiet", "aenean", "fermentum", "varius", "vivamus", "suspendisse", "commodo", "luctus", "dapibus", "ullamcorper", "viverra", "congue", "hendrerit", "pharetra", "tempor", "eleifend", "lectus", "te" - }; - - var numWords = TestRandom.Instance.Next(maxWords - minWords) + minWords + 1; + "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit", "proin", "eget", "iaculis", "quam", "pellentesque", "elementum", "gravida", "nulla", + "at", "tincidunt", "donec", "vulputate", "velit", "sapien", "a", "auctor", "justo", "id", "nunc", "et", "consequat", "magna", "in", "blandit", "ut", "eros", + "tempus", "condimentum", "sem", "ac", "feugiat", "tellus", "curabitur", "aliquet", "ultrices", "arcu", "eu", "lacinia", "aliquam", "integer", "non", "venenatis", + "sed", "accumsan", "massa", "nibh", "vestibulum", "nec", "porta", "libero", "vel", "ex", "molestie", "pretium", "dignissim", "ligula", "maximus", "placerat", + "nisl", "felis", "fringilla", "efficitur", "mi", "nam", "vitae", "orci", "suscipit", "porttitor", "leo", "posuere", "sollicitudin", "dictum", "tristique", "dui", + "urna", "quis", "quisque", "semper", "diam", "pulvinar", "erat", "ornare", "maecenas", "euismod", "odio", "tortor", "cursus", "convallis", "enim", "sodales", + "facilisis", "faucibus", "fusce", "scelerisque", "purus", "praesent", "interdum", "turpis", "mauris", "duis", "finibus", "augue", "nullam", "mollis", "lacus", + "egestas", "metus", "mattis", "morbi", "laoreet", "bibendum", "phasellus", "risus", "neque", "volutpat", "lobortis", "malesuada", "sagittis", "rhoncus", "est", + "imperdiet", "aenean", "fermentum", "varius", "vivamus", "suspendisse", "commodo", "luctus", "dapibus", "ullamcorper", "viverra", "congue", "hendrerit", "pharetra", + "tempor", "eleifend", "lectus", "te" + }; - var result = string.Empty; - - for (var w = 0; w < numWords; w++) - { - if (w > 0) - { - result += " "; - } - result += words[TestRandom.Instance.Next(words.Length)]; - } + protected override string Next() + { + return _words[TestRandom.Instance.Next(_words.Length)]; + } + public static string Generate(int minWords, int maxWords, bool asSentence) + { + int wordCount = TestRandom.Next(minWords, maxWords); + string loremIpsumText = string.Join(" ", new LoremIpsumGenerator().Take(wordCount)); if (asSentence) { - result += ". "; + loremIpsumText += "."; } - return result; - } + + return loremIpsumText; + } } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/MobileLineGenerator.cs b/src/abstractions/Backend.Fx/RandomData/MobileLineGenerator.cs new file mode 100644 index 00000000..094e7a25 --- /dev/null +++ b/src/abstractions/Backend.Fx/RandomData/MobileLineGenerator.cs @@ -0,0 +1,21 @@ +using System.Linq; + +namespace Backend.Fx.RandomData +{ + public class MobileLineGenerator : Generator + { + public static string Generate() + { + return new MobileLineGenerator().First(); + } + + + protected override string Next() + { + var generated = Numbers.MobileNetworks.Random(); + while (generated.Length < TestRandom.Instance.Next(11)) generated += Numbers.Ciphers.Random(); + + return generated; + } + } +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/Names.cs b/src/abstractions/Backend.Fx/RandomData/Names.cs index ea9992eb..31e5bdd4 100644 --- a/src/abstractions/Backend.Fx/RandomData/Names.cs +++ b/src/abstractions/Backend.Fx/RandomData/Names.cs @@ -2,7 +2,8 @@ { public static class Names { - public static readonly string[] LegalEntityTypes = { + public static readonly string[] LegalEntityTypes = + { "GmbH", "AG", "KG", @@ -13,7 +14,8 @@ public static class Names "" }; - public static readonly string[] CompanyTypes = { + public static readonly string[] CompanyTypes = + { "Schuldnerberatung", "Autohaus", "Apotheke", @@ -24,7 +26,8 @@ public static class Names "Bauunternehmen" }; - public static readonly string[] Streets = { + public static readonly string[] Streets = + { "Abboweg", "Abteistraße", "Achatweg", @@ -3419,7 +3422,8 @@ public static class Names "Zwitschergasse" }; - public static readonly string[] Female = { + public static readonly string[] Female = + { "Andrea", "Angelika", "Anja", @@ -3492,188 +3496,112 @@ public static class Names "Yvonne" }; - public static readonly string[] Family = { - "Abel", - "Adelung", - "Albers", - "Baars", - "Baden", - "Bagwell", - "Bartels", - "zum Beck", - "Becker", - "Beckmann", - "Behrmann", - "Bendel", - "vom Berge", - "Berghof", - "Bergmann", - "Bier", - "Blindhammer", - "Blöte", - "Boelter", - "Boers", - "Bothmann", - "Brammer", - "Brandt", - "Braul", - "Brelie", - "Brink", - "Brockmann", - "Bruhn", - "Buckentin", - "Bünger", - "Brümmerhoff", - "Bunke", - "Bussmann", - "Böttcher", - "Clasen", - "Clausen", - "Cohrs", - "Cordes", - "Dammann", - "Drewes", - "Dreyer", - "Duhne", - "Ebeling", - "Eggersglüß", - "Ehlers", - "Eickhoff", - "Eitz", - "Elling", - "Engelke", - "Euhus", - "Feldmann", - "Fehlau", - "Finke", - "Fintel", + public static readonly string[] Family = + { + "Müller", + "Schmidt", + "Schneider", "Fischer", - "Fornaçon", - "Freudenthal", - "Friederichs", - "Fuhrhop", - "Gaudineer", - "Grohnholz", - "Grotenholte", - "Grünhagen", - "Hambrock", - "Hambruch", - "Harms", - "Hartz", - "Hasselhoff", - "Hebenbrock", - "Heldberg", - "Hellwinkel", - "Heuer", - "Hinrichs", - "Hövermann", - "Hoops", - "Hude", - "Hundt", - "Husemann", - "Jacobs", - "John", - "Johnson", - "von der Kammer", - "Kassebart", - "Keusemann", - "Kiss", - "Klinker", - "Knupper", - "Köhnken", - "Kothe", - "Kroells", - "Kronsnest", - "Küster", - "Langeloh", - "Langer", - "Leitzmann", - "Loetzke", - "Lohmann", - "Lindemann", - "Lüders", - "Lüning", - "Lütjens", - "Marquard", - "Meffert", + "Weber", "Meyer", - "Meyerhoff", - "Meyermann", - "Meyers", - "Müller", - "Nester", - "Nolte", - "Nottorf", - "Nowka", - "Oelfke", - "Ohlau", - "Oldenburg", - "Ossowski", - "Otten", - "Panning", - "Piepjunge", - "Plumhoff", - "Quast", - "Ragotzki", - "Ratje", - "Reinitz", - "Reinhardt", - "Rischmann", - "Rodewald", - "Röders", - "Röhrs", - "Roloff", - "Rosebrock", - "Röttger", - "von Salzen", - "Schade", - "Schepperus", - "Schloendorf", - "Schmuck", + "Wagner", + "Becker", + "Schulz", + "Hoffmann", + "Schäfer", + "Koch", + "Bauer", + "Richter", + "Klein", + "Wolf", "Schröder", - "Schweikert", - "Sciarra", - "Siemering", - "Söhnholz", - "Springhorn", - "Steinbach", - "Stellmann", - "Stoeckmann", - "Stolte", - "Struckhoff", - "Stünkel", - "Suhrmüller", - "Sump", - "Tanke", - "Tetzlaff", - "Thalmann", - "Tietjen", - "Tutas", - "Vollmer", - "Voß", - "Wallmann", - "Weber", - "Wehrs", - "Weide", - "Whitters", - "Wiebke", - "Willing", - "Winkelmann", - "Wischoff", - "Wischhoff", - "Witte", - "Wittig", - "Wohlde", - "Wolkenhauer", - "Wolters", - "Wrigge", - "Wroge", - "Wüsthoff", - "Wulfers", - "Würdemann" + "Neumann", + "Schwarz", + "Zimmermann", + "Braun", + "Krüger", + "Hofmann", + "Hartmann", + "Lange", + "Schmitt", + "Werner", + "Schmitz", + "Krause", + "Meier", + "Lehmann", + "Schmid", + "Schulze", + "Maier", + "Köhler", + "Herrmann", + "König", + "Walter", + "Mayer", + "Huber", + "Kaiser", + "Fuchs", + "Peters", + "Lang", + "Scholz", + "Möller", + "Weiß", + "Jung", + "Hahn", + "Schubert", + "Vogel", + "Friedrich", + "Keller", + "Günther", + "Frank", + "Berger", + "Winkler", + "Roth", + "Beck", + "Lorenz", + "Baumann", + "Franke", + "Albrecht", + "Schuster", + "Simon", + "Ludwig", + "Böhm", + "Winter", + "Kraus", + "Martin", + "Schumacher", + "Krämer", + "Vogt", + "Stein", + "Jäger", + "Otto", + "Sommer", + "Groß", + "Seidel", + "Heinrich", + "Brandt", + "Haas", + "Schreiber", + "Graf", + "Schulte", + "Dietrich", + "Ziegler", + "Kuhn", + "Kühn", + "Pohl", + "Engel", + "Horn", + "Busch", + "Bergmann", + "Thomas", + "Voigt", + "Sauer", + "Arnold", + "Wolff", + "Pfeiffer" }; - public static readonly string[] Male = { + public static readonly string[] Male = + { "Alexander", "Andreas", "Benjamin", @@ -3741,7 +3669,8 @@ public static class Names "Wolfgang" }; - public static readonly string[] Departments = { + public static readonly string[] Departments = + { "Vertrieb", "Einkauf", "IT", @@ -3749,7 +3678,8 @@ public static class Names "Geschäftsleitung" }; - public static readonly string[] Cities = { + public static readonly string[] Cities = + { "Aach (Hegau)", "Aach", "Aachen", @@ -5818,10 +5748,11 @@ public static class Names "Zwönitz" }; - public static readonly string[] Countries = { + public static readonly string[] Countries = + { "Austria", "Germany", - "Switzerland", + "Switzerland" }; } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/Numbers.cs b/src/abstractions/Backend.Fx/RandomData/Numbers.cs index dfeacd49..182cd7c8 100644 --- a/src/abstractions/Backend.Fx/RandomData/Numbers.cs +++ b/src/abstractions/Backend.Fx/RandomData/Numbers.cs @@ -1,14 +1,15 @@ -namespace Backend.Fx.RandomData -{ - using System.Globalization; - using System.Linq; +using System.Globalization; +using System.Linq; - public class Numbers +namespace Backend.Fx.RandomData +{ + public static class Numbers { public static readonly string[] Ciphers = Enumerable.Range(0, 10).Select(i => i.ToString()).ToArray(); public static readonly string[] PostalCodes = Enumerable.Range(9000, 90999).Select(i => i.ToString("00000")).ToArray(); - public static readonly string[] LandLineNetworks = { + public static readonly string[] LandLineNetworks = + { "0201", "0202", "0203", @@ -5220,7 +5221,9 @@ public class Numbers "09977", "09978" }; - public static readonly string[] MobileNetworks = { + + public static readonly string[] MobileNetworks = + { "01510", "01511", "01512", @@ -5285,14 +5288,9 @@ public static string RandomHouseNumber() { var next = TestRandom.Next(100); var nr = TestRandom.Next(100); - if (next < 10) - { - return $"{nr} - {nr + TestRandom.Next(1, 5)}"; - } - if (next < 30) - { - return nr.ToString(CultureInfo.InvariantCulture) + "abcd".Random(); - } + if (next < 10) return $"{nr} - {nr + TestRandom.Next(1, 5)}"; + + if (next < 30) return nr.ToString(CultureInfo.InvariantCulture) + "abcd".Random(); return nr.ToString(CultureInfo.InvariantCulture); } diff --git a/src/abstractions/Backend.Fx/RandomData/TestAddress.cs b/src/abstractions/Backend.Fx/RandomData/TestAddress.cs index 46c7f672..735e3f9e 100644 --- a/src/abstractions/Backend.Fx/RandomData/TestAddress.cs +++ b/src/abstractions/Backend.Fx/RandomData/TestAddress.cs @@ -1,6 +1,9 @@ -namespace Backend.Fx.RandomData +using System.Collections.Generic; +using Backend.Fx.BuildingBlocks; + +namespace Backend.Fx.RandomData { - public class TestAddress + public class TestAddress : ValueObject { public TestAddress(string street, string number, string postalCode, string city, string country) { @@ -20,5 +23,14 @@ public TestAddress(string street, string number, string postalCode, string city, public string City { get; } public string Country { get; } + + protected override IEnumerable GetEqualityComponents() + { + yield return Street; + yield return Number; + yield return PostalCode; + yield return City; + yield return Country; + } } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/TestAddressGenerator.cs b/src/abstractions/Backend.Fx/RandomData/TestAddressGenerator.cs index fcb5519f..588d99f2 100644 --- a/src/abstractions/Backend.Fx/RandomData/TestAddressGenerator.cs +++ b/src/abstractions/Backend.Fx/RandomData/TestAddressGenerator.cs @@ -1,34 +1,23 @@ -namespace Backend.Fx.RandomData -{ - using System; - using System.Collections.Generic; +using System.Linq; - public class TestAddressGenerator +namespace Backend.Fx.RandomData +{ + public class TestAddressGenerator : Generator { - public int GenerateCount { get; set; } = 100; - - public Random Random { get; set; } = TestRandom.Instance; - - public IEnumerable GenerateTestAddresses() + public static TestAddress Generate() { - var distinctCheck = new HashSet(); - for (var i = 0; i < GenerateCount; i++) - { - TestAddress generated = null; + return new TestAddressGenerator().First(); + } - while (generated == null || distinctCheck.Contains(generated.Street + generated.Number)) - { - generated = new TestAddress( - Names.Streets.Random(), - Numbers.RandomHouseNumber(), - Numbers.RandomPostalCode(), - Names.Cities.Random(), - Names.Countries.Random()); - } - distinctCheck.Add(generated.Street + generated.Number); - yield return generated; - } + protected override TestAddress Next() + { + return new TestAddress( + Names.Streets.Random(), + Numbers.RandomHouseNumber(), + Numbers.RandomPostalCode(), + Names.Cities.Random(), + Names.Countries.Random()); } } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/TestChemical.cs b/src/abstractions/Backend.Fx/RandomData/TestChemical.cs index 8cf2eb11..af09d77e 100644 --- a/src/abstractions/Backend.Fx/RandomData/TestChemical.cs +++ b/src/abstractions/Backend.Fx/RandomData/TestChemical.cs @@ -1,10 +1,21 @@ -namespace Backend.Fx.RandomData -{ - using System; - using System.Linq; +using System; +using System.Linq; +namespace Backend.Fx.RandomData +{ public class TestChemical { + public TestChemical(string name, string description, string alternativeNames, string formula, decimal molecularWeight, string casRegistryNumber, string molFile) + { + Name = name; + Description = description; + AlternativeNames = alternativeNames; + Formula = formula; + MolecularWeight = molecularWeight; + CasRegistryNumber = casRegistryNumber; + MolFile = molFile; + } + public string Name { get; } public string Description { get; } public string AlternativeNames { get; } @@ -12,80 +23,178 @@ public class TestChemical public decimal MolecularWeight { get; } public string CasRegistryNumber { get; } public string MolFile { get; } - public string RPhrases { get; } - public string SPhrases { get; } - - public string[] RPhrasesArray { - get - { - return RPhrases?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray(); - } - } - public string[] SPhrasesArray { - get - { - return SPhrases?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray(); - } - } - public string[] AlternativeNamesArray { - get - { - return AlternativeNames?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray(); - } + get { return AlternativeNames?.Split(new[] {';'}, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray(); } } // data taken from http://webbook.nist.gov/chemistry/name-ser.html - public static TestChemical[] All { get; } = { + public static TestChemical[] All { get; } = + { //name, description, altName, formula, mol weight, casNo, mol file - new TestChemical("1,2-Dichloroethan", "1,2-Dichlorethan (Ethylendichlorid, EDC) ist eine farblose, brennbare und giftige Flüssigkeit mit chloroformartigem Geruch. Diese chemische Verbindung gehört zu den Chlorkohlenwasserstoffen.", "α,β-Dichloroethane; s-Dichloroethane; Brocide; Dutch liquid; Ethylene chloride; Ethylene dichloride; Freon 150; Glycol dichloride; 1,2-Bichloroethane; 1,2-Dichlorethane; 1,2-Dichloroethane; CH2ClCH2Cl; sym-Dichloroethane; Aethylenchlorid; Bichlorure D'ethylene; Borer sol; Chlorure D'ethylene; Cloruro di ethene; 1,2-DCE; Destruxol borer-sol; 1,2-Dichloorethaan; 1,2-Dichlor-aethan; Dichloremulsion; Di-chlor-mulsion; Dichloro-1,2-ethane; 1,2-Dicloroetano; Dutch oil; EDC; ENT 1,656; Ethane dichloride; Ethyleendichloride; 1,2-Ethylene dichloride; NCI-C00511; Rcra waste number U077; UN 1184; DCE; EDC (halocarbon); HCC 150; 1,2-dichloroethane (ethylene dichloride)", "C2H4Cl2", 98.959m, "107-06-2", "\n\n\n 8 7 0 0 0 1 V2000\n 2.3785 0.7551 0.8326 C 0 0 0 0 0 0 0 0 0\n 1.5134 1.8542 1.3880 C 0 0 0 0 0 0 0 0 0\n 0.0000 1.1383 1.9817 Cl 0 0 0 0 0 0 0 0 0\n 3.8896 1.4714 0.2337 Cl 0 0 0 0 0 0 0 0 0\n 1.8822 0.2254 0.0000 H 0 0 0 0 0 0 0 0 0\n 2.6233 0.0000 1.6007 H 0 0 0 0 0 0 0 0 0\n 1.2715 2.6111 0.6209 H 0 0 0 0 0 0 0 0 0\n 2.0083 2.3814 2.2230 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 5 1 0 0 0\n 1 6 1 0 0 0\n 2 3 1 0 0 0\n 2 7 1 0 0 0\n 2 8 1 0 0 0\nM END\n", "R11;R45;R36/37/38", "S45;S53"), - new TestChemical("1-Buten", "Butene (auch Butylene) sind eine Gruppe von vier isomeren Kohlenwasserstoffen mit der allgemeinen Summenformel C4H8, die über eine C–C-Doppelbindung verfügen. Sie zählen damit zu den Alkenen. Zwei der Isomere unterscheiden sich durch cis-trans-Isomerie. Butene sind unter Standardbedingungen farblose, brennbare Gase mit einer größeren Dichte als Luft. Unter Druck lassen sich die Isomere verflüssigen. Sie wirken in höheren Konzentrationen narkotisierend und erstickend. Mit Luft bilden sie explosive Gemische.", "α-Butene; α-Butylene; But-1-ene; Butene-1; Ethylethylene; 1-Butylene; 1-C4H8", "C4H8", 56.1063m, "106-98-9", "\n\n\n 12 11 0 0 0 1 V2000\n 1.3702 2.1763 1.6455 C 0 0 0 0 0 0 0 0 0\n 0.4451 2.8431 0.9657 C 0 0 0 0 0 0 0 0 0\n 1.9783 0.9053 1.1655 C 0 0 0 0 0 0 0 0 0\n 3.4886 0.9497 1.2530 C 0 0 0 0 0 0 0 0 0\n 1.7309 2.5402 2.6153 H 0 0 0 0 0 0 0 0 0\n 0.0000 3.7633 1.3313 H 0 0 0 0 0 0 0 0 0\n 0.0658 2.5199 0.0000 H 0 0 0 0 0 0 0 0 0\n 1.5835 0.0745 1.7845 H 0 0 0 0 0 0 0 0 0\n 1.6643 0.6803 0.1265 H 0 0 0 0 0 0 0 0 0\n 3.9327 0.0000 0.9290 H 0 0 0 0 0 0 0 0 0\n 3.9065 1.7417 0.6183 H 0 0 0 0 0 0 0 0 0\n 3.8311 1.1398 2.2789 H 0 0 0 0 0 0 0 0 0\n 1 2 2 0 0 0\n 1 3 1 0 0 0\n 1 5 1 0 0 0\n 2 6 1 0 0 0\n 2 7 1 0 0 0\n 3 4 1 0 0 0\n 3 8 1 0 0 0\n 3 9 1 0 0 0\n 4 10 1 0 0 0\n 4 11 1 0 0 0\n 4 12 1 0 0 0\nM END\n", "", ""), - new TestChemical("1-Hexen", "Hexene (Betonung auf der zweiten Silbe) sind chemische Verbindungen aus der Gruppe der Alkene mit der Summenformel C6H12. Der Wortstamm Hex weist auf die sechs Kohlenstoffatome, die Endung en auf die Doppelbindung zwischen zwei der Kohlenstoffatome hin. Es existieren verschiedene Isomere, die sich in der Position der Doppelbindung und dem Vorhandensein bzw. der Lage einer Verzweigung der Kohlenstoffkette unterscheiden. Die in der Industrie am häufigsten eingesetzte Verbindung ist 1-Hexen, welche z. B. als Comonomer bei der Produktion von Polyethen eingesetzt wird.", "Hexene-1; 1-n-Hexene; 1-C6H12; Butylethylene; Hexene; Hex-1-ene; UN 2370; Hexylene; Neodene 6 XHP; NSC 74121; Dialene 6", "C6H12", 84.1595m, "592-41-6", "\n\n\n 18 17 0 0 0 0 0 0 0 0 V2000\n -2.3439 -0.9761 0.0000 H 0000000000000000000\n -2.5829 -2.8063 0.0000 H 0000000000000000000\n -1.8943 -1.9654 0.0000 C 0000000000000000000\n -0.1940 -3.1757 0.0000 H 0000000000000000000\n -0.5736 -2.1523 0.0000 C 0000000000000000000\n 1.1457 -1.2481 0.8727 H 0000000000000000000\n 1.1457 -1.2481 -0.8727 H 0000000000000000000\n 0.4932 -1.0856 0.0000 C 0000000000000000000\n -0.6369 0.5346 -0.8794 H 0000000000000000000\n -0.6369 0.5346 0.8794 H 0000000000000000000\n 0.0000 0.3648 0.0000 C 0000000000000000000\n 1.7839 1.2154 0.8785 H 0000000000000000000\n 1.7839 1.2154 -0.8785 H 0000000000000000000\n 1.1455 1.3862 0.0000 C 0000000000000000000\n 0.0421 3.0486 -0.8848 H 0000000000000000000\n 0.0421 3.0486 0.8848 H 0000000000000000000\n 1.4941 3.5435 0.0000 H 0000000000000000000\n 0.6554 2.8379 0.0000 C 0000000000000000000\n 1 3 1 0 0 0\n 2 3 1 0 0 0\n 3 5 2 0 0 0\n 4 5 1 0 0 0\n 5 8 1 0 0 0\n 6 8 1 0 0 0\n 7 8 1 0 0 0\n 8 11 1 0 0 0\n 9 11 1 0 0 0\n 10 11 1 0 0 0\n 11 14 1 0 0 0\n 12 14 1 0 0 0\n 13 14 1 0 0 0\n 14 18 1 0 0 0\n 15 18 1 0 0 0\n 16 18 1 0 0 0\n 17 18 1 0 0 0\nM END\n", "", ""), - new TestChemical("2,3-Dimethylpentan", "3-Methylhexan und 2,3-Dimethylpentan sind chirale Verbindungen, von denen zwei Enantiomere existieren; Chiralitätszentrum ist das C3, das als Substituenten Wasserstoff und je einen Methyl-, Ethyl- und Propyl- bzw. Isopropylrest trägt. Sie sind die einfachsten chiralen Alkane.", "2,3-Dimethylpentane; 3,4-Dimethylpentane", "C7H16", 100.2019m, "565-59-3", "\n\n\n 23 22 0 0 0 1 V2000\n 4.3751 2.6128 2.7529 C 0 0 0 0 0 0 0 0 0\n 3.2736 1.9976 1.8651 C 0 0 0 0 0 0 0 0 0\n 1.9284 2.6959 2.0896 C 0 0 0 0 0 0 0 0 0\n 3.1364 0.5066 2.1417 C 0 0 0 0 0 0 0 0 0\n 4.5445 4.1009 2.4849 C 0 0 0 0 0 0 0 0 0\n 5.7094 1.9144 2.5314 C 0 0 0 0 0 0 0 0 0\n 0.9524 2.4445 0.9624 C 0 0 0 0 0 0 0 0 0\n 4.0701 2.4735 3.8192 H 0 0 0 0 0 0 0 0 0\n 3.5772 2.1391 0.7982 H 0 0 0 0 0 0 0 0 0\n 1.4902 2.3691 3.0539 H 0 0 0 0 0 0 0 0 0\n 2.0935 3.7916 2.1977 H 0 0 0 0 0 0 0 0 0\n 2.7404 0.3139 3.1481 H 0 0 0 0 0 0 0 0 0\n 2.4601 0.0256 1.4233 H 0 0 0 0 0 0 0 0 0\n 4.1132 0.0000 2.0752 H 0 0 0 0 0 0 0 0 0\n 5.2661 4.5496 3.1789 H 0 0 0 0 0 0 0 0 0\n 4.9040 4.2910 1.4645 H 0 0 0 0 0 0 0 0 0\n 3.5891 4.6380 2.5995 H 0 0 0 0 0 0 0 0 0\n 6.1048 2.1059 1.5247 H 0 0 0 0 0 0 0 0 0\n 6.4620 2.2562 3.2527 H 0 0 0 0 0 0 0 0 0\n 5.6092 0.8222 2.6385 H 0 0 0 0 0 0 0 0 0\n 0.7285 1.3752 0.8458 H 0 0 0 0 0 0 0 0 0\n 0.0000 2.9597 1.1415 H 0 0 0 0 0 0 0 0 0\n 1.3427 2.8017 0.0000 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 5 1 0 0 0\n 1 6 1 0 0 0\n 1 8 1 0 0 0\n 2 3 1 0 0 0\n 2 4 1 0 0 0\n 2 9 1 0 0 0\n 3 7 1 0 0 0\n 3 10 1 0 0 0\n 3 11 1 0 0 0\n 4 12 1 0 0 0\n 4 13 1 0 0 0\n 4 14 1 0 0 0\n 5 15 1 0 0 0\n 5 16 1 0 0 0\n 5 17 1 0 0 0\n 6 18 1 0 0 0\n 6 19 1 0 0 0\n 6 20 1 0 0 0\n 7 21 1 0 0 0\n 7 22 1 0 0 0\n 7 23 1 0 0 0\nM END\n", "", ""), - new TestChemical("Essigsäure", "Essigsäure (systematisch Ethansäure, lateinisch acidum aceticum) ist eine farblose, flüssige, ätzende und typisch riechende Carbonsäure der Zusammensetzung C2H4O2 (Halbstrukturformel CH3COOH). Als Lebensmittelzusatzstoff trägt sie die E-Nummer E 260. Wässrige Lösungen der Essigsäure werden trivial nur Essig und reine Essigsäure Eisessig genannt. Die Salze und Ester der Essigsäure heißen Acetate oder (systematisch) Ethanoate", "Ethanoic acid; Ethylic acid; Glacial acetic acid; Methanecarboxylic acid; Vinegar acid; CH3COOH; Acetasol; Acide acetique; Acido acetico; Azijnzuur; Essigsaeure; Octowy kwas; Acetic acid, glacial; Kyselina octova; UN 2789; Aci-jel; Shotgun; Ethanoic acid monomer; NSC 132953", "C2H4O2", 60.052m, "64-19-7", "\n\n\n 8 7 0 0 0 0 0 0 0 0999 V2000\n 0.7724 0.9670 1.0069 C 0 0 0 0 0\n 2.0576 1.7541 0.9321 C 0 0 0 0 0\n 3.0833 0.9883 0.4751 O 0 0 0 0 0\n 2.1974 2.9214 1.2200 O 0 0 0 0 0\n 0.0114 1.5612 1.5141 H 0 0 0 0 0\n 0.4326 0.7231 -0.0064 H 0 0 0 0 0\n 0.9331 0.0221 1.5356 H 0 0 0 0 0\n 3.8606 1.5761 0.4447 H 0 0 0 0 0\n 6 1 1 0 0 0\n 2 1 1 0 0 0\n 1 5 1 0 0 0\n 1 7 1 0 0 0\n 3 2 1 0 0 0\n 2 4 2 0 0 0\n 8 3 1 0 0 0\nM END\n", "", ""), - new TestChemical("Aceton", "Aceton [at͡səˈtoːn] (auch: Azeton) ist der Trivialname für die organisch-chemische Verbindung Propanon bzw. Dimethylketon. Aceton ist eine farblose Flüssigkeit und findet Verwendung als polares, aprotisches Lösungsmittel und als Ausgangsstoff für viele Synthesen der organischen Chemie. Es ist mit seinem Strukturmerkmal der Carbonylgruppe (>C=O), die zwei Methylgruppen trägt, das einfachste Keton.", "2-Propanone; β-Ketopropane; Dimethyl ketone; Dimethylformaldehyde; Methyl ketone; Propanone; Pyroacetic ether; (CH3)2CO; Dimethylketal; Ketone propane; Ketone, dimethyl-; Chevron acetone; Rcra waste number U002; UN 1090; Sasetone; Propan-2-one; NSC 135802", "C3H6O", 58.0791m, "67-64-1", "\n\n\n 10 9 0 0 0 0 0 0 0 0 V2000\n 0.0000 0.0000 0.1857 C 0000000000000000000\n 0.0000 0.0000 1.4013 O 0000000000000000000\n 0.0000 1.2929 -0.6150 C 0000000000000000000\n 0.0000 -1.2929 -0.6150 C 0000000000000000000\n 0.0000 2.1487 0.0628 H 0000000000000000000\n 0.0000 -2.1487 0.0628 H 0000000000000000000\n 0.8810 1.3407 -1.2674 H 0000000000000000000\n -0.8810 1.3407 -1.2674 H 0000000000000000000\n -0.8810 -1.3407 -1.2674 H 0000000000000000000\n 0.8810 -1.3407 -1.2674 H 0000000000000000000\n 1 2 2 0 0 0\n 1 3 1 0 0 0\n 1 4 1 0 0 0\n 3 5 1 0 0 0\n 3 7 1 0 0 0\n 3 8 1 0 0 0\n 4 6 1 0 0 0\n 4 9 1 0 0 0\n 4 10 1 0 0 0\nM END\n", "", ""), - new TestChemical("Acetonitril", "Acetonitril ist ein organisches Lösungsmittel und gehört zur Stoffgruppe der Nitrile.", "Cyanomethane; Ethanenitrile; Ethyl nitrile; Methane, cyano-; Methanecarbonitrile; Methyl cyanide; CH3CN; Acetonitril; Cyanure de methyl; USAF EK-488; Methylkyanid; NA 1648; NCI-C60822; Rcra waste number U003; UN 1648; Ethanonitrile", "C2H3N", 41.0519m, "75-05-8", "\n\n\n 6 5 0 0 0 1 V2000\n 0.4219 0.8958 0.5193 C 0 0 0 0 0 0 0 0 0\n 1.8612 0.9422 0.4897 C 0 0 0 0 0 0 0 0 0\n 3.0198 0.9796 0.4659 N 0 0 0 0 0 0 0 0 0\n 0.0000 1.7811 0.0257 H 0 0 0 0 0 0 0 0 0\n 0.0568 0.0000 0.0000 H 0 0 0 0 0 0 0 0 0\n 0.0607 0.8694 1.5558 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 5 1 0 0 0\n 1 6 1 0 0 0\n 2 3 3 0 0 0\nM END\n", "", ""), - new TestChemical("Benzen", "Benzol (nach der IUPAC Benzen) ist eine flüssige organische Verbindung mit einem charakteristischen aromatischen Geruch. Die Verbindung mit der Summenformel C6H6 ist ein aromatischer Kohlenwasserstoff und das einfachste und zugleich klassische Beispiel für die Aromatizität bestimmter Verbindungen. Benzol ist mischbar mit fast allen organischen Solventien, jedoch kaum mit Wasser. Als Lösungsmittel hat Benzol seine Bedeutung verloren, da es krebserregend ist. Als mutagenes Klastogen wirken Benzol bzw. dessen Metabolite als Gift, welches Chromosomenaberrationen hervorrufen kann.", "[6]Annulene; Benzol; Benzole; Coal naphtha; Cyclohexatriene; Phenyl hydride; Pyrobenzol; Pyrobenzole; Benzolene; Bicarburet of hydrogen; Carbon oil; Mineral naphtha; Motor benzol; Benzeen; Benzen; Benzin; Benzine; Benzolo; Fenzen; NCI-C55276; Phene; Rcra waste number U019; UN 1114; NSC 67315; 1,3,5-Cyclohexatriene", "C6H6", 78.1118m, "71-43-2", "\n\n\n 12 12 0 0 0 1 V2000\n 3.2883 3.3891 0.2345 C 0 0 0 0 0 0 0 0 0\n 1.9047 3.5333 0.2237 C 0 0 0 0 0 0 0 0 0\n 3.8560 2.1213 0.1612 C 0 0 0 0 0 0 0 0 0\n 1.0888 2.4099 0.1396 C 0 0 0 0 0 0 0 0 0\n 3.0401 0.9977 0.0771 C 0 0 0 0 0 0 0 0 0\n 1.6565 1.1421 0.0663 C 0 0 0 0 0 0 0 0 0\n 3.9303 4.2734 0.3007 H 0 0 0 0 0 0 0 0 0\n 1.4582 4.5312 0.2815 H 0 0 0 0 0 0 0 0 0\n 4.9448 2.0077 0.1699 H 0 0 0 0 0 0 0 0 0\n 0.0000 2.5234 0.1311 H 0 0 0 0 0 0 0 0 0\n 3.4870 0.0000 0.0197 H 0 0 0 0 0 0 0 0 0\n 1.0145 0.2578 0.0000 H 0 0 0 0 0 0 0 0 0\n 2 1 2 0 0 0\n 1 3 1 0 0 0\n 1 7 1 0 0 0\n 4 2 1 0 0 0\n 2 8 1 0 0 0\n 3 5 2 0 0 0\n 3 9 1 0 0 0\n 6 4 2 0 0 0\n 4 10 1 0 0 0\n 5 6 1 0 0 0\n 5 11 1 0 0 0\n 6 12 1 0 0 0\nM END\n", "", ""), - new TestChemical("Kohlentetrachlorid", "Kohlenstoffgruppe oder Kohlenstoff-Silicium-Gruppe bezeichnet die 4. Hauptgruppe („Tetrele“) (nach neuer Nummerierung der IUPAC Gruppe 14) des Periodensystems. Sie umfasst die Elemente Kohlenstoff (C), Silicium (Si), Germanium (Ge), Zinn (Sn) und Blei (Pb). Auch ein radioaktives Element, das Flerovium (Fl), ist vertreten.", "Methane, tetrachloro-; Benzinoform; Carbon chloride (CCl4); Carbona; Fasciolin; Flukoids; Freon 10; Necatorina; Perchloromethane; Tetrachlorocarbon; Tetrachloromethane; Tetrafinol; Tetraform; Tetrasol; Univerm; Vermoestricid; CCl4; Benzenoform; Carbon tet; Methane tetrachloride; Czterochlorek wegla; ENT 4,705; Halon 1040; Necatorine; R 10; Tetrachloorkoolstof; Tetrachloormetaan; Tetrachlorkohlenstoff, tetra; Tetrachlormethan; Tetrachlorure de carbone; Tetraclorometano; Tetracloruro di carbonio; Chlorid uhlicity; ENT 27164; Rcra waste number U211; UN 1846; Katharin; Seretin; Thawpit; NSC 97063; R 10 (Refrigerant)", "CCl4", 153.823m, "56-23-5", "\n\n\n 5 4 0 0 0 0 0 0 0 0 V2000\n 0.0000 0.0000 0.0000 C 0000000000000000000\n 1.0350 1.0350 1.0350 Cl 0000000000000000000\n -1.0350 -1.0350 1.0350 Cl 0000000000000000000\n -1.0350 1.0350 -1.0350 Cl 0000000000000000000\n 1.0350 -1.0350 -1.0350 Cl 0000000000000000000\n 1 2 1 0 0 0\n 1 3 1 0 0 0\n 1 4 1 0 0 0\n 1 5 1 0 0 0\nM END\n", "", ""), - new TestChemical("Chloroform", "Chloroform (systematische Bezeichnung Trichlormethan) ist ein chlorierter Kohlenwasserstoff mit der Summenformel CHCl3.", "Chloroform; Freon 20; Methane, trichloro-; R 20; Trichloroform; CHCl3; Formyl trichloride; Methane trichloride; Methenyl trichloride; Methyl trichloride; Chloroforme; Cloroformio; NCI-C02686; R 20 (refrigerant); Trichloormethaan; Trichlormethan; Triclorometano; Rcra waste number U044; UN 1888; NSC 77361; F 20", "CHCl3", 119.378m, "67-66-3", "\n\n\n 5 4 0 0 0 0 0 0 0 0 V2000\n 0.0000 0.0000 0.4548 C 0000000000000000000\n 0.0000 0.0000 1.5402 H 0000000000000000000\n 0.0000 1.7050 -0.0837 Cl 0000000000000000000\n 1.4766 -0.8525 -0.0837 Cl 0000000000000000000\n -1.4766 -0.8525 -0.0837 Cl 0000000000000000000\n 1 2 1 0 0 0\n 1 3 1 0 0 0\n 1 4 1 0 0 0\n 1 5 1 0 0 0\nM END\n", "", ""), - new TestChemical("Cyclohexan", "Cyclohexan (auch Hexahydrobenzol, Hexamethylen, Naphthen) ist eine farblose Flüssigkeit. Es ist ein Cycloalkan mit der Summenformel C6H12, das im Erdöl vorkommt und als Lösungsmittel und Grundstoff in der Synthese genutzt wird.", "Benzene, hexahydro-; Hexahydrobenzene; Hexamethylene; Hexanaphthene; Cicloesano; Cykloheksan; Rcra waste number U056; UN 1145; NSC 406835", "C6H12", 84.1595m, "110-82-7", "\n\n\n 18 18 0 0 0 0 0 0 0 0 V2000\n 0.0000 -1.4672 0.2293 C 0000000000000000000\n -1.2706 0.7336 0.2293 C 0000000000000000000\n 1.2706 0.7336 0.2293 C 0000000000000000000\n 0.0000 1.4672 -0.2293 C 0000000000000000000\n -1.2706 -0.7336 -0.2293 C 0000000000000000000\n 1.2706 -0.7336 -0.2293 C 0000000000000000000\n 0.0000 -1.5355 1.3276 H 0000000000000000000\n -1.3298 0.7677 1.3276 H 0000000000000000000\n 1.3298 0.7677 1.3276 H 0000000000000000000\n 0.0000 1.5355 -1.3276 H 0000000000000000000\n -1.3298 -0.7677 -1.3276 H 0000000000000000000\n 1.3298 -0.7677 -1.3276 H 0000000000000000000\n 0.0000 -2.4985 -0.1466 H 0000000000000000000\n -2.1638 1.2493 -0.1466 H 0000000000000000000\n 2.1638 1.2493 -0.1466 H 0000000000000000000\n 0.0000 2.4985 0.1466 H 0000000000000000000\n -2.1638 -1.2493 0.1466 H 0000000000000000000\n 2.1638 -1.2493 0.1466 H 0000000000000000000\n 1 5 1 0 0 0\n 1 6 1 0 0 0\n 1 7 1 0 0 0\n 1 13 1 0 0 0\n 2 4 1 0 0 0\n 2 5 1 0 0 0\n 2 8 1 0 0 0\n 2 14 1 0 0 0\n 3 4 1 0 0 0\n 3 6 1 0 0 0\n 3 9 1 0 0 0\n 3 15 1 0 0 0\n 4 10 1 0 0 0\n 4 16 1 0 0 0\n 5 11 1 0 0 0\n 5 17 1 0 0 0\n 6 12 1 0 0 0\n 6 18 1 0 0 0\nM END\n", "", ""), - new TestChemical("Cyclopenten", "Cyclopenten ist eine organische Verbindung mit der Summenformel C5H8. Sie besteht aus einem fünfgliedrigen, ungesättigten Ring, welcher eine Doppelbindung aufweist. In der homologen Reihe der Cycloalkene steht Cyclopenten zwischen Cyclobuten und Cyclohexen. Formal handelt es sich um ein einfach hydriertes Cyclopentadien beziehungsweise ein einfach dehydriertes Cyclopentan. Cyclopenten besitzt nur wenige Anwendungen.", "", "C5H8", 68.1170m, "142-29-0", "\n\n\n 13 13 0 0 0 1 V2000\n 0.7026 2.1487 1.2803 C 0 0 0 0 0 0 0 0 0\n 1.9143 3.0520 1.0028 C 0 0 0 0 0 0 0 0 0\n 3.1024 2.1512 1.1173 C 0 0 0 0 0 0 0 0 0\n 2.7505 0.8940 1.4098 C 0 0 0 0 0 0 0 0 0\n 1.2685 0.7440 1.5398 C 0 0 0 0 0 0 0 0 0\n 0.1217 2.5129 2.1461 H 0 0 0 0 0 0 0 0 0\n 0.0000 2.1470 0.4281 H 0 0 0 0 0 0 0 0 0\n 1.8684 3.5149 0.0000 H 0 0 0 0 0 0 0 0 0\n 1.9848 3.8877 1.7230 H 0 0 0 0 0 0 0 0 0\n 4.1143 2.5269 0.9699 H 0 0 0 0 0 0 0 0 0\n 3.4198 0.0455 1.5471 H 0 0 0 0 0 0 0 0 0\n 0.8848 0.0000 0.8178 H 0 0 0 0 0 0 0 0 0\n 0.9989 0.3651 2.5427 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 5 1 1 0 0 0\n 1 6 1 0 0 0\n 1 7 1 0 0 0\n 2 3 1 0 0 0\n 2 8 1 0 0 0\n 2 9 1 0 0 0\n 3 4 2 0 0 0\n 3 10 1 0 0 0\n 4 5 1 0 0 0\n 4 11 1 0 0 0\n 5 12 1 0 0 0\n 5 13 1 0 0 0\nM END\n", "", ""), - new TestChemical("Diethylether", "Diethylether ist der wichtigste Vertreter der organisch-chemischen Verbindungsklasse der Ether und wird deshalb häufig auch einfach als Ether (standardsprachlich und in der älteren wissenschaftlichen Literatur Äther) bezeichnet. Aufgrund der Herstellung aus Ethanol und Schwefelsäure war die historische Bezeichnung Schwefeläther, obwohl Diethylether keinen Schwefel enthält.", "Ethane, 1,1'-oxybis-; Anaesthetic ether; Anesthesia ether; Anesthetic ether; Diethyl ether; Diethyl oxide; Ethoxyethane; Pronarcol; Solvent ether; 1,1'-Oxybisethane; (C2H5)2O; Aether; Diaethylaether; Dwuetylowy eter; Etere etilico; Ether ethylique; Ether, ethyl; Ethyl ether, tech.; Ethyl oxide; Oxyde d'ethyle; Rcra waste number U117; UN 1155; 3-Oxapentane; Ether; Ethyl ether anhydrous A.C.S.; Sulfuric ether; NSC 100036", "C4H10O", 74.1216m, "60-29-7", "\n\n\n 15 14 0 0 0 1 V2000\n 0.9744 2.7710 2.3574 C 0 0 0 0 0 0 0 0 0\n 0.9176 4.1589 1.7420 C 0 0 0 0 0 0 0 0 0\n 1.8106 1.8731 1.6596 O 0 0 0 0 0 0 0 0 0\n 3.2030 2.0811 1.8614 C 0 0 0 0 0 0 0 0 0\n 3.9340 1.0107 1.0734 C 0 0 0 0 0 0 0 0 0\n 0.0000 2.2572 2.2827 H 0 0 0 0 0 0 0 0 0\n 1.2519 2.8230 3.4284 H 0 0 0 0 0 0 0 0 0\n 0.1388 4.7597 2.2281 H 0 0 0 0 0 0 0 0 0\n 0.6868 4.1218 0.6694 H 0 0 0 0 0 0 0 0 0\n 1.8666 4.7007 1.8520 H 0 0 0 0 0 0 0 0 0\n 3.4420 2.0182 2.9412 H 0 0 0 0 0 0 0 0 0\n 3.4890 3.0953 1.5205 H 0 0 0 0 0 0 0 0 0\n 5.0187 1.1245 1.1931 H 0 0 0 0 0 0 0 0 0\n 3.7104 1.0671 0.0000 H 0 0 0 0 0 0 0 0 0\n 3.6667 0.0000 1.4086 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 3 1 0 0 0\n 1 6 1 0 0 0\n 1 7 1 0 0 0\n 2 8 1 0 0 0\n 2 9 1 0 0 0\n 2 10 1 0 0 0\n 3 4 1 0 0 0\n 4 5 1 0 0 0\n 4 11 1 0 0 0\n 4 12 1 0 0 0\n 5 13 1 0 0 0\n 5 14 1 0 0 0\n 5 15 1 0 0 0\nM END\n", "", ""), - new TestChemical("Ethanol", "Ethanol (häufige Trivialnamen: Äthanol, Ethylalkohol, Alkohol) ist ein aliphatischer, einwertiger Alkohol mit der Summenformel C2H6O. Die reine Substanz ist eine bei Raumtemperatur farblose, leicht entzündliche Flüssigkeit mit einem brennenden Geschmack und einem charakteristischen, würzigen (süßlichen) Geruch. Die als Lebergift eingestufte Droge wird bei der Herstellung von Genussmitteln und alkoholischen Getränken wie Wein, Bier und Spirituosen aus kohlehydrathaltigem Material durch eine von Hefen ausgelöste Gärung in relativ großen Mengen produziert.", "Ethyl alcohol; Alcohol; Alcohol anhydrous; Algrain; Anhydrol; Denatured ethanol; Ethyl hydrate; Ethyl hydroxide; Jaysol; Jaysol S; Methylcarbinol; SD Alchol 23-hydrogen; Tecsol; C2H5OH; Absolute ethanol; Cologne spirit; Fermentation alcohol; Grain alcohol; Molasses alcohol; Potato alcohol; Aethanol; Aethylalkohol; Alcohol, dehydrated; Alcool ethylique; Alcool etilico; Alkohol; Cologne spirits; Denatured alcohol CD-10; Denatured alcohol CD-5; Denatured alcohol CD-5a; Denatured alcohol SD-1; Denatured alcohol SD-13a; Denatured alcohol SD-17; Denatured alcohol SD-23a; Denatured alcohol SD-28; Denatured alcohol SD-3a; Denatured alcohol SD-30; Denatured alcohol SD-39b; Denatured alcohol SD-39c; Denatured alcohol SD-40m; Etanolo; Ethanol 200 proof; Ethyl alc; Etylowy alkohol; EtOH; NCI-C03134; Spirits of wine; Spirt; Alkoholu etylowego; Ethyl alcohol anhydrous; SD alcohol 23-hydrogen; UN 1170; Tecsol C; Alcare Hand Degermer; Absolute alcohol; Denatured alcohol; Ethanol, silent spirit; Ethylol; Punctilious ethyl alcohol; SD 3A", "C2H6O", 46.0684m, "64-17-5", "\n\n\n 9 8 0 0 0 0 0 0 0 0999 V2000\n 1.0195 0.8856 0.9752 C 0 0 0 0 0\n 1.8780 1.9882 1.5739 C 0 0 0 0 0\n 3.1989 1.4758 1.7291 O 0 0 0 0 0\n -0.0045 1.2392 0.8098 H 0 0 0 0 0\n 0.9875 0.0188 1.6438 H 0 0 0 0 0\n 1.4360 0.5612 0.0153 H 0 0 0 0 0\n 1.8717 2.8699 0.9114 H 0 0 0 0 0\n 1.4594 2.3055 2.5439 H 0 0 0 0 0\n 3.7472 2.1776 2.1115 H 0 0 0 0 0\n 6 1 1 0 0 0\n 4 1 1 0 0 0\n 1 2 1 0 0 0\n 1 5 1 0 0 0\n 7 2 1 0 0 0\n 2 3 1 0 0 0\n 2 8 1 0 0 0\n 3 9 1 0 0 0\nM END\n", "", ""), - new TestChemical("Essigsäureethylester", "Essigsäureethylester, auch Ethylacetat oder Essigester, ist eine chemische Verbindung aus der Gruppe der Carbonsäureester. Es ist der Ester von Essigsäure und Ethanol. Die farblose Flüssigkeit ist ein charakteristisch nach Klebstoff riechendes Lösungsmittel, das in der chemischen Industrie und in Laboratorien oft verwendet wird.", "Acetic acid, ethyl ester; Acetic ether; Acetidin; Acetoxyethane; Ethyl acetic ester; Ethyl ethanoate; Vinegar naphtha; CH3COOC2H5; Aethylacetat; Essigester; Ethyle (acetate d'); Etile (acetato di); Ethylacetaat; Ethylester kyseliny octove; Rcra waste number U112; UN 1173; Ethyl ester of acetic acid; 1-Acetoxyethane; NSC 70930; ac. acetic ethyl ester", "C4H8O2", 88.1051m, "141-78-6", "\n\n\n 14 13 0 0 0 0 0 0 0 0999 V2000\n 3.4731 2.2227 2.4832 O 0 0 0 0 0\n 2.1749 2.8339 2.4803 C 0 0 0 0 0\n 2.2477 4.1592 1.7469 C 0 0 0 0 0\n 3.7187 1.3350 1.4827 C 0 0 0 0 0\n 5.1346 0.8483 1.5545 C 0 0 0 0 0\n 2.9285 1.0057 0.6092 O 0 0 0 0 0\n 1.4145 2.1783 2.0421 H 0 0 0 0 0\n 1.8932 3.0019 3.5247 H 0 0 0 0 0\n 1.2769 4.6623 1.7528 H 0 0 0 0 0\n 2.9878 4.8162 2.2162 H 0 0 0 0 0\n 2.5643 4.0145 0.7089 H 0 0 0 0 0\n 5.1634 -0.2191 1.3199 H 0 0 0 0 0\n 5.7451 1.4089 0.8424 H 0 0 0 0 0\n 5.5400 0.9802 2.5620 H 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 2 3 1 0 0 0\n 2 7 1 0 0 0\n 2 8 1 0 0 0\n 3 9 1 0 0 0\n 3 10 1 0 0 0\n 3 11 1 0 0 0\n 4 5 1 0 0 0\n 4 6 2 0 0 0\n 5 12 1 0 0 0\n 5 13 1 0 0 0\n 5 14 1 0 0 0\nM END\n", "", ""), - new TestChemical("Ethylpropylether", "Ethylpropylether", "Ether, ethyl propyl; Ethyl n-propyl ether; Ethyl propyl ether; Propyl ethyl ether; 1-Ethoxypropane; n-C3H7OC2H5; UN 2615", "C5H12O", 88.1482m, "628-32-0", "\n\n\n 18 17 0 0 0 1 V2000\n 3.1079 1.7452 1.0645 O 0 0 0 0 0 0 0 0 0\n 4.4441 1.2694 1.1114 C 0 0 0 0 0 0 0 0 0\n 5.3490 2.4893 1.2632 C 0 0 0 0 0 0 0 0 0\n 2.1504 0.7063 0.9251 C 0 0 0 0 0 0 0 0 0\n 6.8002 2.0698 1.3208 C 0 0 0 0 0 0 0 0 0\n 0.7827 1.3612 0.8832 C 0 0 0 0 0 0 0 0 0\n 4.6830 0.7077 0.1862 H 0 0 0 0 0 0 0 0 0\n 4.5718 0.5698 1.9616 H 0 0 0 0 0 0 0 0 0\n 5.0789 3.0544 2.1773 H 0 0 0 0 0 0 0 0 0\n 5.1881 3.1923 0.4219 H 0 0 0 0 0 0 0 0 0\n 2.2307 0.0000 1.7749 H 0 0 0 0 0 0 0 0 0\n 2.3473 0.1294 0.0000 H 0 0 0 0 0 0 0 0 0\n 7.4601 2.9403 1.4280 H 0 0 0 0 0 0 0 0 0\n 7.1058 1.5371 0.4105 H 0 0 0 0 0 0 0 0 0\n 6.9969 1.4029 2.1707 H 0 0 0 0 0 0 0 0 0\n 0.0000 0.5997 0.7766 H 0 0 0 0 0 0 0 0 0\n 0.6863 2.0571 0.0395 H 0 0 0 0 0 0 0 0 0\n 0.5712 1.9289 1.7988 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 2 3 1 0 0 0\n 2 7 1 0 0 0\n 2 8 1 0 0 0\n 3 5 1 0 0 0\n 3 9 1 0 0 0\n 3 10 1 0 0 0\n 4 6 1 0 0 0\n 4 11 1 0 0 0\n 4 12 1 0 0 0\n 5 13 1 0 0 0\n 5 14 1 0 0 0\n 5 15 1 0 0 0\n 6 16 1 0 0 0\n 6 17 1 0 0 0\n 6 18 1 0 0 0\nM END\n", "", ""), - new TestChemical("Essigsäureisopropylester", "Essigsäureisopropylester ist eine chemische Verbindung aus der Gruppe der Carbonsäureester. Es ist eine farblose, leichtentzündliche und flüchtige Flüssigkeit.", "Acetic acid, 1-methylethyl ester; Acetic acid, isopropyl ester; 2-Acetoxypropane; 2-Propyl acetate; CH3COOCH(CH3)2; Acetate d'isopropyle; Isopropile(acetato di); Isopropyl ethanoate; Isopropyl (acetate d'); Isopropylacetaat; Isopropylacetat; Isopropylester kyseliny octove; UN 1220; Isopropyl ester of acetic acid; sec-Propyl acetate; Acetic acid, 2-propyl ester; 1-Methylethyl acetate; NSC 9295", "C5H10O2", 102.1317m, "108-21-4", "\n\n\n 17 16 0 0 0 1 V2000\n 3.1938 1.0106 1.8174 O 0 0 0 0 0 0 0 0 0\n 2.0014 1.7632 2.1082 C 0 0 0 0 0 0 0 0 0\n 4.4164 1.5950 1.9641 C 0 0 0 0 0 0 0 0 0\n 0.9499 0.7230 2.4807 C 0 0 0 0 0 0 0 0 0\n 1.5706 2.5640 0.8846 C 0 0 0 0 0 0 0 0 0\n 5.5443 0.6574 1.6359 C 0 0 0 0 0 0 0 0 0\n 4.5054 2.7501 2.3330 O 0 0 0 0 0 0 0 0 0\n 2.1755 2.4482 2.9734 H 0 0 0 0 0 0 0 0 0\n 1.2515 0.1324 3.3552 H 0 0 0 0 0 0 0 0 0\n 0.7564 0.0192 1.6598 H 0 0 0 0 0 0 0 0 0\n 0.0000 1.2159 2.7240 H 0 0 0 0 0 0 0 0 0\n 1.4415 1.9261 0.0000 H 0 0 0 0 0 0 0 0 0\n 0.6116 3.0626 1.0738 H 0 0 0 0 0 0 0 0 0\n 2.3036 3.3399 0.6292 H 0 0 0 0 0 0 0 0 0\n 5.3046 0.0000 0.7896 H 0 0 0 0 0 0 0 0 0\n 5.7670 0.0184 2.5009 H 0 0 0 0 0 0 0 0 0\n 6.4576 1.2122 1.3851 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 3 1 0 0 0\n 2 4 1 0 0 0\n 2 5 1 0 0 0\n 2 8 1 0 0 0\n 3 6 1 0 0 0\n 3 7 2 0 0 0\n 4 9 1 0 0 0\n 4 10 1 0 0 0\n 4 11 1 0 0 0\n 5 12 1 0 0 0\n 5 13 1 0 0 0\n 5 14 1 0 0 0\n 6 15 1 0 0 0\n 6 16 1 0 0 0\n 6 17 1 0 0 0\nM END\n", "", ""), - new TestChemical("Methan", "Methan ist eine chemische Verbindung aus der Gruppe der Alkane mit der Summenformel CH4. Das farb- und geruchlose, brennbare Gas kommt in der Natur vor und ist ein Hauptbestandteil von Erdgas. Es dient als Heizgas und ist in der chemischen Industrie als Ausgangsprodukt für technische Synthesen von großer Bedeutung.", "Marsh gas; Methyl hydride; CH4; Fire Damp; R 50; Biogas; R 50 (refrigerant)", "CH4", 16.0425m, "74-82-8", "\n\n\n 5 4 0 0 0 1 V2000\n 1.0582 0.9353 0.8103 C 0 0 0 0 0 0 0 0 0\n 1.4145 1.5662 0.0000 H 0 0 0 0 0 0 0 0 0\n 1.2065 1.4452 1.7588 H 0 0 0 0 0 0 0 0 0\n 0.0000 0.7294 0.6710 H 0 0 0 0 0 0 0 0 0\n 1.6121 0.0000 0.8114 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 3 1 0 0 0\n 1 4 1 0 0 0\n 1 5 1 0 0 0\nM END\n", "", ""), - new TestChemical("Methylalkohol", "Methanol, auch Methylalkohol, ist eine organische chemische Verbindung mit der Summenformel CH4O (Halbstrukturformel: CH3OH) und der einfachste Vertreter aus der Stoffgruppe der Alkohole. Unter Normalbedingungen ist Methanol eine klare, farblose, entzündliche und leicht flüchtige Flüssigkeit mit alkoholischem Geruch.", "Methanol; Carbinol; Methyl hydroxide; Methylol; Monohydroxymethane; Wood alcohol; CH3OH; Colonial spirit; Columbian spirit; Hydroxymethane; Wood naphtha; Alcool methylique; Alcool metilico; Columbian spirits; Metanolo; Methylalkohol; Metylowy alkohol; Pyroxylic spirit; Wood spirit; Rcra waste number U154; UN 1230; Pyro alcohol; Spirit of wood; Bieleski's solution; NSC 85232", "CH4O", 32.0419m, "67-56-1", "\n\n\n 6 5 0 0 0 1 V2000\n 0.2453 0.8386 1.6056 H 0 0 0 0 0 0 0 0 0\n 0.6776 0.9803 0.6074 C 0 0 0 0 0 0 0 0 0\n 0.0000 1.5869 0.0000 H 0 0 0 0 0 0 0 0 0\n 0.8133 0.0000 0.1338 H 0 0 0 0 0 0 0 0 0\n 1.8631 1.7142 0.6464 O 0 0 0 0 0 0 0 0 0\n 2.4856 1.2216 1.1660 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 2 3 1 0 0 0\n 2 4 1 0 0 0\n 2 5 1 0 0 0\n 5 6 1 0 0 0\nM END\n", "", ""), - new TestChemical("2-Butanone", "Butanon ist neben Aceton eines der wichtigsten industriell genutzten Ketone. Es ist eine farblose, leicht bewegliche Flüssigkeit mit einem typischen Geruch und wird allgemein als Methylethylketon (MEK) bezeichnet.", "Butan-2-one; Butanone; Ethyl methyl ketone; Ketone, methyl ethyl; Methyl ethyl ketone; MEK; C2H5COCH3; Acetone, methyl-; Aethylmethylketon; 3-Butanone; Butanone 2; Ethyl methyl cetone; Ethylmethylketon; Ketone, ethyl methyl; Meetco; Methyl acetone; Metiletilchetone; Metyloetyloketon; Rcra waste number U159; UN 1193; 2-Oxobutane; 2-Butanal; 2-butanone (MEK; methyl ethyl ketone); 2-butanone (MEK)", "C4H8O", 72.1057m, "78-93-3", "\n\n\n 13 12 0 0 0 1 V2000\n 1.7893 1.4200 2.8269 C 0 0 0 0 0 0 0 0 0\n 0.8451 2.5827 2.6164 C 0 0 0 0 0 0 0 0 0\n 2.3705 0.9490 1.5071 C 0 0 0 0 0 0 0 0 0\n 3.5557 1.7224 0.9969 C 0 0 0 0 0 0 0 0 0\n 1.9075 0.0000 0.9038 O 0 0 0 0 0 0 0 0 0\n 2.6155 1.7113 3.5049 H 0 0 0 0 0 0 0 0 0\n 1.2589 0.5906 3.3355 H 0 0 0 0 0 0 0 0 0\n 0.0000 2.3072 1.9722 H 0 0 0 0 0 0 0 0 0\n 0.4300 2.9289 3.5716 H 0 0 0 0 0 0 0 0 0\n 1.3499 3.4390 2.1488 H 0 0 0 0 0 0 0 0 0\n 3.8802 1.3974 0.0000 H 0 0 0 0 0 0 0 0 0\n 3.3200 2.7937 0.9410 H 0 0 0 0 0 0 0 0 0\n 4.4074 1.6050 1.6799 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 3 1 0 0 0\n 1 6 1 0 0 0\n 1 7 1 0 0 0\n 2 8 1 0 0 0\n 2 9 1 0 0 0\n 2 10 1 0 0 0\n 3 4 1 0 0 0\n 3 5 2 0 0 0\n 4 11 1 0 0 0\n 4 12 1 0 0 0\n 4 13 1 0 0 0\nM END\n", "", ""), - new TestChemical("Butan", "Die Butane sind eine Stoffgruppe innerhalb der Alkane, die die Summenformel C4H10 aufweisen. Sie besteht aus den beiden Vertretern n-Butan und iso-Butan, die zueinander isomer sind. Beide Butane sind farblose, brennbare, leicht zu verflüssigende Gase („Flüssiggase“), die sich kaum in Wasser, aber gut in Ethanol und Ether lösen.", "n-Butane; Diethyl; Freon 600; Liquefied petroleum gas; LPG; n-C4H10; Butanen; Butani; Methylethylmethane; UN 1011; A 21; HC 600; HC 600 (hydrocarbon); R 600; R 600 (alkane)", "C4H10", 58.1222m, "106-97-8", "\n\n\n 14 13 0 0 0 1 V2000\n 3.5864 1.1360 0.9321 C 0 0 0 0 0 0 0 0 0\n 2.5594 0.8276 1.9979 C 0 0 0 0 0 0 0 0 0\n 1.7180 2.0488 2.3336 C 0 0 0 0 0 0 0 0 0\n 0.6912 1.7404 3.3995 C 0 0 0 0 0 0 0 0 0\n 3.1136 1.4730 0.0000 H 0 0 0 0 0 0 0 0 0\n 4.1901 0.2515 0.6922 H 0 0 0 0 0 0 0 0 0\n 4.2769 1.9280 1.2514 H 0 0 0 0 0 0 0 0 0\n 3.0625 0.4552 2.9124 H 0 0 0 0 0 0 0 0 0\n 1.9030 0.0000 1.6631 H 0 0 0 0 0 0 0 0 0\n 1.2148 2.4207 1.4190 H 0 0 0 0 0 0 0 0 0\n 2.3747 2.8766 2.6678 H 0 0 0 0 0 0 0 0 0\n 0.0000 0.9495 3.0791 H 0 0 0 0 0 0 0 0 0\n 0.0880 2.6250 3.6404 H 0 0 0 0 0 0 0 0 0\n 1.1635 1.4017 4.3313 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 5 1 0 0 0\n 1 6 1 0 0 0\n 1 7 1 0 0 0\n 2 3 1 0 0 0\n 2 8 1 0 0 0\n 2 9 1 0 0 0\n 3 4 1 0 0 0\n 3 10 1 0 0 0\n 3 11 1 0 0 0\n 4 12 1 0 0 0\n 4 13 1 0 0 0\n 4 14 1 0 0 0\nM END\n", "", ""), - new TestChemical("Butanol", "1-Butanol (auch n-Butanol oder nach IUPAC Butan-1-ol) ist eine chemische Verbindung aus der Gruppe der Alkanole. Der primäre Alkohol leitet sich vom aliphatischen Kohlenwasserstoff n-Butan ab.", "Butyl alcohol; n-Butan-1-ol; n-Butanol; n-Butyl alcohol; Butyl hydroxide; CCS 203; Hemostyp; Methylolpropane; Propylcarbinol; n-C4H9OH; Butanol; Butan-1-ol; 1-Hydroxybutane; Alcool butylique; Butanolo; Butylowy alkohol; Butyric alcohol; Propylmethanol; Butanolen; 1-Butyl alcohol; Rcra waste number U031; Butanol-1; NSC 62782", "C4H10O", 74.1216m, "71-36-3", "\n\n\n 15 14 0 0 0 1 V2000\n 2.9651 2.0464 2.4042 C 0 0 0 0 0 0 0 0 0\n 2.3281 2.7103 1.1934 C 0 0 0 0 0 0 0 0 0\n 0.8652 2.3158 0.9860 C 0 0 0 0 0 0 0 0 0\n 3.3109 0.5952 2.1559 C 0 0 0 0 0 0 0 0 0\n 0.0000 2.7697 1.9922 O 0 0 0 0 0 0 0 0 0\n 3.8836 2.6000 2.6826 H 0 0 0 0 0 0 0 0 0\n 2.2886 2.1307 3.2779 H 0 0 0 0 0 0 0 0 0\n 2.8987 2.4514 0.2796 H 0 0 0 0 0 0 0 0 0\n 2.4074 3.8120 1.2923 H 0 0 0 0 0 0 0 0 0\n 0.5089 2.6739 0.0000 H 0 0 0 0 0 0 0 0 0\n 0.7244 1.2206 1.0177 H 0 0 0 0 0 0 0 0 0\n 2.4210 0.0000 1.9086 H 0 0 0 0 0 0 0 0 0\n 3.7712 0.1403 3.0423 H 0 0 0 0 0 0 0 0 0\n 4.0193 0.4827 1.3240 H 0 0 0 0 0 0 0 0 0\n 0.1229 3.7075 2.0732 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 6 1 0 0 0\n 1 7 1 0 0 0\n 2 3 1 0 0 0\n 2 8 1 0 0 0\n 2 9 1 0 0 0\n 3 5 1 0 0 0\n 3 10 1 0 0 0\n 3 11 1 0 0 0\n 4 12 1 0 0 0\n 4 13 1 0 0 0\n 4 14 1 0 0 0\n 5 15 1 0 0 0\nM END\n", "", ""), - new TestChemical("Heptan", "Heptane sind zu den Alkanen zählende Kohlenwasserstoffe mit der Summenformel C7H16. Es existieren neun Konstitutionsisomere", "n-Heptane; Dipropylmethane; Heptyl hydride; Skellysolve C; n-C7H16; Eptani; Heptan; Heptanen; Gettysolve-C; NSC 62784", "C7H16", 100.2019m, "142-82-5", "\n\n\n 23 22 0 0 0 1 V2000\n 4.9119 1.1117 1.6160 C 0 0 0 0 0 0 0 0 0\n 3.6978 1.8855 1.1285 C 0 0 0 0 0 0 0 0 0\n 2.4040 1.2047 1.5468 C 0 0 0 0 0 0 0 0 0\n 6.2065 1.7895 1.1965 C 0 0 0 0 0 0 0 0 0\n 1.1708 1.9589 1.0725 C 0 0 0 0 0 0 0 0 0\n 7.4159 1.0214 1.6802 C 0 0 0 0 0 0 0 0 0\n 0.9041 3.2087 1.8814 C 0 0 0 0 0 0 0 0 0\n 4.8843 0.0753 1.2230 H 0 0 0 0 0 0 0 0 0\n 4.8794 1.0110 2.7196 H 0 0 0 0 0 0 0 0 0\n 3.7266 2.9218 1.5233 H 0 0 0 0 0 0 0 0 0\n 3.7322 1.9883 0.0250 H 0 0 0 0 0 0 0 0 0\n 2.3839 0.1736 1.1402 H 0 0 0 0 0 0 0 0 0\n 2.3759 1.0904 2.6492 H 0 0 0 0 0 0 0 0 0\n 6.2340 2.8252 1.5899 H 0 0 0 0 0 0 0 0 0\n 6.2388 1.8907 0.0934 H 0 0 0 0 0 0 0 0 0\n 1.2758 2.2178 0.0000 H 0 0 0 0 0 0 0 0 0\n 0.2900 1.2893 1.1316 H 0 0 0 0 0 0 0 0 0\n 7.4323 0.9394 2.7752 H 0 0 0 0 0 0 0 0 0\n 8.3484 1.5122 1.3742 H 0 0 0 0 0 0 0 0 0\n 7.4348 0.0000 1.2773 H 0 0 0 0 0 0 0 0 0\n 1.7349 3.9244 1.8126 H 0 0 0 0 0 0 0 0 0\n 0.7607 2.9796 2.9459 H 0 0 0 0 0 0 0 0 0\n 0.0000 3.7234 1.5317 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 8 1 0 0 0\n 1 9 1 0 0 0\n 2 3 1 0 0 0\n 2 10 1 0 0 0\n 2 11 1 0 0 0\n 3 5 1 0 0 0\n 3 12 1 0 0 0\n 3 13 1 0 0 0\n 4 6 1 0 0 0\n 4 14 1 0 0 0\n 4 15 1 0 0 0\n 5 7 1 0 0 0\n 5 16 1 0 0 0\n 5 17 1 0 0 0\n 6 18 1 0 0 0\n 6 19 1 0 0 0\n 6 20 1 0 0 0\n 7 21 1 0 0 0\n 7 22 1 0 0 0\n 7 23 1 0 0 0\nM END\n", "", ""), - new TestChemical("Hexan", "n-Hexan ist eine den Alkanen (gesättigte Kohlenwasserstoffe) zugehörige chemische Verbindung. Es ist eine farblose Flüssigkeit mit der Summenformel C6H14. Es ist das unverzweigte Isomer der fünf Hexanisomeren.", "n-Hexane; Skellysolve B; n-C6H14; Esani; Heksan; Hexanen; Hexyl hydride; Gettysolve-B; NCI-C60571; NSC 68472", "C6H14", 86.1754m, "110-54-3", "\n\n\n 20 19 0 0 0 1 V2000\n 3.0831 1.0831 1.9494 C 0 0 0 0 0 0 0 0 0\n 2.9735 1.4738 3.4178 C 0 0 0 0 0 0 0 0 0\n 3.7734 2.7246 3.7510 C 0 0 0 0 0 0 0 0 0\n 2.1289 1.8691 1.0623 C 0 0 0 0 0 0 0 0 0\n 5.2567 2.5130 3.5423 C 0 0 0 0 0 0 0 0 0\n 0.6830 1.5862 1.4060 C 0 0 0 0 0 0 0 0 0\n 2.8829 0.0000 1.8355 H 0 0 0 0 0 0 0 0 0\n 4.1330 1.2389 1.6114 H 0 0 0 0 0 0 0 0 0\n 1.9001 1.6293 3.6713 H 0 0 0 0 0 0 0 0 0\n 3.3194 0.6360 4.0538 H 0 0 0 0 0 0 0 0 0\n 3.4222 3.5715 3.1275 H 0 0 0 0 0 0 0 0 0\n 3.5761 3.0236 4.7988 H 0 0 0 0 0 0 0 0 0\n 2.3340 2.9542 1.1601 H 0 0 0 0 0 0 0 0 0\n 2.3218 1.6234 0.0000 H 0 0 0 0 0 0 0 0 0\n 5.8188 3.4487 3.6460 H 0 0 0 0 0 0 0 0 0\n 5.6708 1.7956 4.2625 H 0 0 0 0 0 0 0 0 0\n 5.4489 2.1109 2.5328 H 0 0 0 0 0 0 0 0 0\n 0.5108 1.7246 2.4871 H 0 0 0 0 0 0 0 0 0\n 0.0000 2.2530 0.8669 H 0 0 0 0 0 0 0 0 0\n 0.4010 0.5540 1.1613 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 7 1 0 0 0\n 1 8 1 0 0 0\n 2 3 1 0 0 0\n 2 9 1 0 0 0\n 2 10 1 0 0 0\n 3 5 1 0 0 0\n 3 11 1 0 0 0\n 3 12 1 0 0 0\n 4 6 1 0 0 0\n 4 13 1 0 0 0\n 4 14 1 0 0 0\n 5 15 1 0 0 0\n 5 16 1 0 0 0\n 5 17 1 0 0 0\n 6 18 1 0 0 0\n 6 19 1 0 0 0\n 6 20 1 0 0 0\nM END\n", "", ""), - new TestChemical("Octan", "n-Octan ist eine farblose Flüssigkeit, die zu den Alkanen zählt. In der Chemie wird es entsprechend den aktuellen Nomenklaturregeln als n-Octan geschrieben, in Deutschland wird jedoch oft – gerade im Zusammenhang mit der Oktanzahl – die veraltete Schreibweise Oktan bevorzugt. Es handelt sich um den unverzweigten Vertreter der 18 Isomere der Octane.", "n-Octane; n-C8H18; Oktan; Oktanen; Ottani; UN 1262", "C8H18", 114.2285m, "111-65-9", "\n\n\n 26 25 0 0 0 1 V2000\n 3.8144 2.3469 1.6808 C 0 0 0 0 0 0 0 0 0\n 2.8536 2.9952 2.6656 C 0 0 0 0 0 0 0 0 0\n 1.4042 2.8035 2.2422 C 0 0 0 0 0 0 0 0 0\n 5.2606 2.6768 2.0091 C 0 0 0 0 0 0 0 0 0\n 1.0820 3.5021 0.9282 C 0 0 0 0 0 0 0 0 0\n 6.2381 2.0228 1.0442 C 0 0 0 0 0 0 0 0 0\n 0.8478 4.9859 1.0983 C 0 0 0 0 0 0 0 0 0\n 6.4020 0.5402 1.2955 C 0 0 0 0 0 0 0 0 0\n 3.5588 2.6881 0.6508 H 0 0 0 0 0 0 0 0 0\n 3.6652 1.2480 1.6733 H 0 0 0 0 0 0 0 0 0\n 3.0793 4.0774 2.7579 H 0 0 0 0 0 0 0 0 0\n 3.0069 2.5713 3.6775 H 0 0 0 0 0 0 0 0 0\n 0.7310 3.1711 3.0415 H 0 0 0 0 0 0 0 0 0\n 1.1916 1.7202 2.1456 H 0 0 0 0 0 0 0 0 0\n 5.4911 2.3686 3.0487 H 0 0 0 0 0 0 0 0 0\n 5.4015 3.7762 1.9864 H 0 0 0 0 0 0 0 0 0\n 0.1878 3.0372 0.4697 H 0 0 0 0 0 0 0 0 0\n 1.9182 3.3248 0.2134 H 0 0 0 0 0 0 0 0 0\n 7.2242 2.5203 1.1318 H 0 0 0 0 0 0 0 0 0\n 5.9096 2.1958 0.0000 H 0 0 0 0 0 0 0 0 0\n 0.6299 5.4668 0.1362 H 0 0 0 0 0 0 0 0 0\n 0.0000 5.1885 1.7663 H 0 0 0 0 0 0 0 0 0\n 1.7255 5.4904 1.5249 H 0 0 0 0 0 0 0 0 0\n 5.4519 0.0000 1.1826 H 0 0 0 0 0 0 0 0 0\n 7.1172 0.0943 0.5924 H 0 0 0 0 0 0 0 0 0\n 6.7707 0.3401 2.3104 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 9 1 0 0 0\n 1 10 1 0 0 0\n 2 3 1 0 0 0\n 2 11 1 0 0 0\n 2 12 1 0 0 0\n 3 5 1 0 0 0\n 3 13 1 0 0 0\n 3 14 1 0 0 0\n 4 6 1 0 0 0\n 4 15 1 0 0 0\n 4 16 1 0 0 0\n 5 7 1 0 0 0\n 5 17 1 0 0 0\n 5 18 1 0 0 0\n 6 8 1 0 0 0\n 6 19 1 0 0 0\n 6 20 1 0 0 0\n 7 21 1 0 0 0\n 7 22 1 0 0 0\n 7 23 1 0 0 0\n 8 24 1 0 0 0\n 8 25 1 0 0 0\n 8 26 1 0 0 0\nM END\n", "", ""), - new TestChemical("Pentan", "Pentane sind Kohlenwasserstoffe mit der Summenformel C5H12 und zählen zu den Alkanen. Es existieren drei Konstitutionsisomere: n-Pentan, Isopentan und Neopentan.", "n-Pentane; Skellysolve A; n-C5H12; Pentan; Pentanen; Pentani; Amyl hydride; NSC 72415", "C5H12", 72.1488m, "109-66-0", "\n\n\n 17 16 0 0 0 1 V2000\n 3.7280 2.4135 2.8751 C 0 0 0 0 0 0 0 0 0\n 2.5997 1.4666 2.4988 C 0 0 0 0 0 0 0 0 0\n 1.8538 1.9510 1.2659 C 0 0 0 0 0 0 0 0 0\n 4.4698 1.9317 4.1013 C 0 0 0 0 0 0 0 0 0\n 0.7349 1.0066 0.8889 C 0 0 0 0 0 0 0 0 0\n 3.3234 3.4299 3.0526 H 0 0 0 0 0 0 0 0 0\n 4.4308 2.5184 2.0246 H 0 0 0 0 0 0 0 0 0\n 1.8961 1.3604 3.3490 H 0 0 0 0 0 0 0 0 0\n 3.0039 0.4497 2.3206 H 0 0 0 0 0 0 0 0 0\n 2.5589 2.0599 0.4178 H 0 0 0 0 0 0 0 0 0\n 1.4479 2.9664 1.4462 H 0 0 0 0 0 0 0 0 0\n 5.2821 2.6183 4.3713 H 0 0 0 0 0 0 0 0 0\n 3.8049 1.8514 4.9716 H 0 0 0 0 0 0 0 0 0\n 4.9169 0.9415 3.9412 H 0 0 0 0 0 0 0 0 0\n 0.0000 0.9059 1.6988 H 0 0 0 0 0 0 0 0 0\n 1.1125 0.0000 0.6654 H 0 0 0 0 0 0 0 0 0\n 0.1970 1.3600 0.0000 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 6 1 0 0 0\n 1 7 1 0 0 0\n 2 3 1 0 0 0\n 2 8 1 0 0 0\n 2 9 1 0 0 0\n 3 5 1 0 0 0\n 3 10 1 0 0 0\n 3 11 1 0 0 0\n 4 12 1 0 0 0\n 4 13 1 0 0 0\n 4 14 1 0 0 0\n 5 15 1 0 0 0\n 5 16 1 0 0 0\n 5 17 1 0 0 0\nM END\n", "", ""), - new TestChemical("1-Pentanol", "1-Pentanol (veraltet: Amylalkohol) ist eine organische chemische Verbindung und gehört zu den Alkoholen. 1-Pentanol ist Bestandteil der Fuselöle.", "Pentyl alcohol; n-Amyl alcohol; n-Butylcarbinol; n-Pentan-1-ol; n-Pentanol; n-Pentyl alcohol; Amyl alcohol; Amylol; Pentanol; 1-Pentyl alcohol; n-C5H11OH; Pentan-1-ol; Pentanol-1; Pentasol; n-Amylalkohol; Alcool amylique; Amyl alcohol, n-; Amyl alcohol, normal; Primary amyl alcohol; UN 1105; 1-Pentol; Primary-N-amyl alcohol; Butyl carbinol; NSC 5707", "C5H12O", 88.1482m, "71-41-0", "\n\n\n 18 17 0 0 0 1 V2000\n 2.0264 2.2581 1.9056 C 0 0 0 0 0 0 0 0 0\n 3.2462 1.6488 1.2359 C 0 0 0 0 0 0 0 0 0\n 4.4464 1.6425 2.1693 C 0 0 0 0 0 0 0 0 0\n 0.8116 2.2939 0.9786 C 0 0 0 0 0 0 0 0 0\n 5.6613 1.0380 1.5026 C 0 0 0 0 0 0 0 0 0\n 0.3196 1.0290 0.6256 O 0 0 0 0 0 0 0 0 0\n 2.2542 3.2923 2.2314 H 0 0 0 0 0 0 0 0 0\n 1.7856 1.7003 2.8338 H 0 0 0 0 0 0 0 0 0\n 3.0191 0.6161 0.9030 H 0 0 0 0 0 0 0 0 0\n 3.4892 2.2109 0.3117 H 0 0 0 0 0 0 0 0 0\n 4.6718 2.6757 2.5011 H 0 0 0 0 0 0 0 0 0\n 4.2030 1.0807 3.0931 H 0 0 0 0 0 0 0 0 0\n 1.0538 2.7449 0.0000 H 0 0 0 0 0 0 0 0 0\n 0.0000 2.8939 1.4351 H 0 0 0 0 0 0 0 0 0\n 5.4791 0.0000 1.1934 H 0 0 0 0 0 0 0 0 0\n 5.9510 1.5990 0.6041 H 0 0 0 0 0 0 0 0 0\n 6.5258 1.0323 2.1788 H 0 0 0 0 0 0 0 0 0\n 0.1634 0.5491 1.4297 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 7 1 0 0 0\n 1 8 1 0 0 0\n 2 3 1 0 0 0\n 2 9 1 0 0 0\n 2 10 1 0 0 0\n 3 5 1 0 0 0\n 3 11 1 0 0 0\n 3 12 1 0 0 0\n 4 6 1 0 0 0\n 4 13 1 0 0 0\n 4 14 1 0 0 0\n 5 15 1 0 0 0\n 5 16 1 0 0 0\n 5 17 1 0 0 0\n 6 18 1 0 0 0\nM END\n", "", ""), - new TestChemical("1-Propanol", "Propanole sind Alkohole mit drei Kohlenstoffatomen und einer Hydroxygruppe (–OH). Sie haben die allgemeine Summenformel C3H8O und eine molare Masse von 60,10 g/mol. Es gibt nur zwei Isomere.", "Propyl alcohol; n-Propan-1-ol; n-Propanol; n-Propyl alcohol; Ethylcarbinol; Optal; Osmosol extra; Propanol; Propylic alcohol; 1-Propyl alcohol; n-C3H7OH; 1-Hydroxypropane; Propanol-1; Propan-1-ol; n-Propyl alkohol; Alcool propilico; Alcool propylique; Propanole; Propanolen; Propanoli; Propylowy alkohol; UN 1274; Propylan-propyl alcohol; NSC 30300; Alcohol, propyl", "C3H8O", 60.0950m, "71-23-8", "\n\n\n 12 11 0 0 0 1 V2000\n 0.7713 1.5705 1.3838 C 0 0 0 0 0 0 0 0 0\n 2.1696 1.0226 1.0958 C 0 0 0 0 0 0 0 0 0\n 3.2631 1.9141 1.6398 C 0 0 0 0 0 0 0 0 0\n 0.3563 1.3950 2.7118 O 0 0 0 0 0 0 0 0 0\n 0.7013 2.6399 1.1017 H 0 0 0 0 0 0 0 0 0\n 0.0000 1.0213 0.8161 H 0 0 0 0 0 0 0 0 0\n 2.2872 0.9154 0.0000 H 0 0 0 0 0 0 0 0 0\n 2.2716 0.0000 1.5099 H 0 0 0 0 0 0 0 0 0\n 3.2248 1.9921 2.7352 H 0 0 0 0 0 0 0 0 0\n 3.1932 2.9340 1.2381 H 0 0 0 0 0 0 0 0 0\n 4.2564 1.5270 1.3793 H 0 0 0 0 0 0 0 0 0\n 1.0138 1.7939 3.2691 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 5 1 0 0 0\n 1 6 1 0 0 0\n 2 3 1 0 0 0\n 2 7 1 0 0 0\n 2 8 1 0 0 0\n 3 9 1 0 0 0\n 3 10 1 0 0 0\n 3 11 1 0 0 0\n 4 12 1 0 0 0\nM END\n", "", ""), - new TestChemical("Phenol", "Phenol (nach IUPAC: Benzenol, veraltet: Karbolsäure oder kurz Karbol) ist eine aromatische, organische Verbindung und besteht aus einer Phenylgruppe (–C6H5), an die eine Hydroxygruppe (–OH) gebunden ist. Der farblose, kristalline Feststoff ist eine wichtige Industriechemikalie und dient als Zwischenprodukt besonders zur Herstellung diverser Kunststoffe.", "Carbolic acid; Baker's P and S Liquid and Ointment; Benzenol; Hydroxybenzene; Izal; Monohydroxybenzene; Monophenol; Oxybenzene; Phenic acid; Phenyl alcohol; Phenyl hydrate; Phenyl hydroxide; Phenylic acid; Phenylic alcohol; PhOH; Benzene, hydroxy-; Acide carbolique; Baker's P & S liquid & Ointment; Fenol; Fenolo; NCI-C50124; Paoscle; Phenole; Carbolsaure; NA 2821; Phenol alcohol; Phenol, molten; Rcra waste number U188; UN 1671; UN 2312; UN 2821; Phenic alcohol; NSC 36808", "C6H6O", 94.1112m, "108-95-2", "\n\n\n 13 13 0 0 0 1 V2000\n 0.3792 2.3991 0.0767 O 0 0 0 0 0 0 0 0 0\n 1.7410 2.2635 0.0604 C 0 0 0 0 0 0 0 0 0\n 2.4822 3.4537 0.0746 C 0 0 0 0 0 0 0 0 0\n 2.3800 1.0169 0.0335 C 0 0 0 0 0 0 0 0 0\n 3.8685 3.3780 0.0622 C 0 0 0 0 0 0 0 0 0\n 3.7689 0.9695 0.0214 C 0 0 0 0 0 0 0 0 0\n 4.5128 2.1443 0.0358 C 0 0 0 0 0 0 0 0 0\n 0.0000 1.5292 0.0598 H 0 0 0 0 0 0 0 0 0\n 1.9740 4.4239 0.0950 H 0 0 0 0 0 0 0 0 0\n 1.8001 0.0870 0.0221 H 0 0 0 0 0 0 0 0 0\n 4.4597 4.2998 0.0734 H 0 0 0 0 0 0 0 0 0\n 4.2770 0.0000 0.0000 H 0 0 0 0 0 0 0 0 0\n 5.6063 2.0990 0.0278 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 8 1 0 0 0\n 2 3 2 0 0 0\n 4 2 1 0 0 0\n 3 5 1 0 0 0\n 3 9 1 0 0 0\n 6 4 2 0 0 0\n 4 10 1 0 0 0\n 5 7 2 0 0 0\n 5 11 1 0 0 0\n 7 6 1 0 0 0\n 6 12 1 0 0 0\n 7 13 1 0 0 0\nM END\n", "", ""), - new TestChemical("Propan", "Propan ist ein farbloses brennbares Gas und gehört zu den Kohlenwasserstoffen. Es steht in der homologen Reihe der Alkane an dritter Stelle.", "n-Propane; Dimethylmethane; Freon 290; Liquefied petroleum gas; LPG; Propyl hydride; R 290; C3H8; UN 1978; A-108; Hydrocarbon propellant A-108; HC 290", "C3H8", 44.0956m, "74-98-6", "\n\n\n 11 10 0 0 0 1 V2000\n 3.3461 1.6436 1.3326 C 0 0 0 0 0 0 0 0 0\n 2.0042 1.0740 0.9307 C 0 0 0 0 0 0 0 0 0\n 0.9734 1.2486 2.0232 C 0 0 0 0 0 0 0 0 0\n 3.7393 1.1540 2.2333 H 0 0 0 0 0 0 0 0 0\n 3.2805 2.7182 1.5491 H 0 0 0 0 0 0 0 0 0\n 4.0907 1.5138 0.5370 H 0 0 0 0 0 0 0 0 0\n 1.6524 1.5612 0.0000 H 0 0 0 0 0 0 0 0 0\n 2.1104 0.0000 0.6811 H 0 0 0 0 0 0 0 0 0\n 0.0000 0.8391 1.7246 H 0 0 0 0 0 0 0 0 0\n 0.8224 2.3078 2.2707 H 0 0 0 0 0 0 0 0 0\n 1.2746 0.7385 2.9479 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 5 1 0 0 0\n 1 6 1 0 0 0\n 2 3 1 0 0 0\n 2 7 1 0 0 0\n 2 8 1 0 0 0\n 3 9 1 0 0 0\n 3 10 1 0 0 0\n 3 11 1 0 0 0\nM END\n", "", ""), - new TestChemical("p-Xylen", "", "Benzene, 1,4-dimethyl-; p-Dimethylbenzene; p-Xylol; 1,4-Dimethylbenzene; 1,4-Xylene; p-Methyltoluene; para-Xylene; Chromar; Scintillar; 4-Methyltoluene; NSC 72419; 1,4-dimethyl-benzene ( p-xylene)", "C8H10", 106.1650m, "106-42-3", "\n\n\n 18 18 0 0 0 1 V2000\n 1.0336 0.8636 0.7240 C 0 0 0 0 0 0 0 0 0\n 1.8222 1.2612 1.9184 C 0 0 0 0 0 0 0 0 0\n 3.2175 1.2383 1.8736 C 0 0 0 0 0 0 0 0 0\n 1.1814 1.6695 3.0884 C 0 0 0 0 0 0 0 0 0\n 3.9602 1.6220 2.9826 C 0 0 0 0 0 0 0 0 0\n 1.9245 2.0535 4.1976 C 0 0 0 0 0 0 0 0 0\n 3.3192 2.0345 4.1521 C 0 0 0 0 0 0 0 0 0\n 4.1123 2.4606 5.3336 C 0 0 0 0 0 0 0 0 0\n 0.9917 1.6889 0.0000 H 0 0 0 0 0 0 0 0 0\n 0.0000 0.5972 0.9816 H 0 0 0 0 0 0 0 0 0\n 1.4818 0.0000 0.2149 H 0 0 0 0 0 0 0 0 0\n 3.7265 0.9144 0.9590 H 0 0 0 0 0 0 0 0 0\n 0.0866 1.6867 3.1337 H 0 0 0 0 0 0 0 0 0\n 5.0549 1.5997 2.9402 H 0 0 0 0 0 0 0 0 0\n 1.4139 2.3716 5.1134 H 0 0 0 0 0 0 0 0 0\n 5.0993 1.9796 5.3595 H 0 0 0 0 0 0 0 0 0\n 3.6010 2.2196 6.2750 H 0 0 0 0 0 0 0 0 0\n 4.2743 3.5470 5.3142 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 9 1 0 0 0\n 1 10 1 0 0 0\n 1 11 1 0 0 0\n 3 2 2 0 0 0\n 2 4 1 0 0 0\n 5 3 1 0 0 0\n 3 12 1 0 0 0\n 4 6 2 0 0 0\n 4 13 1 0 0 0\n 7 5 2 0 0 0\n 5 14 1 0 0 0\n 6 7 1 0 0 0\n 6 15 1 0 0 0\n 7 8 1 0 0 0\n 8 16 1 0 0 0\n 8 17 1 0 0 0\n 8 18 1 0 0 0\nM END\n", "", ""), - new TestChemical("Toluol", "Toluol, Trivialname nach IUPAC auch Toluen, Methylbenzol, Phenylmethan, nach IUPAC-Nomenklatur Methylbenzen genannt, ist eine farblose, charakteristisch riechende, flüchtige Flüssigkeit, die in vielen ihrer Eigenschaften dem Benzol ähnelt. Toluol ist ein aromatischer Kohlenwasserstoff, häufig ersetzt es als Lösungsmittel das giftige Benzol. Es ist unter anderem auch im Benzin enthalten.", "Benzene, methyl; Methacide; Methylbenzene; Methylbenzol; Phenylmethane; Antisal 1a; Toluol; Methane, phenyl-; NCI-C07272; Tolueen; Toluen; Toluolo; Rcra waste number U220; Tolu-sol; UN 1294; Dracyl; Monomethyl benzene; CP 25; NSC 406333; methylbenzene (toluene)", "C7H8", 92.1384m, "108-88-3", "\n\n\n 7 7 0 0 0 1 V2000\n 1.4722 1.7260 0.0000 C 0 0 0 0 0 0 0 0 0\n 0.9645 0.8630 0.0000 C 0 0 0 0 0 0 0 0 0\n 2.4874 1.7260 0.0000 C 0 0 0 0 0 0 0 0 0\n 2.9951 0.8630 0.0000 C 0 0 0 0 0 0 0 0 0\n 2.4874 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0\n 1.4722 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0\n 0.0000 0.8630 0.0000 C 0 0 0 0 0 0 0 0 0\n 1 2 2 0 0 0\n 3 1 1 0 0 0\n 2 6 1 0 0 0\n 2 7 1 0 0 0\n 4 3 2 0 0 0\n 5 4 1 0 0 0\n 6 5 2 0 0 0\nM END\n", "", ""), - new TestChemical("Wasser", "Wasser (H2O) ist eine chemische Verbindung aus den Elementen Sauerstoff (O) und Wasserstoff (H). Wasser ist als Flüssigkeit durchsichtig, weitgehend farb-, geruch- und geschmacklos. Wasser ist die einzige chemische Verbindung auf der Erde, die in der Natur als Flüssigkeit, als Festkörper und als Gas vorkommt. Die Bezeichnung Wasser wird dabei für den flüssigen Aggregatzustand verwendet. Im festen Zustand spricht man von Eis, im gasförmigen Zustand von Wasserdampf. Wasser ist Grundlage des Lebens auf der Erde.", "Water vapor; Distilled water; Ice; H2O; Dihydrogen oxide; steam; Tritiotope", "H2O", 18.0153m, "7732-18-5", "\n\n\n 3 2 0 0 0 0 0 0 0 0999 V2000\n -0.2308 -0.3260 0.0000 O 0 0 0 0 0\n 0.7373 -0.2766 0.0000 H 0 0 0 0 0\n -0.5064 0.6026 0.0000 H 0 0 0 0 0\n 1 2 1 0 0 0\n 1 3 1 0 0 0\nM END\n", "", ""), + new TestChemical("1,2-Dichloroethan", + "1,2-Dichlorethan (Ethylendichlorid, EDC) ist eine farblose, brennbare und giftige Flüssigkeit mit chloroformartigem Geruch. Diese chemische Verbindung gehört zu den Chlorkohlenwasserstoffen.", + "α,β-Dichloroethane; s-Dichloroethane; Brocide; Dutch liquid; Ethylene chloride; Ethylene dichloride; Freon 150; Glycol dichloride; 1,2-Bichloroethane; 1,2-Dichlorethane; 1,2-Dichloroethane; CH2ClCH2Cl; sym-Dichloroethane; Aethylenchlorid; Bichlorure D'ethylene; Borer sol; Chlorure D'ethylene; Cloruro di ethene; 1,2-DCE; Destruxol borer-sol; 1,2-Dichloorethaan; 1,2-Dichlor-aethan; Dichloremulsion; Di-chlor-mulsion; Dichloro-1,2-ethane; 1,2-Dicloroetano; Dutch oil; EDC; ENT 1,656; Ethane dichloride; Ethyleendichloride; 1,2-Ethylene dichloride; NCI-C00511; Rcra waste number U077; UN 1184; DCE; EDC (halocarbon); HCC 150; 1,2-dichloroethane (ethylene dichloride)", + "C2H4Cl2", 98.959m, "107-06-2", + "\n\n\n 8 7 0 0 0 1 V2000\n 2.3785 0.7551 0.8326 C 0 0 0 0 0 0 0 0 0\n 1.5134 1.8542 1.3880 C 0 0 0 0 0 0 0 0 0\n 0.0000 1.1383 1.9817 Cl 0 0 0 0 0 0 0 0 0\n 3.8896 1.4714 0.2337 Cl 0 0 0 0 0 0 0 0 0\n 1.8822 0.2254 0.0000 H 0 0 0 0 0 0 0 0 0\n 2.6233 0.0000 1.6007 H 0 0 0 0 0 0 0 0 0\n 1.2715 2.6111 0.6209 H 0 0 0 0 0 0 0 0 0\n 2.0083 2.3814 2.2230 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 5 1 0 0 0\n 1 6 1 0 0 0\n 2 3 1 0 0 0\n 2 7 1 0 0 0\n 2 8 1 0 0 0\nM END\n"), + new TestChemical("1-Buten", + "Butene (auch Butylene) sind eine Gruppe von vier isomeren Kohlenwasserstoffen mit der allgemeinen Summenformel C4H8, die über eine C–C-Doppelbindung verfügen. Sie zählen damit zu den Alkenen. Zwei der Isomere unterscheiden sich durch cis-trans-Isomerie. Butene sind unter Standardbedingungen farblose, brennbare Gase mit einer größeren Dichte als Luft. Unter Druck lassen sich die Isomere verflüssigen. Sie wirken in höheren Konzentrationen narkotisierend und erstickend. Mit Luft bilden sie explosive Gemische.", + "α-Butene; α-Butylene; But-1-ene; Butene-1; Ethylethylene; 1-Butylene; 1-C4H8", "C4H8", 56.1063m, "106-98-9", + "\n\n\n 12 11 0 0 0 1 V2000\n 1.3702 2.1763 1.6455 C 0 0 0 0 0 0 0 0 0\n 0.4451 2.8431 0.9657 C 0 0 0 0 0 0 0 0 0\n 1.9783 0.9053 1.1655 C 0 0 0 0 0 0 0 0 0\n 3.4886 0.9497 1.2530 C 0 0 0 0 0 0 0 0 0\n 1.7309 2.5402 2.6153 H 0 0 0 0 0 0 0 0 0\n 0.0000 3.7633 1.3313 H 0 0 0 0 0 0 0 0 0\n 0.0658 2.5199 0.0000 H 0 0 0 0 0 0 0 0 0\n 1.5835 0.0745 1.7845 H 0 0 0 0 0 0 0 0 0\n 1.6643 0.6803 0.1265 H 0 0 0 0 0 0 0 0 0\n 3.9327 0.0000 0.9290 H 0 0 0 0 0 0 0 0 0\n 3.9065 1.7417 0.6183 H 0 0 0 0 0 0 0 0 0\n 3.8311 1.1398 2.2789 H 0 0 0 0 0 0 0 0 0\n 1 2 2 0 0 0\n 1 3 1 0 0 0\n 1 5 1 0 0 0\n 2 6 1 0 0 0\n 2 7 1 0 0 0\n 3 4 1 0 0 0\n 3 8 1 0 0 0\n 3 9 1 0 0 0\n 4 10 1 0 0 0\n 4 11 1 0 0 0\n 4 12 1 0 0 0\nM END\n"), + new TestChemical("1-Hexen", + "Hexene (Betonung auf der zweiten Silbe) sind chemische Verbindungen aus der Gruppe der Alkene mit der Summenformel C6H12. Der Wortstamm Hex weist auf die sechs Kohlenstoffatome, die Endung en auf die Doppelbindung zwischen zwei der Kohlenstoffatome hin. Es existieren verschiedene Isomere, die sich in der Position der Doppelbindung und dem Vorhandensein bzw. der Lage einer Verzweigung der Kohlenstoffkette unterscheiden. Die in der Industrie am häufigsten eingesetzte Verbindung ist 1-Hexen, welche z. B. als Comonomer bei der Produktion von Polyethen eingesetzt wird.", + "Hexene-1; 1-n-Hexene; 1-C6H12; Butylethylene; Hexene; Hex-1-ene; UN 2370; Hexylene; Neodene 6 XHP; NSC 74121; Dialene 6", "C6H12", 84.1595m, + "592-41-6", + "\n\n\n 18 17 0 0 0 0 0 0 0 0 V2000\n -2.3439 -0.9761 0.0000 H 0000000000000000000\n -2.5829 -2.8063 0.0000 H 0000000000000000000\n -1.8943 -1.9654 0.0000 C 0000000000000000000\n -0.1940 -3.1757 0.0000 H 0000000000000000000\n -0.5736 -2.1523 0.0000 C 0000000000000000000\n 1.1457 -1.2481 0.8727 H 0000000000000000000\n 1.1457 -1.2481 -0.8727 H 0000000000000000000\n 0.4932 -1.0856 0.0000 C 0000000000000000000\n -0.6369 0.5346 -0.8794 H 0000000000000000000\n -0.6369 0.5346 0.8794 H 0000000000000000000\n 0.0000 0.3648 0.0000 C 0000000000000000000\n 1.7839 1.2154 0.8785 H 0000000000000000000\n 1.7839 1.2154 -0.8785 H 0000000000000000000\n 1.1455 1.3862 0.0000 C 0000000000000000000\n 0.0421 3.0486 -0.8848 H 0000000000000000000\n 0.0421 3.0486 0.8848 H 0000000000000000000\n 1.4941 3.5435 0.0000 H 0000000000000000000\n 0.6554 2.8379 0.0000 C 0000000000000000000\n 1 3 1 0 0 0\n 2 3 1 0 0 0\n 3 5 2 0 0 0\n 4 5 1 0 0 0\n 5 8 1 0 0 0\n 6 8 1 0 0 0\n 7 8 1 0 0 0\n 8 11 1 0 0 0\n 9 11 1 0 0 0\n 10 11 1 0 0 0\n 11 14 1 0 0 0\n 12 14 1 0 0 0\n 13 14 1 0 0 0\n 14 18 1 0 0 0\n 15 18 1 0 0 0\n 16 18 1 0 0 0\n 17 18 1 0 0 0\nM END\n"), + new TestChemical("2,3-Dimethylpentan", + "3-Methylhexan und 2,3-Dimethylpentan sind chirale Verbindungen, von denen zwei Enantiomere existieren; Chiralitätszentrum ist das C3, das als Substituenten Wasserstoff und je einen Methyl-, Ethyl- und Propyl- bzw. Isopropylrest trägt. Sie sind die einfachsten chiralen Alkane.", + "2,3-Dimethylpentane; 3,4-Dimethylpentane", "C7H16", 100.2019m, "565-59-3", + "\n\n\n 23 22 0 0 0 1 V2000\n 4.3751 2.6128 2.7529 C 0 0 0 0 0 0 0 0 0\n 3.2736 1.9976 1.8651 C 0 0 0 0 0 0 0 0 0\n 1.9284 2.6959 2.0896 C 0 0 0 0 0 0 0 0 0\n 3.1364 0.5066 2.1417 C 0 0 0 0 0 0 0 0 0\n 4.5445 4.1009 2.4849 C 0 0 0 0 0 0 0 0 0\n 5.7094 1.9144 2.5314 C 0 0 0 0 0 0 0 0 0\n 0.9524 2.4445 0.9624 C 0 0 0 0 0 0 0 0 0\n 4.0701 2.4735 3.8192 H 0 0 0 0 0 0 0 0 0\n 3.5772 2.1391 0.7982 H 0 0 0 0 0 0 0 0 0\n 1.4902 2.3691 3.0539 H 0 0 0 0 0 0 0 0 0\n 2.0935 3.7916 2.1977 H 0 0 0 0 0 0 0 0 0\n 2.7404 0.3139 3.1481 H 0 0 0 0 0 0 0 0 0\n 2.4601 0.0256 1.4233 H 0 0 0 0 0 0 0 0 0\n 4.1132 0.0000 2.0752 H 0 0 0 0 0 0 0 0 0\n 5.2661 4.5496 3.1789 H 0 0 0 0 0 0 0 0 0\n 4.9040 4.2910 1.4645 H 0 0 0 0 0 0 0 0 0\n 3.5891 4.6380 2.5995 H 0 0 0 0 0 0 0 0 0\n 6.1048 2.1059 1.5247 H 0 0 0 0 0 0 0 0 0\n 6.4620 2.2562 3.2527 H 0 0 0 0 0 0 0 0 0\n 5.6092 0.8222 2.6385 H 0 0 0 0 0 0 0 0 0\n 0.7285 1.3752 0.8458 H 0 0 0 0 0 0 0 0 0\n 0.0000 2.9597 1.1415 H 0 0 0 0 0 0 0 0 0\n 1.3427 2.8017 0.0000 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 5 1 0 0 0\n 1 6 1 0 0 0\n 1 8 1 0 0 0\n 2 3 1 0 0 0\n 2 4 1 0 0 0\n 2 9 1 0 0 0\n 3 7 1 0 0 0\n 3 10 1 0 0 0\n 3 11 1 0 0 0\n 4 12 1 0 0 0\n 4 13 1 0 0 0\n 4 14 1 0 0 0\n 5 15 1 0 0 0\n 5 16 1 0 0 0\n 5 17 1 0 0 0\n 6 18 1 0 0 0\n 6 19 1 0 0 0\n 6 20 1 0 0 0\n 7 21 1 0 0 0\n 7 22 1 0 0 0\n 7 23 1 0 0 0\nM END\n"), + new TestChemical("Essigsäure", + "Essigsäure (systematisch Ethansäure, lateinisch acidum aceticum) ist eine farblose, flüssige, ätzende und typisch riechende Carbonsäure der Zusammensetzung C2H4O2 (Halbstrukturformel CH3COOH). Als Lebensmittelzusatzstoff trägt sie die E-Nummer E 260. Wässrige Lösungen der Essigsäure werden trivial nur Essig und reine Essigsäure Eisessig genannt. Die Salze und Ester der Essigsäure heißen Acetate oder (systematisch) Ethanoate", + "Ethanoic acid; Ethylic acid; Glacial acetic acid; Methanecarboxylic acid; Vinegar acid; CH3COOH; Acetasol; Acide acetique; Acido acetico; Azijnzuur; Essigsaeure; Octowy kwas; Acetic acid, glacial; Kyselina octova; UN 2789; Aci-jel; Shotgun; Ethanoic acid monomer; NSC 132953", + "C2H4O2", 60.052m, "64-19-7", + "\n\n\n 8 7 0 0 0 0 0 0 0 0999 V2000\n 0.7724 0.9670 1.0069 C 0 0 0 0 0\n 2.0576 1.7541 0.9321 C 0 0 0 0 0\n 3.0833 0.9883 0.4751 O 0 0 0 0 0\n 2.1974 2.9214 1.2200 O 0 0 0 0 0\n 0.0114 1.5612 1.5141 H 0 0 0 0 0\n 0.4326 0.7231 -0.0064 H 0 0 0 0 0\n 0.9331 0.0221 1.5356 H 0 0 0 0 0\n 3.8606 1.5761 0.4447 H 0 0 0 0 0\n 6 1 1 0 0 0\n 2 1 1 0 0 0\n 1 5 1 0 0 0\n 1 7 1 0 0 0\n 3 2 1 0 0 0\n 2 4 2 0 0 0\n 8 3 1 0 0 0\nM END\n"), + new TestChemical( + "Aceton", + "Aceton [at͡səˈtoːn] (auch: Azeton) ist der Trivialname für die organisch-chemische Verbindung Propanon bzw. Dimethylketon. Aceton ist eine farblose Flüssigkeit und findet Verwendung als polares, aprotisches Lösungsmittel und als Ausgangsstoff für viele Synthesen der organischen Chemie. Es ist mit seinem Strukturmerkmal der Carbonylgruppe (>C=O), die zwei Methylgruppen trägt, das einfachste Keton.", + "2-Propanone; β-Ketopropane; Dimethyl ketone; Dimethylformaldehyde; Methyl ketone; Propanone; Pyroacetic ether; (CH3)2CO; Dimethylketal; Ketone propane; Ketone, dimethyl-; Chevron acetone; Rcra waste number U002; UN 1090; Sasetone; Propan-2-one; NSC 135802", + "C3H6O", 58.0791m, "67-64-1", + "\n\n\n 10 9 0 0 0 0 0 0 0 0 V2000\n 0.0000 0.0000 0.1857 C 0000000000000000000\n 0.0000 0.0000 1.4013 O 0000000000000000000\n 0.0000 1.2929 -0.6150 C 0000000000000000000\n 0.0000 -1.2929 -0.6150 C 0000000000000000000\n 0.0000 2.1487 0.0628 H 0000000000000000000\n 0.0000 -2.1487 0.0628 H 0000000000000000000\n 0.8810 1.3407 -1.2674 H 0000000000000000000\n -0.8810 1.3407 -1.2674 H 0000000000000000000\n -0.8810 -1.3407 -1.2674 H 0000000000000000000\n 0.8810 -1.3407 -1.2674 H 0000000000000000000\n 1 2 2 0 0 0\n 1 3 1 0 0 0\n 1 4 1 0 0 0\n 3 5 1 0 0 0\n 3 7 1 0 0 0\n 3 8 1 0 0 0\n 4 6 1 0 0 0\n 4 9 1 0 0 0\n 4 10 1 0 0 0\nM END\n"), + new TestChemical("Acetonitril", "Acetonitril ist ein organisches Lösungsmittel und gehört zur Stoffgruppe der Nitrile.", + "Cyanomethane; Ethanenitrile; Ethyl nitrile; Methane, cyano-; Methanecarbonitrile; Methyl cyanide; CH3CN; Acetonitril; Cyanure de methyl; USAF EK-488; Methylkyanid; NA 1648; NCI-C60822; Rcra waste number U003; UN 1648; Ethanonitrile", + "C2H3N", 41.0519m, "75-05-8", + "\n\n\n 6 5 0 0 0 1 V2000\n 0.4219 0.8958 0.5193 C 0 0 0 0 0 0 0 0 0\n 1.8612 0.9422 0.4897 C 0 0 0 0 0 0 0 0 0\n 3.0198 0.9796 0.4659 N 0 0 0 0 0 0 0 0 0\n 0.0000 1.7811 0.0257 H 0 0 0 0 0 0 0 0 0\n 0.0568 0.0000 0.0000 H 0 0 0 0 0 0 0 0 0\n 0.0607 0.8694 1.5558 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 5 1 0 0 0\n 1 6 1 0 0 0\n 2 3 3 0 0 0\nM END\n"), + new TestChemical( + "Benzen", + "Benzol (nach der IUPAC Benzen) ist eine flüssige organische Verbindung mit einem charakteristischen aromatischen Geruch. Die Verbindung mit der Summenformel C6H6 ist ein aromatischer Kohlenwasserstoff und das einfachste und zugleich klassische Beispiel für die Aromatizität bestimmter Verbindungen. Benzol ist mischbar mit fast allen organischen Solventien, jedoch kaum mit Wasser. Als Lösungsmittel hat Benzol seine Bedeutung verloren, da es krebserregend ist. Als mutagenes Klastogen wirken Benzol bzw. dessen Metabolite als Gift, welches Chromosomenaberrationen hervorrufen kann.", + "[6]Annulene; Benzol; Benzole; Coal naphtha; Cyclohexatriene; Phenyl hydride; Pyrobenzol; Pyrobenzole; Benzolene; Bicarburet of hydrogen; Carbon oil; Mineral naphtha; Motor benzol; Benzeen; Benzen; Benzin; Benzine; Benzolo; Fenzen; NCI-C55276; Phene; Rcra waste number U019; UN 1114; NSC 67315; 1,3,5-Cyclohexatriene", + "C6H6", 78.1118m, "71-43-2", + "\n\n\n 12 12 0 0 0 1 V2000\n 3.2883 3.3891 0.2345 C 0 0 0 0 0 0 0 0 0\n 1.9047 3.5333 0.2237 C 0 0 0 0 0 0 0 0 0\n 3.8560 2.1213 0.1612 C 0 0 0 0 0 0 0 0 0\n 1.0888 2.4099 0.1396 C 0 0 0 0 0 0 0 0 0\n 3.0401 0.9977 0.0771 C 0 0 0 0 0 0 0 0 0\n 1.6565 1.1421 0.0663 C 0 0 0 0 0 0 0 0 0\n 3.9303 4.2734 0.3007 H 0 0 0 0 0 0 0 0 0\n 1.4582 4.5312 0.2815 H 0 0 0 0 0 0 0 0 0\n 4.9448 2.0077 0.1699 H 0 0 0 0 0 0 0 0 0\n 0.0000 2.5234 0.1311 H 0 0 0 0 0 0 0 0 0\n 3.4870 0.0000 0.0197 H 0 0 0 0 0 0 0 0 0\n 1.0145 0.2578 0.0000 H 0 0 0 0 0 0 0 0 0\n 2 1 2 0 0 0\n 1 3 1 0 0 0\n 1 7 1 0 0 0\n 4 2 1 0 0 0\n 2 8 1 0 0 0\n 3 5 2 0 0 0\n 3 9 1 0 0 0\n 6 4 2 0 0 0\n 4 10 1 0 0 0\n 5 6 1 0 0 0\n 5 11 1 0 0 0\n 6 12 1 0 0 0\nM END\n"), + new TestChemical("Kohlentetrachlorid", + "Kohlenstoffgruppe oder Kohlenstoff-Silicium-Gruppe bezeichnet die 4. Hauptgruppe („Tetrele“) (nach neuer Nummerierung der IUPAC Gruppe 14) des Periodensystems. Sie umfasst die Elemente Kohlenstoff (C), Silicium (Si), Germanium (Ge), Zinn (Sn) und Blei (Pb). Auch ein radioaktives Element, das Flerovium (Fl), ist vertreten.", + "Methane, tetrachloro-; Benzinoform; Carbon chloride (CCl4); Carbona; Fasciolin; Flukoids; Freon 10; Necatorina; Perchloromethane; Tetrachlorocarbon; Tetrachloromethane; Tetrafinol; Tetraform; Tetrasol; Univerm; Vermoestricid; CCl4; Benzenoform; Carbon tet; Methane tetrachloride; Czterochlorek wegla; ENT 4,705; Halon 1040; Necatorine; R 10; Tetrachloorkoolstof; Tetrachloormetaan; Tetrachlorkohlenstoff, tetra; Tetrachlormethan; Tetrachlorure de carbone; Tetraclorometano; Tetracloruro di carbonio; Chlorid uhlicity; ENT 27164; Rcra waste number U211; UN 1846; Katharin; Seretin; Thawpit; NSC 97063; R 10 (Refrigerant)", + "CCl4", 153.823m, "56-23-5", + "\n\n\n 5 4 0 0 0 0 0 0 0 0 V2000\n 0.0000 0.0000 0.0000 C 0000000000000000000\n 1.0350 1.0350 1.0350 Cl 0000000000000000000\n -1.0350 -1.0350 1.0350 Cl 0000000000000000000\n -1.0350 1.0350 -1.0350 Cl 0000000000000000000\n 1.0350 -1.0350 -1.0350 Cl 0000000000000000000\n 1 2 1 0 0 0\n 1 3 1 0 0 0\n 1 4 1 0 0 0\n 1 5 1 0 0 0\nM END\n"), + new TestChemical("Chloroform", "Chloroform (systematische Bezeichnung Trichlormethan) ist ein chlorierter Kohlenwasserstoff mit der Summenformel CHCl3.", + "Chloroform; Freon 20; Methane, trichloro-; R 20; Trichloroform; CHCl3; Formyl trichloride; Methane trichloride; Methenyl trichloride; Methyl trichloride; Chloroforme; Cloroformio; NCI-C02686; R 20 (refrigerant); Trichloormethaan; Trichlormethan; Triclorometano; Rcra waste number U044; UN 1888; NSC 77361; F 20", + "CHCl3", 119.378m, "67-66-3", + "\n\n\n 5 4 0 0 0 0 0 0 0 0 V2000\n 0.0000 0.0000 0.4548 C 0000000000000000000\n 0.0000 0.0000 1.5402 H 0000000000000000000\n 0.0000 1.7050 -0.0837 Cl 0000000000000000000\n 1.4766 -0.8525 -0.0837 Cl 0000000000000000000\n -1.4766 -0.8525 -0.0837 Cl 0000000000000000000\n 1 2 1 0 0 0\n 1 3 1 0 0 0\n 1 4 1 0 0 0\n 1 5 1 0 0 0\nM END\n"), + new TestChemical("Cyclohexan", + "Cyclohexan (auch Hexahydrobenzol, Hexamethylen, Naphthen) ist eine farblose Flüssigkeit. Es ist ein Cycloalkan mit der Summenformel C6H12, das im Erdöl vorkommt und als Lösungsmittel und Grundstoff in der Synthese genutzt wird.", + "Benzene, hexahydro-; Hexahydrobenzene; Hexamethylene; Hexanaphthene; Cicloesano; Cykloheksan; Rcra waste number U056; UN 1145; NSC 406835", "C6H12", + 84.1595m, "110-82-7", + "\n\n\n 18 18 0 0 0 0 0 0 0 0 V2000\n 0.0000 -1.4672 0.2293 C 0000000000000000000\n -1.2706 0.7336 0.2293 C 0000000000000000000\n 1.2706 0.7336 0.2293 C 0000000000000000000\n 0.0000 1.4672 -0.2293 C 0000000000000000000\n -1.2706 -0.7336 -0.2293 C 0000000000000000000\n 1.2706 -0.7336 -0.2293 C 0000000000000000000\n 0.0000 -1.5355 1.3276 H 0000000000000000000\n -1.3298 0.7677 1.3276 H 0000000000000000000\n 1.3298 0.7677 1.3276 H 0000000000000000000\n 0.0000 1.5355 -1.3276 H 0000000000000000000\n -1.3298 -0.7677 -1.3276 H 0000000000000000000\n 1.3298 -0.7677 -1.3276 H 0000000000000000000\n 0.0000 -2.4985 -0.1466 H 0000000000000000000\n -2.1638 1.2493 -0.1466 H 0000000000000000000\n 2.1638 1.2493 -0.1466 H 0000000000000000000\n 0.0000 2.4985 0.1466 H 0000000000000000000\n -2.1638 -1.2493 0.1466 H 0000000000000000000\n 2.1638 -1.2493 0.1466 H 0000000000000000000\n 1 5 1 0 0 0\n 1 6 1 0 0 0\n 1 7 1 0 0 0\n 1 13 1 0 0 0\n 2 4 1 0 0 0\n 2 5 1 0 0 0\n 2 8 1 0 0 0\n 2 14 1 0 0 0\n 3 4 1 0 0 0\n 3 6 1 0 0 0\n 3 9 1 0 0 0\n 3 15 1 0 0 0\n 4 10 1 0 0 0\n 4 16 1 0 0 0\n 5 11 1 0 0 0\n 5 17 1 0 0 0\n 6 12 1 0 0 0\n 6 18 1 0 0 0\nM END\n"), + new TestChemical("Cyclopenten", + "Cyclopenten ist eine organische Verbindung mit der Summenformel C5H8. Sie besteht aus einem fünfgliedrigen, ungesättigten Ring, welcher eine Doppelbindung aufweist. In der homologen Reihe der Cycloalkene steht Cyclopenten zwischen Cyclobuten und Cyclohexen. Formal handelt es sich um ein einfach hydriertes Cyclopentadien beziehungsweise ein einfach dehydriertes Cyclopentan. Cyclopenten besitzt nur wenige Anwendungen.", + "", "C5H8", 68.1170m, "142-29-0", + "\n\n\n 13 13 0 0 0 1 V2000\n 0.7026 2.1487 1.2803 C 0 0 0 0 0 0 0 0 0\n 1.9143 3.0520 1.0028 C 0 0 0 0 0 0 0 0 0\n 3.1024 2.1512 1.1173 C 0 0 0 0 0 0 0 0 0\n 2.7505 0.8940 1.4098 C 0 0 0 0 0 0 0 0 0\n 1.2685 0.7440 1.5398 C 0 0 0 0 0 0 0 0 0\n 0.1217 2.5129 2.1461 H 0 0 0 0 0 0 0 0 0\n 0.0000 2.1470 0.4281 H 0 0 0 0 0 0 0 0 0\n 1.8684 3.5149 0.0000 H 0 0 0 0 0 0 0 0 0\n 1.9848 3.8877 1.7230 H 0 0 0 0 0 0 0 0 0\n 4.1143 2.5269 0.9699 H 0 0 0 0 0 0 0 0 0\n 3.4198 0.0455 1.5471 H 0 0 0 0 0 0 0 0 0\n 0.8848 0.0000 0.8178 H 0 0 0 0 0 0 0 0 0\n 0.9989 0.3651 2.5427 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 5 1 1 0 0 0\n 1 6 1 0 0 0\n 1 7 1 0 0 0\n 2 3 1 0 0 0\n 2 8 1 0 0 0\n 2 9 1 0 0 0\n 3 4 2 0 0 0\n 3 10 1 0 0 0\n 4 5 1 0 0 0\n 4 11 1 0 0 0\n 5 12 1 0 0 0\n 5 13 1 0 0 0\nM END\n"), + new TestChemical("Diethylether", + "Diethylether ist der wichtigste Vertreter der organisch-chemischen Verbindungsklasse der Ether und wird deshalb häufig auch einfach als Ether (standardsprachlich und in der älteren wissenschaftlichen Literatur Äther) bezeichnet. Aufgrund der Herstellung aus Ethanol und Schwefelsäure war die historische Bezeichnung Schwefeläther, obwohl Diethylether keinen Schwefel enthält.", + "Ethane, 1,1'-oxybis-; Anaesthetic ether; Anesthesia ether; Anesthetic ether; Diethyl ether; Diethyl oxide; Ethoxyethane; Pronarcol; Solvent ether; 1,1'-Oxybisethane; (C2H5)2O; Aether; Diaethylaether; Dwuetylowy eter; Etere etilico; Ether ethylique; Ether, ethyl; Ethyl ether, tech.; Ethyl oxide; Oxyde d'ethyle; Rcra waste number U117; UN 1155; 3-Oxapentane; Ether; Ethyl ether anhydrous A.C.S.; Sulfuric ether; NSC 100036", + "C4H10O", 74.1216m, "60-29-7", + "\n\n\n 15 14 0 0 0 1 V2000\n 0.9744 2.7710 2.3574 C 0 0 0 0 0 0 0 0 0\n 0.9176 4.1589 1.7420 C 0 0 0 0 0 0 0 0 0\n 1.8106 1.8731 1.6596 O 0 0 0 0 0 0 0 0 0\n 3.2030 2.0811 1.8614 C 0 0 0 0 0 0 0 0 0\n 3.9340 1.0107 1.0734 C 0 0 0 0 0 0 0 0 0\n 0.0000 2.2572 2.2827 H 0 0 0 0 0 0 0 0 0\n 1.2519 2.8230 3.4284 H 0 0 0 0 0 0 0 0 0\n 0.1388 4.7597 2.2281 H 0 0 0 0 0 0 0 0 0\n 0.6868 4.1218 0.6694 H 0 0 0 0 0 0 0 0 0\n 1.8666 4.7007 1.8520 H 0 0 0 0 0 0 0 0 0\n 3.4420 2.0182 2.9412 H 0 0 0 0 0 0 0 0 0\n 3.4890 3.0953 1.5205 H 0 0 0 0 0 0 0 0 0\n 5.0187 1.1245 1.1931 H 0 0 0 0 0 0 0 0 0\n 3.7104 1.0671 0.0000 H 0 0 0 0 0 0 0 0 0\n 3.6667 0.0000 1.4086 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 3 1 0 0 0\n 1 6 1 0 0 0\n 1 7 1 0 0 0\n 2 8 1 0 0 0\n 2 9 1 0 0 0\n 2 10 1 0 0 0\n 3 4 1 0 0 0\n 4 5 1 0 0 0\n 4 11 1 0 0 0\n 4 12 1 0 0 0\n 5 13 1 0 0 0\n 5 14 1 0 0 0\n 5 15 1 0 0 0\nM END\n"), + new TestChemical("Ethanol", + "Ethanol (häufige Trivialnamen: Äthanol, Ethylalkohol, Alkohol) ist ein aliphatischer, einwertiger Alkohol mit der Summenformel C2H6O. Die reine Substanz ist eine bei Raumtemperatur farblose, leicht entzündliche Flüssigkeit mit einem brennenden Geschmack und einem charakteristischen, würzigen (süßlichen) Geruch. Die als Lebergift eingestufte Droge wird bei der Herstellung von Genussmitteln und alkoholischen Getränken wie Wein, Bier und Spirituosen aus kohlehydrathaltigem Material durch eine von Hefen ausgelöste Gärung in relativ großen Mengen produziert.", + "Ethyl alcohol; Alcohol; Alcohol anhydrous; Algrain; Anhydrol; Denatured ethanol; Ethyl hydrate; Ethyl hydroxide; Jaysol; Jaysol S; Methylcarbinol; SD Alchol 23-hydrogen; Tecsol; C2H5OH; Absolute ethanol; Cologne spirit; Fermentation alcohol; Grain alcohol; Molasses alcohol; Potato alcohol; Aethanol; Aethylalkohol; Alcohol, dehydrated; Alcool ethylique; Alcool etilico; Alkohol; Cologne spirits; Denatured alcohol CD-10; Denatured alcohol CD-5; Denatured alcohol CD-5a; Denatured alcohol SD-1; Denatured alcohol SD-13a; Denatured alcohol SD-17; Denatured alcohol SD-23a; Denatured alcohol SD-28; Denatured alcohol SD-3a; Denatured alcohol SD-30; Denatured alcohol SD-39b; Denatured alcohol SD-39c; Denatured alcohol SD-40m; Etanolo; Ethanol 200 proof; Ethyl alc; Etylowy alkohol; EtOH; NCI-C03134; Spirits of wine; Spirt; Alkoholu etylowego; Ethyl alcohol anhydrous; SD alcohol 23-hydrogen; UN 1170; Tecsol C; Alcare Hand Degermer; Absolute alcohol; Denatured alcohol; Ethanol, silent spirit; Ethylol; Punctilious ethyl alcohol; SD 3A", + "C2H6O", 46.0684m, "64-17-5", + "\n\n\n 9 8 0 0 0 0 0 0 0 0999 V2000\n 1.0195 0.8856 0.9752 C 0 0 0 0 0\n 1.8780 1.9882 1.5739 C 0 0 0 0 0\n 3.1989 1.4758 1.7291 O 0 0 0 0 0\n -0.0045 1.2392 0.8098 H 0 0 0 0 0\n 0.9875 0.0188 1.6438 H 0 0 0 0 0\n 1.4360 0.5612 0.0153 H 0 0 0 0 0\n 1.8717 2.8699 0.9114 H 0 0 0 0 0\n 1.4594 2.3055 2.5439 H 0 0 0 0 0\n 3.7472 2.1776 2.1115 H 0 0 0 0 0\n 6 1 1 0 0 0\n 4 1 1 0 0 0\n 1 2 1 0 0 0\n 1 5 1 0 0 0\n 7 2 1 0 0 0\n 2 3 1 0 0 0\n 2 8 1 0 0 0\n 3 9 1 0 0 0\nM END\n"), + new TestChemical("Essigsäureethylester", + "Essigsäureethylester, auch Ethylacetat oder Essigester, ist eine chemische Verbindung aus der Gruppe der Carbonsäureester. Es ist der Ester von Essigsäure und Ethanol. Die farblose Flüssigkeit ist ein charakteristisch nach Klebstoff riechendes Lösungsmittel, das in der chemischen Industrie und in Laboratorien oft verwendet wird.", + "Acetic acid, ethyl ester; Acetic ether; Acetidin; Acetoxyethane; Ethyl acetic ester; Ethyl ethanoate; Vinegar naphtha; CH3COOC2H5; Aethylacetat; Essigester; Ethyle (acetate d'); Etile (acetato di); Ethylacetaat; Ethylester kyseliny octove; Rcra waste number U112; UN 1173; Ethyl ester of acetic acid; 1-Acetoxyethane; NSC 70930; ac. acetic ethyl ester", + "C4H8O2", 88.1051m, "141-78-6", + "\n\n\n 14 13 0 0 0 0 0 0 0 0999 V2000\n 3.4731 2.2227 2.4832 O 0 0 0 0 0\n 2.1749 2.8339 2.4803 C 0 0 0 0 0\n 2.2477 4.1592 1.7469 C 0 0 0 0 0\n 3.7187 1.3350 1.4827 C 0 0 0 0 0\n 5.1346 0.8483 1.5545 C 0 0 0 0 0\n 2.9285 1.0057 0.6092 O 0 0 0 0 0\n 1.4145 2.1783 2.0421 H 0 0 0 0 0\n 1.8932 3.0019 3.5247 H 0 0 0 0 0\n 1.2769 4.6623 1.7528 H 0 0 0 0 0\n 2.9878 4.8162 2.2162 H 0 0 0 0 0\n 2.5643 4.0145 0.7089 H 0 0 0 0 0\n 5.1634 -0.2191 1.3199 H 0 0 0 0 0\n 5.7451 1.4089 0.8424 H 0 0 0 0 0\n 5.5400 0.9802 2.5620 H 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 2 3 1 0 0 0\n 2 7 1 0 0 0\n 2 8 1 0 0 0\n 3 9 1 0 0 0\n 3 10 1 0 0 0\n 3 11 1 0 0 0\n 4 5 1 0 0 0\n 4 6 2 0 0 0\n 5 12 1 0 0 0\n 5 13 1 0 0 0\n 5 14 1 0 0 0\nM END\n"), + new TestChemical("Ethylpropylether", "Ethylpropylether", + "Ether, ethyl propyl; Ethyl n-propyl ether; Ethyl propyl ether; Propyl ethyl ether; 1-Ethoxypropane; n-C3H7OC2H5; UN 2615", "C5H12O", 88.1482m, + "628-32-0", + "\n\n\n 18 17 0 0 0 1 V2000\n 3.1079 1.7452 1.0645 O 0 0 0 0 0 0 0 0 0\n 4.4441 1.2694 1.1114 C 0 0 0 0 0 0 0 0 0\n 5.3490 2.4893 1.2632 C 0 0 0 0 0 0 0 0 0\n 2.1504 0.7063 0.9251 C 0 0 0 0 0 0 0 0 0\n 6.8002 2.0698 1.3208 C 0 0 0 0 0 0 0 0 0\n 0.7827 1.3612 0.8832 C 0 0 0 0 0 0 0 0 0\n 4.6830 0.7077 0.1862 H 0 0 0 0 0 0 0 0 0\n 4.5718 0.5698 1.9616 H 0 0 0 0 0 0 0 0 0\n 5.0789 3.0544 2.1773 H 0 0 0 0 0 0 0 0 0\n 5.1881 3.1923 0.4219 H 0 0 0 0 0 0 0 0 0\n 2.2307 0.0000 1.7749 H 0 0 0 0 0 0 0 0 0\n 2.3473 0.1294 0.0000 H 0 0 0 0 0 0 0 0 0\n 7.4601 2.9403 1.4280 H 0 0 0 0 0 0 0 0 0\n 7.1058 1.5371 0.4105 H 0 0 0 0 0 0 0 0 0\n 6.9969 1.4029 2.1707 H 0 0 0 0 0 0 0 0 0\n 0.0000 0.5997 0.7766 H 0 0 0 0 0 0 0 0 0\n 0.6863 2.0571 0.0395 H 0 0 0 0 0 0 0 0 0\n 0.5712 1.9289 1.7988 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 2 3 1 0 0 0\n 2 7 1 0 0 0\n 2 8 1 0 0 0\n 3 5 1 0 0 0\n 3 9 1 0 0 0\n 3 10 1 0 0 0\n 4 6 1 0 0 0\n 4 11 1 0 0 0\n 4 12 1 0 0 0\n 5 13 1 0 0 0\n 5 14 1 0 0 0\n 5 15 1 0 0 0\n 6 16 1 0 0 0\n 6 17 1 0 0 0\n 6 18 1 0 0 0\nM END\n"), + new TestChemical("Essigsäureisopropylester", + "Essigsäureisopropylester ist eine chemische Verbindung aus der Gruppe der Carbonsäureester. Es ist eine farblose, leichtentzündliche und flüchtige Flüssigkeit.", + "Acetic acid, 1-methylethyl ester; Acetic acid, isopropyl ester; 2-Acetoxypropane; 2-Propyl acetate; CH3COOCH(CH3)2; Acetate d'isopropyle; Isopropile(acetato di); Isopropyl ethanoate; Isopropyl (acetate d'); Isopropylacetaat; Isopropylacetat; Isopropylester kyseliny octove; UN 1220; Isopropyl ester of acetic acid; sec-Propyl acetate; Acetic acid, 2-propyl ester; 1-Methylethyl acetate; NSC 9295", + "C5H10O2", 102.1317m, "108-21-4", + "\n\n\n 17 16 0 0 0 1 V2000\n 3.1938 1.0106 1.8174 O 0 0 0 0 0 0 0 0 0\n 2.0014 1.7632 2.1082 C 0 0 0 0 0 0 0 0 0\n 4.4164 1.5950 1.9641 C 0 0 0 0 0 0 0 0 0\n 0.9499 0.7230 2.4807 C 0 0 0 0 0 0 0 0 0\n 1.5706 2.5640 0.8846 C 0 0 0 0 0 0 0 0 0\n 5.5443 0.6574 1.6359 C 0 0 0 0 0 0 0 0 0\n 4.5054 2.7501 2.3330 O 0 0 0 0 0 0 0 0 0\n 2.1755 2.4482 2.9734 H 0 0 0 0 0 0 0 0 0\n 1.2515 0.1324 3.3552 H 0 0 0 0 0 0 0 0 0\n 0.7564 0.0192 1.6598 H 0 0 0 0 0 0 0 0 0\n 0.0000 1.2159 2.7240 H 0 0 0 0 0 0 0 0 0\n 1.4415 1.9261 0.0000 H 0 0 0 0 0 0 0 0 0\n 0.6116 3.0626 1.0738 H 0 0 0 0 0 0 0 0 0\n 2.3036 3.3399 0.6292 H 0 0 0 0 0 0 0 0 0\n 5.3046 0.0000 0.7896 H 0 0 0 0 0 0 0 0 0\n 5.7670 0.0184 2.5009 H 0 0 0 0 0 0 0 0 0\n 6.4576 1.2122 1.3851 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 3 1 0 0 0\n 2 4 1 0 0 0\n 2 5 1 0 0 0\n 2 8 1 0 0 0\n 3 6 1 0 0 0\n 3 7 2 0 0 0\n 4 9 1 0 0 0\n 4 10 1 0 0 0\n 4 11 1 0 0 0\n 5 12 1 0 0 0\n 5 13 1 0 0 0\n 5 14 1 0 0 0\n 6 15 1 0 0 0\n 6 16 1 0 0 0\n 6 17 1 0 0 0\nM END\n"), + new TestChemical( + "Methan", + "Methan ist eine chemische Verbindung aus der Gruppe der Alkane mit der Summenformel CH4. Das farb- und geruchlose, brennbare Gas kommt in der Natur vor und ist ein Hauptbestandteil von Erdgas. Es dient als Heizgas und ist in der chemischen Industrie als Ausgangsprodukt für technische Synthesen von großer Bedeutung.", + "Marsh gas; Methyl hydride; CH4; Fire Damp; R 50; Biogas; R 50 (refrigerant)", "CH4", 16.0425m, "74-82-8", + "\n\n\n 5 4 0 0 0 1 V2000\n 1.0582 0.9353 0.8103 C 0 0 0 0 0 0 0 0 0\n 1.4145 1.5662 0.0000 H 0 0 0 0 0 0 0 0 0\n 1.2065 1.4452 1.7588 H 0 0 0 0 0 0 0 0 0\n 0.0000 0.7294 0.6710 H 0 0 0 0 0 0 0 0 0\n 1.6121 0.0000 0.8114 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 3 1 0 0 0\n 1 4 1 0 0 0\n 1 5 1 0 0 0\nM END\n"), + new TestChemical("Methylalkohol", + "Methanol, auch Methylalkohol, ist eine organische chemische Verbindung mit der Summenformel CH4O (Halbstrukturformel: CH3OH) und der einfachste Vertreter aus der Stoffgruppe der Alkohole. Unter Normalbedingungen ist Methanol eine klare, farblose, entzündliche und leicht flüchtige Flüssigkeit mit alkoholischem Geruch.", + "Methanol; Carbinol; Methyl hydroxide; Methylol; Monohydroxymethane; Wood alcohol; CH3OH; Colonial spirit; Columbian spirit; Hydroxymethane; Wood naphtha; Alcool methylique; Alcool metilico; Columbian spirits; Metanolo; Methylalkohol; Metylowy alkohol; Pyroxylic spirit; Wood spirit; Rcra waste number U154; UN 1230; Pyro alcohol; Spirit of wood; Bieleski's solution; NSC 85232", + "CH4O", 32.0419m, "67-56-1", + "\n\n\n 6 5 0 0 0 1 V2000\n 0.2453 0.8386 1.6056 H 0 0 0 0 0 0 0 0 0\n 0.6776 0.9803 0.6074 C 0 0 0 0 0 0 0 0 0\n 0.0000 1.5869 0.0000 H 0 0 0 0 0 0 0 0 0\n 0.8133 0.0000 0.1338 H 0 0 0 0 0 0 0 0 0\n 1.8631 1.7142 0.6464 O 0 0 0 0 0 0 0 0 0\n 2.4856 1.2216 1.1660 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 2 3 1 0 0 0\n 2 4 1 0 0 0\n 2 5 1 0 0 0\n 5 6 1 0 0 0\nM END\n"), + new TestChemical("2-Butanone", + "Butanon ist neben Aceton eines der wichtigsten industriell genutzten Ketone. Es ist eine farblose, leicht bewegliche Flüssigkeit mit einem typischen Geruch und wird allgemein als Methylethylketon (MEK) bezeichnet.", + "Butan-2-one; Butanone; Ethyl methyl ketone; Ketone, methyl ethyl; Methyl ethyl ketone; MEK; C2H5COCH3; Acetone, methyl-; Aethylmethylketon; 3-Butanone; Butanone 2; Ethyl methyl cetone; Ethylmethylketon; Ketone, ethyl methyl; Meetco; Methyl acetone; Metiletilchetone; Metyloetyloketon; Rcra waste number U159; UN 1193; 2-Oxobutane; 2-Butanal; 2-butanone (MEK; methyl ethyl ketone); 2-butanone (MEK)", + "C4H8O", 72.1057m, "78-93-3", + "\n\n\n 13 12 0 0 0 1 V2000\n 1.7893 1.4200 2.8269 C 0 0 0 0 0 0 0 0 0\n 0.8451 2.5827 2.6164 C 0 0 0 0 0 0 0 0 0\n 2.3705 0.9490 1.5071 C 0 0 0 0 0 0 0 0 0\n 3.5557 1.7224 0.9969 C 0 0 0 0 0 0 0 0 0\n 1.9075 0.0000 0.9038 O 0 0 0 0 0 0 0 0 0\n 2.6155 1.7113 3.5049 H 0 0 0 0 0 0 0 0 0\n 1.2589 0.5906 3.3355 H 0 0 0 0 0 0 0 0 0\n 0.0000 2.3072 1.9722 H 0 0 0 0 0 0 0 0 0\n 0.4300 2.9289 3.5716 H 0 0 0 0 0 0 0 0 0\n 1.3499 3.4390 2.1488 H 0 0 0 0 0 0 0 0 0\n 3.8802 1.3974 0.0000 H 0 0 0 0 0 0 0 0 0\n 3.3200 2.7937 0.9410 H 0 0 0 0 0 0 0 0 0\n 4.4074 1.6050 1.6799 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 3 1 0 0 0\n 1 6 1 0 0 0\n 1 7 1 0 0 0\n 2 8 1 0 0 0\n 2 9 1 0 0 0\n 2 10 1 0 0 0\n 3 4 1 0 0 0\n 3 5 2 0 0 0\n 4 11 1 0 0 0\n 4 12 1 0 0 0\n 4 13 1 0 0 0\nM END\n"), + new TestChemical( + "Butan", + "Die Butane sind eine Stoffgruppe innerhalb der Alkane, die die Summenformel C4H10 aufweisen. Sie besteht aus den beiden Vertretern n-Butan und iso-Butan, die zueinander isomer sind. Beide Butane sind farblose, brennbare, leicht zu verflüssigende Gase („Flüssiggase“), die sich kaum in Wasser, aber gut in Ethanol und Ether lösen.", + "n-Butane; Diethyl; Freon 600; Liquefied petroleum gas; LPG; n-C4H10; Butanen; Butani; Methylethylmethane; UN 1011; A 21; HC 600; HC 600 (hydrocarbon); R 600; R 600 (alkane)", + "C4H10", 58.1222m, "106-97-8", + "\n\n\n 14 13 0 0 0 1 V2000\n 3.5864 1.1360 0.9321 C 0 0 0 0 0 0 0 0 0\n 2.5594 0.8276 1.9979 C 0 0 0 0 0 0 0 0 0\n 1.7180 2.0488 2.3336 C 0 0 0 0 0 0 0 0 0\n 0.6912 1.7404 3.3995 C 0 0 0 0 0 0 0 0 0\n 3.1136 1.4730 0.0000 H 0 0 0 0 0 0 0 0 0\n 4.1901 0.2515 0.6922 H 0 0 0 0 0 0 0 0 0\n 4.2769 1.9280 1.2514 H 0 0 0 0 0 0 0 0 0\n 3.0625 0.4552 2.9124 H 0 0 0 0 0 0 0 0 0\n 1.9030 0.0000 1.6631 H 0 0 0 0 0 0 0 0 0\n 1.2148 2.4207 1.4190 H 0 0 0 0 0 0 0 0 0\n 2.3747 2.8766 2.6678 H 0 0 0 0 0 0 0 0 0\n 0.0000 0.9495 3.0791 H 0 0 0 0 0 0 0 0 0\n 0.0880 2.6250 3.6404 H 0 0 0 0 0 0 0 0 0\n 1.1635 1.4017 4.3313 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 5 1 0 0 0\n 1 6 1 0 0 0\n 1 7 1 0 0 0\n 2 3 1 0 0 0\n 2 8 1 0 0 0\n 2 9 1 0 0 0\n 3 4 1 0 0 0\n 3 10 1 0 0 0\n 3 11 1 0 0 0\n 4 12 1 0 0 0\n 4 13 1 0 0 0\n 4 14 1 0 0 0\nM END\n"), + new TestChemical("Butanol", + "1-Butanol (auch n-Butanol oder nach IUPAC Butan-1-ol) ist eine chemische Verbindung aus der Gruppe der Alkanole. Der primäre Alkohol leitet sich vom aliphatischen Kohlenwasserstoff n-Butan ab.", + "Butyl alcohol; n-Butan-1-ol; n-Butanol; n-Butyl alcohol; Butyl hydroxide; CCS 203; Hemostyp; Methylolpropane; Propylcarbinol; n-C4H9OH; Butanol; Butan-1-ol; 1-Hydroxybutane; Alcool butylique; Butanolo; Butylowy alkohol; Butyric alcohol; Propylmethanol; Butanolen; 1-Butyl alcohol; Rcra waste number U031; Butanol-1; NSC 62782", + "C4H10O", 74.1216m, "71-36-3", + "\n\n\n 15 14 0 0 0 1 V2000\n 2.9651 2.0464 2.4042 C 0 0 0 0 0 0 0 0 0\n 2.3281 2.7103 1.1934 C 0 0 0 0 0 0 0 0 0\n 0.8652 2.3158 0.9860 C 0 0 0 0 0 0 0 0 0\n 3.3109 0.5952 2.1559 C 0 0 0 0 0 0 0 0 0\n 0.0000 2.7697 1.9922 O 0 0 0 0 0 0 0 0 0\n 3.8836 2.6000 2.6826 H 0 0 0 0 0 0 0 0 0\n 2.2886 2.1307 3.2779 H 0 0 0 0 0 0 0 0 0\n 2.8987 2.4514 0.2796 H 0 0 0 0 0 0 0 0 0\n 2.4074 3.8120 1.2923 H 0 0 0 0 0 0 0 0 0\n 0.5089 2.6739 0.0000 H 0 0 0 0 0 0 0 0 0\n 0.7244 1.2206 1.0177 H 0 0 0 0 0 0 0 0 0\n 2.4210 0.0000 1.9086 H 0 0 0 0 0 0 0 0 0\n 3.7712 0.1403 3.0423 H 0 0 0 0 0 0 0 0 0\n 4.0193 0.4827 1.3240 H 0 0 0 0 0 0 0 0 0\n 0.1229 3.7075 2.0732 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 6 1 0 0 0\n 1 7 1 0 0 0\n 2 3 1 0 0 0\n 2 8 1 0 0 0\n 2 9 1 0 0 0\n 3 5 1 0 0 0\n 3 10 1 0 0 0\n 3 11 1 0 0 0\n 4 12 1 0 0 0\n 4 13 1 0 0 0\n 4 14 1 0 0 0\n 5 15 1 0 0 0\nM END\n"), + new TestChemical("Heptan", "Heptane sind zu den Alkanen zählende Kohlenwasserstoffe mit der Summenformel C7H16. Es existieren neun Konstitutionsisomere", + "n-Heptane; Dipropylmethane; Heptyl hydride; Skellysolve C; n-C7H16; Eptani; Heptan; Heptanen; Gettysolve-C; NSC 62784", "C7H16", 100.2019m, + "142-82-5", + "\n\n\n 23 22 0 0 0 1 V2000\n 4.9119 1.1117 1.6160 C 0 0 0 0 0 0 0 0 0\n 3.6978 1.8855 1.1285 C 0 0 0 0 0 0 0 0 0\n 2.4040 1.2047 1.5468 C 0 0 0 0 0 0 0 0 0\n 6.2065 1.7895 1.1965 C 0 0 0 0 0 0 0 0 0\n 1.1708 1.9589 1.0725 C 0 0 0 0 0 0 0 0 0\n 7.4159 1.0214 1.6802 C 0 0 0 0 0 0 0 0 0\n 0.9041 3.2087 1.8814 C 0 0 0 0 0 0 0 0 0\n 4.8843 0.0753 1.2230 H 0 0 0 0 0 0 0 0 0\n 4.8794 1.0110 2.7196 H 0 0 0 0 0 0 0 0 0\n 3.7266 2.9218 1.5233 H 0 0 0 0 0 0 0 0 0\n 3.7322 1.9883 0.0250 H 0 0 0 0 0 0 0 0 0\n 2.3839 0.1736 1.1402 H 0 0 0 0 0 0 0 0 0\n 2.3759 1.0904 2.6492 H 0 0 0 0 0 0 0 0 0\n 6.2340 2.8252 1.5899 H 0 0 0 0 0 0 0 0 0\n 6.2388 1.8907 0.0934 H 0 0 0 0 0 0 0 0 0\n 1.2758 2.2178 0.0000 H 0 0 0 0 0 0 0 0 0\n 0.2900 1.2893 1.1316 H 0 0 0 0 0 0 0 0 0\n 7.4323 0.9394 2.7752 H 0 0 0 0 0 0 0 0 0\n 8.3484 1.5122 1.3742 H 0 0 0 0 0 0 0 0 0\n 7.4348 0.0000 1.2773 H 0 0 0 0 0 0 0 0 0\n 1.7349 3.9244 1.8126 H 0 0 0 0 0 0 0 0 0\n 0.7607 2.9796 2.9459 H 0 0 0 0 0 0 0 0 0\n 0.0000 3.7234 1.5317 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 8 1 0 0 0\n 1 9 1 0 0 0\n 2 3 1 0 0 0\n 2 10 1 0 0 0\n 2 11 1 0 0 0\n 3 5 1 0 0 0\n 3 12 1 0 0 0\n 3 13 1 0 0 0\n 4 6 1 0 0 0\n 4 14 1 0 0 0\n 4 15 1 0 0 0\n 5 7 1 0 0 0\n 5 16 1 0 0 0\n 5 17 1 0 0 0\n 6 18 1 0 0 0\n 6 19 1 0 0 0\n 6 20 1 0 0 0\n 7 21 1 0 0 0\n 7 22 1 0 0 0\n 7 23 1 0 0 0\nM END\n"), + new TestChemical( + "Hexan", + "n-Hexan ist eine den Alkanen (gesättigte Kohlenwasserstoffe) zugehörige chemische Verbindung. Es ist eine farblose Flüssigkeit mit der Summenformel C6H14. Es ist das unverzweigte Isomer der fünf Hexanisomeren.", + "n-Hexane; Skellysolve B; n-C6H14; Esani; Heksan; Hexanen; Hexyl hydride; Gettysolve-B; NCI-C60571; NSC 68472", "C6H14", 86.1754m, "110-54-3", + "\n\n\n 20 19 0 0 0 1 V2000\n 3.0831 1.0831 1.9494 C 0 0 0 0 0 0 0 0 0\n 2.9735 1.4738 3.4178 C 0 0 0 0 0 0 0 0 0\n 3.7734 2.7246 3.7510 C 0 0 0 0 0 0 0 0 0\n 2.1289 1.8691 1.0623 C 0 0 0 0 0 0 0 0 0\n 5.2567 2.5130 3.5423 C 0 0 0 0 0 0 0 0 0\n 0.6830 1.5862 1.4060 C 0 0 0 0 0 0 0 0 0\n 2.8829 0.0000 1.8355 H 0 0 0 0 0 0 0 0 0\n 4.1330 1.2389 1.6114 H 0 0 0 0 0 0 0 0 0\n 1.9001 1.6293 3.6713 H 0 0 0 0 0 0 0 0 0\n 3.3194 0.6360 4.0538 H 0 0 0 0 0 0 0 0 0\n 3.4222 3.5715 3.1275 H 0 0 0 0 0 0 0 0 0\n 3.5761 3.0236 4.7988 H 0 0 0 0 0 0 0 0 0\n 2.3340 2.9542 1.1601 H 0 0 0 0 0 0 0 0 0\n 2.3218 1.6234 0.0000 H 0 0 0 0 0 0 0 0 0\n 5.8188 3.4487 3.6460 H 0 0 0 0 0 0 0 0 0\n 5.6708 1.7956 4.2625 H 0 0 0 0 0 0 0 0 0\n 5.4489 2.1109 2.5328 H 0 0 0 0 0 0 0 0 0\n 0.5108 1.7246 2.4871 H 0 0 0 0 0 0 0 0 0\n 0.0000 2.2530 0.8669 H 0 0 0 0 0 0 0 0 0\n 0.4010 0.5540 1.1613 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 7 1 0 0 0\n 1 8 1 0 0 0\n 2 3 1 0 0 0\n 2 9 1 0 0 0\n 2 10 1 0 0 0\n 3 5 1 0 0 0\n 3 11 1 0 0 0\n 3 12 1 0 0 0\n 4 6 1 0 0 0\n 4 13 1 0 0 0\n 4 14 1 0 0 0\n 5 15 1 0 0 0\n 5 16 1 0 0 0\n 5 17 1 0 0 0\n 6 18 1 0 0 0\n 6 19 1 0 0 0\n 6 20 1 0 0 0\nM END\n"), + new TestChemical( + "Octan", + "n-Octan ist eine farblose Flüssigkeit, die zu den Alkanen zählt. In der Chemie wird es entsprechend den aktuellen Nomenklaturregeln als n-Octan geschrieben, in Deutschland wird jedoch oft – gerade im Zusammenhang mit der Oktanzahl – die veraltete Schreibweise Oktan bevorzugt. Es handelt sich um den unverzweigten Vertreter der 18 Isomere der Octane.", + "n-Octane; n-C8H18; Oktan; Oktanen; Ottani; UN 1262", "C8H18", 114.2285m, "111-65-9", + "\n\n\n 26 25 0 0 0 1 V2000\n 3.8144 2.3469 1.6808 C 0 0 0 0 0 0 0 0 0\n 2.8536 2.9952 2.6656 C 0 0 0 0 0 0 0 0 0\n 1.4042 2.8035 2.2422 C 0 0 0 0 0 0 0 0 0\n 5.2606 2.6768 2.0091 C 0 0 0 0 0 0 0 0 0\n 1.0820 3.5021 0.9282 C 0 0 0 0 0 0 0 0 0\n 6.2381 2.0228 1.0442 C 0 0 0 0 0 0 0 0 0\n 0.8478 4.9859 1.0983 C 0 0 0 0 0 0 0 0 0\n 6.4020 0.5402 1.2955 C 0 0 0 0 0 0 0 0 0\n 3.5588 2.6881 0.6508 H 0 0 0 0 0 0 0 0 0\n 3.6652 1.2480 1.6733 H 0 0 0 0 0 0 0 0 0\n 3.0793 4.0774 2.7579 H 0 0 0 0 0 0 0 0 0\n 3.0069 2.5713 3.6775 H 0 0 0 0 0 0 0 0 0\n 0.7310 3.1711 3.0415 H 0 0 0 0 0 0 0 0 0\n 1.1916 1.7202 2.1456 H 0 0 0 0 0 0 0 0 0\n 5.4911 2.3686 3.0487 H 0 0 0 0 0 0 0 0 0\n 5.4015 3.7762 1.9864 H 0 0 0 0 0 0 0 0 0\n 0.1878 3.0372 0.4697 H 0 0 0 0 0 0 0 0 0\n 1.9182 3.3248 0.2134 H 0 0 0 0 0 0 0 0 0\n 7.2242 2.5203 1.1318 H 0 0 0 0 0 0 0 0 0\n 5.9096 2.1958 0.0000 H 0 0 0 0 0 0 0 0 0\n 0.6299 5.4668 0.1362 H 0 0 0 0 0 0 0 0 0\n 0.0000 5.1885 1.7663 H 0 0 0 0 0 0 0 0 0\n 1.7255 5.4904 1.5249 H 0 0 0 0 0 0 0 0 0\n 5.4519 0.0000 1.1826 H 0 0 0 0 0 0 0 0 0\n 7.1172 0.0943 0.5924 H 0 0 0 0 0 0 0 0 0\n 6.7707 0.3401 2.3104 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 9 1 0 0 0\n 1 10 1 0 0 0\n 2 3 1 0 0 0\n 2 11 1 0 0 0\n 2 12 1 0 0 0\n 3 5 1 0 0 0\n 3 13 1 0 0 0\n 3 14 1 0 0 0\n 4 6 1 0 0 0\n 4 15 1 0 0 0\n 4 16 1 0 0 0\n 5 7 1 0 0 0\n 5 17 1 0 0 0\n 5 18 1 0 0 0\n 6 8 1 0 0 0\n 6 19 1 0 0 0\n 6 20 1 0 0 0\n 7 21 1 0 0 0\n 7 22 1 0 0 0\n 7 23 1 0 0 0\n 8 24 1 0 0 0\n 8 25 1 0 0 0\n 8 26 1 0 0 0\nM END\n"), + new TestChemical( + "Pentan", + "Pentane sind Kohlenwasserstoffe mit der Summenformel C5H12 und zählen zu den Alkanen. Es existieren drei Konstitutionsisomere: n-Pentan, Isopentan und Neopentan.", + "n-Pentane; Skellysolve A; n-C5H12; Pentan; Pentanen; Pentani; Amyl hydride; NSC 72415", "C5H12", 72.1488m, "109-66-0", + "\n\n\n 17 16 0 0 0 1 V2000\n 3.7280 2.4135 2.8751 C 0 0 0 0 0 0 0 0 0\n 2.5997 1.4666 2.4988 C 0 0 0 0 0 0 0 0 0\n 1.8538 1.9510 1.2659 C 0 0 0 0 0 0 0 0 0\n 4.4698 1.9317 4.1013 C 0 0 0 0 0 0 0 0 0\n 0.7349 1.0066 0.8889 C 0 0 0 0 0 0 0 0 0\n 3.3234 3.4299 3.0526 H 0 0 0 0 0 0 0 0 0\n 4.4308 2.5184 2.0246 H 0 0 0 0 0 0 0 0 0\n 1.8961 1.3604 3.3490 H 0 0 0 0 0 0 0 0 0\n 3.0039 0.4497 2.3206 H 0 0 0 0 0 0 0 0 0\n 2.5589 2.0599 0.4178 H 0 0 0 0 0 0 0 0 0\n 1.4479 2.9664 1.4462 H 0 0 0 0 0 0 0 0 0\n 5.2821 2.6183 4.3713 H 0 0 0 0 0 0 0 0 0\n 3.8049 1.8514 4.9716 H 0 0 0 0 0 0 0 0 0\n 4.9169 0.9415 3.9412 H 0 0 0 0 0 0 0 0 0\n 0.0000 0.9059 1.6988 H 0 0 0 0 0 0 0 0 0\n 1.1125 0.0000 0.6654 H 0 0 0 0 0 0 0 0 0\n 0.1970 1.3600 0.0000 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 6 1 0 0 0\n 1 7 1 0 0 0\n 2 3 1 0 0 0\n 2 8 1 0 0 0\n 2 9 1 0 0 0\n 3 5 1 0 0 0\n 3 10 1 0 0 0\n 3 11 1 0 0 0\n 4 12 1 0 0 0\n 4 13 1 0 0 0\n 4 14 1 0 0 0\n 5 15 1 0 0 0\n 5 16 1 0 0 0\n 5 17 1 0 0 0\nM END\n"), + new TestChemical("1-Pentanol", + "1-Pentanol (veraltet: Amylalkohol) ist eine organische chemische Verbindung und gehört zu den Alkoholen. 1-Pentanol ist Bestandteil der Fuselöle.", + "Pentyl alcohol; n-Amyl alcohol; n-Butylcarbinol; n-Pentan-1-ol; n-Pentanol; n-Pentyl alcohol; Amyl alcohol; Amylol; Pentanol; 1-Pentyl alcohol; n-C5H11OH; Pentan-1-ol; Pentanol-1; Pentasol; n-Amylalkohol; Alcool amylique; Amyl alcohol, n-; Amyl alcohol, normal; Primary amyl alcohol; UN 1105; 1-Pentol; Primary-N-amyl alcohol; Butyl carbinol; NSC 5707", + "C5H12O", 88.1482m, "71-41-0", + "\n\n\n 18 17 0 0 0 1 V2000\n 2.0264 2.2581 1.9056 C 0 0 0 0 0 0 0 0 0\n 3.2462 1.6488 1.2359 C 0 0 0 0 0 0 0 0 0\n 4.4464 1.6425 2.1693 C 0 0 0 0 0 0 0 0 0\n 0.8116 2.2939 0.9786 C 0 0 0 0 0 0 0 0 0\n 5.6613 1.0380 1.5026 C 0 0 0 0 0 0 0 0 0\n 0.3196 1.0290 0.6256 O 0 0 0 0 0 0 0 0 0\n 2.2542 3.2923 2.2314 H 0 0 0 0 0 0 0 0 0\n 1.7856 1.7003 2.8338 H 0 0 0 0 0 0 0 0 0\n 3.0191 0.6161 0.9030 H 0 0 0 0 0 0 0 0 0\n 3.4892 2.2109 0.3117 H 0 0 0 0 0 0 0 0 0\n 4.6718 2.6757 2.5011 H 0 0 0 0 0 0 0 0 0\n 4.2030 1.0807 3.0931 H 0 0 0 0 0 0 0 0 0\n 1.0538 2.7449 0.0000 H 0 0 0 0 0 0 0 0 0\n 0.0000 2.8939 1.4351 H 0 0 0 0 0 0 0 0 0\n 5.4791 0.0000 1.1934 H 0 0 0 0 0 0 0 0 0\n 5.9510 1.5990 0.6041 H 0 0 0 0 0 0 0 0 0\n 6.5258 1.0323 2.1788 H 0 0 0 0 0 0 0 0 0\n 0.1634 0.5491 1.4297 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 7 1 0 0 0\n 1 8 1 0 0 0\n 2 3 1 0 0 0\n 2 9 1 0 0 0\n 2 10 1 0 0 0\n 3 5 1 0 0 0\n 3 11 1 0 0 0\n 3 12 1 0 0 0\n 4 6 1 0 0 0\n 4 13 1 0 0 0\n 4 14 1 0 0 0\n 5 15 1 0 0 0\n 5 16 1 0 0 0\n 5 17 1 0 0 0\n 6 18 1 0 0 0\nM END\n"), + new TestChemical("1-Propanol", + "Propanole sind Alkohole mit drei Kohlenstoffatomen und einer Hydroxygruppe (–OH). Sie haben die allgemeine Summenformel C3H8O und eine molare Masse von 60,10 g/mol. Es gibt nur zwei Isomere.", + "Propyl alcohol; n-Propan-1-ol; n-Propanol; n-Propyl alcohol; Ethylcarbinol; Optal; Osmosol extra; Propanol; Propylic alcohol; 1-Propyl alcohol; n-C3H7OH; 1-Hydroxypropane; Propanol-1; Propan-1-ol; n-Propyl alkohol; Alcool propilico; Alcool propylique; Propanole; Propanolen; Propanoli; Propylowy alkohol; UN 1274; Propylan-propyl alcohol; NSC 30300; Alcohol, propyl", + "C3H8O", 60.0950m, "71-23-8", + "\n\n\n 12 11 0 0 0 1 V2000\n 0.7713 1.5705 1.3838 C 0 0 0 0 0 0 0 0 0\n 2.1696 1.0226 1.0958 C 0 0 0 0 0 0 0 0 0\n 3.2631 1.9141 1.6398 C 0 0 0 0 0 0 0 0 0\n 0.3563 1.3950 2.7118 O 0 0 0 0 0 0 0 0 0\n 0.7013 2.6399 1.1017 H 0 0 0 0 0 0 0 0 0\n 0.0000 1.0213 0.8161 H 0 0 0 0 0 0 0 0 0\n 2.2872 0.9154 0.0000 H 0 0 0 0 0 0 0 0 0\n 2.2716 0.0000 1.5099 H 0 0 0 0 0 0 0 0 0\n 3.2248 1.9921 2.7352 H 0 0 0 0 0 0 0 0 0\n 3.1932 2.9340 1.2381 H 0 0 0 0 0 0 0 0 0\n 4.2564 1.5270 1.3793 H 0 0 0 0 0 0 0 0 0\n 1.0138 1.7939 3.2691 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 5 1 0 0 0\n 1 6 1 0 0 0\n 2 3 1 0 0 0\n 2 7 1 0 0 0\n 2 8 1 0 0 0\n 3 9 1 0 0 0\n 3 10 1 0 0 0\n 3 11 1 0 0 0\n 4 12 1 0 0 0\nM END\n"), + new TestChemical( + "Phenol", + "Phenol (nach IUPAC: Benzenol, veraltet: Karbolsäure oder kurz Karbol) ist eine aromatische, organische Verbindung und besteht aus einer Phenylgruppe (–C6H5), an die eine Hydroxygruppe (–OH) gebunden ist. Der farblose, kristalline Feststoff ist eine wichtige Industriechemikalie und dient als Zwischenprodukt besonders zur Herstellung diverser Kunststoffe.", + "Carbolic acid; Baker's P and S Liquid and Ointment; Benzenol; Hydroxybenzene; Izal; Monohydroxybenzene; Monophenol; Oxybenzene; Phenic acid; Phenyl alcohol; Phenyl hydrate; Phenyl hydroxide; Phenylic acid; Phenylic alcohol; PhOH; Benzene, hydroxy-; Acide carbolique; Baker's P & S liquid & Ointment; Fenol; Fenolo; NCI-C50124; Paoscle; Phenole; Carbolsaure; NA 2821; Phenol alcohol; Phenol, molten; Rcra waste number U188; UN 1671; UN 2312; UN 2821; Phenic alcohol; NSC 36808", + "C6H6O", 94.1112m, "108-95-2", + "\n\n\n 13 13 0 0 0 1 V2000\n 0.3792 2.3991 0.0767 O 0 0 0 0 0 0 0 0 0\n 1.7410 2.2635 0.0604 C 0 0 0 0 0 0 0 0 0\n 2.4822 3.4537 0.0746 C 0 0 0 0 0 0 0 0 0\n 2.3800 1.0169 0.0335 C 0 0 0 0 0 0 0 0 0\n 3.8685 3.3780 0.0622 C 0 0 0 0 0 0 0 0 0\n 3.7689 0.9695 0.0214 C 0 0 0 0 0 0 0 0 0\n 4.5128 2.1443 0.0358 C 0 0 0 0 0 0 0 0 0\n 0.0000 1.5292 0.0598 H 0 0 0 0 0 0 0 0 0\n 1.9740 4.4239 0.0950 H 0 0 0 0 0 0 0 0 0\n 1.8001 0.0870 0.0221 H 0 0 0 0 0 0 0 0 0\n 4.4597 4.2998 0.0734 H 0 0 0 0 0 0 0 0 0\n 4.2770 0.0000 0.0000 H 0 0 0 0 0 0 0 0 0\n 5.6063 2.0990 0.0278 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 8 1 0 0 0\n 2 3 2 0 0 0\n 4 2 1 0 0 0\n 3 5 1 0 0 0\n 3 9 1 0 0 0\n 6 4 2 0 0 0\n 4 10 1 0 0 0\n 5 7 2 0 0 0\n 5 11 1 0 0 0\n 7 6 1 0 0 0\n 6 12 1 0 0 0\n 7 13 1 0 0 0\nM END\n"), + new TestChemical( + "Propan", "Propan ist ein farbloses brennbares Gas und gehört zu den Kohlenwasserstoffen. Es steht in der homologen Reihe der Alkane an dritter Stelle.", + "n-Propane; Dimethylmethane; Freon 290; Liquefied petroleum gas; LPG; Propyl hydride; R 290; C3H8; UN 1978; A-108; Hydrocarbon propellant A-108; HC 290", "C3H8", + 44.0956m, "74-98-6", + "\n\n\n 11 10 0 0 0 1 V2000\n 3.3461 1.6436 1.3326 C 0 0 0 0 0 0 0 0 0\n 2.0042 1.0740 0.9307 C 0 0 0 0 0 0 0 0 0\n 0.9734 1.2486 2.0232 C 0 0 0 0 0 0 0 0 0\n 3.7393 1.1540 2.2333 H 0 0 0 0 0 0 0 0 0\n 3.2805 2.7182 1.5491 H 0 0 0 0 0 0 0 0 0\n 4.0907 1.5138 0.5370 H 0 0 0 0 0 0 0 0 0\n 1.6524 1.5612 0.0000 H 0 0 0 0 0 0 0 0 0\n 2.1104 0.0000 0.6811 H 0 0 0 0 0 0 0 0 0\n 0.0000 0.8391 1.7246 H 0 0 0 0 0 0 0 0 0\n 0.8224 2.3078 2.2707 H 0 0 0 0 0 0 0 0 0\n 1.2746 0.7385 2.9479 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 4 1 0 0 0\n 1 5 1 0 0 0\n 1 6 1 0 0 0\n 2 3 1 0 0 0\n 2 7 1 0 0 0\n 2 8 1 0 0 0\n 3 9 1 0 0 0\n 3 10 1 0 0 0\n 3 11 1 0 0 0\nM END\n"), + new TestChemical("p-Xylen", "", + "Benzene, 1,4-dimethyl-; p-Dimethylbenzene; p-Xylol; 1,4-Dimethylbenzene; 1,4-Xylene; p-Methyltoluene; para-Xylene; Chromar; Scintillar; 4-Methyltoluene; NSC 72419; 1,4-dimethyl-benzene ( p-xylene)", + "C8H10", 106.1650m, "106-42-3", + "\n\n\n 18 18 0 0 0 1 V2000\n 1.0336 0.8636 0.7240 C 0 0 0 0 0 0 0 0 0\n 1.8222 1.2612 1.9184 C 0 0 0 0 0 0 0 0 0\n 3.2175 1.2383 1.8736 C 0 0 0 0 0 0 0 0 0\n 1.1814 1.6695 3.0884 C 0 0 0 0 0 0 0 0 0\n 3.9602 1.6220 2.9826 C 0 0 0 0 0 0 0 0 0\n 1.9245 2.0535 4.1976 C 0 0 0 0 0 0 0 0 0\n 3.3192 2.0345 4.1521 C 0 0 0 0 0 0 0 0 0\n 4.1123 2.4606 5.3336 C 0 0 0 0 0 0 0 0 0\n 0.9917 1.6889 0.0000 H 0 0 0 0 0 0 0 0 0\n 0.0000 0.5972 0.9816 H 0 0 0 0 0 0 0 0 0\n 1.4818 0.0000 0.2149 H 0 0 0 0 0 0 0 0 0\n 3.7265 0.9144 0.9590 H 0 0 0 0 0 0 0 0 0\n 0.0866 1.6867 3.1337 H 0 0 0 0 0 0 0 0 0\n 5.0549 1.5997 2.9402 H 0 0 0 0 0 0 0 0 0\n 1.4139 2.3716 5.1134 H 0 0 0 0 0 0 0 0 0\n 5.0993 1.9796 5.3595 H 0 0 0 0 0 0 0 0 0\n 3.6010 2.2196 6.2750 H 0 0 0 0 0 0 0 0 0\n 4.2743 3.5470 5.3142 H 0 0 0 0 0 0 0 0 0\n 1 2 1 0 0 0\n 1 9 1 0 0 0\n 1 10 1 0 0 0\n 1 11 1 0 0 0\n 3 2 2 0 0 0\n 2 4 1 0 0 0\n 5 3 1 0 0 0\n 3 12 1 0 0 0\n 4 6 2 0 0 0\n 4 13 1 0 0 0\n 7 5 2 0 0 0\n 5 14 1 0 0 0\n 6 7 1 0 0 0\n 6 15 1 0 0 0\n 7 8 1 0 0 0\n 8 16 1 0 0 0\n 8 17 1 0 0 0\n 8 18 1 0 0 0\nM END\n"), + new TestChemical( + "Toluol", + "Toluol, Trivialname nach IUPAC auch Toluen, Methylbenzol, Phenylmethan, nach IUPAC-Nomenklatur Methylbenzen genannt, ist eine farblose, charakteristisch riechende, flüchtige Flüssigkeit, die in vielen ihrer Eigenschaften dem Benzol ähnelt. Toluol ist ein aromatischer Kohlenwasserstoff, häufig ersetzt es als Lösungsmittel das giftige Benzol. Es ist unter anderem auch im Benzin enthalten.", + "Benzene, methyl; Methacide; Methylbenzene; Methylbenzol; Phenylmethane; Antisal 1a; Toluol; Methane, phenyl-; NCI-C07272; Tolueen; Toluen; Toluolo; Rcra waste number U220; Tolu-sol; UN 1294; Dracyl; Monomethyl benzene; CP 25; NSC 406333; methylbenzene (toluene)", + "C7H8", 92.1384m, "108-88-3", + "\n\n\n 7 7 0 0 0 1 V2000\n 1.4722 1.7260 0.0000 C 0 0 0 0 0 0 0 0 0\n 0.9645 0.8630 0.0000 C 0 0 0 0 0 0 0 0 0\n 2.4874 1.7260 0.0000 C 0 0 0 0 0 0 0 0 0\n 2.9951 0.8630 0.0000 C 0 0 0 0 0 0 0 0 0\n 2.4874 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0\n 1.4722 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0\n 0.0000 0.8630 0.0000 C 0 0 0 0 0 0 0 0 0\n 1 2 2 0 0 0\n 3 1 1 0 0 0\n 2 6 1 0 0 0\n 2 7 1 0 0 0\n 4 3 2 0 0 0\n 5 4 1 0 0 0\n 6 5 2 0 0 0\nM END\n"), + new TestChemical( + "Wasser", + "Wasser (H2O) ist eine chemische Verbindung aus den Elementen Sauerstoff (O) und Wasserstoff (H). Wasser ist als Flüssigkeit durchsichtig, weitgehend farb-, geruch- und geschmacklos. Wasser ist die einzige chemische Verbindung auf der Erde, die in der Natur als Flüssigkeit, als Festkörper und als Gas vorkommt. Die Bezeichnung Wasser wird dabei für den flüssigen Aggregatzustand verwendet. Im festen Zustand spricht man von Eis, im gasförmigen Zustand von Wasserdampf. Wasser ist Grundlage des Lebens auf der Erde.", + "Water vapor; Distilled water; Ice; H2O; Dihydrogen oxide; steam; Tritiotope", "H2O", 18.0153m, "7732-18-5", + "\n\n\n 3 2 0 0 0 0 0 0 0 0999 V2000\n -0.2308 -0.3260 0.0000 O 0 0 0 0 0\n 0.7373 -0.2766 0.0000 H 0 0 0 0 0\n -0.5064 0.6026 0.0000 H 0 0 0 0 0\n 1 2 1 0 0 0\n 1 3 1 0 0 0\nM END\n") }; - - public TestChemical(string name, string description, string alternativeNames, string formula, decimal molecularWeight, string casRegistryNumber, string molFile, string rPhrases, string sPhrases) - { - Name = name; - Description = description; - AlternativeNames = alternativeNames; - Formula = formula; - MolecularWeight = molecularWeight; - CasRegistryNumber = casRegistryNumber; - MolFile = molFile; - RPhrases = rPhrases; - SPhrases = sPhrases; - } } -} +} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/TestPerson.cs b/src/abstractions/Backend.Fx/RandomData/TestPerson.cs index 34396ab2..94886676 100644 --- a/src/abstractions/Backend.Fx/RandomData/TestPerson.cs +++ b/src/abstractions/Backend.Fx/RandomData/TestPerson.cs @@ -1,10 +1,11 @@ -namespace Backend.Fx.RandomData -{ - using System; - using System.Collections.Generic; - using System.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using Backend.Fx.BuildingBlocks; - public class TestPerson +namespace Backend.Fx.RandomData +{ + public class TestPerson : ValueObject { public enum Genders { @@ -12,71 +13,72 @@ public enum Genders Female } - private static readonly Dictionary InvalidCharacterReplacements = new Dictionary { - { "À", "A" }, - { "Á", "A" }, - { "Â", "A" }, - { "Ã", "A" }, - { "Ä", "A" }, - { "Å", "A" }, - { "Æ", "A" }, - { "Ç", "C" }, - { "È", "E" }, - { "É", "E" }, - { "Ê", "E" }, - { "Ë", "E" }, - { "Ì", "I" }, - { "Í", "I" }, - { "Î", "I" }, - { "Ï", "I" }, - { "Ð", "D" }, - { "Ñ", "N" }, - { "Ò", "O" }, - { "Ó", "O" }, - { "Ô", "O" }, - { "Õ", "O" }, - { "Ö", "O" }, - { "×", "x" }, - { "Ø", "O" }, - { "Ù", "U" }, - { "Ú", "U" }, - { "Û", "U" }, - { "Ü", "U" }, - { "Ý", "Y" }, - { "Þ", "p" }, - { "ß", "ss" }, - { "à", "a" }, - { "á", "a" }, - { "â", "a" }, - { "ã", "a" }, - { "ä", "a" }, - { "å", "a" }, - { "æ", "a" }, - { "ç", "c" }, - { "è", "e" }, - { "é", "e" }, - { "ê", "e" }, - { "ë", "e" }, - { "ì", "i" }, - { "í", "i" }, - { "î", "i" }, - { "ï", "i" }, - { "ð", "o" }, - { "ñ", "n" }, - { "ò", "o" }, - { "ó", "o" }, - { "ô", "o" }, - { "õ", "o" }, - { "ö", "o" }, - { "÷", "" }, - { "ø", "o" }, - { "ù", "" }, - { "ú", "u" }, - { "û", "u" }, - { "ü", "u" }, - { "ý", "y" }, - { "þ", "p" }, - { "ÿ", "y" } + private static readonly Dictionary InvalidCharacterReplacements = new Dictionary + { + {"À", "A"}, + {"Á", "A"}, + {"Â", "A"}, + {"Ã", "A"}, + {"Ä", "A"}, + {"Å", "A"}, + {"Æ", "A"}, + {"Ç", "C"}, + {"È", "E"}, + {"É", "E"}, + {"Ê", "E"}, + {"Ë", "E"}, + {"Ì", "I"}, + {"Í", "I"}, + {"Î", "I"}, + {"Ï", "I"}, + {"Ð", "D"}, + {"Ñ", "N"}, + {"Ò", "O"}, + {"Ó", "O"}, + {"Ô", "O"}, + {"Õ", "O"}, + {"Ö", "O"}, + {"×", "x"}, + {"Ø", "O"}, + {"Ù", "U"}, + {"Ú", "U"}, + {"Û", "U"}, + {"Ü", "U"}, + {"Ý", "Y"}, + {"Þ", "p"}, + {"ß", "ss"}, + {"à", "a"}, + {"á", "a"}, + {"â", "a"}, + {"ã", "a"}, + {"ä", "a"}, + {"å", "a"}, + {"æ", "a"}, + {"ç", "c"}, + {"è", "e"}, + {"é", "e"}, + {"ê", "e"}, + {"ë", "e"}, + {"ì", "i"}, + {"í", "i"}, + {"î", "i"}, + {"ï", "i"}, + {"ð", "o"}, + {"ñ", "n"}, + {"ò", "o"}, + {"ó", "o"}, + {"ô", "o"}, + {"õ", "o"}, + {"ö", "o"}, + {"÷", ""}, + {"ø", "o"}, + {"ù", ""}, + {"ú", "u"}, + {"û", "u"}, + {"ü", "u"}, + {"ý", "y"}, + {"þ", "p"}, + {"ÿ", "y"} }; private readonly string _email; @@ -102,13 +104,11 @@ public TestPerson(string firstName, string middleName, string lastName, string t public int Age { - get { - var today = DateTime.Today; + get + { + DateTime today = DateTime.Today; var age = today.Year - DateOfBirth.Year; - if (DateOfBirth > today.AddYears(-age)) - { - age--; - } + if (DateOfBirth > today.AddYears(-age)) age--; return age; } @@ -124,12 +124,19 @@ public int Age private static string SanitizeForUserName(string s) { - s = new string((s.Where(char.IsLetterOrDigit).ToArray())); - foreach (var invalidCharacterReplacement in InvalidCharacterReplacements) - { - s = s.Replace(invalidCharacterReplacement.Key, invalidCharacterReplacement.Value); - } + s = new string(s.Where(char.IsLetterOrDigit).ToArray()); + foreach (var invalidCharacterReplacement in InvalidCharacterReplacements) s = s.Replace(invalidCharacterReplacement.Key, invalidCharacterReplacement.Value); return s; } + + protected override IEnumerable GetEqualityComponents() + { + yield return DateOfBirth; + yield return FirstName; + yield return Gender; + yield return LastName; + yield return MiddleName; + yield return Title; + } } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/TestPersonGenerator.cs b/src/abstractions/Backend.Fx/RandomData/TestPersonGenerator.cs index f4a2a93b..730e898c 100644 --- a/src/abstractions/Backend.Fx/RandomData/TestPersonGenerator.cs +++ b/src/abstractions/Backend.Fx/RandomData/TestPersonGenerator.cs @@ -1,44 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; + namespace Backend.Fx.RandomData { - using System; - using System.Collections.Generic; - - public class TestPersonGenerator + public class TestPersonGenerator : Generator { - public int FemalePercentage { get; set; } = 55; + private const int Year = 365; + private readonly Random _random = TestRandom.Instance; + private readonly HashSet _uniqueNames = new HashSet(); - public int GenerateCount { get; set; } = 100; - - public int MaximumAgeInDays { get; set; } = 36500; - - public int MinimumAgeInDays { get; set; } = 3650; + public bool EnforceUniqueNames { get; set; } = false; + public int FemalePercentage { get; set; } = 55; + public int MaximumAgeInDays { get; set; } = 80 * Year; + public int MinimumAgeInDays { get; set; } = 18 * Year; - public Random Random { get; set; } = TestRandom.Instance; + public static TestAddress Generate() + { + return new TestAddressGenerator().First(); + } - public IEnumerable GenerateTestPersons() + protected override TestPerson Next() { - var distinctCheck = new HashSet(); - for (var i = 0; i < GenerateCount; i++) + var isFemale = _random.Next(1, 100) < FemalePercentage; + TestPerson generated; + do { - var isFemale = Random.Next(1, 100) < FemalePercentage; - TestPerson generated = null; - - while (generated == null || distinctCheck.Contains(generated.LastName + generated.FirstName)) - { - generated = new TestPerson( - isFemale ? Names.Female.Random() : Names.Male.Random(), - Random.Next(100) < 30 - ? isFemale ? Names.Female.Random() : Names.Male.Random() - : "", - Names.Family.Random(), - Random.Next(100) < 20 ? "Dr." : "", - isFemale ? TestPerson.Genders.Female : TestPerson.Genders.Male, - DateTime.Now.AddDays(-Random.Next(MinimumAgeInDays, MaximumAgeInDays))); - } - - distinctCheck.Add(generated.LastName + generated.FirstName); - yield return generated; + generated = new TestPerson( + isFemale ? Names.Female.Random() : Names.Male.Random(), + _random.Next(100) < 30 + ? isFemale ? Names.Female.Random() : Names.Male.Random() + : "", + Names.Family.Random(), + _random.Next(100) < 20 ? "Dr." : "", + isFemale ? TestPerson.Genders.Female : TestPerson.Genders.Male, + DateTime.Now.AddDays(-_random.Next(MinimumAgeInDays, MaximumAgeInDays)).Date); + } while (EnforceUniqueNames && _uniqueNames.Contains($"{generated.FirstName}{generated.LastName}")); + + if (EnforceUniqueNames) + { + _uniqueNames.Add($"{generated.FirstName}{generated.LastName}"); } + + return generated; } } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/TestPhoneGenerator.cs b/src/abstractions/Backend.Fx/RandomData/TestPhoneGenerator.cs deleted file mode 100644 index 7f5fb2d7..00000000 --- a/src/abstractions/Backend.Fx/RandomData/TestPhoneGenerator.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Backend.Fx.RandomData -{ - public static class TestPhoneGenerator - { - public static string Mobile() - { - var phone = Numbers.MobileNetworks.Random(); - while (phone.Length < TestRandom.Instance.Next(11, 12)) - { - phone += Numbers.Ciphers.Random(); - } - return phone; - } - - public static string LandLine() - { - var phone = Numbers.LandLineNetworks.Random(); - while (phone.Length < TestRandom.Instance.Next(8, 11)) - { - phone += Numbers.Ciphers.Random(); - } - return phone; - } - } -} \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/RandomData/TestRandom.cs b/src/abstractions/Backend.Fx/RandomData/TestRandom.cs index 60745af6..0649468b 100644 --- a/src/abstractions/Backend.Fx/RandomData/TestRandom.cs +++ b/src/abstractions/Backend.Fx/RandomData/TestRandom.cs @@ -1,18 +1,15 @@ -namespace Backend.Fx.RandomData -{ - using System; - using System.Collections.Generic; +using System; +using System.Collections.Generic; +namespace Backend.Fx.RandomData +{ public static class TestRandom { public static Random Instance { get; set; } = new Random(429756); public static IEnumerable Next(int amount, Func generate) { - for (var i = 0; i < amount; i++) - { - yield return generate(); - } + for (var i = 0; i < amount; i++) yield return generate(); } public static int Next() @@ -30,7 +27,8 @@ public static int Next(int min, int max) return Instance.Next(min, max); } - public static bool NextBool() { + public static bool NextBool() + { return Instance.Next(2) == 1; } @@ -65,8 +63,8 @@ public static string NextPassword(int length = 10) password[i] = rnd < 60 ? letters.Random() : rnd < 90 - ? numbers.Random() - : specials.Random(); + ? numbers.Random() + : specials.Random(); } return new string(password); @@ -74,7 +72,7 @@ public static string NextPassword(int length = 10) public static decimal NextDecimal(decimal minimum, decimal maximum) { - return (decimal)Instance.NextDouble() * (maximum - minimum) + minimum; + return (decimal) Instance.NextDouble() * (maximum - minimum) + minimum; } public static bool NextProbability(int p) diff --git a/src/environments/Backend.Fx.AspNetCore.Mvc/Backend.Fx.AspNetCore.Mvc.csproj b/src/environments/Backend.Fx.AspNetCore.Mvc/Backend.Fx.AspNetCore.Mvc.csproj index 4694b7ec..644fc593 100644 --- a/src/environments/Backend.Fx.AspNetCore.Mvc/Backend.Fx.AspNetCore.Mvc.csproj +++ b/src/environments/Backend.Fx.AspNetCore.Mvc/Backend.Fx.AspNetCore.Mvc.csproj @@ -1,7 +1,7 @@ - netstandard2.0;netcoreapp3.1 + netcoreapp3.1 Library true snupkg @@ -24,16 +24,12 @@ https://github.com/marcwittke/Backend.Fx.git - + - - - - - + diff --git a/src/environments/Backend.Fx.AspNetCore.Mvc/DependencyInjection/BackendFxApplicationActivator.cs b/src/environments/Backend.Fx.AspNetCore.Mvc/DependencyInjection/BackendFxApplicationActivator.cs index b475e40c..dd085911 100644 --- a/src/environments/Backend.Fx.AspNetCore.Mvc/DependencyInjection/BackendFxApplicationActivator.cs +++ b/src/environments/Backend.Fx.AspNetCore.Mvc/DependencyInjection/BackendFxApplicationActivator.cs @@ -6,15 +6,15 @@ namespace Backend.Fx.AspNetCore.Mvc.DependencyInjection public abstract class BackendFxApplicationActivator { private readonly IBackendFxApplication _application; - + protected BackendFxApplicationActivator(IBackendFxApplication application) { _application = application; } - public object GetInstance(Type t) + protected object GetInstance(Type t) { - return _application.CompositionRoot.GetInstance(t); + return _application.CompositionRoot.InstanceProvider.GetInstance(t); } } -} +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore.Mvc/DependencyInjection/BackendFxApplicationControllerActivator.cs b/src/environments/Backend.Fx.AspNetCore.Mvc/DependencyInjection/BackendFxApplicationControllerActivator.cs index c8f2fc02..83ae9a3d 100644 --- a/src/environments/Backend.Fx.AspNetCore.Mvc/DependencyInjection/BackendFxApplicationControllerActivator.cs +++ b/src/environments/Backend.Fx.AspNetCore.Mvc/DependencyInjection/BackendFxApplicationControllerActivator.cs @@ -8,20 +8,24 @@ namespace Backend.Fx.AspNetCore.Mvc.DependencyInjection public class BackendFxApplicationControllerActivator : BackendFxApplicationActivator, IControllerActivator { private static readonly ILogger Logger = LogManager.Create(); + public BackendFxApplicationControllerActivator(IBackendFxApplication application) : base(application) - { } + { + } public virtual object Create(ControllerContext c) { - var instance = GetInstance(c.ActionDescriptor.ControllerTypeInfo.AsType()); + object instance = GetInstance(c.ActionDescriptor.ControllerTypeInfo.AsType()); if (instance == null) { Logger.Warn($"Activation of {c.ActionDescriptor.ControllerTypeInfo.AsType()} returned NULL"); } + return instance; } public virtual void Release(ControllerContext c, object controller) - { } + { + } } } \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore.Mvc/DependencyInjection/BackendFxApplicationHubActivator.cs b/src/environments/Backend.Fx.AspNetCore.Mvc/DependencyInjection/BackendFxApplicationHubActivator.cs new file mode 100644 index 00000000..225c1bda --- /dev/null +++ b/src/environments/Backend.Fx.AspNetCore.Mvc/DependencyInjection/BackendFxApplicationHubActivator.cs @@ -0,0 +1,22 @@ +using Backend.Fx.Patterns.DependencyInjection; +using Microsoft.AspNetCore.SignalR; + +namespace Backend.Fx.AspNetCore.Mvc.DependencyInjection +{ + public class BackendFxApplicationHubActivator : BackendFxApplicationActivator, IHubActivator where T : Hub + { + public BackendFxApplicationHubActivator(IBackendFxApplication application) : base(application) + { + } + + + public T Create() + { + return (T)GetInstance(typeof(T)); + } + + public void Release(T hub) + { + } + } +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore.Mvc/DependencyInjection/BackendFxApplicationViewComponentActivator.cs b/src/environments/Backend.Fx.AspNetCore.Mvc/DependencyInjection/BackendFxApplicationViewComponentActivator.cs index 8e25a782..2b72b0fd 100644 --- a/src/environments/Backend.Fx.AspNetCore.Mvc/DependencyInjection/BackendFxApplicationViewComponentActivator.cs +++ b/src/environments/Backend.Fx.AspNetCore.Mvc/DependencyInjection/BackendFxApplicationViewComponentActivator.cs @@ -6,7 +6,8 @@ namespace Backend.Fx.AspNetCore.Mvc.DependencyInjection public class BackendFxApplicationViewComponentActivator : BackendFxApplicationActivator, IViewComponentActivator { public BackendFxApplicationViewComponentActivator(IBackendFxApplication application) : base(application) - { } + { + } public object Create(ViewComponentContext context) { @@ -14,6 +15,7 @@ public object Create(ViewComponentContext context) } public void Release(ViewComponentContext context, object viewComponent) - { } + { + } } } \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore.Mvc/ErrorHandling/ExceptionLoggingFilter.cs b/src/environments/Backend.Fx.AspNetCore.Mvc/ErrorHandling/ExceptionLoggingFilter.cs deleted file mode 100644 index d33a56bc..00000000 --- a/src/environments/Backend.Fx.AspNetCore.Mvc/ErrorHandling/ExceptionLoggingFilter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Backend.Fx.Logging; -using Microsoft.AspNetCore.Mvc.Filters; - -namespace Backend.Fx.AspNetCore.Mvc.ErrorHandling -{ - public class ExceptionLoggingFilter : IActionFilter - { - public void OnActionExecuting(ActionExecutingContext context) - { } - - public void OnActionExecuted(ActionExecutedContext context) - { - if (context.Exception != null && !context.HttpContext.Items.ContainsKey("ExceptionLogged")) - { - ILogger logger = LogManager.Create(context.Controller.GetType().FullName); - IExceptionLogger exceptionLogger = new ExceptionLogger(logger); - exceptionLogger.LogException(context.Exception); - } - } - } -} diff --git a/src/environments/Backend.Fx.AspNetCore.Mvc/Throttling/ThrottlingBaseAttribute.cs b/src/environments/Backend.Fx.AspNetCore.Mvc/Throttling/ThrottlingBaseAttribute.cs index c690bac3..b10e2ee2 100644 --- a/src/environments/Backend.Fx.AspNetCore.Mvc/Throttling/ThrottlingBaseAttribute.cs +++ b/src/environments/Backend.Fx.AspNetCore.Mvc/Throttling/ThrottlingBaseAttribute.cs @@ -32,4 +32,4 @@ protected virtual int CalculateRepeatedTimeoutFactor(int repetition) return 1; } } -} +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore.Mvc/UnitOfWork/UnitOfWorkFilter.cs b/src/environments/Backend.Fx.AspNetCore.Mvc/UnitOfWork/UnitOfWorkFilter.cs deleted file mode 100644 index d1f16f6c..00000000 --- a/src/environments/Backend.Fx.AspNetCore.Mvc/UnitOfWork/UnitOfWorkFilter.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.Patterns.UnitOfWork; -using Microsoft.AspNetCore.Mvc.Filters; - -namespace Backend.Fx.AspNetCore.Mvc.UnitOfWork -{ - public class UnitOfWorkFilter : IActionFilter - { - private static readonly ILogger Logger = LogManager.Create(); - private readonly IBackendFxApplication _application; - - public UnitOfWorkFilter(IBackendFxApplication application) - { - _application = application; - } - - public void OnActionExecuting(ActionExecutingContext context) - { - var unitOfWork = _application.CompositionRoot.GetInstance(); - unitOfWork.Begin(); - } - - public void OnActionExecuted(ActionExecutedContext context) - { - var unitOfWork = _application.CompositionRoot.GetInstance(); - - if (context.Exception == null) - { - unitOfWork.Complete(); - } - else - { - Logger.Warn($"Preventing unit of work completion due to {context.Exception.GetType().Name}: {context.Exception.Message}"); - } - } - } -} diff --git a/src/environments/Backend.Fx.AspNetCore.Mvc/Validation/ModelStateEx.cs b/src/environments/Backend.Fx.AspNetCore.Mvc/Validation/ModelStateEx.cs index 2abeddbf..b06d1c4f 100644 --- a/src/environments/Backend.Fx.AspNetCore.Mvc/Validation/ModelStateEx.cs +++ b/src/environments/Backend.Fx.AspNetCore.Mvc/Validation/ModelStateEx.cs @@ -17,7 +17,7 @@ public static string ToDebugString(this ModelStateDictionary modelState) public static Errors ToErrorsDictionary(this ModelStateDictionary modelState) { - Errors errors = new Errors(); + var errors = new Errors(); foreach (var keyValuePair in modelState) { errors.Add(keyValuePair.Key, keyValuePair.Value.Errors.Select(err => err.ErrorMessage)); @@ -26,7 +26,7 @@ public static Errors ToErrorsDictionary(this ModelStateDictionary modelState) return errors; } - + public static void Add(this ModelStateDictionary modelState, Errors errors) { foreach (var keyValuePair in errors) @@ -38,4 +38,4 @@ public static void Add(this ModelStateDictionary modelState, Errors errors) } } } -} +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore.Mvc/Validation/ModelValidationFilter.cs b/src/environments/Backend.Fx.AspNetCore.Mvc/Validation/ModelValidationFilter.cs index 6cb428f4..ebba74b3 100644 --- a/src/environments/Backend.Fx.AspNetCore.Mvc/Validation/ModelValidationFilter.cs +++ b/src/environments/Backend.Fx.AspNetCore.Mvc/Validation/ModelValidationFilter.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Backend.Fx.Exceptions; using Backend.Fx.Logging; @@ -12,10 +13,12 @@ public abstract class ModelValidationFilter : IActionFilter { public abstract void OnActionExecuting(ActionExecutingContext context); public abstract void OnActionExecuted(ActionExecutedContext context); - + protected void LogErrors(FilterContext context, string controllerName, Errors errors) { - ILogger logger = LogManager.Create(controllerName); + ILogger logger = TryGetControllerType(controllerName, out Type controllerType) + ? LogManager.Create(controllerType) + : LogManager.Create(); logger.Warn($"Model validation failed during {context.HttpContext.Request.Method} {context.HttpContext.Request.PathBase}: " + string.Join(System.Environment.NewLine, errors)); } @@ -31,5 +34,20 @@ protected bool AcceptsHtml(FilterContext context) IList accept = context.HttpContext.Request.GetTypedHeaders().Accept; return accept?.Any(mth => mth.Type == "text" && mth.SubType == "html") == true; } + + private static bool TryGetControllerType(string controllerName, out Type type) + { + try + { + type = Type.GetType(controllerName); + } + catch + { + type = null; + } + + return type != null; + } + } } \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore.Mvc/Validation/RedirectBackToGetActionModelValidationFilter.cs b/src/environments/Backend.Fx.AspNetCore.Mvc/Validation/RedirectBackToGetActionModelValidationFilter.cs index ffe38c52..851e0b22 100644 --- a/src/environments/Backend.Fx.AspNetCore.Mvc/Validation/RedirectBackToGetActionModelValidationFilter.cs +++ b/src/environments/Backend.Fx.AspNetCore.Mvc/Validation/RedirectBackToGetActionModelValidationFilter.cs @@ -29,7 +29,6 @@ public override void OnActionExecuting(ActionExecutingContext context) { ViewName = context.RouteData.Values["action"].ToString(), ViewData = viewData, - }; } } @@ -46,14 +45,15 @@ public override void OnActionExecuted(ActionExecutedContext context) BeforeRedirect(viewData); context.Result = new ViewResult { - ViewName = context.RouteData.Values["action"].ToString(), - ViewData = viewData, - + ViewName = context.RouteData.Values["action"].ToString(), + ViewData = viewData, }; context.ExceptionHandled = true; } } - protected virtual void BeforeRedirect(ViewDataDictionary viewData) { } + protected virtual void BeforeRedirect(ViewDataDictionary viewData) + { + } } } \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore.Mvc/Validation/ReturnModelStateAsJsonModelValidationFilter.cs b/src/environments/Backend.Fx.AspNetCore.Mvc/Validation/ReturnModelStateAsJsonModelValidationFilter.cs index fc429faf..b7d0ea5b 100644 --- a/src/environments/Backend.Fx.AspNetCore.Mvc/Validation/ReturnModelStateAsJsonModelValidationFilter.cs +++ b/src/environments/Backend.Fx.AspNetCore.Mvc/Validation/ReturnModelStateAsJsonModelValidationFilter.cs @@ -22,6 +22,7 @@ protected virtual IActionResult CreateResult(Errors errors) } public override void OnActionExecuted(ActionExecutedContext context) - {} + { + } } } \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Backend.Fx.AspNetCore.csproj b/src/environments/Backend.Fx.AspNetCore/Backend.Fx.AspNetCore.csproj index ffbb97b9..5e448048 100644 --- a/src/environments/Backend.Fx.AspNetCore/Backend.Fx.AspNetCore.csproj +++ b/src/environments/Backend.Fx.AspNetCore/Backend.Fx.AspNetCore.csproj @@ -1,7 +1,7 @@ - netstandard2.0;netcoreapp3.1 + netcoreapp3.1 Library true snupkg @@ -24,21 +24,16 @@ https://github.com/marcwittke/Backend.Fx.git - + - + - - - - - diff --git a/src/environments/Backend.Fx.AspNetCore/BackendFxApplicationIntegration.cs b/src/environments/Backend.Fx.AspNetCore/BackendFxApplicationIntegration.cs new file mode 100644 index 00000000..4e40927c --- /dev/null +++ b/src/environments/Backend.Fx.AspNetCore/BackendFxApplicationIntegration.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.AspNetCore.Scoping; +using Backend.Fx.Logging; +using Backend.Fx.Patterns.DependencyInjection; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Backend.Fx.AspNetCore +{ + public static class BackendFxApplicationIntegration + { + private static readonly ILogger Logger = LogManager.Create(typeof(BackendFxApplicationIntegration)); + + public static void AddBackendFxApplication(this IServiceCollection services, IBackendFxApplication application) + { + services.AddSingleton(application); + } + + public static void UseBackendFxApplication(this IApplicationBuilder app) + where TBackendFxMiddleware : IBackendFxMiddleware + { + app.UseMiddleware(); + + // booting the application when web host enters started phase + app.ApplicationServices + .GetRequiredService() + .ApplicationStarted + .Register(async () => + { + try + { + var application = app.ApplicationServices.GetService(); + + if (SynchronizationContext.Current == null) + { + // normal ASP.Net Core environment does not have a synchronization context, + // no problem with await here, it will be executed on the thread pool + await application.Boot(); + } + else + { + // xunit uses it's own SynchronizationContext that allows a maximum thread count + // equal to the logical cpu count (that is 1 on our single cpu build agents). So + // when we're trying to await something here, the task gets scheduled to xunit + // synchronization context, which is already at it's limit running the test thread + // so we end up in a deadlock here. + // solution is to run the await explicitly on the thread pool by using Task.Run + Task.Run(() => application.Boot()).Wait(); + } + + Logger.Info("Application startup finished successfully"); + } + catch (Exception ex) + { + Logger.Fatal(ex, "Application could not be started"); + app.ApplicationServices.GetRequiredService().StopApplication(); + } + }); + + // backendFxApplication should be gracefully disposed on shutdown + app.ApplicationServices + .GetRequiredService() + .ApplicationStopping + .Register(app.ApplicationServices.GetService().Dispose); + } + } +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Bootstrapping/WaitForBootMiddleware.cs b/src/environments/Backend.Fx.AspNetCore/Bootstrapping/WaitForBootMiddleware.cs index 38c6bf89..7c8de079 100644 --- a/src/environments/Backend.Fx.AspNetCore/Bootstrapping/WaitForBootMiddleware.cs +++ b/src/environments/Backend.Fx.AspNetCore/Bootstrapping/WaitForBootMiddleware.cs @@ -32,7 +32,8 @@ public async Task Invoke(HttpContext context) { Logger.Info("Queuing Request while application is booting..."); } + await _next.Invoke(context); } } -} +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Configuration/ConfigurationEx.cs b/src/environments/Backend.Fx.AspNetCore/Configuration/ConfigurationEx.cs index e76fbd16..d01f9edf 100644 --- a/src/environments/Backend.Fx.AspNetCore/Configuration/ConfigurationEx.cs +++ b/src/environments/Backend.Fx.AspNetCore/Configuration/ConfigurationEx.cs @@ -1,16 +1,16 @@ -namespace Backend.Fx.AspNetCore.Configuration -{ - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.Options; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +namespace Backend.Fx.AspNetCore.Configuration +{ public static class ConfigurationEx { public static TOptions Load(this IConfiguration configuration) where TOptions : class, new() { - var configurationSection = configuration.GetSection(typeof(TOptions).Name); + IConfigurationSection configurationSection = configuration.GetSection(typeof(TOptions).Name); var configurationOptions = new NamedConfigureFromConfigurationOptions( - typeof(TOptions).Name, - configurationSection); + typeof(TOptions).Name, + configurationSection); var options = new TOptions(); configurationOptions.Action(options); return options; diff --git a/src/environments/Backend.Fx.AspNetCore/ErrorHandling/ErrorHandlingMiddleware.cs b/src/environments/Backend.Fx.AspNetCore/ErrorHandling/ErrorHandlingMiddleware.cs index 4509e9c8..c8bfdf9e 100644 --- a/src/environments/Backend.Fx.AspNetCore/ErrorHandling/ErrorHandlingMiddleware.cs +++ b/src/environments/Backend.Fx.AspNetCore/ErrorHandling/ErrorHandlingMiddleware.cs @@ -1,17 +1,17 @@ -namespace Backend.Fx.AspNetCore.ErrorHandling -{ - using System; - using System.Globalization; - using System.Net; - using System.Threading.Tasks; - using Exceptions; - using JetBrains.Annotations; - using Microsoft.AspNetCore.Http; +using System; +using System.Globalization; +using System.Net; +using System.Threading.Tasks; +using Backend.Fx.Exceptions; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Http; +namespace Backend.Fx.AspNetCore.ErrorHandling +{ public abstract class ErrorHandlingMiddleware { private readonly RequestDelegate _next; - + /// /// This constructor is being called by the framework DI container /// @@ -48,23 +48,23 @@ public async Task Invoke(HttpContext context) } catch (NotFoundException nfex) { - await HandleClientError(context, (int)HttpStatusCode.NotFound, HttpStatusCode.NotFound.ToString(), nfex); + await HandleClientError(context, (int) HttpStatusCode.NotFound, HttpStatusCode.NotFound.ToString(), nfex); } catch (ConflictedException confex) { - await HandleClientError(context, (int)HttpStatusCode.Conflict, HttpStatusCode.Conflict.ToString(), confex); + await HandleClientError(context, (int) HttpStatusCode.Conflict, HttpStatusCode.Conflict.ToString(), confex); } catch (ForbiddenException uex) { - await HandleClientError(context, (int)HttpStatusCode.Forbidden, HttpStatusCode.Forbidden.ToString(), uex); + await HandleClientError(context, (int) HttpStatusCode.Forbidden, HttpStatusCode.Forbidden.ToString(), uex); } catch (UnauthorizedException uex) { - await HandleClientError(context, (int)HttpStatusCode.Unauthorized, HttpStatusCode.Unauthorized.ToString(), uex); + await HandleClientError(context, (int) HttpStatusCode.Unauthorized, HttpStatusCode.Unauthorized.ToString(), uex); } catch (ClientException cex) { - await HandleClientError(context, (int)HttpStatusCode.BadRequest, HttpStatusCode.BadRequest.ToString(), cex); + await HandleClientError(context, (int) HttpStatusCode.BadRequest, HttpStatusCode.BadRequest.ToString(), cex); } catch (Exception ex) { @@ -83,5 +83,4 @@ public async Task Invoke(HttpContext context) protected abstract Task HandleServerError(HttpContext context, Exception exception); } - -} +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/ErrorHandling/ErrorLoggingMiddleware.cs b/src/environments/Backend.Fx.AspNetCore/ErrorHandling/ErrorLoggingMiddleware.cs index 04b9f5b8..b384b1e1 100644 --- a/src/environments/Backend.Fx.AspNetCore/ErrorHandling/ErrorLoggingMiddleware.cs +++ b/src/environments/Backend.Fx.AspNetCore/ErrorHandling/ErrorLoggingMiddleware.cs @@ -1,11 +1,11 @@ -namespace Backend.Fx.AspNetCore.ErrorHandling -{ - using System; - using System.Threading.Tasks; - using JetBrains.Annotations; - using Logging; - using Microsoft.AspNetCore.Http; +using System; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Backend.Fx.Logging; +using Microsoft.AspNetCore.Http; +namespace Backend.Fx.AspNetCore.ErrorHandling +{ public class ErrorLoggingMiddleware { private readonly RequestDelegate _next; @@ -27,13 +27,14 @@ public async Task Invoke(HttpContext context) } catch (Exception exception) { - if (!context.Items.ContainsKey("ExceptionLogged")) + if (!context.Items.ContainsKey("ExceptionLogged")) { _exceptionLogger.LogException(exception); context.Items["ExceptionLogged"] = true; } + throw; } } } -} +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/ErrorHandling/JsonErrorHandlingMiddleware.cs b/src/environments/Backend.Fx.AspNetCore/ErrorHandling/JsonErrorHandlingMiddleware.cs index 885a8755..506579ac 100644 --- a/src/environments/Backend.Fx.AspNetCore/ErrorHandling/JsonErrorHandlingMiddleware.cs +++ b/src/environments/Backend.Fx.AspNetCore/ErrorHandling/JsonErrorHandlingMiddleware.cs @@ -1,17 +1,17 @@ -namespace Backend.Fx.AspNetCore.ErrorHandling -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Net; - using System.Threading.Tasks; - using Exceptions; - using Logging; - using Microsoft.AspNetCore.Http; - using Microsoft.Net.Http.Headers; - using Newtonsoft.Json; - using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Backend.Fx.Exceptions; +using Backend.Fx.Logging; +using Microsoft.AspNetCore.Http; +using Microsoft.Net.Http.Headers; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +namespace Backend.Fx.AspNetCore.ErrorHandling +{ public class JsonErrorHandlingMiddleware : ErrorHandlingMiddleware { private readonly bool _showInternalServerErrorDetails; @@ -25,7 +25,7 @@ public class JsonErrorHandlingMiddleware : ErrorHandlingMiddleware }, }; - public JsonErrorHandlingMiddleware(RequestDelegate next, bool showInternalServerErrorDetails) + public JsonErrorHandlingMiddleware(RequestDelegate next, bool showInternalServerErrorDetails) : base(next) { _showInternalServerErrorDetails = showInternalServerErrorDetails; @@ -45,12 +45,12 @@ protected override async Task HandleClientError(HttpContext context, int httpSta Logger.Warn("exception cannot be handled correctly, because the response has already started"); return; } - + // convention: only the errors array will be transmitted to the client, allowing technical (possibly // revealing) information in the exception message. Errors errors = exception.HasErrors() - ? exception.Errors - : new Errors().Add($"HTTP{httpStatusCode}: {message}"); + ? exception.Errors + : new Errors().Add($"HTTP{httpStatusCode}: {message}"); context.Response.StatusCode = httpStatusCode; string serializedErrors = SerializeErrors(errors); @@ -65,10 +65,11 @@ protected override async Task HandleServerError(HttpContext context, Exception e Logger.Warn("exception cannot be handled correctly, because the response has already started"); return; } + context.Response.StatusCode = (int) HttpStatusCode.InternalServerError; var responseContent = _showInternalServerErrorDetails - ? JsonConvert.SerializeObject(new { message = exception.Message, stackTrace = exception.StackTrace }, JsonSerializerSettings) - : JsonConvert.SerializeObject(new { message = "An internal error occured" }, JsonSerializerSettings); + ? JsonConvert.SerializeObject(new {message = exception.Message, stackTrace = exception.StackTrace}, JsonSerializerSettings) + : JsonConvert.SerializeObject(new {message = "An internal error occured"}, JsonSerializerSettings); context.Response.ContentType = "application/json; charset=utf-8"; await context.Response.WriteAsync(responseContent); } @@ -79,4 +80,4 @@ protected virtual string SerializeErrors(Errors errors) return JsonConvert.SerializeObject(errorsDictionaryForJson, JsonSerializerSettings); } } -} +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/HttpRequestEx.cs b/src/environments/Backend.Fx.AspNetCore/HttpRequestEx.cs index 297d3ecf..1d9e01fb 100644 --- a/src/environments/Backend.Fx.AspNetCore/HttpRequestEx.cs +++ b/src/environments/Backend.Fx.AspNetCore/HttpRequestEx.cs @@ -10,4 +10,4 @@ public static bool IsSafe(this HttpRequest request) return method == "OPTIONS" || method == "GET" || method == "HEAD"; } } -} +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Scoping/BackendFxMiddleware.cs b/src/environments/Backend.Fx.AspNetCore/Scoping/BackendFxMiddleware.cs new file mode 100644 index 00000000..d5d57774 --- /dev/null +++ b/src/environments/Backend.Fx.AspNetCore/Scoping/BackendFxMiddleware.cs @@ -0,0 +1,37 @@ +using System.Security.Principal; +using System.Threading.Tasks; +using Backend.Fx.Environment.MultiTenancy; +using Backend.Fx.Patterns.DependencyInjection; +using Microsoft.AspNetCore.Http; + +namespace Backend.Fx.AspNetCore.Scoping +{ + public interface IBackendFxMiddleware : IMiddleware + { + } + + public abstract class BackendFxMiddleware : IBackendFxMiddleware + { + private readonly IBackendFxApplicationAsyncInvoker _invoker; + + protected BackendFxMiddleware(IBackendFxApplicationAsyncInvoker invoker) + { + _invoker = invoker; + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + TenantId tenantId = FindMatchingTenantId(context); + IIdentity identity = context.User.Identity; + + await _invoker.InvokeAsync(_ => next.Invoke(context), identity, tenantId); + } + + protected abstract TenantId FindMatchingTenantId(HttpContext context); + + protected virtual IIdentity GetIdentity(HttpContext context) + { + return context.User.Identity; + } + } +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Scoping/HoldCurrentMiddleware.cs b/src/environments/Backend.Fx.AspNetCore/Scoping/HoldCurrentMiddleware.cs index ddc7addc..8962dcae 100644 --- a/src/environments/Backend.Fx.AspNetCore/Scoping/HoldCurrentMiddleware.cs +++ b/src/environments/Backend.Fx.AspNetCore/Scoping/HoldCurrentMiddleware.cs @@ -8,7 +8,7 @@ namespace Backend.Fx.AspNetCore.Scoping public abstract class HoldCurrentMiddleware : IMiddleware where T : class { private readonly ICurrentTHolder _currentTHolder; - + [UsedImplicitly] protected HoldCurrentMiddleware(ICurrentTHolder currentTHolder) { @@ -24,4 +24,4 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next) protected abstract T GetCurrent(HttpContext context); } -} +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Security/SecurityHeadersMiddleware.cs b/src/environments/Backend.Fx.AspNetCore/Security/SecurityHeadersMiddleware.cs index 6cb591b9..595bc347 100644 --- a/src/environments/Backend.Fx.AspNetCore/Security/SecurityHeadersMiddleware.cs +++ b/src/environments/Backend.Fx.AspNetCore/Security/SecurityHeadersMiddleware.cs @@ -1,13 +1,12 @@ -namespace Backend.Fx.AspNetCore.Security +using System; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; + +namespace Backend.Fx.AspNetCore.Security { - using System; - using System.Threading.Tasks; - using JetBrains.Annotations; - using Microsoft.AspNetCore.Http; - using Microsoft.Extensions.Options; - using Microsoft.Extensions.Primitives; - - public class SecurityHeadersMiddleware { private readonly RequestDelegate _next; @@ -24,12 +23,10 @@ public SecurityHeadersMiddleware(RequestDelegate next, IOptions 0) { - string cspHeaderKey = csp.ReportOnly ? - "Content-Security-Policy-Report-Only" : - "Content-Security-Policy"; + string cspHeaderKey = csp.ReportOnly ? "Content-Security-Policy-Report-Only" : "Content-Security-Policy"; string completeCsp = csp.ContentSecurityPolicy; @@ -59,4 +56,4 @@ protected virtual bool ShouldAppendReportUri(HttpContext context) return true; } } -} +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Versioning/VersionHeaderMiddleware.cs b/src/environments/Backend.Fx.AspNetCore/Versioning/VersionHeaderMiddleware.cs index 8def0469..ea4908e8 100644 --- a/src/environments/Backend.Fx.AspNetCore/Versioning/VersionHeaderMiddleware.cs +++ b/src/environments/Backend.Fx.AspNetCore/Versioning/VersionHeaderMiddleware.cs @@ -1,18 +1,42 @@ -namespace Backend.Fx.AspNetCore.Versioning -{ - using System.Reflection; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Http; - using Microsoft.Extensions.Primitives; +using System; +using System.Reflection; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; - public class VersionHeaderMiddleware : IMiddleware +namespace Backend.Fx.AspNetCore.Versioning +{ + [UsedImplicitly] + public class VersionHeaderMiddleware { - private readonly AssemblyName _entryAssemblyName = Assembly.GetEntryAssembly().GetName(); + private readonly RequestDelegate _next; + private readonly string _assemblyName; + private readonly string _version; + + public VersionHeaderMiddleware(RequestDelegate next) + { + _next = next; + var entryAssembly = Assembly.GetEntryAssembly(); + if (entryAssembly == null) + { + throw new InvalidOperationException("Unable to determine the entry assembly. The Version Header Middleware cannot be used in this environment"); + } + + AssemblyName entryAssemblyName = entryAssembly.GetName(); + if (entryAssemblyName.Version == null) + { + throw new InvalidOperationException("Unable to determine the version of the entry assembly. The Version Header Middleware cannot be used in this environment"); + } + + _assemblyName = entryAssemblyName.Name; + _version = entryAssemblyName.Version.ToString(3); + } - public async Task InvokeAsync(HttpContext context, RequestDelegate next) + public async Task InvokeAsync(HttpContext context) { - context.Response.Headers.Add(_entryAssemblyName.Name, new StringValues(_entryAssemblyName.Version.ToString(3))); - await next.Invoke(context); + context.Response.Headers.Add(_assemblyName, new StringValues(_version)); + await _next.Invoke(context); } } } \ No newline at end of file diff --git a/src/environments/Backend.Fx.NetCore/Backend.Fx.NetCore.csproj b/src/environments/Backend.Fx.NetCore/Backend.Fx.NetCore.csproj index 04c79e46..d472b275 100644 --- a/src/environments/Backend.Fx.NetCore/Backend.Fx.NetCore.csproj +++ b/src/environments/Backend.Fx.NetCore/Backend.Fx.NetCore.csproj @@ -25,7 +25,7 @@ - + diff --git a/src/environments/Backend.Fx.NetCore/Logging/BackendFxLoggerProvider.cs b/src/environments/Backend.Fx.NetCore/Logging/BackendFxLoggerProvider.cs index df39d27d..4b9d7ead 100644 --- a/src/environments/Backend.Fx.NetCore/Logging/BackendFxLoggerProvider.cs +++ b/src/environments/Backend.Fx.NetCore/Logging/BackendFxLoggerProvider.cs @@ -12,6 +12,8 @@ public Microsoft.Extensions.Logging.ILogger CreateLogger(string name) return new FrameworkToBackendFxLogger(LogManager.Create(name)); } - public void Dispose() { } + public void Dispose() + { + } } } \ No newline at end of file diff --git a/src/environments/Backend.Fx.NetCore/Logging/FrameworkToBackendFxLogger.cs b/src/environments/Backend.Fx.NetCore/Logging/FrameworkToBackendFxLogger.cs index 699f811b..a7336d7a 100644 --- a/src/environments/Backend.Fx.NetCore/Logging/FrameworkToBackendFxLogger.cs +++ b/src/environments/Backend.Fx.NetCore/Logging/FrameworkToBackendFxLogger.cs @@ -11,7 +11,7 @@ namespace Backend.Fx.NetCore.Logging public class FrameworkToBackendFxLogger : NetFxILogger { private readonly ILogger _logger; - + public FrameworkToBackendFxLogger(ILogger logger) { _logger = logger; @@ -19,25 +19,28 @@ public FrameworkToBackendFxLogger(ILogger logger) public void Log(NetFxLogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, TState state, Exception exception, Func formatter) { + var formatted = formatter(state, exception); + formatted = formatted?.Replace("{", "{{").Replace("}", "}}"); + switch (logLevel) { case NetFxLogLevel.Critical: - _logger.Fatal(exception, formatter(state, exception)); + _logger.Fatal(exception, formatted); return; case NetFxLogLevel.Debug: - _logger.Debug(exception, formatter(state, exception)); + _logger.Debug(exception, formatted); return; case NetFxLogLevel.Error: - _logger.Error(exception, formatter(state, exception)); + _logger.Error(exception, formatted); return; case NetFxLogLevel.Information: - _logger.Info(exception, formatter(state, exception)); + _logger.Info(exception, formatted); return; case NetFxLogLevel.Trace: - _logger.Trace(exception, formatter(state, exception)); + _logger.Trace(exception, formatted); return; case NetFxLogLevel.Warning: - _logger.Warn(exception, formatter(state, exception)); + _logger.Warn(exception, formatted); return; default: return; @@ -65,4 +68,4 @@ public IDisposable BeginScope(TState state) return _logger.InfoDuration(state.ToString()); } } -} +} \ No newline at end of file diff --git a/src/environments/Backend.Fx.NetCore/Logging/FrameworkToBackendFxLoggerFactory.cs b/src/environments/Backend.Fx.NetCore/Logging/FrameworkToBackendFxLoggerFactory.cs index 2d723ded..eb2de97f 100644 --- a/src/environments/Backend.Fx.NetCore/Logging/FrameworkToBackendFxLoggerFactory.cs +++ b/src/environments/Backend.Fx.NetCore/Logging/FrameworkToBackendFxLoggerFactory.cs @@ -6,7 +6,9 @@ namespace Backend.Fx.NetCore.Logging [DebuggerStepThrough] public class FrameworkToBackendFxLoggerFactory : Microsoft.Extensions.Logging.ILoggerFactory { - public void Dispose() { } + public void Dispose() + { + } public Microsoft.Extensions.Logging.ILogger CreateLogger(string categoryName) { @@ -14,6 +16,7 @@ public Microsoft.Extensions.Logging.ILogger CreateLogger(string categoryName) } public void AddProvider(Microsoft.Extensions.Logging.ILoggerProvider provider) - { } + { + } } -} +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/AggregateMapping.cs b/src/implementations/Backend.Fx.EfCorePersistence/AggregateMapping.cs index 1afc4561..a7ce812b 100644 --- a/src/implementations/Backend.Fx.EfCorePersistence/AggregateMapping.cs +++ b/src/implementations/Backend.Fx.EfCorePersistence/AggregateMapping.cs @@ -1,11 +1,11 @@ -namespace Backend.Fx.EfCorePersistence -{ - using System; - using System.Collections.Generic; - using System.Linq.Expressions; - using BuildingBlocks; - using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using Backend.Fx.BuildingBlocks; +using Microsoft.EntityFrameworkCore; +namespace Backend.Fx.EfCorePersistence +{ public abstract class AggregateMapping : IAggregateMapping where T : AggregateRoot { public abstract IEnumerable>> IncludeDefinitions { get; } diff --git a/src/implementations/Backend.Fx.EfCorePersistence/Backend.Fx.EfCorePersistence.csproj b/src/implementations/Backend.Fx.EfCorePersistence/Backend.Fx.EfCorePersistence.csproj index bbdc5989..4e63596d 100644 --- a/src/implementations/Backend.Fx.EfCorePersistence/Backend.Fx.EfCorePersistence.csproj +++ b/src/implementations/Backend.Fx.EfCorePersistence/Backend.Fx.EfCorePersistence.csproj @@ -24,9 +24,9 @@ - - - + + + diff --git a/src/implementations/Backend.Fx.EfCorePersistence/Bootstrapping/DbContextTransactionOperationDecorator.cs b/src/implementations/Backend.Fx.EfCorePersistence/Bootstrapping/DbContextTransactionOperationDecorator.cs new file mode 100644 index 00000000..c52b9404 --- /dev/null +++ b/src/implementations/Backend.Fx.EfCorePersistence/Bootstrapping/DbContextTransactionOperationDecorator.cs @@ -0,0 +1,26 @@ +using System.Data; +using System.Data.Common; +using Backend.Fx.Environment.Persistence; +using Backend.Fx.Patterns.DependencyInjection; +using Microsoft.EntityFrameworkCore; + +namespace Backend.Fx.EfCorePersistence.Bootstrapping +{ + public class DbContextTransactionOperationDecorator : DbTransactionOperationDecorator + { + private readonly DbContext _dbContext; + + public DbContextTransactionOperationDecorator(DbContext dbContext, IDbConnection dbConnection, IOperation operation) + : base(dbConnection, operation) + { + _dbContext = dbContext; + } + + public override void Begin() + { + base.Begin(); + _dbContext.Database.UseTransaction((DbTransaction) CurrentTransaction); + + } + } +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/Bootstrapping/EfCorePersistenceModule.cs b/src/implementations/Backend.Fx.EfCorePersistence/Bootstrapping/EfCorePersistenceModule.cs new file mode 100644 index 00000000..c8db9817 --- /dev/null +++ b/src/implementations/Backend.Fx.EfCorePersistence/Bootstrapping/EfCorePersistenceModule.cs @@ -0,0 +1,73 @@ +using System; +using System.Data; +using System.Linq; +using System.Reflection; +using Backend.Fx.BuildingBlocks; +using Backend.Fx.Environment.Persistence; +using Backend.Fx.Patterns.DependencyInjection; +using Backend.Fx.Patterns.EventAggregation.Domain; +using Backend.Fx.Patterns.IdGeneration; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Backend.Fx.EfCorePersistence.Bootstrapping +{ + public class EfCorePersistenceModule : IModule + where TDbContext : DbContext + { + private readonly ILoggerFactory _loggerFactory; + private readonly Action, IDbConnection> _configure; + private readonly IDbConnectionFactory _dbConnectionFactory; + private readonly IEntityIdGenerator _entityIdGenerator; + private readonly Assembly[] _assemblies; + + public EfCorePersistenceModule(IDbConnectionFactory dbConnectionFactory, IEntityIdGenerator entityIdGenerator, + ILoggerFactory loggerFactory, Action, IDbConnection> configure, params Assembly[] assemblies) + { + _dbConnectionFactory = dbConnectionFactory; + _entityIdGenerator = entityIdGenerator; + _loggerFactory = loggerFactory; + _configure = configure; + _assemblies = assemblies; + } + + public void Register(ICompositionRoot compositionRoot) + { + // by letting the container create the connection we can be sure, that only one connection per scope is used, and disposing is done accordingly + compositionRoot.InfrastructureModule.RegisterScoped(() => _dbConnectionFactory.Create()); + + // singleton id generator + compositionRoot.InfrastructureModule.RegisterInstance(_entityIdGenerator); + + // EF core requires us to flush frequently, because of a missing identity map + compositionRoot.InfrastructureModule.RegisterScoped(); + + // EF Repositories + compositionRoot.InfrastructureModule.RegisterScoped(typeof(IRepository<>), typeof(EfRepository<>)); + + // IQueryable is supported, but should be use with caution, since it bypasses authorization + compositionRoot.InfrastructureModule.RegisterScoped(typeof(IQueryable<>), typeof(EntityQueryable<>)); + + // DbContext is injected into repositories + compositionRoot.InfrastructureModule.RegisterScoped(() => CreateDbContextOptions(compositionRoot.InstanceProvider.GetInstance())); + compositionRoot.InfrastructureModule.RegisterScoped(); + + // wrapping the operation: connection.open - transaction.begin - operation - (flush) - transaction.commit - connection.close + compositionRoot.InfrastructureModule.RegisterDecorator(); + compositionRoot.InfrastructureModule.RegisterDecorator(); + compositionRoot.InfrastructureModule.RegisterDecorator(); + + // ensure everything dirty is flushed to the db before handling domain events + compositionRoot.InfrastructureModule.RegisterDecorator(); + + compositionRoot.InfrastructureModule.RegisterScoped(typeof(IAggregateMapping<>), _assemblies); + } + + protected virtual DbContextOptions CreateDbContextOptions(IDbConnection connection) + { + var dbContextOptionsBuilder = new DbContextOptionsBuilder(); + _configure.Invoke(dbContextOptionsBuilder, connection); + return dbContextOptionsBuilder.UseLoggerFactory(_loggerFactory).Options; + } + } +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/DbContextExtensions.cs b/src/implementations/Backend.Fx.EfCorePersistence/DbContextExtensions.cs index 90ad8912..bcc42d2f 100644 --- a/src/implementations/Backend.Fx.EfCorePersistence/DbContextExtensions.cs +++ b/src/implementations/Backend.Fx.EfCorePersistence/DbContextExtensions.cs @@ -22,7 +22,7 @@ public static void DisableChangeTracking(this DbContext dbContext) dbContext.ChangeTracker.AutoDetectChangesEnabled = false; dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; } - + public static void RegisterRowVersionProperty(this ModelBuilder modelBuilder) { modelBuilder.Model @@ -43,136 +43,24 @@ public static void ApplyAggregateMappings(this DbContext dbContext, ModelBuilder { //CAVE: IAggregateMapping implementations must reside in the same assembly as the Applications DbContext-type var aggregateDefinitionTypeInfos = dbContext - .GetType() - .GetTypeInfo() - .Assembly - .ExportedTypes - .Select(t => t.GetTypeInfo()) - .Where(t => t.IsClass && !t.IsAbstract && !t.IsGenericType && typeof(IAggregateMapping).GetTypeInfo().IsAssignableFrom(t)); + .GetType() + .GetTypeInfo() + .Assembly + .ExportedTypes + .Select(t => t.GetTypeInfo()) + .Where(t => t.IsClass && !t.IsAbstract && !t.IsGenericType && typeof(IAggregateMapping).GetTypeInfo().IsAssignableFrom(t)); foreach (TypeInfo typeInfo in aggregateDefinitionTypeInfos) { - var aggregateMapping = (IAggregateMapping)Activator.CreateInstance(typeInfo.AsType()); + var aggregateMapping = (IAggregateMapping) Activator.CreateInstance(typeInfo.AsType()); aggregateMapping.ApplyEfMapping(modelBuilder); } } - public static void UpdateTrackingProperties(this DbContext dbContext, string userId, DateTime utcNow) - { - userId ??= "anonymous"; - var isTraceEnabled = Logger.IsTraceEnabled(); - int count = 0; - - // Modifying an entity (also removing an entity from an aggregate) should leave the aggregate root as modified - dbContext.ChangeTracker - .Entries() - .Where(entry => entry.State == EntityState.Added || entry.State == EntityState.Modified || entry.State == EntityState.Deleted) - .Where(entry => !(entry.Entity is AggregateRoot)) - .ToArray() - .ForAll(entry => - { - EntityEntry aggregateRootEntry = FindAggregateRootEntry(dbContext.ChangeTracker, entry); - if (aggregateRootEntry.State == EntityState.Unchanged) - { - aggregateRootEntry.State = EntityState.Modified; - } - }); - - dbContext.ChangeTracker - .Entries() - .Where(entry => entry.State == EntityState.Added || entry.State == EntityState.Modified) - .ForAll(entry => - { - try - { - count++; - Entity entity = entry.Entity; - - if (entry.State == EntityState.Added) - { - if (isTraceEnabled) - { - Logger.Trace("tracking that {0}[{1}] was created by {2} at {3:T} UTC", entity.GetType().Name, entity.Id, userId, utcNow); - } - entity.SetCreatedProperties(userId, utcNow); - } - else if (entry.State == EntityState.Modified) - { - if (isTraceEnabled) - { - Logger.Trace("tracking that {0}[{1}] was modified by {2} at {3:T} UTC", entity.GetType().Name, entity.Id, userId, utcNow); - } - entity.SetModifiedProperties(userId, utcNow); - - // this line causes the recent changes of tracking properties to be detected before flushing - entry.State = EntityState.Modified; - } - } - catch (Exception ex) - { - Logger.Warn(ex, "Updating tracking properties failed"); - throw; - } - }); - if (count > 0) - { - Logger.Debug($"Tracked {count} entities as created/changed on {utcNow:u} by {userId}"); - } - } - - /// - /// This method finds the EntityEntry<AggregateRoot> of an EntityEntry<Entity> - /// assuming it has been loaded and is being tracked by the change tracker. - /// - private static EntityEntry FindAggregateRootEntry(ChangeTracker changeTracker, EntityEntry entry) - { - Logger.Debug($"Searching aggregate root of {entry.Entity.GetType().Name}[{(entry.Entity as Identified)?.Id}]"); - foreach (NavigationEntry navigation in entry.Navigations) - { - TypeInfo navTargetTypeInfo = navigation.Metadata.GetTargetType().ClrType.GetTypeInfo(); - int navigationTargetForeignKeyValue; - - if (navigation.CurrentValue == null) - { - // orphaned entity, original value contains the foreign key value - if (navigation.Metadata.ForeignKey.Properties.Count > 1) - { - throw new InvalidOperationException("Foreign Keys with multiple properties are not supported."); - } - - IProperty property = navigation.Metadata.ForeignKey.Properties[0]; - navigationTargetForeignKeyValue = (int)entry.OriginalValues[property]; - } - else - { - // added or modified entity, current value contains the foreign key value - navigationTargetForeignKeyValue = ((Entity)navigation.CurrentValue).Id; - } - - // assumption: an entity cannot be loaded on its own. Everything on the navigation path starting from the - // aggregate root must have been loaded before, therefore we can find it using the change tracker - EntityEntry navigationTargetEntry = changeTracker - .Entries() - .Single(ent => Equals(ent.Entity.GetType().GetTypeInfo(), navTargetTypeInfo) - && ent.Property(nameof(Entity.Id)).CurrentValue.Equals(navigationTargetForeignKeyValue)); - - // if the target is AggregateRoot, no (further) recursion is needed - if (typeof(AggregateRoot).GetTypeInfo().IsAssignableFrom(navTargetTypeInfo)) - { - return navigationTargetEntry; - } - - // recurse in case of "Entity -> Entity -> AggregateRoot" - Logger.Debug("Recursing..."); - return FindAggregateRootEntry(changeTracker, navigationTargetEntry); - } - - return null; - } + public static void TraceChangeTrackerState(this DbContext dbContext) { if (Logger.IsTraceEnabled()) - { try { var added = dbContext.ChangeTracker.Entries().Where(entry => entry.State == EntityState.Added).ToArray(); @@ -182,37 +70,28 @@ public static void TraceChangeTrackerState(this DbContext dbContext) var stateDumpBuilder = new StringBuilder(); stateDumpBuilder.AppendFormat("{0} entities added{1}{2}", added.Length, deleted.Length == 0 ? "." : ":", System.Environment.NewLine); - foreach (var entry in added) - { + foreach (EntityEntry entry in added) stateDumpBuilder.AppendFormat("added: {0}[{1}]{2}", entry.Entity.GetType().Name, GetPrimaryKeyValue(entry), System.Environment.NewLine); - } stateDumpBuilder.AppendFormat("{0} entities modified{1}{2}", modified.Length, deleted.Length == 0 ? "." : ":", System.Environment.NewLine); - foreach (var entry in modified) - { + foreach (EntityEntry entry in modified) stateDumpBuilder.AppendFormat("modified: {0}[{1}]{2}", entry.Entity.GetType().Name, GetPrimaryKeyValue(entry), System.Environment.NewLine); - } stateDumpBuilder.AppendFormat("{0} entities deleted{1}{2}", deleted.Length, deleted.Length == 0 ? "." : ":", System.Environment.NewLine); - foreach (var entry in deleted) - { + foreach (EntityEntry entry in deleted) stateDumpBuilder.AppendFormat("deleted: {0}[{1}]{2}", entry.Entity.GetType().Name, GetPrimaryKeyValue(entry), System.Environment.NewLine); - } stateDumpBuilder.AppendFormat("{0} entities unchanged{1}{2}", unchanged.Length, deleted.Length == 0 ? "." : ":", System.Environment.NewLine); - foreach (var entry in unchanged) - { + foreach (EntityEntry entry in unchanged) stateDumpBuilder.AppendFormat("unchanged: {0}[{1}]{2}", entry.Entity.GetType().Name, GetPrimaryKeyValue(entry), System.Environment.NewLine); - } Logger.Trace(stateDumpBuilder.ToString()); } catch (Exception ex) { Logger.Warn(ex, "Change tracker state could not be dumped"); } - } } - + private static string GetPrimaryKeyValue(EntityEntry entry) { return (entry.Entity as Entity)?.Id.ToString(CultureInfo.InvariantCulture) ?? "?"; } } -} +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/DbContextTransactionDecorator.cs b/src/implementations/Backend.Fx.EfCorePersistence/DbContextTransactionDecorator.cs deleted file mode 100644 index 64d72983..00000000 --- a/src/implementations/Backend.Fx.EfCorePersistence/DbContextTransactionDecorator.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Data.Common; -using Backend.Fx.Patterns.Transactions; -using Backend.Fx.Patterns.UnitOfWork; -using Microsoft.EntityFrameworkCore; - -namespace Backend.Fx.EfCorePersistence -{ - /// - /// Makes sure the DbContext gets enlisted in a transaction. - /// - public class DbContextTransactionDecorator : DbTransactionDecorator - { - public DbContextTransactionDecorator(ITransactionContext transactionContext, DbContext dbContext, IUnitOfWork unitOfWork) - : base(transactionContext, unitOfWork) - { - DbContext = dbContext; - } - - public DbContext DbContext { get; } - - public override void Begin() - { - base.Begin(); - if (DbContext.Database.GetDbConnection() != TransactionContext.CurrentTransaction.Connection) - { - throw new InvalidOperationException("The connection used by the DbContext instance does not equal the connection of the transaction context"); - } - DbContext.Database.UseTransaction((DbTransaction) TransactionContext.CurrentTransaction); - } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/EfFlush.cs b/src/implementations/Backend.Fx.EfCorePersistence/EfFlush.cs new file mode 100644 index 00000000..881280c2 --- /dev/null +++ b/src/implementations/Backend.Fx.EfCorePersistence/EfFlush.cs @@ -0,0 +1,190 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Security.Principal; +using Backend.Fx.BuildingBlocks; +using Backend.Fx.Environment.DateAndTime; +using Backend.Fx.Environment.Persistence; +using Backend.Fx.Exceptions; +using Backend.Fx.Extensions; +using Backend.Fx.Logging; +using Backend.Fx.Patterns.DependencyInjection; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace Backend.Fx.EfCorePersistence +{ + public class EfFlush : ICanFlush + { + private static readonly ILogger Logger = LogManager.Create(); + public DbContext DbContext { get; } + public ICurrentTHolder IdentityHolder { get; } + public IClock Clock { get; } + + public EfFlush(DbContext dbContext, ICurrentTHolder identityHolder, IClock clock) + { + DbContext = dbContext; + Logger.Info("Disabling auto detect changes on this DbContext. Changes will be detected explicitly when flushing."); + DbContext.ChangeTracker.AutoDetectChangesEnabled = false; + IdentityHolder = identityHolder; + Clock = clock; + } + + public void Flush() + { + DetectChanges(); + UpdateTrackingProperties(); + DbContext.TraceChangeTrackerState(); + CheckForMissingTenantIds(); + SaveChanges(); + } + + private void DetectChanges() + { + using (Logger.DebugDuration("Detecting changes")) + { + DbContext.ChangeTracker.DetectChanges(); + } + } + + private void UpdateTrackingProperties() + { + using (Logger.DebugDuration("Updating tracking properties of created and modified entities")) + { + UpdateTrackingProperties(IdentityHolder.Current.Name, Clock.UtcNow); + } + } + + private void CheckForMissingTenantIds() + { + using (Logger.DebugDuration("Checking for missing tenant ids")) + { + AggregateRoot[] aggregatesWithoutTenantId = DbContext + .ChangeTracker + .Entries() + .Where(e => e.State == EntityState.Added) + .Select(e => e.Entity) + .OfType() + .Where(ent => ent.TenantId == 0) + .ToArray(); + if (aggregatesWithoutTenantId.Length > 0) + { + throw new InvalidOperationException( + $"Attempt to save aggregate root entities without tenant id: {string.Join(",", aggregatesWithoutTenantId.Select(agg => agg.DebuggerDisplay))}"); + } + } + } + + private void SaveChanges() + { + using (Logger.DebugDuration("Saving changes")) + { + try + { + DbContext.SaveChanges(); + } + catch (DbUpdateConcurrencyException concurrencyException) + { + throw new ConflictedException("Saving changes failed due to optimistic concurrency violation", concurrencyException); + } + } + } + + private void UpdateTrackingProperties(string userId, DateTime utcNow) + { + userId ??= "anonymous"; + var isTraceEnabled = Logger.IsTraceEnabled(); + var count = 0; + + // Modifying an entity (also removing an entity from an aggregate) should leave the aggregate root as modified + DbContext.ChangeTracker + .Entries() + .Where(entry => entry.State == EntityState.Added || entry.State == EntityState.Modified || entry.State == EntityState.Deleted) + .Where(entry => !(entry.Entity is AggregateRoot)) + .ToArray() + .ForAll(entry => + { + EntityEntry aggregateRootEntry = FindAggregateRootEntry(DbContext.ChangeTracker, entry); + if (aggregateRootEntry.State == EntityState.Unchanged) aggregateRootEntry.State = EntityState.Modified; + }); + + DbContext.ChangeTracker + .Entries() + .Where(entry => entry.State == EntityState.Added || entry.State == EntityState.Modified) + .ForAll(entry => + { + try + { + count++; + Entity entity = entry.Entity; + + if (entry.State == EntityState.Added) + { + if (isTraceEnabled) Logger.Trace("tracking that {0}[{1}] was created by {2} at {3:T} UTC", entity.GetType().Name, entity.Id, userId, utcNow); + entity.SetCreatedProperties(userId, utcNow); + } + else if (entry.State == EntityState.Modified) + { + if (isTraceEnabled) Logger.Trace("tracking that {0}[{1}] was modified by {2} at {3:T} UTC", entity.GetType().Name, entity.Id, userId, utcNow); + entity.SetModifiedProperties(userId, utcNow); + + // this line causes the recent changes of tracking properties to be detected before flushing + entry.State = EntityState.Modified; + } + } + catch (Exception ex) + { + Logger.Warn(ex, "Updating tracking properties failed"); + throw; + } + }); + if (count > 0) Logger.Debug($"Tracked {count} entities as created/changed on {utcNow:u} by {userId}"); + } + + /// + /// This method finds the EntityEntry<AggregateRoot> of an EntityEntry<Entity> + /// assuming it has been loaded and is being tracked by the change tracker. + /// + private static EntityEntry FindAggregateRootEntry(ChangeTracker changeTracker, EntityEntry entry) + { + Logger.Debug($"Searching aggregate root of {entry.Entity.GetType().Name}[{(entry.Entity as Identified)?.Id}]"); + foreach (NavigationEntry navigation in entry.Navigations) + { + TypeInfo navTargetTypeInfo = navigation.Metadata.TargetEntityType.ClrType.GetTypeInfo(); + int navigationTargetForeignKeyValue; + + if (navigation.CurrentValue == null) + { + var navigationMetadata = ((INavigation)navigation.Metadata); + // orphaned entity, original value contains the foreign key value + if (navigationMetadata.ForeignKey.Properties.Count > 1) throw new InvalidOperationException("Foreign Keys with multiple properties are not supported."); + + IProperty property = navigationMetadata.ForeignKey.Properties[0]; + navigationTargetForeignKeyValue = (int) entry.OriginalValues[property]; + } + else + { + // added or modified entity, current value contains the foreign key value + navigationTargetForeignKeyValue = ((Entity) navigation.CurrentValue).Id; + } + + // assumption: an entity cannot be loaded on its own. Everything on the navigation path starting from the + // aggregate root must have been loaded before, therefore we can find it using the change tracker + var navigationTargetEntry = changeTracker + .Entries() + .Single(ent => Equals(ent.Entity.GetType().GetTypeInfo(), navTargetTypeInfo) + && ent.Property(nameof(Entity.Id)).CurrentValue.Equals(navigationTargetForeignKeyValue)); + + // if the target is AggregateRoot, no (further) recursion is needed + if (typeof(AggregateRoot).GetTypeInfo().IsAssignableFrom(navTargetTypeInfo)) return navigationTargetEntry; + + // recurse in case of "Entity -> Entity -> AggregateRoot" + Logger.Debug("Recursing..."); + return FindAggregateRootEntry(changeTracker, navigationTargetEntry); + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/EfRepository.cs b/src/implementations/Backend.Fx.EfCorePersistence/EfRepository.cs index 616edc96..3d7bd9b7 100644 --- a/src/implementations/Backend.Fx.EfCorePersistence/EfRepository.cs +++ b/src/implementations/Backend.Fx.EfCorePersistence/EfRepository.cs @@ -5,24 +5,24 @@ using Backend.Fx.BuildingBlocks; using Backend.Fx.Environment.MultiTenancy; using Backend.Fx.Logging; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; -using Microsoft.EntityFrameworkCore.Infrastructure; using Backend.Fx.Patterns.Authorization; using Backend.Fx.Patterns.DependencyInjection; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; +using Microsoft.EntityFrameworkCore.Infrastructure; namespace Backend.Fx.EfCorePersistence { public class EfRepository : Repository where TAggregateRoot : AggregateRoot { private static readonly ILogger Logger = LogManager.Create>(); - private DbContext _dbContext; - private readonly IAggregateMapping _aggregateMapping; private readonly IAggregateAuthorization _aggregateAuthorization; + private readonly IAggregateMapping _aggregateMapping; + private DbContext _dbContext; public EfRepository([CanBeNull] DbContext dbContext, IAggregateMapping aggregateMapping, - ICurrentTHolder currentTenantIdHolder, IAggregateAuthorization aggregateAuthorization) + ICurrentTHolder currentTenantIdHolder, IAggregateAuthorization aggregateAuthorization) : base(currentTenantIdHolder, aggregateAuthorization) { _dbContext = dbContext; @@ -36,27 +36,37 @@ public EfRepository([CanBeNull] DbContext dbContext, IAggregateMapping _dbContext ?? throw new InvalidOperationException("This EfRepository does not have a DbContext yet. You might either make sure a proper DbContext gets injected or the DbContext is initialized later using a derived class"); + get => _dbContext ?? throw new InvalidOperationException( + "This EfRepository does not have a DbContext yet. You might either make sure a proper DbContext gets injected or the DbContext is initialized later using a derived class") + ; protected set { - if (_dbContext != null) - { - throw new InvalidOperationException("This EfRepository has already a DbContext assigned. It is not allowed to change it later."); - } + if (_dbContext != null) throw new InvalidOperationException("This EfRepository has already a DbContext assigned. It is not allowed to change it later."); _dbContext = value; var localViewListener = _dbContext?.GetService(); localViewListener?.RegisterView(AuthorizeChanges); } } + protected override IQueryable RawAggregateQueryable + { + get + { + IQueryable queryable = DbContext.Set(); + if (_aggregateMapping.IncludeDefinitions != null) + foreach (var include in _aggregateMapping.IncludeDefinitions) + queryable = queryable.Include(include); + return queryable; + } + } + /// - /// Due to the fact, that a real lifecycle hook API is not yet available (see issue https://github.com/aspnet/EntityFrameworkCore/issues/626) - /// we are using an internal service to achieve the same goal: When a state change occurs from unchanged to modified, and this repository is - /// handling this type of aggregate, we're calling IAggregateAuthorization.CanModify to enforce user privilege checking. - /// - /// We're accepting the possible instability of EF Core internals due to the fact that there is a full API feature in the pipeline that will - /// make this workaround obsolete. Also, not much of an effort was done to write this code, so if we have to deal with this issue in the future - /// again, we do not loose a lot. + /// Due to the fact, that a real lifecycle hook API is not yet available (see issue https://github.com/aspnet/EntityFrameworkCore/issues/626) + /// we are using an internal service to achieve the same goal: When a state change occurs from unchanged to modified, and this repository is + /// handling this type of aggregate, we're calling IAggregateAuthorization.CanModify to enforce user privilege checking. + /// We're accepting the possible instability of EF Core internals due to the fact that there is a full API feature in the pipeline that will + /// make this workaround obsolete. Also, not much of an effort was done to write this code, so if we have to deal with this issue in the future + /// again, we do not loose a lot. /// /// /// @@ -66,10 +76,7 @@ private void AuthorizeChanges(InternalEntityEntry entry, EntityState previousSta if (previousState == EntityState.Unchanged && entry.EntityState == EntityState.Modified && entry.EntityType.ClrType == typeof(TAggregateRoot)) { var aggregateRoot = (TAggregateRoot) entry.Entity; - if (!_aggregateAuthorization.CanModify(aggregateRoot)) - { - throw new SecurityException($"You are not allowed to modify {AggregateTypeName}[{aggregateRoot.Id}]"); - } + if (!_aggregateAuthorization.CanModify(aggregateRoot)) throw new SecurityException($"You are not allowed to modify {AggregateTypeName}[{aggregateRoot.Id}]"); } } @@ -90,21 +97,5 @@ protected override void DeletePersistent(TAggregateRoot aggregateRoot) Logger.Debug($"Persistently removing {aggregateRoot.DebuggerDisplay}"); DbContext.Set().Remove(aggregateRoot); } - - protected override IQueryable RawAggregateQueryable - { - get - { - IQueryable queryable = DbContext.Set(); - if (_aggregateMapping.IncludeDefinitions != null) - { - foreach (var include in _aggregateMapping.IncludeDefinitions) - { - queryable = queryable.Include(include); - } - } - return queryable; - } - } } -} +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/EfUnitOfWork.cs b/src/implementations/Backend.Fx.EfCorePersistence/EfUnitOfWork.cs deleted file mode 100644 index 72c0de5b..00000000 --- a/src/implementations/Backend.Fx.EfCorePersistence/EfUnitOfWork.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Security.Principal; -using Backend.Fx.Environment.DateAndTime; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.Patterns.EventAggregation.Domain; -using Backend.Fx.Patterns.EventAggregation.Integration; -using Backend.Fx.Patterns.UnitOfWork; -using Microsoft.EntityFrameworkCore; - -namespace Backend.Fx.EfCorePersistence -{ - public class EfUnitOfWork : UnitOfWork - { - - public EfUnitOfWork(IClock clock, ICurrentTHolder identityHolder, IDomainEventAggregator eventAggregator, - IEventBusScope eventBusScope, DbContext dbContext) - : base(clock, identityHolder, eventAggregator, eventBusScope) - { - DbContext = dbContext; - DbContext.ChangeTracker.AutoDetectChangesEnabled = false; - } - - public DbContext DbContext { get; } - - public override void Flush() - { - DbContext.ChangeTracker.DetectChanges(); - base.Flush(); - DbContext.SaveChanges(); - } - - protected override void UpdateTrackingProperties(string userId, DateTime utcNow) - { - DbContext.UpdateTrackingProperties(userId, utcNow); - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - DbContext?.Dispose(); - } - base.Dispose(disposing); - } - } -} diff --git a/src/implementations/Backend.Fx.EfCorePersistence/EntityQueryable.cs b/src/implementations/Backend.Fx.EfCorePersistence/EntityQueryable.cs index bd76220f..48d4a959 100644 --- a/src/implementations/Backend.Fx.EfCorePersistence/EntityQueryable.cs +++ b/src/implementations/Backend.Fx.EfCorePersistence/EntityQueryable.cs @@ -1,15 +1,14 @@ -using JetBrains.Annotations; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Backend.Fx.BuildingBlocks; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; namespace Backend.Fx.EfCorePersistence { - using System; - using System.Collections; - using System.Collections.Generic; - using System.Linq; - using System.Linq.Expressions; - using BuildingBlocks; - using Microsoft.EntityFrameworkCore; - public class EntityQueryable : IQueryable where TEntity : Entity { [CanBeNull] private DbContext _dbContext; @@ -21,17 +20,18 @@ public EntityQueryable(DbContext dbContext) public DbContext DbContext { - get => _dbContext ?? throw new InvalidOperationException("This EntityQueryable does not have a DbContext yet. You might either make sure a proper DbContext gets injected or the DbContext is initialized later using a derived class"); + get => _dbContext ?? throw new InvalidOperationException( + "This EntityQueryable does not have a DbContext yet. You might either make sure a proper DbContext gets injected or the DbContext is initialized later using a derived class") + ; protected set { - if (_dbContext != null) - { - throw new InvalidOperationException("This EntityQueryable has already a DbContext assigned. It is not allowed to change it later."); - } + if (_dbContext != null) throw new InvalidOperationException("This EntityQueryable has already a DbContext assigned. It is not allowed to change it later."); _dbContext = value; } } + private IQueryable InnerQueryable => DbContext.Set(); + public IEnumerator GetEnumerator() { return InnerQueryable.GetEnumerator(); @@ -39,7 +39,7 @@ public IEnumerator GetEnumerator() IEnumerator IEnumerable.GetEnumerator() { - return ((IEnumerable)InnerQueryable).GetEnumerator(); + return ((IEnumerable) InnerQueryable).GetEnumerator(); } public Type ElementType => InnerQueryable.ElementType; @@ -47,7 +47,5 @@ IEnumerator IEnumerable.GetEnumerator() public Expression Expression => InnerQueryable.Expression; public IQueryProvider Provider => InnerQueryable.Provider; - - private IQueryable InnerQueryable => DbContext.Set(); } -} +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/IAggregateMapping.cs b/src/implementations/Backend.Fx.EfCorePersistence/IAggregateMapping.cs index e5abdf56..76dd9fd5 100644 --- a/src/implementations/Backend.Fx.EfCorePersistence/IAggregateMapping.cs +++ b/src/implementations/Backend.Fx.EfCorePersistence/IAggregateMapping.cs @@ -1,11 +1,11 @@ -namespace Backend.Fx.EfCorePersistence -{ - using System; - using System.Collections.Generic; - using System.Linq.Expressions; - using BuildingBlocks; - using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using Backend.Fx.BuildingBlocks; +using Microsoft.EntityFrameworkCore; +namespace Backend.Fx.EfCorePersistence +{ public interface IAggregateMapping { void ApplyEfMapping(ModelBuilder modelBuilder); @@ -13,6 +13,6 @@ public interface IAggregateMapping public interface IAggregateMapping : IAggregateMapping where T : AggregateRoot { - IEnumerable>> IncludeDefinitions { get; } + IEnumerable>> IncludeDefinitions { get; } } -} +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/IFullTextSearchIndex.cs b/src/implementations/Backend.Fx.EfCorePersistence/IFullTextSearchIndex.cs deleted file mode 100644 index 88de9127..00000000 --- a/src/implementations/Backend.Fx.EfCorePersistence/IFullTextSearchIndex.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Backend.Fx.EfCorePersistence -{ - using Microsoft.EntityFrameworkCore; - - public interface IFullTextSearchIndex - { - void EnsureIndex(DbContext dbContext); - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/Mssql/MsSqlSequence.cs b/src/implementations/Backend.Fx.EfCorePersistence/Mssql/MsSqlSequence.cs index 4caba68a..28fa80de 100644 --- a/src/implementations/Backend.Fx.EfCorePersistence/Mssql/MsSqlSequence.cs +++ b/src/implementations/Backend.Fx.EfCorePersistence/Mssql/MsSqlSequence.cs @@ -15,7 +15,9 @@ protected MsSqlSequence(IDbConnectionFactory dbConnectionFactory) { _dbConnectionFactory = dbConnectionFactory; } - + + protected abstract string SequenceName { get; } + public void EnsureSequence() { Logger.Info($"Ensuring existence of mssql sequence {SequenceName}"); @@ -36,7 +38,7 @@ public void EnsureSequence() else { Logger.Info($"Sequence {SequenceName} does not exist yet and will be created now"); - using (var cmd = dbConnection.CreateCommand()) + using (IDbCommand cmd = dbConnection.CreateCommand()) { cmd.CommandText = $"CREATE SEQUENCE {SequenceName} START WITH 1 INCREMENT BY {Increment}"; cmd.ExecuteNonQuery(); @@ -64,6 +66,5 @@ public int GetNextValue() } public abstract int Increment { get; } - protected abstract string SequenceName { get; } } -} +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/Oracle/OracleSequence.cs b/src/implementations/Backend.Fx.EfCorePersistence/Oracle/OracleSequence.cs index a98e6ffe..93d58eb3 100644 --- a/src/implementations/Backend.Fx.EfCorePersistence/Oracle/OracleSequence.cs +++ b/src/implementations/Backend.Fx.EfCorePersistence/Oracle/OracleSequence.cs @@ -1,22 +1,34 @@ -using Backend.Fx.EfCorePersistence.Bootstrapping; +using System; +using System.Data; +using Backend.Fx.EfCorePersistence.Bootstrapping; +using Backend.Fx.Logging; using Backend.Fx.Patterns.IdGeneration; namespace Backend.Fx.EfCorePersistence.Oracle { - using System; - using System.Data; - using Logging; - public abstract class OracleSequence : ISequence { - private readonly IDbConnectionFactory _dbConnectionFactory; private static readonly ILogger Logger = LogManager.Create(); + private readonly IDbConnectionFactory _dbConnectionFactory; protected OracleSequence(IDbConnectionFactory dbConnectionFactory) { _dbConnectionFactory = dbConnectionFactory; } + protected abstract string SequenceName { get; } + protected abstract string SchemaName { get; } + + private string SchemaPrefix + { + get + { + if (string.IsNullOrEmpty(SchemaName)) return string.Empty; + + return SchemaName + "."; + } + } + public void EnsureSequence() { Logger.Info($"Ensuring existence of oracle sequence {SchemaPrefix}{SequenceName}"); @@ -38,7 +50,7 @@ public void EnsureSequence() else { Logger.Info($"Sequence {SchemaPrefix}{SequenceName} does not exist yet and will be created now"); - using (var cmd = dbConnection.CreateCommand()) + using (IDbCommand cmd = dbConnection.CreateCommand()) { cmd.CommandText = $"CREATE SEQUENCE {SchemaPrefix}{SequenceName} START WITH 1 INCREMENT BY {Increment}"; cmd.ExecuteNonQuery(); @@ -67,20 +79,5 @@ public int GetNextValue() } public abstract int Increment { get; } - protected abstract string SequenceName { get; } - protected abstract string SchemaName { get; } - - private string SchemaPrefix - { - get - { - if (string.IsNullOrEmpty(SchemaName)) - { - return string.Empty; - } - - return SchemaName + "."; - } - } } -} +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/PlainAggregateMapping.cs b/src/implementations/Backend.Fx.EfCorePersistence/PlainAggregateMapping.cs index 291b5f0e..2bf4efec 100644 --- a/src/implementations/Backend.Fx.EfCorePersistence/PlainAggregateMapping.cs +++ b/src/implementations/Backend.Fx.EfCorePersistence/PlainAggregateMapping.cs @@ -1,17 +1,18 @@ -namespace Backend.Fx.EfCorePersistence -{ - using System; - using System.Collections.Generic; - using System.Linq.Expressions; - using BuildingBlocks; - using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using Backend.Fx.BuildingBlocks; +using Microsoft.EntityFrameworkCore; +namespace Backend.Fx.EfCorePersistence +{ public class PlainAggregateMapping : AggregateMapping where TAggregateRoot : AggregateRoot { - public override void ApplyEfMapping(ModelBuilder modelBuilder) - { } - public override IEnumerable>> IncludeDefinitions => new Expression>[0]; + + public override void ApplyEfMapping(ModelBuilder modelBuilder) + { + } } } \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/Postgres/PostgresSequence.cs b/src/implementations/Backend.Fx.EfCorePersistence/Postgres/PostgresSequence.cs index f5c32aaa..a1bc6364 100644 --- a/src/implementations/Backend.Fx.EfCorePersistence/Postgres/PostgresSequence.cs +++ b/src/implementations/Backend.Fx.EfCorePersistence/Postgres/PostgresSequence.cs @@ -1,12 +1,11 @@ -using Backend.Fx.EfCorePersistence.Bootstrapping; +using System; +using System.Data; +using Backend.Fx.EfCorePersistence.Bootstrapping; +using Backend.Fx.Logging; using Backend.Fx.Patterns.IdGeneration; namespace Backend.Fx.EfCorePersistence.Postgres { - using System; - using System.Data; - using Logging; - public abstract class PostgresSequence : ISequence { private static readonly ILogger Logger = LogManager.Create(); @@ -17,6 +16,9 @@ protected PostgresSequence(IDbConnectionFactory dbConnectionFactory) _dbConnectionFactory = dbConnectionFactory; } + protected abstract string SequenceName { get; } + protected abstract string SchemaName { get; } + public void EnsureSequence() { Logger.Info($"Ensuring existence of postgres sequence {SchemaName}.{SequenceName}"); @@ -38,7 +40,7 @@ public void EnsureSequence() else { Logger.Info($"Sequence {SchemaName}.{SequenceName} does not exist yet and will be created now"); - using (var cmd = dbConnection.CreateCommand()) + using (IDbCommand cmd = dbConnection.CreateCommand()) { cmd.CommandText = $"CREATE SEQUENCE {SchemaName}.{SequenceName} START WITH 1 INCREMENT BY {Increment}"; cmd.ExecuteNonQuery(); @@ -67,7 +69,5 @@ public int GetNextValue() } public abstract int Increment { get; } - protected abstract string SequenceName { get; } - protected abstract string SchemaName { get; } } -} +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.EfCorePersistence/Properties/AssemblyInfo.cs b/src/implementations/Backend.Fx.EfCorePersistence/Properties/AssemblyInfo.cs index 43603728..5f565d7d 100644 --- a/src/implementations/Backend.Fx.EfCorePersistence/Properties/AssemblyInfo.cs +++ b/src/implementations/Backend.Fx.EfCorePersistence/Properties/AssemblyInfo.cs @@ -3,4 +3,4 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyVersion("0.0.0.0")] [assembly: AssemblyFileVersion("0.0.0.0")] -[assembly: AssemblyInformationalVersion("0.0.0.0")] +[assembly: AssemblyInformationalVersion("0.0.0.0")] \ No newline at end of file diff --git a/src/implementations/Backend.Fx.InMemoryPersistence/Backend.Fx.InMemoryPersistence.csproj b/src/implementations/Backend.Fx.InMemoryPersistence/Backend.Fx.InMemoryPersistence.csproj index 505bcd4a..dd5b6e40 100644 --- a/src/implementations/Backend.Fx.InMemoryPersistence/Backend.Fx.InMemoryPersistence.csproj +++ b/src/implementations/Backend.Fx.InMemoryPersistence/Backend.Fx.InMemoryPersistence.csproj @@ -26,7 +26,7 @@ - + diff --git a/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryRepository.cs b/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryRepository.cs index aeafa911..07708f7a 100644 --- a/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryRepository.cs +++ b/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryRepository.cs @@ -10,14 +10,14 @@ namespace Backend.Fx.InMemoryPersistence { public class InMemoryRepository : Repository where T : AggregateRoot { - public virtual IInMemoryStore Store { get; } - - public InMemoryRepository(IInMemoryStore store, ICurrentTHolder currentTenantIdHolder, IAggregateAuthorization aggregateAuthorization) + public InMemoryRepository(IInMemoryStore store, ICurrentTHolder currentTenantIdHolder, IAggregateAuthorization aggregateAuthorization) : base(currentTenantIdHolder, aggregateAuthorization) { Store = store; } + public virtual IInMemoryStore Store { get; } + protected override IQueryable RawAggregateQueryable => Store.Values.AsQueryable(); public void Clear() diff --git a/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryStore.cs b/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryStore.cs index 715b5d90..67a5d975 100644 --- a/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryStore.cs +++ b/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryStore.cs @@ -5,9 +5,10 @@ namespace Backend.Fx.InMemoryPersistence { public interface IInMemoryStore : IDictionary where T : AggregateRoot - { } - - public class InMemoryStore : IInMemoryStore where T:AggregateRoot + { + } + + public class InMemoryStore : IInMemoryStore where T : AggregateRoot { private readonly IDictionary _dictionaryImplementation = new Dictionary(); diff --git a/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryTenantManager.cs b/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryTenantManager.cs index 73dc1802..04a5c5a0 100644 --- a/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryTenantManager.cs +++ b/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryTenantManager.cs @@ -7,7 +7,7 @@ namespace Backend.Fx.InMemoryPersistence public class InMemoryTenantRepository : ITenantRepository { private readonly Dictionary _store = new Dictionary(); - + public Tenant[] GetTenants() { return _store.Values.ToArray(); @@ -17,7 +17,7 @@ public Tenant GetTenant(TenantId tenantId) { return _store[tenantId.Value]; } - + public void SaveTenant(Tenant tenant) { if (tenant.Id == 0) @@ -27,7 +27,6 @@ public void SaveTenant(Tenant tenant) } else { - _store[tenant.Id].DefaultCultureName = tenant.DefaultCultureName; _store[tenant.Id].Description = tenant.Description; _store[tenant.Id].IsDemoTenant = tenant.IsDemoTenant; _store[tenant.Id].Name = tenant.Name; @@ -35,4 +34,4 @@ public void SaveTenant(Tenant tenant) } } } -} +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryUnitOfWork.cs b/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryUnitOfWork.cs deleted file mode 100644 index ba1d666c..00000000 --- a/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryUnitOfWork.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Security.Principal; -using Backend.Fx.Environment.DateAndTime; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.Patterns.EventAggregation.Domain; -using Backend.Fx.Patterns.EventAggregation.Integration; -using Backend.Fx.Patterns.UnitOfWork; - -namespace Backend.Fx.InMemoryPersistence -{ - public class InMemoryUnitOfWork : UnitOfWork - { - public int CommitCalls { get; private set; } - - public InMemoryUnitOfWork(IClock clock, ICurrentTHolder identityHolder, - IDomainEventAggregator eventAggregator, IEventBusScope eventBusScope) - : base(clock, identityHolder, eventAggregator, eventBusScope) - { } - - protected override void UpdateTrackingProperties(string userId, DateTime utcNow) - { } - - public override void Complete() - { - CommitCalls++; - } - } -} diff --git a/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryView.cs b/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryView.cs index 98e2eac6..cd4ff074 100644 --- a/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryView.cs +++ b/src/implementations/Backend.Fx.InMemoryPersistence/InMemoryView.cs @@ -10,20 +10,24 @@ namespace Backend.Fx.InMemoryPersistence public class InMemoryView : IView { private readonly IList _list; + public InMemoryView(IList list) { _list = list; } + public IEnumerator GetEnumerator() { return _list.GetEnumerator(); } + IEnumerator IEnumerable.GetEnumerator() { - return ((IEnumerable)_list).GetEnumerator(); + return ((IEnumerable) _list).GetEnumerator(); } + public Type ElementType => typeof(T); public Expression Expression => _list.AsQueryable().Expression; public IQueryProvider Provider => _list.AsQueryable().Provider; } -} +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.Log4NetLogging/Backend.Fx.Log4NetLogging.csproj b/src/implementations/Backend.Fx.Log4NetLogging/Backend.Fx.Log4NetLogging.csproj index 8462b72a..f147fe52 100644 --- a/src/implementations/Backend.Fx.Log4NetLogging/Backend.Fx.Log4NetLogging.csproj +++ b/src/implementations/Backend.Fx.Log4NetLogging/Backend.Fx.Log4NetLogging.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/src/implementations/Backend.Fx.Log4NetLogging/Log4NetLoggerFactory.cs b/src/implementations/Backend.Fx.Log4NetLogging/Log4NetLoggerFactory.cs index c20e7209..f341fe2b 100644 --- a/src/implementations/Backend.Fx.Log4NetLogging/Log4NetLoggerFactory.cs +++ b/src/implementations/Backend.Fx.Log4NetLogging/Log4NetLoggerFactory.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; using System.Reflection; using log4net.Repository; using BackendFxILogger = Backend.Fx.Logging.ILogger; @@ -23,6 +24,23 @@ public BackendFxILogger Create(string s) return new Log4NetLogger(log4net.LogManager.GetLogger(_loggerRepository.Name, s)); } + public BackendFxILogger Create(Type t) + { + string s = t.FullName; + var indexOf = s?.IndexOf('[') ?? 0; + if (indexOf > 0) + { + s = s?.Substring(0, indexOf); + } + + return Create(s); + } + + public BackendFxILogger Create() + { + return Create(typeof(T)); + } + public void BeginActivity(int activityIndex) { log4net.LogicalThreadContext.Properties["app-Activity"] = activityIndex; diff --git a/src/implementations/Backend.Fx.NLogLogging/Backend.Fx.NLogLogging.csproj b/src/implementations/Backend.Fx.NLogLogging/Backend.Fx.NLogLogging.csproj index e95e7906..4ad5f9fb 100644 --- a/src/implementations/Backend.Fx.NLogLogging/Backend.Fx.NLogLogging.csproj +++ b/src/implementations/Backend.Fx.NLogLogging/Backend.Fx.NLogLogging.csproj @@ -25,8 +25,8 @@ - - + + diff --git a/src/implementations/Backend.Fx.NLogLogging/Configurations.cs b/src/implementations/Backend.Fx.NLogLogging/Configurations.cs index 6e69b181..7ac3c822 100644 --- a/src/implementations/Backend.Fx.NLogLogging/Configurations.cs +++ b/src/implementations/Backend.Fx.NLogLogging/Configurations.cs @@ -18,7 +18,7 @@ public static void ForTests(string appRootNamespace, string logfilename = "tests lock (SyncLock) { if (LogManager.Configuration != null) return; - + Logging.LogManager.Initialize(new NLogLoggerFactory()); var config = new LoggingConfiguration(); @@ -36,7 +36,7 @@ public static void ForTests(string appRootNamespace, string logfilename = "tests FileName = @"${basedir}/" + logfilename, Layout = new Log4JXmlEventLayout(), MaxArchiveFiles = 1, - ArchiveAboveSize = 10 * 1024 * 1024, + ArchiveAboveSize = 10 * 1024 * 1024 }; config.AddTarget("file", fileTarget); config.LoggingRules.Add(new LoggingRule(appRootNamespace + ".*", LogLevel.Debug, fileTarget)); @@ -82,4 +82,4 @@ public static IEnumerable LogMessages(string level) return LogManager.Configuration?.FindTargetByName(level)?.Logs ?? Enumerable.Empty(); } } -} +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.NLogLogging/LoggingFixture.cs b/src/implementations/Backend.Fx.NLogLogging/LoggingFixture.cs index fbd7861c..12278692 100644 --- a/src/implementations/Backend.Fx.NLogLogging/LoggingFixture.cs +++ b/src/implementations/Backend.Fx.NLogLogging/LoggingFixture.cs @@ -1,11 +1,12 @@ using System; using System.Reflection; +using Backend.Fx.Logging; namespace Backend.Fx.NLogLogging { /// - /// To be implemented and used as AssemblyFixture - /// Configures NLog and shuts down logging after test execution + /// To be implemented and used as AssemblyFixture + /// Configures NLog and shuts down logging after test execution /// public abstract class LoggingFixture : IDisposable { @@ -15,7 +16,7 @@ protected LoggingFixture(string appRootNamespace) { Configurations.ForTests(appRootNamespace, GetType().GetTypeInfo().Assembly.GetName().Name + ".xlog"); - _lifetimeLogger = Logging.LogManager.Create().InfoDuration("Test run started", "Test run finished"); + _lifetimeLogger = LogManager.Create().InfoDuration("Test run started", "Test run finished"); } public void Dispose() @@ -24,4 +25,4 @@ public void Dispose() NLog.LogManager.Shutdown(); } } -} +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.NLogLogging/NLogLogger.cs b/src/implementations/Backend.Fx.NLogLogging/NLogLogger.cs index fbe7b03d..56a90db3 100644 --- a/src/implementations/Backend.Fx.NLogLogging/NLogLogger.cs +++ b/src/implementations/Backend.Fx.NLogLogging/NLogLogger.cs @@ -1,12 +1,14 @@ -namespace Backend.Fx.NLogLogging +using System; +using System.Diagnostics; +using System.Globalization; +using Backend.Fx.Logging; +using NLog; + +namespace Backend.Fx.NLogLogging { - using System; - using System.Diagnostics; - using System.Globalization; using BackendFxILogger = Logging.ILogger; - using NLogILogger = NLog.ILogger; - using NLogLogLevel = NLog.LogLevel; + using NLogLogLevel = LogLevel; [DebuggerStepThrough] public class NLogLogger : BackendFxILogger @@ -19,6 +21,7 @@ internal NLogLogger(NLogILogger nlogLogger) } #region fatal + public Exception Fatal(Exception exception) { _nlogLogger.Fatal(exception); @@ -35,9 +38,11 @@ public Exception Fatal(Exception exception, string format, params object[] args) _nlogLogger.Fatal(exception, CultureInfo.InvariantCulture, format, args); return exception; } + #endregion #region error + public Exception Error(Exception exception) { _nlogLogger.Error(exception); @@ -54,9 +59,11 @@ public Exception Error(Exception exception, string format, params object[] args) _nlogLogger.Error(exception, CultureInfo.InvariantCulture, format, args); return exception; } + #endregion #region warn + public Exception Warn(Exception exception) { _nlogLogger.Warn(exception); @@ -73,9 +80,11 @@ public Exception Warn(Exception exception, string format, params object[] args) _nlogLogger.Warn(exception, CultureInfo.InvariantCulture, format, args); return exception; } + #endregion #region info + public Exception Info(Exception exception) { _nlogLogger.Info(exception); @@ -84,12 +93,12 @@ public Exception Info(Exception exception) public IDisposable InfoDuration(string activity) { - return new Logging.DurationLogger(s => Info(s), activity); + return new DurationLogger(s => Info(s), activity); } public IDisposable InfoDuration(string beginMessage, string endMessage) { - return new Logging.DurationLogger(s => Info(s), beginMessage, endMessage); + return new DurationLogger(s => Info(s), beginMessage, endMessage); } public void Info(string format, params object[] args) @@ -102,9 +111,11 @@ public Exception Info(Exception exception, string format, params object[] args) _nlogLogger.Info(exception, CultureInfo.InvariantCulture, format, args); return exception; } + #endregion #region debug + public bool IsDebugEnabled() { return _nlogLogger.IsDebugEnabled; @@ -118,17 +129,16 @@ public Exception Debug(Exception exception) public IDisposable DebugDuration(string activity) { - return new Logging.DurationLogger(s => Debug(s), activity); + return new DurationLogger(s => Debug(s), activity); } public IDisposable DebugDuration(string beginMessage, string endMessage) { - return new Logging.DurationLogger(s => Debug(s), beginMessage, endMessage); + return new DurationLogger(s => Debug(s), beginMessage, endMessage); } public void Debug(string format, params object[] args) { - _nlogLogger.Debug(CultureInfo.InvariantCulture, format, args); } @@ -137,9 +147,11 @@ public Exception Debug(Exception exception, string format, params object[] args) _nlogLogger.Debug(exception, CultureInfo.InvariantCulture, format, args); return exception; } + #endregion #region trace + public bool IsTraceEnabled() { return _nlogLogger.IsEnabled(NLogLogLevel.Trace); @@ -153,32 +165,26 @@ public Exception Trace(Exception exception) public IDisposable TraceDuration(string activity) { - return new Logging.DurationLogger(s => Trace(s), activity); + return new DurationLogger(s => Trace(s), activity); } public IDisposable TraceDuration(string beginMessage, string endMessage) { - return new Logging.DurationLogger(s => Trace(s), beginMessage, endMessage); + return new DurationLogger(s => Trace(s), beginMessage, endMessage); } public void Trace(string format, params object[] args) { - if (IsTraceEnabled()) - { - _nlogLogger.Trace(CultureInfo.InvariantCulture, format, args); - } - + if (IsTraceEnabled()) _nlogLogger.Trace(CultureInfo.InvariantCulture, format, args); } public Exception Trace(Exception exception, string format, params object[] args) { - if (IsTraceEnabled()) - { - _nlogLogger.Trace(exception, CultureInfo.InvariantCulture, format, args); - } + if (IsTraceEnabled()) _nlogLogger.Trace(exception, CultureInfo.InvariantCulture, format, args); return exception; } + #endregion } -} +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.NLogLogging/NLogLoggerFactory.cs b/src/implementations/Backend.Fx.NLogLogging/NLogLoggerFactory.cs index 9ef7e402..911c289a 100644 --- a/src/implementations/Backend.Fx.NLogLogging/NLogLoggerFactory.cs +++ b/src/implementations/Backend.Fx.NLogLogging/NLogLoggerFactory.cs @@ -1,34 +1,57 @@ -namespace Backend.Fx.NLogLogging +using System; +using System.Diagnostics; +using Backend.Fx.Logging; +using NLog; +using NLog.Config; +using ILogger = Backend.Fx.Logging.ILogger; +using LogManager = NLog.LogManager; + +namespace Backend.Fx.NLogLogging { - using System.Diagnostics; - [DebuggerStepThrough] - public class NLogLoggerFactory : Logging.ILoggerFactory + public class NLogLoggerFactory : ILoggerFactory { public NLogLoggerFactory() { BeginActivity(0); } - public Logging.ILogger Create(string s) + public ILogger Create(string s) + { + return new NLogLogger(LogManager.GetLogger(s)); + } + + public ILogger Create(Type t) + { + string s = t.FullName; + var indexOf = s?.IndexOf('[') ?? 0; + if (indexOf > 0) + { + s = s?.Substring(0, indexOf); + } + + return Create(s); + } + + public ILogger Create() { - return new NLogLogger(NLog.LogManager.GetLogger(s)); + return Create(typeof(T)); } public void BeginActivity(int activityIndex) { - NLog.MappedDiagnosticsLogicalContext.Set("app-Activity", activityIndex); + MappedDiagnosticsLogicalContext.Set("app-Activity", activityIndex); } public void Shutdown() { - NLog.LogManager.Shutdown(); + LogManager.Shutdown(); } public static void Configure(string nlogConfigPath) { Logging.LogManager.Initialize(new NLogLoggerFactory()); - NLog.LogManager.Configuration = new NLog.Config.XmlLoggingConfiguration(nlogConfigPath); + LogManager.Configuration = new XmlLoggingConfiguration(nlogConfigPath); } } } \ No newline at end of file diff --git a/src/implementations/Backend.Fx.NLogLogging/Properties/AssemblyInfo.cs b/src/implementations/Backend.Fx.NLogLogging/Properties/AssemblyInfo.cs index 43603728..5f565d7d 100644 --- a/src/implementations/Backend.Fx.NLogLogging/Properties/AssemblyInfo.cs +++ b/src/implementations/Backend.Fx.NLogLogging/Properties/AssemblyInfo.cs @@ -3,4 +3,4 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyVersion("0.0.0.0")] [assembly: AssemblyFileVersion("0.0.0.0")] -[assembly: AssemblyInformationalVersion("0.0.0.0")] +[assembly: AssemblyInformationalVersion("0.0.0.0")] \ No newline at end of file diff --git a/src/implementations/Backend.Fx.RabbitMq/Backend.Fx.RabbitMq.csproj b/src/implementations/Backend.Fx.RabbitMq/Backend.Fx.RabbitMq.csproj index c65017e7..4a22d8f5 100644 --- a/src/implementations/Backend.Fx.RabbitMq/Backend.Fx.RabbitMq.csproj +++ b/src/implementations/Backend.Fx.RabbitMq/Backend.Fx.RabbitMq.csproj @@ -14,7 +14,7 @@ Marc Wittke anic GmbH All rights reserved. Distributed under the terms of the MIT License. - An event bus implementation based on RabbitMQ + A message bus implementation based on RabbitMQ False MIT https://github.com/marcwittke/Backend.Fx @@ -27,7 +27,7 @@ - + diff --git a/src/implementations/Backend.Fx.RabbitMq/Properties/AssemblyInfo.cs b/src/implementations/Backend.Fx.RabbitMq/Properties/AssemblyInfo.cs index 43603728..5f565d7d 100644 --- a/src/implementations/Backend.Fx.RabbitMq/Properties/AssemblyInfo.cs +++ b/src/implementations/Backend.Fx.RabbitMq/Properties/AssemblyInfo.cs @@ -3,4 +3,4 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyVersion("0.0.0.0")] [assembly: AssemblyFileVersion("0.0.0.0")] -[assembly: AssemblyInformationalVersion("0.0.0.0")] +[assembly: AssemblyInformationalVersion("0.0.0.0")] \ No newline at end of file diff --git a/src/implementations/Backend.Fx.RabbitMq/RabbitMQChannel.cs b/src/implementations/Backend.Fx.RabbitMq/RabbitMQChannel.cs index 535d0d44..cae23410 100644 --- a/src/implementations/Backend.Fx.RabbitMq/RabbitMQChannel.cs +++ b/src/implementations/Backend.Fx.RabbitMq/RabbitMQChannel.cs @@ -1,46 +1,45 @@ -namespace Backend.Fx.RabbitMq +using System; +using System.Collections.Generic; +using System.Net.Sockets; +using System.Text; +using Backend.Fx.Logging; +using Backend.Fx.Patterns.EventAggregation.Integration; +using Newtonsoft.Json; +using Polly; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using RabbitMQ.Client.Exceptions; + +namespace Backend.Fx.RabbitMq { - using System; - using System.Collections.Generic; - using System.Net.Sockets; - using System.Text; - using Logging; - using Newtonsoft.Json; - using Patterns.EventAggregation.Integration; - using Polly; - using RabbitMQ.Client; - using RabbitMQ.Client.Events; - using RabbitMQ.Client.Exceptions; - public class RabbitMqChannel : IDisposable { private static readonly ILogger Logger = LogManager.Create(); - private readonly string _brokerName; + private readonly string _exchangeName; + private readonly IMessageNameProvider _messageNameProvider; private readonly IConnectionFactory _connectionFactory; private readonly string _queueName; private readonly int _retryCount; - private readonly object _syncRoot = new object(); private readonly HashSet _subscribedEventNames = new HashSet(); + private readonly object _syncRoot = new object(); private IConnection _connection; private EventingBasicConsumer _consumer; private bool _isDisposed; - private IModel _model; + private IModel _channel; - public RabbitMqChannel(IConnectionFactory connectionFactory, string brokerName, string queueName, int retryCount) + public RabbitMqChannel(IMessageNameProvider messageNameProvider, IConnectionFactory connectionFactory, string exchangeName, string queueName, int retryCount) { + _messageNameProvider = messageNameProvider; _connectionFactory = connectionFactory; - _brokerName = brokerName; + _exchangeName = exchangeName; _queueName = queueName; _retryCount = retryCount; } public void Dispose() { - if (_isDisposed) - { - return; - } + if (_isDisposed) return; _isDisposed = true; @@ -62,16 +61,13 @@ public void EnsureClosed() _consumer = null; } - if (_model != null) + if (_channel != null) { - _model.CallbackException -= OnCallbackException; - if (_model.IsOpen) - { - _model.Close(); - } + _channel.CallbackException -= OnCallbackException; + if (_channel.IsOpen) _channel.Close(); - _model.Dispose(); - _model = null; + _channel.Dispose(); + _channel = null; } if (_connection != null) @@ -79,10 +75,7 @@ public void EnsureClosed() _connection.ConnectionShutdown -= OnConnectionShutdown; _connection.CallbackException -= OnCallbackException; _connection.ConnectionBlocked -= OnConnectionBlocked; - if (_connection.IsOpen) - { - _connection.Close(); - } + if (_connection.IsOpen) _connection.Close(); _connection.Dispose(); _connection = null; @@ -91,7 +84,7 @@ public void EnsureClosed() public bool EnsureOpen() { - if (!_isDisposed && _connection?.IsOpen == true && _model?.IsOpen == true && _consumer?.IsRunning == true) + if (!_isDisposed && _connection?.IsOpen == true && _channel?.IsOpen == true && _consumer?.IsRunning == true) { return true; } @@ -122,63 +115,68 @@ private bool Open() _connection.CallbackException += OnCallbackException; _connection.ConnectionBlocked += OnConnectionBlocked; - Logger.Info($"RabbitMQ persistent connection acquired a connection {_connection.Endpoint.HostName} and is subscribed to failure events"); + Logger.Info($"Acquired a connection to RabbitMQ host {_connection.Endpoint.HostName} and is subscribed to failure events"); - _model = _connection.CreateModel(); - _model.ExchangeDeclare(_brokerName, "direct"); - _model.QueueDeclare(_queueName, true, false, false, null); - _consumer = new EventingBasicConsumer(_model); + _channel = _connection.CreateModel(); + _channel.ExchangeDeclare(_exchangeName, "direct"); + _channel.QueueDeclare(_queueName, true, false, false, null); + _consumer = new EventingBasicConsumer(_channel); _consumer.Received += OnMessageReceived; - _model.BasicConsume(_queueName, false, _consumer); - _model.CallbackException += OnCallbackException; + _channel.BasicConsume(_queueName, false, _consumer); + _channel.CallbackException += OnCallbackException; foreach (var subscribedEventName in _subscribedEventNames) { - _model.QueueBind(_queueName, _brokerName, subscribedEventName); + Logger.Info($"Binding messages on exchange {_exchangeName} with routing key {subscribedEventName} to queue {_queueName}"); + _channel.QueueBind(_queueName, _exchangeName, subscribedEventName); } return true; } - Logger.Error("RabbitMQ connections could not be created and opened"); + Logger.Error("RabbitMQ connection could not be created and opened"); return false; } } public void PublishEvent(IIntegrationEvent integrationEvent) { - var eventName = integrationEvent.GetType().Name; + var messageName = _messageNameProvider.GetMessageName(integrationEvent); var message = JsonConvert.SerializeObject(integrationEvent); var body = Encoding.UTF8.GetBytes(message); - Policy.Handle() - .Or() - .WaitAndRetry(_retryCount, - retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), - (ex, time) => { Logger.Warn(ex.ToString()); }) - .Execute(() => _model.BasicPublish(_brokerName, eventName, null, body)); + DoResilent(() => _channel.BasicPublish(_exchangeName, messageName, null, body)); } - public void Subscribe(string eventName) + public void Subscribe(string messageName) { EnsureOpen(); - _model.QueueBind(_queueName, _brokerName, eventName); - _subscribedEventNames.Add(eventName); + _channel.QueueBind(_queueName, _exchangeName, messageName); + _subscribedEventNames.Add(messageName); } public void Unsubscribe(string eventName) { EnsureOpen(); - _model.QueueUnbind(_queueName, _brokerName, eventName); + _channel.QueueUnbind(_queueName, _exchangeName, eventName); _subscribedEventNames.Remove(eventName); } + public void Acknowledge(ulong deliveryTag) + { + Logger.Debug($"Acknowledging {deliveryTag}"); + DoResilent(() => _channel.BasicAck(deliveryTag, false)); + } + + public void NAcknowledge(ulong deliveryTag) + { + Logger.Debug($"NAcknowledging {deliveryTag}"); + DoResilent(() => _channel.BasicNack(deliveryTag, false, false)); + } + private void OnCallbackException(object sender, CallbackExceptionEventArgs e) { - if (_isDisposed) - { - return; - } + if (_isDisposed) return; Logger.Warn(e.Exception, "A RabbitMQ connection threw an exception."); Open(); @@ -186,10 +184,7 @@ private void OnCallbackException(object sender, CallbackExceptionEventArgs e) private void OnConnectionBlocked(object sender, ConnectionBlockedEventArgs e) { - if (_isDisposed) - { - return; - } + if (_isDisposed) return; Logger.Warn($"A RabbitMQ connection is blocked with reason {e.Reason}"); Open(); @@ -197,10 +192,7 @@ private void OnConnectionBlocked(object sender, ConnectionBlockedEventArgs e) private void OnConnectionShutdown(object sender, ShutdownEventArgs reason) { - if (_isDisposed) - { - return; - } + if (_isDisposed) return; Logger.Warn($"A RabbitMQ connection is shut down with reason {reason}."); Open(); @@ -210,5 +202,15 @@ private void OnMessageReceived(object sender, BasicDeliverEventArgs basicDeliver { MessageReceived?.Invoke(this, basicDeliverEventArgs); } + + private void DoResilent(Action action) + { + Policy.Handle() + .Or() + .WaitAndRetry(_retryCount, + retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + (ex, time) => { Logger.Warn(ex.ToString()); }) + .Execute(action); + } } } \ No newline at end of file diff --git a/src/implementations/Backend.Fx.RabbitMq/RabbitMQEventBus.cs b/src/implementations/Backend.Fx.RabbitMq/RabbitMqMessageBus.cs similarity index 56% rename from src/implementations/Backend.Fx.RabbitMq/RabbitMQEventBus.cs rename to src/implementations/Backend.Fx.RabbitMq/RabbitMqMessageBus.cs index feb64c01..c53b6f05 100644 --- a/src/implementations/Backend.Fx.RabbitMq/RabbitMQEventBus.cs +++ b/src/implementations/Backend.Fx.RabbitMq/RabbitMqMessageBus.cs @@ -1,74 +1,75 @@ -using Backend.Fx.Patterns.DependencyInjection; +using System; +using System.Text; +using System.Threading.Tasks; +using Backend.Fx.Environment.MultiTenancy; +using Backend.Fx.Logging; +using Backend.Fx.Patterns.EventAggregation.Integration; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; namespace Backend.Fx.RabbitMq { - using System; - using System.Text; - using System.Threading.Tasks; - using Environment.MultiTenancy; - using Logging; - using Newtonsoft.Json; - using Newtonsoft.Json.Linq; - using Patterns.EventAggregation.Integration; - using RabbitMQ.Client; - using RabbitMQ.Client.Events; - - public class RabbitMqEventBus : EventBus + public class RabbitMqMessageBus : MessageBus { - private static readonly ILogger Logger = LogManager.Create(); + private static readonly ILogger Logger = LogManager.Create(); private readonly RabbitMqChannel _channel; - - public RabbitMqEventBus(IBackendFxApplication application, - IConnectionFactory connectionFactory, - int retryCount, - string brokerName, - string queueName) - : base(application) + + public RabbitMqMessageBus(IConnectionFactory connectionFactory, int retryCount, string exchangeName, string receiveQueueName) { - _channel = new RabbitMqChannel(connectionFactory, brokerName, queueName, retryCount); + _channel = new RabbitMqChannel(MessageNameProvider, connectionFactory, exchangeName, receiveQueueName, retryCount); } public override void Connect() { - Logger.Info("OPening a channel to RabbitMQ..."); + Logger.Info("Opening a channel to RabbitMQ..."); if (_channel.EnsureOpen()) { _channel.MessageReceived += ChannelOnMessageReceived; Logger.Info("Channel to RabbitMQ opened"); } } - + private void ChannelOnMessageReceived(object sender, BasicDeliverEventArgs args) { Logger.Debug($"RabbitMQ message with routing key {args.RoutingKey} received"); - Process(args.RoutingKey, new RabbitMqEventProcessingContext(args.Body)); + try + { + Process(args.RoutingKey, new RabbitMqEventProcessingContext(args.Body)); + _channel.Acknowledge(args.DeliveryTag); + } + catch + { + _channel.NAcknowledge(args.DeliveryTag); + throw; + } } - protected override Task PublishOnEventBus(IIntegrationEvent integrationEvent) + protected override Task PublishOnMessageBus(IIntegrationEvent integrationEvent) { - Logger.Info($"Publishing {integrationEvent.GetType().Name}"); + Logger.Info($"Publishing {MessageNameProvider.GetMessageName(integrationEvent)}"); _channel.EnsureOpen(); _channel.PublishEvent(integrationEvent); return Task.CompletedTask; } - - protected override void Subscribe(string eventName) + + protected override void Subscribe(string messageName) { - Logger.Info($"Subscribing to {eventName}"); + Logger.Info($"Subscribing to {messageName}"); _channel.EnsureOpen(); - _channel.Subscribe(eventName); + _channel.Subscribe(messageName); } - protected override void Unsubscribe(string eventName) + protected override void Unsubscribe(string messageName) { - Logger.Info($"Unsubscribing from {eventName}"); - _channel.Unsubscribe(eventName); + Logger.Info($"Unsubscribing from {messageName}"); + _channel.Unsubscribe(messageName); } protected override void Dispose(bool disposing) { if (disposing) - { if (_channel != null) { Logger.Info("Closing RabbitMQ channel..."); @@ -76,7 +77,6 @@ protected override void Dispose(bool disposing) _channel.Dispose(); Logger.Info("RabbitMQ channel closed"); } - } base.Dispose(disposing); } @@ -86,15 +86,12 @@ private class RabbitMqEventProcessingContext : EventProcessingContext private readonly string _jsonString; public RabbitMqEventProcessingContext(object rawReceivedMessage) - { - Logger.Debug($"Deserializing a message of type {rawReceivedMessage?.GetType().FullName ?? "???"}"); - if (!(rawReceivedMessage is byte[] rawEventPayloadBytes)) - { - throw new InvalidOperationException("Raw event payload is not a binary JSON string"); - } + { + Logger.Trace($"Deserializing a message of type {rawReceivedMessage?.GetType().Name ?? "???"}"); + if (!(rawReceivedMessage is byte[] rawEventPayloadBytes)) throw new InvalidOperationException("Raw event payload is not a binary JSON string"); _jsonString = Encoding.UTF8.GetString(rawEventPayloadBytes); - var eventStub = JsonConvert.DeserializeAnonymousType(_jsonString, new {tenantId = 0, correlationId = Guid.Empty} ); + var eventStub = JsonConvert.DeserializeAnonymousType(_jsonString, new {tenantId = 0, correlationId = Guid.Empty}); TenantId = new TenantId(eventStub.tenantId); CorrelationId = eventStub.correlationId; } @@ -109,5 +106,5 @@ public override IIntegrationEvent GetTypedEvent(Type eventType) return (IIntegrationEvent) JsonConvert.DeserializeObject(_jsonString, eventType); } } - } + } } \ No newline at end of file diff --git a/src/implementations/Backend.Fx.SerilogLogging/Backend.Fx.SerilogLogging.csproj b/src/implementations/Backend.Fx.SerilogLogging/Backend.Fx.SerilogLogging.csproj new file mode 100644 index 00000000..8e2a2d53 --- /dev/null +++ b/src/implementations/Backend.Fx.SerilogLogging/Backend.Fx.SerilogLogging.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0 + + + + + + + + + + + + diff --git a/src/implementations/Backend.Fx.SerilogLogging/SerilogLogger.cs b/src/implementations/Backend.Fx.SerilogLogging/SerilogLogger.cs new file mode 100644 index 00000000..8e104ff8 --- /dev/null +++ b/src/implementations/Backend.Fx.SerilogLogging/SerilogLogger.cs @@ -0,0 +1,185 @@ +using System; +using System.Diagnostics; +using Serilog; +using Serilog.Events; + +namespace Backend.Fx.SerilogLogging +{ + [DebuggerStepThrough] + public class SerilogLogger : Logging.ILogger + { + private readonly ILogger _logger; + + internal SerilogLogger(ILogger logger) + { + _logger = logger; + } + + #region fatal + + public Exception Fatal(Exception exception) + { + _logger.Fatal(exception, string.Empty); + return exception; + } + + public void Fatal(string format, params object[] args) + { + _logger.Fatal(format, args); + } + + public Exception Fatal(Exception exception, string format, params object[] args) + { + _logger.Fatal(exception, format, args); + return exception; + } + + #endregion + + #region error + + public Exception Error(Exception exception) + { + _logger.Error(exception, string.Empty); + return exception; + } + + public void Error(string format, params object[] args) + { + _logger.Error(format, args); + } + + public Exception Error(Exception exception, string format, params object[] args) + { + _logger.Error(exception, format, args); + return exception; + } + + #endregion + + #region warn + + public Exception Warn(Exception exception) + { + _logger.Warning(exception, string.Empty); + return exception; + } + + public void Warn(string format, params object[] args) + { + _logger.Warning(format, args); + } + + public Exception Warn(Exception exception, string format, params object[] args) + { + _logger.Warning(exception, format, args); + return exception; + } + + #endregion + + #region info + + public Exception Info(Exception exception) + { + _logger.Information(exception, string.Empty); + return exception; + } + + public IDisposable InfoDuration(string activity) + { + return new Logging.DurationLogger(s => Info(s), activity); + } + + public IDisposable InfoDuration(string beginMessage, string endMessage) + { + return new Logging.DurationLogger(s => Info(s), beginMessage, endMessage); + } + + public void Info(string format, params object[] args) + { + _logger.Information(format, args); + } + + public Exception Info(Exception exception, string format, params object[] args) + { + _logger.Information(exception, format, args); + return exception; + } + + #endregion + + #region debug + + public bool IsDebugEnabled() + { + return _logger.IsEnabled(LogEventLevel.Debug); + } + + public Exception Debug(Exception exception) + { + _logger.Warning(exception, string.Empty); + return exception; + } + + public IDisposable DebugDuration(string activity) + { + return new Logging.DurationLogger(s => Debug(s), activity); + } + + public IDisposable DebugDuration(string beginMessage, string endMessage) + { + return new Logging.DurationLogger(s => Debug(s), beginMessage, endMessage); + } + + public void Debug(string format, params object[] args) + { + _logger.Debug(format, args); + } + + public Exception Debug(Exception exception, string format, params object[] args) + { + _logger.Debug(exception, format, args); + return exception; + } + + #endregion + + #region trace + + public bool IsTraceEnabled() + { + return _logger.IsEnabled(LogEventLevel.Verbose); + } + + public Exception Trace(Exception exception) + { + _logger.Verbose(exception, string.Empty); + return exception; + } + + public IDisposable TraceDuration(string activity) + { + return new Logging.DurationLogger(s => Trace(s), activity); + } + + public IDisposable TraceDuration(string beginMessage, string endMessage) + { + return new Logging.DurationLogger(s => Trace(s), beginMessage, endMessage); + } + + public void Trace(string format, params object[] args) + { + if (IsTraceEnabled()) _logger.Verbose(format, args); + } + + public Exception Trace(Exception exception, string format, params object[] args) + { + if (IsTraceEnabled()) _logger.Verbose(exception, format, args); + + return exception; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.SerilogLogging/SerilogLoggerFactory.cs b/src/implementations/Backend.Fx.SerilogLogging/SerilogLoggerFactory.cs new file mode 100644 index 00000000..7fa22b7a --- /dev/null +++ b/src/implementations/Backend.Fx.SerilogLogging/SerilogLoggerFactory.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using Backend.Fx.Logging; +using Serilog; +using Serilog.Core; +using Serilog.Events; +using ILogger = Backend.Fx.Logging.ILogger; + +namespace Backend.Fx.SerilogLogging +{ + [DebuggerStepThrough] + public class SerilogLoggerFactory : ILoggerFactory + { + private readonly Logger _rootLogger; + + public SerilogLoggerFactory(Logger logger = null) + { + _rootLogger = logger ?? new LoggerConfiguration().WriteTo.Console(LogEventLevel.Debug).CreateLogger(); + } + + public ILogger Create(string s) + { + return TryGetContextTypeFromString(s, out Type t) + ? new SerilogLogger(_rootLogger.ForContext(t)) + : new SerilogLogger(_rootLogger); + } + + public ILogger Create(Type t) + { + return new SerilogLogger(_rootLogger.ForContext(t)); + } + + public ILogger Create() + { + return new SerilogLogger(_rootLogger.ForContext()); + } + + public void BeginActivity(int activityIndex) + { + //TODO + } + + public void Shutdown() + { + Log.CloseAndFlush(); + } + + private static bool TryGetContextTypeFromString(string s, out Type type) + { + try + { + type = Type.GetType(s); + } + catch + { + type = null; + } + + return type != null; + } + } +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Backend.Fx.SimpleInjectorDependencyInjection.csproj b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Backend.Fx.SimpleInjectorDependencyInjection.csproj index 51c5a05a..f78caa91 100644 --- a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Backend.Fx.SimpleInjectorDependencyInjection.csproj +++ b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Backend.Fx.SimpleInjectorDependencyInjection.csproj @@ -25,7 +25,7 @@ - + diff --git a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/InfrastructureModule.cs b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/InfrastructureModule.cs deleted file mode 100644 index 7d19a5f5..00000000 --- a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/InfrastructureModule.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Reflection; -using System.Security.Principal; -using Backend.Fx.Environment.Authentication; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DataGeneration; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.Patterns.EventAggregation.Domain; -using Backend.Fx.Patterns.EventAggregation.Integration; -using SimpleInjector; - -namespace Backend.Fx.SimpleInjectorDependencyInjection.Modules -{ - public class InfrastructureModule : SimpleInjectorModule - { - private readonly IExceptionLogger _exceptionLogger; - private readonly IEventBus _eventBus; - private static readonly ILogger Logger = LogManager.Create(); - - private Func _domainEventAggregatorFactory; - - public InfrastructureModule(IExceptionLogger exceptionLogger, IEventBus eventBus) - { - _exceptionLogger = exceptionLogger; - _eventBus = eventBus; - } - - public override void Register(ICompositionRoot compositionRoot) - { - _domainEventAggregatorFactory = () => new DomainEventAggregator(compositionRoot); - base.Register(compositionRoot); - } - - protected override void Register(Container container, ScopedLifestyle scopedLifestyle) - { - container.RegisterInstance(_exceptionLogger); - - // the current IIdentity is resolved using the scoped CurrentIdentityHolder that is maintained when opening a scope - Logger.Debug($"Registering {nameof(CurrentIdentityHolder)} as {nameof(ICurrentTHolder)}"); - container.Register, CurrentIdentityHolder>(); - - // same for the current TenantId - Logger.Debug($"Registering {nameof(CurrentTenantIdHolder)} as {nameof(ICurrentTHolder)}"); - container.Register, CurrentTenantIdHolder>(); - - // domain event subsystem - Logger.Debug("Registering event aggregator"); - container.Register(_domainEventAggregatorFactory); - - // integration event subsystem - Logger.Debug("Registering event bus"); - container.RegisterInstance(_eventBus); - - // initial data generators collection (using the current assembly causes an empty collection) - container.Collection.Register(typeof(InfrastructureModule).GetTypeInfo().Assembly); - - container.Register(); - - } - } -} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorDataGenerationModule.cs b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorDataGenerationModule.cs new file mode 100644 index 00000000..1f06ce66 --- /dev/null +++ b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorDataGenerationModule.cs @@ -0,0 +1,26 @@ +using System; +using System.Reflection; +using Backend.Fx.Logging; +using Backend.Fx.Patterns.DataGeneration; +using Backend.Fx.Patterns.DependencyInjection; +using SimpleInjector; + +namespace Backend.Fx.SimpleInjectorDependencyInjection.Modules +{ + public class SimpleInjectorDataGenerationModule : IModule + { + private static readonly ILogger Logger = LogManager.Create(); + private readonly Assembly[] _domainAssemblies; + + public SimpleInjectorDataGenerationModule(params Assembly[] domainAssemblies) + { + _domainAssemblies = domainAssemblies; + } + + public void Register(ICompositionRoot compositionRoot) + { + Container container = ((SimpleInjectorCompositionRoot) compositionRoot).Container; + container.Collection.Register(container.GetTypesToRegister(typeof(IDataGenerator), _domainAssemblies)); + } + } +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/DomainModule.cs b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorDomainModule.cs similarity index 83% rename from src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/DomainModule.cs rename to src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorDomainModule.cs index 2d03b94b..48c0dfa5 100644 --- a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/DomainModule.cs +++ b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorDomainModule.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Reflection; using System.Security.Principal; using Backend.Fx.BuildingBlocks; @@ -7,6 +8,7 @@ using Backend.Fx.Patterns.Authorization; using Backend.Fx.Patterns.DataGeneration; using Backend.Fx.Patterns.EventAggregation.Domain; +using Backend.Fx.Patterns.EventAggregation.Integration; using Backend.Fx.Patterns.Jobs; using SimpleInjector; @@ -18,13 +20,13 @@ namespace Backend.Fx.SimpleInjectorDependencyInjection.Modules /// the collections of s, s and s /// found in the given list of domain assemblies. /// - public abstract class DomainModule : SimpleInjectorModule + public class SimpleInjectorDomainModule : SimpleInjectorModule { - private static readonly ILogger Logger = LogManager.Create(); + private static readonly ILogger Logger = LogManager.Create(); private readonly Assembly[] _domainAssemblies; private readonly string _domainAssembliesForLogging; - protected DomainModule(params Assembly[] domainAssemblies) + public SimpleInjectorDomainModule(params Assembly[] domainAssemblies) { _domainAssemblies = domainAssemblies; _domainAssembliesForLogging = string.Join(",", _domainAssemblies.Select(ass => ass.GetName().Name)); @@ -37,25 +39,25 @@ protected override void Register(Container container, ScopedLifestyle scopedLife RegisterAuthorization(container); // all jobs are dynamically registered - foreach (var jobType in container.GetTypesToRegister(typeof(IJob), _domainAssemblies)) + foreach (Type jobType in container.GetTypesToRegister(typeof(IJob), _domainAssemblies)) { Logger.Debug($"Registering {jobType.Name}"); container.Register(jobType); } - // initial data generation subsystem - foreach (var dataGeneratorType in container.GetTypesToRegister(typeof(IDataGenerator), _domainAssemblies)) - { - Logger.Debug($"Appending {dataGeneratorType.Name} to list of IDataGenerators"); - container.Collection.Append(typeof(IDataGenerator), dataGeneratorType); - } - // domain event handlers - foreach (var domainEventHandlerType in container.GetTypesToRegister(typeof(IDomainEventHandler<>), _domainAssemblies)) + foreach (Type domainEventHandlerType in container.GetTypesToRegister(typeof(IDomainEventHandler<>), _domainAssemblies)) { Logger.Debug($"Appending {domainEventHandlerType.Name} to list of IDomainEventHandler"); container.Collection.Append(typeof(IDomainEventHandler<>), domainEventHandlerType); } + + // integration message handlers + foreach (Type integrationMessageHandlerType in container.GetTypesToRegister(typeof(IIntegrationMessageHandler<>), _domainAssemblies)) + { + Logger.Debug($"Registering {integrationMessageHandlerType.Name}"); + container.Register(integrationMessageHandlerType); + } } private void RegisterDomainAndApplicationServices(Container container) @@ -91,7 +93,7 @@ private void RegisterAuthorization(Container container) { Logger.Debug($"Registering authorization services from {_domainAssembliesForLogging}"); var aggregateRootAuthorizationTypes = container.GetTypesToRegister(typeof(IAggregateAuthorization<>), _domainAssemblies).ToArray(); - foreach (var aggregateAuthorizationType in aggregateRootAuthorizationTypes) + foreach (Type aggregateAuthorizationType in aggregateRootAuthorizationTypes) { var serviceTypes = aggregateAuthorizationType .GetTypeInfo() @@ -100,7 +102,7 @@ private void RegisterAuthorization(Container container) && impif.GenericTypeArguments.Length == 1 && typeof(AggregateRoot).GetTypeInfo().IsAssignableFrom(impif.GenericTypeArguments[0].GetTypeInfo())); - foreach (var serviceType in serviceTypes) + foreach (Type serviceType in serviceTypes) { Logger.Debug($"Registering scoped authorization service {serviceType.Name} with implementation {aggregateAuthorizationType.Name}"); container.Register(serviceType, aggregateAuthorizationType); diff --git a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorInfrastructureModule.cs b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorInfrastructureModule.cs new file mode 100644 index 00000000..e9331a84 --- /dev/null +++ b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorInfrastructureModule.cs @@ -0,0 +1,52 @@ +using System; +using System.Reflection; +using Backend.Fx.Patterns.DependencyInjection; +using SimpleInjector; + +namespace Backend.Fx.SimpleInjectorDependencyInjection.Modules +{ + public class SimpleInjectorInfrastructureModule : IInfrastructureModule + { + private readonly Container _container; + + public SimpleInjectorInfrastructureModule(Container container) + { + _container = container; + } + + public void RegisterScoped() where TService : class where TImpl : class, TService + { + _container.Register(); + } + + public void RegisterScoped(Func factory) where TService : class + { + _container.Register(factory); + } + + public void RegisterScoped(Type serviceType, Assembly[] assembliesToScan) + { + _container.Register(serviceType, assembliesToScan); + } + + public void RegisterDecorator() where TService : class where TImpl : class, TService + { + _container.RegisterDecorator(); + } + + public void RegisterScoped(Type serviceType, Type implementationType) + { + _container.Register(serviceType, implementationType); + } + + public void RegisterSingleton() where TService : class where TImpl : class, TService + { + _container.RegisterSingleton(); + } + + public void RegisterInstance(TService instance) where TService : class + { + _container.RegisterInstance(instance); + } + } +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorModule.cs b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorModule.cs index 0c3fbd28..cd8abf4d 100644 --- a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorModule.cs +++ b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorModule.cs @@ -13,7 +13,7 @@ public abstract class SimpleInjectorModule : IModule public virtual void Register(ICompositionRoot compositionRoot) { Logger.Debug($"Registering {GetType().Name}"); - SimpleInjectorCompositionRoot simpleInjectorCompositionRoot = (SimpleInjectorCompositionRoot) compositionRoot; + var simpleInjectorCompositionRoot = (SimpleInjectorCompositionRoot) compositionRoot; Register(simpleInjectorCompositionRoot.Container, simpleInjectorCompositionRoot.ScopedLifestyle); } } diff --git a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorCompositionRoot.cs b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorCompositionRoot.cs index f20dcf81..18086308 100644 --- a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorCompositionRoot.cs +++ b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorCompositionRoot.cs @@ -1,9 +1,11 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Threading; using Backend.Fx.Logging; using Backend.Fx.Patterns.DependencyInjection; using Backend.Fx.Patterns.EventAggregation.Domain; +using Backend.Fx.SimpleInjectorDependencyInjection.Modules; using SimpleInjector; using SimpleInjector.Advanced; using SimpleInjector.Lifestyles; @@ -17,6 +19,7 @@ public class SimpleInjectorCompositionRoot : ICompositionRoot { private static readonly ILogger Logger = LogManager.Create(); + private int _scopeSequenceNumber = 1; /// /// This constructor creates a composition root that prefers scoped lifestyle /// @@ -30,7 +33,8 @@ public SimpleInjectorCompositionRoot(ILifestyleSelectionBehavior lifestyleBehavi ScopedLifestyle = scopedLifestyle; Container.Options.LifestyleSelectionBehavior = lifestyleBehavior; Container.Options.DefaultScopedLifestyle = ScopedLifestyle; - Container.Register, CurrentCorrelationHolder>(); + InfrastructureModule = new SimpleInjectorInfrastructureModule(Container); + InstanceProvider = new SimpleInjectorInstanceProvider(Container); } public Container Container { get; } = new Container(); @@ -84,24 +88,14 @@ public IEnumerable GetInstances() where T : class } /// - public IDisposable BeginScope() + public IInjectionScope BeginScope() { - var scope = AsyncScopedLifestyle.BeginScope(Container); - return scope; + return new SimpleInjectorInjectionScope(Interlocked.Increment(ref _scopeSequenceNumber), AsyncScopedLifestyle.BeginScope(Container)); } - public bool TryGetCurrentCorrelation(out Correlation correlation) - { - Scope scope = ScopedLifestyle.GetCurrentScope(Container); - if (scope == null) - { - correlation = null; - return false; - } + public IInstanceProvider InstanceProvider { get; } - correlation = scope.GetInstance>().Current; - return true; - } + public IInfrastructureModule InfrastructureModule { get; } public Scope GetCurrentScope() { diff --git a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorInjectionScope.cs b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorInjectionScope.cs new file mode 100644 index 00000000..c22eedc3 --- /dev/null +++ b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorInjectionScope.cs @@ -0,0 +1,24 @@ +using Backend.Fx.Patterns.DependencyInjection; +using SimpleInjector; + +namespace Backend.Fx.SimpleInjectorDependencyInjection +{ + public class SimpleInjectorInjectionScope : InjectionScope + { + private readonly Scope _scope; + + public SimpleInjectorInjectionScope(int sequenceNumber, Scope scope) : base(sequenceNumber) + { + _scope = scope; + InstanceProvider = new SimpleInjectorInstanceProvider(_scope.Container); + } + + public override IInstanceProvider InstanceProvider { get; } + + + public override void Dispose() + { + _scope.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorInstanceProvider.cs b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorInstanceProvider.cs new file mode 100644 index 00000000..610dd71b --- /dev/null +++ b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/SimpleInjectorInstanceProvider.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Backend.Fx.Patterns.DependencyInjection; +using SimpleInjector; + +namespace Backend.Fx.SimpleInjectorDependencyInjection +{ + public class SimpleInjectorInstanceProvider : IInstanceProvider + { + private readonly Container _container; + + public SimpleInjectorInstanceProvider(Container container) + { + _container = container; + } + + public object GetInstance(Type serviceType) + { + return _container.GetInstance(serviceType); + } + + public IEnumerable GetInstances(Type serviceType) + { + return (IEnumerable) _container.GetInstance(typeof(IEnumerable<>).MakeGenericType(serviceType)); + } + + public T GetInstance() where T : class + { + return _container.GetInstance(); + } + + public IEnumerable GetInstances() where T : class + { + return _container.GetInstance>(); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/Backend.Fx.EfCorePersistence.Tests.csproj b/tests/Backend.Fx.EfCorePersistence.Tests/Backend.Fx.EfCorePersistence.Tests.csproj index 492df4cc..6bdd9f62 100644 --- a/tests/Backend.Fx.EfCorePersistence.Tests/Backend.Fx.EfCorePersistence.Tests.csproj +++ b/tests/Backend.Fx.EfCorePersistence.Tests/Backend.Fx.EfCorePersistence.Tests.csproj @@ -5,20 +5,23 @@ - + - - - - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + - + diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/DbConnectionEx.cs b/tests/Backend.Fx.EfCorePersistence.Tests/DbConnectionEx.cs index c4c1d899..499d8a51 100644 --- a/tests/Backend.Fx.EfCorePersistence.Tests/DbConnectionEx.cs +++ b/tests/Backend.Fx.EfCorePersistence.Tests/DbConnectionEx.cs @@ -9,7 +9,7 @@ public static class DbConnectionEx { public static void ExecuteNonQuery(this IDbConnection openConnection, string cmd) { - using (var command = openConnection.CreateCommand()) + using (IDbCommand command = openConnection.CreateCommand()) { command.CommandText = cmd; command.ExecuteNonQuery(); @@ -18,29 +18,23 @@ public static void ExecuteNonQuery(this IDbConnection openConnection, string cmd public static T ExecuteScalar(this IDbConnection openConnection, string cmd) { - using (var command = openConnection.CreateCommand()) + using (IDbCommand command = openConnection.CreateCommand()) { command.CommandText = cmd; object scalarResult = command.ExecuteScalar(); - if (typeof(T) == typeof(int)) - { - return (T)(object)Convert.ToInt32(scalarResult); - } - return (T)scalarResult; + if (typeof(T) == typeof(int)) return (T) (object) Convert.ToInt32(scalarResult); + return (T) scalarResult; } } [UsedImplicitly] public static IEnumerable ExecuteReader(this IDbConnection openConnection, string cmd, Func forEachResultFunc) { - using (var command = openConnection.CreateCommand()) + using (IDbCommand command = openConnection.CreateCommand()) { command.CommandText = cmd; IDataReader reader = command.ExecuteReader(); - while (reader.NextResult()) - { - yield return forEachResultFunc(reader); - } + while (reader.NextResult()) yield return forEachResultFunc(reader); } } } diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/DbSession.cs b/tests/Backend.Fx.EfCorePersistence.Tests/DbSession.cs deleted file mode 100644 index 8a466999..00000000 --- a/tests/Backend.Fx.EfCorePersistence.Tests/DbSession.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Data.Common; -using System.Security.Principal; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence; -using Backend.Fx.Environment.Authentication; -using Backend.Fx.Environment.DateAndTime; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.Patterns.EventAggregation.Domain; -using Backend.Fx.Patterns.EventAggregation.Integration; -using Backend.Fx.Patterns.Transactions; -using Backend.Fx.Patterns.UnitOfWork; -using FakeItEasy; -using Microsoft.EntityFrameworkCore; - -namespace Backend.Fx.EfCorePersistence.Tests -{ - public class DbSession : IDisposable - { - public DbSession(DbConnection connection, DbContextOptionsBuilder optionsBuilder) - { - Connection = connection; - OptionsBuilder = optionsBuilder; - connection.Open(); - } - - public DbConnection Connection { get; } - public DbContextOptionsBuilder OptionsBuilder { get; } - - public IUnitOfWork BeginUnitOfWork(bool asReadonly = false, IClock clock = null, IIdentity identity = null) - { - ICurrentTHolder currentIdentityHolder = new CurrentIdentityHolder(); - currentIdentityHolder.ReplaceCurrent(identity ?? new SystemIdentity()); - - var dbContext = new TestDbContext(OptionsBuilder.Options); - IUnitOfWork unitOfWork = new EfUnitOfWork(clock ?? new FrozenClock(), - currentIdentityHolder, - A.Fake(), - A.Fake(), - dbContext); - - ITransactionContext transactionContext = new TransactionContext(Connection); - if (asReadonly) - { - transactionContext = new ReadonlyDecorator(transactionContext); - } - - unitOfWork = new DbContextTransactionDecorator(transactionContext,dbContext, unitOfWork); - // unitOfWork = new DbConnectionDecorator(Connection, unitOfWork); - - unitOfWork.Begin(); - return unitOfWork; - } - - public void Dispose() - { - Connection?.Close(); - } - } - - public static class TestEx - { - public static DbContext GetDbContext(this IUnitOfWork unitOfWork) - { - if (unitOfWork is EfUnitOfWork efUnitOfWork) - { - return efUnitOfWork.DbContext; - } - - if (unitOfWork is DbContextTransactionDecorator dbcTransactionDecorator) - { - return GetDbContext(dbcTransactionDecorator.UnitOfWork); - } - - if (unitOfWork is DbTransactionDecorator transactionDecorator) - { - return GetDbContext(transactionDecorator.UnitOfWork); - } - - if (unitOfWork is DbConnectionDecorator connectionDecorator) - { - return GetDbContext(connectionDecorator.UnitOfWork); - } - - throw new InvalidOperationException(); - } - - public static DbTransaction GetDbTransaction(this IUnitOfWork unitOfWork) - { - if (unitOfWork is DbContextTransactionDecorator dbcTransactionDecorator) - { - return (DbTransaction) dbcTransactionDecorator.TransactionContext.CurrentTransaction; - } - - if (unitOfWork is DbTransactionDecorator transactionDecorator) - { - return (DbTransaction) transactionDecorator.TransactionContext.CurrentTransaction; - } - - if (unitOfWork is DbConnectionDecorator connectionDecorator) - { - return GetDbTransaction(connectionDecorator.UnitOfWork); - } - - throw new InvalidOperationException(); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/Blog.cs b/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/Blog.cs index 5cb852e7..27c8be41 100644 --- a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/Blog.cs +++ b/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/Blog.cs @@ -8,7 +8,9 @@ namespace Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain public class Blog : AggregateRoot { [UsedImplicitly] - private Blog() { } + private Blog() + { + } public Blog(int id, string name) : base(id) { @@ -19,7 +21,7 @@ public Blog(int id, string name) : base(id) public ISet Posts { get; } = new HashSet(); - public Post AddPost(IEntityIdGenerator idGenerator, string name, bool isPublic=false) + public Post AddPost(IEntityIdGenerator idGenerator, string name, bool isPublic = false) { var post = new Post(idGenerator.NextId(), this, name, isPublic); Posts.Add(post); @@ -29,10 +31,7 @@ public Post AddPost(IEntityIdGenerator idGenerator, string name, bool isPublic=f public void Modify(string modified) { Name = modified; - foreach (var post in Posts) - { - post.SetName(modified); - } + foreach (Post post in Posts) post.SetName(modified); } } } \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/BlogAuthorization.cs b/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/BlogAuthorization.cs index f89f183a..958e27a8 100644 --- a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/BlogAuthorization.cs +++ b/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/BlogAuthorization.cs @@ -5,5 +5,6 @@ namespace Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain { [UsedImplicitly] public class BlogAuthorization : AllowAll - { } -} + { + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/Blogger.cs b/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/Blogger.cs index 86ace3c3..a21db5ab 100644 --- a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/Blogger.cs +++ b/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/Blogger.cs @@ -7,7 +7,8 @@ public class Blogger : AggregateRoot { [UsedImplicitly] private Blogger() - { } + { + } public Blogger(int id, string lastName, string firstName) : base(id) { diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/Post.cs b/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/Post.cs index 4e5c869e..e71875c0 100644 --- a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/Post.cs +++ b/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Domain/Post.cs @@ -7,9 +7,11 @@ namespace Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain public class Post : Entity { [UsedImplicitly] - private Post() { } + private Post() + { + } - public Post(int id, Blog blog, string name, bool isPublic=false) : base(id) + public Post(int id, Blog blog, string name, bool isPublic = false) : base(id) { Blog = blog; BlogId = blog.Id; @@ -17,14 +19,13 @@ public Post(int id, Blog blog, string name, bool isPublic=false) : base(id) TargetAudience = new TargetAudience {IsPublic = isPublic, Culture = "fr-FR"}; } - [UsedImplicitly] - public int BlogId { get; private set; } - [UsedImplicitly] - public Blog Blog { get; private set; } - [UsedImplicitly] - public string Name { get; private set; } - [UsedImplicitly] - public TargetAudience TargetAudience { get; set; } + [UsedImplicitly] public int BlogId { get; private set; } + + [UsedImplicitly] public Blog Blog { get; private set; } + + [UsedImplicitly] public string Name { get; private set; } + + [UsedImplicitly] public TargetAudience TargetAudience { get; set; } public void SetName(string name) { @@ -37,6 +38,7 @@ public class TargetAudience : ValueObject public string Culture { get; set; } public bool IsPublic { get; set; } + protected override IEnumerable GetEqualityComponents() { yield return Culture; diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/BlogMapping.cs b/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/BlogMapping.cs index 4d5b8411..4f21dddb 100644 --- a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/BlogMapping.cs +++ b/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/BlogMapping.cs @@ -8,21 +8,20 @@ namespace Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence { public class BlogMapping : AggregateMapping { - public override void ApplyEfMapping(ModelBuilder modelBuilder) - { - modelBuilder.Entity().OwnsOne(p => p.TargetAudience); - } - public override IEnumerable>> IncludeDefinitions { get { return new Expression>[] { - blog => blog.Posts, + blog => blog.Posts }; } } + public override void ApplyEfMapping(ModelBuilder modelBuilder) + { + modelBuilder.Entity().OwnsOne(p => p.TargetAudience); + } } } \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/BloggerMapping.cs b/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/BloggerMapping.cs index d8afb740..2659bcd9 100644 --- a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/BloggerMapping.cs +++ b/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/BloggerMapping.cs @@ -3,5 +3,6 @@ namespace Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence { public class BloggerMapping : PlainAggregateMapping - { } + { + } } \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/TestDbContext.cs b/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/TestDbContext.cs index fea17c48..bd40f2f5 100644 --- a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/TestDbContext.cs +++ b/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/TestDbContext.cs @@ -12,17 +12,17 @@ public TestDbContext([NotNull] DbContextOptions options) : base(o Database.AutoTransactionsEnabled = false; } + public DbSet Bloggers { get; [UsedImplicitly] set; } + + public DbSet Blogs { get; [UsedImplicitly] set; } + public DbSet Posts { get; [UsedImplicitly] set; } + public DbSet Tenants { get; [UsedImplicitly] set; } + protected override void OnModelCreating(ModelBuilder modelBuilder) { this.ApplyAggregateMappings(modelBuilder); modelBuilder.RegisterRowVersionProperty(); modelBuilder.RegisterEntityIdAsNeverGenerated(); } - - public DbSet Bloggers { get; [UsedImplicitly] set; } - - public DbSet Blogs { get; [UsedImplicitly] set; } - public DbSet Posts { get; [UsedImplicitly] set; } - public DbSet Tenants { get; [UsedImplicitly] set; } } -} +} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/TestDbContextFactory.cs b/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/TestDbContextFactory.cs index 85280300..cefd3713 100644 --- a/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/TestDbContextFactory.cs +++ b/tests/Backend.Fx.EfCorePersistence.Tests/DummyImpl/Persistence/TestDbContextFactory.cs @@ -5,7 +5,8 @@ namespace Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence { - [UsedImplicitly, Obsolete("Only for migration support at design time")] + [UsedImplicitly] + [Obsolete("Only for migration support at design time")] public class TestDbContextFactory : IDesignTimeDbContextFactory { public TestDbContext CreateDbContext(string[] args) @@ -13,4 +14,4 @@ public TestDbContext CreateDbContext(string[] args) return new TestDbContext(new DbContextOptionsBuilder().UseSqlite("DataSource=:memory:").Options); } } -} +} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/DatabaseFixture.cs b/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/DatabaseFixture.cs index 4a8c820e..59578f8c 100644 --- a/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/DatabaseFixture.cs +++ b/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/DatabaseFixture.cs @@ -1,10 +1,14 @@ -using System.Data.Common; +using System.Data; +using System.Security.Principal; using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence; +using Backend.Fx.Environment.Authentication; +using Backend.Fx.Environment.DateAndTime; +using Backend.Fx.Environment.Persistence; using Microsoft.EntityFrameworkCore; namespace Backend.Fx.EfCorePersistence.Tests.Fixtures { - public abstract class DatabaseFixture + public abstract class DatabaseFixture { public void CreateDatabase() { @@ -16,8 +20,28 @@ public void CreateDatabase() protected abstract DbContextOptions GetDbContextOptionsForDbCreation(); - protected abstract DbContextOptionsBuilder GetDbContextOptionsBuilder(DbConnection connection); + public abstract DbContextOptionsBuilder GetDbContextOptionsBuilder(IDbConnection connection); - public abstract DbSession UseDbSession(); + public abstract DbConnectionOperationDecorator UseOperation(); + + public TestDbSession CreateTestDbSession(DbConnectionOperationDecorator operation = null, IIdentity asIdentity = null, IClock clock = null) + { + CurrentIdentityHolder CreateAsIdentity() + { + var cih = new CurrentIdentityHolder(); + cih.ReplaceCurrent(asIdentity); + return cih; + } + + clock ??= new WallClock(); + operation ??= UseOperation(); + + operation.Begin(); + + var identityHolder = asIdentity == null + ? CurrentIdentityHolder.CreateSystem() + : CreateAsIdentity(); + return new TestDbSession(this, operation, identityHolder, clock); + } } } \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/SqlServerDatabaseFixture.cs b/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/SqlServerDatabaseFixture.cs index 14da31b6..5f57d5b2 100644 --- a/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/SqlServerDatabaseFixture.cs +++ b/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/SqlServerDatabaseFixture.cs @@ -1,8 +1,9 @@ using System; using System.Data; -using System.Data.Common; using System.Data.SqlClient; using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence; +using Backend.Fx.Environment.Persistence; +using Backend.Fx.Patterns.DependencyInjection; using Microsoft.EntityFrameworkCore; namespace Backend.Fx.EfCorePersistence.Tests.Fixtures @@ -15,29 +16,30 @@ public class SqlServerDatabaseFixture : DatabaseFixture public SqlServerDatabaseFixture() { - string dbName = $"TestFixture_{_testindex++:000}"; + var dbName = $"TestFixture_{_testindex++:000}"; var sqlConnectionStringBuilder = new SqlConnectionStringBuilder("Server=.\\SQLExpress;Trusted_Connection=True;"); using (IDbConnection connection = new SqlConnection(sqlConnectionStringBuilder.ConnectionString)) { connection.Open(); - using (var dropCommand = connection.CreateCommand()) + using (IDbCommand dropCommand = connection.CreateCommand()) { dropCommand.CommandText = $"IF EXISTS(SELECT * FROM sys.Databases WHERE Name='{dbName}') ALTER DATABASE [{dbName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE"; dropCommand.ExecuteNonQuery(); } - using (var dropCommand = connection.CreateCommand()) + using (IDbCommand dropCommand = connection.CreateCommand()) { dropCommand.CommandText = $"IF EXISTS(SELECT * FROM sys.Databases WHERE Name='{dbName}') DROP DATABASE [{dbName}]"; dropCommand.ExecuteNonQuery(); } - using (var createCommand = connection.CreateCommand()) + using (IDbCommand createCommand = connection.CreateCommand()) { createCommand.CommandText = $"CREATE DATABASE [{dbName}]"; createCommand.ExecuteNonQuery(); } + connection.Close(); } @@ -50,16 +52,18 @@ protected override DbContextOptions GetDbContextOptionsForDbCreat return new DbContextOptionsBuilder().UseSqlServer(_connectionString).Options; } - - protected override DbContextOptionsBuilder GetDbContextOptionsBuilder(DbConnection connection) + + public override DbContextOptionsBuilder GetDbContextOptionsBuilder(IDbConnection connection) { - return new DbContextOptionsBuilder().UseSqlServer(connection); + return new DbContextOptionsBuilder().UseSqlServer((SqlConnection) connection); } - public override DbSession UseDbSession() + public override DbConnectionOperationDecorator UseOperation() { - var sqlConnection = new SqlConnection(_connectionString); - return new DbSession(sqlConnection, GetDbContextOptionsBuilder(sqlConnection)); + var sqliteConnection = new SqlConnection(_connectionString); + IOperation operation = new Operation(); + operation = new DbTransactionOperationDecorator(sqliteConnection, operation); + return new DbConnectionOperationDecorator(sqliteConnection, operation); } } } \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/SqliteDatabaseFixture.cs b/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/SqliteDatabaseFixture.cs index 0ba762ea..1fa813b7 100644 --- a/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/SqliteDatabaseFixture.cs +++ b/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/SqliteDatabaseFixture.cs @@ -1,6 +1,9 @@ +using System.Data; using System.Data.Common; using System.IO; using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence; +using Backend.Fx.Environment.Persistence; +using Backend.Fx.Patterns.DependencyInjection; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; @@ -15,15 +18,17 @@ protected override DbContextOptions GetDbContextOptionsForDbCreat return new DbContextOptionsBuilder().UseSqlite(_connectionString).Options; } - protected override DbContextOptionsBuilder GetDbContextOptionsBuilder(DbConnection connection) + public override DbContextOptionsBuilder GetDbContextOptionsBuilder(IDbConnection connection) { - return new DbContextOptionsBuilder().UseSqlite(connection); + return new DbContextOptionsBuilder().UseSqlite((SqliteConnection) connection); } - public override DbSession UseDbSession() + public override DbConnectionOperationDecorator UseOperation() { var sqliteConnection = new SqliteConnection(_connectionString); - return new DbSession(sqliteConnection, GetDbContextOptionsBuilder(sqliteConnection)); + IOperation operation = new Operation(); + operation = new DbTransactionOperationDecorator(sqliteConnection, operation); + return new DbConnectionOperationDecorator(sqliteConnection, operation); } } } \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/TestDbSession.cs b/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/TestDbSession.cs new file mode 100644 index 00000000..8fe47f7f --- /dev/null +++ b/tests/Backend.Fx.EfCorePersistence.Tests/Fixtures/TestDbSession.cs @@ -0,0 +1,40 @@ +using System; +using System.Data; +using System.Security.Principal; +using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence; +using Backend.Fx.Environment.DateAndTime; +using Backend.Fx.Environment.Persistence; +using Backend.Fx.Patterns.DependencyInjection; + +namespace Backend.Fx.EfCorePersistence.Tests.Fixtures +{ + public class TestDbSession : ICanFlush, IDisposable + { + private readonly DbConnectionOperationDecorator _operation; + private readonly EfFlush _efFlush; + + public TestDbSession(DatabaseFixture fixture, DbConnectionOperationDecorator operation, ICurrentTHolder identityHolder, IClock clock) + { + _operation = operation; + DbContext = new TestDbContext(fixture.GetDbContextOptionsBuilder(operation.DbConnection).Options); + _efFlush = new EfFlush(DbContext, identityHolder, clock); + DbConnection = operation.DbConnection; + } + + + public TestDbContext DbContext { get; } + public IDbConnection DbConnection { get; } + + public void Flush() + { + _efFlush.Flush(); + } + + public void Dispose() + { + _efFlush.Flush(); + DbContext.Dispose(); + _operation.Complete(); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/Migrations/20190624150947_Initial.cs b/tests/Backend.Fx.EfCorePersistence.Tests/Migrations/20190624150947_Initial.cs index 53fad055..841db0cf 100644 --- a/tests/Backend.Fx.EfCorePersistence.Tests/Migrations/20190624150947_Initial.cs +++ b/tests/Backend.Fx.EfCorePersistence.Tests/Migrations/20190624150947_Initial.cs @@ -1,5 +1,6 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; + // ReSharper disable RedundantArgumentDefaultValue namespace Backend.Fx.EfCorePersistence.Tests.Migrations @@ -9,8 +10,8 @@ public partial class Initial : Migration protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( - name: "Bloggers", - columns: table => new + "Bloggers", + table => new { Id = table.Column(nullable: false), CreatedOn = table.Column(nullable: false), @@ -23,14 +24,11 @@ protected override void Up(MigrationBuilder migrationBuilder) Bio = table.Column(nullable: true), RowVersion = table.Column(rowVersion: true, nullable: true) }, - constraints: table => - { - table.PrimaryKey("PK_Bloggers", x => x.Id); - }); + constraints: table => { table.PrimaryKey("PK_Bloggers", x => x.Id); }); migrationBuilder.CreateTable( - name: "Blogs", - columns: table => new + "Blogs", + table => new { Id = table.Column(nullable: false), CreatedOn = table.Column(nullable: false), @@ -41,31 +39,25 @@ protected override void Up(MigrationBuilder migrationBuilder) Name = table.Column(nullable: true), RowVersion = table.Column(rowVersion: true, nullable: true) }, - constraints: table => - { - table.PrimaryKey("PK_Blogs", x => x.Id); - }); + constraints: table => { table.PrimaryKey("PK_Blogs", x => x.Id); }); migrationBuilder.CreateTable( - name: "Tenants", - columns: table => new + "Tenants", + table => new { Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), + .Annotation("Sqlite:Autoincrement", true), Name = table.Column(nullable: false), Description = table.Column(nullable: true), IsDemoTenant = table.Column(nullable: false), State = table.Column(nullable: false), DefaultCultureName = table.Column(nullable: true) }, - constraints: table => - { - table.PrimaryKey("PK_Tenants", x => x.Id); - }); + constraints: table => { table.PrimaryKey("PK_Tenants", x => x.Id); }); migrationBuilder.CreateTable( - name: "Posts", - columns: table => new + "Posts", + table => new { Id = table.Column(nullable: false), CreatedOn = table.Column(nullable: false), @@ -82,32 +74,32 @@ protected override void Up(MigrationBuilder migrationBuilder) { table.PrimaryKey("PK_Posts", x => x.Id); table.ForeignKey( - name: "FK_Posts_Blogs_BlogId", - column: x => x.BlogId, - principalTable: "Blogs", - principalColumn: "Id", + "FK_Posts_Blogs_BlogId", + x => x.BlogId, + "Blogs", + "Id", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateIndex( - name: "IX_Posts_BlogId", - table: "Posts", - column: "BlogId"); + "IX_Posts_BlogId", + "Posts", + "BlogId"); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( - name: "Bloggers"); + "Bloggers"); migrationBuilder.DropTable( - name: "Posts"); + "Posts"); migrationBuilder.DropTable( - name: "Tenants"); + "Tenants"); migrationBuilder.DropTable( - name: "Blogs"); + "Blogs"); } } -} +} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/TestConfig.cs b/tests/Backend.Fx.EfCorePersistence.Tests/TestConfig.cs index 92a3eeb0..6fde5b9a 100644 --- a/tests/Backend.Fx.EfCorePersistence.Tests/TestConfig.cs +++ b/tests/Backend.Fx.EfCorePersistence.Tests/TestConfig.cs @@ -1,14 +1,17 @@ -using Backend.Fx.NLogLogging; +using Backend.Fx.EfCorePersistence.Tests; +using Backend.Fx.NLogLogging; using MarcWittke.Xunit.AssemblyFixture; +using Xunit; -[assembly: Xunit.TestFramework("MarcWittke.Xunit.AssemblyFixture.XunitTestFrameworkWithAssemblyFixture", "MarcWittke.Xunit.AssemblyFixture")] -[assembly: AssemblyFixture(typeof(Backend.Fx.EfCorePersistence.Tests.TestLoggingFixture))] +[assembly: TestFramework("MarcWittke.Xunit.AssemblyFixture.XunitTestFrameworkWithAssemblyFixture", "MarcWittke.Xunit.AssemblyFixture")] +[assembly: AssemblyFixture(typeof(TestLoggingFixture))] namespace Backend.Fx.EfCorePersistence.Tests { public class TestLoggingFixture : LoggingFixture { public TestLoggingFixture() : base("Backend.Fx") - { } + { + } } } \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/TheDbContext.cs b/tests/Backend.Fx.EfCorePersistence.Tests/TheDbContext.cs index c71e15e6..07fdd6b1 100644 --- a/tests/Backend.Fx.EfCorePersistence.Tests/TheDbContext.cs +++ b/tests/Backend.Fx.EfCorePersistence.Tests/TheDbContext.cs @@ -1,7 +1,5 @@ -using System; using System.Linq; using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence; using Backend.Fx.EfCorePersistence.Tests.Fixtures; using Microsoft.EntityFrameworkCore; using Xunit; @@ -10,64 +8,50 @@ namespace Backend.Fx.EfCorePersistence.Tests { public class TheDbContext { - private readonly DatabaseFixture _fixture; - private static int _nextTenantId = 2675; - private readonly int _tenantId = _nextTenantId++; - public TheDbContext() { _fixture = new SqliteDatabaseFixture(); _fixture.CreateDatabase(); } + private readonly DatabaseFixture _fixture; + private static int _nextTenantId = 2675; + private readonly int _tenantId = _nextTenantId++; + [Fact] public void CanClearAndReplaceDependentEntites() { - using (DbSession dbs = _fixture.UseDbSession()) + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) { - using (var dbContext = new TestDbContext(dbs.OptionsBuilder.Options)) - { - var blog = new Blog(1, "original blog") {TenantId = _tenantId}; - blog.Posts.Add(new Post(1, blog, "new name 1")); - blog.Posts.Add(new Post(2, blog, "new name 2")); - blog.Posts.Add(new Post(3, blog, "new name 3")); - blog.Posts.Add(new Post(4, blog, "new name 4")); - blog.Posts.Add(new Post(5, blog, "new name 5")); - dbContext.Add(blog); - dbContext.UpdateTrackingProperties("me", DateTime.Now); - dbContext.SaveChanges(); - } + var blog = new Blog(1, "original blog") {TenantId = _tenantId}; + blog.Posts.Add(new Post(1, blog, "new name 1")); + blog.Posts.Add(new Post(2, blog, "new name 2")); + blog.Posts.Add(new Post(3, blog, "new name 3")); + blog.Posts.Add(new Post(4, blog, "new name 4")); + blog.Posts.Add(new Post(5, blog, "new name 5")); + dbSession.DbContext.Add(blog); + } + + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) + { + Blog blog = dbSession.DbContext.Blogs.Include(b => b.Posts).Single(b => b.Id == 1); + blog.Posts.Clear(); + blog.Posts.Add(new Post(6, blog, "new name 6")); + blog.Posts.Add(new Post(7, blog, "new name 7")); + blog.Posts.Add(new Post(8, blog, "new name 8")); + blog.Posts.Add(new Post(9, blog, "new name 9")); + blog.Posts.Add(new Post(10, blog, "new name 10")); + } + + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) + { + Blog blog = dbSession.DbContext.Blogs.Include(b => b.Posts).Single(b => b.Id == 1); + + Assert.Equal(5, blog.Posts.Count); - using (var dbContext = new TestDbContext(dbs.OptionsBuilder.Options)) - { - Blog blog = dbContext.Blogs.Include(b => b.Posts).Single(b => b.Id == 1); - blog.Posts.Clear(); - var post6 = new Post(6, blog, "new name 6"); - blog.Posts.Add(post6); - blog.Posts.Add(new Post(7, blog, "new name 7")); - blog.Posts.Add(new Post(8, blog, "new name 8")); - blog.Posts.Add(new Post(9, blog, "new name 9")); - blog.Posts.Add(new Post(10, blog, "new name 10")); - dbContext.UpdateTrackingProperties("me", DateTime.Now); - dbContext.SaveChanges(); - } + for (var i = 1; i <= 5; i++) Assert.DoesNotContain(blog.Posts, p => p.Id == i); - using (var dbContext = new TestDbContext(dbs.OptionsBuilder.Options)) - { - Blog blog = dbContext.Blogs.Include(b => b.Posts).Single(b => b.Id == 1); - - Assert.Equal(5, blog.Posts.Count); - - for (int i = 1; i <= 5; i++) - { - Assert.DoesNotContain(blog.Posts, p => p.Id == i); - } - - for (int i = 6; i <= 10; i++) - { - Assert.Contains(blog.Posts, p => p.Id == i); - } - } + for (var i = 6; i <= 10; i++) Assert.Contains(blog.Posts, p => p.Id == i); } } } diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/TheEfUnitOfWork.cs b/tests/Backend.Fx.EfCorePersistence.Tests/TheEfUnitOfWork.cs deleted file mode 100644 index 7f0bf112..00000000 --- a/tests/Backend.Fx.EfCorePersistence.Tests/TheEfUnitOfWork.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using System.Linq; -using Backend.Fx.BuildingBlocks; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence; -using Backend.Fx.EfCorePersistence.Tests.Fixtures; -using Backend.Fx.Environment.Authentication; -using Backend.Fx.Environment.DateAndTime; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Patterns.Authorization; -using Backend.Fx.Patterns.EventAggregation.Domain; -using Backend.Fx.Patterns.EventAggregation.Integration; -using Backend.Fx.Patterns.UnitOfWork; -using FakeItEasy; -using Microsoft.EntityFrameworkCore.Storage; -using Xunit; - -namespace Backend.Fx.EfCorePersistence.Tests -{ - public class TheEfUnitOfWork - { - private readonly DatabaseFixture _fixture; - private static int _nextTenantId = 2675; - private readonly int _tenantId = _nextTenantId++; - - public TheEfUnitOfWork() - { - _fixture = new SqliteDatabaseFixture(); - //_fixture = new SqlServerDatabaseFixture(); - _fixture.CreateDatabase(); - } - - [Fact] - public void OpensTransaction() - { - using (DbSession dbs = _fixture.UseDbSession()) - { - IUnitOfWork sut = dbs.BeginUnitOfWork(); - Assert.NotNull(sut.GetDbContext().Database.CurrentTransaction); - Assert.Equal(dbs.Connection, sut.GetDbContext().Database.CurrentTransaction.GetDbTransaction().Connection); - - sut.GetDbContext().Add(new Blogger(333, "Metulsky", "Bratislav")); - sut.Complete(); - - Assert.Null(sut.GetDbTransaction()); - Assert.Throws(() => sut.GetDbContext().Database.CurrentTransaction.Commit()); // because sut.Complete() did it already - - - sut = dbs.BeginUnitOfWork(); - Assert.NotNull(sut.GetDbContext().Set().SingleOrDefault(b => b.Id == 333 && b.FirstName == "Bratislav" && b.LastName == "Metulsky")); - } - } - - [Fact] - public void RollsBackTransactionOnDisposal() - { - using (DbSession dbs = _fixture.UseDbSession()) - { - - IUnitOfWork sut = dbs.BeginUnitOfWork(); - sut.GetDbContext().Add(new Blogger(333, "Metulsky", "Bratislav")); - sut.GetDbContext().SaveChanges(); - // no sut.Complete() - sut.GetDbTransaction().Dispose(); - Assert.Throws(() => - sut.GetDbContext().Database.CurrentTransaction.Commit()); // because sut.Dispose() has already rolled back the open tx - - sut = dbs.BeginUnitOfWork(); - - Assert.Null(sut.GetDbContext().Set().SingleOrDefault(b => b.Id == 333 && b.FirstName == "Bratislav" && b.LastName == "Metulsky")); - } - } - - - private class AnEvent : IDomainEvent { } - - private class AnEventHandler : IDomainEventHandler - { - private readonly IRepository _blogRepository; - - public AnEventHandler(IRepository blogRepository) - { - _blogRepository = blogRepository; - } - - public void Handle(AnEvent domainEvent) - { - _blogRepository.Add(new Blog(99999, "Created via Event Handling")); - } - } - - [Fact] - public void FlushesAfterDomainEventHandling() - { - var fakeEventHandlerProvider = A.Fake(); - var domainEventAggregator = new DomainEventAggregator(fakeEventHandlerProvider); - - - using (DbSession dbs = _fixture.UseDbSession()) - { - var sut = new EfUnitOfWork(new FrozenClock(), - CurrentIdentityHolder.CreateSystem(), - domainEventAggregator, - A.Fake(), - // ReSharper disable once AccessToDisposedClosure - new TestDbContext(dbs.OptionsBuilder.Options)); - - sut.Begin(); - A.CallTo(() => fakeEventHandlerProvider.GetAllEventHandlers()) - .ReturnsLazily(() => - { - // ReSharper disable once AccessToDisposedClosure - var repo = new EfRepository(sut.DbContext, new BlogMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - return new[] {new AnEventHandler(repo)}; - }); - - domainEventAggregator.PublishDomainEvent(new AnEvent()); - Blog createdViaEvent = sut.DbContext.Set().Find(99999); - Assert.Null(createdViaEvent); - sut.Complete(); - } - - using (DbSession dbs = _fixture.UseDbSession()) - { - IUnitOfWork sut = dbs.BeginUnitOfWork(); - Blog createdViaEvent = sut.GetDbContext().Set().Find(99999); - Assert.NotNull(createdViaEvent); - Assert.Equal("Created via Event Handling", createdViaEvent.Name); - } - } - } -} diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/TheReadonlyTransactionContext.cs b/tests/Backend.Fx.EfCorePersistence.Tests/TheReadonlyTransactionContext.cs deleted file mode 100644 index 06865dfd..00000000 --- a/tests/Backend.Fx.EfCorePersistence.Tests/TheReadonlyTransactionContext.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain; -using Backend.Fx.EfCorePersistence.Tests.Fixtures; -using Backend.Fx.Patterns.UnitOfWork; -using Xunit; - -namespace Backend.Fx.EfCorePersistence.Tests -{ - public class TheReadonlyTransactionContext - { - private readonly DatabaseFixture _fixture; - - public TheReadonlyTransactionContext() - { - _fixture = new SqliteDatabaseFixture(); - //_fixture = new SqlServerDatabaseFixture(); - _fixture.CreateDatabase(); - } - - [Fact] - public void OpensTransaction() - { - using(DbSession dbs = _fixture.UseDbSession()) - { - IUnitOfWork sut = dbs.BeginUnitOfWork(true); - Assert.NotNull(sut.GetDbContext()); - Assert.NotNull(sut.GetDbContext().Database.CurrentTransaction); - sut.Complete(); - Assert.Throws(() => sut.GetDbContext().Database.CurrentTransaction.Commit()); - } - } - - [Fact] - public void RollbacksTransactionOnComplete() - { - using (DbSession dbs = _fixture.UseDbSession()) - { - IUnitOfWork sut = dbs.BeginUnitOfWork(true); - sut.GetDbContext().Add(new Blogger(334, "Bratislav", "Metulsky")); - sut.Complete(); - Assert.Throws(() => sut.GetDbContext().Database.CurrentTransaction.Commit()); - } - - using (DbSession dbs = _fixture.UseDbSession()) - { - IUnitOfWork sut = dbs.BeginUnitOfWork(); - Assert.Empty(sut.GetDbContext().Set()); - } - } - - [Fact] - public void RollbacksTransactionOnDisposal() - { - using (DbSession dbs = _fixture.UseDbSession()) - { - IUnitOfWork sut = dbs.BeginUnitOfWork(true); - sut.GetDbContext().Add(new Blogger(335, "Bratislav", "Metulsky")); - sut.GetDbTransaction().Dispose(); - Assert.Throws(() => sut.GetDbContext().Database.CurrentTransaction.Commit()); - } - - using (DbSession dbs = _fixture.UseDbSession()) - { - IUnitOfWork sut = dbs.BeginUnitOfWork(); - Assert.Empty(sut.GetDbContext().Set()); - } - } - } -} diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/TheRepositoryOfComposedAggregate.cs b/tests/Backend.Fx.EfCorePersistence.Tests/TheRepositoryOfComposedAggregate.cs index c2d55194..a0de99dd 100644 --- a/tests/Backend.Fx.EfCorePersistence.Tests/TheRepositoryOfComposedAggregate.cs +++ b/tests/Backend.Fx.EfCorePersistence.Tests/TheRepositoryOfComposedAggregate.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Data; using System.Linq; using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Domain; using Backend.Fx.EfCorePersistence.Tests.DummyImpl.Persistence; @@ -9,7 +10,6 @@ using Backend.Fx.Extensions; using Backend.Fx.Patterns.Authorization; using Backend.Fx.Patterns.IdGeneration; -using Backend.Fx.Patterns.UnitOfWork; using FakeItEasy; using Xunit; @@ -17,15 +17,6 @@ namespace Backend.Fx.EfCorePersistence.Tests { public class TheRepositoryOfComposedAggregate { - private static int _nextTenantId = 57839; - private static int _nextId = 1; - private readonly int _tenantId = _nextTenantId++; - private readonly IEqualityComparer _tolerantDateTimeComparer = new TolerantDateTimeComparer(TimeSpan.FromMilliseconds(5000)); - private readonly IEntityIdGenerator _idGenerator = A.Fake(); - private readonly DatabaseFixture _fixture; - private readonly IClock _clock = new FrozenClock(); - - public TheRepositoryOfComposedAggregate() { A.CallTo(() => _idGenerator.NextId()).ReturnsLazily(() => _nextId++); @@ -34,108 +25,128 @@ public TheRepositoryOfComposedAggregate() _fixture.CreateDatabase(); } - [Fact] - public void CanCreate() + private static int _nextTenantId = 57839; + private static int _nextId = 1; + private readonly int _tenantId = _nextTenantId++; + private readonly IEqualityComparer _tolerantDateTimeComparer = new TolerantDateTimeComparer(TimeSpan.FromMilliseconds(5000)); + private readonly IEntityIdGenerator _idGenerator = A.Fake(); + private readonly DatabaseFixture _fixture; + + private int CreateBlogWithPost(IDbConnection dbConnection, int postCount = 1) { - using (DbSession dbs = _fixture.UseDbSession()) { - - { - int count = dbs.Connection.ExecuteScalar("SELECT count(*) FROM Blogs"); - Assert.Equal(0, count); - - count = dbs.Connection.ExecuteScalar("SELECT count(*) FROM Posts"); - Assert.Equal(0, count); - } - - { - IUnitOfWork uow = dbs.BeginUnitOfWork(clock:_clock); - var sut = new EfRepository(uow.GetDbContext(), new BlogMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - var blog = new Blog(_idGenerator.NextId(), "my blog"); - blog.AddPost(_idGenerator, "my post"); - sut.Add(blog); - uow.Complete(); - } - - - { - int count = dbs.Connection.ExecuteScalar("SELECT count(*) FROM Blogs"); - Assert.Equal(1, count); - - count = dbs.Connection.ExecuteScalar("SELECT count(*) FROM Posts"); - Assert.Equal(1, count); - } + var blogId = _nextId++; + dbConnection.ExecuteNonQuery( + $"INSERT INTO Blogs (Id, TenantId, Name, CreatedOn, CreatedBy) VALUES ({blogId}, {CurrentTenantIdHolder.Create(_tenantId).Current.Value}, 'my blog', CURRENT_TIMESTAMP, 'persistence test')"); + var count = dbConnection.ExecuteScalar("SELECT count(*) FROM Blogs"); + Assert.Equal(1, count); + + for (var i = 0; i < postCount; i++) + dbConnection.ExecuteNonQuery( + $"INSERT INTO Posts (Id, BlogId, Name, TargetAudience_IsPublic, TargetAudience_Culture, CreatedOn, CreatedBy) VALUES ({_nextId++}, {blogId}, 'my post {i:00}', '1', 'de-DE', CURRENT_TIMESTAMP, 'persistence test')"); + + return blogId; } } + //FAILING!!!! + // this shows, that ValueObjects treated as OwnedTypes are not supported very well + //[Fact] + //public void CanUpdateDependantValueObject() + //{ + // using (DbSession dbs = _fixture.UseDbSession()) + // { + // int id = CreateBlogWithPost(dbSession.DbConnection, 10); + // Post post; + + // using (var uow = dbs.UseUnitOfWork(_clock)) + // { + // var sut = new EfRepository(uow.DbContext, new BlogMapping(), CurrentTenantIdHolder.Create(_tenantId), + // new AllowAll()); + // var blog = sut.Single(id); + // post = blog.Posts.First(); + // post.TargetAudience = new TargetAudience{Culture = "es-AR", IsPublic = false}; + // uow.Complete(); + // } + + // + // { + // string culture = dbSession.DbConnection.ExecuteScalar($"SELECT TargetAudience_Culture ame FROM Posts where id = {post.Id}"); + // Assert.Equal("es-AR", culture); + + // string strChangedOn = dbSession.DbConnection.ExecuteScalar($"SELECT ChangedOn FROM Posts where id = {post.Id}"); + // DateTime changedOn = DateTime.Parse(strChangedOn); + // Assert.Equal(_clock.UtcNow, changedOn, new TolerantDateTimeComparer(TimeSpan.FromMilliseconds(500))); + // } + // } + //} + [Fact] - public void CanRead() + public void CanAddDependent() { - using (DbSession dbs = _fixture.UseDbSession()) + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) { - var id = CreateBlogWithPost(dbs); - Blog blog; - - { - IUnitOfWork uow = dbs.BeginUnitOfWork(clock:_clock); - var sut = new EfRepository(uow.GetDbContext(), new BlogMapping(), - CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - blog = sut.Single(id); - uow.Complete(); - } - - - { - Assert.NotNull(blog); - Assert.Equal(id, blog.Id); - Assert.Equal("my blog", blog.Name); - Assert.NotEmpty(blog.Posts); - } + var id = CreateBlogWithPost(dbSession.DbConnection, 10); + var sut = new EfRepository(dbSession.DbContext, new BlogMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); + Blog blog = sut.Single(id); + blog.Posts.Add(new Post(_idGenerator.NextId(), blog, "added")); + } + + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) + { + var count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Posts"); + Assert.Equal(11, count); } } [Fact] - public void CanUpdate() + public void CanCreate() { - using (DbSession dbs = _fixture.UseDbSession()) + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) + { + var count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Blogs"); + Assert.Equal(0, count); + + count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Posts"); + Assert.Equal(0, count); + } + + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) { - var id = CreateBlogWithPost(dbs); - - { - IUnitOfWork uow = dbs.BeginUnitOfWork(clock:_clock); - var sut = new EfRepository(uow.GetDbContext(), new BlogMapping(), - CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - Blog blog = sut.Single(id); - blog.Modify("modified"); - uow.Complete(); - } - - Assert.Equal(1, dbs.Connection.ExecuteScalar("SELECT count(*) FROM Blogs")); - Assert.Equal(id, dbs.Connection.ExecuteScalar("SELECT Id FROM Blogs LIMIT 1")); - Assert.Equal("modified", dbs.Connection.ExecuteScalar("SELECT Name FROM Blogs LIMIT 1")); - Assert.Equal("modified", dbs.Connection.ExecuteScalar("SELECT Name FROM Posts LIMIT 1")); + var sut = new EfRepository(dbSession.DbContext, new BlogMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); + var blog = new Blog(_idGenerator.NextId(), "my blog"); + blog.AddPost(_idGenerator, "my post"); + sut.Add(blog); + } + + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) + { + var count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Blogs"); + Assert.Equal(1, count); + + count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Posts"); + Assert.Equal(1, count); } } [Fact] public void CanDelete() { - using (DbSession dbs = _fixture.UseDbSession()) + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) { - var id = CreateBlogWithPost(dbs); + var id = CreateBlogWithPost(dbSession.DbConnection); - { - IUnitOfWork uow = dbs.BeginUnitOfWork(clock:_clock); - var sut = new EfRepository(uow.GetDbContext(), new BlogMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - Blog blog = sut.Single(id); - sut.Delete(blog); - uow.Complete(); - } + var sut = new EfRepository(dbSession.DbContext, new BlogMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); + Blog blog = sut.Single(id); + sut.Delete(blog); + } - int count = dbs.Connection.ExecuteScalar("SELECT count(*) FROM Blogs"); + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) + { + var count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Blogs"); Assert.Equal(0, count); - count = dbs.Connection.ExecuteScalar("SELECT count(*) FROM Posts"); + count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Posts"); Assert.Equal(0, count); } } @@ -143,189 +154,172 @@ public void CanDelete() [Fact] public void CanDeleteDependent() { - using (DbSession dbs = _fixture.UseDbSession()) + int id; + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) { - int id = CreateBlogWithPost(dbs, 10); - - { - int count = dbs.Connection.ExecuteScalar("SELECT count(*) FROM Posts"); - Assert.Equal(10, count); - } - - { - IUnitOfWork uow = dbs.BeginUnitOfWork(clock:_clock); - var sut = new EfRepository(uow.GetDbContext(), new BlogMapping(), - CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - Blog blog = sut.Single(id); - Post firstPost = blog.Posts.First(); - firstPost.SetName("sadfasfsadf"); - blog.Posts.Remove(firstPost); - uow.Complete(); - } - - { - var count = dbs.Connection.ExecuteScalar("SELECT count(*) FROM Posts"); - Assert.Equal(9, count); - } + id = CreateBlogWithPost(dbSession.DbConnection, 10); + var count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Posts"); + Assert.Equal(10, count); + } + + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) + { + var sut = new EfRepository(dbSession.DbContext, new BlogMapping(), + CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); + Blog blog = sut.Single(id); + Post firstPost = blog.Posts.First(); + firstPost.SetName("sadfasfsadf"); + blog.Posts.Remove(firstPost); + } + + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) + { + var count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Posts"); + Assert.Equal(9, count); } } + [Fact] - public void CanUpdateDependant() + public void CanRead() { - using (DbSession dbs = _fixture.UseDbSession()) + int id; + Blog blog; + + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) { - int id = CreateBlogWithPost(dbs, 10); - Post post; - - { - IUnitOfWork uow = dbs.BeginUnitOfWork(clock:_clock); - var sut = new EfRepository(uow.GetDbContext(), new BlogMapping(), CurrentTenantIdHolder.Create(_tenantId), - new AllowAll()); - Blog blog = sut.Single(id); - post = blog.Posts.First(); - post.SetName("modified"); - uow.Complete(); - } - - - { - string name = dbs.Connection.ExecuteScalar($"SELECT name FROM Posts where id = {post.Id}"); - Assert.Equal("modified", name); - - string strChangedOn = dbs.Connection.ExecuteScalar($"SELECT changedon FROM Posts where id = {post.Id}"); - DateTime changedOn = DateTime.Parse(strChangedOn); - Assert.Equal(_clock.UtcNow, changedOn, new TolerantDateTimeComparer(TimeSpan.FromMilliseconds(500))); - } + id = CreateBlogWithPost(dbSession.DbConnection); } - } - //FAILING!!!! - // this shows, that ValueObjects treated as OwnedTypes are not supported very well - //[Fact] - //public void CanUpdateDependantValueObject() - //{ - // using (DbSession dbs = _fixture.UseDbSession()) - // { - // int id = CreateBlogWithPost(dbs, 10); - // Post post; - - // using (var uow = dbs.UseUnitOfWork(_clock)) - // { - // var sut = new EfRepository(uow.DbContext, new BlogMapping(), CurrentTenantIdHolder.Create(_tenantId), - // new AllowAll()); - // var blog = sut.Single(id); - // post = blog.Posts.First(); - // post.TargetAudience = new TargetAudience{Culture = "es-AR", IsPublic = false}; - // uow.Complete(); - // } + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) + { + var sut = new EfRepository(dbSession.DbContext, new BlogMapping(), + CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); + blog = sut.Single(id); + } - // - // { - // string culture = dbs.Connection.ExecuteScalar($"SELECT TargetAudience_Culture ame FROM Posts where id = {post.Id}"); - // Assert.Equal("es-AR", culture); + Assert.NotNull(blog); + Assert.Equal(id, blog.Id); + Assert.Equal("my blog", blog.Name); + Assert.NotEmpty(blog.Posts); + } - // string strChangedOn = dbs.Connection.ExecuteScalar($"SELECT ChangedOn FROM Posts where id = {post.Id}"); - // DateTime changedOn = DateTime.Parse(strChangedOn); - // Assert.Equal(_clock.UtcNow, changedOn, new TolerantDateTimeComparer(TimeSpan.FromMilliseconds(500))); - // } - // } - //} [Fact] - public void CanAddDependent() + public void CanReplaceDependentCollection() { - using (DbSession dbs = _fixture.UseDbSession()) + int id; + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) { - int id = CreateBlogWithPost(dbs, 10); - - { - IUnitOfWork uow = dbs.BeginUnitOfWork(clock:_clock); - var sut = new EfRepository(uow.GetDbContext(), new BlogMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - Blog blog = sut.Single(id); - blog.Posts.Add(new Post(_idGenerator.NextId(), blog, "added")); - uow.Complete(); - - int count = dbs.Connection.ExecuteScalar("SELECT count(*) FROM Posts"); - Assert.Equal(11, count); - } + id = CreateBlogWithPost(dbSession.DbConnection, 10); } - } + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) + { + var sut = new EfRepository(dbSession.DbContext, new BlogMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); + Blog blog = sut.Single(id); + blog.Posts.Clear(); + blog.Posts.Add(new Post(_idGenerator.NextId(), blog, "new name 1")); + blog.Posts.Add(new Post(_idGenerator.NextId(), blog, "new name 2")); + blog.Posts.Add(new Post(_idGenerator.NextId(), blog, "new name 3")); + blog.Posts.Add(new Post(_idGenerator.NextId(), blog, "new name 4")); + blog.Posts.Add(new Post(_idGenerator.NextId(), blog, "new name 5")); + } + + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) + { + var count = dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Posts"); + Assert.Equal(5, count); + } + } [Fact] - public void CanReplaceDependentCollection() + public void CanUpdate() { - using (DbSession dbs = _fixture.UseDbSession()) + int id; + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) { - var id = CreateBlogWithPost(dbs, 10); - - { - IUnitOfWork uow = dbs.BeginUnitOfWork(clock:_clock); - var sut = new EfRepository(uow.GetDbContext(), new BlogMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - Blog blog = sut.Single(id); - blog.Posts.Clear(); - blog.Posts.Add(new Post(_idGenerator.NextId(), blog, "new name 1")); - blog.Posts.Add(new Post(_idGenerator.NextId(), blog, "new name 2")); - blog.Posts.Add(new Post(_idGenerator.NextId(), blog, "new name 3")); - blog.Posts.Add(new Post(_idGenerator.NextId(), blog, "new name 4")); - blog.Posts.Add(new Post(_idGenerator.NextId(), blog, "new name 5")); - uow.Complete(); - } - - - { - int count = dbs.Connection.ExecuteScalar("SELECT count(*) FROM Posts"); - Assert.Equal(5, count); - } + id = CreateBlogWithPost(dbSession.DbConnection); + } + + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) + { + var sut = new EfRepository(dbSession.DbContext, new BlogMapping(), + CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); + Blog blog = sut.Single(id); + blog.Modify("modified"); + } + + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) + { + Assert.Equal(1, dbSession.DbConnection.ExecuteScalar("SELECT count(*) FROM Blogs")); + Assert.Equal(id, dbSession.DbConnection.ExecuteScalar("SELECT Id FROM Blogs LIMIT 1")); + Assert.Equal("modified", dbSession.DbConnection.ExecuteScalar("SELECT Name FROM Blogs LIMIT 1")); + Assert.Equal("modified", dbSession.DbConnection.ExecuteScalar("SELECT Name FROM Posts LIMIT 1")); } } [Fact] - public void UpdatesAggregateTrackingPropertiesOnDeleteOfDependant() + public void CanUpdateDependant() { - using (DbSession dbs = _fixture.UseDbSession()) + var clock = new AdjustableClock(new WallClock()); + clock.OverrideUtcNow(new DateTime(2020, 01, 20, 20, 30, 40)); + + int id; + using (TestDbSession dbSession = _fixture.CreateTestDbSession(clock: clock)) { - int id = CreateBlogWithPost(dbs, 10); - - DateTime expectedModifiedOn = _clock.UtcNow.AddHours(1); - _clock.OverrideUtcNow(expectedModifiedOn); - - { - IUnitOfWork uow = dbs.BeginUnitOfWork(clock:_clock); - var sut = new EfRepository(uow.GetDbContext(), new BlogMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - Blog b = sut.Single(id); - b.Posts.Remove(b.Posts.First()); - uow.Complete(); - } - - { - IUnitOfWork uow = dbs.BeginUnitOfWork(clock:_clock); - Blog blog = uow.GetDbContext().Set().Find(id); - Assert.NotNull(blog.ChangedOn); - Assert.Equal(expectedModifiedOn, blog.ChangedOn.Value, _tolerantDateTimeComparer); - } + id = CreateBlogWithPost(dbSession.DbConnection, 10); + } + + Post post; + + using (TestDbSession dbSession = _fixture.CreateTestDbSession(clock: clock)) + { + var sut = new EfRepository(dbSession.DbContext, new BlogMapping(), CurrentTenantIdHolder.Create(_tenantId), + new AllowAll()); + Blog blog = sut.Single(id); + post = blog.Posts.First(); + post.SetName("modified"); + } + + using (TestDbSession dbSession = _fixture.CreateTestDbSession(clock: clock)) + { + var name = dbSession.DbConnection.ExecuteScalar($"SELECT name FROM Posts where id = {post.Id}"); + Assert.Equal("modified", name); + + var strChangedOn = dbSession.DbConnection.ExecuteScalar($"SELECT changedon FROM Posts where id = {post.Id}"); + DateTime changedOn = DateTime.Parse(strChangedOn); + Assert.Equal(clock.UtcNow, changedOn, new TolerantDateTimeComparer(TimeSpan.FromMilliseconds(500))); } } - private int CreateBlogWithPost(DbSession dbs, int postCount = 1) + [Fact] + public void UpdatesAggregateTrackingPropertiesOnDeleteOfDependant() { + var clock = new AdjustableClock(new WallClock()); + clock.OverrideUtcNow(new DateTime(2020, 01, 20, 20, 30, 40)); + int id; + using (TestDbSession dbSession = _fixture.CreateTestDbSession(clock:clock)) { - int blogId = _nextId++; - dbs.Connection.ExecuteNonQuery( - $"INSERT INTO Blogs (Id, TenantId, Name, CreatedOn, CreatedBy) VALUES ({blogId}, {CurrentTenantIdHolder.Create(_tenantId).Current.Value}, 'my blog', CURRENT_TIMESTAMP, 'persistence test')"); - int count = dbs.Connection.ExecuteScalar("SELECT count(*) FROM Blogs"); - Assert.Equal(1, count); + id = CreateBlogWithPost(dbSession.DbConnection, 10); + } - for (int i = 0; i < postCount; i++) - { - dbs.Connection.ExecuteNonQuery( - $"INSERT INTO Posts (Id, BlogId, Name, TargetAudience_IsPublic, TargetAudience_Culture, CreatedOn, CreatedBy) VALUES ({_nextId++}, {blogId}, 'my post {i:00}', '1', 'de-DE', CURRENT_TIMESTAMP, 'persistence test')"); - } + DateTime expectedModifiedOn = clock.Advance(TimeSpan.FromHours(1)); - return blogId; + using (TestDbSession dbSession = _fixture.CreateTestDbSession(clock:clock)) + { + var sut = new EfRepository(dbSession.DbContext, new BlogMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); + Blog b = sut.Single(id); + b.Posts.Remove(b.Posts.First()); + } + + using (TestDbSession dbSession = _fixture.CreateTestDbSession(clock:clock)) + { + Blog blog = dbSession.DbContext.Set().Find(id); + Assert.NotNull(blog.ChangedOn); + Assert.Equal(expectedModifiedOn, blog.ChangedOn.Value, _tolerantDateTimeComparer); } } } -} +} \ No newline at end of file diff --git a/tests/Backend.Fx.EfCorePersistence.Tests/TheRepositoryOfPlainAggregate.cs b/tests/Backend.Fx.EfCorePersistence.Tests/TheRepositoryOfPlainAggregate.cs index 0d591e4c..79d0818e 100644 --- a/tests/Backend.Fx.EfCorePersistence.Tests/TheRepositoryOfPlainAggregate.cs +++ b/tests/Backend.Fx.EfCorePersistence.Tests/TheRepositoryOfPlainAggregate.cs @@ -6,17 +6,12 @@ using Backend.Fx.Environment.MultiTenancy; using Backend.Fx.Extensions; using Backend.Fx.Patterns.Authorization; -using Backend.Fx.Patterns.UnitOfWork; using Xunit; namespace Backend.Fx.EfCorePersistence.Tests { public class TheRepositoryOfPlainAggregate { - private static int _nextTenantId = 12312; - private readonly int _tenantId = _nextTenantId++; - private readonly DatabaseFixture _fixture; - public TheRepositoryOfPlainAggregate() { //_fixture = new SqlServerDatabaseFixture(); @@ -24,81 +19,74 @@ public TheRepositoryOfPlainAggregate() _fixture.CreateDatabase(); } + private static int _nextTenantId = 12312; + private readonly int _tenantId = _nextTenantId++; + private readonly DatabaseFixture _fixture; + [Fact] public void CanCreate() { - using (var dbs = _fixture.UseDbSession()) + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) { - { - IUnitOfWork uow = dbs.BeginUnitOfWork(); - var repo = new EfRepository(uow.GetDbContext(), new BloggerMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - repo.Add(new Blogger(345, "Metulsky", "Bratislav")); - uow.Complete(); - } + var repo = new EfRepository(dbSession.DbContext, new BloggerMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); + repo.Add(new Blogger(345, "Metulsky", "Bratislav")); + } - - { - int count = dbs.Connection.ExecuteScalar("SELECT Count(*) FROM Bloggers"); - Assert.Equal(1L, count); + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) + { + var count = dbSession.DbConnection.ExecuteScalar("SELECT Count(*) FROM Bloggers"); + Assert.Equal(1L, count); - count = dbs.Connection.ExecuteScalar($"SELECT Count(*) FROM Bloggers WHERE FirstName = 'Bratislav' AND LastName = 'Metulsky' AND TenantId = '{_tenantId}'"); - Assert.Equal(1L, count); - } + count = dbSession.DbConnection.ExecuteScalar( + $"SELECT Count(*) FROM Bloggers WHERE FirstName = 'Bratislav' AND LastName = 'Metulsky' AND TenantId = '{_tenantId}'"); + Assert.Equal(1L, count); } } [Fact] - public void CanRead() + public void CanDelete() { - using (var dbs = _fixture.UseDbSession()) + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) { - - { - dbs.Connection.ExecuteNonQuery( - "INSERT INTO Bloggers (Id, TenantId, CreatedOn, CreatedBy, FirstName, LastName, Bio) " + - $"VALUES (444, {_tenantId}, '2012-05-12 23:12:09', 'the test', 'Bratislav', 'Metulsky', 'whatever')"); - } - - { - IUnitOfWork uow = dbs.BeginUnitOfWork(); - var repo = new EfRepository(uow.GetDbContext(), new BloggerMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - Blogger bratislavMetulsky = repo.Single(444); - Assert.Equal(_tenantId, bratislavMetulsky.TenantId); - Assert.Equal("the test", bratislavMetulsky.CreatedBy); - Assert.Equal(new DateTime(2012, 05, 12, 23, 12, 09), bratislavMetulsky.CreatedOn); - Assert.Equal("Bratislav", bratislavMetulsky.FirstName); - Assert.Equal("Metulsky", bratislavMetulsky.LastName); - Assert.Equal("whatever", bratislavMetulsky.Bio); - uow.Complete(); - } + dbSession.DbConnection.ExecuteNonQuery( + "INSERT INTO Bloggers (Id, TenantId, CreatedOn, CreatedBy, FirstName, LastName, Bio) " + + $"VALUES (555, {_tenantId}, '2012-05-12 23:12:09', 'the test', 'Bratislav', 'Metulsky', 'whatever')"); + } + + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) + { + var repo = new EfRepository(dbSession.DbContext, new BloggerMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); + Blogger bratislavMetulsky = repo.Single(555); + repo.Delete(bratislavMetulsky); + } + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) + { + var count = dbSession.DbConnection.ExecuteScalar("SELECT Count(*) FROM Bloggers"); + Assert.Equal(0L, count); } } + [Fact] - public void CanDelete() + public void CanRead() { - using (var dbs = _fixture.UseDbSession()) + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) { - - { - dbs.Connection.ExecuteNonQuery( - "INSERT INTO Bloggers (Id, TenantId, CreatedOn, CreatedBy, FirstName, LastName, Bio) " + - $"VALUES (555, {_tenantId}, '2012-05-12 23:12:09', 'the test', 'Bratislav', 'Metulsky', 'whatever')"); - } + dbSession.DbConnection.ExecuteNonQuery( + "INSERT INTO Bloggers (Id, TenantId, CreatedOn, CreatedBy, FirstName, LastName, Bio) " + + $"VALUES (444, {_tenantId}, '2012-05-12 23:12:09', 'the test', 'Bratislav', 'Metulsky', 'whatever')"); - { - IUnitOfWork uow = dbs.BeginUnitOfWork(); - var repo = new EfRepository(uow.GetDbContext(), new BloggerMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - Blogger bratislavMetulsky = repo.Single(555); - repo.Delete(bratislavMetulsky); - uow.Complete(); - } - { - int count = dbs.Connection.ExecuteScalar("SELECT Count(*) FROM Bloggers"); - Assert.Equal(0L, count); + var repo = new EfRepository(dbSession.DbContext, new BloggerMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); + Blogger bratislavMetulsky = repo.Single(444); + Assert.Equal(_tenantId, bratislavMetulsky.TenantId); + Assert.Equal("the test", bratislavMetulsky.CreatedBy); + Assert.Equal(new DateTime(2012, 05, 12, 23, 12, 09), bratislavMetulsky.CreatedOn); + Assert.Equal("Bratislav", bratislavMetulsky.FirstName); + Assert.Equal("Metulsky", bratislavMetulsky.LastName); + Assert.Equal("whatever", bratislavMetulsky.Bio); } } } @@ -106,47 +94,39 @@ public void CanDelete() [Fact] public void CanUpdate() { - using (var dbs = _fixture.UseDbSession()) + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) { - - { - dbs.Connection.ExecuteNonQuery( - "INSERT INTO Bloggers (Id, TenantId, CreatedOn, CreatedBy, FirstName, LastName, Bio) " + - $"VALUES (456, {_tenantId}, '2012-05-12 23:12:09', 'the test', 'Bratislav', 'Metulsky', 'whatever')"); - } + dbSession.DbConnection.ExecuteNonQuery( + "INSERT INTO Bloggers (Id, TenantId, CreatedOn, CreatedBy, FirstName, LastName, Bio) " + + $"VALUES (456, {_tenantId}, '2012-05-12 23:12:09', 'the test', 'Bratislav', 'Metulsky', 'whatever')"); + } - { - IUnitOfWork uow = dbs.BeginUnitOfWork(); - var repo = new EfRepository(uow.GetDbContext(), new BloggerMapping(), - CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - Blogger bratislavMetulsky = repo.Single(456); - bratislavMetulsky.FirstName = "Johnny"; - bratislavMetulsky.LastName = "Flash"; - bratislavMetulsky.Bio = "Der lustige Clown"; - uow.Complete(); - } + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) + { + var repo = new EfRepository(dbSession.DbContext, new BloggerMapping(), + CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); + Blogger bratislavMetulsky = repo.Single(456); + bratislavMetulsky.FirstName = "Johnny"; + bratislavMetulsky.LastName = "Flash"; + bratislavMetulsky.Bio = "Der lustige Clown"; } - using (var dbs = _fixture.UseDbSession()) + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) { - - { - var count = dbs.Connection.ExecuteScalar( - $"SELECT Count(*) FROM Bloggers WHERE FirstName = 'Johnny' AND LastName = 'Flash' AND TenantId = '{_tenantId}'"); - Assert.Equal(1L, count); - } + var count = dbSession.DbConnection.ExecuteScalar( + $"SELECT Count(*) FROM Bloggers WHERE FirstName = 'Johnny' AND LastName = 'Flash' AND TenantId = '{_tenantId}'"); + Assert.Equal(1L, count); + } - { - IUnitOfWork uow = dbs.BeginUnitOfWork(); - var repo = new EfRepository(uow.GetDbContext(), new BloggerMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); - Blogger johnnyFlash = repo.Single(456); - Assert.Equal(DateTime.UtcNow, johnnyFlash.ChangedOn, new TolerantDateTimeComparer(TimeSpan.FromMilliseconds(5000))); - Assert.Equal(new SystemIdentity().Name, johnnyFlash.ChangedBy); - Assert.Equal("Johnny", johnnyFlash.FirstName); - Assert.Equal("Flash", johnnyFlash.LastName); - uow.Complete(); - } + using (TestDbSession dbSession = _fixture.CreateTestDbSession()) + { + var repo = new EfRepository(dbSession.DbContext, new BloggerMapping(), CurrentTenantIdHolder.Create(_tenantId), new AllowAll()); + Blogger johnnyFlash = repo.Single(456); + Assert.Equal(DateTime.UtcNow, johnnyFlash.ChangedOn, new TolerantDateTimeComparer(TimeSpan.FromMilliseconds(5000))); + Assert.Equal(new SystemIdentity().Name, johnnyFlash.ChangedBy); + Assert.Equal("Johnny", johnnyFlash.FirstName); + Assert.Equal("Flash", johnnyFlash.LastName); } } } -} +} \ No newline at end of file diff --git a/tests/Backend.Fx.RabbitMq.Tests/Backend.Fx.RabbitMq.Tests.csproj b/tests/Backend.Fx.RabbitMq.Tests/Backend.Fx.RabbitMq.Tests.csproj new file mode 100644 index 00000000..8e6347f4 --- /dev/null +++ b/tests/Backend.Fx.RabbitMq.Tests/Backend.Fx.RabbitMq.Tests.csproj @@ -0,0 +1,26 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/tests/Backend.Fx.RabbitMq.Tests/TestConfig.cs b/tests/Backend.Fx.RabbitMq.Tests/TestConfig.cs new file mode 100644 index 00000000..934832c9 --- /dev/null +++ b/tests/Backend.Fx.RabbitMq.Tests/TestConfig.cs @@ -0,0 +1,17 @@ +using Backend.Fx.NLogLogging; +using Backend.Fx.RabbitMq.Tests; +using MarcWittke.Xunit.AssemblyFixture; +using Xunit; + +[assembly: TestFramework("MarcWittke.Xunit.AssemblyFixture.XunitTestFrameworkWithAssemblyFixture", "MarcWittke.Xunit.AssemblyFixture")] +[assembly: AssemblyFixture(typeof(TestLoggingFixture))] + +namespace Backend.Fx.RabbitMq.Tests +{ + public class TestLoggingFixture : LoggingFixture + { + public TestLoggingFixture() : base("Backend.Fx") + { + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.RabbitMq.Tests/TheRabbitMqChannel.cs b/tests/Backend.Fx.RabbitMq.Tests/TheRabbitMqChannel.cs new file mode 100644 index 00000000..ebe301e5 --- /dev/null +++ b/tests/Backend.Fx.RabbitMq.Tests/TheRabbitMqChannel.cs @@ -0,0 +1,114 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using Xunit; +using Xunit.Abstractions; + +namespace Backend.Fx.RabbitMq.Tests +{ + public class TheRabbitMqChannel + { + private readonly ITestOutputHelper _testOutputHelper; + + private readonly AutoResetEvent _shutdown = new AutoResetEvent(false); + + private readonly ConnectionFactory _factory = new ConnectionFactory + { + HostName = "localhost", + UserName = "anicors", + Password = "R4bb!tMQ" + }; + + public TheRabbitMqChannel(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + + //[Fact] + public void CanSendAndReceive() + { + var receivingChannel = new ReceivingChannel(_testOutputHelper, _factory); + receivingChannel.Connect(); + + var sendingChannel = new SendingChannel(_testOutputHelper, _factory); + sendingChannel.Connect(); + + sendingChannel.Send("gnarf", "whatever"); + Assert.False(receivingChannel.Received.WaitOne(1000)); + + sendingChannel.Send("info", "whatever"); + Assert.True(receivingChannel.Received.WaitOne(1000)); + + _shutdown.Set(); + } + + private class SendingChannel + { + private readonly ITestOutputHelper _testOutputHelper; + private readonly ConnectionFactory _connectionFactory; + private IConnection _connection; + private IModel _channel; + + public SendingChannel(ITestOutputHelper testOutputHelper, ConnectionFactory connectionFactory) + { + _testOutputHelper = testOutputHelper; + _connectionFactory = connectionFactory; + } + + public void Connect() + { + _connection = _connectionFactory.CreateConnection(); + _channel = _connection.CreateModel(); + _channel.ExchangeDeclare(exchange: "direct_logs", type: "direct"); + } + + public void Send(string severity, string message) + { + var body = Encoding.UTF8.GetBytes(message); + _channel.BasicPublish(exchange: "direct_logs", routingKey: severity, basicProperties: null, body: body); + } + } + + private class ReceivingChannel + { + private readonly ITestOutputHelper _testOutputHelper; + private readonly ConnectionFactory _connectionFactory; + private IConnection _connection; + private IModel _channel; + public AutoResetEvent Received { get; } = new AutoResetEvent(false); + + public ReceivingChannel(ITestOutputHelper testOutputHelper, ConnectionFactory connectionFactory) + { + _testOutputHelper = testOutputHelper; + _connectionFactory = connectionFactory; + } + + public void Connect() + { + _connection = _connectionFactory.CreateConnection(); + _channel = _connection.CreateModel(); + _channel.ExchangeDeclare(exchange: "direct_logs", type: "direct"); + _channel.QueueDeclare("myqueue", true, false, false, null); + _channel.QueueBind(queue: "myqueue", exchange: "direct_logs", routingKey: "info"); + _testOutputHelper.WriteLine(" [*] Waiting for messages."); + + var consumer = new EventingBasicConsumer(_channel); + consumer.Received += (model, ea) => + { + var body = ea.Body.ToArray(); + var message = Encoding.UTF8.GetString(body); + var routingKey = ea.RoutingKey; + _testOutputHelper.WriteLine($" [x] Received '{routingKey}':'{message}'"); + Received.Set(); + _channel.BasicAck(ea.DeliveryTag, false); + }; + _channel.BasicConsume(queue: "myqueue", autoAck: false, consumer: consumer); + } + } + + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.RabbitMq.Tests/TheRabbitMqMessageBus.cs b/tests/Backend.Fx.RabbitMq.Tests/TheRabbitMqMessageBus.cs new file mode 100644 index 00000000..bfbe88e7 --- /dev/null +++ b/tests/Backend.Fx.RabbitMq.Tests/TheRabbitMqMessageBus.cs @@ -0,0 +1,86 @@ +using System; +using System.Diagnostics; +using System.Threading; +using Backend.Fx.Patterns.DependencyInjection; +using Backend.Fx.Patterns.EventAggregation.Integration; +using FakeItEasy; +using RabbitMQ.Client; +using Xunit; + +namespace Backend.Fx.RabbitMq.Tests +{ + public class TheRabbitMqMessageBus + { + private readonly ManualResetEvent _received = new ManualResetEvent(false); + private readonly BackendFxApplicationInvoker _senderInvoker; + private readonly BackendFxApplicationInvoker _receiverInvoker; + + public TheRabbitMqMessageBus() + { + var fakeSenderApplication = A.Fake(); + _senderInvoker = new BackendFxApplicationInvoker(fakeSenderApplication.CompositionRoot); + + + var fakeReceiverApplication = A.Fake(); + _receiverInvoker = new BackendFxApplicationInvoker(fakeReceiverApplication.CompositionRoot); + var fakeScope = A.Fake(); + var fakeInstanceProvider = A.Fake(); + A.CallTo(() => fakeReceiverApplication.CompositionRoot.BeginScope()).Returns(fakeScope); + A.CallTo(() => fakeScope.InstanceProvider).Returns(fakeInstanceProvider); + A.CallTo(() => fakeInstanceProvider.GetInstance(A.That.IsEqualTo(typeof(TestIntegrationEventHandler)))) + .Returns(new TestIntegrationEventHandler(_received)); + } + + //[Fact] + public void CanBeUsedWithBackendFxApplication() + { + IMessageBus sender = new RabbitMqMessageBus(new ConnectionFactory + { + HostName = "localhost", + UserName = "anicors", + Password = "R4bb!tMQ" + }, 5, "unittest", "testSender"); + sender.ProvideInvoker(_senderInvoker); + sender.Connect(); + + var receiver = new RabbitMqMessageBus(new ConnectionFactory + { + HostName = "localhost", + UserName = "anicors", + Password = "R4bb!tMQ" + }, 5, "unittest", "testReceiver"); + + receiver.ProvideInvoker(_receiverInvoker); + receiver.Connect(); + receiver.Subscribe(); + + sender.Publish(new TestIntegrationEvent(1)); + Assert.True(_received.WaitOne(Debugger.IsAttached ? int.MaxValue : 5000)); + } + + public class TestIntegrationEventHandler : IIntegrationMessageHandler + { + private readonly ManualResetEvent _received; + + public TestIntegrationEventHandler(ManualResetEvent received) + { + _received = received; + } + + public void Handle(TestIntegrationEvent eventData) + { + _received.Set(); + } + } + + public class TestIntegrationEvent : IntegrationEvent + { + public TestIntegrationEvent(int sequencenumber) : base(999) + { + Sequencenumber = sequencenumber; + } + + public int Sequencenumber { get; } + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests.csproj b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests.csproj index 51067e4f..38b170c0 100644 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests.csproj +++ b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests.csproj @@ -5,12 +5,12 @@ - + - + - + diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/ADemoAggregateGenerator.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADemoAggregateGenerator.cs similarity index 97% rename from tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/ADemoAggregateGenerator.cs rename to tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADemoAggregateGenerator.cs index e4bddb47..a43d9900 100644 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/ADemoAggregateGenerator.cs +++ b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADemoAggregateGenerator.cs @@ -1,7 +1,7 @@ using Backend.Fx.BuildingBlocks; using Backend.Fx.Patterns.DataGeneration; -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.Domain +namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain { public class ADemoAggregateGenerator : DataGenerator, IDemoDataGenerator { diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/ADomainEvent.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainEvent.cs similarity index 89% rename from tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/ADomainEvent.cs rename to tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainEvent.cs index e889c033..80e47e33 100644 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/ADomainEvent.cs +++ b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainEvent.cs @@ -1,6 +1,6 @@ using Backend.Fx.Patterns.EventAggregation.Domain; -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.Domain +namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain { public class ADomainEvent : IDomainEvent { diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/ADomainEventHandler1.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainEventHandler1.cs similarity index 93% rename from tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/ADomainEventHandler1.cs rename to tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainEventHandler1.cs index 134be0c8..6f1f7214 100644 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/ADomainEventHandler1.cs +++ b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainEventHandler1.cs @@ -1,6 +1,6 @@ using Backend.Fx.Patterns.EventAggregation.Domain; -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.Domain +namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain { public class ADomainEventHandler1 : IDomainEventHandler { diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/ADomainEventHandler2.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainEventHandler2.cs similarity index 93% rename from tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/ADomainEventHandler2.cs rename to tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainEventHandler2.cs index 3d55f6c0..e52b0fe5 100644 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/ADomainEventHandler2.cs +++ b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainEventHandler2.cs @@ -1,6 +1,6 @@ using Backend.Fx.Patterns.EventAggregation.Domain; -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.Domain +namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain { public class ADomainEventHandler2 : IDomainEventHandler { diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/ADomainEventHandler3.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainEventHandler3.cs similarity index 93% rename from tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/ADomainEventHandler3.cs rename to tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainEventHandler3.cs index be4b6cdf..72bb7c2b 100644 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/ADomainEventHandler3.cs +++ b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainEventHandler3.cs @@ -1,6 +1,6 @@ using Backend.Fx.Patterns.EventAggregation.Domain; -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.Domain +namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain { public class ADomainEventHandler3 : IDomainEventHandler { diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Bootstrapping/ADomainModule.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainModule.cs similarity index 77% rename from tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Bootstrapping/ADomainModule.cs rename to tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainModule.cs index e2a18c9a..6dbb4c02 100644 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Bootstrapping/ADomainModule.cs +++ b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainModule.cs @@ -1,11 +1,10 @@ using System.Reflection; using Backend.Fx.SimpleInjectorDependencyInjection.Modules; -using Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.Domain; using SimpleInjector; -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.Bootstrapping +namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain { - public class ADomainModule : DomainModule + public class ADomainModule : SimpleInjectorDomainModule { public ADomainModule(params Assembly[] domainAssemblies) : base(domainAssemblies) @@ -15,6 +14,7 @@ public ADomainModule(params Assembly[] domainAssemblies) protected override void Register(Container container, ScopedLifestyle scopedLifestyle) { base.Register(container, scopedLifestyle); + container.RegisterSingleton(); container.Register(scopedLifestyle); } } diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/ADomainService.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainService.cs similarity index 94% rename from tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/ADomainService.cs rename to tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainService.cs index cf9b0f84..d2feebf0 100644 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/ADomainService.cs +++ b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ADomainService.cs @@ -1,6 +1,6 @@ using Backend.Fx.BuildingBlocks; -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.Domain +namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain { public interface ITestDomainService : IDomainService { } diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/AJob.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AJob.cs similarity index 91% rename from tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/AJob.cs rename to tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AJob.cs index 50e4ddb2..fa1ac6d0 100644 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/AJob.cs +++ b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AJob.cs @@ -1,7 +1,7 @@ using Backend.Fx.Patterns.Jobs; using JetBrains.Annotations; -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.Domain +namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain { [UsedImplicitly] public class AJob : IJob diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/AProdAggregateGenerator.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AProdAggregateGenerator.cs similarity index 97% rename from tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/AProdAggregateGenerator.cs rename to tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AProdAggregateGenerator.cs index 3fb69001..d8d1e2cd 100644 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/AProdAggregateGenerator.cs +++ b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AProdAggregateGenerator.cs @@ -1,7 +1,7 @@ using Backend.Fx.BuildingBlocks; using Backend.Fx.Patterns.DataGeneration; -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.Domain +namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain { public class AProdAggregateGenerator : DataGenerator, IProductiveDataGenerator { diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ASingletonService.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ASingletonService.cs new file mode 100644 index 00000000..2ca6e09d --- /dev/null +++ b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/ASingletonService.cs @@ -0,0 +1,9 @@ +namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain +{ + public interface ISingletonService {} + + public class ASingletonService : ISingletonService + { + + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/AnAggregate.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AnAggregate.cs similarity index 93% rename from tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/AnAggregate.cs rename to tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AnAggregate.cs index 30efc8d5..592f21ba 100644 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/AnAggregate.cs +++ b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AnAggregate.cs @@ -1,6 +1,6 @@ using Backend.Fx.BuildingBlocks; -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.Domain +namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain { public class AnAggregate : AggregateRoot { diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/AnAggregateAuthorization.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AnAggregateAuthorization.cs similarity index 89% rename from tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/AnAggregateAuthorization.cs rename to tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AnAggregateAuthorization.cs index aae96b12..7b43b787 100644 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/AnAggregateAuthorization.cs +++ b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AnAggregateAuthorization.cs @@ -1,6 +1,6 @@ using Backend.Fx.Patterns.Authorization; -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.Domain +namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain { public class AnAggregateAuthorization : AllowAll { } } diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/AnApplicationService.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AnApplicationService.cs similarity index 92% rename from tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/AnApplicationService.cs rename to tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AnApplicationService.cs index 1f530b5d..ee4fa719 100644 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/AnApplicationService.cs +++ b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AnApplicationService.cs @@ -1,6 +1,6 @@ using Backend.Fx.BuildingBlocks; -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.Domain +namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain { public interface ITestApplicationService : IApplicationService { } diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/AnIntegrationEvent.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AnIntegrationEvent.cs similarity index 95% rename from tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/AnIntegrationEvent.cs rename to tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AnIntegrationEvent.cs index 7e086f6a..85cbacb6 100644 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/AnIntegrationEvent.cs +++ b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/AnIntegrationEvent.cs @@ -1,7 +1,7 @@ using Backend.Fx.Patterns.EventAggregation.Integration; using JetBrains.Annotations; -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.Domain +namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain { [UsedImplicitly] public class AnIntegrationEvent : IntegrationEvent diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/SomeState.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/SomeState.cs similarity index 87% rename from tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/SomeState.cs rename to tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/SomeState.cs index a484b5fd..a76d2934 100644 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Domain/SomeState.cs +++ b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/ASimpleDomain/SomeState.cs @@ -1,4 +1,4 @@ -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.Domain +namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain { public class SomeState { diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Bootstrapping/APersistenceModule.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Bootstrapping/APersistenceModule.cs deleted file mode 100644 index 7d744df6..00000000 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Bootstrapping/APersistenceModule.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Backend.Fx.BuildingBlocks; -using Backend.Fx.ConfigurationSettings; -using Backend.Fx.Environment.Authentication; -using Backend.Fx.Environment.DateAndTime; -using Backend.Fx.InMemoryPersistence; -using Backend.Fx.Patterns.EventAggregation.Domain; -using Backend.Fx.Patterns.EventAggregation.Integration; -using Backend.Fx.Patterns.IdGeneration; -using Backend.Fx.Patterns.UnitOfWork; -using Backend.Fx.SimpleInjectorDependencyInjection.Modules; -using SimpleInjector; - -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.Bootstrapping -{ - public class APersistenceModule : SimpleInjectorModule - { - public Dictionary Stores { get; } - - public APersistenceModule(params Assembly[] domainAssemblies) - { - Stores = domainAssemblies.SelectMany(ass => ass.GetExportedTypes()) - .Where(t => !t.GetTypeInfo().IsAbstract && t.GetTypeInfo().IsClass) - .Where(t => typeof(AggregateRoot).IsAssignableFrom(t)) - .Select(t => { - var storeType = typeof(InMemoryStore<>).MakeGenericType(t); - var store = Activator.CreateInstance(storeType); - return new { t, store }; - }) - .ToDictionary(arg => arg.t, arg => arg.store); - - // the aggregate root "setting" resides outside the scanned assembly. Adding a repo manually now. - Stores.Add(typeof(Setting), new InMemoryStore()); - } - - protected override void Register(Container container, ScopedLifestyle scopedLifestyle) - { - foreach (var store in Stores) - { - container.RegisterSingleton(typeof(IInMemoryStore<>).MakeGenericType(store.Key), () => store.Value); - } - - container.Register(typeof(IRepository<>), typeof(InMemoryRepository<>)); - container.Register(typeof(IQueryable<>), typeof(InMemoryQueryable<>)); - - var uowRegistration = Lifestyle.Scoped.CreateRegistration( - () => new InMemoryUnitOfWork(new FrozenClock(), CurrentIdentityHolder.CreateSystem(), - container.GetInstance(), - container.GetInstance()), - container); - container.AddRegistration(typeof(IUnitOfWork), uowRegistration); - container.AddRegistration(typeof(ICanFlush), uowRegistration); - - container.RegisterInstance(new InMemoryEntityIdGenerator()); - } - } -} diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Bootstrapping/AnApplication.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Bootstrapping/AnApplication.cs deleted file mode 100644 index 01279562..00000000 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/DummyImpl/Bootstrapping/AnApplication.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.InMemoryPersistence; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.Patterns.EventAggregation.Integration; -using Xunit; - -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.Bootstrapping -{ - public class AnApplication : BackendFxApplication - { - private readonly ITenantRepository _tenantRepository; - private static readonly ILogger Logger = LogManager.Create(); - - public AnApplication(ICompositionRoot compositionRoot) - : this(compositionRoot, new InMemoryTenantRepository()) - { } - - public AnApplication(ICompositionRoot compositionRoot, ITenantRepository tenantRepository) - : base(compositionRoot, new TenantIdService(tenantRepository), new ExceptionLogger(Logger)) - { - _tenantRepository = tenantRepository; - } - - public void EnsureProdTenant() - { - - var prodTenantId = TenantIdService.GetActiveProductionTenantIds().FirstOrDefault(); - if (prodTenantId == null) - { - ManualResetEventSlim prodTenantActivated = new ManualResetEventSlim(false); - var eventBus = CompositionRoot.GetInstance(); - eventBus.Subscribe(new DelegateIntegrationEventHandler(ta => prodTenantActivated.Set())); - ProdTenantId = new TenantService(eventBus, _tenantRepository).CreateProductionTenant("prod", "unit test created", "en-US"); - Assert.True(prodTenantActivated.Wait(Debugger.IsAttached ? int.MaxValue : 10000)); - } - else - { - ProdTenantId = new TenantId(prodTenantId.Value); - } - } - - public void EnsureDemoTenant() - { - var demoTenantId = TenantIdService.GetActiveDemonstrationTenantIds().FirstOrDefault(); - if (demoTenantId == null) - { - ManualResetEventSlim demoTenantActivated = new ManualResetEventSlim(false); - var eventBus = CompositionRoot.GetInstance(); - eventBus.Subscribe(new DelegateIntegrationEventHandler(ta => demoTenantActivated.Set())); - DemoTenantId = new TenantService(eventBus, _tenantRepository).CreateDemonstrationTenant("demo", "unit test created", "en-US"); - Assert.True(demoTenantActivated.Wait(Debugger.IsAttached ? int.MaxValue : 10000)); - } - else - { - DemoTenantId = new TenantId(demoTenantId.Value); - } - } - - public TenantId ProdTenantId { get; private set; } - - public TenantId DemoTenantId { get; private set; } - - protected override Task OnBooted(CancellationToken cancellationToken) - { - var tenantService = new TenantService(CompositionRoot.GetInstance(), _tenantRepository); - this.RegisterSeedActionForNewlyCreatedTenants(tenantService); - this.SeedDataForAllActiveTenants(); - return Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/TheBackendFxApplication.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/TheBackendFxApplication.cs deleted file mode 100644 index 45c7a120..00000000 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/TheBackendFxApplication.cs +++ /dev/null @@ -1,192 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Linq; -using System.Security.Principal; -using System.Threading.Tasks; -using Backend.Fx.BuildingBlocks; -using Backend.Fx.Environment.Authentication; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.InMemoryPersistence; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.DependencyInjection; -using Backend.Fx.Patterns.EventAggregation.Integration; -using Backend.Fx.SimpleInjectorDependencyInjection.Modules; -using Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.Bootstrapping; -using Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.Domain; -using Xunit; - -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests -{ - public class TheBackendFxApplication - { - private readonly APersistenceModule _persistenceModule; - - public TheBackendFxApplication() - { - _persistenceModule = new APersistenceModule(typeof(AnApplication).Assembly, typeof(InMemoryStore<>).Assembly); - } - - [Fact] - public async Task RunsProdDataGeneratorsOnEveryBoot() - { - using (AnApplication sut = CreateSystemUnderTest()) - { - await sut.Boot(); - sut.EnsureProdTenant(); - - using (sut.BeginScope(new SystemIdentity(), sut.ProdTenantId)) - { - IRepository repository = sut.CompositionRoot.GetInstance>(); - AnAggregate[] allAggregates = repository.GetAll(); - Assert.Single(allAggregates); - Assert.Equal(1, allAggregates.Count(agg => agg.Name == AProdAggregateGenerator.Name)); - } - } - - using (AnApplication sut = CreateSystemUnderTest()) - { - await sut.Boot(); - sut.EnsureProdTenant(); - - using (sut.BeginScope(new SystemIdentity(), sut.ProdTenantId)) - { - IRepository repository = sut.CompositionRoot.GetInstance>(); - AnAggregate[] allAggregates = repository.GetAll(); - Assert.Equal(2, allAggregates.Length); - Assert.Equal(2, allAggregates.Count(agg => agg.Name == AProdAggregateGenerator.Name)); - } - } - } - - [Fact] - public async Task RunsDemoDataGeneratorsOnEveryBoot() - { - using (AnApplication sut = CreateSystemUnderTest()) - { - await sut.Boot(); - sut.EnsureDemoTenant(); - using (sut.BeginScope(new SystemIdentity(), sut.DemoTenantId)) - { - IRepository repository = sut.CompositionRoot.GetInstance>(); - AnAggregate[] allAggregates = repository.GetAll(); - Assert.Equal(2, allAggregates.Length); - Assert.NotNull(allAggregates.SingleOrDefault(agg => agg.Name == ADemoAggregateGenerator.Name)); - Assert.NotNull(allAggregates.SingleOrDefault(agg => agg.Name == AProdAggregateGenerator.Name)); - } - } - - using (AnApplication sut = CreateSystemUnderTest()) - { - await sut.Boot(); - sut.EnsureDemoTenant(); - - using (sut.BeginScope(new SystemIdentity(), sut.DemoTenantId)) - { - IRepository repository = sut.CompositionRoot.GetInstance>(); - AnAggregate[] allAggregates = repository.GetAll(); - Assert.Equal(4, allAggregates.Length); - Assert.Equal(2, allAggregates.Count(agg => agg.Name == ADemoAggregateGenerator.Name)); - Assert.Equal(2, allAggregates.Count(agg => agg.Name == AProdAggregateGenerator.Name)); - } - } - } - - [Fact] - public async Task MaintainsTenantIdWhenBeginningScopes() - { - using (AnApplication sut = CreateSystemUnderTest()) - { - await sut.Boot(); - Enumerable.Range(1, 100).AsParallel().ForAll(i => - { - // ReSharper disable AccessToDisposedClosure - using (sut.BeginScope(new SystemIdentity(), new TenantId(i))) - { - TenantId insideScopeTenantId = sut.CompositionRoot.GetInstance>().Current; - Assert.True(insideScopeTenantId.HasValue); - Assert.Equal(i, insideScopeTenantId.Value); - } - // ReSharper restore AccessToDisposedClosure - }); - } - } - - [Fact] - public async Task CanConfigureScopeOnInvoke() - { - using (AnApplication sut = CreateSystemUnderTest()) - { - await sut.Boot(); - Enumerable.Range(1, 100).AsParallel().ForAll(i => - { - // ReSharper disable AccessToDisposedClosure - sut.Invoke(() => - { - Assert.Equal(i.ToString(), sut.CompositionRoot.GetInstance().Value); - }, - new SystemIdentity(), - new TenantId(1), - cr => - { - cr.GetInstance().Value = i.ToString(); - }); - // ReSharper restore AccessToDisposedClosure - }); - } - } - - [Fact] - public async Task MaintainsCorrelationWhenBeginningScopes() - { - using (AnApplication sut = CreateSystemUnderTest()) - { - await sut.Boot(); - var usedCorrelationIds = new ConcurrentBag(); - Enumerable.Range(1, 100).AsParallel().ForAll(i => - { - // ReSharper disable AccessToDisposedClosure - using (sut.BeginScope(new SystemIdentity(), new TenantId(i))) - { - Correlation insideScopeCorrelation = sut.CompositionRoot.GetInstance>().Current; - Assert.NotEqual(Guid.Empty, insideScopeCorrelation.Id); - Assert.DoesNotContain(insideScopeCorrelation.Id, usedCorrelationIds); - usedCorrelationIds.Add(insideScopeCorrelation.Id); - } - // ReSharper restore AccessToDisposedClosure - }); - } - } - - [Fact] - public async Task MaintainsIdentityWhenBeginningScopes() - { - using (AnApplication sut = CreateSystemUnderTest()) - { - await sut.Boot(); - Enumerable.Range(1, 100).AsParallel().ForAll(i => - { - // ReSharper disable AccessToDisposedClosure - using (sut.BeginScope(new GenericIdentity(i.ToString()), new TenantId(100))) - { - IIdentity insideScopeIdentity = sut.CompositionRoot.GetInstance>().Current; - Assert.Equal(i.ToString(), insideScopeIdentity.Name); - } - // ReSharper restore AccessToDisposedClosure - }); - } - } - - private AnApplication CreateSystemUnderTest() - { - var compositionRoot = new SimpleInjectorCompositionRoot(); - var sut = new AnApplication(compositionRoot); - IEventBus eventBus = new InMemoryEventBus(sut); - sut.CompositionRoot.RegisterModules( - new InfrastructureModule(new DebugExceptionLogger(), eventBus), - new ADomainModule(typeof(AnApplication).Assembly, typeof(AggregateRoot).Assembly), - _persistenceModule); - - return sut; - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/TheMisconfiguredSimpleInjectorCompositionRoot.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/TheMisconfiguredSimpleInjectorCompositionRoot.cs index 43258181..0f300b10 100644 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/TheMisconfiguredSimpleInjectorCompositionRoot.cs +++ b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/TheMisconfiguredSimpleInjectorCompositionRoot.cs @@ -11,7 +11,7 @@ public class TheMisconfiguredSimpleInjectorCompositionRoot [Fact] public void ThrowsOnValidation() { - SimpleInjectorCompositionRoot sut = new SimpleInjectorCompositionRoot(); + var sut = new SimpleInjectorCompositionRoot(); sut.RegisterModules(new BadModule()); Assert.Throws(() => sut.Verify()); } diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/TheSimpleInjectorCompositionRoot.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/TheSimpleInjectorCompositionRoot.cs index 7480afa5..1c83a4bc 100644 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/TheSimpleInjectorCompositionRoot.cs +++ b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/TheSimpleInjectorCompositionRoot.cs @@ -3,15 +3,7 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; -using Backend.Fx.Environment.DateAndTime; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.EventAggregation.Integration; -using Backend.Fx.Patterns.IdGeneration; -using Backend.Fx.SimpleInjectorDependencyInjection.Modules; -using Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.Bootstrapping; -using Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.Domain; -using FakeItEasy; +using Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.ASimpleDomain; using SimpleInjector; using Xunit; @@ -21,35 +13,12 @@ public class TheSimpleInjectorCompositionRoot : IDisposable { private readonly SimpleInjectorCompositionRoot _sut; - private class ADomainModule : DomainModule - { - public ADomainModule(params Assembly[] domainAssemblies) : base(domainAssemblies) - { } - - protected override void Register(Container container, ScopedLifestyle scopedLifestyle) - { - base.Register(container, scopedLifestyle); - container.Register(); - } - } - public TheSimpleInjectorCompositionRoot() { - - var tenantManager = A.Fake(); - TenantId[] tenantIds = { new TenantId(999) }; - A.CallTo(() => tenantManager.GetActiveTenantIds()).Returns(tenantIds); - - _sut = new SimpleInjectorCompositionRoot(); - var domainAssembly = typeof(AnAggregate).GetTypeInfo().Assembly; - _sut.RegisterModules( - new InfrastructureModule(new DebugExceptionLogger(), A.Fake()), - new ADomainModule(domainAssembly), - new APersistenceModule(domainAssembly)); - + Assembly domainAssembly = typeof(AnAggregate).GetTypeInfo().Assembly; + _sut.RegisterModules(new ADomainModule(domainAssembly)); _sut.Verify(); - } [Fact] @@ -91,22 +60,22 @@ public void ProvidesAutoRegisteredApplicationServices() [Fact] public void ProvidesScopedInstancesWhenScopeHasBeenStarted() { - IClock scope1Clock; - IClock scope2Clock; + ITestDomainService scope1Instance; + ITestDomainService scope2Instance; using (_sut.BeginScope()) { - scope1Clock = _sut.GetInstance(); - Assert.NotNull(scope1Clock); + scope1Instance = _sut.GetInstance(); + Assert.NotNull(scope1Instance); } using (_sut.BeginScope()) { - scope2Clock = _sut.GetInstance(); - Assert.NotNull(scope2Clock); + scope2Instance = _sut.GetInstance(); + Assert.NotNull(scope2Instance); } - Assert.NotEqual(scope1Clock, scope2Clock); + Assert.NotEqual(scope1Instance, scope2Instance); } [Fact] @@ -118,7 +87,7 @@ public void ProvidesSingletonAndScopedInstancesAccordingly() object[] singletonInstances = new object[parallelScopeCount]; Task[] tasks = new Task[parallelScopeCount]; - ManualResetEvent waiter = new ManualResetEvent(false); + var waiter = new ManualResetEvent(false); // resolving a singleton service and a scoped service in a massive parallel scenario for (int index = 0; index < parallelScopeCount; index++) @@ -130,8 +99,8 @@ public void ProvidesSingletonAndScopedInstancesAccordingly() waiter.WaitOne(); using (_sut.BeginScope()) { - scopedInstances[indexClosure] = _sut.GetInstance(); - singletonInstances[indexClosure] = _sut.GetInstance(); + scopedInstances[indexClosure] = _sut.GetInstance(); + singletonInstances[indexClosure] = _sut.GetInstance(); } }); } @@ -161,22 +130,22 @@ public void ProvidesSingletonAndScopedInstancesAccordingly() [Fact] public void ThrowsWhenScopedInstanceIsRequestedOutsideScope() { - Assert.Throws(() => _sut.GetInstance()); - Assert.Throws(() => _sut.GetInstance(typeof(IClock))); + Assert.Throws(() => _sut.GetInstance()); + Assert.Throws(() => _sut.GetInstance(typeof(ITestDomainService))); Assert.Null(_sut.GetCurrentScope()); using (_sut.BeginScope()) { - var sutClock = _sut.GetInstance(); - var scopeClock = _sut.GetInstance(); - Assert.NotNull(sutClock); - Assert.NotNull(scopeClock); - Assert.Equal(sutClock, scopeClock); + var sutInstance = _sut.GetInstance(); + var scopeInstance = _sut.GetInstance(); + Assert.NotNull(sutInstance); + Assert.NotNull(scopeInstance); + Assert.Equal(sutInstance, scopeInstance); } Assert.Null(_sut.GetCurrentScope()); - Assert.Throws(() => _sut.GetInstance()); - Assert.Throws(() => _sut.GetInstance(typeof(IClock))); + Assert.Throws(() => _sut.GetInstance()); + Assert.Throws(() => _sut.GetInstance(typeof(ITestDomainService))); } [Fact] diff --git a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/TheTenantService.cs b/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/TheTenantService.cs deleted file mode 100644 index 01b46397..00000000 --- a/tests/Backend.Fx.SimpleInjectorDependencyInjection.Tests/TheTenantService.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Diagnostics; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using Backend.Fx.BuildingBlocks; -using Backend.Fx.Environment.MultiTenancy; -using Backend.Fx.InMemoryPersistence; -using Backend.Fx.Logging; -using Backend.Fx.Patterns.EventAggregation.Integration; -using Backend.Fx.SimpleInjectorDependencyInjection.Modules; -using Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.Bootstrapping; -using Backend.Fx.SimpleInjectorDependencyInjection.Tests.DummyImpl.Domain; -using FakeItEasy; -using Xunit; - -namespace Backend.Fx.SimpleInjectorDependencyInjection.Tests -{ - public class TheTenantService - { - private readonly ITenantService _sut; - private readonly IEventBus _eventBus = A.Fake(); - - public TheTenantService() - { - A.CallTo(() => _eventBus.Publish(A._)).Invokes((IIntegrationEvent iev) => - { - _sut.ActivateTenant(new TenantId(iev.TenantId)); - }); - - var compositionRoot = new SimpleInjectorCompositionRoot(); - var domainAssembly = typeof(AnAggregate).GetTypeInfo().Assembly; - var backendfxAssembly = typeof(Entity).GetTypeInfo().Assembly; - compositionRoot.RegisterModules( - new InfrastructureModule(new DebugExceptionLogger(), _eventBus), - new ADomainModule(domainAssembly, backendfxAssembly), - new APersistenceModule(domainAssembly)); - - compositionRoot.Verify(); - - _sut = new TenantService(_eventBus, new InMemoryTenantRepository()); - } - - [Fact] - public void RaisesTenantCreatedEvent() - { - ManualResetEvent ev = new ManualResetEvent(false); - A.CallTo(() => _eventBus.Publish(A._)).Invokes(() => ev.Set()); - Task.Run(() => _sut.CreateProductionTenant("prod", "unit test created", "de-DE")); - Assert.True(ev.WaitOne(Debugger.IsAttached ? int.MaxValue : 10000)); - } - - [Fact] - public void RaisesTenantActivatedEvent() - { - ManualResetEvent ev = new ManualResetEvent(false); - A.CallTo(() => _eventBus.Publish(A._)).Invokes(() => ev.Set()); - - Task.Run(() => _sut.CreateProductionTenant("prod", "unit test created", "de-DE")); - Assert.True(ev.WaitOne(Debugger.IsAttached ? int.MaxValue : 10000)); - } - } -} diff --git a/tests/Backend.Fx.Tests/Backend.Fx.Tests.csproj b/tests/Backend.Fx.Tests/Backend.Fx.Tests.csproj index c1be376d..fe6f8fd0 100644 --- a/tests/Backend.Fx.Tests/Backend.Fx.Tests.csproj +++ b/tests/Backend.Fx.Tests/Backend.Fx.Tests.csproj @@ -5,13 +5,17 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - + - + - + diff --git a/tests/Backend.Fx.Tests/BuildingBlocks/TheAggregateRoot.cs b/tests/Backend.Fx.Tests/BuildingBlocks/TheAggregateRoot.cs index 4fbc596f..8a336b70 100644 --- a/tests/Backend.Fx.Tests/BuildingBlocks/TheAggregateRoot.cs +++ b/tests/Backend.Fx.Tests/BuildingBlocks/TheAggregateRoot.cs @@ -1,13 +1,12 @@ -using JetBrains.Annotations; +using System; +using System.Collections.Generic; +using Backend.Fx.BuildingBlocks; +using Backend.Fx.RandomData; +using JetBrains.Annotations; using Xunit; namespace Backend.Fx.Tests.BuildingBlocks { - using System; - using System.Collections.Generic; - using Fx.BuildingBlocks; - using RandomData; - public class TheAggregateRoot { private static int _nextId; @@ -22,10 +21,9 @@ public TestAggregateRoot(int id, string name) : base(id) Children.Add(new TestEntity("Child 3", this)); } - [UsedImplicitly] - public string Name { get; private set; } + [UsedImplicitly] public string Name { get; private set; } - public ISet Children { get; } = new HashSet(); + public ISet Children { get; } = new HashSet(); } public class TestEntity : Entity @@ -36,30 +34,19 @@ public TestEntity(string name, TestAggregateRoot parent) Parent = parent; } - [UsedImplicitly] - public string Name { get; set; } - [UsedImplicitly] - public TestAggregateRoot Parent { get; set; } - } + [UsedImplicitly] public string Name { get; set; } - [Fact] - public void CreatedByPropertyIsStoredCorrectly() - { - DateTime now = DateTime.Now; - var sut = new TestAggregateRoot(_nextId++, "gaga"); - sut.SetCreatedProperties("me", now); - Assert.Equal("me", sut.CreatedBy); - Assert.Null(sut.ChangedBy); + [UsedImplicitly] public TestAggregateRoot Parent { get; set; } } [Fact] - public void CreatedOnPropertyIsStoredCorrectly() + public void ChangedByPropertyIsChoppedAt100Chars() { DateTime now = DateTime.Now; var sut = new TestAggregateRoot(_nextId++, "gaga"); - sut.SetCreatedProperties("me", now); - Assert.Equal(now, sut.CreatedOn); - Assert.Null(sut.ChangedOn); + var moreThanHundred = Letters.RandomLowerCase(110); + sut.SetModifiedProperties(moreThanHundred, now); + Assert.Equal(moreThanHundred.Substring(0, 99) + "…", sut.ChangedBy); } [Fact] @@ -79,7 +66,7 @@ public void ChangedOnPropertyIsStoredCorrectly() var sut = new TestAggregateRoot(_nextId++, "gaga"); sut.SetModifiedProperties("me", now); Assert.Equal(now, sut.ChangedOn); - Assert.Equal(default(DateTime), sut.CreatedOn); + Assert.Equal(default, sut.CreatedOn); } [Fact] @@ -93,29 +80,31 @@ public void CreatedByPropertyIsChoppedAt100Chars() } [Fact] - public void ChangedByPropertyIsChoppedAt100Chars() + public void CreatedByPropertyIsStoredCorrectly() { DateTime now = DateTime.Now; var sut = new TestAggregateRoot(_nextId++, "gaga"); - var moreThanHundred = Letters.RandomLowerCase(110); - sut.SetModifiedProperties(moreThanHundred, now); - Assert.Equal(moreThanHundred.Substring(0, 99) + "…", sut.ChangedBy); + sut.SetCreatedProperties("me", now); + Assert.Equal("me", sut.CreatedBy); + Assert.Null(sut.ChangedBy); } [Fact] - public void ThrowsGivenNullCreatedBy() + public void CreatedOnPropertyIsStoredCorrectly() { + DateTime now = DateTime.Now; var sut = new TestAggregateRoot(_nextId++, "gaga"); - // ReSharper disable once AssignNullToNotNullAttribute - Assert.Throws(() => sut.SetCreatedProperties(null, DateTime.Now)); + sut.SetCreatedProperties("me", now); + Assert.Equal(now, sut.CreatedOn); + Assert.Null(sut.ChangedOn); } [Fact] - public void ThrowsGivenNullChangedBy() + public void ThrowsGivenEmptyChangedBy() { var sut = new TestAggregateRoot(_nextId++, "gaga"); // ReSharper disable once AssignNullToNotNullAttribute - Assert.Throws(() => sut.SetModifiedProperties(null, DateTime.Now)); + Assert.Throws(() => sut.SetModifiedProperties("", DateTime.Now)); } [Fact] @@ -127,12 +116,19 @@ public void ThrowsGivenEmptyCreatedBy() } [Fact] - public void ThrowsGivenEmptyChangedBy() + public void ThrowsGivenNullChangedBy() { var sut = new TestAggregateRoot(_nextId++, "gaga"); // ReSharper disable once AssignNullToNotNullAttribute - Assert.Throws(() => sut.SetModifiedProperties("", DateTime.Now)); + Assert.Throws(() => sut.SetModifiedProperties(null, DateTime.Now)); } + [Fact] + public void ThrowsGivenNullCreatedBy() + { + var sut = new TestAggregateRoot(_nextId++, "gaga"); + // ReSharper disable once AssignNullToNotNullAttribute + Assert.Throws(() => sut.SetCreatedProperties(null, DateTime.Now)); + } } -} +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/BuildingBlocks/TheRepository.cs b/tests/Backend.Fx.Tests/BuildingBlocks/TheRepository.cs index 6a89206f..800ab514 100644 --- a/tests/Backend.Fx.Tests/BuildingBlocks/TheRepository.cs +++ b/tests/Backend.Fx.Tests/BuildingBlocks/TheRepository.cs @@ -1,67 +1,130 @@ -using Backend.Fx.InMemoryPersistence; +using System; +using System.Linq; +using Backend.Fx.Environment.MultiTenancy; +using Backend.Fx.Exceptions; +using Backend.Fx.InMemoryPersistence; +using Backend.Fx.Patterns.Authorization; +using FakeItEasy; using Xunit; namespace Backend.Fx.Tests.BuildingBlocks { - using System; - using System.Linq; - using FakeItEasy; - using Fx.Environment.MultiTenancy; - using Fx.Exceptions; - using Fx.Patterns.Authorization; - public class TheRepository { [Fact] - public void ReturnsByIdOnSingle() + public void AcceptsNullArrayToResolve() { var authorization = A.Fake>(); A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); A.CallTo(() => authorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(false); + A.CallTo(() => authorization.CanCreate(A._)).Returns(true); - var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), authorization); - var agg1 = new TheAggregateRoot.TestAggregateRoot(23, "whatever") { TenantId = 234 }; - var agg2 = new TheAggregateRoot.TestAggregateRoot(24, "whatever") { TenantId = 234 }; - var agg3 = new TheAggregateRoot.TestAggregateRoot(25, "whatever") { TenantId = 234 }; - var agg4 = new TheAggregateRoot.TestAggregateRoot(26, "whatever") { TenantId = 234 }; + var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), + authorization); + Assert.Empty(sut.Resolve(null)); + } + + [Fact] + public void CanResolveListOfIds() + { + var authorization = A.Fake>(); + A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); + A.CallTo(() => authorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); + A.CallTo(() => authorization.CanCreate(A._)).Returns(true); + + var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), + authorization); + var agg1 = new TheAggregateRoot.TestAggregateRoot(23, "whatever") {TenantId = 234}; + var agg2 = new TheAggregateRoot.TestAggregateRoot(24, "whatever") {TenantId = 234}; + var agg3 = new TheAggregateRoot.TestAggregateRoot(25, "whatever") {TenantId = 234}; + var agg4 = new TheAggregateRoot.TestAggregateRoot(26, "whatever") {TenantId = 234}; sut.Store.Add(agg1.Id, agg1); sut.Store.Add(agg2.Id, agg2); sut.Store.Add(agg3.Id, agg3); sut.Store.Add(agg4.Id, agg4); - Assert.Equal(agg1, sut.Single(agg1.Id)); - Assert.Equal(agg2, sut.Single(agg2.Id)); - Assert.Equal(agg3, sut.Single(agg3.Id)); - Assert.Equal(agg4, sut.Single(agg4.Id)); - Assert.Throws>(() => sut.Single(235421354)); + var resolved = sut.Resolve(new[] {23, 24, 25, 26}); + Assert.Equal(4, resolved.Length); + Assert.Contains(agg1, resolved); + Assert.Contains(agg2, resolved); + Assert.Contains(agg3, resolved); + Assert.Contains(agg4, resolved); } [Fact] - public void ReturnsByIdOnSingleOrDefault() + public void ThrowsOnAttemptToAddNull() + { + var sut = new InMemoryRepository(new InMemoryStore(), + CurrentTenantIdHolder.Create(234), + new AllowAll()); + Assert.Throws(() => sut.AddRange(null!)); + Assert.Throws(() => sut.Add(null!)); + } + + [Fact] + public void ThrowsOnAttemptToDeleteNull() + { + var sut = new InMemoryRepository(new InMemoryStore(), + CurrentTenantIdHolder.Create(234), + new AllowAll()); + Assert.Throws(() => sut.Delete(null!)); + } + + [Fact] + public void DeletesItemFromMyTenant() { var authorization = A.Fake>(); A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); A.CallTo(() => authorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(false); + A.CallTo(() => authorization.CanCreate(A._)).Returns(true); + A.CallTo(() => authorization.CanDelete(A._)).Returns(true); - var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), authorization); - var agg1 = new TheAggregateRoot.TestAggregateRoot(23, "whatever") { TenantId = 234 }; - var agg2 = new TheAggregateRoot.TestAggregateRoot(24, "whatever") { TenantId = 234 }; - var agg3 = new TheAggregateRoot.TestAggregateRoot(25, "whatever") { TenantId = 234 }; - var agg4 = new TheAggregateRoot.TestAggregateRoot(26, "whatever") { TenantId = 234 }; + var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), + authorization); + var agg1 = new TheAggregateRoot.TestAggregateRoot(12123123, "whatever") {TenantId = 234}; sut.Store.Add(agg1.Id, agg1); - sut.Store.Add(agg2.Id, agg2); - sut.Store.Add(agg3.Id, agg3); - sut.Store.Add(agg4.Id, agg4); - Assert.Equal(agg1, sut.SingleOrDefault(agg1.Id)); - Assert.Equal(agg2, sut.SingleOrDefault(agg2.Id)); - Assert.Equal(agg3, sut.SingleOrDefault(agg3.Id)); - Assert.Equal(agg4, sut.SingleOrDefault(agg4.Id)); - Assert.Null(sut.SingleOrDefault(235421354)); + sut.Delete(agg1); + + Assert.Empty(sut.Store); + } + + [Fact] + public void DoesNotReturnItemsFromOtherTenants() + { + var authorization = A.Fake>(); + A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); + A.CallTo(() => authorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); + A.CallTo(() => authorization.CanCreate(A._)).Returns(true); + + var store = new InMemoryStore(); + var sut = new InMemoryRepository(store, CurrentTenantIdHolder.Create(234), authorization); + + sut.Add(new TheAggregateRoot.TestAggregateRoot(22, "1")); + sut.Add(new TheAggregateRoot.TestAggregateRoot(23, "2")); + sut.Add(new TheAggregateRoot.TestAggregateRoot(24, "3")); + + // now I am in another tenant + sut = new InMemoryRepository(store, CurrentTenantIdHolder.Create(233), authorization); + Assert.Empty(sut.AggregateQueryable); + } + + [Fact] + public void MaintainsTenantIdOnAdd() + { + var authorization = A.Fake>(); + A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); + A.CallTo(() => authorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); + A.CallTo(() => authorization.CanCreate(A._)).Returns(true); + + var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), + authorization); + + var agg1 = new TheAggregateRoot.TestAggregateRoot(22, "1"); + sut.Add(agg1); + Assert.Equal(234, agg1.TenantId); } [Fact] @@ -72,13 +135,14 @@ public void ProvidesCorrectAny() A.CallTo(() => authorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); A.CallTo(() => authorization.CanCreate(A._)).Returns(false); - var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), authorization); + var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), + authorization); Assert.False(sut.Any()); - var agg1 = new TheAggregateRoot.TestAggregateRoot(23, "whatever") { TenantId = 234 }; - var agg2 = new TheAggregateRoot.TestAggregateRoot(24, "whatever") { TenantId = 234 }; - var agg3 = new TheAggregateRoot.TestAggregateRoot(25, "whatever") { TenantId = 234 }; - var agg4 = new TheAggregateRoot.TestAggregateRoot(26, "whatever") { TenantId = 234 }; + var agg1 = new TheAggregateRoot.TestAggregateRoot(23, "whatever") {TenantId = 234}; + var agg2 = new TheAggregateRoot.TestAggregateRoot(24, "whatever") {TenantId = 234}; + var agg3 = new TheAggregateRoot.TestAggregateRoot(25, "whatever") {TenantId = 234}; + var agg4 = new TheAggregateRoot.TestAggregateRoot(26, "whatever") {TenantId = 234}; sut.Store.Add(agg1.Id, agg1); sut.Store.Add(agg2.Id, agg2); @@ -89,60 +153,89 @@ public void ProvidesCorrectAny() } [Fact] - public void ThrowsOnAddWhenTenantIdIsEmpty() + public void ReturnsAll() { var authorization = A.Fake>(); - var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(null), authorization); - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.CanCreate(A._)).Returns(true); A.CallTo(() => authorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); - Assert.Throws(() => sut.Add(new TheAggregateRoot.TestAggregateRoot(77, "whatever"))); + A.CallTo(() => authorization.CanCreate(A._)).Returns(true); - // even when I don't have permissions - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => false); - A.CallTo(() => authorization.CanCreate(A._)).Returns(false); - Assert.Throws(() => sut.Add(new TheAggregateRoot.TestAggregateRoot(78, "whatever"))); + var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), + authorization); + var agg1 = new TheAggregateRoot.TestAggregateRoot(12123123, "whatever") {TenantId = 234}; + var agg2 = new TheAggregateRoot.TestAggregateRoot(12123124, "whatever") {TenantId = 234}; + var agg3 = new TheAggregateRoot.TestAggregateRoot(12123125, "whatever") {TenantId = 234}; + var agg4 = new TheAggregateRoot.TestAggregateRoot(12123126, "whatever") {TenantId = 234}; + + sut.AddRange(new[] {agg1, agg2, agg3, agg4}); + + Assert.Equal(4, sut.GetAll().Length); + Assert.Contains(agg1, sut.GetAll()); + Assert.Contains(agg2, sut.GetAll()); + Assert.Contains(agg3, sut.GetAll()); + Assert.Contains(agg4, sut.GetAll()); } [Fact] - public void ThrowsOnDeleteWhenTenantIdHolderIsEmpty() + public void ReturnsByIdOnSingle() { var authorization = A.Fake>(); A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); A.CallTo(() => authorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(true); + A.CallTo(() => authorization.CanCreate(A._)).Returns(false); - var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(null), authorization); + var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), + authorization); + var agg1 = new TheAggregateRoot.TestAggregateRoot(23, "whatever") {TenantId = 234}; + var agg2 = new TheAggregateRoot.TestAggregateRoot(24, "whatever") {TenantId = 234}; + var agg3 = new TheAggregateRoot.TestAggregateRoot(25, "whatever") {TenantId = 234}; + var agg4 = new TheAggregateRoot.TestAggregateRoot(26, "whatever") {TenantId = 234}; - var agg1 = new TheAggregateRoot.TestAggregateRoot(12123123, "whatever") { TenantId = 234 }; sut.Store.Add(agg1.Id, agg1); + sut.Store.Add(agg2.Id, agg2); + sut.Store.Add(agg3.Id, agg3); + sut.Store.Add(agg4.Id, agg4); - Assert.Throws(() => sut.Delete(agg1)); + Assert.Equal(agg1, sut.Single(agg1.Id)); + Assert.Equal(agg2, sut.Single(agg2.Id)); + Assert.Equal(agg3, sut.Single(agg3.Id)); + Assert.Equal(agg4, sut.Single(agg4.Id)); + Assert.Throws>(() => sut.Single(235421354)); } [Fact] - public void ThrowsOnDeleteWhenUnauthorized() + public void ReturnsByIdOnSingleOrDefault() { var authorization = A.Fake>(); A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); A.CallTo(() => authorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(true); - A.CallTo(() => authorization.CanDelete(A._)).Returns(false); + A.CallTo(() => authorization.CanCreate(A._)).Returns(false); - var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), authorization); + var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), + authorization); + var agg1 = new TheAggregateRoot.TestAggregateRoot(23, "whatever") {TenantId = 234}; + var agg2 = new TheAggregateRoot.TestAggregateRoot(24, "whatever") {TenantId = 234}; + var agg3 = new TheAggregateRoot.TestAggregateRoot(25, "whatever") {TenantId = 234}; + var agg4 = new TheAggregateRoot.TestAggregateRoot(26, "whatever") {TenantId = 234}; - var agg1 = new TheAggregateRoot.TestAggregateRoot(12123123, "whatever") { TenantId = 234 }; sut.Store.Add(agg1.Id, agg1); + sut.Store.Add(agg2.Id, agg2); + sut.Store.Add(agg3.Id, agg3); + sut.Store.Add(agg4.Id, agg4); - Assert.Throws(() => sut.Delete(agg1)); + Assert.Equal(agg1, sut.SingleOrDefault(agg1.Id)); + Assert.Equal(agg2, sut.SingleOrDefault(agg2.Id)); + Assert.Equal(agg3, sut.SingleOrDefault(agg3.Id)); + Assert.Equal(agg4, sut.SingleOrDefault(agg4.Id)); + Assert.Null(sut.SingleOrDefault(235421354)); } [Fact] public void ReturnsEmptyWhenTenantIdHolderIsEmpty() { var authorization = A.Fake>(); - var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(null), authorization); + var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(null), + authorization); A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); A.CallTo(() => authorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); @@ -151,38 +244,32 @@ public void ReturnsEmptyWhenTenantIdHolderIsEmpty() } [Fact] - public void MaintainsTenantIdOnAdd() - { - var authorization = A.Fake>(); - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(true); - - var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), authorization); - - var agg1 = new TheAggregateRoot.TestAggregateRoot(22,"1"); - sut.Add(agg1); - Assert.Equal(234, agg1.TenantId); - } - - [Fact] - public void DoesNotReturnItemsFromOtherTenants() + public void ReturnsOnlyAuthorizedRecords() { var authorization = A.Fake>(); A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(true); + A.CallTo(() => authorization.Filter(A>._)) + .ReturnsLazily((IQueryable q) => q.Where(agg => agg.Id == 25 || agg.Id == 26)); + A.CallTo(() => authorization.CanCreate(A._)).Returns(false); - var store = new InMemoryStore(); - var sut = new InMemoryRepository(store, CurrentTenantIdHolder.Create(234), authorization); + var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), + authorization); + var agg1 = new TheAggregateRoot.TestAggregateRoot(23, "whatever") {TenantId = 234}; + var agg2 = new TheAggregateRoot.TestAggregateRoot(24, "whatever") {TenantId = 234}; + var agg3 = new TheAggregateRoot.TestAggregateRoot(25, "whatever") {TenantId = 234}; + var agg4 = new TheAggregateRoot.TestAggregateRoot(26, "whatever") {TenantId = 234}; - sut.Add(new TheAggregateRoot.TestAggregateRoot(22, "1")); - sut.Add(new TheAggregateRoot.TestAggregateRoot(23, "2")); - sut.Add(new TheAggregateRoot.TestAggregateRoot(24, "3")); + sut.Store.Add(agg1.Id, agg1); + sut.Store.Add(agg2.Id, agg2); + sut.Store.Add(agg3.Id, agg3); + sut.Store.Add(agg4.Id, agg4); - // now I am in another tenant - sut = new InMemoryRepository(store, CurrentTenantIdHolder.Create(233), authorization); - Assert.Empty(sut.AggregateQueryable); + var all = sut.GetAll(); + Assert.Equal(2, all.Length); + Assert.DoesNotContain(agg1, all); + Assert.DoesNotContain(agg2, all); + Assert.Contains(agg3, all); + Assert.Contains(agg4, all); } [Fact] @@ -222,98 +309,65 @@ public void ReturnsOnlyItemsFromMyTenant() } [Fact] - public void DeletesItemFromMyTenant() + public void ThrowsOnAddWhenTenantIdIsEmpty() { - var authorization = A.Fake>(); + var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(null), + authorization); + A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); A.CallTo(() => authorization.CanCreate(A._)).Returns(true); - A.CallTo(() => authorization.CanDelete(A._)).Returns(true); - - var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), authorization); - - var agg1 = new TheAggregateRoot.TestAggregateRoot(12123123, "whatever") { TenantId = 234 }; - sut.Store.Add(agg1.Id, agg1); - - sut.Delete(agg1); + A.CallTo(() => authorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); + Assert.Throws(() => sut.Add(new TheAggregateRoot.TestAggregateRoot(77, "whatever"))); - Assert.Empty(sut.Store); + // even when I don't have permissions + A.CallTo(() => authorization.HasAccessExpression).Returns(agg => false); + A.CallTo(() => authorization.CanCreate(A._)).Returns(false); + Assert.Throws(() => sut.Add(new TheAggregateRoot.TestAggregateRoot(78, "whatever"))); } [Fact] - public void ReturnsAll() + public void ThrowsOnAddRangeWhenTenantIdIsEmpty() { var authorization = A.Fake>(); + var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(null), + authorization); + A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); A.CallTo(() => authorization.CanCreate(A._)).Returns(true); + A.CallTo(() => authorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); + Assert.Throws(() => sut.Add(new TheAggregateRoot.TestAggregateRoot(77, "whatever"))); - var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), authorization); - var agg1 = new TheAggregateRoot.TestAggregateRoot(12123123, "whatever") { TenantId = 234 }; - var agg2 = new TheAggregateRoot.TestAggregateRoot(12123124, "whatever") { TenantId = 234 }; - var agg3 = new TheAggregateRoot.TestAggregateRoot(12123125, "whatever") { TenantId = 234 }; - var agg4 = new TheAggregateRoot.TestAggregateRoot(12123126, "whatever") { TenantId = 234 }; - - sut.Add(agg1); - sut.Add(agg2); - sut.Add(agg3); - sut.Add(agg4); - - Assert.Equal(4, sut.GetAll().Length); - Assert.Contains(agg1, sut.GetAll()); - Assert.Contains(agg2, sut.GetAll()); - Assert.Contains(agg3, sut.GetAll()); - Assert.Contains(agg4, sut.GetAll()); + // even when I don't have permissions + A.CallTo(() => authorization.HasAccessExpression).Returns(agg => false); + A.CallTo(() => authorization.CanCreate(A._)).Returns(false); + Assert.Throws(() => sut.AddRange(new[] {new TheAggregateRoot.TestAggregateRoot(78, "whatever")})); } [Fact] - public void CanResolveListOfIds() + public void ThrowsOnAddWhenUnauthorized() { var authorization = A.Fake>(); + var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), + authorization); + A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); A.CallTo(() => authorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(true); - - var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), authorization); - var agg1 = new TheAggregateRoot.TestAggregateRoot(23, "whatever") { TenantId = 234 }; - var agg2 = new TheAggregateRoot.TestAggregateRoot(24, "whatever") { TenantId = 234 }; - var agg3 = new TheAggregateRoot.TestAggregateRoot(25, "whatever") { TenantId = 234 }; - var agg4 = new TheAggregateRoot.TestAggregateRoot(26, "whatever") { TenantId = 234 }; - - sut.Store.Add(agg1.Id, agg1); - sut.Store.Add(agg2.Id, agg2); - sut.Store.Add(agg3.Id, agg3); - sut.Store.Add(agg4.Id, agg4); - - var resolved = sut.Resolve(new[] { 23, 24, 25, 26 }); - Assert.Equal(4, resolved.Length); - Assert.Contains(agg1, resolved); - Assert.Contains(agg2, resolved); - Assert.Contains(agg3, resolved); - Assert.Contains(agg4, resolved); + A.CallTo(() => authorization.CanCreate(A._)).Returns(false); + Assert.Throws(() => sut.Add(new TheAggregateRoot.TestAggregateRoot(44, "whatever"))); } [Fact] - public void ThrowsOnResolveWhenTenantDoesNotMatch() + public void ThrowsOnAddRangeWhenUnauthorized() { var authorization = A.Fake>(); + var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), + authorization); + A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); A.CallTo(() => authorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(true); - - var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), authorization); - var agg1 = new TheAggregateRoot.TestAggregateRoot(23, "whatever") { TenantId = 234 }; - var agg2 = new TheAggregateRoot.TestAggregateRoot(24, "whatever") { TenantId = 234 }; - var agg3 = new TheAggregateRoot.TestAggregateRoot(25, "whatever") { TenantId = 234 }; - var agg4 = new TheAggregateRoot.TestAggregateRoot(26, "whatever") { TenantId = 999 }; - - sut.Store.Add(agg1.Id, agg1); - sut.Store.Add(agg2.Id, agg2); - sut.Store.Add(agg3.Id, agg3); - sut.Store.Add(agg4.Id, agg4); - - Assert.Throws(() => sut.Resolve(new[] { 23, 24, 25, 26 })); + A.CallTo(() => authorization.CanCreate(A._)).Returns(false); + Assert.Throws(() => sut.AddRange(new[] {new TheAggregateRoot.TestAggregateRoot(44, "whatever")})); } [Fact] @@ -324,11 +378,12 @@ public void ThrowsOnDeleteWhenTenantDoesNotMatch() A.CallTo(() => authorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); A.CallTo(() => authorization.CanCreate(A._)).Returns(true); - var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), authorization); - var agg1 = new TheAggregateRoot.TestAggregateRoot(23, "whatever") { TenantId = 234 }; - var agg2 = new TheAggregateRoot.TestAggregateRoot(24, "whatever") { TenantId = 234 }; - var agg3 = new TheAggregateRoot.TestAggregateRoot(25, "whatever") { TenantId = 234 }; - var agg4 = new TheAggregateRoot.TestAggregateRoot(26, "whatever") { TenantId = 999 }; + var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), + authorization); + var agg1 = new TheAggregateRoot.TestAggregateRoot(23, "whatever") {TenantId = 234}; + var agg2 = new TheAggregateRoot.TestAggregateRoot(24, "whatever") {TenantId = 234}; + var agg3 = new TheAggregateRoot.TestAggregateRoot(25, "whatever") {TenantId = 234}; + var agg4 = new TheAggregateRoot.TestAggregateRoot(26, "whatever") {TenantId = 999}; sut.Store.Add(agg1.Id, agg1); sut.Store.Add(agg2.Id, agg2); @@ -339,54 +394,61 @@ public void ThrowsOnDeleteWhenTenantDoesNotMatch() } [Fact] - public void ThrowsOnAddWhenUnauthorized() + public void ThrowsOnDeleteWhenTenantIdHolderIsEmpty() { var authorization = A.Fake>(); - var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), authorization); - A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); A.CallTo(() => authorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); - A.CallTo(() => authorization.CanCreate(A._)).Returns(false); - Assert.Throws(() => sut.Add(new TheAggregateRoot.TestAggregateRoot(44, "whatever"))); + A.CallTo(() => authorization.CanCreate(A._)).Returns(true); + + var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(null), + authorization); + + var agg1 = new TheAggregateRoot.TestAggregateRoot(12123123, "whatever") {TenantId = 234}; + sut.Store.Add(agg1.Id, agg1); + + Assert.Throws(() => sut.Delete(agg1)); } [Fact] - public void ReturnsOnlyAuthorizedRecords() + public void ThrowsOnDeleteWhenUnauthorized() { var authorization = A.Fake>(); A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); - A.CallTo(() => authorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q.Where(agg => agg.Id == 25 || agg.Id == 26)); - A.CallTo(() => authorization.CanCreate(A._)).Returns(false); + A.CallTo(() => authorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); + A.CallTo(() => authorization.CanCreate(A._)).Returns(true); + A.CallTo(() => authorization.CanDelete(A._)).Returns(false); - var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), authorization); - var agg1 = new TheAggregateRoot.TestAggregateRoot(23, "whatever") { TenantId = 234 }; - var agg2 = new TheAggregateRoot.TestAggregateRoot(24, "whatever") { TenantId = 234 }; - var agg3 = new TheAggregateRoot.TestAggregateRoot(25, "whatever") { TenantId = 234 }; - var agg4 = new TheAggregateRoot.TestAggregateRoot(26, "whatever") { TenantId = 234 }; + var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), + authorization); + var agg1 = new TheAggregateRoot.TestAggregateRoot(12123123, "whatever") {TenantId = 234}; sut.Store.Add(agg1.Id, agg1); - sut.Store.Add(agg2.Id, agg2); - sut.Store.Add(agg3.Id, agg3); - sut.Store.Add(agg4.Id, agg4); - var all = sut.GetAll(); - Assert.Equal(2, all.Length); - Assert.DoesNotContain(agg1, all); - Assert.DoesNotContain(agg2, all); - Assert.Contains(agg3, all); - Assert.Contains(agg4, all); + Assert.Throws(() => sut.Delete(agg1)); } [Fact] - public void AcceptsNullArrayToResolve() + public void ThrowsOnResolveWhenTenantDoesNotMatch() { var authorization = A.Fake>(); A.CallTo(() => authorization.HasAccessExpression).Returns(agg => true); A.CallTo(() => authorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); A.CallTo(() => authorization.CanCreate(A._)).Returns(true); - var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), authorization); - Assert.Empty(sut.Resolve(null)); + var sut = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(234), + authorization); + var agg1 = new TheAggregateRoot.TestAggregateRoot(23, "whatever") {TenantId = 234}; + var agg2 = new TheAggregateRoot.TestAggregateRoot(24, "whatever") {TenantId = 234}; + var agg3 = new TheAggregateRoot.TestAggregateRoot(25, "whatever") {TenantId = 234}; + var agg4 = new TheAggregateRoot.TestAggregateRoot(26, "whatever") {TenantId = 999}; + + sut.Store.Add(agg1.Id, agg1); + sut.Store.Add(agg2.Id, agg2); + sut.Store.Add(agg3.Id, agg3); + sut.Store.Add(agg4.Id, agg4); + + Assert.Throws(() => sut.Resolve(new[] {23, 24, 25, 26})); } } } \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/BuildingBlocks/TheValueObject.cs b/tests/Backend.Fx.Tests/BuildingBlocks/TheValueObject.cs new file mode 100644 index 00000000..3d975d38 --- /dev/null +++ b/tests/Backend.Fx.Tests/BuildingBlocks/TheValueObject.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using Backend.Fx.BuildingBlocks; +using Xunit; + +namespace Backend.Fx.Tests.BuildingBlocks +{ + public class TheValueObject + { + [Fact] + public void IsConsideredEqualWhenAllPropertiesAreEqual() + { + var myValueObject1 = new MyValueObject(333, "gnarf"); + var myValueObject2 = new MyValueObject(333, "gnarf"); + Assert.True(myValueObject1.Equals(myValueObject2)); + Assert.True(myValueObject2.Equals(myValueObject1)); + Assert.True(Equals(myValueObject1, myValueObject2)); + + object myDumbValueObject1 = myValueObject1; + object myDumbValueObject2 = myValueObject2; + Assert.True(myDumbValueObject1.Equals(myDumbValueObject2)); + Assert.True(myDumbValueObject2.Equals(myDumbValueObject1)); + Assert.True(Equals(myDumbValueObject1, myDumbValueObject2)); + + // attention! R# warns you, though + // ReSharper disable once PossibleUnintendedReferenceComparison + Assert.False(myValueObject1 == myValueObject2); + } + + [Fact] + public void IsNotConsideredEqualWhenOnePropertyDoesNotMatch() + { + var myValueObject1 = new MyValueObject(333, "gnarf"); + var myValueObject2 = new MyValueObject(334, "gnarf"); + Assert.False(myValueObject1.Equals(myValueObject2)); + Assert.False(myValueObject2.Equals(myValueObject1)); + Assert.False(Equals(myValueObject1, myValueObject2)); + + object myDumbValueObject1 = myValueObject1; + object myDumbValueObject2 = myValueObject2; + Assert.False(myDumbValueObject1.Equals(myDumbValueObject2)); + Assert.False(myDumbValueObject2.Equals(myDumbValueObject1)); + Assert.False(Equals(myDumbValueObject1, myDumbValueObject2)); + + var myValueObject3 = new MyValueObject(333, "gnarfe"); + var myValueObject4 = new MyValueObject(333, "gnarfo"); + Assert.False(myValueObject3.Equals(myValueObject4)); + Assert.False(myValueObject4.Equals(myValueObject3)); + Assert.False(Equals(myValueObject3, myValueObject4)); + } + + [Fact] + public void CanBeCompared() + { + var myValueObject1 = new MyValueObject(333, "gnarf"); + var myValueObject2 = new MyValueObject(334, "gnarf"); + var myValueObject3 = new MyValueObject(334, "gnarf"); + + Assert.True(myValueObject1.CompareTo(myValueObject2) == -1); + Assert.True(myValueObject2.CompareTo(myValueObject1) == 1); + Assert.True(myValueObject2.CompareTo(myValueObject3) == 0); + Assert.True(myValueObject3.CompareTo(myValueObject2) == 0); + + object myDumbValueObject1 = myValueObject1; + object myDumbValueObject2 = myValueObject2; + object myDumbValueObject3 = myValueObject3; + + Assert.True(myValueObject1.CompareTo(myDumbValueObject2) == -1); + Assert.True(myValueObject2.CompareTo(myDumbValueObject1) == 1); + Assert.True(myValueObject2.CompareTo(myDumbValueObject3) == 0); + Assert.True(myValueObject3.CompareTo(myDumbValueObject2) == 0); + } + + [Fact] + public void DoesNotEqualNull() + { + var myValueObject1 = new MyValueObject(333, "gnarf"); + Assert.False(myValueObject1.Equals(null)); + Assert.False(Equals(myValueObject1, null)); + } + + [Fact] + public void CanCompareToNull() + { + var myValueObject1 = new MyValueObject(333, "gnarf"); + Assert.Equal(1,myValueObject1.CompareTo(null)); + } + + private class MyValueObject : ComparableValueObject + { + private int Order { get; } + private string Name { get; } + + + public MyValueObject(int order, string name) + { + Order = order; + Name = name; + } + + protected override IEnumerable GetEqualityComponents() + { + yield return Order; + yield return Name; + } + + protected override IEnumerable GetComparableComponents() + { + yield return Order; + } + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/ConfigurationSettings/TheSetting.cs b/tests/Backend.Fx.Tests/ConfigurationSettings/TheSetting.cs index d46253d6..d7571cca 100644 --- a/tests/Backend.Fx.Tests/ConfigurationSettings/TheSetting.cs +++ b/tests/Backend.Fx.Tests/ConfigurationSettings/TheSetting.cs @@ -1,51 +1,28 @@ -using JetBrains.Annotations; +using System; +using Backend.Fx.BuildingBlocks; +using Backend.Fx.ConfigurationSettings; +using Backend.Fx.Patterns.IdGeneration; +using JetBrains.Annotations; using Xunit; namespace Backend.Fx.Tests.ConfigurationSettings { - using System; - using Fx.BuildingBlocks; - using Fx.ConfigurationSettings; - using Fx.Patterns.IdGeneration; - public class TheSetting { [UsedImplicitly] public class TestSettingsService : SettingsService { - - public TestSettingsService(IEntityIdGenerator idGenerator, IRepository settingRepository) + public TestSettingsService(IEntityIdGenerator idGenerator, IRepository settingRepository) : base("Test", idGenerator, settingRepository, new SettingSerializerFactory()) - { } - } - - [Fact] - public void CanStoreString() - { - const string stringValue = "sdufhpsdfb ^ ÄÜÖÄÜ psdj"; - Setting sut = new Setting(1, "key"); - sut.SetValue(new StringSerializer(), stringValue); - Assert.Equal(stringValue, sut.SerializedValue); - var stringValueRead = sut.GetValue(new StringSerializer()); - Assert.Equal(stringValue, stringValueRead); - } - - [Fact] - public void CanStoreNullString() - { - const string stringValue = null; - Setting sut = new Setting(2, "key"); - sut.SetValue(new StringSerializer(), stringValue); - Assert.Equal(stringValue, sut.SerializedValue); - var stringValueRead = sut.GetValue(new StringSerializer()); - Assert.Equal(stringValue, stringValueRead); + { + } } [Fact] public void CanStoreBoolean() { const bool booleanValue = true; - Setting sut = new Setting(3, "key"); + var sut = new Setting(3, "key"); sut.SetValue(new BooleanSerializer(), booleanValue); Assert.Equal("True", sut.SerializedValue); var booleanValueRead = sut.GetValue(new BooleanSerializer()); @@ -53,51 +30,72 @@ public void CanStoreBoolean() } [Fact] - public void CanStoreNullBoolean() + public void CanStoreDateTime() { - Setting sut = new Setting(4, "key"); - sut.SetValue(new BooleanSerializer(), null); - Assert.Null(sut.SerializedValue); - var booleanValueRead = sut.GetValue(new BooleanSerializer()); - Assert.Null(booleanValueRead); + var dateTimeValue = new DateTime(1987, 4, 22, 23, 12, 11); + var sut = new Setting(9, "key"); + sut.SetValue(new DateTimeSerializer(), dateTimeValue); + Assert.Equal("1987-04-22T23:12:11.0000000", sut.SerializedValue); + var dateTimeValueRead = sut.GetValue(new DateTimeSerializer()); + Assert.Equal(dateTimeValue, dateTimeValueRead); } [Fact] public void CanStoreDouble() { const double doubleValue = 2354.2341234d; - Setting sut = new Setting(5, "key"); + var sut = new Setting(5, "key"); sut.SetValue(new DoubleSerializer(), doubleValue); Assert.Equal("2354.2341234", sut.SerializedValue); var doubleeanValueRead = sut.GetValue(new DoubleSerializer()); Assert.Equal(doubleValue, doubleeanValueRead); } - [Fact] - public void CanStoreNullDouble() - { - Setting sut = new Setting(6, "key"); - sut.SetValue(new DoubleSerializer(), null); - Assert.Null(sut.SerializedValue); - var doubleValueRead = sut.GetValue(new DoubleSerializer()); - Assert.Null(doubleValueRead); - } - [Fact] public void CanStoreInt() { const int intValue = 235234; - Setting sut = new Setting(7, "key"); + var sut = new Setting(7, "key"); sut.SetValue(new IntegerSerializer(), intValue); Assert.Equal("235234", sut.SerializedValue); var inteanValueRead = sut.GetValue(new IntegerSerializer()); Assert.Equal(intValue, inteanValueRead); } + [Fact] + public void CanStoreNullBoolean() + { + var sut = new Setting(4, "key"); + sut.SetValue(new BooleanSerializer(), null); + Assert.Null(sut.SerializedValue); + var booleanValueRead = sut.GetValue(new BooleanSerializer()); + Assert.Null(booleanValueRead); + } + + [Fact] + public void CanStoreNullDateTime() + { + var sut = new Setting(10, "key"); + sut.SetValue(new DateTimeSerializer(), null); + Assert.Null(sut.SerializedValue); + var dateTimeValueRead = sut.GetValue(new DateTimeSerializer()); + Assert.Null(dateTimeValueRead); + } + + [Fact] + public void CanStoreNullDouble() + { + var sut = new Setting(6, "key"); + sut.SetValue(new DoubleSerializer(), null); + Assert.Null(sut.SerializedValue); + var doubleValueRead = sut.GetValue(new DoubleSerializer()); + Assert.Null(doubleValueRead); + } + [Fact] public void CanStoreNullInt() { - Setting sut = new Setting(8, "key"); + var sut = new Setting(8, "key"); sut.SetValue(new IntegerSerializer(), null); Assert.Null(sut.SerializedValue); var intValueRead = sut.GetValue(new IntegerSerializer()); @@ -105,24 +103,25 @@ public void CanStoreNullInt() } [Fact] - public void CanStoreDateTime() + public void CanStoreNullString() { - DateTime dateTimeValue = new DateTime(1987, 4, 22, 23, 12, 11); - Setting sut = new Setting(9, "key"); - sut.SetValue(new DateTimeSerializer(), dateTimeValue); - Assert.Equal("1987-04-22T23:12:11.0000000", sut.SerializedValue); - var dateTimeValueRead = sut.GetValue(new DateTimeSerializer()); - Assert.Equal(dateTimeValue, dateTimeValueRead); + const string stringValue = null; + var sut = new Setting(2, "key"); + sut.SetValue(new StringSerializer(), stringValue); + Assert.Equal(stringValue, sut.SerializedValue); + var stringValueRead = sut.GetValue(new StringSerializer()); + Assert.Equal(stringValue, stringValueRead); } [Fact] - public void CanStoreNullDateTime() + public void CanStoreString() { - Setting sut = new Setting(10, "key"); - sut.SetValue(new DateTimeSerializer(), null); - Assert.Null(sut.SerializedValue); - var dateTimeValueRead = sut.GetValue(new DateTimeSerializer()); - Assert.Null(dateTimeValueRead); + const string stringValue = "sdufhpsdfb ^ ÄÜÖÄÜ psdj"; + var sut = new Setting(1, "key"); + sut.SetValue(new StringSerializer(), stringValue); + Assert.Equal(stringValue, sut.SerializedValue); + var stringValueRead = sut.GetValue(new StringSerializer()); + Assert.Equal(stringValue, stringValueRead); } } -} +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/ConfigurationSettings/TheSettingSerializerFactory.cs b/tests/Backend.Fx.Tests/ConfigurationSettings/TheSettingSerializerFactory.cs index 284e03d7..5b79e8aa 100644 --- a/tests/Backend.Fx.Tests/ConfigurationSettings/TheSettingSerializerFactory.cs +++ b/tests/Backend.Fx.Tests/ConfigurationSettings/TheSettingSerializerFactory.cs @@ -1,10 +1,9 @@ -using Xunit; +using System; +using Backend.Fx.ConfigurationSettings; +using Xunit; namespace Backend.Fx.Tests.ConfigurationSettings { - using System; - using Fx.ConfigurationSettings; - public class TheSettingSerializerFactory { private readonly SettingSerializerFactory _sut = new SettingSerializerFactory(); @@ -17,10 +16,10 @@ public void ProvidesBooleanSerializerForNullableBool() } [Fact] - public void ProvidesBooleanSerializerForNullableInt() + public void ProvidesBooleanSerializerForNullableDateTime() { - var serializer = _sut.GetSerializer(); - Assert.IsType(serializer); + var serializer = _sut.GetSerializer(); + Assert.IsType(serializer); } [Fact] @@ -31,10 +30,10 @@ public void ProvidesBooleanSerializerForNullableDouble() } [Fact] - public void ProvidesBooleanSerializerForNullableDateTime() + public void ProvidesBooleanSerializerForNullableInt() { - var serializer = _sut.GetSerializer(); - Assert.IsType(serializer); + var serializer = _sut.GetSerializer(); + Assert.IsType(serializer); } [Fact] @@ -47,13 +46,13 @@ public void ProvidesBooleanSerializerForString() [Fact] public void ProvidesNoSerializerForBool() { - Assert.Throws(()=>_sut.GetSerializer()); + Assert.Throws(() => _sut.GetSerializer()); } [Fact] - public void ProvidesNoSerializerForInt() + public void ProvidesNoSerializerForDateTime() { - Assert.Throws(() => _sut.GetSerializer()); + Assert.Throws(() => _sut.GetSerializer()); } [Fact] @@ -63,9 +62,9 @@ public void ProvidesNoSerializerForDouble() } [Fact] - public void ProvidesNoSerializerForDateTime() + public void ProvidesNoSerializerForInt() { - Assert.Throws(() => _sut.GetSerializer()); + Assert.Throws(() => _sut.GetSerializer()); } } -} +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/ConfigurationSettings/TheSettingsService.cs b/tests/Backend.Fx.Tests/ConfigurationSettings/TheSettingsService.cs index 0f318459..bba31cea 100644 --- a/tests/Backend.Fx.Tests/ConfigurationSettings/TheSettingsService.cs +++ b/tests/Backend.Fx.Tests/ConfigurationSettings/TheSettingsService.cs @@ -13,12 +13,26 @@ namespace Backend.Fx.Tests.ConfigurationSettings { public class TheSettingsService { + public TheSettingsService() + { + var settingAuthorization = A.Fake>(); + A.CallTo(() => settingAuthorization.HasAccessExpression).Returns(setting => true); + A.CallTo(() => settingAuthorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); + A.CallTo(() => settingAuthorization.CanCreate(A._)).Returns(true); + + _idGenerator = A.Fake(); + var nextId = 1; + A.CallTo(() => _idGenerator.NextId()).ReturnsLazily(() => nextId++); + _settingRepository = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(999), settingAuthorization); + } + public class MySettingsService : SettingsService { - public MySettingsService(IEntityIdGenerator idGenerator, IRepository repo) + public MySettingsService(IEntityIdGenerator idGenerator, IRepository repo) : base("My", idGenerator, repo, new SettingSerializerFactory()) { } + public int SmtpPort { get => ReadSetting(nameof(SmtpPort)) ?? 25; @@ -35,29 +49,23 @@ public string SmtpHost private readonly InMemoryRepository _settingRepository; private readonly IEntityIdGenerator _idGenerator; - public TheSettingsService() + [Fact] + public void ReadsNonExistingSettingAsDefaultFromRepository() { - var settingAuthorization = A.Fake>(); - A.CallTo(() => settingAuthorization.HasAccessExpression).Returns(setting => true); - A.CallTo(() => settingAuthorization.Filter(A>._)).ReturnsLazily((IQueryable q) => q); - A.CallTo(() => settingAuthorization.CanCreate(A._)).Returns(true); - - _idGenerator = A.Fake(); - int nextId=1; - A.CallTo(() => _idGenerator.NextId()).ReturnsLazily(() => nextId++); - _settingRepository = new InMemoryRepository(new InMemoryStore(), CurrentTenantIdHolder.Create(999), settingAuthorization); + var sut = new MySettingsService(_idGenerator, _settingRepository); + Assert.Null(sut.SmtpHost); } [Fact] - public void StoresSettingsInRepository() + public void ReadsNullSettingFromRepository() { - MySettingsService sut = new MySettingsService(_idGenerator, _settingRepository) {SmtpPort = 333}; - Assert.Equal(333, sut.SmtpPort); + var setting = new Setting(3, "My.SmtpHost"); + setting.SetPrivate(set => set.SerializedValue, null); - Setting[] settings = _settingRepository.GetAll(); - Assert.Single(settings); - Assert.Equal("333", settings[0].SerializedValue); - Assert.Equal("My.SmtpPort", settings[0].Key); + _settingRepository.Add(setting); + + var sut = new MySettingsService(_idGenerator, _settingRepository); + Assert.Null(sut.SmtpHost); } [Fact] @@ -68,27 +76,20 @@ public void ReadsSettingFromRepository() _settingRepository.Add(setting); - MySettingsService sut = new MySettingsService(_idGenerator, _settingRepository); + var sut = new MySettingsService(_idGenerator, _settingRepository); Assert.Equal(333, sut.SmtpPort); } [Fact] - public void ReadsNullSettingFromRepository() + public void StoresSettingsInRepository() { - var setting = new Setting(3,"My.SmtpHost"); - setting.SetPrivate(set => set.SerializedValue, null); - - _settingRepository.Add(setting); - - MySettingsService sut = new MySettingsService(_idGenerator, _settingRepository); - Assert.Null(sut.SmtpHost); - } + var sut = new MySettingsService(_idGenerator, _settingRepository) {SmtpPort = 333}; + Assert.Equal(333, sut.SmtpPort); - [Fact] - public void ReadsNonExistingSettingAsDefaultFromRepository() - { - MySettingsService sut = new MySettingsService(_idGenerator, _settingRepository); - Assert.Null(sut.SmtpHost); + var settings = _settingRepository.GetAll(); + Assert.Single(settings); + Assert.Equal("333", settings[0].SerializedValue); + Assert.Equal("My.SmtpPort", settings[0].Key); } } -} +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Environment/Authentication/TheAnonymousIdentity.cs b/tests/Backend.Fx.Tests/Environment/Authentication/TheAnonymousIdentity.cs index f0efd678..b57810a4 100644 --- a/tests/Backend.Fx.Tests/Environment/Authentication/TheAnonymousIdentity.cs +++ b/tests/Backend.Fx.Tests/Environment/Authentication/TheAnonymousIdentity.cs @@ -1,17 +1,10 @@ -using Xunit; +using Backend.Fx.Environment.Authentication; +using Xunit; namespace Backend.Fx.Tests.Environment.Authentication { - using Fx.Environment.Authentication; - public class TheAnonymousIdentity { - [Fact] - public void IsNotAuthenticated() - { - Assert.False(new AnonymousIdentity().IsAuthenticated); - } - [Fact] public void HasNameAnonymous() { @@ -23,5 +16,11 @@ public void HasNoAuthenticationType() { Assert.Equal(string.Empty, new AnonymousIdentity().AuthenticationType); } + + [Fact] + public void IsNotAuthenticated() + { + Assert.False(new AnonymousIdentity().IsAuthenticated); + } } } \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Environment/Authentication/TheCurrentIdentityHolder.cs b/tests/Backend.Fx.Tests/Environment/Authentication/TheCurrentIdentityHolder.cs index c05b1c53..4f5b5b44 100644 --- a/tests/Backend.Fx.Tests/Environment/Authentication/TheCurrentIdentityHolder.cs +++ b/tests/Backend.Fx.Tests/Environment/Authentication/TheCurrentIdentityHolder.cs @@ -1,32 +1,31 @@ -using Xunit; +using Backend.Fx.Environment.Authentication; +using Xunit; namespace Backend.Fx.Tests.Environment.Authentication { - using Fx.Environment.Authentication; - public class TheCurrentIdentityHolder { [Fact] - public void InitializesWithAnonymousIdentity() + public void FallsBackToInitialValueWhenReplacingWithNull() { var currentIdentityHolder = new CurrentIdentityHolder(); + currentIdentityHolder.ReplaceCurrent(null); Assert.Equal("ANONYMOUS", currentIdentityHolder.Current.Name); } [Fact] - public void ReplacesCurrentIdentity() + public void InitializesWithAnonymousIdentity() { var currentIdentityHolder = new CurrentIdentityHolder(); - currentIdentityHolder.ReplaceCurrent(new SystemIdentity()); - Assert.Equal("SYSTEM", currentIdentityHolder.Current.Name); + Assert.Equal("ANONYMOUS", currentIdentityHolder.Current.Name); } [Fact] - public void FallsBackToInitialValueWhenReplacingWithNull() + public void ReplacesCurrentIdentity() { var currentIdentityHolder = new CurrentIdentityHolder(); - currentIdentityHolder.ReplaceCurrent(null); - Assert.Equal("ANONYMOUS", currentIdentityHolder.Current.Name); + currentIdentityHolder.ReplaceCurrent(new SystemIdentity()); + Assert.Equal("SYSTEM", currentIdentityHolder.Current.Name); } } -} +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Environment/Authentication/TheSystemIdentity.cs b/tests/Backend.Fx.Tests/Environment/Authentication/TheSystemIdentity.cs index d9e915ac..493f47a4 100644 --- a/tests/Backend.Fx.Tests/Environment/Authentication/TheSystemIdentity.cs +++ b/tests/Backend.Fx.Tests/Environment/Authentication/TheSystemIdentity.cs @@ -1,15 +1,14 @@ -using Xunit; +using Backend.Fx.Environment.Authentication; +using Xunit; namespace Backend.Fx.Tests.Environment.Authentication { - using Fx.Environment.Authentication; - public class TheSystemIdentity { [Fact] - public void IsAuthenticated() + public void HasAuthenticationTypeSystemInternal() { - Assert.True(new SystemIdentity().IsAuthenticated); + Assert.Equal("system internal", new SystemIdentity().AuthenticationType); } [Fact] @@ -19,9 +18,9 @@ public void HasNameSystem() } [Fact] - public void HasAuthenticationTypeSystemInternal() + public void IsAuthenticated() { - Assert.Equal("system internal", new SystemIdentity().AuthenticationType); + Assert.True(new SystemIdentity().IsAuthenticated); } } -} +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Environment/DateAndTime/TheClock.cs b/tests/Backend.Fx.Tests/Environment/DateAndTime/TheAdjustableClock.cs similarity index 70% rename from tests/Backend.Fx.Tests/Environment/DateAndTime/TheClock.cs rename to tests/Backend.Fx.Tests/Environment/DateAndTime/TheAdjustableClock.cs index fb3b8f2f..f18eea00 100644 --- a/tests/Backend.Fx.Tests/Environment/DateAndTime/TheClock.cs +++ b/tests/Backend.Fx.Tests/Environment/DateAndTime/TheAdjustableClock.cs @@ -1,20 +1,18 @@ +using System; +using System.Threading; +using Backend.Fx.Environment.DateAndTime; using Xunit; namespace Backend.Fx.Tests.Environment.DateAndTime { - using System; - using System.Threading; - using Fx.Environment.DateAndTime; - - public class TheClock + public class TheAdjustableClock { [Fact] public void AllowsOverridingOfUtcNow() { var overriddenUtcNow = new DateTime(2000, 1, 1, 12, 0, 0); - Clock sut = new WallClock(); + var sut = new AdjustableClock(new WallClock()); sut.OverrideUtcNow(overriddenUtcNow); - Assert.Equal(overriddenUtcNow, sut.UtcNow); Thread.Sleep(100); Assert.Equal(overriddenUtcNow, sut.UtcNow); diff --git a/tests/Backend.Fx.Tests/Environment/DateAndTime/TheFrozenClock.cs b/tests/Backend.Fx.Tests/Environment/DateAndTime/TheFrozenClock.cs index 517fe38c..16ad6745 100644 --- a/tests/Backend.Fx.Tests/Environment/DateAndTime/TheFrozenClock.cs +++ b/tests/Backend.Fx.Tests/Environment/DateAndTime/TheFrozenClock.cs @@ -1,19 +1,18 @@ -using Xunit; +using System; +using System.Threading; +using Backend.Fx.Environment.DateAndTime; +using Xunit; namespace Backend.Fx.Tests.Environment.DateAndTime { - using System; - using System.Threading; - using Fx.Environment.DateAndTime; - public class TheFrozenClock { [Fact] public void IsFrozen() { - DateTime systemUtcNow = DateTime.UtcNow; - IClock sut = FrozenClock.WithFrozenUtcNow(systemUtcNow); - Assert.Equal(systemUtcNow, sut.UtcNow); + + IClock sut = new FrozenClock(new WallClock()); + DateTime systemUtcNow = sut.UtcNow; Thread.Sleep(100); Assert.Equal(systemUtcNow, sut.UtcNow); Assert.NotEqual(DateTime.UtcNow, sut.UtcNow); diff --git a/tests/Backend.Fx.Tests/Environment/DateAndTime/TheWallClock.cs b/tests/Backend.Fx.Tests/Environment/DateAndTime/TheWallClock.cs index 52103a05..9e8b2788 100644 --- a/tests/Backend.Fx.Tests/Environment/DateAndTime/TheWallClock.cs +++ b/tests/Backend.Fx.Tests/Environment/DateAndTime/TheWallClock.cs @@ -15,7 +15,7 @@ public class TheWallClock public void IsTheSystemClock() { IClock sut = new WallClock(); - + Assert.Equal(DateTime.UtcNow, sut.UtcNow, _tolerantDateTimeComparer); Thread.Sleep(100); @@ -23,4 +23,4 @@ public void IsTheSystemClock() Assert.Equal(DateTime.UtcNow, sut.UtcNow, _tolerantDateTimeComparer); } } -} +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheAllTenantBackendFxApplicationInvoker.cs b/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheAllTenantBackendFxApplicationInvoker.cs new file mode 100644 index 00000000..f3773baa --- /dev/null +++ b/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheAllTenantBackendFxApplicationInvoker.cs @@ -0,0 +1,45 @@ +using System; +using System.Linq; +using System.Security.Principal; +using Backend.Fx.Environment.MultiTenancy; +using Backend.Fx.Patterns.DependencyInjection; +using FakeItEasy; +using Xunit; + +namespace Backend.Fx.Tests.Environment.MultiTenancy +{ + public class TheAllTenantBackendFxApplicationInvoker + { + public TheAllTenantBackendFxApplicationInvoker() + { + _sut = new AllTenantBackendFxApplicationInvoker(_tenantService, _invoker); + } + + private readonly AllTenantBackendFxApplicationInvoker _sut; + private readonly ITenantIdProvider _tenantService = A.Fake(); + private readonly IBackendFxApplicationInvoker _invoker = A.Fake(); + + [Fact] + public void InvokesActionForAllTenants() + { + var demoTenantIds = Enumerable.Range(0, 10).Select(i => new TenantId(i)).ToArray(); + var prodTenantIds = Enumerable.Range(10, 10).Select(i => new TenantId(i)).ToArray(); + A.CallTo(() => _tenantService.GetActiveDemonstrationTenantIds()).Returns(demoTenantIds); + A.CallTo(() => _tenantService.GetActiveProductionTenantIds()).Returns(prodTenantIds); + + _sut.Invoke(_ => { }); + + foreach (TenantId tenantId in demoTenantIds) + { + A.CallTo(() => _invoker.Invoke(A>._, A._, A.That.IsSameAs(tenantId), A._)) + .MustHaveHappenedOnceExactly(); + } + + foreach (TenantId tenantId in prodTenantIds) + { + A.CallTo(() => _invoker.Invoke(A>._, A._, A.That.IsSameAs(tenantId), A._)) + .MustHaveHappenedOnceExactly(); + } + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheCurrentTenantIdHolder.cs b/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheCurrentTenantIdHolder.cs index 19c90e6f..cca7733b 100644 --- a/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheCurrentTenantIdHolder.cs +++ b/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheCurrentTenantIdHolder.cs @@ -1,9 +1,8 @@ -using Xunit; +using Backend.Fx.Environment.MultiTenancy; +using Xunit; namespace Backend.Fx.Tests.Environment.MultiTenancy { - using Fx.Environment.MultiTenancy; - public class TheCurrentTenantIdHolder { [Fact] @@ -21,4 +20,4 @@ public void ReplacesCurrentTenantId() Assert.Equal(345, currentTenantIdHolder.Current.Value); } } -} +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheMultiTenantApplication.cs b/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheMultiTenantApplication.cs new file mode 100644 index 00000000..c0386d96 --- /dev/null +++ b/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheMultiTenantApplication.cs @@ -0,0 +1,52 @@ +using System.Threading; +using Backend.Fx.Environment.MultiTenancy; +using Backend.Fx.Patterns.DependencyInjection; +using Backend.Fx.Patterns.EventAggregation.Integration; +using FakeItEasy; +using Xunit; + +namespace Backend.Fx.Tests.Environment.MultiTenancy +{ + public class TheMultiTenantApplication + { + private readonly IBackendFxApplication _sut; + private readonly IModule _multiTenancyModule = A.Fake(); + private readonly ITenantService _tenantService = A.Fake(); + private readonly IBackendFxApplication _application = A.Fake(); + + public TheMultiTenantApplication() + { + _sut = new MultiTenantApplication(_application); + } + + + [Fact] + public void DelegatesAllCalls() + { + // ReSharper disable once UnusedVariable + IBackendFxApplicationAsyncInvoker ai = _sut.AsyncInvoker; + A.CallTo(() => _application.AsyncInvoker).MustHaveHappenedOnceExactly(); + + // ReSharper disable once UnusedVariable + ICompositionRoot cr = _sut.CompositionRoot; + A.CallTo(() => _application.CompositionRoot).MustHaveHappenedOnceOrMore(); + + // ReSharper disable once UnusedVariable + IBackendFxApplicationInvoker i = _sut.Invoker; + A.CallTo(() => _application.Invoker).MustHaveHappenedOnceOrMore(); + + // ReSharper disable once UnusedVariable + IMessageBus mb = _sut.MessageBus; + A.CallTo(() => _application.MessageBus).MustHaveHappenedOnceExactly(); + + _sut.Boot(); + A.CallTo(() => _application.Boot(A._)).MustHaveHappenedOnceExactly(); + + _sut.Dispose(); + A.CallTo(() => _application.Dispose()).MustHaveHappenedOnceExactly(); + + _sut.WaitForBoot(); + A.CallTo(() => _application.WaitForBoot(A._, A._)).MustHaveHappenedOnceExactly(); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheSingleTenantApplication.cs b/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheSingleTenantApplication.cs new file mode 100644 index 00000000..3c2f6416 --- /dev/null +++ b/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheSingleTenantApplication.cs @@ -0,0 +1,78 @@ +using System.Threading; +using Backend.Fx.Environment.MultiTenancy; +using Backend.Fx.Patterns.DependencyInjection; +using Backend.Fx.Patterns.EventAggregation.Integration; +using FakeItEasy; +using Xunit; + +namespace Backend.Fx.Tests.Environment.MultiTenancy +{ + public class TheSingleTenantApplication + { + private readonly IBackendFxApplication _sut; + private readonly ITenantService _tenantService = A.Fake(); + private readonly TenantCreationParameters _tenantCreationParameters = new TenantCreationParameters("n", "d", false); + private readonly ICompositionRoot _compositionRoot = A.Fake(); + + public TheSingleTenantApplication() + { + var application = A.Fake(); + + A.CallTo(() => application.CompositionRoot).Returns(_compositionRoot); + + _sut = new SingleTenantApplication(_tenantCreationParameters, + _tenantService, + application); + } + + [Fact] + public void CreatesTenantOnBootWhenNotExistent() + { + _sut.Boot(); + A.CallTo(() => _tenantService.CreateTenant(A.That.IsEqualTo(_tenantCreationParameters))).MustHaveHappenedOnceExactly(); + } + + [Fact] + public void CreatesNoTenantOnBootWhenExistent() + { + A.CallTo(() => _tenantService.GetActiveTenantIds()).Returns(new[] {new TenantId(1)}); + _sut.Boot(); + A.CallTo(() => _tenantService.CreateTenant(A.That.IsEqualTo(_tenantCreationParameters))).MustNotHaveHappened(); + } + + [Fact] + public void DelegatesAllCalls() + { + var application = A.Fake(); + var sut = new SingleTenantApplication(_tenantCreationParameters, + A.Fake(), + application); + + Fake.ClearRecordedCalls(application); + // ReSharper disable once UnusedVariable + IBackendFxApplicationAsyncInvoker ai = sut.AsyncInvoker; + A.CallTo(() => application.AsyncInvoker).MustHaveHappenedOnceExactly(); + + // ReSharper disable once UnusedVariable + ICompositionRoot cr = sut.CompositionRoot; + A.CallTo(() => application.CompositionRoot).MustHaveHappenedOnceExactly(); + + // ReSharper disable once UnusedVariable + IBackendFxApplicationInvoker i = sut.Invoker; + A.CallTo(() => application.Invoker).MustHaveHappenedOnceExactly(); + + // ReSharper disable once UnusedVariable + IMessageBus mb = sut.MessageBus; + A.CallTo(() => application.MessageBus).MustHaveHappenedOnceExactly(); + + sut.Boot(); + A.CallTo(() => application.Boot(A._)).MustHaveHappenedOnceExactly(); + + sut.Dispose(); + A.CallTo(() => application.Dispose()).MustHaveHappenedOnceExactly(); + + sut.WaitForBoot(); + A.CallTo(() => application.WaitForBoot(A._, A._)).MustHaveHappenedOnceExactly(); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheTenant.cs b/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheTenant.cs index fcded6b9..ed996687 100644 --- a/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheTenant.cs +++ b/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheTenant.cs @@ -1,28 +1,27 @@ -namespace Backend.Fx.Tests.Environment.MultiTenancy -{ - using System; - using Fx.Environment.MultiTenancy; - using Xunit; +using System; +using Backend.Fx.Environment.MultiTenancy; +using Xunit; +namespace Backend.Fx.Tests.Environment.MultiTenancy +{ public class TheTenant { [Fact] - public void InitializesCorrectly() + public void CannotBeInitializedWithoutName() { - Tenant tenant = new Tenant("name", "description", true, "de-DE"); - Assert.Equal("name", tenant.Name); - Assert.Equal("description", tenant.Description); - Assert.True(tenant.IsDemoTenant); + Assert.Throws(() => new Tenant("", "", false)); + // ReSharper disable once AssignNullToNotNullAttribute - testing null case exception + Assert.Throws(() => new Tenant(null, "", false)); + Assert.Throws(() => new Tenant(" ", "", false)); } [Fact] - public void CannotBeInitializedWithoutName() + public void InitializesCorrectly() { - Assert.Throws(() => new Tenant("", "", false, "en-US")); - // ReSharper disable once AssignNullToNotNullAttribute - testing null case exception - Assert.Throws(() => new Tenant(null, "", false, "es-AR")); - Assert.Throws(() => new Tenant(" ", "", false, "fr-FR")); + var tenant = new Tenant("name", "description", true); + Assert.Equal("name", tenant.Name); + Assert.Equal("description", tenant.Description); + Assert.True(tenant.IsDemoTenant); } } -} - +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheTenantId.cs b/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheTenantId.cs index b737edda..37226de2 100644 --- a/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheTenantId.cs +++ b/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheTenantId.cs @@ -1,10 +1,9 @@ -using Xunit; +using System; +using Backend.Fx.Environment.MultiTenancy; +using Xunit; namespace Backend.Fx.Tests.Environment.MultiTenancy { - using System; - using Fx.Environment.MultiTenancy; - public class TheTenantId { [Fact] @@ -21,4 +20,4 @@ public void ThrowsOnAccessingTheValueWhenInitializedWithNull() Assert.Throws(() => sut.Value); } } -} +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheTenantService.cs b/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheTenantService.cs new file mode 100644 index 00000000..a831cd1f --- /dev/null +++ b/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheTenantService.cs @@ -0,0 +1,73 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Environment.MultiTenancy; +using Backend.Fx.InMemoryPersistence; +using Backend.Fx.Patterns.EventAggregation.Integration; +using FakeItEasy; +using Xunit; + +namespace Backend.Fx.Tests.Environment.MultiTenancy +{ + public class TheTenantService + { + public TheTenantService() + { + _sut = new TenantService(_messageBus, _tenantRepository); + } + + private readonly ITenantService _sut; + private readonly IMessageBus _messageBus = A.Fake(); + private readonly InMemoryTenantRepository _tenantRepository = new InMemoryTenantRepository(); + + [Fact] + public void CannotCreateTenantWithoutName() + { + Assert.Throws(() => _sut.CreateTenant(new TenantCreationParameters("", "d", true))); + Assert.Throws(() => _sut.CreateTenant(new TenantCreationParameters(null, "d", true))); + } + + [Fact] + public void CannotCreateTenantWithSameName() + { + _sut.CreateTenant(new TenantCreationParameters("n", "d", true)); + Assert.Throws(() => _sut.CreateTenant(new TenantCreationParameters("n", "d", true))); + Assert.Throws(() => _sut.CreateTenant(new TenantCreationParameters("n", "d", false))); + Assert.Throws(() => _sut.CreateTenant(new TenantCreationParameters("N", "d", true))); + } + + [Fact] + public void GetsProductiveTenantIds() + { + var tenants = Enumerable.Range(1, 7) + .Select(i => new Tenant("n" + i, "d" + i, i % 2 == 0)) + .ToArray(); + + foreach (Tenant tenant in tenants) + { + tenant.State = TenantState.Active; + _tenantRepository.SaveTenant(tenant); + } + + var tenantIds = tenants.Select(t => new TenantId(t.Id)).ToArray(); + var demoTenantIds = tenants.Where(t => t.IsDemoTenant).Select(t => new TenantId(t.Id)).ToArray(); + var prodTenantIds = tenants.Where(t => !t.IsDemoTenant).Select(t => new TenantId(t.Id)).ToArray(); + + Assert.Equal(tenantIds, _sut.GetActiveTenantIds()); + Assert.Equal(prodTenantIds, _sut.GetActiveProductionTenantIds()); + Assert.Equal(demoTenantIds, _sut.GetActiveDemonstrationTenantIds()); + } + + [Fact] + public void RaisesTenantActivatedEvent() + { + var ev = new ManualResetEvent(false); + A.CallTo(() => _messageBus.Publish(A._)).Invokes(() => ev.Set()); + Task.Run(() => _sut.CreateTenant(new TenantCreationParameters + {Name = "prod", Description = "unit test created", IsDemonstrationTenant = false})); + Assert.True(ev.WaitOne(Debugger.IsAttached ? int.MaxValue : 10000)); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Exceptions/TheNotFOundException.cs b/tests/Backend.Fx.Tests/Exceptions/TheNotFOundException.cs index c65dfeff..cc6fb251 100644 --- a/tests/Backend.Fx.Tests/Exceptions/TheNotFOundException.cs +++ b/tests/Backend.Fx.Tests/Exceptions/TheNotFOundException.cs @@ -1,10 +1,9 @@ -using Xunit; +using Backend.Fx.Exceptions; +using Backend.Fx.Tests.BuildingBlocks; +using Xunit; namespace Backend.Fx.Tests.Exceptions { - using BuildingBlocks; - using Fx.Exceptions; - public class TheNotFoundException { [Fact] @@ -15,4 +14,4 @@ public void FillsNameAndIdProperties() Assert.Equal(4711, exception.Id); } } -} +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Exceptions/TheUnprocessableExceptionBuilder.cs b/tests/Backend.Fx.Tests/Exceptions/TheUnprocessableExceptionBuilder.cs index 05632793..9c9f51b6 100644 --- a/tests/Backend.Fx.Tests/Exceptions/TheUnprocessableExceptionBuilder.cs +++ b/tests/Backend.Fx.Tests/Exceptions/TheUnprocessableExceptionBuilder.cs @@ -1,15 +1,15 @@ -namespace Backend.Fx.Tests.Exceptions -{ - using BuildingBlocks; - using Fx.Exceptions; - using Xunit; +using Backend.Fx.Exceptions; +using Backend.Fx.Tests.BuildingBlocks; +using Xunit; +namespace Backend.Fx.Tests.Exceptions +{ public class TheUnprocessableExceptionBuilder { [Fact] public void AddsExceptionWhenAggregateIsNull() { - var sut = UnprocessableException.UseBuilder(); + IExceptionBuilder sut = UnprocessableException.UseBuilder(); sut.AddNotFoundWhenNull(1111, null); Assert.Throws(() => sut.Dispose()); } @@ -17,33 +17,33 @@ public void AddsExceptionWhenAggregateIsNull() [Fact] public void AddsNoExceptionWhenAggregateIsNotNull() { - var sut = UnprocessableException.UseBuilder(); + IExceptionBuilder sut = UnprocessableException.UseBuilder(); sut.AddNotFoundWhenNull(1111, new TheAggregateRoot.TestAggregateRoot(12345, "gaga")); sut.Dispose(); } [Fact] - public void ThrowsExceptionWhenAddingError() + public void DoesNotThrowExceptionWhenNotAddingConditionalError() { - var sut = UnprocessableException.UseBuilder(); - sut.Add("something is broken"); - Assert.Throws(() => sut.Dispose()); + IExceptionBuilder sut = UnprocessableException.UseBuilder(); + sut.AddIf(false, "something is broken"); + sut.Dispose(); } [Fact] public void ThrowsExceptionWhenAddingConditionalError() { - var sut = UnprocessableException.UseBuilder(); + IExceptionBuilder sut = UnprocessableException.UseBuilder(); sut.AddIf(true, "something is broken"); Assert.Throws(() => sut.Dispose()); } [Fact] - public void DoesNotThrowExceptionWhenNotAddingConditionalError() + public void ThrowsExceptionWhenAddingError() { - var sut = UnprocessableException.UseBuilder(); - sut.AddIf(false, "something is broken"); - sut.Dispose(); + IExceptionBuilder sut = UnprocessableException.UseBuilder(); + sut.Add("something is broken"); + Assert.Throws(() => sut.Dispose()); } } -} +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Extensions/TheDateTimeEx.cs b/tests/Backend.Fx.Tests/Extensions/TheDateTimeEx.cs index 33d97626..8bd3ad7f 100644 --- a/tests/Backend.Fx.Tests/Extensions/TheDateTimeEx.cs +++ b/tests/Backend.Fx.Tests/Extensions/TheDateTimeEx.cs @@ -1,10 +1,9 @@ -using Xunit; +using System; +using Backend.Fx.Extensions; +using Xunit; namespace Backend.Fx.Tests.Extensions { - using System; - using Fx.Extensions; - public class TheDateTimeEx { [Fact] @@ -13,7 +12,15 @@ public void CanConvertToUnixEpochDate() Assert.Equal(0L, new DateTime(1970, 1, 1).ToUnixEpochDate()); Assert.Equal(1495675672L, new DateTime(2017, 5, 25, 1, 27, 52).ToUnixEpochDate()); Assert.Equal(int.MaxValue, new DateTime(2038, 1, 19, 3, 14, 7).ToUnixEpochDate()); - Assert.Equal((long)int.MaxValue + 1, new DateTime(2038, 1, 19, 3, 14, 8).ToUnixEpochDate()); + Assert.Equal((long) int.MaxValue + 1, new DateTime(2038, 1, 19, 3, 14, 8).ToUnixEpochDate()); + } + + [Fact] + public void CanGetStartOfWeek() + { + var dt = new DateTime(2017, 05, 24, 1, 2, 3); + Assert.Equal(new DateTime(2017, 05, 22), dt.StartOfWeek()); + Assert.Equal(new DateTime(2017, 05, 21), dt.StartOfWeek(DayOfWeek.Sunday)); } [Fact] @@ -28,13 +35,5 @@ public void CanGetWeekDay() Assert.Equal(new DateTime(2017, 05, 27), dt.GetWeekDay(DayOfWeek.Saturday)); Assert.Equal(new DateTime(2017, 05, 28), dt.GetWeekDay(DayOfWeek.Sunday)); } - - [Fact] - public void CanGetStartOfWeek() - { - var dt = new DateTime(2017, 05, 24, 1, 2, 3); - Assert.Equal(new DateTime(2017, 05, 22), dt.StartOfWeek()); - Assert.Equal(new DateTime(2017, 05, 21), dt.StartOfWeek(DayOfWeek.Sunday)); - } } -} +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Extensions/TheEnumerableEx.cs b/tests/Backend.Fx.Tests/Extensions/TheEnumerableEx.cs index 036878d1..2cd33b54 100644 --- a/tests/Backend.Fx.Tests/Extensions/TheEnumerableEx.cs +++ b/tests/Backend.Fx.Tests/Extensions/TheEnumerableEx.cs @@ -1,11 +1,10 @@ -using Xunit; +using System.Collections.Generic; +using System.Linq; +using Backend.Fx.Extensions; +using Xunit; namespace Backend.Fx.Tests.Extensions { - using System.Collections.Generic; - using System.Linq; - using Fx.Extensions; - public class TheEnumerableEx { private class Item @@ -22,4 +21,4 @@ public void ExecutesActionForAll() Assert.All(enumerable, itm => Assert.True(itm.Touched)); } } -} +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Extensions/TheStringEnumUtil.cs b/tests/Backend.Fx.Tests/Extensions/TheStringEnumUtil.cs index 27e1a5dc..d4e83889 100644 --- a/tests/Backend.Fx.Tests/Extensions/TheStringEnumUtil.cs +++ b/tests/Backend.Fx.Tests/Extensions/TheStringEnumUtil.cs @@ -36,4 +36,4 @@ public void ThrowsOnInvalidString() Assert.Throws(() => "whatever".Parse()); } } -} +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Logging/TheLogManager.cs b/tests/Backend.Fx.Tests/Logging/TheLogManager.cs index d4e31b3a..ba1dcb02 100644 --- a/tests/Backend.Fx.Tests/Logging/TheLogManager.cs +++ b/tests/Backend.Fx.Tests/Logging/TheLogManager.cs @@ -1,31 +1,28 @@ -using Xunit; +using System; +using System.Security; +using Backend.Fx.Logging; +using FakeItEasy; +using Xunit; namespace Backend.Fx.Tests.Logging { - using System; - using System.Security; - using FakeItEasy; - using Fx.Logging; - public class TheLogManager { - private readonly ILoggerFactory _loggerFactory; - public TheLogManager() { - _loggerFactory = A.Fake(); + var loggerFactory = A.Fake(); var logger = A.Fake(); - A.CallTo(() => _loggerFactory.Create(A.Ignored)).Returns(logger); + A.CallTo(() => loggerFactory.Create()).Returns(logger); } [Fact] public void DoesNotThrowOnZeroConfig() { Exception ex = new SecurityException("very bad"); - string msg = "the log message"; + var msg = "the log message"; ILogger logger = LogManager.Create(); - + logger.Fatal(ex); logger.Fatal(ex, msg); logger.Fatal(msg); @@ -49,14 +46,5 @@ public void DoesNotThrowOnZeroConfig() logger.DebugDuration(msg).Dispose(); logger.InfoDuration(msg).Dispose(); } - - [Fact] - public void TakesTypeFullNameAsLoggerName() - { - LogManager.Initialize(_loggerFactory); - LogManager.Create(); - A.CallTo(() => _loggerFactory.Create(A.That.Matches(s => s == "Backend.Fx.Tests.Logging.TheLogManager"))) - .MustHaveHappenedOnceExactly(); - } } -} +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Logging/TheLogger.cs b/tests/Backend.Fx.Tests/Logging/TheLogger.cs index 3727c281..4a5f2c72 100644 --- a/tests/Backend.Fx.Tests/Logging/TheLogger.cs +++ b/tests/Backend.Fx.Tests/Logging/TheLogger.cs @@ -1,52 +1,62 @@ +using System; +using Backend.Fx.Logging; +using FakeItEasy; using Xunit; namespace Backend.Fx.Tests.Logging { - using System; - using FakeItEasy; - using Fx.Logging; - public class TheLogger { - private readonly ILoggerFactory _loggerFactory; - private readonly InvalidOperationException _exception = new InvalidOperationException("whatever"); - public TheLogger() { _loggerFactory = A.Fake(); - ILogger logger = A.Fake(); + var logger = A.Fake(); - A.CallTo(() => _loggerFactory.Create(A.Ignored)).Returns(logger); + A.CallTo(() => _loggerFactory.Create()).Returns(logger); A.CallTo(() => logger.DebugDuration(A.Ignored)).ReturnsLazily((string activity) => new DurationLogger(s => logger.Debug(s), activity)); A.CallTo(() => logger.InfoDuration(A.Ignored)).ReturnsLazily((string activity) => new DurationLogger(s => logger.Info(s), activity)); A.CallTo(() => logger.TraceDuration(A.Ignored)).ReturnsLazily((string activity) => new DurationLogger(s => logger.Trace(s), activity)); } + private readonly ILoggerFactory _loggerFactory; + private readonly InvalidOperationException _exception = new InvalidOperationException("whatever"); + [Fact] - public void CanLogFatal() + public void CanLogDebug() { - string msg = "the log message"; - var logger = _loggerFactory.Create("aaa"); - logger.Fatal(msg); + var msg = "the log message"; + ILogger logger = _loggerFactory.Create(); + logger.Debug(msg); - A.CallTo(() => logger.Fatal(A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); + A.CallTo(() => logger.Debug(A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); } [Fact] - public void CanLogFatalWithException() + public void CanLogDebugDuration() { - string msg = "the log message"; - var logger = _loggerFactory.Create("aaa"); - logger.Fatal(_exception, msg); + var msg = "the log message"; + ILogger logger = _loggerFactory.Create(); + logger.DebugDuration(msg).Dispose(); - A.CallTo(() => logger.Fatal(A.That.Matches(ex => ex == _exception), A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); + A.CallTo(() => logger.Debug(A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); + A.CallTo(() => logger.Debug(A.That.Matches(s => s.StartsWith(msg + " - Duration:")))).MustHaveHappenedOnceExactly(); + } + + [Fact] + public void CanLogDebugWithException() + { + var msg = "the log message"; + ILogger logger = _loggerFactory.Create(); + logger.Debug(_exception, msg); + + A.CallTo(() => logger.Debug(A.That.Matches(ex => ex == _exception), A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); } [Fact] public void CanLogError() { - string msg = "the log message"; - var logger = _loggerFactory.Create("aaa"); + var msg = "the log message"; + ILogger logger = _loggerFactory.Create(); logger.Error(msg); A.CallTo(() => logger.Error(A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); @@ -55,58 +65,48 @@ public void CanLogError() [Fact] public void CanLogErrorWithException() { - string msg = "the log message"; - var logger = _loggerFactory.Create("aaa"); + var msg = "the log message"; + ILogger logger = _loggerFactory.Create(); logger.Error(_exception, msg); A.CallTo(() => logger.Error(A.That.Matches(ex => ex == _exception), A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); } [Fact] - public void CanLogWarning() + public void CanLogFatal() { - string msg = "the log message"; - var logger = _loggerFactory.Create("aaa"); - logger.Warn(msg); + var msg = "the log message"; + ILogger logger = _loggerFactory.Create(); + logger.Fatal(msg); - A.CallTo(() => logger.Warn(A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); + A.CallTo(() => logger.Fatal(A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); } [Fact] - public void CanLogWarnWithException() + public void CanLogFatalWithException() { - string msg = "the log message"; - var logger = _loggerFactory.Create("aaa"); - logger.Warn(_exception, msg); + var msg = "the log message"; + ILogger logger = _loggerFactory.Create(); + logger.Fatal(_exception, msg); - A.CallTo(() => logger.Warn(A.That.Matches(ex => ex == _exception), A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); + A.CallTo(() => logger.Fatal(A.That.Matches(ex => ex == _exception), A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); } [Fact] public void CanLogInfo() { - string msg = "the log message"; - var logger = _loggerFactory.Create("aaa"); + var msg = "the log message"; + ILogger logger = _loggerFactory.Create(); logger.Info(msg); A.CallTo(() => logger.Info(A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); } - [Fact] - public void CanLogInfoWithException() - { - string msg = "the log message"; - var logger = _loggerFactory.Create("aaa"); - logger.Info(_exception, msg); - - A.CallTo(() => logger.Info(A.That.Matches(ex => ex == _exception), A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); - } - [Fact] public void CanLogInfoDuration() { - string msg = "the log message"; - var logger = _loggerFactory.Create("aaa"); + var msg = "the log message"; + ILogger logger = _loggerFactory.Create(); logger.InfoDuration(msg).Dispose(); A.CallTo(() => logger.Info(A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); @@ -114,65 +114,64 @@ public void CanLogInfoDuration() } [Fact] - public void CanLogDebug() + public void CanLogInfoWithException() { - string msg = "the log message"; - var logger = _loggerFactory.Create("aaa"); - logger.Debug(msg); + var msg = "the log message"; + ILogger logger = _loggerFactory.Create(); + logger.Info(_exception, msg); - A.CallTo(() => logger.Debug(A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); + A.CallTo(() => logger.Info(A.That.Matches(ex => ex == _exception), A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); } [Fact] - public void CanLogDebugWithException() + public void CanLogTrace() { - string msg = "the log message"; - var logger = _loggerFactory.Create("aaa"); - logger.Debug(_exception, msg); + var msg = "the log message"; + ILogger logger = _loggerFactory.Create(); + logger.Trace(msg); - A.CallTo(() => logger.Debug(A.That.Matches(ex => ex == _exception), A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); + A.CallTo(() => logger.Trace(A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); } [Fact] - public void CanLogDebugDuration() + public void CanLogTraceDuration() { - string msg = "the log message"; - var logger = _loggerFactory.Create("aaa"); - logger.DebugDuration(msg).Dispose(); + var msg = "the log message"; + ILogger logger = _loggerFactory.Create(); + logger.TraceDuration(msg).Dispose(); - A.CallTo(() => logger.Debug(A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); - A.CallTo(() => logger.Debug(A.That.Matches(s => s.StartsWith(msg+" - Duration:")))).MustHaveHappenedOnceExactly(); + A.CallTo(() => logger.Trace(A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); + A.CallTo(() => logger.Trace(A.That.Matches(s => s.StartsWith(msg + " - Duration:")))).MustHaveHappenedOnceExactly(); } [Fact] - public void CanLogTrace() + public void CanLogTraceWithException() { - string msg = "the log message"; - var logger = _loggerFactory.Create("aaa"); - logger.Trace(msg); + var msg = "the log message"; + ILogger logger = _loggerFactory.Create(); + logger.Trace(_exception, msg); - A.CallTo(() => logger.Trace(A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); + A.CallTo(() => logger.Trace(A.That.Matches(ex => ex == _exception), A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); } [Fact] - public void CanLogTraceWithException() + public void CanLogWarning() { - string msg = "the log message"; - var logger = _loggerFactory.Create("aaa"); - logger.Trace(_exception, msg); + var msg = "the log message"; + ILogger logger = _loggerFactory.Create(); + logger.Warn(msg); - A.CallTo(() => logger.Trace(A.That.Matches(ex => ex == _exception), A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); + A.CallTo(() => logger.Warn(A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); } [Fact] - public void CanLogTraceDuration() + public void CanLogWarnWithException() { - string msg = "the log message"; - var logger = _loggerFactory.Create("aaa"); - logger.TraceDuration(msg).Dispose(); + var msg = "the log message"; + ILogger logger = _loggerFactory.Create(); + logger.Warn(_exception, msg); - A.CallTo(() => logger.Trace(A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); - A.CallTo(() => logger.Trace(A.That.Matches(s => s.StartsWith(msg + " - Duration:")))).MustHaveHappenedOnceExactly(); + A.CallTo(() => logger.Warn(A.That.Matches(ex => ex == _exception), A.That.Matches(s => s == msg))).MustHaveHappenedOnceExactly(); } } } \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/Authorization/TheAllowAllImplementation.cs b/tests/Backend.Fx.Tests/Patterns/Authorization/TheAllowAllImplementation.cs new file mode 100644 index 00000000..da07574a --- /dev/null +++ b/tests/Backend.Fx.Tests/Patterns/Authorization/TheAllowAllImplementation.cs @@ -0,0 +1,38 @@ +using System.Linq; +using Backend.Fx.Patterns.Authorization; +using Backend.Fx.Tests.BuildingBlocks; +using Xunit; + +namespace Backend.Fx.Tests.Patterns.Authorization +{ + public class TheAllowAllImplementation + { + private readonly IAggregateAuthorization _sut = new AllowAll(); + private readonly TheAggregateRoot.TestAggregateRoot _testAggregateRoot = new TheAggregateRoot.TestAggregateRoot(1,"e"); + + [Fact] + public void AllowsAccess() + { + Assert.True(_sut.HasAccessExpression.Compile().Invoke(_testAggregateRoot)); + Assert.Contains(_testAggregateRoot, _sut.Filter(new[] {_testAggregateRoot}.AsQueryable())); + } + + [Fact] + public void AllowsCreation() + { + Assert.True(_sut.CanCreate(_testAggregateRoot)); + } + + [Fact] + public void AllowsModification() + { + Assert.True(_sut.CanModify(_testAggregateRoot)); + } + + [Fact] + public void AllowsDeletion() + { + Assert.True(_sut.CanDelete(_testAggregateRoot)); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/Authorization/TheDenyAllImplementation.cs b/tests/Backend.Fx.Tests/Patterns/Authorization/TheDenyAllImplementation.cs new file mode 100644 index 00000000..6807eac3 --- /dev/null +++ b/tests/Backend.Fx.Tests/Patterns/Authorization/TheDenyAllImplementation.cs @@ -0,0 +1,39 @@ +using System.Linq; +using Backend.Fx.Patterns.Authorization; +using Backend.Fx.Tests.BuildingBlocks; +using Xunit; + +namespace Backend.Fx.Tests.Patterns.Authorization +{ + public class TheDenyAllImplementation + { + private readonly IAggregateAuthorization _sut = new DenyAll(); + private readonly TheAggregateRoot.TestAggregateRoot _testAggregateRoot = new TheAggregateRoot.TestAggregateRoot(1,"e"); + + + [Fact] + public void DeniesAccess() + { + Assert.False(_sut.HasAccessExpression.Compile().Invoke(_testAggregateRoot)); + Assert.Empty(_sut.Filter(new[] {_testAggregateRoot}.AsQueryable())); + } + + [Fact] + public void DeniesCreation() + { + Assert.False(_sut.CanCreate(_testAggregateRoot)); + } + + [Fact] + public void DeniesModification() + { + Assert.False(_sut.CanModify(_testAggregateRoot)); + } + + [Fact] + public void DeniesDeletion() + { + Assert.False(_sut.CanDelete(_testAggregateRoot)); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DataGeneration/TheDataGenerationContext.cs b/tests/Backend.Fx.Tests/Patterns/DataGeneration/TheDataGenerationContext.cs new file mode 100644 index 00000000..84e56ed0 --- /dev/null +++ b/tests/Backend.Fx.Tests/Patterns/DataGeneration/TheDataGenerationContext.cs @@ -0,0 +1,104 @@ +using System.Linq; +using System.Threading; +using Backend.Fx.Environment.MultiTenancy; +using Backend.Fx.Patterns.DataGeneration; +using Backend.Fx.Patterns.DependencyInjection; +using Backend.Fx.Patterns.EventAggregation.Integration; +using Backend.Fx.Tests.Patterns.DependencyInjection; +using FakeItEasy; +using Xunit; + +namespace Backend.Fx.Tests.Patterns.DataGeneration +{ + public class TheDataGenerationContext + { + public TheDataGenerationContext() + { + var fakes = new DiTestFakes(); + A.CallTo(() => fakes.InstanceProvider.GetInstances()).Returns(_demoDataGenerators.Concat(_prodDataGenerators.Cast()).ToArray()); + + var application = A.Fake(); + A.CallTo(() => application.Invoker).Returns(fakes.Invoker); + A.CallTo(() => application.WaitForBoot(A._, A._)).Returns(true); + + var messageBus = new InMemoryMessageBus(); + messageBus.ProvideInvoker(application.Invoker); + + var tenantIdProvider = A.Fake(); + A.CallTo(() => tenantIdProvider.GetActiveDemonstrationTenantIds()).Returns(_demoTenants); + A.CallTo(() => tenantIdProvider.GetActiveProductionTenantIds()).Returns(_prodTenants); + + _sut = new DataGenerationContext(fakes.CompositionRoot, + fakes.Invoker); + } + + private readonly DataGenerationContext _sut; + private readonly IDemoDataGenerator[] _demoDataGenerators = {new DemoDataGenerator1(), new DemoDataGenerator2()}; + private readonly IProductiveDataGenerator[] _prodDataGenerators = {new ProdDataGenerator1()}; + private readonly TenantId[] _demoTenants = {new TenantId(1), new TenantId(2)}; + private readonly TenantId[] _prodTenants = {new TenantId(11), new TenantId(12)}; + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CallsDataGeneratorWhenSeedingForSpecificTenant(bool isDemoTenant) + { + _sut.SeedDataForTenant(new TenantId(123), isDemoTenant); + + foreach (IProductiveDataGenerator dataGenerator in _prodDataGenerators) + A.CallTo(() => ((ProdDataGenerator) dataGenerator).Impl.Generate()).MustHaveHappenedOnceExactly(); + + foreach (IDemoDataGenerator dataGenerator in _demoDataGenerators) + if (isDemoTenant) + A.CallTo(() => ((DemoDataGenerator) dataGenerator).Impl.Generate()).MustHaveHappenedOnceExactly(); + else + A.CallTo(() => ((DemoDataGenerator) dataGenerator).Impl.Generate()).MustNotHaveHappened(); + } + + private abstract class DemoDataGenerator : IDemoDataGenerator + { + public readonly IDemoDataGenerator Impl; + + protected DemoDataGenerator() + { + Impl = A.Fake(o => o.Named(GetType().Name)); + } + + public int Priority => Impl.Priority; + + public void Generate() + { + Impl.Generate(); + } + } + + private class DemoDataGenerator1 : DemoDataGenerator + { + } + + private class DemoDataGenerator2 : DemoDataGenerator + { + } + + private abstract class ProdDataGenerator : IProductiveDataGenerator + { + public readonly IProductiveDataGenerator Impl; + + protected ProdDataGenerator() + { + Impl = A.Fake(o => o.Named(GetType().Name)); + } + + public int Priority => Impl.Priority; + + public void Generate() + { + Impl.Generate(); + } + } + + private class ProdDataGenerator1 : ProdDataGenerator + { + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DataGeneration/TheGenerateDataOnBootDecorator.cs b/tests/Backend.Fx.Tests/Patterns/DataGeneration/TheGenerateDataOnBootDecorator.cs new file mode 100644 index 00000000..c68831b4 --- /dev/null +++ b/tests/Backend.Fx.Tests/Patterns/DataGeneration/TheGenerateDataOnBootDecorator.cs @@ -0,0 +1,88 @@ +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Environment.MultiTenancy; +using Backend.Fx.Hacking; +using Backend.Fx.Patterns.DataGeneration; +using Backend.Fx.Patterns.DependencyInjection; +using Backend.Fx.Patterns.EventAggregation.Integration; +using FakeItEasy; +using Xunit; + +namespace Backend.Fx.Tests.Patterns.DataGeneration +{ + public class TheGenerateDataOnBootDecorator + { + public TheGenerateDataOnBootDecorator() + { + _compositionRoot = A.Fake(); + _dataGenerationModule = A.Fake(); + _dataGenerationContext = A.Fake(); + _tenantIdProvider = A.Fake(); + + var backendFxApplication = A.Fake(); + A.CallTo(() => backendFxApplication.CompositionRoot).Returns(_compositionRoot); + _sut = new GenerateDataOnBoot(_tenantIdProvider, + _dataGenerationModule, backendFxApplication); + + _sut.SetPrivate(f => f.DataGenerationContext, _dataGenerationContext); + } + + private readonly GenerateDataOnBoot _sut; + private readonly IModule _dataGenerationModule; + private readonly IDataGenerationContext _dataGenerationContext; + private readonly ICompositionRoot _compositionRoot; + private readonly ITenantIdProvider _tenantIdProvider; + + [Fact] + public void DelegatesAllOtherCalls() + { + var app = A.Fake(); + IBackendFxApplication sut = new GenerateDataOnBoot(A.Fake(), A.Fake(), app); + + + // ReSharper disable UnusedVariable + IBackendFxApplicationAsyncInvoker ai = sut.AsyncInvoker; + A.CallTo(() => app.AsyncInvoker).MustHaveHappenedOnceExactly(); + + ICompositionRoot cr = sut.CompositionRoot; + A.CallTo(() => app.CompositionRoot).MustHaveHappenedOnceOrMore(); + + sut.Dispose(); + A.CallTo(() => app.Dispose()).MustHaveHappenedOnceExactly(); + + IBackendFxApplicationInvoker i = sut.Invoker; + A.CallTo(() => app.Invoker).MustHaveHappenedOnceOrMore(); + + IMessageBus mb = sut.MessageBus; + A.CallTo(() => app.MessageBus).MustHaveHappenedOnceExactly(); + + var b = sut.WaitForBoot(); + A.CallTo(() => app.WaitForBoot(A._, A._)).MustHaveHappenedOnceExactly(); + // ReSharper restore UnusedVariable + } + + [Fact] + public async Task RegistersDataGenerationModuleOnBoot() + { + await _sut.Boot(); + A.CallTo(() => _compositionRoot.RegisterModules(A.That.Contains(_dataGenerationModule))).MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task RunsDataGeneratorsOnBoot() + { + var tenantId1 = new TenantId(1); + var tenantId2 = new TenantId(2); + var tenantId3 = new TenantId(3); + var tenantId4 = new TenantId(4); + + A.CallTo(() => _tenantIdProvider.GetActiveDemonstrationTenantIds()).Returns(new[] {tenantId1, tenantId2}); + A.CallTo(() => _tenantIdProvider.GetActiveProductionTenantIds()).Returns(new[] {tenantId3, tenantId4}); + await _sut.Boot(); + A.CallTo(() => _dataGenerationContext.SeedDataForTenant(A.That.IsEqualTo(tenantId1), A.That.IsEqualTo(true))).MustHaveHappenedOnceExactly(); + A.CallTo(() => _dataGenerationContext.SeedDataForTenant(A.That.IsEqualTo(tenantId2), A.That.IsEqualTo(true))).MustHaveHappenedOnceExactly(); + A.CallTo(() => _dataGenerationContext.SeedDataForTenant(A.That.IsEqualTo(tenantId3), A.That.IsEqualTo(false))).MustHaveHappenedOnceExactly(); + A.CallTo(() => _dataGenerationContext.SeedDataForTenant(A.That.IsEqualTo(tenantId4), A.That.IsEqualTo(false))).MustHaveHappenedOnceExactly(); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DataGeneration/TheInitialDataGenerator.cs b/tests/Backend.Fx.Tests/Patterns/DataGeneration/TheInitialDataGenerator.cs index 41f75fe7..66a21843 100644 --- a/tests/Backend.Fx.Tests/Patterns/DataGeneration/TheInitialDataGenerator.cs +++ b/tests/Backend.Fx.Tests/Patterns/DataGeneration/TheInitialDataGenerator.cs @@ -1,12 +1,10 @@ -using Xunit; +using Backend.Fx.Patterns.DataGeneration; +using Xunit; namespace Backend.Fx.Tests.Patterns.DataGeneration { - using Fx.Patterns.DataGeneration; - public class ADataGenerator : DataGenerator { - public bool ShouldRunOverride { get; set; } public int GenerateCoreCalled { get; private set; } public int ShouldRunCalled { get; private set; } @@ -54,4 +52,4 @@ public void RespectsPositiveShouldRunMethodResult() Assert.Equal(1, _sut.InitializeCalled); } } -} +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/DITestFakes.cs b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/DITestFakes.cs new file mode 100644 index 00000000..6198d8c1 --- /dev/null +++ b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/DITestFakes.cs @@ -0,0 +1,45 @@ +using System; +using System.Security.Principal; +using Backend.Fx.Environment.MultiTenancy; +using Backend.Fx.Logging; +using Backend.Fx.Patterns.DependencyInjection; +using Backend.Fx.Patterns.EventAggregation.Integration; +using Backend.Fx.RandomData; +using FakeItEasy; + +namespace Backend.Fx.Tests.Patterns.DependencyInjection +{ + public class DiTestFakes + { + private readonly int _sequenceNumber = TestRandom.Next(100000); + + public DiTestFakes() + { + A.CallTo(() => InstanceProvider.GetInstance>()).Returns(CurrentCorrelationHolder); + A.CallTo(() => InstanceProvider.GetInstance>()).Returns(TenantIdHolder); + A.CallTo(() => InstanceProvider.GetInstance>()).Returns(IdentityHolder); + A.CallTo(() => InstanceProvider.GetInstance()).Returns(Operation); + + A.CallTo(() => InjectionScope.SequenceNumber).Returns(_sequenceNumber++); + A.CallTo(() => InjectionScope.InstanceProvider).Returns(InstanceProvider); + + A.CallTo(() => CompositionRoot.BeginScope()).Returns(InjectionScope); + A.CallTo(() => CompositionRoot.InfrastructureModule).Returns(InfrastructureModule); + + A.CallTo(() => Invoker.Invoke(A>._, A._, A._, A._)) + .Invokes((Action a, IIdentity i, TenantId t, Guid? g) => a.Invoke(InstanceProvider)); + } + + public ICurrentTHolder TenantIdHolder { get; } = A.Fake>(); + public ICurrentTHolder IdentityHolder { get; } = A.Fake>(); + public IOperation Operation { get; } = A.Fake(); + public ICompositionRoot CompositionRoot { get; } = A.Fake(); + public CurrentCorrelationHolder CurrentCorrelationHolder { get; } = new CurrentCorrelationHolder(); + public IInjectionScope InjectionScope { get; } = A.Fake(); + public IExceptionLogger ExceptionLogger { get; } = A.Fake(); + public IInstanceProvider InstanceProvider { get; } = A.Fake(); + public IMessageBus MessageBus { get; } = A.Fake(); + public IInfrastructureModule InfrastructureModule { get; } = A.Fake(); + public IBackendFxApplicationInvoker Invoker { get; } = A.Fake(); + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/SimulatedException.cs b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/SimulatedException.cs new file mode 100644 index 00000000..b6949ea3 --- /dev/null +++ b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/SimulatedException.cs @@ -0,0 +1,12 @@ +using System; + +namespace Backend.Fx.Tests.Patterns.DependencyInjection +{ + public class SimulatedException : Exception + { + public SimulatedException() : base("This exception was intentionally thrown by the unit test. If you see this message unexpectedly, probably the exception handling is broken") + { + + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheBackendFxApplication.cs b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheBackendFxApplication.cs new file mode 100644 index 00000000..60c2e916 --- /dev/null +++ b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheBackendFxApplication.cs @@ -0,0 +1,121 @@ +using System; +using System.Diagnostics; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Environment.Authentication; +using Backend.Fx.Environment.MultiTenancy; +using Backend.Fx.Logging; +using Backend.Fx.Patterns.DependencyInjection; +using Backend.Fx.Patterns.EventAggregation.Domain; +using Backend.Fx.Patterns.EventAggregation.Integration; +using FakeItEasy; +using Xunit; + +namespace Backend.Fx.Tests.Patterns.DependencyInjection +{ + public class TheBackendFxApplication + { + public TheBackendFxApplication() + { + _fakes = new DiTestFakes(); + + Func domainEventAggregatorFactory = () => null; + A.CallTo(() => _fakes.InfrastructureModule.RegisterScoped(A>._)) + .Invokes((Func f) => domainEventAggregatorFactory = f); + A.CallTo(() => _fakes.InstanceProvider.GetInstance()).ReturnsLazily(() => domainEventAggregatorFactory.Invoke()); + + Func messageBusScopeFactory = () => null; + A.CallTo(() => _fakes.InfrastructureModule.RegisterScoped(A>._)) + .Invokes((Func f) => messageBusScopeFactory = f); + A.CallTo(() => _fakes.InstanceProvider.GetInstance()).ReturnsLazily(() => messageBusScopeFactory.Invoke()); + + + _sut = new BackendFxApplication(_fakes.CompositionRoot, _fakes.MessageBus, A.Fake()); + } + + private readonly IBackendFxApplication _sut; + private readonly DiTestFakes _fakes; + + + [Fact] + public void CanWaitForBoot() + { + int bootTime = 200; + A.CallTo(() => _fakes.CompositionRoot.Verify()).Invokes(() => Thread.Sleep(bootTime)); + var sw = new Stopwatch(); + + Task.Factory.StartNew(() => _sut.Boot()); + sw.Start(); + Assert.True(_sut.WaitForBoot()); + Assert.True(sw.ElapsedMilliseconds >= bootTime); + } + + [Fact] + public void DisposesCompositionRootOnDispose() + { + _sut.Boot(); + _sut.Dispose(); + A.CallTo(() => _fakes.CompositionRoot.Dispose()).MustHaveHappenedOnceExactly(); + } + + [Fact] + public void ProvidesExceptionLoggingAsyncInvoker() + { + Assert.IsType(_sut.AsyncInvoker); + } + + [Fact] + public void ProvidesDomainEventAggregator() + { + using (IInjectionScope scope = _sut.CompositionRoot.BeginScope()) + { + var domainEventAggregator = scope.InstanceProvider.GetInstance(); + Assert.NotNull(domainEventAggregator); + } + + A.CallTo(() => _fakes.InstanceProvider.GetInstance()).MustHaveHappenedOnceExactly(); + } + + [Fact] + public void ProvidesExceptionLoggingInvoker() + { + Assert.IsType(_sut.Invoker); + } + + [Fact] + public void IntegratesWithMessageBus() + { + A.CallTo(() => _fakes.MessageBus.ProvideInvoker(A._)).MustHaveHappenedOnceExactly(); + } + + [Fact] + public void ProvidesMessageBusScope() + { + using (IInjectionScope scope = _sut.CompositionRoot.BeginScope()) + { + var messageBusScope = scope.InstanceProvider.GetInstance(); + Assert.NotNull(messageBusScope); + } + + A.CallTo(() => _fakes.InstanceProvider.GetInstance()).MustHaveHappenedOnceExactly(); + } + + [Fact] + public void RegistersInfrastructureModule() + { + A.CallTo(() => _fakes.InfrastructureModule.RegisterScoped, CurrentCorrelationHolder>()).MustHaveHappenedOnceExactly(); + A.CallTo(() => _fakes.InfrastructureModule.RegisterScoped(A>._)).MustHaveHappenedOnceExactly(); + A.CallTo(() => _fakes.InfrastructureModule.RegisterScoped,CurrentIdentityHolder>()).MustHaveHappenedOnceExactly(); + A.CallTo(() => _fakes.InfrastructureModule.RegisterScoped(A>._)).MustHaveHappenedOnceExactly(); + A.CallTo(() => _fakes.InfrastructureModule.RegisterScoped,CurrentTenantIdHolder>()).MustHaveHappenedOnceExactly(); + } + + [Fact] + public void VerifiesCompositionRootOnBoot() + { + _sut.Boot(); + A.CallTo(() => _fakes.CompositionRoot.Verify()).MustHaveHappenedOnceExactly(); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheBackendFxApplicationAsyncInvoker.cs b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheBackendFxApplicationAsyncInvoker.cs new file mode 100644 index 00000000..1252a3ca --- /dev/null +++ b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheBackendFxApplicationAsyncInvoker.cs @@ -0,0 +1,101 @@ +using System; +using System.Reflection; +using System.Security.Principal; +using System.Threading.Tasks; +using Backend.Fx.Environment.Authentication; +using Backend.Fx.Environment.MultiTenancy; +using Backend.Fx.Patterns.DependencyInjection; +using FakeItEasy; +using Xunit; + +namespace Backend.Fx.Tests.Patterns.DependencyInjection +{ + public class TheBackendFxApplicationAsyncInvoker + { + public TheBackendFxApplicationAsyncInvoker() + { + _fakes = new DiTestFakes(); + _sut = new BackendFxApplicationInvoker(_fakes.CompositionRoot); + } + + private readonly IBackendFxApplicationAsyncInvoker _sut; + private readonly DiTestFakes _fakes; + + [Fact] + public async Task BeginsNewScopeForEveryInvocation() + { + await _sut.InvokeAsync(ip => Task.CompletedTask, new AnonymousIdentity(), new TenantId(111)); + A.CallTo(() => _fakes.CompositionRoot.BeginScope()).MustHaveHappenedOnceExactly(); + A.CallTo(() => _fakes.InjectionScope.Dispose()).MustHaveHappenedOnceExactly(); + + await _sut.InvokeAsync(ip => Task.CompletedTask, new AnonymousIdentity(), new TenantId(111)); + A.CallTo(() => _fakes.CompositionRoot.BeginScope()).MustHaveHappenedTwiceExactly(); + A.CallTo(() => _fakes.InjectionScope.Dispose()).MustHaveHappenedTwiceExactly(); + } + + [Fact] + public async Task BeginsNewOperationForEveryInvocation() + { + await _sut.InvokeAsync(ip => Task.CompletedTask, new AnonymousIdentity(), new TenantId(111)); + A.CallTo(() => _fakes.Operation.Begin()).MustHaveHappenedOnceExactly(); + A.CallTo(() => _fakes.Operation.Complete()).MustHaveHappenedOnceExactly(); + + await _sut.InvokeAsync(ip => Task.CompletedTask, new AnonymousIdentity(), new TenantId(111)); + A.CallTo(() => _fakes.Operation.Begin()).MustHaveHappenedTwiceExactly(); + A.CallTo(() => _fakes.Operation.Complete()).MustHaveHappenedTwiceExactly(); + } + + [Fact] + public async Task DoesNotCatchFrameworkExceptions() + { + A.CallTo(() => _fakes.CompositionRoot.BeginScope()).Throws(); + await Assert.ThrowsAsync(async () => await _sut.InvokeAsync(ip => Task.CompletedTask, new AnonymousIdentity(), new TenantId(111))); + A.CallTo(() => _fakes.Operation.Begin()).MustNotHaveHappened(); + A.CallTo(() => _fakes.Operation.Complete()).MustNotHaveHappened(); + } + + [Fact] + public async Task DoesNotCatchOperationExceptions() + { + await Assert.ThrowsAsync(async () => await _sut.InvokeAsync(ip => throw new SimulatedException(), new AnonymousIdentity(), new TenantId(111))); + A.CallTo(() => _fakes.Operation.Begin()).MustHaveHappenedOnceExactly(); + A.CallTo(() => _fakes.Operation.Complete()).MustNotHaveHappened(); + } + + [Fact] + public async Task MaintainsCorrelationIdOnInvocation() + { + var correlationId = Guid.NewGuid(); + await _sut.InvokeAsync(ip => Task.CompletedTask, new AnonymousIdentity(), new TenantId(123), correlationId); + Assert.Equal(correlationId, _fakes.CurrentCorrelationHolder.Current.Id); + } + + [Fact] + public async Task MaintainsIdentityOnInvocation() + { + var identity = new GenericIdentity("me"); + await _sut.InvokeAsync(ip => Task.CompletedTask, identity, new TenantId(123)); + A.CallTo(() => _fakes.IdentityHolder.ReplaceCurrent(A.That.IsEqualTo(identity))).MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task MaintainsTenantIdOnInvocation() + { + var tenantId = new TenantId(123); + await _sut.InvokeAsync(ip => Task.CompletedTask, new AnonymousIdentity(), tenantId); + A.CallTo(() => _fakes.TenantIdHolder.ReplaceCurrent(A.That.IsEqualTo(tenantId))).MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task ProvidesInstanceProviderForInvocation() + { + IInstanceProvider provided = null; + await _sut.InvokeAsync(ip => + { + provided = ip; + return Task.CompletedTask; + }, new AnonymousIdentity(), new TenantId(111)); + Assert.StrictEqual(_fakes.InstanceProvider, provided); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheBackendFxApplicationInvoker.cs b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheBackendFxApplicationInvoker.cs new file mode 100644 index 00000000..00920046 --- /dev/null +++ b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheBackendFxApplicationInvoker.cs @@ -0,0 +1,97 @@ +using System; +using System.Reflection; +using System.Security.Principal; +using Backend.Fx.Environment.Authentication; +using Backend.Fx.Environment.MultiTenancy; +using Backend.Fx.Patterns.DependencyInjection; +using FakeItEasy; +using Xunit; + +namespace Backend.Fx.Tests.Patterns.DependencyInjection +{ + public class TheBackendFxApplicationInvoker + { + public TheBackendFxApplicationInvoker() + { + _fakes = new DiTestFakes(); + _sut = new BackendFxApplicationInvoker(_fakes.CompositionRoot); + } + + private readonly IBackendFxApplicationInvoker _sut; + private readonly DiTestFakes _fakes; + + [Fact] + public void BeginsNewScopeForEveryInvocation() + { + _sut.Invoke(ip => { }, new AnonymousIdentity(), new TenantId(111)); + A.CallTo(() => _fakes.CompositionRoot.BeginScope()).MustHaveHappenedOnceExactly(); + A.CallTo(() => _fakes.InjectionScope.Dispose()).MustHaveHappenedOnceExactly(); + + _sut.Invoke(ip => { }, new AnonymousIdentity(), new TenantId(111)); + A.CallTo(() => _fakes.CompositionRoot.BeginScope()).MustHaveHappenedTwiceExactly(); + A.CallTo(() => _fakes.InjectionScope.Dispose()).MustHaveHappenedTwiceExactly(); + } + + [Fact] + public void BeginsNewOperationForEveryInvocation() + { + _sut.Invoke(ip => { }, new AnonymousIdentity(), new TenantId(111)); + A.CallTo(() => _fakes.Operation.Begin()).MustHaveHappenedOnceExactly(); + A.CallTo(() => _fakes.Operation.Complete()).MustHaveHappenedOnceExactly(); + + _sut.Invoke(ip => { }, new AnonymousIdentity(), new TenantId(111)); + A.CallTo(() => _fakes.Operation.Begin()).MustHaveHappenedTwiceExactly(); + A.CallTo(() => _fakes.Operation.Complete()).MustHaveHappenedTwiceExactly(); + } + + [Fact] + public void DoesNotCatchFrameworkExceptions() + { + A.CallTo(() => _fakes.CompositionRoot.BeginScope()).Throws(); + Assert.Throws(() => _sut.Invoke(ip => { }, new AnonymousIdentity(), new TenantId(111))); + A.CallTo(() => _fakes.Operation.Begin()).MustNotHaveHappened(); + A.CallTo(() => _fakes.Operation.Complete()).MustNotHaveHappened(); + } + + [Fact] + public void DoesNotCatchOperationExceptions() + { + Assert.Throws(() => _sut.Invoke(ip => throw new SimulatedException(), new AnonymousIdentity(), new TenantId(111))); + A.CallTo(() => _fakes.Operation.Begin()).MustHaveHappenedOnceExactly(); + A.CallTo(() => _fakes.Operation.Complete()).MustNotHaveHappened(); + } + + [Fact] + public void MaintainsCorrelationIdOnInvocation() + { + var correlationId = Guid.NewGuid(); + _sut.Invoke(ip => { }, new AnonymousIdentity(), new TenantId(123), correlationId); + Assert.Equal(correlationId, _fakes.CurrentCorrelationHolder.Current.Id); + } + + [Fact] + public void MaintainsIdentityOnInvocation() + { + var identity = new GenericIdentity("me"); + _sut.Invoke(ip => { }, identity, new TenantId(123)); + A.CallTo(() => _fakes.IdentityHolder.ReplaceCurrent(A.That.IsEqualTo(identity))).MustHaveHappenedOnceExactly(); + } + + [Fact] + public void MaintainsTenantIdOnInvocation() + { + var tenantId = new TenantId(123); + _sut.Invoke(ip => { }, new AnonymousIdentity(), tenantId); + A.CallTo(() => _fakes.TenantIdHolder.ReplaceCurrent(A.That.IsEqualTo(tenantId))).MustHaveHappenedOnceExactly(); + } + + + [Fact] + public void ProvidesInstanceProviderForInvocation() + { + IInstanceProvider provided = null; + _sut.Invoke(ip => provided = ip, new AnonymousIdentity(), new TenantId(111)); + Assert.StrictEqual(_fakes.InstanceProvider, provided); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheBackendFxDbApplication.cs b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheBackendFxDbApplication.cs new file mode 100644 index 00000000..02ab326c --- /dev/null +++ b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheBackendFxDbApplication.cs @@ -0,0 +1,77 @@ +using System.Threading; +using Backend.Fx.Environment.Persistence; +using Backend.Fx.Logging; +using Backend.Fx.Patterns.DependencyInjection; +using Backend.Fx.Patterns.EventAggregation.Integration; +using FakeItEasy; +using Xunit; + +namespace Backend.Fx.Tests.Patterns.DependencyInjection +{ + public class TheBackendFxDbApplication + { + public TheBackendFxDbApplication() + { + IBackendFxApplication application = new BackendFxApplication(_fakes.CompositionRoot, _fakes.MessageBus, A.Fake()); + _sut = new BackendFxDbApplication(_databaseBootstrapper, _databaseAvailabilityAwaiter, application); + } + + private readonly DiTestFakes _fakes = new DiTestFakes(); + private readonly IBackendFxApplication _sut; + private readonly IDatabaseBootstrapper _databaseBootstrapper = A.Fake(); + private readonly IDatabaseAvailabilityAwaiter _databaseAvailabilityAwaiter = A.Fake(); + + [Fact] + public void CallsDatabaseBootExtensionPointsOnBoot() + { + A.CallTo(() => _databaseAvailabilityAwaiter.WaitForDatabase(A._)).MustNotHaveHappened(); + A.CallTo(() => _databaseBootstrapper.EnsureDatabaseExistence()).MustNotHaveHappened(); + _sut.Boot(); + A.CallTo(() => _databaseAvailabilityAwaiter.WaitForDatabase(A._)).MustHaveHappenedOnceExactly(); + A.CallTo(() => _databaseBootstrapper.EnsureDatabaseExistence()).MustHaveHappenedOnceExactly(); + } + + [Fact] + public void CallsDatabaseBootstrapperDisposeOnDispose() + { + _sut.Dispose(); + A.CallTo(() => _databaseBootstrapper.Dispose()).MustHaveHappenedOnceExactly(); + } + + [Fact] + public void DelegatesAllCalls() + { + var application =A.Fake(); + var sut = new BackendFxDbApplication(A.Fake(), + A.Fake(), + application); + + // ReSharper disable once UnusedVariable + IBackendFxApplicationAsyncInvoker ai = sut.AsyncInvoker; + A.CallTo(() => application.AsyncInvoker).MustHaveHappenedOnceExactly(); + + // ReSharper disable once UnusedVariable + ICompositionRoot cr = sut.CompositionRoot; + A.CallTo(() => application.CompositionRoot).MustHaveHappenedOnceExactly(); + + // ReSharper disable once UnusedVariable + IBackendFxApplicationInvoker i = sut.Invoker; + A.CallTo(() => application.Invoker).MustHaveHappenedOnceExactly(); + + // ReSharper disable once UnusedVariable + IMessageBus mb = sut.MessageBus; + A.CallTo(() => application.MessageBus).MustHaveHappenedOnceExactly(); + + sut.Boot(); + A.CallTo(() => application.Boot(A._)).MustHaveHappenedOnceExactly(); + + sut.Dispose(); + A.CallTo(() => application.Dispose()).MustHaveHappenedOnceExactly(); + + sut.WaitForBoot(); + A.CallTo(() => application.WaitForBoot(A._, A._)).MustHaveHappenedOnceExactly(); + } + + + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheCorrelation.cs b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheCorrelation.cs new file mode 100644 index 00000000..b88d27a0 --- /dev/null +++ b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheCorrelation.cs @@ -0,0 +1,25 @@ +using System; +using Backend.Fx.Patterns.DependencyInjection; +using Xunit; + +namespace Backend.Fx.Tests.Patterns.DependencyInjection +{ + public class TheCorrelation + { + private readonly Correlation _sut = new Correlation(); + + [Fact] + public void CanResume() + { + var resumedCorrelationId = Guid.NewGuid(); + _sut.Resume(resumedCorrelationId); + Assert.Equal(resumedCorrelationId, _sut.Id); + } + + [Fact] + public void InitializesWithId() + { + Assert.NotEqual(Guid.Empty, _sut.Id); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheCurrentTHolder.cs b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheCurrentTHolder.cs new file mode 100644 index 00000000..de15fba6 --- /dev/null +++ b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheCurrentTHolder.cs @@ -0,0 +1,26 @@ +using Backend.Fx.Environment.MultiTenancy; +using Backend.Fx.Patterns.DependencyInjection; +using Xunit; + +namespace Backend.Fx.Tests.Patterns.DependencyInjection +{ + public class TheCurrentTHolder + { + private readonly ICurrentTHolder _sut = new CurrentTenantIdHolder(); + private readonly TenantId _instance2 = new TenantId(2); + + [Fact] + public void CanReplaceCurrent() + { + _sut.ReplaceCurrent(_instance2); + Assert.StrictEqual(_instance2, _sut.Current); + } + + [Fact] + public void HoldsCurrent() + { + TenantId current = _sut.Current; + Assert.False(current.HasValue); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheInjectionScope.cs b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheInjectionScope.cs new file mode 100644 index 00000000..1304c108 --- /dev/null +++ b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheInjectionScope.cs @@ -0,0 +1,41 @@ +using System; +using Backend.Fx.Patterns.DependencyInjection; +using FakeItEasy; +using Xunit; + +namespace Backend.Fx.Tests.Patterns.DependencyInjection +{ + public class TheInjectionScope + { + private readonly IInstanceProvider _instanceProvider = A.Fake(); + + private class TestInjectionScope : InjectionScope + { + public TestInjectionScope(int sequenceNumber, IInstanceProvider instanceProvider) : base(sequenceNumber) + { + InstanceProvider = instanceProvider; + } + + public override IInstanceProvider InstanceProvider { get; } + + public override void Dispose() + { + throw new NotImplementedException(); + } + } + + [Fact] + public void InitializesWithSequenceNumber() + { + var injectionScope = new TestInjectionScope(42, _instanceProvider); + Assert.Equal(42, injectionScope.SequenceNumber); + } + + [Fact] + public void KeepsInstanceProvider() + { + var injectionScope = new TestInjectionScope(42, _instanceProvider); + Assert.Equal(_instanceProvider, injectionScope.InstanceProvider); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheSequentializingBackendFxApplicationInvoker.cs b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheSequentializingBackendFxApplicationInvoker.cs new file mode 100644 index 00000000..a782817b --- /dev/null +++ b/tests/Backend.Fx.Tests/Patterns/DependencyInjection/TheSequentializingBackendFxApplicationInvoker.cs @@ -0,0 +1,77 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Environment.Authentication; +using Backend.Fx.Environment.MultiTenancy; +using Backend.Fx.Logging; +using Backend.Fx.Patterns.DependencyInjection; +using Xunit; + +namespace Backend.Fx.Tests.Patterns.DependencyInjection +{ + public class TheSequentializingBackendFxApplicationInvoker + { + private static readonly ILogger Logger = LogManager.Create(); + + public TheSequentializingBackendFxApplicationInvoker() + { + var fakes = new DiTestFakes(); + _invoker = new BackendFxApplicationInvoker(fakes.CompositionRoot); + _decoratedInvoker = new SequentializingBackendFxApplicationInvoker(_invoker); + } + + private readonly int _actionDuration = 100; + private readonly IBackendFxApplicationInvoker _invoker; + private readonly IBackendFxApplicationInvoker _decoratedInvoker; + + private async Task InvokeSomeActions(int count, IBackendFxApplicationInvoker invoker) + { + var tasks = Enumerable + .Range(0, count) + .Select(i => Task.Run(() => invoker.Invoke(DoTheAction, new AnonymousIdentity(), new TenantId(1)))) + .ToArray(); + + await Task.WhenAll(tasks); + } + + private void DoTheAction(IInstanceProvider _) + { + Logger.Debug("start"); + Thread.Sleep(_actionDuration); + Logger.Debug("end"); + } + + [Fact] + public async Task IsReallyNeeded() + { + if (System.Environment.ProcessorCount > 1) + { + // negative test: without sequentialization all tasks run in parallel + var count = 10; + var sw = new Stopwatch(); + sw.Start(); + await InvokeSomeActions(count, _invoker); + long actualDuration = sw.ElapsedMilliseconds; + var expectedDuration = _actionDuration * count; + Assert.True(actualDuration < expectedDuration, + $"Actual duration of {actualDuration}ms is greater than maximum expected duration of {expectedDuration}ms"); + } + else + { + // fails on CI Pipeline due to CPU count + } + } + + [Fact] + public async Task SequentializesInvocations() + { + var count = 10; + var sw = new Stopwatch(); + sw.Start(); + await InvokeSomeActions(count, _decoratedInvoker); + Assert.True(sw.ElapsedMilliseconds >= _actionDuration * count); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/FailingDomainEventHandler.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/FailingDomainEventHandler.cs index dc24754b..f4c2f8d0 100644 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/FailingDomainEventHandler.cs +++ b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/FailingDomainEventHandler.cs @@ -1,8 +1,8 @@ +using System; +using Backend.Fx.Patterns.EventAggregation.Domain; + namespace Backend.Fx.Tests.Patterns.EventAggregation.Domain { - using System; - using Fx.Patterns.EventAggregation.Domain; - public class FailingDomainEventHandler : IDomainEventHandler { public void Handle(TestDomainEvent testDomainEvent) diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TestDomainEvent.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TestDomainEvent.cs index fd394ead..c9fb4e59 100644 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TestDomainEvent.cs +++ b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TestDomainEvent.cs @@ -1,7 +1,7 @@ -namespace Backend.Fx.Tests.Patterns.EventAggregation.Domain -{ - using Fx.Patterns.EventAggregation.Domain; +using Backend.Fx.Patterns.EventAggregation.Domain; +namespace Backend.Fx.Tests.Patterns.EventAggregation.Domain +{ public class TestDomainEvent : IDomainEvent { public TestDomainEvent(int id) diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TestDomainEventHandler.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TestDomainEventHandler.cs index dd04f474..faa65c19 100644 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TestDomainEventHandler.cs +++ b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TestDomainEventHandler.cs @@ -1,15 +1,15 @@ +using System.Collections.Generic; +using Backend.Fx.Patterns.EventAggregation.Domain; + namespace Backend.Fx.Tests.Patterns.EventAggregation.Domain { - using System.Collections.Generic; - using Fx.Patterns.EventAggregation.Domain; - public class TestDomainEventHandler : IDomainEventHandler { + public List Events { get; } = new List(); + public void Handle(TestDomainEvent testDomainEvent) { Events.Add(testDomainEvent); } - - public List Events { get; } = new List(); } } \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TestIntegrationEvent.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TestIntegrationEvent.cs index 35421dca..f2adb89e 100644 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TestIntegrationEvent.cs +++ b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TestIntegrationEvent.cs @@ -1,18 +1,16 @@ -using JetBrains.Annotations; +using Backend.Fx.Patterns.EventAggregation.Integration; +using JetBrains.Annotations; namespace Backend.Fx.Tests.Patterns.EventAggregation.Domain { - using Fx.Patterns.EventAggregation.Integration; - [UsedImplicitly] public class TestIntegrationEvent : IntegrationEvent { - [UsedImplicitly] - public int Whatever { get; } - public TestIntegrationEvent(int whatever, int tenantId) : base(tenantId) { Whatever = whatever; } + + [UsedImplicitly] public int Whatever { get; } } } \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TheEventAggregator.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TheEventAggregator.cs index fb1448af..59263995 100644 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TheEventAggregator.cs +++ b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Domain/TheEventAggregator.cs @@ -1,11 +1,10 @@ -using Xunit; +using System; +using Backend.Fx.Patterns.EventAggregation.Domain; +using FakeItEasy; +using Xunit; namespace Backend.Fx.Tests.Patterns.EventAggregation.Domain { - using System; - using FakeItEasy; - using Fx.Patterns.EventAggregation.Domain; - public class TheEventAggregator { [Fact] @@ -13,8 +12,8 @@ public void CallsAllDomainEventHandlers() { var handler1 = new TestDomainEventHandler(); var handler2 = new TestDomainEventHandler(); - IDomainEventHandlerProvider fakeDomainEventHandlerProvider = A.Fake(); - A.CallTo(() => fakeDomainEventHandlerProvider.GetAllEventHandlers()).Returns(new[] { handler1, handler2 }); + var fakeDomainEventHandlerProvider = A.Fake(); + A.CallTo(() => fakeDomainEventHandlerProvider.GetAllEventHandlers()).Returns(new[] {handler1, handler2}); IDomainEventAggregator sut = new DomainEventAggregator(fakeDomainEventHandlerProvider); @@ -36,12 +35,12 @@ public void CallsAllDomainEventHandlers() public void DoesNotSwallowExceptionOnDomainEventHandling() { IDomainEventHandler handler = new FailingDomainEventHandler(); - IDomainEventHandlerProvider fakeDomainEventHandlerProvider = A.Fake(); - A.CallTo(() => fakeDomainEventHandlerProvider.GetAllEventHandlers()).Returns(new[] { handler }); + var fakeDomainEventHandlerProvider = A.Fake(); + A.CallTo(() => fakeDomainEventHandlerProvider.GetAllEventHandlers()).Returns(new[] {handler}); IDomainEventAggregator sut = new DomainEventAggregator(fakeDomainEventHandlerProvider); sut.PublishDomainEvent(new TestDomainEvent(444)); Assert.Throws(() => sut.RaiseEvents()); } } -} +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/DynamicEventHandler.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/DynamicEventHandler.cs deleted file mode 100644 index 887949db..00000000 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/DynamicEventHandler.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration -{ - using Fx.Patterns.EventAggregation.Integration; - - public class DynamicEventHandler : IIntegrationEventHandler - { - private readonly IIntegrationEventHandler _integrationEventHandlerImplementation; - - public DynamicEventHandler(IIntegrationEventHandler integrationEventHandlerImplementation) - { - _integrationEventHandlerImplementation = integrationEventHandlerImplementation; - } - - public void Handle(dynamic eventData) - { - _integrationEventHandlerImplementation.Handle(eventData); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/DynamicMessageHandler.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/DynamicMessageHandler.cs new file mode 100644 index 00000000..e0cafc60 --- /dev/null +++ b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/DynamicMessageHandler.cs @@ -0,0 +1,19 @@ +using Backend.Fx.Patterns.EventAggregation.Integration; + +namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration +{ + public class DynamicMessageHandler : IIntegrationMessageHandler + { + private readonly IIntegrationMessageHandler _integrationMessageHandlerImplementation; + + public DynamicMessageHandler(IIntegrationMessageHandler integrationMessageHandlerImplementation) + { + _integrationMessageHandlerImplementation = integrationMessageHandlerImplementation; + } + + public void Handle(dynamic eventData) + { + _integrationMessageHandlerImplementation.Handle(eventData); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/LongRunningEventHandler.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/LongRunningEventHandler.cs deleted file mode 100644 index 08a28f41..00000000 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/LongRunningEventHandler.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration -{ - using System.Threading; - using Fx.Patterns.EventAggregation.Integration; - - public class LongRunningEventHandler : IIntegrationEventHandler - { - public void Handle(TestIntegrationEvent eventData) - { - Thread.Sleep(1000); - eventData.Processed.Set(); - } - }} diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/LongRunningMessageHandler.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/LongRunningMessageHandler.cs new file mode 100644 index 00000000..2cf3567c --- /dev/null +++ b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/LongRunningMessageHandler.cs @@ -0,0 +1,21 @@ +using System.Threading; +using Backend.Fx.Patterns.EventAggregation.Integration; + +namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration +{ + public class LongRunningMessageHandler : IIntegrationMessageHandler + { + private readonly IIntegrationMessageHandler _handler; + + public LongRunningMessageHandler(IIntegrationMessageHandler handler) + { + _handler = handler; + } + + public void Handle(TestIntegrationEvent eventData) + { + Thread.Sleep(1000); + _handler.Handle(eventData); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/SerializingEventBus.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/SerializingMessageBus.cs similarity index 55% rename from tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/SerializingEventBus.cs rename to tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/SerializingMessageBus.cs index 40eca17a..906488d3 100644 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/SerializingEventBus.cs +++ b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/SerializingMessageBus.cs @@ -1,39 +1,36 @@ -using Backend.Fx.Patterns.DependencyInjection; +using System; +using System.Threading.Tasks; +using Backend.Fx.Environment.MultiTenancy; +using Backend.Fx.Patterns.EventAggregation.Integration; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration { - using System; - using System.Threading.Tasks; - using Fx.Environment.MultiTenancy; - using Fx.Patterns.EventAggregation.Integration; - using Newtonsoft.Json; - using Newtonsoft.Json.Linq; - - public class SerializingEventBus : EventBus + public class SerializingMessageBus : MessageBus { - public SerializingEventBus(IBackendFxApplication application) - : base(application) - { } - public override void Connect() - { } + { + } - protected override Task PublishOnEventBus(IIntegrationEvent integrationEvent) + protected override Task PublishOnMessageBus(IIntegrationEvent integrationEvent) { var jsonString = JsonConvert.SerializeObject(integrationEvent); - return Task.Run(()=>Process(integrationEvent.GetType().FullName, new SerializingProcessingContext(jsonString))); + return Task.Run(() => Process(MessageNameProvider.GetMessageName(), new SerializingProcessingContext(jsonString))); } - protected override void Subscribe(string eventName) - {} + protected override void Subscribe(string messageName) + { + } - protected override void Unsubscribe(string eventName) - {} + protected override void Unsubscribe(string messageName) + { + } - private class SerializingProcessingContext : EventProcessingContext + private class SerializingProcessingContext : EventProcessingContext { private readonly string _jsonString; - + public SerializingProcessingContext(string jsonString) { _jsonString = jsonString; @@ -42,8 +39,8 @@ public SerializingProcessingContext(string jsonString) CorrelationId = eventStub.correlationId; } - public override TenantId TenantId {get; } - + public override TenantId TenantId { get; } + public override dynamic DynamicEvent => JObject.Parse(_jsonString); public override Guid CorrelationId { get; } diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TestIntegrationEvent.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TestIntegrationEvent.cs index b0295701..09e356c0 100644 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TestIntegrationEvent.cs +++ b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TestIntegrationEvent.cs @@ -1,20 +1,21 @@ -namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration +using System.Threading; +using Backend.Fx.Patterns.EventAggregation.Integration; + +namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration { - using System.Threading; - using Backend.Fx.Patterns.EventAggregation.Integration; - public class TestIntegrationEvent : IntegrationEvent { - public int IntParam { get; } - - public string StringParam { get; } - - public ManualResetEventSlim Processed = new ManualResetEventSlim(false); - public TestIntegrationEvent(int intParam, string stringParam) : base(55) { IntParam = intParam; StringParam = stringParam; } + + public int IntParam { get; } + + public string StringParam { get; } + + public ManualResetEventSlim TypedProcessed { get; } = new ManualResetEventSlim(false); + public ManualResetEventSlim DynamicProcessed { get; } = new ManualResetEventSlim(false); } } \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TheEventBus.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TheEventBus.cs deleted file mode 100644 index f34a787b..00000000 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TheEventBus.cs +++ /dev/null @@ -1,161 +0,0 @@ - -namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration -{ - using System; - using System.Diagnostics; - using System.Threading.Tasks; - using FakeItEasy; - using Fx.Environment.MultiTenancy; - using Fx.Logging; - using Fx.Patterns.DependencyInjection; - using Fx.Patterns.EventAggregation.Integration; - using JetBrains.Annotations; - using Xunit; - - public sealed class TheInMemoryEventBus : TheEventBus - { - protected override IEventBus Create(IBackendFxApplication application) - { - return new InMemoryEventBus(application); - } - - [Fact] - public async Task HandlesEventsAsynchronously() - { - Sut.Subscribe(); - Stopwatch sw = new Stopwatch(); - sw.Start(); - var integrationEvent = new TestIntegrationEvent(1,"a"); - await Sut.Publish(integrationEvent); - Assert.True(sw.ElapsedMilliseconds < 900); - integrationEvent.Processed.Wait(Debugger.IsAttached ? int.MaxValue : 10000); - Assert.True(sw.ElapsedMilliseconds > 1000); - } - } - - [UsedImplicitly] - public sealed class TheSerializingEventBus : TheEventBus - { - protected override IEventBus Create(IBackendFxApplication scopeManager) - { - return new SerializingEventBus(scopeManager); - } - } - - public abstract class TheEventBus - { - private readonly AFakeApplication _app = new AFakeApplication(); - public IEventBus Sut { get; } - - protected TheEventBus() - { - // ReSharper disable once VirtualMemberCallInConstructor - Sut = Create(_app); - } - - protected abstract IEventBus Create(IBackendFxApplication application); - - [Fact] - public async Task CallsTypedEventHandler() - { - Sut.Subscribe(); - var integrationEvent = new TestIntegrationEvent(34, "gaga"); - await Sut.Publish(integrationEvent); - integrationEvent.Processed.Wait(Debugger.IsAttached ? int.MaxValue : 10000); - A.CallTo(() => _app.TypedHandler.Handle(A - .That - .Matches(evt => evt.IntParam == 34 && evt.StringParam == "gaga"))) - .MustHaveHappenedOnceExactly(); - - A.CallTo(() => _app.DynamicHandler.Handle(A._)).MustNotHaveHappened(); - } - - [Fact] - public async void HandlesExceptionFromTypedEventHandler() - { - Sut.Subscribe(); - var integrationEvent = new TestIntegrationEvent(34, "gaga"); - await Sut.Publish(integrationEvent); - integrationEvent.Processed.Wait(Debugger.IsAttached ? int.MaxValue : 10000); - - A.CallTo(() => _app.ExceptionLogger.LogException(A - .That - .Matches(ex => ex.Message == ThrowingTypedEventHandler.ExceptionMessage))) - .MustHaveHappenedOnceExactly(); - } - - [Fact] - public async void CallsDynamicEventHandler() - { - Sut.Subscribe(typeof(TestIntegrationEvent).FullName); - var integrationEvent = new TestIntegrationEvent(34, "gaga"); - await Sut.Publish(integrationEvent); - integrationEvent.Processed.Wait(Debugger.IsAttached ? int.MaxValue : 10000); - - A.CallTo(() => _app.TypedHandler.Handle(A._)).MustNotHaveHappened(); - A.CallTo(() => _app.DynamicHandler.Handle(A._)).MustHaveHappenedOnceExactly(); - } - - [Fact] - public async void HandlesExceptionFromDynamicEventHandler() - { - Sut.Subscribe(typeof(TestIntegrationEvent).FullName); - var integrationEvent = new TestIntegrationEvent(34, "gaga"); - await Sut.Publish(integrationEvent); - integrationEvent.Processed.Wait(Debugger.IsAttached ? int.MaxValue : 10000); - - A.CallTo(() => _app.ExceptionLogger.LogException(A - .That - .Matches(ex => ex.Message == ThrowingDynamicEventHandler.ExceptionMessage))) - .MustHaveHappenedOnceExactly(); - } - - [Fact] - public async void CallsMixedEventHandlers() - { - Sut.Subscribe(typeof(TestIntegrationEvent).FullName); - Sut.Subscribe(); - var integrationEvent = new TestIntegrationEvent(34, "gaga"); - await Sut.Publish(integrationEvent); - integrationEvent.Processed.Wait(Debugger.IsAttached ? int.MaxValue : 10000); - - A.CallTo(() => _app.TypedHandler.Handle(A - .That - .Matches(evt => evt.IntParam == 34 && evt.StringParam == "gaga"))) - .MustHaveHappenedOnceExactly(); - - A.CallTo(() => _app.DynamicHandler.Handle(A._)).MustHaveHappenedOnceExactly(); - } - - private class AFakeApplication : BackendFxApplication - { - public IIntegrationEventHandler TypedHandler { get; } = A.Fake>(); - public IIntegrationEventHandler DynamicHandler { get; } = A.Fake(); - - public AFakeApplication() : this (A.Fake()) - {} - - private AFakeApplication(ICompositionRoot compositionRoot) - : base(compositionRoot, A.Fake(), A.Fake()) - { - A.CallTo(() => CompositionRoot.BeginScope()) - .Returns(A.Fake()); - - A.CallTo(() => CompositionRoot.GetInstance(A.That.IsEqualTo(typeof(TypedEventHandler)))) - .Returns(new TypedEventHandler(TypedHandler)); - - A.CallTo(() => CompositionRoot.GetInstance(A.That.IsEqualTo(typeof(LongRunningEventHandler)))) - .Returns(new LongRunningEventHandler()); - - A.CallTo(() => CompositionRoot.GetInstance(A.That.IsEqualTo(typeof(ThrowingTypedEventHandler)))) - .Returns(new ThrowingTypedEventHandler()); - - A.CallTo(() => CompositionRoot.GetInstance(A.That.IsEqualTo(typeof(DynamicEventHandler)))) - .Returns(new DynamicEventHandler(DynamicHandler)); - - A.CallTo(() => CompositionRoot.GetInstance(A.That.IsEqualTo(typeof(ThrowingDynamicEventHandler)))) - .Returns(new ThrowingDynamicEventHandler()); - } - } - } -} diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TheMessageBus.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TheMessageBus.cs new file mode 100644 index 00000000..9baff8a0 --- /dev/null +++ b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TheMessageBus.cs @@ -0,0 +1,188 @@ +using System; +using System.Diagnostics; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using Backend.Fx.Environment.MultiTenancy; +using Backend.Fx.Patterns.DependencyInjection; +using Backend.Fx.Patterns.EventAggregation.Integration; +using FakeItEasy; +using JetBrains.Annotations; +using Xunit; + +namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration +{ + public sealed class TheInMemoryMessageBus : TheMessageBus + { + public TheInMemoryMessageBus() + { + Sut.ProvideInvoker(FakeApplication.Invoker); + } + + [Fact] + public async Task HandlesEventsAsynchronously() + { + Sut.Subscribe(); + var sw = new Stopwatch(); + sw.Start(); + await Sut.Publish(Invoker.IntegrationEvent); + Assert.True(sw.ElapsedMilliseconds < 900); + Assert.True(Invoker.IntegrationEvent.TypedProcessed.Wait(Debugger.IsAttached ? int.MaxValue : 10000)); + Assert.True(sw.ElapsedMilliseconds > 1000); + } + + protected override IMessageBus Sut { get; } = new InMemoryMessageBus(); + } + + [UsedImplicitly] + public sealed class TheSerializingMessageBus : TheMessageBus + { + public TheSerializingMessageBus() + { + Sut.ProvideInvoker(FakeApplication.Invoker); + } + + protected override IMessageBus Sut { get; } = new SerializingMessageBus(); + } + + public abstract class TheMessageBus + { + protected IBackendFxApplication FakeApplication { get; } = A.Fake(); + + protected TheMessageBus() + { + A.CallTo(() => FakeApplication.Invoker).Returns(Invoker); + A.CallTo(() => FakeApplication.WaitForBoot(A._, A._)).Returns(true); + } + + protected TestInvoker Invoker { get; } = new TestInvoker(); + protected abstract IMessageBus Sut { get; } + + + public class TestInvoker : IBackendFxApplicationInvoker + { + public TestIntegrationEvent IntegrationEvent = new TestIntegrationEvent(34, "gaga"); + + public TestInvoker() + { + A.CallTo(() => TypedHandler.Handle(A._)).Invokes((TestIntegrationEvent e) => IntegrationEvent.TypedProcessed.Set()); + A.CallTo(() => DynamicHandler.Handle(new object())).WithAnyArguments().Invokes((object e) => IntegrationEvent.DynamicProcessed.Set()); + + A.CallTo(() => FakeInstanceProvider.GetInstance(A.That.IsEqualTo(typeof(TypedMessageHandler)))) + .Returns(new TypedMessageHandler(TypedHandler)); + + A.CallTo(() => FakeInstanceProvider.GetInstance(A.That.IsEqualTo(typeof(LongRunningMessageHandler)))) + .Returns(new LongRunningMessageHandler(TypedHandler)); + + A.CallTo(() => FakeInstanceProvider.GetInstance(A.That.IsEqualTo(typeof(ThrowingTypedMessageHandler)))) + .Returns(new ThrowingTypedMessageHandler(TypedHandler)); + + A.CallTo(() => FakeInstanceProvider.GetInstance(A.That.IsEqualTo(typeof(DynamicMessageHandler)))) + .Returns(new DynamicMessageHandler(DynamicHandler)); + + A.CallTo(() => FakeInstanceProvider.GetInstance(A.That.IsEqualTo(typeof(ThrowingDynamicMessageHandler)))) + .Returns(new ThrowingDynamicMessageHandler(DynamicHandler)); + } + + public IIntegrationMessageHandler TypedHandler { get; } = A.Fake>(); + public IIntegrationMessageHandler DynamicHandler { get; } = A.Fake(); + public IInstanceProvider FakeInstanceProvider { get; } = A.Fake(); + + public void Invoke(Action action, IIdentity identity, TenantId tenantId, Guid? correlationId = null) + { + action(FakeInstanceProvider); + } + } + + [Fact] + public async void CallsDynamicEventHandler() + { + Sut.Subscribe(Sut.MessageNameProvider.GetMessageName()); + await Sut.Publish(Invoker.IntegrationEvent); + Assert.True(Invoker.IntegrationEvent.DynamicProcessed.Wait(Debugger.IsAttached ? int.MaxValue : 10000)); + + A.CallTo(() => Invoker.TypedHandler.Handle(A._)).MustNotHaveHappened(); + A.CallTo(() => Invoker.DynamicHandler.Handle(A._)).MustHaveHappenedOnceExactly(); + } + + [Fact] + public async void CallsMixedEventHandlers() + { + Sut.Subscribe(Sut.MessageNameProvider.GetMessageName()); + Sut.Subscribe(); + + await Sut.Publish(Invoker.IntegrationEvent); + Assert.True(Invoker.IntegrationEvent.TypedProcessed.Wait(Debugger.IsAttached ? int.MaxValue : 10000)); + Assert.True(Invoker.IntegrationEvent.DynamicProcessed.Wait(Debugger.IsAttached ? int.MaxValue : 10000)); + + A.CallTo(() => Invoker.TypedHandler.Handle(A + .That + .Matches(evt => evt.IntParam == 34 && evt.StringParam == "gaga"))) + .MustHaveHappenedOnceExactly(); + + A.CallTo(() => Invoker.DynamicHandler.Handle(A._)).MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task CallsTypedEventHandler() + { + Sut.Subscribe(); + await Sut.Publish(Invoker.IntegrationEvent); + Assert.True(Invoker.IntegrationEvent.TypedProcessed.Wait(Debugger.IsAttached ? int.MaxValue : 10000)); + A.CallTo(() => Invoker.TypedHandler.Handle(A + .That + .Matches(evt => evt.IntParam == 34 && evt.StringParam == "gaga"))) + .MustHaveHappenedOnceExactly(); + + A.CallTo(() => Invoker.DynamicHandler.Handle(A._)).MustNotHaveHappened(); + } + + [Fact] + public async Task DoesNotCallUnsubscribedTypedEventHandler() + { + Sut.Subscribe(); + Sut.Unsubscribe(); + await Sut.Publish(Invoker.IntegrationEvent); + A.CallTo(() => Invoker.TypedHandler.Handle(A + .That + .Matches(evt => evt.IntParam == 34 && evt.StringParam == "gaga"))) + .MustNotHaveHappened(); + } + + [Fact] + public async void DoesNotCallUnsubscribedDynamicEventHandler() + { + Sut.Subscribe(Sut.MessageNameProvider.GetMessageName()); + Sut.Unsubscribe(Sut.MessageNameProvider.GetMessageName()); + await Sut.Publish(Invoker.IntegrationEvent); + A.CallTo(() => Invoker.DynamicHandler.Handle(A._)).MustNotHaveHappened(); + } + + [Fact] + public async void DoesNotCallUnsubscribedDelegateEventHandler() + { + var handled = new ManualResetEvent(false); + var handler = new DelegateIntegrationMessageHandler(ev => handled.Set()); + Sut.Subscribe(handler); + Sut.Unsubscribe(handler); + await Sut.Publish(Invoker.IntegrationEvent); + Assert.False(handled.WaitOne(Debugger.IsAttached ? int.MaxValue : 1000)); + } + + [Fact] + public void CannCallConnectButItDoesNothing() + { + Sut.Connect(); + } + + [Fact] + public async void DelegateIntegrationMessageHandler() + { + var handled = new ManualResetEvent(false); + var handler = new DelegateIntegrationMessageHandler(ev => handled.Set()); + Sut.Subscribe(handler); + await Sut.Publish(Invoker.IntegrationEvent); + Assert.True(handled.WaitOne(Debugger.IsAttached ? int.MaxValue : 10000)); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TheMessageBusScope.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TheMessageBusScope.cs new file mode 100644 index 00000000..6ffc8fae --- /dev/null +++ b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TheMessageBusScope.cs @@ -0,0 +1,53 @@ +using Backend.Fx.Patterns.DependencyInjection; +using Backend.Fx.Patterns.EventAggregation.Integration; +using FakeItEasy; +using Xunit; + +namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration +{ + public class TheMessageBusScope + { + private readonly IMessageBus _messageBus = A.Fake(); + private readonly ICurrentTHolder _currentCorrelationHolder = new CurrentCorrelationHolder(); + private readonly IMessageBusScope _sut; + + public TheMessageBusScope() + { + _sut = new MessageBusScope(_messageBus, _currentCorrelationHolder); + } + + [Fact] + public void MaintainsCorrelationIdOnPublish() + { + var testIntegrationEvent = new Domain.TestIntegrationEvent(44, 1111); + _sut.Publish(testIntegrationEvent); + Assert.Equal(_currentCorrelationHolder.Current.Id, testIntegrationEvent.CorrelationId); + } + + [Fact] + public void DoesNotPublishOnBusWhenPublishing() + { + var testIntegrationEvent = new Domain.TestIntegrationEvent(44, 1111); + _sut.Publish(testIntegrationEvent); + A.CallTo(()=>_messageBus.Publish(A._)).MustNotHaveHappened(); + } + + [Fact] + public void PublishesAllEventsOnRaise() + { + var ev1 = new Domain.TestIntegrationEvent(44, 1111); + var ev2 = new Domain.TestIntegrationEvent(45, 1111); + var ev3 = new Domain.TestIntegrationEvent(46, 1111); + var ev4 = new Domain.TestIntegrationEvent(47, 1111); + _sut.Publish(ev1); + _sut.Publish(ev2); + _sut.Publish(ev3); + _sut.Publish(ev4); + _sut.RaiseEvents(); + A.CallTo(()=>_messageBus.Publish(A.That.IsEqualTo(ev1))).MustHaveHappenedOnceExactly(); + A.CallTo(()=>_messageBus.Publish(A.That.IsEqualTo(ev2))).MustHaveHappenedOnceExactly(); + A.CallTo(()=>_messageBus.Publish(A.That.IsEqualTo(ev3))).MustHaveHappenedOnceExactly(); + A.CallTo(()=>_messageBus.Publish(A.That.IsEqualTo(ev4))).MustHaveHappenedOnceExactly(); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/ThrowingDynamicEventHandler.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/ThrowingDynamicEventHandler.cs index b6337b22..a5dd498e 100644 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/ThrowingDynamicEventHandler.cs +++ b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/ThrowingDynamicEventHandler.cs @@ -1,14 +1,21 @@ -namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration -{ - using System; - using Fx.Patterns.EventAggregation.Integration; +using System; +using Backend.Fx.Patterns.EventAggregation.Integration; - public class ThrowingDynamicEventHandler : IIntegrationEventHandler +namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration +{ + public class ThrowingDynamicMessageHandler : IIntegrationMessageHandler { public const string ExceptionMessage = "From ThrowingDynamicEventHandler"; + private readonly IIntegrationMessageHandler _handler; + + public ThrowingDynamicMessageHandler(IIntegrationMessageHandler handler) + { + _handler = handler; + } public void Handle(dynamic eventData) { + _handler.Handle(eventData); throw new InvalidOperationException(ExceptionMessage); } } diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/ThrowingTypedEventHandler.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/ThrowingTypedEventHandler.cs deleted file mode 100644 index cf47dc64..00000000 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/ThrowingTypedEventHandler.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration -{ - using System; - using Fx.Patterns.EventAggregation.Integration; - - public class ThrowingTypedEventHandler : IIntegrationEventHandler - { - public const string ExceptionMessage = "From ThrowingTypedEventHandler"; - - public void Handle(TestIntegrationEvent eventData) - { - throw new InvalidOperationException(ExceptionMessage); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/ThrowingTypedMessageHandler.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/ThrowingTypedMessageHandler.cs new file mode 100644 index 00000000..3bdd71d9 --- /dev/null +++ b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/ThrowingTypedMessageHandler.cs @@ -0,0 +1,22 @@ +using System; +using Backend.Fx.Patterns.EventAggregation.Integration; + +namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration +{ + public class ThrowingTypedMessageHandler : IIntegrationMessageHandler + { + public const string ExceptionMessage = "From ThrowingTypedEventHandler"; + private readonly IIntegrationMessageHandler _handler; + + public ThrowingTypedMessageHandler(IIntegrationMessageHandler handler) + { + _handler = handler; + } + + public void Handle(TestIntegrationEvent eventData) + { + _handler.Handle(eventData); + throw new InvalidOperationException(ExceptionMessage); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TypedEventHandler.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TypedEventHandler.cs deleted file mode 100644 index e107e410..00000000 --- a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TypedEventHandler.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration -{ - using Fx.Patterns.EventAggregation.Integration; - - public class TypedEventHandler : IIntegrationEventHandler - { - private readonly IIntegrationEventHandler _integrationEventHandlerImplementation; - - public TypedEventHandler(IIntegrationEventHandler integrationEventHandlerImplementation) - { - _integrationEventHandlerImplementation = integrationEventHandlerImplementation; - } - - public void Handle(TestIntegrationEvent eventData) - { - _integrationEventHandlerImplementation.Handle(eventData); - eventData.Processed.Set(); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TypedMessageHandler.cs b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TypedMessageHandler.cs new file mode 100644 index 00000000..e76416b8 --- /dev/null +++ b/tests/Backend.Fx.Tests/Patterns/EventAggregation/Integration/TypedMessageHandler.cs @@ -0,0 +1,20 @@ +using Backend.Fx.Patterns.EventAggregation.Integration; + +namespace Backend.Fx.Tests.Patterns.EventAggregation.Integration +{ + public class TypedMessageHandler : IIntegrationMessageHandler + { + private readonly IIntegrationMessageHandler _integrationMessageHandlerImplementation; + + public TypedMessageHandler(IIntegrationMessageHandler integrationMessageHandlerImplementation) + { + _integrationMessageHandlerImplementation = integrationMessageHandlerImplementation; + } + + public void Handle(TestIntegrationEvent eventData) + { + _integrationMessageHandlerImplementation.Handle(eventData); + eventData.TypedProcessed.Set(); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/IdGeneration/TheHiLoIdGenerator.cs b/tests/Backend.Fx.Tests/Patterns/IdGeneration/TheHiLoIdGenerator.cs index c5500bb5..0d0c5c73 100644 --- a/tests/Backend.Fx.Tests/Patterns/IdGeneration/TheHiLoIdGenerator.cs +++ b/tests/Backend.Fx.Tests/Patterns/IdGeneration/TheHiLoIdGenerator.cs @@ -1,20 +1,21 @@ -using Xunit; +using System.Linq; +using Backend.Fx.Patterns.IdGeneration; +using Xunit; namespace Backend.Fx.Tests.Patterns.IdGeneration { - using System.Linq; - using Fx.Patterns.IdGeneration; - public class TheHiLoIdGenerator { private readonly HiLoIdGenerator _sut = new InMemoryHiLoIdGenerator(1, 100); - [Fact] - public void StartsWithInitialValueAndCountsUp() + private class IdConsument { - for (int i = 1; i < 1000; i++) + public int[] Ids { get; private set; } + + public void GetIds(int count, IIdGenerator idGenerator) { - Assert.Equal(i, _sut.NextId()); + Ids = new int[count]; + for (var i = 0; i < count; i++) Ids[i] = idGenerator.NextId(); } } @@ -23,33 +24,22 @@ public void AllowsMultipleThreadsToGetIds() { const int consumentCount = 50; const int idCountPerConsument = 1000; - IdConsument[] idConsuments = new IdConsument[consumentCount]; + var idConsuments = new IdConsument[consumentCount]; - for (int i = 0; i < consumentCount; i++) - { - idConsuments[i] = new IdConsument(); - } - - idConsuments.AsParallel().ForAll(idConsument=> { idConsument.GetIds(idCountPerConsument, _sut); }); + for (var i = 0; i < consumentCount; i++) idConsuments[i] = new IdConsument(); + + idConsuments.AsParallel().ForAll(idConsument => { idConsument.GetIds(idCountPerConsument, _sut); }); - int[] allIds = idConsuments.SelectMany(idConsument => idConsument.Ids).ToArray(); - Assert.Equal(consumentCount*idCountPerConsument, allIds.Length); - Assert.Equal(consumentCount*idCountPerConsument, allIds.Distinct().Count()); - Assert.Equal(consumentCount * idCountPerConsument + 1,_sut.NextId()); + var allIds = idConsuments.SelectMany(idConsument => idConsument.Ids).ToArray(); + Assert.Equal(consumentCount * idCountPerConsument, allIds.Length); + Assert.Equal(consumentCount * idCountPerConsument, allIds.Distinct().Count()); + Assert.Equal(consumentCount * idCountPerConsument + 1, _sut.NextId()); } - private class IdConsument + [Fact] + public void StartsWithInitialValueAndCountsUp() { - public int[] Ids { get; private set; } - - public void GetIds(int count, IIdGenerator idGenerator) - { - Ids = new int[count]; - for (int i = 0; i < count; i++) - { - Ids[i] = idGenerator.NextId(); - } - } + for (var i = 1; i < 1000; i++) Assert.Equal(i, _sut.NextId()); } } @@ -64,17 +54,17 @@ public InMemoryHiLoIdGenerator(int start, int increment) BlockSize = increment; } + protected override int BlockSize { get; } + protected override int GetNextBlockStart() { lock (_synclock) { // this simulates the behavior of a SQL sequence for example - int result = _nextBlockStart; + var result = _nextBlockStart; _nextBlockStart += BlockSize; return result; } } - - protected override int BlockSize { get; } } -} +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/IdGeneration/TheSequenceHiLoIdGenerator.cs b/tests/Backend.Fx.Tests/Patterns/IdGeneration/TheSequenceHiLoIdGenerator.cs new file mode 100644 index 00000000..f1a82151 --- /dev/null +++ b/tests/Backend.Fx.Tests/Patterns/IdGeneration/TheSequenceHiLoIdGenerator.cs @@ -0,0 +1,35 @@ +using Backend.Fx.Patterns.IdGeneration; +using FakeItEasy; +using Xunit; + +namespace Backend.Fx.Tests.Patterns.IdGeneration +{ + public class TheSequenceHiLoIdGenerator + { + public TheSequenceHiLoIdGenerator() + { + A.CallTo(() => _sequence.Increment).Returns(10); + _sut = new TestIdGenerator(_sequence); + } + + private readonly ISequence _sequence = A.Fake(); + private readonly SequenceHiLoIdGenerator _sut; + + private class TestIdGenerator : SequenceHiLoIdGenerator + { + public TestIdGenerator(ISequence sequence) : base(sequence) + { + } + } + + [Fact] + public void CallsSequenceNextValueOnBlockStart() + { + for (var i = 0; i < 10; i++) _sut.NextId(); + + A.CallTo(() => _sequence.GetNextValue()).MustHaveHappenedOnceExactly(); + _sut.NextId(); + A.CallTo(() => _sequence.GetNextValue()).MustHaveHappenedTwiceExactly(); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/Transactions/TheDbTransactionOperationDecorator.cs b/tests/Backend.Fx.Tests/Patterns/Transactions/TheDbTransactionOperationDecorator.cs new file mode 100644 index 00000000..7ea4fb88 --- /dev/null +++ b/tests/Backend.Fx.Tests/Patterns/Transactions/TheDbTransactionOperationDecorator.cs @@ -0,0 +1,202 @@ +using System; +using System.Data; +using Backend.Fx.Environment.Persistence; +using Backend.Fx.Patterns.DependencyInjection; +using FakeItEasy; +using Xunit; + +namespace Backend.Fx.Tests.Patterns.Transactions +{ + public class TheDbTransactionOperationDecorator + { + public TheDbTransactionOperationDecorator() + { + _sut = new DbTransactionOperationDecorator(_dbConnection, _operation); + A.CallTo(() => _dbConnection.BeginTransaction(A._)).Returns(_dbTransaction); + } + + private readonly IDbConnection _dbConnection = A.Fake(); + private readonly IDbTransaction _dbTransaction = A.Fake(); + private readonly IOperation _operation = new Operation(); + private readonly DbTransactionOperationDecorator _sut; + + [Theory] + [InlineData(IsolationLevel.ReadCommitted)] + [InlineData(IsolationLevel.ReadUncommitted)] + [InlineData(IsolationLevel.Serializable)] + public void BeginsTransactionInSpecificIsolationLevel(IsolationLevel isolationLevel) + { + _sut.SetIsolationLevel(isolationLevel); + A.CallTo(() => _dbConnection.State).Returns(ConnectionState.Open); + _sut.Begin(); + A.CallTo(() => _dbConnection.BeginTransaction(A.That.IsEqualTo(isolationLevel))).MustHaveHappenedOnceExactly(); + Assert.Equal(_sut.CurrentTransaction, _dbTransaction); + } + + [Fact] + public void BeginsTransactionInUnspecifiedIsolationLevel() + { + A.CallTo(() => _dbConnection.State).Returns(ConnectionState.Open); + _sut.Begin(); + A.CallTo(() => _dbConnection.BeginTransaction(A.That.IsEqualTo(IsolationLevel.Unspecified))).MustHaveHappenedOnceExactly(); + Assert.Equal(_sut.CurrentTransaction, _dbTransaction); + } + + [Theory] + [InlineData(ConnectionState.Broken)] + [InlineData(ConnectionState.Connecting)] + [InlineData(ConnectionState.Executing)] + [InlineData(ConnectionState.Fetching)] + public void CannotBeginWithConnectionIsInWrongState(ConnectionState wrongState) + { + A.CallTo(() => _dbConnection.State).Returns(wrongState); + var sut = new DbTransactionOperationDecorator(_dbConnection, _operation); + Assert.Throws(() => sut.Begin()); + } + + [Fact] + public void ClosesAndDisposesOnCancel() + { + A.CallTo(() => _dbConnection.State).Returns(ConnectionState.Closed); + _sut.Begin(); + A.CallTo(() => _dbConnection.State).Returns(ConnectionState.Open); + _sut.Cancel(); + A.CallTo(() => _dbConnection.Close()).MustHaveHappenedOnceExactly(); + A.CallTo(() => _dbTransaction.Dispose()).MustHaveHappenedOnceExactly(); + A.CallTo(() => _dbTransaction.Commit()).MustNotHaveHappened(); + } + + [Fact] + public void DisposesOnCancel() + { + A.CallTo(() => _dbConnection.State).Returns(ConnectionState.Open); + _sut.Begin(); + _sut.Cancel(); + A.CallTo(() => _dbConnection.Close()).MustNotHaveHappened(); + A.CallTo(() => _dbTransaction.Dispose()).MustHaveHappenedOnceExactly(); + } + + [Fact] + public void DoesNotAllowToChangeIsolationLevenWhenBegun() + { + _sut.SetIsolationLevel(IsolationLevel.ReadCommitted); + A.CallTo(() => _dbConnection.State).Returns(ConnectionState.Open); + _sut.Begin(); + Assert.Throws(() => _sut.SetIsolationLevel(IsolationLevel.Chaos)); + } + + [Fact] + public void DoesNotCommitbutRollbackOnCancel() + { + A.CallTo(() => _dbConnection.State).Returns(ConnectionState.Open); + _sut.Begin(); + A.CallTo(() => _dbConnection.Open()).MustNotHaveHappened(); + _sut.Cancel(); + A.CallTo(() => _dbTransaction.Commit()).MustNotHaveHappened(); + A.CallTo(() => _dbTransaction.Rollback()).MustHaveHappenedOnceExactly(); + } + + [Fact] + public void DoesNotMaintainConnectionStateOnCompleteWhenProvidingOpenConnection() + { + A.CallTo(() => _dbConnection.State).Returns(ConnectionState.Open); + + _sut.Begin(); + A.CallTo(() => _dbConnection.Open()).MustNotHaveHappened(); + _sut.Complete(); + A.CallTo(() => _dbConnection.Close()).MustNotHaveHappened(); + } + + [Fact] + public void DoesNotMaintainConnectionStateOnCancelWhenProvidingOpenConnection() + { + A.CallTo(() => _dbConnection.State).Returns(ConnectionState.Open); + + _sut.Begin(); + A.CallTo(() => _dbConnection.Open()).MustNotHaveHappened(); + _sut.Cancel(); + A.CallTo(() => _dbConnection.Close()).MustNotHaveHappened(); + } + + [Fact] + public void DoesNotRollbackButCommitOnComplete() + { + A.CallTo(() => _dbConnection.State).Returns(ConnectionState.Open); + _sut.Begin(); + A.CallTo(() => _dbConnection.Open()).MustNotHaveHappened(); + _sut.Complete(); + A.CallTo(() => _dbTransaction.Rollback()).MustNotHaveHappened(); + A.CallTo(() => _dbTransaction.Commit()).MustHaveHappenedOnceExactly(); + } + + [Fact] + public void InitializesWithoutCurrentTransaction() + { + Assert.Null(_sut.CurrentTransaction); + } + + [Fact] + public void MaintainsConnectionStateOnCompleteWhenProvidingClosedConnection() + { + A.CallTo(() => _dbConnection.State).Returns(ConnectionState.Closed); + _sut.Begin(); + A.CallTo(() => _dbConnection.Open()).MustHaveHappenedOnceExactly(); + _sut.Complete(); + A.CallTo(() => _dbConnection.Close()).MustHaveHappenedOnceExactly(); + + } + + [Fact] + public void MaintainsConnectionStateOnCancelWhenProvidingClosedConnection() + { + A.CallTo(() => _dbConnection.State).Returns(ConnectionState.Closed); + + _sut.Begin(); + A.CallTo(() => _dbConnection.Open()).MustHaveHappenedOnceExactly(); + _sut.Cancel(); + A.CallTo(() => _dbConnection.Close()).MustHaveHappenedOnceExactly(); + } + + [Fact] + public void UsesGivenConnection() + { + _sut.Begin(); + A.CallTo(() => _dbConnection.BeginTransaction(A._)).MustHaveHappenedOnceExactly(); + } + + [Fact] + public void DoesNotAllowBeginWhenActive() + { + _sut.Begin(); + Assert.Throws(() => _sut.Begin()); + } + + [Fact] + public void DoesNotAllowCompleteWhenNotStarted() + { + Assert.Throws(() => _sut.Complete()); + } + + [Fact] + public void DoesNotAllowCancelWhenNotStarted() + { + Assert.Throws(() => _sut.Cancel()); + } + + [Fact] + public void DoesNotAllowCancelWhenCompleted() + { + _sut.Begin(); + _sut.Complete(); + Assert.Throws(() => _sut.Cancel()); + } + + [Fact] + public void DoesNotAllowCompleteWhenRolledBack() + { + _sut.Begin(); + _sut.Cancel(); + Assert.Throws(() => _sut.Complete()); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/Transactions/TheReadonlyDecorator.cs b/tests/Backend.Fx.Tests/Patterns/Transactions/TheReadonlyDecorator.cs new file mode 100644 index 00000000..c2da7500 --- /dev/null +++ b/tests/Backend.Fx.Tests/Patterns/Transactions/TheReadonlyDecorator.cs @@ -0,0 +1,37 @@ +using Backend.Fx.Environment.Persistence; +using Backend.Fx.Patterns.DependencyInjection; +using FakeItEasy; +using Xunit; + +namespace Backend.Fx.Tests.Patterns.Transactions +{ + public class TheReadonlyDecorator + { + private readonly IOperation _operation; + private readonly IOperation _sut; + + public TheReadonlyDecorator() + { + _operation = A.Fake(); + _sut = new ReadonlyDbTransactionOperationDecorator(_operation); + } + [Fact] + public void DelegatesOtherCalls() + { + _sut.Begin(); + A.CallTo(() => _operation.Begin()).MustHaveHappenedOnceExactly(); + + _sut.Cancel(); + A.CallTo(() => _operation.Cancel()).MustHaveHappenedOnceExactly(); + } + + [Fact] + public void CancelsOperationOnComplete() + { + _sut.Begin(); + _sut.Complete(); + A.CallTo(() => _operation.Complete()).MustNotHaveHappened(); + A.CallTo(() => _operation.Cancel()).MustHaveHappenedOnceExactly(); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/UnitOfWork/TestUnitOfWork.cs b/tests/Backend.Fx.Tests/Patterns/UnitOfWork/TestUnitOfWork.cs deleted file mode 100644 index d739d419..00000000 --- a/tests/Backend.Fx.Tests/Patterns/UnitOfWork/TestUnitOfWork.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace Backend.Fx.Tests.Patterns.UnitOfWork -{ - using System; - using System.Security.Principal; - using Fx.Environment.DateAndTime; - using Fx.Patterns.DependencyInjection; - using Fx.Patterns.EventAggregation.Domain; - using Fx.Patterns.EventAggregation.Integration; - using Fx.Patterns.UnitOfWork; - - public class TestUnitOfWork : UnitOfWork - { - public int RollbackCount { get; private set; } - public int UpdateTrackingPropertiesCount { get; private set; } - public int CommitCount { get; private set; } - protected override void UpdateTrackingProperties(string userId, DateTime utcNow) - { - UpdateTrackingPropertiesCount++; - } - - public override void Complete() - { - CommitCount++; - base.Complete(); - } - - protected override void Dispose(bool disposing) - { - RollbackCount++; - base.Dispose(disposing); - } - - public TestUnitOfWork(IClock clock, ICurrentTHolder identityHolder, IDomainEventAggregator eventAggregator, IEventBusScope eventBusScope) - : base(clock, identityHolder, eventAggregator, eventBusScope) - { } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/UnitOfWork/TheReadonlyUnitOfWork.cs b/tests/Backend.Fx.Tests/Patterns/UnitOfWork/TheReadonlyUnitOfWork.cs deleted file mode 100644 index 601a48b2..00000000 --- a/tests/Backend.Fx.Tests/Patterns/UnitOfWork/TheReadonlyUnitOfWork.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Backend.Fx.Patterns.Transactions; -using FakeItEasy; -using Xunit; - -namespace Backend.Fx.Tests.Patterns.UnitOfWork -{ - public class TheReadonlyDecorator - { - [Fact] - public void RollsBackOnComplete() - { - var transactionContext = A.Fake(); - ITransactionContext sut = new ReadonlyDecorator(transactionContext); - sut.BeginTransaction(); - sut.CommitTransaction(); - A.CallTo(() => transactionContext.CommitTransaction()).MustNotHaveHappened(); - } - } -} diff --git a/tests/Backend.Fx.Tests/Patterns/UnitOfWork/TheUnitOfWork.cs b/tests/Backend.Fx.Tests/Patterns/UnitOfWork/TheUnitOfWork.cs deleted file mode 100644 index 4af988d7..00000000 --- a/tests/Backend.Fx.Tests/Patterns/UnitOfWork/TheUnitOfWork.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Xunit; - -namespace Backend.Fx.Tests.Patterns.UnitOfWork -{ - using FakeItEasy; - using Fx.Environment.Authentication; - using Fx.Environment.DateAndTime; - using Fx.Patterns.EventAggregation.Domain; - using Fx.Patterns.EventAggregation.Integration; - - public class TheUnitOfWork - { - private readonly IDomainEventAggregator _eventAggregator = A.Fake(); - private readonly IEventBusScope _eventBusScope = A.Fake(); - - [Fact] - public void RaisesDomainEventsOnComplete() - { - TestUnitOfWork sut = new TestUnitOfWork(new FrozenClock(), CurrentIdentityHolder.CreateSystem(), _eventAggregator, _eventBusScope); - sut.Begin(); - sut.Complete(); - sut.Dispose(); - A.CallTo(() => _eventAggregator.RaiseEvents()).MustHaveHappenedOnceExactly(); - } - - [Fact] - public void RaisesIntegrationEventsOnComplete() - { - TestUnitOfWork sut = new TestUnitOfWork(new FrozenClock(), CurrentIdentityHolder.CreateSystem(), _eventAggregator, _eventBusScope); - sut.Begin(); - sut.Complete(); - sut.Dispose(); - A.CallTo(() => _eventBusScope.RaiseEvents()).MustHaveHappenedOnceExactly(); - } - - [Fact] - public void RaisesNoDomainEventsOnDispose() - { - TestUnitOfWork sut = new TestUnitOfWork(new FrozenClock(), CurrentIdentityHolder.CreateSystem(), _eventAggregator, _eventBusScope); - sut.Begin(); - sut.Dispose(); - A.CallTo(() => _eventAggregator.RaiseEvents()).MustNotHaveHappened(); - } - - [Fact] - public void RaisesNoIntegrationEventsOnDispose() - { - TestUnitOfWork sut = new TestUnitOfWork(new FrozenClock(), CurrentIdentityHolder.CreateSystem(), _eventAggregator, _eventBusScope); - sut.Begin(); - sut.Dispose(); - A.CallTo(() => _eventBusScope.RaiseEvents()).MustNotHaveHappened(); - } - - [Fact] - public void UpdatesTrackingPropertiesOnFlush() - { - TestUnitOfWork sut = new TestUnitOfWork(new FrozenClock(), CurrentIdentityHolder.CreateSystem(), _eventAggregator, _eventBusScope); - sut.Begin(); - sut.Flush(); - Assert.Equal(1, sut.UpdateTrackingPropertiesCount); - } - } -} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/RandomData/TheGenerators.cs b/tests/Backend.Fx.Tests/RandomData/TheGenerators.cs new file mode 100644 index 00000000..ef57f838 --- /dev/null +++ b/tests/Backend.Fx.Tests/RandomData/TheGenerators.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq; +using Backend.Fx.RandomData; +using Xunit; + +namespace Backend.Fx.Tests.RandomData +{ + public class TheLandLineGenerator : TheGenerator{} + public class TheMobileLineGenerator : TheGenerator{} + public class TheTestAddressGenerator : TheGenerator{} + public class TheTestPersonGenerator : TheGenerator{} + + public class TheLoremIpsumGenerator : TheGenerator + { + [Fact] + public void GeneratesAsExpected() + { + var sentence = LoremIpsumGenerator.Generate(10, 10, true); + Assert.Equal(10, sentence.Split(" ").Length); + Assert.True(sentence.EndsWith('.')); + + sentence = LoremIpsumGenerator.Generate(10, 10, false); + Assert.Equal(10, sentence.Split(" ").Length); + Assert.False(sentence.EndsWith('.')); + } + } + + public abstract class TheGenerator where TGen : Generator, new() + { + private readonly TGen _sut; + + protected TheGenerator() + { + _sut = new TGen(); + } + + [Fact] + public void CanGenerateMany() + { + T[] generated = _sut.Take(100).ToArray(); + Assert.Equal(100, generated.Length); + Assert.Equal(100, generated.Distinct().Count()); + } + + [Fact] + public void GeneratesNotEqual() + { + T[] generated = _sut.Take(100).ToArray(); + Assert.Equal(100, generated.Distinct().Count()); + } + } +} \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/TestConfig.cs b/tests/Backend.Fx.Tests/TestConfig.cs index bc96fc52..a905d716 100644 --- a/tests/Backend.Fx.Tests/TestConfig.cs +++ b/tests/Backend.Fx.Tests/TestConfig.cs @@ -1,14 +1,17 @@ using Backend.Fx.NLogLogging; +using Backend.Fx.Tests; using MarcWittke.Xunit.AssemblyFixture; +using Xunit; -[assembly: Xunit.TestFramework("MarcWittke.Xunit.AssemblyFixture.XunitTestFrameworkWithAssemblyFixture", "MarcWittke.Xunit.AssemblyFixture")] -[assembly: AssemblyFixture(typeof(Backend.Fx.Tests.TestLoggingFixture))] +[assembly: TestFramework("MarcWittke.Xunit.AssemblyFixture.XunitTestFrameworkWithAssemblyFixture", "MarcWittke.Xunit.AssemblyFixture")] +[assembly: AssemblyFixture(typeof(TestLoggingFixture))] namespace Backend.Fx.Tests { public class TestLoggingFixture : LoggingFixture { public TestLoggingFixture() : base("Not.Important.Since.Backend.Fx.Is.Logging.By.Default") - { } + { + } } } \ No newline at end of file