< Summary

Information
Class: Elsa.Identity.Services.MemoryRoleStore
Assembly: Elsa.Identity
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Identity/Services/MemoryRoleStore.cs
Line coverage
100%
Covered lines: 48
Uncovered lines: 0
Coverable lines: 48
Total lines: 139
Line coverage: 100%
Branch coverage
100%
Covered branches: 8
Total branches: 8
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
AddAsync(...)100%11100%
DeleteAsync(...)100%11100%
TryDeleteAsync(...)100%22100%
SaveAsync(...)100%11100%
Save(...)100%11100%
EnsureIdIsAvailable(...)100%44100%
CanReplace(...)100%22100%
FindAsync(...)100%11100%
FindManyAsync(...)100%11100%
Filter(...)100%11100%
Clone(...)100%11100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Identity/Services/MemoryRoleStore.cs

#LineLine coverage
 1using Elsa.Common.Multitenancy;
 2using Elsa.Common.Services;
 3using Elsa.Identity.Contracts;
 4using Elsa.Identity.Entities;
 5using Elsa.Identity.Models;
 6
 7namespace Elsa.Identity.Services;
 8
 9/// <summary>
 10/// Represents an in-memory role store.
 11/// </summary>
 12public class MemoryRoleStore : IRoleStore, IRoleStoreWithAtomicDelete
 13{
 14    private readonly MemoryStore<Role> _store;
 15    private readonly ITenantAccessor _tenantAccessor;
 16
 17    /// <summary>
 18    /// Initializes a new instance of the <see cref="MemoryRoleStore"/> class.
 19    /// </summary>
 4720    public MemoryRoleStore(MemoryStore<Role> store, ITenantAccessor tenantAccessor)
 21    {
 4722        _store = store;
 4723        _tenantAccessor = tenantAccessor;
 4724    }
 25
 26    /// <inheritdoc />
 27    public Task AddAsync(Role role, CancellationToken cancellationToken = default)
 28    {
 3829        lock (_store.Sync)
 30        {
 7731            MemoryIdentityUniqueness.EnsureAvailable(_store, role, x => x.Name, "name");
 7432            _store.Add(role, x => x.Id);
 533        }
 34
 535        return Task.CompletedTask;
 36    }
 37
 38    /// <inheritdoc />
 39    public Task DeleteAsync(RoleFilter filter, CancellationToken cancellationToken = default)
 40    {
 241        lock (_store.Sync)
 42        {
 443            var roles = _store.Query(query => Filter(query, filter)).ToList();
 444            _store.DeleteMany(roles, x => x.Id);
 245        }
 46
 247        return Task.CompletedTask;
 48    }
 49
 50    /// <inheritdoc />
 51    /// <remarks>
 52    /// The matching role's key is removed through the underlying concurrent dictionary, whose removal is a single
 53    /// compare-and-remove step. Two callers racing on the same role ID therefore see one <see langword="true"/> and
 54    /// one <see langword="false"/>, rather than both concluding they deleted it.
 55    /// </remarks>
 56    public Task<bool> TryDeleteAsync(string roleId, CancellationToken cancellationToken = default)
 57    {
 458        lock (_store.Sync)
 59        {
 860            var role = _store.Query(query => Filter(query, new RoleFilter { Id = roleId })).FirstOrDefault();
 461            var deleted = role is not null && _store.Delete(role.Id);
 62
 463            return Task.FromResult(deleted);
 64        }
 465    }
 66
 67    /// <inheritdoc />
 68    public Task SaveAsync(Role role, CancellationToken cancellationToken = default)
 69    {
 4770        Save(role);
 4071        return Task.CompletedTask;
 72    }
 73
 74    private void Save(Role role)
 75    {
 4776        lock (_store.Sync)
 77        {
 4778            EnsureIdIsAvailable(role);
 9179            MemoryIdentityUniqueness.EnsureAvailable(_store, role, x => x.Name, "name");
 8080            _store.Save(role, x => x.Id);
 4081        }
 4082    }
 83
 84    private void EnsureIdIsAvailable(Role role)
 85    {
 6686        var existing = _store.Find(x => x.Id == role.Id);
 4787        if (existing is not null && !CanReplace(existing, role))
 88        {
 489            throw new InvalidOperationException(
 490                $"A role already exists with ID '{role.Id}' in tenant '{existing.TenantId}'.");
 91        }
 4392    }
 93
 94    /// <summary>
 95    /// A role may be replaced only when its existing row is visible to the ambient tenant and the incoming role
 96    /// retains that row's tenant marker. This matches the durable store's query-filtered ID lookup while preventing
 97    /// a caller from re-homing a globally keyed row by changing its <c>TenantId</c>.
 98    /// </summary>
 99    private bool CanReplace(Role existing, Role replacement) =>
 11100        string.Equals(existing.TenantId, replacement.TenantId, StringComparison.Ordinal) &&
 11101        TenantVisibility.IsVisible(existing.TenantId, _tenantAccessor.TenantId);
 102
 103    /// <inheritdoc />
 104    public Task<Role?> FindAsync(RoleFilter filter, CancellationToken cancellationToken = default)
 105    {
 156106        var result = _store.Query(query => Filter(query, filter)).Select(Clone).FirstOrDefault();
 78107        return Task.FromResult(result);
 108    }
 109
 110    /// <inheritdoc />
 111    public Task<IEnumerable<Role>> FindManyAsync(RoleFilter filter, CancellationToken cancellationToken = default)
 112    {
 22113        var result = _store.Query(query => Filter(query, filter)).Select(Clone).ToList().AsEnumerable();
 11114        return Task.FromResult(result);
 115    }
 116
 117    /// <remarks>
 118    /// The ambient tenant is applied here rather than left to callers. Isolation previously existed only
 119    /// on the Entity Framework path, and only when multitenancy was enabled, so a deployment running the
 120    /// default in-memory stores had none at all. Null tenant IDs are retained only for the default tenant
 121    /// for backwards compatibility with records created before tenant assignment was added.
 122    /// </remarks>
 123    private IQueryable<Role> Filter(IQueryable<Role> queryable, RoleFilter filter)
 124    {
 95125        var tenantId = _tenantAccessor.TenantId;
 95126        queryable = queryable.Where(x => x.TenantId == tenantId || x.TenantId == Tenant.AgnosticTenantId || (x.TenantId 
 127
 95128        return filter.Apply(queryable);
 129    }
 130
 131    private static Role Clone(Role role) =>
 75132        new()
 75133        {
 75134            Id = role.Id,
 75135            Name = role.Name,
 75136            TenantId = role.TenantId,
 75137            Permissions = role.Permissions.ToList()
 75138        };
 139}