diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index 3d9f61f27..f88e3edd0 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -86,7 +86,7 @@ using StackExchange.Redis; // ReSharper disable RedundantAssignment const PersistenceProvider persistenceProvider = PersistenceProvider.EntityFrameworkCore; -const bool useDbContextPooling = true; +const bool useDbContextPooling = false; const bool useHangfire = false; const bool useQuartz = true; const bool useMassTransit = true; @@ -105,9 +105,9 @@ const bool useTenantsFromConfiguration = true; const bool useSecrets = false; const bool disableVariableWrappers = false; const bool disableVariableCopying = false; -const bool useManualOtelInstrumentation = true; +const bool useManualOtelInstrumentation = false; -ObjectConverter.StrictMode = true; // Default. +ObjectConverter.StrictMode = false; var builder = WebApplication.CreateBuilder(args); var services = builder.Services; diff --git a/src/modules/Elsa.Alterations/Handlers/AlterationPlanCompletedHandler.cs b/src/modules/Elsa.Alterations/Handlers/AlterationPlanCompletedHandler.cs index 18a210510..f9b605055 100644 --- a/src/modules/Elsa.Alterations/Handlers/AlterationPlanCompletedHandler.cs +++ b/src/modules/Elsa.Alterations/Handlers/AlterationPlanCompletedHandler.cs @@ -1,10 +1,9 @@ using Elsa.Alterations.Bookmarks; using Elsa.Alterations.Core.Notifications; using Elsa.Mediator.Contracts; +using Elsa.Workflows; using Elsa.Workflows.Helpers; using Elsa.Workflows.Runtime; -using Elsa.Workflows.Runtime.Contracts; -using Elsa.Workflows.Runtime.Requests; using JetBrains.Annotations; namespace Elsa.Alterations.Handlers; @@ -13,7 +12,7 @@ namespace Elsa.Alterations.Handlers; /// Handles notifications and triggers any workflows that are waiting for the plan to complete. /// [UsedImplicitly] -public class AlterationPlanCompletedHandler(IWorkflowDispatcher workflowDispatcher) : INotificationHandler +public class AlterationPlanCompletedHandler(IBookmarkQueue bookmarkQueue, IStimulusHasher stimulusHasher) : INotificationHandler { /// public async Task HandleAsync(AlterationPlanCompleted notification, CancellationToken cancellationToken) @@ -21,7 +20,12 @@ public class AlterationPlanCompletedHandler(IWorkflowDispatcher workflowDispatch // Trigger any workflow instances that are waiting for the plan to complete. var planId = notification.Plan.Id; var bookmarkPayload = new AlterationPlanCompletedPayload(planId); - var triggerRequest = new DispatchTriggerWorkflowsRequest(ActivityTypeNameHelper.GenerateTypeName(), bookmarkPayload); - await workflowDispatcher.DispatchAsync(triggerRequest, cancellationToken); + var activityTypeName = ActivityTypeNameHelper.GenerateTypeName(); + var item = new NewBookmarkQueueItem + { + ActivityTypeName = activityTypeName, + StimulusHash = stimulusHasher.Hash(activityTypeName, bookmarkPayload) + }; + await bookmarkQueue.EnqueueAsync(item, cancellationToken); } } \ No newline at end of file diff --git a/src/modules/Elsa.Alterations/Services/AlterationPlanManager.cs b/src/modules/Elsa.Alterations/Services/AlterationPlanManager.cs index eb40d5d7c..23e054216 100644 --- a/src/modules/Elsa.Alterations/Services/AlterationPlanManager.cs +++ b/src/modules/Elsa.Alterations/Services/AlterationPlanManager.cs @@ -25,7 +25,7 @@ public class AlterationPlanManager(IAlterationPlanStore planStore, IAlterationJo var jobFilter = new AlterationJobFilter { PlanId = planId, - Statuses = new[] { AlterationJobStatus.Pending, AlterationJobStatus.Running } + Statuses = [AlterationJobStatus.Pending, AlterationJobStatus.Running] }; return await jobStore.CountAsync(jobFilter, cancellationToken) == 0; diff --git a/src/modules/Elsa.Alterations/Services/DefaultAlterationPlanScheduler.cs b/src/modules/Elsa.Alterations/Services/DefaultAlterationPlanScheduler.cs index fd8275282..4ab30f434 100644 --- a/src/modules/Elsa.Alterations/Services/DefaultAlterationPlanScheduler.cs +++ b/src/modules/Elsa.Alterations/Services/DefaultAlterationPlanScheduler.cs @@ -42,7 +42,7 @@ public class DefaultAlterationPlanScheduler : IAlterationPlanScheduler var workflowGraph = await _workflowDefinitionService.FindWorkflowGraphAsync(definitionId, VersionOptions.Published, cancellationToken); if (workflowGraph == null) - throw new Exception($"Workflow definition with ID '{definitionId}' not found"); + throw new($"Workflow definition with ID '{definitionId}' not found"); var serializedPlan = _jsonSerializer.Serialize(planParams); var request = new DispatchWorkflowDefinitionRequest(workflowGraph.Workflow.Identity.Id) diff --git a/src/modules/Elsa.Workflows.Core/Extensions/StringExtensions.cs b/src/modules/Elsa.Common/Extensions/StringExtensions.cs similarity index 100% rename from src/modules/Elsa.Workflows.Core/Extensions/StringExtensions.cs rename to src/modules/Elsa.Common/Extensions/StringExtensions.cs diff --git a/src/modules/Elsa.Common/Multitenancy/Entities/Tenant.cs b/src/modules/Elsa.Common/Multitenancy/Entities/Tenant.cs index a832baa2c..cba4b371b 100644 --- a/src/modules/Elsa.Common/Multitenancy/Entities/Tenant.cs +++ b/src/modules/Elsa.Common/Multitenancy/Entities/Tenant.cs @@ -13,7 +13,7 @@ public class Tenant : Entity /// /// Gets or sets the name. /// - public string Name { get; set; } = default!; + public string Name { get; set; } = null!; /// /// Gets or sets the configuration. @@ -22,7 +22,7 @@ public class Tenant : Entity public static readonly Tenant Default = new() { - Id = string.Empty, + Id = null!, Name = "Default" }; } diff --git a/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantService.cs b/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantService.cs index fca84ebc9..89298e5ac 100644 --- a/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantService.cs +++ b/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantService.cs @@ -1,3 +1,4 @@ +using Elsa.Extensions; using Microsoft.Extensions.DependencyInjection; namespace Elsa.Common.Multitenancy; @@ -19,7 +20,7 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop public async Task FindAsync(string id, CancellationToken cancellationToken = default) { var dictionary = await GetTenantsDictionaryAsync(cancellationToken); - return dictionary.TryGetValue(id, out var tenant) ? tenant : null; + return dictionary.TryGetValue(id.EmptyIfNull(), out var tenant) ? tenant : null; } public async Task FindAsync(TenantFilter filter, CancellationToken cancellationToken = default) @@ -31,7 +32,7 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop public async Task GetAsync(string id, CancellationToken cancellationToken = default) { var dictionary = await GetTenantsDictionaryAsync(cancellationToken); - return dictionary[id]; + return dictionary[id.EmptyIfNull()]; } public async Task GetAsync(TenantFilter filter, CancellationToken cancellationToken = default) @@ -76,7 +77,7 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop var tenantsProvider = scope.ServiceProvider.GetRequiredService(); var currentTenants = await GetTenantsDictionaryAsync(cancellationToken); var currentTenantIds = currentTenants.Keys; - var newTenants = (await tenantsProvider.ListAsync(cancellationToken)).ToDictionary(x => x.Id); + var newTenants = (await tenantsProvider.ListAsync(cancellationToken)).ToDictionary(x => x.Id.EmptyIfNull()); var newTenantIds = newTenants.Keys; var removedTenantIds = currentTenantIds.Except(newTenantIds).ToArray(); var addedTenantIds = newTenantIds.Except(currentTenantIds).ToArray(); @@ -129,21 +130,21 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop private async Task RegisterTenantAsync(Tenant tenant, CancellationToken cancellationToken = default) { var scope = tenantScopeFactory.CreateScope(tenant); - _tenantsDictionary![tenant.Id] = tenant; + _tenantsDictionary![tenant.Id.EmptyIfNull()] = tenant; _tenantScopesDictionary![tenant] = scope; using (tenantAccessor.PushContext(tenant)) - await tenantEvents.TenantActivatedAsync(new TenantActivatedEventArgs(tenant, scope, cancellationToken)); + await tenantEvents.TenantActivatedAsync(new(tenant, scope, cancellationToken)); } private async Task UnregisterTenantAsync(Tenant tenant, CancellationToken cancellationToken = default) { if (_tenantScopesDictionary!.Remove(tenant, out var scope)) { - _tenantsDictionary!.Remove(tenant.Id, out _); + _tenantsDictionary!.Remove(tenant.Id.EmptyIfNull(), out _); using (tenantAccessor.PushContext(tenant)) - await tenantEvents.TenantDeactivatedAsync(new TenantDeactivatedEventArgs(tenant, scope, cancellationToken)); + await tenantEvents.TenantDeactivatedAsync(new(tenant, scope, cancellationToken)); } } } \ No newline at end of file diff --git a/src/modules/Elsa.Common/Multitenancy/Models/TenantFilter.cs b/src/modules/Elsa.Common/Multitenancy/Models/TenantFilter.cs index 1fc1c1fa1..51a88590a 100644 --- a/src/modules/Elsa.Common/Multitenancy/Models/TenantFilter.cs +++ b/src/modules/Elsa.Common/Multitenancy/Models/TenantFilter.cs @@ -11,7 +11,7 @@ public class TenantFilter /// /// Gets or sets the tenant ID to filter for. /// - public string? Id { get; set; } + public string Id { get; set; } = null!; /// /// Applies the filter to the specified queryable. @@ -20,7 +20,7 @@ public class TenantFilter /// The filtered queryable. public IQueryable Apply(IQueryable queryable) { - if (Id != null) queryable = queryable.Where(x => x.Id == Id); + queryable = queryable.Where(x => x.Id == Id); return queryable; } diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs b/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs index 210f7d5a7..2a22f2a7d 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs @@ -1,4 +1,6 @@ using Elsa.Common.Entities; +using Elsa.Common.Multitenancy; +using Elsa.Extensions; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.Extensions.DependencyInjection; @@ -43,6 +45,12 @@ public abstract class ElsaDbContextBase : DbContext, IElsaDbContextSchema // ReSharper disable once VirtualMemberCallInConstructor Schema = !string.IsNullOrWhiteSpace(_elsaDbContextOptions?.SchemaName) ? _elsaDbContextOptions.SchemaName : ElsaSchema; + + var tenantAccessor = serviceProvider.GetService(); + var tenantId = tenantAccessor?.Tenant?.Id; + + if (!string.IsNullOrWhiteSpace(tenantId)) + TenantId = tenantId.NullIfEmpty(); } /// diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/EntityHandlers/ApplyTenantId.cs b/src/modules/Elsa.EntityFrameworkCore.Common/EntityHandlers/ApplyTenantId.cs index 7167f46d6..b55f9af65 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/EntityHandlers/ApplyTenantId.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/EntityHandlers/ApplyTenantId.cs @@ -1,4 +1,5 @@ using Elsa.Common.Entities; +using Elsa.Extensions; using Microsoft.EntityFrameworkCore.ChangeTracking; namespace Elsa.EntityFrameworkCore.EntityHandlers; @@ -12,7 +13,7 @@ public class ApplyTenantId : IEntitySavingHandler public ValueTask HandleAsync(ElsaDbContextBase dbContext, EntityEntry entry, CancellationToken cancellationToken = default) { if (entry.Entity is Entity entity) - entity.TenantId = dbContext.TenantId; + entity.TenantId = dbContext.TenantId.NullIfEmpty(); return default; } diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/EntityHandlers/SetTenantIdFilter.cs b/src/modules/Elsa.EntityFrameworkCore.Common/EntityHandlers/SetTenantIdFilter.cs index 0a352aabe..0e5bb9d9d 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/EntityHandlers/SetTenantIdFilter.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/EntityHandlers/SetTenantIdFilter.cs @@ -1,5 +1,6 @@ using System.Linq.Expressions; using Elsa.Common.Entities; +using Elsa.Extensions; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Query; @@ -17,8 +18,9 @@ public class SetTenantIdFilter : IEntityModelCreatingHandler if (!entityType.ClrType.IsAssignableTo(typeof(Entity))) return; + var tenantId = dbContext.TenantId.NullIfEmpty(); var parameter = Expression.Parameter(entityType.ClrType); - Expression> filterExpr = entity => dbContext.TenantId == entity.TenantId; + Expression> filterExpr = entity => entity.TenantId == tenantId; var body = ReplacingExpressionVisitor.Replace(filterExpr.Parameters[0], parameter, filterExpr.Body); var lambdaExpression = Expression.Lambda(body, parameter); diff --git a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs index 0714ca238..698e1412c 100644 --- a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs +++ b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs @@ -27,7 +27,7 @@ public record ObjectConverterOptions(JsonSerializerOptions? SerializerOptions = /// public static class ObjectConverter { - public static bool StrictMode = true; // Set to false to revert to original flexible behavior. + public static bool StrictMode = false; // Set to true to opt into strict mode. /// /// Attempts to convert the source value into the destination type. diff --git a/src/modules/Elsa.Workflows.Runtime/Services/BookmarkQueueSignaler.cs b/src/modules/Elsa.Workflows.Runtime/Services/BookmarkQueueSignaler.cs index d97b76cd1..3ab6af9cc 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/BookmarkQueueSignaler.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/BookmarkQueueSignaler.cs @@ -37,7 +37,7 @@ public class BookmarkQueueSignaler : IBookmarkQueueSignaler lock (_lock) { // Reset the TCS for the next wait - _tcs = new TaskCompletionSource(); + _tcs = new(); } } } \ No newline at end of file