diff --git a/src/ModularPipelines/Engine/Execution/ModuleResultFactory.cs b/src/ModularPipelines/Engine/Execution/ModuleResultFactory.cs index b4c9b9b84e..edb515aa76 100644 --- a/src/ModularPipelines/Engine/Execution/ModuleResultFactory.cs +++ b/src/ModularPipelines/Engine/Execution/ModuleResultFactory.cs @@ -17,7 +17,7 @@ public static ModuleResult CreateSuccess(T value, ModuleExecutionContext c } /// - /// Creates a failure ModuleResult for the specified exception. + /// Creates a failure ModuleResult for the specified exception (generic version). /// public static ModuleResult CreateFailure(Exception exception, ModuleExecutionContext ctx) { @@ -25,13 +25,37 @@ public static ModuleResult CreateFailure(Exception exception, ModuleExecut } /// - /// Creates a skipped ModuleResult for the specified skip decision. + /// Creates a failure ModuleResult for the specified exception (non-generic version). + /// + /// + /// This returns the non-generic type, which can be + /// implicitly converted to for any T. + /// + public static ModuleResult.Failure CreateFailure(Exception exception, ModuleExecutionContext ctx) + { + return ModuleResult.CreateFailure(exception, ctx); + } + + /// + /// Creates a skipped ModuleResult for the specified skip decision (generic version). /// public static ModuleResult CreateSkipped(SkipDecision decision, ModuleExecutionContext ctx) { return ModuleResult.CreateSkipped(decision, ctx); } + /// + /// Creates a skipped ModuleResult for the specified skip decision (non-generic version). + /// + /// + /// This returns the non-generic type, which can be + /// implicitly converted to for any T. + /// + public static ModuleResult.Skipped CreateSkipped(SkipDecision decision, ModuleExecutionContext ctx) + { + return ModuleResult.CreateSkipped(decision, ctx); + } + /// /// Creates a skipped ModuleResult (type-erased version for engine use). /// @@ -71,6 +95,23 @@ private static IModuleResult CreateFailureGeneric(Exception exception, Module /// public static IModuleResult WithStatus(IModuleResult result, Status status) { + // Handle non-generic Failure/Skipped types directly (most efficient path) + if (result is ModuleResult.Failure failure) + { + return failure with { ModuleStatus = status }; + } + + if (result is ModuleResult.Skipped skipped) + { + return skipped with { ModuleStatus = status }; + } + + // For generic types (Success, FailureWrapper, SkippedWrapper), we need to use + // reflection to call WithStatusGeneric which handles the 'with' expression. + // Wrapper types (FailureWrapper/SkippedWrapper) implement IFailureResult/ISkippedResult + // but require the generic path to maintain proper type safety. The wrapper's _inner + // field is private and all metadata is copied to the wrapper's own properties, so + // using 'with' on the wrapper correctly updates the observable ModuleStatus. var resultType = result.GetType(); // Get the generic type argument from ModuleResult diff --git a/src/ModularPipelines/Models/ModuleResult.cs b/src/ModularPipelines/Models/ModuleResult.cs index 2c6c07105f..2472178126 100644 --- a/src/ModularPipelines/Models/ModuleResult.cs +++ b/src/ModularPipelines/Models/ModuleResult.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; using ModularPipelines.Engine; @@ -6,29 +7,23 @@ namespace ModularPipelines.Models; /// -/// Represents the result of a module execution as a discriminated union. -/// Use pattern matching to handle Success, Failure, and Skipped cases. +/// Non-generic base class for module execution results. +/// Provides common metadata and result types that don't require a type parameter. /// -/// The type of value returned on success. -/// -/// -/// var result = await myModule; -/// switch (result) -/// { -/// case ModuleResult<string>.Success { Value: var value }: -/// Console.WriteLine($"Got: {value}"); -/// break; -/// case ModuleResult<string>.Failure { Exception: var ex }: -/// Console.WriteLine($"Failed: {ex.Message}"); -/// break; -/// case ModuleResult<string>.Skipped { Decision: var skip }: -/// Console.WriteLine($"Skipped: {skip.Reason}"); -/// break; -/// } -/// -/// +/// +/// +/// This base class exists because and results +/// don't carry a typed value - only needs the type parameter. +/// By separating the non-generic parts, we avoid creating unnecessary generic type instantiations +/// for failure and skipped cases. +/// +/// +/// Use when you need type-safe access to success values. +/// Use when working with results generically (e.g., logging, reporting). +/// +/// [JsonConverter(typeof(ModuleResultJsonConverterFactory))] -public abstract record ModuleResult : IModuleResult +public abstract record ModuleResult : IModuleResult { // === Metadata (available on all outcomes) === @@ -56,35 +51,54 @@ public abstract record ModuleResult : IModuleResult /// [JsonIgnore] - public bool IsSuccess => this is Success; + public bool IsSuccess => this is ISuccessResult; /// [JsonIgnore] - public bool IsFailure => this is Failure; + public bool IsFailure => this is Failure or IFailureResult; /// [JsonIgnore] - public bool IsSkipped => this is Skipped; + public bool IsSkipped => this is Skipped or ISkippedResult; // === Safe accessors (no exceptions) === + /// + [JsonIgnore] + object? IModuleResult.ValueOrDefault => GetValueOrDefault(); + /// - /// Gets the value if successful, or default(T) otherwise. Does not throw. + /// Gets the value if successful, or null otherwise. Override in derived classes. /// - [JsonIgnore] - public T? ValueOrDefault => this is Success s ? s.Value : default; + protected abstract object? GetValueOrDefault(); /// [JsonIgnore] - object? IModuleResult.ValueOrDefault => ValueOrDefault; + public Exception? ExceptionOrDefault => this switch + { + Failure f => f.Exception, + IFailureResult => GetExceptionFromWrapper(), + _ => null + }; /// [JsonIgnore] - public Exception? ExceptionOrDefault => this is Failure f ? f.Exception : null; + public SkipDecision? SkipDecisionOrDefault => this switch + { + Skipped sk => sk.Decision, + ISkippedResult => GetSkipDecisionFromWrapper(), + _ => null + }; - /// - [JsonIgnore] - public SkipDecision? SkipDecisionOrDefault => this is Skipped sk ? sk.Decision : null; + /// + /// Gets the exception from a failure wrapper. Override in derived classes. + /// + protected virtual Exception? GetExceptionFromWrapper() => null; + + /// + /// Gets the skip decision from a skipped wrapper. Override in derived classes. + /// + protected virtual SkipDecision? GetSkipDecisionFromWrapper() => null; // === Computed for compatibility === @@ -92,9 +106,11 @@ public abstract record ModuleResult : IModuleResult [JsonIgnore] public ModuleResultType ModuleResultType => this switch { - Success => ModuleResultType.Success, + ISuccessResult => ModuleResultType.Success, Failure => ModuleResultType.Failure, Skipped => ModuleResultType.Skipped, + IFailureResult => ModuleResultType.Failure, + ISkippedResult => ModuleResultType.Skipped, _ => throw new InvalidOperationException("Unknown result type") }; @@ -106,6 +122,155 @@ public abstract record ModuleResult : IModuleResult [JsonIgnore] internal Type? ModuleType { get; init; } + // === Non-generic discriminated variants === + + /// + /// Represents a failed module execution with an exception. + /// + /// + /// This type is non-generic because failure results don't carry a typed value. + /// It can be implicitly converted to for any T. + /// + /// The exception that caused the failure. + public sealed record Failure(Exception Exception) : ModuleResult + { + /// + protected override object? GetValueOrDefault() => null; + } + + /// + /// Represents a skipped module execution. + /// + /// + /// This type is non-generic because skipped results don't carry a typed value. + /// It can be implicitly converted to for any T. + /// + /// The skip decision containing the reason. + public sealed record Skipped(SkipDecision Decision) : ModuleResult + { + /// + protected override object? GetValueOrDefault() => null; + } + + // === Internal factory methods for non-generic results === + + internal static Failure CreateFailure(Exception exception, ModuleExecutionContext ctx) + { + var (start, end, duration) = GetTimingInfo(ctx); + return new(exception) + { + ModuleName = ctx.ModuleType.Name, + ModuleDuration = duration, + ModuleStart = start, + ModuleEnd = end, + ModuleStatus = ctx.Status, + ModuleType = ctx.ModuleType + }; + } + + internal static Skipped CreateSkipped(SkipDecision decision, ModuleExecutionContext ctx) + { + var (start, end, duration) = GetTimingInfo(ctx); + return new(decision) + { + ModuleName = ctx.ModuleType.Name, + ModuleDuration = duration, + ModuleStart = start, + ModuleEnd = end, + ModuleStatus = ctx.Status, + ModuleType = ctx.ModuleType + }; + } + + /// + /// Gets consistent timing information from the execution context. + /// If either start or end time is MinValue, returns TimeSpan.Zero for duration + /// to avoid inconsistent results from calling DateTimeOffset.Now multiple times. + /// + internal static (DateTimeOffset Start, DateTimeOffset End, TimeSpan Duration) GetTimingInfo(ModuleExecutionContext ctx) + { + var now = DateTimeOffset.Now; + var start = ctx.StartTime == DateTimeOffset.MinValue ? now : ctx.StartTime; + var end = ctx.EndTime == DateTimeOffset.MinValue ? now : ctx.EndTime; + + // If either time was originally MinValue, duration is unreliable - use Zero + var duration = (ctx.StartTime == DateTimeOffset.MinValue || ctx.EndTime == DateTimeOffset.MinValue) + ? TimeSpan.Zero + : end - start; + + return (start, end, duration); + } + + // Prevent external inheritance - only Success, Failure, Skipped are valid + private protected ModuleResult() + { + } +} + +/// +/// Marker interface for success results to enable pattern matching across generic types. +/// +internal interface ISuccessResult +{ +} + +/// +/// Marker interface for failure results to enable pattern matching across generic types. +/// +internal interface IFailureResult +{ +} + +/// +/// Marker interface for skipped results to enable pattern matching across generic types. +/// +internal interface ISkippedResult +{ +} + +/// +/// Represents the result of a module execution as a discriminated union. +/// Use pattern matching to handle Success, Failure, and Skipped cases. +/// +/// The type of value returned on success. +/// +/// +/// The type parameter T is only used by the variant. +/// and are non-generic +/// and shared across all ModuleResult<T> types. +/// +/// +/// +/// +/// var result = await myModule; +/// switch (result) +/// { +/// case ModuleResult<string>.Success { Value: var value }: +/// Console.WriteLine($"Got: {value}"); +/// break; +/// case ModuleResult.Failure { Exception: var ex }: +/// Console.WriteLine($"Failed: {ex.Message}"); +/// break; +/// case ModuleResult.Skipped { Decision: var skip }: +/// Console.WriteLine($"Skipped: {skip.Reason}"); +/// break; +/// } +/// +/// +[JsonConverter(typeof(ModuleResultJsonConverterFactory))] +public abstract record ModuleResult : ModuleResult +{ + // === Safe accessors (no exceptions) === + + /// + /// Gets the value if successful, or default(T) otherwise. Does not throw. + /// + [JsonIgnore] + public T? ValueOrDefault => this is Success s ? s.Value : default; + + /// + protected override object? GetValueOrDefault() => ValueOrDefault; + // === Pattern matching helpers === /// @@ -124,6 +289,8 @@ public TResult Match( Success s => onSuccess(s.Value), Failure f => onFailure(f.Exception), Skipped sk => onSkipped(sk.Decision), + FailureWrapper fw => onFailure(fw.Exception), + SkippedWrapper sw => onSkipped(sw.Decision), _ => throw new InvalidOperationException("Unknown result type") }; @@ -149,63 +316,99 @@ public void Switch( case Skipped sk: onSkipped(sk.Decision); break; + case FailureWrapper fw: + onFailure(fw.Exception); + break; + case SkippedWrapper sw: + onSkipped(sw.Decision); + break; } } - // === Discriminated variants === + // === Generic discriminated variant === /// /// Represents a successful module execution with a value. /// /// The value produced by the module. - public sealed record Success(T Value) : ModuleResult; + public sealed record Success(T Value) : ModuleResult, ISuccessResult; + + // === Implicit conversions from non-generic Failure/Skipped === /// - /// Represents a failed module execution with an exception. + /// Implicitly converts a non-generic to . /// - /// The exception that caused the failure. - public sealed record Failure(Exception Exception) : ModuleResult; + /// The failure result to convert. + public static implicit operator ModuleResult(Failure failure) => new FailureWrapper(failure); /// - /// Represents a skipped module execution. + /// Implicitly converts a non-generic to . /// - /// The skip decision containing the reason. - public sealed record Skipped(SkipDecision Decision) : ModuleResult; - - // === Internal factory methods === + /// The skipped result to convert. + public static implicit operator ModuleResult(Skipped skipped) => new SkippedWrapper(skipped); - internal static Success CreateSuccess(T value, ModuleExecutionContext ctx) + /// + /// Wrapper that allows non-generic Failure to be used as ModuleResult<T>. + /// + internal sealed record FailureWrapper : ModuleResult, IFailureResult { - var (start, end, duration) = GetTimingInfo(ctx); - return new(value) + private readonly Failure _inner; + + [SetsRequiredMembers] + internal FailureWrapper(Failure inner) { - ModuleName = ctx.ModuleType.Name, - ModuleDuration = duration, - ModuleStart = start, - ModuleEnd = end, - ModuleStatus = ctx.Status, - ModuleType = ctx.ModuleType - }; + _inner = inner; + ModuleName = inner.ModuleName; + ModuleDuration = inner.ModuleDuration; + ModuleStart = inner.ModuleStart; + ModuleEnd = inner.ModuleEnd; + ModuleStatus = inner.ModuleStatus; + ModuleType = inner.ModuleType; + } + + /// + /// Gets the wrapped exception. + /// + public Exception Exception => _inner.Exception; + + /// + protected override Exception? GetExceptionFromWrapper() => Exception; } - internal static Failure CreateFailure(Exception exception, ModuleExecutionContext ctx) + /// + /// Wrapper that allows non-generic Skipped to be used as ModuleResult<T>. + /// + internal sealed record SkippedWrapper : ModuleResult, ISkippedResult { - var (start, end, duration) = GetTimingInfo(ctx); - return new(exception) + private readonly Skipped _inner; + + [SetsRequiredMembers] + internal SkippedWrapper(Skipped inner) { - ModuleName = ctx.ModuleType.Name, - ModuleDuration = duration, - ModuleStart = start, - ModuleEnd = end, - ModuleStatus = ctx.Status, - ModuleType = ctx.ModuleType - }; + _inner = inner; + ModuleName = inner.ModuleName; + ModuleDuration = inner.ModuleDuration; + ModuleStart = inner.ModuleStart; + ModuleEnd = inner.ModuleEnd; + ModuleStatus = inner.ModuleStatus; + ModuleType = inner.ModuleType; + } + + /// + /// Gets the wrapped skip decision. + /// + public SkipDecision Decision => _inner.Decision; + + /// + protected override SkipDecision? GetSkipDecisionFromWrapper() => Decision; } - internal static Skipped CreateSkipped(SkipDecision decision, ModuleExecutionContext ctx) + // === Internal factory methods === + + internal static Success CreateSuccess(T value, ModuleExecutionContext ctx) { var (start, end, duration) = GetTimingInfo(ctx); - return new(decision) + return new(value) { ModuleName = ctx.ModuleType.Name, ModuleDuration = duration, @@ -216,26 +419,19 @@ internal static Skipped CreateSkipped(SkipDecision decision, ModuleExecutionCont }; } - /// - /// Gets consistent timing information from the execution context. - /// If either start or end time is MinValue, returns TimeSpan.Zero for duration - /// to avoid inconsistent results from calling DateTimeOffset.Now multiple times. - /// - private static (DateTimeOffset Start, DateTimeOffset End, TimeSpan Duration) GetTimingInfo(ModuleExecutionContext ctx) + internal new static FailureWrapper CreateFailure(Exception exception, ModuleExecutionContext ctx) { - var now = DateTimeOffset.Now; - var start = ctx.StartTime == DateTimeOffset.MinValue ? now : ctx.StartTime; - var end = ctx.EndTime == DateTimeOffset.MinValue ? now : ctx.EndTime; - - // If either time was originally MinValue, duration is unreliable - use Zero - var duration = (ctx.StartTime == DateTimeOffset.MinValue || ctx.EndTime == DateTimeOffset.MinValue) - ? TimeSpan.Zero - : end - start; + var failure = ModuleResult.CreateFailure(exception, ctx); + return new FailureWrapper(failure); + } - return (start, end, duration); + internal new static SkippedWrapper CreateSkipped(SkipDecision decision, ModuleExecutionContext ctx) + { + var skipped = ModuleResult.CreateSkipped(decision, ctx); + return new SkippedWrapper(skipped); } - // Prevent external inheritance - only Success, Failure, Skipped are valid + // Prevent external inheritance - only Success, FailureWrapper, SkippedWrapper are valid private protected ModuleResult() { } @@ -355,24 +551,209 @@ public override void Write(Utf8JsonWriter writer, Exception value, JsonSerialize } /// -/// JSON converter factory that creates typed converters for ModuleResult<T>. +/// JSON converter factory that creates typed converters for ModuleResult and ModuleResult<T>. /// internal sealed class ModuleResultJsonConverterFactory : JsonConverterFactory { public override bool CanConvert(Type typeToConvert) { - return typeToConvert.IsGenericType && - typeToConvert.GetGenericTypeDefinition() == typeof(ModuleResult<>); + // Handle non-generic ModuleResult and its subtypes + if (typeToConvert == typeof(ModuleResult) || + typeToConvert == typeof(ModuleResult.Failure) || + typeToConvert == typeof(ModuleResult.Skipped)) + { + return true; + } + + // Handle generic ModuleResult + if (typeToConvert.IsGenericType && + typeToConvert.GetGenericTypeDefinition() == typeof(ModuleResult<>)) + { + return true; + } + + // Handle nested types like ModuleResult.Success + var declaringType = typeToConvert.DeclaringType; + if (declaringType != null && + declaringType.IsGenericType && + declaringType.GetGenericTypeDefinition() == typeof(ModuleResult<>)) + { + return true; + } + + return false; } public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - var valueType = typeToConvert.GetGenericArguments()[0]; + // For non-generic types + if (typeToConvert == typeof(ModuleResult) || + typeToConvert == typeof(ModuleResult.Failure) || + typeToConvert == typeof(ModuleResult.Skipped)) + { + return new ModuleResultNonGenericJsonConverter(); + } + + // For generic types, get the type argument + Type valueType; + if (typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(ModuleResult<>)) + { + valueType = typeToConvert.GetGenericArguments()[0]; + } + else if (typeToConvert.DeclaringType?.IsGenericType == true) + { + valueType = typeToConvert.DeclaringType.GetGenericArguments()[0]; + } + else + { + return null; + } + var converterType = typeof(ModuleResultJsonConverter<>).MakeGenericType(valueType); return (JsonConverter?)Activator.CreateInstance(converterType); } } +/// +/// JSON converter for non-generic ModuleResult types (Failure, Skipped). +/// +internal sealed class ModuleResultNonGenericJsonConverter : JsonConverter +{ + private static readonly ExceptionJsonConverter ExceptionConverter = new(); + + public override ModuleResult? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + string? discriminator = null; + string? moduleName = null; + TimeSpan moduleDuration = TimeSpan.Zero; + DateTimeOffset moduleStart = DateTimeOffset.MinValue; + DateTimeOffset moduleEnd = DateTimeOffset.MinValue; + Status moduleStatus = Status.NotYetStarted; + Exception? exception = null; + SkipDecision? skipDecision = null; + + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("Expected StartObject token"); + } + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + if (reader.TokenType == JsonTokenType.PropertyName) + { + var propertyName = reader.GetString(); + reader.Read(); + + switch (propertyName) + { + case "$type": + discriminator = reader.GetString(); + break; + case "ModuleName": + moduleName = reader.GetString(); + break; + case "ModuleDuration": + moduleDuration = JsonSerializer.Deserialize(ref reader, options); + break; + case "ModuleStart": + moduleStart = reader.GetDateTimeOffset(); + break; + case "ModuleEnd": + moduleEnd = reader.GetDateTimeOffset(); + break; + case "ModuleStatus": + moduleStatus = JsonSerializer.Deserialize(ref reader, options); + break; + case "Exception": + exception = ExceptionConverter.Read(ref reader, typeof(Exception), options); + break; + case "Decision": + skipDecision = JsonSerializer.Deserialize(ref reader, options); + break; + } + } + } + + if (moduleName is null) + { + throw new JsonException("ModuleName is required but was not found in the JSON."); + } + + return discriminator switch + { + "Failure" => exception is not null + ? new ModuleResult.Failure(exception) + { + ModuleName = moduleName, + ModuleDuration = moduleDuration, + ModuleStart = moduleStart, + ModuleEnd = moduleEnd, + ModuleStatus = moduleStatus + } + : throw new JsonException("Failure result requires an Exception property in the JSON."), + "Skipped" => skipDecision is not null + ? new ModuleResult.Skipped(skipDecision) + { + ModuleName = moduleName, + ModuleDuration = moduleDuration, + ModuleStart = moduleStart, + ModuleEnd = moduleEnd, + ModuleStatus = moduleStatus + } + : throw new JsonException("Skipped result requires a Decision property in the JSON."), + _ => throw new JsonException($"Unknown or unsupported discriminator for non-generic ModuleResult: {discriminator}") + }; + } + + public override void Write(Utf8JsonWriter writer, ModuleResult value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + // Write discriminator + var discriminator = value switch + { + ModuleResult.Failure => "Failure", + ModuleResult.Skipped => "Skipped", + _ => throw new JsonException($"Cannot serialize non-generic ModuleResult of type {value.GetType()}") + }; + writer.WriteString("$type", discriminator); + + // Write common properties + writer.WriteString("ModuleName", value.ModuleName); + writer.WritePropertyName("ModuleDuration"); + JsonSerializer.Serialize(writer, value.ModuleDuration, options); + writer.WriteString("ModuleStart", value.ModuleStart); + writer.WriteString("ModuleEnd", value.ModuleEnd); + writer.WritePropertyName("ModuleStatus"); + JsonSerializer.Serialize(writer, value.ModuleStatus, options); + + // Write variant-specific properties + switch (value) + { + case ModuleResult.Failure failure: + writer.WritePropertyName("Exception"); + ExceptionConverter.Write(writer, failure.Exception, options); + break; + case ModuleResult.Skipped skipped: + writer.WritePropertyName("Decision"); + JsonSerializer.Serialize(writer, skipped.Decision, options); + break; + } + + writer.WriteEndObject(); + } +} + /// /// JSON converter for ModuleResult<T> that handles polymorphic serialization/deserialization. /// @@ -465,24 +846,24 @@ internal sealed class ModuleResultJsonConverter : JsonConverter exception is not null - ? new ModuleResult.Failure(exception) + ? new ModuleResult.FailureWrapper(new ModuleResult.Failure(exception) { ModuleName = moduleName, ModuleDuration = moduleDuration, ModuleStart = moduleStart, ModuleEnd = moduleEnd, ModuleStatus = moduleStatus - } + }) : throw new JsonException("Failure result requires an Exception property in the JSON."), "Skipped" => skipDecision is not null - ? new ModuleResult.Skipped(skipDecision) + ? new ModuleResult.SkippedWrapper(new ModuleResult.Skipped(skipDecision) { ModuleName = moduleName, ModuleDuration = moduleDuration, ModuleStart = moduleStart, ModuleEnd = moduleEnd, ModuleStatus = moduleStatus - } + }) : throw new JsonException("Skipped result requires a Decision property in the JSON."), _ => throw new JsonException($"Unknown discriminator: {discriminator}") }; @@ -496,8 +877,10 @@ public override void Write(Utf8JsonWriter writer, ModuleResult value, JsonSer var discriminator = value switch { ModuleResult.Success => "Success", - ModuleResult.Failure => "Failure", - ModuleResult.Skipped => "Skipped", + ModuleResult.FailureWrapper => "Failure", + ModuleResult.SkippedWrapper => "Skipped", + ModuleResult.Failure => "Failure", + ModuleResult.Skipped => "Skipped", _ => throw new JsonException("Unknown ModuleResult type") }; writer.WriteString("$type", discriminator); @@ -518,11 +901,19 @@ public override void Write(Utf8JsonWriter writer, ModuleResult value, JsonSer writer.WritePropertyName("Value"); JsonSerializer.Serialize(writer, success.Value, options); break; - case ModuleResult.Failure failure: + case ModuleResult.FailureWrapper failureWrapper: + writer.WritePropertyName("Exception"); + ExceptionConverter.Write(writer, failureWrapper.Exception, options); + break; + case ModuleResult.SkippedWrapper skippedWrapper: + writer.WritePropertyName("Decision"); + JsonSerializer.Serialize(writer, skippedWrapper.Decision, options); + break; + case ModuleResult.Failure failure: writer.WritePropertyName("Exception"); ExceptionConverter.Write(writer, failure.Exception, options); break; - case ModuleResult.Skipped skipped: + case ModuleResult.Skipped skipped: writer.WritePropertyName("Decision"); JsonSerializer.Serialize(writer, skipped.Decision, options); break; diff --git a/src/ModularPipelines/Models/PipelineSummary.cs b/src/ModularPipelines/Models/PipelineSummary.cs index f3b2d32ede..bd691d9907 100644 --- a/src/ModularPipelines/Models/PipelineSummary.cs +++ b/src/ModularPipelines/Models/PipelineSummary.cs @@ -129,7 +129,7 @@ public async Task> GetModuleResultsAsync() } // Fallback: create a cancellation result for modules that haven't completed - return ModuleResult.CreateFailure(new TaskCanceledException(), new ModuleExecutionContext(x, x.GetType())); + return ModuleResult.CreateFailure(new TaskCanceledException(), new ModuleExecutionContext(x, x.GetType())); }).ProcessInParallel(); }