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

Added internal verify-dependencies command #20

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

Added a built-in `verify-dependencies` command that validates the application's Dependency Injection setup by ensuring all required services are fully and correctly registered in the composition root.

## [3.0.3] - 2024-10-09

### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Neolution.CodeAnalysis" Version="3.1.2">
<PackageReference Include="Neolution.CodeAnalysis" Version="3.2.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
{
"profiles": {
"Neolution.DotNet.Console.SampleAsync": {
"Sample Console App": {
"commandName": "Project",
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
}
},
"Sample Console App: verify-dependencies": {
"commandName": "Project",
"commandLineArgs": "verify-dependencies",
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
}
}
}
}
32 changes: 26 additions & 6 deletions Neolution.DotNet.Console.UnitTests/DotNetConsoleBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Neolution.DotNet.Console.UnitTests.Fakes;
using Neolution.DotNet.Console.UnitTests.Stubs;
using Shouldly;
using Xunit;

Expand All @@ -28,15 +30,15 @@ public void GivenMistypedVerb_WhenDefaultVerbIsDefined_ThenShouldThrowOnBuilding
}

/// <summary>
/// Given a valid argument, when default verb is defined, then should not throw on building.
/// Given a valid argument, when default verb is defined, then should not throw on creating.
/// </summary>
/// <param name="args">The arguments.</param>
[Theory]
[InlineData("")]
[InlineData("echo")]
[InlineData("--silent")]
[InlineData("-s")]
public void GivenValidArgument_WhenDefaultVerbIsDefined_ThenShouldNotThrowOnBuilding(string args)
public void GivenValidArgument_WhenDefaultVerbIsDefined_ThenShouldNotThrowOnCreating(string args)
{
// Arrange
var servicesAssembly = Assembly.GetAssembly(typeof(EchoCommand))!;
Expand All @@ -48,7 +50,7 @@ public void GivenValidArgument_WhenDefaultVerbIsDefined_ThenShouldNotThrowOnBuil
}

/// <summary>
/// Given a reserved argument name, when default verb is defined, then should not throw on building.
/// Given a reserved argument name, when default verb is defined, then should not throw on creating.
/// </summary>
/// <param name="args">The arguments.</param>
[Theory]
Expand All @@ -57,7 +59,7 @@ public void GivenValidArgument_WhenDefaultVerbIsDefined_ThenShouldNotThrowOnBuil
[InlineData("--help")]
[InlineData("--version")]
[InlineData("help echo")]
public void GivenReservedArgumentName_WhenDefaultVerbIsDefined_ThenShouldNotThrowOnBuilding(string args)
public void GivenReservedArgumentName_WhenDefaultVerbIsDefined_ThenShouldNotThrowOnCreating(string args)
{
// Arrange
var servicesAssembly = Assembly.GetAssembly(typeof(EchoCommand))!;
Expand All @@ -69,15 +71,15 @@ public void GivenReservedArgumentName_WhenDefaultVerbIsDefined_ThenShouldNotThro
}

/// <summary>
/// Given invalid arguments, when no default verb is defined, then should not throw on console building.
/// Given invalid arguments, when no default verb is defined, then should not throw on console creating.
/// </summary>
/// <param name="args">The arguments.</param>
[Theory]
[InlineData("")]
[InlineData("verb-that-does-not-exist")]
[InlineData("--option-that-does-not-exist")]
[InlineData("-o")]
public void GivenInvalidArguments_WhenNoDefaultVerbIsDefined_ThenShouldNotThrowOnBuilding(string args)
public void GivenInvalidArguments_WhenNoDefaultVerbIsDefined_ThenShouldNotThrowOnCreating(string args)
{
// Arrange
var servicesAssembly = Assembly.GetAssembly(typeof(EchoCommand))!;
Expand All @@ -88,5 +90,23 @@ public void GivenInvalidArguments_WhenNoDefaultVerbIsDefined_ThenShouldNotThrowO
// Assert
Should.NotThrow(() => DotNetConsole.CreateBuilderWithReference(servicesAssembly, verbTypes, args.Split(" ")));
}

/// <summary>
/// Given the verify-dependencies builder, when registration is missing, then should throw on console building.
/// </summary>
[Fact]
public void GivenVerifyDependenciesCommand_WhenRegistrationIsMissing_ThenShouldThrow()
{
// Arrange
var builder = DotNetConsole.CreateBuilderWithReference(Assembly.GetAssembly(typeof(DefaultCommand))!, new[] { "verify-dependencies" });

// Intentionally only registering the transient service and not the scoped and singleton services.
builder.Services.AddTransient<ITransientServiceStub, TransientServiceStub>();

// Act

// Assert
Should.Throw(() => builder.Build(), typeof(AggregateException));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Neolution.DotNet.Console.Abstractions;
using Neolution.DotNet.Console.UnitTests.Fakes;
using Neolution.DotNet.Console.UnitTests.Spies;
using Shouldly;
Expand Down Expand Up @@ -123,7 +124,7 @@ public async Task GivenBuiltConsoleApp_WhenCallingVerbWithScalarOption_ThenShoul
/// <param name="args">The arguments.</param>
/// <param name="tracker">The logger.</param>
/// <returns>A built console app ready to run.</returns>
private static DotNetConsole CreateConsoleAppWithLogger(string args, IUnitTestLogger tracker)
private static IDotNetConsole CreateConsoleAppWithLogger(string args, IUnitTestLogger tracker)
{
var builder = DotNetConsole.CreateBuilderWithReference(Assembly.GetAssembly(typeof(DefaultCommand))!, args.Split(" "));

Expand Down
5 changes: 5 additions & 0 deletions Neolution.DotNet.Console.UnitTests/DotNetConsoleRunTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public void GivenServicesWithVariousServiceLifetimes_WhenRunningConsoleApp_ThenS
[InlineData("--version")]
[InlineData("help echo")]
[InlineData("echo --help")]
[InlineData("verify-dependencies")]
public void GivenValidArguments_WhenNoDefaultVerbIsDefined_ThenShouldNotThrow([NotNull] string args)
{
if (args == null)
Expand All @@ -67,6 +68,10 @@ public void GivenValidArguments_WhenNoDefaultVerbIsDefined_ThenShouldNotThrow([N
var logger = new UnitTestLogger();
builder.Services.AddSingleton(typeof(IUnitTestLogger), logger);

builder.Services.AddTransient<ITransientServiceStub, TransientServiceStub>();
builder.Services.AddScoped<IScopedServiceStub, ScopedServiceStub>();
builder.Services.AddSingleton<ISingletonServiceStub, SingletonServiceStub>();

var console = builder.Build();

// Act
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Neolution.CodeAnalysis.TestsRuleset" Version="3.1.2">
<PackageReference Include="Neolution.CodeAnalysis.TestsRuleset" Version="3.2.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
22 changes: 22 additions & 0 deletions Neolution.DotNet.Console/Abstractions/IDotNetConsole.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Neolution.DotNet.Console.Abstractions
{
using System;
using System.Threading.Tasks;

/// <summary>
/// The console application.
/// </summary>
public interface IDotNetConsole
{
/// <summary>
/// Gets the services.
/// </summary>
IServiceProvider Services { get; }

/// <summary>
/// Runs the application.
/// </summary>
/// <returns>The <see cref="Task"/>.</returns>
Task RunAsync();
}
}
11 changes: 3 additions & 8 deletions Neolution.DotNet.Console/DotNetConsole.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
/// <summary>
/// The console application.
/// </summary>
public class DotNetConsole
public class DotNetConsole : IDotNetConsole
{
/// <summary>
/// The host
Expand All @@ -34,9 +34,7 @@ public DotNetConsole(IHost host, ParserResult<object> commandLineParserResult)
this.commandLineParserResult = commandLineParserResult;
}

/// <summary>
/// Gets the services.
/// </summary>
/// <inheritdoc />
public IServiceProvider Services => this.host.Services;

/// <summary>
Expand Down Expand Up @@ -81,10 +79,7 @@ public static DotNetConsoleBuilder CreateBuilderWithReference(Assembly servicesA
return DotNetConsoleBuilder.CreateBuilderInternal(servicesAssembly, verbTypes, args);
}

/// <summary>
/// Runs the application.
/// </summary>
/// <returns>The <see cref="Task"/>.</returns>
/// <inheritdoc />
public async Task RunAsync()
{
await this.commandLineParserResult.WithParsedAsync(this.RunWithOptionsAsync);
Expand Down
91 changes: 55 additions & 36 deletions Neolution.DotNet.Console/DotNetConsoleBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ public class DotNetConsoleBuilder
/// </summary>
private readonly ServiceCollection serviceCollection = new();

/// <summary>
/// Run only to verify dependencies.
/// </summary>
private bool verifyDependencies;

/// <summary>
/// Initializes a new instance of the <see cref="DotNetConsoleBuilder"/> class.
/// </summary>
Expand Down Expand Up @@ -68,7 +73,7 @@ public DotNetConsoleBuilder(IHostBuilder hostBuilder, ParserResult<object> comma
/// Builds this instance.
/// </summary>
/// <returns>The <see cref="DotNetConsole"/>.</returns>
public DotNetConsole Build()
public IDotNetConsole Build()
{
// Copy over the services to host builder before it gets built
this.hostBuilder.ConfigureServices(services =>
Expand All @@ -79,6 +84,13 @@ public DotNetConsole Build()
}
});

if (this.verifyDependencies)
{
this.hostBuilder.UseEnvironment("Development");
this.hostBuilder.Build();
return new NoOperationConsole();
}

var host = this.hostBuilder.Build();
return new DotNetConsole(host, this.commandLineParserResult);
}
Expand Down Expand Up @@ -118,46 +130,17 @@ internal static DotNetConsoleBuilder CreateBuilderInternal(Assembly assembly, Ty
.Where(t => t.GetCustomAttribute<VerbAttribute>() != null)
.ToArray();

EnforceStrictVerbMatching(args, verbTypes);
var parsedArguments = Parser.Default.ParseArguments(args, verbTypes);
var consoleBuilder = new DotNetConsoleBuilder(builder, parsedArguments, environment, configuration);

return new DotNetConsoleBuilder(builder, parsedArguments, environment, configuration);
}

/// <summary>
/// Enforce strict verb matching if one verb is marked as default. Otherwise, the default verb will be executed even if that was not the users intention.
/// </summary>
/// <param name="args">The arguments.</param>
/// <param name="availableVerbTypes">The available verb types.</param>
/// <exception cref="Neolution.DotNet.Console.DotNetConsoleException">Cannot create builder, because the specified verb '{firstVerb}' matches no command.</exception>
private static void EnforceStrictVerbMatching(string[] args, Type[] availableVerbTypes)
{
var availableVerbs = availableVerbTypes.Select(t => t.GetCustomAttribute<VerbAttribute>()!).ToList();
if (!availableVerbs.Any(v => v.IsDefault))
if (args.Length == 1 && string.Equals(args[0], "verify-dependencies", StringComparison.OrdinalIgnoreCase))
{
// If no default verb is defined, we do not enforce strict verb matching
return;
consoleBuilder.verifyDependencies = true;
return consoleBuilder;
}

var firstVerb = args.FirstOrDefault();
if (string.IsNullOrWhiteSpace(firstVerb) || firstVerb.StartsWith('-'))
{
// If the user passed no verb, but a default verb is defined, the default verb will be executed
return;
}

// Names reserved by CommandLineParser library
var validFirstArguments = new List<string> { "--help", "--version", "help", "version" };

// Names of all available verbs
validFirstArguments.AddRange(availableVerbs.Select(t => t.Name));

// Check if the first argument can be found in the list of valid arguments
var verbMatched = validFirstArguments.Any(v => v.Equals(firstVerb, StringComparison.OrdinalIgnoreCase));
if (!verbMatched)
{
throw new DotNetConsoleException($"Cannot create builder, because the specified verb '{firstVerb}' matches no command.");
}
CheckStrictVerbMatching(args, verbTypes);
return consoleBuilder;
}

/// <summary>
Expand Down Expand Up @@ -242,5 +225,41 @@ private static void AddCommandLineConfig(IConfigurationBuilder configBuilder, st
configBuilder.AddCommandLine(args);
}
}

/// <summary>
/// Enforce strict verb matching if one verb is marked as default. Otherwise, the default verb will be executed even if that was not the users intention.
/// </summary>
/// <param name="args">The arguments.</param>
/// <param name="availableVerbTypes">The available verb types.</param>
/// <exception cref="Neolution.DotNet.Console.DotNetConsoleException">Cannot create builder, because the specified verb '{firstVerb}' matches no command.</exception>
private static void CheckStrictVerbMatching(string[] args, Type[] availableVerbTypes)
{
var availableVerbs = availableVerbTypes.Select(t => t.GetCustomAttribute<VerbAttribute>()!).ToList();
if (!availableVerbs.Any(v => v.IsDefault))
{
// If no default verb is defined, we do not enforce strict verb matching
return;
}

var firstVerb = args.FirstOrDefault();
if (string.IsNullOrWhiteSpace(firstVerb) || firstVerb.StartsWith('-'))
{
// If the user passed no verb, but a default verb is defined, the default verb will be executed
return;
}

// Names reserved by CommandLineParser library
var validFirstArguments = new List<string> { "--help", "--version", "help", "version" };

// Names of all available verbs
validFirstArguments.AddRange(availableVerbs.Select(t => t.Name));

// Check if the first argument can be found in the list of valid arguments
var verbMatched = validFirstArguments.Any(v => v.Equals(firstVerb, StringComparison.OrdinalIgnoreCase));
if (!verbMatched)
{
throw new DotNetConsoleException($"Cannot create builder, because the specified verb '{firstVerb}' matches no command.");
}
}
}
}
23 changes: 23 additions & 0 deletions Neolution.DotNet.Console/Internal/NoOperationConsole.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Neolution.DotNet.Console.Internal
{
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Neolution.DotNet.Console.Abstractions;

/// <summary>
/// The no operation console application.
/// </summary>
/// <seealso cref="IDotNetConsole" />
public class NoOperationConsole : IDotNetConsole
{
/// <inheritdoc />
public IServiceProvider Services => new ServiceCollection().BuildServiceProvider();

/// <inheritdoc />
public Task RunAsync()
{
return Task.CompletedTask;
}
}
}
2 changes: 1 addition & 1 deletion Neolution.DotNet.Console/Neolution.DotNet.Console.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="8.0.0" />
<PackageReference Include="Neolution.CodeAnalysis" Version="3.1.2">
<PackageReference Include="Neolution.CodeAnalysis" Version="3.2.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down