A lightweight cross platform dependency scanning library
Pronounced dee-pend-us, Dependous grants you the power to inspect a collection of .NET assemblies and extract types implementing specific interfaces. That's it. Simple and easy.
Dependous eliminates the need to explicitly describe different types of dependencies you wish to locate. Instead Dependeous allows you define your dependencies as you develop your classes with a "think about it once and forget about it" approach by employing discovery interfaces.
A discovery interface can be any .NET interface. Dependous implements five interfaces that are specific to IOC container lifetime management.
ITransientDependency Designates an implementor as a discoverable dependency and asserts it's lifetime management should be transient.
ISingletonDependency Designates an implementor as a discoverable dependency and asserts it's lifetime management should be singleton.
IScopedDependency Designates an implementor as a discoverable dependency and asserts it's lifetime management should be scoped.
ISelfTransient Makes a concrete type injectable through the IOC container and sets it's lifetime to trainsient
ISelfSingleton Makes a concrete type injectable through the IOC container and sets it's lifetime to singleton
These five interfaces only assert your intent on how instances of your dependency should be handled. This will be captured in the DependencyMetadata
instance.
It's up to the IOC container integration to actually register your dependencies.
When declaring discovery interfaces, you must bind it to a specific lifetime. Either Singleton, Transient or Scoped.
var scanner = DependencyScannerFactory.Create();
var scanResult = scanner.Scan((f) => f.StartsWith("MyAssembly"));
Action<IDependousConfiguration> configurationBuilder = (config) =>
{
config.AddAdditionalDiscoveryTypes(d => d.RegisterType<IMyService1>(ServiceLifetime.Singleton)
.RegisterType<IMyService2>(ServiceLifetime.Singleton));
configurationBuilder?.Invoke(config);
};
var scanner = DependencyScannerFactory.Create();
var scanResult = scanner.Scan((f) => f.StartsWith("MyAssembly"),configurationBuilder );
In addition to the five default discovery interfaces, this example will register two additional discovery interfaces IMyService1
and IMyService2
. Now when the scanner executes,
any class implementing IMyService1
or IMyService2
will be discovered as a dependency with a lifetime of singleton.
You can also choose to use attributes instead of interfaces. This feature was added to make integrating into other platforms easier.
[Dependency(typeof(IAttributeInterface), ServiceLifetime.Singleton, false)]
public class AttributeImplementation : IAttributeInterface
{
}
...
Action<IDependousConfiguration> configurationBuilder = (config) =>
{
//this code allows you to map your attribute to our attribute.
config.AddAdditionalDiscoveryTypes(x => x.RegisterAttribute<DependencyAttribute>((d) =>
{
return new DependencyAttribute(d.ResolveType, d.LifeTime, d.EnumerationOnly);
}));
//if you use our attribute, you can just use
config.AddAdditionalDiscoveryTypes(x => x.RegisterAttribute()));
};
public class MyModel : ISelfTransient{
public MyModel( ISomeDependency d){
}
}
This registration will allow you to inject new instances of a concrete type. You can register your instance as ISelfTransient, ISelfScoped or ISelfSingleton
public class MyService : IMyService, ISingletonDependency{
}
MyService will be discovered as a dependency with a lifetime of singleton.
By default, the assembly scanning will extract all interfaces implemented by your dependency. Often times this behavior is desirable; however, if you need to target a specific subset of interfaces, use this attribute.
[Target(typeof(IInterface1))]
public class MyDependency : IInterface1, IInterface2, ITransientDependency{
}
MyDependency will be discovered but it's list of implemented interfaces will only contain IInterface1
By default, Dependous will only scan for assemblies located directly in your bin folder. If you wish to expand your search horizons, add your probing paths to the dependous configuration.
var instance = DependencyScannerFactory.Create();
var result = instance.Scan((b) => b.StartsWith("Dependous.Probing"), (c) => c.AddProbingPaths(pb => pb.AddProbingPath("../../../../../tests/Dependous.Probing/bin/Debug/netstandard2.0")));
Assert.True(result.Metadata.Any());
Additionally, if you need to have a different assembly filter, you can pass it along with your probing path:
var instance = DependencyScannerFactory.Create();
var result = instance.Scan((b) => b.StartsWith("Dependous.Probing"), (c) => c.AddProbingPaths(pb => pb.AddProbingPath("../../../../../tests/Dependous.Probing/bin/Debug/netstandard2.0"),cb=>cb.StartsWith("Plugins")));
Assert.True(result.Metadata.Any());
In some scenarios, you may want to inspect the collected dependency metadata from a Dependous scan.
var scanner = DependencyScannerFactory.Create();
var scanResult = scanner.Scan((f) => f.StartsWith("MyAssembly"),cb=>cb.PersistScanResults=true );
//Now you can inject the results into any class
public class MyService{
public MyService(DependencyScanResult dependencyScanResults){
}
}
Now that we have a list of dependencies what do we do with them? It's time to integrate an IOC container. For this example, I chose AutoFac.
For more info on integrating Autofac into ASP.NET go here
# Program.cs
using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseAutoFacContainer(AssemblyPaths.From("Your assembly prefix"), logger: (o) => Console.WriteLine($"{o}"))
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
#Statup.cs
public void ConfigureServices(IServiceCollection services)
{
...
services.AddControllers();
services.AddDependencyScanning();
}
var serviceCollection = new ServiceCollection();
serviceCollection.AddDependencyScanning();
container = serviceCollection.BuildAutoFacContainer((f) => f.StartsWith("Dependous"),
logger: (item) => { output.WriteLine($"{item}"); Console.WriteLine(item); }, configurationBuilder: (cb) =>
{
cb.PersistScanResults = true;
cb.AddProbingPaths(pb => pb.AddProbingPath("../../../../../tests/Dependous.Probing/bin/Debug/netstandard2.0"));
});
In this example, we instruct Dependous to additionally search "../../../../../tests/Dependous.Probing/bin/Debug/netstandard2.0"
for our assemblies. We also set PersistScanResults=true
, allowing you to inject all the metadata collection by the Dependous scan.
Autofac supports quite a few features like Metadata
which is not currently supported by Dependous. It does however, support the usages of named/keyed dependencies.
Documentation
So for example:
public interface IMultipleConsumers{}
[NamedDependency("A")]
public class Consumer1 : IMultipleConsumers, ISingletonDependency{}
[NamedDependency("B")]
public class Consumer2 : IMultipleConsumers, ISingletonDependency{}
public class MyService : IMyService{
//IIndex is an Autofac construct used to resolve keyed dependencies.
public MyService(IIndex<string,IMultipleConsumers> index){
//actually get your service
var myService = index["A"];
}
}
Decoration can be achieved quite easily by implementing a specific interface, IDecorator
public interface IDecoratableService : ISingletonDependency
{
string Method();
}
public class TrueService : IDecoratableService
{
public string Method()
{
return $"Hello";
}
}
public class DecoratorOfTrueService : IDecoratableService, IDecorator<IDecoratableService>
{ public DecoratorOfTrueService(IDecoratableService decoratableService)
{
DecoratableService = decoratableService;
}
public IDecoratableService DecoratableService { get; }
public string Method()
{
var sb = new StringBuilder();
sb.Append("DecoratorOfTrueService.Before");
sb.Append(DecoratableService.Method());
sb.Append("DecoratorOfTrueService.After");
return sb.ToString();
}
}
If you decorate your dependency with an Intercept attribute, you can configure class interception with Castle.DynamicProxy. Try not to use this. It's slow.
public interface IncerceptableService
{
public string Invoke();
}
[Intercept(typeof(LoggingInterceptor))]
public class DoNothingIncerceptableService : IncerceptableService, ITransientDependency
{
public virtual string Invoke()
{
return "true method call";
}
}
public class LoggingInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
Console.Write("Calling method {0} with parameters {1}... ",
invocation.Method.Name,
string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray()));
var test = "before";
invocation.Proceed();
var test2 = "after";
invocation.ReturnValue = $"{test} {invocation.ReturnValue} {test2}";
Console.WriteLine("Done: result was {0}.", invocation.ReturnValue);
}
}
#Program.cs
global using Dependous;
global using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseDependousAutofac(AssemblyPaths.From("Your Pattern"), logger: (o) => Console.WriteLine($"{o}"));
...
builder.Services.AddDependencyScanning();
#Program.cs
global using Dependous;
global using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateDefaultBuilder(args)
.UseAutoFacContainer(AssemblyPaths.From("Your Pattern"), logger: (o) => Console.WriteLine($"{o}"));
...
builder.Services.AddDependencyScanning();
#Program.cs
global using Dependous;
global using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateDefaultBuilder(args)
.UseAutoFacContainer(AssemblyPaths.From("Your Pattern"), logger: (o) => Console.WriteLine($"{o}"),
containerBuilder: (c) =>{
//now you have full access to the AutoFac container and can do whatever you like.
});
...
builder.Services.AddDependencyScanning();