diff --git a/Directory.Packages.props b/Directory.Packages.props
index 3f079533a..302aca426 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -87,6 +87,7 @@
+
diff --git a/NuGet.Config b/NuGet.Config
index 0bcd55102..7f38c9b03 100644
--- a/NuGet.Config
+++ b/NuGet.Config
@@ -3,8 +3,14 @@
-
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowInstances/Contracts/IWorkflowInstancesApi.cs b/src/clients/Elsa.Api.Client/Resources/WorkflowInstances/Contracts/IWorkflowInstancesApi.cs
index 6496b50f5..73b11b092 100644
--- a/src/clients/Elsa.Api.Client/Resources/WorkflowInstances/Contracts/IWorkflowInstancesApi.cs
+++ b/src/clients/Elsa.Api.Client/Resources/WorkflowInstances/Contracts/IWorkflowInstancesApi.cs
@@ -14,7 +14,7 @@ public interface IWorkflowInstancesApi
///
/// Returns a list of workflow instances.
///
- [Get("/workflow-instances")]
+ [Post("/workflow-instances")]
Task> ListAsync(ListWorkflowInstancesRequest request, CancellationToken cancellationToken = default);
///
diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowInstances/Requests/ListWorkflowInstancesRequest.cs b/src/clients/Elsa.Api.Client/Resources/WorkflowInstances/Requests/ListWorkflowInstancesRequest.cs
index 11ab8cac5..0b4283a8b 100644
--- a/src/clients/Elsa.Api.Client/Resources/WorkflowInstances/Requests/ListWorkflowInstancesRequest.cs
+++ b/src/clients/Elsa.Api.Client/Resources/WorkflowInstances/Requests/ListWorkflowInstancesRequest.cs
@@ -13,64 +13,74 @@ public class ListWorkflowInstancesRequest
/// Gets or sets the page number.
///
public int? Page { get; set; }
-
+
///
/// Gets or sets the page size.
///
public int? PageSize { get; set; }
-
+
///
/// Gets or sets the search term.
///
public string? SearchTerm { get; set; }
-
+
///
/// Gets or sets the workflow definition id.
///
public string? DefinitionId { get; set; }
-
+
///
/// Gets or sets the workflow definition ids.
///
- [Query(CollectionFormat.Multi)] public ICollection? DefinitionIds { get; set; }
-
+ public ICollection? DefinitionIds { get; set; }
+
///
/// Gets or sets the correlation id.
///
public string? CorrelationId { get; set; }
-
+
///
/// Gets or sets the version.
///
public int? Version { get; set; }
-
+
+ ///
+ /// Gets or sets whether the workflow instances have incidents or not.
+ ///
+ public bool? HasIncidents { get; set; }
+
///
/// Gets or sets the status.
///
public WorkflowStatus? Status { get; set; }
-
+
///
/// Gets or sets the statuses.
///
- [Query(CollectionFormat.Multi)] public ICollection? Statuses { get; set; }
-
+ public ICollection? Statuses { get; set; }
+
///
/// Gets or sets the sub status.
///
public WorkflowSubStatus? SubStatus { get; set; }
-
+
///
/// Gets or sets the sub statuses.
///
- [Query(CollectionFormat.Multi)] public ICollection? SubStatuses { get; set; }
+ public ICollection? SubStatuses { get; set; }
///
/// Gets or sets the key to order by.
///
public OrderByWorkflowInstance? OrderBy { get; set; }
-
+
///
/// Gets or sets the order direction.
///
public OrderDirection? OrderDirection { get; set; }
+
+ ///
+ /// Gets or sets the timestamp filters.
+ ///
+ public ICollection? TimestampFilters { get; set; }
}
\ No newline at end of file
diff --git a/src/clients/Elsa.Api.Client/Shared/Enums/TimestampFilterOperator.cs b/src/clients/Elsa.Api.Client/Shared/Enums/TimestampFilterOperator.cs
new file mode 100644
index 000000000..cabedd9cf
--- /dev/null
+++ b/src/clients/Elsa.Api.Client/Shared/Enums/TimestampFilterOperator.cs
@@ -0,0 +1,37 @@
+namespace Elsa.Api.Client.Shared.Enums;
+
+///
+/// Specifies the operators for filtering timestamps in a TimestampFilter object.
+///
+public enum TimestampFilterOperator
+{
+ ///
+ /// The timestamp is equal to the specified value.
+ ///
+ Is,
+
+ ///
+ /// The timestamp is not equal to the specified value.
+ ///
+ IsNot,
+
+ ///
+ /// The timestamp is before the specified value.
+ ///
+ LessThan,
+
+ ///
+ /// The timestamp is greater than the specified value.
+ ///
+ GreaterThan,
+
+ ///
+ /// The timestamp is less than or equal to the specified value.
+ ///
+ LessThanOrEqual,
+
+ ///
+ /// The timestamp is greater than or equal to the specified value.
+ ///
+ GreaterThanOrEqual
+}
\ No newline at end of file
diff --git a/src/clients/Elsa.Api.Client/Shared/Models/TimestampFilter.cs b/src/clients/Elsa.Api.Client/Shared/Models/TimestampFilter.cs
new file mode 100644
index 000000000..0e08217cd
--- /dev/null
+++ b/src/clients/Elsa.Api.Client/Shared/Models/TimestampFilter.cs
@@ -0,0 +1,26 @@
+using Elsa.Api.Client.Shared.Enums;
+using JetBrains.Annotations;
+
+namespace Elsa.Api.Client.Shared.Models;
+
+///
+/// Represents a timestamp filter used for filtering data based on a specified timestamp column and operator.
+///
+[UsedImplicitly]
+public class TimestampFilter
+{
+ ///
+ /// Gets or sets the column to filter by.
+ ///
+ public string Column { get; set; } = default!;
+
+ ///
+ /// Gets or sets the operator to use for filtering.
+ ///
+ public TimestampFilterOperator Operator { get; set; }
+
+ ///
+ /// Gets or sets the timestamp to filter by.
+ ///
+ public DateTimeOffset Timestamp { get; set; }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/List/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/List/Endpoint.cs
index 3b56ef650..96bbed0c1 100644
--- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/List/Endpoint.cs
+++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/List/Endpoint.cs
@@ -6,57 +6,98 @@ using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Filters;
using Elsa.Workflows.Management.Models;
using JetBrains.Annotations;
+using Microsoft.AspNetCore.Http;
namespace Elsa.Workflows.Api.Endpoints.WorkflowInstances.List;
-[PublicAPI]
-internal class List : ElsaEndpoint
+[UsedImplicitly]
+internal class List(IWorkflowInstanceStore store) : ElsaEndpoint
{
- private readonly IWorkflowInstanceStore _store;
-
- public List(IWorkflowInstanceStore store)
- {
- _store = store;
- }
-
public override void Configure()
{
- Get("/workflow-instances");
+ Verbs(FastEndpoints.Http.GET, FastEndpoints.Http.POST);
+ Routes("/workflow-instances");
ConfigurePermissions("read:workflow-instances");
}
- public override async Task ExecuteAsync(Request request, CancellationToken cancellationToken)
+ public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var pageArgs = PageArgs.FromPage(request.Page, request.PageSize);
+ if (!await ValidateInputAsync(request, cancellationToken))
+ {
+ await SendErrorsAsync(StatusCodes.Status400BadRequest, cancellationToken);
+ return;
+ }
+
var filter = new WorkflowInstanceFilter
{
SearchTerm = request.SearchTerm,
DefinitionId = request.DefinitionId,
- DefinitionIds = request.DefinitionIds,
+ DefinitionIds = request.DefinitionIds?.Any() == true ? request.DefinitionIds : null,
Version = request.Version,
CorrelationId = request.CorrelationId,
WorkflowStatus = request.Status,
WorkflowSubStatus = request.SubStatus,
- WorkflowStatuses = request.Statuses?.Select(Enum.Parse).ToList(),
- WorkflowSubStatuses = request.SubStatuses?.Select(Enum.Parse).ToList(),
- ToCreatedAt = request.ToCreatedAt,
- FromCreatedAt = request.FromCreatedAt,
+ WorkflowStatuses = request.Statuses?.Any() == true ? request.Statuses : null,
+ WorkflowSubStatuses = request.SubStatuses?.Any() == true ? request.SubStatuses : null,
+ HasIncidents = request.HasIncidents,
+ TimestampFilters = request.TimestampFilters?.Any() == true ? request.TimestampFilters : null,
};
var summaries = await FindAsync(request, filter, pageArgs, cancellationToken);
- return new Response(summaries.Items, summaries.TotalCount);
+ var response = new Response(summaries.Items, summaries.TotalCount);
+ await SendOkAsync(response, cancellationToken);
+ }
+
+ private async Task ValidateInputAsync(Request request, CancellationToken cancellationToken)
+ {
+ if (request.Page is < 0)
+ {
+ AddError("Page must be greater than or equal to 1.");
+ return false;
+ }
+
+ if (request.PageSize is < 1)
+ {
+ AddError("Page size must be greater than or equal to 1.");
+ return false;
+ }
+
+ var columnWhitelist = new[]
+ {
+ "CreatedAt", "UpdatedAt", "FinishedAt"
+ };
+
+ if (request.TimestampFilters?.Any() == true)
+ {
+ foreach (var timestampFilter in request.TimestampFilters)
+ {
+ if (string.IsNullOrWhiteSpace(timestampFilter.Column))
+ {
+ AddError("Column must be specified.");
+ return false;
+ }
+
+ if (!columnWhitelist.Contains(timestampFilter.Column))
+ {
+ AddError($"Invalid column '{timestampFilter.Column}'.");
+ return false;
+ }
+ }
+ }
+
+ return true;
}
private async Task> FindAsync(Request request, WorkflowInstanceFilter filter, PageArgs pageArgs, CancellationToken cancellationToken)
{
- request.OrderBy = request.OrderBy ?? OrderByWorkflowInstance.Created;
- var direction = request.OrderBy == OrderByWorkflowInstance.Name ? (request.OrderDirection ?? OrderDirection.Ascending) : (request.OrderDirection ?? OrderDirection.Descending);
+ request.OrderBy ??= OrderByWorkflowInstance.Created;
+ var direction = request.OrderBy == OrderByWorkflowInstance.Name ? request.OrderDirection ?? OrderDirection.Ascending : request.OrderDirection ?? OrderDirection.Descending;
switch (request.OrderBy)
{
default:
- case OrderByWorkflowInstance.Created:
{
var o = new WorkflowInstanceOrder
{
@@ -64,7 +105,7 @@ internal class List : ElsaEndpoint
Direction = direction
};
- return await _store.SummarizeManyAsync(filter, pageArgs, o, cancellationToken);
+ return await store.SummarizeManyAsync(filter, pageArgs, o, cancellationToken);
}
case OrderByWorkflowInstance.UpdatedAt:
{
@@ -74,7 +115,7 @@ internal class List : ElsaEndpoint
Direction = direction
};
- return await _store.SummarizeManyAsync(filter, pageArgs, o, cancellationToken);
+ return await store.SummarizeManyAsync(filter, pageArgs, o, cancellationToken);
}
case OrderByWorkflowInstance.Finished:
{
@@ -84,7 +125,7 @@ internal class List : ElsaEndpoint
Direction = direction
};
- return await _store.SummarizeManyAsync(filter, pageArgs, o, cancellationToken);
+ return await store.SummarizeManyAsync(filter, pageArgs, o, cancellationToken);
}
case OrderByWorkflowInstance.Name:
{
@@ -94,7 +135,7 @@ internal class List : ElsaEndpoint
Direction = direction
};
- return await _store.SummarizeManyAsync(filter, pageArgs, o, cancellationToken);
+ return await store.SummarizeManyAsync(filter, pageArgs, o, cancellationToken);
}
}
}
diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/List/Models.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/List/Models.cs
index 2e9048e6f..b37960763 100644
--- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/List/Models.cs
+++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/List/Models.cs
@@ -12,25 +12,21 @@ public class Request
public string? DefinitionId { get; set; }
public ICollection? DefinitionIds { get; set; }
public string? CorrelationId { get; set; }
+ public string? Name { get; set; }
public int? Version { get; set; }
+ public bool? HasIncidents { get; set; }
public WorkflowStatus? Status { get; set; }
- public ICollection? Statuses { get; set; }
+ public ICollection? Statuses { get; set; }
public WorkflowSubStatus? SubStatus { get; set; }
- public ICollection? SubStatuses { get; set; }
+ public ICollection? SubStatuses { get; set; }
public OrderByWorkflowInstance? OrderBy { get; set; }
public OrderDirection? OrderDirection { get; set; }
- public DateTimeOffset? FromCreatedAt { get; set; }
- public DateTimeOffset? ToCreatedAt { get; set; }
+ public ICollection? TimestampFilters { get; set; }
}
-public class Response
+internal class Response(ICollection items, long totalCount)
{
- public Response(ICollection items, long totalCount)
- {
- Items = items;
- TotalCount = totalCount;
- }
+ public ICollection Items { get; set; } = items;
+ public long TotalCount { get; set; } = totalCount;
+}
- public ICollection Items { get; set; }
- public long TotalCount { get; set; }
-}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Management/Elsa.Workflows.Management.csproj b/src/modules/Elsa.Workflows.Management/Elsa.Workflows.Management.csproj
index 8e2be0af0..e4baba018 100644
--- a/src/modules/Elsa.Workflows.Management/Elsa.Workflows.Management.csproj
+++ b/src/modules/Elsa.Workflows.Management/Elsa.Workflows.Management.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/src/modules/Elsa.Workflows.Management/Enums/TimestampFilterOperator.cs b/src/modules/Elsa.Workflows.Management/Enums/TimestampFilterOperator.cs
new file mode 100644
index 000000000..ff6913129
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Management/Enums/TimestampFilterOperator.cs
@@ -0,0 +1,37 @@
+namespace Elsa.Workflows.Management.Enums;
+
+///
+/// Specifies the operators for filtering timestamps in a TimestampFilter object.
+///
+public enum TimestampFilterOperator
+{
+ ///
+ /// The timestamp is equal to the specified value.
+ ///
+ Is,
+
+ ///
+ /// The timestamp is not equal to the specified value.
+ ///
+ IsNot,
+
+ ///
+ /// The timestamp is before the specified value.
+ ///
+ LessThan,
+
+ ///
+ /// The timestamp is greater than the specified value.
+ ///
+ GreaterThan,
+
+ ///
+ /// The timestamp is less than or equal to the specified value.
+ ///
+ LessThanOrEqual,
+
+ ///
+ /// The timestamp is greater than or equal to the specified value.
+ ///
+ GreaterThanOrEqual
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs b/src/modules/Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs
index f22feec89..b1e53519f 100644
--- a/src/modules/Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs
+++ b/src/modules/Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs
@@ -1,4 +1,8 @@
+using System.Diagnostics.CodeAnalysis;
using Elsa.Workflows.Management.Entities;
+using Elsa.Workflows.Management.Enums;
+using Elsa.Workflows.Management.Models;
+using System.Linq.Dynamic.Core;
namespace Elsa.Workflows.Management.Filters;
@@ -61,7 +65,7 @@ public class WorkflowInstanceFilter
/// Filter workflow instances by status.
///
public WorkflowStatus? WorkflowStatus { get; set; }
-
+
///
/// Filter workflow instances by a set of statuses.
///
@@ -71,25 +75,26 @@ public class WorkflowInstanceFilter
/// Filter workflow instances by sub-status.
///
public WorkflowSubStatus? WorkflowSubStatus { get; set; }
-
+
///
/// Filter workflow instances by a set of sub-status.
///
public ICollection? WorkflowSubStatuses { get; set; }
///
- /// Filter workflow instances by created date time, start from this date time.
+ /// Filter workflow instances by whether they have incidents.
///
- public DateTimeOffset? FromCreatedAt { get; set; }
+ public bool? HasIncidents { get; set; }
///
- /// Filter workflow instances by created date time, to this date time.
+ /// Filter workflow instances by timestamp.
///
- public DateTimeOffset? ToCreatedAt { get; set; }
+ public ICollection? TimestampFilters { get; set; }
///
/// Applies the filter to the specified query.
///
+ [RequiresUnreferencedCode("The method uses reflection to create an expression tree.")]
public IQueryable Apply(IQueryable query)
{
var filter = this;
@@ -106,8 +111,36 @@ public class WorkflowInstanceFilter
if (filter.WorkflowSubStatus != null) query = query.Where(x => x.SubStatus == filter.WorkflowSubStatus);
if (filter.WorkflowStatuses != null) query = query.Where(x => filter.WorkflowStatuses.Contains(x.Status));
if (filter.WorkflowSubStatuses != null) query = query.Where(x => filter.WorkflowSubStatuses.Contains(x.SubStatus));
- if (filter.ToCreatedAt is not null) query = query.Where(x => filter.ToCreatedAt >= x.CreatedAt);
- if (filter.FromCreatedAt is not null) query = query.Where(x => filter.FromCreatedAt <= x.CreatedAt);
+ if (filter.HasIncidents != null) query = filter.HasIncidents == true ? query.Where(x => x.IncidentCount > 0) : query.Where(x => x.IncidentCount == 0);
+
+ if (TimestampFilters != null)
+ {
+ foreach (TimestampFilter timestampFilter in TimestampFilters)
+ {
+ var column = timestampFilter.Column;
+ var timestamp = timestampFilter.Timestamp;
+ var isZeroTime = timestamp.TimeOfDay == TimeSpan.Zero;
+ var startDay = new DateTimeOffset(timestamp.Date);
+ var endDay = startDay.AddDays(1);
+
+ query = timestampFilter.Operator switch
+ {
+ TimestampFilterOperator.Is when isZeroTime => query.Where($"{column} >= @0 && {column} < @1", startDay, endDay),
+ TimestampFilterOperator.Is => query.Where($"{column} == @0", timestamp),
+ TimestampFilterOperator.IsNot when isZeroTime => query.Where($"{column} < @0 || {column} >= @1", startDay, endDay),
+ TimestampFilterOperator.IsNot => query.Where($"{column} != @0", timestamp),
+ TimestampFilterOperator.GreaterThan when isZeroTime => query.Where($"{column} > @0", endDay),
+ TimestampFilterOperator.GreaterThan => query.Where($"{column} > @0", timestamp),
+ TimestampFilterOperator.GreaterThanOrEqual when isZeroTime => query.Where($"{column} >= @0", startDay),
+ TimestampFilterOperator.GreaterThanOrEqual => query.Where($"{column} >= @0", timestamp),
+ TimestampFilterOperator.LessThan when isZeroTime => query.Where($"{column} < @0", startDay),
+ TimestampFilterOperator.LessThan => query.Where($"{column} < @0", timestamp),
+ TimestampFilterOperator.LessThanOrEqual when isZeroTime => query.Where($"{column} <= @0", endDay),
+ TimestampFilterOperator.LessThanOrEqual => query.Where($"{column} <= @0", timestamp),
+ _ => query
+ };
+ }
+ }
var searchTerm = filter.SearchTerm;
if (!string.IsNullOrWhiteSpace(searchTerm))
@@ -115,10 +148,10 @@ public class WorkflowInstanceFilter
query =
from instance in query
where instance.Name!.Contains(searchTerm)
- || instance.DefinitionVersionId.Equals(searchTerm)
- || instance.Id.Equals(searchTerm)
- || instance.DefinitionId.Equals(searchTerm)
- || instance.CorrelationId!.Equals(searchTerm)
+ || instance.DefinitionVersionId.Contains(searchTerm)
+ || instance.DefinitionId.Contains(searchTerm)
+ || instance.Id.Contains(searchTerm)
+ || instance.CorrelationId!.Contains(searchTerm)
select instance;
}
diff --git a/src/modules/Elsa.Workflows.Management/Models/TimestampFilter.cs b/src/modules/Elsa.Workflows.Management/Models/TimestampFilter.cs
new file mode 100644
index 000000000..1d02f2970
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Management/Models/TimestampFilter.cs
@@ -0,0 +1,26 @@
+using Elsa.Workflows.Management.Enums;
+using JetBrains.Annotations;
+
+namespace Elsa.Workflows.Management.Models;
+
+///
+/// Represents a timestamp filter used for filtering data based on a specified timestamp column and operator.
+///
+[UsedImplicitly]
+public class TimestampFilter
+{
+ ///
+ /// Gets or sets the column to filter by.
+ ///
+ public required string Column { get; set; } = default!;
+
+ ///
+ /// Gets or sets the operator to use for filtering.
+ ///
+ public required TimestampFilterOperator Operator { get; set; }
+
+ ///
+ /// Gets or sets the timestamp to filter by.
+ ///
+ public required DateTimeOffset Timestamp { get; set; }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Management/RequiredMembers.cs b/src/modules/Elsa.Workflows.Management/RequiredMembers.cs
new file mode 100644
index 000000000..e1a4aad6d
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Management/RequiredMembers.cs
@@ -0,0 +1,8 @@
+// See: https://github.com/dotnet/core/issues/8016
+namespace System.Runtime.CompilerServices;
+
+public class RequiredMemberAttribute : Attribute { }
+public class CompilerFeatureRequiredAttribute : Attribute
+{
+ public CompilerFeatureRequiredAttribute(string name) { }
+}
\ No newline at end of file