< Summary

Information
Class: Elsa.Persistence.EFCore.Store<T1, T2>
Assembly: Elsa.Persistence.EFCore.Common
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Persistence.EFCore.Common/Store.cs
Line coverage
56%
Covered lines: 170
Uncovered lines: 131
Coverable lines: 301
Total lines: 806
Line coverage: 56.4%
Branch coverage
52%
Covered branches: 70
Total branches: 134
Branch coverage: 52.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.cctor()100%11100%
CreateDbContextAsync()100%11100%
AddAsync()100%210%
AddAsync()100%44100%
AddManyAsync()100%210%
AddManyAsync()75%4473.33%
<AddManyAsync()100%22100%
SaveAsync()100%210%
SaveAsync()100%1010100%
SaveManyAsync()100%210%
SaveManyAsync()66.66%6696.42%
<SaveManyAsync()100%1212100%
EnsureTenantOwnershipAsync()88.88%181890.47%
CreateOwnershipMismatchException(...)100%11100%
MayReplaceExistingRow(...)100%44100%
get_Key()100%11100%
HandleDbExceptionAsync()50%2266.66%
ExecuteWithDbExceptionHandlingAsync()100%210%
ExecuteWriteWithRetryAsync()66.66%7671.42%
ShouldRetryWrite(...)50%66100%
GetWriteRetryDelay(...)100%210%
UpdateAsync(...)100%210%
UpdateAsync()0%2040%
UpdatePartialAsync()0%2040%
FindAsync()100%210%
FindAsync()0%4260%
FindAsync()100%210%
FindAsync()100%11100%
FindAsync()100%210%
FindAsync()100%210%
FindManyAsync()100%210%
FindManyAsync()0%4260%
FindManyAsync()100%210%
FindManyAsync()0%156120%
ListAsync(...)100%210%
ListAsync()0%4260%
DeleteAsync()0%620%
DeleteWhereAsync()100%22100%
DeleteWhereAsync()100%22100%
QueryAsync()100%11100%
QueryAsync()100%210%
QueryAsync()100%11100%
QueryAsync()87.5%8890.9%
QueryAsync()100%11100%
QueryAsync()75%4487.5%
CountAsync()100%210%
CountAsync()75%4487.5%
AnyAsync()100%210%
AnyAsync()0%620%
CountAsync()100%210%
CountAsync()0%2040%
CountAsync()100%210%
CountAsync()0%2040%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Persistence.EFCore.Common/Store.cs

#LineLine coverage
 1using System.Linq.Expressions;
 2using Elsa.Common.Entities;
 3using Elsa.Common.Models;
 4using Elsa.Common.Multitenancy;
 5using Elsa.Persistence.EFCore.Extensions;
 6using Elsa.Extensions;
 7using Elsa.Tenants.Options;
 8using JetBrains.Annotations;
 9using Microsoft.EntityFrameworkCore;
 10using Microsoft.Extensions.DependencyInjection;
 11using Microsoft.Extensions.Options;
 12using Open.Linq.AsyncExtensions;
 13
 14namespace Elsa.Persistence.EFCore;
 15
 16/// <summary>
 17/// A generic repository class around EF Core for accessing entities.
 18/// </summary>
 19/// <typeparam name="TDbContext">The type of the database context.</typeparam>
 20/// <typeparam name="TEntity">The type of the entity.</typeparam>
 21[PublicAPI]
 439422public class Store<TDbContext, TEntity>(IDbContextFactory<TDbContext> dbContextFactory, IServiceProvider serviceProvider
 23{
 24    private const int WriteMaxRetryCount = 3;
 725    private static readonly TimeSpan WriteBaseDelay = TimeSpan.FromMilliseconds(50);
 26
 27    // ReSharper disable once StaticMemberInGenericType
 28    // Justification: This is a static member that is used to ensure that only one thread can access the database for TE
 729    private static readonly SemaphoreSlim Semaphore = new(1, 1);
 30
 31    /// <summary>
 32    /// Creates a new instance of the database context.
 33    /// </summary>
 34    /// <param name="cancellationToken">The cancellation token.</param>
 35    /// <returns>The database context.</returns>
 2234436    public async Task<TDbContext> CreateDbContextAsync(CancellationToken cancellationToken = default) => await dbContext
 37
 38    /// <summary>
 39    /// Adds the specified entity.
 40    /// </summary>
 41    /// <param name="entity">The entity to add.</param>
 42    /// <param name="cancellationToken">The cancellation token.</param>
 43    public async Task AddAsync(TEntity entity, CancellationToken cancellationToken = default)
 44    {
 045        await AddAsync(entity, null, cancellationToken);
 046    }
 47
 48    /// <summary>
 49    /// Adds the specified entity.
 50    /// </summary>
 51    /// <param name="entity">The entity to add.</param>
 52    /// <param name="onAdding">The callback to invoke before adding the entity.</param>
 53    /// <param name="cancellationToken">The cancellation token.</param>
 54    public async Task AddAsync(TEntity entity, Func<TDbContext, TEntity, CancellationToken, ValueTask>? onAdding, Cancel
 55    {
 5156        await using var dbContext = await CreateDbContextAsync(cancellationToken);
 57
 5158        if (onAdding != null)
 5159            await onAdding(dbContext, entity, cancellationToken);
 60
 5161        var set = dbContext.Set<TEntity>();
 5162        await set.AddAsync(entity, cancellationToken);
 5163        await dbContext.SaveChangesAsync(cancellationToken);
 5164    }
 65
 66    /// <summary>
 67    /// Adds the specified entities.
 68    /// </summary>
 69    /// <param name="entities">The entities to save.</param>
 70    /// <param name="cancellationToken">The cancellation token.</param>
 71    public async Task AddManyAsync(
 72        IEnumerable<TEntity> entities,
 73        CancellationToken cancellationToken = default)
 74    {
 075        await AddManyAsync(entities, null, cancellationToken);
 076    }
 77
 78    /// <summary>
 79    /// Adds the specified entities.
 80    /// </summary>
 81    /// <param name="entities">The entities to save.</param>
 82    /// <param name="onSaving">The callback to invoke before saving the entity.</param>
 83    /// <param name="cancellationToken">The cancellation token.</param>
 84    public async Task AddManyAsync(
 85        IEnumerable<TEntity> entities,
 86        Func<TDbContext, TEntity, CancellationToken, ValueTask>? onSaving = null,
 87        CancellationToken cancellationToken = default)
 88    {
 38489        await Semaphore.WaitAsync(cancellationToken);
 90
 91        try
 92        {
 38493            var entityList = entities.ToList();
 94
 38495            if (entityList.Count == 0)
 096                return;
 97
 38498            await ExecuteWriteWithRetryAsync(async (dbContext, ct) =>
 38499            {
 384100                if (onSaving != null)
 384101                {
 5659102                    var savingTasks = entityList.Select(entity => onSaving(dbContext, entity, ct).AsTask()).ToList();
 384103                    await Task.WhenAll(savingTasks);
 384104                }
 384105
 384106                await dbContext.BulkInsertAsync(entityList, ct);
 768107            }, cancellationToken);
 384108        }
 0109        catch (Exception ex)
 110        {
 0111            await HandleDbExceptionAsync(ex, cancellationToken);
 0112            throw;
 113        }
 114        finally
 115        {
 384116            Semaphore.Release();
 117        }
 384118    }
 119
 120    /// <summary>
 121    /// Saves the entity.
 122    /// </summary>
 123    /// <param name="entity">The entity to save.</param>
 124    /// <param name="keySelector">The key selector to get the primary key property.</param>
 125    /// <param name="cancellationToken">The cancellation token.</param>
 0126    public async Task SaveAsync(TEntity entity, Expression<Func<TEntity, string>> keySelector, CancellationToken cancell
 127
 128    /// <summary>
 129    /// Saves the entity.
 130    /// </summary>
 131    /// <param name="entity">The entity to save.</param>
 132    /// <param name="keySelector">The key selector to get the primary key property.</param>
 133    /// <param name="onSaving">The callback to invoke before saving the entity.</param>
 134    /// <param name="cancellationToken">The cancellation token.</param>
 135    public async Task SaveAsync(TEntity entity, Expression<Func<TEntity, string>> keySelector, Func<TDbContext, TEntity,
 136    {
 487137        await Semaphore.WaitAsync(cancellationToken); // Asynchronous wait
 138
 139        try
 140        {
 487141            await using var dbContext = await CreateDbContextAsync(cancellationToken);
 142
 487143            if (onSaving != null)
 487144                await onSaving(dbContext, entity, cancellationToken);
 145
 487146            var set = dbContext.Set<TEntity>();
 487147            var lambda = keySelector.BuildEqualsExpression(entity);
 487148            var exists = await set.AnyAsync(lambda, cancellationToken);
 487149            set.Entry(entity).State = exists ? EntityState.Modified : EntityState.Added;
 487150            await dbContext.SaveChangesAsync(cancellationToken);
 486151        }
 1152        catch (Exception ex)
 153        {
 1154            await HandleDbExceptionAsync(ex, cancellationToken);
 1155            throw;
 156        }
 157        finally
 158        {
 487159            Semaphore.Release();
 160        }
 486161    }
 162
 163    /// <summary>
 164    /// Saves the specified entities.
 165    /// </summary>
 166    /// <param name="entities">The entities to save.</param>
 167    /// <param name="keySelector">The key selector to get the primary key property.</param>
 168    /// <param name="cancellationToken">The cancellation token.</param>
 0169    public async Task SaveManyAsync(IEnumerable<TEntity> entities, Expression<Func<TEntity, string>> keySelector, Cancel
 170
 171    /// <summary>
 172    /// Saves the specified entities.
 173    /// </summary>
 174    /// <param name="entities">The entities to save.</param>
 175    /// <param name="keySelector">The key selector to get the primary key property.</param>
 176    /// <param name="onSaving">The callback to invoke before saving the entity.</param>
 177    /// <param name="cancellationToken">The cancellation token.</param>
 178    public async Task SaveManyAsync(
 179        IEnumerable<TEntity> entities,
 180        Expression<Func<TEntity, string>> keySelector,
 181        Func<TDbContext, TEntity, CancellationToken, ValueTask>? onSaving = null,
 182        CancellationToken cancellationToken = default)
 183    {
 3258184        await Semaphore.WaitAsync(cancellationToken);
 185
 186        try
 187        {
 3258188            var entityList = entities.ToList();
 189
 3258190            if (entityList.Count == 0)
 0191                return;
 192
 3258193            var tenantId = serviceProvider.GetRequiredService<ITenantAccessor>().TenantId;
 3258194            var tenancyEnabled = serviceProvider.GetService<IOptions<TenantsOptions>>()?.Value.IsEnabled == true;
 195
 3258196            await ExecuteWriteWithRetryAsync(async (dbContext, ct) =>
 3258197            {
 3258198                if (onSaving != null)
 3258199                {
 8835200                    var savingTasks = entityList.Select(entity => onSaving(dbContext, entity, ct).AsTask()).ToList();
 3228201                    await Task.WhenAll(savingTasks);
 3258202                }
 3258203
 3258204                // When doing a custom SQL query (Bulk Upsert), none of the installed query filters will be applied. Hen
 18136205                foreach (var entity in entityList)
 3258206                {
 5810207                    if (entity is Entity entityWithTenant)
 3258208                    {
 3258209                        // Don't touch tenant-agnostic entities (marked with "*")
 5810210                        if (entityWithTenant.TenantId == Tenant.AgnosticTenantId)
 3258211                            continue;
 3258212
 3258213                        // Apply current tenant ID to entities without one
 3535214                        if (entityWithTenant.TenantId == null)
 2751215                            entityWithTenant.TenantId = tenantId;
 3258216                    }
 3258217                }
 3258218
 3258219                if (tenancyEnabled)
 3256220                    await EnsureTenantOwnershipAsync(dbContext, entityList, keySelector, tenantId, ct);
 3258221
 3247222                await dbContext.BulkUpsertAsync(entityList, keySelector, ct);
 6505223            }, cancellationToken);
 3247224        }
 11225        catch (Exception ex)
 226        {
 11227            await HandleDbExceptionAsync(ex, cancellationToken);
 11228            throw;
 229        }
 230        finally
 231        {
 3258232            Semaphore.Release();
 233        }
 3247234    }
 235
 236    /// <summary>
 237    /// #8490: an existing row may be replaced only when the stamped incoming TenantId is the
 238    /// writer's own tenant or "*", and Normalize(existing) == Normalize(incoming).
 239    /// <c>null</c> and "" both count as the default tenant. Forged-TenantId inserts of new keys
 240    /// are not refused here; the import endpoint stamps TenantId with the writer.
 241    /// The lookup and bulk upsert are separate statements; a concurrent insert of a known key
 242    /// between them can still overwrite (accepted check-then-write; see #8490).
 243    /// Iterate returned rows: a key that does not ordinal-match the batch is a collation
 244    /// collision and is refused. Ordinal matches are checked against every incoming occurrence.
 245    /// </summary>
 246    private async Task EnsureTenantOwnershipAsync(
 247        TDbContext dbContext,
 248        IList<TEntity> entities,
 249        Expression<Func<TEntity, string>> keySelector,
 250        string writerTenantId,
 251        CancellationToken cancellationToken)
 252    {
 3256253        if (!typeof(Entity).IsAssignableFrom(typeof(TEntity)))
 0254            return;
 255
 3256256        var getKey = keySelector.Compile();
 3256257        var keyName = keySelector.GetProperty()!.Name;
 3256258        var incomingByKey = entities.ToLookup(getKey, StringComparer.Ordinal);
 259
 22056260        foreach (var keyList in incomingByKey.Select(group => group.Key).Chunk(BulkUpsertExtensions.DefaultBatchSize).Se
 261        {
 3257262            var existingRows = await dbContext.Set<TEntity>()
 3257263                .AsNoTracking()
 3257264                .IgnoreQueryFilters()
 3257265                .Where(entity => keyList.Contains(EF.Property<string>(entity, keyName)))
 3257266                .Select(entity => new ExistingKeyTenant(
 3257267                    EF.Property<string>(entity, keyName),
 3257268                    EF.Property<string?>(entity, nameof(Entity.TenantId))))
 3257269                .ToListAsync(cancellationToken);
 270
 12111271            foreach (var existing in existingRows)
 272            {
 2804273                var incomingMatches = incomingByKey[existing.Key];
 2804274                if (!incomingMatches.Any())
 0275                    throw CreateOwnershipMismatchException(keyList);
 276
 14012277                foreach (var (submittedKey, incomingTenantId) in incomingMatches.Select(entity => (getKey(entity), ((Ent
 278                {
 2805279                    if (!MayReplaceExistingRow(existing.TenantId, incomingTenantId, writerTenantId))
 11280                        throw CreateOwnershipMismatchException([submittedKey]);
 281                }
 282            }
 3246283        }
 3245284    }
 285
 286    private static InvalidOperationException CreateOwnershipMismatchException(IEnumerable<string> submittedKeys)
 287    {
 11288        var keys = string.Join("', '", submittedKeys);
 11289        return new($"Cannot replace {typeof(TEntity).Name} '{keys}': tenant ownership mismatch. Shared rows need TenantI
 290    }
 291
 292    /// <summary>
 293    /// Incoming must already be the writer's tenant or "*"; existing must match that same
 294    /// normalized value. A tenant literally named "default" is not the default tenant ("").
 295    /// </summary>
 296    private static bool MayReplaceExistingRow(string? existingTenantId, string? incomingTenantId, string writerTenantId)
 297    {
 2805298        var existing = existingTenantId.NormalizeTenantId();
 2805299        var incoming = incomingTenantId.NormalizeTenantId();
 300
 2805301        if (incoming != writerTenantId && incoming != Tenant.AgnosticTenantId)
 1302            return false;
 303
 2804304        return existing == incoming;
 305    }
 306
 8413307    private sealed record ExistingKeyTenant(string Key, string? TenantId);
 308
 309    private async Task HandleDbExceptionAsync(Exception exception, CancellationToken cancellationToken)
 310    {
 12311        var handler = serviceProvider.GetService<IDbExceptionHandler>();
 312
 12313        if (handler == null)
 12314            return;
 315
 0316        var context = new DbUpdateExceptionContext(exception, cancellationToken);
 0317        await handler.HandleAsync(context);
 12318    }
 319
 320    /// <summary>
 321    /// Executes a database operation and passes failures through the configured database
 322    /// exception handler.
 323    /// </summary>
 324    /// <typeparam name="TResult">The operation result type.</typeparam>
 325    /// <param name="operation">The database operation to execute.</param>
 326    /// <param name="cancellationToken">The cancellation token.</param>
 327    /// <param name="shouldHandle">A predicate that excludes exceptions which are not database failures.</param>
 328    /// <returns>The result returned by <paramref name="operation"/>.</returns>
 329    internal async Task<TResult> ExecuteWithDbExceptionHandlingAsync<TResult>(
 330        Func<Task<TResult>> operation,
 331        CancellationToken cancellationToken = default,
 332        Func<Exception, bool>? shouldHandle = null)
 333    {
 334        try
 335        {
 0336            return await operation();
 337        }
 0338        catch (Exception exception) when (shouldHandle is null || shouldHandle(exception))
 339        {
 0340            await HandleDbExceptionAsync(exception, cancellationToken);
 0341            throw;
 342        }
 0343    }
 344
 345    /// <summary>
 346    /// Executes a whole database write operation with a fresh context after a provider
 347    /// transient failure. The caller owns the transaction boundary so a retry never
 348    /// resumes a partially completed transaction.
 349    /// </summary>
 350    internal async Task ExecuteWriteWithRetryAsync(
 351        Func<TDbContext, CancellationToken, Task> operation,
 352        CancellationToken cancellationToken)
 353    {
 3642354        for (var attempt = 0;; attempt++)
 355        {
 3642356            var providerName = string.Empty;
 357
 358            try
 359            {
 3642360                await using var dbContext = await CreateDbContextAsync(cancellationToken);
 3642361                providerName = dbContext.Database.ProviderName ?? string.Empty;
 3642362                await operation(dbContext, cancellationToken);
 3631363                return;
 0364            }
 11365            catch (Exception ex)
 366            {
 11367                if (ShouldRetryWrite(providerName, ex, attempt, cancellationToken))
 368                {
 0369                    await Task.Delay(GetWriteRetryDelay(attempt), cancellationToken);
 0370                    continue;
 371                }
 372
 11373                throw;
 374            }
 0375        }
 3631376    }
 377
 378    private static bool ShouldRetryWrite(string providerName, Exception exception, int attempt, CancellationToken cancel
 379    {
 11380        return attempt < WriteMaxRetryCount
 11381               && !cancellationToken.IsCancellationRequested
 11382               && exception is not OperationCanceledException
 11383               && DbExceptionClassifier.IsTransient(providerName, exception);
 384    }
 385
 0386    private static TimeSpan GetWriteRetryDelay(int attempt) => TimeSpan.FromMilliseconds(WriteBaseDelay.TotalMillisecond
 387
 388    /// <summary>
 389    /// Updates the entity.
 390    /// </summary>
 391    /// <param name="entity">The entity to update.</param>
 392    /// <param name="cancellationToken">The cancellation token.</param>
 393    public Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default)
 394    {
 0395        return UpdateAsync(entity, null, cancellationToken);
 396    }
 397
 398    /// <summary>
 399    /// Updates the entity.
 400    /// </summary>
 401    /// <param name="entity">The entity to update.</param>
 402    /// <param name="onSaving">The callback to invoke before saving the entity.</param>
 403    /// <param name="cancellationToken">The cancellation token.</param>
 404    public async Task UpdateAsync(TEntity entity, Func<TDbContext, TEntity, CancellationToken, ValueTask>? onSaving, Can
 405    {
 0406        await using var dbContext = await CreateDbContextAsync(cancellationToken);
 407
 0408        if (onSaving != null)
 0409            await onSaving(dbContext, entity, cancellationToken);
 410
 0411        var set = dbContext.Set<TEntity>();
 0412        set.Entry(entity).State = EntityState.Modified;
 0413        await dbContext.SaveChangesAsync(cancellationToken);
 0414    }
 415
 416    /// <summary>
 417    /// Updates specific properties of an entity in the database.
 418    /// </summary>
 419    /// <param name="entity">The entity to update.</param>
 420    /// <param name="properties">An array of expressions indicating the properties to update.</param>
 421    /// <param name="cancellationToken">The cancellation token.</param>
 422    /// <returns>A task that represents the asynchronous operation.</returns>
 423    public async Task UpdatePartialAsync(TEntity entity, Expression<Func<TEntity, object>>[] properties, CancellationTok
 424    {
 0425        await using var dbContext = await CreateDbContextAsync(cancellationToken);
 0426        dbContext.Attach(entity);
 427
 0428        foreach (var property in properties)
 0429            dbContext.Entry(entity).Property(property).IsModified = true;
 430
 0431        await dbContext.SaveChangesAsync(cancellationToken);
 0432    }
 433
 434    /// <summary>
 435    /// Finds the entity matching the specified predicate.
 436    /// </summary>
 437    /// <param name="predicate">The predicate to use.</param>
 438    /// <param name="cancellationToken">The cancellation token.</param>
 439    /// <returns>The entity if found, otherwise <c>null</c>.</returns>
 0440    public async Task<TEntity?> FindAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken
 441
 442    /// <summary>
 443    /// Finds the entity matching the specified predicate.
 444    /// </summary>
 445    /// <param name="predicate">The predicate to use.</param>
 446    /// <param name="onLoading">A callback to run after the entity is loaded</param>
 447    /// <param name="cancellationToken">The cancellation token.</param>
 448    /// <returns></returns>
 449    public async Task<TEntity?> FindAsync(Expression<Func<TEntity, bool>> predicate, Func<TDbContext, TEntity?, TEntity?
 450    {
 0451        await using var dbContext = await CreateDbContextAsync(cancellationToken);
 0452        var set = dbContext.Set<TEntity>().AsNoTracking();
 0453        var entity = await set.FirstOrDefaultAsync(predicate, cancellationToken);
 454
 0455        if (entity == null)
 0456            return null;
 457
 0458        if (onLoading != null)
 0459            entity = onLoading.Invoke(dbContext, entity);
 460
 0461        return entity;
 0462    }
 463
 464    /// <summary>
 465    /// Finds a single entity using a query
 466    /// </summary>
 467    /// <param name="query">The query to use</param>
 468    /// <param name="onLoading">A callback to run after the entity is loaded</param>
 469    /// <param name="cancellationToken">The cancellation token</param>
 470    /// <returns>The entity if found, otherwise <c>null</c></returns>
 471    public async Task<TEntity?> FindAsync(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, Func<TDbContext, TEntity
 472    {
 0473        return await FindAsync(query, onLoading, false, cancellationToken);
 0474    }
 475
 476    /// <summary>
 477    /// Finds a single entity using a query
 478    /// </summary>
 479    /// <param name="query">The query to use</param>
 480    /// <param name="onLoading">A callback to run after the entity is loaded</param>
 481    /// <param name="tenantAgnostic">Define is the request should be tenant agnostic or not</param>
 482    /// <param name="cancellationToken">The cancellation token</param>
 483    /// <returns>The entity if found, otherwise <c>null</c></returns>
 484    public async Task<TEntity?> FindAsync(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, Func<TDbContext, TEntity
 485    {
 2486        return await QueryAsync(query, onLoading, tenantAgnostic, cancellationToken).FirstOrDefault();
 2487    }
 488
 489    /// <summary>
 490    /// Finds a single entity using a query
 491    /// </summary>
 492    /// <param name="query">The query to use</param>
 493    /// <param name="cancellationToken">The cancellation token</param>
 494    /// <returns>The entity if found, otherwise <c>null</c></returns>
 495    public async Task<TEntity?> FindAsync(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, CancellationToken cancel
 496    {
 0497        return await FindAsync(query, false, cancellationToken);
 0498    }
 499
 500    /// <summary>
 501    /// Finds a single entity using a query
 502    /// </summary>
 503    /// <param name="query">The query to use</param>
 504    /// <param name="tenantAgnostic">Define is the request should be tenant agnostic or not</param>
 505    /// <param name="cancellationToken">The cancellation token</param>
 506    /// <returns>The entity if found, otherwise <c>null</c></returns>
 507    public async Task<TEntity?> FindAsync(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, bool tenantAgnostic = fa
 508    {
 0509        return await QueryAsync(query, tenantAgnostic, cancellationToken).FirstOrDefault();
 0510    }
 511
 512    /// <summary>
 513    /// Finds a list of entities using a query
 514    /// </summary>
 0515    public async Task<IEnumerable<TEntity>> FindManyAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken c
 516
 517    /// <summary>
 518    /// Finds a list of entities using a query
 519    /// </summary>
 520    public async Task<IEnumerable<TEntity>> FindManyAsync(Expression<Func<TEntity, bool>> predicate, Action<TDbContext, 
 521    {
 0522        await using var dbContext = await CreateDbContextAsync(cancellationToken);
 0523        var set = dbContext.Set<TEntity>().AsNoTracking();
 0524        var entities = await set.Where(predicate).ToListAsync(cancellationToken);
 525
 0526        if (onLoading != null)
 0527            foreach (var entity in entities)
 0528                onLoading(dbContext, entity);
 529
 0530        return entities;
 0531    }
 532
 533    /// <summary>
 534    /// Finds a list of entities using a query
 535    /// </summary>
 536    public async Task<Page<TEntity>> FindManyAsync<TKey>(
 537        Expression<Func<TEntity, bool>> predicate,
 538        Expression<Func<TEntity, TKey>> orderBy,
 539        OrderDirection orderDirection = OrderDirection.Ascending,
 540        PageArgs? pageArgs = null,
 541        CancellationToken cancellationToken = default) =>
 0542        await FindManyAsync(predicate, orderBy, orderDirection, pageArgs, null, cancellationToken);
 543
 544    /// <summary>
 545    /// Returns a list of entities using a query
 546    /// </summary>
 547    public async Task<Page<TEntity>> FindManyAsync<TKey>(
 548        Expression<Func<TEntity, bool>>? predicate,
 549        Expression<Func<TEntity, TKey>>? orderBy,
 550        OrderDirection orderDirection = OrderDirection.Ascending,
 551        PageArgs? pageArgs = null,
 552        Func<TDbContext, TEntity?, TEntity?>? onLoading = null,
 553        CancellationToken cancellationToken = default)
 554    {
 0555        await using var dbContext = await CreateDbContextAsync(cancellationToken);
 0556        var set = dbContext.Set<TEntity>().AsNoTracking();
 557
 0558        if (predicate != null)
 0559            set = set.Where(predicate);
 560
 0561        if (orderBy != null)
 0562            set = orderDirection switch
 0563            {
 0564                OrderDirection.Ascending => set.OrderBy(orderBy),
 0565                OrderDirection.Descending => set.OrderByDescending(orderBy),
 0566                _ => set.OrderBy(orderBy)
 0567            };
 568
 0569        var page = await set.PaginateAsync(pageArgs);
 570
 0571        if (onLoading != null)
 0572            page = page with
 0573            {
 0574                Items = page.Items.Select(x => onLoading(dbContext, x)!).ToList()
 0575            };
 576
 0577        return page;
 0578    }
 579
 580    public Task<IEnumerable<TEntity>> ListAsync(CancellationToken cancellationToken = default)
 581    {
 0582        return ListAsync(null, cancellationToken);
 583    }
 584
 585    public async Task<IEnumerable<TEntity>> ListAsync(Action<TDbContext, TEntity?>? onLoading = null, CancellationToken 
 586    {
 0587        await using var dbContext = await CreateDbContextAsync(cancellationToken);
 0588        var set = dbContext.Set<TEntity>().AsNoTracking();
 0589        var entities = await set.ToListAsync(cancellationToken);
 590
 0591        if (onLoading != null)
 0592            foreach (var entity in entities)
 0593                onLoading(dbContext, entity);
 594
 0595        return entities;
 0596    }
 597
 598    /// <summary>
 599    /// Finds a single entity using a query.
 600    /// </summary>
 601    /// <returns>True if the entity was found, otherwise false.</returns>
 602    public async Task<bool> DeleteAsync(TEntity entity, CancellationToken cancellationToken = default)
 603    {
 0604        await using var dbContext = await CreateDbContextAsync(cancellationToken);
 0605        var set = dbContext.Set<TEntity>();
 0606        set.Attach(entity).State = EntityState.Deleted;
 0607        return await dbContext.SaveChangesAsync(cancellationToken) == 1;
 0608    }
 609
 610    /// <summary>
 611    /// Deletes entities using a predicate.
 612    /// </summary>
 613    /// <returns>The number of entities deleted.</returns>
 614    public async Task<long> DeleteWhereAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationTo
 615    {
 7616        await using var dbContext = await CreateDbContextAsync(cancellationToken);
 7617        var set = dbContext.Set<TEntity>().AsNoTracking();
 7618        return await set.Where(predicate).ExecuteDeleteAsync(cancellationToken);
 7619    }
 620
 621    /// <summary>
 622    /// Deletes entities using a query.
 623    /// </summary>
 624    /// <returns>The number of entities deleted.</returns>
 625    public async Task<long> DeleteWhereAsync(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, CancellationToken can
 626    {
 127627        await using var dbContext = await CreateDbContextAsync(cancellationToken);
 127628        var set = dbContext.Set<TEntity>().AsNoTracking();
 127629        var queryable = query(set.AsQueryable());
 127630        return await queryable.ExecuteDeleteAsync(cancellationToken);
 127631    }
 632
 633    /// <summary>
 634    /// Queries the database using a query.
 635    /// </summary>
 636    public async Task<IEnumerable<TEntity>> QueryAsync(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, Cancellatio
 637    {
 310638        return await QueryAsync(query, null, false, cancellationToken);
 310639    }
 640
 641    /// <summary>
 642    /// Queries the database using a query.
 643    /// </summary>
 644    public async Task<IEnumerable<TEntity>> QueryAsync(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, bool tenant
 645    {
 0646        return await QueryAsync(query, null, tenantAgnostic, cancellationToken);
 0647    }
 648
 649    /// <summary>
 650    /// Queries the database using a query and a selector.
 651    /// </summary>
 652    public async Task<IEnumerable<TEntity>> QueryAsync(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, Func<TDbCon
 653    {
 366654        return await QueryAsync(query, onLoading, false, cancellationToken);
 366655    }
 656
 657    /// <summary>
 658    /// Queries the database using a query and a selector.
 659    /// </summary>
 660    public async Task<IEnumerable<TEntity>> QueryAsync(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, Func<TDbCon
 661    {
 17872662        await using var dbContext = await CreateDbContextAsync(cancellationToken);
 17872663        var asNoTracking = onLoading == null;
 17872664        var set = asNoTracking ? dbContext.Set<TEntity>().AsNoTracking() : dbContext.Set<TEntity>();
 17872665        var queryable = query(set.AsQueryable());
 666
 17872667        if (ignoreQueryFilters)
 0668            queryable = queryable.IgnoreQueryFilters();
 669
 17872670        var entities = await queryable.ToListAsync(cancellationToken);
 671
 17872672        if (onLoading != null)
 673        {
 34951674            var loadingTasks = entities.Select(entity => onLoading(dbContext, entity, cancellationToken).AsTask()).ToLis
 17562675            await Task.WhenAll(loadingTasks);
 676        }
 677
 17872678        return entities;
 17872679    }
 680
 681    /// <summary>
 682    /// Queries the database using a query and a selector.
 683    /// </summary>
 684    public async Task<IEnumerable<TResult>> QueryAsync<TResult>(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, Ex
 685    {
 11686        return await QueryAsync(query, selector, false, cancellationToken);
 11687    }
 688
 689    /// <summary>
 690    /// Queries the database using a query and a selector.
 691    /// </summary>
 692    public async Task<IEnumerable<TResult>> QueryAsync<TResult>(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, Ex
 693    {
 11694        await using var dbContext = await CreateDbContextAsync(cancellationToken);
 11695        var set = dbContext.Set<TEntity>().AsNoTracking();
 11696        var queryable = query(set.AsQueryable());
 697
 11698        if (ignoreQueryFilters)
 0699            queryable = queryable.IgnoreQueryFilters();
 700
 11701        queryable = query(queryable);
 11702        return await queryable.Select(selector).ToListAsync(cancellationToken);
 11703    }
 704
 705    /// <summary>
 706    /// Counts the number of entities matching a query.
 707    /// </summary>
 708    public async Task<long> CountAsync(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, CancellationToken cancellat
 709    {
 0710        return await CountAsync(query, false, cancellationToken);
 0711    }
 712
 713    /// <summary>
 714    /// Counts the number of entities matching a query.
 715    /// </summary>
 716    public async Task<long> CountAsync(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, bool ignoreQueryFilters = f
 717    {
 24718        await using var dbContext = await CreateDbContextAsync(cancellationToken);
 24719        var set = dbContext.Set<TEntity>().AsNoTracking();
 24720        var queryable = query(set.AsQueryable());
 721
 24722        if (ignoreQueryFilters)
 0723            queryable = queryable.IgnoreQueryFilters();
 724
 24725        queryable = query(queryable);
 24726        return await queryable.LongCountAsync(cancellationToken: cancellationToken);
 24727    }
 728
 729    /// <summary>
 730    /// Checks if any entities exist.
 731    /// </summary>
 732    public async Task<bool> AnyAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = de
 733    {
 0734        return await AnyAsync(predicate, false, cancellationToken);
 0735    }
 736
 737    /// <summary>
 738    /// Checks if any entities exist.
 739    /// </summary>
 740    public async Task<bool> AnyAsync(Expression<Func<TEntity, bool>> predicate, bool ignoreQueryFilters = false, Cancell
 741    {
 0742        await using var dbContext = await CreateDbContextAsync(cancellationToken);
 0743        var set = dbContext.Set<TEntity>().AsNoTracking();
 0744        return await set.AnyAsync(predicate, cancellationToken);
 0745    }
 746
 747    /// <summary>
 748    /// Counts the number of entities matching a predicate.
 749    /// </summary>
 750    /// <param name="predicate">The predicate.</param>
 751    /// <param name="cancellationToken">The cancellation token.</param>
 752    public async Task<long> CountAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = 
 753    {
 0754        return await CountAsync(predicate, false, cancellationToken);
 0755    }
 756
 757    /// <summary>
 758    /// Counts the number of entities matching a predicate.
 759    /// </summary>
 760    /// <param name="predicate">The predicate.</param>
 761    /// <param name="ignoreQueryFilters">Whether to ignore query filters.</param>
 762    /// <param name="cancellationToken">The cancellation token.</param>
 763    public async Task<long> CountAsync(Expression<Func<TEntity, bool>> predicate, bool ignoreQueryFilters = false, Cance
 764    {
 0765        await using var dbContext = await CreateDbContextAsync(cancellationToken);
 0766        var queryable = dbContext.Set<TEntity>().AsNoTracking();
 767
 0768        if (ignoreQueryFilters)
 0769            queryable = queryable.IgnoreQueryFilters();
 770
 0771        return await queryable.CountAsync(predicate, cancellationToken);
 0772    }
 773
 774    /// <summary>
 775    /// Counts the distinct number of entities matching a predicate.
 776    /// </summary>
 777    /// <param name="predicate">The predicate.</param>
 778    /// <param name="propertySelector">The property selector to distinct by.</param>
 779    /// <param name="cancellationToken">The cancellation token.</param>
 780    public async Task<long> CountAsync<TProperty>(Expression<Func<TEntity, bool>> predicate, Expression<Func<TEntity, TP
 781    {
 0782        return await CountAsync(predicate, propertySelector, false, cancellationToken);
 0783    }
 784
 785    /// <summary>
 786    /// Counts the distinct number of entities matching a predicate.
 787    /// </summary>
 788    /// <param name="predicate">The predicate.</param>
 789    /// <param name="propertySelector">The property selector to distinct by.</param>
 790    /// <param name="ignoreQueryFilters">Whether to ignore query filters.</param>
 791    /// <param name="cancellationToken">The cancellation token.</param>
 792    public async Task<long> CountAsync<TProperty>(Expression<Func<TEntity, bool>> predicate, Expression<Func<TEntity, TP
 793    {
 0794        await using var dbContext = await CreateDbContextAsync(cancellationToken);
 0795        var queryable = dbContext.Set<TEntity>().AsNoTracking();
 796
 0797        if (ignoreQueryFilters)
 0798            queryable = queryable.IgnoreQueryFilters();
 799
 0800        return await queryable
 0801            .Where(predicate)
 0802            .Select(propertySelector)
 0803            .Distinct()
 0804            .CountAsync(cancellationToken);
 0805    }
 806}