Skip to content

Commit

Permalink
Add default bindings for Named Factory (#151)
Browse files Browse the repository at this point in the history
  • Loading branch information
sungam3r authored Jan 4, 2024
1 parent 98a891a commit 71a8553
Show file tree
Hide file tree
Showing 9 changed files with 261 additions and 43 deletions.
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,10 @@ public void ConfigureServices(IServiceCollection services)
Implementation for `IRepositoryFactory` will be generated at runtime.

In fact, each factory method can take one parameter of an arbitrary type - string, enum, custom class, whatever.
In this case, a _named binding_ should be specified.
In this case, a _named binding_ should be specified. Then you may resolve required services passing the name of
the binding into factory methods. If you want to provide a default implementation then you may configure _default
binding_. Default binding is such a binding used in the absence of a named one. A user should set default binding
explicitly to be able to resolve services for unregistered names.

```csharp
public interface IRepositoryFactory
Expand Down Expand Up @@ -236,6 +239,11 @@ public class RandomRepository : IRepository
...
}

public class DefaultRepository : IRepository
{
...
}

public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IRepository, DemoRepository>()
Expand All @@ -245,7 +253,8 @@ public void ConfigureServices(IServiceCollection services)
.For<IRepository>()
.Named<DemoRepository>("demo")
.Named<ProductionRepository>("prod")
.Named<RandomRepository>("rnd");
.Named<RandomRepository>("rnd")
.Default<DefaultRepository>();
}

public class Person
Expand All @@ -262,20 +271,24 @@ public class SomeClassWithDependency
_factory = factory;
}

private bool SomeInterestingCondition => ...

public void DoSomething(Person person)
{
if (person.Name == "demoUser")
_factory.GetPersonsRepo("demo").Save(person);
_factory.GetPersonsRepo("demo").Save(person); // DemoRepository
else if (person.Name.StartsWith("tester"))
_factory.GetPersonsRepo("rnd").Save(person);
_factory.GetPersonsRepo("rnd").Save(person); // RandomRepository
else if (SomeInterestingCondition)
_factory.GetPersonsRepo("prod").Save(person); // ProductionRepository
else
_factory.GetPersonsRepo("prod").Save(person);
_factory.GetPersonsRepo(person.Name).Save(person); // DefaultRepository
}
}
```

In the example above, the `GetPersonsRepo` method will return the corresponding implementation of the `IRepository`
interface, configured for the provided name.
interface, configured for the provided name. For all unregistered names (including null) it will return `DefaultRepository`.

## How it works?

Expand Down
4 changes: 2 additions & 2 deletions src/SteroidsDI.Tests/Cases.Approval/ApiApprovalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ public class ApiApprovalTests
[TestCase(typeof(AspNetCoreHttpScopeProvider))]
public void PublicApi(Type type)
{
string publicApi = type.Assembly.GeneratePublicApi(new ApiGeneratorOptions
string publicApi = type.Assembly.GeneratePublicApi(new()
{
IncludeAssemblyAttributes = false,
AllowNamespacePrefixes = ["System", "Microsoft.Extensions.DependencyInjection"],
ExcludeAttributes = ["System.Diagnostics.DebuggerDisplayAttribute"],
});

publicApi.ShouldMatchApproved(options => options!.WithFilenameGenerator((testMethodInfo, discriminator, fileType, fileExtension) => $"{type.Assembly.GetName().Name!}.{fileType}.{fileExtension}"));
publicApi.ShouldMatchApproved(options => options.NoDiff().WithFilenameGenerator((testMethodInfo, discriminator, fileType, fileExtension) => $"{type.Assembly.GetName().Name!}.{fileType}.{fileExtension}"));
}
}
6 changes: 5 additions & 1 deletion src/SteroidsDI.Tests/Cases.Approval/SteroidsDI.approved.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@ namespace SteroidsDI
{
public BindingContext(Microsoft.Extensions.DependencyInjection.IServiceCollection services) { }
public Microsoft.Extensions.DependencyInjection.IServiceCollection Services { get; }
public SteroidsDI.BindingContext<TService> Default<TImplementation>()
where TImplementation : TService { }
public SteroidsDI.BindingContext<TService> Default<TImplementation>(Microsoft.Extensions.DependencyInjection.ServiceLifetime lifetime)
where TImplementation : TService { }
public SteroidsDI.BindingContext<TService> Named<TImplementation>(object name)
where TImplementation : TService { }
public SteroidsDI.BindingContext<TService> Named<TImplementation>(object name, Microsoft.Extensions.DependencyInjection.ServiceLifetime lifetime = 2)
public SteroidsDI.BindingContext<TService> Named<TImplementation>(object name, Microsoft.Extensions.DependencyInjection.ServiceLifetime lifetime)
where TImplementation : TService { }
}
public sealed class GenericScopeProvider<T> : SteroidsDI.Core.IScopeProvider
Expand Down
149 changes: 132 additions & 17 deletions src/SteroidsDI.Tests/Cases/FactoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,71 @@ public void Named_Binding_Should_Throw_On_Unknown_Lifetime()
{
var services = new ServiceCollection();
var context = services.For<IBuilder>();
Should.Throw<InvalidOperationException>(() => context.Named<SpecialBuilder>("xxx")).Message.ShouldBe(@"The DI container does not have a default binding for the type 'SteroidsDI.Tests.IBuilder', so it is not possible to determine the value of Lifetime.
Use the 'Named' overload with explicit Lifetime or first set the default binding in the DI container.");
Should.Throw<InvalidOperationException>(() => context.Named<SpecialBuilder>("xxx")).Message.ShouldBe(@"The DI container does not register type 'SteroidsDI.Tests.IBuilder', so it is not possible to determine the value of Lifetime.
Use the 'Named'/'Default' overloads with explicit Lifetime or first register 'SteroidsDI.Tests.IBuilder' in the DI container.");
Should.Throw<InvalidOperationException>(() => context.Default<SpecialBuilder>()).Message.ShouldBe(@"The DI container does not register type 'SteroidsDI.Tests.IBuilder', so it is not possible to determine the value of Lifetime.
Use the 'Named'/'Default' overloads with explicit Lifetime or first register 'SteroidsDI.Tests.IBuilder' in the DI container.");
}

[Test]
public void Factory_And_Named_Bindings_Should_Work()
public void Named_Binding_Should_Allow_The_Same_Type_With_Different_Names()
{
using var provider = ServicesBuilder.BuildDefault().BuildServiceProvider(validateScopes: true);
var services = new ServiceCollection()
.AddTransient<IBuilder, Builder>()
.For<IBuilder>()
.Named<SpecialBuilder>("aaa")
.Named<SpecialBuilder>("bbb")
.Named<SpecialBuilder>("ccc")
.Named<SpecialBuilder>("ddd")
.Named<SpecialBuilder>("eee")
.Services;

services.Count.ShouldBe(7);
}

[Test]
public void Default_Binding_Should_Allow_Redeclaration()
{
var services = new ServiceCollection()
.AddTransient<IBuilder, Builder>()
.For<IBuilder>()
.Default<SpecialBuilder>()
.Default<SpecialBuilder>()
.Services;
services.Count.ShouldBe(3);
var def = (NamedBinding)services.Last().ImplementationInstance!;
def.Name.ShouldBe(NamedBinding.DefaultName);
def.ImplementationType.ShouldBe(typeof(SpecialBuilder));
}

[Test]
public void Default_Binding_Should_Allow_Replace()
{
var services = new ServiceCollection()
.AddTransient<IBuilder, Builder>()
.For<IBuilder>()
.Default<SpecialBuilder>()
.Default<SpecialBuilderOver9000Level>()
.Services;
services.Count.ShouldBe(4);
var def = (NamedBinding)services[2].ImplementationInstance!;
def.Name.ShouldBe(NamedBinding.DefaultName);
def.ImplementationType.ShouldBe(typeof(SpecialBuilderOver9000Level));
}

[Test]
[TestCase(true)]
[TestCase(false)]
public void Factory_And_Named_Bindings_Should_Work(bool useDefault)
{
var services = ServicesBuilder.BuildDefault(addDefalt: useDefault);

// not specific to this test - just add some additional registration for another type and place it in front of other registrations to increase code coverage
services.For<IComparable>().Named<string>("some", ServiceLifetime.Singleton);
services.Insert(0, services.Last());
services.RemoveAt(services.Count - 1);

using var provider = services.BuildServiceProvider(validateScopes: true);
using var scope = provider.CreateScope();

GenericScope<ServicesBuilder>.CurrentScope = scope;
Expand Down Expand Up @@ -59,10 +116,12 @@ public void Factory_And_Named_Bindings_Should_Work()
}

[Test]
[TestCase(true)]
[TestCase(false)]
[Category("Generic")]
public void Generic_Factory_And_Named_Bindings_Should_Work()
public void Generic_Factory_And_Named_Bindings_Should_Work(bool useDefault)
{
using var provider = ServicesBuilder.BuildDefault().BuildServiceProvider(validateScopes: true);
using var provider = ServicesBuilder.BuildDefault(addDefalt: useDefault).BuildServiceProvider(validateScopes: true);
using var scope = provider.CreateScope();

GenericScope<ServicesBuilder>.CurrentScope = scope;
Expand Down Expand Up @@ -100,35 +159,91 @@ public void Generic_Factory_And_Named_Bindings_Should_Work()

[Test]
[Category("Throw")]
public void Null_Binding_Should_Throw()
public void Null_Binding_Should_Throw_When_No_Default()
{
using var provider = ServicesBuilder.BuildDefault().BuildServiceProvider(validateScopes: true);
var controller = provider.GetRequiredService<Controller>();

Should.Throw<InvalidOperationException>(() => controller.Factory.CCC(null!));
Should.Throw<InvalidOperationException>(() => controller.Generic.CCC(null!));
var msg1 = Should.Throw<InvalidOperationException>(() => controller.Factory.CCC(null!)).Message;
msg1.ShouldBe("Destination type not found for named binding '' to type 'SteroidsDI.Tests.IBuilder' and no default binding exists. Verify that either a named binding or default binding is specified in the DI container.");
var msg2 = Should.Throw<InvalidOperationException>(() => controller.Generic.CCC(null!)).Message;
msg2.ShouldBe("Destination type not found for named binding '' to type 'SteroidsDI.Tests.IBuilder' and no default binding exists. Verify that either a named binding or default binding is specified in the DI container.");
}

[Test]
[Category("Throw")]
public void Unknown_Binding_Should_Throw()
public void Null_Binding_Should_Work_When_Default()
{
using var provider = ServicesBuilder.BuildDefault(addDefalt: true).BuildServiceProvider(validateScopes: true);
var controller = provider.GetRequiredService<Controller>();

var builder1 = controller.Factory.CCC(null!);
builder1.ShouldBeAssignableTo<DefaultBuilder>();
builder1.Build();

var builder2 = controller.Generic.CCC(null!);
builder2.ShouldBeAssignableTo<DefaultBuilder>();
builder2.Build();
}

[Test]
public void Unknown_Binding_Should_Throw_When_No_Default()
{
using var provider = ServicesBuilder.BuildDefault().BuildServiceProvider(validateScopes: true);
var controller = provider.GetRequiredService<Controller>();

Should.Throw<InvalidOperationException>(() => controller.Factory.CCC("Zorro"));
Should.Throw<InvalidOperationException>(() => controller.Generic.CCC("Zorro"));
var msg1 = Should.Throw<InvalidOperationException>(() => controller.Factory.CCC("Zorro")).Message;
msg1.ShouldBe("Destination type not found for named binding 'Zorro' to type 'SteroidsDI.Tests.IBuilder' and no default binding exists. Verify that either a named binding or default binding is specified in the DI container.");
var msg2 = Should.Throw<InvalidOperationException>(() => controller.Generic.CCC("Zorro")).Message;
msg2.ShouldBe("Destination type not found for named binding 'Zorro' to type 'SteroidsDI.Tests.IBuilder' and no default binding exists. Verify that either a named binding or default binding is specified in the DI container.");

Should.Throw<InvalidOperationException>(() => controller.Factory.DDD(ManagerType.Angry));
Should.Throw<InvalidOperationException>(() => controller.Generic.DDD(ManagerType.Angry));
var msg3 = Should.Throw<InvalidOperationException>(() => controller.Factory.DDD(ManagerType.Angry)).Message;
msg3.ShouldBe("Destination type not found for named binding 'Angry' to type 'SteroidsDI.Tests.IBuilder' and no default binding exists. Verify that either a named binding or default binding is specified in the DI container.");
var msg4 = Should.Throw<InvalidOperationException>(() => controller.Generic.DDD(ManagerType.Angry)).Message;
msg4.ShouldBe("Destination type not found for named binding 'Angry' to type 'SteroidsDI.Tests.IBuilder' and no default binding exists. Verify that either a named binding or default binding is specified in the DI container.");
}

[Test]
[Category("Throw")]
public void Unknown_Binding_Should_Work_When_Default()
{
using var provider = ServicesBuilder.BuildDefault(addDefalt: true).BuildServiceProvider(validateScopes: true);
var controller = provider.GetRequiredService<Controller>();

var builder1 = controller.Factory.CCC("Zorro");
builder1.ShouldBeAssignableTo<DefaultBuilder>();
builder1.Build();

var builder2 = controller.Generic.CCC("Zorro");
builder2.ShouldBeAssignableTo<DefaultBuilder>();
builder2.Build();

var builder3 = controller.Factory.DDD(ManagerType.Angry);
builder3.ShouldBeAssignableTo<DefaultBuilder>();
builder3.Build();

var builder4 = controller.Generic.DDD(ManagerType.Angry);
builder4.ShouldBeAssignableTo<DefaultBuilder>();
builder4.Build();
}

[Test]
[Category("Throw")]
public void Named_Binding_With_Invalid_Properties_Should_Throw()
{
Should.Throw<ArgumentNullException>(() => ServicesBuilder.BuildDefault().For<IBuilder>().Named<SpecialBuilder>(null!).Services.BuildServiceProvider(validateScopes: true));
Should.Throw<InvalidOperationException>(() => ServicesBuilder.BuildDefault().For<IBuilder>().Named<SpecialBuilderOver9000Level>("oops", ServiceLifetime.Transient).Services.BuildServiceProvider(validateScopes: true));
var msg1 = Should.Throw<ArgumentNullException>(() => ServicesBuilder.BuildDefault().For<IBuilder>().Named<SpecialBuilder>(null!).Services.BuildServiceProvider(validateScopes: true)).Message;
msg1.ShouldBe("No binding name specified. (Parameter 'name')");
var msg2 = Should.Throw<InvalidOperationException>(() => ServicesBuilder.BuildDefault().For<IBuilder>().Named<SpecialBuilderOver9000Level>("oops", ServiceLifetime.Transient).Services.BuildServiceProvider(validateScopes: true)).Message;
msg2.ShouldBe(@"It is not possible to add a named binding 'oops' for type SteroidsDI.Tests.IBuilder, because the DI container
already has a binding on type SteroidsDI.Tests.SpecialBuilderOver9000Level with different characteristics. This is a limitation of the current implementation.");
}

[Test]
[Category("Throw")]
public void Default_Binding_With_Invalid_Properties_Should_Throw()
{
var msg = Should.Throw<InvalidOperationException>(() => ServicesBuilder.BuildDefault(addDefalt: true).For<IBuilder>().Default<DefaultBuilder>(ServiceLifetime.Transient).Services.BuildServiceProvider(validateScopes: true)).Message;
msg.ShouldBe(@"It is not possible to add a default binding for type SteroidsDI.Tests.IBuilder, because the DI container
already has a binding on type SteroidsDI.Tests.DefaultBuilder with different characteristics. This is a limitation of the current implementation.");
}

[Test]
Expand Down
5 changes: 5 additions & 0 deletions src/SteroidsDI.Tests/Model/IBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ internal class SpecialBuilderOver9000Level : IBuilder
{
public void Build() => Console.WriteLine("!!!!!!!SpecialBuilderOver9000Level!!!!!!!");
}

internal class DefaultBuilder : IBuilder
{
public void Build() => Console.WriteLine("Default");
}
21 changes: 12 additions & 9 deletions src/SteroidsDI.Tests/ServicesBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace SteroidsDI.Tests;

internal class ServicesBuilder
{
public static IServiceCollection BuildDefault(bool addScopeProvider = true)
public static IServiceCollection BuildDefault(bool addScopeProvider = true, bool addDefalt = false)
{
var services = new ServiceCollection()
.AddDefer()
Expand All @@ -19,14 +19,17 @@ public static IServiceCollection BuildDefault(bool addScopeProvider = true)
.AddFactory<IMegaFactory>()
.AddFactory<IGenericFactory<IBuilder, INotifier>>()
.AddTransient<IBuilder, Builder>()
.AddSingleton<INotifier, Notifier>()
.For<IBuilder>()
.Named<SpecialBuilder>("xxx")
.Named<SpecialBuilder>("yyy")
.Named<SpecialBuilderOver9000Level>("oops", ServiceLifetime.Singleton)
.Named<SpecialBuilder>(ManagerType.Good)
.Named<SpecialBuilderOver9000Level>(ManagerType.Bad, ServiceLifetime.Singleton)
.Services;
.AddSingleton<INotifier, Notifier>();

var context = services.For<IBuilder>()
.Named<SpecialBuilder>("xxx")
.Named<SpecialBuilder>("yyy")
.Named<SpecialBuilderOver9000Level>("oops", ServiceLifetime.Singleton)
.Named<SpecialBuilder>(ManagerType.Good)
.Named<SpecialBuilderOver9000Level>(ManagerType.Bad, ServiceLifetime.Singleton);

if (addDefalt)
context.Default<DefaultBuilder>(ServiceLifetime.Singleton);

if (addScopeProvider)
services.AddSingleton<IScopeProvider, GenericScopeProvider<ServicesBuilder>>();
Expand Down
Loading

0 comments on commit 71a8553

Please sign in to comment.