Servant /səːv(ə)nt/
noun
One who waits on another
Async .NET dependency injection, while you await!
Most existing frameworks predate C#'s async
/await
capabilities,
and are fundamentally incompatible with types that have asynchronous initialisation code.
Such code must be "async all the way down", and that's exactly what Servant is.
// Construct a new, empty, servant.
var servant = new Servant();
// Register type ServerProxy for implicit creation as a singleton.
servant.AddSingleton<ServerProxy>();
// Register type Config for explicit creation via the async Func<> as a transient.
servant.AddTransient<Config>(async (ServerProxy server) => await server.RequestConfig());
// Request Servant to serve up a Config instance.
// The singleton ServerProxy will be instantiated lazily and passed to the above Func<>.
var config = await servant.ServeAsync<Config>();
Servant targets net45
and netstandard1.3
, so you can use it pretty much anywhere that .NET runs.
Install the package from NuGet:
Install-Package Servant
Or clone the repo and build your own version.
Each type registered with a Servant
must be resolvable via one of the following means:
- An implicit constructor or factory call, as with
ServerProxy
above - A
Func<>
returning aT
orTask<T>
, as withServerProxy
above - A pre-constructed instance, such as
servant.AddSingleton(instance)
Types can be class
or struct
, and may be public
, internal
, or private
.
Types may be registered with one of two lifestyles:
Singleton
instances are created per servant. If multiple dependants for the type exist, they receive the same instance.Transient
instances are created per request. Multiple dependants receive their own copies.
Note that the servant tracks singleton instances, and if they implement IDisposable
will dispose them when you call Servant.Dispose
.
Transient instances cannot be tracked by the servant, and their lifespans must be managed by their consumers.
Types can be implicitly constructed via calls such as AddSingleton<SomeType>()
so long as SomeType
meets one of two criteria:
- Either it has a single public constructor, or
- It has a single static factory method (of any name) returning either
SomeType
orTask<SomeType>
.
In general, constructor injection is simplest. However constructors cannot be async
, whereas factory methods can be.
Here are two examples of valid types (member bodies omitted):
public class ConstructorExample
{
// Servant will find this constructor and call it to obtain an instance of the type.
// There must be only a single public constructor to use constructor injection.
public ConstructorExample(DependencyType1 dep1, DependencyType2 dep2)
{ }
}
public class FactoryExample
{
// Servant will find this method and call it to obtain an instance of the type.
// The method name doesn't matter. It must be public and static though.
// The return type can be either Task<FactoryExample> or just FactoryExample.
// There must be only one method on the type that meets these criteria.
public static Task<FactoryExample> CreateAsync(DependencyType1 dep1, DependencyType2 dep2)
{ }
private FactoryExample(/* ... */)
{ }
}
Copyright 2016-2018 Drew Noakes
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.