Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Recommendation for resolving at runtime #129

Unanswered
CrispyDrone asked this question in Q&A
Discussion options

I'm trying to create a console application that will parse its arguments into a command and then handle this command. It relies on an ICommand and ICommandHandler interface à la MediatR to be able to dispatch the commands and have one or multiple handlers process the command.

What is the best way to approach this with Pure.DI?

In many reflection-based dependency injection libraries, you can register open generics making this very trivial.

I've scoured through the discussions and other issues, and I understand Pure.DI requires a different mode of thinking and approach.

I've tried different things, but tend to encounter errors at runtime, need to do some unfortunate casts, and/or have to register everything manually:

I've created a repository with some things I've tried, but here is one example:

// See https://aka.ms/new-console-template for more information
using Pure.DI;
var container = new Composition();
var dispatcher = container.Dispatcher;
args = ["Folder 1"];
var command = ParseArgs();
await dispatcher.Execute(command);
args = ["sftp://user@host.com/my-folder"];
command = ParseArgs();
await dispatcher.Execute(command);
ICommand ParseArgs()
{
 var location = args[0];
 if (location.StartsWith("sftp")) return new ImportRemoteFilesCommand(location);
 else return new ImportLocalFilesCommand(location);
}
public interface ICommand { }
public interface ICommandHandler<in TCommand> : ICommandHandler where TCommand : ICommand
{
 Task Execute(TCommand command);
}
public interface ICommandHandler
{
 Task Execute(ICommand command);
}
public interface IDispatcher
{
 Task Execute(ICommand command);
 // Task Execute<TCommand>(TCommand command) where TCommand : ICommand;
}
//public abstract class CommandHandler<TCommand> :
// ICommandHandler<TCommand> where TCommand : ICommand
//{
// public abstract Task Execute(TCommand command);
//}
sealed class CommandDispatcher : IDispatcher
{
 private readonly Composition _container;
 public CommandDispatcher(Composition container)
 {
 _container = container;
 }
 // does not work
 //public Task Execute<TCommand>(TCommand command) where TCommand : ICommand
 //{
 // var handler = _container.Resolve<ICommandHandler<TCommand>>();
 // return handler.Execute(command);
 //}
 public Task Execute(ICommand command)
 {
 var handler = _container.Resolve(typeof(ICommandHandler<>).MakeGenericType(command.GetType()));
 return ((ICommandHandler)handler).Execute(command);
 }
}
public interface ILocalFileSystem
{
 FileInfo GetFile(string path);
 IEnumerable<FileInfo> GetFiles(DirectoryInfo directory);
}
public class LocalFileSystem : ILocalFileSystem
{
 public FileInfo GetFile(string path)
 {
 return new FileInfo(path);
 }
 public IEnumerable<FileInfo> GetFiles(DirectoryInfo directory)
 {
 yield return new FileInfo("A");
 yield return new FileInfo("B");
 }
}
public record ImportLocalFilesCommand
(
 string ImportDirectory
) : ICommand;
public sealed class ImportLocalFilesCommandHandler : ICommandHandler<ImportLocalFilesCommand>
{
 private readonly ILocalFileSystem _localFileSystem;
 public ImportLocalFilesCommandHandler(ILocalFileSystem localFileSystem)
 {
 _localFileSystem = localFileSystem;
 }
 public Task Execute(ImportLocalFilesCommand command)
 {
 Console.WriteLine("Importing files from the local file system.");
 var files = _localFileSystem.GetFiles(new DirectoryInfo(command.ImportDirectory));
 foreach (var file in files)
 {
 Console.WriteLine($"Reading {file}");
 }
 return Task.CompletedTask;
 }
 public Task Execute(ICommand command)
 {
 return Execute((ImportLocalFilesCommand)command);
 }
}
public interface IRemoteClient
{
 public IEnumerable<string> GetDownloadUrls(string folder);
}
public class RemoteClient : IRemoteClient
{
 public IEnumerable<string> GetDownloadUrls(string folder)
 {
 yield return "url:A";
 yield return "url:B";
 }
}
public record ImportRemoteFilesCommand
(
 string Url
) : ICommand;
public sealed class ImportRemoteFilesCommandHandler : ICommandHandler<ImportRemoteFilesCommand>
{
 private readonly IRemoteClient _remoteClient;
 public ImportRemoteFilesCommandHandler(IRemoteClient remoteClient)
 {
 _remoteClient = remoteClient;
 }
 public Task Execute(ImportRemoteFilesCommand command)
 {
 Console.WriteLine("Importing files from a remote location.");
 var urls = _remoteClient.GetDownloadUrls(command.Url);
 foreach (var url in urls)
 {
 Console.WriteLine($"Downloading {url}");
 }
 return Task.CompletedTask;
 }
 public Task Execute(ICommand command)
 {
 return Execute((ImportRemoteFilesCommand)command);
 }
}
sealed partial class Composition
{
 void Setup() =>
 DI.Setup(nameof(Composition))
 // .GenericTypeArgument<ICommand>()
 // .Roots<ICommandHandler<ICommand>>()
 .RootBind<ICommandHandler<ImportRemoteFilesCommand>>().To<ImportRemoteFilesCommandHandler>()
 .RootBind<ICommandHandler<ImportLocalFilesCommand>>().To<ImportLocalFilesCommandHandler>()
 .Bind().To<RemoteClient>()
 .Bind().To<LocalFileSystem>()
 // .GenericTypeArgument<ICommand>()
 // .Roots<ICommandHandler<ICommand>>()
 .Root<CommandDispatcher>("Dispatcher");
}

This works but I have to reflection in the dispatcher, casting in the handlers, and register every single one manually, which definitely doesn't feel like the right way!

You must be logged in to vote

Replies: 1 comment 2 replies

Comment options

Commands dispatching (I mean generic types) isn't the responsibility of DI. I propose the following solution:

using System.Diagnostics.CodeAnalysis;
using Pure.DI;
var composition = new Composition();
try
{
 args = ["Folder 1"];
 await composition.Dispatcher.Execute(args);
 args = ["sftp://user@host.com/my-folder"];
 await composition.Dispatcher.Execute(args);
}
catch (InvalidOperationException error)
{
 Console.Error.WriteLine("ERROR: " + error.Message);
}
// Commands
interface ICommand
{
 public string Description { get; }
}
record ImportLocalFilesCommand(string Description, string ImportDirectory) : ICommand;
record ImportRemoteFilesCommand(string Description, string Url) : ICommand;
// Parsers
internal interface ICommandParser
{
 bool TryParse(IReadOnlyList<string> args, [NotNullWhen(true)] out ICommand? command);
}
sealed class ImportRemoteFilesCommandParser : ICommandParser
{
 public bool TryParse(IReadOnlyList<string> args, [NotNullWhen(true)] out ICommand? command)
 {
 if (args is [var url, ..] && url.StartsWith("sftp"))
 {
 command = new ImportRemoteFilesCommand("Importing files from a remote location.", url);
 return true;
 }
 command = null;
 return false;
 }
}
sealed class ImportLocalFilesCommandParser : ICommandParser
{
 public bool TryParse(IReadOnlyList<string> args, [NotNullWhen(true)] out ICommand? command)
 {
 if (args is [var importDirectory, ..])
 {
 command = new ImportLocalFilesCommand("Importing files from the local file system.", importDirectory);
 return true;
 }
 command = null;
 return false;
 }
}
// Handlers
interface ICommandHandler
{
 bool CanExecute(ICommand command);
 Task Execute(ICommand command);
}
abstract class CommandHandler<TCommand>
 where TCommand : ICommand
{
 public bool CanExecute(ICommand command) => command is TCommand;
 public Task Execute(ICommand command) => Execute((TCommand)command);
 protected abstract Task Execute(TCommand command);
}
sealed class ImportLocalFilesCommandHandler(ILocalFileSystem localFileSystem)
 : CommandHandler<ImportLocalFilesCommand>, ICommandHandler
{
 protected override Task Execute(ImportLocalFilesCommand command)
 {
 var files = localFileSystem.GetFiles(new DirectoryInfo(command.ImportDirectory));
 foreach (var file in files)
 {
 Console.WriteLine($"Reading {file}");
 }
 return Task.CompletedTask;
 }
}
sealed class ImportRemoteFilesCommandHandler(IRemoteClient remoteClient)
 : CommandHandler<ImportRemoteFilesCommand>, ICommandHandler
{
 protected override Task Execute(ImportRemoteFilesCommand command)
 {
 var urls = remoteClient.GetDownloadUrls(command.Url);
 foreach (var url in urls)
 {
 Console.WriteLine($"Downloading {url}");
 }
 return Task.CompletedTask;
 }
}
// Services
interface ICommandDispatcher
{
 Task Execute(IReadOnlyList<string> args);
}
sealed class CommandDispatcher(
 IReadOnlyCollection<ICommandParser> parsers,
 IReadOnlyCollection<ICommandHandler> handlers)
 : ICommandDispatcher
{
 public Task Execute(IReadOnlyList<string> args)
 {
 foreach(var parser in parsers)
 {
 if (!parser.TryParse(args, out var command))
 {
 continue;
 }
 Console.WriteLine(command.Description);
 return Execute(command);
 }
 throw new InvalidOperationException("No parser found for the given arguments: " + string.Join(" ", args) + ".");
 }
 private async Task Execute(ICommand command)
 {
 foreach (var handler in handlers)
 {
 if (!handler.CanExecute(command))
 {
 continue;
 }
 await handler.Execute(command);
 return;
 }
 throw new InvalidOperationException($"No handler found for {command.GetType().Name}");
 }
}
interface IRemoteClient
{
 public IEnumerable<string> GetDownloadUrls(string folder);
}
class RemoteClient : IRemoteClient
{
 public IEnumerable<string> GetDownloadUrls(string folder)
 {
 yield return "url:A";
 yield return "url:B";
 }
}
public interface ILocalFileSystem
{
 FileInfo GetFile(string path);
 IEnumerable<FileInfo> GetFiles(DirectoryInfo directory);
}
public class LocalFileSystem : ILocalFileSystem
{
 public FileInfo GetFile(string path)
 {
 return new FileInfo(path);
 }
 public IEnumerable<FileInfo> GetFiles(DirectoryInfo directory)
 {
 yield return new FileInfo("A");
 yield return new FileInfo("B");
 }
}
sealed partial class Composition
{
 void Setup() =>
 DI.Setup()
 .DefaultLifetime(Lifetime.Singleton)
 // Parsers
 .Bind(Tag.Unique).To<ImportRemoteFilesCommandParser>()
 .Bind(Tag.Unique).To<ImportLocalFilesCommandParser>()
 // Handlers
 .Bind(Tag.Unique).To<ImportRemoteFilesCommandHandler>()
 .Bind(Tag.Unique).To<ImportLocalFilesCommandHandler>()
 // Services
 .Bind().To<CommandDispatcher>()
 .Bind().To<RemoteClient>()
 .Bind().To<LocalFileSystem>()
 .Root<ICommandDispatcher>("Dispatcher");
}

This solution complies with all SOLID principles and will ensure ease of maintenance. You can combine the parsing and command execution logic in a single class, but I don't recommend getting rid of the ICommandParser and ICommandHandler interfaces.

Instead of writing your own commands implementation, you might consider using the System.CommandLine. Here's an example of using it with the Pure.DI.

A shorter composition:

partial class Composition
{
 void Setup() =>
 DI.Setup()
 // Parsers and handlers
 .Singleton<ImportRemoteFilesCommandParser, ImportRemoteFilesCommandHandler>(Tag.Unique)
 .Singleton<ImportLocalFilesCommandParser, ImportLocalFilesCommandHandler>(Tag.Unique)
 // Services
 .Singleton<CommandDispatcher, RemoteClient, LocalFileSystem>()
 .Root<ICommandDispatcher>("Dispatcher");
}
You must be logged in to vote
2 replies
Comment options

Thank you for the quick reply and helpful example.

I have some thoughts about this solution:

  • What if there are many handlers and they are expensive to create? In case you switch to a Lazy or Func, you can no longer check which handler is actually able to handle the command without actually creating it. I imagine the parser and handler could be linked together (for example a wrapper type that accepts a Lazy or Func of the actual handler) such that it's the parser that implies "CanExecute" is true?

  • I don't necessarily see the manual registering of the handler as a problem, but in my experience it's been something colleagues will bring up as tedious and error prone. What is your opinion on this?

In the meantime, I added another solution to my repository using a builder and parameterless constructors on the handlers such that they can be created. I failed to find any way to "pass information" about which type of handler actually needs to be resolved when using Resolve, so opted for instantiating them using Activator.CreateInstance and then using the BuildUp method.

I think this is an acceptable solution although I'm not used to using properties for injection and it doesn't feel completely right considering the handlers are in an invalid state until they get injected, even though it's not exactly an object that should ever be used in a different way.

Comment options

What if there are many handlers and they are expensive to create? In case you switch to a Lazy or Func, you can no longer check which handler is actually able to handle the command without actually creating it. I imagine the parser and handler could be linked together (for example a wrapper type that accepts a Lazy or Func of the actual handler) such that it's the parser that implies "CanExecute" is true?

In DI, constructors should only contain logic for validating constructor arguments (dependencies) and initializing fields. If constructors don't contain other heavy logic, they run VERY fast. The estimated time for object creation is approximately 10 objects per 1 nanosecond, or 10 million objects per 1 millisecond. If your constructors are written correctly, I wouldn't recommend even considering Lazy<>, Func<>, etc.

I don't necessarily see the manual registering of the handler as a problem, but in my experience it's been something colleagues will bring up as tedious and error prone. What is your opinion on this?

I partially agree with you. I have experience working with many approaches in DI. Automatic dependency registration works quite well when the project is small or when non-abstract types are used during injection. But when the number of bindings exceeds 100, problems and errors arise. For example, you have class A, which implements two interfaces, IC and ID. You create class B, and it implements IC and IF. Registration occurs automatically. Which implementation will be used for IC? Don't forget that some interfaces inherit from other interfaces. And what if you use third-party libraries? All this leads to errors.

You're not the first person to ask this, so I'll probably move toward automatic bindings. But we'll need to come up with a way to minimize problems. For example, the compiler currently displays warnings if a binding has been redefined for a contract.

In the meantime, I added another solution to my repository using a builder and parameterless constructors on the handlers such that they can be created.

Regarding your example, I'll add a few criticisms:

  • Using Resolve() methods is not a good practice. Each such method is a potential runtime exception. It's a performance hit. It's a use of the service locator anti-pattern, which makes code tangled, dependencies implicit, and difficult to test. I try to avoid using such methods.

  • Ideally, there should be a single root of composition — the Program class. This ensures maximum DI efficiency. In your example there are 3 of them.

Please see this post. Here is an application with a single composition root:

using System.Diagnostics.CodeAnalysis;
using Pure.DI;
var composition = new Composition();
var root = composition.Root;
await root.Run();
partial class Program(ICommandDispatcher dispatcher)
{
 async Task Run()
 {
 try
 {
 string[] args = ["Folder 1"];
 await dispatcher.Execute(args);
 args = ["sftp://user@host.com/my-folder"];
 await dispatcher.Execute(args);
 }
 catch (InvalidOperationException error)
 {
 await Console.Error.WriteLineAsync("ERROR: " + error.Message);
 }
 }
}
...
sealed partial class Composition
{
 void Setup() =>
 DI.Setup()
 // Parsers and handlers
 .Singleton<ImportRemoteFilesCommandParser, ImportRemoteFilesCommandHandler>(Tag.Unique)
 .Singleton<ImportLocalFilesCommandParser, ImportLocalFilesCommandHandler>(Tag.Unique)
 // Services
 .Singleton<CommandDispatcher, RemoteClient, LocalFileSystem>()
 .Root<Program>("Root");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
Q&A
Labels
None yet

AltStyle によって変換されたページ (->オリジナル) /