Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable Lifetime Management and add more samples to main readme.md #16

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>

<IsPackable>false</IsPackable>
</PropertyGroup>
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Neleus.DependencyInjection.Extensions.Tests
{
[TestClass]
public class ServiceByNameFactoryLifetimeTests
{
private ServiceCollection _container;

[TestInitialize]
public void Init()
{
_container = new Microsoft.Extensions.DependencyInjection.ServiceCollection();
}

[TestMethod]
public void ServiceByNameFactorySingleton_GetByName_ResolvesSingleton()
{
_container.AddTransient<List<int>>();

_container.AddByName<IList<int>>()
.AddSingleton<List<int>>("1")
.AddSingleton<List<int>>("2")
.BuildSingleton();

var serviceProvider = _container.BuildServiceProvider();

// direct resolution by calling GetByName
var listA1 = serviceProvider.GetService<IServiceByNameFactory<IList<int>>>()
.GetByName("1");
var listA2 = serviceProvider.GetService<IServiceByNameFactory<IList<int>>>()
.GetByName("2");
listA1.Add(1);
listA1.Add(2);
listA2.Add(3);
listA2.Add(4);

var listB1 = serviceProvider.GetService<IServiceByNameFactory<IList<int>>>()
.GetByName("1");
var listB2 = serviceProvider.GetService<IServiceByNameFactory<IList<int>>>()
.GetByName("2");

Assert.AreSame(listA1, listB1);
Assert.AreSame(listA2, listB2);
}

[TestMethod]
public void ServiceByNameFactorySingleton_GetByName_ResolvesTransient()
{
_container.AddTransient<List<int>>();

_container.AddByName<IList<int>>()
.Add<List<int>>("1")
.Add<List<int>>("2")
.Build();

var serviceProvider = _container.BuildServiceProvider();

// direct resolution by calling GetByName
var listA1 = serviceProvider.GetService<IServiceByNameFactory<IList<int>>>()
.GetByName("1");
var listA2 = serviceProvider.GetService<IServiceByNameFactory<IList<int>>>()
.GetByName("2");
listA1.Add(1);
listA1.Add(2);
listA2.Add(3);
listA2.Add(4);

var listB1 = serviceProvider.GetService<IServiceByNameFactory<IList<int>>>()
.GetByName("1");
var listB2 = serviceProvider.GetService<IServiceByNameFactory<IList<int>>>()
.GetByName("2");

Assert.AreNotEqual(listA1, listB1);
Assert.AreNotEqual(listA2, listB2);
}
}
}
13 changes: 13 additions & 0 deletions Neleus.DependencyInjection.Extensions/Lifetime.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace Neleus.DependencyInjection.Extensions
{
public enum Lifetime
{
Transient,
Scoped,
Singleton
}
}
30 changes: 26 additions & 4 deletions Neleus.DependencyInjection.Extensions/ServiceByNameFactory.cs
Original file line number Diff line number Diff line change
@@ -6,9 +6,9 @@ namespace Neleus.DependencyInjection.Extensions
internal class ServiceByNameFactory<TService> : IServiceByNameFactory<TService>
{
private readonly IServiceProvider _serviceProvider;
private readonly IDictionary<string, Type> _registrations;
private readonly IDictionary<string, TypeLifetime> _registrations;

public ServiceByNameFactory(IServiceProvider serviceProvider, IDictionary<string, Type> registrations)
public ServiceByNameFactory(IServiceProvider serviceProvider, IDictionary<string, TypeLifetime> registrations)
{
_serviceProvider = serviceProvider;
_registrations = registrations;
@@ -18,14 +18,36 @@ public TService GetByName(string name)
{
if (!_registrations.TryGetValue(name, out var implementationType))
throw new ArgumentException($"Service name '{name}' is not registered");
return (TService)_serviceProvider.GetService(implementationType);
switch (implementationType.Lifetime)
{
case Lifetime.Scoped:
case Lifetime.Singleton:
if (implementationType.Instance == null)
{
implementationType.Instance = (TService)_serviceProvider.GetService(implementationType.Type);
}
return (TService)implementationType.Instance;
default:
return (TService)_serviceProvider.GetService(implementationType.Type);
}
}

public TService GetRequiredByName(string name)
{
if (!_registrations.TryGetValue(name, out var implementationType))
throw new ArgumentException($"Service name '{name}' is not registered");
return (TService)_serviceProvider.GetRequiredService(implementationType);
switch (implementationType.Lifetime)
{
case Lifetime.Scoped:
case Lifetime.Singleton:
if (implementationType.Instance == null)
{
implementationType.Instance = (TService)_serviceProvider.GetRequiredService(implementationType.Type);
}
return (TService)implementationType.Instance;
default:
return (TService)_serviceProvider.GetRequiredService(implementationType.Type);
}
}

public ICollection<string> GetNames()
63 changes: 59 additions & 4 deletions Neleus.DependencyInjection.Extensions/ServicesByNameBuilder.cs
Original file line number Diff line number Diff line change
@@ -12,14 +12,14 @@ public class ServicesByNameBuilder<TService>
{
private readonly IServiceCollection _services;

private readonly IDictionary<string, Type> _registrations;
private readonly IDictionary<string, TypeLifetime> _registrations;

internal ServicesByNameBuilder(IServiceCollection services, NameBuilderSettings settings)
{
_services = services;
_registrations = settings.CaseInsensitiveNames
? new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
: new Dictionary<string, Type>();
? new Dictionary<string, TypeLifetime>(StringComparer.OrdinalIgnoreCase)
: new Dictionary<string, TypeLifetime>();
}

/// <summary>
@@ -29,7 +29,50 @@ internal ServicesByNameBuilder(IServiceCollection services, NameBuilderSettings
/// </summary>
public ServicesByNameBuilder<TService> Add(string name, Type implemtnationType)
{
_registrations.Add(name, implemtnationType);
var typeLifetime = new TypeLifetime() { Type = implemtnationType, Lifetime = Lifetime.Transient };
_registrations.Add(name, typeLifetime);
return this;
}

/// <summary>
/// Generic version of <see cref="Add"/>
/// </summary>
public ServicesByNameBuilder<TService> AddScoped<TImplementation>(string name)
where TImplementation : TService
{
return AddScoped(name, typeof(TImplementation));
}

/// <summary>
/// Maps name to corresponding implementation.
/// Note that this implementation has to be also registered in IoC container so
/// that <see cref="IServiceByNameFactory&lt;TService&gt;"/> is be able to resolve it.
/// </summary>
public ServicesByNameBuilder<TService> AddScoped(string name, Type implemtnationType)
{
var typeLifetime = new TypeLifetime() { Type = implemtnationType, Lifetime = Lifetime.Scoped };
_registrations.Add(name, typeLifetime);
return this;
}

/// <summary>
/// Generic version of <see cref="Add"/>
/// </summary>
public ServicesByNameBuilder<TService> AddSingleton<TImplementation>(string name)
where TImplementation : TService
{
return AddSingleton(name, typeof(TImplementation));
}

/// <summary>
/// Maps name to corresponding implementation.
/// Note that this implementation has to be also registered in IoC container so
/// that <see cref="IServiceByNameFactory&lt;TService&gt;"/> is be able to resolve it.
/// </summary>
public ServicesByNameBuilder<TService> AddSingleton(string name, Type implemtnationType)
{
var typeLifetime = new TypeLifetime() { Type = implemtnationType, Lifetime = Lifetime.Singleton };
_registrations.Add(name, typeLifetime);
return this;
}

@@ -53,5 +96,17 @@ public void Build()
//Registrations are shared across all instances
_services.AddTransient<IServiceByNameFactory<TService>>(s => new ServiceByNameFactory<TService>(s, registrations));
}
public void BuildScoped()
{
var registrations = _registrations;
//Registrations are shared across all instances
_services.AddScoped<IServiceByNameFactory<TService>>(s => new ServiceByNameFactory<TService>(s, registrations));
}
public void BuildSingleton()
{
var registrations = _registrations;
//Registrations are shared across all instances
_services.AddSingleton<IServiceByNameFactory<TService>>(s => new ServiceByNameFactory<TService>(s, registrations));
}
}
}
13 changes: 13 additions & 0 deletions Neleus.DependencyInjection.Extensions/TypeLifetime.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace Neleus.DependencyInjection.Extensions
{
public class TypeLifetime
{
public Lifetime Lifetime { get; set; } = Lifetime.Transient;
public Type Type { get; set; } = typeof(Type);
public object Instance { get; set; }
}
}
68 changes: 68 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -34,4 +34,72 @@ In this case your client code is clean and has only dependency to `IService` and
}
In this case your client code depends on both IServiceByNameFactory and IService which can be heplful in case when client has its own logic of name resolution.

## Life Time Options

In order to register instances using Singleton or Scope you can use the following examples:

services.AddTransient<ServiceA>();
services.AddTransient<ServiceB>();
services.AddTransient<ServiceC>();
services.AddByName<IService>()
.AddSingleton<ServiceA>("key1")
.AddSingleton<ServiceB>("key2")
.AddSingleton<ServiceC>("key3")
.BuildSingleton();

## Loop through keys

There are times where you want to use the registered names as a way to handle do work against each key.

_container.AddTransient<List<int>>();
_container.AddTransient<HashSet<int>>();

_container.AddByName<IEnumerable<int>>(new NameBuilderSettings()
{
CaseInsensitiveNames = true
})
.Add<List<int>>("list")
.Add<HashSet<int>>("hashSet")
.Build();

var serviceProvider = _container.BuildServiceProvider();

// allow to get each type
var group = serviceProvider.GetService<IServiceByNameFactory<IEnumerable<int>>>();
foreach (var name in group.GetNames())
{
var instance = group.GetByName(name);
switch (name) {
case "list":
Assert.AreEqual(typeof(List<int>), instance.GetType());
break;
case "hashSet":
Assert.AreEqual(typeof(HashSet<int>), instance.GetType());
break;
}
}

## GetRequiredByName

Many times when sharing code between projects you want to throw an exception when a service isn't registered. To do this here is an example:

_container.AddTransient<HashSet<int>>();

_container.AddByName<IEnumerable<int>>(new NameBuilderSettings()
{
CaseInsensitiveNames = true
})
.Add("list", typeof(List<int>))
.Add("hashSet", typeof(HashSet<int>))
.Build();


var serviceProvider = _container.BuildServiceProvider();

var serviceByNameFactory = serviceProvider.GetService<IServiceByNameFactory<IEnumerable<int>>>();

Assert.ThrowsException<InvalidOperationException>(() =>
{
var commandInstance = serviceByNameFactory.GetRequiredByName("list");
});