diff --git a/CLAUDE.md b/CLAUDE.md
index 2bb52015..72bcc74e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -29,6 +29,8 @@ dotnet-coverage collect "dotnet test SysML2.NET.sln --no-build" -f xml -o covera
Test framework: **NUnit**. Test classes use `[TestFixture]` and `[Test]` attributes.
+**When writing or modifying unit tests** in any `*.Tests/` project: read `TESTING.md` at the repo root for the NUnit conventions (one `[Test]` per method-under-test, `Assert.That` everywhere, `Assert.EnterMultipleScope` only for consecutive asserts, mandatory positive + negative coverage, assertion idiom preferences, `Verify{MethodUnderTest}` naming).
+
## Architecture
### Code Generation
@@ -155,3 +157,4 @@ Auto-generated DTOs use structured namespaces reflecting the KerML/SysML package
- Use meaningful variable names instead of single-letter names in any context (e.g., 'charIndex' instead of 'i', 'currentChar' instead of 'c', 'element' instead of 'e')
- Use 'NotSupportedException' (not 'NotImplementedException') for placeholder/stub methods that require manual implementation
- 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
diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/UML/Core/AutoGenExtensions/ElementExtensions.cs b/SysML2.NET.CodeGenerator.Tests/Expected/UML/Core/AutoGenExtensions/ElementExtensions.cs
new file mode 100644
index 00000000..45bb0e93
--- /dev/null
+++ b/SysML2.NET.CodeGenerator.Tests/Expected/UML/Core/AutoGenExtensions/ElementExtensions.cs
@@ -0,0 +1,219 @@
+// -------------------------------------------------------------------------------------------------
+//
+//
+// 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.
+//
+//
+// ------------------------------------------------------------------------------------------------
+
+// ------------------------------------------------------------------------------------------------
+// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!--------
+// ------------------------------------------------------------------------------------------------
+
+namespace SysML2.NET.Extensions
+{
+ using SysML2.NET.Core.POCO.Root.Elements;
+
+ ///
+ /// Auto-generated companion to the hand-coded partial class.
+ ///
+ public static partial class ElementExtensions
+ {
+ ///
+ /// Determines whether is a structurally valid element to be added as an
+ /// of , based on
+ /// the typed composite property declared by the bridge's metaclass in the KerML/SysML2 metamodel.
+ ///
+ /// The bridge .
+ /// The candidate .
+ /// true if 's runtime type satisfies the constraint;
+ /// otherwise false.
+ public static bool QueryIsValidForContainment(this IRelationship bridgeRelationship, IElement target)
+ {
+ return bridgeRelationship switch
+ {
+ SysML2.NET.Core.POCO.Systems.Requirements.IActorMembership => target is SysML2.NET.Core.POCO.Systems.Parts.IPartUsage,
+ SysML2.NET.Core.POCO.Systems.Requirements.IFramedConcernMembership => target is SysML2.NET.Core.POCO.Systems.Requirements.IConcernUsage,
+ SysML2.NET.Core.POCO.Systems.VerificationCases.IRequirementVerificationMembership => target is SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage,
+ SysML2.NET.Core.POCO.Systems.Requirements.IStakeholderMembership => target is SysML2.NET.Core.POCO.Systems.Parts.IPartUsage,
+ SysML2.NET.Core.POCO.Systems.Requirements.ISubjectMembership => target is SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage,
+ SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership => target is SysML2.NET.Core.POCO.Core.Features.IFeature,
+ SysML2.NET.Core.POCO.Systems.Cases.IObjectiveMembership => target is SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage,
+ SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership => target is SysML2.NET.Core.POCO.Core.Features.IFeature,
+ SysML2.NET.Core.POCO.Systems.Requirements.IRequirementConstraintMembership => target is SysML2.NET.Core.POCO.Systems.Constraints.IConstraintUsage,
+ SysML2.NET.Core.POCO.Kernel.Functions.IResultExpressionMembership => target is SysML2.NET.Core.POCO.Kernel.Functions.IExpression,
+ SysML2.NET.Core.POCO.Systems.States.IStateSubactionMembership => target is SysML2.NET.Core.POCO.Systems.Actions.IActionUsage,
+ SysML2.NET.Core.POCO.Systems.States.ITransitionFeatureMembership => target is SysML2.NET.Core.POCO.Kernel.Behaviors.IStep,
+ SysML2.NET.Core.POCO.Systems.Views.IViewRenderingMembership => target is SysML2.NET.Core.POCO.Systems.Views.IRenderingUsage,
+ SysML2.NET.Core.POCO.Kernel.Packages.IElementFilterMembership => target is SysML2.NET.Core.POCO.Kernel.Functions.IExpression,
+ SysML2.NET.Core.POCO.Core.Types.IFeatureMembership => target is SysML2.NET.Core.POCO.Core.Features.IFeature,
+ SysML2.NET.Core.POCO.Kernel.FeatureValues.IFeatureValue => target is SysML2.NET.Core.POCO.Kernel.Functions.IExpression,
+ SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IVariantMembership => target is SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage,
+ SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership => target is not null,
+ SysML2.NET.Core.POCO.Root.Annotations.IAnnotation => target is SysML2.NET.Core.POCO.Root.Annotations.IAnnotatingElement,
+ SysML2.NET.Core.POCO.Root.Elements.IRelationship => target is not null,
+ _ => true,
+ };
+ }
+
+ ///
+ /// Returns the simple C# interface name of the expected target type for the supplied
+ /// , used to compose the error message thrown by
+ /// AssignOwnership when the target is not a valid containment target.
+ ///
+ /// The bridge .
+ /// The expected target type's simple interface name.
+ public static string ExpectedContainmentTypeName(this IRelationship bridgeRelationship)
+ {
+ return bridgeRelationship switch
+ {
+ SysML2.NET.Core.POCO.Systems.Requirements.IActorMembership => nameof(SysML2.NET.Core.POCO.Systems.Parts.IPartUsage),
+ SysML2.NET.Core.POCO.Systems.Requirements.IFramedConcernMembership => nameof(SysML2.NET.Core.POCO.Systems.Requirements.IConcernUsage),
+ SysML2.NET.Core.POCO.Systems.VerificationCases.IRequirementVerificationMembership => nameof(SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage),
+ SysML2.NET.Core.POCO.Systems.Requirements.IStakeholderMembership => nameof(SysML2.NET.Core.POCO.Systems.Parts.IPartUsage),
+ SysML2.NET.Core.POCO.Systems.Requirements.ISubjectMembership => nameof(SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage),
+ SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature),
+ SysML2.NET.Core.POCO.Systems.Cases.IObjectiveMembership => nameof(SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage),
+ SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature),
+ SysML2.NET.Core.POCO.Systems.Requirements.IRequirementConstraintMembership => nameof(SysML2.NET.Core.POCO.Systems.Constraints.IConstraintUsage),
+ SysML2.NET.Core.POCO.Kernel.Functions.IResultExpressionMembership => nameof(SysML2.NET.Core.POCO.Kernel.Functions.IExpression),
+ SysML2.NET.Core.POCO.Systems.States.IStateSubactionMembership => nameof(SysML2.NET.Core.POCO.Systems.Actions.IActionUsage),
+ SysML2.NET.Core.POCO.Systems.States.ITransitionFeatureMembership => nameof(SysML2.NET.Core.POCO.Kernel.Behaviors.IStep),
+ SysML2.NET.Core.POCO.Systems.Views.IViewRenderingMembership => nameof(SysML2.NET.Core.POCO.Systems.Views.IRenderingUsage),
+ SysML2.NET.Core.POCO.Kernel.Packages.IElementFilterMembership => nameof(SysML2.NET.Core.POCO.Kernel.Functions.IExpression),
+ SysML2.NET.Core.POCO.Core.Types.IFeatureMembership => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature),
+ SysML2.NET.Core.POCO.Kernel.FeatureValues.IFeatureValue => nameof(SysML2.NET.Core.POCO.Kernel.Functions.IExpression),
+ SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IVariantMembership => nameof(SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage),
+ SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership => nameof(SysML2.NET.Core.POCO.Root.Elements.IElement),
+ SysML2.NET.Core.POCO.Root.Annotations.IAnnotation => nameof(SysML2.NET.Core.POCO.Root.Annotations.IAnnotatingElement),
+ SysML2.NET.Core.POCO.Root.Elements.IRelationship => nameof(SysML2.NET.Core.POCO.Root.Elements.IElement),
+ _ => nameof(IElement),
+ };
+ }
+
+ ///
+ /// Determines whether is a structurally valid containment owner for
+ /// , based on the typed property declared by the bridge's
+ /// metaclass that subsets in the KerML/SysML2
+ /// metamodel (e.g. an IFeatureMembership requires an IType owner).
+ ///
+ /// The bridge .
+ /// The candidate owner .
+ /// true if 's runtime type satisfies the owner-side
+ /// constraint; otherwise false.
+ public static bool QueryIsValidContainmentOwner(this IRelationship bridgeRelationship, IElement source)
+ {
+ return bridgeRelationship switch
+ {
+ SysML2.NET.Core.POCO.Systems.Requirements.IActorMembership => source is SysML2.NET.Core.POCO.Systems.Parts.IPartUsage,
+ SysML2.NET.Core.POCO.Systems.Requirements.IFramedConcernMembership => source is SysML2.NET.Core.POCO.Systems.Requirements.IConcernUsage,
+ SysML2.NET.Core.POCO.Systems.VerificationCases.IRequirementVerificationMembership => source is SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage,
+ SysML2.NET.Core.POCO.Systems.Requirements.IStakeholderMembership => source is SysML2.NET.Core.POCO.Systems.Parts.IPartUsage,
+ SysML2.NET.Core.POCO.Systems.Requirements.ISubjectMembership => source is SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage,
+ SysML2.NET.Core.POCO.Kernel.Associations.IAssociation => source is SysML2.NET.Core.POCO.Core.Types.IType,
+ SysML2.NET.Core.POCO.Kernel.Connectors.IConnector => source is SysML2.NET.Core.POCO.Core.Features.IFeature,
+ SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership => source is SysML2.NET.Core.POCO.Core.Features.IFeature,
+ SysML2.NET.Core.POCO.Systems.Cases.IObjectiveMembership => source is SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage,
+ SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership => source is SysML2.NET.Core.POCO.Core.Features.IFeature,
+ SysML2.NET.Core.POCO.Systems.Requirements.IRequirementConstraintMembership => source is SysML2.NET.Core.POCO.Systems.Constraints.IConstraintUsage,
+ SysML2.NET.Core.POCO.Kernel.Functions.IResultExpressionMembership => source is SysML2.NET.Core.POCO.Kernel.Functions.IExpression,
+ SysML2.NET.Core.POCO.Systems.States.IStateSubactionMembership => source is SysML2.NET.Core.POCO.Systems.Actions.IActionUsage,
+ SysML2.NET.Core.POCO.Systems.States.ITransitionFeatureMembership => source is SysML2.NET.Core.POCO.Kernel.Behaviors.IStep,
+ SysML2.NET.Core.POCO.Systems.Views.IViewRenderingMembership => source is SysML2.NET.Core.POCO.Systems.Views.IRenderingUsage,
+ SysML2.NET.Core.POCO.Systems.Ports.IConjugatedPortTyping => source is SysML2.NET.Core.POCO.Systems.Ports.IConjugatedPortDefinition,
+ SysML2.NET.Core.POCO.Core.Features.ICrossSubsetting => source is SysML2.NET.Core.POCO.Core.Features.IFeature,
+ SysML2.NET.Core.POCO.Kernel.Packages.IElementFilterMembership => source is SysML2.NET.Core.POCO.Kernel.Functions.IExpression,
+ SysML2.NET.Core.POCO.Core.Types.IFeatureMembership => source is SysML2.NET.Core.POCO.Core.Features.IFeature,
+ SysML2.NET.Core.POCO.Kernel.FeatureValues.IFeatureValue => source is SysML2.NET.Core.POCO.Kernel.Functions.IExpression,
+ SysML2.NET.Core.POCO.Core.Features.IRedefinition => source is SysML2.NET.Core.POCO.Core.Features.IFeature,
+ SysML2.NET.Core.POCO.Core.Features.IReferenceSubsetting => source is SysML2.NET.Core.POCO.Core.Features.IFeature,
+ SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IVariantMembership => source is SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage,
+ SysML2.NET.Core.POCO.Core.Features.IFeatureTyping => source is SysML2.NET.Core.POCO.Core.Types.IType,
+ SysML2.NET.Core.POCO.Root.Namespaces.IMembershipImport => source is SysML2.NET.Core.POCO.Root.Namespaces.IMembership,
+ SysML2.NET.Core.POCO.Root.Namespaces.INamespaceImport => source is SysML2.NET.Core.POCO.Root.Namespaces.INamespace,
+ SysML2.NET.Core.POCO.Systems.Ports.IPortConjugation => source is SysML2.NET.Core.POCO.Systems.Ports.IPortDefinition,
+ SysML2.NET.Core.POCO.Core.Classifiers.ISubclassification => source is SysML2.NET.Core.POCO.Core.Classifiers.IClassifier,
+ SysML2.NET.Core.POCO.Core.Features.ISubsetting => source is SysML2.NET.Core.POCO.Core.Features.IFeature,
+ SysML2.NET.Core.POCO.Core.Types.IConjugation => source is SysML2.NET.Core.POCO.Core.Types.IType,
+ SysML2.NET.Core.POCO.Core.Types.IDifferencing => source is SysML2.NET.Core.POCO.Core.Types.IType,
+ SysML2.NET.Core.POCO.Core.Types.IDisjoining => source is SysML2.NET.Core.POCO.Core.Types.IType,
+ SysML2.NET.Core.POCO.Core.Features.IFeatureChaining => source is SysML2.NET.Core.POCO.Core.Features.IFeature,
+ SysML2.NET.Core.POCO.Core.Features.IFeatureInverting => source is SysML2.NET.Core.POCO.Core.Features.IFeature,
+ SysML2.NET.Core.POCO.Core.Types.IIntersecting => source is SysML2.NET.Core.POCO.Core.Types.IType,
+ SysML2.NET.Core.POCO.Core.Types.ISpecialization => source is SysML2.NET.Core.POCO.Core.Types.IType,
+ SysML2.NET.Core.POCO.Core.Features.ITypeFeaturing => source is SysML2.NET.Core.POCO.Core.Types.IType,
+ SysML2.NET.Core.POCO.Core.Types.IUnioning => source is SysML2.NET.Core.POCO.Core.Types.IType,
+ _ => true,
+ };
+ }
+
+ ///
+ /// Returns the simple C# interface name of the expected owner type for the supplied
+ /// , used to compose the error message thrown by
+ /// AssignOwnership when the source is not a valid containment owner.
+ ///
+ /// The bridge .
+ /// The expected owner type's simple interface name.
+ public static string ExpectedContainmentOwnerTypeName(this IRelationship bridgeRelationship)
+ {
+ return bridgeRelationship switch
+ {
+ SysML2.NET.Core.POCO.Systems.Requirements.IActorMembership => nameof(SysML2.NET.Core.POCO.Systems.Parts.IPartUsage),
+ SysML2.NET.Core.POCO.Systems.Requirements.IFramedConcernMembership => nameof(SysML2.NET.Core.POCO.Systems.Requirements.IConcernUsage),
+ SysML2.NET.Core.POCO.Systems.VerificationCases.IRequirementVerificationMembership => nameof(SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage),
+ SysML2.NET.Core.POCO.Systems.Requirements.IStakeholderMembership => nameof(SysML2.NET.Core.POCO.Systems.Parts.IPartUsage),
+ SysML2.NET.Core.POCO.Systems.Requirements.ISubjectMembership => nameof(SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage),
+ SysML2.NET.Core.POCO.Kernel.Associations.IAssociation => nameof(SysML2.NET.Core.POCO.Core.Types.IType),
+ SysML2.NET.Core.POCO.Kernel.Connectors.IConnector => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature),
+ SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature),
+ SysML2.NET.Core.POCO.Systems.Cases.IObjectiveMembership => nameof(SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage),
+ SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature),
+ SysML2.NET.Core.POCO.Systems.Requirements.IRequirementConstraintMembership => nameof(SysML2.NET.Core.POCO.Systems.Constraints.IConstraintUsage),
+ SysML2.NET.Core.POCO.Kernel.Functions.IResultExpressionMembership => nameof(SysML2.NET.Core.POCO.Kernel.Functions.IExpression),
+ SysML2.NET.Core.POCO.Systems.States.IStateSubactionMembership => nameof(SysML2.NET.Core.POCO.Systems.Actions.IActionUsage),
+ SysML2.NET.Core.POCO.Systems.States.ITransitionFeatureMembership => nameof(SysML2.NET.Core.POCO.Kernel.Behaviors.IStep),
+ SysML2.NET.Core.POCO.Systems.Views.IViewRenderingMembership => nameof(SysML2.NET.Core.POCO.Systems.Views.IRenderingUsage),
+ SysML2.NET.Core.POCO.Systems.Ports.IConjugatedPortTyping => nameof(SysML2.NET.Core.POCO.Systems.Ports.IConjugatedPortDefinition),
+ SysML2.NET.Core.POCO.Core.Features.ICrossSubsetting => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature),
+ SysML2.NET.Core.POCO.Kernel.Packages.IElementFilterMembership => nameof(SysML2.NET.Core.POCO.Kernel.Functions.IExpression),
+ SysML2.NET.Core.POCO.Core.Types.IFeatureMembership => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature),
+ SysML2.NET.Core.POCO.Kernel.FeatureValues.IFeatureValue => nameof(SysML2.NET.Core.POCO.Kernel.Functions.IExpression),
+ SysML2.NET.Core.POCO.Core.Features.IRedefinition => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature),
+ SysML2.NET.Core.POCO.Core.Features.IReferenceSubsetting => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature),
+ SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IVariantMembership => nameof(SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage),
+ SysML2.NET.Core.POCO.Core.Features.IFeatureTyping => nameof(SysML2.NET.Core.POCO.Core.Types.IType),
+ SysML2.NET.Core.POCO.Root.Namespaces.IMembershipImport => nameof(SysML2.NET.Core.POCO.Root.Namespaces.IMembership),
+ SysML2.NET.Core.POCO.Root.Namespaces.INamespaceImport => nameof(SysML2.NET.Core.POCO.Root.Namespaces.INamespace),
+ SysML2.NET.Core.POCO.Systems.Ports.IPortConjugation => nameof(SysML2.NET.Core.POCO.Systems.Ports.IPortDefinition),
+ SysML2.NET.Core.POCO.Core.Classifiers.ISubclassification => nameof(SysML2.NET.Core.POCO.Core.Classifiers.IClassifier),
+ SysML2.NET.Core.POCO.Core.Features.ISubsetting => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature),
+ SysML2.NET.Core.POCO.Core.Types.IConjugation => nameof(SysML2.NET.Core.POCO.Core.Types.IType),
+ SysML2.NET.Core.POCO.Core.Types.IDifferencing => nameof(SysML2.NET.Core.POCO.Core.Types.IType),
+ SysML2.NET.Core.POCO.Core.Types.IDisjoining => nameof(SysML2.NET.Core.POCO.Core.Types.IType),
+ SysML2.NET.Core.POCO.Core.Features.IFeatureChaining => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature),
+ SysML2.NET.Core.POCO.Core.Features.IFeatureInverting => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature),
+ SysML2.NET.Core.POCO.Core.Types.IIntersecting => nameof(SysML2.NET.Core.POCO.Core.Types.IType),
+ SysML2.NET.Core.POCO.Core.Types.ISpecialization => nameof(SysML2.NET.Core.POCO.Core.Types.IType),
+ SysML2.NET.Core.POCO.Core.Features.ITypeFeaturing => nameof(SysML2.NET.Core.POCO.Core.Types.IType),
+ SysML2.NET.Core.POCO.Core.Types.IUnioning => nameof(SysML2.NET.Core.POCO.Core.Types.IType),
+ _ => nameof(IElement),
+ };
+ }
+ }
+}
+
+// ------------------------------------------------------------------------------------------------
+// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!--------
+// ------------------------------------------------------------------------------------------------
diff --git a/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/UmlCorePocoValidationGeneratorTestFixture.cs b/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/UmlCorePocoValidationGeneratorTestFixture.cs
new file mode 100644
index 00000000..6f94d6f8
--- /dev/null
+++ b/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/UmlCorePocoValidationGeneratorTestFixture.cs
@@ -0,0 +1,69 @@
+// -------------------------------------------------------------------------------------------------
+//
+//
+// 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.CodeGenerator.Tests.Generators.UmlHandleBarsGenerators
+{
+ using System.IO;
+ using System.Threading.Tasks;
+
+ using NUnit.Framework;
+
+ using SysML2.NET.CodeGenerator.Generators.UmlHandleBarsGenerators;
+
+ [TestFixture]
+ public class UmlCorePocoValidationGeneratorTestFixture
+ {
+ private DirectoryInfo outputDirectory;
+ private UmlCorePocoValidationGenerator generator;
+
+ [OneTimeSetUp]
+ public void OneTimeSetup()
+ {
+ var directoryInfo = new DirectoryInfo(TestContext.CurrentContext.TestDirectory);
+
+ var path = Path.Combine("UML", "_SysML2.NET.AutoGenExtensions.ElementExtensions");
+
+ this.outputDirectory = directoryInfo.CreateSubdirectory(path);
+ this.generator = new UmlCorePocoValidationGenerator();
+ }
+
+ [Test]
+ public async Task VerifyElementExtensionsAreGenerated()
+ {
+ await Assert.ThatAsync(
+ () => this.generator.GenerateAsync(GeneratorSetupFixture.XmiReaderResult, this.outputDirectory),
+ Throws.Nothing);
+ }
+
+ [Test]
+ [Category("Expected")]
+ public async Task VerifyExpectedElementExtensionsMatches()
+ {
+ var generatedCode = await this.generator.GenerateElementExtensions(
+ GeneratorSetupFixture.XmiReaderResult,
+ this.outputDirectory);
+
+ var expected = await File.ReadAllTextAsync(Path.Combine(TestContext.CurrentContext.TestDirectory,
+ "Expected/UML/Core/AutoGenExtensions/ElementExtensions.cs"));
+
+ Assert.That(generatedCode, Is.EqualTo(expected));
+ }
+ }
+}
diff --git a/SysML2.NET.CodeGenerator.Tests/SysML2.NET.CodeGenerator.Tests.csproj b/SysML2.NET.CodeGenerator.Tests/SysML2.NET.CodeGenerator.Tests.csproj
index bc72a662..a0cee75b 100644
--- a/SysML2.NET.CodeGenerator.Tests/SysML2.NET.CodeGenerator.Tests.csproj
+++ b/SysML2.NET.CodeGenerator.Tests/SysML2.NET.CodeGenerator.Tests.csproj
@@ -19,7 +19,11 @@
+
+
+ Always
+
Always
diff --git a/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/UmlCorePocoValidationGenerator.cs b/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/UmlCorePocoValidationGenerator.cs
new file mode 100644
index 00000000..7fc468d4
--- /dev/null
+++ b/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/UmlCorePocoValidationGenerator.cs
@@ -0,0 +1,538 @@
+// -------------------------------------------------------------------------------------------------
+//
+//
+// 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.CodeGenerator.Generators.UmlHandleBarsGenerators
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Threading.Tasks;
+
+ using SysML2.NET.CodeGenerator.Extensions;
+
+ using uml4net.Classification;
+ using uml4net.HandleBars;
+ using uml4net.StructuredClassifiers;
+ using uml4net.xmi.Readers;
+
+ using ClassHelper = SysML2.NET.CodeGenerator.HandleBarHelpers.ClassHelper;
+ using NamedElementHelper = SysML2.NET.CodeGenerator.HandleBarHelpers.NamedElementHelper;
+ using PropertyHelper = SysML2.NET.CodeGenerator.HandleBarHelpers.PropertyHelper;
+
+ ///
+ /// The generates the auto-generated companion to the
+ /// hand-coded SysML2.NET.Extensions.ElementExtensions partial class. It emits a switch expression
+ /// (QueryIsValidForContainment) and a parallel name lookup (ExpectedContainmentTypeName)
+ /// that map each Relationship metaclass to the runtime-type check for the element it may carry as an
+ /// OwnedRelatedElement, derived from every typed composite property that subsets
+ /// IRelationship.OwnedRelatedElement.
+ ///
+ public class UmlCorePocoValidationGenerator : UmlHandleBarsGenerator
+ {
+ ///
+ /// The name of the Handlebars template that emits the partial-class file.
+ ///
+ private const string CorePocoValidationTemplateName = "core-poco-validation-template";
+
+ ///
+ /// The name of the file to write into the output directory.
+ ///
+ private const string OutputFileName = "ElementExtensions.cs";
+
+ ///
+ /// Generates the file in the supplied .
+ ///
+ /// The with the loaded UML model.
+ /// The target directory.
+ /// An awaitable .
+ /// If either argument is null.
+ public override async Task GenerateAsync(XmiReaderResult xmiReaderResult, DirectoryInfo outputDirectory)
+ {
+ await this.GenerateElementExtensions(xmiReaderResult, outputDirectory);
+ }
+
+ ///
+ /// Renders the file, writes it to ,
+ /// and returns the generated source for assertion in expected-output tests (the "ExpectedResult"
+ /// principle used by sibling generators such as UmlPocoReferenceResolveExtensionGenerator).
+ ///
+ /// The with the loaded UML model.
+ /// The target directory.
+ /// The generated C# source, after CodeCleanup.
+ /// If either argument is null.
+ public async Task GenerateElementExtensions(XmiReaderResult xmiReaderResult, DirectoryInfo outputDirectory)
+ {
+ ArgumentNullException.ThrowIfNull(xmiReaderResult);
+ ArgumentNullException.ThrowIfNull(outputDirectory);
+
+ var payload = QueryContainmentRulePayload(xmiReaderResult);
+
+ var template = this.Templates[CorePocoValidationTemplateName];
+ var rendered = template(payload);
+ rendered = this.CodeCleanup(rendered);
+
+ await WriteAsync(rendered, outputDirectory, OutputFileName);
+
+ return rendered;
+ }
+
+ ///
+ /// Register the custom Handlebars helpers used by the template.
+ ///
+ protected override void RegisterHelpers()
+ {
+ this.Handlebars.RegisterStringHelper();
+ this.Handlebars.RegisterPropertyHelper();
+ this.Handlebars.RegisterClassHelper();
+ NamedElementHelper.RegisterNamedElementHelper(this.Handlebars);
+ PropertyHelper.RegisterPropertyHelper(this.Handlebars);
+ ClassHelper.RegisterClassHelper(this.Handlebars);
+ }
+
+ ///
+ /// Register the code template.
+ ///
+ protected override void RegisterTemplates()
+ {
+ this.RegisterTemplate(CorePocoValidationTemplateName);
+ }
+
+ ///
+ /// Walks the merged UML model and produces two parallel lists of :
+ /// one for the target-side narrowing (typed composite properties subsetting
+ /// OwnedRelatedElement) and one for the owner-side narrowing (typed properties subsetting
+ /// OwningRelatedElement with a type strictly narrower than Element). Each list is
+ /// returned ordered from most-specific to least-specific (deepest inheritance depth first), so
+ /// the emitted switch expressions match a class against its narrowest applicable interface first.
+ ///
+ /// The with the loaded UML model.
+ /// The payload with the two ordered rule lists.
+ private static ContainmentRulePayload QueryContainmentRulePayload(XmiReaderResult xmiReaderResult)
+ {
+ var (_, ownedRelatedElementXmiId, owningRelatedElementXmiId, elementXmiId) =
+ QueryRootContainmentMeta(xmiReaderResult);
+
+ var seenClassXmiIds = new HashSet();
+ var targetRows = new List<(IClass UmlClass, IProperty TypedProperty, int InheritanceDepth)>();
+ var ownerRows = new List<(IClass UmlClass, IProperty TypedProperty, int InheritanceDepth)>();
+
+ foreach (var package in xmiReaderResult.QueryContainedAndImported("SysML"))
+ {
+ foreach (var umlClass in package.PackagedElement.OfType())
+ {
+ if (!seenClassXmiIds.Add(umlClass.XmiId))
+ {
+ continue;
+ }
+
+ var inheritanceDepth = QueryInheritanceDepth(umlClass);
+
+ var typedTarget = QueryTypedSubsettingProperty(umlClass, ownedRelatedElementXmiId, requireComposite: true);
+
+ if (typedTarget != null)
+ {
+ targetRows.Add((umlClass, typedTarget, inheritanceDepth));
+ }
+
+ // Owner-side narrowing is meaningful only when the typed property's type is strictly
+ // narrower than Element — otherwise the arm would be equivalent to 'source is not null'
+ // which is exactly what the '_ => true' default already returns.
+ var typedOwner = QueryTypedSubsettingProperty(umlClass, owningRelatedElementXmiId, requireComposite: false);
+
+ if (typedOwner != null && typedOwner.Type.XmiId != elementXmiId)
+ {
+ ownerRows.Add((umlClass, typedOwner, inheritanceDepth));
+ }
+ }
+ }
+
+ return new ContainmentRulePayload
+ {
+ TargetRules = targetRows
+ .OrderByDescending(x => x.InheritanceDepth)
+ .ThenBy(x => x.UmlClass.Name, StringComparer.Ordinal)
+ .Select(x =>
+ {
+ var targetFqn = x.TypedProperty.Type.QueryFullyQualifiedTypeName();
+ var targetSimpleName = "I" + x.TypedProperty.Type.Name;
+
+ // When the expected target type is the root Element type, 'target is IElement' is
+ // equivalent to 'target is not null' (since the parameter is already typed IElement).
+ // Emit the latter to avoid the CA1508 / IDE0078 "type check succeeds on any
+ // not-null value" analyzer warning.
+ var targetCheck = x.TypedProperty.Type.XmiId == elementXmiId
+ ? "target is not null"
+ : $"target is {targetFqn}";
+
+ return new ContainmentRule
+ {
+ BridgeFqn = x.UmlClass.QueryFullyQualifiedTypeName(),
+ BridgeName = "I" + x.UmlClass.Name,
+ TargetFqn = targetFqn,
+ TargetName = targetSimpleName,
+ TargetCheck = targetCheck
+ };
+ })
+ .ToList(),
+
+ OwnerRules = ownerRows
+ .OrderByDescending(x => x.InheritanceDepth)
+ .ThenBy(x => x.UmlClass.Name, StringComparer.Ordinal)
+ .Select(x =>
+ {
+ var ownerFqn = x.TypedProperty.Type.QueryFullyQualifiedTypeName();
+ var ownerSimpleName = "I" + x.TypedProperty.Type.Name;
+
+ return new ContainmentRule
+ {
+ BridgeFqn = x.UmlClass.QueryFullyQualifiedTypeName(),
+ BridgeName = "I" + x.UmlClass.Name,
+ OwnerFqn = ownerFqn,
+ OwnerName = ownerSimpleName,
+ OwnerCheck = $"source is {ownerFqn}"
+ };
+ })
+ .ToList()
+ };
+ }
+
+ ///
+ /// Discovers the root containment metaclass and its two root containment properties purely from
+ /// the structure of the loaded UML model — no metaclass or property names are used.
+ ///
+ ///
+ /// The structural fingerprint of the root containment metaclass (KerML Relationship) is the
+ /// "self-referential containment" pattern: it declares two owned attributes whose declared Type is
+ /// a strict supertype of the metaclass itself. One attribute is composite (the contained-elements
+ /// side, ownedRelatedElement); the other is non-composite (the owner back-reference,
+ /// owningRelatedElement). The Type of those two attributes is the root Element
+ /// metaclass — i.e. one level above in the inheritance tree.
+ ///
+ /// The with the loaded UML model.
+ /// The relationship class together with the xmi ids of the two root containment
+ /// properties and the xmi id of the root element type they point to.
+ /// If no class in the loaded model matches the
+ /// self-referential containment fingerprint, or if more than one does.
+ private static (IClass RelationshipClass, string OwnedRelatedElementXmiId, string OwningRelatedElementXmiId, string ElementXmiId)
+ QueryRootContainmentMeta(XmiReaderResult xmiReaderResult)
+ {
+ var matches = new List<(IClass Class, IProperty Composite, IProperty BackRef)>();
+
+ foreach (var package in xmiReaderResult.QueryContainedAndImported("SysML"))
+ {
+ foreach (var candidateClass in package.PackagedElement.OfType())
+ {
+ IProperty composite = null;
+ IProperty backRef = null;
+
+ foreach (var attribute in candidateClass.OwnedAttribute)
+ {
+ // Derived attributes (e.g. the 'relatedElement' derived union on Relationship,
+ // which also has Type = Element) are not part of the structural fingerprint —
+ // we want the concrete association ends that subclasses subset, not the
+ // computed-union roots above them.
+ if (attribute.IsDerived)
+ {
+ continue;
+ }
+
+ if (attribute.Type is not IClass attributeType)
+ {
+ continue;
+ }
+
+ // The structural fingerprint: the owned attribute's Type is a strict supertype of
+ // the class that declares it. Only the root containment metaclass (Relationship)
+ // has this self-referential containment pattern.
+ if (!IsStrictDescendantOf(candidateClass, attributeType))
+ {
+ continue;
+ }
+
+ if (attribute.IsComposite && composite == null)
+ {
+ composite = attribute;
+ }
+ else if (!attribute.IsComposite && backRef == null)
+ {
+ backRef = attribute;
+ }
+ }
+
+ if (composite != null && backRef != null)
+ {
+ matches.Add((candidateClass, composite, backRef));
+ }
+ }
+ }
+
+ if (matches.Count == 0)
+ {
+ throw new InvalidOperationException(
+ "Could not structurally identify the root containment metaclass in the loaded UML " +
+ "model: no class declares both a composite and a non-composite owned attribute whose " +
+ "Type is a strict supertype of itself.");
+ }
+
+ if (matches.Count > 1)
+ {
+ throw new InvalidOperationException(
+ $"The structural fingerprint for the root containment metaclass is ambiguous in the " +
+ $"loaded UML model: {matches.Count} classes match. Expected exactly one.");
+ }
+
+ var match = matches[0];
+ return (match.Class, match.Composite.XmiId, match.BackRef.XmiId, ((IClass)match.Composite.Type).XmiId);
+ }
+
+ ///
+ /// Returns true when is a strict (non-reflexive) descendant
+ /// of via transitive walks.
+ ///
+ /// The class whose ancestor chain is walked.
+ /// The class looked for as an ancestor.
+ /// true if is a strict ancestor of
+ /// ; otherwise false.
+ private static bool IsStrictDescendantOf(IClass candidateDescendant, IClass candidateAncestor)
+ {
+ var visited = new HashSet();
+ var queue = new Queue();
+ queue.Enqueue(candidateDescendant);
+ visited.Add(candidateDescendant.XmiId);
+
+ while (queue.Count > 0)
+ {
+ var current = queue.Dequeue();
+
+ foreach (var super in current.SuperClass)
+ {
+ if (super == null)
+ {
+ continue;
+ }
+
+ if (super.XmiId == candidateAncestor.XmiId)
+ {
+ return true;
+ }
+
+ if (visited.Add(super.XmiId))
+ {
+ queue.Enqueue(super);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Returns the most-specific owned attribute of whose
+ /// subsetting/redefinition chain transitively reaches the property identified by
+ /// . Returns null if no such attribute exists on the class
+ /// itself (inherited-only properties are intentionally ignored — the C# pattern-matching switch
+ /// already handles inheritance via interface-pattern fallthrough).
+ ///
+ /// The candidate class.
+ /// The xmi id of the root property in the subsetting chain.
+ /// When true, only composite-aggregation attributes are considered
+ /// (target-side OwnedRelatedElement chain). When false, any aggregation is accepted
+ /// (owner-side OwningRelatedElement chain, which is not composite).
+ /// The typed property, or null.
+ private static IProperty QueryTypedSubsettingProperty(IClass umlClass, string rootPropertyXmiId, bool requireComposite)
+ {
+ IProperty bestMatch = null;
+ var bestDepth = int.MinValue;
+
+ foreach (var attribute in umlClass.OwnedAttribute)
+ {
+ if (requireComposite && !attribute.IsComposite)
+ {
+ continue;
+ }
+
+ var depth = QueryDepthToRootProperty(attribute, rootPropertyXmiId);
+
+ if (depth < 0)
+ {
+ continue;
+ }
+
+ if (depth > bestDepth)
+ {
+ bestMatch = attribute;
+ bestDepth = depth;
+ }
+ }
+
+ return bestMatch;
+ }
+
+ ///
+ /// Computes the shortest distance from to the property identified by
+ /// via subsets and redefinitions. Returns -1 if the chain
+ /// does not reach the root.
+ ///
+ /// The starting property.
+ /// The xmi id of the root property in the subsetting chain.
+ /// The chain depth, or -1.
+ private static int QueryDepthToRootProperty(IProperty property, string rootPropertyXmiId)
+ {
+ var visited = new HashSet();
+ var queue = new Queue<(IProperty Property, int Depth)>();
+ queue.Enqueue((property, 0));
+ visited.Add(property.XmiId);
+
+ while (queue.Count > 0)
+ {
+ var (current, depth) = queue.Dequeue();
+
+ if (current.XmiId == rootPropertyXmiId)
+ {
+ return depth;
+ }
+
+ foreach (var redefined in current.RedefinedProperty.Where(redefined => visited.Add(redefined.XmiId)))
+ {
+ queue.Enqueue((redefined, depth + 1));
+ }
+
+ foreach (var subsetted in current.SubsettedProperty.Where(subsetted => visited.Add(subsetted.XmiId)))
+ {
+ queue.Enqueue((subsetted, depth + 1));
+ }
+ }
+
+ return -1;
+ }
+
+ ///
+ /// Counts the number of distinct ancestor classes reachable from via
+ /// generalisation. Used as the sort key for the emitted switch expression so that more-specific
+ /// metaclasses appear first.
+ ///
+ /// The class to measure.
+ /// The transitive ancestor count.
+ private static int QueryInheritanceDepth(IClass umlClass)
+ {
+ var visited = new HashSet();
+ var queue = new Queue();
+ queue.Enqueue(umlClass);
+ visited.Add(umlClass.XmiId);
+
+ var depth = 0;
+
+ while (queue.Count > 0)
+ {
+ var current = queue.Dequeue();
+
+ foreach (var super in current.SuperClass.Where(super => super != null && visited.Add(super.XmiId)))
+ {
+ queue.Enqueue(super);
+ depth++;
+ }
+ }
+
+ return depth;
+ }
+ }
+
+ ///
+ /// Payload passed to the core-poco-validation-template Handlebars template. Holds two ordered
+ /// lists of : one for the target-side switch (QueryIsValidForContainment
+ /// and ExpectedContainmentTypeName) and one for the owner-side switch
+ /// (QueryIsValidContainmentOwner and ExpectedContainmentOwnerTypeName).
+ ///
+ public class ContainmentRulePayload
+ {
+ ///
+ /// Gets or sets the rules for the target-side switch (each bridge metaclass that declares a typed
+ /// composite subset of OwnedRelatedElement).
+ ///
+ public IReadOnlyList TargetRules { get; set; }
+
+ ///
+ /// Gets or sets the rules for the owner-side switch (each bridge metaclass that declares a typed
+ /// subset of OwningRelatedElement with a type strictly narrower than Element).
+ ///
+ public IReadOnlyList OwnerRules { get; set; }
+ }
+
+ ///
+ /// One arm of a generated switch expression: the bridge metaclass and either its expected target type
+ /// (when emitted into the target-side switch) or its expected owner type (when emitted into the
+ /// owner-side switch). A given instance carries only one side's fields.
+ ///
+ public class ContainmentRule
+ {
+ ///
+ /// Gets or sets the fully-qualified C# interface name of the bridge metaclass
+ /// (e.g. SysML2.NET.Core.POCO.Root.Namespaces.IFeatureMembership).
+ ///
+ public string BridgeFqn { get; set; }
+
+ ///
+ /// Gets or sets the simple C# interface name of the bridge metaclass (e.g. IFeatureMembership).
+ ///
+ public string BridgeName { get; set; }
+
+ ///
+ /// Gets or sets the fully-qualified C# interface name of the expected target type
+ /// (e.g. SysML2.NET.Core.POCO.Core.Features.IFeature). Set only on target-side rules.
+ ///
+ public string TargetFqn { get; set; }
+
+ ///
+ /// Gets or sets the simple C# interface name of the expected target type (e.g. IFeature).
+ /// Set only on target-side rules.
+ ///
+ public string TargetName { get; set; }
+
+ ///
+ /// Gets or sets the pre-composed C# expression rendered on the right-hand side of the
+ /// QueryIsValidForContainment switch arm. Equals target is {TargetFqn} for narrowed
+ /// target types, or target is not null when the expected target type is IElement
+ /// (the parameter type), to avoid the CA1508 / IDE0078 analyzer warning.
+ ///
+ public string TargetCheck { get; set; }
+
+ ///
+ /// Gets or sets the fully-qualified C# interface name of the expected owner type
+ /// (e.g. SysML2.NET.Core.POCO.Core.Types.IType). Set only on owner-side rules.
+ ///
+ public string OwnerFqn { get; set; }
+
+ ///
+ /// Gets or sets the simple C# interface name of the expected owner type (e.g. IType).
+ /// Set only on owner-side rules.
+ ///
+ public string OwnerName { get; set; }
+
+ ///
+ /// Gets or sets the pre-composed C# expression rendered on the right-hand side of the
+ /// QueryIsValidContainmentOwner switch arm. Equals source is {OwnerFqn}. The
+ /// source is not null short-circuit is unnecessary on the owner side because rules whose
+ /// owner type is IElement are filtered out at generation time (they would be equivalent
+ /// to the _ => true default arm).
+ ///
+ public string OwnerCheck { get; set; }
+ }
+}
diff --git a/SysML2.NET.CodeGenerator/SysML2.NET.CodeGenerator.csproj b/SysML2.NET.CodeGenerator/SysML2.NET.CodeGenerator.csproj
index ce4c57da..a3fb8ef8 100644
--- a/SysML2.NET.CodeGenerator/SysML2.NET.CodeGenerator.csproj
+++ b/SysML2.NET.CodeGenerator/SysML2.NET.CodeGenerator.csproj
@@ -205,6 +205,9 @@
Always
+
+ Always
+
Always
diff --git a/SysML2.NET.CodeGenerator/Templates/Uml/core-poco-validation-template.hbs b/SysML2.NET.CodeGenerator/Templates/Uml/core-poco-validation-template.hbs
new file mode 100644
index 00000000..18cee20a
--- /dev/null
+++ b/SysML2.NET.CodeGenerator/Templates/Uml/core-poco-validation-template.hbs
@@ -0,0 +1,115 @@
+// -------------------------------------------------------------------------------------------------
+//
+//
+// 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.
+//
+//
+// ------------------------------------------------------------------------------------------------
+
+// ------------------------------------------------------------------------------------------------
+// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!--------
+// ------------------------------------------------------------------------------------------------
+
+namespace SysML2.NET.Extensions
+{
+ using SysML2.NET.Core.POCO.Root.Elements;
+
+ ///
+ /// Auto-generated companion to the hand-coded partial class.
+ ///
+ public static partial class ElementExtensions
+ {
+ ///
+ /// Determines whether is a structurally valid element to be added as an
+ /// of , based on
+ /// the typed composite property declared by the bridge's metaclass in the KerML/SysML2 metamodel.
+ ///
+ /// The bridge .
+ /// The candidate .
+ /// true if 's runtime type satisfies the constraint;
+ /// otherwise false.
+ public static bool QueryIsValidForContainment(this IRelationship bridgeRelationship, IElement target)
+ {
+ return bridgeRelationship switch
+ {
+ {{#each TargetRules as | rule |}}
+ {{rule.BridgeFqn}} => {{rule.TargetCheck}},
+ {{/each}}
+ _ => true,
+ };
+ }
+
+ ///
+ /// Returns the simple C# interface name of the expected target type for the supplied
+ /// , used to compose the error message thrown by
+ /// AssignOwnership when the target is not a valid containment target.
+ ///
+ /// The bridge .
+ /// The expected target type's simple interface name.
+ public static string ExpectedContainmentTypeName(this IRelationship bridgeRelationship)
+ {
+ return bridgeRelationship switch
+ {
+ {{#each TargetRules as | rule |}}
+ {{rule.BridgeFqn}} => nameof({{rule.TargetFqn}}),
+ {{/each}}
+ _ => nameof(IElement),
+ };
+ }
+
+ ///
+ /// Determines whether is a structurally valid containment owner for
+ /// , based on the typed property declared by the bridge's
+ /// metaclass that subsets in the KerML/SysML2
+ /// metamodel (e.g. an IFeatureMembership requires an IType owner).
+ ///
+ /// The bridge .
+ /// The candidate owner .
+ /// true if 's runtime type satisfies the owner-side
+ /// constraint; otherwise false.
+ public static bool QueryIsValidContainmentOwner(this IRelationship bridgeRelationship, IElement source)
+ {
+ return bridgeRelationship switch
+ {
+ {{#each OwnerRules as | rule |}}
+ {{rule.BridgeFqn}} => {{rule.OwnerCheck}},
+ {{/each}}
+ _ => true,
+ };
+ }
+
+ ///
+ /// Returns the simple C# interface name of the expected owner type for the supplied
+ /// , used to compose the error message thrown by
+ /// AssignOwnership when the source is not a valid containment owner.
+ ///
+ /// The bridge .
+ /// The expected owner type's simple interface name.
+ public static string ExpectedContainmentOwnerTypeName(this IRelationship bridgeRelationship)
+ {
+ return bridgeRelationship switch
+ {
+ {{#each OwnerRules as | rule |}}
+ {{rule.BridgeFqn}} => nameof({{rule.OwnerFqn}}),
+ {{/each}}
+ _ => nameof(IElement),
+ };
+ }
+ }
+}
+
+// ------------------------------------------------------------------------------------------------
+// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!--------
+// ------------------------------------------------------------------------------------------------
diff --git a/SysML2.NET.Tests/Extend/FeatureExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/FeatureExtensionsTestFixture.cs
index 2b928133..b6fed013 100644
--- a/SysML2.NET.Tests/Extend/FeatureExtensionsTestFixture.cs
+++ b/SysML2.NET.Tests/Extend/FeatureExtensionsTestFixture.cs
@@ -44,17 +44,17 @@ public void VerifyComputeOwnedFeatureChaining()
Assert.That(feature.ComputeOwnedFeatureChaining(), Has.Count.EqualTo(0));
var subsetting = new Subsetting();
- feature.AssignOwnership(subsetting, new Feature());
+ feature.AssignOwnership(subsetting);
Assert.That(feature.ComputeOwnedFeatureChaining(), Has.Count.EqualTo(0));
var chainingTarget1 = new Feature();
var chaining1 = new FeatureChaining { ChainingFeature = chainingTarget1 };
- feature.AssignOwnership(chaining1, new Feature());
+ feature.AssignOwnership(chaining1);
var chainingTarget2 = new Feature();
var chaining2 = new FeatureChaining { ChainingFeature = chainingTarget2 };
- feature.AssignOwnership(chaining2, new Feature());
+ feature.AssignOwnership(chaining2);
var result = feature.ComputeOwnedFeatureChaining();
@@ -77,12 +77,12 @@ public void VerifyComputeOwnedFeatureInverting()
var otherFeature = new Feature();
var invertingPointingElsewhere = new FeatureInverting { FeatureInverted = otherFeature };
- feature.AssignOwnership(invertingPointingElsewhere, new Feature());
+ feature.AssignOwnership(invertingPointingElsewhere);
Assert.That(feature.ComputeOwnedFeatureInverting(), Has.Count.EqualTo(0));
var invertingPointingSelf = new FeatureInverting { FeatureInverted = feature };
- feature.AssignOwnership(invertingPointingSelf, new Feature());
+ feature.AssignOwnership(invertingPointingSelf);
var result = feature.ComputeOwnedFeatureInverting();
@@ -104,13 +104,13 @@ public void VerifyComputeOwnedTypeFeaturing()
var otherFeature = new Feature();
var typeFeaturingPointingElsewhere = new TypeFeaturing { FeatureOfType = otherFeature };
- feature.AssignOwnership(typeFeaturingPointingElsewhere, new Feature());
+ feature.AssignOwnership(typeFeaturingPointingElsewhere);
Assert.That(feature.ComputeOwnedTypeFeaturing(), Has.Count.EqualTo(0));
var featuringType = new Type();
var typeFeaturingPointingSelf = new TypeFeaturing { FeatureOfType = feature, FeaturingType = featuringType };
- feature.AssignOwnership(typeFeaturingPointingSelf, new Feature());
+ feature.AssignOwnership(typeFeaturingPointingSelf);
var result = feature.ComputeOwnedTypeFeaturing();
@@ -131,18 +131,18 @@ public void VerifyComputeOwnedSubsetting()
Assert.That(feature.ComputeOwnedSubsetting(), Has.Count.EqualTo(0));
var specialization = new Specialization();
- feature.AssignOwnership(specialization, new Feature());
+ feature.AssignOwnership(specialization);
Assert.That(feature.ComputeOwnedSubsetting(), Has.Count.EqualTo(0));
var subsetting = new Subsetting();
- feature.AssignOwnership(subsetting, new Feature());
+ feature.AssignOwnership(subsetting);
var redefinition = new Redefinition();
- feature.AssignOwnership(redefinition, new Feature());
+ feature.AssignOwnership(redefinition);
var referenceSubsetting = new ReferenceSubsetting();
- feature.AssignOwnership(referenceSubsetting, new Feature());
+ feature.AssignOwnership(referenceSubsetting);
var result = feature.ComputeOwnedSubsetting();
@@ -159,15 +159,15 @@ public void VerifyComputeOwnedRedefinition()
Assert.That(feature.ComputeOwnedRedefinition(), Has.Count.EqualTo(0));
var subsetting = new Subsetting();
- feature.AssignOwnership(subsetting, new Feature());
+ feature.AssignOwnership(subsetting);
Assert.That(feature.ComputeOwnedRedefinition(), Has.Count.EqualTo(0));
var redefinition1 = new Redefinition();
- feature.AssignOwnership(redefinition1, new Feature());
+ feature.AssignOwnership(redefinition1);
var redefinition2 = new Redefinition();
- feature.AssignOwnership(redefinition2, new Feature());
+ feature.AssignOwnership(redefinition2);
var result = feature.ComputeOwnedRedefinition();
@@ -189,7 +189,7 @@ public void VerifyComputeOwnedReferenceSubsetting()
Assert.That(feature.ComputeOwnedReferenceSubsetting(), Is.Null);
var referenceSubsetting = new ReferenceSubsetting();
- feature.AssignOwnership(referenceSubsetting, new Feature());
+ feature.AssignOwnership(referenceSubsetting);
Assert.That(feature.ComputeOwnedReferenceSubsetting(), Is.SameAs(referenceSubsetting));
}
@@ -204,7 +204,7 @@ public void VerifyComputeOwnedCrossSubsetting()
Assert.That(feature.ComputeOwnedCrossSubsetting(), Is.Null);
var crossSubsetting = new CrossSubsetting();
- feature.AssignOwnership(crossSubsetting, new Feature());
+ feature.AssignOwnership(crossSubsetting);
Assert.That(feature.ComputeOwnedCrossSubsetting(), Is.SameAs(crossSubsetting));
}
@@ -219,17 +219,17 @@ public void VerifyComputeOwnedTyping()
Assert.That(feature.ComputeOwnedTyping(), Has.Count.EqualTo(0));
var specialization = new Specialization();
- feature.AssignOwnership(specialization, new Feature());
+ feature.AssignOwnership(specialization);
Assert.That(feature.ComputeOwnedTyping(), Has.Count.EqualTo(0));
var type1 = new Type();
var typing1 = new FeatureTyping { Type = type1 };
- feature.AssignOwnership(typing1, new Feature());
+ feature.AssignOwnership(typing1);
var type2 = new Type();
var typing2 = new FeatureTyping { Type = type2 };
- feature.AssignOwnership(typing2, new Feature());
+ feature.AssignOwnership(typing2);
var result = feature.ComputeOwnedTyping();
@@ -252,11 +252,11 @@ public void VerifyComputeChainingFeature()
var target1 = new Feature();
var chaining1 = new FeatureChaining { ChainingFeature = target1 };
- feature.AssignOwnership(chaining1, new Feature());
+ feature.AssignOwnership(chaining1);
var target2 = new Feature();
var chaining2 = new FeatureChaining { ChainingFeature = target2 };
- feature.AssignOwnership(chaining2, new Feature());
+ feature.AssignOwnership(chaining2);
var result = feature.ComputeChainingFeature();
@@ -325,11 +325,11 @@ public void VerifyComputeFeatureTarget()
var target1 = new Feature();
var chaining1 = new FeatureChaining { ChainingFeature = target1 };
- feature.AssignOwnership(chaining1, new Feature());
+ feature.AssignOwnership(chaining1);
var lastTarget = new Feature();
var chaining2 = new FeatureChaining { ChainingFeature = lastTarget };
- feature.AssignOwnership(chaining2, new Feature());
+ feature.AssignOwnership(chaining2);
Assert.That(feature.ComputeFeatureTarget(), Is.SameAs(lastTarget));
}
@@ -369,19 +369,19 @@ public void VerifyComputeCrossFeature()
var crossedFeature = new Feature();
var crossSubsetting = new CrossSubsetting { CrossedFeature = crossedFeature };
- feature.AssignOwnership(crossSubsetting, new Feature());
+ feature.AssignOwnership(crossSubsetting);
Assert.That(feature.ComputeCrossFeature(), Is.Null);
var chainingTarget1 = new Feature();
var chaining1 = new FeatureChaining { ChainingFeature = chainingTarget1 };
- crossedFeature.AssignOwnership(chaining1, new Feature());
+ crossedFeature.AssignOwnership(chaining1);
Assert.That(feature.ComputeCrossFeature(), Is.Null);
var chainingTarget2 = new Feature();
var chaining2 = new FeatureChaining { ChainingFeature = chainingTarget2 };
- crossedFeature.AssignOwnership(chaining2, new Feature());
+ crossedFeature.AssignOwnership(chaining2);
Assert.That(feature.ComputeCrossFeature(), Is.SameAs(chainingTarget2));
}
@@ -397,7 +397,7 @@ public void VerifyComputeFeaturingType()
var theType = new Type();
var typeFeaturing = new TypeFeaturing { FeatureOfType = feature, FeaturingType = theType };
- feature.AssignOwnership(typeFeaturing, new Feature());
+ feature.AssignOwnership(typeFeaturing);
var result = feature.ComputeFeaturingType();
@@ -410,10 +410,10 @@ public void VerifyComputeFeaturingType()
var chainingTarget = new Feature();
var chainingTargetType = new Type();
var chainingTypeFeaturing = new TypeFeaturing { FeatureOfType = chainingTarget, FeaturingType = chainingTargetType };
- chainingTarget.AssignOwnership(chainingTypeFeaturing, new Feature());
+ chainingTarget.AssignOwnership(chainingTypeFeaturing);
var chaining = new FeatureChaining { ChainingFeature = chainingTarget };
- feature.AssignOwnership(chaining, new Feature());
+ feature.AssignOwnership(chaining);
var resultWithChaining = feature.ComputeFeaturingType();
@@ -431,11 +431,11 @@ public void VerifyComputeType()
var type1 = new Type();
var typing1 = new FeatureTyping { Type = type1 };
- feature.AssignOwnership(typing1, new Feature());
+ feature.AssignOwnership(typing1);
var type2 = new Type();
var typing2 = new FeatureTyping { Type = type2 };
- feature.AssignOwnership(typing2, new Feature());
+ feature.AssignOwnership(typing2);
var result = feature.ComputeType();
@@ -458,13 +458,13 @@ public void VerifyComputeNamingFeatureOperation()
var redefinedFeature = new Feature { DeclaredName = "redefined" };
var redefinition = new Redefinition { RedefinedFeature = redefinedFeature };
- feature.AssignOwnership(redefinition, new Feature());
+ feature.AssignOwnership(redefinition);
Assert.That(feature.ComputeNamingFeatureOperation(), Is.SameAs(redefinedFeature));
var redefinedFeature2 = new Feature { DeclaredName = "redefined2" };
var redefinition2 = new Redefinition { RedefinedFeature = redefinedFeature2 };
- feature.AssignOwnership(redefinition2, new Feature());
+ feature.AssignOwnership(redefinition2);
Assert.That(feature.ComputeNamingFeatureOperation(), Is.SameAs(redefinedFeature));
}
@@ -489,13 +489,13 @@ public void VerifyComputeRedefinedEffectiveShortNameOperation()
var featureWithRedefinition = new Feature();
var namingFeature = new Feature { DeclaredShortName = "nfShort" };
var redefinition = new Redefinition { RedefinedFeature = namingFeature };
- featureWithRedefinition.AssignOwnership(redefinition, new Feature());
+ featureWithRedefinition.AssignOwnership(redefinition);
Assert.That(featureWithRedefinition.ComputeRedefinedEffectiveShortNameOperation(), Is.EqualTo("nfShort"));
var featureNoNaming = new Feature();
var redefinitionNoTarget = new Redefinition { RedefinedFeature = new Feature() };
- featureNoNaming.AssignOwnership(redefinitionNoTarget, new Feature());
+ featureNoNaming.AssignOwnership(redefinitionNoTarget);
Assert.That(featureNoNaming.ComputeRedefinedEffectiveShortNameOperation(), Is.Null);
}
@@ -520,13 +520,13 @@ public void VerifyComputeRedefinedEffectiveNameOperation()
var featureWithRedefinition = new Feature();
var namingFeature = new Feature { DeclaredName = "nfName" };
var redefinition = new Redefinition { RedefinedFeature = namingFeature };
- featureWithRedefinition.AssignOwnership(redefinition, new Feature());
+ featureWithRedefinition.AssignOwnership(redefinition);
Assert.That(featureWithRedefinition.ComputeRedefinedEffectiveNameOperation(), Is.EqualTo("nfName"));
var featureNoNaming = new Feature();
var redefinitionNoTarget = new Redefinition { RedefinedFeature = new Feature() };
- featureNoNaming.AssignOwnership(redefinitionNoTarget, new Feature());
+ featureNoNaming.AssignOwnership(redefinitionNoTarget);
Assert.That(featureNoNaming.ComputeRedefinedEffectiveNameOperation(), Is.Null);
}
@@ -571,7 +571,7 @@ public void VerifyComputeRedefinesOperation()
var otherFeature = new Feature();
var redefinition = new Redefinition { RedefinedFeature = otherFeature };
- feature.AssignOwnership(redefinition, new Feature());
+ feature.AssignOwnership(redefinition);
using (Assert.EnterMultipleScope())
{
@@ -640,13 +640,13 @@ public void VerifyComputeIsCartesianProductOperation()
var theType = new Type();
var typing = new FeatureTyping { Type = theType };
- feature.AssignOwnership(typing, new Feature());
+ feature.AssignOwnership(typing);
Assert.That(feature.ComputeIsCartesianProductOperation(), Is.False);
var featuringType = new Type();
var typeFeaturing = new TypeFeaturing { FeatureOfType = feature, FeaturingType = featuringType };
- feature.AssignOwnership(typeFeaturing, new Feature());
+ feature.AssignOwnership(typeFeaturing);
Assert.That(feature.ComputeIsCartesianProductOperation(), Is.True);
}
@@ -664,7 +664,7 @@ public void VerifyComputeAsCartesianProductOperation()
var theType = new Type();
var typing = new FeatureTyping { Type = theType };
- feature.AssignOwnership(typing, new Feature());
+ feature.AssignOwnership(typing);
result = feature.ComputeAsCartesianProductOperation();
@@ -673,7 +673,7 @@ public void VerifyComputeAsCartesianProductOperation()
var featuringType = new Type();
var typeFeaturing = new TypeFeaturing { FeatureOfType = feature, FeaturingType = featuringType };
- feature.AssignOwnership(typeFeaturing, new Feature());
+ feature.AssignOwnership(typeFeaturing);
result = feature.ComputeAsCartesianProductOperation();
@@ -747,7 +747,7 @@ public void VerifyComputeAllRedefinedFeaturesOperation()
var redefinedB = new Feature();
var redefinitionAB = new Redefinition { RedefinedFeature = redefinedB };
- feature.AssignOwnership(redefinitionAB, new Feature());
+ feature.AssignOwnership(redefinitionAB);
result = feature.ComputeAllRedefinedFeaturesOperation();
@@ -760,7 +760,7 @@ public void VerifyComputeAllRedefinedFeaturesOperation()
var redefinedC = new Feature();
var redefinitionBC = new Redefinition { RedefinedFeature = redefinedC };
- redefinedB.AssignOwnership(redefinitionBC, new Feature());
+ redefinedB.AssignOwnership(redefinitionBC);
result = feature.ComputeAllRedefinedFeaturesOperation();
diff --git a/SysML2.NET.Tests/Extend/NamespaceExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/NamespaceExtensionsTestFixture.cs
index f3472a73..e2a51dca 100644
--- a/SysML2.NET.Tests/Extend/NamespaceExtensionsTestFixture.cs
+++ b/SysML2.NET.Tests/Extend/NamespaceExtensionsTestFixture.cs
@@ -48,7 +48,7 @@ public void VerifyComputeImportedMembership()
importedNamespace.AssignOwnership(importedMembership, importedElement);
var namespaceImport = new NamespaceImport { ImportedNamespace = importedNamespace };
- namespaceElement.AssignOwnership(namespaceImport, new Namespace());
+ namespaceElement.AssignOwnership(namespaceImport);
Assert.That(namespaceElement.ComputeImportedMembership(), Is.EquivalentTo([importedMembership]));
}
@@ -95,7 +95,7 @@ public void VerifyComputeOwnedImport()
Assert.That(namespaceElement.ComputeOwnedImport(), Has.Count.EqualTo(0));
var namespaceImport = new NamespaceImport();
- namespaceElement.AssignOwnership(namespaceImport, new Namespace());
+ namespaceElement.AssignOwnership(namespaceImport);
Assert.That(namespaceElement.ComputeOwnedImport(), Is.EquivalentTo([namespaceImport]));
}
@@ -146,7 +146,7 @@ public void VerifyComputeNamesOfOperation()
Assert.That(namespaceElement.ComputeNamesOfOperation(element), Has.Count.EqualTo(0));
var membership = new Membership { MemberName = "elementName", MemberShortName = "en", MemberElement = element };
- namespaceElement.AssignOwnership(membership, element);
+ namespaceElement.AssignOwnership(membership);
Assert.That(namespaceElement.ComputeNamesOfOperation(element), Is.EquivalentTo(["en", "elementName"]));
@@ -229,7 +229,7 @@ public void VerifyComputeImportedMembershipsOperation()
importedNamespace.AssignOwnership(importedMembership, importedElement);
var namespaceImport = new NamespaceImport { ImportedNamespace = importedNamespace };
- namespaceElement.AssignOwnership(namespaceImport, new Namespace());
+ namespaceElement.AssignOwnership(namespaceImport);
Assert.That(namespaceElement.ComputeImportedMembershipsOperation([]), Is.EquivalentTo([importedMembership]));
diff --git a/SysML2.NET.Tests/Extend/PackageExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/PackageExtensionsTestFixture.cs
index 9886cf4c..fc4743c4 100644
--- a/SysML2.NET.Tests/Extend/PackageExtensionsTestFixture.cs
+++ b/SysML2.NET.Tests/Extend/PackageExtensionsTestFixture.cs
@@ -66,9 +66,8 @@ public void VerifyComputeRedefinedImportedMembershipsOperation()
Assert.That(package.ComputeRedefinedImportedMembershipsOperation([]), Is.Empty);
var importMember = new MembershipImport();
- var type = new Type();
-
- package.AssignOwnership(importMember, type);
+
+ package.AssignOwnership(importMember);
Assert.That(()=> package.ComputeRedefinedImportedMembershipsOperation([]), Throws.InstanceOf());
var membership = new ElementFilterMembership();
diff --git a/SysML2.NET.Tests/Extensions/ElementExtensionsTestFixture.cs b/SysML2.NET.Tests/Extensions/ElementExtensionsTestFixture.cs
index ee3940e1..50164e9d 100644
--- a/SysML2.NET.Tests/Extensions/ElementExtensionsTestFixture.cs
+++ b/SysML2.NET.Tests/Extensions/ElementExtensionsTestFixture.cs
@@ -21,69 +21,275 @@
namespace SysML2.NET.Tests.Extensions
{
using System;
-
+
+ using NUnit.Framework;
+
using SysML2.NET.Core.POCO.Core.Features;
using SysML2.NET.Core.POCO.Core.Types;
+ using SysML2.NET.Core.POCO.Kernel.Expressions;
+ using SysML2.NET.Core.POCO.Kernel.FeatureValues;
+ using SysML2.NET.Core.POCO.Kernel.Packages;
+ using SysML2.NET.Core.POCO.Root.Annotations;
+ using SysML2.NET.Core.POCO.Root.Namespaces;
using SysML2.NET.Core.POCO.Systems.Parts;
using SysML2.NET.Extensions;
-
- using NUnit.Framework;
+
+ using IContainedRelationship = SysML2.NET.Core.POCO.Root.Elements.IContainedRelationship;
[TestFixture]
public class ElementExtensionsTestFixture
{
private PartDefinition source;
private FeatureMembership bridgeRelationship;
+ private Specialization referenceBridgeRelationship;
private Feature target;
[SetUp]
public void SetUp()
{
- source = new PartDefinition();
- bridgeRelationship = new FeatureMembership();
- target = new Feature();
+ this.source = new PartDefinition();
+ this.bridgeRelationship = new FeatureMembership();
+ // Specialization's owner-side narrowing is IType — PartDefinition IS-A IType, so the fixture
+ // source can validly own the reference bridge under the stricter owner-type guard.
+ this.referenceBridgeRelationship = new Specialization();
+ this.target = new Feature();
}
[Test]
- public void AssignOwnership_WithNullSource_ThrowsArgumentNullException()
+ public void AssignOwnership_OwnerOnly_WithContainmentCycle_ThrowsInvalidOperationException()
{
- Assert.Throws(() => ElementExtensions.AssignOwnership(null, bridgeRelationship, target));
+ // Arrange: bridge already directly contains source so that source.OwningRelationship == bridge.
+ ((IContainedRelationship)this.referenceBridgeRelationship).OwnedRelatedElement.Add(this.source);
+
+ Assert.That(
+ () => this.source.AssignOwnership(this.referenceBridgeRelationship),
+ Throws.TypeOf().With.Message.Contains("Containment cycle"));
+ }
+
+ [Test]
+ public void AssignOwnership_OwnerOnly_WithInvalidArguments_Throws()
+ {
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(() => ElementExtensions.AssignOwnership(null, this.referenceBridgeRelationship), Throws.TypeOf());
+ Assert.That(() => this.source.AssignOwnership(null), Throws.TypeOf());
+ Assert.That(() => this.referenceBridgeRelationship.AssignOwnership(this.referenceBridgeRelationship), Throws.TypeOf());
+ Assert.That(() => this.source.AssignOwnership(this.bridgeRelationship), Throws.TypeOf());
+ }
+ }
+
+ [Test]
+ public void AssignOwnership_OwnerOnly_WithMembershipAndNonNamespaceSource_ThrowsInvalidOperationException()
+ {
+ var nonNamespaceSource = new Comment();
+ var membership = new Membership();
+
+ Assert.That(
+ () => nonNamespaceSource.AssignOwnership(membership),
+ Throws.TypeOf().With.Message.Contains("INamespace"));
+ }
+
+ [Test]
+ public void AssignOwnership_OwnerOnly_WithValidParameters_AssignsOwnershipCorrectly()
+ {
+ this.source.AssignOwnership(this.referenceBridgeRelationship);
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(this.referenceBridgeRelationship.OwningRelatedElement, Is.EqualTo(this.source));
+ Assert.That(this.source.OwnedRelationship, Does.Contain(this.referenceBridgeRelationship));
+ Assert.That(this.referenceBridgeRelationship.OwnedRelatedElement, Is.Empty);
+ }
+ }
+
+ [Test]
+ public void AssignOwnership_WithAnnotation_AssignsOwnershipCorrectly()
+ {
+ var annotation = new Annotation();
+ var comment = new Comment();
+
+ this.source.AssignOwnership(annotation, comment);
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(annotation.OwningRelatedElement, Is.EqualTo(this.source));
+ Assert.That(this.source.OwnedRelationship, Does.Contain(annotation));
+ Assert.That(annotation.OwnedRelatedElement, Does.Contain(comment));
+ }
+ }
+
+ [Test]
+ public void AssignOwnership_WithBridgeEqualsTarget_ThrowsInvalidOperationException()
+ {
+ Assert.That(
+ () => this.source.AssignOwnership(this.bridgeRelationship, this.bridgeRelationship),
+ Throws.TypeOf());
+ }
+
+ [Test]
+ public void AssignOwnership_WithContainmentCycle_ThrowsInvalidOperationException()
+ {
+ // Scenario 1: bridge directly contains source.
+ var bridgeWithSourceInside = new FeatureMembership();
+ var sourceInsideBridge = new PartDefinition();
+ ((IContainedRelationship)bridgeWithSourceInside).OwnedRelatedElement.Add(sourceInsideBridge);
+
+ // Scenario 2: target transitively contains source through a multi-level chain.
+ // Features are used throughout because FeatureMembership requires its target to be an IFeature
+ // (enforced by the QueryIsValidForContainment guard); Feature is also an INamespace, so it can
+ // serve as the owner of further FeatureMemberships in the chain.
+ var ancestorTarget = new Feature();
+ var midMembership = new FeatureMembership();
+ var midElement = new Feature();
+ ancestorTarget.AssignOwnership(midMembership, midElement);
+
+ var leafMembership = new FeatureMembership();
+ var leafElement = new Feature();
+ midElement.AssignOwnership(leafMembership, leafElement);
+
+ var newBridge = new FeatureMembership();
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(
+ () => sourceInsideBridge.AssignOwnership(bridgeWithSourceInside, new Feature()),
+ Throws.TypeOf().With.Message.Contains("Containment cycle"));
+
+ Assert.That(
+ () => leafElement.AssignOwnership(newBridge, ancestorTarget),
+ Throws.TypeOf().With.Message.Contains("Containment cycle"));
+ }
+ }
+
+ [Test]
+ public void AssignOwnership_WithMembershipAndNonNamespaceSource_ThrowsInvalidOperationException()
+ {
+ // Comment is neither an INamespace nor an IType, so a generic OwningMembership owner check
+ // (requires INamespace) fires.
+ var nonNamespaceSource = new Comment();
+ var owningMembership = new OwningMembership();
+ var memberElement = new Feature();
+
+ Assert.That(
+ () => nonNamespaceSource.AssignOwnership(owningMembership, memberElement),
+ Throws.TypeOf().With.Message.Contains("INamespace"));
+ }
+
+ [Test]
+ public void AssignOwnership_WithIncompatibleOwnerType_ThrowsInvalidOperationException()
+ {
+ // Package IS-A INamespace but NOT IType; FeatureMembership requires its owner to be IType
+ // per the KerML specification. The coarse "is IMembership → source is INamespace" check
+ // (the previous, lax behaviour) would have accepted this. The stricter generated guard rejects it.
+ var packageSource = new Package();
+ var featureMembership = new FeatureMembership();
+ var memberFeature = new Feature();
+
+ Assert.That(
+ () => packageSource.AssignOwnership(featureMembership, memberFeature),
+ Throws.TypeOf().With.Message.Contains("not a valid containment owner"));
}
[Test]
public void AssignOwnership_WithNullBridge_ThrowsArgumentNullException()
{
- Assert.Throws(() => ElementExtensions.AssignOwnership(source, null, target));
+ Assert.That(
+ () => this.source.AssignOwnership(null, this.target),
+ Throws.TypeOf());
+ }
+
+ [Test]
+ public void AssignOwnership_WithNullSource_ThrowsArgumentNullException()
+ {
+ Assert.That(
+ () => ElementExtensions.AssignOwnership(null, this.bridgeRelationship, this.target),
+ Throws.TypeOf());
}
[Test]
public void AssignOwnership_WithNullTarget_ThrowsArgumentNullException()
{
- Assert.Throws(() => ElementExtensions.AssignOwnership(source, bridgeRelationship, null));
+ Assert.That(
+ () => this.source.AssignOwnership(this.bridgeRelationship, null),
+ Throws.TypeOf());
}
[Test]
- public void AssignOwnership_WithSourceEqualsTarget_ThrowsInvalidOperationException()
+ public void AssignOwnership_WithCompatibleTargetType_AssignsOwnershipCorrectly()
{
- Assert.Throws(() => ElementExtensions.AssignOwnership(source, bridgeRelationship, source));
+ // FeatureValue's owner-side narrowing is IFeature, so the owner must be a Feature.
+ var featureValueOwner = new Feature();
+ var featureValue = new FeatureValue();
+ var literal = new LiteralBoolean();
+
+ var annotationOwner = new PartDefinition();
+ var annotation = new Annotation();
+ var comment = new Comment();
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(
+ () => featureValueOwner.AssignOwnership(featureValue, literal),
+ Throws.Nothing);
+ Assert.That(
+ () => annotationOwner.AssignOwnership(annotation, comment),
+ Throws.Nothing);
+ }
}
[Test]
- public void AssignOwnership_WithBridgeEqualsTarget_ThrowsInvalidOperationException()
+ public void AssignOwnership_WithIncompatibleTargetType_ThrowsInvalidOperationException()
+ {
+ using (Assert.EnterMultipleScope())
+ {
+ // FeatureMembership requires an IFeature target; Comment is not an IFeature.
+ Assert.That(
+ () => this.source.AssignOwnership(this.bridgeRelationship, new Comment()),
+ Throws.TypeOf().With.Message.Contains("not a valid containment target"));
+
+ // FeatureValue requires an IExpression target; Feature is not an IExpression.
+ // The owner (a Feature) is valid since FeatureValue requires an IFeature owner.
+ var featureValueOwner = new Feature();
+ var featureValue = new FeatureValue();
+ Assert.That(
+ () => featureValueOwner.AssignOwnership(featureValue, new Feature()),
+ Throws.TypeOf().With.Message.Contains("not a valid containment target"));
+
+ // Annotation requires an IAnnotatingElement target; Feature is not an IAnnotatingElement.
+ var annotationOwner = new PartDefinition();
+ var annotation = new Annotation();
+ Assert.That(
+ () => annotationOwner.AssignOwnership(annotation, new Feature()),
+ Throws.TypeOf().With.Message.Contains("not a valid containment target"));
+ }
+ }
+
+ [Test]
+ public void AssignOwnership_WithReferenceOnlyRelationship_ThrowsInvalidOperationException()
+ {
+ Assert.That(
+ () => this.source.AssignOwnership(this.referenceBridgeRelationship, this.target),
+ Throws.TypeOf());
+ }
+
+ [Test]
+ public void AssignOwnership_WithSourceEqualsTarget_ThrowsInvalidOperationException()
{
- Assert.Throws(() => ElementExtensions.AssignOwnership(source, bridgeRelationship, bridgeRelationship));
+ Assert.That(
+ () => this.source.AssignOwnership(this.bridgeRelationship, this.source),
+ Throws.TypeOf());
}
[Test]
public void AssignOwnership_WithValidParameters_AssignsOwnershipCorrectly()
{
- ElementExtensions.AssignOwnership(source, bridgeRelationship, target);
+ this.source.AssignOwnership(this.bridgeRelationship, this.target);
using (Assert.EnterMultipleScope())
{
- Assert.That(bridgeRelationship.OwningRelatedElement, Is.EqualTo(source));
- Assert.That(source.OwnedRelationship, Does.Contain(bridgeRelationship));
- Assert.That(bridgeRelationship.OwnedRelatedElement, Does.Contain(target));
+ Assert.That(this.bridgeRelationship.OwningRelatedElement, Is.EqualTo(this.source));
+ Assert.That(this.source.OwnedRelationship, Does.Contain(this.bridgeRelationship));
+ Assert.That(this.bridgeRelationship.OwnedRelatedElement, Does.Contain(this.target));
}
}
}
diff --git a/SysML2.NET/Extensions/AutoGenExtensions/ElementExtensions.cs b/SysML2.NET/Extensions/AutoGenExtensions/ElementExtensions.cs
new file mode 100644
index 00000000..f4aa334a
--- /dev/null
+++ b/SysML2.NET/Extensions/AutoGenExtensions/ElementExtensions.cs
@@ -0,0 +1,181 @@
+// -------------------------------------------------------------------------------------------------
+//
+//
+// 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.
+//
+//
+// ------------------------------------------------------------------------------------------------
+
+// ------------------------------------------------------------------------------------------------
+// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!--------
+// ------------------------------------------------------------------------------------------------
+
+namespace SysML2.NET.Extensions
+{
+ using SysML2.NET.Core.POCO.Root.Elements;
+
+ ///
+ /// Auto-generated companion to the hand-coded partial class.
+ ///
+ public static partial class ElementExtensions
+ {
+ ///
+ /// Determines whether is a structurally valid element to be added as an
+ /// of , based on
+ /// the typed composite property declared by the bridge's metaclass in the KerML/SysML2 metamodel.
+ ///
+ /// The bridge .
+ /// The candidate .
+ /// true if 's runtime type satisfies the constraint;
+ /// otherwise false.
+ public static bool QueryIsValidForContainment(this IRelationship bridgeRelationship, IElement target)
+ {
+ return bridgeRelationship switch
+ {
+ SysML2.NET.Core.POCO.Systems.Requirements.IActorMembership => target is SysML2.NET.Core.POCO.Systems.Parts.IPartUsage,
+ SysML2.NET.Core.POCO.Systems.Requirements.IFramedConcernMembership => target is SysML2.NET.Core.POCO.Systems.Requirements.IConcernUsage,
+ SysML2.NET.Core.POCO.Systems.VerificationCases.IRequirementVerificationMembership => target is SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage,
+ SysML2.NET.Core.POCO.Systems.Requirements.IStakeholderMembership => target is SysML2.NET.Core.POCO.Systems.Parts.IPartUsage,
+ SysML2.NET.Core.POCO.Systems.Requirements.ISubjectMembership => target is SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage,
+ SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership => target is SysML2.NET.Core.POCO.Core.Features.IFeature,
+ SysML2.NET.Core.POCO.Systems.Cases.IObjectiveMembership => target is SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage,
+ SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership => target is SysML2.NET.Core.POCO.Core.Features.IFeature,
+ SysML2.NET.Core.POCO.Systems.Requirements.IRequirementConstraintMembership => target is SysML2.NET.Core.POCO.Systems.Constraints.IConstraintUsage,
+ SysML2.NET.Core.POCO.Kernel.Functions.IResultExpressionMembership => target is SysML2.NET.Core.POCO.Kernel.Functions.IExpression,
+ SysML2.NET.Core.POCO.Systems.States.IStateSubactionMembership => target is SysML2.NET.Core.POCO.Systems.Actions.IActionUsage,
+ SysML2.NET.Core.POCO.Systems.States.ITransitionFeatureMembership => target is SysML2.NET.Core.POCO.Kernel.Behaviors.IStep,
+ SysML2.NET.Core.POCO.Systems.Views.IViewRenderingMembership => target is SysML2.NET.Core.POCO.Systems.Views.IRenderingUsage,
+ SysML2.NET.Core.POCO.Kernel.Packages.IElementFilterMembership => target is SysML2.NET.Core.POCO.Kernel.Functions.IExpression,
+ SysML2.NET.Core.POCO.Core.Types.IFeatureMembership => target is SysML2.NET.Core.POCO.Core.Features.IFeature,
+ SysML2.NET.Core.POCO.Kernel.FeatureValues.IFeatureValue => target is SysML2.NET.Core.POCO.Kernel.Functions.IExpression,
+ SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IVariantMembership => target is SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage,
+ SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership => target is not null,
+ SysML2.NET.Core.POCO.Root.Annotations.IAnnotation => target is SysML2.NET.Core.POCO.Root.Annotations.IAnnotatingElement,
+ SysML2.NET.Core.POCO.Root.Elements.IRelationship => target is not null,
+ _ => true,
+ };
+ }
+
+ ///
+ /// Returns the simple C# interface name of the expected target type for the supplied
+ /// , used to compose the error message thrown by
+ /// AssignOwnership when the target is not a valid containment target.
+ ///
+ /// The bridge .
+ /// The expected target type's simple interface name.
+ public static string ExpectedContainmentTypeName(this IRelationship bridgeRelationship)
+ {
+ return bridgeRelationship switch
+ {
+ SysML2.NET.Core.POCO.Systems.Requirements.IActorMembership => nameof(SysML2.NET.Core.POCO.Systems.Parts.IPartUsage),
+ SysML2.NET.Core.POCO.Systems.Requirements.IFramedConcernMembership => nameof(SysML2.NET.Core.POCO.Systems.Requirements.IConcernUsage),
+ SysML2.NET.Core.POCO.Systems.VerificationCases.IRequirementVerificationMembership => nameof(SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage),
+ SysML2.NET.Core.POCO.Systems.Requirements.IStakeholderMembership => nameof(SysML2.NET.Core.POCO.Systems.Parts.IPartUsage),
+ SysML2.NET.Core.POCO.Systems.Requirements.ISubjectMembership => nameof(SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage),
+ SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature),
+ SysML2.NET.Core.POCO.Systems.Cases.IObjectiveMembership => nameof(SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage),
+ SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature),
+ SysML2.NET.Core.POCO.Systems.Requirements.IRequirementConstraintMembership => nameof(SysML2.NET.Core.POCO.Systems.Constraints.IConstraintUsage),
+ SysML2.NET.Core.POCO.Kernel.Functions.IResultExpressionMembership => nameof(SysML2.NET.Core.POCO.Kernel.Functions.IExpression),
+ SysML2.NET.Core.POCO.Systems.States.IStateSubactionMembership => nameof(SysML2.NET.Core.POCO.Systems.Actions.IActionUsage),
+ SysML2.NET.Core.POCO.Systems.States.ITransitionFeatureMembership => nameof(SysML2.NET.Core.POCO.Kernel.Behaviors.IStep),
+ SysML2.NET.Core.POCO.Systems.Views.IViewRenderingMembership => nameof(SysML2.NET.Core.POCO.Systems.Views.IRenderingUsage),
+ SysML2.NET.Core.POCO.Kernel.Packages.IElementFilterMembership => nameof(SysML2.NET.Core.POCO.Kernel.Functions.IExpression),
+ SysML2.NET.Core.POCO.Core.Types.IFeatureMembership => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature),
+ SysML2.NET.Core.POCO.Kernel.FeatureValues.IFeatureValue => nameof(SysML2.NET.Core.POCO.Kernel.Functions.IExpression),
+ SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IVariantMembership => nameof(SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage),
+ SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership => nameof(SysML2.NET.Core.POCO.Root.Elements.IElement),
+ SysML2.NET.Core.POCO.Root.Annotations.IAnnotation => nameof(SysML2.NET.Core.POCO.Root.Annotations.IAnnotatingElement),
+ SysML2.NET.Core.POCO.Root.Elements.IRelationship => nameof(SysML2.NET.Core.POCO.Root.Elements.IElement),
+ _ => nameof(IElement),
+ };
+ }
+
+ ///
+ /// Determines whether is a structurally valid containment owner for
+ /// , based on the typed property declared by the bridge's
+ /// metaclass that subsets in the KerML/SysML2
+ /// metamodel (e.g. an IFeatureMembership requires an IType owner).
+ ///
+ /// The bridge .
+ /// The candidate owner .
+ /// true if 's runtime type satisfies the owner-side
+ /// constraint; otherwise false.
+ public static bool QueryIsValidContainmentOwner(this IRelationship bridgeRelationship, IElement source)
+ {
+ return bridgeRelationship switch
+ {
+ SysML2.NET.Core.POCO.Core.Features.ICrossSubsetting => source is SysML2.NET.Core.POCO.Core.Features.IFeature,
+ SysML2.NET.Core.POCO.Core.Types.IFeatureMembership => source is SysML2.NET.Core.POCO.Core.Types.IType,
+ SysML2.NET.Core.POCO.Kernel.FeatureValues.IFeatureValue => source is SysML2.NET.Core.POCO.Core.Features.IFeature,
+ SysML2.NET.Core.POCO.Core.Features.IReferenceSubsetting => source is SysML2.NET.Core.POCO.Core.Features.IFeature,
+ SysML2.NET.Core.POCO.Core.Features.IFeatureTyping => source is SysML2.NET.Core.POCO.Core.Features.IFeature,
+ SysML2.NET.Core.POCO.Systems.Ports.IPortConjugation => source is SysML2.NET.Core.POCO.Systems.Ports.IConjugatedPortDefinition,
+ SysML2.NET.Core.POCO.Core.Classifiers.ISubclassification => source is SysML2.NET.Core.POCO.Core.Classifiers.IClassifier,
+ SysML2.NET.Core.POCO.Core.Features.ISubsetting => source is SysML2.NET.Core.POCO.Core.Features.IFeature,
+ SysML2.NET.Core.POCO.Core.Types.IConjugation => source is SysML2.NET.Core.POCO.Core.Types.IType,
+ SysML2.NET.Core.POCO.Core.Types.IDifferencing => source is SysML2.NET.Core.POCO.Core.Types.IType,
+ SysML2.NET.Core.POCO.Core.Types.IDisjoining => source is SysML2.NET.Core.POCO.Core.Types.IType,
+ SysML2.NET.Core.POCO.Core.Features.IFeatureChaining => source is SysML2.NET.Core.POCO.Core.Features.IFeature,
+ SysML2.NET.Core.POCO.Core.Features.IFeatureInverting => source is SysML2.NET.Core.POCO.Core.Features.IFeature,
+ SysML2.NET.Core.POCO.Root.Namespaces.IImport => source is SysML2.NET.Core.POCO.Root.Namespaces.INamespace,
+ SysML2.NET.Core.POCO.Core.Types.IIntersecting => source is SysML2.NET.Core.POCO.Core.Types.IType,
+ SysML2.NET.Core.POCO.Root.Namespaces.IMembership => source is SysML2.NET.Core.POCO.Root.Namespaces.INamespace,
+ SysML2.NET.Core.POCO.Core.Types.ISpecialization => source is SysML2.NET.Core.POCO.Core.Types.IType,
+ SysML2.NET.Core.POCO.Core.Features.ITypeFeaturing => source is SysML2.NET.Core.POCO.Core.Features.IFeature,
+ SysML2.NET.Core.POCO.Core.Types.IUnioning => source is SysML2.NET.Core.POCO.Core.Types.IType,
+ _ => true,
+ };
+ }
+
+ ///
+ /// Returns the simple C# interface name of the expected owner type for the supplied
+ /// , used to compose the error message thrown by
+ /// AssignOwnership when the source is not a valid containment owner.
+ ///
+ /// The bridge .
+ /// The expected owner type's simple interface name.
+ public static string ExpectedContainmentOwnerTypeName(this IRelationship bridgeRelationship)
+ {
+ return bridgeRelationship switch
+ {
+ SysML2.NET.Core.POCO.Core.Features.ICrossSubsetting => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature),
+ SysML2.NET.Core.POCO.Core.Types.IFeatureMembership => nameof(SysML2.NET.Core.POCO.Core.Types.IType),
+ SysML2.NET.Core.POCO.Kernel.FeatureValues.IFeatureValue => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature),
+ SysML2.NET.Core.POCO.Core.Features.IReferenceSubsetting => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature),
+ SysML2.NET.Core.POCO.Core.Features.IFeatureTyping => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature),
+ SysML2.NET.Core.POCO.Systems.Ports.IPortConjugation => nameof(SysML2.NET.Core.POCO.Systems.Ports.IConjugatedPortDefinition),
+ SysML2.NET.Core.POCO.Core.Classifiers.ISubclassification => nameof(SysML2.NET.Core.POCO.Core.Classifiers.IClassifier),
+ SysML2.NET.Core.POCO.Core.Features.ISubsetting => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature),
+ SysML2.NET.Core.POCO.Core.Types.IConjugation => nameof(SysML2.NET.Core.POCO.Core.Types.IType),
+ SysML2.NET.Core.POCO.Core.Types.IDifferencing => nameof(SysML2.NET.Core.POCO.Core.Types.IType),
+ SysML2.NET.Core.POCO.Core.Types.IDisjoining => nameof(SysML2.NET.Core.POCO.Core.Types.IType),
+ SysML2.NET.Core.POCO.Core.Features.IFeatureChaining => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature),
+ SysML2.NET.Core.POCO.Core.Features.IFeatureInverting => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature),
+ SysML2.NET.Core.POCO.Root.Namespaces.IImport => nameof(SysML2.NET.Core.POCO.Root.Namespaces.INamespace),
+ SysML2.NET.Core.POCO.Core.Types.IIntersecting => nameof(SysML2.NET.Core.POCO.Core.Types.IType),
+ SysML2.NET.Core.POCO.Root.Namespaces.IMembership => nameof(SysML2.NET.Core.POCO.Root.Namespaces.INamespace),
+ SysML2.NET.Core.POCO.Core.Types.ISpecialization => nameof(SysML2.NET.Core.POCO.Core.Types.IType),
+ SysML2.NET.Core.POCO.Core.Features.ITypeFeaturing => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature),
+ SysML2.NET.Core.POCO.Core.Types.IUnioning => nameof(SysML2.NET.Core.POCO.Core.Types.IType),
+ _ => nameof(IElement),
+ };
+ }
+ }
+}
+
+// ------------------------------------------------------------------------------------------------
+// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!--------
+// ------------------------------------------------------------------------------------------------
diff --git a/SysML2.NET/Extensions/ElementExtensions.cs b/SysML2.NET/Extensions/ElementExtensions.cs
index 39ace00d..fb90cfaa 100644
--- a/SysML2.NET/Extensions/ElementExtensions.cs
+++ b/SysML2.NET/Extensions/ElementExtensions.cs
@@ -23,13 +23,76 @@ namespace SysML2.NET.Extensions
using System;
using System.Linq;
+ using SysML2.NET.Core.POCO.Root.Annotations;
using SysML2.NET.Core.POCO.Root.Elements;
+ using SysML2.NET.Core.POCO.Root.Namespaces;
///
/// Extension method for the interface
///
- public static class ElementExtensions
+ public static partial class ElementExtensions
{
+ ///
+ /// Assigns the containment ownership of the to a source ,
+ /// without contributing any element to .
+ ///
+ /// The container that will own the
+ /// The to be owned by the
+ /// If or is null
+ /// If equals ,
+ /// if is an (which always owns its
+ /// member element and therefore requires the three-argument overload),
+ /// if 's runtime type does not satisfy the typed property declared by the bridge's
+ /// metaclass for its (e.g. an IFeatureMembership requires
+ /// an IType source; an IMembership requires an INamespace source — per the KerML specification),
+ /// or if already transitively contains (which would
+ /// produce a containment cycle)
+ ///
+ /// Use this overload for the "owning reference" pattern, where the owns the relationship
+ /// but the relationship merely references its other related elements rather than containing them. Typical examples
+ /// are FeatureTyping, Subsetting, Redefinition, and Conjugation: the typed/specializing
+ /// feature owns the relationship, while the referenced type/specialized feature is not an owned related element.
+ /// For the "owning containment" pattern (e.g. FeatureMembership, OwningMembership), use the
+ /// overload instead.
+ ///
+ public static void AssignOwnership(this IElement source, IRelationship bridgeRelationship)
+ {
+ if (source == null)
+ {
+ throw new ArgumentNullException(nameof(source));
+ }
+
+ if (bridgeRelationship == null)
+ {
+ throw new ArgumentNullException(nameof(bridgeRelationship));
+ }
+
+ if (source == bridgeRelationship)
+ {
+ throw new InvalidOperationException("The relationship can not own itself.");
+ }
+
+ if (bridgeRelationship is IOwningMembership)
+ {
+ throw new InvalidOperationException(
+ $"The relationship of type '{bridgeRelationship.GetType().Name}' is an IOwningMembership and always owns a member element. Use the AssignOwnership(IElement, IRelationship, IElement) overload to supply the owned target element.");
+ }
+
+ if (!bridgeRelationship.QueryIsValidContainmentOwner(source))
+ {
+ throw new InvalidOperationException(
+ $"The source of type '{source.GetType().Name}' is not a valid containment owner for the relationship of type '{bridgeRelationship.GetType().Name}'; expected an instance of '{bridgeRelationship.ExpectedContainmentOwnerTypeName()}'.");
+ }
+
+ if (IsContainedTransitivelyBy(source, bridgeRelationship))
+ {
+ throw new InvalidOperationException(
+ $"Containment cycle: the supplied source of type '{source.GetType().Name}' is already transitively contained by the bridge relationship of type '{bridgeRelationship.GetType().Name}'.");
+ }
+
+ AssignOwnershipCore(source, bridgeRelationship);
+ }
+
///
/// Assigns the containment ownership of a target to a source via the bridge
///
@@ -37,9 +100,29 @@ public static class ElementExtensions
/// The bridge
/// The contained
/// If one of the parameter is null
- /// If the equals the
- /// or if the equals the
- /// Note: The source is the container element and the target is the containee
+ /// If the equals the ,
+ /// if the equals the ,
+ /// if is neither an nor an
+ /// (the only relationship metaclasses that subset
+ /// with composite aggregation),
+ /// if 's runtime type does not satisfy the typed property declared by the bridge's
+ /// metaclass for its (e.g. an IFeatureMembership
+ /// requires an IType source; an IMembership requires an INamespace source — per the
+ /// KerML specification),
+ /// if 's runtime type does not satisfy the typed composite property declared by the
+ /// bridge's metaclass for its (e.g. an
+ /// IFeatureMembership requires an IFeature target),
+ /// or if or already transitively contains
+ /// (which would produce a containment cycle)
+ ///
+ /// Use this overload for the "owning containment" pattern, where the contains
+ /// the as one of its . The only relationship
+ /// metaclasses that can carry an owned related element are (and all its specialisations
+ /// such as FeatureMembership, ParameterMembership, VariantMembership, FeatureValue, etc.)
+ /// and . For the "owning reference" pattern (e.g. FeatureTyping, Subsetting),
+ /// use the overload instead.
+ /// Note: the source is the container element and the target is the containee.
+ ///
public static void AssignOwnership(this IElement source, IRelationship bridgeRelationship, IElement target)
{
if (source == null)
@@ -57,6 +140,11 @@ public static void AssignOwnership(this IElement source, IRelationship bridgeRel
throw new ArgumentNullException(nameof(target));
}
+ if (source == bridgeRelationship)
+ {
+ throw new InvalidOperationException("The relationship can not own itself.");
+ }
+
if (source == target)
{
throw new InvalidOperationException("The parent cannot own itself.");
@@ -67,26 +155,134 @@ public static void AssignOwnership(this IElement source, IRelationship bridgeRel
throw new InvalidOperationException("The relationship can not own itself.");
}
- // Missing logic: Target can not contain Source at any containment level
+ if (bridgeRelationship is not IOwningMembership and not IAnnotation)
+ {
+ throw new InvalidOperationException(
+ $"The relationship of type '{bridgeRelationship.GetType().Name}' is neither an IOwningMembership nor an IAnnotation and therefore cannot own a related element. Use the AssignOwnership(IElement, IRelationship) overload instead.");
+ }
+
+ if (!bridgeRelationship.QueryIsValidContainmentOwner(source))
+ {
+ throw new InvalidOperationException(
+ $"The source of type '{source.GetType().Name}' is not a valid containment owner for the relationship of type '{bridgeRelationship.GetType().Name}'; expected an instance of '{bridgeRelationship.ExpectedContainmentOwnerTypeName()}'.");
+ }
+
+ if (!bridgeRelationship.QueryIsValidForContainment(target))
+ {
+ throw new InvalidOperationException(
+ $"The target of type '{target.GetType().Name}' is not a valid containment target for the relationship of type '{bridgeRelationship.GetType().Name}'; expected an instance of '{ExpectedContainmentTypeName(bridgeRelationship)}'.");
+ }
+
+ if (IsContainedTransitivelyBy(source, bridgeRelationship))
+ {
+ throw new InvalidOperationException(
+ $"Containment cycle: the supplied source of type '{source.GetType().Name}' is already transitively contained by the bridge relationship of type '{bridgeRelationship.GetType().Name}'.");
+ }
+
+ if (IsContainedTransitivelyBy(source, target))
+ {
+ throw new InvalidOperationException(
+ $"Containment cycle: the supplied source of type '{source.GetType().Name}' is already transitively contained by the target of type '{target.GetType().Name}'.");
+ }
+
+ AssignOwnershipCore(source, bridgeRelationship);
+
+ if (target.OwningRelationship != null && target.OwningRelationship != bridgeRelationship)
+ {
+ ((IContainedRelationship)target.OwningRelationship).OwnedRelatedElement.Remove(target);
+ }
+ ((IContainedRelationship)bridgeRelationship).OwnedRelatedElement.Add(target);
+ }
+
+ ///
+ /// Performs the owner-side mutation that attaches a to a
+ /// without applying any of the public-overload guard rules.
+ ///
+ /// The container
+ /// The to be attached to the
+ ///
+ /// Internal helper shared by both AssignOwnership public overloads. Callers are responsible for argument
+ /// validation; this method performs no checks of its own.
+ ///
+ private static void AssignOwnershipCore(IElement source, IRelationship bridgeRelationship)
+ {
if (bridgeRelationship.OwningRelatedElement != null && bridgeRelationship.OwningRelatedElement != source)
{
((IContainedElement)bridgeRelationship.OwningRelatedElement).OwnedRelationship.Remove(bridgeRelationship);
}
((IContainedRelationship)bridgeRelationship).OwningRelatedElement = source;
-
+
if (!source.OwnedRelationship.Contains(bridgeRelationship))
{
((IContainedElement)source).OwnedRelationship.Add(bridgeRelationship);
}
+ }
- if (target.OwningRelationship != null && target.OwningRelationship != bridgeRelationship)
+ ///
+ /// Determines whether is currently — directly or transitively — contained by
+ /// via the chain of and
+ /// pointers.
+ ///
+ /// The whose ancestor chain is walked
+ /// The looked for in the ancestor chain
+ /// true if appears as an
+ /// at any level above ; otherwise false
+ ///
+ /// Used by the public AssignOwnership overloads to reject containment cycles before any state mutation.
+ ///
+ private static bool IsContainedTransitivelyBy(IElement element, IRelationship candidateBridge)
+ {
+ var current = element;
+
+ while (current != null)
{
- ((IContainedRelationship)target.OwningRelationship).OwnedRelatedElement.Remove(target);
- }
+ var owningRelationship = current.OwningRelationship;
- ((IContainedRelationship)bridgeRelationship).OwnedRelatedElement.Add(target);
+ if (owningRelationship == null)
+ {
+ return false;
+ }
+
+ if (owningRelationship == candidateBridge)
+ {
+ return true;
+ }
+
+ current = owningRelationship.OwningRelatedElement;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Determines whether is currently — directly or transitively — contained by
+ /// via the chain of and
+ /// pointers.
+ ///
+ /// The whose ancestor chain is walked
+ /// The looked for in the ancestor chain
+ /// true if appears as an ancestor of ;
+ /// otherwise false
+ ///
+ /// Used by the public AssignOwnership overloads to reject containment cycles before any state mutation.
+ ///
+ private static bool IsContainedTransitivelyBy(IElement element, IElement candidateAncestor)
+ {
+ var current = element.OwningRelationship?.OwningRelatedElement;
+
+ while (current != null)
+ {
+ if (current == candidateAncestor)
+ {
+ return true;
+ }
+
+ current = current.OwningRelationship?.OwningRelatedElement;
+ }
+
+ return false;
}
}
}
diff --git a/TESTING.md b/TESTING.md
new file mode 100644
index 00000000..7c6bf617
--- /dev/null
+++ b/TESTING.md
@@ -0,0 +1,248 @@
+# TESTING.md — Unit-test conventions for SysML2.NET
+
+> **When to read this file:** load it whenever you are writing or modifying unit tests in any `*.Tests/` project of this solution. It is the authoritative companion to `CLAUDE.md` for everything test-related. Outside of test authoring, you do not need it.
+
+This document captures the conventions enforced for the NUnit test fixtures across `SysML2.NET.Tests/`, `SysML2.NET.Serializer.Json.Tests/`, `SysML2.NET.Serializer.Xmi.Tests/`, `SysML2.NET.Extensions.Tests/`, `SysML2.NET.CodeGenerator.Tests/`, and every other `*.Tests` project. The rules below have been distilled from explicit author-review feedback — diverging from them produces churn and PR back-and-forth, so treat them as binding.
+
+---
+
+## 1. Test framework
+
+- **Framework:** NUnit
+- **Fixture attribute:** `[TestFixture]`
+- **Test attribute:** `[Test]`
+- **Project layout:** mirror the production namespace under the test project (e.g. `SysML2.NET/Extend/FeatureExtensions.cs` → `SysML2.NET.Tests/Extend/FeatureExtensionsTestFixture.cs`).
+
+---
+
+## 2. One `[Test]` per method-under-test (default)
+
+**Default to a single `[Test]` method per `Compute*` / method-under-test** and pack every scenario — happy path, edge cases, null guards, alternate inputs — into multiple `Assert.That` calls inside that one test.
+
+**Why this is the default:** keeps the test list compact, removes duplicated arrange boilerplate, and makes the intent obvious — one `Compute*` operation has one combined coverage test. It works particularly well for the `Compute*` derivation tests where scenarios build on one shared subject incrementally (null guard → empty subject → wrong-target negative → positive populated case → multi-element ordered case).
+
+### When separated `[Test]` methods ARE appropriate
+
+The combined-form is the default, **not absolute**. Split into separate `[Test]` methods when **each scenario has a genuinely distinct, complex setup** that would tangle if packed into one test:
+
+- different bridge / relationship metaclass per scenario,
+- a multi-step containment chain that exists only for one scenario,
+- an incompatible-source-or-target shape that requires its own local POCO fixtures.
+
+In those cases, separate `[Test]` methods with descriptive scenario suffixes are clearer than one mega-test with branching arrange blocks. The canonical *separated* example is `SysML2.NET.Tests/Extensions/ElementExtensionsTestFixture.cs` — each `AssignOwnership_*` method exercises a distinct scenario (cycle, incompatible owner type, incompatible target type, valid parameters, …) with its own local construction.
+
+Rule of thumb: if you find yourself naming locals `feature1` / `feature2` / `bridge1` / `bridge2` or writing more than ~3 lines of arrange between asserts, that scenario probably wants its own `[Test]`.
+
+### Canonical example
+
+`SysML2.NET.Tests/Extend/FeatureExtensionsTestFixture.cs`:
+
+```csharp
+[Test]
+public void VerifyComputeOwnedFeatureChaining()
+{
+ Assert.That(() => ((IFeature)null).ComputeOwnedFeatureChaining(), Throws.TypeOf());
+
+ var feature = new Feature();
+
+ Assert.That(feature.ComputeOwnedFeatureChaining(), Has.Count.EqualTo(0));
+
+ var subsetting = new Subsetting();
+ feature.AssignOwnership(subsetting);
+
+ Assert.That(feature.ComputeOwnedFeatureChaining(), Has.Count.EqualTo(0));
+
+ var chainingTarget1 = new Feature();
+ var chaining1 = new FeatureChaining { ChainingFeature = chainingTarget1 };
+ feature.AssignOwnership(chaining1);
+
+ var chainingTarget2 = new Feature();
+ var chaining2 = new FeatureChaining { ChainingFeature = chainingTarget2 };
+ feature.AssignOwnership(chaining2);
+
+ var result = feature.ComputeOwnedFeatureChaining();
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(result, Has.Count.EqualTo(2));
+ Assert.That(result[0], Is.SameAs(chaining1));
+ Assert.That(result[1], Is.SameAs(chaining2));
+ }
+}
+```
+
+One test, five logical scenarios: null subject → empty subject → unrelated-ownership subject → first populated case → multiple-element ordered case.
+
+---
+
+## 3. Cover positive AND negative cases in the same test
+
+Each `[Test]` MUST exercise both directions of the method-under-test. Build up state incrementally and assert the delta after each arrange step.
+
+**Negative cases (always include where applicable):**
+
+| Negative scenario | Assertion |
+| -------------------------- | ------------------------------------------------------------------------ |
+| Null subject | `Assert.That(() => …, Throws.TypeOf())` |
+| Empty / unpopulated subject | `Assert.That(result, Has.Count.EqualTo(0))` |
+| Wrong-target scenario | populated subject whose owned elements don't match the rule's filter — assert `Has.Count.EqualTo(0)` |
+| Out-of-scope stub | `Assert.That(() => …, Throws.TypeOf())` — see §9 |
+
+**Positive cases:** populated subject, multiple elements with verified ordering, dedup semantics where the OCL says so.
+
+**Pre/post-arrange assert ladder** — see `SysML2.NET.Tests/Extend/NamespaceExtensionsTestFixture.cs`:
+
+```csharp
+[Test]
+public void VerifyComputeImportedMembership()
+{
+ Assert.That(() => ((INamespace)null).ComputeImportedMembership(), Throws.TypeOf());
+
+ var namespaceElement = new Namespace();
+
+ Assert.That(namespaceElement.ComputeImportedMembership(), Has.Count.EqualTo(0));
+
+ var importedNamespace = new Namespace();
+ var importedElement = new Definition { DeclaredName = "imported" };
+ var importedMembership = new OwningMembership { Visibility = VisibilityKind.Public };
+ importedNamespace.AssignOwnership(importedMembership, importedElement);
+
+ var namespaceImport = new NamespaceImport { ImportedNamespace = importedNamespace };
+ namespaceElement.AssignOwnership(namespaceImport);
+
+ Assert.That(namespaceElement.ComputeImportedMembership(), Is.EquivalentTo([importedMembership]));
+}
+```
+
+The wrong-target negative case is illustrated in `FeatureExtensionsTestFixture.VerifyComputeOwnedFeatureInverting` — a `FeatureInverting` pointing at *another* feature is added first (asserted to produce count 0) **before** the positive-case `FeatureInverting` pointing at the subject is added.
+
+---
+
+## 4. Always use `Assert.That` — no legacy forms
+
+Every assertion (including exception assertions) MUST use the fluent `Assert.That(...)` form.
+
+**Forbidden legacy forms:**
+
+| Don't write | Write instead |
+| ------------------------ | ------------------------------------------------------------------------ |
+| `Assert.Throws(…)` | `Assert.That(() => …, Throws.TypeOf())` |
+| `Assert.IsTrue(x)` | `Assert.That(x, Is.True)` |
+| `Assert.IsFalse(x)` | `Assert.That(x, Is.False)` |
+| `Assert.AreEqual(a, b)` | `Assert.That(b, Is.EqualTo(a))` |
+| `Assert.IsNull(x)` | `Assert.That(x, Is.Null)` |
+| `Assert.IsNotNull(x)` | `Assert.That(x, Is.Not.Null)` |
+
+**Exception-message assertions** are still a single fluent chain — no scope wrapper:
+
+```csharp
+Assert.That(() => parser.Parse(input),
+ Throws.TypeOf().With.Message.Contains("unexpected token"));
+```
+
+**Why:** consistency and readability. The mixed-style `Assert.Throws` / `Assert.IsTrue` family was explicitly called out as non-idiomatic.
+
+---
+
+## 5. `Assert.EnterMultipleScope` — use only for consecutive asserts
+
+Use `using (Assert.EnterMultipleScope()) { … }` **only** when **two or more consecutive** `Assert.That` calls follow each other and you want all of them to be evaluated even if earlier ones fail.
+
+**Do:**
+
+```csharp
+using (Assert.EnterMultipleScope())
+{
+ Assert.That(result, Has.Count.EqualTo(2));
+ Assert.That(result[0], Is.SameAs(chaining1));
+ Assert.That(result[1], Is.SameAs(chaining2));
+}
+```
+
+**Don't** wrap a single fluent assertion in a scope — even when it is a long chain like `Throws.TypeOf().With.Message.Contains("…")`. A single assertion does not need it.
+
+**Don't** put the early null-guard `Assert.That` inside a scope that ends before the next `Assert.That` — those are standalone and stay outside any scope.
+
+---
+
+## 6. Test naming
+
+**Combined-form tests (the default — §2):**
+
+- One test method = `public void Verify{MethodUnderTest}()` (e.g. `VerifyComputeOwnedFeatureChaining`, `VerifyComputeImportedMembership`).
+- Matches the existing convention across `SysML2.NET.Tests/Extend/*` and is what reviewers will look for.
+- Do **not** suffix with scenario names — scenarios live inside the body.
+
+**Separated-form tests (the §2 exception):**
+
+- Use `{MethodUnderTest}_{ScenarioDescription}_{ExpectedOutcome}` (e.g. `AssignOwnership_WithIncompatibleOwnerType_ThrowsInvalidOperationException`, `AssignOwnership_WithContainmentCycle_ThrowsInvalidOperationException`).
+- Each method should be self-explanatory from its name alone — the scenario is in the title because it's distinct enough to deserve its own test.
+
+---
+
+## 7. Arrange / Act / Assert inside a combined test
+
+- Separate arrange / act / assert blocks with **blank lines**.
+- Add short inline comments only when the delta from the previous step is non-obvious.
+- Re-call the method-under-test after each new arrange step so each `Assert.That` describes one increment of state.
+- Reuse the same locals (`feature`, `namespaceElement`, `result`) across the test — don't fork into `feature1` / `feature2` unless you really need two independent subjects.
+
+---
+
+## 8. Assertion idiom preferences
+
+| Concern | Prefer | Avoid |
+| ----------------------- | ------------------------------------------------------------ | ------------------------------------------------- |
+| Collection count | `Has.Count.EqualTo(n)` | `result.Count, Is.EqualTo(n)` |
+| Reference equality (POCO) | `Is.SameAs(expected)` | `Is.EqualTo(expected)` (relies on `Equals`) |
+| Order-irrelevant collection equality | `Is.EquivalentTo([…])` | `Is.EqualTo([…])` (forces order) |
+| Order-relevant collection equality | `Is.EqualTo([…])` | hand-rolled `for (int i …)` loops |
+| First / last element | `result[0]` / `result[^1]` (indexer) | `result.First()` / `result.Last()` (LINQ) |
+| Range / slice | `result[1..^1]` | `result.Skip(1).Take(n)` |
+| String empty/null | `Is.Null.Or.Empty` | `string.IsNullOrEmpty(x), Is.True` |
+| Substring | `Does.Contain("…")` or `Contains.Substring("…")` | `x.Contains("…"), Is.True` |
+
+The LINQ/indexer preference aligns with the project-wide quality rule documented in `CLAUDE.md` ("Prefer indexer syntax and range syntax over LINQ methods when applicable").
+
+---
+
+## 9. Scope discipline — assert, don't fix
+
+If a test you are writing crosses into a method that currently throws `NotSupportedException` because it's an out-of-scope stub, **assert that exception** — do **not** implement the stub to make the test pass:
+
+```csharp
+Assert.That(() => member.ComputeOwnedMemberFeature(),
+ Throws.TypeOf());
+```
+
+When the stub is implemented later (in its own scoped change), the assertion will be replaced with the real positive case in the same `[Test]`.
+
+This is the testing-side companion to the broader scope-discipline feedback: a task scoped to file X must not silently grow to also modify file Y, even if file Y is "one line away from working".
+
+---
+
+## 10. Anti-pattern checklist (what NOT to do)
+
+- ❌ Splitting one method-under-test into many `…_WhenX_DoesY` tests **when the scenarios share setup and could trivially combine** (combined-form is the default — see §2). Splitting is acceptable when each scenario has a genuinely distinct, complex setup.
+- ❌ `Assert.Throws` / `Assert.IsTrue` / `Assert.AreEqual` / `Assert.IsNull` (any legacy NUnit API).
+- ❌ Wrapping a single `Assert.That` inside `Assert.EnterMultipleScope`.
+- ❌ Putting standalone, non-adjacent asserts inside a scope that's already closed.
+- ❌ Suffixing the combined-form `Verify{MethodUnderTest}` test name with a scenario (`VerifyComputeFooWhenNull`) — scenarios live in the body. (The separated-form `{Method}_{Scenario}_{Outcome}` style is the §6 exception and is fine when §2's separated-form criteria apply.)
+- ❌ Implementing an out-of-scope stub from within a test fixture change.
+- ❌ Asserting only the positive case (or only the negative case) — every test covers both.
+- ❌ `result.First()` / `result.Last()` / `result.Count() == 0` where indexer / `Has.Count.EqualTo` works.
+
+---
+
+## 11. Reference fixtures
+
+When in doubt, model new fixtures on these (they reflect the current canonical styles):
+
+**Combined-form (the default — §2):**
+
+- `SysML2.NET.Tests/Extend/FeatureExtensionsTestFixture.cs` — null guard → empty subject → wrong-target negative → populated positive → multi-assert scope.
+- `SysML2.NET.Tests/Extend/NamespaceExtensionsTestFixture.cs` — pre/post-arrange assert ladder, `Is.EquivalentTo([…])` collection idiom, no `EnterMultipleScope` when assertions are non-consecutive.
+
+**Separated-form (the §2 exception):**
+
+- `SysML2.NET.Tests/Extensions/ElementExtensionsTestFixture.cs` — each `AssignOwnership_*` `[Test]` exercises a distinct scenario (cycle, incompatible owner type, incompatible target type, null guards, valid parameters, reference-only bridge, …) with its own local POCO construction. Scenario-suffixed names are appropriate here because each scenario is too distinct to combine cleanly.