diff --git a/src/EntityDb.Abstractions/Entities/IEntityRepository.cs b/src/EntityDb.Abstractions/Entities/IEntityRepository.cs index 8859d888..1f8251e4 100644 --- a/src/EntityDb.Abstractions/Entities/IEntityRepository.cs +++ b/src/EntityDb.Abstractions/Entities/IEntityRepository.cs @@ -1,5 +1,4 @@ -using EntityDb.Abstractions.Snapshots; -using EntityDb.Abstractions.Transactions; +using EntityDb.Abstractions.Transactions; using System; using System.Threading.Tasks; @@ -11,24 +10,25 @@ namespace EntityDb.Abstractions.Entities /// The type of the entity. public interface IEntityRepository : IDisposable, IAsyncDisposable { - /// - ITransactionRepository TransactionRepository { get; } - - /// - ISnapshotRepository? SnapshotRepository { get; } - /// - /// Returns a snapshot or default(). + /// Returns the most recent snapshot of a or default(). + /// + /// The id of the entity. + /// The most recent snapshot of a or constructs a new . + Task GetSnapshotOrDefault(Guid entityId); + + /// + /// Returns the current state of a or constructs a new . /// /// The id of the entity. - /// A snapshot or default(). - Task Get(Guid entityId); + /// The current state of a or constructs a new . + Task GetCurrentOrConstruct(Guid entityId); /// /// Inserts a single transaction with an atomic commit. /// /// The transaction. - /// true if the insert suceeded, or false if the insert failed. - Task Put(ITransaction transaction); + /// true if the insert succeeded, or false if the insert failed. + Task PutTransaction(ITransaction transaction); } } diff --git a/src/EntityDb.Abstractions/Entities/IEntityRepositoryFactory.cs b/src/EntityDb.Abstractions/Entities/IEntityRepositoryFactory.cs new file mode 100644 index 00000000..447e6b65 --- /dev/null +++ b/src/EntityDb.Abstractions/Entities/IEntityRepositoryFactory.cs @@ -0,0 +1,12 @@ +using EntityDb.Abstractions.Snapshots; +using EntityDb.Abstractions.Transactions; +using System.Threading.Tasks; + +namespace EntityDb.Abstractions.Entities +{ + public interface IEntityRepositoryFactory + { + Task> CreateRepository(ITransactionSessionOptions transactionSessionOptions, + ISnapshotSessionOptions? snapshotSessionOptions = null); + } +} diff --git a/src/EntityDb.Abstractions/Queries/Filters/ICommandFilter.cs b/src/EntityDb.Abstractions/Queries/Filters/ICommandFilter.cs new file mode 100644 index 00000000..8c294148 --- /dev/null +++ b/src/EntityDb.Abstractions/Queries/Filters/ICommandFilter.cs @@ -0,0 +1,18 @@ +using EntityDb.Abstractions.Queries.FilterBuilders; + +namespace EntityDb.Abstractions.Queries.Filters +{ + /// + /// Represents a type that supplies filtering for a . + /// + public interface ICommandFilter + { + /// + /// Returns a built from a command filter builder. + /// + /// The type of filter used by the repository. + /// The command filter builder. + /// A built from . + TFilter GetFilter(ICommandFilterBuilder builder); + } +} diff --git a/src/EntityDb.Abstractions/Queries/Filters/IFactFilter.cs b/src/EntityDb.Abstractions/Queries/Filters/IFactFilter.cs new file mode 100644 index 00000000..66cbd82e --- /dev/null +++ b/src/EntityDb.Abstractions/Queries/Filters/IFactFilter.cs @@ -0,0 +1,18 @@ +using EntityDb.Abstractions.Queries.FilterBuilders; + +namespace EntityDb.Abstractions.Queries.Filters +{ + /// + /// Represents a type that supplies filtering for a . + /// + public interface IFactFilter + { + /// + /// Returns a built from a fact filter builder. + /// + /// The type of filter used by the repository. + /// The fact filter builder. + /// A built from . + TFilter GetFilter(IFactFilterBuilder builder); + } +} diff --git a/src/EntityDb.Abstractions/Queries/Filters/ILeaseFilter.cs b/src/EntityDb.Abstractions/Queries/Filters/ILeaseFilter.cs new file mode 100644 index 00000000..ca48de3d --- /dev/null +++ b/src/EntityDb.Abstractions/Queries/Filters/ILeaseFilter.cs @@ -0,0 +1,18 @@ +using EntityDb.Abstractions.Queries.FilterBuilders; + +namespace EntityDb.Abstractions.Queries.Filters +{ + /// + /// Represents a type that supplies filtering for a . + /// + public interface ILeaseFilter + { + /// + /// Returns a built from a lease filter builder. + /// + /// The type of filter used by the repository. + /// The lease filter builder. + /// A built from . + TFilter GetFilter(ILeaseFilterBuilder builder); + } +} diff --git a/src/EntityDb.Abstractions/Queries/Filters/ISourceFilter.cs b/src/EntityDb.Abstractions/Queries/Filters/ISourceFilter.cs new file mode 100644 index 00000000..c3aa57c6 --- /dev/null +++ b/src/EntityDb.Abstractions/Queries/Filters/ISourceFilter.cs @@ -0,0 +1,18 @@ +using EntityDb.Abstractions.Queries.FilterBuilders; + +namespace EntityDb.Abstractions.Queries.Filters +{ + /// + /// Represents a type that supplies filtering for a . + /// + public interface ISourceFilter + { + /// + /// Returns a built from a source filter builder. + /// + /// The type of filter used by the repository. + /// The source filter builder. + /// A built from . + TFilter GetFilter(ISourceFilterBuilder builder); + } +} diff --git a/src/EntityDb.Abstractions/Queries/Filters/ITagFilter.cs b/src/EntityDb.Abstractions/Queries/Filters/ITagFilter.cs new file mode 100644 index 00000000..ab019eae --- /dev/null +++ b/src/EntityDb.Abstractions/Queries/Filters/ITagFilter.cs @@ -0,0 +1,18 @@ +using EntityDb.Abstractions.Queries.FilterBuilders; + +namespace EntityDb.Abstractions.Queries.Filters +{ + /// + /// Represents a type that supplies filtering for a . + /// + public interface ITagFilter + { + /// + /// Returns a built from a tag filter builder. + /// + /// The type of filter used by the repository. + /// The tag filter builder. + /// A built from . + TFilter GetFilter(ITagFilterBuilder builder); + } +} diff --git a/src/EntityDb.Abstractions/Queries/ICommandQuery.cs b/src/EntityDb.Abstractions/Queries/ICommandQuery.cs index 651fbe32..8807a922 100644 --- a/src/EntityDb.Abstractions/Queries/ICommandQuery.cs +++ b/src/EntityDb.Abstractions/Queries/ICommandQuery.cs @@ -1,4 +1,4 @@ -using EntityDb.Abstractions.Queries.FilterBuilders; +using EntityDb.Abstractions.Queries.Filters; using EntityDb.Abstractions.Queries.SortBuilders; namespace EntityDb.Abstractions.Queries @@ -6,16 +6,8 @@ namespace EntityDb.Abstractions.Queries /// /// Abstracts a query on commands. /// - public interface ICommandQuery : IQuery + public interface ICommandQuery : IQuery, ICommandFilter { - /// - /// Returns a built from a command filter builder. - /// - /// The type of filter used by the repository. - /// The command filter builder. - /// A built from . - TFilter GetFilter(ICommandFilterBuilder builder); - /// /// Returns a built from a command sort builder. /// diff --git a/src/EntityDb.Abstractions/Queries/IFactQuery.cs b/src/EntityDb.Abstractions/Queries/IFactQuery.cs index 09d080d4..d9b3a6c6 100644 --- a/src/EntityDb.Abstractions/Queries/IFactQuery.cs +++ b/src/EntityDb.Abstractions/Queries/IFactQuery.cs @@ -1,4 +1,4 @@ -using EntityDb.Abstractions.Queries.FilterBuilders; +using EntityDb.Abstractions.Queries.Filters; using EntityDb.Abstractions.Queries.SortBuilders; namespace EntityDb.Abstractions.Queries @@ -6,16 +6,8 @@ namespace EntityDb.Abstractions.Queries /// /// Abstracts a query on facts. /// - public interface IFactQuery : IQuery + public interface IFactQuery : IQuery, IFactFilter { - /// - /// Returns a built from a fact filter builder. - /// - /// The type of filter used by the repository. - /// The fact filter builder. - /// A built from . - TFilter GetFilter(IFactFilterBuilder builder); - /// /// Returns a built from a fact sort builder. /// diff --git a/src/EntityDb.Abstractions/Queries/ILeaseQuery.cs b/src/EntityDb.Abstractions/Queries/ILeaseQuery.cs index 4d32e7c9..46b901cb 100644 --- a/src/EntityDb.Abstractions/Queries/ILeaseQuery.cs +++ b/src/EntityDb.Abstractions/Queries/ILeaseQuery.cs @@ -1,4 +1,4 @@ -using EntityDb.Abstractions.Queries.FilterBuilders; +using EntityDb.Abstractions.Queries.Filters; using EntityDb.Abstractions.Queries.SortBuilders; namespace EntityDb.Abstractions.Queries @@ -6,16 +6,8 @@ namespace EntityDb.Abstractions.Queries /// /// Abstracts a query on leases. /// - public interface ILeaseQuery : IQuery + public interface ILeaseQuery : IQuery, ILeaseFilter { - /// - /// Returns a built from a lease filter builder. - /// - /// The type of filter used by the repository. - /// The lease filter builder. - /// A built from . - TFilter GetFilter(ILeaseFilterBuilder builder); - /// /// Returns a built from a lease sort builder. /// diff --git a/src/EntityDb.Abstractions/Queries/ISourceQuery.cs b/src/EntityDb.Abstractions/Queries/ISourceQuery.cs index 9edd8f42..cf63aad5 100644 --- a/src/EntityDb.Abstractions/Queries/ISourceQuery.cs +++ b/src/EntityDb.Abstractions/Queries/ISourceQuery.cs @@ -1,4 +1,4 @@ -using EntityDb.Abstractions.Queries.FilterBuilders; +using EntityDb.Abstractions.Queries.Filters; using EntityDb.Abstractions.Queries.SortBuilders; namespace EntityDb.Abstractions.Queries @@ -6,16 +6,8 @@ namespace EntityDb.Abstractions.Queries /// /// Abstracts a query on sources. /// - public interface ISourceQuery : IQuery + public interface ISourceQuery : IQuery, ISourceFilter { - /// - /// Returns a built from a source filter builder. - /// - /// The type of filter used by the repository. - /// The source filter builder. - /// A built from . - TFilter GetFilter(ISourceFilterBuilder builder); - /// /// Returns a built from a source sort builder. /// diff --git a/src/EntityDb.Abstractions/Queries/ITagQuery.cs b/src/EntityDb.Abstractions/Queries/ITagQuery.cs index c8329c3d..a9be79e2 100644 --- a/src/EntityDb.Abstractions/Queries/ITagQuery.cs +++ b/src/EntityDb.Abstractions/Queries/ITagQuery.cs @@ -1,4 +1,4 @@ -using EntityDb.Abstractions.Queries.FilterBuilders; +using EntityDb.Abstractions.Queries.Filters; using EntityDb.Abstractions.Queries.SortBuilders; namespace EntityDb.Abstractions.Queries @@ -6,16 +6,8 @@ namespace EntityDb.Abstractions.Queries /// /// Abstracts a query on tags. /// - public interface ITagQuery : IQuery + public interface ITagQuery : IQuery, ITagFilter { - /// - /// Returns a built from a tag filter builder. - /// - /// The type of filter used by the repository. - /// The tag filter builder. - /// A built from . - TFilter GetFilter(ITagFilterBuilder builder); - /// /// Returns a built from a tag sort builder. /// diff --git a/src/EntityDb.Abstractions/Strategies/IAuthorizingStrategy.cs b/src/EntityDb.Abstractions/Strategies/IAuthorizingStrategy.cs index 520feba2..45a0463f 100644 --- a/src/EntityDb.Abstractions/Strategies/IAuthorizingStrategy.cs +++ b/src/EntityDb.Abstractions/Strategies/IAuthorizingStrategy.cs @@ -14,8 +14,7 @@ public interface IAuthorizingStrategy /// /// The entity. /// The command. - /// The agent. /// true if execution is authorized, or false if execution is not authorized. - bool IsAuthorized(TEntity entity, ICommand command, IAgent agent); + bool IsAuthorized(TEntity entity, ICommand command); } } diff --git a/src/EntityDb.Abstractions/Strategies/ISnapshottingStrategy.cs b/src/EntityDb.Abstractions/Strategies/ISnapshottingStrategy.cs index 6013596f..9c40a01f 100644 --- a/src/EntityDb.Abstractions/Strategies/ISnapshottingStrategy.cs +++ b/src/EntityDb.Abstractions/Strategies/ISnapshottingStrategy.cs @@ -4,7 +4,7 @@ /// Represents a type used to determine if the next version of an entity should be put into snapshot storage. /// /// The type of the entity that can be put into snapshot storage. - public interface ISnapshottingStrategy + public interface ISnapshottingStrategy { /// /// Determines if the next version of an entity should be put into snapshot storage. diff --git a/src/EntityDb.Abstractions/Transactions/ITransactionCommand.cs b/src/EntityDb.Abstractions/Transactions/ITransactionCommand.cs index 68cb7399..661c0014 100644 --- a/src/EntityDb.Abstractions/Transactions/ITransactionCommand.cs +++ b/src/EntityDb.Abstractions/Transactions/ITransactionCommand.cs @@ -12,11 +12,6 @@ namespace EntityDb.Abstractions.Transactions /// The type of entity to be modified. public interface ITransactionCommand { - /// - /// A snapshot of the entity before the command. - /// - TEntity? PreviousSnapshot { get; } - /// /// A snapshot of the entity after the command. /// diff --git a/src/EntityDb.Abstractions/Transactions/ITransactionSubscriber.cs b/src/EntityDb.Abstractions/Transactions/ITransactionSubscriber.cs new file mode 100644 index 00000000..5a5ce246 --- /dev/null +++ b/src/EntityDb.Abstractions/Transactions/ITransactionSubscriber.cs @@ -0,0 +1,15 @@ +namespace EntityDb.Abstractions.Transactions +{ + /// + /// + /// + /// + public interface ITransactionSubscriber + { + /// + /// Foo + /// + /// + void Notify(ITransaction transaction); + } +} diff --git a/src/EntityDb.Common.Tests/Extensions/IServiceProviderExtensionsTests.cs b/src/EntityDb.Common.Tests/Extensions/IServiceProviderExtensionsTests.cs deleted file mode 100644 index eb7a871b..00000000 --- a/src/EntityDb.Common.Tests/Extensions/IServiceProviderExtensionsTests.cs +++ /dev/null @@ -1,64 +0,0 @@ -using EntityDb.Common.Extensions; -using EntityDb.TestImplementations.Commands; -using EntityDb.TestImplementations.Entities; -using Shouldly; -using System; -using Xunit; - -namespace EntityDb.Common.Tests.Extensions -{ - public class IServiceProviderExtensionsTests : TestsBase - { - public IServiceProviderExtensionsTests(IServiceProvider serviceProvider) : base(serviceProvider) - { - } - - [Fact] - public void GivenNoLeasingStrategy_WhenGettingLeases_ThenReturnEmptyArray() - { - // ARRANGE - - var serviceProvider = GetEmptyServiceProvider(); - - // ACT - - var leases = serviceProvider.GetLeases(new TransactionEntity()); - - // ASSERT - - leases.ShouldBeEmpty(); - } - - [Fact] - public void GivenNoAuthorizingStrategy_ThenIsAuthorizedReturnsTrue() - { - // ARRANGE - - var serviceProvider = GetEmptyServiceProvider(); - - // ACT - - var isAuthorized = serviceProvider.IsAuthorized(new TransactionEntity(), new DoNothing()); - - // ASSERT - - isAuthorized.ShouldBeTrue(); - } - - [Fact] - public void GivenNoCachingStrategy_WhenCheckingIfShouldCache_ThenReturnFalse() - { - // ARRANGE - - var serviceProvider = GetEmptyServiceProvider(); - - // ACT - - var shouldCache = serviceProvider.ShouldPutSnapshot(default, default!); - - // ASSERT - - shouldCache.ShouldBeFalse(); - } - } -} diff --git a/src/EntityDb.Common.Tests/Projections/ProjectionServiceProviderTests.cs b/src/EntityDb.Common.Tests/Projections/ProjectionServiceProviderTests.cs deleted file mode 100644 index 5a7c969a..00000000 --- a/src/EntityDb.Common.Tests/Projections/ProjectionServiceProviderTests.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace EntityDb.Common.Tests.Projections -{ - public class ProjectionServiceProviderTests - { - } -} diff --git a/src/EntityDb.Common.Tests/SnapshotTransactions/SnapshotTransactionsTestsBase.cs b/src/EntityDb.Common.Tests/SnapshotTransactions/SnapshotTransactionsTestsBase.cs index 693aa4e3..ff983c94 100644 --- a/src/EntityDb.Common.Tests/SnapshotTransactions/SnapshotTransactionsTestsBase.cs +++ b/src/EntityDb.Common.Tests/SnapshotTransactions/SnapshotTransactionsTestsBase.cs @@ -1,7 +1,6 @@ using EntityDb.Abstractions.Entities; using EntityDb.Abstractions.Strategies; using EntityDb.Abstractions.Transactions; -using EntityDb.Common.Extensions; using EntityDb.Common.Snapshots; using EntityDb.Common.Transactions; using EntityDb.TestImplementations.Commands; @@ -25,7 +24,7 @@ protected SnapshotTransactionsTestsBase(IServiceProvider serviceProvider) : base private static async Task> BuildTransaction(Guid entityId, ulong from, ulong to, IServiceProvider serviceProvider, IEntityRepository? entityRepository = null) { - var transactionBuilder = serviceProvider.GetTransactionBuilder(); + var transactionBuilder = serviceProvider.GetRequiredService>(); if (entityRepository != null) { @@ -46,14 +45,14 @@ private static async Task> BuildTransaction(Guid [Theory] [InlineData(10, 20)] - public async Task GivenCachingOnNthVersion_WhenPuttingTransactionWithNthVersion_ThenSnapshotExistsAtNthVersion( + public async Task GivenSnapshottingOnNthVersion_WhenPuttingTransactionWithNthVersion_ThenSnapshotExistsAtNthVersion( ulong expectedSnapshotVersion, ulong expectedCurrentVersion) { // ARRANGE - var cachingStrategyMock = new Mock>(MockBehavior.Strict); + var snapshottingStrategyMock = new Mock>(MockBehavior.Strict); - cachingStrategyMock + snapshottingStrategyMock .Setup(strategy => strategy.ShouldPutSnapshot(It.IsAny(), It.IsAny())) .Returns((TransactionEntity? _, TransactionEntity nextEntity) => @@ -61,29 +60,29 @@ public async Task GivenCachingOnNthVersion_WhenPuttingTransactionWithNthVersion_ var serviceProvider = GetServiceProviderWithOverrides(serviceCollection => { - serviceCollection.AddSingleton(_ => cachingStrategyMock.Object); + serviceCollection.AddSingleton(_ => snapshottingStrategyMock.Object); }); var entityId = Guid.NewGuid(); await using var entityRepository = - await serviceProvider.CreateEntityRepository(new TransactionSessionOptions(), + await serviceProvider.GetRequiredService>().CreateRepository(new TransactionSessionOptions(), new SnapshotSessionOptions()); var firstTransaction = await BuildTransaction(entityId, 1, expectedSnapshotVersion, serviceProvider); - await entityRepository.Put(firstTransaction); + await entityRepository.PutTransaction(firstTransaction); var secondTransaction = await BuildTransaction(entityId, expectedSnapshotVersion, expectedCurrentVersion, serviceProvider, entityRepository); - await entityRepository.Put(secondTransaction); + await entityRepository.PutTransaction(secondTransaction); // ACT - var current = await entityRepository.Get(entityId); + var current = await entityRepository.GetCurrentOrConstruct(entityId); - var snapshot = await entityRepository.SnapshotRepository!.GetSnapshot(entityId); + var snapshot = await entityRepository.GetSnapshotOrDefault(entityId); // ASSERT diff --git a/src/EntityDb.Common.Tests/Snapshots/SnapshotTestsBase.cs b/src/EntityDb.Common.Tests/Snapshots/SnapshotTestsBase.cs index 7e83520a..8eaa8809 100644 --- a/src/EntityDb.Common.Tests/Snapshots/SnapshotTestsBase.cs +++ b/src/EntityDb.Common.Tests/Snapshots/SnapshotTestsBase.cs @@ -1,4 +1,4 @@ -using EntityDb.Common.Extensions; +using EntityDb.Abstractions.Snapshots; using EntityDb.Common.Snapshots; using EntityDb.TestImplementations.Entities; using Shouldly; @@ -10,11 +10,11 @@ namespace EntityDb.Common.Tests.Snapshots { public abstract class SnapshotTestsBase { - private readonly IServiceProvider _serviceProvider; + private readonly ISnapshotRepositoryFactory _snapshotRepositoryFactory; - public SnapshotTestsBase(IServiceProvider serviceProvider) + public SnapshotTestsBase(ISnapshotRepositoryFactory snapshotRepositoryFactory) { - _serviceProvider = serviceProvider; + _snapshotRepositoryFactory = snapshotRepositoryFactory; } [Fact] @@ -27,7 +27,7 @@ public async Task GivenEmptySnapshotRepository_WhenGoingThroughFullCycle_ThenOri var entityId = Guid.NewGuid(); await using var snapshotRepository = - await _serviceProvider.CreateSnapshotRepository(new SnapshotSessionOptions()); + await _snapshotRepositoryFactory.CreateRepository(new SnapshotSessionOptions()); // ACT diff --git a/src/EntityDb.Common.Tests/Startup.cs b/src/EntityDb.Common.Tests/Startup.cs index de692f91..94392498 100644 --- a/src/EntityDb.Common.Tests/Startup.cs +++ b/src/EntityDb.Common.Tests/Startup.cs @@ -1,6 +1,7 @@ using EntityDb.Common.Extensions; using EntityDb.TestImplementations.Agents; using EntityDb.TestImplementations.Entities; +using EntityDb.TestImplementations.Strategies; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Xunit.DependencyInjection; @@ -23,8 +24,8 @@ public void ConfigureServices(IServiceCollection serviceCollection) serviceCollection.AddAgentAccessor(); - serviceCollection.AddConstructingStrategy(); - serviceCollection.AddVersionedEntityVersioningStrategy(); + serviceCollection.AddEntity(); + serviceCollection.AddLeasedEntityLeasingStrategy(); serviceCollection.AddAuthorizedEntityAuthorizingStrategy(); } diff --git a/src/EntityDb.Common.Tests/TestsBase.cs b/src/EntityDb.Common.Tests/TestsBase.cs index 84e2debc..9eb467be 100644 --- a/src/EntityDb.Common.Tests/TestsBase.cs +++ b/src/EntityDb.Common.Tests/TestsBase.cs @@ -2,8 +2,13 @@ using EntityDb.Abstractions.Queries; using EntityDb.Abstractions.Transactions; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Moq; using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; using System.Threading.Tasks; namespace EntityDb.Common.Tests @@ -17,18 +22,52 @@ public TestsBase(IServiceProvider serviceProvider) _serviceProvider = serviceProvider; } - public IServiceProvider GetEmptyServiceProvider() + private IServiceCollection GetParaisteServiceCollection(Type[]? omittedTypes = null) { - return new ServiceCollection().BuildServiceProvider(); - } + var serviceCollection = new ServiceCollection(); + + // Use reflection to get all service descriptors + + var engine = _serviceProvider.GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Single(x => x.Name == "Engine") + .GetValue(_serviceProvider); + + var callSiteFactory = engine!.GetType() + .GetProperties(BindingFlags.NonPublic | BindingFlags.Instance) + .Single(x => x.Name == "CallSiteFactory") + .GetValue(engine); + + var descriptors = (callSiteFactory!.GetType() + .GetFields(BindingFlags.NonPublic | BindingFlags.Instance) + .Single(x => x.Name == "_descriptors") + .GetValue(callSiteFactory) as List)!; + + foreach (var descriptor in descriptors) + { + if (omittedTypes?.Contains(descriptor.ServiceType) == true) + { + continue; + } + + serviceCollection.Add(descriptor); + } + return serviceCollection; + } + public IServiceProvider GetServiceProviderWithOverrides(Action configureOverrides) { - var overrideServiceCollection = new ServiceCollection(); + var serviceCollection = GetParaisteServiceCollection(); - configureOverrides.Invoke(overrideServiceCollection); + configureOverrides.Invoke(serviceCollection); - return new ServiceProviderWithOverrides(overrideServiceCollection.BuildServiceProvider(), _serviceProvider); + return serviceCollection.BuildServiceProvider(); + } + + public IServiceProvider GetServiceProviderWithOmission() + { + return GetParaisteServiceCollection(new[] { typeof(TOmittedType) }).BuildServiceProvider(); } public static ITransactionRepositoryFactory GetMockedTransactionRepositoryFactory( @@ -63,14 +102,5 @@ public static ITransactionRepositoryFactory GetMockedTransactionReposit return transactionRepositoryFactoryMock.Object; } - - private record ServiceProviderWithOverrides(IServiceProvider OverrideServiceProvider, - IServiceProvider ServiceProvider) : IServiceProvider - { - public object? GetService(Type serviceType) - { - return OverrideServiceProvider.GetService(serviceType) ?? ServiceProvider.GetService(serviceType); - } - } } } diff --git a/src/EntityDb.Common.Tests/Transactions/TransactionBuilderTests.cs b/src/EntityDb.Common.Tests/Transactions/TransactionBuilderTests.cs index 020b180e..b391eaaf 100644 --- a/src/EntityDb.Common.Tests/Transactions/TransactionBuilderTests.cs +++ b/src/EntityDb.Common.Tests/Transactions/TransactionBuilderTests.cs @@ -1,10 +1,13 @@ using EntityDb.Abstractions.Agents; using EntityDb.Abstractions.Commands; +using EntityDb.Abstractions.Entities; using EntityDb.Abstractions.Facts; using EntityDb.Abstractions.Strategies; +using EntityDb.Common.Entities; using EntityDb.Common.Exceptions; using EntityDb.Common.Extensions; using EntityDb.Common.Facts; +using EntityDb.Common.Transactions; using EntityDb.TestImplementations.Commands; using EntityDb.TestImplementations.Entities; using EntityDb.TestImplementations.Facts; @@ -22,6 +25,24 @@ public class TransactionBuilderTests : TestsBase public TransactionBuilderTests(IServiceProvider serviceProvider) : base(serviceProvider) { } + + [Fact] + public void GivenNoAuthorizingStrategy_WhenExecutingUnauthorizedCommand_ThenBuildSucceeds() + { + // ARRANGE + + var serviceProvider = GetServiceProviderWithOmission>(); + + var transactionBuilder = serviceProvider.GetRequiredService>(); + + var entityId = Guid.NewGuid(); + + // ACT & ASSERT + + Should.NotThrow(() => transactionBuilder + .Create(entityId, new SetRole("Foo")) + .Append(entityId, new DoNothing())); + } [Fact] public async Task GivenExistingEntityId_WhenExecutingUnauthorizedCommand_ThenAppendThrows() @@ -34,7 +55,7 @@ public async Task GivenExistingEntityId_WhenExecutingUnauthorizedCommand_ThenApp authorizingStrategyMock .Setup(strategy => strategy.IsAuthorized(It.IsAny(), - It.IsAny>(), It.IsAny())) + It.IsAny>())) .Returns(false); var authorizingStrategy = authorizingStrategyMock.Object; @@ -50,10 +71,10 @@ public async Task GivenExistingEntityId_WhenExecutingUnauthorizedCommand_ThenApp serviceCollection.AddScoped(_ => authorizingStrategy); }); - var transactionBuilder = serviceProvider.GetTransactionBuilder(); + var transactionBuilder = serviceProvider.GetRequiredService>(); await using var entityRepository = - await serviceProvider.CreateEntityRepository(default!); + await serviceProvider.GetRequiredService>().CreateRepository(default!); // ACT @@ -78,7 +99,7 @@ public void GivenNonExistingEntityId_WhenExecutingUnauthorizedCommand_ThenCreate authorizingStrategyMock .Setup(strategy => strategy.IsAuthorized(It.IsAny(), - It.IsAny>(), It.IsAny())) + It.IsAny>())) .Returns(false); var authorizingStrategy = authorizingStrategyMock.Object; @@ -91,7 +112,7 @@ public void GivenNonExistingEntityId_WhenExecutingUnauthorizedCommand_ThenCreate serviceCollection.AddScoped(_ => authorizingStrategy); }); - var transactionBuilder = serviceProvider.GetTransactionBuilder(); + var transactionBuilder = serviceProvider.GetRequiredService>(); // ASSERT @@ -114,7 +135,7 @@ public void GivenNonExistingEntityId_WhenUsingEntityIdForAppend_ThenAppendThrows GetMockedTransactionRepositoryFactory()); }); - var transactionBuilder = serviceProvider.GetTransactionBuilder(); + var transactionBuilder = serviceProvider.GetRequiredService>(); // ASSERT @@ -137,7 +158,7 @@ public void GivenExistingEntityId_WhenUsingEntityIdForCreate_ThenCreateThrows() GetMockedTransactionRepositoryFactory()); }); - var transactionBuilder = serviceProvider.GetTransactionBuilder(); + var transactionBuilder = serviceProvider.GetRequiredService>(); // ACT @@ -151,6 +172,48 @@ public void GivenExistingEntityId_WhenUsingEntityIdForCreate_ThenCreateThrows() }); } + [Fact] + public void GivenLeasingStrategy_WhenBuildingNewEntityWithLease_ThenTransactionDoesInsertLeases() + { + // ARRANGE + + var transactionBuilder = _serviceProvider.GetRequiredService>(); + + // ACT + + var transaction = transactionBuilder + .Create(default, new AddLease(default!, default!, default!)) + .Build(default, default!); + + // ASSERT + + transaction.Commands.Length.ShouldBe(1); + + transaction.Commands[0].Leases.Insert.ShouldNotBeEmpty(); + } + + [Fact] + public void GivenNoLeasingStrategy_WhenBuildingNewEntityWithLease_ThenTransactionDoesNotInsertLeases() + { + // ARRANGE + + var serviceProvider = GetServiceProviderWithOmission>(); + + var transactionBuilder = serviceProvider.GetRequiredService>(); + + // ACT + + var transaction = transactionBuilder + .Create(default, new AddLease(default!, default!, default!)) + .Build(default, default!); + + // ASSERT + + transaction.Commands.Length.ShouldBe(1); + + transaction.Commands[0].Leases.Insert.ShouldBeEmpty(); + } + [Fact] public async Task GivenExistingEntityId_WhenUsingEntityIdForLoadTwice_ThenLoadThrows() { @@ -165,10 +228,10 @@ public async Task GivenExistingEntityId_WhenUsingEntityIdForLoadTwice_ThenLoadTh new IFact[] { new VersionNumberSet(1) })); }); - var transactionBuilder = serviceProvider.GetTransactionBuilder(); + var transactionBuilder = serviceProvider.GetRequiredService>(); await using var entityRepository = - await serviceProvider.CreateEntityRepository(default!); + await serviceProvider.GetRequiredService>().CreateRepository(default!); // ACT @@ -193,10 +256,10 @@ public async Task GivenNonExistentEntityId_WhenLoadingEntity_ThenLoadThrows() GetMockedTransactionRepositoryFactory()); }); - var transactionBuilder = serviceProvider.GetTransactionBuilder(); + var transactionBuilder = serviceProvider.GetRequiredService>(); await using var entityRepository = - await serviceProvider.CreateEntityRepository(default!); + await serviceProvider.GetRequiredService>().CreateRepository(default!); // ASSERT @@ -221,7 +284,7 @@ public void GivenNonExistingEntityId_WhenUsingValidVersioningStrategy_ThenVersio GetMockedTransactionRepositoryFactory()); }); - var transactionBuilder = serviceProvider.GetTransactionBuilder(); + var transactionBuilder = serviceProvider.GetRequiredService>(); // ACT @@ -258,10 +321,10 @@ public async Task GivenExistingEntity_WhenAppendingNewCommand_ThenTransactionBui })); }); - var transactionBuilder = serviceProvider.GetTransactionBuilder(); + var transactionBuilder = serviceProvider.GetRequiredService>(); await using var entityRepository = - await serviceProvider.CreateEntityRepository(default!); + await serviceProvider.GetRequiredService>().CreateRepository(default!); // ACT diff --git a/src/EntityDb.Common.Tests/Transactions/TransactionTestsBase.cs b/src/EntityDb.Common.Tests/Transactions/TransactionTestsBase.cs index 874e3226..2d270dc1 100644 --- a/src/EntityDb.Common.Tests/Transactions/TransactionTestsBase.cs +++ b/src/EntityDb.Common.Tests/Transactions/TransactionTestsBase.cs @@ -3,6 +3,7 @@ using EntityDb.Abstractions.Leases; using EntityDb.Abstractions.Loggers; using EntityDb.Abstractions.Queries; +using EntityDb.Abstractions.Strategies; using EntityDb.Abstractions.Tags; using EntityDb.Abstractions.Transactions; using EntityDb.Common.Entities; @@ -20,6 +21,7 @@ using EntityDb.TestImplementations.Queries; using EntityDb.TestImplementations.Source; using EntityDb.TestImplementations.Tags; +using Microsoft.Extensions.DependencyInjection; using Moq; using Shouldly; using System; @@ -43,7 +45,7 @@ protected TransactionTestsBase(IServiceProvider serviceProvider) private Task> CreateRepository(bool readOnly = false, bool tolerateLag = false, ILogger? loggerOverride = null) { - return _serviceProvider.CreateTransactionRepository(new TransactionSessionOptions + return _serviceProvider.GetRequiredService>().CreateRepository(new TransactionSessionOptions { ReadOnly = readOnly, SecondaryPreferred = tolerateLag, LoggerOverride = loggerOverride }); @@ -472,7 +474,7 @@ private Task TestGetTags(ITagQuery query, List> private ITransaction BuildTransaction(Guid transactionId, Guid entityId, object source, ICommand[] commands, DateTime? timeStampOverride = null) { - var transactionBuilder = _serviceProvider.GetTransactionBuilder(); + var transactionBuilder = _serviceProvider.GetRequiredService>(); transactionBuilder.Create(entityId, commands[0]); @@ -507,7 +509,6 @@ public async Task GivenReadOnlyMode_WhenPuttingTransaction_ThenThrow() { new TransactionCommand { - PreviousSnapshot = default, NextSnapshot = default!, EntityId = Guid.NewGuid(), ExpectedPreviousVersionNumber = 0, @@ -545,7 +546,6 @@ static ITransaction NewTransaction(Guid transactionId) { new TransactionCommand { - PreviousSnapshot = default, NextSnapshot = default!, EntityId = Guid.NewGuid(), ExpectedPreviousVersionNumber = 0, @@ -588,7 +588,6 @@ public async Task GivenNonUniqueVersionNumbers_WhenInsertingCommands_ThenReturnF { new TransactionCommand { - PreviousSnapshot = default, NextSnapshot = default!, EntityId = entityId, ExpectedPreviousVersionNumber = previousVersionNumber, @@ -599,7 +598,6 @@ public async Task GivenNonUniqueVersionNumbers_WhenInsertingCommands_ThenReturnF }, new TransactionCommand { - PreviousSnapshot = default, NextSnapshot = default!, EntityId = entityId, ExpectedPreviousVersionNumber = previousVersionNumber, @@ -643,7 +641,6 @@ static ITransaction NewTransaction(Guid entityId, ulong previ { new TransactionCommand { - PreviousSnapshot = default, NextSnapshot = default!, EntityId = entityId, ExpectedPreviousVersionNumber = previousVersionNumber, @@ -696,7 +693,6 @@ public async Task GivenNonUniqueSubversionNumbers_WhenInsertingFacts_ThenReturnF { new TransactionCommand { - PreviousSnapshot = default, NextSnapshot = default!, EntityId = entityId, ExpectedPreviousVersionNumber = 0, @@ -745,7 +741,6 @@ public async Task GivenNonUniqueTags_WhenInsertingTagDocuments_ThenReturnTrue() { new TransactionCommand { - PreviousSnapshot = default, NextSnapshot = default!, EntityId = Guid.NewGuid(), ExpectedPreviousVersionNumber = 0, @@ -759,7 +754,6 @@ public async Task GivenNonUniqueTags_WhenInsertingTagDocuments_ThenReturnTrue() }, new TransactionCommand { - PreviousSnapshot = default, NextSnapshot = default!, EntityId = Guid.NewGuid(), ExpectedPreviousVersionNumber = 0, @@ -801,7 +795,6 @@ public async Task GivenNonUniqueLeases_WhenInsertingLeaseDocuments_ThenReturnFal { new TransactionCommand { - PreviousSnapshot = default, NextSnapshot = default!, EntityId = Guid.NewGuid(), ExpectedPreviousVersionNumber = 0, @@ -815,7 +808,6 @@ public async Task GivenNonUniqueLeases_WhenInsertingLeaseDocuments_ThenReturnFal }, new TransactionCommand { - PreviousSnapshot = default, NextSnapshot = default!, EntityId = Guid.NewGuid(), ExpectedPreviousVersionNumber = 0, @@ -852,7 +844,7 @@ public async Task GivenEntityInserted_WhenGettingEntity_ThenReturnEntity() await using var transactionRepository = await CreateRepository(); - var entityRepository = new EntityRepository(_serviceProvider, transactionRepository); + var entityRepository = EntityRepository.Create(_serviceProvider, transactionRepository); var transaction = BuildTransaction(Guid.NewGuid(), entityId, new NoSource(), new ICommand[] { new DoNothing() }); @@ -861,7 +853,7 @@ public async Task GivenEntityInserted_WhenGettingEntity_ThenReturnEntity() // ACT - var actualEntity = await entityRepository.Get(entityId); + var actualEntity = await entityRepository.GetCurrentOrConstruct(entityId); // ASSERT @@ -873,7 +865,7 @@ public async Task GivenEntityInsertedWithTags_WhenRemovingAllTags_ThenFinalEntit { // ARRANGE - var transactionBuilder = _serviceProvider.GetTransactionBuilder(); + var transactionBuilder = _serviceProvider.GetRequiredService>(); var expectedInitialTags = new[] { new Tag("Foo", "Bar") }.ToImmutableArray(); @@ -913,7 +905,7 @@ public async Task GivenEntityInsertedWithLeases_WhenRemovingAllLeases_ThenFinalE { // ARRANGE - var transactionBuilder = _serviceProvider.GetTransactionBuilder(); + var transactionBuilder = _serviceProvider.GetRequiredService>(); var expectedInitialLeases = new[] { new Lease("Foo", "Bar", "Baz") }.ToImmutableArray(); @@ -956,13 +948,13 @@ public async Task GivenTransactionCreatesEntity_WhenQueryingForVersionOne_ThenRe var expectedCommand = new Count(1); var transaction = _serviceProvider - .GetTransactionBuilder() + .GetRequiredService>() .Create(Guid.NewGuid(), expectedCommand) .Build(Guid.NewGuid(), new NoSource()); var versionOneCommandQuery = new EntityVersionNumberQuery(1, 1); - using var transactionRepository = await CreateRepository(); + await using var transactionRepository = await CreateRepository(); // ACT @@ -991,7 +983,7 @@ public async Task var entityId = Guid.NewGuid(); - var transactionBuilder = _serviceProvider.GetTransactionBuilder(); + var transactionBuilder = _serviceProvider.GetRequiredService>(); var firstTransaction = transactionBuilder .Create(entityId, new Count(1)) @@ -1003,7 +995,7 @@ public async Task var versionTwoCommandQuery = new EntityVersionNumberQuery(2, 2); - using var transactionRepository = await CreateRepository(); + await using var transactionRepository = await CreateRepository(); await transactionRepository.PutTransaction(firstTransaction); @@ -1051,7 +1043,7 @@ public async Task GivenTransactionAlreadyInserted_WhenQueryingByTransactionTimeS var commands = new ICommand[] { new Count(i) }; - var facts = new[] { new Counted(i), _serviceProvider.GetVersionNumberFact(1) }; + var facts = new[] { new Counted(i), _serviceProvider.GetRequiredService>().GetVersionNumberFact(1) }; var leases = new[] { new CountLease(i) }; @@ -1117,7 +1109,7 @@ public async Task GivenTransactionAlreadyInserted_WhenQueryingByTransactionId_Th var commands = new ICommand[] { new Count(i) }; - var facts = new[] { new Counted(i), _serviceProvider.GetVersionNumberFact(1) }; + var facts = new[] { new Counted(i), _serviceProvider.GetRequiredService>().GetVersionNumberFact(1) }; var leases = new[] { new CountLease(i) }; @@ -1177,7 +1169,7 @@ public async Task GivenTransactionAlreadyInserted_WhenQueryingByEntityId_ThenRet var commands = new ICommand[] { new Count(i) }; - var facts = new[] { new Counted(i), _serviceProvider.GetVersionNumberFact(1) }; + var facts = new[] { new Counted(i), _serviceProvider.GetRequiredService>().GetVersionNumberFact(1) }; var leases = new[] { new CountLease(i) }; @@ -1229,7 +1221,7 @@ public async Task GivenTransactionAlreadyInserted_WhenQueryingByEntityVersionNum var facts = new[] { - new Counted(i), _serviceProvider.GetVersionNumberFact((ulong)i) + new Counted(i), _serviceProvider.GetRequiredService>().GetVersionNumberFact((ulong)i) }; var leases = new[] { new CountLease(i) }; diff --git a/src/EntityDb.Common/Entities/EntityRepository.cs b/src/EntityDb.Common/Entities/EntityRepository.cs index e6699dda..8df4f4b0 100644 --- a/src/EntityDb.Common/Entities/EntityRepository.cs +++ b/src/EntityDb.Common/Entities/EntityRepository.cs @@ -1,75 +1,99 @@ using EntityDb.Abstractions.Entities; +using EntityDb.Abstractions.Loggers; using EntityDb.Abstractions.Snapshots; +using EntityDb.Abstractions.Strategies; using EntityDb.Abstractions.Transactions; using EntityDb.Common.Extensions; using EntityDb.Common.Queries; +using Microsoft.Extensions.DependencyInjection; using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Threading.Tasks; namespace EntityDb.Common.Entities { internal class EntityRepository : IEntityRepository { - private readonly IServiceProvider _serviceProvider; + private readonly IConstructingStrategy _constructingStrategy; + private readonly IVersioningStrategy _versioningStrategy; + private readonly ILogger _logger; + private readonly IEnumerable> _transactionSubscribers; + private readonly ITransactionRepository _transactionRepository; + private readonly ISnapshotRepository? _snapshotRepository; public EntityRepository ( - IServiceProvider serviceProvider, + ILoggerFactory loggerFactory, + IConstructingStrategy constructingStrategy, + IVersioningStrategy versioningStrategy, + IEnumerable> transactionSubscribers, ITransactionRepository transactionRepository, ISnapshotRepository? snapshotRepository = null ) { - _serviceProvider = serviceProvider; - TransactionRepository = transactionRepository; - SnapshotRepository = snapshotRepository; + _logger = loggerFactory.CreateLogger>(); + _constructingStrategy = constructingStrategy; + _versioningStrategy = versioningStrategy; + _transactionSubscribers = transactionSubscribers; + _transactionRepository = transactionRepository; + _snapshotRepository = snapshotRepository; } - - public ITransactionRepository TransactionRepository { get; } - - public ISnapshotRepository? SnapshotRepository { get; } - - public async Task Get(Guid entityId) + + private void Publish(ITransaction transaction) { - TEntity? snapshot = default; - - if (SnapshotRepository != null) + foreach (var transactionSubscriber in _transactionSubscribers) { - snapshot = await SnapshotRepository.GetSnapshot(entityId); + try + { + transactionSubscriber.Notify(transaction); + } + catch (Exception exception) + { + _logger.LogError(exception, $"{transactionSubscriber.GetType()}.{nameof(transactionSubscriber.Notify)}({transaction.Id})"); + } } + } + + public async Task GetSnapshotOrDefault(Guid entityId) + { + if (_snapshotRepository != null) + { + return await _snapshotRepository.GetSnapshot(entityId); + } + + return default; + } - var entity = snapshot ?? _serviceProvider.Construct(entityId); + public async Task GetCurrentOrConstruct(Guid entityId) + { + var snapshot = await GetSnapshotOrDefault(entityId); - var versionNumber = _serviceProvider.GetVersionNumber(entity); + var entity = snapshot ?? _constructingStrategy.Construct(entityId); + + var versionNumber = _versioningStrategy.GetVersionNumber(entity); var factQuery = new GetEntityQuery(entityId, versionNumber); - var facts = await TransactionRepository.GetFacts(factQuery); + var facts = await _transactionRepository.GetFacts(factQuery); entity = entity.Reduce(facts); return entity; } - public Task Put(ITransaction transaction) + public async Task PutTransaction(ITransaction transaction) { - if (SnapshotRepository != null) - { - var lastCommands = transaction.Commands - .GroupBy(command => command.EntityId) - .Select(group => group.Last()); + var success = await _transactionRepository.PutTransaction(transaction); - foreach (var lastCommand in lastCommands) - { - if (_serviceProvider.ShouldPutSnapshot(lastCommand.PreviousSnapshot, lastCommand.NextSnapshot)) - { - SnapshotRepository.PutSnapshot(lastCommand.EntityId, lastCommand.NextSnapshot); - } - } + if (success == false) + { + return false; } - return TransactionRepository.PutTransaction(transaction); + Publish(transaction); + + return true; } [ExcludeFromCodeCoverage] @@ -80,12 +104,28 @@ public void Dispose() public async ValueTask DisposeAsync() { - await TransactionRepository.DisposeAsync(); + await _transactionRepository.DisposeAsync(); - if (SnapshotRepository != null) + if (_snapshotRepository != null) + { + await _snapshotRepository.DisposeAsync(); + } + } + + public static EntityRepository Create + ( + IServiceProvider serviceProvider, + ITransactionRepository transactionRepository, + ISnapshotRepository? snapshotRepository = null + ) + { + if (snapshotRepository == null) { - await SnapshotRepository.DisposeAsync(); + return ActivatorUtilities.CreateInstance>(serviceProvider, transactionRepository); } + + return ActivatorUtilities.CreateInstance>(serviceProvider, transactionRepository, + snapshotRepository); } } } diff --git a/src/EntityDb.Common/Entities/EntityRepositoryFactory.cs b/src/EntityDb.Common/Entities/EntityRepositoryFactory.cs new file mode 100644 index 00000000..a12064b7 --- /dev/null +++ b/src/EntityDb.Common/Entities/EntityRepositoryFactory.cs @@ -0,0 +1,40 @@ +using EntityDb.Abstractions.Entities; +using EntityDb.Abstractions.Snapshots; +using EntityDb.Abstractions.Transactions; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Threading.Tasks; + +namespace EntityDb.Common.Entities +{ + internal class EntityRepositoryFactory : IEntityRepositoryFactory + { + private readonly IServiceProvider _serviceProvider; + private readonly ITransactionRepositoryFactory _transactionRepositoryFactory; + private readonly ISnapshotRepositoryFactory? _snapshotRepositoryFactory; + + public EntityRepositoryFactory(IServiceProvider serviceProvider, ITransactionRepositoryFactory transactionRepositoryFactory, + ISnapshotRepositoryFactory? snapshotRepositoryFactory = null) + { + _serviceProvider = serviceProvider; + _transactionRepositoryFactory = transactionRepositoryFactory; + _snapshotRepositoryFactory = snapshotRepositoryFactory; + } + + public async Task> CreateRepository(ITransactionSessionOptions transactionSessionOptions, + ISnapshotSessionOptions? snapshotSessionOptions = null) + { + var transactionRepository = await _transactionRepositoryFactory.CreateRepository(transactionSessionOptions); + + if (_snapshotRepositoryFactory == null || snapshotSessionOptions == null) + { + return ActivatorUtilities.CreateInstance>(_serviceProvider, + transactionRepository); + } + + var snapshotRepository = await _snapshotRepositoryFactory.CreateRepository(snapshotSessionOptions); + + return ActivatorUtilities.CreateInstance>(_serviceProvider, transactionRepository, snapshotRepository); + } + } +} diff --git a/src/EntityDb.Common/Entities/IAuthorizedEntity.cs b/src/EntityDb.Common/Entities/IAuthorizedEntity.cs index ae774235..068e8541 100644 --- a/src/EntityDb.Common/Entities/IAuthorizedEntity.cs +++ b/src/EntityDb.Common/Entities/IAuthorizedEntity.cs @@ -10,7 +10,7 @@ namespace EntityDb.Common.Entities /// The type of the entity to be authorized. public interface IAuthorizedEntity { - /// + /// bool IsAuthorized(ICommand command, IAgent agent); } } diff --git a/src/EntityDb.Common/Extensions/IQueryExtensions.cs b/src/EntityDb.Common/Extensions/IQueryExtensions.cs index 6157c171..6ca4e003 100644 --- a/src/EntityDb.Common/Extensions/IQueryExtensions.cs +++ b/src/EntityDb.Common/Extensions/IQueryExtensions.cs @@ -1,6 +1,6 @@ using EntityDb.Abstractions.Queries; +using EntityDb.Abstractions.Queries.Filters; using EntityDb.Common.Queries.Filtered; -using EntityDb.Common.Queries.Filters; using EntityDb.Common.Queries.Modified; namespace EntityDb.Common.Extensions diff --git a/src/EntityDb.Common/Extensions/IServiceCollectionExtensions.cs b/src/EntityDb.Common/Extensions/IServiceCollectionExtensions.cs index a371cffe..c53dc019 100644 --- a/src/EntityDb.Common/Extensions/IServiceCollectionExtensions.cs +++ b/src/EntityDb.Common/Extensions/IServiceCollectionExtensions.cs @@ -1,13 +1,16 @@ using EntityDb.Abstractions.Agents; +using EntityDb.Abstractions.Entities; using EntityDb.Abstractions.Loggers; using EntityDb.Abstractions.Strategies; using EntityDb.Common.Entities; using EntityDb.Common.Loggers; using EntityDb.Common.Strategies; using EntityDb.Common.Strategies.Resolving; +using EntityDb.Common.Transactions; using Microsoft.Extensions.DependencyInjection; using System; using System.Reflection; +using System.Runtime.CompilerServices; namespace EntityDb.Common.Extensions { @@ -76,44 +79,6 @@ public static void AddAgentAccessor(this IServiceCollection serv serviceCollection.AddScoped(); } - /// - /// Adds a custom implementation of to a service collection. - /// - /// The type of the entity to be snapshotted. - /// The type that implements . - /// The service collection - public static void AddSnapshottingStrategy( - this IServiceCollection serviceCollection) - where TSnapshottingStrategy : class, ISnapshottingStrategy - { - serviceCollection.AddSingleton, TSnapshottingStrategy>(); - } - - /// - /// Adds a custom implementation of to a service collection. - /// - /// The type of the entity to be constructed. - /// The type that implements . - /// The service collection - public static void AddConstructingStrategy( - this IServiceCollection serviceCollection) - where TConstructingStrategy : class, IConstructingStrategy - { - serviceCollection.AddSingleton, TConstructingStrategy>(); - } - - /// - /// Adds an internal implementation of to a service collection for an - /// entity that implements . - /// - /// The type of the entity to be versioned. - /// The service collection. - public static void AddVersionedEntityVersioningStrategy(this IServiceCollection serviceCollection) - where TEntity : IVersionedEntity - { - serviceCollection.AddSingleton, VersionedEntityVersioningStrategy>(); - } - /// /// Adds an internal implementation of to a service collection for an entity /// that implements . @@ -150,5 +115,18 @@ public static void AddAuthorizedEntityAuthorizingStrategy(this IService serviceCollection .AddSingleton, AuthorizedEntityAuthorizingStrategy>(); } + + public static void AddEntity(this IServiceCollection serviceCollection) + where TEntity : IVersionedEntity + where TConstructingStrategy : class, IConstructingStrategy + { + serviceCollection.AddTransient>(); + + serviceCollection.AddTransient, EntityRepositoryFactory>(); + + serviceCollection.AddSingleton, TConstructingStrategy>(); + + serviceCollection.AddSingleton, VersionedEntityVersioningStrategy>(); + } } } diff --git a/src/EntityDb.Common/Extensions/IServiceProviderExtensions.cs b/src/EntityDb.Common/Extensions/IServiceProviderExtensions.cs deleted file mode 100644 index 9fe6855d..00000000 --- a/src/EntityDb.Common/Extensions/IServiceProviderExtensions.cs +++ /dev/null @@ -1,249 +0,0 @@ -using EntityDb.Abstractions.Agents; -using EntityDb.Abstractions.Commands; -using EntityDb.Abstractions.Entities; -using EntityDb.Abstractions.Facts; -using EntityDb.Abstractions.Leases; -using EntityDb.Abstractions.Snapshots; -using EntityDb.Abstractions.Strategies; -using EntityDb.Abstractions.Tags; -using EntityDb.Abstractions.Transactions; -using EntityDb.Common.Entities; -using EntityDb.Common.Transactions; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace EntityDb.Common.Extensions -{ - /// - /// Extensions for service providers. - /// - public static class IServiceProviderExtensions - { - /// - /// Returns the associated with the current service scope. - /// - /// The service provider. - /// - public static IAgent GetAgent(this IServiceProvider serviceProvider) - { - var agentAccessor = serviceProvider.GetRequiredService(); - - return agentAccessor.GetAgent(); - } - - /// - /// Returns a new instance of . - /// - /// The type of the entity for which a transaction is to be built. - /// The service provider. - /// - public static TransactionBuilder GetTransactionBuilder(this IServiceProvider serviceProvider) - { - return new TransactionBuilder(serviceProvider); - } - - /// - /// Returns the resolved or throws if the cannot be resolved. - /// - /// The servie provider. - /// Describes the type that needs to be resolved. - /// The resolved . - public static Type ResolveType(this IServiceProvider serviceProvider, - IReadOnlyDictionary headers) - { - var resolvingStrategyChain = serviceProvider.GetRequiredService(); - - return resolvingStrategyChain.ResolveType(headers); - } - - /// - /// Creates a new instance of . - /// - /// The type of the entity. - /// The service provider. - /// The agent's use case for the repository. - /// A new instance of . - public static Task> CreateTransactionRepository( - this IServiceProvider serviceProvider, ITransactionSessionOptions transactionSessionOptions) - { - var transactionRepositoryFactory = - serviceProvider.GetRequiredService>(); - - return transactionRepositoryFactory.CreateRepository(transactionSessionOptions); - } - - /// - /// Create a new instance of - /// - /// The type of the entity. - /// The service provider. - /// The agent's use case for the repository. - /// A new instance of . - public static Task> CreateSnapshotRepository( - this IServiceProvider serviceProvider, ISnapshotSessionOptions snapshotSessionOptions) - { - var snapshotRepositoryFactory = serviceProvider.GetRequiredService>(); - - return snapshotRepositoryFactory.CreateRepository(snapshotSessionOptions); - } - - /// - /// Create a new instance of - /// - /// The type of the entity. - /// The service provider. - /// The agent's use case for the nested transaction repository. - /// The agent's use case for the nested snapshot repository, if needed. - /// - public static async Task> CreateEntityRepository( - this IServiceProvider serviceProvider, ITransactionSessionOptions transactionSessionOptions, - ISnapshotSessionOptions? snapshotSessionOptions = null) - { - var transactionRepositoryFactory = - serviceProvider.GetRequiredService>(); - - var transactionRepository = await transactionRepositoryFactory.CreateRepository(transactionSessionOptions); - - if (snapshotSessionOptions == null) - { - return new EntityRepository(serviceProvider, transactionRepository); - } - - var snapshotRepositoryFactory = serviceProvider.GetRequiredService>(); - - var snapshotRepository = await snapshotRepositoryFactory.CreateRepository(snapshotSessionOptions); - - return new EntityRepository(serviceProvider, transactionRepository, snapshotRepository); - } - - /// - /// Returns a new instance of a . - /// - /// The type of the entity. - /// The service provider. - /// The id of the entity. - /// A new instance of . - public static TEntity Construct(this IServiceProvider serviceProvider, Guid entityId) - { - var constructingStrategy = serviceProvider.GetRequiredService>(); - - return constructingStrategy.Construct(entityId); - } - - /// - /// Returns the version number of a . - /// - /// The type of the entity. - /// The service provider. - /// The entity. - /// The version number of . - public static ulong GetVersionNumber(this IServiceProvider serviceProvider, TEntity entity) - { - var versioningStrategy = serviceProvider.GetRequiredService>(); - - return versioningStrategy.GetVersionNumber(entity); - } - - /// - /// Creates a new version number modifier for an entity. - /// - /// The type of the entity. - /// The service provider. - /// The desired version number. - /// A new version number modifier for on an entity. - public static IFact GetVersionNumberFact(this IServiceProvider serviceProvider, - ulong versionNumber) - { - var versioningStrategy = serviceProvider.GetRequiredService>(); - - return versioningStrategy.GetVersionNumberFact(versionNumber); - } - - /// - /// Returns the leases for a . - /// - /// The type of the entity. - /// The service provider. - /// The entity. - /// The leases for . - public static ILease[] GetLeases(this IServiceProvider serviceProvider, TEntity entity) - { - var leasingStrategy = serviceProvider.GetService>(); - - if (leasingStrategy != null) - { - return leasingStrategy.GetLeases(entity); - } - - return Array.Empty(); - } - - /// - /// Returns the tags for a . - /// - /// The type of the entity. - /// The service provider. - /// The entity. - /// The tags for . - public static ITag[] GetTags(this IServiceProvider serviceProvider, TEntity entity) - { - var taggingStrategy = serviceProvider.GetService>(); - - if (taggingStrategy != null) - { - return taggingStrategy.GetTags(entity); - } - - return Array.Empty(); - } - - /// - /// Determines if the agent is authorized to execute a command on an entity. - /// - /// The type of the entity. - /// The service provider. - /// The entity. - /// The command. - /// true if execution is authorized, or false if execution is not authorized. - public static bool IsAuthorized(this IServiceProvider serviceProvider, TEntity entity, - ICommand command) - { - var authorizingStrategy = serviceProvider.GetService>(); - - if (authorizingStrategy != null) - { - var agent = serviceProvider.GetAgent(); - - return authorizingStrategy.IsAuthorized(entity, command, agent); - } - - return true; - } - - /// - /// Determines if the next version of an entity should be put into snapshot storage. - /// - /// The type of the entity. - /// The service provider. - /// The previous version of the entity, if it exists. - /// The next version of the entity. - /// - /// true if the next version of the entity should be cached, or false if the next version of the - /// entity should not be put into snapshot storage. - /// - public static bool ShouldPutSnapshot(this IServiceProvider serviceProvider, TEntity? previousEntity, - TEntity nextEntity) - { - var snapshottingStrategy = serviceProvider.GetService>(); - - if (snapshottingStrategy != null) - { - return snapshottingStrategy.ShouldPutSnapshot(previousEntity, nextEntity); - } - - return false; - } - } -} diff --git a/src/EntityDb.Common/Queries/DeleteLeasesQuery.cs b/src/EntityDb.Common/Queries/DeleteLeasesQuery.cs index 714d26ad..ccd63c87 100644 --- a/src/EntityDb.Common/Queries/DeleteLeasesQuery.cs +++ b/src/EntityDb.Common/Queries/DeleteLeasesQuery.cs @@ -3,15 +3,13 @@ using EntityDb.Abstractions.Queries.FilterBuilders; using EntityDb.Abstractions.Queries.SortBuilders; using EntityDb.Common.Leases; -using EntityDb.Common.Queries.Filters; using System; using System.Collections.Generic; using System.Linq; namespace EntityDb.Common.Queries { - internal sealed record DeleteLeasesQuery(Guid EntityId, IReadOnlyCollection Leases) : ILeaseQuery, - ILeaseFilter + internal sealed record DeleteLeasesQuery(Guid EntityId, IReadOnlyCollection Leases) : ILeaseQuery { public TFilter GetFilter(ILeaseFilterBuilder builder) { diff --git a/src/EntityDb.Common/Queries/DeleteTagsQuery.cs b/src/EntityDb.Common/Queries/DeleteTagsQuery.cs index 95d144f3..93b707cb 100644 --- a/src/EntityDb.Common/Queries/DeleteTagsQuery.cs +++ b/src/EntityDb.Common/Queries/DeleteTagsQuery.cs @@ -1,40 +1,39 @@ -using EntityDb.Abstractions.Queries; -using EntityDb.Abstractions.Queries.FilterBuilders; -using EntityDb.Abstractions.Queries.SortBuilders; -using EntityDb.Abstractions.Tags; -using EntityDb.Common.Queries.Filters; -using EntityDb.Common.Tags; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace EntityDb.Common.Queries -{ - internal sealed record DeleteTagsQuery(Guid EntityId, IReadOnlyCollection Tags) : ITagQuery, ITagFilter - { - public TFilter GetFilter(ITagFilterBuilder builder) - { - return builder.And - ( - builder.EntityIdIn(EntityId), - builder.Or - ( - Tags - .Select(deleteLease => builder.TagMatches((Tag lease) => - lease.Label == deleteLease.Label && - lease.Value == deleteLease.Value)) - .ToArray() - ) - ); - } - - public TSort? GetSort(ITagSortBuilder builder) - { - return default; - } - - public int? Skip => null; - - public int? Take => null; - } -} +using EntityDb.Abstractions.Queries; +using EntityDb.Abstractions.Queries.FilterBuilders; +using EntityDb.Abstractions.Queries.SortBuilders; +using EntityDb.Abstractions.Tags; +using EntityDb.Common.Tags; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace EntityDb.Common.Queries +{ + internal sealed record DeleteTagsQuery(Guid EntityId, IReadOnlyCollection Tags) : ITagQuery + { + public TFilter GetFilter(ITagFilterBuilder builder) + { + return builder.And + ( + builder.EntityIdIn(EntityId), + builder.Or + ( + Tags + .Select(deleteLease => builder.TagMatches((Tag lease) => + lease.Label == deleteLease.Label && + lease.Value == deleteLease.Value)) + .ToArray() + ) + ); + } + + public TSort? GetSort(ITagSortBuilder builder) + { + return default; + } + + public int? Skip => null; + + public int? Take => null; + } +} diff --git a/src/EntityDb.Common/Queries/ExactLeaseQuery.cs b/src/EntityDb.Common/Queries/ExactLeaseQuery.cs deleted file mode 100644 index 2d2e5330..00000000 --- a/src/EntityDb.Common/Queries/ExactLeaseQuery.cs +++ /dev/null @@ -1,35 +0,0 @@ -using EntityDb.Abstractions.Leases; -using EntityDb.Abstractions.Queries; -using EntityDb.Abstractions.Queries.FilterBuilders; -using EntityDb.Abstractions.Queries.SortBuilders; -using EntityDb.Common.Leases; - -namespace EntityDb.Common.Queries -{ - /// - /// A query for the exact lease. - /// - public sealed record ExactLeaseQuery(ILease Lease) : ILeaseQuery - { - /// - public TFilter GetFilter(ILeaseFilterBuilder builder) - { - return builder.LeaseMatches(x => - x.Scope == Lease.Scope && - x.Label == Lease.Label && - x.Value == Lease.Value); - } - - /// - public TSort? GetSort(ILeaseSortBuilder builder) - { - return default; - } - - /// - public int? Skip => null; - - /// - public int? Take => 1; - } -} diff --git a/src/EntityDb.Common/Queries/Filtered/FilteredCommandQuery.cs b/src/EntityDb.Common/Queries/Filtered/FilteredCommandQuery.cs index 382d9e04..121de4d8 100644 --- a/src/EntityDb.Common/Queries/Filtered/FilteredCommandQuery.cs +++ b/src/EntityDb.Common/Queries/Filtered/FilteredCommandQuery.cs @@ -1,7 +1,7 @@ using EntityDb.Abstractions.Queries; using EntityDb.Abstractions.Queries.FilterBuilders; +using EntityDb.Abstractions.Queries.Filters; using EntityDb.Abstractions.Queries.SortBuilders; -using EntityDb.Common.Queries.Filters; namespace EntityDb.Common.Queries.Filtered { diff --git a/src/EntityDb.Common/Queries/Filtered/FilteredFactQuery.cs b/src/EntityDb.Common/Queries/Filtered/FilteredFactQuery.cs index ff87409d..3bb851de 100644 --- a/src/EntityDb.Common/Queries/Filtered/FilteredFactQuery.cs +++ b/src/EntityDb.Common/Queries/Filtered/FilteredFactQuery.cs @@ -1,7 +1,7 @@ using EntityDb.Abstractions.Queries; using EntityDb.Abstractions.Queries.FilterBuilders; +using EntityDb.Abstractions.Queries.Filters; using EntityDb.Abstractions.Queries.SortBuilders; -using EntityDb.Common.Queries.Filters; namespace EntityDb.Common.Queries.Filtered { diff --git a/src/EntityDb.Common/Queries/Filtered/FilteredLeaseQuery.cs b/src/EntityDb.Common/Queries/Filtered/FilteredLeaseQuery.cs index 5a337127..10acdbc2 100644 --- a/src/EntityDb.Common/Queries/Filtered/FilteredLeaseQuery.cs +++ b/src/EntityDb.Common/Queries/Filtered/FilteredLeaseQuery.cs @@ -1,7 +1,7 @@ using EntityDb.Abstractions.Queries; using EntityDb.Abstractions.Queries.FilterBuilders; +using EntityDb.Abstractions.Queries.Filters; using EntityDb.Abstractions.Queries.SortBuilders; -using EntityDb.Common.Queries.Filters; namespace EntityDb.Common.Queries.Filtered { diff --git a/src/EntityDb.Common/Queries/Filtered/FilteredSourceQuery.cs b/src/EntityDb.Common/Queries/Filtered/FilteredSourceQuery.cs index 4ed3cc18..d9865e7d 100644 --- a/src/EntityDb.Common/Queries/Filtered/FilteredSourceQuery.cs +++ b/src/EntityDb.Common/Queries/Filtered/FilteredSourceQuery.cs @@ -1,7 +1,7 @@ using EntityDb.Abstractions.Queries; using EntityDb.Abstractions.Queries.FilterBuilders; +using EntityDb.Abstractions.Queries.Filters; using EntityDb.Abstractions.Queries.SortBuilders; -using EntityDb.Common.Queries.Filters; namespace EntityDb.Common.Queries.Filtered { diff --git a/src/EntityDb.Common/Queries/Filtered/FilteredTagQuery.cs b/src/EntityDb.Common/Queries/Filtered/FilteredTagQuery.cs index c25ae610..644fcabc 100644 --- a/src/EntityDb.Common/Queries/Filtered/FilteredTagQuery.cs +++ b/src/EntityDb.Common/Queries/Filtered/FilteredTagQuery.cs @@ -1,7 +1,7 @@ using EntityDb.Abstractions.Queries; using EntityDb.Abstractions.Queries.FilterBuilders; +using EntityDb.Abstractions.Queries.Filters; using EntityDb.Abstractions.Queries.SortBuilders; -using EntityDb.Common.Queries.Filters; namespace EntityDb.Common.Queries.Filtered { diff --git a/src/EntityDb.Common/Queries/Filters/ICommandFilter.cs b/src/EntityDb.Common/Queries/Filters/ICommandFilter.cs deleted file mode 100644 index e4138a99..00000000 --- a/src/EntityDb.Common/Queries/Filters/ICommandFilter.cs +++ /dev/null @@ -1,14 +0,0 @@ -using EntityDb.Abstractions.Queries; -using EntityDb.Abstractions.Queries.FilterBuilders; - -namespace EntityDb.Common.Queries.Filters -{ - /// - /// Represents a type that supplies additional filtering for a . - /// - public interface ICommandFilter - { - /// - TFilter GetFilter(ICommandFilterBuilder builder); - } -} diff --git a/src/EntityDb.Common/Queries/Filters/IFactFilter.cs b/src/EntityDb.Common/Queries/Filters/IFactFilter.cs deleted file mode 100644 index 6249de31..00000000 --- a/src/EntityDb.Common/Queries/Filters/IFactFilter.cs +++ /dev/null @@ -1,14 +0,0 @@ -using EntityDb.Abstractions.Queries; -using EntityDb.Abstractions.Queries.FilterBuilders; - -namespace EntityDb.Common.Queries.Filters -{ - /// - /// Represents a type that supplies additional filtering for a . - /// - public interface IFactFilter - { - /// - TFilter GetFilter(IFactFilterBuilder builder); - } -} diff --git a/src/EntityDb.Common/Queries/Filters/ILeaseFilter.cs b/src/EntityDb.Common/Queries/Filters/ILeaseFilter.cs deleted file mode 100644 index 2600d45e..00000000 --- a/src/EntityDb.Common/Queries/Filters/ILeaseFilter.cs +++ /dev/null @@ -1,14 +0,0 @@ -using EntityDb.Abstractions.Queries; -using EntityDb.Abstractions.Queries.FilterBuilders; - -namespace EntityDb.Common.Queries.Filters -{ - /// - /// Represents a type that supplies additional filtering for a . - /// - public interface ILeaseFilter - { - /// - TFilter GetFilter(ILeaseFilterBuilder builder); - } -} diff --git a/src/EntityDb.Common/Queries/Filters/ISourceFilter.cs b/src/EntityDb.Common/Queries/Filters/ISourceFilter.cs deleted file mode 100644 index 133add0c..00000000 --- a/src/EntityDb.Common/Queries/Filters/ISourceFilter.cs +++ /dev/null @@ -1,14 +0,0 @@ -using EntityDb.Abstractions.Queries; -using EntityDb.Abstractions.Queries.FilterBuilders; - -namespace EntityDb.Common.Queries.Filters -{ - /// - /// Represents a type that supplies additional filtering for a . - /// - public interface ISourceFilter - { - /// - TFilter GetFilter(ISourceFilterBuilder builder); - } -} diff --git a/src/EntityDb.Common/Queries/Filters/ITagFilter.cs b/src/EntityDb.Common/Queries/Filters/ITagFilter.cs deleted file mode 100644 index 6e2ac383..00000000 --- a/src/EntityDb.Common/Queries/Filters/ITagFilter.cs +++ /dev/null @@ -1,14 +0,0 @@ -using EntityDb.Abstractions.Queries; -using EntityDb.Abstractions.Queries.FilterBuilders; - -namespace EntityDb.Common.Queries.Filters -{ - /// - /// Represents a type that supplies additional filtering for a . - /// - public interface ITagFilter - { - /// - TFilter GetFilter(ITagFilterBuilder builder); - } -} diff --git a/src/EntityDb.Common/Strategies/AuthorizedEntityAuthorizingStrategy.cs b/src/EntityDb.Common/Strategies/AuthorizedEntityAuthorizingStrategy.cs index e79b1de4..658aaa25 100644 --- a/src/EntityDb.Common/Strategies/AuthorizedEntityAuthorizingStrategy.cs +++ b/src/EntityDb.Common/Strategies/AuthorizedEntityAuthorizingStrategy.cs @@ -1,16 +1,25 @@ -using EntityDb.Abstractions.Agents; -using EntityDb.Abstractions.Commands; -using EntityDb.Abstractions.Strategies; -using EntityDb.Common.Entities; - -namespace EntityDb.Common.Strategies -{ - internal sealed class AuthorizedEntityAuthorizingStrategy : IAuthorizingStrategy - where TEntity : IAuthorizedEntity - { - public bool IsAuthorized(TEntity entity, ICommand command, IAgent agent) - { - return entity.IsAuthorized(command, agent); - } - } -} +using EntityDb.Abstractions.Agents; +using EntityDb.Abstractions.Commands; +using EntityDb.Abstractions.Strategies; +using EntityDb.Common.Entities; + +namespace EntityDb.Common.Strategies +{ + internal sealed class AuthorizedEntityAuthorizingStrategy : IAuthorizingStrategy + where TEntity : IAuthorizedEntity + { + private readonly IAgentAccessor _agentAccessor; + + public AuthorizedEntityAuthorizingStrategy(IAgentAccessor agentAccessor) + { + _agentAccessor = agentAccessor; + } + + public bool IsAuthorized(TEntity entity, ICommand command) + { + var agent = _agentAccessor.GetAgent(); + + return entity.IsAuthorized(command, agent); + } + } +} diff --git a/src/EntityDb.Common/Transactions/AsyncTransactionSubscriber.cs b/src/EntityDb.Common/Transactions/AsyncTransactionSubscriber.cs new file mode 100644 index 00000000..d356d3cf --- /dev/null +++ b/src/EntityDb.Common/Transactions/AsyncTransactionSubscriber.cs @@ -0,0 +1,38 @@ +using EntityDb.Abstractions.Transactions; +using System.Threading.Tasks; + +namespace EntityDb.Common.Transactions +{ + /// + /// Represents an asynchronous subscription to transactions. + /// + /// + public abstract class AsyncTransactionSubscriber : ITransactionSubscriber + { + private readonly bool _testMode; + + /// + /// Constructs a new instance of . + /// + /// If true then the task will be synchronously awaited before returning. + protected AsyncTransactionSubscriber(bool testMode) + { + _testMode = testMode; + } + + /// + public void Notify(ITransaction transaction) + { + var task = Task.Run(() => NotifyAsync(transaction)); + + if (_testMode) + { + task.Wait(); + } + } + + /// + /// A task that handles notification asynchronously. + protected abstract Task NotifyAsync(ITransaction transaction); + } +} diff --git a/src/EntityDb.Common/Transactions/SnapshottingTransactionSubscriber.cs b/src/EntityDb.Common/Transactions/SnapshottingTransactionSubscriber.cs new file mode 100644 index 00000000..f52734b2 --- /dev/null +++ b/src/EntityDb.Common/Transactions/SnapshottingTransactionSubscriber.cs @@ -0,0 +1,48 @@ +using EntityDb.Abstractions.Snapshots; +using EntityDb.Abstractions.Transactions; +using EntityDb.Common.Snapshots; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace EntityDb.Common.Transactions +{ + internal class SnapshottingTransactionSubscriber : AsyncTransactionSubscriber + { + private readonly ISnapshotRepositoryFactory _snapshotRepositoryFactory; + private readonly ISnapshotSessionOptions _snapshotSessionOptions = new SnapshotSessionOptions(); + + public SnapshottingTransactionSubscriber + ( + ISnapshotRepositoryFactory snapshotRepositoryFactory, + bool testMode + ) : base(testMode) + { + _snapshotRepositoryFactory = snapshotRepositoryFactory; + } + + protected override async Task NotifyAsync(ITransaction transaction) + { + var commandGroups = transaction.Commands + .GroupBy(command => command.EntityId); + + foreach (var commandGroup in commandGroups) + { + var entityId = commandGroup.Key; + var nextSnapshot = commandGroup.Last().NextSnapshot; + + await using var snapshotRepository = + await _snapshotRepositoryFactory.CreateRepository(_snapshotSessionOptions); + + await snapshotRepository.PutSnapshot(entityId, nextSnapshot); + } + } + + public static SnapshottingTransactionSubscriber Create(IServiceProvider serviceProvider, bool testMode) + { + return ActivatorUtilities.CreateInstance>(serviceProvider, + testMode); + } + } +} diff --git a/src/EntityDb.Common/Transactions/TransactionBuilder.cs b/src/EntityDb.Common/Transactions/TransactionBuilder.cs index e1c1f231..36c84d18 100644 --- a/src/EntityDb.Common/Transactions/TransactionBuilder.cs +++ b/src/EntityDb.Common/Transactions/TransactionBuilder.cs @@ -1,6 +1,9 @@ using EntityDb.Abstractions.Commands; using EntityDb.Abstractions.Entities; using EntityDb.Abstractions.Facts; +using EntityDb.Abstractions.Leases; +using EntityDb.Abstractions.Strategies; +using EntityDb.Abstractions.Tags; using EntityDb.Abstractions.Transactions; using EntityDb.Common.Exceptions; using EntityDb.Common.Extensions; @@ -19,17 +22,31 @@ namespace EntityDb.Common.Transactions /// The type of the entity in the transaction. public sealed class TransactionBuilder { + private readonly IConstructingStrategy _constructingStrategy; + private readonly IVersioningStrategy _versioningStrategy; + private readonly IAuthorizingStrategy? _authorizingStrategy; + private readonly ILeasingStrategy? _leasingStrategy; + private readonly ITaggingStrategy? _taggingStrategy; private readonly Dictionary _knownEntities = new(); - private readonly IServiceProvider _serviceProvider; private readonly List> _transactionCommands = new(); /// /// Initializes a new instance of . /// - /// The service provider. - public TransactionBuilder(IServiceProvider serviceProvider) + public TransactionBuilder + ( + IConstructingStrategy constructingStrategy, + IVersioningStrategy versioningStrategy, + IAuthorizingStrategy? authorizingStrategy = null, + ILeasingStrategy? leasingStrategy = null, + ITaggingStrategy? taggingStrategy = null + ) { - _serviceProvider = serviceProvider; + _constructingStrategy = constructingStrategy; + _versioningStrategy = versioningStrategy; + _authorizingStrategy = authorizingStrategy; + _leasingStrategy = leasingStrategy; + _taggingStrategy = taggingStrategy; } private static ImmutableArray> GetTransactionFacts(IEnumerable> facts) @@ -61,34 +78,48 @@ private static ITransactionMetaData GetTransactionMetaData }; } + private bool IsAuthorized(TEntity entity, ICommand command) + { + return _authorizingStrategy?.IsAuthorized(entity, command) ?? true; + } + + private ILease[] GetLeases(TEntity entity) + { + return _leasingStrategy?.GetLeases(entity) ?? Array.Empty(); + } + + private ITag[] GetTags(TEntity entity) + { + return _taggingStrategy?.GetTags(entity) ?? Array.Empty(); + } + private void AddTransactionCommand(Guid entityId, ICommand command) { var previousEntity = _knownEntities[entityId]; - var previousVersionNumber = _serviceProvider.GetVersionNumber(previousEntity); + var previousVersionNumber = _versioningStrategy.GetVersionNumber(previousEntity); - CommandNotAuthorizedException.ThrowIfFalse(_serviceProvider.IsAuthorized(previousEntity, command)); + CommandNotAuthorizedException.ThrowIfFalse(IsAuthorized(previousEntity, command)); var nextFacts = previousEntity.Execute(command); - nextFacts.Add(_serviceProvider.GetVersionNumberFact(previousVersionNumber + 1)); + nextFacts.Add(_versioningStrategy.GetVersionNumberFact(previousVersionNumber + 1)); var nextEntity = previousEntity.Reduce(nextFacts); _transactionCommands.Add(new TransactionCommand { - PreviousSnapshot = previousEntity, NextSnapshot = nextEntity, EntityId = entityId, ExpectedPreviousVersionNumber = previousVersionNumber, Command = command, Facts = GetTransactionFacts(nextFacts), - Leases = GetTransactionMetaData(previousEntity, nextEntity, _serviceProvider.GetLeases), - Tags = GetTransactionMetaData(previousEntity, nextEntity, _serviceProvider.GetTags) + Leases = GetTransactionMetaData(previousEntity, nextEntity, GetLeases), + Tags = GetTransactionMetaData(previousEntity, nextEntity, GetTags) }); _knownEntities[entityId] = nextEntity; } - + /// /// Loads an already-existing entity into the builder. /// @@ -106,9 +137,9 @@ public async Task Load(Guid entityId, IEntityRepository entityRepositor throw new EntityAlreadyLoadedException(); } - var entity = await entityRepository.Get(entityId); + var entity = await entityRepository.GetCurrentOrConstruct(entityId); - if (_serviceProvider.GetVersionNumber(entity) == 0) + if (_versioningStrategy.GetVersionNumber(entity) == 0) { throw new EntityNotCreatedException(); } @@ -132,7 +163,7 @@ public TransactionBuilder Create(Guid entityId, ICommand comma throw new EntityAlreadyCreatedException(); } - var entity = _serviceProvider.Construct(entityId); + var entity = _constructingStrategy.Construct(entityId); _knownEntities.Add(entityId, entity); diff --git a/src/EntityDb.Common/Transactions/TransactionCommand.cs b/src/EntityDb.Common/Transactions/TransactionCommand.cs index 06d74afa..8ffd295a 100644 --- a/src/EntityDb.Common/Transactions/TransactionCommand.cs +++ b/src/EntityDb.Common/Transactions/TransactionCommand.cs @@ -1,21 +1,20 @@ -using EntityDb.Abstractions.Commands; -using EntityDb.Abstractions.Leases; -using EntityDb.Abstractions.Tags; -using EntityDb.Abstractions.Transactions; -using System; -using System.Collections.Immutable; - -namespace EntityDb.Common.Transactions -{ - internal sealed record TransactionCommand : ITransactionCommand - { - public TEntity? PreviousSnapshot { get; init; } - public TEntity NextSnapshot { get; init; } = default!; - public Guid EntityId { get; init; } - public ulong ExpectedPreviousVersionNumber { get; init; } - public ICommand Command { get; init; } = default!; - public ImmutableArray> Facts { get; init; } - public ITransactionMetaData Leases { get; init; } = default!; - public ITransactionMetaData Tags { get; init; } = default!; - } -} +using EntityDb.Abstractions.Commands; +using EntityDb.Abstractions.Leases; +using EntityDb.Abstractions.Tags; +using EntityDb.Abstractions.Transactions; +using System; +using System.Collections.Immutable; + +namespace EntityDb.Common.Transactions +{ + internal sealed record TransactionCommand : ITransactionCommand + { + public TEntity NextSnapshot { get; init; } = default!; + public Guid EntityId { get; init; } + public ulong ExpectedPreviousVersionNumber { get; init; } + public ICommand Command { get; init; } = default!; + public ImmutableArray> Facts { get; init; } + public ITransactionMetaData Leases { get; init; } = default!; + public ITransactionMetaData Tags { get; init; } = default!; + } +} diff --git a/src/EntityDb.MongoDb.Tests/Startup.cs b/src/EntityDb.MongoDb.Tests/Startup.cs index b1a7fa6f..c07b3753 100644 --- a/src/EntityDb.MongoDb.Tests/Startup.cs +++ b/src/EntityDb.MongoDb.Tests/Startup.cs @@ -1,42 +1,42 @@ -using EntityDb.Common.Extensions; -using EntityDb.MongoDb.Provisioner.Extensions; -using EntityDb.TestImplementations.Agents; -using EntityDb.TestImplementations.Entities; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Xunit.DependencyInjection; -using Xunit.DependencyInjection.Logging; - -namespace EntityDb.MongoDb.Tests -{ - public class Startup - { - public void ConfigureServices(IServiceCollection serviceCollection) - { - serviceCollection.AddDefaultLogger(); - - serviceCollection.AddAgentAccessor(); - - serviceCollection.AddDefaultResolvingStrategy(); - - serviceCollection.AddLifoResolvingStrategyChain(); - - serviceCollection.AddConstructingStrategy(); - serviceCollection.AddVersionedEntityVersioningStrategy(); - serviceCollection.AddLeasedEntityLeasingStrategy(); - serviceCollection.AddTaggedEntityTaggingStrategy(); - serviceCollection.AddAuthorizedEntityAuthorizingStrategy(); - - serviceCollection.AddAutoProvisionTestModeMongoDbTransactions - ( - TransactionEntity.MongoCollectionName, - _ => "mongodb://127.0.0.1:27017/?connect=direct&replicaSet=entitydb" - ); - } - - public void Configure(ILoggerFactory loggerFactory, ITestOutputHelperAccessor testOutputHelperAccessor) - { - loggerFactory.AddProvider(new XunitTestOutputLoggerProvider(testOutputHelperAccessor)); - } - } -} +using EntityDb.Common.Extensions; +using EntityDb.MongoDb.Provisioner.Extensions; +using EntityDb.TestImplementations.Agents; +using EntityDb.TestImplementations.Entities; +using EntityDb.TestImplementations.Strategies; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit.DependencyInjection; +using Xunit.DependencyInjection.Logging; + +namespace EntityDb.MongoDb.Tests +{ + public class Startup + { + public void ConfigureServices(IServiceCollection serviceCollection) + { + serviceCollection.AddDefaultLogger(); + + serviceCollection.AddAgentAccessor(); + + serviceCollection.AddDefaultResolvingStrategy(); + + serviceCollection.AddLifoResolvingStrategyChain(); + + serviceCollection.AddEntity(); + serviceCollection.AddLeasedEntityLeasingStrategy(); + serviceCollection.AddTaggedEntityTaggingStrategy(); + serviceCollection.AddAuthorizedEntityAuthorizingStrategy(); + + serviceCollection.AddAutoProvisionTestModeMongoDbTransactions + ( + TransactionEntity.MongoCollectionName, + _ => "mongodb://127.0.0.1:27017/?connect=direct&replicaSet=entitydb" + ); + } + + public void Configure(ILoggerFactory loggerFactory, ITestOutputHelperAccessor testOutputHelperAccessor) + { + loggerFactory.AddProvider(new XunitTestOutputLoggerProvider(testOutputHelperAccessor)); + } + } +} diff --git a/src/EntityDb.MongoDb.Tests/Transactions/TransactionTests.cs b/src/EntityDb.MongoDb.Tests/Transactions/TransactionTests.cs index 3ecdd1f8..69f6a133 100644 --- a/src/EntityDb.MongoDb.Tests/Transactions/TransactionTests.cs +++ b/src/EntityDb.MongoDb.Tests/Transactions/TransactionTests.cs @@ -1,12 +1,15 @@ -using EntityDb.Common.Tests.Transactions; -using System; - -namespace EntityDb.MongoDb.Tests.Transactions -{ - public class TransactionTests : TransactionTestsBase - { - public TransactionTests(IServiceProvider serviceProvider) : base(serviceProvider) - { - } - } -} +using EntityDb.Abstractions.Strategies; +using EntityDb.Abstractions.Transactions; +using EntityDb.Common.Tests.Transactions; +using EntityDb.TestImplementations.Entities; +using System; + +namespace EntityDb.MongoDb.Tests.Transactions +{ + public class TransactionTests : TransactionTestsBase + { + public TransactionTests(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + } +} diff --git a/src/EntityDb.Redis.Tests/Sessions/RedisSessionTests.cs b/src/EntityDb.Redis.Tests/Sessions/RedisSessionTests.cs index c407e3fc..6f57df1b 100644 --- a/src/EntityDb.Redis.Tests/Sessions/RedisSessionTests.cs +++ b/src/EntityDb.Redis.Tests/Sessions/RedisSessionTests.cs @@ -1,4 +1,5 @@ -using EntityDb.Common.Extensions; +using EntityDb.Abstractions.Snapshots; +using EntityDb.Common.Extensions; using EntityDb.Common.Snapshots; using EntityDb.Redis.Snapshots; using EntityDb.TestImplementations.Entities; @@ -11,20 +12,20 @@ namespace EntityDb.Redis.Tests.Sessions { public class RedisSessionTests { - private readonly IServiceProvider _serviceProvider; + private readonly ISnapshotRepositoryFactory _snapshotRepositoryFactory; - public RedisSessionTests(IServiceProvider serviceProvider) + public RedisSessionTests(ISnapshotRepositoryFactory snapshotRepositoryFactory) { - _serviceProvider = serviceProvider; + _snapshotRepositoryFactory = snapshotRepositoryFactory; } [Fact] public async Task GivenValidRedisSession_WhenThrowingDuringExecuteQuery_ThenReturnDefault() { - var snapshotRepositoryFactory = - await _serviceProvider.CreateSnapshotRepository(new SnapshotSessionOptions()); + var snapshotRepository = + await _snapshotRepositoryFactory.CreateRepository(new SnapshotSessionOptions()); - if (snapshotRepositoryFactory is RedisSnapshotRepository redisSnapshotRepository) + if (snapshotRepository is RedisSnapshotRepository redisSnapshotRepository) { // ARRANGE @@ -48,12 +49,12 @@ public async Task GivenValidRedisSession_WhenThrowingDuringExecuteQuery_ThenRetu [Fact] - public async Task GivenValidRedisSession_WhenThrowingDuringExecuteComand_ThenReturnFalse() + public async Task GivenValidRedisSession_WhenThrowingDuringExecuteCommand_ThenReturnFalse() { - var snapshotRepositoryFactory = - await _serviceProvider.CreateSnapshotRepository(new SnapshotSessionOptions()); + var snapshotRepository = + await _snapshotRepositoryFactory.CreateRepository(new SnapshotSessionOptions()); - if (snapshotRepositoryFactory is RedisSnapshotRepository redisSnapshotRepository) + if (snapshotRepository is RedisSnapshotRepository redisSnapshotRepository) { // ARRANGE diff --git a/src/EntityDb.Redis.Tests/Snapshots/SnapshotTests.cs b/src/EntityDb.Redis.Tests/Snapshots/SnapshotTests.cs index 8dacf62a..60f2744a 100644 --- a/src/EntityDb.Redis.Tests/Snapshots/SnapshotTests.cs +++ b/src/EntityDb.Redis.Tests/Snapshots/SnapshotTests.cs @@ -1,11 +1,12 @@ -using EntityDb.Common.Tests.Snapshots; -using System; +using EntityDb.Abstractions.Snapshots; +using EntityDb.Common.Tests.Snapshots; +using EntityDb.TestImplementations.Entities; namespace EntityDb.Redis.Tests.Snapshots { public class SnapshotTests : SnapshotTestsBase { - public SnapshotTests(IServiceProvider serviceProvider) : base(serviceProvider) + public SnapshotTests(ISnapshotRepositoryFactory snapshotRepositoryFactory) : base(snapshotRepositoryFactory) { } } diff --git a/src/EntityDb.Redis.Tests/Startup.cs b/src/EntityDb.Redis.Tests/Startup.cs index 17c174b0..c7b2ce38 100644 --- a/src/EntityDb.Redis.Tests/Startup.cs +++ b/src/EntityDb.Redis.Tests/Startup.cs @@ -1,33 +1,36 @@ -using EntityDb.Common.Extensions; -using EntityDb.Redis.Extensions; -using EntityDb.TestImplementations.Entities; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Xunit.DependencyInjection; -using Xunit.DependencyInjection.Logging; - -namespace EntityDb.Redis.Tests -{ - public class Startup - { - public void ConfigureServices(IServiceCollection serviceCollection) - { - serviceCollection.AddDefaultLogger(); - - serviceCollection.AddDefaultResolvingStrategy(); - - serviceCollection.AddLifoResolvingStrategyChain(); - - serviceCollection.AddTestModeRedisSnapshots - ( - TransactionEntity.RedisKeyNamespace, - _ => "127.0.0.1:6379" - ); - } - - public void Configure(ILoggerFactory loggerFactory, ITestOutputHelperAccessor testOutputHelperAccessor) - { - loggerFactory.AddProvider(new XunitTestOutputLoggerProvider(testOutputHelperAccessor)); - } - } -} +using EntityDb.Common.Extensions; +using EntityDb.Redis.Extensions; +using EntityDb.TestImplementations.Entities; +using EntityDb.TestImplementations.Strategies; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit.DependencyInjection; +using Xunit.DependencyInjection.Logging; + +namespace EntityDb.Redis.Tests +{ + public class Startup + { + public void ConfigureServices(IServiceCollection serviceCollection) + { + serviceCollection.AddDefaultLogger(); + + serviceCollection.AddDefaultResolvingStrategy(); + + serviceCollection.AddLifoResolvingStrategyChain(); + + serviceCollection.AddEntity(); + + serviceCollection.AddTestModeRedisSnapshots + ( + TransactionEntity.RedisKeyNamespace, + _ => "127.0.0.1:6379" + ); + } + + public void Configure(ILoggerFactory loggerFactory, ITestOutputHelperAccessor testOutputHelperAccessor) + { + loggerFactory.AddProvider(new XunitTestOutputLoggerProvider(testOutputHelperAccessor)); + } + } +} diff --git a/src/EntityDb.Redis/Extensions/IServiceCollectionExtensions.cs b/src/EntityDb.Redis/Extensions/IServiceCollectionExtensions.cs index 82a98d8d..f4750728 100644 --- a/src/EntityDb.Redis/Extensions/IServiceCollectionExtensions.cs +++ b/src/EntityDb.Redis/Extensions/IServiceCollectionExtensions.cs @@ -1,6 +1,11 @@ using EntityDb.Abstractions.Snapshots; +using EntityDb.Abstractions.Strategies; +using EntityDb.Abstractions.Transactions; +using EntityDb.Common.Entities; +using EntityDb.Common.Transactions; using EntityDb.Redis.Snapshots; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using System; using System.Diagnostics.CodeAnalysis; @@ -20,8 +25,6 @@ public static class IServiceCollectionExtensions /// The namespace used to build a Redis key. /// A function that retrieves the Redis connection string. /// - /// - /// /// The production-ready implementation will store snapshots as they come in. If you need write an integration test, /// consider using /// @@ -31,6 +34,9 @@ public static class IServiceCollectionExtensions public static void AddRedisSnapshots(this IServiceCollection serviceCollection, string keyNamespace, Func getConnectionString) { + serviceCollection.AddSingleton>(serviceProvider => + SnapshottingTransactionSubscriber.Create(serviceProvider, false)); + serviceCollection.AddSingleton>(serviceProvider => { var connectionString = getConnectionString.Invoke(serviceProvider); @@ -56,6 +62,9 @@ public static void AddRedisSnapshots(this IServiceCollection serviceCol public static void AddTestModeRedisSnapshots(this IServiceCollection serviceCollection, string keyNamespace, Func getConnectionString) { + serviceCollection.AddSingleton>(serviceProvider => + SnapshottingTransactionSubscriber.Create(serviceProvider, true)); + serviceCollection.AddSingleton>(serviceProvider => { var connectionString = getConnectionString.Invoke(serviceProvider); diff --git a/src/EntityDb.Redis/Snapshots/RedisSnapshotRepository.cs b/src/EntityDb.Redis/Snapshots/RedisSnapshotRepository.cs index a31b9c74..36b6b457 100644 --- a/src/EntityDb.Redis/Snapshots/RedisSnapshotRepository.cs +++ b/src/EntityDb.Redis/Snapshots/RedisSnapshotRepository.cs @@ -1,4 +1,5 @@ using EntityDb.Abstractions.Snapshots; +using EntityDb.Abstractions.Strategies; using EntityDb.Redis.Envelopes; using EntityDb.Redis.Sessions; using System; @@ -10,24 +11,41 @@ namespace EntityDb.Redis.Snapshots internal class RedisSnapshotRepository : ISnapshotRepository { private readonly string _keyNamespace; + private readonly ISnapshottingStrategy? _snapshottingStrategy; - public RedisSnapshotRepository(IRedisSession redisSession, string keyNamespace) + public RedisSnapshotRepository + ( + IRedisSession redisSession, + string keyNamespace, + ISnapshottingStrategy? snapshottingStrategy = null + ) { RedisSession = redisSession; _keyNamespace = keyNamespace; + _snapshottingStrategy = snapshottingStrategy; } public IRedisSession RedisSession { get; } - public virtual Task PutSnapshot(Guid entityId, TEntity entity) + public virtual async Task PutSnapshot(Guid entityId, TEntity entity) { - return RedisSession.ExecuteCommand + if (_snapshottingStrategy != null) + { + var previousSnapshot = await GetSnapshot(entityId); + + if (_snapshottingStrategy.ShouldPutSnapshot(previousSnapshot, entity) == false) + { + return false; + } + } + + return await RedisSession.ExecuteCommand ( - (serviceProvider, redisTransaction) => + (logger, redisTransaction) => { - var jsonElementEnvelope = JsonElementEnvelope.Deconstruct(entity, serviceProvider); + var jsonElementEnvelope = JsonElementEnvelope.Deconstruct(entity, logger); - var snapshotValue = jsonElementEnvelope.Serialize(serviceProvider); + var snapshotValue = jsonElementEnvelope.Serialize(logger); var key = GetKey(entityId); diff --git a/src/EntityDb.Redis/Snapshots/RedisSnapshotRepositoryFactory.cs b/src/EntityDb.Redis/Snapshots/RedisSnapshotRepositoryFactory.cs index 83e5f137..23b43616 100644 --- a/src/EntityDb.Redis/Snapshots/RedisSnapshotRepositoryFactory.cs +++ b/src/EntityDb.Redis/Snapshots/RedisSnapshotRepositoryFactory.cs @@ -13,18 +13,21 @@ namespace EntityDb.Redis.Snapshots { internal class RedisSnapshotRepositoryFactory : ISnapshotRepositoryFactory { - protected readonly string _connectionString; + private readonly ILogger _logger; + private readonly IResolvingStrategyChain _resolvingStrategyChain; + private readonly string _connectionString; + protected readonly string _keyNamespace; - protected ILogger _logger; - protected IResolvingStrategyChain _resolvingStrategyChain; + protected readonly ISnapshottingStrategy? _snapshottingStrategy; public RedisSnapshotRepositoryFactory(ILoggerFactory loggerFactory, - IResolvingStrategyChain resolvingStrategyChain, string connectionString, string keyNamespace) + IResolvingStrategyChain resolvingStrategyChain, string connectionString, string keyNamespace, ISnapshottingStrategy? snapshottingStrategy = null) { _logger = loggerFactory.CreateLogger(); _resolvingStrategyChain = resolvingStrategyChain; _connectionString = connectionString; _keyNamespace = keyNamespace; + _snapshottingStrategy = snapshottingStrategy; } public async Task> CreateRepository(ISnapshotSessionOptions snapshotSessionOptions) @@ -39,7 +42,7 @@ public async Task> CreateRepository(ISnapshotSessio [ExcludeFromCodeCoverage(Justification = "Tests use TestMode.")] internal virtual ISnapshotRepository CreateRepository(IRedisSession redisSession) { - return new RedisSnapshotRepository(redisSession, _keyNamespace); + return new RedisSnapshotRepository(redisSession, _keyNamespace, _snapshottingStrategy); } public static RedisSnapshotRepositoryFactory Create(IServiceProvider serviceProvider, diff --git a/src/EntityDb.Redis/Snapshots/TestModeRedisSnapshotRepository.cs b/src/EntityDb.Redis/Snapshots/TestModeRedisSnapshotRepository.cs index 94d1029f..0fcdbc68 100644 --- a/src/EntityDb.Redis/Snapshots/TestModeRedisSnapshotRepository.cs +++ b/src/EntityDb.Redis/Snapshots/TestModeRedisSnapshotRepository.cs @@ -1,4 +1,5 @@ -using EntityDb.Redis.Sessions; +using EntityDb.Abstractions.Strategies; +using EntityDb.Redis.Sessions; using System; using System.Collections.Generic; using System.Linq; @@ -8,27 +9,42 @@ namespace EntityDb.Redis.Snapshots { internal sealed class TestModeRedisSnapshotRepository : RedisSnapshotRepository { - private readonly List _disposeEntityIds = new(); + private readonly TestModeRedisSnapshotRepositoryDisposer _disposer; - public TestModeRedisSnapshotRepository(IRedisSession redisSession, string keyNamespace) : base(redisSession, - keyNamespace) + public TestModeRedisSnapshotRepository + ( + TestModeRedisSnapshotRepositoryDisposer disposer, + IRedisSession redisSession, + string keyNamespace, + ISnapshottingStrategy? snapshottingStrategy = null + ) : base(redisSession, keyNamespace, snapshottingStrategy) { + _disposer = disposer; + + _disposer.Lock(); } public override Task PutSnapshot(Guid entityId, TEntity entity) { - _disposeEntityIds.Add(entityId); + _disposer.AddDisposeId(entityId); return base.PutSnapshot(entityId, entity); } public override async ValueTask DisposeAsync() { + _disposer.Release(); + + if (_disposer.TryDispose(out var disposeIds) == false) + { + return; + } + await RedisSession.ExecuteCommand ( (_, redisTransaction) => { - var tasks = _disposeEntityIds + var tasks = disposeIds .Select(GetKey) .Select(key => redisTransaction.KeyDeleteAsync(key)) .ToArray(); diff --git a/src/EntityDb.Redis/Snapshots/TestModeRedisSnapshotRepositoryDisposer.cs b/src/EntityDb.Redis/Snapshots/TestModeRedisSnapshotRepositoryDisposer.cs new file mode 100644 index 00000000..feb2f250 --- /dev/null +++ b/src/EntityDb.Redis/Snapshots/TestModeRedisSnapshotRepositoryDisposer.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; + +namespace EntityDb.Redis.Snapshots +{ + internal class TestModeRedisSnapshotRepositoryDisposer + { + private uint _locks = 0; + private readonly List _disposeIds = new(); + + public void Lock() + { + lock (this) + { + _locks += 1; + } + } + + public void AddDisposeId(Guid disposeId) + { + lock (this) + { + _disposeIds.Add(disposeId); + } + } + + public void Release() + { + lock (this) + { + _locks -= 1; + } + } + + public bool TryDispose(out Guid[] disposeIds) + { + lock (this) + { + if (_locks > 0) + { + disposeIds = Array.Empty(); + return false; + } + + disposeIds = _disposeIds.ToArray(); + + _disposeIds.Clear(); + + return true; + } + } + } +} diff --git a/src/EntityDb.Redis/Snapshots/TestModeRedisSnapshotRepositoryFactory.cs b/src/EntityDb.Redis/Snapshots/TestModeRedisSnapshotRepositoryFactory.cs index 783d243a..3084c880 100644 --- a/src/EntityDb.Redis/Snapshots/TestModeRedisSnapshotRepositoryFactory.cs +++ b/src/EntityDb.Redis/Snapshots/TestModeRedisSnapshotRepositoryFactory.cs @@ -4,20 +4,23 @@ using EntityDb.Redis.Sessions; using Microsoft.Extensions.DependencyInjection; using System; +using System.Collections.Concurrent; namespace EntityDb.Redis.Snapshots { internal sealed class TestModeRedisSnapshotRepositoryFactory : RedisSnapshotRepositoryFactory { + private readonly TestModeRedisSnapshotRepositoryDisposer _disposer = new(); + public TestModeRedisSnapshotRepositoryFactory(ILoggerFactory loggerFactory, - IResolvingStrategyChain resolvingStrategyChain, string connectionString, string keyNamespace) : base( - loggerFactory, resolvingStrategyChain, connectionString, keyNamespace) + IResolvingStrategyChain resolvingStrategyChain, string connectionString, string keyNamespace, ISnapshottingStrategy? snapshottingStrategy = null) : base( + loggerFactory, resolvingStrategyChain, connectionString, keyNamespace, snapshottingStrategy) { } internal override ISnapshotRepository CreateRepository(IRedisSession redisSession) { - return new TestModeRedisSnapshotRepository(redisSession, _keyNamespace); + return new TestModeRedisSnapshotRepository(_disposer, redisSession, _keyNamespace, _snapshottingStrategy); } public static new TestModeRedisSnapshotRepositoryFactory Create(IServiceProvider serviceProvider, diff --git a/src/EntityDb.RedisMongoDb.Tests/Startup.cs b/src/EntityDb.RedisMongoDb.Tests/Startup.cs index 613ff397..f543aa6a 100644 --- a/src/EntityDb.RedisMongoDb.Tests/Startup.cs +++ b/src/EntityDb.RedisMongoDb.Tests/Startup.cs @@ -1,48 +1,48 @@ -using EntityDb.Common.Extensions; -using EntityDb.MongoDb.Provisioner.Extensions; -using EntityDb.Redis.Extensions; -using EntityDb.TestImplementations.Agents; -using EntityDb.TestImplementations.Entities; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Xunit.DependencyInjection; -using Xunit.DependencyInjection.Logging; - -namespace EntityDb.RedisMongoDb.Tests -{ - public class Startup - { - public void ConfigureServices(IServiceCollection serviceCollection) - { - serviceCollection.AddDefaultLogger(); - - serviceCollection.AddAgentAccessor(); - - serviceCollection.AddDefaultResolvingStrategy(); - - serviceCollection.AddLifoResolvingStrategyChain(); - - serviceCollection.AddConstructingStrategy(); - serviceCollection.AddVersionedEntityVersioningStrategy(); - serviceCollection.AddLeasedEntityLeasingStrategy(); - serviceCollection.AddAuthorizedEntityAuthorizingStrategy(); - - serviceCollection.AddTestModeRedisSnapshots - ( - TransactionEntity.RedisKeyNamespace, - _ => "127.0.0.1:6379" - ); - - serviceCollection.AddAutoProvisionTestModeMongoDbTransactions - ( - TransactionEntity.MongoCollectionName, - _ => "mongodb://127.0.0.1:27017/?connect=direct&replicaSet=entitydb" - ); - } - - public void Configure(ILoggerFactory loggerFactory, ITestOutputHelperAccessor testOutputHelperAccessor) - { - loggerFactory.AddProvider(new XunitTestOutputLoggerProvider(testOutputHelperAccessor)); - } - } -} +using EntityDb.Common.Extensions; +using EntityDb.MongoDb.Provisioner.Extensions; +using EntityDb.Redis.Extensions; +using EntityDb.TestImplementations.Agents; +using EntityDb.TestImplementations.Entities; +using EntityDb.TestImplementations.Strategies; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit.DependencyInjection; +using Xunit.DependencyInjection.Logging; + +namespace EntityDb.RedisMongoDb.Tests +{ + public class Startup + { + public void ConfigureServices(IServiceCollection serviceCollection) + { + serviceCollection.AddDefaultLogger(); + + serviceCollection.AddAgentAccessor(); + + serviceCollection.AddDefaultResolvingStrategy(); + + serviceCollection.AddLifoResolvingStrategyChain(); + + serviceCollection.AddEntity(); + serviceCollection.AddLeasedEntityLeasingStrategy(); + serviceCollection.AddAuthorizedEntityAuthorizingStrategy(); + + serviceCollection.AddTestModeRedisSnapshots + ( + TransactionEntity.RedisKeyNamespace, + _ => "127.0.0.1:6379" + ); + + serviceCollection.AddAutoProvisionTestModeMongoDbTransactions + ( + TransactionEntity.MongoCollectionName, + _ => "mongodb://127.0.0.1:27017/?connect=direct&replicaSet=entitydb" + ); + } + + public void Configure(ILoggerFactory loggerFactory, ITestOutputHelperAccessor testOutputHelperAccessor) + { + loggerFactory.AddProvider(new XunitTestOutputLoggerProvider(testOutputHelperAccessor)); + } + } +} diff --git a/src/EntityDb.TestImplementations/Commands/SetRole.cs b/src/EntityDb.TestImplementations/Commands/SetRole.cs new file mode 100644 index 00000000..a0fc2b18 --- /dev/null +++ b/src/EntityDb.TestImplementations/Commands/SetRole.cs @@ -0,0 +1,16 @@ +using EntityDb.Abstractions.Commands; +using EntityDb.Abstractions.Facts; +using EntityDb.TestImplementations.Entities; +using EntityDb.TestImplementations.Facts; +using System.Collections.Generic; + +namespace EntityDb.TestImplementations.Commands +{ + public record SetRole(string Role) : ICommand + { + public IEnumerable> Execute(TransactionEntity entity) + { + yield return new RoleSet(Role); + } + } +} diff --git a/src/EntityDb.TestImplementations/Entities/ProjectionEntity.cs b/src/EntityDb.TestImplementations/Entities/ProjectionEntity.cs deleted file mode 100644 index f3e4f2c3..00000000 --- a/src/EntityDb.TestImplementations/Entities/ProjectionEntity.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace EntityDb.TestImplementations.Entities -{ - public class ProjectionEntity - { - } -} diff --git a/src/EntityDb.TestImplementations/Facts/RoleSet.cs b/src/EntityDb.TestImplementations/Facts/RoleSet.cs new file mode 100644 index 00000000..83a40d4c --- /dev/null +++ b/src/EntityDb.TestImplementations/Facts/RoleSet.cs @@ -0,0 +1,13 @@ +using EntityDb.Abstractions.Facts; +using EntityDb.TestImplementations.Entities; + +namespace EntityDb.TestImplementations.Facts +{ + public record RoleSet(string Role) : IFact + { + public TransactionEntity Reduce(TransactionEntity entity) + { + return entity with { Role = Role }; + } + } +} diff --git a/src/EntityDb.TestImplementations/Queries/CountMask.cs b/src/EntityDb.TestImplementations/Queries/CountMask.cs index af4c6f79..f9c057d3 100644 --- a/src/EntityDb.TestImplementations/Queries/CountMask.cs +++ b/src/EntityDb.TestImplementations/Queries/CountMask.cs @@ -1,5 +1,5 @@ using EntityDb.Abstractions.Queries.FilterBuilders; -using EntityDb.Common.Queries.Filters; +using EntityDb.Abstractions.Queries.Filters; using EntityDb.TestImplementations.Commands; using EntityDb.TestImplementations.Facts; using EntityDb.TestImplementations.Leases; diff --git a/src/EntityDb.TestImplementations/Entities/TransactionStateConstructingStrategy.cs b/src/EntityDb.TestImplementations/Strategies/TransactionStateConstructingStrategy.cs similarity index 72% rename from src/EntityDb.TestImplementations/Entities/TransactionStateConstructingStrategy.cs rename to src/EntityDb.TestImplementations/Strategies/TransactionStateConstructingStrategy.cs index 7ee1316f..8ed58ca8 100644 --- a/src/EntityDb.TestImplementations/Entities/TransactionStateConstructingStrategy.cs +++ b/src/EntityDb.TestImplementations/Strategies/TransactionStateConstructingStrategy.cs @@ -1,13 +1,14 @@ -using EntityDb.Abstractions.Strategies; -using System; - -namespace EntityDb.TestImplementations.Entities -{ - public class TransactionEntityConstructingStrategy : IConstructingStrategy - { - public TransactionEntity Construct(Guid entityId) - { - return new TransactionEntity(); - } - } -} +using EntityDb.Abstractions.Strategies; +using EntityDb.TestImplementations.Entities; +using System; + +namespace EntityDb.TestImplementations.Strategies +{ + public class TransactionEntityConstructingStrategy : IConstructingStrategy + { + public TransactionEntity Construct(Guid entityId) + { + return new TransactionEntity(); + } + } +}