Skip to content

Commit

Permalink
Support named services (#153)
Browse files Browse the repository at this point in the history
  • Loading branch information
pakrym authored Dec 20, 2023
1 parent 0981bd7 commit 37627e2
Show file tree
Hide file tree
Showing 24 changed files with 950 additions and 419 deletions.
33 changes: 33 additions & 0 deletions src/Jab.FunctionalTests.Common/ContainerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1241,6 +1241,39 @@ internal partial class SupportsInstancePropertyFactoriesOnModulesContainer
{
Func<IService> Instance = () => new ServiceImplementation();
}

[Fact]
public void SupportsNamedServices()
{
SupportsNamedServicesContainer c = new();

var notNamed = c.GetService<IService>();
Assert.IsType<ServiceImplementation>(notNamed);

var named = c.GetService<IService>("Named");
Assert.IsType<ServiceImplementation2>(named);

var onlyNamed = c.GetService<IAnotherService>("OnlyNamed");
Assert.IsType<AnotherServiceImplementation>(onlyNamed);

var service = c.GetService<ServiceImplementationWithNamed<IService>>();
Assert.IsType<ServiceImplementation2>(service.InnerService);
Assert.Same(named, service.InnerService);

var services = c.GetService<IEnumerable<IService>>();
var single = Assert.Single(services);
Assert.Same(notNamed, single);
}

[ServiceProvider]
[Singleton(typeof(IService), typeof(ServiceImplementation))]
[Singleton(typeof(IService), typeof(ServiceImplementation), Name="Named")]
[Singleton(typeof(IService), typeof(ServiceImplementation2), Name="Named")]
[Singleton(typeof(IAnotherService), typeof(AnotherServiceImplementation), Name="OnlyNamed")]
[Singleton(typeof(ServiceImplementationWithNamed<IService>))]
internal partial class SupportsNamedServicesContainer
{
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/Jab.FunctionalTests.Common/Mocks/ServiceImplementation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ internal class ServiceImplementation : IService, IService1, IService2, IService3
{
}

internal class ServiceImplementation2 : IService, IService1, IService2, IService3
{
}

internal class ServiceImplementation<T> : IService<T>
{
public ServiceImplementation(T innerService)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Jab;

namespace JabTests;

public class ServiceImplementationWithNamed<T>: IService<T>
{
public T InnerService { get; }
public ServiceImplementationWithNamed([FromNamedServices("Named")] T innerService)
{
InnerService = innerService;
}
}
43 changes: 41 additions & 2 deletions src/Jab.FunctionalTests.MEDI/MEDIContainerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public void CanUseIsService()
{
CanUseIsServiceContainer c = new();
IServiceProviderIsService iss = c;

Assert.True(iss.IsService(typeof(IServiceProvider)));
Assert.True(iss.IsService(typeof(IServiceProviderIsService)));
Assert.True(iss.IsService(typeof(IServiceScopeFactory)));
Expand All @@ -65,7 +65,7 @@ internal partial class CanUseIsServiceContainer
public void CanResolveIsService()
{
CanUseIsServiceContainer c = new();

Assert.True(c.GetService<IServiceProviderIsService>().IsService(typeof(IServiceProvider)));
Assert.Same(c, c.CreateScope().GetService<IServiceProviderIsService>());
Assert.True(c.CreateScope().GetService<IServiceProviderIsService>().IsService(typeof(IServiceProvider)));
Expand All @@ -76,5 +76,44 @@ internal partial class CanResolveIsServiceContainer
{
}
#endif

#if NET8_OR_GREATER
[Fact]
public void SupportsKeyedServices()
{
SupportsKeyedServicesContainer c = new();

Assert.IsAssignableFrom<IKeyedServiceProvider>(c);

Assert.NotNull(c.GetKeyedService<ServiceImplementation>("Key"));
Assert.NotNull(c.GetRequiredKeyedService<ServiceImplementation>("Key"));

Assert.Null(c.GetKeyedService<ServiceImplementation>("Bla"));
Assert.Null(c.GetKeyedService<IService>("Bla"));
Assert.Throws<InvalidOperationException>(() => c.GetRequiredKeyedService<ServiceImplementation>("Bla"));
Assert.Throws<InvalidOperationException>(() => c.GetRequiredKeyedService<IService>("Bla"));

var serviceWithKeyedParameter = c.GetService<ServiceWithKeyedParameter<ServiceImplementation>>();
Assert.NotNull(serviceWithKeyedParameter);
Assert.NotNull(serviceWithKeyedParameter.InnerService);
}

[ServiceProvider]
[Singleton(typeof(ServiceImplementation), Name="Key")]
[Singleton(typeof(ServiceWithKeyedParameter<ServiceImplementation>))]
internal partial class SupportsKeyedServicesContainer
{
}

internal class ServiceWithKeyedParameter<T>
{
public T InnerService { get; }

public ServiceWithKeyedParameter([FromKeyedServices(typeof(string))] T innerService)
{
InnerService = innerService;
}
}
#endif
}
}
118 changes: 111 additions & 7 deletions src/Jab.Tests/DiagnosticsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,25 @@ await Verify.VerifyAnalyzerAsync(testCode,
.WithArguments("IDependency", "Service"));
}


[Fact]
public async Task ProducesJAB0019WhenRequiredNamedDependencyNotFound()
{
string testCode = $@"
class Dependency {{ }}
class Service {{ public Service([FromNamedServices(""Named"")] Dependency dep) {{}} }}
[ServiceProvider]
[{{|#1:Transient(typeof(Service))|}}]
[Transient(typeof(Dependency))]
public partial class Container {{}}
";
await Verify.VerifyAnalyzerAsync(testCode,
DiagnosticResult
.CompilerError("JAB0019")
.WithLocation(1)
.WithArguments("Dependency", "Named", "Service"));
}

[Fact]
public async Task ProducesJAB0002WhenRequiredDependenciesNotFound()
{
Expand Down Expand Up @@ -204,9 +223,9 @@ public async Task ProducesJAB0008WhenCircularChainDetected()
interface IService {{}}
class FirstService {{ public FirstService(IService s) {{}} }}
class Service : IService {{ public Service(AnotherService s) {{}} }}
class AnotherService {{ public AnotherService(IService s) {{}} }}
class AnotherService {{ public AnotherService({{|#1:IService|}} s) {{}} }}
[ServiceProvider]
[{{|#1:Transient(typeof(FirstService))|}}]
[Transient(typeof(FirstService))]
[Transient(typeof(IService), typeof(Service))]
[Transient(typeof(AnotherService))]
public partial class Container {{}}
Expand All @@ -215,7 +234,7 @@ await Verify.VerifyAnalyzerAsync(testCode,
DiagnosticResult
.CompilerError("JAB0008")
.WithLocation(1)
.WithArguments("FirstService", "IService", "FirstService -> IService -> Service -> AnotherService -> IService"));
.WithArguments("IService", "FirstService -> IService -> Service -> AnotherService -> IService"));
}

[Fact]
Expand Down Expand Up @@ -249,21 +268,31 @@ await Verify.VerifyAnalyzerAsync(testCode,
}

[Fact]
public async Task ProducesJAB0010IfGetServiceCallTypeUnregistered()
public async Task ProducesJAB0010OrJAB0018IfGetServiceCallTypeUnregistered()
{
string testCode = $@"
interface IService {{}}
[ServiceProvider]
public partial class Container {{
public T GetService<T>() => default;
public static void Main() {{ var container = new Container(); {{|#1:container.GetService<IService>()|}}; }}
public T GetService<T>(string name) => default;
public static void Main() {{
var container = new Container();
{{|#1:container.GetService<IService>()|}};
{{|#2:container.GetService<IService>(""Named"")|}};
}}
}}
";
await Verify.VerifyAnalyzerAsync(testCode,
DiagnosticResult
.CompilerError("JAB0010")
.WithLocation(1)
.WithArguments("IService"));
.WithArguments("IService"),

DiagnosticResult
.CompilerError("JAB0018")
.WithLocation(2)
.WithArguments("IService", "Named"));
}

[Fact]
Expand All @@ -281,6 +310,81 @@ await Verify.VerifyAnalyzerAsync(testCode,
.WithArguments("IService"));
}

[Fact]
public async Task ProducesDiagnosticWhenServiceNameNotAlphanumeric()
{
string testCode = @"
public class Service {}
[ServiceProvider]
[{|#1:Singleton(typeof(Service), Name = """")|}]
[{|#2:Singleton(typeof(Service), Name = ""'"")|}]
[{|#3:Singleton(typeof(Service), Name = ""1a"")|}]
[Singleton(typeof(Service), Name = ""aA10"")]
public partial class Container {}
";
await Verify.VerifyAnalyzerAsync(testCode,
DiagnosticResult
.CompilerError("JAB0015")
.WithLocation(1)
.WithArguments(""),

DiagnosticResult
.CompilerError("JAB0015")
.WithLocation(2)
.WithArguments("'"),

DiagnosticResult
.CompilerError("JAB0015")
.WithLocation(3)
.WithArguments("1a"));
}

[Fact]
public async Task ProducesDiagnosticWhenBuiltInServicesRequestedAsNamed()
{
string testCode = @"
public class Service {
public Service(
[FromNamedServices(""A"")] {|#1:IServiceProvider|} sp
) {}
}
[ServiceProvider]
[Singleton(typeof(Service))]
public partial class Container {}
";
await Verify.VerifyAnalyzerAsync(testCode,
DiagnosticResult
.CompilerError("JAB0016")
.WithLocation(1)
.WithArguments("System.IServiceProvider"));
}


[Fact]
public async Task ProducesDiagnosticWhenImplicitIEnumerableRequestedAsNamed()
{
string testCode = @"
public class Service1 {}
public class Service {
public Service(
[FromNamedServices(""A"")] {|#1:IEnumerable<Service1>|} s,
IEnumerable<Service1> ss
) {}
}
[ServiceProvider]
[Singleton(typeof(Service))]
[Singleton(typeof(Service1))]
public partial class Container {}
";
await Verify.VerifyAnalyzerAsync(testCode,
DiagnosticResult
.CompilerError("JAB0017")
.WithLocation(1)
.WithArguments("System.Collections.Generic.IEnumerable<Service1>"));
}

[Fact]
public async Task ProducesJAB0013WhenNullableNonOptionalDependencyNotFound()
{
Expand Down Expand Up @@ -316,7 +420,7 @@ public partial class Container {{}}
await Verify.VerifyAnalyzerAsync(testCode,
DiagnosticResult
.CompilerError("JAB0014")
.WithSeverity(DiagnosticSeverity.Warning)
.WithSeverity(DiagnosticSeverity.Info)
.WithLocation(1)
.WithArguments("IDependency?", "Service"));
}
Expand Down
1 change: 1 addition & 0 deletions src/Jab.Tests/GeneratorAnalyzerVerifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public static Task VerifyAnalyzerAsync(string source, params DiagnosticResult[]
{
source = @"
using System;
using System.Collections.Generic;
using Jab;
" + source;
var test = new GeneratorAnalyzerTest<TAnalyzer>
Expand Down
4 changes: 2 additions & 2 deletions src/Jab/ArrayServiceCallSite.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

internal record ArrayServiceCallSite: ServiceCallSite
{
public ArrayServiceCallSite(INamedTypeSymbol serviceType, INamedTypeSymbol implementationType, ITypeSymbol itemType, ServiceCallSite[] items, ServiceLifetime lifetime)
: base(serviceType, implementationType, lifetime, 0, false)
public ArrayServiceCallSite(ServiceIdentity identity, INamedTypeSymbol implementationType, ITypeSymbol itemType, ServiceCallSite[] items, ServiceLifetime lifetime)
: base(identity, implementationType, lifetime, false)
{
ItemType = itemType;
Items = items;
Expand Down
42 changes: 38 additions & 4 deletions src/Jab/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ class SingletonAttribute: Attribute
{
public Type ServiceType { get; }

public string? Name { get; set; }

public Type? ImplementationType { get; }

public string? Instance { get; set; }
Expand Down Expand Up @@ -88,6 +90,7 @@ public SingletonAttribute(Type serviceType, Type implementationType)
class TransientAttribute : Attribute
{
public Type ServiceType { get; }
public string? Name { get; set; }

public Type? ImplementationType { get; }

Expand Down Expand Up @@ -115,6 +118,7 @@ public TransientAttribute(Type serviceType, Type implementationType)
class ScopedAttribute : Attribute
{
public Type ServiceType { get; }
public string? Name { get; set; }

public Type? ImplementationType { get; }

Expand All @@ -132,6 +136,23 @@ public ScopedAttribute(Type serviceType, Type implementationType)
}
}


[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
#if JAB_ATTRIBUTES_PACKAGE
public
#else
internal
#endif
class FromNamedServicesAttribute : Attribute
{
public string? Name { get; set; }

public FromNamedServicesAttribute(string name)
{
Name = name;
}
}

#if GENERIC_ATTRIBUTES
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = true)]
#if JAB_ATTRIBUTES_PACKAGE
Expand Down Expand Up @@ -250,13 +271,26 @@ interface IServiceProvider<T>
#else
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Jab", null)]
internal
#endif
interface INamedServiceProvider<T>
{
T GetService(string name);
}

#if JAB_ATTRIBUTES_PACKAGE
public
#else
internal
#endif
static class JabHelpers
{
public static InvalidOperationException CreateServiceNotFoundException<T>()
{
return new InvalidOperationException($"Service Type {typeof(T)} not registered");
}
public static InvalidOperationException CreateServiceNotFoundException<T>(string? name = null) =>
CreateServiceNotFoundException(typeof(T), name);
public static InvalidOperationException CreateServiceNotFoundException(Type type, string? name = null) =>
new InvalidOperationException(
name != null ?
$"Service with type {type} and name {name} not registered" :
$"Service with type {type} not registered");
}
}

Expand Down
Loading

0 comments on commit 37627e2

Please sign in to comment.