Addresses warnings and enforces null safety (#7051)

* Enhance null-safety annotations across modules and refactor for improved consistency:

- Added `null!` annotations to enforce non-nullability expectations.
- Updated workflows, tests, and runtime services to handle default null values reliably.
- Removed obsolete and unused APIs, simplifying interfaces and improving maintainability.
- Refactored methods and properties for clarity, thread-safety, and consistency.
- Adjusted test configurations for code coverage tracking and integration improvements.

* Refactor activity iteration in container serialization tests to simplify type casting.
This commit is contained in:
Sipke Schoorstra 2025-11-12 16:56:31 +01:00 committed by GitHub
parent 490c8a2c9e
commit 2c0b3da5de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 119 additions and 145 deletions

View file

@ -160,7 +160,7 @@ public static class JsonObjectExtensions
{
return model.GetProperty<T>(path);
}
catch (Exception e)
catch (Exception)
{
return default;
}

View file

@ -28,7 +28,7 @@ public interface ICommandSender
/// <param name="cancellationToken">The cancellation token.</param>
/// <typeparam name="T">The type of the result.</typeparam>
/// <returns>The result.</returns>
Task<T> SendAsync<T>(ICommand<T> command, ICommandStrategy strategy, CancellationToken cancellationToken = default);
Task<T> SendAsync<T>(ICommand<T> command, ICommandStrategy? strategy, CancellationToken cancellationToken = default);
/// <summary>
/// Sends a command using the specified strategy.
@ -39,7 +39,7 @@ public interface ICommandSender
/// <param name="cancellationToken">The cancellation token.</param>
/// <typeparam name="T">The type of the result.</typeparam>
/// <returns>The result.</returns>
Task<T> SendAsync<T>(ICommand<T> command, ICommandStrategy strategy, IDictionary<object, object> headers, CancellationToken cancellationToken = default);
Task<T> SendAsync<T>(ICommand<T> command, ICommandStrategy? strategy, IDictionary<object, object> headers, CancellationToken cancellationToken = default);
/// <summary>
/// Sends a command using the default strategy.
@ -54,7 +54,7 @@ public interface ICommandSender
/// <param name="command">The command to send.</param>
/// <param name="strategy">The command strategy to use.</param>
/// <param name="cancellationToken">The cancellation token.</param>
Task SendAsync(ICommand command, ICommandStrategy strategy, CancellationToken cancellationToken = default);
Task SendAsync(ICommand command, ICommandStrategy? strategy, CancellationToken cancellationToken = default);
/// <summary>
/// Sends a command using the specified strategy.
@ -63,5 +63,5 @@ public interface ICommandSender
/// <param name="strategy">The command strategy to use.</param>
/// <param name="headers">Any headers to pass along.</param>
/// <param name="cancellationToken">The cancellation token.</param>
Task SendAsync(ICommand command, ICommandStrategy strategy, IDictionary<object, object> headers, CancellationToken cancellationToken = default);
Task SendAsync(ICommand command, ICommandStrategy? strategy, IDictionary<object, object> headers, CancellationToken cancellationToken = default);
}

View file

@ -39,7 +39,7 @@ public static class MediatorExtensions
/// <param name="cancellationToken">The cancellation token.</param>
public static async Task SendAsync(this IMediator mediator, ICommand command, CancellationToken cancellationToken = default)
{
await mediator.SendAsync(command, default, cancellationToken);
await mediator.SendAsync(command, null, cancellationToken);
}
/// <summary>
@ -50,6 +50,6 @@ public static class MediatorExtensions
/// <param name="cancellationToken">The cancellation token.</param>
public static async Task SendAsync(this ICommandSender commandSender, ICommand command, CancellationToken cancellationToken = default)
{
await commandSender.SendAsync(command, default, cancellationToken);
await commandSender.SendAsync(command, null, cancellationToken);
}
}

View file

@ -54,9 +54,10 @@ public class DefaultMediator : IMediator
return (T)context.Response;
}
public async Task<T> SendAsync<T>(ICommand<T> command, ICommandStrategy strategy, IDictionary<object, object> headers, CancellationToken cancellationToken = default)
public async Task<T> SendAsync<T>(ICommand<T> command, ICommandStrategy? strategy, IDictionary<object, object> headers, CancellationToken cancellationToken = default)
{
var resultType = typeof(T);
strategy ??= _defaultCommandStrategy;
var context = new CommandContext(command, strategy, resultType, headers, _serviceProvider, cancellationToken);
await _commandPipeline.InvokeAsync(context);
@ -92,7 +93,7 @@ public class DefaultMediator : IMediator
}
/// <inheritdoc />
public Task<T> SendAsync<T>(ICommand<T> command, ICommandStrategy strategy, CancellationToken cancellationToken = default)
public Task<T> SendAsync<T>(ICommand<T> command, ICommandStrategy? strategy, CancellationToken cancellationToken = default)
{
return SendAsync(command, strategy, new Dictionary<object, object>(), cancellationToken);
}

View file

@ -63,7 +63,7 @@ public static class DispatchWorkflowExtensions
var workflowDispatcher = serviceProvider.GetRequiredService<IWorkflowDispatcher>();
var dispatchWorkflowResponse = await workflowDispatcher.DispatchAsync(new DispatchWorkflowDefinitionRequest
{
DefinitionVersionId = workflow.DefinitionHandle.DefinitionVersionId,
DefinitionVersionId = workflow.DefinitionHandle.DefinitionVersionId!,
InstanceId = instanceId ?? Guid.NewGuid().ToString(),
});
dispatchWorkflowResponse.ThrowIfFailed();

View file

@ -3,33 +3,24 @@ using Xunit.Abstractions;
namespace Elsa.Testing.Shared;
public class XunitLogger : ILogger
public class XunitLogger(ITestOutputHelper testOutputHelper, string categoryName) : ILogger
{
private readonly ITestOutputHelper _testOutputHelper;
private readonly string _categoryName;
public XunitLogger(ITestOutputHelper testOutputHelper, string categoryName)
{
_testOutputHelper = testOutputHelper;
_categoryName = categoryName;
}
public IDisposable BeginScope<TState>(TState state) => NoopDisposable.Instance;
public IDisposable BeginScope<TState>(TState state) where TState : notnull => NoopDisposable.Instance;
public bool IsEnabled(LogLevel logLevel)
=> true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
_testOutputHelper.WriteLine($"{_categoryName} [{eventId}] {formatter(state, exception)}");
testOutputHelper.WriteLine($"{categoryName} [{eventId}] {formatter(state, exception)}");
if (exception != null)
_testOutputHelper.WriteLine(exception.ToString());
testOutputHelper.WriteLine(exception.ToString());
}
private class NoopDisposable : IDisposable
{
public static readonly NoopDisposable Instance = new NoopDisposable();
public static readonly NoopDisposable Instance = new();
public void Dispose()
{

View file

@ -2,16 +2,9 @@
namespace Elsa.Expressions.Liquid.Helpers;
public class ConfigurationSectionWrapper
public class ConfigurationSectionWrapper(IConfigurationSection section)
{
private readonly IConfigurationSection _section;
public override string ToString() => section.Value!;
public ConfigurationSectionWrapper(IConfigurationSection section)
{
_section = section;
}
public override string ToString() => _section.Value;
public ConfigurationSectionWrapper GetSection(string name) => new(_section.GetSection(name));
public ConfigurationSectionWrapper GetSection(string name) => new(section.GetSection(name));
}

View file

@ -461,7 +461,7 @@ public class HttpEndpoint : Trigger<HttpRequest>
var contentType = httpRequest.ContentType!;
var headers = httpRequest.Headers.ToDictionary(x => x.Key, x => x.Value.ToArray());
return await context.ParseContentAsync(contentStream, contentType, targetType, headers, cancellationToken);
return await context.ParseContentAsync(contentStream, contentType, targetType, headers!, cancellationToken);
}
private static bool HasContent(HttpRequest httpRequest) => httpRequest.Headers.ContentLength > 0;

View file

@ -240,8 +240,8 @@ public abstract class SendHttpRequestBase(string? source = null, int? line = nul
_ => typeof(string)
};
var contentHeadersDictionary = contentHeaders.ToDictionary(x => x.Key, x => x.Value.Cast<string?>().ToArray(), StringComparer.OrdinalIgnoreCase);
var responseHeadersDictionary = responseHeaders.ToDictionary(x => x.Key, x => x.Value.Cast<string?>().ToArray(), StringComparer.OrdinalIgnoreCase);
var contentHeadersDictionary = contentHeaders.ToDictionary(x => x.Key, x => x.Value.ToArray(), StringComparer.OrdinalIgnoreCase);
var responseHeadersDictionary = responseHeaders.ToDictionary(x => x.Key, x => x.Value.ToArray(), StringComparer.OrdinalIgnoreCase);
var headersDictionary = contentHeadersDictionary.Concat(responseHeadersDictionary).ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase);
return await context.ParseContentAsync(contentStream, contentType, targetType, headersDictionary, cancellationToken);
}

View file

@ -101,7 +101,7 @@ public class WriteHttpResponse : Activity
// Add headers.
var headers = context.GetHeaders(ResponseHeaders);
foreach (var header in headers)
response.Headers.Add(header.Key, header.Value);
response.Headers[header.Key] = header.Value;
// Get content and content type.
var content = context.Get(Content);

View file

@ -20,7 +20,7 @@ public class FormUrlEncodedHttpContentFactory : IHttpContentFactory
return dictionary.ToDictionary(x => x.Key, x => x.Value.ToString() ?? string.Empty);
if (content is string or JsonObject)
return JsonSerializer.Deserialize<Dictionary<string, string>>(JsonSerializer.Serialize(content));
return JsonSerializer.Deserialize<Dictionary<string, string>>(JsonSerializer.Serialize(content))!;
var jsonElement = JsonSerializer.SerializeToElement(content);
return jsonElement.EnumerateObject().ToDictionary(x => x.Name, x => x.Value.ToString());

View file

@ -8,7 +8,7 @@ namespace Elsa.Extensions;
internal static class HttpActivityExecutionContextExtensions
{
public static async Task<object?> ParseContentAsync(this ActivityExecutionContext context, Stream content, string contentType, Type? returnType, Dictionary<string, string?[]> headers, CancellationToken cancellationToken)
public static async Task<object?> ParseContentAsync(this ActivityExecutionContext context, Stream content, string contentType, Type? returnType, Dictionary<string, string[]> headers, CancellationToken cancellationToken)
{
var parsers = context.GetServices<IHttpContentParser>().OrderByDescending(x => x.Priority).ToList();
var httpResponseParserContext = new HttpResponseParserContext(content, contentType, returnType, headers, cancellationToken);

View file

@ -1,8 +1,6 @@
using Elsa.Http.Options;
using Elsa.Mediator.Contracts;
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Runtime.Notifications;
using JetBrains.Annotations;
using Microsoft.Extensions.Options;
namespace Elsa.Http.Handlers;
@ -10,7 +8,7 @@ namespace Elsa.Http.Handlers;
/// A handler that updates the route table when workflow triggers and bookmarks are indexed.
/// </summary>
[UsedImplicitly]
public class UpdateRouteTable(IRouteTableUpdater routeTableUpdater, IOptions<HttpActivityOptions> options) :
public class UpdateRouteTable(IRouteTableUpdater routeTableUpdater) :
INotificationHandler<WorkflowTriggersIndexed>,
INotificationHandler<WorkflowBookmarksIndexed>
{

View file

@ -113,7 +113,7 @@ public static class BulkUpsertExtensions
var paramName = $"{{{parameterCount++}}}";
// If it's a shadow property, retrieve value via Entry(..).Property(..)
object? value = property.IsShadowProperty()
var value = property.IsShadowProperty()
? dbContext.Entry(entity).Property(property.Name).CurrentValue
: property.PropertyInfo?.GetValue(entity);
@ -176,7 +176,7 @@ public static class BulkUpsertExtensions
{
var paramName = $"{{{parameterCount++}}}";
object? value = property.IsShadowProperty()
var value = property.IsShadowProperty()
? dbContext.Entry(entity).Property(property.Name).CurrentValue
: property.PropertyInfo?.GetValue(entity);
@ -185,7 +185,7 @@ public static class BulkUpsertExtensions
value = converter.ConvertToProvider(value);
placeholders.Add(paramName);
parameters.Add(value);
parameters.Add(value!);
}
sb.Append($"({string.Join(", ", placeholders)})");
@ -238,7 +238,7 @@ public static class BulkUpsertExtensions
{
var paramName = $"{{{parameterCount++}}}";
object? value = property.IsShadowProperty()
var value = property.IsShadowProperty()
? dbContext.Entry(entity).Property(property.Name).CurrentValue
: property.PropertyInfo?.GetValue(entity);
@ -255,7 +255,7 @@ public static class BulkUpsertExtensions
else
placeholders.Add(paramName);
parameters.Add(value);
parameters.Add(value!);
}
sb.Append($"({string.Join(", ", placeholders)})");
@ -308,7 +308,7 @@ public static class BulkUpsertExtensions
{
var paramName = $"{{{parameterCount++}}}";
object? value = property.IsShadowProperty()
var value = property.IsShadowProperty()
? dbContext.Entry(entity).Property(property.Name).CurrentValue
: property.PropertyInfo?.GetValue(entity);
@ -317,7 +317,7 @@ public static class BulkUpsertExtensions
value = converter.ConvertToProvider(value);
placeholders.Add(paramName);
parameters.Add(value);
parameters.Add(value!);
}
sb.Append($"({string.Join(", ", placeholders)})");
@ -374,7 +374,7 @@ public static class BulkUpsertExtensions
{
var paramName = $"{{{parameterCount++}}}";
object? value = property.IsShadowProperty()
var value = property.IsShadowProperty()
? dbContext.Entry(entity).Property(property.Name).CurrentValue
: property.PropertyInfo?.GetValue(entity);
@ -382,7 +382,7 @@ public static class BulkUpsertExtensions
if (converter != null)
value = converter.ConvertToProvider(value);
parameters.Add(value);
parameters.Add(value!);
// Oracle aliases must match the column name
var alias = property.GetColumnName(storeObject);

View file

@ -13,7 +13,10 @@ namespace Elsa.Persistence.EFCore;
/// For plain objects, fall back to deep equality comparison using JSON serialization
/// (safe, but inefficient).
/// </remarks>
public class JsonValueComparer<T> : ValueComparer<T> {
public class JsonValueComparer<T>() : ValueComparer<T>((t1, t2) => DoEquals(t1!, t2!),
t => DoGetHashCode(t),
t => DoGetSnapshot(t))
{
private static string Json(T instance) {
return JsonSerializer.Serialize(instance);
@ -24,9 +27,7 @@ public class JsonValueComparer<T> : ValueComparer<T> {
if (instance is ICloneable cloneable)
return (T)cloneable.Clone();
var result = (T)JsonSerializer.Deserialize<T>(Json(instance));
return result;
return JsonSerializer.Deserialize<T>(Json(instance))!;
}
private static int DoGetHashCode(T instance) {
@ -43,15 +44,6 @@ public class JsonValueComparer<T> : ValueComparer<T> {
if (left is IEquatable<T> equatable)
return equatable.Equals(right);
var result = Json(left).Equals(Json(right));
return result;
return Json(left).Equals(Json(right));
}
public JsonValueComparer() : base(
(t1, t2) => DoEquals(t1, t2),
t => DoGetHashCode(t),
t => DoGetSnapshot(t)) {
}
}

View file

@ -15,7 +15,7 @@ internal static class JsonValueConverterHelper
public static string Serialize<T>(T obj) where T : class
{
return (obj == null ? null : JsonSerializer.Serialize(obj, JsonSerializerOptions))!;
return (obj == null! ? null : JsonSerializer.Serialize(obj, JsonSerializerOptions))!;
}
private static JsonSerializerOptions CreateJsonSerializerOptions()

View file

@ -221,7 +221,7 @@ public class EFCoreWorkflowDefinitionStore(EntityStore<ManagementElsaDbContext,
if (filter.IsSystem != null)
queryable = filter.IsSystem == true
? queryable.Where(x => x.IsSystem == true)
: queryable.Where(x => x.IsSystem == false || x.IsSystem == null);
: queryable.Where(x => x.IsSystem == false || x.IsSystem == null!);
if (filter.IsReadonly != null) queryable = queryable.Where(x => x.IsReadonly == filter.IsReadonly);
return queryable;

View file

@ -6,7 +6,6 @@ using Elsa.Common.Codecs;
using Elsa.Common.Entities;
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Management.Options;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Entities;
using Elsa.Workflows.Runtime.Extensions;
@ -15,7 +14,6 @@ using Elsa.Workflows.Runtime.OrderDefinitions;
using Elsa.Workflows.State;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Open.Linq.AsyncExtensions;
namespace Elsa.Persistence.EFCore.Modules.Runtime;
@ -26,10 +24,8 @@ namespace Elsa.Persistence.EFCore.Modules.Runtime;
[UsedImplicitly]
public class EFCoreActivityExecutionStore(
EntityStore<RuntimeElsaDbContext, ActivityExecutionRecord> store,
ISafeSerializer safeSerializer,
IPayloadSerializer payloadSerializer,
ICompressionCodecResolver compressionCodecResolver,
IOptions<ManagementOptions> options) : IActivityExecutionStore
ICompressionCodecResolver compressionCodecResolver) : IActivityExecutionStore
{
/// <inheritdoc />
public async Task SaveAsync(ActivityExecutionRecord record, CancellationToken cancellationToken = default) => await store.SaveAsync(record, OnSaveAsync, cancellationToken);

View file

@ -48,13 +48,14 @@ internal class Export : ElsaEndpoint<Request>
{
if (request.DefinitionId != null)
await DownloadSingleWorkflowAsync(request.DefinitionId, request.VersionOptions, cancellationToken);
else
else if (request.Ids != null)
await DownloadMultipleWorkflowsAsync(request.Ids, cancellationToken);
else await Send.NoContentAsync(cancellationToken);
}
private async Task DownloadMultipleWorkflowsAsync(ICollection<string> ids, CancellationToken cancellationToken)
{
List<WorkflowDefinition> definitions = (await _store.FindManyAsync(new WorkflowDefinitionFilter
List<WorkflowDefinition> definitions = (await _store.FindManyAsync(new()
{
Ids = ids
}, cancellationToken)).ToList();
@ -88,7 +89,7 @@ internal class Export : ElsaEndpoint<Request>
private async Task DownloadSingleWorkflowAsync(string definitionId, string? versionOptions, CancellationToken cancellationToken)
{
var parsedVersionOptions = string.IsNullOrEmpty(versionOptions) ? VersionOptions.Latest : VersionOptions.FromString(versionOptions);
WorkflowDefinition? definition = (await _store.FindManyAsync(new WorkflowDefinitionFilter
WorkflowDefinition? definition = (await _store.FindManyAsync(new()
{
DefinitionId = definitionId,
VersionOptions = parsedVersionOptions

View file

@ -151,7 +151,7 @@ internal class List(IWorkflowInstanceStore store) : ElsaEndpoint<Request, Respon
{
var o = new WorkflowInstanceOrder<string>
{
KeySelector = p => p.Name,
KeySelector = p => p.Name!,
Direction = direction
};

View file

@ -26,18 +26,19 @@ public class NotReadOnlyRequirementHandler : AuthorizationHandler<NotReadOnlyReq
}
/// <inheritdoc />
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, NotReadOnlyRequirement requirement, NotReadOnlyResource resource)
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, NotReadOnlyRequirement requirement, NotReadOnlyResource resource)
{
if (_managementOptions.Value.IsReadOnlyMode)
{
context.Fail(new AuthorizationFailureReason(this, "Workflow edit is not allowed when the read-only mode is enabled."));
context.Fail(new(this, "Workflow edit is not allowed when the read-only mode is enabled."));
}
if (resource.WorkflowDefinition != null && (resource.WorkflowDefinition.IsReadonly || resource.WorkflowDefinition.IsSystem))
{
context.Fail(new AuthorizationFailureReason(this, "Workflow edit is not allowed for a readonly or system workflow."));
context.Fail(new(this, "Workflow edit is not allowed for a readonly or system workflow."));
}
context.Succeed(requirement);
return Task.CompletedTask;
}
}

View file

@ -33,7 +33,7 @@ public class ArgumentJsonConverter : JsonConverter<ArgumentDefinition>
var typeAlias = _wellKnownTypeRegistry.TryGetAlias(typeName, out var alias) ? alias : null;
var isArray = typeName.IsArray;
var isCollection = typeName.IsCollectionType();
var elementTypeName = isArray ? typeName.GetElementType() : isCollection ? typeName.GenericTypeArguments[0] : typeName;
var elementTypeName = isArray ? typeName.GetElementType()! : isCollection ? typeName.GenericTypeArguments[0] : typeName;
var elementTypeAlias = _wellKnownTypeRegistry.GetAliasOrDefault(elementTypeName);
var isAliasedArray = (isArray || isCollection) && typeAlias != null;
var finalTypeAlias = isArray || isCollection ? typeAlias ?? elementTypeAlias : elementTypeAlias;

View file

@ -90,7 +90,7 @@ public class ParallelForEach<T> : Activity
private ICollection<Guid> GetTagList(ActivityExecutionContext context, string propertyName)
{
// Read the list of tags from the context using the specified property name. The value is stored as JsonArray, so we need to deserialize it.
var jsonArray = context.GetProperty<JsonArray>(propertyName);
var jsonArray = context.GetProperty<JsonArray>(propertyName)!;
return jsonArray.Select(x => x.ConvertTo<Guid>()).ToList();
}

View file

@ -6,7 +6,7 @@ using Elsa.Workflows.Models;
namespace Elsa.Workflows.Builders;
/// <inheritdoc />
public class WorkflowBuilder(IActivityVisitor activityVisitor, IIdentityGraphService identityGraphService, IActivityRegistry activityRegistry, IIdentityGenerator identityGenerator)
public class WorkflowBuilder(IActivityVisitor activityVisitor, IIdentityGraphService identityGraphService, IActivityRegistry activityRegistry)
: IWorkflowBuilder
{
/// <inheritdoc />

View file

@ -402,7 +402,7 @@ public partial class WorkflowExecutionContext : IExecutionContext
/// <summary>
/// The expression execution context for the current workflow execution.
/// </summary>
public ExpressionExecutionContext? ExpressionExecutionContext { get; private set; }
public ExpressionExecutionContext ExpressionExecutionContext { get; private set; } = null!;
/// <inheritdoc />
public IEnumerable<Variable> Variables => Workflow.Variables;

View file

@ -1,4 +1,3 @@
using Elsa.Common;
using Elsa.Extensions;
using Elsa.Workflows.Pipelines.ActivityExecution;
using Microsoft.Extensions.Logging;
@ -19,7 +18,7 @@ public static class ExceptionHandlingMiddlewareExtensions
/// <summary>
/// Catches any exceptions thrown by downstream components and transitions the workflow into the faulted state.
/// </summary>
public class ExceptionHandlingMiddleware(ActivityMiddlewareDelegate next, IIncidentStrategyResolver incidentStrategyResolver, ISystemClock systemClock, ILogger<ExceptionHandlingMiddleware> logger)
public class ExceptionHandlingMiddleware(ActivityMiddlewareDelegate next, IIncidentStrategyResolver incidentStrategyResolver, ILogger<ExceptionHandlingMiddleware> logger)
: IActivityExecutionMiddleware
{
/// <inheritdoc />

View file

@ -4,7 +4,6 @@ using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using Elsa.Expressions.Contracts;
using Elsa.Extensions;
using Elsa.Workflows.Serialization.ReferenceHandlers;
using Newtonsoft.Json.Linq;
@ -14,7 +13,7 @@ namespace Elsa.Workflows.Serialization.Converters;
/// <summary>
/// Reads objects as primitive types rather than <see cref="JsonElement"/> values while also maintaining the .NET type name for reconstructing the actual type.
/// </summary>
public class PolymorphicObjectConverter(IWellKnownTypeRegistry wellKnownTypeRegistry) : JsonConverter<object>
public class PolymorphicObjectConverter : JsonConverter<object>
{
private const string TypePropertyName = "_type";
private const string ItemsPropertyName = "_items";
@ -77,7 +76,7 @@ public class PolymorphicObjectConverter(IWellKnownTypeRegistry wellKnownTypeRegi
{
var parsedModel = JsonElement.ParseValue(ref reader);
var systemTextJson = parsedModel.GetProperty(IslandPropertyName).GetString();
return !string.IsNullOrWhiteSpace(systemTextJson) ? JsonObject.Parse(systemTextJson) : new JsonObject();
return !string.IsNullOrWhiteSpace(systemTextJson) ? JsonNode.Parse(systemTextJson)! : new JsonObject();
}
var isJsonArray = targetType == typeof(JsonArray);
@ -86,7 +85,7 @@ public class PolymorphicObjectConverter(IWellKnownTypeRegistry wellKnownTypeRegi
{
var parsedModel = JsonElement.ParseValue(ref reader);
var systemTextJson = parsedModel.GetProperty(IslandPropertyName).GetString();
return !string.IsNullOrWhiteSpace(systemTextJson) ? JsonArray.Parse(systemTextJson) : new JsonArray();
return !string.IsNullOrWhiteSpace(systemTextJson) ? JsonNode.Parse(systemTextJson)! : new JsonArray();
}
var isDictionary = typeof(IDictionary).IsAssignableFrom(targetType);

View file

@ -51,6 +51,6 @@ public class PolymorphicObjectConverterFactory : JsonConverterFactory
if (typeof(IDictionary<string, object>).IsAssignableFrom(typeToConvert))
return new PolymorphicDictionaryConverter(options, _wellKnownTypeRegistry);
return new PolymorphicObjectConverter(_wellKnownTypeRegistry);
return new PolymorphicObjectConverter();
}
}

View file

@ -33,7 +33,7 @@ public class WorkflowInstanceStorageDriver(IPayloadSerializer payloadSerializer,
try
{
var node = JsonSerializer.SerializeToNode(value);
dictionary[id] = node;
dictionary[id] = node!;
}
catch (Exception ex) when (ex is JsonException or NotSupportedException or ObjectDisposedException)
{

View file

@ -125,6 +125,13 @@ public class WorkflowDefinitionActivity : Composite, IInitializable
var serviceProvider = context.GetRequiredService<IServiceProvider>();
var activityDescriptor = await FindActivityDescriptorAsync(serviceProvider);
if (activityDescriptor == null)
{
var logger = serviceProvider.GetRequiredService<ILogger<WorkflowDefinitionActivity>>();
logger.LogWarning("Could not find activity descriptor for activity type {ActivityType}", Type);
return;
}
DeclareInputAsVariables(activityDescriptor, (descriptor, variable) =>
{
var inputName = descriptor.Name;

View file

@ -81,7 +81,7 @@ public class VariableDefinitionMapper(IWellKnownTypeRegistry wellKnownTypeRegist
var isArray = valueType.IsArray;
var isCollection = valueType.IsCollectionType();
var elementValueType = isArray ? valueType.GetElementType() : isCollection ? valueType.GenericTypeArguments[0] : valueType;
var elementValueType = isArray ? valueType.GetElementType()! : isCollection ? valueType.GenericTypeArguments[0] : valueType;
var elementTypeAlias = wellKnownTypeRegistry.GetAliasOrDefault(elementValueType);
return new(source.Id, source.Name, elementTypeAlias, isArray, serializedValue, storageDriverTypeName);

View file

@ -11,20 +11,20 @@ public interface IWorkflowDispatcher
/// <summary>
/// Dispatches a request to execute the specified workflow definition.
/// </summary>
Task<DispatchWorkflowResponse> DispatchAsync(DispatchWorkflowDefinitionRequest request, DispatchWorkflowOptions options, CancellationToken cancellationToken = default);
Task<DispatchWorkflowResponse> DispatchAsync(DispatchWorkflowDefinitionRequest request, DispatchWorkflowOptions? options, CancellationToken cancellationToken = default);
/// <summary>
/// Dispatches a request to execute the specified workflow instance.
/// </summary>
Task<DispatchWorkflowResponse> DispatchAsync(DispatchWorkflowInstanceRequest request, DispatchWorkflowOptions options, CancellationToken cancellationToken = default);
Task<DispatchWorkflowResponse> DispatchAsync(DispatchWorkflowInstanceRequest request, DispatchWorkflowOptions? options, CancellationToken cancellationToken = default);
/// <summary>
/// Starts all workflows and resumes existing workflow instances based on the specified activity type and bookmark payload.
/// </summary>
Task<DispatchWorkflowResponse> DispatchAsync(DispatchTriggerWorkflowsRequest request, DispatchWorkflowOptions options, CancellationToken cancellationToken = default);
Task<DispatchWorkflowResponse> DispatchAsync(DispatchTriggerWorkflowsRequest request, DispatchWorkflowOptions? options, CancellationToken cancellationToken = default);
/// <summary>
/// Resumes the workflow waiting for the specified bookmark.
/// </summary>
Task<DispatchWorkflowResponse> DispatchAsync(DispatchResumeWorkflowsRequest request, DispatchWorkflowOptions options, CancellationToken cancellationToken = default);
Task<DispatchWorkflowResponse> DispatchAsync(DispatchResumeWorkflowsRequest request, DispatchWorkflowOptions? options, CancellationToken cancellationToken = default);
}

View file

@ -15,11 +15,6 @@ public interface IWorkflowExecutionLogStore : ILogRecordStore<WorkflowExecutionL
/// </summary>
Task AddAsync(WorkflowExecutionLogRecord record, CancellationToken cancellationToken = default);
/// <summary>
/// Adds the specified set of <see cref="WorkflowExecutionLogRecord"/> objects to te persistence store.
/// </summary>
Task AddManyAsync(IEnumerable<WorkflowExecutionLogRecord> records, CancellationToken cancellationToken = default);
/// <summary>
/// Adds or updates the specified <see cref="WorkflowExecutionLogRecord"/> in the persistence store.
/// </summary>

View file

@ -17,7 +17,6 @@ using Elsa.Workflows.Runtime.Entities;
using Elsa.Workflows.Runtime.Handlers;
using Elsa.Workflows.Runtime.Options;
using Elsa.Workflows.Runtime.Providers;
using Elsa.Workflows.Runtime.Services;
using Elsa.Workflows.Runtime.Stores;
using Elsa.Workflows.Runtime.Tasks;
using Elsa.Workflows.Runtime.UIHints;

View file

@ -69,7 +69,7 @@ public class TriggerFilter
if (WorkflowDefinitionVersionId != null) queryable = queryable.Where(x => x.WorkflowDefinitionVersionId == WorkflowDefinitionVersionId);
if (WorkflowDefinitionVersionIds != null) queryable = queryable.Where(x => WorkflowDefinitionVersionIds.Contains(x.WorkflowDefinitionVersionId));
if (Name != null) queryable = queryable.Where(x => x.Name == Name);
if (Names != null) queryable = queryable.Where(x => Names.Contains(x.Name));
if (Names != null) queryable = queryable.Where(x => Names.Contains(x.Name!));
if (Hash != null) queryable = queryable.Where(x => x.Hash == Hash);
return queryable;
}

View file

@ -4,7 +4,6 @@ using Elsa.Workflows.Notifications;
using Elsa.Workflows.Runtime.Activities;
using Elsa.Workflows.Runtime.Stimuli;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
namespace Elsa.Workflows.Runtime.Handlers;
@ -12,7 +11,7 @@ namespace Elsa.Workflows.Runtime.Handlers;
/// Resumes any blocking <see cref="DispatchWorkflow"/> activities when its child workflow completes.
/// </summary>
[PublicAPI]
internal class ResumeDispatchWorkflowActivity(IBookmarkQueue bookmarkQueue, IStimulusHasher stimulusHasher, ILogger<ResumeDispatchWorkflowActivity> logger) : INotificationHandler<WorkflowExecuted>
internal class ResumeDispatchWorkflowActivity(IBookmarkQueue bookmarkQueue, IStimulusHasher stimulusHasher) : INotificationHandler<WorkflowExecuted>
{
private static readonly string ActivityTypeName = ActivityTypeNameHelper.GenerateTypeName<DispatchWorkflow>();

View file

@ -67,7 +67,7 @@ public class ObsoleteWorkflowRuntime(
TriggerActivityId = options?.TriggerActivityId
};
var response = await client.CreateAndRunInstanceAsync(createRequest, cancellationToken);
return new(response.WorkflowInstanceId, response.Status, response.SubStatus, response.Bookmarks, response.Incidents, null, null);
return new(response.WorkflowInstanceId, response.Status, response.SubStatus, response.Bookmarks, response.Incidents, null, new Dictionary<string, object>());
}
public async Task<ICollection<WorkflowExecutionResult>> StartWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null)
@ -82,7 +82,7 @@ public class ObsoleteWorkflowRuntime(
Input = options?.Input
};
var result = await stimulusSender.SendAsync(activityTypeName, bookmarkPayload, metadata, cancellationToken);
var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents, null, null)).ToList();
var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents, null, new Dictionary<string, object>())).ToList();
return results;
}
@ -110,7 +110,7 @@ public class ObsoleteWorkflowRuntime(
var response = await workflowClient.RunInstanceAsync(runWorkflowRequest, cancellationToken);
return new(response.WorkflowInstanceId, response.Status, response.SubStatus, response.Bookmarks, response.Incidents,null, null);
return new(response.WorkflowInstanceId, response.Status, response.SubStatus, response.Bookmarks, response.Incidents,null, new Dictionary<string, object>());
}
public async Task<ICollection<WorkflowExecutionResult>> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null)
@ -125,7 +125,7 @@ public class ObsoleteWorkflowRuntime(
Input = options?.Input
};
var result = await stimulusSender.SendAsync(activityTypeName, bookmarkPayload, metadata, cancellationToken);
var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents, null, null)).ToList();
var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents, null, new Dictionary<string, object>())).ToList();
return results;
}
@ -141,7 +141,7 @@ public class ObsoleteWorkflowRuntime(
Input = options?.Input
};
var result = await stimulusSender.SendAsync(activityTypeName, bookmarkPayload, metadata, cancellationToken);
var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents, null, null)).ToList();
var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents, null, new Dictionary<string, object>())).ToList();
return new(results);
}

View file

@ -182,7 +182,7 @@ public class StimulusProxyWorkflowInbox(
new List<Bookmark>(),
response.Incidents,
null,
null)
new Dictionary<string, object>())
);
}
}

View file

@ -3,14 +3,13 @@ using Elsa.Mediator.Contracts;
using Elsa.Workflows.Runtime.Entities;
using Elsa.Workflows.Runtime.Notifications;
namespace Elsa.Workflows.Runtime.Services;
namespace Elsa.Workflows.Runtime;
/// <summary>
/// This implementation saves <see cref="ActivityExecutionRecord"/> directly through the store.
/// </summary>
public class StoreActivityExecutionLogSink(
IActivityExecutionStore activityExecutionStore,
IActivityExecutionMapper mapper,
INotificationSender notificationSender)
: ILogRecordSink<ActivityExecutionRecord>
{

View file

@ -15,7 +15,7 @@ public class ValidatingWorkflowDispatcher(IWorkflowDispatcher decoratedService,
private IWorkflowDispatcher DecoratedService { get; set; } = decoratedService;
/// <inheritdoc />
public async Task<DispatchWorkflowResponse> DispatchAsync(DispatchWorkflowDefinitionRequest request, DispatchWorkflowOptions? options = default, CancellationToken cancellationToken = default)
public async Task<DispatchWorkflowResponse> DispatchAsync(DispatchWorkflowDefinitionRequest request, DispatchWorkflowOptions? options = null, CancellationToken cancellationToken = default)
{
if (!ValidateChannel(options?.Channel))
return DispatchWorkflowResponse.UnknownChannel();
@ -24,7 +24,7 @@ public class ValidatingWorkflowDispatcher(IWorkflowDispatcher decoratedService,
}
/// <inheritdoc />
public async Task<DispatchWorkflowResponse> DispatchAsync(DispatchWorkflowInstanceRequest request, DispatchWorkflowOptions? options = default, CancellationToken cancellationToken = default)
public async Task<DispatchWorkflowResponse> DispatchAsync(DispatchWorkflowInstanceRequest request, DispatchWorkflowOptions? options = null, CancellationToken cancellationToken = default)
{
if (!ValidateChannel(options?.Channel))
return DispatchWorkflowResponse.UnknownChannel();
@ -33,7 +33,7 @@ public class ValidatingWorkflowDispatcher(IWorkflowDispatcher decoratedService,
}
/// <inheritdoc />
public async Task<DispatchWorkflowResponse> DispatchAsync(DispatchTriggerWorkflowsRequest request, DispatchWorkflowOptions? options = default, CancellationToken cancellationToken = default)
public async Task<DispatchWorkflowResponse> DispatchAsync(DispatchTriggerWorkflowsRequest request, DispatchWorkflowOptions? options = null, CancellationToken cancellationToken = default)
{
if (!ValidateChannel(options?.Channel))
return DispatchWorkflowResponse.UnknownChannel();
@ -42,7 +42,7 @@ public class ValidatingWorkflowDispatcher(IWorkflowDispatcher decoratedService,
}
/// <inheritdoc />
public async Task<DispatchWorkflowResponse> DispatchAsync(DispatchResumeWorkflowsRequest request, DispatchWorkflowOptions? options = default, CancellationToken cancellationToken = default)
public async Task<DispatchWorkflowResponse> DispatchAsync(DispatchResumeWorkflowsRequest request, DispatchWorkflowOptions? options = null, CancellationToken cancellationToken = default)
{
if (!ValidateChannel(options?.Channel))
return DispatchWorkflowResponse.UnknownChannel();

View file

@ -45,7 +45,7 @@ public class TriggerSignal : CodeActivity
/// <inheritdoc />
public TriggerSignal(Input<string> eventName, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : this(source, line) => EventName = eventName;
public Input<string> EventName { get; set; }
public Input<string> EventName { get; set; } = null!;
protected override void Execute(ActivityExecutionContext context)
{

View file

@ -127,7 +127,7 @@ public class BulkDispatchWorkflowsTests : AppComponentTest
Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowExecutionContext.SubStatus);
}
private async Task<T> GetWorkflowVariableAsync<T>(TestWorkflowExecutionResult result, string variableName)
private async Task<T?> GetWorkflowVariableAsync<T>(TestWorkflowExecutionResult result, string variableName)
{
var variableManager = Scope.ServiceProvider.GetRequiredService<IWorkflowInstanceVariableManager>();
var variables = await variableManager.GetVariablesAsync(result.WorkflowExecutionContext);

View file

@ -188,7 +188,7 @@ public class FlowchartNextActivityTests
var result = await _workflowRunner.RunAsync(workflow);
var lines = _capturingTextWriter.Lines.ToList();
Assert.Equal(WorkflowSubStatus.Faulted, result.WorkflowState.SubStatus);
Assert.Equal(1, result.WorkflowState.Incidents.Count());
Assert.Single(result.WorkflowState.Incidents);
Assert.Equal("Invalid backward connection: Every path from the source ('WriteLineE') must go through the target ('WriteLineC') when tracing back to the start.", result.WorkflowState.Incidents.First().Message);
Assert.Equal(new[]
{

View file

@ -30,6 +30,6 @@ public class IncidentStrategyTests
var lines = _capturingTextWriter.Lines.ToList();
Assert.Equal(expectedOutput, lines);
Assert.Equal(expectedSubStatus, workflowState.SubStatus);
Assert.Equal(1, workflowState.Incidents.Count);
Assert.Single(workflowState.Incidents);
}
}

View file

@ -24,8 +24,8 @@ public class Tests
public async Task Test1()
{
await _services.PopulateRegistriesAsync();
var workflowDefinition = _publisher.New();
var root = _serializer.Deserialize(workflowDefinition.StringData);
var workflowDefinition = await _publisher.NewAsync();
var root = _serializer.Deserialize(workflowDefinition.StringData!);
Assert.NotNull(root);
}
}

View file

@ -79,8 +79,8 @@ public class Tests
new(start, writeLine),
new(writeLine, end),
},
RunAsynchronously = false
};
container.RunAsynchronously = false;
// Act
@ -93,12 +93,11 @@ public class Tests
}
[Fact]
public async void SerializeSequenceContainerTest()
public async Task SerializeSequenceContainerTest()
{
await _services.PopulateRegistriesAsync();
// Arrange
var container = new Sequence
{
Id = "sequence",
@ -127,9 +126,9 @@ public class Tests
{ "int", 10 },
{ "bool", false },
{ "string", "str"},
}
},
RunAsynchronously = false
};
container.RunAsynchronously = false;
// Act
@ -142,12 +141,11 @@ public class Tests
}
[Fact]
public async void SerializeParallelContainerTest()
public async Task SerializeParallelContainerTest()
{
await _services.PopulateRegistriesAsync();
// Arrange
var container = new Workflows.Activities.Parallel
{
Id = "parallel",
@ -177,9 +175,9 @@ public class Tests
{ "int", 10 },
{ "bool", false },
{ "string", "str"},
}
},
RunAsynchronously = false
};
container.RunAsynchronously = false;
// Act
@ -199,10 +197,16 @@ public class Tests
// Assert.Equivalent has trouble with the Behavior.Owner reference - since these aren't serialzied anyway, ignore them
deserializedContainer.Behaviors.Clear();
container.Behaviors.Clear();
foreach (Activity activity in deserializedContainer.Activities)
foreach (var activity1 in deserializedContainer.Activities)
{
var activity = (Activity)activity1;
activity.Behaviors.Clear();
foreach (Activity activity in container.Activities)
}
foreach (var activity in container.Activities.Cast<Activity>())
{
activity.Behaviors.Clear();
}
// strict:false here allows "actual" to have extra public members that aren't part of "expected", and collection
// comparison allows "actual" to have more data in it than is present in "expected".

View file

@ -158,7 +158,7 @@ public class SerializationTests(ITestOutputHelper testOutputHelper)
"StatusCode", "Created"
},
{
"Content", isArray ? (type == typeof(JArray) ? JArray.Parse(jsonContent) : JsonArray.Parse(jsonContent)) : (type == typeof(JObject) ? JObject.Parse(jsonContent) : JsonObject.Parse(jsonContent))
"Content", (isArray ? type == typeof(JArray) ? JArray.Parse(jsonContent) : JsonNode.Parse(jsonContent) : type == typeof(JObject) ? JObject.Parse(jsonContent) : JsonNode.Parse(jsonContent))!
}
};
return dict;

View file

@ -45,11 +45,11 @@ public class Tests
var description = await activityDescriber.DescribeActivityAsync(typeof(TestActivity));
var inputDescription = description.Inputs.First();
Assert.True(inputDescription.UISpecifications.ContainsKey(InputUIHints.DropDown));
Assert.True(inputDescription.UISpecifications!.ContainsKey(InputUIHints.DropDown));
Assert.True(inputDescription.UISpecifications[InputUIHints.DropDown] is DropDownProps);
var dropDownProperties = (DropDownProps) inputDescription.UISpecifications[InputUIHints.DropDown];
Assert.Collection(dropDownProperties.SelectList.Items,
Assert.Collection(dropDownProperties.SelectList!.Items,
item => { Assert.Equal("OptionsAreNice", item.Text); Assert.Equal("OptionsAreNice", item.Value); },
item => { Assert.Equal("ToHave", item.Text); Assert.Equal("ToHave", item.Value); },
item => { Assert.Equal("IfYouCanChooseThem", item.Text); Assert.Equal("IfYouCanChooseThem", item.Value); });

View file

@ -8,9 +8,9 @@ namespace Elsa.Workflows.PerformanceTests;
[Config(typeof(Config))]
public class ConsoleActivitiesBenchmark
{
private WriteLine _writeLineWorkflow;
private IWorkflowRunner _workflowRunner;
private ServiceProvider _serviceProvider;
private WriteLine _writeLineWorkflow = null!;
private IWorkflowRunner _workflowRunner = null!;
private ServiceProvider _serviceProvider = null!;
[GlobalSetup]
public void GlobalSetup()
@ -21,7 +21,7 @@ public class ConsoleActivitiesBenchmark
_serviceProvider = services.BuildServiceProvider();
_workflowRunner = _serviceProvider.GetRequiredService<IWorkflowRunner>();
_writeLineWorkflow = new WriteLine("Hello, World!");
_writeLineWorkflow = new("Hello, World!");
}
[Benchmark]

View file

@ -50,7 +50,7 @@ public class ExecuteWorkflowTests
opts.CorrelationId == DefaultCorrelationId &&
opts.Input != null && opts.Input.ContainsKey("Key1") && (string)opts.Input["Key1"] == "Value1" &&
opts.Input.ContainsKey("ParentInstanceId") && (string)opts.Input["ParentInstanceId"] == parentInstanceId &&
opts.Properties.ContainsKey("ParentInstanceId") && (string)opts.Properties["ParentInstanceId"] == parentInstanceId &&
opts.Properties!.ContainsKey("ParentInstanceId") && (string)opts.Properties["ParentInstanceId"] == parentInstanceId &&
(waitForCompletion ? opts.Properties.ContainsKey("WaitForCompletion") && (bool)opts.Properties["WaitForCompletion"] : !opts.Properties.ContainsKey("WaitForCompletion"))
),
Arg.Any<CancellationToken>()
@ -226,7 +226,7 @@ public class ExecuteWorkflowTests
{
Status = status,
SubStatus = subStatus,
Output = output
Output = output ?? new Dictionary<string, object>()
};
var workflowResult = new RunWorkflowResult(null!, workflowState, workflow, null, Journal.Empty);