-
Notifications
You must be signed in to change notification settings - Fork 440
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.
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 }
- Create your first project
- Learning framework key bindings
- Adding resource stores
- Adding custom key bindings
- Adding custom fonts