Skip to content
Merged
Show file tree
Hide file tree
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
6 changes: 0 additions & 6 deletions DependencyInjection.Attributed.sln
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Attributed", "src\Dependenc
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Attributed.Tests", "src\DependencyInjection.Attributed.Tests\Attributed.Tests.csproj", "{F2E67084-FED3-4E17-A012-0E8948FD3E06}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FE116B5E-AE0C-4901-B0FE-BE41EF18EF06}"
ProjectSection(SolutionItems) = preProject
src\Directory.props = src\Directory.props
readme.md = readme.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodeAnalysis.Tests", "src\CodeAnalysis.Tests\CodeAnalysis.Tests.csproj", "{E512DEBA-FB35-47FD-AF25-3BAECCF667B1}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{3C5A7AC8-E8CC-40D6-B472-A693F742152A}"
Expand Down
27 changes: 27 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,31 @@ And that's it. The source generator will discover annotated types in the current
project and all its references too. Since the registration code is generated at
compile-time, there is no run-time reflection (or dependencies) whatsoever.

You can also avoid attributes entirely by using a convention-based approach, which
is nevertheless still compile-time checked and source-generated. This allows
registering services for which you don't even have the source code to annotate:

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddServices(typeof(IRepository), ServiceLifetime.Scoped);
// ...
```

You can also use a regular expression to match services by name instead:

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddServices(".*Service$"); // defaults to ServiceLifetime.Singleton
// ...
```

Or a combination of both, as needed. In all cases, NO run-time reflection is
ever performed, and the compile-time source generator will evaluate the types
that are assignable to the given type or matching full type names and emit
the typed registrations as needed.

### Keyed Services

[Keyed services](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-8.0#keyed-services)
Expand Down Expand Up @@ -105,6 +130,8 @@ right `INotificationService` will be injected, based on the key provided.
Note you can also register the same service using multiple keys, as shown in the
`EmailNotificationService` above.

> Keyed services are a feature of version 8.0+ of Microsoft.Extensions.DependencyInjection

## How It Works

The generated code that implements the registration looks like the following:
Expand Down
46 changes: 35 additions & 11 deletions src/CodeAnalysis.Tests/AddServicesAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

namespace Tests.CodeAnalysis;

public record AddServicesAnalyzerTests(ITestOutputHelper Output)
public class AddServicesAnalyzerTests(ITestOutputHelper Output)
{
[Fact]
public async Task NoWarningIfAddServicesPresent()
Expand All @@ -41,13 +41,19 @@ public static void Main()
""",
TestState =
{
Sources =
{
ThisAssembly.Resources.AttributedServicesExtension.Text,
ThisAssembly.Resources.ServiceAttribute.Text,
ThisAssembly.Resources.ServiceAttribute_1.Text,
},
ReferenceAssemblies = new ReferenceAssemblies(
"net6.0",
"net8.0",
new PackageIdentity(
"Microsoft.NETCore.App.Ref", "6.0.0"),
Path.Combine("ref", "net6.0"))
"Microsoft.NETCore.App.Ref", "8.0.0"),
Path.Combine("ref", "net8.0"))
.AddPackages(ImmutableArray.Create(
new PackageIdentity("Microsoft.Extensions.DependencyInjection", "6.0.0")))
new PackageIdentity("Microsoft.Extensions.DependencyInjection", "8.0.0")))
},
};

Expand Down Expand Up @@ -81,13 +87,19 @@ public static void Main()
""",
TestState =
{
Sources =
{
ThisAssembly.Resources.AttributedServicesExtension.Text,
ThisAssembly.Resources.ServiceAttribute.Text,
ThisAssembly.Resources.ServiceAttribute_1.Text,
},
ReferenceAssemblies = new ReferenceAssemblies(
"net6.0",
"net8.0",
new PackageIdentity(
"Microsoft.NETCore.App.Ref", "6.0.0"),
Path.Combine("ref", "net6.0"))
"Microsoft.NETCore.App.Ref", "8.0.0"),
Path.Combine("ref", "net8.0"))
.AddPackages(ImmutableArray.Create(
new PackageIdentity("Microsoft.Extensions.DependencyInjection.Abstractions", "6.0.0")))
new PackageIdentity("Microsoft.Extensions.DependencyInjection", "8.0.0")))
},
};

Expand Down Expand Up @@ -116,6 +128,12 @@ public static void Main()
""",
TestState =
{
Sources =
{
ThisAssembly.Resources.AttributedServicesExtension.Text,
ThisAssembly.Resources.ServiceAttribute.Text,
ThisAssembly.Resources.ServiceAttribute_1.Text,
},
ReferenceAssemblies = new ReferenceAssemblies(
"net8.0",
new PackageIdentity(
Expand Down Expand Up @@ -157,6 +175,12 @@ public static void Main()
""",
TestState =
{
Sources =
{
ThisAssembly.Resources.AttributedServicesExtension.Text,
ThisAssembly.Resources.ServiceAttribute.Text,
ThisAssembly.Resources.ServiceAttribute_1.Text,
},
ReferenceAssemblies = new ReferenceAssemblies(
"net8.0",
new PackageIdentity(
Expand All @@ -173,8 +197,8 @@ public static void Main()
await test.RunAsync();
}

class GeneratorsTest : CSharpSourceGeneratorTest<StaticGenerator, DefaultVerifier>
class GeneratorsTest : CSharpSourceGeneratorTest<IncrementalGenerator, DefaultVerifier>
{
protected override IEnumerable<Type> GetSourceGenerators() => base.GetSourceGenerators().Concat([typeof(IncrementalGenerator)]);
//protected override IEnumerable<Type> GetSourceGenerators() => base.GetSourceGenerators().Concat([typeof(IncrementalGenerator)]);
}
}
5 changes: 4 additions & 1 deletion src/CodeAnalysis.Tests/CodeAnalysis.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
Expand All @@ -13,10 +13,13 @@
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" Version="1.1.2" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" Version="1.1.2" />
<PackageReference Include="ThisAssembly.Resources" Version="2.0.8" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\DependencyInjection.Attributed\Attributed.csproj" />
</ItemGroup>

<Import Project="..\DependencyInjection.Attributed.Tests\ContentFiles.targets" />

</Project>
156 changes: 156 additions & 0 deletions src/CodeAnalysis.Tests/ConventionAnalyzerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Devlooped.Extensions.DependencyInjection.Attributed;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using Xunit;
using Xunit.Abstractions;
using AnalyzerTest = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerTest<Devlooped.Extensions.DependencyInjection.Attributed.ConventionsAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
using Verifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Devlooped.Extensions.DependencyInjection.Attributed.ConventionsAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;

namespace Tests.CodeAnalysis;

public class ConventionAnalyzerTests(ITestOutputHelper Output)
{
[Fact]
public async Task ErrorIfNonTypeOf()
{
var test = new AnalyzerTest
{
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck,
TestCode =
"""
using System;
using Microsoft.Extensions.DependencyInjection;

public static class Program
{
public static void Main()
{
var services = new ServiceCollection();
var type = typeof(IDisposable);
services.AddServices({|#0:type|});
}
}
""",
TestState =
{
Sources =
{
ThisAssembly.Resources.AttributedServicesExtension.Text,
ThisAssembly.Resources.ServiceAttribute.Text,
ThisAssembly.Resources.ServiceAttribute_1.Text,
},
ReferenceAssemblies = new ReferenceAssemblies(
"net8.0",
new PackageIdentity(
"Microsoft.NETCore.App.Ref", "8.0.0"),
Path.Combine("ref", "net8.0"))
.AddPackages(ImmutableArray.Create(
new PackageIdentity("Microsoft.Extensions.DependencyInjection", "8.0.0")))
},
};

var expected = Verifier.Diagnostic(ConventionsAnalyzer.AssignableTypeOfRequired).WithLocation(0);
test.ExpectedDiagnostics.Add(expected);

await test.RunAsync();
}

[Fact]
public async Task NoErrorOnTypeOfAndLifetime()
{
var test = new AnalyzerTest
{
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck,
TestCode =
"""
using System;
using Microsoft.Extensions.DependencyInjection;

public static class Program
{
public static void Main()
{
var services = new ServiceCollection();
services.AddServices(typeof(IDisposable), ServiceLifetime.Scoped);
}
}
""",
TestState =
{
Sources =
{
ThisAssembly.Resources.AttributedServicesExtension.Text,
ThisAssembly.Resources.ServiceAttribute.Text,
ThisAssembly.Resources.ServiceAttribute_1.Text,
},
ReferenceAssemblies = new ReferenceAssemblies(
"net8.0",
new PackageIdentity(
"Microsoft.NETCore.App.Ref", "8.0.0"),
Path.Combine("ref", "net8.0"))
.AddPackages(ImmutableArray.Create(
new PackageIdentity("Microsoft.Extensions.DependencyInjection", "8.0.0")))
},
};

//var expected = Verifier.Diagnostic(ConventionsAnalyzer.AssignableTypeOfRequired).WithLocation(0);
//test.ExpectedDiagnostics.Add(expected);

await test.RunAsync();
}

[Fact]
public async Task WarnIfOpenGeneric()
{
var test = new AnalyzerTest
{
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck,
TestCode =
"""
using System;
using Microsoft.Extensions.DependencyInjection;

public interface IRepository<T> { }
public class Repository<T> : IRepository<T> { }

public static class Program
{
public static void Main()
{
var services = new ServiceCollection();
services.AddServices({|#0:typeof(Repository<>)|}, ServiceLifetime.Scoped);
}
}
""",
TestState =
{
Sources =
{
ThisAssembly.Resources.AttributedServicesExtension.Text,
ThisAssembly.Resources.ServiceAttribute.Text,
ThisAssembly.Resources.ServiceAttribute_1.Text,
},
ReferenceAssemblies = new ReferenceAssemblies(
"net8.0",
new PackageIdentity(
"Microsoft.NETCore.App.Ref", "8.0.0"),
Path.Combine("ref", "net8.0"))
.AddPackages(ImmutableArray.Create(
new PackageIdentity("Microsoft.Extensions.DependencyInjection", "8.0.0")))
},
};

var expected = Verifier.Diagnostic(ConventionsAnalyzer.OpenGenericType).WithLocation(0);
test.ExpectedDiagnostics.Add(expected);

await test.RunAsync();
}

}
15 changes: 14 additions & 1 deletion src/DependencyInjection.Attributed.Tests/Attributed.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<Import Project="..\DependencyInjection.Attributed\Devlooped.Extensions.DependencyInjection.Attributed.props" />

Expand All @@ -8,6 +8,18 @@
<RootNamespace>Tests</RootNamespace>
</PropertyGroup>

<ItemGroup>
<Compile Remove="ComponentModelTests.cs" />
<Compile Remove="CompositionTests.cs" />
<Compile Remove="GenerationTests.cs" />
</ItemGroup>

<ItemGroup>
<None Include="ComponentModelTests.cs" />
<None Include="CompositionTests.cs" />
<None Include="GenerationTests.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.7.0" />
Expand All @@ -28,6 +40,7 @@
<Using Include="Xunit.Abstractions" />
</ItemGroup>

<Import Project="ContentFiles.targets" />
<Import Project="..\DependencyInjection.Attributed\Devlooped.Extensions.DependencyInjection.Attributed.targets" />

</Project>
16 changes: 16 additions & 0 deletions src/DependencyInjection.Attributed.Tests/ContentFiles.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project>
<!-- Simulates including the files under contentFiles/cs/netstandard2.0 in the final package -->

<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)..\DependencyInjection.Attributed\AttributedServicesExtension.cs" Link="AttributedServicesExtension.cs" Visible="false" />
<Compile Include="$(MSBuildThisFileDirectory)..\DependencyInjection.Attributed\ServiceAttribute.cs" Link="ServiceAttribute.cs" Visible="false" />
<Compile Include="$(MSBuildThisFileDirectory)..\DependencyInjection.Attributed\ServiceAttribute`1.cs" Link="ServiceAttribute`1.cs" Visible="false" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)..\DependencyInjection.Attributed\AttributedServicesExtension.cs" Type="Non-Resx" />
<EmbeddedResource Include="$(MSBuildThisFileDirectory)..\DependencyInjection.Attributed\ServiceAttribute.cs" Type="Non-Resx" />
<EmbeddedResource Include="$(MSBuildThisFileDirectory)..\DependencyInjection.Attributed\ServiceAttribute`1.cs" Type="Non-Resx" />
</ItemGroup>

</Project>
Loading