Skip to content

Commit

Permalink
Added support for other dependency injection containers (e.g. Autofac)
Browse files Browse the repository at this point in the history
- Added support for other dependency injection containers (e.g. Autofac) when
  only `Microsoft.Extensions.DependencyInjection.Abstractions` package (version >= 2.1.1) is added to the project.
  You can set your custom service provider with the extension method `Cli.Ext.SetServiceProvider`.
  In previous version, this method already existed but accepted a `ServiceProvider` parameter, instead of
  `IServiceProvider` parameter which allows 3rd party implementations other than the default one in `Microsoft.Extensions.DependencyInjection`.
- Reduced minimum version requirement for `Microsoft.Extensions.DependencyInjection` from `6.0.0` to `2.1.1`
  so that you don't need to update it in legacy projects. Added `Cli.Ext.GetServiceCollection` and
  `Cli.Ext.GetServiceProviderOrDefault` extension methods.
  • Loading branch information
calacayir committed Feb 21, 2024
1 parent 4e220e3 commit 890d624
Show file tree
Hide file tree
Showing 206 changed files with 689 additions and 492 deletions.
28 changes: 27 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,7 @@ public class ValidationCliCommand

## Dependency Injection

Commands can have injected dependencies, this is supported via `Microsoft.Extensions.DependencyInjection` package (version >= 6.0.0).
Commands can have injected dependencies, this is supported via `Microsoft.Extensions.DependencyInjection` package (version >= 2.1.1).
In your project directory, via dotnet cli:
```console
dotnet add package Microsoft.Extensions.DependencyInjection
Expand Down Expand Up @@ -852,6 +852,32 @@ public sealed class SingletonClass : IDisposable
public void Dispose() => Console.WriteLine($"{nameof(SingletonClass)}.Dispose()");
}
```
Other dependency injection containers (e.g. Autofac) are also supported
via `Microsoft.Extensions.DependencyInjection.Abstractions` package (version >= 2.1.1).
In your project directory, via dotnet cli:
```console
dotnet add package Microsoft.Extensions.DependencyInjection.Abstractions
```
or in Visual Studio Package Manager Console:
```console
PM> Install-Package Microsoft.Extensions.DependencyInjection.Abstractions
```
When the source generator detects that your project has reference to `Microsoft.Extensions.DependencyInjection.Abstractions`,
it will generate extension methods for supporting custom service providers.
For example, you can now set your custom service provider with the extension method `Cli.Ext.SetServiceProvider`:
```c#
using DotMake.CommandLine;
using Autofac.Core;
using Autofac.Core.Registration;

var cb = new ContainerBuilder();
cb.RegisterType<object>();
var container = cb.Build();

Cli.Ext.SetServiceProvider(container);

Cli.Run<RootCliCommand>();
```
## Help output

When you run the app via
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<!-- https://learn.microsoft.com/en-us/dotnet/core/project-sdk/msbuild-props#generateassemblyinfo -->
<VersionPrefix>1.8.0</VersionPrefix>
<VersionPrefix>1.8.1</VersionPrefix>
<Product>DotMake Command-Line</Product>
<Company>DotMake</Company>
<!-- Copyright is also used for NuGet metadata -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Microsoft.Extensions.DependencyInjection;
using System;

namespace DotMake.CommandLine
{
/// <summary>
/// Provides <see cref="IServiceCollection"/> related extension methods for <see cref="Cli"/> services feature.
/// <para>
/// Requires dependency <c>Microsoft.Extensions.DependencyInjection (>= 2.1.1)</c>.
/// <br/>Default implementation <see cref="ServiceCollection"/> is in <c>Microsoft.Extensions.DependencyInjection</c> assembly.
/// </para>
/// </summary>
public static class CliServiceCollectionExtensions
{
private static readonly IServiceCollection ServiceCollection = new ServiceCollection();
private static IServiceProvider serviceProvider;

/// <summary>
/// Registers services into the <see cref="Cli"/>'s default service collection.
/// </summary>
/// <param name="ext">The CliExtensions instance to extend.</param>
/// <param name="configure">An <see cref="Action{IServiceCollection}"/> to configure the <see cref="Cli"/>'s default service collection.</param>
public static void ConfigureServices(this CliExtensions ext, Action<IServiceCollection> configure)
{
configure(ServiceCollection);
}

/// <summary>
/// Gets the <see cref="Cli"/>'s default service collection.
/// </summary>
/// <param name="ext">The CliExtensions instance to extend.</param>
/// <returns>A <see cref="IServiceCollection"/> instance.</returns>
public static IServiceCollection GetServiceCollection(this CliExtensions ext)
{
return ServiceCollection;
}

/// <summary>
/// Gets the service provider built from <see cref="Cli"/>'s default service collection (built on first access).
/// If <see cref="CliServiceProviderExtensions.SetServiceProvider"/> was used, then gets the custom <see cref="IServiceProvider"/> that was set.
/// </summary>
/// <param name="ext">The CliExtensions instance to extend.</param>
/// <returns>A <see cref="IServiceProvider"/> instance.</returns>
public static IServiceProvider GetServiceProviderOrDefault(this CliExtensions ext)
{
return serviceProvider ??= CliServiceProviderExtensions.GetServiceProvider(null)
?? ServiceCollection.BuildServiceProvider();
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using Microsoft.Extensions.DependencyInjection;

namespace DotMake.CommandLine
{
/// <summary>
/// Provides <see cref="IServiceProvider"/> related extension methods for <see cref="Cli"/> services feature.
/// <para>
/// Requires dependency <c>Microsoft.Extensions.DependencyInjection.Abstractions (>= 2.1.1)</c>.
/// <br/>Although <see cref="IServiceProvider"/> is in <c>System.ComponentModel</c> assembly,
/// used class <see cref="ActivatorUtilities"/> is in <c>Microsoft.Extensions.DependencyInjection.Abstractions</c> assembly.
/// </para>
/// </summary>
public static class CliServiceProviderExtensions
{
private static IServiceProvider serviceProvider;

/// <summary>
/// Gets the custom service provider.
/// If <see cref="SetServiceProvider"/> was used, then gets the custom <see cref="IServiceProvider"/> that was set.
/// </summary>
/// <param name="ext">The CliExtensions instance to extend.</param>
/// <returns>A <see cref="IServiceProvider"/> instance or <see langword="null"/>.</returns>
public static IServiceProvider GetServiceProvider(this CliExtensions ext)
{
return serviceProvider;
}

/// <summary>
/// Sets a custom service provider built from a custom service collection (to override the internal default one).
/// When <see cref="GetServiceProvider"/> is called, this custom <see cref="IServiceProvider"/> will be returned.
/// </summary>
/// <param name="ext">The CliExtensions instance to extend.</param>
/// <param name="customServiceProvider">The custom <see cref="IServiceProvider"/> instance to use.</param>
public static void SetServiceProvider(this CliExtensions ext, IServiceProvider customServiceProvider)
{
serviceProvider = customServiceProvider;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.1" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
Expand Down
10 changes: 7 additions & 3 deletions src/DotMake.CommandLine.SourceGeneration.Embedded/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ Code should compile against DotMake.CommandLine.dll (not project due to circular
which will be available during runtime.

Make sure it compiles for lowest supported langversion 9.0 as source may be generated in a netstandard2.0 project.
- `ModuleInitializerAttribute` requires LangVersion 9.0 (polyfill injected only when attribute does not exist).
- `ModuleInitializerAttribute` requires LangVersion 9.0 (polyfill injected only when attribute does not exist).
If your target framework is below net5.0, you need `<LangVersion>9.0</LangVersion>` tag (minimum) in your .csproj file.
- `RequiredMemberAttribute` requires LangVersion 11.0 (polyfill injected only when 11+ and attribute does not exist).
- `RequiredMemberAttribute` requires LangVersion 11.0 (polyfill injected only when 11+ and attribute does not exist).
If your target framework is below net7.0, you need `<LangVersion>11.0</LangVersion>` tag (minimum) in your .csproj file
- `CliServiceExtensions` feature injected only when project references `Microsoft.Extensions.DependencyInjection`.
- `CliServiceProviderExtensions` feature injected only when project references `Microsoft.Extensions.DependencyInjection.Abstractions (>= 2.1.1)`.
Although `IServiceProvider` is in `System.ComponentModel` assembly,
used class `ActivatorUtilities` is in `Microsoft.Extensions.DependencyInjection.Abstractions` assembly.
- `CliServiceCollectionExtensions` feature injected only when project references `Microsoft.Extensions.DependencyInjection (>= 2.1.1)`.
Default implementation `ServiceCollection` is in `Microsoft.Extensions.DependencyInjection` assembly.
11 changes: 8 additions & 3 deletions src/DotMake.CommandLine.SourceGeneration/CliCommandGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ public void Initialize(IncrementalGeneratorInitializationContext initializationC

var cliCommandInfos = initializationContext.SyntaxProvider.ForAttributeWithMetadataName(
CliCommandInfo.AttributeFullName,

(syntaxNode, _) => CliCommandInfo.IsMatch(syntaxNode),
(attributeSyntaxContext, _) => CliCommandInfo.From(attributeSyntaxContext)
);
Expand Down Expand Up @@ -88,10 +87,16 @@ private static void GenerateReferenceDependantSourceCode(SourceProductionContext
GetSourceTextFromEmbeddedResource("RequiredMemberAttribute.cs", analyzerConfigOptions)
);

if (referenceDependantInfo.HasMsDependencyInjectionAbstractions)
sourceProductionContext.AddSource(
"(CliServiceProviderExtensions).g.cs",
GetSourceTextFromEmbeddedResource("CliServiceProviderExtensions.cs", analyzerConfigOptions)
);

if (referenceDependantInfo.HasMsDependencyInjection)
sourceProductionContext.AddSource(
"(CliServiceExtensions).g.cs",
GetSourceTextFromEmbeddedResource("CliServiceExtensions.cs", analyzerConfigOptions)
"(CliServiceCollectionExtensions).g.cs",
GetSourceTextFromEmbeddedResource("CliServiceCollectionExtensions.cs", analyzerConfigOptions)
);

}
Expand Down
22 changes: 16 additions & 6 deletions src/DotMake.CommandLine.SourceGeneration/CliCommandInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ private void Analyze()
if (Symbol.IsAbstract || Symbol.IsGenericType)
AddDiagnostic(DiagnosticDescriptors.ErrorClassNotNonAbstractNonGeneric, DiagnosticName);

if (!ReferenceDependantInfo.HasMsDependencyInjection
if (!ReferenceDependantInfo.HasMsDependencyInjectionAbstractions
&& !Symbol.InstanceConstructors.Any(c =>
c.Parameters.IsEmpty
&& (c.DeclaredAccessibility == Accessibility.Public || c.DeclaredAccessibility == Accessibility.Internal)
Expand Down Expand Up @@ -246,17 +246,27 @@ public void AppendCSharpDefineString(CodeStringBuilder sb, bool addNamespaceBloc

using (sb.AppendBlockStart($"private {definitionClass} CreateInstance()"))
{
if (ReferenceDependantInfo.HasMsDependencyInjection)
if (ReferenceDependantInfo.HasMsDependencyInjectionAbstractions || ReferenceDependantInfo.HasMsDependencyInjection)
{
sb.AppendLine("var serviceProvider = DotMake.CommandLine.CliServiceExtensions.GetServiceProvider(null);");
sb.AppendLine(ReferenceDependantInfo.HasMsDependencyInjection
? "var serviceProvider = DotMake.CommandLine.CliServiceCollectionExtensions.GetServiceProviderOrDefault(null);"
: "var serviceProvider = DotMake.CommandLine.CliServiceProviderExtensions.GetServiceProvider(null);");
sb.AppendLine("if (serviceProvider != null)");
sb.AppendIndent();
sb.AppendLine("return Microsoft.Extensions.DependencyInjection.ActivatorUtilities");
sb.AppendIndent();
sb.AppendIndent();
sb.AppendLine($".CreateInstance<{definitionClass}>(serviceProvider);");
}
else if (memberHasRequiredModifier)
//in case serviceProvider is null (i.e. not set with SetServiceProvider)
//call Activator.CreateInstance which will throw exception if class has no default constructor
//but at least it avoids compile time error in generated code with new()
sb.AppendLine();
sb.AppendLine($"return System.Activator.CreateInstance<{definitionClass}>();");
}
else
sb.AppendLine($"return new {definitionClass}();");
sb.AppendLine(memberHasRequiredModifier
? $"return System.Activator.CreateInstance<{definitionClass}>();"
: $"return new {definitionClass}();");
}
sb.AppendLine();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,8 @@
<EmbeddedResource Remove="..\DotMake.CommandLine.SourceGeneration.Embedded\obj\**\*" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Remove="..\DotMake.CommandLine.SourceGeneration.Embedded\CliServiceExtensions.cs" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ public ReferenceDependantInfo(Compilation compilation)
{
switch (referencedAssembly.Name)
{
case "Microsoft.Extensions.DependencyInjection.Abstractions":
if (referencedAssembly.Version >= new Version(2, 1, 1))
HasMsDependencyInjectionAbstractions = true;
break;
case "Microsoft.Extensions.DependencyInjection":
if (referencedAssembly.Version < new Version(6, 0))
continue;

HasMsDependencyInjection = true;
if (referencedAssembly.Version >= new Version(2, 1, 1))
HasMsDependencyInjection = true;
break;
}
}
Expand All @@ -31,6 +33,8 @@ public ReferenceDependantInfo(Compilation compilation)

public bool HasRequiredMember { get; }

public bool HasMsDependencyInjectionAbstractions { get; }

public bool HasMsDependencyInjection { get; }

public bool Equals(ReferenceDependantInfo other)
Expand All @@ -42,6 +46,7 @@ public bool Equals(ReferenceDependantInfo other)

return HasModuleInitializer == other.HasModuleInitializer
&& HasRequiredMember == other.HasRequiredMember
&& HasMsDependencyInjectionAbstractions == other.HasMsDependencyInjectionAbstractions
&& HasMsDependencyInjection == other.HasMsDependencyInjection;
}
}
Expand Down
23 changes: 8 additions & 15 deletions src/DotMake.CommandLine/DotMake.CommandLine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,14 @@
<Description>Declarative syntax for System.CommandLine via attributes for easy, fast, strongly-typed (no reflection) usage. Includes a source generator which automagically converts your classes to CLI commands and properties to CLI options or CLI arguments.</Description>
<PackageTags>command-line CLI console System.CommandLine declarative attributes parsing command argument option generator</PackageTags>
<PackageReleaseNotes>
- Switched to using latest daily builds of System.CommandLine (currently `2.0.0-beta4.24112.1`).
Since official `2.0.0-beta4.22272.1` release (dated 6/2/2022), System.CommandLine API is vastly changed
so we migrated those changes to DotMake.CommandLine, this way we can get all the bug fixes.

As daily builds are only published to Microsoft's private feed and not to official NuGet site,
we dropped the dependency to System.CommandLine package and instead we are now bundling System.CommandLine DLLs inside our
own NuGet package. Once System.CommandLine becomes GA and latest version is published to official NuGet site,
we will readd the dependency to System.CommandLine package.
- Renamed Global property in CliOptionAttribute to Recursive to match new System.CommandLine API.
- In `Cli.Run` and `Cli.RunAsync` methods; configureBuilder, useBuilderDefaults, console parameters are removed and
instead they now accept new `CliSettings` parameter, which is used to configure settings in one place.
- In `Cli.Parse` methods return type is now ParseResult and ParseResult.Bind method can be used to get an instance for
the command definition class.
- `InvocationContext` in System.CommandLine latest builds, is removed so we added a new class named `CliContext`
which supports command invocation by providing access to parse results and other services.
- Added support for other dependency injection containers (e.g. Autofac) when
only `Microsoft.Extensions.DependencyInjection.Abstractions` package (version >= 2.1.1) is added to the project.
You can set your custom service provider with the extension method `Cli.Ext.SetServiceProvider`.
In previous version, this method already existed but accepted a `ServiceProvider` parameter, instead of
`IServiceProvider` parameter which allows 3rd party implementations other than the default one in `Microsoft.Extensions.DependencyInjection`.
- Reduced minimum version requirement for `Microsoft.Extensions.DependencyInjection` from `6.0.0` to `2.1.1`
so that you don't need to update it in legacy projects. Added `Cli.Ext.GetServiceCollection` and
`Cli.Ext.GetServiceProviderOrDefault` extension methods.
</PackageReleaseNotes>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// <auto-generated />
// Generated by DotMake.CommandLine.SourceGeneration v1.8.0.0
// Roslyn (Microsoft.CodeAnalysis) v4.800.23.57201
// Generated by DotMake.CommandLine.SourceGeneration v1.8.1.0
// Roslyn (Microsoft.CodeAnalysis) v4.900.24.8111
// Generation: 1

#if !NET5_0_OR_GREATER
Expand Down

0 comments on commit 890d624

Please sign in to comment.