diff --git a/ECoreNetto.Tests/Resource/GuardedGetEObjectTestFixture.cs b/ECoreNetto.Tests/Resource/GuardedGetEObjectTestFixture.cs
new file mode 100644
index 0000000..202f755
--- /dev/null
+++ b/ECoreNetto.Tests/Resource/GuardedGetEObjectTestFixture.cs
@@ -0,0 +1,191 @@
+// -------------------------------------------------------------------------------------------------
+//
+//
+// Copyright 2017-2025 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 ECoreNetto.Tests.Resource
+{
+ using System;
+ using System.IO;
+
+ using ECoreNetto.Resource;
+
+ using NUnit.Framework;
+
+ ///
+ /// Suite of tests that verify the typed, guarded resolver and the
+ /// unsafe-cast call sites that use it (see issue #29 / #32). Unresolved or wrong-typed references must
+ /// yield a clear that names the offending fragment, instead of a
+ /// vague , or stack overflow.
+ ///
+ [TestFixture]
+ public class GuardedGetEObjectTestFixture
+ {
+ private ResourceSet resourceSet = null!;
+ private Resource resource = null!;
+
+ [SetUp]
+ public void SetUp()
+ {
+ this.resourceSet = new ResourceSet();
+ this.resource = new Resource { ResourceSet = this.resourceSet };
+ this.resourceSet.Resources.Add(this.resource);
+ }
+
+ [Test]
+ public void Verify_that_a_cached_object_of_the_expected_type_is_returned()
+ {
+ var eClass = new EClass(this.resource);
+ this.resource.Cache.Add("a-fragment", eClass);
+
+ Assert.That(this.resource.GetEObject("a-fragment"), Is.SameAs(eClass));
+ }
+
+ [Test]
+ public void Verify_that_resolving_an_EClass_reference_to_a_wrong_type_throws_a_descriptive_exception()
+ {
+ // an EDataType is an EClassifier but not an EClass; this is what an EClass.eSuperTypes
+ // pointing at a datatype would yield
+ this.resource.Cache.Add("super-type", new EDataType(this.resource));
+
+ var exception = Assert.Throws(
+ () => this.resource.GetEObject("super-type"));
+ var message = exception!.Message;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(message, Does.Contain("super-type"));
+ Assert.That(message, Does.Contain(nameof(EClass)));
+ Assert.That(message, Does.Contain(nameof(EDataType)));
+ });
+ }
+
+ [Test]
+ public void Verify_that_resolving_an_EClassifier_reference_to_a_wrong_type_throws_a_descriptive_exception()
+ {
+ // an EReference is not an EClassifier; this is what an eType pointing at a feature would yield
+ this.resource.Cache.Add("the-type", new EReference(this.resource));
+
+ var exception = Assert.Throws(
+ () => this.resource.GetEObject("the-type"));
+ var message = exception!.Message;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(message, Does.Contain("the-type"));
+ Assert.That(message, Does.Contain(nameof(EClassifier)));
+ });
+ }
+
+ [Test]
+ public void Verify_that_resolving_an_EReference_reference_to_a_wrong_type_throws_a_descriptive_exception()
+ {
+ // an EAttribute is an EStructuralFeature but not an EReference; this is what an eOpposite
+ // pointing at an attribute would yield
+ this.resource.Cache.Add("the-opposite", new EAttribute(this.resource));
+
+ var exception = Assert.Throws(
+ () => this.resource.GetEObject("the-opposite"));
+ var message = exception!.Message;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(message, Does.Contain("the-opposite"));
+ Assert.That(message, Does.Contain(nameof(EReference)));
+ });
+ }
+
+ [Test]
+ public void Verify_that_resolving_an_unresolvable_fragment_throws_a_descriptive_exception()
+ {
+ // the fragment is not in the cache, is not a known ECore type, and does not point at another
+ // .ecore resource: the base GetEObject returns null and the typed resolver reports it clearly
+ var exception = Assert.Throws(
+ () => this.resource.GetEObject("does-not-exist"));
+ var message = exception!.Message;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(message, Does.Contain("does-not-exist"));
+ Assert.That(message, Does.Contain("null (unresolved)"));
+ });
+ }
+
+ [Test]
+ public void Verify_that_the_base_resolver_returns_null_for_an_unresolvable_fragment()
+ {
+ Assert.That(this.resource.GetEObject("does-not-exist"), Is.Null);
+ }
+
+ [Test]
+ public void Verify_that_an_unresolved_eSuperTypes_reference_is_reported_when_loading()
+ {
+ // EClass.SetProperties resolves eSuperTypes via GetEObject
+ const string model =
+ "\r\n" +
+ "\r\n" +
+ " \r\n" +
+ "";
+
+ // the file name must match the root package name so the implicit '#//' reference,
+ // which is rewritten to '.ecore#//...', resolves back to this resource
+ var resource = this.CreateResourceForContent("superpkg.ecore", model);
+
+ var exception = Assert.Throws(() => resource.Load(null));
+ Assert.That(exception!.Message, Does.Contain("DoesNotExist"));
+ }
+
+ [Test]
+ public void Verify_that_an_unresolved_eType_reference_is_reported_when_loading()
+ {
+ // ETypedElement.SetProperties resolves eType via GetEObject
+ const string model =
+ "\r\n" +
+ "\r\n" +
+ " \r\n" +
+ " \r\n" +
+ " \r\n" +
+ "";
+
+ // the file name must match the root package name so the implicit '#//' reference,
+ // which is rewritten to '.ecore#//...', resolves back to this resource
+ var resource = this.CreateResourceForContent("typepkg.ecore", model);
+
+ var exception = Assert.Throws(() => resource.Load(null));
+ Assert.That(exception!.Message, Does.Contain("Missing"));
+ }
+
+ ///
+ /// Writes the provided to a file in the test directory and
+ /// creates a for it.
+ ///
+ private Resource CreateResourceForContent(string fileName, string content)
+ {
+ var path = Path.Combine(TestContext.CurrentContext.TestDirectory, fileName);
+ File.WriteAllText(path, content);
+
+ var uri = new Uri(Path.GetFullPath(path));
+
+ return this.resourceSet.CreateResource(uri);
+ }
+ }
+}
diff --git a/ECoreNetto/ModelElement/NamedElement/Classifier/EClass.cs b/ECoreNetto/ModelElement/NamedElement/Classifier/EClass.cs
index 3d4b4d0..28c5f1a 100644
--- a/ECoreNetto/ModelElement/NamedElement/Classifier/EClass.cs
+++ b/ECoreNetto/ModelElement/NamedElement/Classifier/EClass.cs
@@ -183,7 +183,7 @@ internal override void SetProperties()
var typeNames = output.Split(' ');
foreach (var typeName in typeNames)
{
- this.ESuperTypes.Add((EClass)this.EResource.GetEObject(typeName));
+ this.ESuperTypes.Add(this.EResource.GetEObject(typeName));
}
}
}
diff --git a/ECoreNetto/ModelElement/NamedElement/ETypedElement.cs b/ECoreNetto/ModelElement/NamedElement/ETypedElement.cs
index b02b107..458a8e0 100644
--- a/ECoreNetto/ModelElement/NamedElement/ETypedElement.cs
+++ b/ECoreNetto/ModelElement/NamedElement/ETypedElement.cs
@@ -136,7 +136,7 @@ internal override void SetProperties()
var parts = output.Split(' ');
var typeName = parts[parts.Length - 1];
- this.EType = (EClassifier)this.EResource.GetEObject(typeName);
+ this.EType = this.EResource.GetEObject(typeName);
}
}
}
diff --git a/ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EReference.cs b/ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EReference.cs
index 7f2fb18..b36f667 100644
--- a/ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EReference.cs
+++ b/ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EReference.cs
@@ -119,7 +119,7 @@ internal override void SetProperties()
if (this.Attributes.TryGetValue("eOpposite", out output))
{
var typeName = output;
- this.EOpposite = (EReference)this.EResource.GetEObject($"EStructuralFeature::{typeName}");
+ this.EOpposite = this.EResource.GetEObject($"EStructuralFeature::{typeName}");
}
}
}
diff --git a/ECoreNetto/Resource/Resource.cs b/ECoreNetto/Resource/Resource.cs
index 0c16cae..f429811 100644
--- a/ECoreNetto/Resource/Resource.cs
+++ b/ECoreNetto/Resource/Resource.cs
@@ -225,7 +225,7 @@ public string GetURIFragment(EObject eObject)
///
/// The resolved object for the given fragment, or null if it can't be resolved.
///
- public EObject GetEObject(string uriFragment)
+ public EObject? GetEObject(string uriFragment)
{
this.logger.LogTrace("Getting EObject for resources {0}", uriFragment);
@@ -254,7 +254,11 @@ public EObject GetEObject(string uriFragment)
var uriFragments = uriFragment.Split('#');
if (!uriFragments[0].Contains(".ecore"))
{
- throw new ArgumentException($"The resource {uriFragments[0]} is invalid.");
+ // the fragment does not point at another .ecore resource and was not found in
+ // this resource's cache or the known ECore types: it cannot be resolved.
+ this.logger.LogTrace("EObject using uri fragment '{0}' could not be resolved", uriFragment);
+
+ return null;
}
var index = this.URI.AbsolutePath.LastIndexOf('/');
@@ -274,11 +278,50 @@ public EObject GetEObject(string uriFragment)
resource.Load(null);
}
+ if (resource == this)
+ {
+ // the fragment points back at the current resource but was not found in its cache:
+ // it cannot be resolved. Returning here avoids unbounded self-recursion.
+ this.logger.LogTrace("EObject using uri fragment '{0}' could not be resolved", uriFragment);
+
+ return null;
+ }
+
return resource.GetEObject(uriFragment);
}
///
- /// Saves the resource using the specified options.
+ /// Resolves the given and returns it typed as .
+ ///
+ ///
+ /// The expected subtype of the resolved object.
+ ///
+ ///
+ /// the fragment to resolve.
+ ///
+ ///
+ /// The resolved object, typed as .
+ ///
+ ///
+ /// Thrown when the cannot be resolved, or resolves to an object
+ /// that is not a . The exception message names the offending fragment.
+ ///
+ public T GetEObject(string uriFragment) where T : EObject
+ {
+ var eObject = this.GetEObject(uriFragment);
+
+ if (eObject is not T typedEObject)
+ {
+ throw new InvalidOperationException(
+ $"The reference '{uriFragment}' could not be resolved to a '{typeof(T).Name}'; " +
+ $"it resolved to '{(eObject == null ? "null (unresolved)" : eObject.GetType().Name)}'.");
+ }
+
+ return typedEObject;
+ }
+
+ ///
+ /// Saves the resource using the specified options.
///
///
/// Options are handled generically as feature-to-setting entries; the resource will ignore options it doesn't recognize.