< Summary

Information
Class: Elsa.Workflows.Management.Filters.WorkflowInstanceFilter
Assembly: Elsa.Workflows.Management
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs
Line coverage
81%
Covered lines: 89
Uncovered lines: 20
Coverable lines: 109
Total lines: 265
Line coverage: 81.6%
Branch coverage
61%
Covered branches: 50
Total branches: 81
Branch coverage: 61.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
get_AllowedTimestampFilterColumns()100%11100%
get_Id()100%11100%
get_Ids()100%11100%
get_SearchTerm()100%11100%
get_Name()100%11100%
get_DefinitionId()100%11100%
get_DefinitionVersionId()100%11100%
get_DefinitionIds()100%11100%
get_DefinitionVersionIds()100%11100%
get_Version()100%11100%
get_ParentWorkflowInstanceIds()100%11100%
get_CorrelationId()100%11100%
get_CorrelationIds()100%11100%
get_WorkflowStatus()100%11100%
get_WorkflowStatuses()100%11100%
get_WorkflowSubStatus()100%11100%
get_WorkflowSubStatuses()100%11100%
get_IsExecuting()100%11100%
get_HasIncidents()100%11100%
get_IsSystem()100%11100%
get_BeforeLastUpdated()100%11100%
get_TimestampFilters()100%11100%
get_Names()100%11100%
Apply(...)46.37%2456966.66%
ValidateTimestampFilters()87.5%8887.5%
TryNormalizeTimestampFilterColumn(...)100%44100%
NormalizeTimestampFilterColumn(...)100%22100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs

#LineLine coverage
 1using System.Diagnostics.CodeAnalysis;
 2using Elsa.Workflows.Management.Entities;
 3using Elsa.Workflows.Management.Enums;
 4using Elsa.Workflows.Management.Models;
 5using System.Linq.Dynamic.Core;
 6
 7namespace Elsa.Workflows.Management.Filters;
 8
 9/// <summary>
 10/// A filter for querying workflow instances.
 11/// </summary>
 12public class WorkflowInstanceFilter
 13{
 114    private static readonly string[] TimestampFilterColumns =
 115    [
 116        nameof(WorkflowInstance.CreatedAt),
 117        nameof(WorkflowInstance.UpdatedAt),
 118        nameof(WorkflowInstance.FinishedAt)
 119    ];
 20
 21    /// <summary>
 22    /// The workflow instance timestamp columns that can be used by <see cref="TimestampFilters"/>.
 23    /// </summary>
 224    public static IReadOnlyCollection<string> AllowedTimestampFilterColumns { get; } = Array.AsReadOnly(TimestampFilterC
 25
 26    /// <summary>
 27    /// Filter workflow instances by ID.
 28    /// </summary>
 47729    public string? Id { get; set; }
 30
 31    /// <summary>
 32    /// Filter workflow instances by IDs.
 33    /// </summary>
 26234    public ICollection<string>? Ids { get; set; }
 35
 36    /// <summary>
 37    /// Filter workflow instances that match the specified search term.
 38    /// </summary>
 24039    public string? SearchTerm { get; set; }
 40
 41    /// <summary>
 42    /// Filter workflow instances that match the specified name.
 43    /// </summary>
 24244    public string? Name { get; set; }
 45
 46    /// <summary>
 47    /// Filter workflow instances by definition ID.
 48    /// </summary>
 28549    public string? DefinitionId { get; set; }
 50
 51    /// <summary>
 52    /// Filter workflow instances by definition version ID.
 53    /// </summary>
 24254    public string? DefinitionVersionId { get; set; }
 55
 56    /// <summary>
 57    /// Filter workflow instances by definition IDs.
 58    /// </summary>
 25459    public ICollection<string>? DefinitionIds { get; set; }
 60
 61    /// <summary>
 62    /// Filter workflow instances by definition version IDs.
 63    /// </summary>
 24264    public ICollection<string>? DefinitionVersionIds { get; set; }
 65
 66    /// <summary>
 67    /// Filter workflow instances by version.
 68    /// </summary>
 24269    public int? Version { get; set; }
 70
 71    /// <summary>
 72    /// Filter workflow instances by their parent instance IDs.
 73    /// </summary>
 25374    public ICollection<string>? ParentWorkflowInstanceIds { get; set; }
 75
 76    /// <summary>
 77    /// Filter workflow instances by correlation ID.
 78    /// </summary>
 24879    public string? CorrelationId { get; set; }
 80
 81    /// <summary>
 82    /// Filter workflow instances by correlation IDs.
 83    /// </summary>
 24284    public ICollection<string>? CorrelationIds { get; set; }
 85
 86    /// <summary>
 87    /// Filter workflow instances by status.
 88    /// </summary>
 48089    public WorkflowStatus? WorkflowStatus { get; set; }
 90
 91    /// <summary>
 92    /// Filter workflow instances by a set of statuses.
 93    /// </summary>
 24294    public ICollection<WorkflowStatus>? WorkflowStatuses { get; set; }
 95
 96    /// <summary>
 97    /// Filter workflow instances by sub-status.
 98    /// </summary>
 28099    public WorkflowSubStatus? WorkflowSubStatus { get; set; }
 100
 101    /// <summary>
 102    /// Filter workflow instances by a set of sub-status.
 103    /// </summary>
 242104    public ICollection<WorkflowSubStatus>? WorkflowSubStatuses { get; set; }
 105
 106    /// <summary>
 107    /// Filter workflow instances by whether they are executing.
 108    /// </summary>
 458109    public bool? IsExecuting { get; set; }
 110
 111    /// <summary>
 112    /// Filter workflow instances by whether they have incidents.
 113    /// </summary>
 242114    public bool? HasIncidents { get; set; }
 115
 116    /// <summary>
 117    /// Filter on workflows that are system workflows.
 118    /// </summary>
 242119    public bool? IsSystem { get; set; }
 120
 121    /// <summary>
 122    /// Filter workflow instances that are older than the specified timestamp.
 123    /// </summary>
 458124    public DateTimeOffset? BeforeLastUpdated { get; set; }
 125
 126    /// <summary>
 127    /// Filter workflow instances by timestamp.
 128    /// </summary>
 252129    public ICollection<TimestampFilter>? TimestampFilters { get; set; }
 130
 131    /// <summary>
 132    /// Filter workflow instances by name.
 133    /// </summary>
 242134    public List<string>? Names { get; set; }
 135
 136    /// <summary>
 137    /// Applies the filter to the specified query.
 138    /// </summary>
 139    [RequiresUnreferencedCode("The method uses reflection to create an expression tree.")]
 140    public IQueryable<WorkflowInstance> Apply(IQueryable<WorkflowInstance> query)
 141    {
 242142        var filter = this;
 143
 363144        if (!string.IsNullOrWhiteSpace(filter.Id)) query = query.Where(x => x.Id == filter.Id);
 252145        if (filter.Ids != null) query = query.Where(x => filter.Ids.Contains(x.Id));
 263146        if (!string.IsNullOrWhiteSpace(filter.DefinitionId)) query = query.Where(x => x.DefinitionId == filter.Definitio
 242147        if (!string.IsNullOrWhiteSpace(filter.DefinitionVersionId)) query = query.Where(x => x.DefinitionVersionId == fi
 243148        if (filter.DefinitionIds != null) query = query.Where(x => filter.DefinitionIds.Contains(x.DefinitionId));
 242149        if (filter.DefinitionVersionIds != null) query = query.Where(x => filter.DefinitionVersionIds.Contains(x.Definit
 242150        if (filter.Version != null) query = query.Where(x => x.Version == filter.Version);
 248151        if (filter.ParentWorkflowInstanceIds != null) query = query.Where(x => x.ParentWorkflowInstanceId != null && fil
 245152        if (!string.IsNullOrWhiteSpace(filter.CorrelationId)) query = query.Where(x => x.CorrelationId == filter.Correla
 242153        if (filter.CorrelationIds != null) query = query.Where(x => filter.CorrelationIds.Contains(x.CorrelationId!));
 242154        if (filter.Names != null) query = query.Where(x => filter.Names.Contains(x.Name!));
 328155        if (filter.WorkflowStatus != null) query = query.Where(x => x.Status == filter.WorkflowStatus);
 255156        if (filter.WorkflowSubStatus != null) query = query.Where(x => x.SubStatus == filter.WorkflowSubStatus);
 242157        if (filter.WorkflowStatuses != null) query = query.Where(x => filter.WorkflowStatuses.Contains(x.Status));
 242158        if (filter.WorkflowSubStatuses != null) query = query.Where(x => filter.WorkflowSubStatuses.Contains(x.SubStatus
 314159        if (filter.IsExecuting != null) query = query.Where(x => x.IsExecuting == filter.IsExecuting);
 242160        if (filter.HasIncidents != null) query = filter.HasIncidents == true ? query.Where(x => x.IncidentCount > 0) : q
 242161        if (filter.IsSystem != null) query = query.Where(x => x.IsSystem == filter.IsSystem);
 242162        if (filter.Name != null) query = query.Where(x => x.Name!.ToLower().Contains(filter.Name.ToLower()));
 314163        if (filter.BeforeLastUpdated != null) query = query.Where(x => x.UpdatedAt < filter.BeforeLastUpdated);
 164
 242165        if (TimestampFilters != null)
 166        {
 18167            foreach (var timestampFilter in TimestampFilters)
 168            {
 5169                if (timestampFilter == null)
 1170                    throw new ArgumentException("Timestamp filter must be specified.", nameof(TimestampFilters));
 171
 4172                var column = NormalizeTimestampFilterColumn(timestampFilter.Column);
 3173                var timestamp = timestampFilter.Timestamp;
 3174                var isZeroTime = timestamp.TimeOfDay == TimeSpan.Zero;
 3175                var startDay = new DateTimeOffset(timestamp.Date);
 3176                var endDay = startDay.AddDays(1);
 177
 3178                query = timestampFilter.Operator switch
 3179                {
 0180                    TimestampFilterOperator.Is when isZeroTime => query.Where($"{column} >= @0 && {column} < @1", startD
 0181                    TimestampFilterOperator.Is => query.Where($"{column} == @0", timestamp),
 0182                    TimestampFilterOperator.IsNot when isZeroTime => query.Where($"{column} < @0 || {column} >= @1", sta
 0183                    TimestampFilterOperator.IsNot => query.Where($"{column} != @0", timestamp),
 0184                    TimestampFilterOperator.GreaterThan when isZeroTime => query.Where($"{column} > @0", endDay),
 0185                    TimestampFilterOperator.GreaterThan => query.Where($"{column} > @0", timestamp),
 3186                    TimestampFilterOperator.GreaterThanOrEqual when isZeroTime => query.Where($"{column} >= @0", startDa
 3187                    TimestampFilterOperator.GreaterThanOrEqual => query.Where($"{column} >= @0", timestamp),
 0188                    TimestampFilterOperator.LessThan when isZeroTime => query.Where($"{column} < @0", startDay),
 0189                    TimestampFilterOperator.LessThan => query.Where($"{column} < @0", timestamp),
 0190                    TimestampFilterOperator.LessThanOrEqual when isZeroTime => query.Where($"{column} <= @0", endDay),
 0191                    TimestampFilterOperator.LessThanOrEqual => query.Where($"{column} <= @0", timestamp),
 0192                    _ => query
 3193                };
 194            }
 195        }
 196
 240197        var searchTerm = filter.SearchTerm;
 240198        if (!string.IsNullOrWhiteSpace(searchTerm))
 199        {
 0200            query =
 0201                from instance in query
 0202                where instance.Name!.ToLower().Contains(searchTerm.ToLower())
 0203                      || instance.DefinitionVersionId.Contains(searchTerm)
 0204                      || instance.DefinitionId.Contains(searchTerm)
 0205                      || instance.Id.Contains(searchTerm)
 0206                      || instance.CorrelationId!.Contains(searchTerm)
 0207                select instance;
 208        }
 209
 240210        return query;
 211    }
 212
 213    /// <summary>
 214    /// Validates timestamp filters.
 215    /// </summary>
 216    public static IEnumerable<string> ValidateTimestampFilters(IEnumerable<TimestampFilter>? timestampFilters)
 217    {
 2218        if (timestampFilters == null)
 0219            yield break;
 220
 10221        foreach (var (timestampFilter, index) in timestampFilters.Select((value, index) => (value, index)))
 222        {
 2223            if (timestampFilter == null)
 224            {
 1225                yield return $"Timestamp filter at index {index} must be specified.";
 1226                continue;
 227            }
 228
 1229            if (!TryNormalizeTimestampFilterColumn(timestampFilter.Column, out _, out var error))
 1230                yield return $"Timestamp filter at index {index}: {error}";
 231        }
 2232    }
 233
 234    /// <summary>
 235    /// Resolves a timestamp filter column to its canonical workflow instance property name.
 236    /// </summary>
 237    public static bool TryNormalizeTimestampFilterColumn(string? column, [NotNullWhen(true)] out string? normalizedColum
 238    {
 5239        normalizedColumn = null;
 5240        error = null;
 241
 5242        if (string.IsNullOrWhiteSpace(column))
 243        {
 1244            error = "Timestamp filter column must be specified.";
 1245            return false;
 246        }
 247
 4248        var trimmedColumn = column.Trim();
 13249        normalizedColumn = TimestampFilterColumns.FirstOrDefault(x => string.Equals(x, trimmedColumn, StringComparison.O
 250
 4251        if (normalizedColumn != null)
 3252            return true;
 253
 1254        error = $"Invalid timestamp filter column. Allowed columns are: {string.Join(", ", TimestampFilterColumns)}.";
 1255        return false;
 256    }
 257
 258    private static string NormalizeTimestampFilterColumn(string? column)
 259    {
 4260        if (TryNormalizeTimestampFilterColumn(column, out var normalizedColumn, out var error))
 3261            return normalizedColumn;
 262
 1263        throw new ArgumentException(error, $"{nameof(TimestampFilters)}.Column");
 264    }
 265}