Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## Version 1.2.3

### Fixes

- Fixed a regression from 1.2.1 where component and asset bindings on an abstract or base type (e.g. `BindComponent<Collider>()`) no longer resolved derived instances. Concrete-type candidate filtering now uses `IsAssignableFrom` instead of exact type equality, so a binding resolves the bound type or any subclass, matching Unity's `GetComponent<T>()` semantics. Interface plus concrete bindings still require candidates to satisfy both types.

### Tests

- Added regression coverage for base and abstract class component resolution, and for interface plus concrete bindings resolving a subclass of the concrete type.

## Version 1.2.2

### Unity 6000.4 and 6000.5 support
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,10 +198,10 @@ private static IEnumerable<T> GetTypeAndDependencyFilteredCandidates<T>(
IEnumerable<T> candidates) where T : Object
{
if (bindingNode.InterfaceType != null)
candidates = candidates?.Where(asset => bindingNode.InterfaceType.IsAssignableFrom(asset.GetType()));
candidates = candidates?.Where(x => bindingNode.InterfaceType.IsAssignableFrom(x.GetType()));

if (bindingNode.ConcreteType != null)
candidates = candidates?.Where(asset => asset.GetType() == bindingNode.ConcreteType);
candidates = candidates?.Where(x => bindingNode.ConcreteType.IsAssignableFrom(x.GetType()));

if (candidates != null && bindingNode.DependencyFilters.Count > 0)
try
Expand Down
2 changes: 1 addition & 1 deletion UnityProject/Saneject/Assets/Plugins/Saneject/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "com.alexanderlarsen.saneject",
"author": "Alexander Larsen",
"displayName": "Saneject",
"version": "1.2.2",
"version": "1.2.3",
"description": "Inject dependencies in the Unity Editor, not Play Mode, by writing them directly into serialized fields at edit-time using familiar DI APIs, so everything stays visible in the Inspector, including interfaces.\n\nNo runtime container. No startup cost. No hidden wiring. No weird lifecycles. Just simple, deterministic edit-time DI that works with Unity, not around it.",
"documentationUrl": "https://github.com/alexanderlarsen/Saneject/blob/main/README.md",
"changelogUrl": "https://github.com/alexanderlarsen/Saneject/blob/main/CHANGELOG.md",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,49 @@ public void Inject_TInterfaceTConcrete_WHEN_MultipleInterfaceImplementers_Inject
Assert.That(dependency, Is.Not.Null);
Assert.That(target.dependency, Is.EqualTo(dependency));
}

[Test]
public void Inject_TConcrete_WHEN_CandidateIsSubclass_ResolvesSubclassToConcreteField()
{
// Set up scene
TestScene scene = TestScene.Create(roots: 1, width: 3, depth: 3);
TestScope scope = scene.Add<TestScope>("Root 1");
SingleConcreteComponentTarget target = scene.Add<SingleConcreteComponentTarget>("Root 1/Child 1");

// Find dependency (a subclass of the bound concrete type)
DerivedComponentDependency dependency = scene.Add<DerivedComponentDependency>("Root 1/Child 1/Child 1");

// Bind the base type; resolution should behave like GetComponent<ComponentDependency>() and match the subclass
scope.BindComponent<ComponentDependency>().FromDescendants(includeSelf: false);

// Inject
InjectionRunner.Run(scene.Roots, ContextWalkFilter.SceneObjects);

// Assert
Assert.That(dependency, Is.Not.Null);
Assert.That(target.dependency, Is.EqualTo(dependency));
}

[Test]
public void Inject_TInterfaceTConcrete_WHEN_CandidateIsSubclassOfConcrete_ResolvesSubclassFulfillingBoth()
{
// Set up scene
TestScene scene = TestScene.Create(roots: 1, width: 3, depth: 3);
TestScope scope = scene.Add<TestScope>("Root 1");
SingleInterfaceTarget target = scene.Add<SingleInterfaceTarget>("Root 1/Child 1");

// Find dependency (subclass of the bound concrete, also implements the interface)
DerivedComponentDependency dependency = scene.Add<DerivedComponentDependency>("Root 1/Child 1/Child 1");

// Bind interface + concrete; the candidate must be assignable to both
scope.BindComponent<IDependency, ComponentDependency>().FromDescendants(includeSelf: false);

// Inject
InjectionRunner.Run(scene.Roots, ContextWalkFilter.SceneObjects);

// Assert
Assert.That(dependency, Is.Not.Null);
Assert.That(target.dependency, Is.EqualTo(dependency));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Tests.Saneject.Fixtures.Scripts.Dependencies
{
public class DerivedComponentDependency : ComponentDependency
{
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading