Merge pull request #756 from craigfowler/feature/748-multi-tenant-EntityFramework

Resolve #748 multi tenant entity framework
This commit is contained in:
Craig Fowler 2021-03-14 14:34:20 +00:00 committed by GitHub
commit 994e55abe1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 404 additions and 93 deletions

View file

@ -9,19 +9,119 @@ namespace Elsa.Persistence.EntityFramework.Core.Extensions
{
public static class ServiceCollectionExtensions
{
public static ElsaOptions UseEntityFrameworkPersistence(this ElsaOptions elsa, Action<DbContextOptionsBuilder> configure, bool autoRunMigrations = true) =>
/// <summary>
/// Configures Elsa to use Entity Framework Core for persistence, using pooled DB Context instances.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// </remarks>
/// <param name="elsa">An Elsa options builder</param>
/// <param name="configure">A configuration builder callback</param>
/// <param name="autoRunMigrations">If <c>true</c> then migration scripts will auto-run on application startup; if <c>false</c> then they will not</param>
/// <returns>The Elsa options builder, so calls may be chained</returns>
public static ElsaOptions UseEntityFrameworkPersistence(this ElsaOptions elsa,
Action<DbContextOptionsBuilder> configure,
bool autoRunMigrations = true) =>
elsa.UseEntityFrameworkPersistence((_, builder) => configure(builder), autoRunMigrations);
public static ElsaOptions UseEntityFrameworkPersistence(this ElsaOptions elsa, Action<IServiceProvider, DbContextOptionsBuilder> configure, bool autorunMigrations = true)
/// <summary>
/// Configures Elsa to use Entity Framework Core for persistence, using pooled DB Context instances.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// </remarks>
/// <param name="elsa">An Elsa options builder</param>
/// <param name="configure">A configuration builder callback, which also provides access to a service provider</param>
/// <param name="autoRunMigrations">If <c>true</c> then migration scripts will auto-run on application startup; if <c>false</c> then they will not</param>
/// <returns>The Elsa options builder, so calls may be chained</returns>
public static ElsaOptions UseEntityFrameworkPersistence(this ElsaOptions elsa,
Action<IServiceProvider, DbContextOptionsBuilder> configure,
bool autoRunMigrations = true) =>
UseEntityFrameworkPersistence(elsa, configure, autoRunMigrations, true, ServiceLifetime.Singleton);
/// <summary>
/// Configures Elsa to use Entity Framework Core for persistence, without using pooled DB Context instances.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// 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 <see cref="RunMigrations"/> class in a manner
/// which is suitable for your use-case.
/// </para>
/// </remarks>
/// <param name="elsa">An Elsa options builder</param>
/// <param name="configure">A configuration builder callback</param>
/// <param name="serviceLifetime">The service lifetime which will be used for each DB Context instance</param>
/// <returns>The Elsa options builder, so calls may be chained</returns>
public static ElsaOptions UseNonPooledEntityFrameworkPersistence(this ElsaOptions elsa,
Action<DbContextOptionsBuilder> configure,
ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) =>
elsa.UseNonPooledEntityFrameworkPersistence((_, builder) => configure(builder), serviceLifetime);
/// <summary>
/// Configures Elsa to use Entity Framework Core for persistence, without using pooled DB Context instances.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// 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 <see cref="RunMigrations"/> class in a manner
/// which is suitable for your use-case.
/// </para>
/// </remarks>
/// <param name="elsa">An Elsa options builder</param>
/// <param name="configure">A configuration builder callback, which also provides access to a service provider</param>
/// <param name="serviceLifetime">The service lifetime which will be used for each DB Context instance</param>
/// <returns>The Elsa options builder, so calls may be chained</returns>
public static ElsaOptions UseNonPooledEntityFrameworkPersistence(this ElsaOptions elsa,
Action<IServiceProvider, DbContextOptionsBuilder> configure,
ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) =>
UseEntityFrameworkPersistence(elsa, configure, false, false, serviceLifetime);
static ElsaOptions UseEntityFrameworkPersistence(ElsaOptions elsa,
Action<IServiceProvider, DbContextOptionsBuilder> 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<ElsaContext>(configure);
else
elsa.Services.AddDbContextFactory<ElsaContext>(configure, serviceLifetime);
elsa.Services
.AddPooledDbContextFactory<ElsaContext>(configure)
.AddScoped<EntityFrameworkWorkflowDefinitionStore>()
.AddScoped<EntityFrameworkWorkflowInstanceStore>()
.AddScoped<EntityFrameworkWorkflowExecutionLogRecordStore>()
.AddScoped<EntityFrameworkBookmarkStore>();
if (autorunMigrations)
if (autoRunMigrations)
elsa.Services.AddStartupTask<RunMigrations>();
return elsa

View file

@ -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);
}
}
}

View file

@ -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<UserTask>();
elsa.AddWorkflow<PersistableWorkflow>();
elsa
.UseMongoDbPersistence(opts => {
opts.ConnectionString = "mongodb://localhost:27017";
opts.DatabaseName = "IntegrationTests";
})
.AddPersistableWorkflow();
})
;
}, parameter);

View file

@ -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<UserTask>();
elsa.AddWorkflow<PersistableWorkflow>();
});
.AddElsa(elsa => elsa.AddPersistableWorkflow());
}, parameter);
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,20 @@
using Elsa.Activities.UserTask.Activities;
using Elsa.Core.IntegrationTests.Workflows;
namespace Elsa.Core.IntegrationTests.Autofixture
{
public static class TestingElsaOptionsExtensions
{
/// <summary>
/// Adds the minimal activities and workflow applicable to running the <see cref="PersistableWorkflow"/>.
/// </summary>
/// <param name="elsa">An elsa options</param>
/// <returns>The same else options, so calls may be chained</returns>
public static ElsaOptions AddPersistableWorkflow(this ElsaOptions elsa)
{
elsa.AddActivity<UserTask>();
elsa.AddWorkflow<PersistableWorkflow>();
return elsa;
}
}
}

View file

@ -1,55 +1,55 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Autofixture" Version="4.15.0" />
<PackageReference Include="Autofixture.AutoMoq" Version="4.15.0" />
<PackageReference Include="autofixture.xunit2" Version="4.15.0" />
<PackageReference Include="Moq" Version="4.16.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="NodaTime.Testing" Version="3.0.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="XunitXml.TestLogger" Version="3.0.62" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="YesSql.Core" Version="2.0.0-beta-1637" />
<PackageReference Include="YesSql.Provider.Sqlite" Version="1.0.0-beta-1637" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.19" />
<PackageReference Include="Hangfire.InMemory" Version="0.3.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.2" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Autofixture" Version="4.15.0" />
<PackageReference Include="Autofixture.AutoMoq" Version="4.15.0" />
<PackageReference Include="autofixture.xunit2" Version="4.15.0" />
<PackageReference Include="Moq" Version="4.16.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="NodaTime.Testing" Version="3.0.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="XunitXml.TestLogger" Version="3.0.62" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="YesSql.Core" Version="2.0.0-beta-1637" />
<PackageReference Include="YesSql.Provider.Sqlite" Version="1.0.0-beta-1637" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.19" />
<PackageReference Include="Hangfire.InMemory" Version="0.3.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.2" />
</ItemGroup>
<ItemGroup>
<None Update="Elsa.Core.UnitTests.xunit.runner.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Assets\hello-world-workflow.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<None Update="Elsa.Core.UnitTests.xunit.runner.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Assets\hello-world-workflow.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\core\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\shared\Elsa.Testing.Shared\Elsa.Testing.Shared.csproj" />
<ProjectReference Include="..\..\..\src\activities\Elsa.Activities.Temporal.Common\Elsa.Activities.Temporal.Common.csproj" />
<ProjectReference Include="..\..\..\src\activities\Elsa.Activities.Temporal.Hangfire\Elsa.Activities.Temporal.Hangfire.csproj" />
<ProjectReference Include="..\..\..\src\activities\Elsa.Activities.Temporal.Quartz\Elsa.Activities.Temporal.Quartz.csproj" />
<ProjectReference Include="..\..\..\src\persistence\Elsa.Persistence.MongoDb\Elsa.Persistence.MongoDb.csproj" />
<ProjectReference Include="..\..\..\src\persistence\Elsa.Persistence.YesSql\Elsa.Persistence.YesSql.csproj" />
<ProjectReference Include="..\..\..\src\persistence\Elsa.Persistence.EntityFramework\Elsa.Persistence.EntityFramework.Core\Elsa.Persistence.EntityFramework.Core.csproj" />
<ProjectReference Include="..\..\..\src\persistence\Elsa.Persistence.EntityFramework\Elsa.Persistence.EntityFramework.Sqlite\Elsa.Persistence.EntityFramework.Sqlite.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\core\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\shared\Elsa.Testing.Shared\Elsa.Testing.Shared.csproj" />
<ProjectReference Include="..\..\..\src\activities\Elsa.Activities.Temporal.Common\Elsa.Activities.Temporal.Common.csproj" />
<ProjectReference Include="..\..\..\src\activities\Elsa.Activities.Temporal.Hangfire\Elsa.Activities.Temporal.Hangfire.csproj" />
<ProjectReference Include="..\..\..\src\activities\Elsa.Activities.Temporal.Quartz\Elsa.Activities.Temporal.Quartz.csproj" />
<ProjectReference Include="..\..\..\src\persistence\Elsa.Persistence.MongoDb\Elsa.Persistence.MongoDb.csproj" />
<ProjectReference Include="..\..\..\src\persistence\Elsa.Persistence.YesSql\Elsa.Persistence.YesSql.csproj" />
<ProjectReference Include="..\..\..\src\persistence\Elsa.Persistence.EntityFramework\Elsa.Persistence.EntityFramework.Core\Elsa.Persistence.EntityFramework.Core.csproj" />
<ProjectReference Include="..\..\..\src\persistence\Elsa.Persistence.EntityFramework\Elsa.Persistence.EntityFramework.Sqlite\Elsa.Persistence.EntityFramework.Sqlite.csproj" />
</ItemGroup>
</Project>

View file

@ -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<HostedWorkflowRunner>());
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<ElsaContext> 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<IDbContextFactory<ElsaContext>>();
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<ElsaContext> 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<IDbContextFactory<ElsaContext>>();
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<PersistableWorkflow>();
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));
}
}
}
}

View file

@ -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<HostedWorkflowRunner<PersistableWorkflow>>();
@ -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<HostedWorkflowRunner<PersistableWorkflow.OnSuspend>>();
@ -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<HostedWorkflowRunner<PersistableWorkflow.OnActivityExecuted>>();
@ -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<HostedWorkflowRunner<PersistableWorkflow.OnWorkflowBurst>>();
@ -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<HostedWorkflowRunner<PersistableWorkflow.OnWorkflowPassCompleted>>();

View file

@ -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<HostedWorkflowRunner>());
var host = await hostBuilder.StartAsync();

View file

@ -7,31 +7,17 @@ using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Testing.Shared.AutoFixture.Customizations
{
public class HostBubilderUsingServicesCustomization : ICustomization
public class HostBubilderUsingServicesCustomization : SpecimenBuilderForParameterCustomization
{
readonly Action<IServiceCollection> 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<IServiceCollection> serviceConfig,
ParameterInfo? parameter = null)
ParameterInfo? parameter) : base(parameter)
{
this.parameter = parameter ?? throw new ArgumentNullException(nameof(parameter));
this.serviceConfig = serviceConfig ?? throw new ArgumentNullException(nameof(serviceConfig));
}
}
}

View file

@ -0,0 +1,36 @@
using System.Reflection;
using AutoFixture;
using AutoFixture.Kernel;
namespace Elsa.Testing.Shared.AutoFixture.Customizations
{
/// <summary>
/// Helper base class for creating an Autofixture customization which customizes
/// a single parameter value with a specified specimen builder.
/// </summary>
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;
}
}
}

View file

@ -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) {}
}
}

View file

@ -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<ElsaContext>()? GetContext(context) : new NoSpecimen();
static object GetContext(ISpecimenContext context)
{
var options = (DbContextOptions<ElsaContext>) context.Resolve(typeof(DbContextOptions<ElsaContext>));
return new ElsaContext(options);
}
}
}

View file

@ -20,10 +20,12 @@
<PackageReference Include="NodaTime" Version="3.0.3" />
<PackageReference Include="xunit.extensibility.core" Version="2.4.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\core\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\..\src\persistence\Elsa.Persistence.EntityFramework\Elsa.Persistence.EntityFramework.Core\Elsa.Persistence.EntityFramework.Core.csproj" />
</ItemGroup>
</Project>