< Summary

Information
Class: Elsa.Persistence.EFCore.Modules.Alterations.EFCoreAlterationJobStore
Assembly: Elsa.Persistence.EFCore
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Persistence.EFCore/Modules/Alterations/AlterationJobStore.cs
Line coverage
95%
Covered lines: 92
Uncovered lines: 4
Coverable lines: 96
Total lines: 199
Line coverage: 95.8%
Branch coverage
86%
Covered branches: 26
Total branches: 30
Branch coverage: 86.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
.ctor(...)100%11100%
SaveAsync()100%22100%
<SaveAsync()83.33%66100%
SaveManyAsync()75%4490%
<SaveManyAsync()100%22100%
<SaveManyAsync()87.5%88100%
FindAsync()100%11100%
FindManyAsync()100%11100%
FindManyIdsAsync()100%11100%
CountAsync()100%11100%
UpsertAsync()100%66100%
UpdateOwnedAsync()100%11100%
ExecuteWriteAsync()50%22100%
OnSave(...)100%11100%
OnSaveAsync(...)100%11100%
OnLoadAsync(...)50%2280%
Filter(...)100%11100%
IsDatabaseException(...)100%66100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Persistence.EFCore/Modules/Alterations/AlterationJobStore.cs

#LineLine coverage
 1using System.Data.Common;
 2using System.Text.Json;
 3using Elsa.Alterations.Core.Contracts;
 4using Elsa.Alterations.Core.Entities;
 5using Elsa.Alterations.Core.Filters;
 6using Elsa.Alterations.Core.Models;
 7using Elsa.Alterations.Core.Stores;
 8using Elsa.Tenants.Options;
 9using Microsoft.EntityFrameworkCore;
 10using Microsoft.Extensions.DependencyInjection;
 11using Microsoft.Extensions.Options;
 12using Open.Linq.AsyncExtensions;
 13
 14namespace Elsa.Persistence.EFCore.Modules.Alterations;
 15
 16/// <summary>
 17/// An EF Core implementation of <see cref="IAlterationJobStore"/>.
 18/// </summary>
 19public class EFCoreAlterationJobStore : IAlterationJobStore
 20{
 21    private readonly EntityStore<AlterationsElsaDbContext, AlterationJob> _store;
 22    private readonly bool _tenantEnabled;
 23
 24    /// <summary>
 25    /// Constructor.
 26    /// </summary>
 27    public EFCoreAlterationJobStore(EntityStore<AlterationsElsaDbContext, AlterationJob> store)
 028        : this(store, Options.Create(new TenantsOptions()))
 29    {
 030    }
 31
 32    /// <summary>
 33    /// Constructor used by dependency injection. Direct construction through the legacy
 34    /// overload keeps tenancy-aware upsert disabled for compatibility.
 35    /// </summary>
 36    [ActivatorUtilitiesConstructor]
 44537    public EFCoreAlterationJobStore(EntityStore<AlterationsElsaDbContext, AlterationJob> store, IOptions<TenantsOptions>
 38    {
 44539        _store = store;
 44540        _tenantEnabled = tenantsOptions.Value.IsEnabled;
 44541    }
 42
 43    /// <inheritdoc />
 44    public async Task SaveAsync(AlterationJob record, CancellationToken cancellationToken = default)
 45    {
 3146        if (!_tenantEnabled)
 47        {
 248            await _store.SaveAsync(record, OnSaveAsync, cancellationToken);
 249            return;
 50        }
 51
 2952        await _store.ExecuteWithDbExceptionHandlingAsync(
 2953            async () =>
 2954            {
 2955                await using var dbContext = await _store.CreateDbContextAsync(cancellationToken);
 2956                await UpsertAsync(dbContext, record, cancellationToken, handleDbExceptions: false);
 2557                return true;
 2958            },
 2959            cancellationToken,
 2960            IsDatabaseException);
 5261    }
 62
 63    /// <inheritdoc />
 64    public async Task SaveManyAsync(IEnumerable<AlterationJob> jobs, CancellationToken cancellationToken = default)
 65    {
 1466        if (!_tenantEnabled)
 67        {
 168            await _store.SaveManyAsync(jobs, OnSaveAsync, cancellationToken);
 169            return;
 70        }
 71
 1772        var list = jobs.OrderBy(job => job.Id, StringComparer.Ordinal).ToList();
 1373        if (list.Count == 0)
 074            return;
 75
 1376        await _store.ExecuteWithDbExceptionHandlingAsync(
 1377            async () =>
 1378            {
 1379                await _store.ExecuteWriteWithRetryAsync(async (dbContext, ct) =>
 1380                {
 1381                    await using var transaction = await dbContext.Database.BeginTransactionAsync(ct);
 1382
 4983                    foreach (var job in list)
 1584                        await UpsertAsync(dbContext, job, ct, handleDbExceptions: false);
 1385
 686                    await transaction.CommitAsync(ct);
 1387                }, cancellationToken);
 688                return true;
 689            },
 1390            cancellationToken,
 1391            IsDatabaseException);
 1392    }
 93
 94    /// <inheritdoc />
 95    public async Task<AlterationJob?> FindAsync(AlterationJobFilter filter, CancellationToken cancellationToken = defaul
 96    {
 2297        return await _store.FindAsync(filter.Apply, OnLoadAsync, cancellationToken);
 2298    }
 99
 100    /// <inheritdoc />
 101    public async Task<IEnumerable<AlterationJob>> FindManyAsync(AlterationJobFilter filter, CancellationToken cancellati
 102    {
 9103        return await _store.QueryAsync(filter.Apply, OnLoadAsync, cancellationToken).ToList();
 9104    }
 105
 106    /// <inheritdoc />
 107    public async Task<IEnumerable<string>> FindManyIdsAsync(AlterationJobFilter filter, CancellationToken cancellationTo
 108    {
 2109        return await _store.QueryAsync(filter.Apply, x => x.Id, cancellationToken);
 2110    }
 111
 112    /// <inheritdoc />
 113    public async Task<long> CountAsync(AlterationJobFilter filter, CancellationToken cancellationToken = default)
 114    {
 9115        return await _store.CountAsync(queryable => Filter(queryable, filter), cancellationToken);
 3116    }
 117
 118    private async Task UpsertAsync(
 119        AlterationsElsaDbContext dbContext,
 120        AlterationJob record,
 121        CancellationToken cancellationToken,
 122        bool handleDbExceptions = true)
 123    {
 44124        var ambientTenantId = AlterationTenantOwnedUpsert.AmbientTenantId(dbContext);
 44125        AlterationTenantOwnedUpsert.StampTenantId(record, ambientTenantId);
 44126        OnSave(dbContext, record);
 127
 44128        var planId = record.PlanId;
 44129        var workflowInstanceId = record.WorkflowInstanceId;
 44130        var status = record.Status;
 44131        var createdAt = record.CreatedAt;
 44132        var startedAt = record.StartedAt;
 44133        var completedAt = record.CompletedAt;
 44134        var serializedLog = dbContext.Entry(record).Property<string>("SerializedLog").CurrentValue;
 135
 44136        var query = dbContext.Set<AlterationJob>()
 44137            .IgnoreQueryFilters()
 44138            .Where(AlterationTenantOwnedUpsert.OwnedId<AlterationJob>(record.Id, record.TenantId, ambientTenantId));
 139
 140        // Inline lambda so net8/net9 bind SetPropertyCalls and net10 binds UpdateSettersBuilder.
 53141        Task<int> UpdateOwnedAsync() => query.ExecuteUpdateAsync(
 53142            setters => setters
 53143                .SetProperty(job => job.PlanId, planId)
 53144                .SetProperty(job => job.WorkflowInstanceId, workflowInstanceId)
 53145                .SetProperty(job => job.Status, status)
 53146                .SetProperty(job => job.CreatedAt, createdAt)
 53147                .SetProperty(job => job.StartedAt, startedAt)
 53148                .SetProperty(job => job.CompletedAt, completedAt)
 53149                .SetProperty(job => EF.Property<string>(job, "SerializedLog"), serializedLog),
 53150            cancellationToken);
 151
 152        Task<TResult> ExecuteWriteAsync<TResult>(Func<Task<TResult>> operation) =>
 93153            handleDbExceptions
 93154                ? _store.ExecuteWithDbExceptionHandlingAsync(operation, cancellationToken)
 93155                : operation();
 156
 44157        var updated = await ExecuteWriteAsync(UpdateOwnedAsync);
 158
 43159        if (updated == 0)
 160        {
 40161            var inserted = await ExecuteWriteAsync(
 80162                () => AlterationTenantOwnedUpsert.InsertIfAbsentAsync(dbContext, record, cancellationToken));
 39163            if (!inserted)
 164            {
 9165                var retried = await ExecuteWriteAsync(UpdateOwnedAsync);
 166
 9167                if (retried == 0)
 9168                    throw AlterationStoreConflict.HiddenJobId(record.Id);
 169            }
 170        }
 33171    }
 172
 173    private static void OnSave(AlterationsElsaDbContext elsaDbContext, AlterationJob entity)
 174    {
 48175        elsaDbContext.Entry(entity).Property("SerializedLog").CurrentValue = JsonSerializer.Serialize(entity.Log);
 48176    }
 177
 178    private static ValueTask OnSaveAsync(AlterationsElsaDbContext dbContext, AlterationJob entity, CancellationToken can
 179    {
 4180        OnSave(dbContext, entity);
 4181        return default;
 182    }
 183
 184    private static ValueTask OnLoadAsync(AlterationsElsaDbContext elsaDbContext, AlterationJob? entity, CancellationToke
 185    {
 38186        if (entity is null)
 0187            return default;
 188
 38189        var logJson = elsaDbContext.Entry(entity).Property<string>("SerializedLog").CurrentValue;
 38190        entity.Log = JsonSerializer.Deserialize<AlterationLogEntry[]>(logJson)!;
 191
 38192        return default;
 193    }
 194
 6195    private static IQueryable<AlterationJob> Filter(IQueryable<AlterationJob> queryable, AlterationJobFilter filter) => 
 196
 197    private static bool IsDatabaseException(Exception exception) =>
 11198        exception is DbException or DbUpdateException || exception.InnerException is DbException;
 199}