< Summary

Information
Class: Elsa.Labels.Services.InMemoryLabelStore
Assembly: Elsa.Labels
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Labels/Services/InMemoryLabelStore.cs
Line coverage
100%
Covered lines: 82
Uncovered lines: 0
Coverable lines: 82
Total lines: 191
Line coverage: 100%
Branch coverage
95%
Covered branches: 38
Total branches: 40
Branch coverage: 95%
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%
DeleteAsync(...)100%66100%
DeleteManyAsync(...)100%44100%
FindByIdAsync(...)100%22100%
ListAsync(...)100%11100%
FindManyByIdAsync(...)100%11100%
FindVisibleLabel(...)100%11100%
Clone(...)100%11100%
IsVisible(...)100%11100%
get_CurrentTenantId()50%44100%
ApplyCurrentTenant(...)100%66100%
SyncNormalizedName(...)100%11100%
EnsureNormalizedNameAvailable(...)100%1414100%
DuplicateNormalizedName(...)100%11100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Labels/Services/InMemoryLabelStore.cs

#LineLine coverage
 1using Elsa.Common.Entities;
 2using Elsa.Common.Models;
 3using Elsa.Common.Multitenancy;
 4using Elsa.Common.Services;
 5using Elsa.Extensions;
 6using Elsa.Labels.Contracts;
 7using Elsa.Labels.Entities;
 8
 9namespace Elsa.Labels.Services;
 10
 11/// <summary>
 12/// An in-memory store of labels.
 13/// </summary>
 14/// <remarks>
 15/// Ambient tenant is applied here rather than in callers.
 16/// EF owns that via <c>SetTenantIdFilter</c> / <c>ApplyTenantId</c>; Memory must compensate.
 17/// Labels contracts have no TenantAgnostic flag, so isolation always applies (EF query filter).
 18/// </remarks>
 19public class InMemoryLabelStore : ILabelStore
 20{
 21    private readonly MemoryStore<Label> _labelStore;
 22    private readonly MemoryStore<WorkflowDefinitionLabel> _workflowDefinitionLabelStore;
 23    private readonly ITenantAccessor? _tenantAccessor;
 24
 25    /// <summary>
 26    /// Constructor.
 27    /// </summary>
 3128    public InMemoryLabelStore(
 3129        MemoryStore<Label> labelStore,
 3130        MemoryStore<WorkflowDefinitionLabel> workflowDefinitionLabelStore,
 3131        ITenantAccessor? tenantAccessor = null)
 32    {
 3133        _labelStore = labelStore;
 3134        _workflowDefinitionLabelStore = workflowDefinitionLabelStore;
 3135        _tenantAccessor = tenantAccessor;
 3136    }
 37
 38    /// <inheritdoc />
 39    public Task SaveAsync(Label record, CancellationToken cancellationToken = default)
 40    {
 6441        lock (_labelStore.Sync)
 42        {
 6443            ApplyCurrentTenant(record);
 6444            SyncNormalizedName(record);
 6445            EnsureNormalizedNameAvailable(record, [record]);
 11446            _labelStore.Save(record, x => x.Id);
 5747        }
 48
 5749        return Task.CompletedTask;
 50    }
 51
 52    /// <inheritdoc />
 53    public Task SaveManyAsync(IEnumerable<Label> records, CancellationToken cancellationToken = default)
 54    {
 755        var list = records.ToList();
 56
 757        lock (_labelStore.Sync)
 58        {
 3459            foreach (var record in list)
 60            {
 1061                ApplyCurrentTenant(record);
 1062                SyncNormalizedName(record);
 63            }
 64
 2665            foreach (var record in list)
 866                EnsureNormalizedNameAvailable(record, list);
 67
 768            _labelStore.SaveMany(list, x => x.Id);
 369        }
 70
 371        return Task.CompletedTask;
 72    }
 73
 74    /// <inheritdoc />
 75    public Task<bool> DeleteAsync(string id, CancellationToken cancellationToken = default)
 76    {
 477        lock (_labelStore.Sync)
 478        lock (_workflowDefinitionLabelStore.Sync)
 79        {
 1480            var deleted = _labelStore.DeleteWhere(x => x.Id == id && IsVisible(x));
 81
 482            if (deleted == 0)
 383                return Task.FromResult(false);
 84
 585            _workflowDefinitionLabelStore.DeleteWhere(x => x.LabelId == id && IsVisible(x));
 186            return Task.FromResult(true);
 87        }
 488    }
 89
 90    /// <inheritdoc />
 91    public Task<long> DeleteManyAsync(IEnumerable<string> ids, CancellationToken cancellationToken = default)
 92    {
 393        var idList = ids.ToList();
 94
 395        lock (_labelStore.Sync)
 396        lock (_workflowDefinitionLabelStore.Sync)
 97        {
 1398            var deleted = _labelStore.DeleteWhere(x => idList.Contains(x.Id) && IsVisible(x));
 1099            _workflowDefinitionLabelStore.DeleteWhere(x => idList.Contains(x.LabelId) && IsVisible(x));
 3100            return Task.FromResult(deleted);
 101        }
 3102    }
 103
 104    /// <inheritdoc />
 105    public Task<Label?> FindByIdAsync(string id, CancellationToken cancellationToken = default)
 106    {
 38107        var found = FindVisibleLabel(id);
 38108        return Task.FromResult(found is null ? null : Clone(found));
 109    }
 110
 111    /// <inheritdoc />
 112    public Task<Page<Label>> ListAsync(PageArgs? pageArgs = default, CancellationToken cancellationToken = default)
 113    {
 13114        var query = _labelStore.List().AsQueryable().WhereVisibleToTenant(CurrentTenantId).OrderBy(x => x.Name);
 13115        var page = query.ToPage(pageArgs);
 13116        return Task.FromResult(Page.Of(page.Items.Select(Clone).ToList(), page.TotalCount));
 117    }
 118
 119    /// <inheritdoc />
 120    public Task<IEnumerable<Label>> FindManyByIdAsync(IEnumerable<string> ids, CancellationToken cancellationToken)
 121    {
 2122        var idList = ids.ToList();
 4123        var records = _labelStore.Query(query => query.WhereVisibleToTenant(CurrentTenantId).Where(x => idList.Contains(
 2124            .Select(Clone)
 2125            .ToList();
 2126        return Task.FromResult<IEnumerable<Label>>(records);
 127    }
 128
 129    private Label? FindVisibleLabel(string id) =>
 76130        _labelStore.Query(query => query.WhereVisibleToTenant(CurrentTenantId).Where(x => x.Id == id)).FirstOrDefault();
 131
 132    /// <summary>
 133    /// Clone-on-read, matching Memory identity stores. Labels.Update does Find → mutate
 134    /// Name (which writes NormalizedName) → Save. Without a copy, a rejected rename
 135    /// would already have mutated the stored row.
 136    /// </summary>
 137    private static Label Clone(Label label) =>
 53138        new()
 53139        {
 53140            Id = label.Id,
 53141            TenantId = label.TenantId,
 53142            Name = label.Name,
 53143            NormalizedName = label.NormalizedName,
 53144            Description = label.Description,
 53145            Color = label.Color
 53146        };
 147
 16148    private bool IsVisible(Entity entity) => TenantVisibility.IsVisible(entity.TenantId, CurrentTenantId);
 149
 69150    private string CurrentTenantId => _tenantAccessor?.TenantId ?? Tenant.DefaultTenantId;
 151
 152    private void ApplyCurrentTenant(Entity entity)
 153    {
 74154        if (entity.TenantId == Tenant.AgnosticTenantId || _tenantAccessor is null)
 10155            return;
 156
 64157        entity.TenantId ??= _tenantAccessor.TenantId;
 64158    }
 159
 160    private static void SyncNormalizedName(Label record) =>
 74161        record.NormalizedName = record.Name.ToLowerInvariant();
 162
 163    /// <summary>
 164    /// Memory counterpart of the EF unique index on <c>(TenantId, NormalizedName)</c>.
 165    /// Same-Id upserts are allowed so a row can rename itself. Incoming batch rows
 166    /// replace same-Id store rows, so those store rows are ignored here.
 167    /// </summary>
 168    private void EnsureNormalizedNameAvailable(Label record, IReadOnlyCollection<Label> batch)
 169    {
 148170        var batchIds = batch.Select(x => x.Id).ToHashSet();
 72171        var existing = _labelStore.Find(candidate =>
 166172            candidate.TenantId == record.TenantId
 166173            && candidate.Id != record.Id
 166174            && !batchIds.Contains(candidate.Id)
 166175            && candidate.NormalizedName == record.NormalizedName);
 176
 72177        if (existing is not null)
 9178            throw DuplicateNormalizedName(record);
 179
 63180        if (batch.Any(other =>
 130181                other.Id != record.Id
 130182                && other.TenantId == record.TenantId
 130183                && other.NormalizedName == record.NormalizedName))
 184        {
 2185            throw DuplicateNormalizedName(record);
 186        }
 61187    }
 188
 189    private static InvalidOperationException DuplicateNormalizedName(Label record) =>
 11190        new($"A label already exists with normalized name '{record.NormalizedName}' in tenant '{record.TenantId}'.");
 191}