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

WIP Feature: Use Microsoft KeyedServiceProvider #1075

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4a5123c
Use KeyedServiceProvide instead of ContractDictionary for Splat.Micro…
OleksandrTsvirkun Feb 16, 2024
221eea8
Merge branch 'main' into feature/update-microsoft-di-keyed-service-pr…
ChrisPulman Feb 17, 2024
a0f94b3
Merge branch 'main' into feature/update-microsoft-di-keyed-service-pr…
ChrisPulman Feb 17, 2024
a7b8f27
refactor service contract check
dpvreony Feb 23, 2024
400641e
add another use of refactor, update xmldoc
dpvreony Feb 23, 2024
5c80516
Merge branch 'main' into feature/update-microsoft-di-keyed-service-pr…
ChrisPulman Feb 26, 2024
0ba3592
Merge branch 'main' into feature/update-microsoft-di-keyed-service-pr…
ChrisPulman Feb 29, 2024
5de1f56
Merge branch 'main' into feature/update-microsoft-di-keyed-service-pr…
dpvreony Mar 5, 2024
5365add
Merge branch 'main' into feature/update-microsoft-di-keyed-service-pr…
dpvreony Mar 18, 2024
437c9a5
add net8 as test framework
dpvreony Mar 18, 2024
a9b8a55
Merge branch 'main' into feature/update-microsoft-di-keyed-service-pr…
ChrisPulman Apr 13, 2024
9eaa2e3
Merge branch 'main' into feature/update-microsoft-di-keyed-service-pr…
dpvreony Apr 26, 2024
77555d6
Merge branch 'main' into feature/update-microsoft-di-keyed-service-pr…
ChrisPulman May 1, 2024
83a2927
Merge branch 'main' into feature/update-microsoft-di-keyed-service-pr…
ChrisPulman May 1, 2024
e327002
restore contract dictionary
dpvreony May 8, 2024
d08a6a4
Merge branch 'main' into feature/update-microsoft-di-keyed-service-pr…
dpvreony May 8, 2024
7f8d1b0
remove contract dictionary, not actually needed
dpvreony May 9, 2024
17fe3a1
Merge branch 'main' into feature/update-microsoft-di-keyed-service-pr…
dpvreony May 15, 2024
File filter

Filter by extension

Filter by extension


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

<PropertyGroup>
<TargetFrameworks>net6.0</TargetFrameworks>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<NoWarn>$(NoWarn);1591;CA1707;SA1633;CA2000</NoWarn>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using System.Collections.Concurrent;
using System.Data;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.DependencyInjection;

namespace Splat.Microsoft.Extensions.DependencyInjection;
Expand All @@ -17,7 +15,6 @@
public class MicrosoftDependencyResolver : IDependencyResolver
{
private const string ImmutableExceptionMessage = "This container has already been built and cannot be modified.";
private static readonly Type _dictionaryType = typeof(ContractDictionary<>);
private readonly object _syncLock = new();
private IServiceCollection? _serviceCollection;
private bool _isImmutable;
Expand Down Expand Up @@ -91,29 +88,27 @@
var isNull = serviceType is null;
serviceType ??= typeof(NullServiceType);

IEnumerable<object> services;
IEnumerable<object> services = Enumerable.Empty<object>();

if (contract is null || string.IsNullOrWhiteSpace(contract))
{
// this is to deal with CS8613 that GetServices returns IEnumerable<object?>?
services = ServiceProvider.GetServices(serviceType)
.Where(a => a is not null)
.Select(a => a!);

if (isNull)
{
services = services
.Cast<NullServiceType>()
.Select(nst => nst.Factory()!);
}
}
else
else if (ServiceProvider is IKeyedServiceProvider serviceProvider)
dpvreony marked this conversation as resolved.
Show resolved Hide resolved
{
services = serviceProvider.GetKeyedServices(serviceType, contract)
.Where(a => a is not null)
.Select(a => a!);
}

if (isNull)
{
var dic = GetContractDictionary(serviceType, false);
services = dic?
.GetFactories(contract)
.Select(f => f()!)
?? Array.Empty<object>();
services = services
.Cast<NullServiceType>()
.Select(nst => nst.Factory()!);
}

return services;
Expand Down Expand Up @@ -142,9 +137,10 @@
}
else
{
var dic = GetContractDictionary(serviceType, true);

dic?.AddFactory(contract, factory);
_serviceCollection?.AddKeyedTransient(serviceType, contract, (_, __) =>
isNull
? new NullServiceType(factory)
: factory()!);
}

// required so that it gets rebuilt if not injected externally.
Expand All @@ -166,22 +162,18 @@
{
if (contract is null || string.IsNullOrWhiteSpace(contract))
{
var sd = _serviceCollection?.LastOrDefault(s => s.ServiceType == serviceType);
var sd = _serviceCollection?.LastOrDefault(s => !s.IsKeyedService && s.ServiceType == serviceType);
if (sd is not null)
{
_serviceCollection?.Remove(sd);
}
}
else
{
var dic = GetContractDictionary(serviceType, false);
if (dic is not null)
var sd = _serviceCollection?.LastOrDefault(sd => MatchesKeyedContract(serviceType, contract, sd));
if (sd is not null)
{
dic.RemoveLastFactory(contract);
if (dic.IsEmpty)
{
RemoveContractService(serviceType);
}
_serviceCollection?.Remove(sd);
}
}

Expand All @@ -196,7 +188,7 @@
/// ignoring the <paramref name="serviceType"/> argument.
/// </summary>
/// <param name="serviceType">The service type to unregister.</param>
/// <param name="contract">This parameter is ignored. Service will be removed from all contracts.</param>
/// <param name="contract">A optional value which will remove only an object registered with the same contract.</param>
dpvreony marked this conversation as resolved.
Show resolved Hide resolved
public virtual void UnregisterAll(Type? serviceType, string? contract = null)
{
if (_isImmutable)
Expand All @@ -208,34 +200,28 @@

lock (_syncLock)
{
switch (contract)
if (_serviceCollection is null)
dpvreony marked this conversation as resolved.
Show resolved Hide resolved
{
// required so that it gets rebuilt if not injected externally.
_serviceProvider = null;
return;

Check warning on line 207 in src/Splat.Microsoft.Extensions.DependencyInjection/MicrosoftDependencyResolver.cs

View check run for this annotation

Codecov / codecov/patch

src/Splat.Microsoft.Extensions.DependencyInjection/MicrosoftDependencyResolver.cs#L206-L207

Added lines #L206 - L207 were not covered by tests
}

IEnumerable<ServiceDescriptor> sds = Enumerable.Empty<ServiceDescriptor>();

if (contract is null || string.IsNullOrWhiteSpace(contract))
{
case null when _serviceCollection is not null:
{
var sds = _serviceCollection
.Where(s => s.ServiceType == serviceType)
.ToList();

foreach (var sd in sds)
{
_serviceCollection.Remove(sd);
}

break;
}

case null:
throw new ArgumentException("There must be a valid contract if there is no service collection.", nameof(contract));
default:
{
var dic = GetContractDictionary(serviceType, false);
if (dic?.TryRemoveContract(contract) == true && dic.IsEmpty)
{
RemoveContractService(serviceType);
}

break;
}
sds = _serviceCollection.Where(s => !s.IsKeyedService && s.ServiceType == serviceType);
}
else
{
sds = _serviceCollection
.Where(sd => MatchesKeyedContract(serviceType, contract, sd));
}

foreach (var sd in sds.ToList())
{
_serviceCollection.Remove(sd);
}

// required so that it gets rebuilt if not injected externally.
Expand All @@ -255,16 +241,10 @@
{
if (contract is null || string.IsNullOrWhiteSpace(contract))
{
return _serviceCollection?.Any(sd => sd.ServiceType == serviceType) == true;
return _serviceCollection?.Any(sd => !sd.IsKeyedService && sd.ServiceType == serviceType) == true;
}

var dictionary = (ContractDictionary?)_serviceCollection?.FirstOrDefault(sd => sd.ServiceType == GetDictionaryType(serviceType))?.ImplementationInstance;

return dictionary switch
{
null => false,
_ => dictionary.GetFactories(contract).Select(f => f()).Any()
};
return _serviceCollection?.Any(sd => MatchesKeyedContract(serviceType, contract, sd)) == true;
}

if (contract is null)
Expand All @@ -273,8 +253,12 @@
return service is not null;
}

var dic = GetContractDictionary(serviceType, false);
return dic?.IsEmpty == false;
if (_serviceProvider is IKeyedServiceProvider keyedServiceProvider)
{
return keyedServiceProvider.GetKeyedService(serviceType, contract) is not null;

Check warning on line 258 in src/Splat.Microsoft.Extensions.DependencyInjection/MicrosoftDependencyResolver.cs

View check run for this annotation

Codecov / codecov/patch

src/Splat.Microsoft.Extensions.DependencyInjection/MicrosoftDependencyResolver.cs#L258

Added line #L258 was not covered by tests
}

return false;

Check warning on line 261 in src/Splat.Microsoft.Extensions.DependencyInjection/MicrosoftDependencyResolver.cs

View check run for this annotation

Codecov / codecov/patch

src/Splat.Microsoft.Extensions.DependencyInjection/MicrosoftDependencyResolver.cs#L261

Added line #L261 was not covered by tests
}

/// <inheritdoc />
Expand All @@ -292,103 +276,9 @@
{
}

private static Type GetDictionaryType(Type serviceType) => _dictionaryType.MakeGenericType(serviceType);
dpvreony marked this conversation as resolved.
Show resolved Hide resolved

private void RemoveContractService(Type serviceType)
{
var dicType = GetDictionaryType(serviceType);
var sd = _serviceCollection?.SingleOrDefault(s => s.ServiceType == serviceType);

if (sd is not null)
{
_serviceCollection?.Remove(sd);
}
}

[SuppressMessage("Naming Rules", "SA1300", Justification = "Intentional")]
private ContractDictionary? GetContractDictionary(Type serviceType, bool createIfNotExists)
{
var dicType = GetDictionaryType(serviceType);

if (ServiceProvider is null)
{
throw new InvalidOperationException("The ServiceProvider is null.");
}

if (_isImmutable)
{
return (ContractDictionary?)ServiceProvider.GetService(dicType);
}

var dic = getDictionary();
if (createIfNotExists && dic is null)
{
lock (_syncLock)
{
if (createIfNotExists)
{
dic = (ContractDictionary?)Activator.CreateInstance(dicType);

if (dic is not null)
{
_serviceCollection?.AddSingleton(dicType, dic);
}
}
}
}

return dic;

ContractDictionary? getDictionary() => _serviceCollection?
.Where(sd => sd.ServiceType == dicType)
.Select(sd => sd.ImplementationInstance)
.Cast<ContractDictionary>()
.SingleOrDefault();
}

private class ContractDictionary
{
private readonly ConcurrentDictionary<string, List<Func<object?>>> _dictionary = new();

public bool IsEmpty => _dictionary.IsEmpty;

public bool TryRemoveContract(string contract) =>
_dictionary.TryRemove(contract, out var _);

public Func<object?>? GetFactory(string contract) =>
GetFactories(contract)
.LastOrDefault();

public IEnumerable<Func<object?>> GetFactories(string contract) =>
_dictionary.TryGetValue(contract, out var collection)
? collection ?? Enumerable.Empty<Func<object?>>()
: Array.Empty<Func<object?>>();

public void AddFactory(string contract, Func<object?> factory) =>
_dictionary.AddOrUpdate(contract, _ => new() { factory }, (_, list) =>
{
(list ??= []).Add(factory);
return list;
});

public void RemoveLastFactory(string contract) =>
_dictionary.AddOrUpdate(contract, [], (_, list) =>
{
var lastIndex = list.Count - 1;
if (lastIndex > 0)
{
list.RemoveAt(lastIndex);
}

// TODO if list empty remove contract entirely
// need to find how to atomically update or remove
// https://github.com/dotnet/corefx/issues/24246
return list;
});
}

[SuppressMessage("Design", "CA1812: Unused class.", Justification = "Used in reflection.")]
private sealed class ContractDictionary<T> : ContractDictionary
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool MatchesKeyedContract(Type? serviceType, string contract, ServiceDescriptor sd) =>
sd.ServiceType == serviceType
&& sd is { IsKeyedService: true, ServiceKey: string serviceKey }
&& serviceKey == contract;
}