diff --git a/Source/DotNET/Canonical/Expectations.cs b/Source/DotNET/Canonical/Expectations.cs index b460352..cbbf4d1 100644 --- a/Source/DotNET/Canonical/Expectations.cs +++ b/Source/DotNET/Canonical/Expectations.cs @@ -29,26 +29,28 @@ enum ExpectationKind sealed record Expectation(ExpectationKind Kind, string Value) { - public bool IsMetBy(GeneratedScreenplayDefinition result) => Kind switch + static readonly Dictionary> _checks = new() { - ExpectationKind.Source => result.Source.Contains(Value, StringComparison.Ordinal), - ExpectationKind.NotSource => !result.Source.Contains(Value, StringComparison.Ordinal), - ExpectationKind.SourceHash => IsSourceHash(result, Value), - ExpectationKind.Diagnostic => result.Diagnostics.Any(_ => _.Code == Value), - ExpectationKind.DiagnosticMessage => IsDiagnosticMessageIn(result, Value), - ExpectationKind.Artifact => result.Graph.Artifacts.Any(_ => $"{_.Key.Kind}:{_.Variants[0].Definition.Name}" == Value), - ExpectationKind.ArtifactCount => IsArtifactCountIn(result, Value), - ExpectationKind.Evidence => IsEvidenceIn(result, Value), - ExpectationKind.EvidenceCount => IsEvidenceCountIn(result, Value), - ExpectationKind.Relationship => IsRelationshipIn(result, Value), - ExpectationKind.RelationshipCount => IsRelationshipCountIn(result, Value), - ExpectationKind.Identifier => IsIdentifierIn(result, Value), - ExpectationKind.NotIdentifier => !IsIdentifierIn(result, Value), - ExpectationKind.Grouping => IsGroupingIn(result, Value), - ExpectationKind.FanOut => IsFanOutIn(result, Value), - _ => false + [ExpectationKind.Source] = (result, value) => result.Source.Contains(value, StringComparison.Ordinal), + [ExpectationKind.NotSource] = (result, value) => !result.Source.Contains(value, StringComparison.Ordinal), + [ExpectationKind.SourceHash] = IsSourceHash, + [ExpectationKind.Diagnostic] = (result, value) => result.Diagnostics.Any(_ => _.Code == value), + [ExpectationKind.DiagnosticMessage] = IsDiagnosticMessageIn, + [ExpectationKind.Artifact] = (result, value) => result.Graph.Artifacts.Any(_ => $"{_.Key.Kind}:{_.Variants[0].Definition.Name}" == value), + [ExpectationKind.ArtifactCount] = IsArtifactCountIn, + [ExpectationKind.Evidence] = IsEvidenceIn, + [ExpectationKind.EvidenceCount] = IsEvidenceCountIn, + [ExpectationKind.Relationship] = IsRelationshipIn, + [ExpectationKind.RelationshipCount] = IsRelationshipCountIn, + [ExpectationKind.Identifier] = IsIdentifierIn, + [ExpectationKind.NotIdentifier] = (result, value) => !IsIdentifierIn(result, value), + [ExpectationKind.Grouping] = IsGroupingIn, + [ExpectationKind.FanOut] = IsFanOutIn }; + public bool IsMetBy(GeneratedScreenplayDefinition result) => + _checks.TryGetValue(Kind, out var check) && check(result, Value); + public override string ToString() => $"{Kind}: {Value}"; static bool IsDiagnosticMessageIn(GeneratedScreenplayDefinition result, string value) @@ -180,20 +182,24 @@ static bool IsFanOutIn(GeneratedScreenplayDefinition result, string value) return false; } - var parent = result.Graph.Artifacts - .FirstOrDefault(_ => - _.Key.Kind == ArtifactKind.Event && - _.Variants.Any(variant => variant.Definition.Name == parts[0]))?.Key.Subject.Value; + var parent = EventSubject(result, parts[0]); return parent is not null && result.Graph.Relationships.Any(relationship => relationship.Key.Kind == RelationshipKind.Consumes && ArtifactName(result, relationship.Key.Source, ArtifactKind.Reducer) is not null && ArtifactName(result, relationship.Key.Target, ArtifactKind.Event) == parts[1] && - relationship.Definitions.Any(definition => - definition.Key.Discriminator?.StartsWith($"marten:fan-out-child:{parent}:{parts[3]}:", StringComparison.Ordinal) == true && - definition.SourceMember == parts[2] && - definition.IsCollection)); + relationship.Definitions.Any(definition => IsFanOutChild(definition, parent, parts[2], parts[3]))); } + static string? EventSubject(GeneratedScreenplayDefinition result, string name) => result.Graph.Artifacts + .FirstOrDefault(_ => + _.Key.Kind == ArtifactKind.Event && + _.Variants.Any(variant => variant.Definition.Name == name))?.Key.Subject.Value; + + static bool IsFanOutChild(RelationshipDefinition definition, string parent, string member, string mode) => + definition.Key.Discriminator?.StartsWith($"marten:fan-out-child:{parent}:{mode}:", StringComparison.Ordinal) == true && + definition.SourceMember == member && + definition.IsCollection; + static string? ArtifactName( GeneratedScreenplayDefinition result, SubjectId subject, @@ -206,6 +212,25 @@ static bool IsFanOutIn(GeneratedScreenplayDefinition result, string value) static class Expectations { + static readonly Dictionary _kinds = new() + { + ["source"] = ExpectationKind.Source, + ["not-source"] = ExpectationKind.NotSource, + ["source-sha256"] = ExpectationKind.SourceHash, + ["diagnostic"] = ExpectationKind.Diagnostic, + ["diagnostic-message"] = ExpectationKind.DiagnosticMessage, + ["artifact"] = ExpectationKind.Artifact, + ["artifact-count"] = ExpectationKind.ArtifactCount, + ["evidence"] = ExpectationKind.Evidence, + ["evidence-count"] = ExpectationKind.EvidenceCount, + ["relationship"] = ExpectationKind.Relationship, + ["relationship-count"] = ExpectationKind.RelationshipCount, + ["identifier"] = ExpectationKind.Identifier, + ["not-identifier"] = ExpectationKind.NotIdentifier, + ["grouping"] = ExpectationKind.Grouping, + ["fan-out"] = ExpectationKind.FanOut + }; + public static IReadOnlyList Read(string path) => [ .. File.ReadAllLines(path) @@ -222,25 +247,11 @@ static Expectation Parse(string value) throw new InvalidExpectation(value); } - var kind = value[..separator] switch + if (!_kinds.TryGetValue(value[..separator], out var kind)) { - "source" => ExpectationKind.Source, - "not-source" => ExpectationKind.NotSource, - "source-sha256" => ExpectationKind.SourceHash, - "diagnostic" => ExpectationKind.Diagnostic, - "diagnostic-message" => ExpectationKind.DiagnosticMessage, - "artifact" => ExpectationKind.Artifact, - "artifact-count" => ExpectationKind.ArtifactCount, - "evidence" => ExpectationKind.Evidence, - "evidence-count" => ExpectationKind.EvidenceCount, - "relationship" => ExpectationKind.Relationship, - "relationship-count" => ExpectationKind.RelationshipCount, - "identifier" => ExpectationKind.Identifier, - "not-identifier" => ExpectationKind.NotIdentifier, - "grouping" => ExpectationKind.Grouping, - "fan-out" => ExpectationKind.FanOut, - _ => throw new InvalidExpectation(value) - }; + throw new InvalidExpectation(value); + } + return new(kind, value[(separator + 1)..].Trim()); } } diff --git a/Source/DotNET/CritterStack.Specs/Golden/wolverine-returns-and-links.txt b/Source/DotNET/CritterStack.Specs/Golden/wolverine-returns-and-links.txt new file mode 100644 index 0000000..f06e00a --- /dev/null +++ b/Source/DotNET/CritterStack.Specs/Golden/wolverine-returns-and-links.txt @@ -0,0 +1,294 @@ +fact artifact Command ComputedAppend file=Variants/Links.cs properties=[slot,note,share] | id=wolverine:command:dotnet://Variants/Variants/Variants.ComputedAppend | subject=dotnet://Variants/Variants/Variants.ComputedAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:61:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:61:5 +fact relationship Produces dotnet://Variants/Variants/Variants.ComputedAppend -> dotnet://Variants/Variants/Variants.Happened discriminator=imperative | id=wolverine:produces:dotnet://Variants/Variants/Variants.ComputedAppend:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.ComputedAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:61:5 +fact artifact Command ConditionalAppend file=Variants/Links.cs properties=[id,note,share] | id=wolverine:command:dotnet://Variants/Variants/Variants.ConditionalAppend | subject=dotnet://Variants/Variants/Variants.ConditionalAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:35:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:35:5 +fact relationship Produces dotnet://Variants/Variants/Variants.ConditionalAppend -> dotnet://Variants/Variants/Variants.Happened discriminator=imperative | id=wolverine:produces:dotnet://Variants/Variants/Variants.ConditionalAppend:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.ConditionalAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:35:5 +fact artifact Command ConditionalComputed file=Variants/Links.cs properties=[slot,note,share] | id=wolverine:command:dotnet://Variants/Variants/Variants.ConditionalComputed | subject=dotnet://Variants/Variants/Variants.ConditionalComputed | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:67:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:67:5 +fact relationship Produces dotnet://Variants/Variants/Variants.ConditionalComputed -> dotnet://Variants/Variants/Variants.Happened discriminator=imperative | id=wolverine:produces:dotnet://Variants/Variants/Variants.ConditionalComputed:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.ConditionalComputed | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:67:5 +fact artifact Command EarlyExitAppend file=Variants/Links.cs properties=[id,note,skip] | id=wolverine:command:dotnet://Variants/Variants/Variants.EarlyExitAppend | subject=dotnet://Variants/Variants/Variants.EarlyExitAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:43:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:43:5 +fact relationship Produces dotnet://Variants/Variants/Variants.EarlyExitAppend -> dotnet://Variants/Variants/Variants.Happened discriminator=imperative | id=wolverine:produces:dotnet://Variants/Variants/Variants.EarlyExitAppend:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.EarlyExitAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:43:5 +fact artifact Command IndirectAppend file=Variants/Links.cs properties=[id,note] | id=wolverine:command:dotnet://Variants/Variants/Variants.IndirectAppend | subject=dotnet://Variants/Variants/Variants.IndirectAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:96:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:96:5 +fact relationship Produces dotnet://Variants/Variants/Variants.IndirectAppend -> dotnet://Variants/Variants/Variants.Happened discriminator=imperative | id=wolverine:produces:dotnet://Variants/Variants/Variants.IndirectAppend:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.IndirectAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:96:5 +fact artifact Command LambdaAppend file=Variants/Links.cs properties=[id,note] | id=wolverine:command:dotnet://Variants/Variants/Variants.LambdaAppend | subject=dotnet://Variants/Variants/Variants.LambdaAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:87:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:87:5 +fact relationship Produces dotnet://Variants/Variants/Variants.LambdaAppend -> dotnet://Variants/Variants/Variants.Happened discriminator=imperative | id=wolverine:produces:dotnet://Variants/Variants/Variants.LambdaAppend:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.LambdaAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:87:5 +fact artifact Command LoopAppend file=Variants/Links.cs properties=[id,note] | id=wolverine:command:dotnet://Variants/Variants/Variants.LoopAppend | subject=dotnet://Variants/Variants/Variants.LoopAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:129:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:129:5 +fact relationship Produces dotnet://Variants/Variants/Variants.LoopAppend -> dotnet://Variants/Variants/Variants.Happened discriminator=imperative | id=wolverine:produces:dotnet://Variants/Variants/Variants.LoopAppend:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.LoopAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:129:5 +fact artifact Command OtherParameterAppend file=Variants/Links.cs properties=[id,note] | id=wolverine:command:dotnet://Variants/Variants/Variants.OtherParameterAppend | subject=dotnet://Variants/Variants/Variants.OtherParameterAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:137:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:137:5 +fact relationship Produces dotnet://Variants/Variants/Variants.OtherParameterAppend -> dotnet://Variants/Variants/Variants.Happened discriminator=imperative | id=wolverine:produces:dotnet://Variants/Variants/Variants.OtherParameterAppend:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.OtherParameterAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:137:5 +fact artifact Command PatternMissing file=Variants/Patterns.cs properties=[] | id=wolverine:command:dotnet://Variants/Variants/Variants.PatternMissing | subject=dotnet://Variants/Variants/Variants.PatternMissing | Exact | Wolverine message handler with persistence effects | Variants/Patterns.cs:56:5 +fact artifact Event Another file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Another | subject=dotnet://Variants/Variants/Variants.Another | Exact | Wolverine message handler with persistence effects | Variants/Patterns.cs:56:5 +fact relationship Produces dotnet://Variants/Variants/Variants.PatternMissing -> dotnet://Variants/Variants/Variants.Another discriminator=imperative | id=wolverine:produces:dotnet://Variants/Variants/Variants.PatternMissing:dotnet://Variants/Variants/Variants.Another | subject=dotnet://Variants/Variants/Variants.PatternMissing | Exact | Wolverine message handler with persistence effects | Variants/Patterns.cs:56:5 +fact artifact Command PatternTimeoutAgree file=Variants/Patterns.cs properties=[] | id=wolverine:command:dotnet://Variants/Variants/Variants.PatternTimeoutAgree | subject=dotnet://Variants/Variants/Variants.PatternTimeoutAgree | Exact | Wolverine message handler with persistence effects | Variants/Patterns.cs:38:5 +fact artifact Event Another file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Another | subject=dotnet://Variants/Variants/Variants.Another | Exact | Wolverine message handler with persistence effects | Variants/Patterns.cs:38:5 +fact relationship Produces dotnet://Variants/Variants/Variants.PatternTimeoutAgree -> dotnet://Variants/Variants/Variants.Another discriminator=imperative | id=wolverine:produces:dotnet://Variants/Variants/Variants.PatternTimeoutAgree:dotnet://Variants/Variants/Variants.Another | subject=dotnet://Variants/Variants/Variants.PatternTimeoutAgree | Exact | Wolverine message handler with persistence effects | Variants/Patterns.cs:38:5 +fact artifact Command PatternTimeoutMessage file=Variants/Patterns.cs properties=[] | id=wolverine:command:dotnet://Variants/Variants/Variants.PatternTimeoutMessage | subject=dotnet://Variants/Variants/Variants.PatternTimeoutMessage | Exact | Wolverine message handler with persistence effects | Variants/Patterns.cs:32:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Patterns.cs:32:5 +fact relationship Produces dotnet://Variants/Variants/Variants.PatternTimeoutMessage -> dotnet://Variants/Variants/Variants.Happened discriminator=imperative | id=wolverine:produces:dotnet://Variants/Variants/Variants.PatternTimeoutMessage:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.PatternTimeoutMessage | Exact | Wolverine message handler with persistence effects | Variants/Patterns.cs:32:5 +fact artifact Command PatternTranslated file=Variants/Patterns.cs properties=[note] | id=wolverine:command:dotnet://Variants/Variants/Variants.PatternTranslated | subject=dotnet://Variants/Variants/Variants.PatternTranslated | Exact | Wolverine message handler with persistence effects | Variants/Patterns.cs:45:5 +fact artifact Event Third file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Third | subject=dotnet://Variants/Variants/Variants.Third | Exact | Wolverine message handler with persistence effects | Variants/Patterns.cs:45:5 +fact relationship Produces dotnet://Variants/Variants/Variants.PatternTranslated -> dotnet://Variants/Variants/Variants.Third discriminator=imperative | id=wolverine:produces:dotnet://Variants/Variants/Variants.PatternTranslated:dotnet://Variants/Variants/Variants.Third | subject=dotnet://Variants/Variants/Variants.PatternTranslated | Exact | Wolverine message handler with persistence effects | Variants/Patterns.cs:45:5 +fact artifact Command PlainAppend file=Variants/Links.cs properties=[id,note] | id=wolverine:command:dotnet://Variants/Variants/Variants.PlainAppend | subject=dotnet://Variants/Variants/Variants.PlainAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:23:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:23:5 +fact relationship Produces dotnet://Variants/Variants/Variants.PlainAppend -> dotnet://Variants/Variants/Variants.Happened discriminator=declarative | id=wolverine:produces:dotnet://Variants/Variants/Variants.PlainAppend:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.PlainAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:23:5 +fact artifact Command ReturnAggregateEvent file=Variants/Returns.cs properties=[id] | id=wolverine:command:dotnet://Variants/Variants/Variants.ReturnAggregateEvent | subject=dotnet://Variants/Variants/Variants.ReturnAggregateEvent | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:42:5 +fact artifact ReadModel State file=Variants/Events.cs properties=[id] | id=wolverine:read-model:dotnet://Variants/Variants/Variants.State | subject=dotnet://Variants/Variants/Variants.State | Conventional | Wolverine loads this model as aggregate decision state | Variants/Events.cs:8:1 +fact relationship Reads dotnet://Variants/Variants/Variants.ReturnAggregateEvent -> dotnet://Variants/Variants/Variants.State discriminator= | id=wolverine:reads:dotnet://Variants/Variants/Variants.ReturnAggregateEvent:dotnet://Variants/Variants/Variants.State | subject=dotnet://Variants/Variants/Variants.ReturnAggregateEvent | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:42:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:42:5 +fact relationship Produces dotnet://Variants/Variants/Variants.ReturnAggregateEvent -> dotnet://Variants/Variants/Variants.Happened discriminator=declarative | id=wolverine:produces:dotnet://Variants/Variants/Variants.ReturnAggregateEvent:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.ReturnAggregateEvent | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:42:5 +fact artifact Command ReturnAggregateOutgoing file=Variants/Returns.cs properties=[id] | id=wolverine:command:dotnet://Variants/Variants/Variants.ReturnAggregateOutgoing | subject=dotnet://Variants/Variants/Variants.ReturnAggregateOutgoing | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:48:5 +fact artifact ReadModel State file=Variants/Events.cs properties=[id] | id=wolverine:read-model:dotnet://Variants/Variants/Variants.State | subject=dotnet://Variants/Variants/Variants.State | Conventional | Wolverine loads this model as aggregate decision state | Variants/Events.cs:8:1 +fact relationship Reads dotnet://Variants/Variants/Variants.ReturnAggregateOutgoing -> dotnet://Variants/Variants/Variants.State discriminator= | id=wolverine:reads:dotnet://Variants/Variants/Variants.ReturnAggregateOutgoing:dotnet://Variants/Variants/Variants.State | subject=dotnet://Variants/Variants/Variants.ReturnAggregateOutgoing | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:48:5 +fact artifact Event Another file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Another | subject=dotnet://Variants/Variants/Variants.Another | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:48:5 +fact relationship Produces dotnet://Variants/Variants/Variants.ReturnAggregateOutgoing -> dotnet://Variants/Variants/Variants.Another discriminator=declarative | id=wolverine:produces:dotnet://Variants/Variants/Variants.ReturnAggregateOutgoing:dotnet://Variants/Variants/Variants.Another | subject=dotnet://Variants/Variants/Variants.ReturnAggregateOutgoing | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:48:5 +fact artifact Event Follow file=Variants/Events.cs properties=[text] | id=wolverine:event:dotnet://Variants/Variants/Variants.Follow | subject=dotnet://Variants/Variants/Variants.Follow | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:48:5 +fact relationship Produces dotnet://Variants/Variants/Variants.ReturnAggregateOutgoing -> dotnet://Variants/Variants/Variants.Follow discriminator=declarative | id=wolverine:produces:dotnet://Variants/Variants/Variants.ReturnAggregateOutgoing:dotnet://Variants/Variants/Variants.Follow | subject=dotnet://Variants/Variants/Variants.ReturnAggregateOutgoing | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:48:5 +fact artifact Message ReturnCascade file=Variants/Returns.cs properties=[text] | id=wolverine:message:dotnet://Variants/Variants/Variants.ReturnCascade | subject=dotnet://Variants/Variants/Variants.ReturnCascade | Exact | Wolverine message handler with direct bus or return automation consequences | Variants/Returns.cs:31:44 +fact artifact Reaction ReturnCascade file=Variants/Returns.cs properties=[] | id=wolverine:reaction:dotnet://Variants/Variants/Variants.ReturnCascadeHandler#method:M%3AVariants.ReturnCascadeHandler.Handle%28Variants.ReturnCascade%29:reaction | subject=dotnet://Variants/Variants/Variants.ReturnCascadeHandler#method:M%3AVariants.ReturnCascadeHandler.Handle%28Variants.ReturnCascade%29:reaction | Exact | Wolverine message handler with direct bus or return automation consequences | Variants/Returns.cs:31:44 +fact relationship Handles dotnet://Variants/Variants/Variants.ReturnCascadeHandler#method:M%3AVariants.ReturnCascadeHandler.Handle%28Variants.ReturnCascade%29:reaction -> dotnet://Variants/Variants/Variants.ReturnCascade discriminator= | id=wolverine:handles:dotnet://Variants/Variants/Variants.ReturnCascadeHandler#method:M%3AVariants.ReturnCascadeHandler.Handle%28Variants.ReturnCascade%29:reaction:dotnet://Variants/Variants/Variants.ReturnCascade | subject=dotnet://Variants/Variants/Variants.ReturnCascadeHandler#method:M%3AVariants.ReturnCascadeHandler.Handle%28Variants.ReturnCascade%29:reaction | Exact | Wolverine message handler with direct bus or return automation consequences | Variants/Returns.cs:31:44 +fact artifact Message Follow file=Variants/Events.cs properties=[text] | id=wolverine:message:dotnet://Variants/Variants/Variants.Follow | subject=dotnet://Variants/Variants/Variants.Follow | Exact | Wolverine message handler with direct bus or return automation consequences | Variants/Returns.cs:31:44 +fact relationship Cascades dotnet://Variants/Variants/Variants.ReturnCascadeHandler#method:M%3AVariants.ReturnCascadeHandler.Handle%28Variants.ReturnCascade%29:reaction -> dotnet://Variants/Variants/Variants.Follow discriminator=return-slot:0 | id=wolverine:cascades:return:dotnet://Variants/Variants/Variants.ReturnCascadeHandler#method:M%3AVariants.ReturnCascadeHandler.Handle%28Variants.ReturnCascade%29:reaction:0:dotnet://Variants/Variants/Variants.Follow | subject=dotnet://Variants/Variants/Variants.ReturnCascadeHandler#method:M%3AVariants.ReturnCascadeHandler.Handle%28Variants.ReturnCascade%29:reaction | Exact | Wolverine message handler with direct bus or return automation consequences | Variants/Returns.cs:31:44 +fact artifact Command ReturnCustomStore file=Variants/Returns.cs properties=[text] | id=wolverine:command:dotnet://Variants/Variants/Variants.ReturnCustomStore | subject=dotnet://Variants/Variants/Variants.ReturnCustomStore | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:27:48 +fact artifact ReadModel Document file=Variants/Events.cs properties=[id] | id=wolverine:read-model:dotnet://Variants/Variants/Variants.Document | subject=dotnet://Variants/Variants/Variants.Document | Exact | Wolverine persists this model through a returned storage action | Variants/Events.cs:9:1 +fact relationship Stores dotnet://Variants/Variants/Variants.ReturnCustomStore -> dotnet://Variants/Variants/Variants.Document discriminator=storage-action:0 | id=wolverine:stores:dotnet://Variants/Variants/Variants.ReturnCustomStore:dotnet://Variants/Variants/Variants.Document:0 | subject=dotnet://Variants/Variants/Variants.ReturnCustomStore | Exact | Wolverine applies this returned storage action to the persistence layer | Variants/Returns.cs:27:48 +fact artifact Command ReturnStore file=Variants/Returns.cs properties=[text] | id=wolverine:command:dotnet://Variants/Variants/Variants.ReturnStore | subject=dotnet://Variants/Variants/Variants.ReturnStore | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:25:42 +fact artifact ReadModel Document file=Variants/Events.cs properties=[id] | id=wolverine:read-model:dotnet://Variants/Variants/Variants.Document | subject=dotnet://Variants/Variants/Variants.Document | Exact | Wolverine persists this model through a returned storage action | Variants/Events.cs:9:1 +fact relationship Stores dotnet://Variants/Variants/Variants.ReturnStore -> dotnet://Variants/Variants/Variants.Document discriminator=storage-action:0 | id=wolverine:stores:dotnet://Variants/Variants/Variants.ReturnStore:dotnet://Variants/Variants/Variants.Document:0 | subject=dotnet://Variants/Variants/Variants.ReturnStore | Exact | Wolverine applies this returned storage action to the persistence layer | Variants/Returns.cs:25:42 +fact artifact Message ReturnTask file=Variants/Returns.cs properties=[text] | id=wolverine:message:dotnet://Variants/Variants/Variants.ReturnTask | subject=dotnet://Variants/Variants/Variants.ReturnTask | Exact | Wolverine message handler with direct bus or return automation consequences | Variants/Returns.cs:33:41 +fact artifact Reaction ReturnTask file=Variants/Returns.cs properties=[] | id=wolverine:reaction:dotnet://Variants/Variants/Variants.ReturnTaskHandler#method:M%3AVariants.ReturnTaskHandler.Handle%28Variants.ReturnTask%29:reaction | subject=dotnet://Variants/Variants/Variants.ReturnTaskHandler#method:M%3AVariants.ReturnTaskHandler.Handle%28Variants.ReturnTask%29:reaction | Exact | Wolverine message handler with direct bus or return automation consequences | Variants/Returns.cs:33:41 +fact relationship Handles dotnet://Variants/Variants/Variants.ReturnTaskHandler#method:M%3AVariants.ReturnTaskHandler.Handle%28Variants.ReturnTask%29:reaction -> dotnet://Variants/Variants/Variants.ReturnTask discriminator= | id=wolverine:handles:dotnet://Variants/Variants/Variants.ReturnTaskHandler#method:M%3AVariants.ReturnTaskHandler.Handle%28Variants.ReturnTask%29:reaction:dotnet://Variants/Variants/Variants.ReturnTask | subject=dotnet://Variants/Variants/Variants.ReturnTaskHandler#method:M%3AVariants.ReturnTaskHandler.Handle%28Variants.ReturnTask%29:reaction | Exact | Wolverine message handler with direct bus or return automation consequences | Variants/Returns.cs:33:41 +fact artifact Message Follow file=Variants/Events.cs properties=[text] | id=wolverine:message:dotnet://Variants/Variants/Variants.Follow | subject=dotnet://Variants/Variants/Variants.Follow | Exact | Wolverine message handler with direct bus or return automation consequences | Variants/Returns.cs:33:41 +fact relationship Cascades dotnet://Variants/Variants/Variants.ReturnTaskHandler#method:M%3AVariants.ReturnTaskHandler.Handle%28Variants.ReturnTask%29:reaction -> dotnet://Variants/Variants/Variants.Follow discriminator=return-slot:0 | id=wolverine:cascades:return:dotnet://Variants/Variants/Variants.ReturnTaskHandler#method:M%3AVariants.ReturnTaskHandler.Handle%28Variants.ReturnTask%29:reaction:0:dotnet://Variants/Variants/Variants.Follow | subject=dotnet://Variants/Variants/Variants.ReturnTaskHandler#method:M%3AVariants.ReturnTaskHandler.Handle%28Variants.ReturnTask%29:reaction | Exact | Wolverine message handler with direct bus or return automation consequences | Variants/Returns.cs:33:41 +fact artifact Message ReturnTimeout file=Variants/Returns.cs properties=[text] | id=wolverine:message:dotnet://Variants/Variants/Variants.ReturnTimeout | subject=dotnet://Variants/Variants/Variants.ReturnTimeout | Exact | Wolverine message handler with direct bus or return automation consequences | Variants/Returns.cs:30:44 +fact artifact Reaction ReturnTimeout file=Variants/Returns.cs properties=[] | id=wolverine:reaction:dotnet://Variants/Variants/Variants.ReturnTimeoutHandler#method:M%3AVariants.ReturnTimeoutHandler.Handle%28Variants.ReturnTimeout%29:reaction | subject=dotnet://Variants/Variants/Variants.ReturnTimeoutHandler#method:M%3AVariants.ReturnTimeoutHandler.Handle%28Variants.ReturnTimeout%29:reaction | Exact | Wolverine message handler with direct bus or return automation consequences | Variants/Returns.cs:30:44 +fact relationship Handles dotnet://Variants/Variants/Variants.ReturnTimeoutHandler#method:M%3AVariants.ReturnTimeoutHandler.Handle%28Variants.ReturnTimeout%29:reaction -> dotnet://Variants/Variants/Variants.ReturnTimeout discriminator= | id=wolverine:handles:dotnet://Variants/Variants/Variants.ReturnTimeoutHandler#method:M%3AVariants.ReturnTimeoutHandler.Handle%28Variants.ReturnTimeout%29:reaction:dotnet://Variants/Variants/Variants.ReturnTimeout | subject=dotnet://Variants/Variants/Variants.ReturnTimeoutHandler#method:M%3AVariants.ReturnTimeoutHandler.Handle%28Variants.ReturnTimeout%29:reaction | Exact | Wolverine message handler with direct bus or return automation consequences | Variants/Returns.cs:30:44 +fact artifact Message Expiry file=Variants/Events.cs properties=[] | id=wolverine:message:dotnet://Variants/Variants/Variants.Expiry | subject=dotnet://Variants/Variants/Variants.Expiry | Exact | Wolverine message handler with direct bus or return automation consequences | Variants/Returns.cs:30:44 +fact relationship Cascades dotnet://Variants/Variants/Variants.ReturnTimeoutHandler#method:M%3AVariants.ReturnTimeoutHandler.Handle%28Variants.ReturnTimeout%29:reaction -> dotnet://Variants/Variants/Variants.Expiry discriminator=return-slot:0 | id=wolverine:cascades:return:dotnet://Variants/Variants/Variants.ReturnTimeoutHandler#method:M%3AVariants.ReturnTimeoutHandler.Handle%28Variants.ReturnTimeout%29:reaction:0:dotnet://Variants/Variants/Variants.Expiry | subject=dotnet://Variants/Variants/Variants.ReturnTimeoutHandler#method:M%3AVariants.ReturnTimeoutHandler.Handle%28Variants.ReturnTimeout%29:reaction | Exact | Wolverine message handler with direct bus or return automation consequences | Variants/Returns.cs:30:44 +fact artifact Command ReturnTuple file=Variants/Returns.cs properties=[text] | id=wolverine:command:dotnet://Variants/Variants/Variants.ReturnTuple | subject=dotnet://Variants/Variants/Variants.ReturnTuple | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:37:5 +fact artifact Message Reply file=Variants/Events.cs properties=[text] | id=wolverine:message:dotnet://Variants/Variants/Variants.Reply | subject=dotnet://Variants/Variants/Variants.Reply | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:37:5 +fact relationship Cascades dotnet://Variants/Variants/Variants.ReturnTuple -> dotnet://Variants/Variants/Variants.Reply discriminator=return-slot:0 | id=wolverine:cascades:return:dotnet://Variants/Variants/Variants.ReturnTuple:0:dotnet://Variants/Variants/Variants.Reply | subject=dotnet://Variants/Variants/Variants.ReturnTuple | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:37:5 +fact artifact Message Follow file=Variants/Events.cs properties=[text] | id=wolverine:message:dotnet://Variants/Variants/Variants.Follow | subject=dotnet://Variants/Variants/Variants.Follow | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:37:5 +fact relationship Cascades dotnet://Variants/Variants/Variants.ReturnTuple -> dotnet://Variants/Variants/Variants.Follow discriminator=return-slot:3 | id=wolverine:cascades:return:dotnet://Variants/Variants/Variants.ReturnTuple:3:dotnet://Variants/Variants/Variants.Follow | subject=dotnet://Variants/Variants/Variants.ReturnTuple | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:37:5 +fact artifact Message Expiry file=Variants/Events.cs properties=[] | id=wolverine:message:dotnet://Variants/Variants/Variants.Expiry | subject=dotnet://Variants/Variants/Variants.Expiry | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:37:5 +fact relationship Cascades dotnet://Variants/Variants/Variants.ReturnTuple -> dotnet://Variants/Variants/Variants.Expiry discriminator=return-slot:5 | id=wolverine:cascades:return:dotnet://Variants/Variants/Variants.ReturnTuple:5:dotnet://Variants/Variants/Variants.Expiry | subject=dotnet://Variants/Variants/Variants.ReturnTuple | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:37:5 +fact artifact ReadModel Document file=Variants/Events.cs properties=[id] | id=wolverine:read-model:dotnet://Variants/Variants/Variants.Document | subject=dotnet://Variants/Variants/Variants.Document | Exact | Wolverine persists this model through a returned storage action | Variants/Events.cs:9:1 +fact relationship Stores dotnet://Variants/Variants/Variants.ReturnTuple -> dotnet://Variants/Variants/Variants.Document discriminator=storage-action:4 | id=wolverine:stores:dotnet://Variants/Variants/Variants.ReturnTuple:dotnet://Variants/Variants/Variants.Document:4 | subject=dotnet://Variants/Variants/Variants.ReturnTuple | Exact | Wolverine applies this returned storage action to the persistence layer | Variants/Returns.cs:37:5 +fact artifact Command ReturnUnit file=Variants/Returns.cs properties=[text] | id=wolverine:command:dotnet://Variants/Variants/Variants.ReturnUnit | subject=dotnet://Variants/Variants/Variants.ReturnUnit | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:26:41 +fact artifact ReadModel Document file=Variants/Events.cs properties=[id] | id=wolverine:read-model:dotnet://Variants/Variants/Variants.Document | subject=dotnet://Variants/Variants/Variants.Document | Exact | Wolverine persists this model through a returned storage action | Variants/Events.cs:9:1 +fact relationship Stores dotnet://Variants/Variants/Variants.ReturnUnit -> dotnet://Variants/Variants/Variants.Document discriminator=storage-action:0 | id=wolverine:stores:dotnet://Variants/Variants/Variants.ReturnUnit:dotnet://Variants/Variants/Variants.Document:0 | subject=dotnet://Variants/Variants/Variants.ReturnUnit | Exact | Wolverine applies this returned storage action to the persistence layer | Variants/Returns.cs:26:41 +fact artifact Command SagaStateAppend file=Variants/Links.cs properties=[id] | id=wolverine:command:dotnet://Variants/Variants/Variants.SagaStateAppend | subject=dotnet://Variants/Variants/Variants.SagaStateAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:143:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:143:5 +fact relationship Produces dotnet://Variants/Variants/Variants.SagaStateAppend -> dotnet://Variants/Variants/Variants.Happened discriminator=declarative | id=wolverine:produces:dotnet://Variants/Variants/Variants.SagaStateAppend:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.SagaStateAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:143:5 +fact artifact Command StartedAppend file=Variants/Links.cs properties=[note] | id=wolverine:command:dotnet://Variants/Variants/Variants.StartedAppend | subject=dotnet://Variants/Variants/Variants.StartedAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:75:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:75:5 +fact relationship Produces dotnet://Variants/Variants/Variants.StartedAppend -> dotnet://Variants/Variants/Variants.Happened discriminator=imperative | id=wolverine:produces:dotnet://Variants/Variants/Variants.StartedAppend:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.StartedAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:75:5 +fact artifact Command StartedWithId file=Variants/Links.cs properties=[id,note] | id=wolverine:command:dotnet://Variants/Variants/Variants.StartedWithId | subject=dotnet://Variants/Variants/Variants.StartedWithId | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:81:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:81:5 +fact relationship Produces dotnet://Variants/Variants/Variants.StartedWithId -> dotnet://Variants/Variants/Variants.Happened discriminator=declarative | id=wolverine:produces:dotnet://Variants/Variants/Variants.StartedWithId:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.StartedWithId | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:81:5 +fact artifact Command StringAppend file=Variants/Links.cs properties=[key,note] | id=wolverine:command:dotnet://Variants/Variants/Variants.StringAppend | subject=dotnet://Variants/Variants/Variants.StringAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:29:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:29:5 +fact relationship Produces dotnet://Variants/Variants/Variants.StringAppend -> dotnet://Variants/Variants/Variants.Happened discriminator=declarative | id=wolverine:produces:dotnet://Variants/Variants/Variants.StringAppend:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.StringAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:29:5 +fact artifact Command SwitchAppend file=Variants/Links.cs properties=[id,kind,note] | id=wolverine:command:dotnet://Variants/Variants/Variants.SwitchAppend | subject=dotnet://Variants/Variants/Variants.SwitchAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:117:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:117:5 +fact relationship Produces dotnet://Variants/Variants/Variants.SwitchAppend -> dotnet://Variants/Variants/Variants.Happened discriminator=imperative | id=wolverine:produces:dotnet://Variants/Variants/Variants.SwitchAppend:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.SwitchAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:117:5 +fact artifact Event Another file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Another | subject=dotnet://Variants/Variants/Variants.Another | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:117:5 +fact relationship Produces dotnet://Variants/Variants/Variants.SwitchAppend -> dotnet://Variants/Variants/Variants.Another discriminator=imperative | id=wolverine:produces:dotnet://Variants/Variants/Variants.SwitchAppend:dotnet://Variants/Variants/Variants.Another | subject=dotnet://Variants/Variants/Variants.SwitchAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:117:5 +fact artifact Command UnconditionalAfterGuard file=Variants/Links.cs properties=[id,note] | id=wolverine:command:dotnet://Variants/Variants/Variants.UnconditionalAfterGuard | subject=dotnet://Variants/Variants/Variants.UnconditionalAfterGuard | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:52:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:52:5 +fact relationship Produces dotnet://Variants/Variants/Variants.UnconditionalAfterGuard -> dotnet://Variants/Variants/Variants.Happened discriminator=declarative | id=wolverine:produces:dotnet://Variants/Variants/Variants.UnconditionalAfterGuard:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.UnconditionalAfterGuard | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:52:5 +fact artifact Command VersionedAppend file=Variants/Links.cs properties=[id,note] | id=wolverine:command:dotnet://Variants/Variants/Variants.VersionedAppend | subject=dotnet://Variants/Variants/Variants.VersionedAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:111:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:111:5 +fact relationship Produces dotnet://Variants/Variants/Variants.VersionedAppend -> dotnet://Variants/Variants/Variants.Happened discriminator=declarative | id=wolverine:produces:dotnet://Variants/Variants/Variants.VersionedAppend:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.VersionedAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:111:5 +fact artifact Event Third file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Third | subject=dotnet://Variants/Variants/Variants.Third | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:111:5 +fact relationship Produces dotnet://Variants/Variants/Variants.VersionedAppend -> dotnet://Variants/Variants/Variants.Third discriminator=declarative | id=wolverine:produces:dotnet://Variants/Variants/Variants.VersionedAppend:dotnet://Variants/Variants/Variants.Third | subject=dotnet://Variants/Variants/Variants.VersionedAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:111:5 +fact artifact Command YieldAfterGuard file=Variants/Yields.cs properties=[id,skip] | id=wolverine:command:dotnet://Variants/Variants/Variants.YieldAfterGuard | subject=dotnet://Variants/Variants/Variants.YieldAfterGuard | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:62:5 +fact artifact ReadModel State file=Variants/Events.cs properties=[id] | id=wolverine:read-model:dotnet://Variants/Variants/Variants.State | subject=dotnet://Variants/Variants/Variants.State | Conventional | Wolverine loads this model as aggregate decision state | Variants/Events.cs:8:1 +fact relationship Reads dotnet://Variants/Variants/Variants.YieldAfterGuard -> dotnet://Variants/Variants/Variants.State discriminator= | id=wolverine:reads:dotnet://Variants/Variants/Variants.YieldAfterGuard:dotnet://Variants/Variants/Variants.State | subject=dotnet://Variants/Variants/Variants.YieldAfterGuard | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:62:5 +fact artifact Event Another file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Another | subject=dotnet://Variants/Variants/Variants.Another | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:62:5 +fact relationship Produces dotnet://Variants/Variants/Variants.YieldAfterGuard -> dotnet://Variants/Variants/Variants.Another discriminator=imperative | id=wolverine:produces:dotnet://Variants/Variants/Variants.YieldAfterGuard:dotnet://Variants/Variants/Variants.Another | subject=dotnet://Variants/Variants/Variants.YieldAfterGuard | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:62:5 +fact artifact Command YieldBreak file=Variants/Yields.cs properties=[id,stop] | id=wolverine:command:dotnet://Variants/Variants/Variants.YieldBreak | subject=dotnet://Variants/Variants/Variants.YieldBreak | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:72:5 +fact artifact ReadModel State file=Variants/Events.cs properties=[id] | id=wolverine:read-model:dotnet://Variants/Variants/Variants.State | subject=dotnet://Variants/Variants/Variants.State | Conventional | Wolverine loads this model as aggregate decision state | Variants/Events.cs:8:1 +fact relationship Reads dotnet://Variants/Variants/Variants.YieldBreak -> dotnet://Variants/Variants/Variants.State discriminator= | id=wolverine:reads:dotnet://Variants/Variants/Variants.YieldBreak:dotnet://Variants/Variants/Variants.State | subject=dotnet://Variants/Variants/Variants.YieldBreak | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:72:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:72:5 +fact relationship Produces dotnet://Variants/Variants/Variants.YieldBreak -> dotnet://Variants/Variants/Variants.Happened discriminator=declarative | id=wolverine:produces:dotnet://Variants/Variants/Variants.YieldBreak:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.YieldBreak | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:72:5 +fact artifact Command YieldConditional file=Variants/Yields.cs properties=[id,go] | id=wolverine:command:dotnet://Variants/Variants/Variants.YieldConditional | subject=dotnet://Variants/Variants/Variants.YieldConditional | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:34:5 +fact artifact ReadModel State file=Variants/Events.cs properties=[id] | id=wolverine:read-model:dotnet://Variants/Variants/Variants.State | subject=dotnet://Variants/Variants/Variants.State | Conventional | Wolverine loads this model as aggregate decision state | Variants/Events.cs:8:1 +fact relationship Reads dotnet://Variants/Variants/Variants.YieldConditional -> dotnet://Variants/Variants/Variants.State discriminator= | id=wolverine:reads:dotnet://Variants/Variants/Variants.YieldConditional:dotnet://Variants/Variants/Variants.State | subject=dotnet://Variants/Variants/Variants.YieldConditional | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:34:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:34:5 +fact relationship Produces dotnet://Variants/Variants/Variants.YieldConditional -> dotnet://Variants/Variants/Variants.Happened discriminator=imperative | id=wolverine:produces:dotnet://Variants/Variants/Variants.YieldConditional:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.YieldConditional | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:34:5 +fact artifact Command YieldConditionalNoIdentity file=Variants/Yields.cs properties=[note,go] | id=wolverine:command:dotnet://Variants/Variants/Variants.YieldConditionalNoIdentity | subject=dotnet://Variants/Variants/Variants.YieldConditionalNoIdentity | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:43:5 +fact artifact ReadModel State file=Variants/Events.cs properties=[id] | id=wolverine:read-model:dotnet://Variants/Variants/Variants.State | subject=dotnet://Variants/Variants/Variants.State | Conventional | Wolverine loads this model as aggregate decision state | Variants/Events.cs:8:1 +fact relationship Reads dotnet://Variants/Variants/Variants.YieldConditionalNoIdentity -> dotnet://Variants/Variants/Variants.State discriminator= | id=wolverine:reads:dotnet://Variants/Variants/Variants.YieldConditionalNoIdentity:dotnet://Variants/Variants/Variants.State | subject=dotnet://Variants/Variants/Variants.YieldConditionalNoIdentity | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:43:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:43:5 +fact relationship Produces dotnet://Variants/Variants/Variants.YieldConditionalNoIdentity -> dotnet://Variants/Variants/Variants.Happened discriminator=imperative | id=wolverine:produces:dotnet://Variants/Variants/Variants.YieldConditionalNoIdentity:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.YieldConditionalNoIdentity | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:43:5 +fact artifact Command YieldIndirect file=Variants/Yields.cs properties=[id] | id=wolverine:command:dotnet://Variants/Variants/Variants.YieldIndirect | subject=dotnet://Variants/Variants/Variants.YieldIndirect | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:52:5 +fact artifact ReadModel State file=Variants/Events.cs properties=[id] | id=wolverine:read-model:dotnet://Variants/Variants/Variants.State | subject=dotnet://Variants/Variants/Variants.State | Conventional | Wolverine loads this model as aggregate decision state | Variants/Events.cs:8:1 +fact relationship Reads dotnet://Variants/Variants/Variants.YieldIndirect -> dotnet://Variants/Variants/Variants.State discriminator= | id=wolverine:reads:dotnet://Variants/Variants/Variants.YieldIndirect:dotnet://Variants/Variants/Variants.State | subject=dotnet://Variants/Variants/Variants.YieldIndirect | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:52:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:52:5 +fact relationship Produces dotnet://Variants/Variants/Variants.YieldIndirect -> dotnet://Variants/Variants/Variants.Happened discriminator=imperative | id=wolverine:produces:dotnet://Variants/Variants/Variants.YieldIndirect:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.YieldIndirect | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:52:5 +fact artifact Command YieldLocal file=Variants/Yields.cs properties=[id] | id=wolverine:command:dotnet://Variants/Variants/Variants.YieldLocal | subject=dotnet://Variants/Variants/Variants.YieldLocal | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:82:5 +fact artifact ReadModel State file=Variants/Events.cs properties=[id] | id=wolverine:read-model:dotnet://Variants/Variants/Variants.State | subject=dotnet://Variants/Variants/Variants.State | Conventional | Wolverine loads this model as aggregate decision state | Variants/Events.cs:8:1 +fact relationship Reads dotnet://Variants/Variants/Variants.YieldLocal -> dotnet://Variants/Variants/Variants.State discriminator= | id=wolverine:reads:dotnet://Variants/Variants/Variants.YieldLocal:dotnet://Variants/Variants/Variants.State | subject=dotnet://Variants/Variants/Variants.YieldLocal | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:82:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:82:5 +fact relationship Produces dotnet://Variants/Variants/Variants.YieldLocal -> dotnet://Variants/Variants/Variants.Happened discriminator=declarative | id=wolverine:produces:dotnet://Variants/Variants/Variants.YieldLocal:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.YieldLocal | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:82:5 +fact artifact Command YieldNoIdentity file=Variants/Yields.cs properties=[note] | id=wolverine:command:dotnet://Variants/Variants/Variants.YieldNoIdentity | subject=dotnet://Variants/Variants/Variants.YieldNoIdentity | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:25:5 +fact artifact ReadModel State file=Variants/Events.cs properties=[id] | id=wolverine:read-model:dotnet://Variants/Variants/Variants.State | subject=dotnet://Variants/Variants/Variants.State | Conventional | Wolverine loads this model as aggregate decision state | Variants/Events.cs:8:1 +fact relationship Reads dotnet://Variants/Variants/Variants.YieldNoIdentity -> dotnet://Variants/Variants/Variants.State discriminator= | id=wolverine:reads:dotnet://Variants/Variants/Variants.YieldNoIdentity:dotnet://Variants/Variants/Variants.State | subject=dotnet://Variants/Variants/Variants.YieldNoIdentity | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:25:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:25:5 +fact relationship Produces dotnet://Variants/Variants/Variants.YieldNoIdentity -> dotnet://Variants/Variants/Variants.Happened discriminator=imperative | id=wolverine:produces:dotnet://Variants/Variants/Variants.YieldNoIdentity:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.YieldNoIdentity | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:25:5 +fact artifact Command YieldPlain file=Variants/Yields.cs properties=[id] | id=wolverine:command:dotnet://Variants/Variants/Variants.YieldPlain | subject=dotnet://Variants/Variants/Variants.YieldPlain | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:16:5 +fact artifact ReadModel State file=Variants/Events.cs properties=[id] | id=wolverine:read-model:dotnet://Variants/Variants/Variants.State | subject=dotnet://Variants/Variants/Variants.State | Conventional | Wolverine loads this model as aggregate decision state | Variants/Events.cs:8:1 +fact relationship Reads dotnet://Variants/Variants/Variants.YieldPlain -> dotnet://Variants/Variants/Variants.State discriminator= | id=wolverine:reads:dotnet://Variants/Variants/Variants.YieldPlain:dotnet://Variants/Variants/Variants.State | subject=dotnet://Variants/Variants/Variants.YieldPlain | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:16:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:16:5 +fact relationship Produces dotnet://Variants/Variants/Variants.YieldPlain -> dotnet://Variants/Variants/Variants.Happened discriminator=declarative | id=wolverine:produces:dotnet://Variants/Variants/Variants.YieldPlain:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.YieldPlain | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:16:5 +fact artifact Command PostReply file=Variants/Endpoints.cs properties=[text] | id=wolverine:command:dotnet://Variants/Variants/Variants.PostReply | subject=dotnet://Variants/Variants/Variants.PostReply | Exact | Wolverine HTTP POST endpoint | Variants/Endpoints.cs:14:5 +fact artifact Command PostEmpty file=Variants/Endpoints.cs properties=[text] | id=wolverine:command:dotnet://Variants/Variants/Variants.PostEmpty | subject=dotnet://Variants/Variants/Variants.PostEmpty | Exact | Wolverine HTTP POST endpoint | Variants/Endpoints.cs:15:5 +fact artifact Message Follow file=Variants/Events.cs properties=[text] | id=wolverine:message:dotnet://Variants/Variants/Variants.Follow | subject=dotnet://Variants/Variants/Variants.Follow | Exact | Wolverine HTTP POST endpoint | Variants/Endpoints.cs:15:5 +fact relationship Cascades dotnet://Variants/Variants/Variants.PostEmpty -> dotnet://Variants/Variants/Variants.Follow discriminator=return-slot:0 | id=wolverine:cascades:return:dotnet://Variants/Variants/Variants.PostEmpty:0:dotnet://Variants/Variants/Variants.Follow | subject=dotnet://Variants/Variants/Variants.PostEmpty | Exact | Wolverine HTTP POST endpoint | Variants/Endpoints.cs:15:5 +fact artifact Command PostTuple file=Variants/Endpoints.cs properties=[text] | id=wolverine:command:dotnet://Variants/Variants/Variants.PostTuple | subject=dotnet://Variants/Variants/Variants.PostTuple | Exact | Wolverine HTTP POST endpoint | Variants/Endpoints.cs:16:5 +fact artifact Message Follow file=Variants/Events.cs properties=[text] | id=wolverine:message:dotnet://Variants/Variants/Variants.Follow | subject=dotnet://Variants/Variants/Variants.Follow | Exact | Wolverine HTTP POST endpoint | Variants/Endpoints.cs:16:5 +fact relationship Cascades dotnet://Variants/Variants/Variants.PostTuple -> dotnet://Variants/Variants/Variants.Follow discriminator=return-slot:1 | id=wolverine:cascades:return:dotnet://Variants/Variants/Variants.PostTuple:1:dotnet://Variants/Variants/Variants.Follow | subject=dotnet://Variants/Variants/Variants.PostTuple | Exact | Wolverine HTTP POST endpoint | Variants/Endpoints.cs:16:5 +fact artifact Command PostResult file=Variants/Endpoints.cs properties=[text] | id=wolverine:command:dotnet://Variants/Variants/Variants.PostResult | subject=dotnet://Variants/Variants/Variants.PostResult | Exact | Wolverine HTTP POST endpoint | Variants/Endpoints.cs:17:5 +fact artifact Command PostCreation file=Variants/Endpoints.cs properties=[text] | id=wolverine:command:dotnet://Variants/Variants/Variants.PostCreation | subject=dotnet://Variants/Variants/Variants.PostCreation | Exact | Wolverine HTTP POST endpoint | Variants/Endpoints.cs:18:5 +fact artifact Command PostOutgoing file=Variants/Endpoints.cs properties=[text] | id=wolverine:command:dotnet://Variants/Variants/Variants.PostOutgoing | subject=dotnet://Variants/Variants/Variants.PostOutgoing | Exact | Wolverine HTTP POST endpoint | Variants/Endpoints.cs:19:5 +fact artifact Message Reply file=Variants/Events.cs properties=[text] | id=wolverine:message:dotnet://Variants/Variants/Variants.Reply | subject=dotnet://Variants/Variants/Variants.Reply | Exact | Wolverine HTTP POST endpoint | Variants/Endpoints.cs:19:5 +fact relationship Cascades dotnet://Variants/Variants/Variants.PostOutgoing -> dotnet://Variants/Variants/Variants.Reply discriminator=return-slot:1 | id=wolverine:cascades:return:dotnet://Variants/Variants/Variants.PostOutgoing:1:dotnet://Variants/Variants/Variants.Reply | subject=dotnet://Variants/Variants/Variants.PostOutgoing | Exact | Wolverine HTTP POST endpoint | Variants/Endpoints.cs:19:5 +fact artifact Command PostVoid file=Variants/Endpoints.cs properties=[text] | id=wolverine:command:dotnet://Variants/Variants/Variants.PostVoid | subject=dotnet://Variants/Variants/Variants.PostVoid | Exact | Wolverine HTTP POST endpoint | Variants/Endpoints.cs:20:5 +fact artifact Command PatternHttpPost file=Variants/Patterns.cs properties=[note] | id=wolverine:command:dotnet://Variants/Variants/Variants.PatternHttpPost | subject=dotnet://Variants/Variants/Variants.PatternHttpPost | Exact | Wolverine HTTP POST endpoint | Variants/Patterns.cs:17:5 +fact artifact Event Happened file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine HTTP POST endpoint | Variants/Patterns.cs:17:5 +fact relationship Produces dotnet://Variants/Variants/Variants.PatternHttpPost -> dotnet://Variants/Variants/Variants.Happened discriminator=imperative | id=wolverine:produces:dotnet://Variants/Variants/Variants.PatternHttpPost:dotnet://Variants/Variants/Variants.Happened | subject=dotnet://Variants/Variants/Variants.PatternHttpPost | Exact | Wolverine HTTP POST endpoint | Variants/Patterns.cs:17:5 +fact artifact Query Get file=Variants/Patterns.cs properties=[] | id=wolverine:query:dotnet://Variants/Variants/Variants.PatternEndpoints#method:M%3AVariants.PatternEndpoints.Get:query | subject=dotnet://Variants/Variants/Variants.PatternEndpoints#method:M%3AVariants.PatternEndpoints.Get:query | Exact | Wolverine HTTP GET endpoint | Variants/Patterns.cs:20:5 +fact artifact ReadModel Reply file=Variants/Events.cs properties=[text] | id=wolverine:read-model:dotnet://Variants/Variants/Variants.Reply | subject=dotnet://Variants/Variants/Variants.Reply | Exact | Wolverine HTTP GET endpoint | Variants/Patterns.cs:20:5 +fact relationship Returns dotnet://Variants/Variants/Variants.PatternEndpoints#method:M%3AVariants.PatternEndpoints.Get:query -> dotnet://Variants/Variants/Variants.Reply discriminator= | id=wolverine:returns:dotnet://Variants/Variants/Variants.PatternEndpoints#method:M%3AVariants.PatternEndpoints.Get:query:dotnet://Variants/Variants/Variants.Reply | subject=dotnet://Variants/Variants/Variants.PatternEndpoints#method:M%3AVariants.PatternEndpoints.Get:query | Exact | Wolverine HTTP GET endpoint | Variants/Patterns.cs:20:5 +fact artifact Command Head file=Variants/Patterns.cs properties=[] | id=wolverine:command:dotnet://Variants/Variants/Variants.PatternEndpoints#method:M%3AVariants.PatternEndpoints.Head:command | subject=dotnet://Variants/Variants/Variants.PatternEndpoints#method:M%3AVariants.PatternEndpoints.Head:command | Exact | Wolverine HTTP HEAD endpoint | Variants/Patterns.cs:23:5 +fact artifact Command PatternAgreeing file=Variants/Patterns.cs properties=[] | id=wolverine:command:dotnet://Variants/Variants/Variants.PatternAgreeing | subject=dotnet://Variants/Variants/Variants.PatternAgreeing | Exact | Wolverine HTTP POST endpoint | Variants/Patterns.cs:26:5 +fact artifact Event Another file=Variants/Events.cs properties=[note] | id=wolverine:event:dotnet://Variants/Variants/Variants.Another | subject=dotnet://Variants/Variants/Variants.Another | Exact | Wolverine HTTP POST endpoint | Variants/Patterns.cs:26:5 +fact relationship Produces dotnet://Variants/Variants/Variants.PatternAgreeing -> dotnet://Variants/Variants/Variants.Another discriminator=imperative | id=wolverine:produces:dotnet://Variants/Variants/Variants.PatternAgreeing:dotnet://Variants/Variants/Variants.Another | subject=dotnet://Variants/Variants/Variants.PatternAgreeing | Exact | Wolverine HTTP POST endpoint | Variants/Patterns.cs:26:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.ComputedAppend | subject=dotnet://Variants/Variants/Variants.ComputedAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:61:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.ComputedAppend | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:61:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.ConditionalAppend | subject=dotnet://Variants/Variants/Variants.ConditionalAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:35:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.ConditionalAppend | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:35:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.ConditionalComputed | subject=dotnet://Variants/Variants/Variants.ConditionalComputed | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:67:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.ConditionalComputed | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:67:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.EarlyExitAppend | subject=dotnet://Variants/Variants/Variants.EarlyExitAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:43:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.EarlyExitAppend | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:43:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.IndirectAppend | subject=dotnet://Variants/Variants/Variants.IndirectAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:96:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.IndirectAppend | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:96:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.LambdaAppend | subject=dotnet://Variants/Variants/Variants.LambdaAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:87:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.LambdaAppend | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:87:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.LoopAppend | subject=dotnet://Variants/Variants/Variants.LoopAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:129:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.LoopAppend | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:129:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.OtherParameterAppend | subject=dotnet://Variants/Variants/Variants.OtherParameterAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:137:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.OtherParameterAppend | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:137:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.PatternMissing | subject=dotnet://Variants/Variants/Variants.PatternMissing | Exact | Wolverine message handler with persistence effects | Variants/Patterns.cs:56:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Another:dotnet://Variants/Variants/Variants.PatternMissing | subject=dotnet://Variants/Variants/Variants.Another | Exact | Wolverine message handler with persistence effects | Variants/Patterns.cs:56:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.PatternTimeoutAgree | subject=dotnet://Variants/Variants/Variants.PatternTimeoutAgree | Exact | Wolverine message handler with persistence effects; sliceClassification(strength=Exact, source=Variants/Patterns.cs:38:6): Proven TimeoutMessage scheduler trigger classifies the slice as Automation; declared SlicePattern=Automation | Variants/Patterns.cs:38:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Another:dotnet://Variants/Variants/Variants.PatternTimeoutAgree | subject=dotnet://Variants/Variants/Variants.Another | Exact | Wolverine message handler with persistence effects; sliceClassification(strength=Exact, source=Variants/Patterns.cs:38:6): Proven TimeoutMessage scheduler trigger classifies the slice as Automation; declared SlicePattern=Automation | Variants/Patterns.cs:38:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.PatternTimeoutMessage | subject=dotnet://Variants/Variants/Variants.PatternTimeoutMessage | Exact | Wolverine message handler with persistence effects; sliceClassification(strength=Exact, source=Variants/Patterns.cs:32:6): Proven TimeoutMessage scheduler trigger classifies the slice as Automation; declared SlicePattern=Translate | Variants/Patterns.cs:32:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.PatternTimeoutMessage | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects; sliceClassification(strength=Exact, source=Variants/Patterns.cs:32:6): Proven TimeoutMessage scheduler trigger classifies the slice as Automation; declared SlicePattern=Translate | Variants/Patterns.cs:32:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.PatternTranslated | subject=dotnet://Variants/Variants/Variants.PatternTranslated | Exact | Wolverine message handler with persistence effects; sliceClassification(strength=Exact, source=Variants/Patterns.cs:42:2): Exact Wolverine SlicePattern declaration classifies the slice as Translate | Variants/Patterns.cs:45:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Third:dotnet://Variants/Variants/Variants.PatternTranslated | subject=dotnet://Variants/Variants/Variants.Third | Exact | Wolverine message handler with persistence effects; sliceClassification(strength=Exact, source=Variants/Patterns.cs:42:2): Exact Wolverine SlicePattern declaration classifies the slice as Translate | Variants/Patterns.cs:45:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.PlainAppend | subject=dotnet://Variants/Variants/Variants.PlainAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:23:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.PlainAppend | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:23:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.ReturnAggregateEvent | subject=dotnet://Variants/Variants/Variants.ReturnAggregateEvent | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:42:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.ReturnAggregateEvent | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:42:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.ReturnAggregateOutgoing | subject=dotnet://Variants/Variants/Variants.ReturnAggregateOutgoing | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:48:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Another:dotnet://Variants/Variants/Variants.ReturnAggregateOutgoing | subject=dotnet://Variants/Variants/Variants.Another | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:48:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Follow:dotnet://Variants/Variants/Variants.ReturnAggregateOutgoing | subject=dotnet://Variants/Variants/Variants.Follow | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:48:5 +fact ArtifactPlacementFact | id=wolverine:placement:reaction:dotnet://Variants/Variants/Variants.ReturnCascadeHandler#method:M%3AVariants.ReturnCascadeHandler.Handle%28Variants.ReturnCascade%29:reaction | subject=dotnet://Variants/Variants/Variants.ReturnCascadeHandler#method:M%3AVariants.ReturnCascadeHandler.Handle%28Variants.ReturnCascade%29:reaction | Exact | Wolverine message handler with direct bus or return automation consequences | Variants/Returns.cs:31:44 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.ReturnCustomStore | subject=dotnet://Variants/Variants/Variants.ReturnCustomStore | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:27:48 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.ReturnStore | subject=dotnet://Variants/Variants/Variants.ReturnStore | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:25:42 +fact ArtifactPlacementFact | id=wolverine:placement:reaction:dotnet://Variants/Variants/Variants.ReturnTaskHandler#method:M%3AVariants.ReturnTaskHandler.Handle%28Variants.ReturnTask%29:reaction | subject=dotnet://Variants/Variants/Variants.ReturnTaskHandler#method:M%3AVariants.ReturnTaskHandler.Handle%28Variants.ReturnTask%29:reaction | Exact | Wolverine message handler with direct bus or return automation consequences | Variants/Returns.cs:33:41 +fact ArtifactPlacementFact | id=wolverine:placement:reaction:dotnet://Variants/Variants/Variants.ReturnTimeoutHandler#method:M%3AVariants.ReturnTimeoutHandler.Handle%28Variants.ReturnTimeout%29:reaction | subject=dotnet://Variants/Variants/Variants.ReturnTimeoutHandler#method:M%3AVariants.ReturnTimeoutHandler.Handle%28Variants.ReturnTimeout%29:reaction | Exact | Wolverine message handler with direct bus or return automation consequences | Variants/Returns.cs:30:44 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.ReturnTuple | subject=dotnet://Variants/Variants/Variants.ReturnTuple | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:37:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.ReturnUnit | subject=dotnet://Variants/Variants/Variants.ReturnUnit | Exact | Wolverine message handler with persistence effects | Variants/Returns.cs:26:41 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.SagaStateAppend | subject=dotnet://Variants/Variants/Variants.SagaStateAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:143:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.SagaStateAppend | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:143:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.StartedAppend | subject=dotnet://Variants/Variants/Variants.StartedAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:75:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.StartedAppend | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:75:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.StartedWithId | subject=dotnet://Variants/Variants/Variants.StartedWithId | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:81:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.StartedWithId | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:81:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.StringAppend | subject=dotnet://Variants/Variants/Variants.StringAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:29:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.StringAppend | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:29:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.SwitchAppend | subject=dotnet://Variants/Variants/Variants.SwitchAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:117:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.SwitchAppend | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:117:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Another:dotnet://Variants/Variants/Variants.SwitchAppend | subject=dotnet://Variants/Variants/Variants.Another | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:117:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.UnconditionalAfterGuard | subject=dotnet://Variants/Variants/Variants.UnconditionalAfterGuard | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:52:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.UnconditionalAfterGuard | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:52:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.VersionedAppend | subject=dotnet://Variants/Variants/Variants.VersionedAppend | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:111:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.VersionedAppend | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:111:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Third:dotnet://Variants/Variants/Variants.VersionedAppend | subject=dotnet://Variants/Variants/Variants.Third | Exact | Wolverine message handler with persistence effects | Variants/Links.cs:111:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.YieldAfterGuard | subject=dotnet://Variants/Variants/Variants.YieldAfterGuard | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:62:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Another:dotnet://Variants/Variants/Variants.YieldAfterGuard | subject=dotnet://Variants/Variants/Variants.Another | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:62:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.YieldBreak | subject=dotnet://Variants/Variants/Variants.YieldBreak | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:72:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.YieldBreak | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:72:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.YieldConditional | subject=dotnet://Variants/Variants/Variants.YieldConditional | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:34:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.YieldConditional | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:34:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.YieldConditionalNoIdentity | subject=dotnet://Variants/Variants/Variants.YieldConditionalNoIdentity | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:43:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.YieldConditionalNoIdentity | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:43:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.YieldIndirect | subject=dotnet://Variants/Variants/Variants.YieldIndirect | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:52:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.YieldIndirect | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:52:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.YieldLocal | subject=dotnet://Variants/Variants/Variants.YieldLocal | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:82:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.YieldLocal | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:82:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.YieldNoIdentity | subject=dotnet://Variants/Variants/Variants.YieldNoIdentity | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:25:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.YieldNoIdentity | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:25:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.YieldPlain | subject=dotnet://Variants/Variants/Variants.YieldPlain | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:16:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.YieldPlain | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine message handler with persistence effects | Variants/Yields.cs:16:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.PostReply | subject=dotnet://Variants/Variants/Variants.PostReply | Exact | Wolverine HTTP POST endpoint; sliceClassification(strength=Exact, source=Variants/Endpoints.cs:14:5): Proven HTTP POST trigger classifies the slice as StateChange | Variants/Endpoints.cs:14:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.PostEmpty | subject=dotnet://Variants/Variants/Variants.PostEmpty | Exact | Wolverine HTTP POST endpoint; sliceClassification(strength=Exact, source=Variants/Endpoints.cs:15:5): Proven HTTP POST trigger classifies the slice as StateChange | Variants/Endpoints.cs:15:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.PostTuple | subject=dotnet://Variants/Variants/Variants.PostTuple | Exact | Wolverine HTTP POST endpoint; sliceClassification(strength=Exact, source=Variants/Endpoints.cs:16:5): Proven HTTP POST trigger classifies the slice as StateChange | Variants/Endpoints.cs:16:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.PostResult | subject=dotnet://Variants/Variants/Variants.PostResult | Exact | Wolverine HTTP POST endpoint; sliceClassification(strength=Exact, source=Variants/Endpoints.cs:17:5): Proven HTTP POST trigger classifies the slice as StateChange | Variants/Endpoints.cs:17:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.PostCreation | subject=dotnet://Variants/Variants/Variants.PostCreation | Exact | Wolverine HTTP POST endpoint; sliceClassification(strength=Exact, source=Variants/Endpoints.cs:18:5): Proven HTTP POST trigger classifies the slice as StateChange | Variants/Endpoints.cs:18:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.PostOutgoing | subject=dotnet://Variants/Variants/Variants.PostOutgoing | Exact | Wolverine HTTP POST endpoint; sliceClassification(strength=Exact, source=Variants/Endpoints.cs:19:5): Proven HTTP POST trigger classifies the slice as StateChange | Variants/Endpoints.cs:19:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.PostVoid | subject=dotnet://Variants/Variants/Variants.PostVoid | Exact | Wolverine HTTP POST endpoint; sliceClassification(strength=Exact, source=Variants/Endpoints.cs:20:5): Proven HTTP POST trigger classifies the slice as StateChange | Variants/Endpoints.cs:20:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.PatternHttpPost | subject=dotnet://Variants/Variants/Variants.PatternHttpPost | Exact | Wolverine HTTP POST endpoint; sliceClassification(strength=Exact, source=Variants/Patterns.cs:17:48): Proven HTTP POST trigger classifies the slice as StateChange; declared SlicePattern=StateView | Variants/Patterns.cs:17:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Happened:dotnet://Variants/Variants/Variants.PatternHttpPost | subject=dotnet://Variants/Variants/Variants.Happened | Exact | Wolverine HTTP POST endpoint; sliceClassification(strength=Exact, source=Variants/Patterns.cs:17:48): Proven HTTP POST trigger classifies the slice as StateChange; declared SlicePattern=StateView | Variants/Patterns.cs:17:5 +fact ArtifactPlacementFact | id=wolverine:placement:query:dotnet://Variants/Variants/Variants.PatternEndpoints#method:M%3AVariants.PatternEndpoints.Get:query | subject=dotnet://Variants/Variants/Variants.PatternEndpoints#method:M%3AVariants.PatternEndpoints.Get:query | Exact | Wolverine HTTP GET endpoint; sliceClassification(strength=Exact, source=Variants/Patterns.cs:20:51): Proven HTTP GET trigger classifies the slice as StateView; declared SlicePattern=StateChange | Variants/Patterns.cs:20:5 +fact ArtifactPlacementFact | id=wolverine:placement:read-model:dotnet://Variants/Variants/Variants.Reply | subject=dotnet://Variants/Variants/Variants.Reply | Heuristic | The returned model name provides the default Screenplay placement; sliceClassification(strength=Exact, source=Variants/Patterns.cs:20:51): Proven HTTP GET trigger classifies the slice as StateView; declared SlicePattern=StateChange | Variants/Patterns.cs:20:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.PatternEndpoints#method:M%3AVariants.PatternEndpoints.Head:command | subject=dotnet://Variants/Variants/Variants.PatternEndpoints#method:M%3AVariants.PatternEndpoints.Head:command | Exact | Wolverine HTTP HEAD endpoint; sliceClassification(strength=Exact, source=Variants/Patterns.cs:23:53): Exact Wolverine SlicePattern declaration classifies the slice as Automation | Variants/Patterns.cs:23:5 +fact ArtifactPlacementFact | id=wolverine:placement:command:dotnet://Variants/Variants/Variants.PatternAgreeing | subject=dotnet://Variants/Variants/Variants.PatternAgreeing | Exact | Wolverine HTTP POST endpoint; sliceClassification(strength=Exact, source=Variants/Patterns.cs:26:54): Proven HTTP POST trigger classifies the slice as StateChange; declared SlicePattern=StateChange | Variants/Patterns.cs:26:5 +fact ArtifactPlacementFact | id=wolverine:placement:event:dotnet://Variants/Variants/Variants.Another:dotnet://Variants/Variants/Variants.PatternAgreeing | subject=dotnet://Variants/Variants/Variants.Another | Exact | Wolverine HTTP POST endpoint; sliceClassification(strength=Exact, source=Variants/Patterns.cs:26:54): Proven HTTP POST trigger classifies the slice as StateChange; declared SlicePattern=StateChange | Variants/Patterns.cs:26:5 +diagnostic WOLVERINE0025 Warning Unknown | Variants/Links.cs:62:9 | subject=dotnet://Variants/Variants/Variants.ComputedAppend | Command 'ComputedAppend' is not linked to event 'Happened' with produces because the stream identity is not read directly from a property of the command; the model references the handler instead +diagnostic WOLVERINE0025 Warning Unsupported | Variants/Links.cs:37:30 | subject=dotnet://Variants/Variants/Variants.ConditionalAppend | Command 'ConditionalAppend' is not linked to event 'Happened' with produces because the append only happens on some paths through the handler; the model references the handler instead +diagnostic WOLVERINE0025 Warning Unknown | Variants/Links.cs:69:30 | subject=dotnet://Variants/Variants/Variants.ConditionalComputed | Command 'ConditionalComputed' is not linked to event 'Happened' with produces because the stream identity is not read directly from a property of the command; the model references the handler instead +diagnostic WOLVERINE0025 Warning Unsupported | Variants/Links.cs:46:9 | subject=dotnet://Variants/Variants/Variants.EarlyExitAppend | Command 'EarlyExitAppend' is not linked to event 'Happened' with produces because the append only happens on some paths through the handler; the model references the handler instead +diagnostic WOLVERINE0025 Warning Unknown | Variants/Links.cs:99:9 | subject=dotnet://Variants/Variants/Variants.IndirectAppend | Command 'IndirectAppend' is not linked to event 'Happened' with produces because the appended payload is not a direct event creation; the model references the handler instead +diagnostic WOLVERINE0025 Warning Unsupported | Variants/Links.cs:89:45 | subject=dotnet://Variants/Variants/Variants.LambdaAppend | Command 'LambdaAppend' is not linked to event 'Happened' with produces because the append only happens on some paths through the handler; the model references the handler instead +diagnostic WOLVERINE0025 Warning Unsupported | Variants/Links.cs:131:56 | subject=dotnet://Variants/Variants/Variants.LoopAppend | Command 'LoopAppend' is not linked to event 'Happened' with produces because the append only happens on some paths through the handler; the model references the handler instead +diagnostic WOLVERINE0025 Warning Unknown | Variants/Links.cs:138:9 | subject=dotnet://Variants/Variants/Variants.OtherParameterAppend | Command 'OtherParameterAppend' is not linked to event 'Happened' with produces because the stream identity is not read directly from a property of the command; the model references the handler instead +diagnostic WOLVERINE0022 Warning Unsupported | Variants/Patterns.cs:56:6 | subject=dotnet://Variants/Variants/Variants.PatternMissingHandler#method:M%3AVariants.PatternMissingHandler.Handle%28Variants.PatternMissing%2CMarten.IDocumentSession%29 | The SlicePattern declaration on 'PatternMissingHandler.Handle(PatternMissing, Marten.IDocumentSession)' has an undefined value; independently inferred classification is retained +diagnostic WOLVERINE0025 Warning Unknown | Variants/Patterns.cs:57:91 | subject=dotnet://Variants/Variants/Variants.PatternMissing | Command 'PatternMissing' is not linked to event 'Another' with produces because the stream identity is not read directly from a property of the command; the model references the handler instead +diagnostic WOLVERINE0025 Warning Unknown | Variants/Patterns.cs:39:96 | subject=dotnet://Variants/Variants/Variants.PatternTimeoutAgree | Command 'PatternTimeoutAgree' is not linked to event 'Another' with produces because the stream identity is not read directly from a property of the command; the model references the handler instead +diagnostic WOLVERINE0023 Warning Conflict | Variants/Patterns.cs:32:6 | subject=dotnet://Variants/Variants/Variants.PatternTimeoutHandler#method:M%3AVariants.PatternTimeoutHandler.Handle%28Variants.PatternTimeoutMessage%2CMarten.IDocumentSession%29 | The SlicePattern declaration 'Translate' conflicts with the proven TimeoutMessage trigger 'Automation' on 'PatternTimeoutHandler.Handle(PatternTimeoutMessage, Marten.IDocumentSession)'; the trigger takes precedence +diagnostic WOLVERINE0025 Warning Unknown | Variants/Patterns.cs:33:98 | subject=dotnet://Variants/Variants/Variants.PatternTimeoutMessage | Command 'PatternTimeoutMessage' is not linked to event 'Happened' with produces because the stream identity is not read directly from a property of the command; the model references the handler instead +diagnostic WOLVERINE0025 Warning Unknown | Variants/Patterns.cs:45:94 | subject=dotnet://Variants/Variants/Variants.PatternTranslated | Command 'PatternTranslated' is not linked to event 'Third' with produces because the stream identity is not read directly from a property of the command; the model references the handler instead +diagnostic WOLVERINE0024 Warning Unsupported | Variants/Patterns.cs:50:6 | subject=dotnet://Variants/Variants/Variants.PatternUnplacedHandler#method:M%3AVariants.PatternUnplacedHandler.Handle%28Variants.PatternUnplaced%29 | The SlicePattern declaration on 'PatternUnplacedHandler.Handle(PatternUnplaced)' cannot obtain a supported artifact placement; classification does not invent handler behavior +diagnostic WOLVERINE0025 Warning Unknown | Variants/Links.cs:76:9 | subject=dotnet://Variants/Variants/Variants.StartedAppend | Command 'StartedAppend' is not linked to event 'Happened' with produces because the stream identity is assigned by Marten rather than read from the command; the model references the handler instead +diagnostic WOLVERINE0025 Warning Unsupported | Variants/Links.cs:122:22 | subject=dotnet://Variants/Variants/Variants.SwitchAppend | Command 'SwitchAppend' is not linked to event 'Another' with produces because the append only happens on some paths through the handler; the model references the handler instead +diagnostic WOLVERINE0025 Warning Unsupported | Variants/Links.cs:121:21 | subject=dotnet://Variants/Variants/Variants.SwitchAppend | Command 'SwitchAppend' is not linked to event 'Happened' with produces because the append only happens on some paths through the handler; the model references the handler instead +diagnostic WOLVERINE0025 Warning Unsupported | Variants/Yields.cs:66:9 | subject=dotnet://Variants/Variants/Variants.YieldAfterGuard | Command 'YieldAfterGuard' is not linked to event 'Another' with produces because the handler yields it only on some paths, and the condition is not represented; the model references the handler instead +diagnostic WOLVERINE0025 Warning Unsupported | Variants/Yields.cs:37:27 | subject=dotnet://Variants/Variants/Variants.YieldConditional | Command 'YieldConditional' is not linked to event 'Happened' with produces because the handler yields it only on some paths, and the condition is not represented; the model references the handler instead +diagnostic WOLVERINE0025 Warning Unsupported | Variants/Yields.cs:46:27 | subject=dotnet://Variants/Variants/Variants.YieldConditionalNoIdentity | Command 'YieldConditionalNoIdentity' is not linked to event 'Happened' with produces because the handler yields it only on some paths, and the condition is not represented; the model references the handler instead +diagnostic WOLVERINE0025 Warning Unknown | Variants/Yields.cs:56:9 | subject=dotnet://Variants/Variants/Variants.YieldIndirect | Command 'YieldIndirect' is not linked to event 'Happened' with produces because the yielded value is not a direct event creation; the model references the handler instead +diagnostic WOLVERINE0025 Warning Unknown | Variants/Yields.cs:28:9 | subject=dotnet://Variants/Variants/Variants.YieldNoIdentity | Command 'YieldNoIdentity' is not linked to event 'Happened' with produces because the aggregate stream identity is not a command property; the model references the handler instead +diagnostic WOLVERINE0002 Information Unsupported | Variants/Endpoints.cs:14:5 | subject=dotnet://Variants/Variants/Variants.PostReply | HTTP POST route '/reply' for 'PostReply' is not represented by the current Screenplay language +diagnostic WOLVERINE0002 Information Unsupported | Variants/Endpoints.cs:15:5 | subject=dotnet://Variants/Variants/Variants.PostEmpty | HTTP POST route '/empty' for 'PostEmpty' is not represented by the current Screenplay language +diagnostic WOLVERINE0002 Information Unsupported | Variants/Endpoints.cs:16:5 | subject=dotnet://Variants/Variants/Variants.PostTuple | HTTP POST route '/tuple' for 'PostTuple' is not represented by the current Screenplay language +diagnostic WOLVERINE0002 Information Unsupported | Variants/Endpoints.cs:17:5 | subject=dotnet://Variants/Variants/Variants.PostResult | HTTP POST route '/result' for 'PostResult' is not represented by the current Screenplay language +diagnostic WOLVERINE0002 Information Unsupported | Variants/Endpoints.cs:18:5 | subject=dotnet://Variants/Variants/Variants.PostCreation | HTTP POST route '/creation' for 'PostCreation' is not represented by the current Screenplay language +diagnostic WOLVERINE0002 Information Unsupported | Variants/Endpoints.cs:19:5 | subject=dotnet://Variants/Variants/Variants.PostOutgoing | HTTP POST route '/outgoing' for 'PostOutgoing' is not represented by the current Screenplay language +diagnostic WOLVERINE0002 Information Unsupported | Variants/Endpoints.cs:20:5 | subject=dotnet://Variants/Variants/Variants.PostVoid | HTTP POST route '/void' for 'PostVoid' is not represented by the current Screenplay language +diagnostic WOLVERINE0023 Warning Conflict | Variants/Patterns.cs:17:48 | subject=dotnet://Variants/Variants/Variants.PatternEndpoints#method:M%3AVariants.PatternEndpoints.Post%28Variants.PatternHttpPost%2CMarten.IDocumentSession%29 | The SlicePattern declaration 'StateView' conflicts with the proven HTTP trigger 'StateChange' on 'PatternEndpoints.Post(PatternHttpPost, Marten.IDocumentSession)'; the trigger takes precedence +diagnostic WOLVERINE0025 Warning Unknown | Variants/Patterns.cs:18:90 | subject=dotnet://Variants/Variants/Variants.PatternHttpPost | Command 'PatternHttpPost' is not linked to event 'Happened' with produces because the stream identity is not read directly from a property of the command; the model references the handler instead +diagnostic WOLVERINE0002 Information Unsupported | Variants/Patterns.cs:17:5 | subject=dotnet://Variants/Variants/Variants.PatternHttpPost | HTTP POST route '/pattern' for 'PatternHttpPost' is not represented by the current Screenplay language +diagnostic WOLVERINE0023 Warning Conflict | Variants/Patterns.cs:20:51 | subject=dotnet://Variants/Variants/Variants.PatternEndpoints#method:M%3AVariants.PatternEndpoints.Get | The SlicePattern declaration 'StateChange' conflicts with the proven HTTP trigger 'StateView' on 'PatternEndpoints.Get()'; the trigger takes precedence +diagnostic WOLVERINE0002 Information Unsupported | Variants/Patterns.cs:20:5 | subject=dotnet://Variants/Variants/Variants.PatternEndpoints#method:M%3AVariants.PatternEndpoints.Get:query | HTTP GET route '/pattern-get' for query 'Get' is not represented by the current Screenplay language +diagnostic WOLVERINE0002 Information Unsupported | Variants/Patterns.cs:23:5 | subject=dotnet://Variants/Variants/Variants.PatternEndpoints#method:M%3AVariants.PatternEndpoints.Head:command | HTTP HEAD route '/pattern-head' for 'Head' is not represented by the current Screenplay language +diagnostic WOLVERINE0025 Warning Unknown | Variants/Patterns.cs:27:91 | subject=dotnet://Variants/Variants/Variants.PatternAgreeing | Command 'PatternAgreeing' is not linked to event 'Another' with produces because the stream identity is not read directly from a property of the command; the model references the handler instead +diagnostic WOLVERINE0002 Information Unsupported | Variants/Patterns.cs:26:5 | subject=dotnet://Variants/Variants/Variants.PatternAgreeing | HTTP POST route '/pattern-agree' for 'PatternAgreeing' is not represented by the current Screenplay language diff --git a/Source/DotNET/CritterStack.Specs/Golden/wolverine-saga-variants.txt b/Source/DotNET/CritterStack.Specs/Golden/wolverine-saga-variants.txt new file mode 100644 index 0000000..93ab1af --- /dev/null +++ b/Source/DotNET/CritterStack.Specs/Golden/wolverine-saga-variants.txt @@ -0,0 +1,137 @@ +diagnostic WOLVERINE0016|Information|Unsupported|~AlphaSaga|Saga/Alpha.cs:9:1-12:2|Saga 'AlphaSaga' +diagnostic WOLVERINE0016|Information|Unsupported|~AttrFromSaga|Saga/Correlation.cs:12:1-12:123|Saga 'AttrFromSaga' +diagnostic WOLVERINE0017|Information|Unknown|~AttributeAmbiguousSaga#method:M%3AVariants.AttributeAmbiguousSaga.Handle%28Variants.AttributeAmbiguousMessage%29|Saga/Correlation.cs:9:56-9:113|Saga handler for 'AttributeAmbiguousSaga' cannot safely select an authored correlation member for 'AttributeAmbiguousMessage' because multiple public [SagaIdentity] members are visible; correlation remains runtime-resolved +diagnostic WOLVERINE0016|Information|Unsupported|~AttributeAmbiguousSaga|Saga/Correlation.cs:9:1-9:115|Saga 'AttributeAmbiguousSaga' +diagnostic WOLVERINE0016|Information|Unsupported|~AttributeSaga|Saga/Correlation.cs:6:1-6:97|Saga 'AttributeSaga' +diagnostic WOLVERINE0018|Warning|Unknown|~ChainSaga#method:M%3AVariants.ChainSaga.Starts%28Variants.NoChainMessage%29|Saga/Roles.cs:34:5-34:69|Wolverine saga role 'ChainSaga.Starts(NoChainMessage)' was not admitted because no other exact lifecycle method for the same message establishes a Wolverine SagaChain +diagnostic WOLVERINE0016|Information|Unsupported|~ChainSaga|Saga/Roles.cs:32:1-37:2|Saga 'ChainSaga' +diagnostic WOLVERINE0016|Information|Unsupported|~CompletionSaga|Saga/Completion.cs:8:58-8:73|Saga 'CompletionSaga' invokes Wolverine.Saga.MarkCompleted() within Wolverine-managed lifecycle. Authored source does not safely establish portable conditional completion or a portable domain workflow; neutral Saga/Handler/Handles facts are retained as realization/provenance, and Screenplay uses ordinary Event Modeling building blocks +diagnostic WOLVERINE0018|Warning|Unknown|~CtorNotFoundSaga#method:M%3AVariants.CtorNotFoundSaga.NotFound%28Variants.CtorNotFoundAloneMessage%29|Saga/Roles.cs:68:5-68:70|Wolverine saga role 'CtorNotFoundSaga.NotFound(CtorNotFoundAloneMessage)' was not admitted because Wolverine must create saga state, but the saga has no accessible public parameterless constructor and no exact returned saga supplies creation +diagnostic WOLVERINE0016|Information|Unsupported|~CtorNotFoundSaga|Saga/Roles.cs:65:1-71:2|Saga 'CtorNotFoundSaga' +diagnostic WOLVERINE0018|Warning|Unknown|~CtorStartOrHandleSaga#method:M%3AVariants.CtorStartOrHandleSaga.StartOrHandle%28Variants.CtorAloneMessage%29|Saga/Roles.cs:46:5-46:60|Wolverine saga role 'CtorStartOrHandleSaga.StartOrHandle(CtorAloneMessage)' was not admitted because Wolverine must create saga state, but the saga has no accessible public parameterless constructor and no exact returned saga supplies creation +diagnostic WOLVERINE0018|Warning|Unknown|~CtorStartOrHandleSaga#method:M%3AVariants.CtorStartOrHandleSaga.StartOrHandle%28Variants.CtorMixedMessage%29|Saga/Roles.cs:44:5-44:60|Wolverine saga role 'CtorStartOrHandleSaga.StartOrHandle(CtorMixedMessage)' was not admitted because Wolverine must create saga state, but the saga has no accessible public parameterless constructor and no exact returned saga supplies creation +diagnostic WOLVERINE0016|Information|Unsupported|~CtorStartOrHandleSaga|Saga/Roles.cs:41:1-47:2|Saga 'CtorStartOrHandleSaga' +diagnostic WOLVERINE0018|Warning|Unknown|~CtorStartSaga#method:M%3AVariants.CtorStartSaga.Start%28Variants.CtorOrchestratedMessage%29|Saga/Roles.cs:58:5-58:81|Wolverine saga role 'CtorStartSaga.Start(CtorOrchestratedMessage)' was not admitted because Wolverine must create saga state, but the saga has no accessible public parameterless constructor and no exact returned saga supplies creation +diagnostic WOLVERINE0018|Warning|Unknown|~CtorStartSaga#method:M%3AVariants.CtorStartSaga.Start%28Variants.CtorVoidMessage%29|Saga/Roles.cs:57:5-57:58|Wolverine saga role 'CtorStartSaga.Start(CtorVoidMessage)' was not admitted because Wolverine must create saga state, but the saga has no accessible public parameterless constructor and no exact returned saga supplies creation +diagnostic WOLVERINE0016|Information|Unsupported|~CtorStartSaga|Saga/Roles.cs:53:1-61:2|Saga 'CtorStartSaga' +diagnostic WOLVERINE0017|Information|Unknown|~FieldBeforePropertySaga#method:M%3AVariants.FieldBeforePropertySaga.Handle%28Variants.FieldBeforePropertyMessage%29|Saga/Correlation.cs:79:57-79:115|Saga handler for 'FieldBeforePropertySaga' cannot safely select an authored correlation member for 'FieldBeforePropertyMessage' because multiple public members match Wolverine convention 'SagaId'; correlation remains runtime-resolved +diagnostic WOLVERINE0016|Information|Unsupported|~FieldBeforePropertySaga|Saga/Correlation.cs:79:1-79:117|Saga 'FieldBeforePropertySaga' +diagnostic WOLVERINE0016|Information|Unsupported|~FieldSaga|Saga/Correlation.cs:75:1-75:89|Saga 'FieldSaga' +diagnostic WOLVERINE0017|Information|Unknown|~FromAmbiguousSaga#method:M%3AVariants.FromAmbiguousSaga.Handle%28Variants.FromAmbiguousMessage%29|Saga/Correlation.cs:36:51-36:132|Saga handler for 'FromAmbiguousSaga' cannot safely select an authored correlation member for 'FromAmbiguousMessage' because multiple public members match explicit [SagaIdentityFrom] name 'Picked'; correlation remains runtime-resolved +diagnostic WOLVERINE0016|Information|Unsupported|~FromAmbiguousSaga|Saga/Correlation.cs:36:1-36:134|Saga 'FromAmbiguousSaga' +diagnostic WOLVERINE0017|Information|Unknown|~FromConflictSaga#method:M%3AVariants.FromConflictSaga.Handle%28Variants.FromConflictMessage%2CSystem.Int32%29|Saga/Correlation.cs:20:5-20:101|Saga handler for 'FromConflictSaga' cannot safely select an authored correlation member for 'FromConflictMessage' because conflicting [SagaIdentityFrom] member names are declared across parameters in the same message chain; correlation remains runtime-resolved +diagnostic WOLVERINE0017|Information|Unknown|~FromConflictSaga#method:M%3AVariants.FromConflictSaga.Handles%28Variants.FromConflictMessage%2CSystem.String%29|Saga/Correlation.cs:21:5-21:103|Saga handler for 'FromConflictSaga' cannot safely select an authored correlation member for 'FromConflictMessage' because conflicting [SagaIdentityFrom] member names are declared across parameters in the same message chain; correlation remains runtime-resolved +diagnostic WOLVERINE0016|Information|Unsupported|~FromConflictSaga|Saga/Correlation.cs:18:1-22:2|Saga 'FromConflictSaga' +diagnostic WOLVERINE0016|Information|Unsupported|~FromMissingSaga|Saga/Correlation.cs:32:1-32:128|Saga 'FromMissingSaga' +diagnostic WOLVERINE0016|Information|Unsupported|~FromRepeatedSaga|Saga/Correlation.cs:25:1-29:2|Saga 'FromRepeatedSaga' +diagnostic WOLVERINE0016|Information|Unsupported|~FromSaga|Saga/Correlation.cs:15:1-15:116|Saga 'FromSaga' +diagnostic WOLVERINE0017|Information|Unknown|~FullAmbiguousSaga#method:M%3AVariants.FullAmbiguousSaga.Handle%28Variants.FullAmbiguousMessage%29|Saga/Correlation.cs:43:51-43:103|Saga handler for 'FullAmbiguousSaga' cannot safely select an authored correlation member for 'FullAmbiguousMessage' because multiple public members match Wolverine convention 'FullAmbiguousSagaId'; correlation remains runtime-resolved +diagnostic WOLVERINE0016|Information|Unsupported|~FullAmbiguousSaga|Saga/Correlation.cs:43:1-43:105|Saga 'FullAmbiguousSaga' +diagnostic WOLVERINE0016|Information|Unsupported|~FullNameSaga|Saga/Correlation.cs:39:1-39:95|Saga 'FullNameSaga' +diagnostic WOLVERINE0016|Information|Unsupported|~GeneratedMemberSaga|Saga/Correlation.cs:82:1-82:109|Saga 'GeneratedMemberSaga' +diagnostic WOLVERINE0017|Information|Unknown|~IdAmbiguousSaga#method:M%3AVariants.IdAmbiguousSaga.Handle%28Variants.IdAmbiguousMessage%29|Saga/Correlation.cs:66:49-66:99|Saga handler for 'IdAmbiguousSaga' cannot safely select an authored correlation member for 'IdAmbiguousMessage' because multiple public members match Wolverine's case-insensitive Id convention; correlation remains runtime-resolved +diagnostic WOLVERINE0016|Information|Unsupported|~IdAmbiguousSaga|Saga/Correlation.cs:66:1-66:101|Saga 'IdAmbiguousSaga' +diagnostic WOLVERINE0016|Information|Unsupported|~IdSaga|Saga/Correlation.cs:63:1-63:83|Saga 'IdSaga' +diagnostic WOLVERINE0016|Information|Unsupported|~MemberKindsSaga|Saga/Correlation.cs:72:1-72:101|Saga 'MemberKindsSaga' +diagnostic WOLVERINE0017|Information|Unknown|~NoCorrelationSaga#method:M%3AVariants.NoCorrelationSaga.Handle%28Variants.NoCorrelationMessage%29|Saga/Correlation.cs:69:51-69:103|Saga handler 'NoCorrelationSaga.Handle' has no authored message correlation member for 'NoCorrelationMessage'; correlation remains runtime-resolved +diagnostic WOLVERINE0016|Information|Unsupported|~NoCorrelationSaga|Saga/Correlation.cs:69:1-69:105|Saga 'NoCorrelationSaga' +diagnostic WOLVERINE0016|Information|Unsupported|~NotCompletedSaga|Saga/Completion.cs:13:1-16:2|Saga 'NotCompletedSaga' +diagnostic WOLVERINE0016|Information|Unsupported|~OrdersagaSaga|Saga/Correlation.cs:49:1-49:104|Saga 'OrdersagaSaga' +diagnostic WOLVERINE0018|Warning|Unknown|~RoleShapesSaga#method:M%3AVariants.RoleShapesSaga.Handle%28Variants.RoleMessage%29|Saga/Roles.cs:10:5-10:49|Wolverine saga role 'RoleShapesSaga.Handle(RoleMessage)' was not admitted because the method must be a public, ordinary, non-generic method with a message parameter +diagnostic WOLVERINE0018|Warning|Unknown|~RoleShapesSaga#method:M%3AVariants.RoleShapesSaga.Handle%60%601%28Variants.RoleMessage%29|Saga/Roles.cs:11:5-11:51|Wolverine saga role 'RoleShapesSaga.Handle`1(RoleMessage)' was not admitted because the method must be a public, ordinary, non-generic method with a message parameter +diagnostic WOLVERINE0018|Warning|Unknown|~RoleShapesSaga#method:M%3AVariants.RoleShapesSaga.Handle|Saga/Roles.cs:12:5-12:29|Wolverine saga role 'RoleShapesSaga.Handle()' was not admitted because the method must be a public, ordinary, non-generic method with a message parameter +diagnostic WOLVERINE0018|Warning|Unknown|~RoleShapesSaga#method:M%3AVariants.RoleShapesSaga.Handle%28Variants.SecondRoleMessage%29|Saga/Roles.cs:13:5-13:55|Wolverine saga role 'RoleShapesSaga.Handle(SecondRoleMessage)' was not admitted because Wolverine handler discovery rejects methods that directly return a primitive type +diagnostic WOLVERINE0018|Warning|Unknown|~RoleShapesSaga#method:M%3AVariants.RoleShapesSaga.Handles%28System.Object%29|Saga/Roles.cs:14:5-14:44|Wolverine saga role 'RoleShapesSaga.Handles(object)' was not admitted because the first parameter must be an authored non-saga message payload +diagnostic WOLVERINE0018|Warning|Unknown|~RoleShapesSaga#method:M%3AVariants.RoleShapesSaga.Handles%28System.Int32%29|Saga/Roles.cs:15:5-15:41|Wolverine saga role 'RoleShapesSaga.Handles(int)' was not admitted because the first parameter must be an authored non-saga message payload +diagnostic WOLVERINE0018|Warning|Unknown|~RoleShapesSaga#method:M%3AVariants.RoleShapesSaga.Handles%28System.String%29|Saga/Roles.cs:16:5-16:44|Wolverine saga role 'RoleShapesSaga.Handles(string)' was not admitted because the first parameter must be an authored non-saga message payload +diagnostic WOLVERINE0018|Warning|Unknown|~RoleShapesSaga#method:M%3AVariants.RoleShapesSaga.Consume%28System.Guid%5B%5D%29|Saga/Roles.cs:17:5-17:51|Wolverine saga role 'RoleShapesSaga.Consume(System.Guid[])' was not admitted because the first parameter must be an authored non-saga message payload +diagnostic WOLVERINE0018|Warning|Unknown|~RoleShapesSaga#method:M%3AVariants.RoleShapesSaga.Consumes%28Variants.RoleShapesSaga%29|Saga/Roles.cs:18:5-18:53|Wolverine saga role 'RoleShapesSaga.Consumes(RoleShapesSaga)' was not admitted because the first parameter must be an authored non-saga message payload +diagnostic WOLVERINE0018|Warning|Unknown|~RoleShapesSaga#method:M%3AVariants.RoleShapesSaga.Orchestrate%28Variants.RoleMessage%29|Saga/Roles.cs:19:5-19:60|Wolverine saga role 'RoleShapesSaga.Orchestrate(RoleMessage)' was not admitted because an existing-state role must be an instance method +diagnostic WOLVERINE0018|Warning|Unknown|~RoleShapesSaga#method:M%3AVariants.RoleShapesSaga.StartOrHandle%28Variants.RoleMessage%29|Saga/Roles.cs:20:5-20:62|Wolverine saga role 'RoleShapesSaga.StartOrHandle(RoleMessage)' was not admitted because a start-or-handle role must be an instance method +diagnostic WOLVERINE0018|Warning|Unknown|~RoleShapesSaga#method:M%3AVariants.RoleShapesSaga.NotFound%28Variants.RoleMessage%29|Saga/Roles.cs:21:5-21:50|Wolverine saga role 'RoleShapesSaga.NotFound(RoleMessage)' was not admitted because a not-found role must be a static method +diagnostic WOLVERINE0018|Warning|Unknown|~RoleShapesSaga#method:M%3AVariants.RoleShapesSaga.Orchestrates%28System.Collections.Generic.List%7BVariants.RoleMessage%7D%29|Saga/Roles.cs:22:5-22:87|Wolverine saga role 'RoleShapesSaga.Orchestrates(System.Collections.Generic.List)' was not admitted because the first parameter must be an authored non-saga message payload +diagnostic WOLVERINE0016|Information|Unsupported|~RoleShapesSaga|Saga/Roles.cs:6:1-28:2|Saga 'RoleShapesSaga' +diagnostic WOLVERINE0017|Information|Unknown|~SagaIdAmbiguousSaga#method:M%3AVariants.SagaIdAmbiguousSaga.Handle%28Variants.SagaIdAmbiguousMessage%29|Saga/Correlation.cs:60:53-60:107|Saga handler for 'SagaIdAmbiguousSaga' cannot safely select an authored correlation member for 'SagaIdAmbiguousMessage' because multiple public members match Wolverine convention 'SagaId'; correlation remains runtime-resolved +diagnostic WOLVERINE0016|Information|Unsupported|~SagaIdAmbiguousSaga|Saga/Correlation.cs:60:1-60:109|Saga 'SagaIdAmbiguousSaga' +diagnostic WOLVERINE0016|Information|Unsupported|~SagaIdConventionSaga|Saga/Correlation.cs:56:1-56:111|Saga 'SagaIdConventionSaga' +diagnostic WOLVERINE0017|Information|Unknown|~StrippedAmbiguousSaga#method:M%3AVariants.StrippedAmbiguousSaga.Handle%28Variants.StrippedAmbiguousMessage%29|Saga/Correlation.cs:53:55-53:111|Saga handler for 'StrippedAmbiguousSaga' cannot safely select an authored correlation member for 'StrippedAmbiguousMessage' because multiple public members match Wolverine convention 'StrippedAmbiguousId'; correlation remains runtime-resolved +diagnostic WOLVERINE0016|Information|Unsupported|~StrippedAmbiguousSaga|Saga/Correlation.cs:53:1-53:113|Saga 'StrippedAmbiguousSaga' +diagnostic WOLVERINE0016|Information|Unsupported|~StrippedSaga|Saga/Correlation.cs:46:1-46:95|Saga 'StrippedSaga' +diagnostic WOLVERINE0001|Warning|Unsupported|~TimeoutSaga#method:M%3AVariants.TimeoutSaga.Handle%28Variants.TimeoutMessageIn%29|Saga/Completion.cs:23:5-23:106|Saga handler 'TimeoutSaga.Handle' returns timeout message 'ReminderTimeout', whose delayed delivery cannot be represented by the current Screenplay language +diagnostic WOLVERINE0001|Warning|Unsupported|~TimeoutSaga#method:M%3AVariants.TimeoutSaga.Handle%28Variants.TimeoutMessageIn%29|Saga/Completion.cs:23:5-23:106|Saga handler 'TimeoutSaga.Handle' returns timeout message 'ExpiryTimeout', whose delayed delivery cannot be represented by the current Screenplay language +diagnostic WOLVERINE0016|Information|Unsupported|~TimeoutSaga|Saga/Completion.cs:21:1-24:2|Saga 'TimeoutSaga' +diagnostic WOLVERINE0016|Information|Unsupported|~ZetaSaga|Saga/Alpha.cs:3:1-7:2|Saga 'ZetaSaga' +saga wolverine:saga:~AlphaSaga| +relationship ~AlphaSaga#method:M%3AVariants.AlphaSaga.Handle%28Variants.OrderAMessage%29:~OrderAMessage:wolverine:saga:orchestrate|Conventional|Wolverine saga correlation member convention 'SagaId'|Saga/Zeta.cs:4:29-4:47|sagaId +saga wolverine:saga:~AttrFromSaga| +relationship ~AttrFromSaga#method:M%3AVariants.AttrFromSaga.Handle%28Variants.AttrFromMessage%29:~AttrFromMessage:wolverine:saga:orchestrate|Exact|Exact Wolverine [SagaIdentity] message member|Saga/Correlation.cs:11:32-11:86|chosen +saga wolverine:saga:~AttributeAmbiguousSaga| +relationship ~AttributeAmbiguousSaga#method:M%3AVariants.AttributeAmbiguousSaga.Handle%28Variants.AttributeAmbiguousMessage%29:~AttributeAmbiguousMessage:wolverine:saga:orchestrate|Conventional|Authored Wolverine saga orchestrate role method|Saga/Correlation.cs:9:56-9:113| +saga wolverine:saga:~AttributeSaga| +relationship ~AttributeSaga#method:M%3AVariants.AttributeSaga.Handle%28Variants.AttributeMessage%29:~AttributeMessage:wolverine:saga:orchestrate|Exact|Exact Wolverine [SagaIdentity] message member|Saga/Correlation.cs:5:33-5:87|chosen +saga wolverine:saga:~ChainSaga| +relationship ~ChainSaga#method:M%3AVariants.ChainSaga.Starts%28Variants.ChainedMessage%29:~ChainedMessage:wolverine:saga:start|Conventional|Wolverine saga correlation member convention 'SagaId'|Saga/Roles.cs:31:30-31:48|sagaId +relationship ~ChainSaga#method:M%3AVariants.ChainSaga.Handles%28Variants.ChainedMessage%29:~ChainedMessage:wolverine:saga:orchestrate|Conventional|Wolverine saga correlation member convention 'SagaId'|Saga/Roles.cs:31:30-31:48|sagaId +saga wolverine:saga:~CompletionSaga| +relationship ~CompletionSaga#method:M%3AVariants.CompletionSaga.Handle%28Variants.CompletionFirstMessage%29:~CompletionFirstMessage:wolverine:saga:orchestrate|Conventional|Wolverine saga correlation member convention 'SagaId'|Saga/Completion.cs:3:38-3:56|sagaId +relationship ~CompletionSaga#method:M%3AVariants.CompletionSaga.Handle%28Variants.CompletionSecondMessage%29:~CompletionSecondMessage:wolverine:saga:orchestrate|Conventional|Wolverine saga correlation member convention 'SagaId'|Saga/Completion.cs:4:39-4:57|sagaId +saga wolverine:saga:~CtorNotFoundSaga| +relationship ~CtorNotFoundSaga#method:M%3AVariants.CtorNotFoundSaga.NotFound%28Variants.CtorNotFoundHandledMessage%29:~CtorNotFoundHandledMessage:wolverine:saga:not-found|Conventional|Wolverine saga correlation member convention 'SagaId'|Saga/Roles.cs:64:42-64:60|sagaId +relationship ~CtorNotFoundSaga#method:M%3AVariants.CtorNotFoundSaga.Handle%28Variants.CtorNotFoundHandledMessage%29:~CtorNotFoundHandledMessage:wolverine:saga:orchestrate|Conventional|Wolverine saga correlation member convention 'SagaId'|Saga/Roles.cs:64:42-64:60|sagaId +saga wolverine:saga:~CtorStartOrHandleSaga| +relationship ~CtorStartOrHandleSaga#method:M%3AVariants.CtorStartOrHandleSaga.Handle%28Variants.CtorMixedMessage%29:~CtorMixedMessage:wolverine:saga:orchestrate|Conventional|Wolverine saga correlation member convention 'SagaId'|Saga/Roles.cs:39:32-39:50|sagaId +saga wolverine:saga:~CtorStartSaga| +relationship ~CtorStartSaga#method:M%3AVariants.CtorStartSaga.StartAsync%28Variants.CtorAsyncMessage%29:~CtorAsyncMessage:wolverine:saga:start|Conventional|Wolverine saga correlation member convention 'SagaId'|Saga/Roles.cs:52:32-52:50|sagaId +relationship ~CtorStartSaga#method:M%3AVariants.CtorStartSaga.Handle%28Variants.CtorOrchestratedMessage%29:~CtorOrchestratedMessage:wolverine:saga:orchestrate|Conventional|Wolverine saga correlation member convention 'SagaId'|Saga/Roles.cs:51:39-51:57|sagaId +relationship ~CtorStartSaga#method:M%3AVariants.CtorStartSaga.Start%28Variants.CtorReturnedMessage%29:~CtorReturnedMessage:wolverine:saga:start|Conventional|Wolverine saga correlation member convention 'SagaId'|Saga/Roles.cs:49:35-49:53|sagaId +saga wolverine:saga:~FieldBeforePropertySaga| +relationship ~FieldBeforePropertySaga#method:M%3AVariants.FieldBeforePropertySaga.Handle%28Variants.FieldBeforePropertyMessage%29:~FieldBeforePropertyMessage:wolverine:saga:orchestrate|Conventional|Authored Wolverine saga orchestrate role method|Saga/Correlation.cs:79:57-79:115| +saga wolverine:saga:~FieldSaga| +relationship ~FieldSaga#method:M%3AVariants.FieldSaga.Handle%28Variants.FieldMessage%29:~FieldMessage:wolverine:saga:orchestrate|Conventional|Wolverine saga correlation member convention 'FieldSagaId'|Saga/Correlation.cs:74:48-74:59|fieldSagaId +saga wolverine:saga:~FromAmbiguousSaga| +relationship ~FromAmbiguousSaga#method:M%3AVariants.FromAmbiguousSaga.Handle%28Variants.FromAmbiguousMessage%29:~FromAmbiguousMessage:wolverine:saga:orchestrate|Conventional|Authored Wolverine saga orchestrate role method|Saga/Correlation.cs:36:51-36:132| +saga wolverine:saga:~FromConflictSaga| +relationship ~FromConflictSaga#method:M%3AVariants.FromConflictSaga.Handle%28Variants.FromConflictMessage%2CSystem.Int32%29:~FromConflictMessage:wolverine:saga:orchestrate|Exact|Exact Wolverine [SagaIdentityFrom] handler parameter|Saga/Correlation.cs:20:53-20:96| +relationship ~FromConflictSaga#method:M%3AVariants.FromConflictSaga.Handles%28Variants.FromConflictMessage%2CSystem.String%29:~FromConflictMessage:wolverine:saga:orchestrate|Exact|Exact Wolverine [SagaIdentityFrom] handler parameter|Saga/Correlation.cs:20:53-20:96| +saga wolverine:saga:~FromMissingSaga| +relationship ~FromMissingSaga#method:M%3AVariants.FromMissingSaga.Handle%28Variants.FromMissingMessage%29:~FromMissingMessage:wolverine:saga:orchestrate|Conventional|Wolverine saga correlation member convention 'FromMissingId'|Saga/Correlation.cs:31:86-31:132|fromMissingId +saga wolverine:saga:~FromRepeatedSaga| +relationship ~FromRepeatedSaga#method:M%3AVariants.FromRepeatedSaga.Handle%28Variants.FromRepeatedMessage%2CSystem.Int32%29:~FromRepeatedMessage:wolverine:saga:orchestrate|Exact|Exact Wolverine [SagaIdentityFrom] handler parameter|Saga/Correlation.cs:27:53-27:94|primary +relationship ~FromRepeatedSaga#method:M%3AVariants.FromRepeatedSaga.Handles%28Variants.FromRepeatedMessage%2CSystem.String%29:~FromRepeatedMessage:wolverine:saga:orchestrate|Exact|Exact Wolverine [SagaIdentityFrom] handler parameter|Saga/Correlation.cs:27:53-27:94|primary +saga wolverine:saga:~FromSaga| +relationship ~FromSaga#method:M%3AVariants.FromSaga.Handle%28Variants.FromMessage%29:~FromMessage:wolverine:saga:orchestrate|Exact|Exact Wolverine [SagaIdentityFrom] handler parameter|Saga/Correlation.cs:15:61-15:109|picked +saga wolverine:saga:~FullAmbiguousSaga| +relationship ~FullAmbiguousSaga#method:M%3AVariants.FullAmbiguousSaga.Handle%28Variants.FullAmbiguousMessage%29:~FullAmbiguousMessage:wolverine:saga:orchestrate|Conventional|Authored Wolverine saga orchestrate role method|Saga/Correlation.cs:43:51-43:103| +saga wolverine:saga:~FullNameSaga| +relationship ~FullNameSaga#method:M%3AVariants.FullNameSaga.Handle%28Variants.FullNameMessage%29:~FullNameMessage:wolverine:saga:orchestrate|Conventional|Wolverine saga correlation member convention 'FullNameSagaId'|Saga/Correlation.cs:38:32-38:79|fullNameSagaId +saga wolverine:saga:~GeneratedMemberSaga| +relationship ~GeneratedMemberSaga#method:M%3AVariants.GeneratedMemberSaga.Handle%28Variants.GeneratedMemberMessage%29:~GeneratedMemberMessage:wolverine:saga:orchestrate|Conventional|Wolverine case-insensitive Id saga correlation convention|Saga/Correlation.cs:81:47-81:82|id +saga wolverine:saga:~IdAmbiguousSaga| +relationship ~IdAmbiguousSaga#method:M%3AVariants.IdAmbiguousSaga.Handle%28Variants.IdAmbiguousMessage%29:~IdAmbiguousMessage:wolverine:saga:orchestrate|Conventional|Authored Wolverine saga orchestrate role method|Saga/Correlation.cs:66:49-66:99| +saga wolverine:saga:~IdSaga| +relationship ~IdSaga#method:M%3AVariants.IdSaga.Handle%28Variants.IdMessage%29:~IdMessage:wolverine:saga:orchestrate|Conventional|Wolverine case-insensitive Id saga correlation convention|Saga/Correlation.cs:62:26-62:61|iD +saga wolverine:saga:~MemberKindsSaga| +relationship ~MemberKindsSaga#method:M%3AVariants.MemberKindsSaga.Handle%28Variants.MemberKindsMessage%29:~MemberKindsMessage:wolverine:saga:orchestrate|Conventional|Wolverine case-insensitive Id saga correlation convention|Saga/Correlation.cs:71:182-71:217|id +saga wolverine:saga:~NoCorrelationSaga| +relationship ~NoCorrelationSaga#method:M%3AVariants.NoCorrelationSaga.Handle%28Variants.NoCorrelationMessage%29:~NoCorrelationMessage:wolverine:saga:orchestrate|Conventional|Authored Wolverine saga orchestrate role method|Saga/Correlation.cs:69:51-69:103| +saga wolverine:saga:~NotCompletedSaga| +relationship ~NotCompletedSaga#method:M%3AVariants.NotCompletedSaga.Handle%28Variants.NotCompletedMessage%29:~NotCompletedMessage:wolverine:saga:orchestrate|Conventional|Wolverine saga correlation member convention 'SagaId'|Saga/Completion.cs:11:35-11:53|sagaId +saga wolverine:saga:~OrdersagaSaga| +relationship ~OrdersagaSaga#method:M%3AVariants.OrdersagaSaga.Handle%28Variants.StrippedAnywhereMessage%29:~StrippedAnywhereMessage:wolverine:saga:orchestrate|Conventional|Wolverine saga correlation member convention 'OrderId'|Saga/Correlation.cs:48:40-48:80|orderId +saga wolverine:saga:~RoleShapesSaga|sagaId +relationship ~RoleShapesSaga#method:M%3AVariants.RoleShapesSaga.HandleAsync%28Variants.RoleMessage%29:~RoleMessage:wolverine:saga:orchestrate|Conventional|Wolverine saga correlation member convention 'SagaId'|Saga/Roles.cs:3:27-3:45|sagaId +saga wolverine:saga:~SagaIdAmbiguousSaga| +relationship ~SagaIdAmbiguousSaga#method:M%3AVariants.SagaIdAmbiguousSaga.Handle%28Variants.SagaIdAmbiguousMessage%29:~SagaIdAmbiguousMessage:wolverine:saga:orchestrate|Conventional|Authored Wolverine saga orchestrate role method|Saga/Correlation.cs:60:53-60:107| +saga wolverine:saga:~SagaIdConventionSaga| +relationship ~SagaIdConventionSaga#method:M%3AVariants.SagaIdConventionSaga.Handle%28Variants.SagaIdConventionMessage%29:~SagaIdConventionMessage:wolverine:saga:orchestrate|Conventional|Wolverine saga correlation member convention 'SagaId'|Saga/Correlation.cs:55:40-55:79|sagaId +saga wolverine:saga:~StrippedAmbiguousSaga| +relationship ~StrippedAmbiguousSaga#method:M%3AVariants.StrippedAmbiguousSaga.Handle%28Variants.StrippedAmbiguousMessage%29:~StrippedAmbiguousMessage:wolverine:saga:orchestrate|Conventional|Authored Wolverine saga orchestrate role method|Saga/Correlation.cs:53:55-53:111| +saga wolverine:saga:~StrippedSaga| +relationship ~StrippedSaga#method:M%3AVariants.StrippedSaga.Handle%28Variants.StrippedMessage%29:~StrippedMessage:wolverine:saga:orchestrate|Conventional|Wolverine saga correlation member convention 'StrippedId'|Saga/Correlation.cs:45:32-45:75|strippedId +saga wolverine:saga:~TimeoutSaga| +relationship ~TimeoutSaga#method:M%3AVariants.TimeoutSaga.Handle%28Variants.TimeoutMessageIn%29:~TimeoutMessageIn:wolverine:saga:orchestrate|Conventional|Wolverine saga correlation member convention 'SagaId'|Saga/Completion.cs:20:32-20:50|sagaId +saga wolverine:saga:~ZetaSaga| +relationship ~ZetaSaga#method:M%3AVariants.ZetaSaga.Handle%28Variants.OrderAMessage%29:~OrderAMessage:wolverine:saga:orchestrate|Conventional|Wolverine saga correlation member convention 'SagaId'|Saga/Zeta.cs:4:29-4:47|sagaId +relationship ~ZetaSaga#method:M%3AVariants.ZetaSaga.Handle%28Variants.OrderAMessage%2CSystem.String%29:~OrderAMessage:wolverine:saga:orchestrate|Conventional|Wolverine saga correlation member convention 'SagaId'|Saga/Zeta.cs:4:29-4:47|sagaId +relationship ~ZetaSaga#method:M%3AVariants.ZetaSaga.Handles%28Variants.OrderZMessage%29:~OrderZMessage:wolverine:saga:orchestrate|Conventional|Wolverine saga correlation member convention 'SagaId'|Saga/Zeta.cs:3:29-3:47|sagaId +relationship ~ZetaSaga#method:M%3AVariants.ZetaSaga.Handle%28Variants.OrderZMessage%29:~OrderZMessage:wolverine:saga:orchestrate|Conventional|Wolverine saga correlation member convention 'SagaId'|Saga/Zeta.cs:3:29-3:47|sagaId diff --git a/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayAdapter/when_characterizing_wolverine_discovery_variants.cs b/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayAdapter/when_characterizing_wolverine_discovery_variants.cs new file mode 100644 index 0000000..4667c77 --- /dev/null +++ b/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayAdapter/when_characterizing_wolverine_discovery_variants.cs @@ -0,0 +1,23 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.CritterStack.Screenplay.for_CritterStackScreenplayAdapter; + +public class when_characterizing_wolverine_discovery_variants : given.a_wolverine_policy_variants_application +{ + const string ExpectedDiagnostics = + """ + WOLVERINE0007|Warning|Unknown|dotnet://DiscoveryVariants/DiscoveryVariants/DiscoveryVariants.EarlierConfiguration|DiscoveryVariants/Earlier.cs:7:9-7:66|Wolverine handler discovery call 'CustomizeHandlerDiscovery' was not applied because custom handler predicates and lambdas are not statically interpreted + WOLVERINE0007|Warning|Unknown|dotnet://DiscoveryVariants/DiscoveryVariants/DiscoveryVariants.EarlierConfiguration|DiscoveryVariants/Earlier.cs:8:9-8:64|Wolverine handler discovery call 'DisableConventionalDiscovery' was not applied because the enabled state is not a compile-time constant + WOLVERINE0007|Warning|Unknown|dotnet://DiscoveryVariants/DiscoveryVariants/DiscoveryVariants.Configuration|DiscoveryVariants/Program.cs:18:9-18:48|Wolverine handler discovery call 'IncludeType' was not applied because the included handler type is not authored source in this compilation + WOLVERINE0007|Warning|Unknown|dotnet://DiscoveryVariants/DiscoveryVariants/DiscoveryVariants.Configuration|DiscoveryVariants/Program.cs:19:9-19:51|Wolverine handler discovery call 'IncludeType' was not applied because the included handler type is not authored source in this compilation + WOLVERINE0007|Warning|Unknown|dotnet://DiscoveryVariants/DiscoveryVariants/DiscoveryVariants.Configuration|DiscoveryVariants/Program.cs:20:9-20:51|Wolverine handler discovery call 'IncludeType' was not applied because the included handler type is not authored source in this compilation + WOLVERINE0007|Warning|Unknown|dotnet://DiscoveryVariants/DiscoveryVariants/DiscoveryVariants.Configuration|DiscoveryVariants/Program.cs:22:9-22:67|Wolverine handler discovery call 'IncludeAssembly' was not applied because assembly scanning outside the current source compilation is not supported + WOLVERINE0007|Warning|Unknown|dotnet://DiscoveryVariants/DiscoveryVariants/DiscoveryVariants.Configuration|DiscoveryVariants/Program.cs:23:9-23:59|Wolverine handler discovery call 'IncludeAssembly' was not applied because assembly scanning outside the current source compilation is not supported + WOLVERINE0007|Warning|Unknown|dotnet://DiscoveryVariants/DiscoveryVariants/DiscoveryVariants.Configuration|DiscoveryVariants/Program.cs:24:9-24:66|Wolverine handler discovery call 'CustomizeHandlerDiscovery' was not applied because custom handler predicates and lambdas are not statically interpreted + WOLVERINE0007|Warning|Unknown|dotnet://DiscoveryVariants/DiscoveryVariants/DiscoveryVariants.Configuration|DiscoveryVariants/Program.cs:25:9-25:73|Wolverine handler discovery call 'IgnoreAssembly' was not applied because assembly removal cannot be resolved from the current source compilation + WOLVERINE0007|Warning|Conflict|dotnet://DiscoveryVariants/DiscoveryVariants/DiscoveryVariants.EarlierConfiguration|DiscoveryVariants/Earlier.cs:8:9-8:64|Wolverine handler discovery call 'DisableConventionalDiscovery' was not applied because conflicting enabled states depend on runtime execution order + """; + + [Fact] void should_pin_the_discovery_diagnostics_with_ranges() => string.Join('\n', Diagnostics(DiscoveryContribution, "WOLVERINE0007")).ShouldEqual(ExpectedDiagnostics); +} diff --git a/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayAdapter/when_characterizing_wolverine_policy_variants.cs b/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayAdapter/when_characterizing_wolverine_policy_variants.cs new file mode 100644 index 0000000..822c20e --- /dev/null +++ b/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayAdapter/when_characterizing_wolverine_policy_variants.cs @@ -0,0 +1,57 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.CritterStack.Screenplay.for_CritterStackScreenplayAdapter; + +public class when_characterizing_wolverine_policy_variants : given.a_wolverine_policy_variants_application +{ + const string ExpectedDiagnostics = + """ + WOLVERINE0009|Warning|Unknown|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.EarlierConfiguration|PolicyVariants/Earlier.cs:10:9-10:80|Wolverine validation call 'UseFluentValidation' was not applied because FluentValidation option 'IncludeInternalTypes' is not a compile-time constant + WOLVERINE0011|Warning|Unknown|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.EarlierConfiguration|PolicyVariants/Earlier.cs:11:24-11:52|Wolverine authorization configuration call 'RequireAuthorizeOnAll' was not applied because the global authorization call is conditionally executed at runtime + WOLVERINE0009|Warning|Unknown|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.EarlierConfiguration|PolicyVariants/Earlier.cs:12:9-12:79|Wolverine validation call 'UseFluentValidation' was not applied because validator applicability depends on explicit runtime container registrations + WOLVERINE0009|Warning|Unknown|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:19:9-19:85|Wolverine validation call 'UseFluentValidation' was not applied because validator applicability depends on explicit runtime container registrations + WOLVERINE0009|Warning|Unknown|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:21:9-21:84|Wolverine validation call 'UseFluentValidation' was not applied because the FluentValidation configuration callback is executed at runtime + WOLVERINE0009|Warning|Unknown|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:22:9-22:47|Wolverine validation call 'UseFluentValidation' was not applied because the FluentValidation configuration callback is executed at runtime + WOLVERINE0009|Warning|Unknown|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:23:9-23:145|Wolverine validation call 'UseFluentValidation' was not applied because validator applicability depends on explicit runtime container registrations + WOLVERINE0009|Warning|Unknown|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:25:9-25:80|Wolverine validation call 'UseFluentValidation' was not applied because FluentValidation option 'IncludeInternalTypes' is not a compile-time constant + WOLVERINE0009|Warning|Unknown|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:26:9-26:105|Wolverine validation call 'UseFluentValidation' was not applied because the FluentValidation configuration callback contains runtime or custom behavior + WOLVERINE0009|Warning|Unknown|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:27:9-27:79|Wolverine validation call 'UseFluentValidation' was not applied because the FluentValidation configuration callback contains runtime or custom behavior + WOLVERINE0009|Warning|Unknown|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:28:9-28:102|Wolverine validation call 'UseFluentValidation' was not applied because FluentValidation option 'RegistrationBehavior' is not a compile-time constant + WOLVERINE0009|Warning|Unknown|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:29:9-29:88|Wolverine validation call 'UseFluentValidation' was not applied because a FluentValidation option is not a compile-time constant + WOLVERINE0009|Warning|Unknown|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:30:9-30:46|Wolverine validation call 'UseFluentValidation' was not applied because the FluentValidation configuration callback contains runtime or custom behavior + WOLVERINE0009|Warning|Unknown|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:31:9-31:63|Wolverine validation call 'UseFluentValidation' was not applied because FluentValidation option 'Unsupported' is not statically supported + WOLVERINE0009|Warning|Unknown|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:32:9-32:39|Wolverine validation call 'UseFluentValidation' was not applied because a FluentValidation option uses an unsupported runtime configuration shape + WOLVERINE0011|Warning|Unknown|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:40:9-40:109|Wolverine authorization configuration call 'ConfigureEndpoints' was not applied because the endpoint authorization policy is conditional or has runtime arguments + WOLVERINE0011|Warning|Unknown|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:41:9-41:86|Wolverine authorization configuration call 'ConfigureEndpoints' was not applied because the endpoint authorization policy is conditional or has runtime arguments + WOLVERINE0009|Warning|Unknown|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:44:13-44:42|Wolverine validation call 'UseFluentValidation' was not applied because the policy call is conditionally executed at runtime + WOLVERINE0009|Warning|Unknown|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:45:13-45:51|Wolverine validation call 'UseDataAnnotationsValidation' was not applied because the policy call is conditionally executed at runtime + WOLVERINE0011|Warning|Unknown|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:46:13-46:91|Wolverine authorization configuration call 'ConfigureEndpoints' was not applied because the endpoint authorization policy is conditional or has runtime arguments + WOLVERINE0008|Information|Unsupported|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.EarlierConfiguration|PolicyVariants/Earlier.cs:10:9-10:80|Wolverine FluentValidation message handler policy is enabled by 'UseFluentValidation', but current generation contracts cannot represent policy activation + WOLVERINE0008|Information|Unsupported|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.EarlierConfiguration|PolicyVariants/Earlier.cs:12:9-12:79|Wolverine FluentValidation message handler policy is enabled by 'UseFluentValidation', but current generation contracts cannot represent policy activation + WOLVERINE0008|Information|Unsupported|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:19:9-19:85|Wolverine FluentValidation message handler policy is enabled by 'UseFluentValidation', but current generation contracts cannot represent policy activation + WOLVERINE0008|Information|Unsupported|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:20:9-20:88|Wolverine FluentValidation message handler policy is enabled by 'UseFluentValidation', but current generation contracts cannot represent policy activation + WOLVERINE0008|Information|Unsupported|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:21:9-21:84|Wolverine FluentValidation message handler policy is enabled by 'UseFluentValidation', but current generation contracts cannot represent policy activation + WOLVERINE0008|Information|Unsupported|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:22:9-22:47|Wolverine FluentValidation message handler policy is enabled by 'UseFluentValidation', but current generation contracts cannot represent policy activation + WOLVERINE0008|Information|Unsupported|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:23:9-23:145|Wolverine FluentValidation message handler policy is enabled by 'UseFluentValidation', but current generation contracts cannot represent policy activation + WOLVERINE0008|Information|Unsupported|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:24:9-24:72|Wolverine FluentValidation message handler policy is enabled by 'UseFluentValidation', but current generation contracts cannot represent policy activation + WOLVERINE0008|Information|Unsupported|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:25:9-25:80|Wolverine FluentValidation message handler policy is enabled by 'UseFluentValidation', but current generation contracts cannot represent policy activation + WOLVERINE0008|Information|Unsupported|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:26:9-26:105|Wolverine FluentValidation message handler policy is enabled by 'UseFluentValidation', but current generation contracts cannot represent policy activation + WOLVERINE0008|Information|Unsupported|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:27:9-27:79|Wolverine FluentValidation message handler policy is enabled by 'UseFluentValidation', but current generation contracts cannot represent policy activation + WOLVERINE0008|Information|Unsupported|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:28:9-28:102|Wolverine FluentValidation message handler policy is enabled by 'UseFluentValidation', but current generation contracts cannot represent policy activation + WOLVERINE0008|Information|Unsupported|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:29:9-29:88|Wolverine FluentValidation message handler policy is enabled by 'UseFluentValidation', but current generation contracts cannot represent policy activation + WOLVERINE0008|Information|Unsupported|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:30:9-30:46|Wolverine FluentValidation message handler policy is enabled by 'UseFluentValidation', but current generation contracts cannot represent policy activation + WOLVERINE0008|Information|Unsupported|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:31:9-31:63|Wolverine FluentValidation message handler policy is enabled by 'UseFluentValidation', but current generation contracts cannot represent policy activation + WOLVERINE0008|Information|Unsupported|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:32:9-32:39|Wolverine FluentValidation message handler policy is enabled by 'UseFluentValidation', but current generation contracts cannot represent policy activation + WOLVERINE0008|Information|Unsupported|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:33:9-33:47|Wolverine DataAnnotations message handler policy is enabled by 'UseDataAnnotationsValidation', but current generation contracts cannot represent policy activation + WOLVERINE0008|Information|Unsupported|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:34:9-34:58|Wolverine FluentValidation HTTP endpoint policy is enabled by 'UseFluentValidationProblemDetailMiddleware', but current generation contracts cannot represent policy activation + WOLVERINE0008|Information|Unsupported|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:35:9-35:67|Wolverine DataAnnotations HTTP endpoint policy is enabled by 'UseDataAnnotationsValidationProblemDetailMiddleware', but current generation contracts cannot represent policy activation + WOLVERINE0010|Warning|Unsupported|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.EarlierConfiguration|PolicyVariants/Earlier.cs:9:9-9:37|Global Wolverine HTTP authorization is enabled by 'RequireAuthorizeOnAll', but current generation contracts cannot represent authorization without overloading an unrelated relationship + WOLVERINE0010|Warning|Unsupported|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:36:9-36:37|Global Wolverine HTTP authorization is enabled by 'RequireAuthorizeOnAll', but current generation contracts cannot represent authorization without overloading an unrelated relationship + WOLVERINE0010|Warning|Unsupported|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:37:9-37:90|Global Wolverine HTTP authorization is enabled by 'ConfigureEndpoints(... RequireAuthorization(...))', but current generation contracts cannot represent authorization without overloading an unrelated relationship + WOLVERINE0010|Warning|Unsupported|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:38:9-38:107|Global Wolverine HTTP authorization is enabled by 'ConfigureEndpoints(... RequireAuthorization(...))', but current generation contracts cannot represent authorization without overloading an unrelated relationship + WOLVERINE0010|Warning|Unsupported|dotnet://PolicyVariants/PolicyVariants/PolicyVariants.Configuration|PolicyVariants/Program.cs:49:17-49:45|Global Wolverine HTTP authorization is enabled by 'RequireAuthorizeOnAll', but current generation contracts cannot represent authorization without overloading an unrelated relationship + """; + + [Fact] void should_pin_the_validation_and_authorization_diagnostics_with_ranges() => string.Join('\n', Diagnostics(PolicyContribution, "WOLVERINE0005", "WOLVERINE0008", "WOLVERINE0009", "WOLVERINE0010", "WOLVERINE0011")).ShouldEqual(ExpectedDiagnostics); +} diff --git a/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayAdapter/when_characterizing_wolverine_returns_and_links.cs b/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayAdapter/when_characterizing_wolverine_returns_and_links.cs new file mode 100644 index 0000000..2249fe1 --- /dev/null +++ b/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayAdapter/when_characterizing_wolverine_returns_and_links.cs @@ -0,0 +1,40 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.CritterStack.Screenplay.for_CritterStackScreenplayAdapter; + +/// +/// Pins every fact and diagnostic for one handler per return consequence kind, session append and yield shape, and +/// declared SlicePattern, against a golden file captured from the pre-refactor code. A production link exists only when +/// the loss reason is null; any loss reason turns it into a handler reference with the reason in a diagnostic. +/// +public class when_characterizing_wolverine_returns_and_links : given.a_wolverine_returns_and_links_variants_application +{ + (string Expected, string Actual) _transcript; + + void Because() + { + Analyze(); + _transcript = given.contribution_transcript.Compare(Contribution, "wolverine-returns-and-links"); + } + + [Fact] void should_have_compiler_checked_input() => Project.Compilation.GetDiagnostics(TestContext.Current.CancellationToken).Where(_ => _.Severity == DiagnosticSeverity.Error).ShouldBeEmpty(); + [Fact] void should_preserve_every_fact_and_diagnostic() => _transcript.Actual.ShouldEqual(_transcript.Expected); + [Fact] void should_keep_the_golden_non_vacuous() => _transcript.Expected.Split('\n').Length.ShouldBeGreaterThan(200); + + [Fact] void should_let_an_unreadable_identity_win_over_a_conditional_append() => Message("ConditionalComputed").ShouldContain("the stream identity is not read directly from a property of the command"); + [Fact] void should_report_a_conditional_append_as_unsupported() => Outcome("ConditionalAppend").ShouldEqual(GenerationDiagnosticOutcome.Unsupported); + [Fact] void should_report_an_unreadable_identity_as_unknown() => Outcome("ComputedAppend").ShouldEqual(GenerationDiagnosticOutcome.Unknown); + [Fact] void should_report_a_conditional_yield_as_unsupported_even_without_an_identity() => Outcome("YieldConditionalNoIdentity").ShouldEqual(GenerationDiagnosticOutcome.Unsupported); + [Fact] void should_prefer_the_conditional_yield_reason_over_the_missing_identity() => Message("YieldConditionalNoIdentity").ShouldContain("the handler yields it only on some paths, and the condition is not represented"); + [Fact] void should_report_a_missing_yield_identity_when_unconditional() => Message("YieldNoIdentity").ShouldContain("the aggregate stream identity is not a command property"); + [Fact] void should_not_report_a_loss_for_a_plain_append() => Omitted("PlainAppend").ShouldBeEmpty(); + [Fact] void should_not_report_a_loss_for_a_plain_yield() => Omitted("YieldPlain").ShouldBeEmpty(); + + string Message(string command) => Omitted(command).Single().Message; + + GenerationDiagnosticOutcome? Outcome(string command) => Omitted(command).Single().Outcome; + + IReadOnlyList Omitted(string command) => + [.. Contribution.Diagnostics.Where(_ => _.Code == WolverineDiagnosticCodes.ProductionLinkOmitted && _.Message.Contains($"Command '{command}' ", StringComparison.Ordinal))]; +} diff --git a/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayAdapter/when_characterizing_wolverine_saga_variants.cs b/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayAdapter/when_characterizing_wolverine_saga_variants.cs new file mode 100644 index 0000000..04319d7 --- /dev/null +++ b/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayAdapter/when_characterizing_wolverine_saga_variants.cs @@ -0,0 +1,41 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.CritterStack.Screenplay.for_CritterStackScreenplayAdapter; + +/// +/// Pins the full ordered diagnostics and saga facts of one isolated saga per behavior: correlation precedence +/// ([SagaIdentity], then [SagaIdentityFrom], then the saga-name conventions, then SagaId, then Id), role admission and +/// rejection reasons, creation and chain rules, role ordering across files and completion reporting. The expected +/// lines are the checked-in golden file wolverine-saga-variants.txt, captured from the pre-refactor code. +/// +public class when_characterizing_wolverine_saga_variants : given.a_wolverine_saga_variants_application +{ + string[] _expected = []; + + void Because() + { + Analyze(); + _expected = File.ReadAllLines(Path.Combine(AppContext.BaseDirectory, "Golden", "wolverine-saga-variants.txt")); + } + + [Fact] + void should_report_the_diagnostics_in_the_pinned_order() => + string.Join('\n', Lines.Where(_ => _.StartsWith("diagnostic ", StringComparison.Ordinal))) + .ShouldEqual(string.Join('\n', _expected.Where(_ => _.StartsWith("diagnostic ", StringComparison.Ordinal)))); + + [Fact] + void should_report_the_saga_facts_in_the_pinned_order() => + string.Join('\n', Lines.Where(_ => !_.StartsWith("diagnostic ", StringComparison.Ordinal))) + .ShouldEqual(string.Join('\n', _expected.Where(_ => !_.StartsWith("diagnostic ", StringComparison.Ordinal)))); + + [Fact] void should_keep_the_golden_non_vacuous() => _expected.Length.ShouldEqual(Lines.Count); + + [Fact] + void should_prefer_the_saga_identity_attribute_over_saga_identity_from() => + For(Lines, "AttrFromSaga").Any(_ => _.Contains("Exact Wolverine [SagaIdentity] message member", StringComparison.Ordinal) && _.EndsWith("|chosen", StringComparison.Ordinal)).ShouldBeTrue(); + + [Fact] + void should_not_fall_back_to_the_full_name_convention_when_saga_identity_from_names_a_missing_member() => + For(Lines, "FromMissingSaga").Any(_ => _.EndsWith("convention 'FromMissingId'|Saga/Correlation.cs:31:86-31:132|fromMissingId", StringComparison.Ordinal)).ShouldBeTrue(); +} diff --git a/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayAdapter/when_characterizing_wolverine_validator_option_resolution.cs b/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayAdapter/when_characterizing_wolverine_validator_option_resolution.cs new file mode 100644 index 0000000..dad8647 --- /dev/null +++ b/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayAdapter/when_characterizing_wolverine_validator_option_resolution.cs @@ -0,0 +1,43 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.CritterStack.Screenplay.for_CritterStackScreenplayAdapter; + +public class when_characterizing_wolverine_validator_option_resolution : given.a_wolverine_validator_option_scenarios +{ + const string ConfigurationSubject = "dotnet://Scenario/Scenario/Scenario.Configuration"; + const string HandlerSubject = "dotnet://Scenario/Scenario/Scenario.Handler"; + const string Tail = "but current generation contracts cannot represent validation without overloading an unrelated relationship"; + + const string PublicValidation = + $"WOLVERINE0005|Warning|Unsupported|{HandlerSubject}|Scenario/Program.cs:8:14-8:36|FluentValidation message handler validation for 'PublicRequest' is applied by validator 'Scenario.PublicRequestValidator' after 'UseFluentValidation' enabled the policy, {Tail}"; + + const string InternalValidation = + $"WOLVERINE0005|Warning|Unsupported|{HandlerSubject}|Scenario/Program.cs:9:16-9:40|FluentValidation message handler validation for 'InternalRequest' is applied by validator 'Scenario.InternalRequestValidator' after 'UseFluentValidation' enabled the policy, {Tail}"; + + [Fact] void should_validate_only_public_validators_by_default() => Outcomes["default"].ShouldEqual([Activation(38), PublicValidation]); + + [Fact] void should_validate_internal_validators_when_the_positional_argument_includes_them() => Outcomes["positional-include-internal"].ShouldEqual([Activation(94), PublicValidation, InternalValidation]); + + [Fact] void should_validate_only_public_validators_when_the_positional_argument_excludes_internal_ones() => Outcomes["positional-exclude-internal"].ShouldEqual([Activation(95), PublicValidation]); + + [Fact] void should_not_validate_anything_when_the_positional_argument_requires_explicit_registration() => Outcomes["positional-explicit-registration"].ShouldEqual([Unresolved(85), Activation(85)]); + + [Fact] void should_validate_internal_validators_when_the_callback_includes_them() => Outcomes["callback-include-internal"].ShouldEqual([Activation(77), PublicValidation, InternalValidation]); + + [Fact] void should_let_the_later_callback_assignment_exclude_internal_validators() => Outcomes["callback-include-internal-then-exclude"].ShouldEqual([Activation(109), PublicValidation]); + + [Fact] void should_let_the_later_callback_assignment_include_internal_validators() => Outcomes["callback-exclude-internal-then-include"].ShouldEqual([Activation(109), PublicValidation, InternalValidation]); + + [Fact] void should_not_validate_anything_when_the_callback_requires_explicit_registration() => Outcomes["callback-explicit-registration"].ShouldEqual([Unresolved(109), Activation(109)]); + + [Fact] void should_let_the_later_callback_assignment_restore_validator_discovery() => Outcomes["callback-explicit-then-discover"].ShouldEqual([Activation(222), PublicValidation, InternalValidation]); + + [Fact] void should_let_the_later_callback_assignment_require_explicit_registration() => Outcomes["callback-discover-then-explicit"].ShouldEqual([Unresolved(222), Activation(222)]); + + static string Activation(int endColumn) => + $"WOLVERINE0008|Information|Unsupported|{ConfigurationSubject}|Scenario/Program.cs:21:9-21:{endColumn}|Wolverine FluentValidation message handler policy is enabled by 'UseFluentValidation', but current generation contracts cannot represent policy activation"; + + static string Unresolved(int endColumn) => + $"WOLVERINE0009|Warning|Unknown|{ConfigurationSubject}|Scenario/Program.cs:21:9-21:{endColumn}|Wolverine validation call 'UseFluentValidation' was not applied because validator applicability depends on explicit runtime container registrations"; +} diff --git a/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayAdapter/when_several_source_owners_are_missing.cs b/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayAdapter/when_several_source_owners_are_missing.cs new file mode 100644 index 0000000..19ab737 --- /dev/null +++ b/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayAdapter/when_several_source_owners_are_missing.cs @@ -0,0 +1,53 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.CritterStack.Screenplay.for_CritterStackScreenplayAdapter; + +public class when_several_source_owners_are_missing : given.a_shared_source_placement_application +{ + IReadOnlyList _ownersInArtifactOrder = null!; + IReadOnlyList _ownersAgainstArtifactOrder = null!; + + void Because() + { + var context = new DotNetAnalysisContext([Project]); + var baseline = Adapter.Analyze(context, AdapterOptions); + var command = baseline.Facts.OfType().Single(_ => _.Definition.Key.Kind == ArtifactKind.Command && _.Definition.Name == "SubmitOrder"); + var query = baseline.Facts.OfType().Single(_ => _.Definition.Key.Kind == ArtifactKind.Query && _.Definition.Name == "GetOrder"); + var commandPlacement = CritterStackSourcePlacement.CompatibilityPlacement(Project, AdapterOptions, "SubmitOrder", "SubmitOrder", GenerationSliceKind.StateChange); + var queryPlacement = CritterStackSourcePlacement.CompatibilityPlacement(Project, AdapterOptions, "OrderSummary", "GetOrder", GenerationSliceKind.StateView); + _ownersInArtifactOrder = MissingOwnerSignatures(context, command, commandPlacement, "missing:a", query, queryPlacement, "missing:z"); + _ownersAgainstArtifactOrder = MissingOwnerSignatures(context, command, commandPlacement, "missing:z", query, queryPlacement, "missing:a"); + } + + [Fact] + void should_report_the_missing_owners_ordered_by_owner_subject_when_it_agrees_with_artifact_order() => _ownersInArtifactOrder.ShouldEqual( + "DOTNETSP0010|Error|Unsupported|missing:a|Application/Source/Orders/Submit/Submit.cs|10|5|Source owner 'missing:a' for artifact 'dotnet://Application/Application/Application.Orders.Submit.SubmitOrder' has no host-supplied source structure", + "DOTNETSP0010|Error|Unsupported|missing:z|Application/Source/Orders/Summary/OrderEndpoints.cs|5|5|Source owner 'missing:z' for artifact 'dotnet://Application/Application/Application.Orders.Summary.OrderEndpoints#method:M%3AApplication.Orders.Summary.OrderEndpoints.GetOrder%28System.Guid%29:query' has no host-supplied source structure"); + + [Fact] + void should_report_the_missing_owners_ordered_by_owner_subject_when_it_disagrees_with_artifact_order() => _ownersAgainstArtifactOrder.ShouldEqual( + "DOTNETSP0010|Error|Unsupported|missing:a|Application/Source/Orders/Summary/OrderEndpoints.cs|5|5|Source owner 'missing:a' for artifact 'dotnet://Application/Application/Application.Orders.Summary.OrderEndpoints#method:M%3AApplication.Orders.Summary.OrderEndpoints.GetOrder%28System.Guid%29:query' has no host-supplied source structure", + "DOTNETSP0010|Error|Unsupported|missing:z|Application/Source/Orders/Submit/Submit.cs|10|5|Source owner 'missing:z' for artifact 'dotnet://Application/Application/Application.Orders.Submit.SubmitOrder' has no host-supplied source structure"); + + IReadOnlyList MissingOwnerSignatures( + DotNetAnalysisContext context, + ArtifactFact command, + ArtifactPlacement commandPlacement, + string commandOwner, + ArtifactFact query, + ArtifactPlacement queryPlacement, + string queryOwner) + { + var diagnostics = new List(); + CritterStackSourcePlacement.Derive( + context, + AdapterOptions, + [ + new CritterStackPlacementIntent("test:placement:command", command.Definition.Key, new SubjectId { Value = commandOwner }, commandPlacement, command.Evidence), + new CritterStackPlacementIntent("test:placement:query", query.Definition.Key, new SubjectId { Value = queryOwner }, queryPlacement, query.Evidence) + ], + diagnostics); + return [.. diagnostics.Select(_ => $"{_.Code}|{_.Severity}|{_.Outcome}|{_.Subject?.Value}|{_.Source?.Path}|{_.Source?.StartLine}|{_.Source?.StartColumn}|{_.Message}")]; + } +} diff --git a/Source/DotNET/CritterStack.Specs/for_CritterStackSource/when_mapping_legacy_declaration_paths.cs b/Source/DotNET/CritterStack.Specs/for_CritterStackSource/when_mapping_legacy_declaration_paths.cs new file mode 100644 index 0000000..ec2f2b7 --- /dev/null +++ b/Source/DotNET/CritterStack.Specs/for_CritterStackSource/when_mapping_legacy_declaration_paths.cs @@ -0,0 +1,55 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.CritterStack.Screenplay.for_CritterStackSource; + +public class when_mapping_legacy_declaration_paths : given.a_critter_stack_source_context +{ + const string Declaration = "namespace Referenced;\n\npublic sealed class SharedType;"; + + SourceRange? _nested; + SourceRange? _normalized; + SourceRange? _escaping; + SourceRange? _sibling; + SourceRange? _blankRoot; + SourceRange? _blankPath; + SourceRange? _rootItself; + + void Because() + { + _nested = RangeFor("/checkout", "/checkout/Referenced/A/SharedType.cs"); + _normalized = RangeFor("/checkout/", "/checkout/Referenced/../Other/SharedType.cs"); + _escaping = RangeFor("/checkout/Sub", "/checkout/Other/SharedType.cs"); + _sibling = RangeFor("/checkout", "/checkout-other/SharedType.cs"); + _blankRoot = RangeFor(" ", "/checkout/SharedType.cs"); + _blankPath = RangeFor("/checkout", " "); + _rootItself = RangeFor("/checkout/SharedType.cs", "/checkout/SharedType.cs"); + } + + [Fact] void should_map_a_nested_declaration_to_a_one_based_range() => AssertRange(_nested, "Referenced/A/SharedType.cs"); + [Fact] void should_normalize_dot_dot_segments_inside_the_root() => AssertRange(_normalized, "Other/SharedType.cs"); + [Fact] void should_omit_a_declaration_that_escapes_the_root() => _escaping.ShouldBeNull(); + [Fact] void should_omit_a_declaration_in_a_sibling_directory_with_the_root_as_prefix() => _sibling.ShouldBeNull(); + [Fact] void should_omit_source_for_a_blank_root() => _blankRoot.ShouldBeNull(); + [Fact] void should_omit_source_for_a_blank_path() => _blankPath.ShouldBeNull(); + [Fact] void should_omit_source_for_the_root_itself() => _rootItself.ShouldBeNull(); + + static SourceRange? RangeFor(string sourceRoot, string path) + { + var referencedTree = SourceTree(Declaration, path); + var referencedCompilation = SourceCompilation("Referenced", [referencedTree]); + var project = ReferencingProject(referencedCompilation, sourceRoot); + + return EvidenceFor(ReferencedType(project, "Referenced.SharedType"), project).Source; + } + + static void AssertRange(SourceRange? range, string path) + { + range.ShouldNotBeNull(); + range.Path.ShouldEqual(path); + range.StartLine.ShouldEqual(3); + range.StartColumn.ShouldEqual(1); + range.EndLine.ShouldEqual(3); + range.EndColumn.ShouldEqual(32); + } +} diff --git a/Source/DotNET/CritterStack.Specs/for_Expectations/when_checking_a_fan_out_whose_child_is_not_a_collection.cs b/Source/DotNET/CritterStack.Specs/for_Expectations/when_checking_a_fan_out_whose_child_is_not_a_collection.cs new file mode 100644 index 0000000..d035a77 --- /dev/null +++ b/Source/DotNET/CritterStack.Specs/for_Expectations/when_checking_a_fan_out_whose_child_is_not_a_collection.cs @@ -0,0 +1,45 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Cratis.CritterStack.Screenplay.Canonical; + +namespace Cratis.CritterStack.Screenplay.for_Expectations; + +public class when_checking_a_fan_out_whose_child_is_not_a_collection : given.a_marten_multi_stream_grouping_application +{ + const string FanOut = "OrderImported|LineImported|lines|after-grouping"; + + bool _collectionMet; + bool _singularMet; + + void Because() + { + _collectionMet = IsFanOutMet(Graph); + + var singular = new AdapterContribution + { + Adapter = Contribution.Adapter, + Diagnostics = Contribution.Diagnostics, + Facts = + [ + .. Contribution.Facts.Select(fact => fact is RelationshipFact relationship && + relationship.Definition.Key.Discriminator?.StartsWith("marten:fan-out-child:", StringComparison.Ordinal) == true + ? relationship with { Definition = relationship.Definition with { IsCollection = false } } + : fact) + ] + }; + _singularMet = IsFanOutMet(new GenerationResolver().Resolve([singular])); + } + + [Fact] void should_meet_the_fan_out_while_its_child_is_a_collection() => _collectionMet.ShouldBeTrue(); + [Fact] void should_not_meet_the_fan_out_when_its_child_is_not_a_collection() => _singularMet.ShouldBeFalse(); + + bool IsFanOutMet(ResolvedApplicationGraph graph) => new Expectation(ExpectationKind.FanOut, FanOut).IsMetBy( + new GeneratedScreenplayDefinition + { + Application = null!, + Source = string.Empty, + Diagnostics = Contribution.Diagnostics, + Graph = graph + }); +} diff --git a/Source/DotNET/CritterStack.Specs/for_Expectations/when_checking_artifact_expectations.cs b/Source/DotNET/CritterStack.Specs/for_Expectations/when_checking_artifact_expectations.cs new file mode 100644 index 0000000..49154da --- /dev/null +++ b/Source/DotNET/CritterStack.Specs/for_Expectations/when_checking_artifact_expectations.cs @@ -0,0 +1,78 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Cratis.CritterStack.Screenplay.Canonical; + +namespace Cratis.CritterStack.Screenplay.for_Expectations; + +public class when_checking_artifact_expectations : given.a_canonical_kitchen_result +{ + const string AdapterId = "cratis.critter-stack"; + const string PlanPath = "Application/Kitchen/Planning/PlanBatch.cs"; + const string CardPath = "Application/Kitchen/Cards/BatchCard.cs"; + + void Because() => Evaluate( + (ExpectationKind.Artifact, "Command:PlanBatch"), + (ExpectationKind.Artifact, "Event:PlanBatch"), + (ExpectationKind.Artifact, "Command:Plan"), + (ExpectationKind.Artifact, "PlanBatch"), + (ExpectationKind.ArtifactCount, "Command|6"), + (ExpectationKind.ArtifactCount, "Event|6"), + (ExpectationKind.ArtifactCount, "Command|5"), + (ExpectationKind.ArtifactCount, "Bogus|1"), + (ExpectationKind.ArtifactCount, "Command|six"), + (ExpectationKind.ArtifactCount, "Command"), + (ExpectationKind.ArtifactCount, "Command|6|x"), + (ExpectationKind.Evidence, $"Event:BatchPlanned|{AdapterId}|{PlanPath}"), + (ExpectationKind.Evidence, $"Event:BatchPlanned|{AdapterId}|{CardPath}"), + (ExpectationKind.Evidence, $"Event:BatchPlanned|other.adapter|{PlanPath}"), + (ExpectationKind.Evidence, $"Event:BatchPlanned|{AdapterId}|Application/Kitchen"), + (ExpectationKind.Evidence, $"Event:Missing|{AdapterId}|{PlanPath}"), + (ExpectationKind.Evidence, $"Event:BatchPlanned|{AdapterId}"), + (ExpectationKind.EvidenceCount, "Event:BatchPlanned|2"), + (ExpectationKind.EvidenceCount, "Event:BatchPlanned|1"), + (ExpectationKind.EvidenceCount, "Event:BatchPlanned|two"), + (ExpectationKind.EvidenceCount, "Event:Missing|1"), + (ExpectationKind.EvidenceCount, "Event:BatchPlanned"), + (ExpectationKind.Identifier, "Command:PlanBatch:id"), + (ExpectationKind.Identifier, "Document:BatchCard:id"), + (ExpectationKind.Identifier, "Command:PlanBatch:recipe"), + (ExpectationKind.Identifier, "Command:PlanBatch:missing"), + (ExpectationKind.Identifier, "ReadModel:BatchCard:id"), + (ExpectationKind.Identifier, "Command:PlanBatch"), + (ExpectationKind.NotIdentifier, "Command:PlanBatch:recipe"), + (ExpectationKind.NotIdentifier, "Command:PlanBatch:id"), + (ExpectationKind.NotIdentifier, "Command:PlanBatch")); + + [Fact] void should_meet_an_artifact_with_that_kind_and_name() => Met(ExpectationKind.Artifact, "Command:PlanBatch").ShouldBeTrue(); + [Fact] void should_not_meet_an_artifact_with_another_kind() => Met(ExpectationKind.Artifact, "Event:PlanBatch").ShouldBeFalse(); + [Fact] void should_not_meet_an_artifact_that_is_only_a_name_prefix() => Met(ExpectationKind.Artifact, "Command:Plan").ShouldBeFalse(); + [Fact] void should_not_meet_an_artifact_without_a_kind() => Met(ExpectationKind.Artifact, "PlanBatch").ShouldBeFalse(); + [Fact] void should_meet_the_artifact_count_of_a_kind() => Met(ExpectationKind.ArtifactCount, "Command|6").ShouldBeTrue(); + [Fact] void should_meet_the_artifact_count_of_another_kind() => Met(ExpectationKind.ArtifactCount, "Event|6").ShouldBeTrue(); + [Fact] void should_not_meet_a_different_artifact_count() => Met(ExpectationKind.ArtifactCount, "Command|5").ShouldBeFalse(); + [Fact] void should_not_meet_an_artifact_count_of_an_unknown_kind() => Met(ExpectationKind.ArtifactCount, "Bogus|1").ShouldBeFalse(); + [Fact] void should_not_meet_an_artifact_count_that_is_not_a_number() => Met(ExpectationKind.ArtifactCount, "Command|six").ShouldBeFalse(); + [Fact] void should_not_meet_an_artifact_count_without_a_count() => Met(ExpectationKind.ArtifactCount, "Command").ShouldBeFalse(); + [Fact] void should_not_meet_an_artifact_count_with_an_extra_part() => Met(ExpectationKind.ArtifactCount, "Command|6|x").ShouldBeFalse(); + [Fact] void should_meet_evidence_from_the_adapter_at_the_first_path() => Met(ExpectationKind.Evidence, $"Event:BatchPlanned|{AdapterId}|{PlanPath}").ShouldBeTrue(); + [Fact] void should_meet_evidence_from_the_adapter_at_the_second_path() => Met(ExpectationKind.Evidence, $"Event:BatchPlanned|{AdapterId}|{CardPath}").ShouldBeTrue(); + [Fact] void should_not_meet_evidence_from_another_adapter() => Met(ExpectationKind.Evidence, $"Event:BatchPlanned|other.adapter|{PlanPath}").ShouldBeFalse(); + [Fact] void should_not_meet_evidence_at_a_path_that_is_only_a_prefix() => Met(ExpectationKind.Evidence, $"Event:BatchPlanned|{AdapterId}|Application/Kitchen").ShouldBeFalse(); + [Fact] void should_not_meet_evidence_for_an_artifact_that_is_missing() => Met(ExpectationKind.Evidence, $"Event:Missing|{AdapterId}|{PlanPath}").ShouldBeFalse(); + [Fact] void should_not_meet_evidence_without_a_path() => Met(ExpectationKind.Evidence, $"Event:BatchPlanned|{AdapterId}").ShouldBeFalse(); + [Fact] void should_meet_the_evidence_count_across_variants() => Met(ExpectationKind.EvidenceCount, "Event:BatchPlanned|2").ShouldBeTrue(); + [Fact] void should_not_meet_a_different_evidence_count() => Met(ExpectationKind.EvidenceCount, "Event:BatchPlanned|1").ShouldBeFalse(); + [Fact] void should_not_meet_an_evidence_count_that_is_not_a_number() => Met(ExpectationKind.EvidenceCount, "Event:BatchPlanned|two").ShouldBeFalse(); + [Fact] void should_not_meet_an_evidence_count_for_an_artifact_that_is_missing() => Met(ExpectationKind.EvidenceCount, "Event:Missing|1").ShouldBeFalse(); + [Fact] void should_not_meet_an_evidence_count_without_a_count() => Met(ExpectationKind.EvidenceCount, "Event:BatchPlanned").ShouldBeFalse(); + [Fact] void should_meet_an_identifier_property_of_a_command() => Met(ExpectationKind.Identifier, "Command:PlanBatch:id").ShouldBeTrue(); + [Fact] void should_meet_an_identifier_property_of_a_document() => Met(ExpectationKind.Identifier, "Document:BatchCard:id").ShouldBeTrue(); + [Fact] void should_not_meet_an_identifier_that_is_an_ordinary_property() => Met(ExpectationKind.Identifier, "Command:PlanBatch:recipe").ShouldBeFalse(); + [Fact] void should_not_meet_an_identifier_that_is_not_declared() => Met(ExpectationKind.Identifier, "Command:PlanBatch:missing").ShouldBeFalse(); + [Fact] void should_not_meet_an_identifier_property_on_a_variant_that_does_not_mark_it() => Met(ExpectationKind.Identifier, "ReadModel:BatchCard:id").ShouldBeFalse(); + [Fact] void should_not_meet_an_identifier_without_a_property() => Met(ExpectationKind.Identifier, "Command:PlanBatch").ShouldBeFalse(); + [Fact] void should_meet_a_not_identifier_for_an_ordinary_property() => Met(ExpectationKind.NotIdentifier, "Command:PlanBatch:recipe").ShouldBeTrue(); + [Fact] void should_not_meet_a_not_identifier_for_an_identifier_property() => Met(ExpectationKind.NotIdentifier, "Command:PlanBatch:id").ShouldBeFalse(); + [Fact] void should_meet_a_not_identifier_that_is_malformed() => Met(ExpectationKind.NotIdentifier, "Command:PlanBatch").ShouldBeTrue(); +} diff --git a/Source/DotNET/CritterStack.Specs/for_Expectations/when_checking_grouping_and_fan_out_expectations.cs b/Source/DotNET/CritterStack.Specs/for_Expectations/when_checking_grouping_and_fan_out_expectations.cs new file mode 100644 index 0000000..2d9f959 --- /dev/null +++ b/Source/DotNET/CritterStack.Specs/for_Expectations/when_checking_grouping_and_fan_out_expectations.cs @@ -0,0 +1,44 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Cratis.CritterStack.Screenplay.Canonical; + +namespace Cratis.CritterStack.Screenplay.for_Expectations; + +public class when_checking_grouping_and_fan_out_expectations : given.a_canonical_multi_stream_result +{ + void Because() => Evaluate( + (ExpectationKind.Grouping, "CustomerOrdersProjection|CustomerAssigned|customerId|false"), + (ExpectationKind.Grouping, "CustomerOrdersProjection|CustomersShared|customerIds|true"), + (ExpectationKind.Grouping, "CustomerOrdersProjection|CustomerAssigned|customerId|true"), + (ExpectationKind.Grouping, "CustomerOrdersProjection|CustomerAssigned|customerIds|false"), + (ExpectationKind.Grouping, "UnsafeSelectorsProjection|CustomerAssigned|customerId|false"), + (ExpectationKind.Grouping, "CustomerOrdersProjection|LineImported|customerId|false"), + (ExpectationKind.Grouping, "CustomerOrdersProjection|CustomerAssigned|customerId|maybe"), + (ExpectationKind.Grouping, "CustomerOrdersProjection|CustomerAssigned|customerId"), + (ExpectationKind.FanOut, "OrderImported|LineImported|lines|after-grouping"), + (ExpectationKind.FanOut, "RouteImported|StopImported|stops|before-grouping"), + (ExpectationKind.FanOut, "RouteImported|StopImported|stops|after-grouping"), + (ExpectationKind.FanOut, "OrderImported|LineImported|items|after-grouping"), + (ExpectationKind.FanOut, "OrderImported|StopImported|lines|after-grouping"), + (ExpectationKind.FanOut, "Missing|LineImported|lines|after-grouping"), + (ExpectationKind.FanOut, "OrderImported|LineImported|lines"), + (ExpectationKind.DiagnosticMessage, "MARTEN0001|Multi-stream projection 'TenantGroupingProjection' groups by tenant through RollUpByTenant")); + + [Fact] void should_meet_a_single_identity_grouping() => Met(ExpectationKind.Grouping, "CustomerOrdersProjection|CustomerAssigned|customerId|false").ShouldBeTrue(); + [Fact] void should_meet_a_one_to_many_identity_grouping() => Met(ExpectationKind.Grouping, "CustomerOrdersProjection|CustomersShared|customerIds|true").ShouldBeTrue(); + [Fact] void should_not_meet_a_grouping_with_the_wrong_cardinality() => Met(ExpectationKind.Grouping, "CustomerOrdersProjection|CustomerAssigned|customerId|true").ShouldBeFalse(); + [Fact] void should_not_meet_a_grouping_with_the_wrong_member() => Met(ExpectationKind.Grouping, "CustomerOrdersProjection|CustomerAssigned|customerIds|false").ShouldBeFalse(); + [Fact] void should_not_meet_a_grouping_for_another_reducer() => Met(ExpectationKind.Grouping, "UnsafeSelectorsProjection|CustomerAssigned|customerId|false").ShouldBeFalse(); + [Fact] void should_not_meet_a_grouping_for_an_event_without_an_identity() => Met(ExpectationKind.Grouping, "CustomerOrdersProjection|LineImported|customerId|false").ShouldBeFalse(); + [Fact] void should_not_meet_a_grouping_whose_cardinality_is_not_a_boolean() => Met(ExpectationKind.Grouping, "CustomerOrdersProjection|CustomerAssigned|customerId|maybe").ShouldBeFalse(); + [Fact] void should_not_meet_a_grouping_without_a_cardinality() => Met(ExpectationKind.Grouping, "CustomerOrdersProjection|CustomerAssigned|customerId").ShouldBeFalse(); + [Fact] void should_meet_a_fan_out_after_grouping() => Met(ExpectationKind.FanOut, "OrderImported|LineImported|lines|after-grouping").ShouldBeTrue(); + [Fact] void should_meet_a_fan_out_before_grouping() => Met(ExpectationKind.FanOut, "RouteImported|StopImported|stops|before-grouping").ShouldBeTrue(); + [Fact] void should_not_meet_a_fan_out_with_the_wrong_mode() => Met(ExpectationKind.FanOut, "RouteImported|StopImported|stops|after-grouping").ShouldBeFalse(); + [Fact] void should_not_meet_a_fan_out_with_the_wrong_member() => Met(ExpectationKind.FanOut, "OrderImported|LineImported|items|after-grouping").ShouldBeFalse(); + [Fact] void should_not_meet_a_fan_out_with_the_wrong_child() => Met(ExpectationKind.FanOut, "OrderImported|StopImported|lines|after-grouping").ShouldBeFalse(); + [Fact] void should_not_meet_a_fan_out_from_an_unknown_parent() => Met(ExpectationKind.FanOut, "Missing|LineImported|lines|after-grouping").ShouldBeFalse(); + [Fact] void should_not_meet_a_fan_out_without_a_mode() => Met(ExpectationKind.FanOut, "OrderImported|LineImported|lines").ShouldBeFalse(); + [Fact] void should_meet_a_diagnostic_message_naming_the_reported_projection() => Met(ExpectationKind.DiagnosticMessage, "MARTEN0001|Multi-stream projection 'TenantGroupingProjection' groups by tenant through RollUpByTenant").ShouldBeTrue(); +} diff --git a/Source/DotNET/CritterStack.Specs/for_Expectations/when_checking_relationship_expectations.cs b/Source/DotNET/CritterStack.Specs/for_Expectations/when_checking_relationship_expectations.cs new file mode 100644 index 0000000..a7803ab --- /dev/null +++ b/Source/DotNET/CritterStack.Specs/for_Expectations/when_checking_relationship_expectations.cs @@ -0,0 +1,48 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Cratis.CritterStack.Screenplay.Canonical; + +namespace Cratis.CritterStack.Screenplay.for_Expectations; + +public class when_checking_relationship_expectations : given.a_canonical_kitchen_result +{ + void Because() => Evaluate( + (ExpectationKind.Relationship, "Produces"), + (ExpectationKind.Relationship, "Bogus"), + (ExpectationKind.Relationship, "Produce"), + (ExpectationKind.Relationship, "Produces Command:PlanBatch -> Event:BatchPlanned"), + (ExpectationKind.Relationship, "Produces Command:PlanBatch -> Event:BatchServed"), + (ExpectationKind.Relationship, "Produces Event:BatchPlanned -> Command:PlanBatch"), + (ExpectationKind.Relationship, "Consumes Command:PlanBatch -> Event:BatchPlanned"), + (ExpectationKind.Relationship, "Bogus Command:PlanBatch -> Event:BatchPlanned"), + (ExpectationKind.Relationship, "Produces Command:PlanBatch"), + (ExpectationKind.Relationship, "Produces Command:PlanBatch -> BatchPlanned"), + (ExpectationKind.RelationshipCount, "Produces|Command:PlanBatch|Event:BatchPlanned|1"), + (ExpectationKind.RelationshipCount, "Produces|Command:PlanBatch|Event:BatchServed|0"), + (ExpectationKind.RelationshipCount, "Reads|Command:ServeBatch|ReadModel:SoupBatch|1"), + (ExpectationKind.RelationshipCount, "Produces|Command:PlanBatch|Event:BatchPlanned|2"), + (ExpectationKind.RelationshipCount, "Produces|Command:PlanBatch|Event:BatchPlanned|0"), + (ExpectationKind.RelationshipCount, "Bogus|Command:PlanBatch|Event:BatchPlanned|1"), + (ExpectationKind.RelationshipCount, "Produces|Command:PlanBatch|Event:BatchPlanned|one"), + (ExpectationKind.RelationshipCount, "Produces|Command:PlanBatch|Event:BatchPlanned")); + + [Fact] void should_meet_a_relationship_kind_that_exists() => Met(ExpectationKind.Relationship, "Produces").ShouldBeTrue(); + [Fact] void should_not_meet_a_relationship_kind_that_does_not_exist() => Met(ExpectationKind.Relationship, "Bogus").ShouldBeFalse(); + [Fact] void should_not_meet_a_relationship_kind_that_is_only_a_prefix() => Met(ExpectationKind.Relationship, "Produce").ShouldBeFalse(); + [Fact] void should_meet_a_relationship_between_the_source_and_the_target() => Met(ExpectationKind.Relationship, "Produces Command:PlanBatch -> Event:BatchPlanned").ShouldBeTrue(); + [Fact] void should_not_meet_a_relationship_to_another_target() => Met(ExpectationKind.Relationship, "Produces Command:PlanBatch -> Event:BatchServed").ShouldBeFalse(); + [Fact] void should_not_meet_a_relationship_in_the_opposite_direction() => Met(ExpectationKind.Relationship, "Produces Event:BatchPlanned -> Command:PlanBatch").ShouldBeFalse(); + [Fact] void should_not_meet_a_relationship_of_another_kind() => Met(ExpectationKind.Relationship, "Consumes Command:PlanBatch -> Event:BatchPlanned").ShouldBeFalse(); + [Fact] void should_not_meet_a_relationship_of_an_unknown_kind() => Met(ExpectationKind.Relationship, "Bogus Command:PlanBatch -> Event:BatchPlanned").ShouldBeFalse(); + [Fact] void should_not_meet_a_relationship_without_an_arrow() => Met(ExpectationKind.Relationship, "Produces Command:PlanBatch").ShouldBeFalse(); + [Fact] void should_not_meet_a_relationship_whose_target_lacks_a_kind() => Met(ExpectationKind.Relationship, "Produces Command:PlanBatch -> BatchPlanned").ShouldBeFalse(); + [Fact] void should_meet_the_relationship_count_between_the_source_and_the_target() => Met(ExpectationKind.RelationshipCount, "Produces|Command:PlanBatch|Event:BatchPlanned|1").ShouldBeTrue(); + [Fact] void should_meet_a_relationship_count_of_zero_for_an_unrelated_pair() => Met(ExpectationKind.RelationshipCount, "Produces|Command:PlanBatch|Event:BatchServed|0").ShouldBeTrue(); + [Fact] void should_meet_the_relationship_count_of_another_kind() => Met(ExpectationKind.RelationshipCount, "Reads|Command:ServeBatch|ReadModel:SoupBatch|1").ShouldBeTrue(); + [Fact] void should_not_meet_a_different_relationship_count() => Met(ExpectationKind.RelationshipCount, "Produces|Command:PlanBatch|Event:BatchPlanned|2").ShouldBeFalse(); + [Fact] void should_not_meet_a_relationship_count_of_zero_for_a_related_pair() => Met(ExpectationKind.RelationshipCount, "Produces|Command:PlanBatch|Event:BatchPlanned|0").ShouldBeFalse(); + [Fact] void should_not_meet_a_relationship_count_of_an_unknown_kind() => Met(ExpectationKind.RelationshipCount, "Bogus|Command:PlanBatch|Event:BatchPlanned|1").ShouldBeFalse(); + [Fact] void should_not_meet_a_relationship_count_that_is_not_a_number() => Met(ExpectationKind.RelationshipCount, "Produces|Command:PlanBatch|Event:BatchPlanned|one").ShouldBeFalse(); + [Fact] void should_not_meet_a_relationship_count_without_a_count() => Met(ExpectationKind.RelationshipCount, "Produces|Command:PlanBatch|Event:BatchPlanned").ShouldBeFalse(); +} diff --git a/Source/DotNET/CritterStack.Specs/for_Expectations/when_checking_source_and_diagnostic_expectations.cs b/Source/DotNET/CritterStack.Specs/for_Expectations/when_checking_source_and_diagnostic_expectations.cs new file mode 100644 index 0000000..ad646db --- /dev/null +++ b/Source/DotNET/CritterStack.Specs/for_Expectations/when_checking_source_and_diagnostic_expectations.cs @@ -0,0 +1,48 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Cratis.CritterStack.Screenplay.Canonical; + +namespace Cratis.CritterStack.Screenplay.for_Expectations; + +public class when_checking_source_and_diagnostic_expectations : given.a_canonical_kitchen_result +{ + void Because() => Evaluate( + (ExpectationKind.Source, "module Sales"), + (ExpectationKind.Source, "module sales"), + (ExpectationKind.Source, "module Billing"), + (ExpectationKind.NotSource, "module Billing"), + (ExpectationKind.NotSource, "module Sales"), + (ExpectationKind.SourceHash, SourceHash), + (ExpectationKind.SourceHash, SourceHash.ToLowerInvariant()), + (ExpectationKind.SourceHash, SourceHash[..^1] + "D"), + (ExpectationKind.SourceHash, SourceHash[..16]), + (ExpectationKind.Diagnostic, "MARTEN0003"), + (ExpectationKind.Diagnostic, "WOLVERINE000"), + (ExpectationKind.Diagnostic, "MARTEN9999"), + (ExpectationKind.DiagnosticMessage, "WOLVERINE0002|HTTP GET route '/batches/{id}'"), + (ExpectationKind.DiagnosticMessage, "WOLVERINE0002|"), + (ExpectationKind.DiagnosticMessage, "MARTEN0003|HTTP GET route"), + (ExpectationKind.DiagnosticMessage, "WOLVERINE0002|no such message"), + (ExpectationKind.DiagnosticMessage, "WOLVERINE0002"), + ((ExpectationKind)99, "x")); + + [Fact] void should_meet_a_source_expectation_that_the_source_contains() => Met(ExpectationKind.Source, "module Sales").ShouldBeTrue(); + [Fact] void should_not_meet_a_source_expectation_that_differs_only_by_case() => Met(ExpectationKind.Source, "module sales").ShouldBeFalse(); + [Fact] void should_not_meet_a_source_expectation_the_source_lacks() => Met(ExpectationKind.Source, "module Billing").ShouldBeFalse(); + [Fact] void should_meet_a_not_source_expectation_the_source_lacks() => Met(ExpectationKind.NotSource, "module Billing").ShouldBeTrue(); + [Fact] void should_not_meet_a_not_source_expectation_that_the_source_contains() => Met(ExpectationKind.NotSource, "module Sales").ShouldBeFalse(); + [Fact] void should_meet_the_upper_case_source_hash() => Met(ExpectationKind.SourceHash, SourceHash).ShouldBeTrue(); + [Fact] void should_meet_the_lower_case_source_hash() => Met(ExpectationKind.SourceHash, SourceHash.ToLowerInvariant()).ShouldBeTrue(); + [Fact] void should_not_meet_a_different_source_hash() => Met(ExpectationKind.SourceHash, SourceHash[..^1] + "D").ShouldBeFalse(); + [Fact] void should_not_meet_a_source_hash_prefix() => Met(ExpectationKind.SourceHash, SourceHash[..16]).ShouldBeFalse(); + [Fact] void should_meet_a_diagnostic_with_that_exact_code() => Met(ExpectationKind.Diagnostic, "MARTEN0003").ShouldBeTrue(); + [Fact] void should_not_meet_a_diagnostic_code_that_is_only_a_prefix() => Met(ExpectationKind.Diagnostic, "WOLVERINE000").ShouldBeFalse(); + [Fact] void should_not_meet_a_diagnostic_code_that_was_not_reported() => Met(ExpectationKind.Diagnostic, "MARTEN9999").ShouldBeFalse(); + [Fact] void should_meet_a_diagnostic_message_with_the_code_and_a_message_substring() => Met(ExpectationKind.DiagnosticMessage, "WOLVERINE0002|HTTP GET route '/batches/{id}'").ShouldBeTrue(); + [Fact] void should_meet_a_diagnostic_message_with_the_code_and_an_empty_substring() => Met(ExpectationKind.DiagnosticMessage, "WOLVERINE0002|").ShouldBeTrue(); + [Fact] void should_not_meet_a_diagnostic_message_that_belongs_to_another_code() => Met(ExpectationKind.DiagnosticMessage, "MARTEN0003|HTTP GET route").ShouldBeFalse(); + [Fact] void should_not_meet_a_diagnostic_message_the_code_did_not_report() => Met(ExpectationKind.DiagnosticMessage, "WOLVERINE0002|no such message").ShouldBeFalse(); + [Fact] void should_not_meet_a_diagnostic_message_with_only_a_code() => Met(ExpectationKind.DiagnosticMessage, "WOLVERINE0002").ShouldBeFalse(); + [Fact] void should_not_meet_an_expectation_kind_it_does_not_know() => Met((ExpectationKind)99, "x").ShouldBeFalse(); +} diff --git a/Source/DotNET/CritterStack.Specs/for_Expectations/when_reading_expectations.cs b/Source/DotNET/CritterStack.Specs/for_Expectations/when_reading_expectations.cs new file mode 100644 index 0000000..8b9e3a7 --- /dev/null +++ b/Source/DotNET/CritterStack.Specs/for_Expectations/when_reading_expectations.cs @@ -0,0 +1,89 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Cratis.CritterStack.Screenplay.Canonical; + +namespace Cratis.CritterStack.Screenplay.for_Expectations; + +public class when_reading_expectations : Specification +{ + string _path = null!; + IReadOnlyList _valid = null!; + IReadOnlyList _kinds = null!; + Exception _unknownKind = null!; + Exception _missingSeparator = null!; + Exception _leadingSeparator = null!; + Exception _firstOfSeveral = null!; + + void Establish() => _path = Path.Combine(Path.GetTempPath(), $"expectations-{Guid.NewGuid():N}.expected"); + + void Because() + { + _valid = Read( + "# a comment", + string.Empty, + " source: module Sales ", + "not-source:module Billing", + "diagnostic-message: WOLVERINE0002|route: '/batches'", + " ", + "# source: ignored"); + _kinds = Read( + "source: a", + "not-source: b", + "source-sha256: c", + "diagnostic: d", + "diagnostic-message: e", + "artifact: f", + "artifact-count: g", + "evidence: h", + "evidence-count: i", + "relationship: j", + "relationship-count: k", + "identifier: l", + "not-identifier: m", + "grouping: n", + "fan-out: o"); + _unknownKind = Catch.Exception(() => Read("bogus: x")); + _missingSeparator = Catch.Exception(() => Read("source")); + _leadingSeparator = Catch.Exception(() => Read(":x")); + _firstOfSeveral = Catch.Exception(() => Read("source: ok", "wrong: first", "also-wrong: second")); + File.Delete(_path); + } + + [Fact] + void should_read_the_expectations_in_file_order_skipping_comments_and_blank_lines() => _valid.ShouldEqual( + "Source: module Sales", + "NotSource: module Billing", + "DiagnosticMessage: WOLVERINE0002|route: '/batches'"); + [Fact] + void should_read_every_kind_in_file_order() => _kinds.ShouldEqual( + "Source: a", + "NotSource: b", + "SourceHash: c", + "Diagnostic: d", + "DiagnosticMessage: e", + "Artifact: f", + "ArtifactCount: g", + "Evidence: h", + "EvidenceCount: i", + "Relationship: j", + "RelationshipCount: k", + "Identifier: l", + "NotIdentifier: m", + "Grouping: n", + "FanOut: o"); + [Fact] void should_reject_an_unknown_kind_with_the_invalid_expectation_exception() => _unknownKind.ShouldBeOfExactType(); + [Fact] void should_name_the_unknown_kind_line_in_the_message() => _unknownKind.Message.ShouldEqual("Invalid canonical expectation 'bogus: x'"); + [Fact] void should_reject_a_line_without_a_separator() => _missingSeparator.ShouldBeOfExactType(); + [Fact] void should_name_the_line_without_a_separator_in_the_message() => _missingSeparator.Message.ShouldEqual("Invalid canonical expectation 'source'"); + [Fact] void should_reject_a_line_that_starts_with_the_separator() => _leadingSeparator.ShouldBeOfExactType(); + [Fact] void should_name_the_line_that_starts_with_the_separator_in_the_message() => _leadingSeparator.Message.ShouldEqual("Invalid canonical expectation ':x'"); + [Fact] void should_report_the_first_invalid_line_with_the_invalid_expectation_exception() => _firstOfSeveral.ShouldBeOfExactType(); + [Fact] void should_report_the_first_invalid_line_when_several_are_invalid() => _firstOfSeveral.Message.ShouldEqual("Invalid canonical expectation 'wrong: first'"); + + IReadOnlyList Read(params string[] lines) + { + File.WriteAllLines(_path, lines); + return [.. Expectations.Read(_path).Select(_ => _.ToString())]; + } +} diff --git a/Source/DotNET/CritterStack.Specs/given/a_canonical_kitchen_result.cs b/Source/DotNET/CritterStack.Specs/given/a_canonical_kitchen_result.cs new file mode 100644 index 0000000..fdffa77 --- /dev/null +++ b/Source/DotNET/CritterStack.Specs/given/a_canonical_kitchen_result.cs @@ -0,0 +1,64 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Cratis.CritterStack.Screenplay.Canonical; + +namespace Cratis.CritterStack.Screenplay.given; + +/// +/// A generated community kitchen graph and diagnostics under a fixed hand-written source, so canonical expectations are +/// checked against literal values. +/// +public class a_canonical_kitchen_result : a_community_kitchen_application +{ + readonly Dictionary<(ExpectationKind, string), bool> _met = []; + + /// + /// The fixed source text of the result. + /// + protected const string Source = "domain Kitchen\n\nmodule Sales\n"; + + /// + /// The SHA-256 of as upper-case hexadecimal. + /// + protected const string SourceHash = "32CE2FBAF12416FE738679715E16D7D1AC4B3410E7C197776030142D084F689C"; + + /// + /// Gets the result expectations are checked against. + /// + protected GeneratedScreenplayDefinition Result { get; private set; } = null!; + + void Establish() + { + var generated = new CritterStackScreenplayGenerator().Generate( + [CreateFeatureFolderProject()], + new CritterStackScreenplayOptions { Domain = "CommunityKitchen" }); + Result = new GeneratedScreenplayDefinition + { + Application = null!, + Source = Source, + Diagnostics = generated.Diagnostics, + Graph = generated.Graph + }; + } + + /// + /// Evaluates expectations against the result, so assertions only read captured outcomes. + /// + /// The expectation kinds and values to evaluate. + private protected void Evaluate(params (ExpectationKind Kind, string Value)[] cases) + { + foreach (var (kind, value) in cases) + { + _met[(kind, value)] = new Expectation(kind, value).IsMetBy(Result); + } + } + + /// + /// Gets the captured outcome of one expectation evaluated by . + /// + /// The expectation kind. + /// The expectation value. + /// when the expectation was met. + private protected bool Met(ExpectationKind kind, string value) => _met[(kind, value)]; +} diff --git a/Source/DotNET/CritterStack.Specs/given/a_canonical_multi_stream_result.cs b/Source/DotNET/CritterStack.Specs/given/a_canonical_multi_stream_result.cs new file mode 100644 index 0000000..03f3f1a --- /dev/null +++ b/Source/DotNET/CritterStack.Specs/given/a_canonical_multi_stream_result.cs @@ -0,0 +1,44 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Cratis.CritterStack.Screenplay.Canonical; + +namespace Cratis.CritterStack.Screenplay.given; + +/// +/// A multi-stream grouping graph and its diagnostics, wrapped as a generated definition so canonical expectations +/// are checked against literal values. +/// +public class a_canonical_multi_stream_result : a_marten_multi_stream_grouping_application +{ + readonly Dictionary<(ExpectationKind, string), bool> _met = []; + GeneratedScreenplayDefinition _result = null!; + + void Establish() => _result = new GeneratedScreenplayDefinition + { + Application = null!, + Source = string.Empty, + Diagnostics = Contribution.Diagnostics, + Graph = Graph + }; + + /// + /// Evaluates expectations against the result, so assertions only read captured outcomes. + /// + /// The expectation kinds and values to evaluate. + private protected void Evaluate(params (ExpectationKind Kind, string Value)[] cases) + { + foreach (var (kind, value) in cases) + { + _met[(kind, value)] = new Expectation(kind, value).IsMetBy(_result); + } + } + + /// + /// Gets the captured outcome of one expectation evaluated by . + /// + /// The expectation kind. + /// The expectation value. + /// when the expectation was met. + private protected bool Met(ExpectationKind kind, string value) => _met[(kind, value)]; +} diff --git a/Source/DotNET/CritterStack.Specs/given/a_wolverine_policy_variants_application.cs b/Source/DotNET/CritterStack.Specs/given/a_wolverine_policy_variants_application.cs new file mode 100644 index 0000000..f8517bd --- /dev/null +++ b/Source/DotNET/CritterStack.Specs/given/a_wolverine_policy_variants_application.cs @@ -0,0 +1,298 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.CritterStack.Screenplay.given; + +/// +/// Two authored projects pinning the alternative forms of Wolverine validation, authorization and handler discovery +/// configuration, each split over an earlier-path file and a later-path file whose textual order is reversed. +/// +public class a_wolverine_policy_variants_application : Specification +{ + const string FrameworkSource = + """ + namespace JasperFx.Core.TypeScanning + { + public class TypeQuery; + } + + namespace Microsoft.AspNetCore.Builder + { + public static class AuthorizationEndpointConventionBuilderExtensions + { + public static T RequireAuthorization(this T builder, params string[] policyNames) => builder; + } + } + + namespace Wolverine.Configuration + { + public class HandlerDiscovery + { + public HandlerDiscovery DisableConventionalDiscovery(bool value = true) => this; + public HandlerDiscovery IncludeType() => this; + public HandlerDiscovery IncludeType(System.Type type) => this; + public HandlerDiscovery IncludeAssembly(System.Reflection.Assembly assembly) => this; + public HandlerDiscovery CustomizeHandlerDiscovery(System.Action configure) => this; + public void IgnoreAssembly(System.Reflection.Assembly assembly) { } + } + } + + namespace Wolverine + { + public interface IWolverineHandler; + public class WolverineOptions + { + public Wolverine.Configuration.HandlerDiscovery Discovery { get; } = new(); + } + } + + namespace Wolverine.FluentValidation + { + public enum RegistrationBehavior + { + DiscoverAndRegisterValidators, + ExplicitRegistration + } + + public class FluentValidationConfiguration + { + public RegistrationBehavior RegistrationBehavior { get; set; } + public bool IncludeInternalTypes { get; set; } + public bool Unsupported { get; set; } + } + + public static class WolverineFluentValidationExtensions + { + public static Wolverine.WolverineOptions UseFluentValidation( + this Wolverine.WolverineOptions options, + RegistrationBehavior behavior = RegistrationBehavior.DiscoverAndRegisterValidators, + bool includeInternalTypes = false) => options; + + public static Wolverine.WolverineOptions UseFluentValidation( + this Wolverine.WolverineOptions options, + System.Action configure) => options; + + public static Wolverine.WolverineOptions UseFluentValidation( + this Wolverine.WolverineOptions options, + int level) => options; + } + } + + namespace Wolverine.DataAnnotationsValidation + { + public static class DataAnnotationsValidationExtensions + { + public static Wolverine.WolverineOptions UseDataAnnotationsValidation(this Wolverine.WolverineOptions options) => options; + } + } + + namespace Wolverine.Http + { + public class HttpChain + { + public object Metadata { get; } = new(); + } + + public class WolverineHttpOptions + { + public void UseDataAnnotationsValidationProblemDetailMiddleware() { } + public void RequireAuthorizeOnAll() { } + public void ConfigureEndpoints(System.Action configure) { } + } + } + + namespace Wolverine.Http.FluentValidation + { + public static class WolverineHttpOptionsExtensions + { + public static void UseFluentValidationProblemDetailMiddleware(this Wolverine.Http.WolverineHttpOptions options) { } + } + } + """; + + const string PolicyProgramSource = + """ + using Microsoft.AspNetCore.Builder; + using Wolverine.DataAnnotationsValidation; + using Wolverine.FluentValidation; + using Wolverine.Http.FluentValidation; + + namespace PolicyVariants; + + public static class Configuration + { + public static void Configure( + Wolverine.WolverineOptions options, + Wolverine.Http.WolverineHttpOptions http, + bool runtime, + int runtimeValue, + string policy, + FluentValidationConfiguration captured, + System.Action configure) + { + options.UseFluentValidation(RegistrationBehavior.ExplicitRegistration, true); + options.UseFluentValidation(RegistrationBehavior.DiscoverAndRegisterValidators); + options.UseFluentValidation(delegate (FluentValidationConfiguration c) { }); + options.UseFluentValidation(configure); + options.UseFluentValidation(c => { c.IncludeInternalTypes = true; c.RegistrationBehavior = RegistrationBehavior.ExplicitRegistration; }); + options.UseFluentValidation(c => c.IncludeInternalTypes = true); + options.UseFluentValidation(c => { c.IncludeInternalTypes = runtime; }); + options.UseFluentValidation(c => { c.IncludeInternalTypes = true; System.Console.WriteLine(); }); + options.UseFluentValidation(c => captured.IncludeInternalTypes = true); + options.UseFluentValidation(c => c.RegistrationBehavior = (RegistrationBehavior)runtimeValue); + options.UseFluentValidation(RegistrationBehavior.ExplicitRegistration, runtime); + options.UseFluentValidation(c => { }); + options.UseFluentValidation(c => c.Unsupported = true); + options.UseFluentValidation(3); + options.UseDataAnnotationsValidation(); + http.UseFluentValidationProblemDetailMiddleware(); + http.UseDataAnnotationsValidationProblemDetailMiddleware(); + http.RequireAuthorizeOnAll(); + http.ConfigureEndpoints(chain => chain.Metadata.RequireAuthorization("fallback")); + http.ConfigureEndpoints((Wolverine.Http.HttpChain chain) => chain.Metadata.RequireAuthorization()); + http.ConfigureEndpoints(chain => { }); + http.ConfigureEndpoints(chain => { if (runtime) { chain.Metadata.RequireAuthorization("inner"); } }); + http.ConfigureEndpoints(chain => chain.Metadata.RequireAuthorization(policy)); + if (runtime) + { + options.UseFluentValidation(); + options.UseDataAnnotationsValidation(); + http.ConfigureEndpoints(chain => chain.Metadata.RequireAuthorization("outer")); + void Local() + { + http.RequireAuthorizeOnAll(); + } + + Local(); + } + } + } + """; + + const string PolicyEarlierSource = + """ + using Wolverine.FluentValidation; + + namespace PolicyVariants; + + public static class EarlierConfiguration + { + public static void Configure(Wolverine.WolverineOptions options, Wolverine.Http.WolverineHttpOptions http, bool runtime) + { + http.RequireAuthorizeOnAll(); + options.UseFluentValidation(c => { c.IncludeInternalTypes = runtime; }); + if (runtime) { http.RequireAuthorizeOnAll(); } + options.UseFluentValidation(RegistrationBehavior.ExplicitRegistration); + } + } + """; + + const string DiscoveryProgramSource = + """ + namespace DiscoveryVariants; + + public record IncludedMessage(System.Guid Id); + public record ExplicitMessage(System.Guid Id); + + public static class Configuration + { + public static void Configure( + Wolverine.WolverineOptions options, + System.Type runtimeType, + System.Reflection.Assembly runtimeAssembly) + { + options.Discovery.DisableConventionalDiscovery(); + options.Discovery.DisableConventionalDiscovery(false); + options.Discovery.IncludeType(); + options.Discovery.IncludeType(); + options.Discovery.IncludeType(typeof(ExplicitActions)); + options.Discovery.IncludeType(); + options.Discovery.IncludeType(typeof(int)); + options.Discovery.IncludeType(runtimeType); + options.Discovery.IncludeAssembly(typeof(Configuration).Assembly); + options.Discovery.IncludeAssembly(typeof(string).Assembly); + options.Discovery.IncludeAssembly(runtimeAssembly); + options.Discovery.CustomizeHandlerDiscovery(query => { }); + options.Discovery.IgnoreAssembly(typeof(Configuration).Assembly); + } + + public sealed class Nested + { + public void Apply(Wolverine.WolverineOptions options) => options.Discovery.IncludeType(typeof(Nested)); + } + } + + public class IncludedHandler + { + public static void Handle(IncludedMessage message) { } + } + + public class ExplicitActions : Wolverine.IWolverineHandler + { + public void Handle(ExplicitMessage message) { } + } + """; + + const string DiscoveryEarlierSource = + """ + namespace DiscoveryVariants; + + public static class EarlierConfiguration + { + public static void Configure(Wolverine.WolverineOptions options, bool runtime) + { + options.Discovery.CustomizeHandlerDiscovery(query => { }); + options.Discovery.DisableConventionalDiscovery(runtime); + } + } + """; + + static readonly IReadOnlyList _references = + [ + .. ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")) + .Split(Path.PathSeparator) + .Select(_ => MetadataReference.CreateFromFile(_)) + ]; + + protected DotNetProjectCompilation PolicyProject = null!; + protected DotNetProjectCompilation DiscoveryProject = null!; + protected AdapterContribution PolicyContribution = null!; + protected AdapterContribution DiscoveryContribution = null!; + + void Establish() + { + PolicyProject = Project( + "PolicyVariants", + (PolicyProgramSource, "/workspace/PolicyVariants/Program.cs"), + (PolicyEarlierSource, "/workspace/PolicyVariants/Earlier.cs")); + DiscoveryProject = Project( + "DiscoveryVariants", + (DiscoveryProgramSource, "/workspace/DiscoveryVariants/Program.cs"), + (DiscoveryEarlierSource, "/workspace/DiscoveryVariants/Earlier.cs")); + var adapter = new CritterStackScreenplayAdapter(); + PolicyContribution = adapter.Analyze(new DotNetAnalysisContext([PolicyProject]), new DotNetAdapterOptions()); + DiscoveryContribution = adapter.Analyze(new DotNetAnalysisContext([DiscoveryProject]), new DotNetAdapterOptions()); + } + + protected static string[] Diagnostics(AdapterContribution contribution, params string[] codes) => + [.. diagnostic_ranges.Of(contribution).Where(_ => codes.Any(code => _.StartsWith(code + "|", StringComparison.Ordinal)))]; + + static DotNetProjectCompilation Project(string name, params (string Source, string Path)[] sources) + { + var trees = new List { CSharpSyntaxTree.ParseText(FrameworkSource, path: "/workspace/Framework.cs") }; + trees.AddRange(sources.Select(_ => CSharpSyntaxTree.ParseText(_.Source, path: _.Path))); + var compilation = CSharpCompilation.Create( + name, + trees, + _references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable)); + return new() + { + Name = name, + ProjectPath = $"/workspace/{name}/{name}.csproj", + SourceRoot = "/workspace", + Compilation = compilation, + AuthoredSyntaxTrees = compilation.SyntaxTrees.ToHashSet() + }; + } +} diff --git a/Source/DotNET/CritterStack.Specs/given/a_wolverine_returns_and_links_variants_application.cs b/Source/DotNET/CritterStack.Specs/given/a_wolverine_returns_and_links_variants_application.cs new file mode 100644 index 0000000..23b2204 --- /dev/null +++ b/Source/DotNET/CritterStack.Specs/given/a_wolverine_returns_and_links_variants_application.cs @@ -0,0 +1,546 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.CritterStack.Screenplay.given; + +/// +/// One isolated handler per behavior of the Wolverine return classification, the Marten session-append and aggregate-yield +/// production links, and the SlicePattern declarations: every consequence kind, the order the kinds are decided in, whether +/// an append is conditional, why a stream identity is not read from the command, and which trigger wins a declared pattern. +/// +public class a_wolverine_returns_and_links_variants_application : Specification +{ + const string FrameworkSource = + """ + namespace Microsoft.AspNetCore.Http + { + public interface IResult; + } + + namespace Marten + { + public interface IDocumentSession + { + Marten.Events.IEventStoreOperations Events { get; } + } + } + + namespace Marten.Events + { + public class StreamAction; + public interface IEventStoreOperations + { + StreamAction Append(System.Guid stream, params object[] events); + StreamAction Append(string stream, params object[] events); + StreamAction Append(System.Guid stream, long expectedVersion, params object[] events); + StreamAction StartStream(System.Guid id, params object[] events) where TAggregate : class; + StreamAction StartStream(params object[] events) where TAggregate : class; + } + } + + namespace Wolverine + { + public class WolverineOptions; + public abstract record TimeoutMessage; + public interface ISideEffect; + public interface IResponseAware; + public class OutgoingMessages : System.Collections.Generic.List, Wolverine.Configuration.IWolverineReturnType; + public abstract class Saga; + } + + namespace Wolverine.Attributes + { + public class WolverineIgnoreAttribute : System.Attribute; + } + + namespace Wolverine.Configuration + { + public interface IWolverineReturnType; + } + + namespace Wolverine.Http + { + public abstract class WolverineHttpMethodAttribute(string route) : System.Attribute; + public class WolverinePostAttribute(string route) : WolverineHttpMethodAttribute(route); + public class WolverineGetAttribute(string route) : WolverineHttpMethodAttribute(route); + public class WolverineHeadAttribute(string route) : WolverineHttpMethodAttribute(route); + public class EmptyResponseAttribute : System.Attribute; + public class CreationResponse(string url); + } + + namespace Wolverine.Marten + { + public class Events : System.Collections.Generic.List, Wolverine.Configuration.IWolverineReturnType; + public interface IStartStream : Wolverine.Configuration.IWolverineReturnType; + public class UpdatedAggregate; + public class AggregateHandlerAttribute : Wolverine.Persistence.EventSourcing.DeciderFunctionAttribute; + } + + namespace Wolverine.Persistence + { + public interface IStorageAction; + public class UnitOfWork : IStorageAction; + } + + namespace Wolverine.Persistence.EventSourcing + { + public class DeciderFunctionAttribute : System.Attribute; + public class EventsToAppend : System.Collections.Generic.List, Wolverine.Configuration.IWolverineReturnType; + [System.AttributeUsage(System.AttributeTargets.Method | System.AttributeTargets.Class, Inherited = true)] + public sealed class SlicePatternAttribute(JasperFx.Events.EventModeling.SlicePattern pattern) : System.Attribute + { + public JasperFx.Events.EventModeling.SlicePattern Pattern { get; } = pattern; + } + } + + namespace JasperFx.Events.EventModeling + { + public enum SlicePattern { Command, View, Automation, Translation } + } + """; + + const string EventsSource = + """ + using Wolverine.Persistence.EventSourcing; + using JasperFx.Events.EventModeling; + namespace Variants; + + public record Happened(string Note); + public record Another(string Note); + public record Third(string Note); + public class State { public System.Guid Id { get; set; } public void Apply(Happened e) { } public void Apply(Another e) { } } + public class Document { public string Id { get; set; } = string.Empty; } + public record Reply(string Text); + public record Follow(string Text); + public class Effect : Wolverine.ISideEffect { } + public class Aware : Wolverine.IResponseAware { } + public class Result : Microsoft.AspNetCore.Http.IResult { } + public class CustomStore : Wolverine.Persistence.IStorageAction { } + public record Expiry : Wolverine.TimeoutMessage; + public class StoreSaga : Wolverine.Saga { public System.Guid Id { get; set; } } + public class CreationResponseLike { } + public class UpdatedAggregateLike { } + """; + + const string LinksSource = + """ + namespace Variants; + + public record PlainAppend(System.Guid Id, string Note); + public record StringAppend(string Key, string Note); + public record ConditionalAppend(System.Guid Id, string Note, bool Share); + public record EarlyExitAppend(System.Guid Id, string Note, bool Skip); + public record UnconditionalAfterGuard(System.Guid Id, string Note); + public record ComputedAppend(string Slot, string Note, bool Share); + public record StartedAppend(string Note); + public record StartedWithId(System.Guid Id, string Note); + public record LambdaAppend(System.Guid Id, string Note); + public record IndirectAppend(System.Guid Id, string Note); + public record ArrayAppend(System.Guid Id, string Note); + public record VersionedAppend(System.Guid Id, string Note); + public record SwitchAppend(System.Guid Id, int Kind, string Note); + public record LoopAppend(System.Guid Id, string Note); + public record OtherParameterAppend(System.Guid Id, string Note); + public record SagaStateAppend(System.Guid Id); + public record ConditionalComputed(string Slot, string Note, bool Share); + + public static class PlainAppendHandler + { + public static void Handle(PlainAppend command, Marten.IDocumentSession session) => + session.Events.Append(command.Id, new Happened(command.Note)); + } + + public static class StringAppendHandler + { + public static void Handle(StringAppend command, Marten.IDocumentSession session) => + session.Events.Append(command.Key, new Happened(command.Note)); + } + + public static class ConditionalAppendHandler + { + public static void Handle(ConditionalAppend command, Marten.IDocumentSession session) + { + if (command.Share) { session.Events.Append(command.Id, new Happened(command.Note)); } + } + } + + public static class EarlyExitAppendHandler + { + public static void Handle(EarlyExitAppend command, Marten.IDocumentSession session) + { + if (command.Skip) { return; } + session.Events.Append(command.Id, new Happened(command.Note)); + } + } + + public static class UnconditionalAfterGuardHandler + { + public static void Handle(UnconditionalAfterGuard command, Marten.IDocumentSession session) + { + var ignored = 1; + session.Events.Append(command.Id, new Happened(command.Note)); + } + } + + public static class ComputedAppendHandler + { + public static void Handle(ComputedAppend command, Marten.IDocumentSession session) => + session.Events.Append(System.Guid.Parse(command.Slot), new Happened(command.Note)); + } + + public static class ConditionalComputedHandler + { + public static void Handle(ConditionalComputed command, Marten.IDocumentSession session) + { + if (command.Share) { session.Events.Append(System.Guid.Parse(command.Slot), new Happened(command.Note)); } + } + } + + public static class StartedAppendHandler + { + public static void Handle(StartedAppend command, Marten.IDocumentSession session) => + session.Events.StartStream(new Happened(command.Note)); + } + + public static class StartedWithIdHandler + { + public static void Handle(StartedWithId command, Marten.IDocumentSession session) => + session.Events.StartStream(command.Id, new Happened(command.Note)); + } + + public static class LambdaAppendHandler + { + public static void Handle(LambdaAppend command, Marten.IDocumentSession session) + { + var later = new System.Action(() => session.Events.Append(command.Id, new Happened(command.Note))); + later(); + } + } + + public static class IndirectAppendHandler + { + public static void Handle(IndirectAppend command, Marten.IDocumentSession session) + { + var created = new Happened(command.Note); + session.Events.Append(command.Id, created); + } + } + + public static class ArrayAppendHandler + { + public static void Handle(ArrayAppend command, Marten.IDocumentSession session) => + session.Events.Append(command.Id, new object[] { new Happened(command.Note), new Another(command.Note) }); + } + + public static class VersionedAppendHandler + { + public static void Handle(VersionedAppend command, Marten.IDocumentSession session) => + session.Events.Append(command.Id, 3L, new Happened(command.Note), new Third(command.Note)); + } + + public static class SwitchAppendHandler + { + public static void Handle(SwitchAppend command, Marten.IDocumentSession session) + { + switch (command.Kind) + { + case 1: session.Events.Append(command.Id, new Happened(command.Note)); break; + default: session.Events.Append(command.Id, new Another(command.Note)); break; + } + } + } + + public static class LoopAppendHandler + { + public static void Handle(LoopAppend command, Marten.IDocumentSession session) + { + foreach (var note in new[] { command.Note }) { session.Events.Append(command.Id, new Happened(note)); } + } + } + + public static class OtherParameterAppendHandler + { + public static void Handle(OtherParameterAppend command, string tenant, Marten.IDocumentSession session) => + session.Events.Append(System.Guid.Parse(tenant), new Happened(command.Note)); + } + + public static class SagaStateAppendHandler + { + public static void Handle(SagaStateAppend command, Marten.IDocumentSession session) => + session.Events.Append(command.Id, new StoreSaga(), new Happened("x")); + } + """; + + const string YieldsSource = + """ + using Wolverine.Marten; + namespace Variants; + + public record YieldPlain(System.Guid Id); + public record YieldNoIdentity(string Note); + public record YieldConditional(System.Guid Id, bool Go); + public record YieldConditionalNoIdentity(string Note, bool Go); + public record YieldIndirect(System.Guid Id); + public record YieldAfterGuard(System.Guid Id, bool Skip); + public record YieldBreak(System.Guid Id, bool Stop); + public record YieldLocal(System.Guid Id); + public record YieldMany(System.Guid Id); + + public static class YieldPlainHandler + { + [AggregateHandler] + public static System.Collections.Generic.IEnumerable Handle(YieldPlain command, State state) + { + yield return new Happened("a"); + } + } + + public static class YieldNoIdentityHandler + { + [AggregateHandler] + public static System.Collections.Generic.IEnumerable Handle(YieldNoIdentity command, State state) + { + yield return new Happened(command.Note); + } + } + + public static class YieldConditionalHandler + { + [AggregateHandler] + public static System.Collections.Generic.IEnumerable Handle(YieldConditional command, State state) + { + if (command.Go) { yield return new Happened("a"); } + } + } + + public static class YieldConditionalNoIdentityHandler + { + [AggregateHandler] + public static System.Collections.Generic.IEnumerable Handle(YieldConditionalNoIdentity command, State state) + { + if (command.Go) { yield return new Happened(command.Note); } + } + } + + public static class YieldIndirectHandler + { + [AggregateHandler] + public static System.Collections.Generic.IEnumerable Handle(YieldIndirect command, State state) + { + var created = new Happened("a"); + yield return created; + } + } + + public static class YieldAfterGuardHandler + { + [AggregateHandler] + public static System.Collections.Generic.IEnumerable Handle(YieldAfterGuard command, State state) + { + if (command.Skip) { yield break; } + yield return new Another("a"); + } + } + + public static class YieldBreakHandler + { + [AggregateHandler] + public static System.Collections.Generic.IEnumerable Handle(YieldBreak command, State state) + { + yield return new Happened("first"); + if (command.Stop) { yield break; } + } + } + + public static class YieldLocalHandler + { + [AggregateHandler] + public static System.Collections.Generic.IEnumerable Handle(YieldLocal command, State state) + { + System.Collections.Generic.IEnumerable Inner() { yield return new Third("inner"); } + yield return new Happened("outer"); + } + } + """; + + const string ReturnsSource = + """ + using Wolverine.Marten; + namespace Variants; + + public record ReturnOutgoing(string Text); + public record ReturnSideEffect(string Text); + public record ReturnAware(string Text); + public record ReturnStore(string Text); + public record ReturnUnit(string Text); + public record ReturnCustomStore(string Text); + public record ReturnEvents(string Text); + public record ReturnAppend(string Text); + public record ReturnTimeout(string Text); + public record ReturnCascade(string Text); + public record ReturnTuple(string Text); + public record ReturnPrimitive(string Text); + public record ReturnTask(string Text); + public record ReturnEnumerable(string Text); + public record ReturnAggregateEvent(System.Guid Id); + public record ReturnAggregateWithStream(System.Guid Id); + public record ReturnAggregateOutgoing(System.Guid Id); + + public static class ReturnOutgoingHandler { public static Wolverine.OutgoingMessages Handle(ReturnOutgoing message) => []; } + public static class ReturnSideEffectHandler { public static Effect Handle(ReturnSideEffect message) => new(); } + public static class ReturnAwareHandler { public static Aware Handle(ReturnAware message) => new(); } + public static class ReturnStoreHandler { public static Wolverine.Persistence.IStorageAction Handle(ReturnStore message) => null!; } + public static class ReturnUnitHandler { public static Wolverine.Persistence.UnitOfWork Handle(ReturnUnit message) => new(); } + public static class ReturnCustomStoreHandler { public static CustomStore Handle(ReturnCustomStore message) => new(); } + public static class ReturnEventsHandler { public static Wolverine.Marten.Events Handle(ReturnEvents message) => []; } + public static class ReturnAppendHandler { public static Wolverine.Persistence.EventSourcing.EventsToAppend Handle(ReturnAppend message) => []; } + public static class ReturnTimeoutHandler { public static Expiry Handle(ReturnTimeout message) => new(); } + public static class ReturnCascadeHandler { public static Follow Handle(ReturnCascade message) => new(message.Text); } + public static class ReturnPrimitiveHandler { public static int Handle(ReturnPrimitive message) => 0; } + public static class ReturnTaskHandler { public static System.Threading.Tasks.Task Handle(ReturnTask message) => System.Threading.Tasks.Task.FromResult(new Follow(message.Text)); } + public static class ReturnEnumerableHandler { public static System.Collections.Generic.IEnumerable Handle(ReturnEnumerable message) => []; } + public static class ReturnTupleHandler + { + public static (Reply, Wolverine.OutgoingMessages, Effect, Follow, Wolverine.Persistence.UnitOfWork, Expiry, int, StoreSaga) Handle(ReturnTuple message) => default; + } + + public static class ReturnAggregateEventHandler + { + [AggregateHandler] + public static Happened Handle(ReturnAggregateEvent command, State state) => new("a"); + } + + public static class ReturnAggregateOutgoingHandler + { + [AggregateHandler] + public static (Another, Wolverine.OutgoingMessages, Follow) Handle(ReturnAggregateOutgoing command, State state) => default; + } + """; + + const string EndpointsSource = + """ + using Wolverine.Http; + namespace Variants; + + public record PostReply(string Text); + public record PostEmpty(string Text); + public record PostTuple(string Text); + public record PostResult(string Text); + public record PostCreation(string Text); + public record PostOutgoing(string Text); + public record PostVoid(string Text); + + public static class Endpoints + { + [WolverinePost("/reply")] public static Reply Reply(PostReply command) => new("x"); + [WolverinePost("/empty"), EmptyResponse] public static Follow Empty(PostEmpty command) => new("x"); + [WolverinePost("/tuple")] public static (Reply, Follow, Wolverine.OutgoingMessages) Tuple(PostTuple command) => default; + [WolverinePost("/result")] public static Result Result(PostResult command) => new(); + [WolverinePost("/creation")] public static CreationResponse Creation(PostCreation command) => new("/x"); + [WolverinePost("/outgoing")] public static (Wolverine.OutgoingMessages, Reply) Outgoing(PostOutgoing command) => default; + [WolverinePost("/void")] public static void Void(PostVoid command) { } + } + """; + + const string PatternsSource = + """ + using Wolverine.Persistence.EventSourcing; + using JasperFx.Events.EventModeling; + namespace Variants; + + public record PatternHttpPost(string Note); + public record PatternHttpGet; + public record PatternHttpHead; + public record PatternTimeoutMessage : Wolverine.TimeoutMessage; + public record PatternAgreeing; + public record PatternUnplaced; + public record PatternMissing; + public record PatternTimeoutAgree : Wolverine.TimeoutMessage; + public record PatternTranslated(string Note); + + public static class PatternEndpoints + { + [Wolverine.Http.WolverinePost("/pattern"), SlicePattern(SlicePattern.View)] + public static void Post(PatternHttpPost command, Marten.IDocumentSession session) => session.Events.Append(System.Guid.NewGuid(), new Happened(command.Note)); + + [Wolverine.Http.WolverineGet("/pattern-get"), SlicePattern(SlicePattern.Command)] + public static Reply Get() => new("x"); + + [Wolverine.Http.WolverineHead("/pattern-head"), SlicePattern(SlicePattern.Automation)] + public static Reply Head() => new("x"); + + [Wolverine.Http.WolverinePost("/pattern-agree"), SlicePattern(SlicePattern.Command)] + public static void Agree(PatternAgreeing command, Marten.IDocumentSession session) => session.Events.Append(System.Guid.NewGuid(), new Another("a")); + } + + public static class PatternTimeoutHandler + { + [SlicePattern(SlicePattern.Translation)] + public static void Handle(PatternTimeoutMessage command, Marten.IDocumentSession session) => session.Events.Append(System.Guid.NewGuid(), new Happened("t")); + } + + public static class PatternTimeoutAgreeHandler + { + [SlicePattern(SlicePattern.Automation)] + public static void Handle(PatternTimeoutAgree command, Marten.IDocumentSession session) => session.Events.Append(System.Guid.NewGuid(), new Another("t")); + } + + [SlicePattern(SlicePattern.Translation)] + public static class PatternTranslatedHandler + { + public static void Handle(PatternTranslated command, Marten.IDocumentSession session) => session.Events.Append(System.Guid.NewGuid(), new Third(command.Note)); + } + + public static class PatternUnplacedHandler + { + [SlicePattern(SlicePattern.Automation)] + public static void Handle(PatternUnplaced command) { } + } + + public static class PatternMissingHandler + { + [SlicePattern((SlicePattern)42)] + public static void Handle(PatternMissing command, Marten.IDocumentSession session) => session.Events.Append(System.Guid.NewGuid(), new Another("m")); + } + """; + + static readonly IReadOnlyList _references = + [ + .. ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")) + .Split(Path.PathSeparator) + .Select(_ => MetadataReference.CreateFromFile(_)) + ]; + + protected DotNetProjectCompilation Project = null!; + protected AdapterContribution Contribution = null!; + + void Establish() + { + var compilation = CSharpCompilation.Create( + "Variants", + [ + CSharpSyntaxTree.ParseText(FrameworkSource, path: "/workspace/Variants/Framework.cs"), + CSharpSyntaxTree.ParseText(EventsSource, path: "/workspace/Variants/Events.cs"), + CSharpSyntaxTree.ParseText(LinksSource, path: "/workspace/Variants/Links.cs"), + CSharpSyntaxTree.ParseText(YieldsSource, path: "/workspace/Variants/Yields.cs"), + CSharpSyntaxTree.ParseText(ReturnsSource, path: "/workspace/Variants/Returns.cs"), + CSharpSyntaxTree.ParseText(EndpointsSource, path: "/workspace/Variants/Endpoints.cs"), + CSharpSyntaxTree.ParseText(PatternsSource, path: "/workspace/Variants/Patterns.cs") + ], + _references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable)); + Project = new() + { + Name = "Variants", + ProjectPath = "/workspace/Variants/Variants.csproj", + SourceRoot = "/workspace", + Compilation = compilation, + AuthoredSyntaxTrees = compilation.SyntaxTrees.ToHashSet() + }; + } + + protected void Analyze() => + Contribution = new CritterStackScreenplayAdapter().Analyze( + new DotNetAnalysisContext([Project]), + new DotNetAdapterOptions()); +} diff --git a/Source/DotNET/CritterStack.Specs/given/a_wolverine_saga_variants_application.cs b/Source/DotNET/CritterStack.Specs/given/a_wolverine_saga_variants_application.cs new file mode 100644 index 0000000..617256c --- /dev/null +++ b/Source/DotNET/CritterStack.Specs/given/a_wolverine_saga_variants_application.cs @@ -0,0 +1,347 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.CritterStack.Screenplay.given; + +/// +/// One isolated saga per behavior of Wolverine saga discovery: the correlation member precedence, role admission, +/// chain and constructor rules, method and message ordering across files, completion and timeout reporting. Every +/// saga owns its messages so its diagnostics and facts can be pinned on their own. +/// +public class a_wolverine_saga_variants_application : Specification +{ + const string FrameworkSource = + """ + namespace Wolverine.Configuration + { + public interface IWolverineReturnType; + } + + namespace Wolverine.Attributes + { + public class WolverineIgnoreAttribute : System.Attribute; + } + + namespace Wolverine.Persistence.Sagas + { + [System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Field)] + public class SagaIdentityAttribute : System.Attribute; + + [System.AttributeUsage(System.AttributeTargets.Parameter)] + public class SagaIdentityFromAttribute(string propertyName) : System.Attribute + { + public string PropertyName { get; } = propertyName; + } + } + + namespace Wolverine + { + public class WolverineIgnoreAttribute : System.Attribute; + public class WolverineOptions; + public abstract class Saga + { + protected void MarkCompleted() { } + } + public abstract record TimeoutMessage(System.TimeSpan DelayTime) : Wolverine.Configuration.IWolverineReturnType; + } + """; + + const string CorrelationSource = + """ + using Wolverine.Persistence.Sagas; + + namespace Variants; + + public class AttributeMessage { [SagaIdentity] public System.Guid Chosen { get; set; } public System.Guid AttributeSagaId { get; set; } public System.Guid SagaId { get; set; } public System.Guid Id { get; set; } } + public class AttributeSaga : Wolverine.Saga { public void Handle(AttributeMessage message) { } } + + public class AttributeAmbiguousMessage { [SagaIdentity] public System.Guid First { get; set; } [SagaIdentity] public System.Guid Second { get; set; } public System.Guid AttributeAmbiguousSagaId { get; set; } } + public class AttributeAmbiguousSaga : Wolverine.Saga { public void Handle(AttributeAmbiguousMessage message) { } } + + public class AttrFromMessage { [SagaIdentity] public System.Guid Chosen { get; set; } public System.Guid Other { get; set; } public System.Guid AttrFromSagaId { get; set; } } + public class AttrFromSaga : Wolverine.Saga { public void Handle([SagaIdentityFrom("Other")] AttrFromMessage message) { } } + + public class FromMessage { public System.Guid Picked { get; set; } public System.Guid FromSagaId { get; set; } public System.Guid SagaId { get; set; } } + public class FromSaga : Wolverine.Saga { public void Handle([SagaIdentityFrom("Picked")] FromMessage message) { } } + + public class FromConflictMessage { public System.Guid Primary { get; set; } public System.Guid Secondary { get; set; } } + public class FromConflictSaga : Wolverine.Saga + { + public void Handle(FromConflictMessage message, [SagaIdentityFrom("Secondary")] int context) { } + public void Handles(FromConflictMessage message, [SagaIdentityFrom("Primary")] string context) { } + } + + public class FromRepeatedMessage { public System.Guid Primary { get; set; } public System.Guid Secondary { get; set; } } + public class FromRepeatedSaga : Wolverine.Saga + { + public void Handle(FromRepeatedMessage message, [SagaIdentityFrom("Primary")] int context) { } + public void Handles(FromRepeatedMessage message, [SagaIdentityFrom("Primary")] string context) { } + } + + public class FromMissingMessage { public System.Guid FromMissingSagaId { get; set; } public System.Guid FromMissingId { get; set; } public System.Guid SagaId { get; set; } public System.Guid Id { get; set; } } + public class FromMissingSaga : Wolverine.Saga { public void Handle([SagaIdentityFrom("Nope")] FromMissingMessage message) { } } + + public class FromAmbiguousBase { public System.Guid Picked { get; set; } } + public class FromAmbiguousMessage : FromAmbiguousBase { public new System.Guid Picked { get; set; } public System.Guid SagaId { get; set; } } + public class FromAmbiguousSaga : Wolverine.Saga { public void Handle([SagaIdentityFrom("Picked")] FromAmbiguousMessage message) { } } + + public class FullNameMessage { public System.Guid FullNameSagaId { get; set; } public System.Guid FullNameId { get; set; } public System.Guid SagaId { get; set; } public System.Guid Id { get; set; } } + public class FullNameSaga : Wolverine.Saga { public void Handle(FullNameMessage message) { } } + + public class FullAmbiguousBase { public System.Guid FullAmbiguousSagaId { get; set; } } + public class FullAmbiguousMessage : FullAmbiguousBase { public new System.Guid FullAmbiguousSagaId { get; set; } public System.Guid SagaId { get; set; } } + public class FullAmbiguousSaga : Wolverine.Saga { public void Handle(FullAmbiguousMessage message) { } } + + public class StrippedMessage { public System.Guid StrippedId { get; set; } public System.Guid SagaId { get; set; } public System.Guid Id { get; set; } } + public class StrippedSaga : Wolverine.Saga { public void Handle(StrippedMessage message) { } } + + public class StrippedAnywhereMessage { public System.Guid OrderId { get; set; } public System.Guid SagaId { get; set; } } + public class OrdersagaSaga : Wolverine.Saga { public void Handle(StrippedAnywhereMessage message) { } } + + public class StrippedAmbiguousBase { public System.Guid StrippedAmbiguousId { get; set; } } + public class StrippedAmbiguousMessage : StrippedAmbiguousBase { public new System.Guid StrippedAmbiguousId { get; set; } public System.Guid SagaId { get; set; } } + public class StrippedAmbiguousSaga : Wolverine.Saga { public void Handle(StrippedAmbiguousMessage message) { } } + + public class SagaIdConventionMessage { public System.Guid SagaId { get; set; } public System.Guid Id { get; set; } } + public class SagaIdConventionSaga : Wolverine.Saga { public void Handle(SagaIdConventionMessage message) { } } + + public class SagaIdAmbiguousBase { public System.Guid SagaId { get; set; } } + public class SagaIdAmbiguousMessage : SagaIdAmbiguousBase { public new System.Guid SagaId { get; set; } public System.Guid Id { get; set; } } + public class SagaIdAmbiguousSaga : Wolverine.Saga { public void Handle(SagaIdAmbiguousMessage message) { } } + + public class IdMessage { public System.Guid ID { get; set; } } + public class IdSaga : Wolverine.Saga { public void Handle(IdMessage message) { } } + + public class IdAmbiguousMessage { public System.Guid Id { get; set; } public System.Guid ID { get; set; } } + public class IdAmbiguousSaga : Wolverine.Saga { public void Handle(IdAmbiguousMessage message) { } } + + public class NoCorrelationMessage { public string Name { get; set; } = string.Empty; } + public class NoCorrelationSaga : Wolverine.Saga { public void Handle(NoCorrelationMessage message) { } } + + public class MemberKindsMessage { internal System.Guid SagaId { get; set; } public static System.Guid MemberKindsSagaId { get; set; } public System.Guid this[int index] => default; public System.Guid Id { get; set; } } + public class MemberKindsSaga : Wolverine.Saga { public void Handle(MemberKindsMessage message) { } } + + public class FieldMessage { public System.Guid FieldSagaId; public System.Guid Id { get; set; } } + public class FieldSaga : Wolverine.Saga { public void Handle(FieldMessage message) { } } + + public class FieldBeforePropertyBase { public System.Guid SagaId; } + public class FieldBeforePropertyMessage : FieldBeforePropertyBase { public new System.Guid SagaId { get; set; } } + public class FieldBeforePropertySaga : Wolverine.Saga { public void Handle(FieldBeforePropertyMessage message) { } } + + public partial class GeneratedMemberMessage { public System.Guid Id { get; set; } } + public class GeneratedMemberSaga : Wolverine.Saga { public void Handle(GeneratedMemberMessage message) { } } + """; + + const string GeneratedSource = + """ + // + namespace Variants; + + public partial class GeneratedMemberMessage { public System.Guid GeneratedMemberSagaId { get; set; } } + """; + + const string RoleSource = + """ + namespace Variants; + + public record RoleMessage(System.Guid SagaId); + public record SecondRoleMessage(System.Guid SagaId); + + public class RoleShapesSaga : Wolverine.Saga + { + public System.Guid SagaId { get; set; } + + private void Handle(RoleMessage message) { } + public void Handle(RoleMessage message) { } + public void Handle() { } + public int Handle(SecondRoleMessage message) => 0; + public void Handles(object message) { } + public void Handles(int message) { } + public void Handles(string message) { } + public void Consume(System.Guid[] message) { } + public void Consumes(RoleShapesSaga message) { } + public static void Orchestrate(RoleMessage message) { } + public static void StartOrHandle(RoleMessage message) { } + public void NotFound(RoleMessage message) { } + public void Orchestrates(System.Collections.Generic.List message) { } + [Wolverine.Attributes.WolverineIgnore] public void OrchestrateAsync(RoleMessage message) { } + [Wolverine.WolverineIgnore] public void StartsOrHandles(RoleMessage message) { } + public void handle(RoleMessage message) { } + public void Other(RoleMessage message) { } + public void HandleAsync(RoleMessage message) { } + } + + public record NoChainMessage(System.Guid SagaId); + public record ChainedMessage(System.Guid SagaId); + public class ChainSaga : Wolverine.Saga + { + public static ChainSaga Starts(NoChainMessage message) => new(); + public static ChainSaga Starts(ChainedMessage message) => new(); + public void Handles(ChainedMessage message) { } + } + + public record CtorMixedMessage(System.Guid SagaId); + public record CtorAloneMessage(System.Guid SagaId); + public class CtorStartOrHandleSaga : Wolverine.Saga + { + CtorStartOrHandleSaga() { } + public void StartOrHandle(CtorMixedMessage message) { } + public void Handle(CtorMixedMessage message) { } + public void StartOrHandle(CtorAloneMessage message) { } + } + + public record CtorReturnedMessage(System.Guid SagaId); + public record CtorVoidMessage(System.Guid SagaId); + public record CtorOrchestratedMessage(System.Guid SagaId); + public record CtorAsyncMessage(System.Guid SagaId); + public class CtorStartSaga : Wolverine.Saga + { + CtorStartSaga() { } + public static CtorStartSaga Start(CtorReturnedMessage message) => new(); + public static void Start(CtorVoidMessage message) { } + public static CtorStartSaga Start(CtorOrchestratedMessage message) => new(); + public void Handle(CtorOrchestratedMessage message) { } + public static System.Threading.Tasks.Task StartAsync(CtorAsyncMessage message) => System.Threading.Tasks.Task.FromResult(new CtorStartSaga()); + } + + public record CtorNotFoundAloneMessage(System.Guid SagaId); + public record CtorNotFoundHandledMessage(System.Guid SagaId); + public class CtorNotFoundSaga : Wolverine.Saga + { + CtorNotFoundSaga() { } + public static void NotFound(CtorNotFoundAloneMessage message) { } + public static void NotFound(CtorNotFoundHandledMessage message) { } + public void Handle(CtorNotFoundHandledMessage message) { } + } + """; + + const string OrderingZetaSource = + """ + namespace Variants; + + public record OrderZMessage(System.Guid SagaId); + public record OrderAMessage(System.Guid SagaId); + + public partial class ZetaSaga : Wolverine.Saga + { + public void Handle(OrderZMessage message) { } + public void Handle(OrderAMessage message, string context) { } + } + """; + + const string OrderingAlphaSource = + """ + namespace Variants; + + public partial class ZetaSaga + { + public void Handles(OrderZMessage message) { } + public void Handle(OrderAMessage message) { } + } + + public class AlphaSaga : Wolverine.Saga + { + public void Handle(OrderAMessage message) { } + } + """; + + const string CompletionSource = + """ + namespace Variants; + + public record CompletionFirstMessage(System.Guid SagaId); + public record CompletionSecondMessage(System.Guid SagaId); + public class CompletionSaga : Wolverine.Saga + { + public void Handle(CompletionSecondMessage message) { var later = new System.Action(() => MarkCompleted()); later(); MarkCompleted(); } + public void Handle(CompletionFirstMessage message) { MarkCompleted(); } + } + + public record NotCompletedMessage(System.Guid SagaId); + public class Closer { public void MarkCompleted() { } } + public class NotCompletedSaga : Wolverine.Saga + { + public void Handle(NotCompletedMessage message) { new Closer().MarkCompleted(); } + } + + public record ReminderTimeout(System.TimeSpan Delay) : Wolverine.TimeoutMessage(Delay); + public record ExpiryTimeout(System.TimeSpan Delay) : Wolverine.TimeoutMessage(Delay); + public record TimeoutMessageIn(System.Guid SagaId); + public class TimeoutSaga : Wolverine.Saga + { + public (ReminderTimeout, ExpiryTimeout, ReminderTimeout) Handle(TimeoutMessageIn message) => default; + } + """; + + static readonly IReadOnlyList _references = + [ + .. ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")) + .Split(Path.PathSeparator) + .Select(_ => MetadataReference.CreateFromFile(_)) + ]; + + protected IReadOnlyList Lines = []; + + protected static string[] For(IReadOnlyList lines, string saga) => + [.. lines.Where(_ => _.Contains($"Variants.{saga}", StringComparison.Ordinal) || _.Contains($"'{saga}'", StringComparison.Ordinal) || _.Contains($"'{saga}.", StringComparison.Ordinal))]; + + DotNetProjectCompilation _project = null!; + + void Establish() => + _project = Project( + ("/workspace/Saga/Framework.cs", FrameworkSource), + ("/workspace/Saga/Correlation.cs", CorrelationSource), + ("/workspace/Saga/Generated.cs", GeneratedSource), + ("/workspace/Saga/Roles.cs", RoleSource), + ("/workspace/Saga/Zeta.cs", OrderingZetaSource), + ("/workspace/Saga/Alpha.cs", OrderingAlphaSource), + ("/workspace/Saga/Completion.cs", CompletionSource)); + + protected void Analyze() + { + var contribution = new CritterStackScreenplayAdapter().Analyze(new DotNetAnalysisContext([_project]), new DotNetAdapterOptions()); + Lines = + [ + .. diagnostic_ranges.Of(contribution).Select(_ => "diagnostic " + _), + .. contribution.Facts.Select(Describe).OfType() + ]; + Lines = [.. Lines.Select(Normalize)]; + } + + static string Normalize(string line) => line + .Replace("dotnet://Variants/Variants/Variants.", "~", StringComparison.Ordinal) + .Replace(LifecycleText, "", StringComparison.Ordinal) + .Replace("wolverine:saga-handles:", string.Empty, StringComparison.Ordinal); + + const string LifecycleText = "uses Wolverine-managed lifecycle. Authored source does not safely establish a portable domain workflow; neutral Saga/Handler/Handles facts are retained as realization/provenance, and Screenplay uses ordinary Event Modeling building blocks"; + + static string? Describe(GenerationFact fact) => fact switch + { + RelationshipFact { Definition.Key.Discriminator: { } discriminator } relationship when discriminator.StartsWith("wolverine:saga", StringComparison.Ordinal) => + $"relationship {relationship.Id.Value}|{relationship.Evidence.Strength}|{relationship.Evidence.Explanation}|{Range(relationship.Evidence.Source)}|{relationship.Definition.TargetMember}", + ArtifactFact { Definition.Key.Kind: ArtifactKind.Saga } artifact => + $"saga {artifact.Id.Value}|{string.Join(',', artifact.Definition.Properties.Select(_ => _.Name))}", + _ => null + }; + + static string Range(SourceRange? source) => source is null + ? "no-source" + : $"{source.Path}:{source.StartLine}:{source.StartColumn}-{source.EndLine}:{source.EndColumn}"; + + static DotNetProjectCompilation Project(params (string Path, string Source)[] sources) + { + var compilation = CSharpCompilation.Create( + "Variants", + sources.Select(_ => CSharpSyntaxTree.ParseText(_.Source, path: _.Path)), + _references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable)); + return new() + { + Name = "Variants", + ProjectPath = "/workspace/Saga/Variants.csproj", + SourceRoot = "/workspace", + Compilation = compilation, + AuthoredSyntaxTrees = compilation.SyntaxTrees.ToHashSet() + }; + } +} diff --git a/Source/DotNET/CritterStack.Specs/given/a_wolverine_validator_option_scenarios.cs b/Source/DotNET/CritterStack.Specs/given/a_wolverine_validator_option_scenarios.cs new file mode 100644 index 0000000..ebf7930 --- /dev/null +++ b/Source/DotNET/CritterStack.Specs/given/a_wolverine_validator_option_scenarios.cs @@ -0,0 +1,142 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Cratis.CritterStack.Screenplay.Wolverine; + +namespace Cratis.CritterStack.Screenplay.given; + +/// +/// Isolated projects, one per FluentValidation option form, each holding a public and an internal validator, so the +/// resolved option values are visible through the validation diagnostics the discovery produces for each request. +/// +public class a_wolverine_validator_option_scenarios : Specification +{ + const string FrameworkSource = + """ + namespace FluentValidation + { + public interface IValidator; + } + + namespace Wolverine + { + public class WolverineOptions; + } + + namespace Wolverine.FluentValidation + { + public enum RegistrationBehavior + { + DiscoverAndRegisterValidators, + ExplicitRegistration + } + + public class FluentValidationConfiguration + { + public RegistrationBehavior RegistrationBehavior { get; set; } + public bool IncludeInternalTypes { get; set; } + } + + public static class WolverineFluentValidationExtensions + { + public static Wolverine.WolverineOptions UseFluentValidation( + this Wolverine.WolverineOptions options, + RegistrationBehavior behavior = RegistrationBehavior.DiscoverAndRegisterValidators, + bool includeInternalTypes = false) => options; + + public static Wolverine.WolverineOptions UseFluentValidation( + this Wolverine.WolverineOptions options, + System.Action configure) => options; + } + } + """; + + static readonly IReadOnlyList _references = + [ + .. ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")) + .Split(Path.PathSeparator) + .Select(_ => MetadataReference.CreateFromFile(_)) + ]; + + protected IReadOnlyDictionary Outcomes = null!; + + void Establish() => Outcomes = new Dictionary + { + ["default"] = Run("options.UseFluentValidation();"), + ["positional-include-internal"] = Run("options.UseFluentValidation(RegistrationBehavior.DiscoverAndRegisterValidators, true);"), + ["positional-exclude-internal"] = Run("options.UseFluentValidation(RegistrationBehavior.DiscoverAndRegisterValidators, false);"), + ["positional-explicit-registration"] = Run("options.UseFluentValidation(RegistrationBehavior.ExplicitRegistration, true);"), + ["callback-include-internal"] = Run("options.UseFluentValidation(c => { c.IncludeInternalTypes = true; });"), + ["callback-include-internal-then-exclude"] = Run("options.UseFluentValidation(c => { c.IncludeInternalTypes = true; c.IncludeInternalTypes = false; });"), + ["callback-exclude-internal-then-include"] = Run("options.UseFluentValidation(c => { c.IncludeInternalTypes = false; c.IncludeInternalTypes = true; });"), + ["callback-explicit-registration"] = Run("options.UseFluentValidation(c => c.RegistrationBehavior = RegistrationBehavior.ExplicitRegistration);"), + ["callback-explicit-then-discover"] = Run("options.UseFluentValidation(c => { c.RegistrationBehavior = RegistrationBehavior.ExplicitRegistration; c.RegistrationBehavior = RegistrationBehavior.DiscoverAndRegisterValidators; c.IncludeInternalTypes = true; });"), + ["callback-discover-then-explicit"] = Run("options.UseFluentValidation(c => { c.IncludeInternalTypes = true; c.RegistrationBehavior = RegistrationBehavior.DiscoverAndRegisterValidators; c.RegistrationBehavior = RegistrationBehavior.ExplicitRegistration; });") + }; + + static string[] Run(string configuration) + { + var source = + $$""" + using Wolverine.FluentValidation; + + namespace Scenario; + + public record PublicRequest(System.Guid Id); + public record InternalRequest(System.Guid Id); + + public class PublicRequestValidator : FluentValidation.IValidator; + internal class InternalRequestValidator : FluentValidation.IValidator; + + public static class Handler + { + public static void Handle(PublicRequest request) { } + public static void Handle(InternalRequest request) { } + } + + public static class Configuration + { + public static void Configure(Wolverine.WolverineOptions options) + { + {{configuration}} + } + } + """; + var compilation = CSharpCompilation.Create( + "Scenario", + [ + CSharpSyntaxTree.ParseText(FrameworkSource, path: "/workspace/Framework.cs"), + CSharpSyntaxTree.ParseText(source, path: "/workspace/Scenario/Program.cs") + ], + _references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable)); + var project = new DotNetProjectCompilation + { + Name = "Scenario", + ProjectPath = "/workspace/Scenario/Scenario.csproj", + SourceRoot = "/workspace", + Compilation = compilation, + AuthoredSyntaxTrees = compilation.SyntaxTrees.ToHashSet() + }; + + var discovery = WolverineValidationAuthorizationDiscovery.Discover(project, new CritterStackSubjectResolver(new([project]))); + var subject = new SubjectId { Value = "dotnet://Scenario/Scenario/Scenario.Handler" }; + var handlers = compilation.GetTypeByMetadataName("Scenario.Handler").GetMembers("Handle").OfType().ToArray(); + return + [ + .. discovery.Diagnostics.Select(Format), + .. handlers.SelectMany(handler => discovery.ValidationDiagnostics( + handler, + (INamedTypeSymbol)handler.Parameters[0].Type, + subject, + false)).Select(Format) + ]; + } + + static string Format(GenerationDiagnostic diagnostic) => + $"{diagnostic.Code}|{diagnostic.Severity}|{diagnostic.Outcome}|{diagnostic.Subject?.Value}|{Range(diagnostic.Source)}|{diagnostic.Message}"; + + static string Range(SourceRange? source) => source is null + ? "no-source" + : $"{source.Path}:{source.StartLine}:{source.StartColumn}-{source.EndLine}:{source.EndColumn}"; +} diff --git a/Source/DotNET/CritterStack/CritterStackSource.cs b/Source/DotNET/CritterStack/CritterStackSource.cs index 0f242a3..5a80f21 100644 --- a/Source/DotNET/CritterStack/CritterStackSource.cs +++ b/Source/DotNET/CritterStack/CritterStackSource.cs @@ -1,6 +1,8 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; + using AdapterIdentity = Cratis.Screenplay.Generation.AdapterIdentity; using CompilationReference = Microsoft.CodeAnalysis.CompilationReference; using DotNetGeneratedSource = Cratis.Screenplay.Generation.DotNet.DotNetGeneratedSource; @@ -104,9 +106,7 @@ location.SourceTree is { } sourceTree && static SourceRange? LegacyRange(Location location, string? sourceRoot) { - if (!location.IsInSource || - location.SourceTree is null || - DotNetGeneratedSource.IsGenerated(location.SourceTree) || + if (!IsHandWrittenSource(location) || !TryGetSafeRelativePath(location, sourceRoot, out var relativePath)) { return null; @@ -123,16 +123,26 @@ location.SourceTree is null || }; } + static bool IsHandWrittenSource(Location location) => + location.IsInSource && + location.SourceTree is { } sourceTree && + !DotNetGeneratedSource.IsGenerated(sourceTree); + + static bool IsFullyQualifiedPath([NotNullWhen(true)] string? path) => + !string.IsNullOrWhiteSpace(path) && Path.IsPathFullyQualified(path); + + static bool IsOutsideRoot(string relativeCandidate) => + string.IsNullOrWhiteSpace(relativeCandidate) || + Path.IsPathFullyQualified(relativeCandidate) || + string.Equals(relativeCandidate, ".", StringComparison.Ordinal) || + string.Equals(relativeCandidate, "..", StringComparison.Ordinal) || + relativeCandidate.StartsWith("../", StringComparison.Ordinal); + static bool TryGetSafeRelativePath(Location location, string? sourceRoot, out string relativePath) { relativePath = string.Empty; - if (string.IsNullOrWhiteSpace(sourceRoot) || !Path.IsPathFullyQualified(sourceRoot)) - { - return false; - } - var physicalPath = location.GetLineSpan().Path; - if (string.IsNullOrWhiteSpace(physicalPath) || !Path.IsPathFullyQualified(physicalPath)) + if (!IsFullyQualifiedPath(sourceRoot) || !IsFullyQualifiedPath(physicalPath)) { return false; } @@ -142,11 +152,7 @@ static bool TryGetSafeRelativePath(Location location, string? sourceRoot, out st var normalizedRoot = Path.GetFullPath(sourceRoot); var normalizedPath = Path.GetFullPath(physicalPath); var candidate = Path.GetRelativePath(normalizedRoot, normalizedPath).Replace('\\', '/'); - if (string.IsNullOrWhiteSpace(candidate) || - Path.IsPathFullyQualified(candidate) || - string.Equals(candidate, ".", StringComparison.Ordinal) || - string.Equals(candidate, "..", StringComparison.Ordinal) || - candidate.StartsWith("../", StringComparison.Ordinal)) + if (IsOutsideRoot(candidate)) { return false; } diff --git a/Source/DotNET/CritterStack/CritterStackSourcePlacement.cs b/Source/DotNET/CritterStack/CritterStackSourcePlacement.cs index 113aed6..df77eeb 100644 --- a/Source/DotNET/CritterStack/CritterStackSourcePlacement.cs +++ b/Source/DotNET/CritterStack/CritterStackSourcePlacement.cs @@ -52,38 +52,14 @@ public static IReadOnlyList Derive( } var structures = snapshot.Structures.ToDictionary(_ => _.Subject); - var missingStructures = strongestIntents - .Where(intent => !structures.ContainsKey(EffectiveOwner(intent))) - .GroupBy(intent => $"{intent.Artifact.Subject.Value}\u001f{(int)intent.Artifact.Kind}\u001f{EffectiveOwner(intent).Value}", StringComparer.Ordinal) - .Select(_ => _.First()) - .Select(MissingSourceMapping) - .OrderBy(_ => _.Code, StringComparer.Ordinal) - .ThenBy(_ => _.Subject?.Value, StringComparer.Ordinal) - .ThenBy(_ => _.Source?.Path, StringComparer.Ordinal) - .ThenBy(_ => _.Source?.StartLine) - .ThenBy(_ => _.Source?.StartColumn) - .ThenBy(_ => _.Message, StringComparer.Ordinal) - .ToArray(); + var missingStructures = MissingStructureDiagnostics(strongestIntents, structures); if (missingStructures.Length > 0) { diagnostics.AddRange(missingStructures); return []; } - var requests = strongestIntents.Select(intent => new DotNetSourcePlacementRequest - { - Artifact = intent.Artifact, - Structure = structures[EffectiveOwner(intent)], - SourceOwner = intent.SourceOwner, - SliceKind = intent.CompatibilityPlacement.SliceKind, - Policy = options.SourceStructurePolicy, - CompatibilityPolicy = new DotNetSourcePlacementCompatibilityPolicy - { - Version = 1, - Placement = intent.CompatibilityPlacement - } - }); - var placementSnapshot = DotNetSourcePlacementDerivation.Derive(requests); + var placementSnapshot = DotNetSourcePlacementDerivation.Derive(PlacementRequests(strongestIntents, structures, options)); diagnostics.AddRange(placementSnapshot.Diagnostics); if (!placementSnapshot.IsSuccess) { @@ -128,6 +104,40 @@ public static ArtifactPlacement CompatibilityPlacement( SliceKind = sliceKind }; + static GenerationDiagnostic[] MissingStructureDiagnostics( + IReadOnlyList intents, + Dictionary structures) => + [ + .. intents + .Where(intent => !structures.ContainsKey(EffectiveOwner(intent))) + .GroupBy(intent => $"{intent.Artifact.Subject.Value}\u001f{(int)intent.Artifact.Kind}\u001f{EffectiveOwner(intent).Value}", StringComparer.Ordinal) + .Select(_ => _.First()) + .Select(MissingSourceMapping) + .OrderBy(_ => _.Code, StringComparer.Ordinal) + .ThenBy(_ => _.Subject?.Value, StringComparer.Ordinal) + .ThenBy(_ => _.Source?.Path, StringComparer.Ordinal) + .ThenBy(_ => _.Source?.StartLine) + .ThenBy(_ => _.Source?.StartColumn) + .ThenBy(_ => _.Message, StringComparer.Ordinal) + ]; + + static IEnumerable PlacementRequests( + IReadOnlyList intents, + Dictionary structures, + DotNetAdapterOptions options) => intents.Select(intent => new DotNetSourcePlacementRequest + { + Artifact = intent.Artifact, + Structure = structures[EffectiveOwner(intent)], + SourceOwner = intent.SourceOwner, + SliceKind = intent.CompatibilityPlacement.SliceKind, + Policy = options.SourceStructurePolicy, + CompatibilityPolicy = new DotNetSourcePlacementCompatibilityPolicy + { + Version = 1, + Placement = intent.CompatibilityPlacement + } + }); + static SubjectId EffectiveOwner(CritterStackPlacementIntent intent) => intent.SourceOwner ?? intent.Artifact.Subject; static GenerationDiagnostic MissingSourceMapping(CritterStackPlacementIntent intent) diff --git a/Source/DotNET/CritterStack/EnumConceptFacts.cs b/Source/DotNET/CritterStack/EnumConceptFacts.cs index 5f80975..d27df77 100644 --- a/Source/DotNET/CritterStack/EnumConceptFacts.cs +++ b/Source/DotNET/CritterStack/EnumConceptFacts.cs @@ -31,11 +31,7 @@ public static AdapterContribution AddTo(DotNetAnalysisContext context, AdapterCo foreach (var candidate in candidates) { - var declaration = DotNetSource.AuthoredDeclarationsOf(candidate.Type, candidate.Project.AuthoredSyntaxTrees) - .Where(_ => !DotNetGeneratedSource.IsGenerated(_.SyntaxTree)) - .Select(_ => _.GetSyntax()) - .OfType() - .SingleOrDefault(); + var declaration = AuthoredDeclaration(candidate.Type, candidate.Project); if (declaration is null) { continue; @@ -48,65 +44,19 @@ public static AdapterContribution AddTo(DotNetAnalysisContext context, AdapterCo candidate.Project, EvidenceStrength.Exact, $"Authored enum '{candidate.Type.Name}' declares its named values"); - if (DotNetSymbols.HasAttribute(candidate.Type, "System.FlagsAttribute")) - { - diagnostics.Add(new GenerationDiagnostic - { - Code = CritterStackDiagnosticCodes.FlagsEnumOmitted, - Severity = GenerationDiagnosticSeverity.Warning, - Outcome = GenerationDiagnosticOutcome.Unsupported, - Subject = subject, - Source = evidence.Source, - Message = $"Flags enum '{candidate.Type.Name}' permits combinations that cannot be represented by an enumeration concept; it is not declared as a concept and its uses keep the plain type name without a concept reference" - }); - continue; - } - var values = declaration.Members.Select(_ => _.Identifier.ValueText).ToArray(); - if (!CanLower(values)) + if (OmissionOf(candidate.Type, subject, evidence, values) is { } omission) { - diagnostics.Add(new GenerationDiagnostic - { - Code = CritterStackDiagnosticCodes.EnumValuesUnsupported, - Severity = GenerationDiagnosticSeverity.Warning, - Outcome = GenerationDiagnosticOutcome.Unsupported, - Subject = subject, - Source = evidence.Source, - Message = values.Length == 0 - ? $"Enum '{candidate.Type.Name}' declares no named values, which an enumeration concept cannot represent; it is not declared as a concept and its uses keep the plain type name without a concept reference" - : $"Enum '{candidate.Type.Name}' has values that collide or are not valid Screenplay identifiers after Screenplay naming; it is not declared as a concept and its uses keep the plain type name without a concept reference" - }); + diagnostics.Add(omission); continue; } if (concepts.Add(subject)) { - facts.Add(new ArtifactFact - { - Id = new FactId { Value = ConceptFactId(contribution.Adapter, subject) }, - Subject = subject, - Evidence = evidence, - Definition = new ArtifactDefinition - { - Key = new ArtifactKey { Subject = subject, Kind = ArtifactKind.Concept }, - Name = candidate.Type.Name, - File = evidence.Source?.Path - } - }); + facts.Add(ConceptFact(contribution.Adapter, subject, candidate.Type.Name, evidence)); } - facts.Add(new ConceptRepresentationFact - { - Id = new FactId { Value = RepresentationFactId(contribution.Adapter, subject) }, - Subject = subject, - Evidence = evidence, - Definition = new ConceptRepresentationDefinition - { - Concept = subject, - Kind = ConceptRepresentationKind.Enumeration, - EnumerationValues = [.. values] - } - }); + facts.Add(RepresentationFact(contribution.Adapter, subject, evidence, values)); } // Bind before resolving captured names, so uses of each enum carry its subject and only other types print its @@ -271,6 +221,74 @@ .. artifact.Definition.Properties.Select(property => IsDropped(property) ]; } + static GenerationDiagnostic? OmissionOf(INamedTypeSymbol type, SubjectId subject, Evidence evidence, string[] values) + { + if (DotNetSymbols.HasAttribute(type, "System.FlagsAttribute")) + { + return OmittedEnum( + CritterStackDiagnosticCodes.FlagsEnumOmitted, + subject, + evidence, + $"Flags enum '{type.Name}' permits combinations that cannot be represented by an enumeration concept; it is not declared as a concept and its uses keep the plain type name without a concept reference"); + } + + if (CanLower(values)) + { + return null; + } + + return OmittedEnum( + CritterStackDiagnosticCodes.EnumValuesUnsupported, + subject, + evidence, + values.Length == 0 + ? $"Enum '{type.Name}' declares no named values, which an enumeration concept cannot represent; it is not declared as a concept and its uses keep the plain type name without a concept reference" + : $"Enum '{type.Name}' has values that collide or are not valid Screenplay identifiers after Screenplay naming; it is not declared as a concept and its uses keep the plain type name without a concept reference"); + } + + static ArtifactFact ConceptFact(AdapterIdentity adapter, SubjectId subject, string name, Evidence evidence) => new() + { + Id = new FactId { Value = ConceptFactId(adapter, subject) }, + Subject = subject, + Evidence = evidence, + Definition = new ArtifactDefinition + { + Key = new ArtifactKey { Subject = subject, Kind = ArtifactKind.Concept }, + Name = name, + File = evidence.Source?.Path + } + }; + + static ConceptRepresentationFact RepresentationFact(AdapterIdentity adapter, SubjectId subject, Evidence evidence, string[] values) => new() + { + Id = new FactId { Value = RepresentationFactId(adapter, subject) }, + Subject = subject, + Evidence = evidence, + Definition = new ConceptRepresentationDefinition + { + Concept = subject, + Kind = ConceptRepresentationKind.Enumeration, + EnumerationValues = [.. values] + } + }; + + static EnumDeclarationSyntax? AuthoredDeclaration(INamedTypeSymbol type, DotNetProjectCompilation project) => + DotNetSource.AuthoredDeclarationsOf(type, project.AuthoredSyntaxTrees) + .Where(_ => !DotNetGeneratedSource.IsGenerated(_.SyntaxTree)) + .Select(_ => _.GetSyntax()) + .OfType() + .SingleOrDefault(); + + static GenerationDiagnostic OmittedEnum(string code, SubjectId subject, Evidence evidence, string message) => new() + { + Code = code, + Severity = GenerationDiagnosticSeverity.Warning, + Outcome = GenerationDiagnosticOutcome.Unsupported, + Subject = subject, + Source = evidence.Source, + Message = message + }; + static GenerationDiagnostic ConflictDiagnostic(ArtifactFact concept, string message) => new() { Code = CritterStackDiagnosticCodes.EnumConceptNameConflict, diff --git a/Source/DotNET/CritterStack/Wolverine/WolverineConventionAlterationDiscovery.cs b/Source/DotNET/CritterStack/Wolverine/WolverineConventionAlterationDiscovery.cs index 4b99452..ca60c15 100644 --- a/Source/DotNET/CritterStack/Wolverine/WolverineConventionAlterationDiscovery.cs +++ b/Source/DotNET/CritterStack/Wolverine/WolverineConventionAlterationDiscovery.cs @@ -14,12 +14,29 @@ public static IReadOnlyList Discover( DotNetProjectCompilation project, CritterStackSubjectResolver subjects) { - var diagnostics = new List(); + List diagnostics = []; + diagnostics.AddRange(AlterationTypeDiagnostics(project, subjects)); + diagnostics.AddRange(MessageDiscoveryCustomizationDiagnostics(project, subjects)); + + return + [ + .. diagnostics + .OrderBy(_ => _.Source?.Path, StringComparer.Ordinal) + .ThenBy(_ => _.Source?.StartLine) + .ThenBy(_ => _.Source?.StartColumn) + .ThenBy(_ => _.Subject?.Value, StringComparer.Ordinal) + ]; + } + + static IEnumerable AlterationTypeDiagnostics( + DotNetProjectCompilation project, + CritterStackSubjectResolver subjects) + { var catalog = new DotNetArtifactCatalog(project.Compilation); foreach (var type in catalog.Types.Where(_ => IsAuthoredConcreteType(_, project) && AltersConventions(_))) { var location = type.Locations.First(_ => IsAuthoredLocation(_, project)); - diagnostics.Add(new() + yield return new() { Code = WolverineDiagnosticCodes.ConventionAlterationOmitted, Severity = GenerationDiagnosticSeverity.Warning, @@ -27,9 +44,14 @@ public static IReadOnlyList Discover( Message = $"Authored Wolverine convention-alteration type '{type.Name}' may change handler discovery or chain behavior at runtime; the model reflects default conventions only", Source = CritterStackSource.RangeForProject(location, project), Subject = subjects.SubjectForType(project, type) - }); + }; } + } + static IEnumerable MessageDiscoveryCustomizationDiagnostics( + DotNetProjectCompilation project, + CritterStackSubjectResolver subjects) + { foreach (var tree in project.Compilation.SyntaxTrees .Where(_ => project.AuthoredSyntaxTrees.Contains(_) && !DotNetGeneratedSource.IsGenerated(_)) .OrderBy(_ => _.FilePath, StringComparer.Ordinal)) @@ -37,15 +59,13 @@ public static IReadOnlyList Discover( var semanticModel = project.Compilation.GetSemanticModel(tree); foreach (var invocation in tree.GetRoot().DescendantNodes().OfType().OrderBy(_ => _.SpanStart)) { - if (semanticModel.GetSymbolInfo(invocation).Symbol is not IMethodSymbol method || - !string.Equals(method.Name, "CustomizeMessageDiscovery", StringComparison.Ordinal) || - DotNetSubjectIds.MetadataName(method.ContainingType.OriginalDefinition) != WellKnownTypes.WolverineHandlerDiscovery) + if (!IsMessageDiscoveryCustomization(semanticModel, invocation)) { continue; } var containingType = semanticModel.GetEnclosingSymbol(invocation.SpanStart)?.ContainingType; - diagnostics.Add(new() + yield return new() { Code = WolverineDiagnosticCodes.ConventionAlterationOmitted, Severity = GenerationDiagnosticSeverity.Warning, @@ -55,32 +75,27 @@ public static IReadOnlyList Discover( Subject = containingType is null ? new SubjectId { Value = $"dotnet://{project.Name}/#wolverine-message-discovery" } : subjects.SubjectForType(project, containingType) - }); + }; } } - - return - [ - .. diagnostics - .OrderBy(_ => _.Source?.Path, StringComparer.Ordinal) - .ThenBy(_ => _.Source?.StartLine) - .ThenBy(_ => _.Source?.StartColumn) - .ThenBy(_ => _.Subject?.Value, StringComparer.Ordinal) - ]; } - static bool AltersConventions(INamedTypeSymbol type) - { - if (DotNetSymbols.Implements(type, WellKnownTypes.WolverineHandlerPolicy) || - DotNetSymbols.Implements(type, WellKnownTypes.WolverinePolicy) || - DotNetSymbols.Implements(type, WellKnownTypes.WolverineExtension)) - { - return true; - } + static bool IsMessageDiscoveryCustomization(SemanticModel semanticModel, InvocationExpressionSyntax invocation) => + semanticModel.GetSymbolInfo(invocation).Symbol is IMethodSymbol method && + string.Equals(method.Name, "CustomizeMessageDiscovery", StringComparison.Ordinal) && + DotNetSubjectIds.MetadataName(method.ContainingType.OriginalDefinition) == WellKnownTypes.WolverineHandlerDiscovery; - return DotNetSubjectIds.MetadataName(type.OriginalDefinition) != WellKnownTypes.WolverineModifyHandlerChainAttribute && - DotNetSymbols.IsOrInheritsFrom(type, WellKnownTypes.WolverineModifyHandlerChainAttribute); - } + static bool AltersConventions(INamedTypeSymbol type) => + ImplementsConventionPolicy(type) || IsDerivedChainAttribute(type); + + static bool ImplementsConventionPolicy(INamedTypeSymbol type) => + DotNetSymbols.Implements(type, WellKnownTypes.WolverineHandlerPolicy) || + DotNetSymbols.Implements(type, WellKnownTypes.WolverinePolicy) || + DotNetSymbols.Implements(type, WellKnownTypes.WolverineExtension); + + static bool IsDerivedChainAttribute(INamedTypeSymbol type) => + DotNetSubjectIds.MetadataName(type.OriginalDefinition) != WellKnownTypes.WolverineModifyHandlerChainAttribute && + DotNetSymbols.IsOrInheritsFrom(type, WellKnownTypes.WolverineModifyHandlerChainAttribute); static bool IsAuthoredConcreteType(INamedTypeSymbol type, DotNetProjectCompilation project) => type.TypeKind == TypeKind.Class && diff --git a/Source/DotNET/CritterStack/Wolverine/WolverineFacts.cs b/Source/DotNET/CritterStack/Wolverine/WolverineFacts.cs index d0245cd..e2d91f0 100644 --- a/Source/DotNET/CritterStack/Wolverine/WolverineFacts.cs +++ b/Source/DotNET/CritterStack/Wolverine/WolverineFacts.cs @@ -214,15 +214,13 @@ static void AnalyzeWithSlicePattern(WolverineFactScope scope, IMethodSymbol meth { var request = RequestParameter(method, scope.Project); var slicePattern = WolverineSlicePatterns.Resolve( + scope, method, request is not null ? MessageElementType(request.Type) : null, - endpoint, - scope.Project, - scope.Adapter, - scope.Diagnostics); + endpoint); var firstPlacement = scope.Placements.Count; analyze(); - WolverineSlicePatterns.Apply(slicePattern, method, scope.Project, scope.Placements, firstPlacement, scope.Diagnostics); + WolverineSlicePatterns.Apply(slicePattern, scope, method, firstPlacement); } } diff --git a/Source/DotNET/CritterStack/Wolverine/WolverineFluentValidationOptions.cs b/Source/DotNET/CritterStack/Wolverine/WolverineFluentValidationOptions.cs new file mode 100644 index 0000000..9cbdfb3 --- /dev/null +++ b/Source/DotNET/CritterStack/Wolverine/WolverineFluentValidationOptions.cs @@ -0,0 +1,193 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Cratis.Screenplay.Generation.DotNet; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Cratis.CritterStack.Screenplay.Wolverine; + +/// +/// The statically resolved options of a Wolverine UseFluentValidation call, or the reason they cannot be resolved. +/// +sealed class WolverineFluentValidationOptions +{ + const string RuntimeCallbackReason = "the FluentValidation configuration callback is executed at runtime"; + const string RuntimeBehaviorReason = "the FluentValidation configuration callback contains runtime or custom behavior"; + + /// + /// Gets or sets a value indicating whether validators are discovered by Wolverine. + /// + public bool CanDiscoverValidators { get; set; } = true; + + /// + /// Gets or sets a value indicating whether internal validators are included. + /// + public bool IncludeInternalValidators { get; set; } + + /// + /// Gets the reason the options could not be resolved, when they could not. + /// + public string? Failure { get; private set; } + + /// + /// Gets a value indicating whether every option was resolved statically. + /// + public bool Resolved => Failure is null; + + /// + /// Resolves the options passed to a UseFluentValidation call. + /// + /// The call to resolve. + /// The semantic model of the call. + /// The resolved options, or the failure reason. + public static WolverineFluentValidationOptions Resolve(InvocationExpressionSyntax invocation, SemanticModel semanticModel) + { + var options = new WolverineFluentValidationOptions(); + foreach (var argument in invocation.ArgumentList.Arguments) + { + if (argument.Expression is LambdaExpressionSyntax lambda) + { + return ResolveCallback(semanticModel, lambda); + } + + if (options.ApplyArgument(semanticModel, argument.Expression) is { } failure) + { + return options.Fail(failure); + } + } + + return options; + } + + static WolverineFluentValidationOptions ResolveCallback(SemanticModel semanticModel, LambdaExpressionSyntax lambda) + { + var options = new WolverineFluentValidationOptions(); + if (WolverineLambdaSyntax.Parameter(lambda, semanticModel) is not { } callbackParameter) + { + return options.Fail("the FluentValidation configuration callback parameter cannot be resolved"); + } + + var assignments = CallbackAssignments(lambda); + if (assignments.Length == 0) + { + return options.Fail(RuntimeBehaviorReason); + } + + foreach (var assignment in assignments) + { + if (options.ApplyAssignment(semanticModel, assignment, callbackParameter) is { } failure) + { + return options.Fail(failure); + } + } + + return options; + } + + static AssignmentExpressionSyntax[] CallbackAssignments(LambdaExpressionSyntax lambda) => lambda.Body switch + { + AssignmentExpressionSyntax assignment => [assignment], + BlockSyntax block when block.Statements.All(_ => _ is ExpressionStatementSyntax { Expression: AssignmentExpressionSyntax }) => + [.. block.Statements.Cast().Select(_ => (AssignmentExpressionSyntax)_.Expression)], + _ => [] + }; + + static bool IsConfigurationCallback(ITypeSymbol? convertedType) => + convertedType is INamedTypeSymbol { TypeKind: TypeKind.Delegate } delegateType && + delegateType.TypeArguments.Any(_ => _ is INamedTypeSymbol named && DotNetSubjectIds.MetadataName(named) == WellKnownTypes.WolverineFluentValidationConfiguration); + + static bool IsRegistrationBehavior(ITypeSymbol? convertedType) => + convertedType is INamedTypeSymbol enumType && + DotNetSubjectIds.MetadataName(enumType.OriginalDefinition) == WellKnownTypes.WolverineFluentValidationRegistrationBehavior; + + static bool TryGetConfigurationProperty( + SemanticModel semanticModel, + AssignmentExpressionSyntax assignment, + IParameterSymbol callbackParameter, + out IPropertySymbol property) + { + property = null!; + if (semanticModel.GetSymbolInfo(assignment.Left).Symbol is not IPropertySymbol found || + !IsConfigurationProperty(found)) + { + return false; + } + + if (!IsRootedMember(semanticModel, assignment.Left, callbackParameter)) + { + return false; + } + + property = found; + return true; + } + + static bool IsConfigurationProperty(IPropertySymbol property) => + DotNetSubjectIds.MetadataName(property.ContainingType) == WellKnownTypes.WolverineFluentValidationConfiguration; + + static bool IsRootedMember(SemanticModel semanticModel, ExpressionSyntax expression, IParameterSymbol callbackParameter) => + expression is MemberAccessExpressionSyntax memberAccess && + WolverineLambdaSyntax.IsRootedInParameter(memberAccess.Expression, callbackParameter, semanticModel); + + WolverineFluentValidationOptions Fail(string reason) + { + Failure = reason; + return this; + } + + string? ApplyArgument(SemanticModel semanticModel, ExpressionSyntax expression) + { + var convertedType = semanticModel.GetTypeInfo(expression).ConvertedType; + if (expression is AnonymousMethodExpressionSyntax || IsConfigurationCallback(convertedType)) + { + return RuntimeCallbackReason; + } + + var constant = semanticModel.GetConstantValue(expression); + if (!constant.HasValue) + { + return "a FluentValidation option is not a compile-time constant"; + } + + if (convertedType?.SpecialType == SpecialType.System_Boolean && constant.Value is bool includeInternal) + { + IncludeInternalValidators = includeInternal; + return null; + } + + if (IsRegistrationBehavior(convertedType) && constant.Value is int behavior) + { + CanDiscoverValidators = behavior == 0; + return null; + } + + return "a FluentValidation option uses an unsupported runtime configuration shape"; + } + + string? ApplyAssignment(SemanticModel semanticModel, AssignmentExpressionSyntax assignment, IParameterSymbol callbackParameter) + { + if (!TryGetConfigurationProperty(semanticModel, assignment, callbackParameter, out var property)) + { + return RuntimeBehaviorReason; + } + + var constant = semanticModel.GetConstantValue(assignment.Right); + if (!constant.HasValue) + { + return $"FluentValidation option '{property.Name}' is not a compile-time constant"; + } + + switch (property.Name) + { + case "IncludeInternalTypes" when constant.Value is bool includeInternal: + IncludeInternalValidators = includeInternal; + return null; + case "RegistrationBehavior" when constant.Value is int behavior: + CanDiscoverValidators = behavior == 0; + return null; + default: + return $"FluentValidation option '{property.Name}' is not statically supported"; + } + } +} diff --git a/Source/DotNET/CritterStack/Wolverine/WolverineHandlerDiscovery.cs b/Source/DotNET/CritterStack/Wolverine/WolverineHandlerDiscovery.cs index 7cb31d0..c05895c 100644 --- a/Source/DotNET/CritterStack/Wolverine/WolverineHandlerDiscovery.cs +++ b/Source/DotNET/CritterStack/Wolverine/WolverineHandlerDiscovery.cs @@ -14,77 +14,84 @@ public static WolverineHandlerDiscoveryResult Discover( DotNetProjectCompilation project, CritterStackSubjectResolver subjects) { - var calls = ConfigurationCalls(project).ToArray(); - var explicitTypes = new List(); - var diagnostics = new List(WolverineConventionAlterationDiscovery.Discover(project, subjects)); - var disableValues = new List(); - var conventionalDiscoveryResolvable = true; + ConfigurationCall[] calls = [.. ConfigurationCalls(project)]; + var state = new DiscoveryState(project, subjects); + state.Diagnostics.AddRange(WolverineConventionAlterationDiscovery.Discover(project, subjects)); foreach (var call in calls) { - switch (call.Method.Name) - { - case "DisableConventionalDiscovery": - if (TryGetDisabledValue(call, out var disabled)) - { - disableValues.Add(disabled); - } - else - { - conventionalDiscoveryResolvable = false; - diagnostics.Add(UnresolvedDiagnostic(project, subjects, call, "the enabled state is not a compile-time constant")); - } - break; - case "IncludeType": - if (TryGetIncludedType(call, out var includedType) && IsAuthoredSourceType(includedType, project)) - { - if (!explicitTypes.Exists(_ => SymbolEqualityComparer.Default.Equals(_, includedType))) - { - explicitTypes.Add(includedType); - } - } - else - { - diagnostics.Add(UnresolvedDiagnostic(project, subjects, call, "the included handler type is not authored source in this compilation")); - } - break; - case "IncludeAssembly": - if (!ReferencesCurrentAssembly(project, call)) - { - diagnostics.Add(UnresolvedDiagnostic(project, subjects, call, "assembly scanning outside the current source compilation is not supported")); - } - break; - case "CustomizeHandlerDiscovery": - conventionalDiscoveryResolvable = false; - diagnostics.Add(UnresolvedDiagnostic(project, subjects, call, "custom handler predicates and lambdas are not statically interpreted")); - break; - case "IgnoreAssembly": - conventionalDiscoveryResolvable = false; - diagnostics.Add(UnresolvedDiagnostic(project, subjects, call, "assembly removal cannot be resolved from the current source compilation")); - break; - } + ApplyCall(state, call); } - var distinctDisableValues = disableValues.Distinct().ToArray(); + bool[] distinctDisableValues = [.. state.DisableValues.Distinct()]; if (distinctDisableValues.Length > 1) { - conventionalDiscoveryResolvable = false; + state.ConventionalDiscoveryResolvable = false; var call = calls.First(_ => _.Method.Name == "DisableConventionalDiscovery"); - diagnostics.Add(UnresolvedDiagnostic( - project, - subjects, - call, - "conflicting enabled states depend on runtime execution order", - GenerationDiagnosticOutcome.Conflict)); + state.Report(call, "conflicting enabled states depend on runtime execution order", GenerationDiagnosticOutcome.Conflict); } var conventionalDiscoveryEnabled = distinctDisableValues.Length == 0 || !distinctDisableValues[0]; var policy = new WolverineHandlerDiscoveryPolicy( conventionalDiscoveryEnabled, - conventionalDiscoveryResolvable, - explicitTypes); + state.ConventionalDiscoveryResolvable, + state.ExplicitTypes); + + return new(policy, state.Diagnostics); + } + + static void ApplyCall(DiscoveryState state, ConfigurationCall call) + { + switch (call.Method.Name) + { + case "DisableConventionalDiscovery": + ApplyDisable(state, call); + break; + case "IncludeType": + ApplyIncludeType(state, call); + break; + case "IncludeAssembly": + if (!ReferencesCurrentAssembly(state.Project, call)) + { + state.Report(call, "assembly scanning outside the current source compilation is not supported"); + } + + break; + case "CustomizeHandlerDiscovery": + state.ConventionalDiscoveryResolvable = false; + state.Report(call, "custom handler predicates and lambdas are not statically interpreted"); + break; + case "IgnoreAssembly": + state.ConventionalDiscoveryResolvable = false; + state.Report(call, "assembly removal cannot be resolved from the current source compilation"); + break; + } + } + + static void ApplyDisable(DiscoveryState state, ConfigurationCall call) + { + if (TryGetDisabledValue(call, out var disabled)) + { + state.DisableValues.Add(disabled); + return; + } - return new(policy, diagnostics); + state.ConventionalDiscoveryResolvable = false; + state.Report(call, "the enabled state is not a compile-time constant"); + } + + static void ApplyIncludeType(DiscoveryState state, ConfigurationCall call) + { + if (!TryGetIncludedType(call, out var includedType) || !IsAuthoredSourceType(includedType, state.Project)) + { + state.Report(call, "the included handler type is not authored source in this compilation"); + return; + } + + if (!state.ExplicitTypes.Exists(_ => SymbolEqualityComparer.Default.Equals(_, includedType))) + { + state.ExplicitTypes.Add(includedType); + } } static IEnumerable ConfigurationCalls(DotNetProjectCompilation project) @@ -130,6 +137,13 @@ static bool TryGetDisabledValue(ConfigurationCall call, out bool disabled) } static bool TryGetIncludedType(ConfigurationCall call, out INamedTypeSymbol type) + { + var found = TryGetGenericTypeArgument(call) ?? TryGetTypeOfArgument(call); + type = found!; + return found is not null; + } + + static INamedTypeSymbol? TryGetGenericTypeArgument(ConfigurationCall call) { var genericName = call.Invocation.Expression switch { @@ -138,24 +152,16 @@ static bool TryGetIncludedType(ConfigurationCall call, out INamedTypeSymbol type _ => null }; var syntaxType = genericName?.TypeArgumentList.Arguments.FirstOrDefault(); - if (syntaxType is not null && call.SemanticModel.GetTypeInfo(syntaxType).Type is INamedTypeSymbol typeArgument) - { - type = typeArgument; - return true; - } + return syntaxType is null ? null : call.SemanticModel.GetTypeInfo(syntaxType).Type as INamedTypeSymbol; + } + static INamedTypeSymbol? TryGetTypeOfArgument(ConfigurationCall call) + { var typeOf = call.Invocation.ArgumentList.Arguments .Select(_ => _.Expression) .OfType() .FirstOrDefault(); - if (typeOf is not null && call.SemanticModel.GetTypeInfo(typeOf.Type).Type is INamedTypeSymbol typeOfArgument) - { - type = typeOfArgument; - return true; - } - - type = null!; - return false; + return typeOf is null ? null : call.SemanticModel.GetTypeInfo(typeOf.Type).Type as INamedTypeSymbol; } static bool ReferencesCurrentAssembly(DotNetProjectCompilation project, ConfigurationCall call) @@ -174,32 +180,43 @@ _.SourceTree is not null && project.AuthoredSyntaxTrees.Contains(_.SourceTree) && !DotNetGeneratedSource.IsGenerated(_.SourceTree)); - static GenerationDiagnostic UnresolvedDiagnostic( - DotNetProjectCompilation project, - CritterStackSubjectResolver subjects, - ConfigurationCall call, - string reason, - GenerationDiagnosticOutcome outcome = GenerationDiagnosticOutcome.Unknown) - { - var containingType = call.SemanticModel.GetEnclosingSymbol(call.Invocation.SpanStart)?.ContainingType; - var subject = containingType is null - ? new SubjectId { Value = $"dotnet://{project.Name}/#wolverine-handler-discovery" } - : subjects.SubjectForType(project, containingType); - return new() - { - Code = WolverineDiagnosticCodes.HandlerDiscoveryConfigurationUnresolved, - Severity = GenerationDiagnosticSeverity.Warning, - Outcome = outcome, - Message = $"Wolverine handler discovery call '{call.Method.Name}' was not applied because {reason}", - Source = CritterStackSource.RangeForProject(call.Invocation.GetLocation(), project), - Subject = subject - }; - } - sealed record ConfigurationCall( InvocationExpressionSyntax Invocation, IMethodSymbol Method, SemanticModel SemanticModel); + + sealed class DiscoveryState(DotNetProjectCompilation project, CritterStackSubjectResolver subjects) + { + public DotNetProjectCompilation Project { get; } = project; + + public List ExplicitTypes { get; } = []; + + public List Diagnostics { get; } = []; + + public List DisableValues { get; } = []; + + public bool ConventionalDiscoveryResolvable { get; set; } = true; + + public void Report( + ConfigurationCall call, + string reason, + GenerationDiagnosticOutcome outcome = GenerationDiagnosticOutcome.Unknown) + { + var containingType = call.SemanticModel.GetEnclosingSymbol(call.Invocation.SpanStart)?.ContainingType; + var subject = containingType is null + ? new SubjectId { Value = $"dotnet://{Project.Name}/#wolverine-handler-discovery" } + : subjects.SubjectForType(Project, containingType); + Diagnostics.Add(new() + { + Code = WolverineDiagnosticCodes.HandlerDiscoveryConfigurationUnresolved, + Severity = GenerationDiagnosticSeverity.Warning, + Outcome = outcome, + Message = $"Wolverine handler discovery call '{call.Method.Name}' was not applied because {reason}", + Source = CritterStackSource.RangeForProject(call.Invocation.GetLocation(), Project), + Subject = subject + }); + } + } } sealed record WolverineHandlerDiscoveryResult( @@ -218,20 +235,21 @@ public bool Includes(INamedTypeSymbol type) return true; } - if (!conventionalDiscoveryEnabled || !conventionalDiscoveryResolvable) - { - return false; - } - - return type.Name.EndsWith("Handler", StringComparison.Ordinal) || - type.Name.EndsWith("Consumer", StringComparison.Ordinal) || - DotNetSymbols.IsOrInheritsFrom(type, WellKnownTypes.WolverineSaga) || - DotNetSymbols.Implements(type, WellKnownTypes.WolverineHandlerInterface) || - HasHandlerAttribute(type) || - type.GetMembers().OfType().Any(HasHandlerAttribute); + return conventionalDiscoveryEnabled && conventionalDiscoveryResolvable && FollowsHandlerConvention(type); } - bool HasHandlerAttribute(ISymbol symbol) => + static bool FollowsHandlerConvention(INamedTypeSymbol type) => + HasHandlerSuffix(type) || + DotNetSymbols.IsOrInheritsFrom(type, WellKnownTypes.WolverineSaga) || + DotNetSymbols.Implements(type, WellKnownTypes.WolverineHandlerInterface) || + HasHandlerAttribute(type) || + type.GetMembers().OfType().Any(HasHandlerAttribute); + + static bool HasHandlerSuffix(INamedTypeSymbol type) => + type.Name.EndsWith("Handler", StringComparison.Ordinal) || + type.Name.EndsWith("Consumer", StringComparison.Ordinal); + + static bool HasHandlerAttribute(ISymbol symbol) => DotNetSymbols.HasAttribute(symbol, WellKnownTypes.WolverineHandlerAttribute) || DotNetSymbols.HasAttribute(symbol, WellKnownTypes.WolverineLegacyHandlerAttribute); } diff --git a/Source/DotNET/CritterStack/Wolverine/WolverineLambdaSyntax.cs b/Source/DotNET/CritterStack/Wolverine/WolverineLambdaSyntax.cs new file mode 100644 index 0000000..fc26c15 --- /dev/null +++ b/Source/DotNET/CritterStack/Wolverine/WolverineLambdaSyntax.cs @@ -0,0 +1,51 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Cratis.CritterStack.Screenplay.Wolverine; + +/// +/// Semantic helpers for the callback lambdas passed to Wolverine configuration calls. +/// +static class WolverineLambdaSyntax +{ + /// + /// Gets the single parameter symbol of a lambda expression. + /// + /// The lambda expression. + /// The semantic model of the lambda. + /// The parameter symbol, or when there is not exactly one resolvable parameter. + public static IParameterSymbol? Parameter(LambdaExpressionSyntax lambda, SemanticModel semanticModel) + { + var parameter = lambda switch + { + SimpleLambdaExpressionSyntax simple => simple.Parameter, + ParenthesizedLambdaExpressionSyntax { ParameterList.Parameters.Count: 1 } parenthesized => parenthesized.ParameterList.Parameters[0], + _ => null + }; + return parameter is null ? null : semanticModel.GetDeclaredSymbol(parameter) as IParameterSymbol; + } + + /// + /// Determines whether an expression is a member chain rooted in the given parameter. + /// + /// The expression to inspect. + /// The parameter the chain must start from. + /// The semantic model of the expression. + /// when rooted in the parameter. + public static bool IsRootedInParameter( + ExpressionSyntax expression, + IParameterSymbol parameter, + SemanticModel semanticModel) => expression switch + { + IdentifierNameSyntax identifier => SymbolEqualityComparer.Default.Equals( + semanticModel.GetSymbolInfo(identifier).Symbol, + parameter), + MemberAccessExpressionSyntax memberAccess => IsRootedInParameter(memberAccess.Expression, parameter, semanticModel), + ParenthesizedExpressionSyntax parenthesized => IsRootedInParameter(parenthesized.Expression, parameter, semanticModel), + CastExpressionSyntax cast => IsRootedInParameter(cast.Expression, parameter, semanticModel), + _ => false + }; +} diff --git a/Source/DotNET/CritterStack/Wolverine/WolverineProductionLinks.cs b/Source/DotNET/CritterStack/Wolverine/WolverineProductionLinks.cs index 6fa0b22..37335a1 100644 --- a/Source/DotNET/CritterStack/Wolverine/WolverineProductionLinks.cs +++ b/Source/DotNET/CritterStack/Wolverine/WolverineProductionLinks.cs @@ -24,6 +24,17 @@ sealed record WolverineProduction( SourceRange? Source, GenerationDiagnosticOutcome LossOutcome = GenerationDiagnosticOutcome.Unknown); +/// +/// The handler declaration, its semantic model and the project a production is read from. +/// +/// The handler method declaration. +/// The semantic model of the declaration's tree. +/// The project compilation. +sealed record WolverineProductionScope( + MethodDeclarationSyntax Declaration, + SemanticModel SemanticModel, + DotNetProjectCompilation Project); + /// /// Reads the events a Wolverine handler produces through direct Marten session appends and aggregate-handler yields. /// @@ -43,54 +54,12 @@ public static IReadOnlyList SessionAppends( IParameterSymbol? request, DotNetProjectCompilation project) { - var productions = new List(); - foreach (var (declaration, semanticModel) in WolverineMethodSyntax.Declarations(method, project)) - { - foreach (var invocationSyntax in declaration.DescendantNodes().OfType()) - { - if (semanticModel.GetOperation(invocationSyntax) is not IInvocationOperation invocation || - !IsSessionAppend(invocation, project)) - { - continue; - } - - var source = CritterStackSource.RangeForProject(invocationSyntax.GetLocation(), project); - var payloadArguments = invocation.Arguments - .Where(_ => _.Parameter is not null && !IsStreamIdentityType(_.Parameter.Type) && !IsMetadataParameter(_.Parameter)) - .ToArray(); - var eventTypes = new List(); - var directPayloads = payloadArguments.Length > 0; - foreach (var argument in payloadArguments) - { - if (!WolverineEventStreams.TryGetDirectPayloads(argument.Value, out var argumentEvents)) - { - directPayloads = false; - break; - } - - eventTypes.AddRange(argumentEvents); - } - - if (!directPayloads) - { - productions.AddRange(UndirectedPayloadTypes(payloadArguments, project) - .Select(_ => new WolverineProduction(_, null, "the appended payload is not a direct event creation", source))); - continue; - } - - var (identity, identityLoss) = StreamIdentity(invocation, request); - var conditional = IsConditional(invocationSyntax, declaration); - var lossReason = identityLoss ?? (conditional ? "the append only happens on some paths through the handler" : null); - var lossOutcome = identityLoss is null && conditional - ? GenerationDiagnosticOutcome.Unsupported - : GenerationDiagnosticOutcome.Unknown; - productions.AddRange(eventTypes - .Where(_ => !WolverineSagaTypes.IsSagaState(_, project)) - .Select(_ => new WolverineProduction(_, lossReason is null ? identity : null, lossReason, source, lossOutcome))); - } - } - - return productions; + return + [ + .. WolverineMethodSyntax.Declarations(method, project) + .SelectMany(_ => _.Declaration.DescendantNodes().OfType() + .SelectMany(invocationSyntax => AppendProductions(invocationSyntax, new(_.Declaration, _.SemanticModel, project), request))) + ]; } /// @@ -105,36 +74,14 @@ public static IReadOnlyList AggregateYields( ISymbol? identity, DotNetProjectCompilation project) { - var productions = new List(); - foreach (var (declaration, semanticModel) in WolverineMethodSyntax.Declarations(method, project)) - { - foreach (var yield in declaration.DescendantNodes() - .OfType() - .Where(_ => _.Expression is not null && IsInHandlerBody(_, declaration))) - { - var source = CritterStackSource.RangeForProject(yield.GetLocation(), project); - if (semanticModel.GetOperation(yield.Expression!) is not { } operation || - !WolverineEventStreams.TryGetDirectPayloads(operation, out var eventTypes)) - { - if (semanticModel.GetTypeInfo(yield.Expression!).Type is INamedTypeSymbol yieldedType && - IsEventType(yieldedType, project)) - { - productions.Add(new(yieldedType, null, "the yielded value is not a direct event creation", source)); - } - - continue; - } - - var conditional = IsConditional(yield, declaration); - var lossReason = YieldLossReason(conditional, identity); - var lossOutcome = conditional ? GenerationDiagnosticOutcome.Unsupported : GenerationDiagnosticOutcome.Unknown; - productions.AddRange(eventTypes - .Where(_ => IsEventType(_, project)) - .Select(_ => new WolverineProduction(_, lossReason is null ? identity : null, lossReason, source, lossOutcome))); - } - } - - return productions; + return + [ + .. WolverineMethodSyntax.Declarations(method, project) + .SelectMany(_ => _.Declaration.DescendantNodes() + .OfType() + .Where(yield => yield.Expression is not null && IsInHandlerBody(yield, _.Declaration)) + .SelectMany(yield => YieldProductions(yield, new(_.Declaration, _.SemanticModel, project), identity))) + ]; } /// @@ -158,6 +105,92 @@ public static bool IsSessionAppend(IInvocationOperation invocation, DotNetProjec @namespace.StartsWith("JasperFx.Events", StringComparison.Ordinal); } + static IEnumerable AppendProductions( + InvocationExpressionSyntax invocationSyntax, + WolverineProductionScope scope, + IParameterSymbol? request) + { + var project = scope.Project; + if (scope.SemanticModel.GetOperation(invocationSyntax) is not IInvocationOperation invocation || + !IsSessionAppend(invocation, project)) + { + return []; + } + + var source = CritterStackSource.RangeForProject(invocationSyntax.GetLocation(), project); + var payloadArguments = PayloadArguments(invocation); + if (!TryDirectEventTypes(payloadArguments, out var eventTypes)) + { + return UndirectedPayloadTypes(payloadArguments, project) + .Select(_ => new WolverineProduction(_, null, "the appended payload is not a direct event creation", source)); + } + + var (identity, identityLoss) = StreamIdentity(invocation, request); + var (lossReason, lossOutcome) = AppendLoss(identityLoss, IsConditional(invocationSyntax, scope.Declaration)); + return eventTypes + .Where(_ => !WolverineSagaTypes.IsSagaState(_, project)) + .Select(_ => new WolverineProduction(_, lossReason is null ? identity : null, lossReason, source, lossOutcome)); + } + + static (string? Reason, GenerationDiagnosticOutcome Outcome) AppendLoss(string? identityLoss, bool conditional) + { + if (identityLoss is not null) + { + return (identityLoss, GenerationDiagnosticOutcome.Unknown); + } + + return conditional + ? ("the append only happens on some paths through the handler", GenerationDiagnosticOutcome.Unsupported) + : (null, GenerationDiagnosticOutcome.Unknown); + } + + static IArgumentOperation[] PayloadArguments(IInvocationOperation invocation) => + [.. invocation.Arguments.Where(_ => _.Parameter is not null && !IsStreamIdentityType(_.Parameter.Type) && !IsMetadataParameter(_.Parameter))]; + + static bool TryDirectEventTypes(IArgumentOperation[] payloadArguments, out List eventTypes) + { + eventTypes = []; + if (payloadArguments.Length == 0) + { + return false; + } + + foreach (var argument in payloadArguments) + { + if (!WolverineEventStreams.TryGetDirectPayloads(argument.Value, out var argumentEvents)) + { + return false; + } + + eventTypes.AddRange(argumentEvents); + } + + return true; + } + + static IEnumerable YieldProductions( + YieldStatementSyntax yield, + WolverineProductionScope scope, + ISymbol? identity) + { + var (declaration, semanticModel, project) = scope; + var source = CritterStackSource.RangeForProject(yield.GetLocation(), project); + if (semanticModel.GetOperation(yield.Expression!) is not { } operation || + !WolverineEventStreams.TryGetDirectPayloads(operation, out var eventTypes)) + { + return semanticModel.GetTypeInfo(yield.Expression!).Type is INamedTypeSymbol yieldedType && IsEventType(yieldedType, project) + ? [new(yieldedType, null, "the yielded value is not a direct event creation", source)] + : []; + } + + var conditional = IsConditional(yield, declaration); + var lossReason = YieldLossReason(conditional, identity); + var lossOutcome = conditional ? GenerationDiagnosticOutcome.Unsupported : GenerationDiagnosticOutcome.Unknown; + return eventTypes + .Where(_ => IsEventType(_, project)) + .Select(_ => new WolverineProduction(_, lossReason is null ? identity : null, lossReason, source, lossOutcome)); + } + static string? YieldLossReason(bool conditional, ISymbol? identity) { if (conditional) diff --git a/Source/DotNET/CritterStack/Wolverine/WolverineReturnConsequences.cs b/Source/DotNET/CritterStack/Wolverine/WolverineReturnConsequences.cs index f270127..b9e4c3a 100644 --- a/Source/DotNET/CritterStack/Wolverine/WolverineReturnConsequences.cs +++ b/Source/DotNET/CritterStack/Wolverine/WolverineReturnConsequences.cs @@ -25,6 +25,13 @@ sealed record WolverineReturnConsequence( WolverineReturnConsequenceKind Kind, ITypeSymbol? EntityType = null); +sealed record WolverineReturnContext( + DotNetProjectCompilation Project, + bool IsHttpEndpoint, + bool AggregateWorkflow, + bool HasEventStream, + bool EmptyResponse); + static class WolverineReturnConsequences { static readonly HashSet _persistenceOperations = @@ -49,86 +56,102 @@ public static IReadOnlyList Classify( bool aggregateWorkflow, bool hasEventStream) { - var emptyResponse = DotNetSymbols.HasAttribute(method, WellKnownTypes.WolverineEmptyResponseAttribute); - return - [ - .. WolverineReturnTypes.CreatedValues(method) - .Select((type, slot) => Classify(type, slot, project, isHttpEndpoint, aggregateWorkflow, hasEventStream, emptyResponse)) - ]; + var context = new WolverineReturnContext( + project, + isHttpEndpoint, + aggregateWorkflow, + hasEventStream, + DotNetSymbols.HasAttribute(method, WellKnownTypes.WolverineEmptyResponseAttribute)); + return [.. WolverineReturnTypes.CreatedValues(method).Select((type, slot) => Classify(type, slot, context))]; } public static bool IsTimeoutMessage(INamedTypeSymbol type) => IsAssignableTo(type, WellKnownTypes.WolverineTimeoutMessage); - static WolverineReturnConsequence Classify( - ITypeSymbol type, - int slot, - DotNetProjectCompilation project, - bool isHttpEndpoint, - bool aggregateWorkflow, - bool hasEventStream, - bool emptyResponse) + static WolverineReturnConsequence Classify(ITypeSymbol type, int slot, WolverineReturnContext context) { - WolverineReturnConsequence Consequence( - WolverineReturnConsequenceKind kind, - ITypeSymbol? entityType = null) => new(slot, type, kind, entityType); - if (type is not INamedTypeSymbol named) { - return Consequence(WolverineReturnConsequenceKind.Unknown); + return new(slot, type, WolverineReturnConsequenceKind.Unknown); } - var metadataName = DotNetSubjectIds.MetadataName(named.OriginalDefinition); - if (IsSagaState(named, project)) + if (MessagingKind(named, context.Project) is { } messaging) { - return Consequence(WolverineReturnConsequenceKind.SagaState); + return new(slot, type, messaging); } - if (IsTimeoutMessage(named)) + if (StorageActionEntityType(named) is { } entityType) { - return Consequence(WolverineReturnConsequenceKind.Cascade); + return new(slot, type, WolverineReturnConsequenceKind.StorageAction, entityType); } - if (metadataName == WellKnownTypes.WolverineOutgoingMessages) + return new(slot, type, ResponseKind(named, slot, context)); + } + + /// + /// The kinds decided before a storage action: saga state, timeouts, outgoing messages and side effects, in that order. + /// + /// The returned type. + /// The project compilation. + /// The kind, or when a later stage decides. + static WolverineReturnConsequenceKind? MessagingKind(INamedTypeSymbol named, DotNetProjectCompilation project) + { + if (IsSagaState(named, project)) { - return Consequence(WolverineReturnConsequenceKind.OutgoingMessages); + return WolverineReturnConsequenceKind.SagaState; } - if (IsAssignableTo(named, WellKnownTypes.WolverineSideEffect)) + if (IsTimeoutMessage(named)) { - return Consequence(WolverineReturnConsequenceKind.SideEffect); + return WolverineReturnConsequenceKind.Cascade; } - if (StorageActionEntityType(named) is { } entityType) + if (DotNetSubjectIds.MetadataName(named.OriginalDefinition) == WellKnownTypes.WolverineOutgoingMessages) { - return Consequence(WolverineReturnConsequenceKind.StorageAction, entityType); + return WolverineReturnConsequenceKind.OutgoingMessages; } - if (_persistenceOperations.Contains(metadataName)) + return IsAssignableTo(named, WellKnownTypes.WolverineSideEffect) ? WolverineReturnConsequenceKind.SideEffect : null; + } + + /// + /// The kinds decided after a storage action: persistence operations, responses, aggregate events, the HTTP body and cascades. + /// + /// The returned type. + /// The position of the value among the created values. + /// The classification context. + /// The kind. + static WolverineReturnConsequenceKind ResponseKind(INamedTypeSymbol named, int slot, WolverineReturnContext context) + { + if (_persistenceOperations.Contains(DotNetSubjectIds.MetadataName(named.OriginalDefinition))) { - return Consequence(WolverineReturnConsequenceKind.PersistenceOperation); + return WolverineReturnConsequenceKind.PersistenceOperation; } if (IsResponse(named)) { - return Consequence(WolverineReturnConsequenceKind.HttpResponse); + return WolverineReturnConsequenceKind.HttpResponse; } - if (aggregateWorkflow && !hasEventStream && IsPayload(named)) + if (IsAggregateEvent(named, context)) { - return Consequence(WolverineReturnConsequenceKind.PersistedEvent); + return WolverineReturnConsequenceKind.PersistedEvent; } - if (isHttpEndpoint && slot == 0 && !emptyResponse) + if (IsHttpBody(slot, context)) { - return Consequence(WolverineReturnConsequenceKind.HttpResponse); + return WolverineReturnConsequenceKind.HttpResponse; } - return IsPayload(named) - ? Consequence(WolverineReturnConsequenceKind.Cascade) - : Consequence(WolverineReturnConsequenceKind.Unknown); + return IsPayload(named) ? WolverineReturnConsequenceKind.Cascade : WolverineReturnConsequenceKind.Unknown; } + static bool IsAggregateEvent(INamedTypeSymbol named, WolverineReturnContext context) => + context.AggregateWorkflow && !context.HasEventStream && IsPayload(named); + + static bool IsHttpBody(int slot, WolverineReturnContext context) => + context.IsHttpEndpoint && slot == 0 && !context.EmptyResponse; + static ITypeSymbol? StorageActionEntityType(INamedTypeSymbol type) { var metadataName = DotNetSubjectIds.MetadataName(type.OriginalDefinition); diff --git a/Source/DotNET/CritterStack/Wolverine/WolverineSagaCorrelations.cs b/Source/DotNET/CritterStack/Wolverine/WolverineSagaCorrelations.cs new file mode 100644 index 0000000..5c1d5b5 --- /dev/null +++ b/Source/DotNET/CritterStack/Wolverine/WolverineSagaCorrelations.cs @@ -0,0 +1,189 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Cratis.Screenplay.Generation; +using Cratis.Screenplay.Generation.DotNet; +using Microsoft.CodeAnalysis; + +namespace Cratis.CritterStack.Screenplay.Wolverine; + +sealed record WolverineSagaCorrelationScope( + DotNetProjectCompilation Project, + AdapterIdentity Adapter, + INamedTypeSymbol SagaType, + INamedTypeSymbol MessageType, + IReadOnlyList Members); + +/// +/// Selects the message member Wolverine correlates a saga message with. The sources are tried in precedence order and the +/// first one that either selects a member or proves the selection ambiguous wins: +/// [SagaIdentity], then [SagaIdentityFrom], then the full saga name convention, then the suffix-stripped saga name, then +/// SagaId, then a case-insensitive Id. +/// +static class WolverineSagaCorrelations +{ + public static WolverineSagaCorrelation For( + DotNetProjectCompilation project, + AdapterIdentity adapter, + INamedTypeSymbol sagaType, + INamedTypeSymbol messageType, + IReadOnlyList allRoles) + { + var scope = new WolverineSagaCorrelationScope(project, adapter, sagaType, messageType, [.. MessageMembers(messageType, project)]); + return Attributed(scope) + ?? Declared(scope, allRoles) + ?? Conventional(scope) + ?? new(null, null); + } + + static WolverineSagaCorrelation? Attributed(WolverineSagaCorrelationScope scope) => Resolve( + scope, + MatchMember(scope.Members, member => DotNetSymbols.HasAttribute(member, WellKnownTypes.WolverineSagaIdentityAttribute)), + "multiple public [SagaIdentity] members are visible", + EvidenceStrength.Exact, + "Exact Wolverine [SagaIdentity] message member"); + + static WolverineSagaCorrelation? Declared(WolverineSagaCorrelationScope scope, IReadOnlyList allRoles) + { + var specified = allRoles + .Where(role => SymbolEqualityComparer.Default.Equals(role.MessageType, scope.MessageType)) + .SelectMany(role => role.Method.Parameters.Select(SagaIdentityFrom)) + .OfType<(string MemberName, IParameterSymbol Parameter)>() + .ToArray(); + var specifiedNames = specified.Select(_ => _.MemberName).Distinct(StringComparer.Ordinal).ToArray(); + return specifiedNames.Length switch + { + 0 => FullName(scope), + 1 => SpecifiedName(scope, specifiedNames[0], specified[0].Parameter), + _ => UnresolvedCorrelation( + scope.SagaType, + scope.MessageType, + "conflicting [SagaIdentityFrom] member names are declared across parameters in the same message chain", + SagaIdentityFromEvidence(scope.Project, scope.Adapter, specified[0].Parameter)) + }; + } + + static WolverineSagaCorrelation? SpecifiedName(WolverineSagaCorrelationScope scope, string name, IParameterSymbol parameter) + { + var specifiedMember = MatchMember(scope.Members, member => string.Equals(member.Name, name, StringComparison.Ordinal)); + if (specifiedMember.IsAmbiguous) + { + return UnresolvedCorrelation(scope.SagaType, scope.MessageType, $"multiple public members match explicit [SagaIdentityFrom] name '{name}'"); + } + + return specifiedMember.Member is null + ? null + : new(LowerFirst(specifiedMember.Member.Name), SagaIdentityFromEvidence(scope.Project, scope.Adapter, parameter)); + } + + static WolverineSagaCorrelation? FullName(WolverineSagaCorrelationScope scope) => ByName(scope, $"{scope.SagaType.Name}Id"); + + static WolverineSagaCorrelation? Conventional(WolverineSagaCorrelationScope scope) + { + var suffixStrippedName = $"{scope.SagaType.Name.Replace("Saga", string.Empty, StringComparison.InvariantCultureIgnoreCase)}Id"; + foreach (var expectedName in new[] { suffixStrippedName, "SagaId" }.Distinct(StringComparer.Ordinal)) + { + if (ByName(scope, expectedName) is { } correlation) + { + return correlation; + } + } + + return Resolve( + scope, + MatchMember(scope.Members, member => string.Equals(member.Name, "Id", StringComparison.OrdinalIgnoreCase)), + "multiple public members match Wolverine's case-insensitive Id convention", + EvidenceStrength.Conventional, + "Wolverine case-insensitive Id saga correlation convention"); + } + + static WolverineSagaCorrelation? ByName(WolverineSagaCorrelationScope scope, string name) => Resolve( + scope, + MatchMember(scope.Members, member => string.Equals(member.Name, name, StringComparison.Ordinal)), + $"multiple public members match Wolverine convention '{name}'", + EvidenceStrength.Conventional, + $"Wolverine saga correlation member convention '{name}'"); + + static WolverineSagaCorrelation? Resolve( + WolverineSagaCorrelationScope scope, + WolverineSagaMemberMatch match, + string ambiguousReason, + EvidenceStrength strength, + string explanation) + { + if (match.IsAmbiguous) + { + return UnresolvedCorrelation(scope.SagaType, scope.MessageType, ambiguousReason); + } + + return match.Member is null + ? null + : CorrelationFromMember(scope.Project, scope.Adapter, match.Member, strength, explanation); + } + + static (string MemberName, IParameterSymbol Parameter)? SagaIdentityFrom(IParameterSymbol parameter) + { + var attribute = parameter.GetAttributes().FirstOrDefault(candidate => + candidate.AttributeClass is not null && + DotNetSubjectIds.MetadataName(candidate.AttributeClass.OriginalDefinition) == WellKnownTypes.WolverineSagaIdentityFromAttribute); + return attribute?.ConstructorArguments.FirstOrDefault().Value is string memberName + ? (memberName, parameter) + : null; + } + + static Evidence SagaIdentityFromEvidence( + DotNetProjectCompilation project, + AdapterIdentity adapter, + IParameterSymbol parameter) => CritterStackSource.EvidenceFor( + parameter, + adapter, + project, + EvidenceStrength.Exact, + "Exact Wolverine [SagaIdentityFrom] handler parameter"); + + static WolverineSagaCorrelation CorrelationFromMember( + DotNetProjectCompilation project, + AdapterIdentity adapter, + ISymbol member, + EvidenceStrength strength, + string explanation) => new( + LowerFirst(member.Name), + CritterStackSource.EvidenceFor(member, adapter, project, strength, explanation)); + + static WolverineSagaCorrelation UnresolvedCorrelation( + INamedTypeSymbol sagaType, + INamedTypeSymbol messageType, + string reason, + Evidence? evidence = null) => new( + null, + evidence, + $"Saga handler for '{sagaType.Name}' cannot safely select an authored correlation member for '{messageType.Name}' because {reason}; correlation remains runtime-resolved"); + + static WolverineSagaMemberMatch MatchMember(IEnumerable members, Func predicate) + { + var matches = members.Where(predicate).Take(2).ToArray(); + return new(matches.Length == 1 ? matches[0] : null, matches.Length > 1); + } + + static IEnumerable MessageMembers(INamedTypeSymbol messageType, DotNetProjectCompilation project) + { + var hierarchy = TypeHierarchy(messageType).ToArray(); + return hierarchy + .SelectMany(type => type.GetMembers().OfType()) + .Where(member => WolverineSagaFacts.IsPublicAuthoredMember(member, project)) + .Cast() + .Concat(hierarchy + .SelectMany(type => type.GetMembers().OfType()) + .Where(member => !member.IsIndexer && WolverineSagaFacts.IsPublicAuthoredMember(member, project))); + } + + static IEnumerable TypeHierarchy(INamedTypeSymbol type) + { + for (var current = type; current is not null; current = current.BaseType) + { + yield return current; + } + } + + static string LowerFirst(string value) => WolverineSagaFacts.LowerFirst(value); +} diff --git a/Source/DotNET/CritterStack/Wolverine/WolverineSagaFacts.cs b/Source/DotNET/CritterStack/Wolverine/WolverineSagaFacts.cs index 39cd640..7470da7 100644 --- a/Source/DotNET/CritterStack/Wolverine/WolverineSagaFacts.cs +++ b/Source/DotNET/CritterStack/Wolverine/WolverineSagaFacts.cs @@ -32,97 +32,42 @@ sealed record WolverineSagaMemberMatch( ISymbol? Member, bool IsAmbiguous); +sealed record WolverineSagaFactScope( + DotNetProjectCompilation Project, + AdapterIdentity Adapter, + CritterStackSubjectResolver Subjects, + List Facts, + List Diagnostics); + +sealed record WolverineSagaRoleScope( + INamedTypeSymbol SagaType, + WolverineSagaRoleMethod Role, + SubjectId HandlerSubject, + Evidence RoleEvidence); + static class WolverineSagaFacts { - static readonly Dictionary _roles = new(StringComparer.Ordinal) - { - ["Start"] = "start", - ["Starts"] = "start", - ["StartOrHandle"] = "start-or-handle", - ["StartsOrHandles"] = "start-or-handle", - ["Orchestrate"] = "orchestrate", - ["Orchestrates"] = "orchestrate", - ["Handle"] = "orchestrate", - ["Handles"] = "orchestrate", - ["Consume"] = "orchestrate", - ["Consumes"] = "orchestrate", - ["NotFound"] = "not-found" - }; - public static WolverineSagaDiscoveryResult Discover( DotNetProjectCompilation project, AdapterIdentity adapter, CritterStackSubjectResolver subjects, WolverineHandlerDiscoveryPolicy discovery) { - var facts = new List(); - var diagnostics = new List(); + var scope = new WolverineSagaFactScope(project, adapter, subjects, [], []); var catalog = new DotNetArtifactCatalog(project.Compilation); foreach (var sagaType in catalog.Types .Where(type => IsAdmissibleType(type, project, discovery)) .OrderBy(DotNetSubjectIds.MetadataName, StringComparer.Ordinal)) { - WolverineSagaRoleAdmission[] admissions = - [ - .. sagaType.GetMembers() - .OfType() - .Select(method => RoleMethod(method, project, adapter)) - .OrderBy(admission => SourcePath(admission.Method), StringComparer.Ordinal) - .ThenBy(admission => SourceStart(admission.Method)) - .ThenBy(admission => MethodSignature(admission.Method), StringComparer.Ordinal) - ]; - diagnostics.AddRange(admissions.Select(_ => _.Diagnostic).OfType()); - - WolverineSagaRoleMethod[] candidateRoles = [.. admissions.Select(_ => _.Role).OfType()]; - var roles = new List(); - foreach (var messageGroup in candidateRoles - .GroupBy(_ => _.MessageType, SymbolEqualityComparer.Default) - .OrderBy(_ => DotNetSubjectIds.MetadataName(_.Key), StringComparer.Ordinal)) - { - var groupedRoles = messageGroup.ToArray(); - if (!groupedRoles.Any(EstablishesSagaChain)) - { - diagnostics.AddRange(groupedRoles.Select(role => RejectedRole( - role.Method, - project, - adapter, - "no other exact lifecycle method for the same message establishes a Wolverine SagaChain").Diagnostic!)); - continue; - } - - if (RequiresPublicParameterlessConstructor(groupedRoles) && - !HasPublicParameterlessConstructor(sagaType)) - { - var isNotFoundOnlyChain = IsNotFoundOnlyChain(groupedRoles); - var rejectedRoles = groupedRoles.Where(role => role.IsCreationCapable || isNotFoundOnlyChain).ToArray(); - diagnostics.AddRange(rejectedRoles.Select(role => RejectedRole( - role.Method, - project, - adapter, - "Wolverine must create saga state, but the saga has no accessible public parameterless constructor and no exact returned saga supplies creation").Diagnostic!)); - groupedRoles = [.. groupedRoles.Except(rejectedRoles)]; - } - - if (!groupedRoles.Any(EstablishesSagaChain)) - { - diagnostics.AddRange(groupedRoles.Select(role => RejectedRole( - role.Method, - project, - adapter, - "the remaining methods for the same message do not establish a Wolverine SagaChain").Diagnostic!)); - continue; - } - - roles.AddRange(groupedRoles); - } - - if (roles.Count > 0) + var admitted = WolverineSagaRoles.Admit(sagaType, project, adapter); + scope.Diagnostics.AddRange(admitted.Diagnostics); + if (admitted.Roles.Count > 0) { - AddSagaFacts(project, adapter, subjects, sagaType, roles, facts, diagnostics); + AddSagaFacts(scope, sagaType, admitted.Roles); } } - return new(facts, diagnostics); + return new(scope.Facts, scope.Diagnostics); } public static bool IsSagaType(INamedTypeSymbol type, DotNetProjectCompilation project) => @@ -134,38 +79,64 @@ internal static SubjectId HandlerSubject(DotNetProjectCompilation project, IMeth internal static string HandlerName(IMethodSymbol method) => DotNetMethodIdentity.DisplayName(method); + internal static bool IsIgnored(ISymbol symbol, DotNetProjectCompilation project) => symbol.GetAttributes().Any(attribute => + attribute.AttributeClass is not null && + (DotNetSubjectIds.MetadataName(attribute.AttributeClass.OriginalDefinition) == WellKnownTypes.WolverineIgnoreAttribute || + DotNetSubjectIds.MetadataName(attribute.AttributeClass.OriginalDefinition) == WellKnownTypes.WolverineLegacyIgnoreAttribute) && + IsAuthoredOrMetadataAttribute(attribute, project)); + + internal static bool IsPublicAuthoredMember(ISymbol member, DotNetProjectCompilation project) => + !member.IsStatic && + member.DeclaredAccessibility == Accessibility.Public && + member.Locations.Any(location => IsAuthoredSourceLocation(location, project)); + + internal static string MethodSignature(IMethodSymbol method) => + method.GetDocumentationCommentId() ?? method.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + + internal static string SourcePath(ISymbol symbol) => symbol.Locations + .Where(location => location.IsInSource) + .Select(location => location.SourceTree?.FilePath ?? string.Empty) + .Order(StringComparer.Ordinal) + .FirstOrDefault() ?? string.Empty; + + internal static int SourceStart(ISymbol symbol) => symbol.Locations + .Where(location => location.IsInSource) + .Select(location => location.SourceSpan.Start) + .DefaultIfEmpty(int.MaxValue) + .Min(); + + internal static string LowerFirst(string value) => value.Length == 0 + ? value + : $"{char.ToLowerInvariant(value[0])}{value[1..]}"; + static void AddSagaFacts( - DotNetProjectCompilation project, - AdapterIdentity adapter, - CritterStackSubjectResolver subjects, + WolverineSagaFactScope scope, INamedTypeSymbol sagaType, - IReadOnlyList roles, - List facts, - List diagnostics) + IReadOnlyList roles) { - var sagaSubject = subjects.SubjectForType(project, sagaType); + var sagaSubject = scope.Subjects.SubjectForType(scope.Project, sagaType); var sagaEvidence = CritterStackSource.EvidenceFor( sagaType, - adapter, - project, + scope.Adapter, + scope.Project, EvidenceStrength.Exact, "Authored public concrete type derived from Wolverine.Saga"); - facts.Add(Artifact( + scope.Facts.Add(Artifact( $"wolverine:saga:{sagaSubject.Value}", sagaSubject, ArtifactKind.Saga, sagaType.Name, sagaEvidence.Source?.Path, - SagaProperties(sagaType, project), + SagaProperties(sagaType, scope.Project), sagaEvidence)); foreach (var role in roles) { - AddRoleFacts(project, adapter, subjects, sagaType, role, roles, facts, diagnostics); + AddRoleFacts(scope, sagaType, role, roles); } - var completion = CompletionInvocation(roles, project); - diagnostics.Add(new GenerationDiagnostic + var completion = CompletionInvocation(roles, scope.Project); + scope.Diagnostics.Add(new GenerationDiagnostic { Code = WolverineDiagnosticCodes.SagaLifecycleRealization, Severity = GenerationDiagnosticSeverity.Information, @@ -179,25 +150,50 @@ static void AddSagaFacts( } static void AddRoleFacts( - DotNetProjectCompilation project, - AdapterIdentity adapter, - CritterStackSubjectResolver subjects, + WolverineSagaFactScope scope, INamedTypeSymbol sagaType, WolverineSagaRoleMethod role, - IReadOnlyList allRoles, - List facts, - List diagnostics) + IReadOnlyList allRoles) { + var project = scope.Project; var method = role.Method; var handlerSubject = HandlerSubject(project, method); - var messageSubject = subjects.SubjectForType(project, role.MessageType); var roleEvidence = CritterStackSource.EvidenceFor( method, - adapter, + scope.Adapter, project, EvidenceStrength.Conventional, $"Authored Wolverine saga {role.Role} role method"); - var correlation = CorrelationFor(project, adapter, sagaType, role.MessageType, allRoles); + var correlation = WolverineSagaCorrelations.For(project, scope.Adapter, sagaType, role.MessageType, allRoles); + AddHandlerFacts(scope, role, handlerSubject, roleEvidence, correlation); + + if (correlation.TargetMember is null) + { + scope.Diagnostics.Add(new GenerationDiagnostic + { + Code = WolverineDiagnosticCodes.SagaCorrelationRuntime, + Severity = GenerationDiagnosticSeverity.Information, + Outcome = GenerationDiagnosticOutcome.Unknown, + Message = correlation.UnresolvedReason ?? $"Saga handler '{sagaType.Name}.{method.Name}' has no authored message correlation member for '{role.MessageType.Name}'; correlation remains runtime-resolved", + Source = roleEvidence.Source, + Subject = handlerSubject + }); + } + + AddConsequenceFacts(scope, new(sagaType, role, handlerSubject, roleEvidence)); + } + + static void AddHandlerFacts( + WolverineSagaFactScope scope, + WolverineSagaRoleMethod role, + SubjectId handlerSubject, + Evidence roleEvidence, + WolverineSagaCorrelation correlation) + { + var project = scope.Project; + var method = role.Method; + var facts = scope.Facts; + var messageSubject = scope.Subjects.SubjectForType(project, role.MessageType); var relationshipEvidence = correlation.Evidence ?? roleEvidence; var discriminator = $"wolverine:saga:{role.Role}"; @@ -225,20 +221,17 @@ static void AddRoleFacts( relationshipEvidence, targetMember: correlation.TargetMember, discriminator: discriminator)); + } - if (correlation.TargetMember is null) - { - diagnostics.Add(new GenerationDiagnostic - { - Code = WolverineDiagnosticCodes.SagaCorrelationRuntime, - Severity = GenerationDiagnosticSeverity.Information, - Outcome = GenerationDiagnosticOutcome.Unknown, - Message = correlation.UnresolvedReason ?? $"Saga handler '{sagaType.Name}.{method.Name}' has no authored message correlation member for '{role.MessageType.Name}'; correlation remains runtime-resolved", - Source = roleEvidence.Source, - Subject = handlerSubject - }); - } - + static void AddConsequenceFacts(WolverineSagaFactScope scope, WolverineSagaRoleScope roleScope) + { + var project = scope.Project; + var subjects = scope.Subjects; + var facts = scope.Facts; + var diagnostics = scope.Diagnostics; + var method = roleScope.Role.Method; + var handlerSubject = roleScope.HandlerSubject; + var roleEvidence = roleScope.RoleEvidence; var returnConsequences = WolverineReturnConsequences.Classify( method, project, @@ -256,7 +249,18 @@ static void AddRoleFacts( facts, diagnostics); WolverineFacts.AddSagaDirectBusConsequences(project, subjects, handlerSubject, method, roleEvidence, facts, diagnostics); + AddTimeoutDiagnostics(diagnostics, roleScope, returnConsequences); + } + static void AddTimeoutDiagnostics( + List diagnostics, + WolverineSagaRoleScope roleScope, + IReadOnlyList returnConsequences) + { + var sagaType = roleScope.SagaType; + var method = roleScope.Role.Method; + var roleEvidence = roleScope.RoleEvidence; + var handlerSubject = roleScope.HandlerSubject; foreach (var timeout in returnConsequences .Where(consequence => consequence.Kind == WolverineReturnConsequenceKind.Cascade) .Select(consequence => consequence.Type) @@ -276,307 +280,6 @@ static void AddRoleFacts( } } - static WolverineSagaRoleAdmission RoleMethod( - IMethodSymbol method, - DotNetProjectCompilation project, - AdapterIdentity adapter) - { - var roleName = method.Name.EndsWith("Async", StringComparison.Ordinal) - ? method.Name[..^"Async".Length] - : method.Name; - if (!_roles.TryGetValue(roleName, out var role) || - IsIgnored(method, project) || - !WolverineMethodSyntax.Declarations(method, project).Any()) - { - return new(method, null, null); - } - - if (method.DeclaredAccessibility != Accessibility.Public || - method.MethodKind != MethodKind.Ordinary || - method.IsGenericMethod || - method.Parameters.Length == 0) - { - return RejectedRole(method, project, adapter, "the method must be a public, ordinary, non-generic method with a message parameter"); - } - - if (IsPrimitiveReturn(method.ReturnType)) - { - return RejectedRole(method, project, adapter, "Wolverine handler discovery rejects methods that directly return a primitive type"); - } - - if (method.Parameters[0].Type is not INamedTypeSymbol messageType || - !WolverineFacts.IsSagaMessagePayloadType(messageType) || - IsSagaType(messageType, project)) - { - return RejectedRole(method, project, adapter, "the first parameter must be an authored non-saga message payload"); - } - - var isStart = string.Equals(role, "start", StringComparison.Ordinal); - var isStartOrHandle = string.Equals(role, "start-or-handle", StringComparison.Ordinal); - var isNotFound = string.Equals(role, "not-found", StringComparison.Ordinal); - var isLegalShape = isStart || (isNotFound ? method.IsStatic : !method.IsStatic); - if (!isLegalShape) - { - var reason = "an existing-state role must be an instance method"; - if (isStartOrHandle) - { - reason = "a start-or-handle role must be an instance method"; - } - else if (isNotFound) - { - reason = "a not-found role must be a static method"; - } - return RejectedRole(method, project, adapter, reason); - } - - return new(method, new(method, messageType, role, isStart || isStartOrHandle), null); - } - - static bool EstablishesSagaChain(WolverineSagaRoleMethod role) => - !role.Method.IsStatic || - string.Equals(role.Method.Name, "Start", StringComparison.Ordinal) || - string.Equals(role.Method.Name, "StartAsync", StringComparison.Ordinal) || - string.Equals(role.Method.Name, "NotFound", StringComparison.Ordinal); - - static bool RequiresPublicParameterlessConstructor(IReadOnlyList roles) - { - if (IsNotFoundOnlyChain(roles)) - { - return true; - } - - var creationRoles = roles.Where(_ => _.IsCreationCapable).ToArray(); - if (creationRoles.Length == 0) - { - return false; - } - - if (creationRoles.Any(_ => !_.Method.IsStatic) || - roles.Any(_ => - string.Equals(_.Role, "start-or-handle", StringComparison.Ordinal) || - string.Equals(_.Role, "orchestrate", StringComparison.Ordinal))) - { - return true; - } - - return !creationRoles.Any(ReturnsExactSaga); - } - - static bool IsNotFoundOnlyChain(IReadOnlyList roles) => - roles.Count > 0 && roles.All(_ => string.Equals(_.Role, "not-found", StringComparison.Ordinal)); - - static bool ReturnsExactSaga(WolverineSagaRoleMethod role) => - WolverineReturnTypes.CreatedValues(role.Method) - .OfType() - .Any(type => SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, role.Method.ContainingType.OriginalDefinition)); - - static bool HasPublicParameterlessConstructor(INamedTypeSymbol sagaType) => - sagaType.InstanceConstructors.Any(constructor => - constructor.Parameters.Length == 0 && - constructor.DeclaredAccessibility == Accessibility.Public); - - static bool IsPrimitiveReturn(ITypeSymbol returnType) => returnType.SpecialType is - SpecialType.System_Boolean or - SpecialType.System_Byte or - SpecialType.System_SByte or - SpecialType.System_Int16 or - SpecialType.System_UInt16 or - SpecialType.System_Int32 or - SpecialType.System_UInt32 or - SpecialType.System_Int64 or - SpecialType.System_UInt64 or - SpecialType.System_Char or - SpecialType.System_Double or - SpecialType.System_Single or - SpecialType.System_IntPtr or - SpecialType.System_UIntPtr; - - static WolverineSagaRoleAdmission RejectedRole( - IMethodSymbol method, - DotNetProjectCompilation project, - AdapterIdentity adapter, - string reason) - { - var evidence = CritterStackSource.EvidenceFor( - method, - adapter, - project, - EvidenceStrength.Exact, - "Authored Wolverine saga lifecycle method outside the admitted role shape"); - return new(method, null, new GenerationDiagnostic - { - Code = WolverineDiagnosticCodes.SagaRoleUnresolved, - Severity = GenerationDiagnosticSeverity.Warning, - Outcome = GenerationDiagnosticOutcome.Unknown, - Message = $"Wolverine saga role '{DotNetMethodIdentity.DisplayName(method)}' was not admitted because {reason}", - Source = evidence.Source, - Subject = DotNetMethodIdentity.SubjectFor(project, method) - }); - } - - static WolverineSagaCorrelation CorrelationFor( - DotNetProjectCompilation project, - AdapterIdentity adapter, - INamedTypeSymbol sagaType, - INamedTypeSymbol messageType, - IReadOnlyList allRoles) - { - var members = MessageMembers(messageType, project).ToArray(); - var attributed = MatchMember(members, member => DotNetSymbols.HasAttribute(member, WellKnownTypes.WolverineSagaIdentityAttribute)); - if (attributed.IsAmbiguous) - { - return UnresolvedCorrelation(sagaType, messageType, "multiple public [SagaIdentity] members are visible"); - } - if (attributed.Member is not null) - { - return CorrelationFromMember(project, adapter, attributed.Member, EvidenceStrength.Exact, "Exact Wolverine [SagaIdentity] message member"); - } - - var specified = allRoles - .Where(role => SymbolEqualityComparer.Default.Equals(role.MessageType, messageType)) - .SelectMany(role => role.Method.Parameters.Select(SagaIdentityFrom)) - .OfType<(string MemberName, IParameterSymbol Parameter)>() - .ToArray(); - var specifiedNames = specified.Select(_ => _.MemberName).Distinct(StringComparer.Ordinal).ToArray(); - if (specifiedNames.Length > 1) - { - var evidence = SagaIdentityFromEvidence(project, adapter, specified[0].Parameter); - return UnresolvedCorrelation( - sagaType, - messageType, - "conflicting [SagaIdentityFrom] member names are declared across parameters in the same message chain", - evidence); - } - - if (specifiedNames.Length == 1) - { - var specifiedMember = MatchMember(members, member => string.Equals(member.Name, specifiedNames[0], StringComparison.Ordinal)); - if (specifiedMember.IsAmbiguous) - { - return UnresolvedCorrelation(sagaType, messageType, $"multiple public members match explicit [SagaIdentityFrom] name '{specifiedNames[0]}'"); - } - if (specifiedMember.Member is not null) - { - return new( - LowerFirst(specifiedMember.Member.Name), - SagaIdentityFromEvidence(project, adapter, specified[0].Parameter)); - } - } - else - { - var fullSagaName = $"{sagaType.Name}Id"; - var fullNameMember = MatchMember(members, member => string.Equals(member.Name, fullSagaName, StringComparison.Ordinal)); - if (fullNameMember.IsAmbiguous) - { - return UnresolvedCorrelation(sagaType, messageType, $"multiple public members match Wolverine convention '{fullSagaName}'"); - } - if (fullNameMember.Member is not null) - { - return CorrelationFromMember(project, adapter, fullNameMember.Member, EvidenceStrength.Conventional, $"Wolverine saga correlation member convention '{fullSagaName}'"); - } - } - - var suffixStrippedName = $"{sagaType.Name.Replace("Saga", string.Empty, StringComparison.InvariantCultureIgnoreCase)}Id"; - foreach (var expectedName in new[] { suffixStrippedName, "SagaId" }.Distinct(StringComparer.Ordinal)) - { - var conventional = MatchMember(members, member => string.Equals(member.Name, expectedName, StringComparison.Ordinal)); - if (conventional.IsAmbiguous) - { - return UnresolvedCorrelation(sagaType, messageType, $"multiple public members match Wolverine convention '{expectedName}'"); - } - if (conventional.Member is not null) - { - return CorrelationFromMember( - project, - adapter, - conventional.Member, - EvidenceStrength.Conventional, - $"Wolverine saga correlation member convention '{expectedName}'"); - } - } - - var id = MatchMember(members, member => string.Equals(member.Name, "Id", StringComparison.OrdinalIgnoreCase)); - if (id.IsAmbiguous) - { - return UnresolvedCorrelation(sagaType, messageType, "multiple public members match Wolverine's case-insensitive Id convention"); - } - if (id.Member is not null) - { - return CorrelationFromMember( - project, - adapter, - id.Member, - EvidenceStrength.Conventional, - "Wolverine case-insensitive Id saga correlation convention"); - } - - return new(null, null); - } - - static (string MemberName, IParameterSymbol Parameter)? SagaIdentityFrom(IParameterSymbol parameter) - { - var attribute = parameter.GetAttributes().FirstOrDefault(candidate => - candidate.AttributeClass is not null && - DotNetSubjectIds.MetadataName(candidate.AttributeClass.OriginalDefinition) == WellKnownTypes.WolverineSagaIdentityFromAttribute); - return attribute?.ConstructorArguments.FirstOrDefault().Value is string memberName - ? (memberName, parameter) - : null; - } - - static Evidence SagaIdentityFromEvidence( - DotNetProjectCompilation project, - AdapterIdentity adapter, - IParameterSymbol parameter) => CritterStackSource.EvidenceFor( - parameter, - adapter, - project, - EvidenceStrength.Exact, - "Exact Wolverine [SagaIdentityFrom] handler parameter"); - - static WolverineSagaCorrelation CorrelationFromMember( - DotNetProjectCompilation project, - AdapterIdentity adapter, - ISymbol member, - EvidenceStrength strength, - string explanation) => new( - LowerFirst(member.Name), - CritterStackSource.EvidenceFor(member, adapter, project, strength, explanation)); - - static WolverineSagaCorrelation UnresolvedCorrelation( - INamedTypeSymbol sagaType, - INamedTypeSymbol messageType, - string reason, - Evidence? evidence = null) => new( - null, - evidence, - $"Saga handler for '{sagaType.Name}' cannot safely select an authored correlation member for '{messageType.Name}' because {reason}; correlation remains runtime-resolved"); - - static WolverineSagaMemberMatch MatchMember(IEnumerable members, Func predicate) - { - var matches = members.Where(predicate).Take(2).ToArray(); - return new(matches.Length == 1 ? matches[0] : null, matches.Length > 1); - } - - static IEnumerable MessageMembers(INamedTypeSymbol messageType, DotNetProjectCompilation project) - { - var hierarchy = TypeHierarchy(messageType).ToArray(); - return hierarchy - .SelectMany(type => type.GetMembers().OfType()) - .Where(member => IsPublicAuthoredMember(member, project)) - .Cast() - .Concat(hierarchy - .SelectMany(type => type.GetMembers().OfType()) - .Where(member => !member.IsIndexer && IsPublicAuthoredMember(member, project))); - } - - static IEnumerable TypeHierarchy(INamedTypeSymbol type) - { - for (var current = type; current is not null; current = current.BaseType) - { - yield return current; - } - } - static IReadOnlyList SagaProperties(INamedTypeSymbol sagaType, DotNetProjectCompilation project) => [ .. sagaType.GetMembers() @@ -605,25 +308,25 @@ .. sagaType.GetMembers() return null; } - foreach (var role in roles) - { - foreach (var (declaration, semanticModel) in WolverineMethodSyntax.Declarations(role.Method, project)) - { - foreach (var invocation in declaration.DescendantNodes().OfType().OrderBy(node => node.SpanStart)) - { - if (semanticModel.GetSymbolInfo(invocation).Symbol is IMethodSymbol invoked && - invoked.Name == "MarkCompleted" && - SymbolEqualityComparer.Default.Equals(invoked.ContainingType.OriginalDefinition, sagaType.OriginalDefinition)) - { - return CritterStackSource.RangeForProject(invocation.GetLocation(), project); - } - } - } - } - - return null; + var invocation = roles + .SelectMany(role => WolverineMethodSyntax.Declarations(role.Method, project)) + .Select(declaration => FirstMarkCompleted(declaration.Declaration, declaration.SemanticModel, sagaType)) + .FirstOrDefault(_ => _ is not null); + return invocation is null ? null : CritterStackSource.RangeForProject(invocation.GetLocation(), project); } + static InvocationExpressionSyntax? FirstMarkCompleted(MethodDeclarationSyntax declaration, SemanticModel semanticModel, INamedTypeSymbol sagaType) => + declaration + .DescendantNodes() + .OfType() + .OrderBy(node => node.SpanStart) + .FirstOrDefault(node => IsMarkCompleted(semanticModel, node, sagaType)); + + static bool IsMarkCompleted(SemanticModel semanticModel, InvocationExpressionSyntax invocation, INamedTypeSymbol sagaType) => + semanticModel.GetSymbolInfo(invocation).Symbol is IMethodSymbol invoked && + invoked.Name == "MarkCompleted" && + SymbolEqualityComparer.Default.Equals(invoked.ContainingType.OriginalDefinition, sagaType.OriginalDefinition); + static bool IsAdmissibleType( INamedTypeSymbol type, DotNetProjectCompilation project, @@ -650,12 +353,6 @@ static bool IsEffectivelyPublic(INamedTypeSymbol type) return true; } - static bool IsIgnored(ISymbol symbol, DotNetProjectCompilation project) => symbol.GetAttributes().Any(attribute => - attribute.AttributeClass is not null && - (DotNetSubjectIds.MetadataName(attribute.AttributeClass.OriginalDefinition) == WellKnownTypes.WolverineIgnoreAttribute || - DotNetSubjectIds.MetadataName(attribute.AttributeClass.OriginalDefinition) == WellKnownTypes.WolverineLegacyIgnoreAttribute) && - IsAuthoredOrMetadataAttribute(attribute, project)); - static bool IsAuthoredOrMetadataAttribute(AttributeData attribute, DotNetProjectCompilation project) { if (attribute.ApplicationSyntaxReference is not { } syntaxReference) @@ -667,35 +364,15 @@ static bool IsAuthoredOrMetadataAttribute(AttributeData attribute, DotNetProject !DotNetGeneratedSource.IsGenerated(syntaxReference.SyntaxTree); } - static bool IsPublicAuthoredMember(ISymbol member, DotNetProjectCompilation project) => - !member.IsStatic && - member.DeclaredAccessibility == Accessibility.Public && - member.Locations.Any(location => IsAuthoredSourceLocation(location, project)); - static bool IsAuthoredSourceLocation(Location location, DotNetProjectCompilation project) => location.IsInSource && location.SourceTree is not null && project.AuthoredSyntaxTrees.Contains(location.SourceTree) && !DotNetGeneratedSource.IsGenerated(location.SourceTree); - static string MethodSignature(IMethodSymbol method) => - method.GetDocumentationCommentId() ?? method.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - static string? SourceFileOf(ISymbol symbol, DotNetProjectCompilation project) => CritterStackSource.EvidenceFor(symbol, new AdapterIdentity { Id = "source", Version = "1" }, project, EvidenceStrength.Exact).Source?.Path; - static string SourcePath(ISymbol symbol) => symbol.Locations - .Where(location => location.IsInSource) - .Select(location => location.SourceTree?.FilePath ?? string.Empty) - .Order(StringComparer.Ordinal) - .FirstOrDefault() ?? string.Empty; - - static int SourceStart(ISymbol symbol) => symbol.Locations - .Where(location => location.IsInSource) - .Select(location => location.SourceSpan.Start) - .DefaultIfEmpty(int.MaxValue) - .Min(); - static ArtifactFact Artifact( string id, SubjectId subject, @@ -741,8 +418,4 @@ static RelationshipFact Relationship( }, Evidence = evidence }; - - static string LowerFirst(string value) => value.Length == 0 - ? value - : $"{char.ToLowerInvariant(value[0])}{value[1..]}"; } diff --git a/Source/DotNET/CritterStack/Wolverine/WolverineSagaRoles.cs b/Source/DotNET/CritterStack/Wolverine/WolverineSagaRoles.cs new file mode 100644 index 0000000..f89db5c --- /dev/null +++ b/Source/DotNET/CritterStack/Wolverine/WolverineSagaRoles.cs @@ -0,0 +1,246 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Cratis.Screenplay.Generation; +using Cratis.Screenplay.Generation.DotNet; +using Microsoft.CodeAnalysis; + +namespace Cratis.CritterStack.Screenplay.Wolverine; + +sealed record WolverineSagaChainScope( + INamedTypeSymbol SagaType, + DotNetProjectCompilation Project, + AdapterIdentity Adapter, + List Diagnostics); + +sealed record WolverineSagaRoleSet( + IReadOnlyList Roles, + IReadOnlyList Diagnostics); + +/// +/// Admits the lifecycle methods of a saga type: which methods are roles, why a method is rejected, and which messages form a +/// saga chain Wolverine can actually run. +/// +static class WolverineSagaRoles +{ + static readonly Dictionary _roles = new(StringComparer.Ordinal) + { + ["Start"] = "start", + ["Starts"] = "start", + ["StartOrHandle"] = "start-or-handle", + ["StartsOrHandles"] = "start-or-handle", + ["Orchestrate"] = "orchestrate", + ["Orchestrates"] = "orchestrate", + ["Handle"] = "orchestrate", + ["Handles"] = "orchestrate", + ["Consume"] = "orchestrate", + ["Consumes"] = "orchestrate", + ["NotFound"] = "not-found" + }; + + public static WolverineSagaRoleSet Admit(INamedTypeSymbol sagaType, DotNetProjectCompilation project, AdapterIdentity adapter) + { + WolverineSagaRoleAdmission[] admissions = + [ + .. sagaType.GetMembers() + .OfType() + .Select(method => RoleMethod(method, project, adapter)) + .OrderBy(admission => WolverineSagaFacts.SourcePath(admission.Method), StringComparer.Ordinal) + .ThenBy(admission => WolverineSagaFacts.SourceStart(admission.Method)) + .ThenBy(admission => WolverineSagaFacts.MethodSignature(admission.Method), StringComparer.Ordinal) + ]; + List diagnostics = [.. admissions.Select(_ => _.Diagnostic).OfType()]; + + WolverineSagaRoleMethod[] candidateRoles = [.. admissions.Select(_ => _.Role).OfType()]; + List roles = []; + foreach (var messageGroup in candidateRoles + .GroupBy(_ => _.MessageType, SymbolEqualityComparer.Default) + .OrderBy(_ => DotNetSubjectIds.MetadataName(_.Key), StringComparer.Ordinal)) + { + roles.AddRange(AdmitChain([.. messageGroup], new(sagaType, project, adapter, diagnostics))); + } + + return new(roles, diagnostics); + } + + static WolverineSagaRoleMethod[] AdmitChain(WolverineSagaRoleMethod[] groupedRoles, WolverineSagaChainScope scope) + { + if (!groupedRoles.Any(EstablishesSagaChain)) + { + Reject(groupedRoles, "no other exact lifecycle method for the same message establishes a Wolverine SagaChain"); + return []; + } + + if (RequiresPublicParameterlessConstructor(groupedRoles) && + !HasPublicParameterlessConstructor(scope.SagaType)) + { + var isNotFoundOnlyChain = IsNotFoundOnlyChain(groupedRoles); + var rejectedRoles = groupedRoles.Where(role => role.IsCreationCapable || isNotFoundOnlyChain).ToArray(); + Reject(rejectedRoles, "Wolverine must create saga state, but the saga has no accessible public parameterless constructor and no exact returned saga supplies creation"); + groupedRoles = [.. groupedRoles.Except(rejectedRoles)]; + } + + if (!groupedRoles.Any(EstablishesSagaChain)) + { + Reject(groupedRoles, "the remaining methods for the same message do not establish a Wolverine SagaChain"); + return []; + } + + return groupedRoles; + + void Reject(IEnumerable rejected, string reason) => + scope.Diagnostics.AddRange(rejected.Select(role => RejectedRole(role.Method, scope.Project, scope.Adapter, reason).Diagnostic!)); + } + + static WolverineSagaRoleAdmission RoleMethod( + IMethodSymbol method, + DotNetProjectCompilation project, + AdapterIdentity adapter) + { + var roleName = method.Name.EndsWith("Async", StringComparison.Ordinal) + ? method.Name[..^"Async".Length] + : method.Name; + if (!_roles.TryGetValue(roleName, out var role) || !IsDiscoverable(method, project)) + { + return new(method, null, null); + } + + if (RejectionReason(method, role, project, out var messageType) is { } reason) + { + return RejectedRole(method, project, adapter, reason); + } + + var isCreationCapable = string.Equals(role, "start", StringComparison.Ordinal) || string.Equals(role, "start-or-handle", StringComparison.Ordinal); + return new(method, new(method, messageType!, role, isCreationCapable), null); + } + + static bool IsDiscoverable(IMethodSymbol method, DotNetProjectCompilation project) => + !WolverineSagaFacts.IsIgnored(method, project) && + WolverineMethodSyntax.Declarations(method, project).Any(); + + static string? RejectionReason( + IMethodSymbol method, + string role, + DotNetProjectCompilation project, + out INamedTypeSymbol? messageType) + { + messageType = null; + if (!IsOrdinaryMessageMethod(method)) + { + return "the method must be a public, ordinary, non-generic method with a message parameter"; + } + + if (IsPrimitiveReturn(method.ReturnType)) + { + return "Wolverine handler discovery rejects methods that directly return a primitive type"; + } + + if (method.Parameters[0].Type is not INamedTypeSymbol firstParameter || !IsMessagePayload(firstParameter, project)) + { + return "the first parameter must be an authored non-saga message payload"; + } + + messageType = firstParameter; + return ShapeRejection(method, role); + } + + static bool IsOrdinaryMessageMethod(IMethodSymbol method) => + method.DeclaredAccessibility == Accessibility.Public && + method.MethodKind == MethodKind.Ordinary && + !method.IsGenericMethod && + method.Parameters.Length > 0; + + static bool IsMessagePayload(INamedTypeSymbol type, DotNetProjectCompilation project) => + WolverineFacts.IsSagaMessagePayloadType(type) && !WolverineSagaFacts.IsSagaType(type, project); + + static string? ShapeRejection(IMethodSymbol method, string role) => role switch + { + "start" => null, + "not-found" => method.IsStatic ? null : "a not-found role must be a static method", + "start-or-handle" => method.IsStatic ? "a start-or-handle role must be an instance method" : null, + _ => method.IsStatic ? "an existing-state role must be an instance method" : null + }; + + static bool EstablishesSagaChain(WolverineSagaRoleMethod role) => + !role.Method.IsStatic || + string.Equals(role.Method.Name, "Start", StringComparison.Ordinal) || + string.Equals(role.Method.Name, "StartAsync", StringComparison.Ordinal) || + string.Equals(role.Method.Name, "NotFound", StringComparison.Ordinal); + + static bool RequiresPublicParameterlessConstructor(IReadOnlyList roles) + { + if (IsNotFoundOnlyChain(roles)) + { + return true; + } + + var creationRoles = roles.Where(_ => _.IsCreationCapable).ToArray(); + if (creationRoles.Length == 0) + { + return false; + } + + if (creationRoles.Any(_ => !_.Method.IsStatic) || roles.Any(IsStatefulRole)) + { + return true; + } + + return !creationRoles.Any(ReturnsExactSaga); + } + + static bool IsStatefulRole(WolverineSagaRoleMethod role) => + string.Equals(role.Role, "start-or-handle", StringComparison.Ordinal) || + string.Equals(role.Role, "orchestrate", StringComparison.Ordinal); + + static bool IsNotFoundOnlyChain(IReadOnlyList roles) => + roles.Count > 0 && roles.All(_ => string.Equals(_.Role, "not-found", StringComparison.Ordinal)); + + static bool ReturnsExactSaga(WolverineSagaRoleMethod role) => + WolverineReturnTypes.CreatedValues(role.Method) + .OfType() + .Any(type => SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, role.Method.ContainingType.OriginalDefinition)); + + static bool HasPublicParameterlessConstructor(INamedTypeSymbol sagaType) => + sagaType.InstanceConstructors.Any(constructor => + constructor.Parameters.Length == 0 && + constructor.DeclaredAccessibility == Accessibility.Public); + + static bool IsPrimitiveReturn(ITypeSymbol returnType) => returnType.SpecialType is + SpecialType.System_Boolean or + SpecialType.System_Byte or + SpecialType.System_SByte or + SpecialType.System_Int16 or + SpecialType.System_UInt16 or + SpecialType.System_Int32 or + SpecialType.System_UInt32 or + SpecialType.System_Int64 or + SpecialType.System_UInt64 or + SpecialType.System_Char or + SpecialType.System_Double or + SpecialType.System_Single or + SpecialType.System_IntPtr or + SpecialType.System_UIntPtr; + + static WolverineSagaRoleAdmission RejectedRole( + IMethodSymbol method, + DotNetProjectCompilation project, + AdapterIdentity adapter, + string reason) + { + var evidence = CritterStackSource.EvidenceFor( + method, + adapter, + project, + EvidenceStrength.Exact, + "Authored Wolverine saga lifecycle method outside the admitted role shape"); + return new(method, null, new GenerationDiagnostic + { + Code = WolverineDiagnosticCodes.SagaRoleUnresolved, + Severity = GenerationDiagnosticSeverity.Warning, + Outcome = GenerationDiagnosticOutcome.Unknown, + Message = $"Wolverine saga role '{DotNetMethodIdentity.DisplayName(method)}' was not admitted because {reason}", + Source = evidence.Source, + Subject = DotNetMethodIdentity.SubjectFor(project, method) + }); + } +} diff --git a/Source/DotNET/CritterStack/Wolverine/WolverineSagaTypes.cs b/Source/DotNET/CritterStack/Wolverine/WolverineSagaTypes.cs index 9928c7c..4ad0b6f 100644 --- a/Source/DotNET/CritterStack/Wolverine/WolverineSagaTypes.cs +++ b/Source/DotNET/CritterStack/Wolverine/WolverineSagaTypes.cs @@ -45,6 +45,11 @@ static bool IsAuthoredOrMetadataAssignableTo( return type.BaseType is not null && IsAuthoredOrMetadataAssignableTo(type.BaseType, target, project, visited); } + return DeclaredBaseTypes(type, project).Any(candidate => IsAuthoredOrMetadataAssignableTo(candidate, target, project, visited)); + } + + static IEnumerable DeclaredBaseTypes(INamedTypeSymbol type, DotNetProjectCompilation project) + { foreach (var syntaxReference in type.DeclaringSyntaxReferences) { if (syntaxReference.GetSyntax() is not TypeDeclarationSyntax { BaseList: not null } declaration || @@ -57,14 +62,11 @@ static bool IsAuthoredOrMetadataAssignableTo( var semanticModel = project.Compilation.GetSemanticModel(declaration.SyntaxTree); foreach (var baseType in declaration.BaseList.Types) { - if (semanticModel.GetTypeInfo(baseType.Type).Type is INamedTypeSymbol candidate && - IsAuthoredOrMetadataAssignableTo(candidate, target, project, visited)) + if (semanticModel.GetTypeInfo(baseType.Type).Type is INamedTypeSymbol candidate) { - return true; + yield return candidate; } } } - - return false; } } diff --git a/Source/DotNET/CritterStack/Wolverine/WolverineSlicePatterns.cs b/Source/DotNET/CritterStack/Wolverine/WolverineSlicePatterns.cs index 7e33f4f..0779849 100644 --- a/Source/DotNET/CritterStack/Wolverine/WolverineSlicePatterns.cs +++ b/Source/DotNET/CritterStack/Wolverine/WolverineSlicePatterns.cs @@ -12,102 +12,36 @@ sealed record WolverineSlicePattern(GenerationSliceKind? Kind, Evidence? Evidenc static class WolverineSlicePatterns { public static WolverineSlicePattern Resolve( + WolverineFactScope scope, IMethodSymbol method, INamedTypeSymbol? request, - HttpEndpoint? endpoint, - DotNetProjectCompilation project, - AdapterIdentity adapter, - List diagnostics) + HttpEndpoint? endpoint) { + var project = scope.Project; var attribute = Declaration(method, project); var source = attribute?.ApplicationSyntaxReference?.GetSyntax().GetLocation(); var range = source is null ? null : CritterStackSource.RangeForProject(source, project); - GenerationSliceKind? declared = null; - if (attribute is not null) - { - var enumType = project.Compilation.GetTypeByMetadataName(WellKnownTypes.JasperFxSlicePattern); - var argument = attribute.ConstructorArguments.FirstOrDefault(); - var resolved = enumType is not null && WolverineSymbolAuthority.IsAuthoredOrMetadataSymbol(enumType, project) && - attribute.AttributeConstructor?.Parameters is [{ Type: var parameterType }] && - SymbolEqualityComparer.Default.Equals(parameterType, enumType) && - argument.Kind == TypedConstantKind.Enum && SymbolEqualityComparer.Default.Equals(argument.Type, enumType); - var name = resolved - ? enumType!.GetMembers().OfType().FirstOrDefault(field => field.HasConstantValue && Equals(field.ConstantValue, argument.Value))?.Name - : null; - declared = name switch - { - "Command" => GenerationSliceKind.StateChange, - "View" => GenerationSliceKind.StateView, - "Automation" => GenerationSliceKind.Automation, - "Translation" => GenerationSliceKind.Translate, - _ => null - }; - if (declared is null) - { - diagnostics.Add(new() - { - Code = WolverineDiagnosticCodes.SlicePatternUnresolved, - Severity = GenerationDiagnosticSeverity.Warning, - Outcome = resolved ? GenerationDiagnosticOutcome.Unsupported : GenerationDiagnosticOutcome.Unknown, - Message = $"The SlicePattern declaration on '{DotNetMethodIdentity.DisplayName(method)}' has an {(resolved ? "undefined" : "unresolved")} value; independently inferred classification is retained", - Source = range, - Subject = DotNetMethodIdentity.SubjectFor(project, method) - }); - } - } - - GenerationSliceKind? trigger = IsAuthoredTimeout(request, project) ? GenerationSliceKind.Automation : null; - if (endpoint is not null) + var declared = attribute is null ? null : Declared(attribute, scope, method, range); + var trigger = Trigger(request, endpoint, project); + if (IsConflict(declared, trigger)) { - // HEAD and OPTIONS are safe verbs that are still analyzed for effects, so the verb - // proves neither a view nor a state change; a declaration may classify them. - trigger = endpoint switch - { - { IsQuery: true } => GenerationSliceKind.StateView, - { IsUnclassifiedSafeVerb: true } => null, - _ => GenerationSliceKind.StateChange - }; - } - if (declared is not null && trigger is not null && declared != trigger) - { - diagnostics.Add(new() - { - Code = WolverineDiagnosticCodes.SlicePatternConflict, - Severity = GenerationDiagnosticSeverity.Warning, - Outcome = GenerationDiagnosticOutcome.Conflict, - Message = $"The SlicePattern declaration '{declared}' conflicts with the proven {(endpoint is null ? "TimeoutMessage" : "HTTP")} trigger '{trigger}' on '{DotNetMethodIdentity.DisplayName(method)}'; the trigger takes precedence", - Source = range, - Subject = DotNetMethodIdentity.SubjectFor(project, method) - }); + scope.Diagnostics.Add(ConflictDiagnostic(scope, method, endpoint, (declared, trigger), range)); } var kind = trigger ?? declared; - var explanation = trigger is not null - ? $"Proven {(endpoint is null ? "TimeoutMessage scheduler" : $"HTTP {endpoint.Verb}")} trigger classifies the slice as {trigger}" - : $"Exact Wolverine SlicePattern declaration classifies the slice as {declared}"; - if (trigger is not null && declared is not null) - { - explanation += $"; declared SlicePattern={declared}"; - } - - var evidence = kind is null ? null : new Evidence - { - Adapter = adapter, - Strength = EvidenceStrength.Exact, - Source = range ?? CritterStackSource.EvidenceFor(method, adapter, project, EvidenceStrength.Exact).Source, - Explanation = explanation - }; + var evidence = kind is null ? null : SliceEvidence(scope, method, endpoint, (trigger, declared), range); return new(kind, evidence, declared is not null); } public static void Apply( WolverineSlicePattern decision, + WolverineFactScope scope, IMethodSymbol method, - DotNetProjectCompilation project, - List placements, - int firstPlacement, - List diagnostics) + int firstPlacement) { + var project = scope.Project; + var diagnostics = scope.Diagnostics; + var placements = scope.Placements; if (decision.HasDeclaration && placements.Count == firstPlacement) { diagnostics.Add(new() @@ -137,25 +71,127 @@ public static void Apply( } } - static AttributeData? Declaration(IMethodSymbol method, DotNetProjectCompilation project) + static GenerationSliceKind? Declared( + AttributeData attribute, + WolverineFactScope scope, + IMethodSymbol method, + SourceRange? range) + { + var (resolved, name) = DeclaredName(attribute, scope.Project); + var declared = KindFor(name); + if (declared is null) + { + scope.Diagnostics.Add(UnresolvedDiagnostic(scope, method, resolved, range)); + } + + return declared; + } + + static GenerationSliceKind? KindFor(string? name) => name switch + { + "Command" => GenerationSliceKind.StateChange, + "View" => GenerationSliceKind.StateView, + "Automation" => GenerationSliceKind.Automation, + "Translation" => GenerationSliceKind.Translate, + _ => null + }; + + static GenerationDiagnostic UnresolvedDiagnostic(WolverineFactScope scope, IMethodSymbol method, bool resolved, SourceRange? range) => new() + { + Code = WolverineDiagnosticCodes.SlicePatternUnresolved, + Severity = GenerationDiagnosticSeverity.Warning, + Outcome = resolved ? GenerationDiagnosticOutcome.Unsupported : GenerationDiagnosticOutcome.Unknown, + Message = $"The SlicePattern declaration on '{DotNetMethodIdentity.DisplayName(method)}' has an {(resolved ? "undefined" : "unresolved")} value; independently inferred classification is retained", + Source = range, + Subject = DotNetMethodIdentity.SubjectFor(scope.Project, method) + }; + + static bool IsConflict(GenerationSliceKind? declared, GenerationSliceKind? trigger) => + declared is not null && trigger is not null && declared != trigger; + + static GenerationDiagnostic ConflictDiagnostic( + WolverineFactScope scope, + IMethodSymbol method, + HttpEndpoint? endpoint, + (GenerationSliceKind? Declared, GenerationSliceKind? Trigger) kinds, + SourceRange? range) => new() + { + Code = WolverineDiagnosticCodes.SlicePatternConflict, + Severity = GenerationDiagnosticSeverity.Warning, + Outcome = GenerationDiagnosticOutcome.Conflict, + Message = $"The SlicePattern declaration '{kinds.Declared}' conflicts with the proven {(endpoint is null ? "TimeoutMessage" : "HTTP")} trigger '{kinds.Trigger}' on '{DotNetMethodIdentity.DisplayName(method)}'; the trigger takes precedence", + Source = range, + Subject = DotNetMethodIdentity.SubjectFor(scope.Project, method) + }; + + static Evidence SliceEvidence( + WolverineFactScope scope, + IMethodSymbol method, + HttpEndpoint? endpoint, + (GenerationSliceKind? Trigger, GenerationSliceKind? Declared) kinds, + SourceRange? range) => new() + { + Adapter = scope.Adapter, + Strength = EvidenceStrength.Exact, + Source = range ?? CritterStackSource.EvidenceFor(method, scope.Adapter, scope.Project, EvidenceStrength.Exact).Source, + Explanation = Explanation(kinds.Trigger, kinds.Declared, endpoint) + }; + + static (bool Resolved, string? Name) DeclaredName(AttributeData attribute, DotNetProjectCompilation project) + { + var enumType = project.Compilation.GetTypeByMetadataName(WellKnownTypes.JasperFxSlicePattern); + var argument = attribute.ConstructorArguments.FirstOrDefault(); + if (!IsSlicePatternArgument(attribute, argument, enumType, project)) + { + return (false, null); + } + + var name = enumType!.GetMembers().OfType() + .FirstOrDefault(field => field.HasConstantValue && Equals(field.ConstantValue, argument.Value))?.Name; + return (true, name); + } + + static bool IsSlicePatternArgument(AttributeData attribute, TypedConstant argument, INamedTypeSymbol? enumType, DotNetProjectCompilation project) => + enumType is not null && WolverineSymbolAuthority.IsAuthoredOrMetadataSymbol(enumType, project) && + attribute.AttributeConstructor?.Parameters is [{ Type: var parameterType }] && + SymbolEqualityComparer.Default.Equals(parameterType, enumType) && + argument.Kind == TypedConstantKind.Enum && SymbolEqualityComparer.Default.Equals(argument.Type, enumType); + + static GenerationSliceKind? Trigger(INamedTypeSymbol? request, HttpEndpoint? endpoint, DotNetProjectCompilation project) => endpoint switch + { + // HEAD and OPTIONS are safe verbs that are still analyzed for effects, so the verb + // proves neither a view nor a state change; a declaration may classify them. + null => IsAuthoredTimeout(request, project) ? GenerationSliceKind.Automation : null, + { IsQuery: true } => GenerationSliceKind.StateView, + { IsUnclassifiedSafeVerb: true } => null, + _ => GenerationSliceKind.StateChange + }; + + static string Explanation(GenerationSliceKind? trigger, GenerationSliceKind? declared, HttpEndpoint? endpoint) + { + if (trigger is null) + { + return $"Exact Wolverine SlicePattern declaration classifies the slice as {declared}"; + } + + var proven = $"Proven {(endpoint is null ? "TimeoutMessage scheduler" : $"HTTP {endpoint.Verb}")} trigger classifies the slice as {trigger}"; + return declared is null ? proven : $"{proven}; declared SlicePattern={declared}"; + } + + static AttributeData? Declaration(IMethodSymbol method, DotNetProjectCompilation project) => + DeclarationSites(method).Select(site => AttributeOn(site, project)).FirstOrDefault(attribute => attribute is not null); + + static IEnumerable DeclarationSites(IMethodSymbol method) { for (var current = method; current is not null; current = current.OverriddenMethod) { - if (AttributeOn(current, project) is { } attribute) - { - return attribute; - } + yield return current; } for (var current = method.ContainingType; current is not null; current = current.BaseType) { - if (AttributeOn(current, project) is { } attribute) - { - return attribute; - } + yield return current; } - - return null; } static AttributeData? AttributeOn(ISymbol symbol, DotNetProjectCompilation project) diff --git a/Source/DotNET/CritterStack/Wolverine/WolverineValidationAuthorizationDiscovery.cs b/Source/DotNET/CritterStack/Wolverine/WolverineValidationAuthorizationDiscovery.cs index df9f002..17ffc43 100644 --- a/Source/DotNET/CritterStack/Wolverine/WolverineValidationAuthorizationDiscovery.cs +++ b/Source/DotNET/CritterStack/Wolverine/WolverineValidationAuthorizationDiscovery.cs @@ -10,341 +10,156 @@ namespace Cratis.CritterStack.Screenplay.Wolverine; static class WolverineValidationAuthorizationDiscovery { + static readonly (string ContainingType, string MethodName, WolverineValidationPolicyKind Kind, WolverineValidationPolicyScope Scope)[] _validationActivations = + [ + (WellKnownTypes.WolverineFluentValidationExtensions, "UseFluentValidation", WolverineValidationPolicyKind.FluentValidation, WolverineValidationPolicyScope.MessageHandlers), + (WellKnownTypes.WolverineDataAnnotationsValidationExtensions, "UseDataAnnotationsValidation", WolverineValidationPolicyKind.DataAnnotations, WolverineValidationPolicyScope.MessageHandlers), + (WellKnownTypes.WolverineHttpFluentValidationExtensions, "UseFluentValidationProblemDetailMiddleware", WolverineValidationPolicyKind.FluentValidation, WolverineValidationPolicyScope.HttpEndpoints), + (WellKnownTypes.WolverineHttpOptions, "UseDataAnnotationsValidationProblemDetailMiddleware", WolverineValidationPolicyKind.DataAnnotations, WolverineValidationPolicyScope.HttpEndpoints) + ]; + public static WolverineValidationAuthorizationDiscoveryResult Discover( DotNetProjectCompilation project, CritterStackSubjectResolver subjects) { - var validationPolicies = new List(); - var authorizationPolicies = new List(); - var diagnostics = new List(); + var context = new DiscoveryContext(project, subjects); var sourceTypes = new DotNetArtifactCatalog(project.Compilation).Types.ToArray(); foreach (var call in ConfigurationCalls(project)) { - var containingType = DotNetSubjectIds.MetadataName(call.Method.ContainingType.OriginalDefinition); - if (IsValidationActivation(containingType, call.Method.Name, out var kind, out var scope)) - { - DiscoverValidationActivation(project, subjects, call, kind, scope, validationPolicies, diagnostics); - continue; - } + DiscoverCall(context, call); + } - if (containingType == WellKnownTypes.WolverineHttpOptions) - { - switch (call.Method.Name) - { - case "RequireAuthorizeOnAll": - DiscoverGlobalAuthorization(project, subjects, call, "RequireAuthorizeOnAll", authorizationPolicies, diagnostics); - break; - case "ConfigureEndpoints": - DiscoverConfiguredEndpointAuthorization(project, subjects, call, authorizationPolicies, diagnostics); - break; - } - } + context.AddActivationDiagnostics(); + return new(project, context.ValidationPolicies, sourceTypes, context.Diagnostics); + } + + static void DiscoverCall(DiscoveryContext context, ConfigurationCall call) + { + var containingType = DotNetSubjectIds.MetadataName(call.Method.ContainingType.OriginalDefinition); + if (IsValidationActivation(containingType, call.Method.Name, out var kind, out var scope)) + { + DiscoverValidationActivation(context, call, kind, scope); + return; } - diagnostics.AddRange(validationPolicies.Select(policy => new GenerationDiagnostic + if (containingType != WellKnownTypes.WolverineHttpOptions) { - Code = WolverineDiagnosticCodes.ValidationPolicyOmitted, - Severity = GenerationDiagnosticSeverity.Information, - Outcome = GenerationDiagnosticOutcome.Unsupported, - Message = $"Wolverine {ValidationName(policy.Kind)} {ScopeName(policy.Scope)} policy is enabled by '{policy.MethodName}', but current generation contracts cannot represent policy activation", - Source = policy.Source, - Subject = policy.Subject - })); - diagnostics.AddRange(authorizationPolicies.Select(policy => new GenerationDiagnostic + return; + } + + switch (call.Method.Name) { - Code = WolverineDiagnosticCodes.AuthorizationOmitted, - Severity = GenerationDiagnosticSeverity.Warning, - Outcome = GenerationDiagnosticOutcome.Unsupported, - Message = $"Global Wolverine HTTP authorization is enabled by {policy.Description}, but current generation contracts cannot represent authorization without overloading an unrelated relationship", - Source = policy.Source, - Subject = policy.Subject - })); - - return new(project, validationPolicies, sourceTypes, diagnostics); + case "RequireAuthorizeOnAll": + DiscoverGlobalAuthorization(context, call, "RequireAuthorizeOnAll"); + break; + case "ConfigureEndpoints": + DiscoverConfiguredEndpointAuthorization(context, call); + break; + } } static void DiscoverValidationActivation( - DotNetProjectCompilation project, - CritterStackSubjectResolver subjects, + DiscoveryContext context, ConfigurationCall call, WolverineValidationPolicyKind kind, - WolverineValidationPolicyScope scope, - List policies, - List diagnostics) + WolverineValidationPolicyScope scope) { if (IsConditionallyExecuted(call.Invocation)) { - diagnostics.Add(UnresolvedValidation( - project, - subjects, - call, - "the policy call is conditionally executed at runtime")); + context.ReportUnresolvedValidation(call, "the policy call is conditionally executed at runtime"); return; } - var canDiscoverValidators = true; - var includeInternalValidators = false; - var optionsResolved = true; - if (kind == WolverineValidationPolicyKind.FluentValidation && - !TryResolveFluentValidationOptions(call, out canDiscoverValidators, out includeInternalValidators, out var reason)) + var isFluent = kind == WolverineValidationPolicyKind.FluentValidation; + var options = isFluent + ? WolverineFluentValidationOptions.Resolve(call.Invocation, call.SemanticModel) + : new WolverineFluentValidationOptions(); + var optionsResolved = options.Resolved; + if (!optionsResolved) { - diagnostics.Add(UnresolvedValidation(project, subjects, call, reason)); - canDiscoverValidators = false; - includeInternalValidators = false; - optionsResolved = false; + context.ReportUnresolvedValidation(call, options.Failure!); + options.CanDiscoverValidators = false; + options.IncludeInternalValidators = false; } - var source = CritterStackSource.RangeForProject(call.Invocation.GetLocation(), project); - var subject = ConfigurationSubject(project, subjects, call); - policies.Add(new( + context.ValidationPolicies.Add(new( kind, scope, - canDiscoverValidators, - includeInternalValidators, - source, - subject, + options.CanDiscoverValidators, + options.IncludeInternalValidators, + context.SourceOf(call), + context.SubjectOf(call), call.Method.Name)); - if (kind == WolverineValidationPolicyKind.FluentValidation && optionsResolved && !canDiscoverValidators) + if (isFluent && optionsResolved && !options.CanDiscoverValidators) { - diagnostics.Add(UnresolvedValidation( - project, - subjects, - call, - "validator applicability depends on explicit runtime container registrations")); + context.ReportUnresolvedValidation(call, "validator applicability depends on explicit runtime container registrations"); } } - static void DiscoverGlobalAuthorization( - DotNetProjectCompilation project, - CritterStackSubjectResolver subjects, - ConfigurationCall call, - string description, - List policies, - List diagnostics) + static void DiscoverGlobalAuthorization(DiscoveryContext context, ConfigurationCall call, string description) { if (IsConditionallyExecuted(call.Invocation)) { - diagnostics.Add(UnresolvedAuthorization( - project, - subjects, - call, - "the global authorization call is conditionally executed at runtime")); + context.ReportUnresolvedAuthorization(call, "the global authorization call is conditionally executed at runtime"); return; } - policies.Add(new( - CritterStackSource.RangeForProject(call.Invocation.GetLocation(), project), - ConfigurationSubject(project, subjects, call), - $"'{description}'")); + context.AuthorizationPolicies.Add(new(context.SourceOf(call), context.SubjectOf(call), $"'{description}'")); } - static void DiscoverConfiguredEndpointAuthorization( - DotNetProjectCompilation project, - CritterStackSubjectResolver subjects, - ConfigurationCall call, - List policies, - List diagnostics) + static void DiscoverConfiguredEndpointAuthorization(DiscoveryContext context, ConfigurationCall call) { if (call.Invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression is not LambdaExpressionSyntax lambda || - LambdaParameter(lambda, call.SemanticModel) is not { } callbackParameter) + WolverineLambdaSyntax.Parameter(lambda, call.SemanticModel) is not { } callbackParameter) { return; } - var authorizationCalls = new List(); - foreach (var invocation in lambda.DescendantNodes().OfType()) - { - if (call.SemanticModel.GetSymbolInfo(invocation).Symbol is IMethodSymbol invoked && - invoked.Name == "RequireAuthorization" && - DotNetSubjectIds.MetadataName((invoked.ReducedFrom ?? invoked).ContainingType.OriginalDefinition) == WellKnownTypes.AspNetAuthorizationEndpointExtensions && - invocation.Expression is MemberAccessExpressionSyntax memberAccess && - IsRootedInParameter(memberAccess.Expression, callbackParameter, call.SemanticModel)) - { - authorizationCalls.Add(invocation); - } - } - + var authorizationCalls = EndpointAuthorizationCalls(lambda, callbackParameter, call.SemanticModel); if (authorizationCalls.Count == 0) { return; } - if (IsConditionallyExecuted(call.Invocation) || - authorizationCalls.Exists(_ => IsConditionallyExecuted(_) || !HasConstantAuthorizationArguments(call.SemanticModel, _))) + if (IsConditionallyExecuted(call.Invocation) || authorizationCalls.Exists(_ => IsRuntimeAuthorization(call.SemanticModel, _))) { - diagnostics.Add(UnresolvedAuthorization( - project, - subjects, - call, - "the endpoint authorization policy is conditional or has runtime arguments")); + context.ReportUnresolvedAuthorization(call, "the endpoint authorization policy is conditional or has runtime arguments"); return; } - policies.Add(new( - CritterStackSource.RangeForProject(call.Invocation.GetLocation(), project), - ConfigurationSubject(project, subjects, call), + context.AuthorizationPolicies.Add(new( + context.SourceOf(call), + context.SubjectOf(call), "'ConfigureEndpoints(... RequireAuthorization(...))'")); } - static bool HasConstantAuthorizationArguments(SemanticModel semanticModel, InvocationExpressionSyntax invocation) => - invocation.ArgumentList.Arguments.All(argument => - semanticModel.GetTypeInfo(argument.Expression).ConvertedType?.SpecialType == SpecialType.System_String && - semanticModel.GetConstantValue(argument.Expression).HasValue); - - static bool TryResolveFluentValidationOptions( - ConfigurationCall call, - out bool canDiscoverValidators, - out bool includeInternalValidators, - out string reason) - { - canDiscoverValidators = true; - includeInternalValidators = false; - reason = string.Empty; - foreach (var argument in call.Invocation.ArgumentList.Arguments) - { - var expression = argument.Expression; - if (expression is LambdaExpressionSyntax lambda) - { - return TryResolveFluentValidationCallback( - call.SemanticModel, - lambda, - out canDiscoverValidators, - out includeInternalValidators, - out reason); - } - - if (expression is AnonymousMethodExpressionSyntax) - { - reason = "the FluentValidation configuration callback is executed at runtime"; - return false; - } - - var convertedType = call.SemanticModel.GetTypeInfo(expression).ConvertedType; - if (convertedType is INamedTypeSymbol { TypeKind: TypeKind.Delegate } delegateType && - delegateType.TypeArguments.Any(_ => _ is INamedTypeSymbol named && DotNetSubjectIds.MetadataName(named) == WellKnownTypes.WolverineFluentValidationConfiguration)) - { - reason = "the FluentValidation configuration callback is executed at runtime"; - return false; - } - - var constant = call.SemanticModel.GetConstantValue(expression); - if (!constant.HasValue) - { - reason = "a FluentValidation option is not a compile-time constant"; - return false; - } - - if (convertedType?.SpecialType == SpecialType.System_Boolean && constant.Value is bool includeInternal) - { - includeInternalValidators = includeInternal; - continue; - } - - if (convertedType is INamedTypeSymbol enumType && - DotNetSubjectIds.MetadataName(enumType.OriginalDefinition) == WellKnownTypes.WolverineFluentValidationRegistrationBehavior && - constant.Value is int behavior) - { - canDiscoverValidators = behavior == 0; - continue; - } - - reason = "a FluentValidation option uses an unsupported runtime configuration shape"; - return false; - } - - return true; - } - - static bool TryResolveFluentValidationCallback( - SemanticModel semanticModel, + static List EndpointAuthorizationCalls( LambdaExpressionSyntax lambda, - out bool canDiscoverValidators, - out bool includeInternalValidators, - out string reason) - { - canDiscoverValidators = true; - includeInternalValidators = false; - reason = string.Empty; - if (LambdaParameter(lambda, semanticModel) is not { } callbackParameter) - { - reason = "the FluentValidation configuration callback parameter cannot be resolved"; - return false; - } + IParameterSymbol callbackParameter, + SemanticModel semanticModel) => + [.. lambda.DescendantNodes() + .OfType() + .Where(_ => IsAuthorizationCallOnParameter(semanticModel, _, callbackParameter))]; - var assignments = lambda.Body switch - { - AssignmentExpressionSyntax assignment => [assignment], - BlockSyntax block when block.Statements.All(_ => _ is ExpressionStatementSyntax { Expression: AssignmentExpressionSyntax }) => - block.Statements.Cast().Select(_ => (AssignmentExpressionSyntax)_.Expression).ToArray(), - _ => [] - }; - if (assignments.Length == 0) - { - reason = "the FluentValidation configuration callback contains runtime or custom behavior"; - return false; - } - - foreach (var assignment in assignments) - { - if (semanticModel.GetSymbolInfo(assignment.Left).Symbol is not IPropertySymbol property || - DotNetSubjectIds.MetadataName(property.ContainingType) != WellKnownTypes.WolverineFluentValidationConfiguration || - assignment.Left is not MemberAccessExpressionSyntax memberAccess || - !IsRootedInParameter(memberAccess.Expression, callbackParameter, semanticModel)) - { - reason = "the FluentValidation configuration callback contains runtime or custom behavior"; - return false; - } - - var constant = semanticModel.GetConstantValue(assignment.Right); - if (!constant.HasValue) - { - reason = $"FluentValidation option '{property.Name}' is not a compile-time constant"; - return false; - } - - switch (property.Name) - { - case "IncludeInternalTypes" when constant.Value is bool includeInternal: - includeInternalValidators = includeInternal; - break; - case "RegistrationBehavior" when constant.Value is int behavior: - canDiscoverValidators = behavior == 0; - break; - default: - reason = $"FluentValidation option '{property.Name}' is not statically supported"; - return false; - } - } - - return true; - } + static bool IsAuthorizationCallOnParameter( + SemanticModel semanticModel, + InvocationExpressionSyntax invocation, + IParameterSymbol callbackParameter) => + semanticModel.GetSymbolInfo(invocation).Symbol is IMethodSymbol invoked && + string.Equals(invoked.Name, "RequireAuthorization", StringComparison.Ordinal) && + DotNetSubjectIds.MetadataName((invoked.ReducedFrom ?? invoked).ContainingType.OriginalDefinition) == WellKnownTypes.AspNetAuthorizationEndpointExtensions && + invocation.Expression is MemberAccessExpressionSyntax memberAccess && + WolverineLambdaSyntax.IsRootedInParameter(memberAccess.Expression, callbackParameter, semanticModel); - static IParameterSymbol? LambdaParameter(LambdaExpressionSyntax lambda, SemanticModel semanticModel) - { - var parameter = lambda switch - { - SimpleLambdaExpressionSyntax simple => simple.Parameter, - ParenthesizedLambdaExpressionSyntax { ParameterList.Parameters.Count: 1 } parenthesized => parenthesized.ParameterList.Parameters[0], - _ => null - }; - return parameter is null ? null : semanticModel.GetDeclaredSymbol(parameter) as IParameterSymbol; - } + static bool IsRuntimeAuthorization(SemanticModel semanticModel, InvocationExpressionSyntax invocation) => + IsConditionallyExecuted(invocation) || !HasConstantAuthorizationArguments(semanticModel, invocation); - static bool IsRootedInParameter( - ExpressionSyntax expression, - IParameterSymbol parameter, - SemanticModel semanticModel) => expression switch - { - IdentifierNameSyntax identifier => SymbolEqualityComparer.Default.Equals( - semanticModel.GetSymbolInfo(identifier).Symbol, - parameter), - MemberAccessExpressionSyntax memberAccess => IsRootedInParameter(memberAccess.Expression, parameter, semanticModel), - ParenthesizedExpressionSyntax parenthesized => IsRootedInParameter(parenthesized.Expression, parameter, semanticModel), - CastExpressionSyntax cast => IsRootedInParameter(cast.Expression, parameter, semanticModel), - _ => false - }; + static bool HasConstantAuthorizationArguments(SemanticModel semanticModel, InvocationExpressionSyntax invocation) => + invocation.ArgumentList.Arguments.All(argument => + semanticModel.GetTypeInfo(argument.Expression).ConvertedType?.SpecialType == SpecialType.System_String && + semanticModel.GetConstantValue(argument.Expression).HasValue); static bool IsValidationActivation( string containingType, @@ -352,32 +167,14 @@ static bool IsValidationActivation( out WolverineValidationPolicyKind kind, out WolverineValidationPolicyScope scope) { - if (containingType == WellKnownTypes.WolverineFluentValidationExtensions && methodName == "UseFluentValidation") - { - kind = WolverineValidationPolicyKind.FluentValidation; - scope = WolverineValidationPolicyScope.MessageHandlers; - return true; - } - - if (containingType == WellKnownTypes.WolverineDataAnnotationsValidationExtensions && methodName == "UseDataAnnotationsValidation") - { - kind = WolverineValidationPolicyKind.DataAnnotations; - scope = WolverineValidationPolicyScope.MessageHandlers; - return true; - } - - if (containingType == WellKnownTypes.WolverineHttpFluentValidationExtensions && methodName == "UseFluentValidationProblemDetailMiddleware") - { - kind = WolverineValidationPolicyKind.FluentValidation; - scope = WolverineValidationPolicyScope.HttpEndpoints; - return true; - } - - if (containingType == WellKnownTypes.WolverineHttpOptions && methodName == "UseDataAnnotationsValidationProblemDetailMiddleware") + foreach (var activation in _validationActivations) { - kind = WolverineValidationPolicyKind.DataAnnotations; - scope = WolverineValidationPolicyScope.HttpEndpoints; - return true; + if (containingType == activation.ContainingType && methodName == activation.MethodName) + { + kind = activation.Kind; + scope = activation.Scope; + return true; + } } kind = default; @@ -427,45 +224,6 @@ ForStatementSyntax or ForEachStatementSyntax or WhileStatementSyntax or DoStatem return false; } - static GenerationDiagnostic UnresolvedValidation( - DotNetProjectCompilation project, - CritterStackSubjectResolver subjects, - ConfigurationCall call, - string reason) => new() - { - Code = WolverineDiagnosticCodes.ValidationConfigurationUnresolved, - Severity = GenerationDiagnosticSeverity.Warning, - Outcome = GenerationDiagnosticOutcome.Unknown, - Message = $"Wolverine validation call '{call.Method.Name}' was not applied because {reason}", - Source = CritterStackSource.RangeForProject(call.Invocation.GetLocation(), project), - Subject = ConfigurationSubject(project, subjects, call) - }; - - static GenerationDiagnostic UnresolvedAuthorization( - DotNetProjectCompilation project, - CritterStackSubjectResolver subjects, - ConfigurationCall call, - string reason) => new() - { - Code = WolverineDiagnosticCodes.AuthorizationConfigurationUnresolved, - Severity = GenerationDiagnosticSeverity.Warning, - Outcome = GenerationDiagnosticOutcome.Unknown, - Message = $"Wolverine authorization configuration call '{call.Method.Name}' was not applied because {reason}", - Source = CritterStackSource.RangeForProject(call.Invocation.GetLocation(), project), - Subject = ConfigurationSubject(project, subjects, call) - }; - - static SubjectId ConfigurationSubject( - DotNetProjectCompilation project, - CritterStackSubjectResolver subjects, - ConfigurationCall call) - { - var containingType = call.SemanticModel.GetEnclosingSymbol(call.Invocation.SpanStart)?.ContainingType; - return containingType is null - ? new SubjectId { Value = $"dotnet://{project.Name}/#wolverine-configuration" } - : subjects.SubjectForType(project, containingType); - } - static string ValidationName(WolverineValidationPolicyKind kind) => kind == WolverineValidationPolicyKind.FluentValidation ? "FluentValidation" : "DataAnnotations"; @@ -478,4 +236,69 @@ sealed record ConfigurationCall( InvocationExpressionSyntax Invocation, IMethodSymbol Method, SemanticModel SemanticModel); + + sealed class DiscoveryContext(DotNetProjectCompilation project, CritterStackSubjectResolver subjects) + { + public List ValidationPolicies { get; } = []; + + public List AuthorizationPolicies { get; } = []; + + public List Diagnostics { get; } = []; + + public SourceRange? SourceOf(ConfigurationCall call) => + CritterStackSource.RangeForProject(call.Invocation.GetLocation(), project); + + public SubjectId SubjectOf(ConfigurationCall call) + { + var containingType = call.SemanticModel.GetEnclosingSymbol(call.Invocation.SpanStart)?.ContainingType; + return containingType is null + ? new SubjectId { Value = $"dotnet://{project.Name}/#wolverine-configuration" } + : subjects.SubjectForType(project, containingType); + } + + public void ReportUnresolvedValidation(ConfigurationCall call, string reason) => + ReportUnresolved( + call, + WolverineDiagnosticCodes.ValidationConfigurationUnresolved, + $"Wolverine validation call '{call.Method.Name}' was not applied because {reason}"); + + public void ReportUnresolvedAuthorization(ConfigurationCall call, string reason) => + ReportUnresolved( + call, + WolverineDiagnosticCodes.AuthorizationConfigurationUnresolved, + $"Wolverine authorization configuration call '{call.Method.Name}' was not applied because {reason}"); + + public void AddActivationDiagnostics() + { + Diagnostics.AddRange(ValidationPolicies.Select(policy => new GenerationDiagnostic + { + Code = WolverineDiagnosticCodes.ValidationPolicyOmitted, + Severity = GenerationDiagnosticSeverity.Information, + Outcome = GenerationDiagnosticOutcome.Unsupported, + Message = $"Wolverine {ValidationName(policy.Kind)} {ScopeName(policy.Scope)} policy is enabled by '{policy.MethodName}', but current generation contracts cannot represent policy activation", + Source = policy.Source, + Subject = policy.Subject + })); + Diagnostics.AddRange(AuthorizationPolicies.Select(policy => new GenerationDiagnostic + { + Code = WolverineDiagnosticCodes.AuthorizationOmitted, + Severity = GenerationDiagnosticSeverity.Warning, + Outcome = GenerationDiagnosticOutcome.Unsupported, + Message = $"Global Wolverine HTTP authorization is enabled by {policy.Description}, but current generation contracts cannot represent authorization without overloading an unrelated relationship", + Source = policy.Source, + Subject = policy.Subject + })); + } + + void ReportUnresolved(ConfigurationCall call, string code, string message) => + Diagnostics.Add(new() + { + Code = code, + Severity = GenerationDiagnosticSeverity.Warning, + Outcome = GenerationDiagnosticOutcome.Unknown, + Message = message, + Source = SourceOf(call), + Subject = SubjectOf(call) + }); + } }