module Athena::DependencyInjection
Overview
Athena's Dependency Injection (DI) component, ADI
for short, adds a service container layer to your project. This allows useful objects, aka services, to be shared throughout the project.
These objects live in a special class called the ADI::ServiceContainer
(SC).
The SC is lazily initialized on fibers; this allows the SC to be accessed anywhere within the project. The Athena::DependencyInjection.container
method will return the SC for the current fiber.
Since the SC is defined on fibers, it allows for each fiber to have its own SC instance. This can be useful for web frameworks as each request would have its own SC scoped to that request.
- See
ADI::Register
for documentation on registering services.
!!!tip It is highly recommended to use interfaces as opposed to concrete types when defining the initializers for both services and non-services.
Using interfaces allows changing the functionality of a type by just changing what service gets injected into it, such as via an alias. See this blog post for an example of this.
Getting Started
If using this component within the [Athena Framework][Athena::Framework], it is already installed and required for you. Checkout the manual for some additional information on how to use it within the framework.
If using it outside of the framework, you will first need to add it as a dependency:
dependencies:
athena-dependency_injection:
github: athena-framework/dependency-injection
version: ~> 0.3.0
Then run shards install
, being sure to require it via require "athena-dependency_injection"
.
From here integration of the component depends on the execution flow of your application, and how it uses Fibers. Since each fiber has its own container instance, if your application only uses Crystal's main fiber and is short lived, then you most likely only need to set up your services and expose one of them as [public][Athena::DependencyInjection::Register--optional-arguments] to serve as the entry point.
If your application is meant to be long lived, such as using a HTTP::Server, then you will want to ensure that each
fiber is truly independent from one another, with them not being reused or sharing state external to the container. An example of this is how HTTP::Server
reuses fibers
for connection: keep-alive
requests. Because of this, or in cases similar to, you may want to manually reset the container via Fiber.current.container = ADI::ServiceContainer.new
.
Defined in:
annotations.crathena-dependency_injection.cr
Constant Summary
-
VERSION =
"0.3.7"
Class Method Summary
-
.container : ADI::ServiceContainer
Returns the
ADI::ServiceContainer
for the current fiber.
Macro Summary
-
auto_configure(type, options)
Applies the provided options to any registered service of the provided type.
-
bind(key, value)
Allows binding a value to a key in order to enable auto registration of that value.
Class Method Detail
Returns the ADI::ServiceContainer
for the current fiber.
Macro Detail
Applies the provided options to any registered service of the provided type.
A common use case of this would be to apply a specific tag to all instances of an interface; thus preventing the need to manually apply the tag for each implementation.
This can be paired with Athena::DependencyInjection.bind
to make working with tags easier.
Example
module ConfigInterface; end
# Automatically apply the `"config"` tag to all instances of `ConfigInterface`.
ADI.auto_configure ConfigInterface, {tags: ["config"]}
@[ADI::Register]
record ConfigOne do
include ConfigInterface
end
@[ADI::Register]
record ConfigTwo do
include ConfigInterface
end
# Options supplied on the annotation itself override the auto configured options.
@[ADI::Register(tags: [] of String)]
record ConfigThree do
include ConfigInterface
end
@[ADI::Register(_configs: "!config", public: true)]
record ConfigClient, configs : Array(ConfigInterface)
ADI.container.config_client.configs # => [ConfigOne(), ConfigTwo()]
Allows binding a value to a key in order to enable auto registration of that value.
Bindings allow scalar values, or those that could not otherwise be handled via [service aliases][Athena::DependencyInjection::Register--aliasing-services], to be auto registered. This allows those arguments to be defined once and reused, as opposed to using named arguments to manually specify them for each service.
Bindings can also be declared with a type restriction to allow taking the type restriction of the argument into account. Typed bindings are always checked first as the most specific type is always preferred. If no typed bindings match the argument's type, then the last defined untyped bindings is used.
Example
module ValueInterface; end
@[ADI::Register(_value: 1, name: "value_one")]
@[ADI::Register(_value: 2, name: "value_two")]
@[ADI::Register(_value: 3, name: "value_three")]
record ValueService, value : Int32 do
include ValueInterface
end
# Untyped bindings
ADI.bind api_key, ENV["API_KEY"]
ADI.bind config, {id: 12_i64, active: true}
ADI.bind static_value, 123
ADI.bind odd_values, ["@value_one", "@value_three"]
ADI.bind value_arr, [true, true, false]
# Typed bindings
ADI.bind value_arr : Array(Int32), [1, 2, 3]
ADI.bind value_arr : Array(Float64), [1.0, 2.0, 3.0]
@[ADI::Register(public: true)]
record BindingClient,
api_key : String,
config : NamedTuple(id: Int64, active: Bool),
static_value : Int32,
odd_values : Array(ValueInterface)
@[ADI::Register(public: true)]
record IntArr, value_arr : Array(Int32)
@[ADI::Register(public: true)]
record FloatArr, value_arr : Array(Float64)
@[ADI::Register(public: true)]
record BoolArr, value_arr : Array(Bool)
ADI.container.binding_client # =>
# BindingClient(
# @api_key="123ABC",
# @config={id: 12, active: true},
# @static_value=123,
# @odd_values=[ValueService(@value=1), ValueService(@value=3)])
ADI.container.int_arr # => IntArr(@value_arr=[1, 2, 3])
ADI.container.float_arr # => FloatArr(@value_arr=[1.0, 2.0, 3.0])
ADI.container.bool_arr # => BoolArr(@value_arr=[true, true, false])