From 7804e35d68361813f68ecfeb4102f20b0c99ab19 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Sat, 13 Mar 2021 12:59:43 +0000 Subject: [PATCH 1/2] WIP #748 - Extend EF Core to support multi-tenant This adds new elective functionality to EF Core persistence: * Use non-pooled DB Contexts * Allow control of (non pooled) DB Contexts service lifetimes --- .../Extensions/ServiceCollectionExtensions.cs | 108 +++++++++++++++++- 1 file changed, 104 insertions(+), 4 deletions(-) diff --git a/src/persistence/Elsa.Persistence.EntityFramework/Elsa.Persistence.EntityFramework.Core/Extensions/ServiceCollectionExtensions.cs b/src/persistence/Elsa.Persistence.EntityFramework/Elsa.Persistence.EntityFramework.Core/Extensions/ServiceCollectionExtensions.cs index 802e8a1d5..3708b60e9 100644 --- a/src/persistence/Elsa.Persistence.EntityFramework/Elsa.Persistence.EntityFramework.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/persistence/Elsa.Persistence.EntityFramework/Elsa.Persistence.EntityFramework.Core/Extensions/ServiceCollectionExtensions.cs @@ -9,19 +9,119 @@ namespace Elsa.Persistence.EntityFramework.Core.Extensions { public static class ServiceCollectionExtensions { - public static ElsaOptions UseEntityFrameworkPersistence(this ElsaOptions elsa, Action configure, bool autoRunMigrations = true) => + /// + /// Configures Elsa to use Entity Framework Core for persistence, using pooled DB Context instances. + /// + /// + /// + /// Pooled DB Context instances is a performance optimisation which is documented in more detail at + /// https://docs.microsoft.com/en-us/ef/core/performance/advanced-performance-topics?tabs=with-constant#dbcontext-pooling. + /// + /// + /// An Elsa options builder + /// A configuration builder callback + /// If true then migration scripts will auto-run on application startup; if false then they will not + /// The Elsa options builder, so calls may be chained + public static ElsaOptions UseEntityFrameworkPersistence(this ElsaOptions elsa, + Action configure, + bool autoRunMigrations = true) => elsa.UseEntityFrameworkPersistence((_, builder) => configure(builder), autoRunMigrations); - public static ElsaOptions UseEntityFrameworkPersistence(this ElsaOptions elsa, Action configure, bool autorunMigrations = true) + /// + /// Configures Elsa to use Entity Framework Core for persistence, using pooled DB Context instances. + /// + /// + /// + /// Pooled DB Context instances is a performance optimisation which is documented in more detail at + /// https://docs.microsoft.com/en-us/ef/core/performance/advanced-performance-topics?tabs=with-constant#dbcontext-pooling. + /// + /// + /// An Elsa options builder + /// A configuration builder callback, which also provides access to a service provider + /// If true then migration scripts will auto-run on application startup; if false then they will not + /// The Elsa options builder, so calls may be chained + public static ElsaOptions UseEntityFrameworkPersistence(this ElsaOptions elsa, + Action configure, + bool autoRunMigrations = true) => + UseEntityFrameworkPersistence(elsa, configure, autoRunMigrations, true, ServiceLifetime.Singleton); + + /// + /// Configures Elsa to use Entity Framework Core for persistence, without using pooled DB Context instances. + /// + /// + /// + /// Use this method when you do not wish to use DB connection pooling, such as when integrating with a multi-tenant + /// application, where re-use of DB Context objects is impractical. + /// + /// + /// Auto-running of migrations is not supported in this scenario. When pooling is not in use and each instance of + /// the DB Context may differ, it is not feasible to try to automatically migrate them. + /// Your application is responsible for executing the contents of the class in a manner + /// which is suitable for your use-case. + /// + /// + /// An Elsa options builder + /// A configuration builder callback + /// The service lifetime which will be used for each DB Context instance + /// The Elsa options builder, so calls may be chained + public static ElsaOptions UseNonPooledEntityFrameworkPersistence(this ElsaOptions elsa, + Action configure, + ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) => + elsa.UseNonPooledEntityFrameworkPersistence((_, builder) => configure(builder), serviceLifetime); + + /// + /// Configures Elsa to use Entity Framework Core for persistence, without using pooled DB Context instances. + /// + /// + /// + /// Use this method when you do not wish to use DB connection pooling, such as when integrating with a multi-tenant + /// application, where re-use of DB Context objects is impractical. + /// + /// + /// Auto-running of migrations is not supported in this scenario. When pooling is not in use and each instance of + /// the DB Context may differ, it is not feasible to try to automatically migrate them. + /// Your application is responsible for executing the contents of the class in a manner + /// which is suitable for your use-case. + /// + /// + /// An Elsa options builder + /// A configuration builder callback, which also provides access to a service provider + /// The service lifetime which will be used for each DB Context instance + /// The Elsa options builder, so calls may be chained + public static ElsaOptions UseNonPooledEntityFrameworkPersistence(this ElsaOptions elsa, + Action configure, + ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) => + UseEntityFrameworkPersistence(elsa, configure, false, false, serviceLifetime); + + static ElsaOptions UseEntityFrameworkPersistence(ElsaOptions elsa, + Action configure, + bool autoRunMigrations, + bool useContextPooling, + ServiceLifetime serviceLifetime) { + /* Auto-running migrations is intentionally unavailable when not using context pooling. + * When we aren't using pooling then it probably means that each DB Context is different + * in some manner. That could easily mean the connection strings (IE: Contexts might not + * all connect to the same DB). In that case, without further logic (which can't be + * pre-empted by Elsa), we can't be sure we're connecting to the right DBs when running + * migrations. + * + * It's much more sane just to explicitly not-support it and leave it to the app developer. + * They can run their own migrations in line with their own logic. + */ + + if(useContextPooling) + elsa.Services.AddPooledDbContextFactory(configure); + else + elsa.Services.AddDbContextFactory(configure, serviceLifetime); + elsa.Services - .AddPooledDbContextFactory(configure) .AddScoped() .AddScoped() .AddScoped() .AddScoped(); - if (autorunMigrations) + if (autoRunMigrations) elsa.Services.AddStartupTask(); return elsa From 4873aac316d5f123e24fa29c2030c4de025aff2b Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Sat, 13 Mar 2021 15:21:44 +0000 Subject: [PATCH 2/2] Resolve #748 - Test coverage for EF Db Contexts As well as providing a unit test which matches other persistence provider tests (round-tripping a workflow instance), two other tests are included here: * When using the default DI registration extension method, this sets up pooling of DB contexts and so a resolved context will come from the pool. * When using the non-pooled DI registration extension method, this sets up EF without pooling, which is more useful in a multi-tenanted environment. There are also some other changes in this commit corresponding to improving the tests. Mainly de-duplicating logic & improving names. --- ...PersistableWorkflowAndEfSqliteAttribute.cs | 28 +++++ ...PersistableWorkflowAndMongoDbAttribute.cs} | 15 ++- ...uilderWithPersistableWorkflowAttribute.cs} | 10 +- .../Autofixture/StubElsaContextAttribute.cs | 13 +++ .../TestingElsaOptionsBuilderExtensions.cs | 20 ++++ .../Elsa.Core.IntegrationTests.csproj | 91 ++++++++-------- .../EntityFrameworkIntegrationTests.cs | 100 ++++++++++++++++++ .../InMemory/InMemoryStoreIntegrationTests.cs | 10 +- .../MongoDb/MongoDbIntegrationTests.cs | 3 +- .../HostBubilderUsingServicesCustomization.cs | 22 +--- ...pecimenBuilderForParameterCustomization.cs | 36 +++++++ .../StubElsaContextCustomization.cs | 15 +++ .../StubElsaContextSpecimenBuilder.cs | 19 ++++ .../Elsa.Testing.Shared.csproj | 2 + 14 files changed, 299 insertions(+), 85 deletions(-) create mode 100644 test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithPersistableWorkflowAndEfSqliteAttribute.cs rename test/integration/Elsa.Core.IntegrationTests/Autofixture/{HostBuilderWithElsaSampleWorkflowAndMongoDbAttribute.cs => HostBuilderWithPersistableWorkflowAndMongoDbAttribute.cs} (58%) rename test/integration/Elsa.Core.IntegrationTests/Autofixture/{HostBuilderWithElsaSampleWorkflowAttribute.cs => HostBuilderWithPersistableWorkflowAttribute.cs} (54%) create mode 100644 test/integration/Elsa.Core.IntegrationTests/Autofixture/StubElsaContextAttribute.cs create mode 100644 test/integration/Elsa.Core.IntegrationTests/Autofixture/TestingElsaOptionsBuilderExtensions.cs create mode 100644 test/integration/Elsa.Core.IntegrationTests/Persistence/EntityFramework/EntityFrameworkIntegrationTests.cs create mode 100644 test/shared/Elsa.Testing.Shared/AutoFixture/Customizations/SpecimenBuilderForParameterCustomization.cs create mode 100644 test/shared/Elsa.Testing.Shared/AutoFixture/Customizations/StubElsaContextCustomization.cs create mode 100644 test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/StubElsaContextSpecimenBuilder.cs diff --git a/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithPersistableWorkflowAndEfSqliteAttribute.cs b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithPersistableWorkflowAndEfSqliteAttribute.cs new file mode 100644 index 000000000..4fd62d32a --- /dev/null +++ b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithPersistableWorkflowAndEfSqliteAttribute.cs @@ -0,0 +1,28 @@ +using System.Reflection; +using AutoFixture; +using AutoFixture.Xunit2; +using Elsa.Testing.Shared.AutoFixture.Customizations; +using Microsoft.Extensions.DependencyInjection; +using Elsa.Persistence.EntityFramework.Core.Extensions; +using Microsoft.EntityFrameworkCore; +using Elsa.Persistence.EntityFramework.Sqlite; + +namespace Elsa.Core.IntegrationTests.Autofixture +{ + public class HostBuilderWithPersistableWorkflowAndEfSqliteAttribute : CustomizeAttribute + { + public override ICustomization GetCustomization(ParameterInfo parameter) + { + return new HostBubilderUsingServicesCustomization(services => { + services + .AddElsa(elsa => { + elsa + .UseEntityFrameworkPersistence(opts => { + opts.UseSqlite("Data Source=elsa.db;", db => db.MigrationsAssembly(typeof(SqliteElsaContextFactory).Assembly.GetName().Name)); + }) + .AddPersistableWorkflow(); + }); + }, parameter); + } + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaSampleWorkflowAndMongoDbAttribute.cs b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithPersistableWorkflowAndMongoDbAttribute.cs similarity index 58% rename from test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaSampleWorkflowAndMongoDbAttribute.cs rename to test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithPersistableWorkflowAndMongoDbAttribute.cs index a721bb2aa..c6548b10f 100644 --- a/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaSampleWorkflowAndMongoDbAttribute.cs +++ b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithPersistableWorkflowAndMongoDbAttribute.cs @@ -6,23 +6,22 @@ using Elsa.Core.IntegrationTests.Workflows; using Elsa.Persistence.MongoDb.Extensions; using Elsa.Testing.Shared.AutoFixture.Customizations; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; namespace Elsa.Core.IntegrationTests.Autofixture { - public class HostBuilderWithElsaSampleWorkflowAndMongoDbAttribute : CustomizeAttribute + public class HostBuilderWithPersistableWorkflowAndMongoDbAttribute : CustomizeAttribute { public override ICustomization GetCustomization(ParameterInfo parameter) { return new HostBubilderUsingServicesCustomization(services => { services .AddElsa(elsa => { - elsa.UseMongoDbPersistence(opts => { - opts.ConnectionString = "mongodb://localhost:27017"; - opts.DatabaseName = "IntegrationTests"; - }); - elsa.AddActivity(); - elsa.AddWorkflow(); + elsa + .UseMongoDbPersistence(opts => { + opts.ConnectionString = "mongodb://localhost:27017"; + opts.DatabaseName = "IntegrationTests"; + }) + .AddPersistableWorkflow(); }) ; }, parameter); diff --git a/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaSampleWorkflowAttribute.cs b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithPersistableWorkflowAttribute.cs similarity index 54% rename from test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaSampleWorkflowAttribute.cs rename to test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithPersistableWorkflowAttribute.cs index d3048e265..6aaeae909 100644 --- a/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaSampleWorkflowAttribute.cs +++ b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithPersistableWorkflowAttribute.cs @@ -1,24 +1,18 @@ using System.Reflection; using AutoFixture; using AutoFixture.Xunit2; -using Elsa.Activities.UserTask.Activities; -using Elsa.Core.IntegrationTests.Workflows; using Elsa.Testing.Shared.AutoFixture.Customizations; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; namespace Elsa.Core.IntegrationTests.Autofixture { - public class HostBuilderWithElsaSampleWorkflowAttribute : CustomizeAttribute + public class HostBuilderWithPersistableWorkflowAttribute : CustomizeAttribute { public override ICustomization GetCustomization(ParameterInfo parameter) { return new HostBubilderUsingServicesCustomization(services => { services - .AddElsa(elsa => { - elsa.AddActivity(); - elsa.AddWorkflow(); - }); + .AddElsa(elsa => elsa.AddPersistableWorkflow()); }, parameter); } } diff --git a/test/integration/Elsa.Core.IntegrationTests/Autofixture/StubElsaContextAttribute.cs b/test/integration/Elsa.Core.IntegrationTests/Autofixture/StubElsaContextAttribute.cs new file mode 100644 index 000000000..a0fafa74a --- /dev/null +++ b/test/integration/Elsa.Core.IntegrationTests/Autofixture/StubElsaContextAttribute.cs @@ -0,0 +1,13 @@ +using System.Reflection; +using AutoFixture; +using AutoFixture.Xunit2; +using Elsa.Testing.Shared.AutoFixture.Customizations; + +namespace Elsa.Core.IntegrationTests.Autofixture +{ + public class StubElsaContextAttribute : CustomizeAttribute + { + public override ICustomization GetCustomization(ParameterInfo parameter) + => new StubElsaContextCustomization(parameter); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Core.IntegrationTests/Autofixture/TestingElsaOptionsBuilderExtensions.cs b/test/integration/Elsa.Core.IntegrationTests/Autofixture/TestingElsaOptionsBuilderExtensions.cs new file mode 100644 index 000000000..3dbb5dbd1 --- /dev/null +++ b/test/integration/Elsa.Core.IntegrationTests/Autofixture/TestingElsaOptionsBuilderExtensions.cs @@ -0,0 +1,20 @@ +using Elsa.Activities.UserTask.Activities; +using Elsa.Core.IntegrationTests.Workflows; + +namespace Elsa.Core.IntegrationTests.Autofixture +{ + public static class TestingElsaOptionsExtensions + { + /// + /// Adds the minimal activities and workflow applicable to running the . + /// + /// An elsa options + /// The same else options, so calls may be chained + public static ElsaOptions AddPersistableWorkflow(this ElsaOptions elsa) + { + elsa.AddActivity(); + elsa.AddWorkflow(); + return elsa; + } + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Core.IntegrationTests/Elsa.Core.IntegrationTests.csproj b/test/integration/Elsa.Core.IntegrationTests/Elsa.Core.IntegrationTests.csproj index 58e23c58f..fa0d66664 100644 --- a/test/integration/Elsa.Core.IntegrationTests/Elsa.Core.IntegrationTests.csproj +++ b/test/integration/Elsa.Core.IntegrationTests/Elsa.Core.IntegrationTests.csproj @@ -1,51 +1,54 @@  - - net5.0 - false - enable - + + net5.0 + false + enable + - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + - - - Always - - - PreserveNewest - - + + + Always + + + PreserveNewest + + - - - - - - - - + + + + + + + + + + diff --git a/test/integration/Elsa.Core.IntegrationTests/Persistence/EntityFramework/EntityFrameworkIntegrationTests.cs b/test/integration/Elsa.Core.IntegrationTests/Persistence/EntityFramework/EntityFrameworkIntegrationTests.cs new file mode 100644 index 000000000..471a418a2 --- /dev/null +++ b/test/integration/Elsa.Core.IntegrationTests/Persistence/EntityFramework/EntityFrameworkIntegrationTests.cs @@ -0,0 +1,100 @@ +using Xunit; +using System.Threading.Tasks; +using Elsa.Core.IntegrationTests.Autofixture; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.DependencyInjection; +using System.Threading; +using Elsa.Services; +using Elsa.Core.IntegrationTests.Workflows; +using Elsa.Persistence; +using Elsa.Persistence.EntityFramework.Core.Extensions; +using Microsoft.EntityFrameworkCore; +using Elsa.Persistence.EntityFramework.Core; +using Microsoft.EntityFrameworkCore.Internal; +using Moq; + +namespace Elsa.Core.IntegrationTests.Persistence.EntityFramework +{ + public class EntityFrameworkIntegrationTests + { + [Theory(DisplayName = "A persistable workflow instance with default persistence behaviour should be persisted-to and readable-from an Entity Framework store after being run"), AutoMoqData] + public async Task APersistableWorkflowInstanceWithDefaultPersistanceBehaviourShouldBeRoundTrippable([HostBuilderWithPersistableWorkflowAndEfSqlite] IHostBuilder hostBuilder) + { + hostBuilder.ConfigureServices((ctx, services) => services.AddHostedService()); + var host = await hostBuilder.StartAsync(); + } + + [Theory(DisplayName = "A resolved context should come from the pool when set up with pooling"), AutoMoqData] + public void DbContextShouldBeCreatedFromPoolWhenSetUpWithPooling(ServiceCollection serviceCollection, + [StubElsaContext] ElsaContext pooledContext, + IDbContextPool pool) + { + serviceCollection + .AddElsa(elsa => { + elsa + .UseEntityFrameworkPersistence(opts => { + opts.UseSqlite("Data Source=:memory:;Mode=Memory;"); + }); + }) + .AddSingleton(pool); + + Mock.Get(pool).Setup(x => x.Rent()).Returns(pooledContext); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + var contextFactory = serviceProvider.GetRequiredService>(); + + using(var context = contextFactory.CreateDbContext()) + { + Assert.Same(pooledContext, context); + } + } + + [Theory(DisplayName = "A resolved context should not come from the pool when set up without pooling"), AutoMoqData] + public void DbContextShouldNotBeCreatedFromPoolWhenSetUpWithoutPooling(ServiceCollection serviceCollection, + [StubElsaContext] ElsaContext pooledContext, + IDbContextPool pool) + { + serviceCollection + .AddElsa(elsa => { + elsa + .UseNonPooledEntityFrameworkPersistence((services, opts) => { + opts.UseSqlite("Data Source=:memory:;Mode=Memory;"); + }, + ServiceLifetime.Transient); + }) + .AddSingleton(pool); + + Mock.Get(pool).Setup(x => x.Rent()).Returns(pooledContext); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + var contextFactory = serviceProvider.GetRequiredService>(); + + using(var context = contextFactory.CreateDbContext()) + { + Assert.NotSame(pooledContext, context); + } + } + + class HostedWorkflowRunner : IHostedService + { + readonly IWorkflowRunner workflowRunner; + readonly IWorkflowInstanceStore instanceStore; + + public async Task StartAsync(CancellationToken cancellationToken) + { + var instance = await workflowRunner.RunWorkflowAsync(); + var retrievedInstance = await instanceStore.FindByIdAsync(instance.Id); + + Assert.NotNull(retrievedInstance); + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public HostedWorkflowRunner(IWorkflowRunner workflowRunner, IWorkflowInstanceStore instanceStore) + { + this.workflowRunner = workflowRunner ?? throw new System.ArgumentNullException(nameof(workflowRunner)); + this.instanceStore = instanceStore ?? throw new System.ArgumentNullException(nameof(instanceStore)); + } + } + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Core.IntegrationTests/Persistence/InMemory/InMemoryStoreIntegrationTests.cs b/test/integration/Elsa.Core.IntegrationTests/Persistence/InMemory/InMemoryStoreIntegrationTests.cs index 20308ff38..de2bbe5f4 100644 --- a/test/integration/Elsa.Core.IntegrationTests/Persistence/InMemory/InMemoryStoreIntegrationTests.cs +++ b/test/integration/Elsa.Core.IntegrationTests/Persistence/InMemory/InMemoryStoreIntegrationTests.cs @@ -13,7 +13,7 @@ namespace Elsa.Core.IntegrationTests.Persistence.InMemory public class InMemoryStoreIntegrationTests { [Theory(DisplayName = "A persistable workflow instance with default persistence behaviour should be persisted-to and readable-from an in-memory store after being run"), AutoMoqData] - public async Task APersistableWorkflowInstanceWithDefaultPersistanceBehaviourShouldBeRoundTrippable([HostBuilderWithElsaSampleWorkflow] IHostBuilder hostBuilder) + public async Task APersistableWorkflowInstanceWithDefaultPersistanceBehaviourShouldBeRoundTrippable([HostBuilderWithPersistableWorkflow] IHostBuilder hostBuilder) { hostBuilder.ConfigureServices((ctx, services) => { services.AddHostedService>(); @@ -22,7 +22,7 @@ namespace Elsa.Core.IntegrationTests.Persistence.InMemory } [Theory(DisplayName = "A persistable-on-suspend workflow instance should be persisted-to and readable-from an in-memory store after being run"), AutoMoqData] - public async Task APersistableOnSuspendWorkflowInstanceShouldBeRoundTrippable([HostBuilderWithElsaSampleWorkflow] IHostBuilder hostBuilder) + public async Task APersistableOnSuspendWorkflowInstanceShouldBeRoundTrippable([HostBuilderWithPersistableWorkflow] IHostBuilder hostBuilder) { hostBuilder.ConfigureServices((ctx, services) => { services.AddHostedService>(); @@ -31,7 +31,7 @@ namespace Elsa.Core.IntegrationTests.Persistence.InMemory } [Theory(DisplayName = "A persistable-on-activity-executed workflow instance should be persisted-to and readable-from an in-memory store after being run"), AutoMoqData] - public async Task APersistableOnActivityExecutedWorkflowInstanceShouldBeRoundTrippable([HostBuilderWithElsaSampleWorkflow] IHostBuilder hostBuilder) + public async Task APersistableOnActivityExecutedWorkflowInstanceShouldBeRoundTrippable([HostBuilderWithPersistableWorkflow] IHostBuilder hostBuilder) { hostBuilder.ConfigureServices((ctx, services) => { services.AddHostedService>(); @@ -40,7 +40,7 @@ namespace Elsa.Core.IntegrationTests.Persistence.InMemory } [Theory(DisplayName = "A persistable-on-workflow-burst workflow instance should be persisted-to and readable-from an in-memory store after being run"), AutoMoqData] - public async Task APersistableOnWorkflowBurstWorkflowInstanceShouldBeRoundTrippable([HostBuilderWithElsaSampleWorkflow] IHostBuilder hostBuilder) + public async Task APersistableOnWorkflowBurstWorkflowInstanceShouldBeRoundTrippable([HostBuilderWithPersistableWorkflow] IHostBuilder hostBuilder) { hostBuilder.ConfigureServices((ctx, services) => { services.AddHostedService>(); @@ -49,7 +49,7 @@ namespace Elsa.Core.IntegrationTests.Persistence.InMemory } [Theory(DisplayName = "A persistable-on-workflow-pass-completed workflow instance should be persisted-to and readable-from an in-memory store after being run"), AutoMoqData] - public async Task APersistableOnWorkflowPassCompletedWorkflowInstanceShouldBeRoundTrippable([HostBuilderWithElsaSampleWorkflow] IHostBuilder hostBuilder) + public async Task APersistableOnWorkflowPassCompletedWorkflowInstanceShouldBeRoundTrippable([HostBuilderWithPersistableWorkflow] IHostBuilder hostBuilder) { hostBuilder.ConfigureServices((ctx, services) => { services.AddHostedService>(); diff --git a/test/integration/Elsa.Core.IntegrationTests/Persistence/MongoDb/MongoDbIntegrationTests.cs b/test/integration/Elsa.Core.IntegrationTests/Persistence/MongoDb/MongoDbIntegrationTests.cs index 27fc34065..e000b24e6 100644 --- a/test/integration/Elsa.Core.IntegrationTests/Persistence/MongoDb/MongoDbIntegrationTests.cs +++ b/test/integration/Elsa.Core.IntegrationTests/Persistence/MongoDb/MongoDbIntegrationTests.cs @@ -7,14 +7,13 @@ using System.Threading; using Elsa.Services; using Elsa.Core.IntegrationTests.Workflows; using Elsa.Persistence; -using System; namespace Elsa.Core.IntegrationTests.Persistence.MongoDb { public class MongoDbIntegrationTests { [Theory(DisplayName = "A persistable workflow instance with default persistence behaviour should be persisted-to and readable-from a MongoDb store after being run"), AutoMoqData] - public async Task APersistableWorkflowInstanceWithDefaultPersistanceBehaviourShouldBeRoundTrippable([HostBuilderWithElsaSampleWorkflowAndMongoDbAttribute] IHostBuilder hostBuilder) + public async Task APersistableWorkflowInstanceWithDefaultPersistanceBehaviourShouldBeRoundTrippable([HostBuilderWithPersistableWorkflowAndMongoDbAttribute] IHostBuilder hostBuilder) { hostBuilder.ConfigureServices((ctx, services) => services.AddHostedService()); var host = await hostBuilder.StartAsync(); diff --git a/test/shared/Elsa.Testing.Shared/AutoFixture/Customizations/HostBubilderUsingServicesCustomization.cs b/test/shared/Elsa.Testing.Shared/AutoFixture/Customizations/HostBubilderUsingServicesCustomization.cs index 7f3d3fd72..e1a1800ee 100644 --- a/test/shared/Elsa.Testing.Shared/AutoFixture/Customizations/HostBubilderUsingServicesCustomization.cs +++ b/test/shared/Elsa.Testing.Shared/AutoFixture/Customizations/HostBubilderUsingServicesCustomization.cs @@ -7,31 +7,17 @@ using Microsoft.Extensions.DependencyInjection; namespace Elsa.Testing.Shared.AutoFixture.Customizations { - public class HostBubilderUsingServicesCustomization : ICustomization + public class HostBubilderUsingServicesCustomization : SpecimenBuilderForParameterCustomization { readonly Action serviceConfig; - readonly ParameterInfo? parameter; - public void Customize(IFixture fixture) - { - fixture.Customizations.Insert(0, GetSpecimenBuilder()); - } - - ISpecimenBuilder GetSpecimenBuilder() - { - var specimenBuilder = new HostBubilderUsingServicesSpecimenBuilder(serviceConfig); - if(parameter is null) return specimenBuilder; - - var paramSpec = new ParameterSpecification(parameter.ParameterType, parameter.Name); - return new FilteringSpecimenBuilder(specimenBuilder, paramSpec); - } + protected override ISpecimenBuilder GetUnfilteredSpecimenBuilder() + => new HostBubilderUsingServicesSpecimenBuilder(serviceConfig); public HostBubilderUsingServicesCustomization(Action serviceConfig, - ParameterInfo? parameter = null) + ParameterInfo? parameter) : base(parameter) { - this.parameter = parameter ?? throw new ArgumentNullException(nameof(parameter)); this.serviceConfig = serviceConfig ?? throw new ArgumentNullException(nameof(serviceConfig)); - } } } \ No newline at end of file diff --git a/test/shared/Elsa.Testing.Shared/AutoFixture/Customizations/SpecimenBuilderForParameterCustomization.cs b/test/shared/Elsa.Testing.Shared/AutoFixture/Customizations/SpecimenBuilderForParameterCustomization.cs new file mode 100644 index 000000000..fcdb765a0 --- /dev/null +++ b/test/shared/Elsa.Testing.Shared/AutoFixture/Customizations/SpecimenBuilderForParameterCustomization.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using AutoFixture; +using AutoFixture.Kernel; + +namespace Elsa.Testing.Shared.AutoFixture.Customizations +{ + /// + /// Helper base class for creating an Autofixture customization which customizes + /// a single parameter value with a specified specimen builder. + /// + public abstract class SpecimenBuilderForParameterCustomization : ICustomization + { + readonly ParameterInfo? parameter; + + public void Customize(IFixture fixture) + { + fixture.Customizations.Insert(0, GetSpecimenBuilder()); + } + + protected virtual ISpecimenBuilder GetSpecimenBuilder() + { + var unfilteredSpecimenBuilder = GetUnfilteredSpecimenBuilder(); + if(parameter is null) return unfilteredSpecimenBuilder; + + var paramSpec = new ParameterSpecification(parameter.ParameterType, parameter.Name); + return new FilteringSpecimenBuilder(unfilteredSpecimenBuilder, paramSpec); + } + + protected abstract ISpecimenBuilder GetUnfilteredSpecimenBuilder(); + + public SpecimenBuilderForParameterCustomization(ParameterInfo? parameter) + { + this.parameter = parameter; + } + } +} \ No newline at end of file diff --git a/test/shared/Elsa.Testing.Shared/AutoFixture/Customizations/StubElsaContextCustomization.cs b/test/shared/Elsa.Testing.Shared/AutoFixture/Customizations/StubElsaContextCustomization.cs new file mode 100644 index 000000000..fc67748a6 --- /dev/null +++ b/test/shared/Elsa.Testing.Shared/AutoFixture/Customizations/StubElsaContextCustomization.cs @@ -0,0 +1,15 @@ +using System.Reflection; +using AutoFixture; +using AutoFixture.Kernel; +using Elsa.Testing.Shared.AutoFixture.SpecimenBuilders; + +namespace Elsa.Testing.Shared.AutoFixture.Customizations +{ + public class StubElsaContextCustomization : SpecimenBuilderForParameterCustomization + { + protected override ISpecimenBuilder GetUnfilteredSpecimenBuilder() + => new StubElsaContextSpecimenBuilder(); + + public StubElsaContextCustomization(ParameterInfo? parameter) : base(parameter) {} + } +} \ No newline at end of file diff --git a/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/StubElsaContextSpecimenBuilder.cs b/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/StubElsaContextSpecimenBuilder.cs new file mode 100644 index 000000000..b86c9bdb4 --- /dev/null +++ b/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/StubElsaContextSpecimenBuilder.cs @@ -0,0 +1,19 @@ +using System; +using AutoFixture.Kernel; +using Elsa.Persistence.EntityFramework.Core; +using Microsoft.EntityFrameworkCore; + +namespace Elsa.Testing.Shared.AutoFixture.SpecimenBuilders +{ + public class StubElsaContextSpecimenBuilder : ISpecimenBuilder + { + public object Create(object request, ISpecimenContext context) + => request.IsAnAutofixtureRequestForType()? GetContext(context) : new NoSpecimen(); + + static object GetContext(ISpecimenContext context) + { + var options = (DbContextOptions) context.Resolve(typeof(DbContextOptions)); + return new ElsaContext(options); + } + } +} \ No newline at end of file diff --git a/test/shared/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj b/test/shared/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj index 18bcc5296..847c79ae9 100644 --- a/test/shared/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj +++ b/test/shared/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj @@ -20,10 +20,12 @@ + +