From 78b1a4724d4cbc554ff3bd59dffeb9d32c2bf1b7 Mon Sep 17 00:00:00 2001 From: atheate Date: Tue, 26 May 2026 13:43:12 +0200 Subject: [PATCH 1/2] Fix #190 --- CLAUDE.md | 32 ++++---- .../AcceptActionUsageExtensionsTestFixture.cs | 10 +-- ...nFeatureMembershipExtensionsTestFixture.cs | 71 +++++++++++++++--- .../TransitionUsageExtensionsTestFixture.cs | 75 +++++++++++++++---- .../Extend/FeatureMembershipExtensions.cs | 9 +-- SysML2.NET/Extend/FeatureValueExtensions.cs | 9 +-- .../Extend/OwningMembershipExtensions.cs | 4 +- .../Extend/ParameterMembershipExtensions.cs | 9 +-- .../Extend/SubjectMembershipExtensions.cs | 9 +-- .../TransitionFeatureMembershipExtensions.cs | 9 ++- SysML2.NET/Extensions/ElementExtensions.cs | 69 +++++++++++++++++ 11 files changed, 227 insertions(+), 79 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0023aa84..1fa453d0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -161,28 +161,26 @@ Auto-generated DTOs use structured namespaces reflecting the KerML/SysML package - Prefer C# property patterns ('x is IType { Prop: value }') over declared-variable-plus-predicate form ('x is IType name && name.Prop == value') when the narrowed variable is only consulted once; the property-pattern form is more concise and intent-revealing - Surround every braced block (`if`, `else if`, `while`, `for`, `foreach`, `switch`, `using`, `try`/`catch`/`finally`, `lock`, `do…while`, anonymous `{ }`) with a blank line on both sides — the rule does NOT apply at the very start/end of a method body, nor between a `}` and a continuation keyword (`else`, `catch`, `finally`, `while` of `do…while`) that belongs to the same control flow - When invoking an operation or derived property on a POCO from inside an extension method, call the POCO's instance member (e.g. `subject.IsDistinguishableFrom(other)`, `subject.qualifiedName`), NOT the static `ComputeXxxOperation` / `ComputeXxx` extension method. Virtual dispatch on the POCO honors operation/property REDEFINITION in subclass POCOs; calling the static extension directly bypasses dispatch and silently skips overrides. The static-extension form is reserved EXCLUSIVELY for the C# translation of OCL `self.oclAsType(SuperType).method()` — an explicit upcast that mandates targeting the SuperType's body (e.g. `Usage::namingFeature()` → `FeatureExtensions.ComputeNamingFeatureOperation(usage)`; `OwningMembership::path()` → `RelationshipExtensions.ComputeRedefinedPathOperation(owningMembership)`) -- **`IRelationship.OwnedRelatedElement` and `IElement.OwnedRelationship` storage collections are `[0..*]` — NEVER cardinality-limited.** The [1..1] / [0..1] multiplicities that appear in the metamodel apply to *derived* / *redefined* properties (e.g. `OwningMembership::ownedMemberElement`, `FeatureMembership::ownedMemberFeature`, `SubjectMembership::ownedSubjectParameter`), NOT to the underlying storage. When implementing such a derivation, **project from the collection — do not assume positional indexing**. The canonical "in-between" pattern is *filter-by-type-then-validate-count*: project with `OfType()`, then validate the projection count against the **derived property's declared multiplicity** (read it from the `[Property(lowerValue:…, upperValue:…)]` attribute on the generated POCO interface, or directly from the UML XMI). The failure mode depends on the multiplicity: +- **`IRelationship.OwnedRelatedElement` and `IElement.OwnedRelationship` storage collections are `[0..*]` — NEVER cardinality-limited.** The [1..1] / [0..1] multiplicities that appear in the metamodel apply to *derived* / *redefined* properties (e.g. `OwningMembership::ownedMemberElement`, `FeatureMembership::ownedMemberFeature`, `SubjectMembership::ownedSubjectParameter`), NOT to the underlying storage. When implementing such a derivation, **project from the collection — do not assume positional indexing**. For the common case of a `[1..1]` derived property, use the canonical shared helper `ElementExtensions.RequireSingleOfType(this IReadOnlyList, string)` from `SysML2.NET/Extensions/ElementExtensions.cs` — it does a zero-allocation index-based scan, early-exits on the second match, and throws `IncompleteModelException` with distinct "missing" vs "more than one" diagnostics: + + ```csharp + // [1..1] type-narrowed redefinition (e.g. SubjectMembership::ownedSubjectParameter : IUsage) + return subject.OwnedRelatedElement.RequireSingleOfType(nameof(subject)); + + // [1..1] non-narrowing redefinition (e.g. OwningMembership::ownedMemberElement : IElement) + return subject.OwnedRelatedElement.RequireSingleOfType(nameof(subject)); + ``` + + The helper signature is `internal static T RequireSingleOfType(this IReadOnlyList elements, string subjectName) where T : class, IElement`. Because `IReadOnlyList` is covariant, the same helper works on `IElement.OwnedRelationship` (whose element type is `IRelationship : IElement`) without an additional overload. + + The failure mode the helper produces matches the **derived property's declared multiplicity** as recorded in the `[Property(lowerValue:…, upperValue:…)]` attribute on the generated POCO interface (or in the UML XMI): | Multiplicity | Empty projection | Single-match projection | 2+ match projection | |---|---|---|---| | `[1..1]` (lowerValue=1, upperValue=1) | `throw IncompleteModelException` | return the match | `throw IncompleteModelException` | - | `[0..1]` (lowerValue=0, upperValue=1) | `return null` | return the match | `throw IncompleteModelException` | + | `[0..1]` (lowerValue=0, upperValue=1) | `return null` (do NOT use the helper — write inline) | return the match | `throw IncompleteModelException` (inline) | | `[0..*]` / `[1..*]` | (use `List` projection; not this pattern) | n/a | n/a | `IncompleteModelException` is the loud signal to SDK users that the model is malformed — DO NOT swallow it as `null` when the multiplicity is `[1..1]`, and DO NOT raise it for the empty case when the multiplicity is `[0..1]` (a legitimately-optional property). - ```csharp - // [1..1] type-narrowed redefinition (e.g. SubjectMembership::ownedSubjectParameter : IUsage) - var matches = subject.OwnedRelatedElement.OfType().ToList(); - - return matches.Count == 1 - ? matches[0] - : throw new IncompleteModelException($"{nameof(subject)} must have exactly one related element of type {nameof(ITargetType)}"); - - // [1..1] non-narrowing redefinition (e.g. OwningMembership::ownedMemberElement : IElement) - return subject.OwnedRelatedElement.Count == 1 - ? subject.OwnedRelatedElement[0] - : throw new IncompleteModelException($"{nameof(subject)} must have exactly one related element"); - ``` - - Do NOT use `.Count != 1 → throw` followed by `OwnedRelatedElement[0] as ITargetType` — that pattern silently drops the correctly-typed element when it does not sit at index 0 (e.g. when an `IAnnotation` target is also present, since `AssignOwnership` allows owned related elements for both `IOwningMembership` AND `IAnnotation`). Same rule applies to any other derived property that subsets one of these two `[0..*]` storage collections. + Do NOT use `.Count != 1 → throw` followed by `OwnedRelatedElement[0] as ITargetType` — that pattern (a) silently drops the correctly-typed element when it does not sit at index 0 (`AssignOwnership` allows owned related elements for both `IOwningMembership` AND `IAnnotation`, so a Membership can carry annotation targets alongside the member element), and (b) always allocates a `List` via `OfType().ToList()` even when the answer is decidable after the first two elements. diff --git a/SysML2.NET.Tests/Extend/AcceptActionUsageExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/AcceptActionUsageExtensionsTestFixture.cs index b617420b..d1bd75b7 100644 --- a/SysML2.NET.Tests/Extend/AcceptActionUsageExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/AcceptActionUsageExtensionsTestFixture.cs @@ -173,16 +173,16 @@ public void VerifyComputeIsTriggerActionOperation() Assert.That(acceptInTransitionC1.ComputeIsTriggerActionOperation(), Is.False); - // Branch C2 — owningType IS TransitionUsage, with Trigger TFM. - // The Where(Kind == Trigger) filter passes, then .transitionFeature is accessed, which dispatches - // through TransitionFeatureMembershipExtensions.ComputeTransitionFeature (still a stub). - // For Later: depends on TransitionFeatureMembershipExtensions.ComputeTransitionFeature at SysML2.NET/Extend/TransitionFeatureMembershipExtensions.cs:51, which is still a stub. + // Branch C2 — owningType IS TransitionUsage, with Trigger TFM whose transitionFeature IS this + // AcceptActionUsage → triggerAction contains self → true. + // Note: a useful negative companion would be a sibling AcceptActionUsage that is NOT wired as the + // transitionFeature of any Trigger TFM (e.g. owned via plain OwningMembership), asserting Is.False. var transitionOwnerC2 = new TransitionUsage(); var triggerTfm = new TransitionFeatureMembership { Kind = TransitionFeatureKind.Trigger }; var acceptInTransitionC2 = new AcceptActionUsage(); transitionOwnerC2.AssignOwnership(triggerTfm, acceptInTransitionC2); - Assert.That(() => acceptInTransitionC2.ComputeIsTriggerActionOperation(), Throws.TypeOf()); + Assert.That(acceptInTransitionC2.ComputeIsTriggerActionOperation(), Is.True); // Branch C3 — owningType IS TransitionUsage, only non-Trigger TFMs (Effect kind). // The Where(Kind == Trigger) filter excludes the Effect TFM → triggerAction returns empty list. diff --git a/SysML2.NET.Tests/Extend/TransitionFeatureMembershipExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/TransitionFeatureMembershipExtensionsTestFixture.cs index eff5ebde..ba4c2c07 100644 --- a/SysML2.NET.Tests/Extend/TransitionFeatureMembershipExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/TransitionFeatureMembershipExtensionsTestFixture.cs @@ -1,38 +1,89 @@ -// ------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- // -// +// // Copyright 2022-2026 Starion Group S.A. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// +// // // ------------------------------------------------------------------------------------------------ namespace SysML2.NET.Tests.Extend { using System; - + using NUnit.Framework; - + + using SysML2.NET.Core.POCO.Root.Elements; + using SysML2.NET.Core.POCO.Root.Namespaces; + using SysML2.NET.Core.POCO.Systems.Actions; using SysML2.NET.Core.POCO.Systems.States; + using SysML2.NET.Exceptions; + using SysML2.NET.Extensions; + + using Type = SysML2.NET.Core.POCO.Core.Types.Type; [TestFixture] public class TransitionFeatureMembershipExtensionsTestFixture { [Test] - public void ComputeTransitionFeature_ThrowsNotSupportedException() + public void VerifyComputeTransitionFeature() { - Assert.That(() => ((ITransitionFeatureMembership)null).ComputeTransitionFeature(), Throws.TypeOf()); + Assert.That(() => ((ITransitionFeatureMembership)null).ComputeTransitionFeature(), Throws.TypeOf()); + + // Empty OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. + var transitionFeatureMembership = new TransitionFeatureMembership(); + + Assert.That(() => transitionFeatureMembership.ComputeTransitionFeature(), Throws.TypeOf()); + + // Single AcceptActionUsage wired via the public API → returned. + var owningType = new Type(); + var acceptActionUsage = new AcceptActionUsage(); + + owningType.AssignOwnership(transitionFeatureMembership, acceptActionUsage); + + Assert.That(transitionFeatureMembership.ComputeTransitionFeature(), Is.SameAs(acceptActionUsage)); + + // Two AcceptActionUsage instances in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. + var twoStepMembership = new TransitionFeatureMembership(); + var firstStep = new AcceptActionUsage(); + var secondStep = new AcceptActionUsage(); + + ((IContainedRelationship)twoStepMembership).OwnedRelatedElement.Add(firstStep); + ((IContainedRelationship)twoStepMembership).OwnedRelatedElement.Add(secondStep); + + Assert.That(() => twoStepMembership.ComputeTransitionFeature(), Throws.TypeOf()); + + // Mixed-type owned related elements: exactly one IStep alongside a non-IStep (Namespace). + // The OfType() projection MUST pick out the IStep regardless of its position + // (this is the core robustness guarantee — never positionally index the unfiltered collection). + var mixedMembership = new TransitionFeatureMembership(); + var siblingNonStep = new Namespace(); + var mixedStep = new AcceptActionUsage(); + + ((IContainedRelationship)mixedMembership).OwnedRelatedElement.Add(siblingNonStep); + ((IContainedRelationship)mixedMembership).OwnedRelatedElement.Add(mixedStep); + + Assert.That(mixedMembership.ComputeTransitionFeature(), Is.SameAs(mixedStep)); + + // OwnedRelatedElement populated with non-IStep element(s) only → no IStep match: + // [1..1] violation, throws IncompleteModelException. + var nonStepMembership = new TransitionFeatureMembership(); + var nonStepElement = new Namespace(); + + ((IContainedRelationship)nonStepMembership).OwnedRelatedElement.Add(nonStepElement); + + Assert.That(() => nonStepMembership.ComputeTransitionFeature(), Throws.TypeOf()); } } } diff --git a/SysML2.NET.Tests/Extend/TransitionUsageExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/TransitionUsageExtensionsTestFixture.cs index c8789e81..491b6cee 100644 --- a/SysML2.NET.Tests/Extend/TransitionUsageExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/TransitionUsageExtensionsTestFixture.cs @@ -57,14 +57,29 @@ public void VerifyComputeEffectAction() Assert.That(transitionUsage.ComputeEffectAction(), Has.Count.EqualTo(0)); - // Effect-kind TFM wired with an ActionUsage as transitionFeature. - // For Later: populated path depends on TransitionFeatureMembershipExtensions.ComputeTransitionFeature - // at SysML2.NET/Extend/TransitionFeatureMembershipExtensions.cs:51, which is still a stub. + // Effect-kind TFM wired with an ActionUsage as transitionFeature → positive case. + // The Trigger TFM already wired above proves Kind-filter discrimination (Trigger excluded by Effect filter). var effectTfm = new TransitionFeatureMembership { Kind = TransitionFeatureKind.Effect }; var effectAction = new ActionUsage(); transitionUsage.AssignOwnership(effectTfm, effectAction); - Assert.That(() => transitionUsage.ComputeEffectAction(), Throws.TypeOf()); + Assert.That(transitionUsage.ComputeEffectAction(), Is.EqualTo([effectAction])); + + // Kind-filter discrimination: add a Guard TFM whose transitionFeature is an IActionUsage → excluded + // because its Kind is Guard, not Effect. + var guardTfmForEffectTest = new TransitionFeatureMembership { Kind = TransitionFeatureKind.Guard }; + var guardActionUsageStep = new ActionUsage(); + transitionUsage.AssignOwnership(guardTfmForEffectTest, guardActionUsageStep); + + Assert.That(transitionUsage.ComputeEffectAction(), Is.EqualTo([effectAction])); + + // Type-discrimination: a second Effect-kind TFM whose transitionFeature is NOT an IActionUsage + // (a LiteralBoolean / IExpression) → excluded by the trailing OfType(). + var effectTfmWrongType = new TransitionFeatureMembership { Kind = TransitionFeatureKind.Effect }; + var expressionStep = new LiteralBoolean(); + transitionUsage.AssignOwnership(effectTfmWrongType, expressionStep); + + Assert.That(transitionUsage.ComputeEffectAction(), Is.EqualTo([effectAction])); } [Test] @@ -84,14 +99,29 @@ public void VerifyComputeGuardExpression() Assert.That(transitionUsage.ComputeGuardExpression(), Has.Count.EqualTo(0)); - // Guard-kind TFM wired with a LiteralBoolean (IExpression) as transitionFeature. - // For Later: populated path depends on TransitionFeatureMembershipExtensions.ComputeTransitionFeature - // at SysML2.NET/Extend/TransitionFeatureMembershipExtensions.cs:51, which is still a stub. + // Guard-kind TFM wired with a LiteralBoolean (IExpression) as transitionFeature → positive case. + // The Trigger TFM already wired above proves Kind-filter discrimination (Trigger excluded by Guard filter). var guardTfm = new TransitionFeatureMembership { Kind = TransitionFeatureKind.Guard }; var guardExpression = new LiteralBoolean(); transitionUsage.AssignOwnership(guardTfm, guardExpression); - Assert.That(() => transitionUsage.ComputeGuardExpression(), Throws.TypeOf()); + Assert.That(transitionUsage.ComputeGuardExpression(), Is.EqualTo([guardExpression])); + + // Kind-filter discrimination: add an Effect TFM whose transitionFeature is an IExpression → excluded + // because its Kind is Effect, not Guard. + var effectTfmForGuardTest = new TransitionFeatureMembership { Kind = TransitionFeatureKind.Effect }; + var effectExpressionStep = new LiteralBoolean(); + transitionUsage.AssignOwnership(effectTfmForGuardTest, effectExpressionStep); + + Assert.That(transitionUsage.ComputeGuardExpression(), Is.EqualTo([guardExpression])); + + // Type-discrimination: a second Guard-kind TFM whose transitionFeature is NOT an IExpression + // (an ActionUsage) → excluded by the trailing OfType(). + var guardTfmWrongType = new TransitionFeatureMembership { Kind = TransitionFeatureKind.Guard }; + var actionUsageStep = new ActionUsage(); + transitionUsage.AssignOwnership(guardTfmWrongType, actionUsageStep); + + Assert.That(transitionUsage.ComputeGuardExpression(), Is.EqualTo([guardExpression])); } [Test] @@ -248,14 +278,29 @@ public void VerifyComputeTriggerAction() Assert.That(transitionUsage.ComputeTriggerAction(), Has.Count.EqualTo(0)); - // Trigger-kind TFM wired with an AcceptActionUsage as transitionFeature. - // For Later: populated path depends on TransitionFeatureMembershipExtensions.ComputeTransitionFeature - // at SysML2.NET/Extend/TransitionFeatureMembershipExtensions.cs:51, which is still a stub. + // Trigger-kind TFM wired with an AcceptActionUsage as transitionFeature → positive case. + // The Effect and Guard TFMs already wired above prove Kind-filter discrimination. var triggerTfm = new TransitionFeatureMembership { Kind = TransitionFeatureKind.Trigger }; var acceptAction = new AcceptActionUsage(); transitionUsage.AssignOwnership(triggerTfm, acceptAction); - Assert.That(() => transitionUsage.ComputeTriggerAction(), Throws.TypeOf()); + Assert.That(transitionUsage.ComputeTriggerAction(), Is.EqualTo([acceptAction])); + + // Kind-filter discrimination: add a Guard TFM whose transitionFeature is an AcceptActionUsage → excluded + // because its Kind is Guard, not Trigger. + var guardTfmForTriggerTest = new TransitionFeatureMembership { Kind = TransitionFeatureKind.Guard }; + var guardAcceptAction = new AcceptActionUsage(); + transitionUsage.AssignOwnership(guardTfmForTriggerTest, guardAcceptAction); + + Assert.That(transitionUsage.ComputeTriggerAction(), Is.EqualTo([acceptAction])); + + // Type-discrimination: a second Trigger-kind TFM whose transitionFeature is NOT an IAcceptActionUsage + // (a plain ActionUsage) → excluded by the trailing OfType(). + var triggerTfmWrongType = new TransitionFeatureMembership { Kind = TransitionFeatureKind.Trigger }; + var actionUsageStep = new ActionUsage(); + transitionUsage.AssignOwnership(triggerTfmWrongType, actionUsageStep); + + Assert.That(transitionUsage.ComputeTriggerAction(), Is.EqualTo([acceptAction])); } [Test] @@ -268,9 +313,9 @@ public void VerifyComputeTriggerPayloadParameterOperation() // No Trigger TFMs → triggerAction is empty → null. Assert.That(transitionUsage.ComputeTriggerPayloadParameterOperation(), Is.Null); - // Trigger TFM wired but access to transitionFeature hits ComputeTransitionFeature stub. - // For Later: populated path depends on TransitionFeatureMembershipExtensions.ComputeTransitionFeature - // at SysML2.NET/Extend/TransitionFeatureMembershipExtensions.cs:51, which is still a stub. + // Trigger TFM wired; triggerAction now resolves (ComputeTransitionFeature is implemented). + // The NotSupportedException is now thrown by AcceptActionUsage.payloadParameter → StepExtensions.ComputeParameter, + // which is still a stub. Expand this test when StepExtensions.ComputeParameter is implemented. var triggerTfm = new TransitionFeatureMembership { Kind = TransitionFeatureKind.Trigger }; var acceptAction = new AcceptActionUsage(); transitionUsage.AssignOwnership(triggerTfm, acceptAction); diff --git a/SysML2.NET/Extend/FeatureMembershipExtensions.cs b/SysML2.NET/Extend/FeatureMembershipExtensions.cs index fb0cc9e1..db9dd15f 100644 --- a/SysML2.NET/Extend/FeatureMembershipExtensions.cs +++ b/SysML2.NET/Extend/FeatureMembershipExtensions.cs @@ -22,14 +22,13 @@ namespace SysML2.NET.Core.POCO.Core.Types { using System; using System.Collections.Generic; - using System.Linq; using SysML2.NET.Core.Root.Namespaces; using SysML2.NET.Core.POCO.Core.Features; using SysML2.NET.Core.POCO.Root.Annotations; using SysML2.NET.Core.POCO.Root.Elements; using SysML2.NET.Core.POCO.Root.Namespaces; - using SysML2.NET.Exceptions; + using SysML2.NET.Extensions; /// /// The class provides extensions methods for @@ -53,11 +52,7 @@ internal static IFeature ComputeOwnedMemberFeature(this IFeatureMembership featu throw new ArgumentNullException(nameof(featureMembershipSubject)); } - var matches = featureMembershipSubject.OwnedRelatedElement.OfType().ToList(); - - return matches.Count == 1 - ? matches[0] - : throw new IncompleteModelException($"{nameof(featureMembershipSubject)} must have exactly one related element of type {nameof(IFeature)}"); + return featureMembershipSubject.OwnedRelatedElement.RequireSingleOfType(nameof(featureMembershipSubject)); } /// diff --git a/SysML2.NET/Extend/FeatureValueExtensions.cs b/SysML2.NET/Extend/FeatureValueExtensions.cs index 5c7d63c8..7c985f69 100644 --- a/SysML2.NET/Extend/FeatureValueExtensions.cs +++ b/SysML2.NET/Extend/FeatureValueExtensions.cs @@ -22,7 +22,6 @@ namespace SysML2.NET.Core.POCO.Kernel.FeatureValues { using System; using System.Collections.Generic; - using System.Linq; using SysML2.NET.Core.Root.Namespaces; using SysML2.NET.Core.POCO.Core.Features; @@ -30,7 +29,7 @@ namespace SysML2.NET.Core.POCO.Kernel.FeatureValues using SysML2.NET.Core.POCO.Root.Annotations; using SysML2.NET.Core.POCO.Root.Elements; using SysML2.NET.Core.POCO.Root.Namespaces; - using SysML2.NET.Exceptions; + using SysML2.NET.Extensions; /// /// The class provides extensions methods for @@ -70,11 +69,7 @@ internal static IExpression ComputeValue(this IFeatureValue featureValueSubject) throw new ArgumentNullException(nameof(featureValueSubject)); } - var matches = featureValueSubject.OwnedRelatedElement.OfType().ToList(); - - return matches.Count == 1 - ? matches[0] - : throw new IncompleteModelException($"{nameof(featureValueSubject)} must have exactly one related element of type {nameof(IExpression)}"); + return featureValueSubject.OwnedRelatedElement.RequireSingleOfType(nameof(featureValueSubject)); } } } diff --git a/SysML2.NET/Extend/OwningMembershipExtensions.cs b/SysML2.NET/Extend/OwningMembershipExtensions.cs index b28d15e7..b32409bd 100644 --- a/SysML2.NET/Extend/OwningMembershipExtensions.cs +++ b/SysML2.NET/Extend/OwningMembershipExtensions.cs @@ -26,7 +26,7 @@ namespace SysML2.NET.Core.POCO.Root.Namespaces using SysML2.NET.Core.Root.Namespaces; using SysML2.NET.Core.POCO.Root.Annotations; using SysML2.NET.Core.POCO.Root.Elements; - using SysML2.NET.Exceptions; + using SysML2.NET.Extensions; /// /// The class provides extensions methods for @@ -50,7 +50,7 @@ internal static IElement ComputeOwnedMemberElement(this IOwningMembership owning throw new ArgumentNullException(nameof(owningMembershipSubject)); } - return owningMembershipSubject.OwnedRelatedElement.Count != 1 ? throw new IncompleteModelException($"{nameof(owningMembershipSubject)} must have exactly one related element") : owningMembershipSubject.OwnedRelatedElement[0]; + return owningMembershipSubject.OwnedRelatedElement.RequireSingleOfType(nameof(owningMembershipSubject)); } /// diff --git a/SysML2.NET/Extend/ParameterMembershipExtensions.cs b/SysML2.NET/Extend/ParameterMembershipExtensions.cs index 0b0e6705..50fc7d96 100644 --- a/SysML2.NET/Extend/ParameterMembershipExtensions.cs +++ b/SysML2.NET/Extend/ParameterMembershipExtensions.cs @@ -22,7 +22,6 @@ namespace SysML2.NET.Core.POCO.Kernel.Behaviors { using System; using System.Collections.Generic; - using System.Linq; using SysML2.NET.Core.Core.Types; using SysML2.NET.Core.Root.Namespaces; @@ -31,7 +30,7 @@ namespace SysML2.NET.Core.POCO.Kernel.Behaviors using SysML2.NET.Core.POCO.Root.Annotations; using SysML2.NET.Core.POCO.Root.Elements; using SysML2.NET.Core.POCO.Root.Namespaces; - using SysML2.NET.Exceptions; + using SysML2.NET.Extensions; /// /// The class provides extensions methods for @@ -55,11 +54,7 @@ internal static IFeature ComputeOwnedMemberParameter(this IParameterMembership p throw new ArgumentNullException(nameof(parameterMembershipSubject)); } - var matches = parameterMembershipSubject.OwnedRelatedElement.OfType().ToList(); - - return matches.Count == 1 - ? matches[0] - : throw new IncompleteModelException($"{nameof(parameterMembershipSubject)} must have exactly one related element of type {nameof(IFeature)}"); + return parameterMembershipSubject.OwnedRelatedElement.RequireSingleOfType(nameof(parameterMembershipSubject)); } /// diff --git a/SysML2.NET/Extend/SubjectMembershipExtensions.cs b/SysML2.NET/Extend/SubjectMembershipExtensions.cs index 8b10bec8..49b9e4aa 100644 --- a/SysML2.NET/Extend/SubjectMembershipExtensions.cs +++ b/SysML2.NET/Extend/SubjectMembershipExtensions.cs @@ -22,10 +22,9 @@ namespace SysML2.NET.Core.POCO.Systems.Requirements { using System; using System.Collections.Generic; - using System.Linq; using SysML2.NET.Core.POCO.Systems.DefinitionAndUsage; - using SysML2.NET.Exceptions; + using SysML2.NET.Extensions; /// /// The class provides extensions methods for @@ -49,11 +48,7 @@ internal static IUsage ComputeOwnedSubjectParameter(this ISubjectMembership subj throw new ArgumentNullException(nameof(subjectMembershipSubject)); } - var matches = subjectMembershipSubject.OwnedRelatedElement.OfType().ToList(); - - return matches.Count == 1 - ? matches[0] - : throw new IncompleteModelException($"{nameof(subjectMembershipSubject)} must have exactly one related element of type {nameof(IUsage)}"); + return subjectMembershipSubject.OwnedRelatedElement.RequireSingleOfType(nameof(subjectMembershipSubject)); } } diff --git a/SysML2.NET/Extend/TransitionFeatureMembershipExtensions.cs b/SysML2.NET/Extend/TransitionFeatureMembershipExtensions.cs index e16c8e7a..70f91b0b 100644 --- a/SysML2.NET/Extend/TransitionFeatureMembershipExtensions.cs +++ b/SysML2.NET/Extend/TransitionFeatureMembershipExtensions.cs @@ -31,6 +31,7 @@ namespace SysML2.NET.Core.POCO.Systems.States using SysML2.NET.Core.POCO.Root.Annotations; using SysML2.NET.Core.POCO.Root.Elements; using SysML2.NET.Core.POCO.Root.Namespaces; + using SysML2.NET.Extensions; /// /// The class provides extensions methods for @@ -47,10 +48,14 @@ internal static class TransitionFeatureMembershipExtensions /// /// the computed result /// - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static IStep ComputeTransitionFeature(this ITransitionFeatureMembership transitionFeatureMembershipSubject) { - throw new NotSupportedException("Create a GitHub issue when this method is required"); + if (transitionFeatureMembershipSubject == null) + { + throw new ArgumentNullException(nameof(transitionFeatureMembershipSubject)); + } + + return transitionFeatureMembershipSubject.OwnedRelatedElement.RequireSingleOfType(nameof(transitionFeatureMembershipSubject)); } } diff --git a/SysML2.NET/Extensions/ElementExtensions.cs b/SysML2.NET/Extensions/ElementExtensions.cs index fb90cfaa..28879ca9 100644 --- a/SysML2.NET/Extensions/ElementExtensions.cs +++ b/SysML2.NET/Extensions/ElementExtensions.cs @@ -21,11 +21,13 @@ namespace SysML2.NET.Extensions { using System; + using System.Collections.Generic; using System.Linq; using SysML2.NET.Core.POCO.Root.Annotations; using SysML2.NET.Core.POCO.Root.Elements; using SysML2.NET.Core.POCO.Root.Namespaces; + using SysML2.NET.Exceptions; /// /// Extension method for the interface @@ -220,6 +222,73 @@ private static void AssignOwnershipCore(IElement source, IRelationship bridgeRel } } + /// + /// Returns the single element of type from . + /// + /// + /// The narrowed element type the caller is interested in (e.g. IUsage, IFeature, + /// IStep, IExpression). Pass for non-narrowing + /// single-element extraction (e.g. OwningMembership::ownedMemberElement). + /// + /// + /// The source [0..*] storage collection — typically + /// or + /// (assignable thanks to covariance). + /// + /// + /// The name of the subject parameter in the calling method (typically nameof(...)), + /// embedded in the diagnostic message produced on a multiplicity violation. + /// + /// The single element of type + /// + /// Thrown when no element of type is present (missing case), or when + /// more than one is present (multiplicity violation). The exception message distinguishes the + /// two cases so SDK consumers can act on the precise model defect. + /// + /// + /// + /// Canonical helper for derived [1..1] composite properties that subset a [0..*] + /// storage collection (e.g. SubjectMembership::ownedSubjectParameter, + /// FeatureMembership::ownedMemberFeature, + /// ParameterMembership::ownedMemberParameter, + /// TransitionFeatureMembership::transitionFeature, + /// FeatureValue::value, + /// OwningMembership::ownedMemberElement with = ). + /// + /// + /// The implementation is zero-allocation (index-based iteration over the + /// ; no LINQ materialisation, no intermediate list) and + /// early-exits on the second match. The storage collection is structurally [0..*]; + /// the [1..1] invariant lives on the derived property and is enforced here at the + /// point of access. + /// + /// + internal static T RequireSingleOfType(this IReadOnlyList elements, string subjectName) + where T : class, IElement + { + T found = null; + + for (var index = 0; index < elements.Count; index++) + { + if (elements[index] is not T match) + { + continue; + } + + if (found is not null) + { + throw new IncompleteModelException( + $"{subjectName} contains more than one element of type {typeof(T).Name}"); + } + + found = match; + } + + return found + ?? throw new IncompleteModelException( + $"{subjectName} must have an element of type {typeof(T).Name}"); + } + /// /// Determines whether is currently — directly or transitively — contained by /// via the chain of and From 769aee0671666f3b5a64d7768b25925043b75494 Mon Sep 17 00:00:00 2001 From: atheate Date: Tue, 26 May 2026 15:01:39 +0200 Subject: [PATCH 2/2] trigger action --- SysML2.NET/Extend/TransitionFeatureMembershipExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/SysML2.NET/Extend/TransitionFeatureMembershipExtensions.cs b/SysML2.NET/Extend/TransitionFeatureMembershipExtensions.cs index 70f91b0b..5cf7c71d 100644 --- a/SysML2.NET/Extend/TransitionFeatureMembershipExtensions.cs +++ b/SysML2.NET/Extend/TransitionFeatureMembershipExtensions.cs @@ -57,6 +57,5 @@ internal static IStep ComputeTransitionFeature(this ITransitionFeatureMembership return transitionFeatureMembershipSubject.OwnedRelatedElement.RequireSingleOfType(nameof(transitionFeatureMembershipSubject)); } - } }