< Summary

Information
Class: Elsa.Persistence.EFCore.Modules.Management.EFCoreWorkflowDefinitionStore
Assembly: Elsa.Persistence.EFCore
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Persistence.EFCore/Modules/Management/WorkflowDefinitionStore.cs
Line coverage
55%
Covered lines: 103
Uncovered lines: 83
Coverable lines: 186
Total lines: 372
Line coverage: 55.3%
Branch coverage
51%
Covered branches: 39
Total branches: 76
Branch coverage: 51.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
FindAsync()100%11100%
FindAsync()100%11100%
FindManyAsync()100%210%
FindManyAsync()100%11100%
FindManyAsync()100%11100%
FindManyAsync()100%11100%
FindSummariesAsync()100%210%
FindSummariesAsync()0%2040%
FindSummariesAsync()100%11100%
FindSummariesAsync()75%4485.71%
FindLastVersionAsync()100%11100%
SaveAsync()100%11100%
SaveManyAsync()100%11100%
TryUpdateLatestAsync()0%210140%
MatchesLoadedSnapshot(...)100%210%
DeleteAsync()75%4487.5%
AnyAsync()100%11100%
CountDistinctAsync()100%11100%
GetIsNameUnique()100%210%
OnSaveAsync(...)100%11100%
SerializeState(...)100%11100%
OnLoadAsync(...)75%4476.47%
Filter(...)78.94%3838100%
Paginate(...)0%7280%
.ctor()100%11100%
.ctor(...)100%11100%
get_Options()100%11100%
get_Variables()100%11100%
get_Inputs()100%11100%
get_Outputs()100%11100%
get_Outcomes()100%11100%
get_CustomProperties()100%11100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Persistence.EFCore/Modules/Management/WorkflowDefinitionStore.cs

#LineLine coverage
 1using System.Diagnostics.CodeAnalysis;
 2using System.Linq.Expressions;
 3using System.Text.Json.Serialization;
 4using Elsa.Common.Entities;
 5using Elsa.Common.Models;
 6using Elsa.Extensions;
 7using Elsa.Workflows;
 8using Elsa.Workflows.Management;
 9using Elsa.Workflows.Management.Entities;
 10using Elsa.Workflows.Management.Filters;
 11using Elsa.Workflows.Management.Models;
 12using Elsa.Workflows.Memory;
 13using Elsa.Workflows.Models;
 14using JetBrains.Annotations;
 15using Microsoft.EntityFrameworkCore;
 16using Microsoft.Extensions.Logging;
 17using Open.Linq.AsyncExtensions;
 18
 19namespace Elsa.Persistence.EFCore.Modules.Management;
 20
 21/// <inheritdoc />
 22[UsedImplicitly]
 45723public class EFCoreWorkflowDefinitionStore(EntityStore<ManagementElsaDbContext, WorkflowDefinition> store, IPayloadSeria
 24    : IWorkflowDefinitionStore
 25{
 26    /// <inheritdoc />
 27    public async Task<WorkflowDefinition?> FindAsync(WorkflowDefinitionFilter filter, CancellationToken cancellationToke
 28    {
 332929        var orderBy = new WorkflowDefinitionOrder<DateTimeOffset>(x => x.CreatedAt, OrderDirection.Ascending);
 332930        return await FindAsync(filter, orderBy, cancellationToken);
 332931    }
 32
 33    /// <inheritdoc />
 34    public async Task<WorkflowDefinition?> FindAsync<TOrderBy>(WorkflowDefinitionFilter filter, WorkflowDefinitionOrder<
 35    {
 668036        return await store.QueryAsync(queryable => Filter(queryable, filter).OrderBy(order), OnLoadAsync, filter.TenantA
 334037    }
 38
 39    /// <inheritdoc />
 40    public async Task<Page<WorkflowDefinition>> FindManyAsync(WorkflowDefinitionFilter filter, PageArgs pageArgs, Cancel
 41    {
 042        var orderBy = new WorkflowDefinitionOrder<DateTimeOffset>(x => x.CreatedAt, OrderDirection.Ascending);
 043        return await FindManyAsync(filter, orderBy, pageArgs, cancellationToken);
 044    }
 45
 46    /// <inheritdoc />
 47    public async Task<Page<WorkflowDefinition>> FindManyAsync<TOrderBy>(WorkflowDefinitionFilter filter, WorkflowDefinit
 48    {
 449        var count = await store.QueryAsync(queryable => Filter(queryable, filter), cancellationToken).LongCount();
 450        var results = await store.QueryAsync(queryable => Filter(queryable, filter).OrderBy(order).Paginate(pageArgs), O
 251        return new(results, count);
 252    }
 53
 54    /// <inheritdoc />
 55    public async Task<IEnumerable<WorkflowDefinition>> FindManyAsync(WorkflowDefinitionFilter filter, CancellationToken 
 56    {
 575457        var orderBy = new WorkflowDefinitionOrder<DateTimeOffset>(x => x.CreatedAt, OrderDirection.Ascending);
 575458        return await FindManyAsync(filter, orderBy, cancellationToken);
 575459    }
 60
 61    /// <inheritdoc />
 62    public async Task<IEnumerable<WorkflowDefinition>> FindManyAsync<TOrderBy>(WorkflowDefinitionFilter filter, Workflow
 63    {
 1150864        return await store.QueryAsync(queryable => Filter(queryable, filter).OrderBy(order), OnLoadAsync, filter.TenantA
 575465    }
 66
 67    /// <inheritdoc />
 68    [RequiresUnreferencedCode("The method 'FindSummariesAsync' is used for serialization and requires unreferenced code 
 69    public async Task<Page<WorkflowDefinitionSummary>> FindSummariesAsync(WorkflowDefinitionFilter filter, PageArgs page
 70    {
 071        var orderBy = new WorkflowDefinitionOrder<DateTimeOffset>(x => x.CreatedAt, OrderDirection.Ascending);
 072        return await FindSummariesAsync(filter, orderBy, pageArgs, cancellationToken);
 073    }
 74
 75    /// <inheritdoc />
 76    [RequiresUnreferencedCode("The method 'FindSummariesAsync' is used for serialization and requires unreferenced code 
 77    public async Task<Page<WorkflowDefinitionSummary>> FindSummariesAsync<TOrderBy>(WorkflowDefinitionFilter filter, Wor
 78    {
 079        await using var dbContext = await store.CreateDbContextAsync(cancellationToken);
 080        var set = dbContext.WorkflowDefinitions.AsNoTracking();
 081        var queryable = Filter(set.AsQueryable(), filter).OrderBy(order);
 82
 083        if (filter.TenantAgnostic)
 084            queryable = queryable.IgnoreQueryFilters();
 85
 086        var count = await queryable.LongCountAsync(cancellationToken);
 087        queryable = Paginate(queryable, pageArgs);
 088        var results = await queryable.Select(WorkflowDefinitionSummary.FromDefinitionExpression()).ToListAsync(cancellat
 089        return Page.Of(results, count);
 090    }
 91
 92    /// <inheritdoc />
 93    [RequiresUnreferencedCode("The method 'FindSummariesAsync' is used for serialization and requires unreferenced code 
 94    public async Task<IEnumerable<WorkflowDefinitionSummary>> FindSummariesAsync(WorkflowDefinitionFilter filter, Cancel
 95    {
 796        var orderBy = new WorkflowDefinitionOrder<DateTimeOffset>(x => x.CreatedAt, OrderDirection.Ascending);
 797        return await FindSummariesAsync(filter, orderBy, cancellationToken);
 798    }
 99
 100    /// <inheritdoc />
 101    [RequiresUnreferencedCode("The method 'FindSummariesAsync' is used for serialization and requires unreferenced code 
 102    public async Task<IEnumerable<WorkflowDefinitionSummary>> FindSummariesAsync<TOrderBy>(WorkflowDefinitionFilter filt
 103    {
 7104        await using var dbContext = await store.CreateDbContextAsync(cancellationToken);
 7105        var set = dbContext.WorkflowDefinitions.AsNoTracking();
 7106        var queryable = Filter(set.AsQueryable(), filter).OrderBy(order);
 107
 7108        if (filter.TenantAgnostic)
 0109            queryable = queryable.IgnoreQueryFilters();
 110
 7111        return await queryable.Select(WorkflowDefinitionSummary.FromDefinitionExpression()).ToListAsync(cancellationToke
 7112    }
 113
 114    /// <inheritdoc />
 115    public async Task<WorkflowDefinition?> FindLastVersionAsync(WorkflowDefinitionFilter filter, CancellationToken cance
 116    {
 34117        return await store.QueryAsync(queryable => Filter(queryable, filter).OrderByDescending(x => x.Version), OnLoadAs
 17118    }
 119
 120    /// <inheritdoc />
 121    public async Task SaveAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default)
 122    {
 37123        await store.SaveAsync(definition, OnSaveAsync, cancellationToken);
 36124    }
 125
 126    /// <inheritdoc />
 127    public async Task SaveManyAsync(IEnumerable<WorkflowDefinition> definitions, CancellationToken cancellationToken = d
 128    {
 2761129        await store.SaveManyAsync(definitions, OnSaveAsync, cancellationToken);
 2760130    }
 131
 132    /// <inheritdoc />
 133    public async Task<WorkflowDefinitionUpdateResult> TryUpdateLatestAsync(
 134        WorkflowDefinitionFilter filter,
 135        Func<WorkflowDefinition, bool> matchesExpected,
 136        Func<WorkflowDefinition, WorkflowDefinition> update,
 137        CancellationToken cancellationToken = default)
 138    {
 0139        await using var dbContext = await store.CreateDbContextAsync(cancellationToken);
 0140        await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
 141
 0142        var queryable = Filter(dbContext.WorkflowDefinitions.AsTracking(), filter);
 143
 0144        if (filter.TenantAgnostic)
 0145            queryable = queryable.IgnoreQueryFilters();
 146
 0147        var current = await queryable.OrderBy(x => x.CreatedAt).FirstOrDefaultAsync(cancellationToken);
 148
 0149        if (current == null)
 0150            return WorkflowDefinitionUpdateResult.NotFound();
 151
 0152        await OnLoadAsync(dbContext, current, cancellationToken);
 153
 0154        if (!matchesExpected(current))
 0155            return WorkflowDefinitionUpdateResult.Conflict();
 156
 0157        var expectedId = current.Id;
 0158        var expectedVersion = current.Version;
 0159        var expectedStringData = current.StringData;
 0160        var expectedName = current.Name;
 0161        var expectedDescription = current.Description;
 0162        var expectedData = (string?)dbContext.Entry(current).Property("Data").CurrentValue;
 163
 164        // Preserve shadow state while loading, then keep the callback outside change tracking.
 0165        dbContext.Entry(current).State = EntityState.Detached;
 166
 0167        var next = update(current);
 0168        var nextData = SerializeState(next);
 0169        var nextUsableAsActivity = next.Options.UsableAsActivity;
 170
 0171        if (next.Id != expectedId)
 172        {
 0173            var unmarked = await dbContext.WorkflowDefinitions
 0174                .Where(MatchesLoadedSnapshot(expectedId, expectedVersion, expectedStringData, expectedName, expectedDesc
 0175                .ExecuteUpdateAsync(setters => setters.SetProperty(x => x.IsLatest, false), cancellationToken);
 176
 0177            if (unmarked == 0)
 0178                return WorkflowDefinitionUpdateResult.Conflict();
 179
 0180            dbContext.WorkflowDefinitions.Add(next);
 0181            dbContext.Entry(next).Property("Data").CurrentValue = nextData;
 0182            dbContext.Entry(next).Property("UsableAsActivity").CurrentValue = nextUsableAsActivity;
 0183            await dbContext.SaveChangesAsync(cancellationToken);
 0184            await transaction.CommitAsync(cancellationToken);
 0185            return WorkflowDefinitionUpdateResult.Updated(next);
 186        }
 187
 0188        var updated = await dbContext.WorkflowDefinitions
 0189            .Where(MatchesLoadedSnapshot(expectedId, expectedVersion, expectedStringData, expectedName, expectedDescript
 0190            .ExecuteUpdateAsync(
 0191                setters => setters
 0192                    .SetProperty(x => x.Name, next.Name)
 0193                    .SetProperty(x => x.Description, next.Description)
 0194                    .SetProperty(x => x.StringData, next.StringData)
 0195                    .SetProperty(x => EF.Property<string?>(x, "Data"), nextData)
 0196                    .SetProperty(x => EF.Property<bool?>(x, "UsableAsActivity"), nextUsableAsActivity),
 0197                cancellationToken);
 198
 0199        if (updated == 0)
 0200            return WorkflowDefinitionUpdateResult.Conflict();
 201
 0202        await transaction.CommitAsync(cancellationToken);
 0203        return WorkflowDefinitionUpdateResult.Updated(next);
 0204    }
 205
 206    /// <summary>
 207    /// The loaded snapshot <see cref="TryUpdateLatestAsync"/> will only overwrite: identity, the ETag-covered
 208    /// graph, the serialized <c>Data</c> blob (source XML, variables, options) and the name/description columns
 209    /// a metadata-only save changes. The row must still be <c>IsLatest</c> so a published→draft loser
 210    /// is Conflict rather than a unique-key failure on <c>(DefinitionId, Version)</c>. Zero rows means
 211    /// another writer got there first.
 212    /// </summary>
 213    private static Expression<Func<WorkflowDefinition, bool>> MatchesLoadedSnapshot(
 214        string expectedId,
 215        int expectedVersion,
 216        string? expectedStringData,
 217        string? expectedName,
 218        string? expectedDescription,
 219        string? expectedData) =>
 0220        x => x.Id == expectedId
 0221             && x.Version == expectedVersion
 0222             && x.IsLatest
 0223             && x.StringData == expectedStringData
 0224             && x.Name == expectedName
 0225             && x.Description == expectedDescription
 0226             && EF.Property<string?>(x, "Data") == expectedData;
 227
 228    /// <inheritdoc />
 229    public async Task<long> DeleteAsync(WorkflowDefinitionFilter filter, CancellationToken cancellationToken = default)
 230    {
 8231        await using var dbContext = await store.CreateDbContextAsync(cancellationToken);
 8232        var set = dbContext.WorkflowDefinitions;
 8233        var queryable = set.AsQueryable();
 234
 8235        if (filter.TenantAgnostic)
 0236            queryable = queryable.IgnoreQueryFilters();
 237
 8238        var ids = await Filter(queryable, filter).Select(x => x.Id).Distinct().ToListAsync(cancellationToken);
 8239        return await store.DeleteWhereAsync(x => ids.Contains(x.Id), cancellationToken);
 8240    }
 241
 242    /// <inheritdoc />
 243    public async Task<bool> AnyAsync(WorkflowDefinitionFilter filter, CancellationToken cancellationToken = default)
 244    {
 2245        return await store.QueryAsync(queryable => Filter(queryable, filter), filter.TenantAgnostic, cancellationToken).
 1246    }
 247
 248    /// <inheritdoc />
 249    public async Task<long> CountDistinctAsync(CancellationToken cancellationToken = default)
 250    {
 1251        return await store.CountAsync(x => true, x => x.DefinitionId, false, cancellationToken);
 1252    }
 253
 254    /// <inheritdoc />
 255    public async Task<bool> GetIsNameUnique(string name, string? definitionId = null, CancellationToken cancellationToke
 256    {
 0257        var exists = await store.AnyAsync(x => x.Name == name && x.DefinitionId != definitionId, false, cancellationToke
 0258        return !exists;
 0259    }
 260
 261    private ValueTask OnSaveAsync(ManagementElsaDbContext managementElsaDbContext, WorkflowDefinition entity, Cancellati
 262    {
 2802263        var json = SerializeState(entity);
 264
 2802265        managementElsaDbContext.Entry(entity).Property("Data").CurrentValue = json;
 2802266        managementElsaDbContext.Entry(entity).Property("UsableAsActivity").CurrentValue = entity.Options.UsableAsActivit
 2802267        return ValueTask.CompletedTask;
 268    }
 269
 270    private string SerializeState(WorkflowDefinition entity)
 271    {
 2802272        var data = new WorkflowDefinitionState(entity.Options, entity.Variables, entity.Inputs, entity.Outputs, entity.O
 2802273        return payloadSerializer.Serialize(data);
 274    }
 275
 276    private ValueTask OnLoadAsync(ManagementElsaDbContext managementElsaDbContext, WorkflowDefinition? entity, Cancellat
 277    {
 10077278        if (entity == null)
 0279            return ValueTask.CompletedTask;
 280
 10077281        var data = new WorkflowDefinitionState(entity.Options, entity.Variables, entity.Inputs, entity.Outputs, entity.O
 10077282        var json = (string?)managementElsaDbContext.Entry(entity).Property("Data").CurrentValue;
 283
 284        try
 285        {
 10077286            if (!string.IsNullOrWhiteSpace(json))
 10077287                data = payloadSerializer.Deserialize<WorkflowDefinitionState>(json);
 10077288        }
 0289        catch (Exception exp)
 290        {
 0291            logger.LogError(exp, "Could not deserialize workflow definition state: {DefinitionId}. Reverting to default 
 0292        }
 293
 10077294        entity.Options = data.Options;
 10077295        entity.Variables = data.Variables;
 10077296        entity.Inputs = data.Inputs;
 10077297        entity.Outputs = data.Outputs;
 10077298        entity.Outcomes = data.Outcomes;
 10077299        entity.CustomProperties = data.CustomProperties;
 300
 10077301        return ValueTask.CompletedTask;
 302    }
 303
 304    private IQueryable<WorkflowDefinition> Filter(IQueryable<WorkflowDefinition> queryable, WorkflowDefinitionFilter fil
 305    {
 9131306        var definitionId = filter.DefinitionId ?? filter.DefinitionHandle?.DefinitionId;
 9131307        var versionOptions = filter.VersionOptions ?? filter.DefinitionHandle?.VersionOptions;
 9131308        var id = filter.Id ?? filter.DefinitionHandle?.DefinitionVersionId;
 309
 17656310        if (definitionId != null) queryable = queryable.Where(x => x.DefinitionId == definitionId);
 9142311        if (filter.DefinitionIds != null) queryable = queryable.Where(x => filter.DefinitionIds.Contains(x.DefinitionId)
 9633312        if (id != null) queryable = queryable.Where(x => x.Id == id);
 9133313        if (filter.Ids != null) queryable = queryable.Where(x => filter.Ids.Contains(x.Id));
 17719314        if (versionOptions != null) queryable = queryable.WithVersion(versionOptions.Value);
 9131315        if (filter.MaterializerName != null) queryable = queryable.Where(x => x.MaterializerName == filter.MaterializerN
 9131316        if (filter.Name != null) queryable = queryable.Where(x => x.Name == filter.Name);
 9131317        if (filter.Names != null) queryable = queryable.Where(x => filter.Names.Contains(x.Name!));
 9372318        if (filter.UsableAsActivity != null) queryable = queryable.Where(x => EF.Property<bool>(x, "UsableAsActivity") =
 9131319        if (!string.IsNullOrWhiteSpace(filter.SearchTerm)) queryable = queryable.Where(x => x.Name!.ToLower().Contains(f
 320
 321        // TEMP: IsSystem may be null when upgrading from older versions of Elsa to 3.2. See issue #5366.
 322        // In a future version, we should remove this check and simply do queryable.Where(x => x.IsSystem == filter.IsSy
 9131323        if (filter.IsSystem != null)
 1324            queryable = filter.IsSystem == true
 1325                ? queryable.Where(x => x.IsSystem == true)
 1326#pragma warning disable CS0472 // The result of the expression is always the same since a value of this type is never eq
 1327                : queryable.Where(x => x.IsSystem == false || x.IsSystem == null!);
 328#pragma warning restore CS0472 // The result of the expression is always the same since a value of this type is never eq
 329
 9142330        if (filter.IsReadonly != null) queryable = queryable.Where(x => x.IsReadonly == filter.IsReadonly);
 9131331        return queryable;
 332    }
 333
 334    private IQueryable<WorkflowDefinition> Paginate(IQueryable<WorkflowDefinition> queryable, PageArgs? pageArgs)
 335    {
 0336        if (pageArgs?.Offset != null) queryable = queryable.Skip(pageArgs.Offset.Value);
 0337        if (pageArgs?.Limit != null) queryable = queryable.Take(pageArgs.Limit.Value);
 0338        return queryable;
 339    }
 340
 341    private class WorkflowDefinitionState
 342    {
 343        [JsonConstructor]
 10077344        public WorkflowDefinitionState()
 345        {
 10077346        }
 347
 12879348        public WorkflowDefinitionState(
 12879349            WorkflowOptions options,
 12879350            ICollection<Variable> variables,
 12879351            ICollection<InputDefinition> inputs,
 12879352            ICollection<OutputDefinition> outputs,
 12879353            ICollection<string> outcomes,
 12879354            IDictionary<string, object> customProperties
 12879355        )
 356        {
 12879357            Options = options;
 12879358            Variables = variables;
 12879359            Inputs = inputs;
 12879360            Outputs = outputs;
 12879361            Outcomes = outcomes;
 12879362            CustomProperties = customProperties;
 12879363        }
 364
 58791365        public WorkflowOptions Options { get; set; } = new();
 58791366        public ICollection<Variable> Variables { get; set; } = new List<Variable>();
 58791367        public ICollection<InputDefinition> Inputs { get; set; } = new List<InputDefinition>();
 58791368        public ICollection<OutputDefinition> Outputs { get; set; } = new List<OutputDefinition>();
 58791369        public ICollection<string> Outcomes { get; set; } = new List<string>();
 58791370        public IDictionary<string, object> CustomProperties { get; set; } = new Dictionary<string, object>();
 371    }
 372}

Methods/Properties

.ctor(Elsa.Persistence.EFCore.EntityStore`2<Elsa.Persistence.EFCore.Modules.Management.ManagementElsaDbContext,Elsa.Workflows.Management.Entities.WorkflowDefinition>,Elsa.Workflows.IPayloadSerializer,Microsoft.Extensions.Logging.ILogger`1<Elsa.Persistence.EFCore.Modules.Management.EFCoreWorkflowDefinitionStore>)
FindAsync()
FindAsync()
FindManyAsync()
FindManyAsync()
FindManyAsync()
FindManyAsync()
FindSummariesAsync()
FindSummariesAsync()
FindSummariesAsync()
FindSummariesAsync()
FindLastVersionAsync()
SaveAsync()
SaveManyAsync()
TryUpdateLatestAsync()
MatchesLoadedSnapshot(System.String,System.Int32,System.String,System.String,System.String,System.String)
DeleteAsync()
AnyAsync()
CountDistinctAsync()
GetIsNameUnique()
OnSaveAsync(Elsa.Persistence.EFCore.Modules.Management.ManagementElsaDbContext,Elsa.Workflows.Management.Entities.WorkflowDefinition,System.Threading.CancellationToken)
SerializeState(Elsa.Workflows.Management.Entities.WorkflowDefinition)
OnLoadAsync(Elsa.Persistence.EFCore.Modules.Management.ManagementElsaDbContext,Elsa.Workflows.Management.Entities.WorkflowDefinition,System.Threading.CancellationToken)
Filter(System.Linq.IQueryable`1<Elsa.Workflows.Management.Entities.WorkflowDefinition>,Elsa.Workflows.Management.Filters.WorkflowDefinitionFilter)
Paginate(System.Linq.IQueryable`1<Elsa.Workflows.Management.Entities.WorkflowDefinition>,Elsa.Common.Models.PageArgs)
.ctor()
.ctor(Elsa.Workflows.Models.WorkflowOptions,System.Collections.Generic.ICollection`1<Elsa.Workflows.Memory.Variable>,System.Collections.Generic.ICollection`1<Elsa.Workflows.Models.InputDefinition>,System.Collections.Generic.ICollection`1<Elsa.Workflows.Models.OutputDefinition>,System.Collections.Generic.ICollection`1<System.String>,System.Collections.Generic.IDictionary`2<System.String,System.Object>)
get_Options()
get_Variables()
get_Inputs()
get_Outputs()
get_Outcomes()
get_CustomProperties()