Skip to content

Commit

Permalink
Drastically simplify implementation by removing customization
Browse files Browse the repository at this point in the history
The customization of the class and namespace for the `AddServices` method added non-trivial complexity to the whole process.

Here we simplify it in two ways:

1 - In non-editor usage, the files are simply added as Compile, no need to process them
2 - In editor usage, we don't include them since we emit the [Obsolete] warning as needed.

This means we only need to do the additional file inclusion when building in the editor, which should minimize the impact of our generators on CI/CLI even more.
  • Loading branch information
kzu committed Dec 6, 2024
1 parent 10a993a commit 4db4b1b
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 160 deletions.
15 changes: 0 additions & 15 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,21 +290,6 @@ parameters), you can annotate it with `[ImportingConstructor]` from either NuGet
([System.Composition](http://nuget.org/packages/System.Composition.AttributedModel))
or .NET MEF ([System.ComponentModel.Composition](https://www.nuget.org/packages/System.ComponentModel.Composition)).
### Customize Generated Class

You can customize the generated class namespace and name with the following
MSBuild properties:

```xml
<PropertyGroup>
<AddServicesNamespace>MyNamespace</AddServicesNamespace>
<AddServicesClassName>MyExtensions</AddServicesClassName>
</PropertyGroup>
```

They default to `Microsoft.Extensions.DependencyInjection` and `AddServicesNoReflectionExtension`
respectively.

<!-- #content -->

# Dogfooding
Expand Down
3 changes: 2 additions & 1 deletion src/DependencyInjection/DependencyInjection.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
<ItemGroup>
<None Update="Devlooped.Extensions.DependencyInjection.props" CopyToOutputDirectory="PreserveNewest" PackFolder="build" />
<None Update="Devlooped.Extensions.DependencyInjection.targets" CopyToOutputDirectory="PreserveNewest" PackFolder="build" />
<EmbeddedCode Include="ServiceAttribute*.cs;AddServicesNoReflectionExtension.cs" />
<None Include="compile\*.cs" CopyToOutputDirectory="PreserveNewest" PackFolder="build\compile" />
<EmbeddedCode Include="compile\*.cs" />
</ItemGroup>

<PropertyGroup Label="SponsorLink">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@
<DevloopedExtensionsDependencyInjectionVersion>42.42.42</DevloopedExtensionsDependencyInjectionVersion>
<AddServiceAttribute Condition="'$(AddServiceAttribute)' == ''">true</AddServiceAttribute>
<AddServicesExtension Condition="'$(AddServicesExtension)' == ''">true</AddServicesExtension>
</PropertyGroup>

<ItemGroup>
<CompilerVisibleProperty Include="IsTestProject" />
<CompilerVisibleProperty Include="AddServicesNamespace" />
<CompilerVisibleProperty Include="AddServicesClassName" />
</ItemGroup>
<IsVisualStudio Condition="'$(ServiceHubLogSessionKey)' != '' or '$(VSAPPIDNAME)' != ''">true</IsVisualStudio>
<IsRider Condition="'$(RESHARPER_FUS_SESSION)' != '' or '$(IDEA_INITIAL_DIRECTORY)' != ''"></IsRider>
<IsEditor Condition="'$(IsVisualStudio)' == 'true' or '$(IsRider)' == 'true'">true</IsEditor>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,27 @@
<PropertyGroup>
<!-- Backwards compatiblity -->
<AddServiceAttribute Condition="'$(IncludeServiceAttribute)' != ''">$(IncludeServiceAttribute)</AddServiceAttribute>
<DefineConstants Condition="'$(Language)' == 'C#' and '$(AddServiceAttribute)' == 'true'">$(DefineConstants);DDI_ADDSERVICE</DefineConstants>
<DefineConstants Condition="'$(Language)' == 'C#' and '$(AddServicesExtension)' == 'true'">$(DefineConstants);DDI_ADDSERVICES</DefineConstants>
</PropertyGroup>

<ItemGroup>
<!-- Brings in the analyzer file to report installation time -->
<FundingPackageId Include="Devlooped.Extensions.DependencyInjection" />
</ItemGroup>

<Target Name="_AddDDI_Constant" BeforeTargets="CoreCompile">
<PropertyGroup>
<DefineConstants Condition="'$(Language)' == 'C#' and '$(AddServiceAttribute)' == 'true' and !$(DefineConstants.Contains('DDI_ADDSERVICE'))">$(DefineConstants);DDI_ADDSERVICE</DefineConstants>
<DefineConstants Condition="'$(Language)' == 'C#' and '$(AddServicesExtension)' == 'true' and !$(DefineConstants.Contains('DDI_ADDSERVICES'))">$(DefineConstants);DDI_ADDSERVICES</DefineConstants>
</PropertyGroup>
</Target>

<ItemGroup>
<CompilerVisibleProperty Include="AddServiceAttribute" />
<CompilerVisibleProperty Include="AddServicesExtension" />
</ItemGroup>

<ItemGroup Condition="'$(IsEditor)' != 'true'">
<Compile Include="$(MSBuildThisFileDirectory)compile\ServiceAttribute*.cs"
Visible="false"
PackageId="Devlooped.Extensions.DependencyInjection"
Condition="'$(AddServiceAttribute)' == 'true'" />
<Compile Include="$(MSBuildThisFileDirectory)compile\AddServices*.cs"
Visible="false"
PackageId="Devlooped.Extensions.DependencyInjection"
Condition="'$(AddServicesExtension)' == 'true'" />
</ItemGroup>

</Project>
113 changes: 74 additions & 39 deletions src/DependencyInjection/IncrementalGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,44 @@ record ServiceRegistration(int Lifetime, TypeSyntax? AssignableTo, string? FullN

public void Initialize(IncrementalGeneratorInitializationContext context)
{
var types = context.CompilationProvider.SelectMany((x, c) =>
var compilation = context.CompilationProvider.Select((compilation, _) =>
{
var visitor = new TypesVisitor(s => x.IsSymbolAccessible(s), c);
x.GlobalNamespace.Accept(visitor);
// Add missing types as needed since we depend on the static generator potentially and can't
// rely on its sources being added.
var parse = (CSharpParseOptions)compilation.SyntaxTrees.FirstOrDefault().Options;

if (compilation.GetTypeByMetadataName("Microsoft.Extensions.DependencyInjection.AddServicesNoReflectionExtension") is null)
{
compilation = compilation.AddSyntaxTrees(
CSharpSyntaxTree.ParseText(ThisAssembly.Resources.AddServicesNoReflectionExtension.Text, parse));
}

if (compilation.GetTypeByMetadataName("Microsoft.Extensions.DependencyInjection.ServiceAttribute") is null)
{
compilation = compilation.AddSyntaxTrees(
CSharpSyntaxTree.ParseText(ThisAssembly.Resources.ServiceAttribute.Text, parse),
CSharpSyntaxTree.ParseText(ThisAssembly.Resources.ServiceAttribute_1.Text, parse));
}

return compilation;
});

var types = compilation.Combine(context.AnalyzerConfigOptionsProvider).SelectMany((x, c) =>
{
(var compilation, var options) = x;

// We won't add any registrations in this case.
if (!options.GlobalOptions.TryGetValue("build_property.AddServicesExtension", out var value) ||
!bool.TryParse(value, out var addServices) || !addServices)
return Enumerable.Empty<INamedTypeSymbol>();

var visitor = new TypesVisitor(s => compilation.IsSymbolAccessible(s), c);
compilation.GlobalNamespace.Accept(visitor);

// Also visit aliased references, which will not become part of the global:: namespace
foreach (var symbol in x.References
foreach (var symbol in compilation.References
.Where(r => !r.Properties.Aliases.IsDefaultOrEmpty)
.Select(r => x.GetAssemblyOrModuleSymbol(r)))
.Select(r => compilation.GetAssemblyOrModuleSymbol(r)))
{
symbol?.Accept(visitor);
}
Expand Down Expand Up @@ -152,8 +182,6 @@ bool IsExport(AttributeData attr)
})
.Where(x => x != null);

var options = context.AnalyzerConfigOptionsProvider.Combine(context.CompilationProvider);

// Only requisite is that we define Scoped = 0, Singleton = 1 and Transient = 2.
// This matches https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicelifetime?view=dotnet-plat-ext-6.0#fields

Expand All @@ -164,11 +192,18 @@ bool IsExport(AttributeData attr)
.CreateSyntaxProvider(
predicate: static (node, _) => node is InvocationExpressionSyntax invocation && invocation.ArgumentList.Arguments.Count != 0 && GetInvokedMethodName(invocation) == nameof(AddServicesNoReflectionExtension.AddServices),
transform: static (ctx, _) => GetServiceRegistration((InvocationExpressionSyntax)ctx.Node, ctx.SemanticModel))
.Where(details => details != null)
.Combine(context.AnalyzerConfigOptionsProvider)
.Where(x =>
{
(var registration, var options) = x;
return options.GlobalOptions.TryGetValue("build_property.AddServicesExtension", out var value) &&
bool.TryParse(value, out var addServices) && addServices && registration is not null;
})
.Select((x, _) => x.Left)
.Collect();

// Project matching service types to register with the given lifetime.
var conventionServices = types.Combine(methodInvocations.Combine(context.CompilationProvider)).SelectMany((pair, cancellationToken) =>
var conventionServices = types.Combine(methodInvocations.Combine(compilation)).SelectMany((pair, cancellationToken) =>
{
var (typeSymbol, (registrations, compilation)) = pair;
var results = ImmutableArray.CreateBuilder<ServiceSymbol>();
Expand Down Expand Up @@ -196,33 +231,33 @@ bool IsExport(AttributeData attr)
.SelectMany((tuple, _) => ImmutableArray.CreateRange([tuple.Item1, tuple.Item2]))
.SelectMany((items, _) => items.Distinct().ToImmutableArray());

RegisterServicesOutput(context, finalServices, options);
RegisterServicesOutput(context, finalServices, compilation);
}

void RegisterServicesOutput(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider<ServiceSymbol> services, IncrementalValueProvider<(AnalyzerConfigOptionsProvider Left, Compilation Right)> options)
void RegisterServicesOutput(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider<ServiceSymbol> services, IncrementalValueProvider<Compilation> compilation)
{
context.RegisterImplementationSourceOutput(
services.Where(x => x!.Lifetime == 0 && x.Key is null).Select((x, _) => new KeyedService(x!.Type, null)).Collect().Combine(options),
services.Where(x => x!.Lifetime == 0 && x.Key is null).Select((x, _) => new KeyedService(x!.Type, null)).Collect().Combine(compilation),
(ctx, data) => AddPartial("AddSingleton", ctx, data));

context.RegisterImplementationSourceOutput(
services.Where(x => x!.Lifetime == 1 && x.Key is null).Select((x, _) => new KeyedService(x!.Type, null)).Collect().Combine(options),
services.Where(x => x!.Lifetime == 1 && x.Key is null).Select((x, _) => new KeyedService(x!.Type, null)).Collect().Combine(compilation),
(ctx, data) => AddPartial("AddScoped", ctx, data));

context.RegisterImplementationSourceOutput(
services.Where(x => x!.Lifetime == 2 && x.Key is null).Select((x, _) => new KeyedService(x!.Type, null)).Collect().Combine(options),
services.Where(x => x!.Lifetime == 2 && x.Key is null).Select((x, _) => new KeyedService(x!.Type, null)).Collect().Combine(compilation),
(ctx, data) => AddPartial("AddTransient", ctx, data));

context.RegisterImplementationSourceOutput(
services.Where(x => x!.Lifetime == 0 && x.Key is not null).Select((x, _) => new KeyedService(x!.Type, x.Key!)).Collect().Combine(options),
services.Where(x => x!.Lifetime == 0 && x.Key is not null).Select((x, _) => new KeyedService(x!.Type, x.Key!)).Collect().Combine(compilation),
(ctx, data) => AddPartial("AddKeyedSingleton", ctx, data));

context.RegisterImplementationSourceOutput(
services.Where(x => x!.Lifetime == 1 && x.Key is not null).Select((x, _) => new KeyedService(x!.Type, x.Key!)).Collect().Combine(options),
services.Where(x => x!.Lifetime == 1 && x.Key is not null).Select((x, _) => new KeyedService(x!.Type, x.Key!)).Collect().Combine(compilation),
(ctx, data) => AddPartial("AddKeyedScoped", ctx, data));

context.RegisterImplementationSourceOutput(
services.Where(x => x!.Lifetime == 2 && x.Key is not null).Select((x, _) => new KeyedService(x!.Type, x.Key!)).Collect().Combine(options),
services.Where(x => x!.Lifetime == 2 && x.Key is not null).Select((x, _) => new KeyedService(x!.Type, x.Key!)).Collect().Combine(compilation),
(ctx, data) => AddPartial("AddKeyedTransient", ctx, data));
}

Expand All @@ -240,13 +275,22 @@ void RegisterServicesOutput(IncrementalGeneratorInitializationContext context, I

var options = (CSharpParseOptions)invocation.SyntaxTree.Options;

// NOTE: we need to add the sources that *another* generator emits (the static files)
// because otherwise all invocations will basically have no semantic info since it wasn't there
// when the source generations invocations started.
var compilation = semanticModel.Compilation.AddSyntaxTrees(
CSharpSyntaxTree.ParseText(ThisAssembly.Resources.ServiceAttribute.Text, options),
CSharpSyntaxTree.ParseText(ThisAssembly.Resources.ServiceAttribute_1.Text, options),
CSharpSyntaxTree.ParseText(ThisAssembly.Resources.AddServicesNoReflectionExtension.Text, options));
var compilation = semanticModel.Compilation;

// Add missing types as needed since we depend on the static generator potentially and can't
// rely on its sources being added.
if (compilation.GetTypeByMetadataName("Microsoft.Extensions.DependencyInjection.AddServicesNoReflectionExtension") is null)
{
compilation = compilation.AddSyntaxTrees(
CSharpSyntaxTree.ParseText(ThisAssembly.Resources.AddServicesNoReflectionExtension.Text, options));
}

if (compilation.GetTypeByMetadataName("Microsoft.Extensions.DependencyInjection.ServiceAttribute") is null)
{
compilation = compilation.AddSyntaxTrees(
CSharpSyntaxTree.ParseText(ThisAssembly.Resources.ServiceAttribute.Text, options),
CSharpSyntaxTree.ParseText(ThisAssembly.Resources.ServiceAttribute_1.Text, options));
}

var model = compilation.GetSemanticModel(invocation.SyntaxTree);

Expand Down Expand Up @@ -292,46 +336,37 @@ void RegisterServicesOutput(IncrementalGeneratorInitializationContext context, I
return null;
}

void AddPartial(string methodName, SourceProductionContext ctx, (ImmutableArray<KeyedService> Types, (AnalyzerConfigOptionsProvider Config, Compilation Compilation) Options) data)
void AddPartial(string methodName, SourceProductionContext ctx, (ImmutableArray<KeyedService> Types, Compilation Compilation) data)
{
var builder = new StringBuilder()
.AppendLine("// <auto-generated />");

var rootNs = data.Options.Config.GlobalOptions.TryGetValue("build_property.AddServicesNamespace", out var value) && !string.IsNullOrEmpty(value)
? value
: "Microsoft.Extensions.DependencyInjection";

var className = data.Options.Config.GlobalOptions.TryGetValue("build_property.AddServicesClassName", out value) && !string.IsNullOrEmpty(value) ?
value : "AddServicesNoReflectionExtension";

foreach (var alias in data.Options.Compilation.References.SelectMany(r => r.Properties.Aliases))
foreach (var alias in data.Compilation.References.SelectMany(r => r.Properties.Aliases))
{
builder.AppendLine($"extern alias {alias};");
}

builder.AppendLine(
$$"""
#if DDI_ADDSERVICES
using Microsoft.Extensions.DependencyInjection.Extensions;
using System;
namespace {{rootNs}}
namespace Microsoft.Extensions.DependencyInjection
{
static partial class {{className}}
static partial class AddServicesNoReflectionExtension
{
static partial void {{methodName}}Services(IServiceCollection services)
{
""");

AddServices(data.Types.Where(x => x.Key is null).Select(x => x.Type), data.Options.Compilation, methodName, builder);
AddKeyedServices(data.Types.Where(x => x.Key is not null), data.Options.Compilation, methodName, builder);
AddServices(data.Types.Where(x => x.Key is null).Select(x => x.Type), data.Compilation, methodName, builder);
AddKeyedServices(data.Types.Where(x => x.Key is not null), data.Compilation, methodName, builder);

builder.AppendLine(
"""
}
}
}
#endif
""");

ctx.AddSource(methodName + ".g", builder.ToString().Replace("\r\n", "\n").Replace("\n", Environment.NewLine));
Expand Down
Loading

0 comments on commit 4db4b1b

Please sign in to comment.