-
Notifications
You must be signed in to change notification settings - Fork 420
Dependency Injection
In osu!framework, we support dependency injection at a Drawable
level. Internally, this is done via DependencyContainer
s, which are passed down the hierarchy and can be overridden at any point for further customisation (or replacement) by a child.
The general usage for this is to fulfill a dependency that can come from a parent (potentially many levels above the point of usage). It is important to understand the general concept of dependency injection before reading on.
osu!framework's dependency injection mechanism heavily leans on C# attributes, namely [Cached]
, [Resolved]
, and [BackgroundDependencyLoader]
. Setting the dependencies up is done via one of two pathways: source generation and reflection. Understanding this is key, as the source generation pathway benefits from compile-time optimisations, but requires consumers to adjust their code accordingly.
Since the 2022.1126.0 release, the primary supported implementation of dependency injection relies on source generators. The primary implications of this for framework consumers are as follows:
- For the source generator-based dependency injection to work,
Drawable
classes must bepartial
so that the source generator can inject the DI machinery into the class. Non-compliant drawables will raise theOFSG001
code inspection. - In more complicated custom DI usages, if it is desired to
.Inject()
dependencies into a custom non-drawable class, it must implement the markerIDependencyInjectionCandidate
interface.
The implementation of the source generator can be viewed here.
For a fast compile-run cycle, source generators are by default only ran on release builds. Debug builds will use the reflection pathway as a fallback.
The original, legacy implementation of dependency injection heavily uses reflection. It will be used if user drawables are not marked partial
, as the source generator cannot attach its own code to such drawables.
Since the source generator pathway was introduced, this implementation is supported for backwards compatibility, but generally not recommended for new projects.
There are a few ways dependencies can be cached (stored) and resolved (retrieved):
This is the simplest implementation.
/// <summary>
/// A class which caches something for use by children.
/// </summary>
public partial class MyGame : Game
{
[Cached]
protected readonly MyStore Store = new MyStore();
public MyGame()
{
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Child = new MyComponent()
}
},
};
}
}
/// <summary>
/// A component that consumed the cached class.
/// </summary>
public partial class MyComponent : CompositeDrawable
{
[Resolved]
protected MyStore FetchedStore { get; private set; } = null!;
protected override void LoadComplete()
{
base.LoadComplete();
InternalChild = new SpriteText
{
Text = FetchedStore.GetAwesomeThing()
};
}
}
/// <summary>
/// A class which is to be cached via DI.
/// </summary>
public class MyStore
{
public string GetAwesomeThing() => "awesome thing!";
}
Members marked with either of these attributes are cached or resolved in their respective classes before the [BackgroundDependencyLoader]
-annotated method is run.
This can be useful if you want to ensure everything happens in the (potentially asynchronous) load()
method.
/// <summary>
/// A class which caches something for use by children.
/// </summary>
public partial class MyGame : Game
{
[Cached]
protected readonly MyStore Store = new MyStore();
public MyGame()
{
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Child = new MyComponent()
}
},
};
}
}
/// <summary>
/// A component that consumed the cached class.
/// </summary>
public partial class MyComponent : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load(MyStore store)
{
InternalChild = new SpriteText
{
Text = store.GetAwesomeThing()
};
}
}
/// <summary>
/// An class which is to be cached via DI.
/// </summary>
public class MyStore
{
public string GetAwesomeThing() => "awesome thing!";
}
Some more advanced scenarios may require use of this method instead of the [Cached]
attribute, such as if late initialisation of the cacheable objects is required.
/// <summary>
/// A class which caches something for use by children.
/// </summary>
public partial class MyGame : Game
{
protected MyStore Store;
public MyGame()
{
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Child = new MyComponent()
}
},
};
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.Cache(Store = new MyStore());
return dependencies;
}
}
Note that the DependencyContainer
class exposes two methods for caching dependencies:
-
.Cache()
will always cache the dependency using its runtime, most derived type. The implications of this are demonstrated by the following example:public abstract class BaseDependency { } public class DerivedDependency : BaseDependency { } public partial class DependencyProvider { private BaseDependency dependency; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); dependencies.Cache(dependency = new DerivedDependency()); return dependencies; } } public partial class DependencyConsumer { [Resolved] private BaseDependency baseDependency { get; set; } // WRONG - will fail at runtime [Resolved] private DerivedDependency derivedDependency { get; set; } // OK }
-
.CacheAs<T>()
will cache the dependency using its declared type, as demonstrated by the following example:public abstract class BaseDependency { } public class DerivedDependency : BaseDependency { } public partial class DependencyProvider { private BaseDependency dependency; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); dependencies.CacheAs(dependency = new DerivedDependency()); return dependencies; } } public partial class DependencyConsumer { [Resolved] private BaseDependency baseDependency { get; set; } // OK [Resolved] private DerivedDependency derivedDependency { get; set; } // WRONG - will fail at runtime }
Drawable classes themselves can be annotated with the [Cached]
attribute. In that case, the attribute is interpreted such that all instances of the drawable class will cache themselves to all of their children.
The caching will use the type at the point of declaration. To illustrate, given the following structure:
[Cached]
public partial class A : Drawable { }
public partial class B : A { }
the following things will happen:
- Instances of
A
will cache themselves to their children using typeA
. - Instances of
B
will cache themselves to their children using typeA
. - Instances of
B
will not cache themselves to their children using typeB
. For that to happen, the[Cached]
attribute would have to be repeated on typeB
.
A variant of type-based caching above is available via interfaces. Interfaces can be annotated with [Cached]
; every Drawable
will cache itself to its children using every interface type annotated with [Cached]
that it implements. As an example:
[Cached]
public interface IFirstInterface { }
[Cached]
public interface ISecondInterface { }
public interface IThirdInterface : ISecondInterface { }
public partial class Dependency : Drawable, IFirstInterface, IThirdInterface { }
all instances of Dependency
:
- will cache themselves to children as
IFirstInterface
, - will cache themselves to children as
ISecondInterface
(transitively viaIThirdInterface
), - will not cache themselves to children as
IThirdInterface
(as, analogously to classes,[Cached]
is only valid on types it is explicitly put on)
- Create your first project
- Learning framework key bindings
- Adding resource stores
- Adding custom key bindings
- Adding custom fonts