< Summary

Information
Class: Elsa.Alterations.Core.Stores.MemoryAlterationJobStore
Assembly: Elsa.Alterations.Core
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Alterations.Core/Stores/MemoryAlterationJobStore.cs
Line coverage
100%
Covered lines: 49
Uncovered lines: 0
Coverable lines: 49
Total lines: 145
Line coverage: 100%
Branch coverage
92%
Covered branches: 26
Total branches: 28
Branch coverage: 92.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
SaveAsync(...)100%11100%
SaveManyAsync(...)100%44100%
FindAsync(...)100%11100%
FindManyAsync(...)100%11100%
FindManyIdsAsync(...)100%11100%
CountAsync(...)100%11100%
Filter(...)100%11100%
get_CurrentTenantId()50%44100%
EnsureIdAvailable(...)100%1414100%
CanReplace(...)100%11100%
ApplyCurrentTenant(...)100%66100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Alterations.Core/Stores/MemoryAlterationJobStore.cs

#LineLine coverage
 1using Elsa.Alterations.Core.Contracts;
 2using Elsa.Alterations.Core.Entities;
 3using Elsa.Alterations.Core.Filters;
 4using Elsa.Common.Entities;
 5using Elsa.Common.Multitenancy;
 6using Elsa.Common.Services;
 7
 8namespace Elsa.Alterations.Core.Stores;
 9
 10/// <summary>
 11/// A memory-based store for alteration jobs.
 12/// </summary>
 13/// <remarks>
 14/// Ambient tenant is applied here rather than in callers.
 15/// EF owns that via <c>SetTenantIdFilter</c> / <c>ApplyTenantId</c>; Memory must compensate.
 16/// Alterations contracts have no TenantAgnostic flag, so isolation always applies (EF query filter).
 17/// </remarks>
 18public class MemoryAlterationJobStore : IAlterationJobStore
 19{
 20    private readonly MemoryStore<AlterationJob> _store;
 21    private readonly ITenantAccessor? _tenantAccessor;
 22
 23    /// <summary>
 24    /// Initializes a new instance of the <see cref="MemoryAlterationJobStore"/> class.
 25    /// </summary>
 3626    public MemoryAlterationJobStore(MemoryStore<AlterationJob> store, ITenantAccessor? tenantAccessor = null)
 27    {
 3628        _store = store;
 3629        _tenantAccessor = tenantAccessor;
 3630    }
 31
 32    /// <inheritdoc />
 33    public Task SaveAsync(AlterationJob job, CancellationToken cancellationToken = default)
 34    {
 4935        lock (_store.Sync)
 36        {
 4937            ApplyCurrentTenant(job);
 4938            EnsureIdAvailable(job);
 9039            _store.Save(job, x => x.Id);
 4540        }
 41
 4542        return Task.CompletedTask;
 43    }
 44
 45    /// <inheritdoc />
 46    public Task SaveManyAsync(IEnumerable<AlterationJob> jobs, CancellationToken cancellationToken = default)
 47    {
 1148        var list = jobs.ToList();
 49
 1150        lock (_store.Sync)
 51        {
 5652            foreach (var job in list)
 1753                ApplyCurrentTenant(job);
 54
 1155            var tenantIdsById = new Dictionary<string, string?>(StringComparer.Ordinal);
 5056            foreach (var job in list)
 1757                EnsureIdAvailable(job, tenantIdsById);
 58
 1459            _store.SaveMany(list, x => x.Id);
 560        }
 61
 562        return Task.CompletedTask;
 63    }
 64
 65    /// <inheritdoc />
 66    public Task<AlterationJob?> FindAsync(AlterationJobFilter filter, CancellationToken cancellationToken = default)
 67    {
 5268        var entity = _store.Query(query => Filter(query, filter)).FirstOrDefault();
 2669        return Task.FromResult(entity);
 70    }
 71
 72    /// <inheritdoc />
 73    public Task<IEnumerable<AlterationJob>> FindManyAsync(AlterationJobFilter filter, CancellationToken cancellationToke
 74    {
 2675        var entities = _store.Query(query => Filter(query, filter)).ToList().AsEnumerable();
 1376        return Task.FromResult(entities);
 77    }
 78
 79    /// <inheritdoc />
 80    public Task<IEnumerable<string>> FindManyIdsAsync(AlterationJobFilter filter, CancellationToken cancellationToken = 
 81    {
 1482        var ids = _store.Query(query => Filter(query, filter)).Select(x => x.Id).ToList().AsEnumerable();
 383        return Task.FromResult(ids);
 84    }
 85
 86    /// <inheritdoc />
 87    public Task<long> CountAsync(AlterationJobFilter filter, CancellationToken cancellationToken = default)
 88    {
 889        var count = _store.Query(query => Filter(query, filter)).LongCount();
 490        return Task.FromResult(count);
 91    }
 92
 93    /// <remarks>
 94    /// Ambient tenant is applied here rather than in <see cref="AlterationJobFilter.Apply"/>.
 95    /// EF owns that via <c>SetTenantIdFilter</c>; Memory must compensate.
 96    /// </remarks>
 97    private IQueryable<AlterationJob> Filter(IQueryable<AlterationJob> query, AlterationJobFilter filter) =>
 4698        filter.Apply(query.WhereVisibleToTenant(CurrentTenantId));
 99
 62100    private string CurrentTenantId => _tenantAccessor?.TenantId ?? Tenant.DefaultTenantId;
 101
 102    private void EnsureIdAvailable(AlterationJob job, IDictionary<string, string?>? tenantIdsById = null)
 103    {
 66104        if (tenantIdsById?.TryGetValue(job.Id, out var stagedTenantId) == true)
 105        {
 4106            if (!CanReplace(stagedTenantId, job.TenantId))
 2107                throw AlterationStoreConflict.HiddenJobId(job.Id);
 108
 109            // A repeated ID in one batch is an update of the row established by the
 110            // earlier item, even when that row was absent before the batch started.
 2111            job.TenantId = stagedTenantId;
 2112            return;
 113        }
 114
 128115        var existing = _store.Find(x => x.Id == job.Id);
 116
 62117        if (existing is null)
 118        {
 50119            tenantIdsById?.Add(job.Id, job.TenantId);
 50120            return;
 121        }
 122
 12123        if (!CanReplace(existing.TenantId, job.TenantId))
 8124            throw AlterationStoreConflict.HiddenJobId(job.Id);
 125
 126        // An accepted update may change the payload, but it must not rehome the row.
 4127        job.TenantId = existing.TenantId;
 4128        tenantIdsById?.Add(job.Id, existing.TenantId);
 4129    }
 130
 131    /// <summary>
 132    /// <c>*</c> is visible to every tenant, but only an agnostic writer may replace it.
 133    /// Named tenants may upsert their own visible rows.
 134    /// </summary>
 135    private bool CanReplace(string? existingTenantId, string? incomingTenantId) =>
 16136        TenantVisibility.CanReplaceOwnedRow(existingTenantId, incomingTenantId, CurrentTenantId);
 137
 138    private void ApplyCurrentTenant(Entity entity)
 139    {
 66140        if (entity.TenantId == Tenant.AgnosticTenantId || _tenantAccessor is null)
 15141            return;
 142
 51143        entity.TenantId ??= _tenantAccessor.TenantId;
 51144    }
 145}