Model: GPT-4
Avatar of the person chatting

/// /// Invokes the specified command asynchronously. /// /// The type of the command to be executed. /// The command instance to be executed. /// /// A representing the asynchronous operation. public Task InvokeAsync(TCommand command, CancellationToken cancellationToken) where TCommand : Command; /// /// Invokes the specified command asynchronously and returns the result. /// /// The type of the command to be executed. /// The type of the result returned by the command. /// The command instance to be executed. /// /// A representing the asynchronous operation, containing the result of the command execution. public Task InvokeAsync(TCommand command, CancellationToken cancellationToken) where TCommand : Command; } What I should write in cancellation token param doccomment tag?

You can describe the CancellationToken parameter as follows:

csharpCopy code
/// <param name="cancellationToken">A cancellation token that can be used to signal the request to cancel the operation.</param>

This provides a clear explanation that the CancellationToken is used to potentially stop the asynchronous operation if necessary. It's a common mechanism in .NET to cancel long-running or potentially infinite tasks.

Avatar of the person chatting

How can I refactor this class to avoid repeated code? public class CommandExecutor : ICommandExecutor { public Task InvokeAsync(TCommand command, CancellationToken cancellationToken) where TCommand : Command { return InvokeCommandAsync(command, cancellationToken); } public Task InvokeAsync(TCommand command, CancellationToken cancellationToken) where TCommand : Command { return InvokeCommandAsync(command, cancellationToken); } private async Task InvokeCommandAsync(TCommand command, CancellationToken cancellationToken) { var executeMethod = typeof(TCommand).GetMethod("ExecuteAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; using var scope = Locator.ServiceProvider!.CreateScope(); var scopedServiceProviderField = typeof(Locator).GetField("_scopedServiceProvider", BindingFlags.NonPublic | BindingFlags.Static)!; ((AsyncLocal)scopedServiceProviderField.GetValue(null)!).Value = scope.ServiceProvider; var commandResult = await (Task)executeMethod.Invoke(command, new object?[] { cancellationToken })!; return commandResult; } private async Task InvokeCommandAsync(TCommand command, CancellationToken cancellationToken) { var executeMethod = typeof(TCommand).GetMethod("ExecuteAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; using var scope = Locator.ServiceProvider!.CreateScope(); var scopedServiceProviderField = typeof(Locator).GetField("_scopedServiceProvider", BindingFlags.NonPublic | BindingFlags.Static)!; ((AsyncLocal)scopedServiceProviderField.GetValue(null)!).Value = scope.ServiceProvider; await (Task)executeMethod.Invoke(command, new object?[] { cancellationToken })!; } }

You could refactor the private methods to a single method using a more generic approach. In this new private method, you would always return a Task<TResult>, but when there's no actual result, TResult would be object and the value would be null.

Here is the refactored code:

csharpCopy code
public class CommandExecutor : ICommandExecutor { public Task InvokeAsync<TCommand>(TCommand command, CancellationToken cancellationToken) where TCommand : Command { // Passing the result type as 'object' as there is no actual result for this overload return InvokeCommandAsync<TCommand, object>(command, cancellationToken); } public Task<TResult> InvokeAsync<TCommand, TResult>(TCommand command, CancellationToken cancellationToken) where TCommand : Command<TResult> { return InvokeCommandAsync<TCommand, TResult>(command, cancellationToken); } private async Task<TResult> InvokeCommandAsync<TCommand, TResult>(TCommand command, CancellationToken cancellationToken) { var executeMethod = typeof(TCommand).GetMethod("ExecuteAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; using var scope = Locator.ServiceProvider!.CreateScope(); var scopedServiceProviderField = typeof(Locator).GetField("_scopedServiceProvider", BindingFlags.NonPublic | BindingFlags.Static)!; ((AsyncLocal<IServiceProvider?>)scopedServiceProviderField.GetValue(null)!).Value = scope.ServiceProvider; // If TResult is object, it means that there's no actual result if (typeof(TResult) == typeof(object)) { await (Task)executeMethod.Invoke(command, new object?[] { cancellationToken })!; return default; // Returns null for Task<object> } var commandResult = await (Task<TResult>)executeMethod.Invoke(command, new object?[] { cancellationToken })!; return commandResult; } }

The above code reduces the repetition and keeps the logic in one place. However, please note that this approach uses type checking at runtime and might not be as efficient or clean as having separate methods. Depending on the specifics of your code and usage, it might be better to keep the methods separate.

Avatar of the person chatting

What about when the result is really an object?

If the result can actually be an object, then my previous suggestion might not work as expected. In this case, we could introduce a special type to denote the absence of a result, and use that type when invoking InvokeCommandAsync from InvokeAsync<TCommand>. This would avoid any issues when the result is really an object.

Here is the updated code:

csharpCopy code
public class CommandExecutor : ICommandExecutor { private class NoResult { } public Task InvokeAsync<TCommand>(TCommand command, CancellationToken cancellationToken) where TCommand : Command { return InvokeCommandAsync<TCommand, NoResult>(command, cancellationToken); } public Task<TResult> InvokeAsync<TCommand, TResult>(TCommand command, CancellationToken cancellationToken) where TCommand : Command<TResult> { return InvokeCommandAsync<TCommand, TResult>(command, cancellationToken); } private async Task<TResult> InvokeCommandAsync<TCommand, TResult>(TCommand command, CancellationToken cancellationToken) { var executeMethod = typeof(TCommand).GetMethod("ExecuteAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; using var scope = Locator.ServiceProvider!.CreateScope(); var scopedServiceProviderField = typeof(Locator).GetField("_scopedServiceProvider", BindingFlags.NonPublic | BindingFlags.Static)!; ((AsyncLocal<IServiceProvider?>)scopedServiceProviderField.GetValue(null)!).Value = scope.ServiceProvider; // If TResult is NoResult, it means that there's no actual result if (typeof(TResult) == typeof(NoResult)) { await (Task)executeMethod.Invoke(command, new object?[] { cancellationToken })!; return default; // Returns null for Task<NoResult> } var commandResult = await (Task<TResult>)executeMethod.Invoke(command, new object?[] { cancellationToken })!; return commandResult; } }

In this code, NoResult is a placeholder type used to indicate the absence of a result. We use it in InvokeAsync<TCommand> when calling InvokeCommandAsync, and we check for it in InvokeCommandAsync when deciding how to handle the result of the ExecuteAsync method call. This way, we can distinguish between the case where there's no result and the case where the result is an object.

Avatar of the person chatting

There is any option without creating a new type?

Yes, you can use a tuple to differentiate between the two cases without creating a new type. Here's how you can modify the InvokeCommandAsync method to achieve this:

csharpCopy code
public class CommandExecutor : ICommandExecutor { public Task InvokeAsync<TCommand>(TCommand command, CancellationToken cancellationToken) where TCommand : Command { return InvokeCommandAsync<TCommand, ValueTuple>(command, cancellationToken); } public Task<TResult> InvokeAsync<TCommand, TResult>(TCommand command, CancellationToken cancellationToken) where TCommand : Command<TResult> { return InvokeCommandAsync<TCommand, TResult>(command, cancellationToken); } private async Task<TResult> InvokeCommandAsync<TCommand, TResult>(TCommand command, CancellationToken cancellationToken) { var executeMethod = typeof(TCommand).GetMethod("ExecuteAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; using var scope = Locator.ServiceProvider!.CreateScope(); var scopedServiceProviderField = typeof(Locator).GetField("_scopedServiceProvider", BindingFlags.NonPublic | BindingFlags.Static)!; ((AsyncLocal<IServiceProvider?>)scopedServiceProviderField.GetValue(null)!).Value = scope.ServiceProvider; // If TResult is ValueTuple, it means that there's no actual result if (typeof(TResult) == typeof(ValueTuple)) { await (Task)executeMethod.Invoke(command, new object?[] { cancellationToken })!; return default; // Returns null for Task<ValueTuple> } var commandResult = await (Task<TResult>)executeMethod.Invoke(command, new object?[] { cancellationToken })!; return commandResult; } }

In this refactored code, we use ValueTuple as a placeholder to indicate the absence of a result. When calling InvokeCommandAsync from InvokeAsync<TCommand>, we pass ValueTuple as the result type, and inside InvokeCommandAsync, we check if TResult is ValueTuple to determine how to handle the result of the ExecuteAsync method call. This way, we can distinguish between cases where there is no result and cases where the result is an object, without introducing a new type.

Avatar of the person chatting

But using this approach, the same problem of using object is present. A command can returns any thing and I cannot control because it is depends of the library consumer

You're correct. If there is a possibility that the command can return anything including a ValueTuple or object, then the previous solution will not work properly.

In this case, it might be best to keep the two methods separate to avoid any ambiguity. Although this might result in some duplicated code, it will avoid any potential confusion or bugs that could arise from trying to handle both cases in the same method.

Here is the original version with two separate methods:

csharpCopy code
public class CommandExecutor : ICommandExecutor { public Task InvokeAsync<TCommand>(TCommand command, CancellationToken cancellationToken) where TCommand : Command { return InvokeCommandAsync(command, cancellationToken); } public Task<TResult> InvokeAsync<TCommand, TResult>(TCommand command, CancellationToken cancellationToken) where TCommand : Command<TResult> { return InvokeCommandAsync<TCommand, TResult>(command, cancellationToken); } private async Task InvokeCommandAsync<TCommand>(TCommand command, CancellationToken cancellationToken) { var executeMethod = typeof(TCommand).GetMethod("ExecuteAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; using var scope = Locator.ServiceProvider!.CreateScope(); var scopedServiceProviderField = typeof(Locator).GetField("_scopedServiceProvider", BindingFlags.NonPublic | BindingFlags.Static)!; ((AsyncLocal<IServiceProvider?>)scopedServiceProviderField.GetValue(null)!).Value = scope.ServiceProvider; await (Task)executeMethod.Invoke(command, new object?[] { cancellationToken })!; } private async Task<TResult> InvokeCommandAsync<TCommand, TResult>(TCommand command, CancellationToken cancellationToken) { var executeMethod = typeof(TCommand).GetMethod("ExecuteAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; using var scope = Locator.ServiceProvider!.CreateScope(); var scopedServiceProviderField = typeof(Locator).GetField("_scopedServiceProvider", BindingFlags.NonPublic | BindingFlags.Static)!; ((AsyncLocal<IServiceProvider?>)scopedServiceProviderField.GetValue(null)!).Value = scope.ServiceProvider; var commandResult = await (Task<TResult>)executeMethod.Invoke(command, new object?[] { cancellationToken })!; return commandResult; } }

This code maintains the necessary separation to handle commands with and without results correctly, even if a command could return any type. It's often best to prioritize clarity and correctness over reducing code duplication, especially when dealing with complex generics and reflection like this.

Avatar of the person chatting

Can extract this part in a common method? var executeMethod = typeof(TCommand).GetMethod("ExecuteAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; using var scope = Locator.ServiceProvider!.CreateScope(); var scopedServiceProviderField = typeof(Locator).GetField("_scopedServiceProvider", BindingFlags.NonPublic | BindingFlags.Static)!; ((AsyncLocal)scopedServiceProviderField.GetValue(null)!).Value = scope.ServiceProvider;

Yes, you can extract the common parts into a separate method. Here's how you could do it:

csharpCopy code
public class CommandExecutor : ICommandExecutor { public Task InvokeAsync<TCommand>(TCommand command, CancellationToken cancellationToken) where TCommand : Command { PrepareCommandEnvironment<TCommand>(); return InvokeCommandAsync(command, cancellationToken); } public Task<TResult> InvokeAsync<TCommand, TResult>(TCommand command, CancellationToken cancellationToken) where TCommand : Command<TResult> { PrepareCommandEnvironment<TCommand>(); return InvokeCommandAsync<TCommand, TResult>(command, cancellationToken); } private void PrepareCommandEnvironment<TCommand>() { var executeMethod = typeof(TCommand).GetMethod("ExecuteAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; using var scope = Locator.ServiceProvider!.CreateScope(); var scopedServiceProviderField = typeof(Locator).GetField("_scopedServiceProvider", BindingFlags.NonPublic | BindingFlags.Static)!; ((AsyncLocal<IServiceProvider?>)scopedServiceProviderField.GetValue(null)!).Value = scope.ServiceProvider; } private async Task InvokeCommandAsync<TCommand>(TCommand command, CancellationToken cancellationToken) { var executeMethod = typeof(TCommand).GetMethod("ExecuteAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; await (Task)executeMethod.Invoke(command, new object?[] { cancellationToken })!; } private async Task<TResult> InvokeCommandAsync<TCommand, TResult>(TCommand command, CancellationToken cancellationToken) { var executeMethod = typeof(TCommand).GetMethod("ExecuteAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; var commandResult = await (Task<TResult>)executeMethod.Invoke(command, new object?[] { cancellationToken })!; return commandResult; } }

However, please note that the using statement in the PrepareCommandEnvironment method will not work as expected since the scope will be disposed at the end of the method. To maintain the same behavior as the original code, you may need to adjust the scope's lifecycle.

Here's an alternative approach where the scope is returned and then used in the InvokeCommandAsync methods:

csharpCopy code
public class CommandExecutor : ICommandExecutor { public Task InvokeAsync<TCommand>(TCommand command, CancellationToken cancellationToken) where TCommand : Command { using var scope = PrepareCommandEnvironment<TCommand>(); return InvokeCommandAsync(command, cancellationToken); } public Task<TResult> InvokeAsync<TCommand, TResult>(TCommand command, CancellationToken cancellationToken) where TCommand : Command<TResult> { using var scope = PrepareCommandEnvironment<TCommand>(); return InvokeCommandAsync<TCommand, TResult>(command, cancellationToken); } private IServiceScope PrepareCommandEnvironment<TCommand>() { var executeMethod = typeof(TCommand).GetMethod("ExecuteAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; var scope = Locator.ServiceProvider!.CreateScope(); var scopedServiceProviderField = typeof(Locator).GetField("_scopedServiceProvider", BindingFlags.NonPublic | BindingFlags.Static)!; ((AsyncLocal<IServiceProvider?>)scopedServiceProviderField.GetValue(null)!).Value = scope.ServiceProvider; return scope; } private async Task InvokeCommandAsync<TCommand>(TCommand command, CancellationToken cancellationToken) { var executeMethod = typeof(TCommand).GetMethod("ExecuteAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; await (Task)executeMethod.Invoke(command, new object?[] { cancellationToken })!; } private async Task<TResult> InvokeCommandAsync<TCommand, TResult>(TCommand command, CancellationToken cancellationToken) { var executeMethod = typeof(TCommand).GetMethod("ExecuteAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; var commandResult = await (Task<TResult
Avatar of the person chatting

This is how the class looks now: public class CommandExecutor : ICommandExecutor { private void _setScope(IServiceScope scope) { var scopedServiceProviderField = typeof(Locator).GetField("_scopedServiceProvider", BindingFlags.NonPublic | BindingFlags.Static)!; ((AsyncLocal)scopedServiceProviderField.GetValue(null)!).Value = scope.ServiceProvider; } private MethodInfo _getExecuteMethod() { var executeMethod = typeof(TCommand).GetMethod("ExecuteAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; return executeMethod; } public async Task InvokeAsync(TCommand command, CancellationToken cancellationToken) where TCommand : Command { using var scope = Locator.ServiceProvider.CreateScope(); _setScope(scope); await (Task)_getExecuteMethod().Invoke(command, new object?[] { cancellationToken })!; } public async Task InvokeAsync(TCommand command, CancellationToken cancellationToken) where TCommand : Command { using var scope = Locator.ServiceProvider.CreateScope(); _setScope(scope); var commandResult = await (Task)_getExecuteMethod().Invoke(command, new object?[] { cancellationToken })!; return commandResult; } } Any issue?

Your refactored code looks good and successfully reduces repetition by abstracting the common parts into the _setScope and _getExecuteMethod methods.

However, there are a few potential issues that you should be aware of:

  1. Reflection Performance: You're using reflection to get the ExecuteAsync method and _scopedServiceProvider field. Reflection can be slow, so if these methods are called frequently, it might have a performance impact. You could mitigate this by caching the MethodInfo and FieldInfo objects if the TCommand types are known in advance and there's a limited number of them.

  2. Reflection Safety: You're using the ! operator to suppress nullability warnings when getting the ExecuteAsync method and _scopedServiceProvider field. This assumes that these members will always exist and be accessible, which might not be the case if the Command class or Locator class changes in the future. It would be safer to check if these members are null and throw an informative exception if they are.

  3. Thread-Safety: You're setting the value of _scopedServiceProvider using an AsyncLocal<IServiceProvider?>, which could have issues if multiple threads are executing commands simultaneously. Make sure that the rest of your code is designed to handle this correctly.

  4. Command Execution: You're invoking the ExecuteAsync method directly on the command object, which bypasses any middleware or other command processing logic that might be in place. Depending on your application, it might be more appropriate to use a command dispatcher or similar construct to execute commands.

Please consider these points in your specific use case, and adjust your code accordingly if needed.

Avatar of the person chatting

Can you explain the Thread-Safety issue?

The AsyncLocal<T> class is used to provide data that is local to the current logical execution context. This class is often used for providing a "context" that flows with the async control flow. In other words, if you set some value in an AsyncLocal<T> and then start a new async operation (e.g., by calling Task.Run or awaiting a Task), the new async operation will "inherit" the current value of the AsyncLocal<T>.

This is helpful in some scenarios, but it can lead to unexpected behavior if you're not careful. For example, if two different threads both set the value of the same AsyncLocal<T> and then both start new async operations, the async operations will see the "last" value that was set in the AsyncLocal<T>, which might not be the value that was set by the "same" thread.

In your case, if two threads are both executing commands at the same time, they could interfere with each other's setting of the AsyncLocal<IServiceProvider?> value. Depending on the exact timing, this could lead to one command being executed with the wrong IServiceProvider, or it could lead to other weird behavior.

The best way to avoid this kind of issue is to ensure that each async operation has its own "context" that is not shared with other operations. In your case, this could mean creating a new IServiceProvider for each operation, rather than reusing the same IServiceProvider for all operations. If creating a new IServiceProvider is not feasible, another option is to use some kind of locking or synchronization to ensure that only one operation at a time can set the AsyncLocal<IServiceProvider?> value.

Avatar of the person chatting

In my case, any other part of the application regardless the CommandExecutor will modify the _scopedServiceProvider. This both test passes: [Fact] public async Task InvokeCommandAsync_DoesNotOverrideServiceScopes() { // Arrange Container.Clear(); Container.Services.AddScoped(); Container.Services.AddTransient(); Container.Initialize(); var command = new SampleCommandWithScopedState(); var tasks = new List&gt;(); // Act for (int i = 0; i &lt; 100; i++) { // Run 100 commands concurrently tasks.Add(Task.Run(async () =&gt; await command.InvokeAsync())); } var results = await Task.WhenAll(tasks); // Assert // Check that each command got its own unique scoped state Assert.Equal(100, results.Length); // 100 commands Assert.Equal(100, results.Distinct().Count()); // Each scoped state should be unique } [Fact] public async Task InvokeCommandAsync_EnsureScopedServiceProviderDoesNotRemains() { // Arrange Container.Clear(); Container.Services.AddTransient(); Container.Services.AddScoped(); Container.Initialize(); // Act var command = new GetProviderCommand(); var provider = await command.InvokeAsync(); // Arrange Assert.NotSame(Container.Services, provider); var asyncLocalSp = (AsyncLocal)typeof(Locator).GetField("_scopedServiceProvider", BindingFlags.Static | BindingFlags.NonPublic)!.GetValue(null)!; Assert.Null(asyncLocalSp.Value); }

Great, passing these tests indicates that your implementation is correctly handling scoped service providers and that concurrency doesn't affect the correct execution of commands. As long as you can guarantee that no other part of the application will modify the _scopedServiceProvider, then your implementation should work as expected.

However, bear in mind that this depends heavily on the internals of your application, and could easily break if those internals were to change. It's also potentially risky to rely on the behavior of AsyncLocal<T> in this way, because it's not designed to be used as a general-purpose thread-local storage. It's specifically designed to flow context across asynchronous operations, which can lead to unintuitive behavior if you're not careful.

As long as you're aware of these potential issues and have taken steps to mitigate them (like the tests you've written), then your current solution should work for your specific use case. But please remember that it may not be the best solution for every use case, and might need to be revised if your application changes in the future.

Avatar of the person chatting

Any other more safe and clean option?

The current solution you have appears to be designed specifically for your use case, and it works correctly based on your tests. That being said, you're using low-level concepts such as reflection and context flow, which can introduce complexity and potential issues. If you're looking for safer and cleaner options, here are some suggestions:

  1. Dependency Injection: Instead of manually managing the service scope and setting the _scopedServiceProvider within the CommandExecutor, you could rely on dependency injection to provide the correct IServiceProvider to each command. This would remove the need to manually manage the AsyncLocal<IServiceProvider?> and service scope.

  2. Command Handler Pattern: Instead of invoking the ExecuteAsync method directly on the command, you could use a command handler pattern. In this pattern, each command would have a corresponding handler, and the handler would be responsible for executing the command. This would remove the need for reflection to find and invoke the ExecuteAsync method.

    Here's an example of what this could look like:

    csharpCopy code
    public interface ICommandHandler<TCommand> { Task HandleAsync(TCommand command, CancellationToken cancellationToken); } public interface ICommandHandler<TCommand, TResult> { Task<TResult> HandleAsync(TCommand command, CancellationToken cancellationToken); }

    With this pattern, you could use dependency injection to get the appropriate command handler for each command, and then just call HandleAsync on the handler.

  3. Middleware Pipeline: If you have additional logic that needs to run before or after each command (e.g., logging, error handling, etc.), you could set up a middleware pipeline for your commands. Each piece of middleware could take a CommandContext and a Func<Task> (for InvokeAsync<TCommand>) or a Func<Task<TResult>> (for InvokeAsync<TCommand, TResult>), and could decide whether to run the next delegate in the pipeline.

These are more advanced patterns that can provide a lot of flexibility and safety, but they also require more upfront setup and might be overkill for simpler applications. However, they can be very powerful tools for managing complexity in larger applications.

Avatar of the person chatting

Is my Command executor a command handler pattern?

Your CommandExecutor appears to take on a role similar to a Command Handler in the Command Handler pattern, as it's responsible for executing the commands. However, it's not quite the traditional Command Handler pattern for a few reasons:

  1. One-for-All: Typically, in a Command Handler pattern, you have a unique handler for each type of command. This provides a clear place to encapsulate the logic for each specific command. In your case, you have a generic CommandExecutor that handles all types of commands, which is less typical.

  2. Reflection: Your CommandExecutor uses reflection to find and invoke the ExecuteAsync method on the command. In a traditional Command Handler pattern, the handler would directly call methods on the command or perform the necessary operations itself, without the need for reflection.

  3. Separation of Concerns: Command handlers typically don't concern themselves with service scope and provider details, as it's more about the command execution logic. Your CommandExecutor is handling both command execution and service scope management.

If you want to fully embrace the Command Handler pattern, you'd typically have an interface like ICommandHandler<TCommand> and for each specific command, you'd have a specific handler. This way, the logic for each command is clearly encapsulated in its respective handler, and the need for reflection is eliminated. But again, it depends on the specific needs and constraints of your application. The current design of your CommandExecutor seems to be serving your needs well based on the information provided.

Avatar of the person chatting

can you help me to implement middleware pipeline?

Sure, I'd be happy to help you with that.

A middleware pipeline for commands would allow you to add pre-processing and post-processing logic to your commands in a flexible and modular way. Each piece of middleware could do some work before and/or after the command is executed.

Firstly, let's define the middleware delegate:

csharpCopy code
public delegate Task CommandMiddlewareDelegate(CommandContext context);

This delegate represents a function that takes a CommandContext and returns a Task.

Next, let's define the CommandContext class:

csharpCopy code
public class CommandContext { public object Command { get; set; } public Type CommandType { get; set; } public CancellationToken CancellationToken { get; set; } public Task Execution { get; set; } public bool HasResult { get; set; } public object Result { get; set; } }

This class holds all the information about the command that's being executed.

Now let's define the CommandMiddleware interface:

csharpCopy code
public interface ICommandMiddleware { Task InvokeAsync(CommandContext context, CommandMiddlewareDelegate next); }

Each piece of middleware will implement this interface.

Finally, let's modify your CommandExecutor to use the middleware pipeline:

csharpCopy code
public class CommandExecutor : ICommandExecutor { private readonly ICommandMiddleware[] _middlewares; public CommandExecutor(ICommandMiddleware[] middlewares) { _middlewares = middlewares; } public Task InvokeAsync<TCommand>(TCommand command, CancellationToken cancellationToken) where TCommand : Command { var context = new CommandContext { Command = command, CommandType = typeof(TCommand), CancellationToken = cancellationToken, HasResult = false }; return InvokeMiddlewarePipeline(context); } public Task<TResult> InvokeAsync<TCommand, TResult>(TCommand command, CancellationToken cancellationToken) where TCommand : Command<TResult> { var context = new CommandContext { Command = command, CommandType = typeof(TCommand), CancellationToken = cancellationToken, HasResult = true }; return InvokeMiddlewarePipeline(context).ContinueWith(t => (TResult)context.Result); } private Task InvokeMiddlewarePipeline(CommandContext context, int index = 0) { if (index >= _middlewares.Length) { return ExecuteCommand(context); } var middleware = _middlewares[index]; return middleware.InvokeAsync(context, ctx => InvokeMiddlewarePipeline(ctx, index + 1)); } private async Task ExecuteCommand(CommandContext context) { // Your existing logic for executing the command goes here. // If the command has a result, you should set context.Result to the result. } }

This code will call each middleware in order. Each middleware can do some work, then call next(context) to call the next middleware in the pipeline, then do some more work. The CommandExecutor will execute the command after all the middleware have been called.

Please note that this is a very basic implementation and it might not cover all your needs. You might want to modify it based on your specific requirements.

Avatar of the person chatting

What about executing middleware before and after the command?

In the given implementation, middleware has the ability to execute code both before and after the command. This is because the middleware has control over when it calls the next delegate.

Here's a basic example of what a middleware might look like:

csharpCopy code
public class LoggingMiddleware : ICommandMiddleware { private readonly ILogger _logger; public LoggingMiddleware(ILogger logger) { _logger = logger; } public async Task InvokeAsync(CommandContext context, CommandMiddlewareDelegate next) { // This code will be executed before the command (and any subsequent middleware). _logger.LogInformation($"Starting execution of command {context.CommandType.Name}."); // Call the next middleware in the pipeline, which might be the command execution itself. await next(context); // This code will be executed after the command (and any previous middleware). _logger.LogInformation($"Finished execution of command {context.CommandType.Name}."); } }

In this example, LoggingMiddleware logs a message both before and after the command execution. When the next delegate is called, control is passed to the next middleware in the pipeline, or to the command execution if there are no more middleware.

This pattern allows each middleware to wrap the command execution and any subsequent middleware with pre-processing and post-processing logic. You can think of it like an onion: each layer of middleware wraps around the inner layers, and control peels through the layers from the outside in, then back out.

Avatar of the person chatting

How are HasResult and Result properties used?

The HasResult and Result properties in the CommandContext class can be used to handle commands that return a result after execution.

Here's how it works:

  1. HasResult: This property is a boolean that indicates whether the command should return a result after execution. If HasResult is true, it means that the command should return a result and the Result property should be populated after the command execution. If HasResult is false, it means that the command does not return a result and the Result property should be ignored.

  2. Result: This property holds the result returned by the command after execution. This property should be populated in the ExecuteCommand method of your CommandExecutor class if HasResult is true. After the command execution, the Result property should hold the result of the command, which can be read by the middleware or by the caller of the InvokeAsync method.

Here's an example of how you might modify the ExecuteCommand method to use these properties:

csharpCopy code
private async Task ExecuteCommand(CommandContext context) { // Your existing logic for executing the command goes here. // ... // If the command has a result, you should set context.Result to the result. if (context.HasResult) { MethodInfo executeMethod = _getExecuteMethod(context.CommandType); context.Result = await (Task<object>)executeMethod.Invoke(context.Command, new object?[] { context.CancellationToken })!; } else { MethodInfo executeMethod = _getExecuteMethod(context.CommandType); await (Task)executeMethod.Invoke(context.Command, new object?[] { context.CancellationToken })!; } }

Then, in your InvokeAsync<TCommand, TResult> method, you can return the result like this:

csharpCopy code
public Task<TResult> InvokeAsync<TCommand, TResult>(TCommand command, CancellationToken cancellationToken) where TCommand : Command<TResult> { var context = new CommandContext { Command = command, CommandType = typeof(TCommand), CancellationToken = cancellationToken, HasResult = true }; return InvokeMiddlewarePipeline(context) .ContinueWith(t => (TResult)context.Result); }

This way, each middleware can access the result of the command execution through the CommandContext, if needed.

Avatar of the person chatting

Parameters are sent into a command by the constructor. Can the context contains the parameters?

Yes, you can extend the CommandContext class to include the parameters of the command. Here's how you might do that:

csharpCopy code
public class CommandContext { public object Command { get; set; } public Type CommandType { get; set; } public CancellationToken CancellationToken { get; set; } public Task Execution { get; set; } public bool HasResult { get; set; } public object Result { get; set; } public IDictionary<string, object> Parameters { get; set; } = new Dictionary<string, object>(); }

In this version of CommandContext, there is a Parameters property which is a dictionary that maps from parameter names to their values.

To populate this dictionary, you'll need to modify your command objects to store their parameters in a way that can be easily retrieved. One way to do this is to have each command object implement an interface like this:

csharpCopy code
public interface ICommand { IDictionary<string, object> GetParameters(); }

Then, when you're creating the CommandContext object in your InvokeAsync methods, you can populate the Parameters property like this:

csharpCopy code
var context = new CommandContext { // ... Parameters = command.GetParameters(), // ... };

This will allow your middleware to access the parameters of the command through the CommandContext. Please note that this will require you to modify all of your command classes to implement the ICommand interface and store their parameters in a retrievable way.

Avatar of the person chatting

any way transparent to the program consumer?

If you want to capture the parameters passed to the command's constructor without requiring the consumer of your library to make any changes, you could use reflection to achieve this. Here's an example of how you might modify the CommandContext creation in your InvokeAsync methods:

csharpCopy code
var context = new CommandContext { // ... Parameters = ExtractParameters(command), // ... }; private IDictionary<string, object> ExtractParameters(object command) { var parameterDictionary = new Dictionary<string, object>(); var constructorInfo = command.GetType().GetConstructors().FirstOrDefault(); if (constructorInfo != null) { var constructorParameters = constructorInfo.GetParameters(); foreach (var parameter in constructorParameters) { // Retrieve the value of the parameter from the command object var value = command.GetType().GetProperty(parameter.Name)?.GetValue(command); parameterDictionary[parameter.Name] = value; } } return parameterDictionary; }

In this example, ExtractParameters method uses reflection to find the constructor of the command object, and then retrieves the values of its parameters from the properties of the command object. This assumes that each parameter to the constructor is stored in a property with the same name.

Please note that this approach might not work if the command classes don't follow the convention of storing constructor parameters in properties with the same names, or if they have multiple constructors. It also won't capture any changes to the properties that are made after the command object is constructed.

This approach also uses reflection extensively, which could have performance implications. If the performance becomes a problem, you might consider caching the ConstructorInfo and PropertyInfo objects for each command type, so you don't have to retrieve them every time a command is executed.

Avatar of the person chatting

Is CommandMiddlewareDelegate used in another site than to select the next middleware? Is a good name?

The CommandMiddlewareDelegate is a delegate type that represents the signature of the function that will be called to invoke the next middleware in the pipeline. It encapsulates a method that takes a CommandContext as a parameter and returns a Task, which is the standard signature for an asynchronous method in .NET.

This delegate type is used to define the next parameter of the InvokeAsync method in the ICommandMiddleware interface. It is used to pass the function that should be called to invoke the next middleware in the pipeline. When a middleware is done with its processing, it should call this function to pass control to the next middleware.

As for whether it's a good name, CommandMiddlewareDelegate is descriptive and follows the .NET naming conventions for delegate types, so it's a reasonable choice. It clearly communicates that this is a delegate type used in the context of command middleware.

However, if you want to make it even clearer what this delegate is used for, you might consider a name like NextCommandMiddlewareDelegate or InvokeNextMiddlewareDelegate. These names make it more explicit that this delegate is used to invoke the next middleware in the pipeline. As always with naming, the most important thing is that the name clearly communicates its purpose to the reader.

Avatar of the person chatting

I'm trying to implement InvokeMiddlewarePipeline method: if (index &gt;= _middlewares.Length) { return ExecuteCommand(context); } var middleware = _middlewares[index]; return middleware.InvokeAsync(context, ctx =&gt; InvokeMiddlewarePipeline(ctx, index + 1)); } The problem here is that the ExecuteCommand method doesn't exist because it depends if using Command or Command. The command execution is done when calling _getExecuteMethod().Invoke(...) method. There are called in two sites, depending on the Command type: public async Task InvokeAsync(TCommand command, CancellationToken cancellationToken) where TCommand : Command { using var scope = Locator.ServiceProvider.CreateScope(); _setScope(scope); await (Task)_getExecuteMethod().Invoke(command, new object?[] { cancellationToken })!; } public async Task InvokeAsync(TCommand command, CancellationToken cancellationToken) where TCommand : Command { using var scope = Locator.ServiceProvider.CreateScope(); _setScope(scope); var commandResult = await (Task)_getExecuteMethod().Invoke(command, new object?[] { cancellationToken })!; return commandResult; }

In the current structure of your CommandExecutor class, the InvokeAsync<TCommand> and InvokeAsync<TCommand, TResult> methods are responsible for both setting up the context (including the service scope) and executing the command. This is why you're having difficulty extracting the command execution into a separate ExecuteCommand method that can be called from InvokeMiddlewarePipeline.

One way to address this would be to refactor your CommandExecutor class so that the setup and execution of the command are separate. Here's an example of how you might do this:

csharpCopy code
public class CommandExecutor : ICommandExecutor { private readonly ICommandMiddleware[] _middlewares; public CommandExecutor(ICommandMiddleware[] middlewares) { _middlewares = middlewares; } public Task InvokeAsync<TCommand>(TCommand command, CancellationToken cancellationToken) where TCommand : Command { var context = CreateCommandContext(command, cancellationToken); return InvokeMiddlewarePipeline(context, 0); } public Task<TResult> InvokeAsync<TCommand, TResult>(TCommand command, CancellationToken cancellationToken) where TCommand : Command<TResult> { var context = CreateCommandContext(command, cancellationToken); return InvokeMiddlewarePipeline(context, 0); } private CommandContext CreateCommandContext<TCommand>(TCommand command, CancellationToken cancellationToken) { var context = new CommandContext { Command = command, CommandType = typeof(TCommand), CancellationToken = cancellationToken, Execution = Task.CompletedTask, // Placeholder HasResult = false, Result = null }; using var scope = Locator.ServiceProvider.CreateScope(); _setScope(scope); return context; } private Task InvokeMiddlewarePipeline(CommandContext context, int index) { if (index >= _middlewares.Length) { return ExecuteCommand(context); } var middleware = _middlewares[index]; return middleware.InvokeAsync(context, ctx => InvokeMiddlewarePipeline(ctx, index + 1)); } private Task ExecuteCommand(CommandContext context) { var command = (dynamic)context.Command; context.Execution = _getExecuteMethod(command).Invoke(command, new object?[] { context.CancellationToken }); return context.Execution; } }

In this version of CommandExecutor, the InvokeAsync methods are responsible only for creating the CommandContext and invoking the middleware pipeline. The execution of the command is handled by the ExecuteCommand method, which is called from InvokeMiddlewarePipeline when there are no more middlewares to invoke.

Note the use of the dynamic keyword in the ExecuteCommand method. This allows us to call the appropriate InvokeAsync method on the command, whether it returns a result or not. The downside of this approach is that it bypasses compile-time type checking, which could lead to runtime errors if the command does not have an InvokeAsync method with the correct signature.

Avatar of the person chatting

The problem here is that Execution is task when comes from a Command, but is a Task when comes from a Command

You're correct, that's a problem. To handle this situation, you could change the type of the Execution property in the CommandContext class from Task to object. Then, in the ExecuteCommand method, you can assign a Task or Task<TResult> to Execution depending on the type of command. Finally, when you need to await the Execution task (for example, in your middleware), you can dynamically cast it back to a Task or Task<TResult> as needed.

Here's how you can modify the CommandContext class and the ExecuteCommand method:

csharpCopy code
public class CommandContext { // ... public object Execution { get; set; } // Change the type to object // ... } private void ExecuteCommand(CommandContext context) { var command = (dynamic)context.Command; context.Execution = _getExecuteMethod(command).Invoke(command, new object?[] { context.CancellationToken }); }

When you need to await the Execution task in your middleware:

csharpCopy code
public class SomeMiddleware : ICommandMiddleware { public async Task InvokeAsync(CommandContext context, CommandMiddlewareDelegate next) { // ... await next(context); // Dynamically cast Execution back to a Task or Task<TResult> and await it if (context.Execution is Task task) { await task; } else if (context.Execution is Task<object> taskWithResult) { var result = await taskWithResult; // Do something with result if needed } } }

This approach uses the dynamic keyword quite heavily, which means you lose the benefits of compile-time type checking. It could also lead to runtime performance overhead. However, this approach allows you to handle commands that return a result and commands that do not return a result in a unified way. If you want to avoid using dynamic, you would need to create separate code paths for commands that return a result and commands that do not return a result.

Avatar of the person chatting

There're more problems. _invokeMiddlewarePipeline is a method that returns a Task, but the ExecuteCommand could returns a Task. Resent the mind, I think could be a more easy way to do it

You're correct, the use of dynamic and different types of tasks complicates the situation. Let's take a step back and consider an approach that would avoid dynamic and the variance in task types.

The approach can still involve a pipeline pattern but would need separate pipelines for commands with a result and without a result. This way you could avoid dynamic casting and keep the benefits of static type checking.

Here's how it could look:

csharpCopy code
public interface ICommandExecutor { Task InvokeAsync<TCommand>(TCommand command, CancellationToken cancellationToken) where TCommand : Command; Task<TResult> InvokeAsync<TCommand, TResult>(TCommand command, CancellationToken cancellationToken) where TCommand : Command<TResult>; } public class CommandExecutor : ICommandExecutor { private readonly ICommandMiddleware[] _middlewares; public CommandExecutor(ICommandMiddleware[] middlewares) { _middlewares = middlewares; } public Task InvokeAsync<TCommand>(TCommand command, CancellationToken cancellationToken) where TCommand : Command { var context = CreateCommandContext(command, cancellationToken); return InvokeMiddlewarePipeline(context, 0); } public Task<TResult> InvokeAsync<TCommand, TResult>(TCommand command, CancellationToken cancellationToken) where TCommand : Command<TResult> { var context = CreateCommandContext(command, cancellationToken); return InvokeMiddlewarePipelineWithResult<TCommand, TResult>(context, 0); } private CommandContext CreateCommandContext<TCommand>(TCommand command, CancellationToken cancellationToken) { // Same as before... } private Task InvokeMiddlewarePipeline(CommandContext context, int index) { // Same as before, but replace ExecuteCommand(context) with this: var command = (Command)context.Command; return _getExecuteMethod(command).Invoke(command, new object?[] { context.CancellationToken }); } private Task<TResult> InvokeMiddlewarePipelineWithResult<TCommand, TResult>(CommandContext context, int index) { if (index >= _middlewares.Length) { var command = (Command<TResult>)context.Command; return (Task<TResult>)_getExecuteMethod(command).Invoke(command, new object?[] { context.CancellationToken }); } var middleware = _middlewares[index]; return middleware.InvokeAsync(context, ctx => InvokeMiddlewarePipelineWithResult<TCommand, TResult>(ctx, index + 1)); } }

In this approach, there are separate methods for invoking the middleware pipeline for commands with a result (InvokeMiddlewarePipelineWithResult) and without a result (InvokeMiddlewarePipeline). This way, the types of tasks returned by ExecuteCommand match the types of tasks returned by the corresponding InvokeMiddlewarePipeline method.

However, this approach duplicates some code and makes it harder to add, remove, or reorder middlewares because you have to do it in two places. It may be a good idea to refactor the common parts of InvokeMiddlewarePipeline and InvokeMiddlewarePipelineWithResult into a separate method.

Avatar of the person chatting

How can I wrap a Task into Task with null value?

To wrap a Task in a Task<T> with a null value, you can create an async method that first awaits the Task, and then returns null. Here is an example:

csharpCopy code
private async Task<T> WrapTaskWithNullResult<T>(Task task) { await task; return default(T); // Returns null for reference types and zero for value types }

You would use it like this:

csharpCopy code
Task task = SomeMethodThatReturnsATask(); Task<object> wrappedTask = WrapTaskWithNullResult<object>(task);

This method creates a Task<T> that, when awaited, waits for the original Task to complete and then produces a null result (or the default value for value types). Note that this will introduce some additional overhead due to the state machine generated for the async method.

Avatar of the person chatting

This is how the class looks for now: public class CommandExecutor : ICommandExecutor { private readonly ICommandMiddleware[] _middlewares; public CommandExecutor(ICommandMiddleware[] middlewares) { _middlewares = middlewares; } private void _setScope(IServiceScope scope) { var scopedServiceProviderField = typeof(Locator).GetField("_scopedServiceProvider", BindingFlags.NonPublic | BindingFlags.Static)!; ((AsyncLocal)scopedServiceProviderField.GetValue(null)!).Value = scope.ServiceProvider; } private MethodInfo _getExecuteMethod() { var executeMethod = typeof(TCommand).GetMethod("ExecuteAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; return executeMethod; } private async Task _invokeMiddlewarePipeline(TCommand command, CommandContext context, int index, CancellationToken cancellationToken) { if (index &gt;= _middlewares.Length) { await (Task)_getExecuteMethod().Invoke(command, new object?[] { cancellationToken })!; context.IsExecuted = true; } var middleware = _middlewares[index]; await middleware.ExecuteAsync(context, ctx =&gt; _invokeMiddlewarePipeline(command, ctx, index + 1, cancellationToken)); } private async Task _invokeMiddlewarePipeline(TCommand command, CommandContext context, int index, CancellationToken cancellationToken) where TCommand : Command { if (index &gt;= _middlewares.Length) { var commandResult = await (Task)_getExecuteMethod().Invoke(command, new object?[] { cancellationToken })!; context.IsExecuted = true; context.HasResult = true; context.Result = commandResult; return commandResult; } var middleware = _middlewares[index]; await middleware.ExecuteAsync(context, ctx =&gt; _invokeMiddlewarePipeline(command, ctx, index + 1, cancellationToken)); return default!; } private CommandContext _createContext(CancellationToken cancellationToken) { var context = new CommandContext { CommandType = typeof(TCommand), CancellationToken = cancellationToken, HasResult = false, Result = null, IsExecuted = false }; return context; } public async Task InvokeAsync(TCommand command, CancellationToken cancellationToken) where TCommand : Command { using var scope = Locator.ServiceProvider.CreateScope(); _setScope(scope); await _invokeMiddlewarePipeline(command, _createContext(cancellationToken), 0, cancellationToken); } public async Task InvokeAsync(TCommand command, CancellationToken cancellationToken) where TCommand : Command { using var scope = Locator.ServiceProvider.CreateScope(); _setScope(scope); return await _invokeMiddlewarePipeline(command, _createContext(cancellationToken), 0, cancellationToken); } } Any issue or consideration?

The refactored class looks good and has addressed the main concerns with respect to code duplication and the introduction of the middleware pipeline. However, there are a couple of points that you might want to consider:

  1. Thread Safety: The _setScope method is still changing the static state. As we discussed before, you may need to ensure that this is not an issue in your specific use case.

  2. Error Handling: Right now, if there's an error in the InvokeAsync methods, the error could be thrown directly to the caller. Depending on your use case, you might want to add some error handling and logging.

  3. Asynchronous Operation: The execution of middlewares and the command itself is inherently serial because each middleware waits for the next one to complete. If a middleware operation is independent of the others, you could consider running them in parallel to increase efficiency.

  4. Middleware Execution: The middleware pipeline execution could be improved. Right now, it calls the next middleware in the pipeline directly from the previous one. This could lead to stack overflow if the number of middlewares is too large. A better approach could be to use a loop to execute the middlewares one by one. This would also make the code easier to understand and debug.

  5. Method Accessibility: The methods _setScope, _getExecuteMethod, _invokeMiddlewarePipeline, and _createContext could probably be marked as private since they seem to be implementation details of the class and not intended to be used by consumers of the class.

  6. Middleware Ordering: There is no explicit control over the order in which the middlewares are applied. Depending on how the middlewares are expected to modify the context, this might or might not be an issue.

Remember that the best solution always depends on your specific requirements and constraints.

Avatar of the person chatting

Can error handling be configured directly in a middleware like this?: try { await next() } catch (Exception) { // handle it }

Yes, error handling can indeed be configured in a middleware like that. This approach allows you to catch and handle exceptions in a centralized way, which can make your code cleaner and easier to maintain. You can catch exceptions, log them, and then either handle them or rethrow them to be caught by the next middleware.

Here is a simple example of how you could implement error handling in a middleware:

csharpCopy code
public class ErrorHandlingMiddleware : ICommandMiddleware { public async Task ExecuteAsync(CommandContext context, CommandMiddlewareDelegate next) { try { await next(context); } catch (Exception ex) { // Log the error Console.WriteLine($"An error occurred: {ex.Message}"); // You could handle the error here (e.g., by setting a result in the context) // or you could rethrow the exception to let the next middleware handle it throw; } } }

By doing this, any exceptions that are thrown during the execution of the command or the subsequent middlewares will be caught by this middleware. This can be particularly useful for logging purposes or for implementing centralized error handling logic. But be aware that the middleware execution order matters: if this middleware is the first in the pipeline, it will catch exceptions from all subsequent middlewares and the command execution itself.

Avatar of the person chatting

This is my test class: using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using IOKode.OpinionatedFramework.Commands; using IOKode.OpinionatedFramework.ConfigureApplication; using IOKode.OpinionatedFramework.Contracts; using Microsoft.Extensions.DependencyInjection; using Xunit; namespace IOKode.OpinionatedFramework.ContractImplementations.CommandExecutor.Tests; public class CommandExecutorTests { [Fact] public async Task InvokeVoidCommand_Success() { // Arrange Container.Clear(); Container.Services.AddTransient(_ =&gt; new CommandExecutor(Array.Empty())); Container.Initialize(); // Act var command = new VoidCommand(); await command.InvokeAsync(); // Assert Assert.True(command.IsExecuted); } [Fact] public async Task InvokeCommandMaintainsScopeInDeep_Success() { // Arrange Container.Clear(); Container.Services.AddScoped(); Container.Services.AddTransient(_ =&gt; new CommandExecutor(Array.Empty())); Container.Initialize(); // Act var command = new InDeepCommand(); var servicesFrom1stExecution = await command.InvokeAsync(); var servicesFrom2ndExecution = await command.InvokeAsync(); // Assert Assert.Same(servicesFrom1stExecution.Item1, servicesFrom1stExecution.Item2); Assert.Same(servicesFrom2ndExecution.Item1, servicesFrom2ndExecution.Item2); Assert.NotSame(servicesFrom1stExecution.Item1, servicesFrom2ndExecution.Item1); } [Fact] public async Task InvokeReturningIntCommand_Success() { // Arrange Container.Clear(); Container.Services.AddTransient(_ =&gt; new CommandExecutor(Array.Empty())); Container.Initialize(); // Act var command = new AddTwoNumbersCommand(3, 5); int result = await command.InvokeAsync(); // Assert Assert.Equal(8, result); } [Fact] public async Task InvokeReturningCommand_Success() { // Arrange Container.Clear(); Container.Services.AddTransient(_ =&gt; new CommandExecutor(Array.Empty())); Container.Initialize(); // Act var command = new ReturningCommand(); int result = await command.InvokeAsync(); // Assert Assert.Equal(26, result); } [Fact] public async Task InvokesCommandWithScopedService_SameServiceIsResolved() { // Arrange Container.Clear(); Container.Services.AddScoped(); Container.Services.AddTransient(_ =&gt; new CommandExecutor(Array.Empty())); Container.Initialize(); // Act var command = new SampleCommand(); var servicesFrom1stExecution = await command.InvokeAsync(); var servicesFrom2ndExecution = await command.InvokeAsync(); // Assert Assert.Same(servicesFrom1stExecution.Item1, servicesFrom1stExecution.Item2); Assert.Same(servicesFrom2ndExecution.Item1, servicesFrom2ndExecution.Item2); Assert.NotSame(servicesFrom1stExecution.Item1, servicesFrom2ndExecution.Item1); } [Fact] public async Task InvokeCommandAsync_DoesNotOverrideServiceScopes() { // Arrange Container.Clear(); Container.Services.AddScoped(); Container.Services.AddTransient(_ =&gt; new CommandExecutor(Array.Empty())); Container.Initialize(); var command = new SampleCommandWithScopedState(); var tasks = new List&gt;(); // Act for (int i = 0; i &lt; 100; i++) { // Run 100 commands concurrently tasks.Add(Task.Run(async () =&gt; await command.InvokeAsync())); } var results = await Task.WhenAll(tasks); // Assert // Check that each command got its own unique scoped state Assert.Equal(100, results.Length); // 100 commands Assert.Equal(100, results.Distinct().Count()); // Each scoped state should be unique } [Fact] public async Task InvokeCommandAsync_EnsureScopedServiceProviderDoesNotRemains() { // Arrange Container.Clear(); Container.Services.AddTransient(_ =&gt; new CommandExecutor(Array.Empty())); Container.Services.AddScoped(); Container.Initialize(); // Act var command = new GetProviderCommand(); var provider = await command.InvokeAsync(); // Arrange Assert.NotSame(Container.Services, provider); var asyncLocalSp = (AsyncLocal)typeof(Locator).GetField("_scopedServiceProvider", BindingFlags.Static | BindingFlags.NonPublic)!.GetValue(null)!; Assert.Null(asyncLocalSp.Value); } private class VoidCommand : Command { public bool IsExecuted = false; protected override Task ExecuteAsync(CancellationToken cancellationToken) { IsExecuted = true; return Task.CompletedTask; } } private class ReturningCommand : Command { protected override Task ExecuteAsync(CancellationToken cancellationToken) { return Task.FromResult(26); } } public class SampleService { } private class SampleCommand : Command&lt;(SampleService, SampleService)&gt; { protected override Task&lt;(SampleService, SampleService)&gt; ExecuteAsync(CancellationToken cancellationToken) { var service = Locator.Resolve(); var service2 = Locator.Resolve(); return Task.FromResult((service, service2)); } } private class GetProviderCommand : Command { protected override Task ExecuteAsync(CancellationToken cancellationToken) { return Task.FromResult(Locator.ServiceProvider!); } } private class ScopedStateService { public string State { get; set; } public ScopedStateService() { State = Guid.NewGuid().ToString(); } } private class SampleCommandWithScopedState : Command { protected override async Task ExecuteAsync(CancellationToken cancellationToken) { var scopedStateService = Locator.Resolve(); var scopedStateToModify = Locator.Resolve(); scopedStateToModify.State += "-modified"; int delay = new Random().Next(1000, 4000); await Task.Delay(delay); return scopedStateService.State; } } private class AddTwoNumbersCommand : Command { private readonly int _a; private readonly int _b; public AddTwoNumbersCommand(int a, int b) { _a = a; _b = b; } protected override Task ExecuteAsync(CancellationToken cancellationToken) { return Task.FromResult(_a + _b); } } private class InDeepCommand : Command&lt;(SampleService, SampleService)&gt; { public class InnerSyncClass { public SampleService Method() { return Locator.Resolve(); } } public class InnerAsyncClass { public async Task Method() { var service = Locator.Resolve(); await Task.Delay(1000); return service; } } protected override async Task&lt;(SampleService, SampleService)&gt; ExecuteAsync(CancellationToken cancellationToken) { var sync = new InnerSyncClass(); var async = new InnerAsyncClass(); var item1 = sync.Method(); var item2 = await async.Method(); return (item1, item2); } } } Please, add new tests testing middleware. IMPORTANT: Don't write the full class with new tests, only write the new tests methods and commands classes. It's difficult to me to find in the class the new methods, so only write them. I wrote the class to you only for context.

Sure, I'll provide new tests that specifically target middleware execution.

Let's start by creating a simple test middleware. This middleware will increment a static counter each time it's invoked, allowing us to verify that it's actually being executed:

csharpCopy code
public class TestMiddleware : ICommandMiddleware { public static int Counter { get; set; } = 0; public async Task ExecuteAsync(CommandContext context, CommandMiddlewareDelegate next) { Counter++; await next(context); } }

Next, we can create a test that verifies this middleware is being executed:

csharpCopy code
[Fact] public async Task MiddlewareIsInvoked_Success() { // Arrange Container.Clear(); Container.Services.AddTransient<ICommandExecutor, CommandExecutor>(_ => new CommandExecutor(new ICommandMiddleware[] { new TestMiddleware() })); Container.Initialize(); // Act var command = new VoidCommand(); await command.InvokeAsync(); // Assert Assert.Equal(1, TestMiddleware.Counter); }

This test verifies that the middleware is invoked once during command execution.

Next, let's create a test that verifies the middleware pipeline executes in the correct order. We'll need a second test middleware for this:

csharpCopy code
public class TestMiddleware2 : ICommandMiddleware { public static int Counter { get; set; } = 0; public async Task ExecuteAsync(CommandContext context, CommandMiddlewareDelegate next) { Counter++; await next(context); } }

And here's the test:

csharpCopy code
[Fact] public async Task MiddlewarePipelineExecutesInOrder_Success() { // Arrange Container.Clear(); Container.Services.AddTransient<ICommandExecutor, CommandExecutor>(_ => new CommandExecutor(new ICommandMiddleware[] { new TestMiddleware(), new TestMiddleware2() })); Container.Initialize(); // Act var command = new VoidCommand(); await command.InvokeAsync(); // Assert Assert.Equal(1, TestMiddleware.Counter); Assert.Equal(1, TestMiddleware2.Counter); Assert.True(TestMiddleware.Counter < TestMiddleware2.Counter); }

This test verifies that TestMiddleware is invoked before TestMiddleware2.

Lastly, let's create a test that verifies the middleware can handle exceptions:

csharpCopy code
public class ExceptionHandlingMiddleware : ICommandMiddleware { public static bool ExceptionHandled { get; set; } = false; public async Task ExecuteAsync(CommandContext context, CommandMiddlewareDelegate next) { try { await next(context); } catch (Exception) { ExceptionHandled = true; } } } public class ExceptionThrowingCommand : Command { protected override Task ExecuteAsync(CancellationToken cancellationToken) { throw new Exception("Test exception"); } } [Fact] public async Task MiddlewareHandlesExceptions_Success() { // Arrange Container.Clear(); Container.Services.AddTransient<ICommandExecutor, CommandExecutor>(_ => new CommandExecutor(new ICommandMiddleware[] { new ExceptionHandlingMiddleware() })); Container.Initialize(); // Act var command = new ExceptionThrowingCommand(); await command.InvokeAsync(); // Assert Assert.True(ExceptionHandlingMiddleware.ExceptionHandled); }

This test verifies that ExceptionHandlingMiddleware is able to catch and handle an exception thrown by a command.

Avatar of the person chatting

The test fails: System.IndexOutOfRangeException: Index was outside the bounds of the array. System.IndexOutOfRangeException Index was outside the bounds of the array. at IOKode.OpinionatedFramework.ContractImplementations.CommandExecutor.CommandExecutor._invokeMiddlewarePipeline[TCommand](TCommand command, CommandContext context, Int32 index, CancellationToken cancellationToken) in /Users/monty/RiderProjects/IOKode/OpinionatedFramework/src/IOKode.OpinionatedFramework.ContractImplementations.CommandExecutor/CommandExecutor.cs:line 40 at IOKode.OpinionatedFramework.ContractImplementations.CommandExecutor.Tests.CommandExecutorTests.TestMiddleware.ExecuteAsync(CommandContext context, InvokeNextMiddlewareDelegate next) in /Users/monty/RiderProjects/IOKode/OpinionatedFramework/src/IOKode.OpinionatedFramework.ContractImplementations.CommandExecutor.Tests/CommandExecutorTests.Middleware.cs:line 74 at IOKode.OpinionatedFramework.ContractImplementations.CommandExecutor.CommandExecutor._invokeMiddlewarePipeline[TCommand](TCommand command, CommandContext context, Int32 index, CancellationToken cancellationToken) in /Users/monty/RiderProjects/IOKode/OpinionatedFramework/src/IOKode.OpinionatedFramework.ContractImplementations.CommandExecutor/CommandExecutor.cs:line 41 at IOKode.OpinionatedFramework.ContractImplementations.CommandExecutor.CommandExecutor.InvokeAsync[TCommand](TCommand command, CancellationToken cancellationToken) in /Users/monty/RiderProjects/IOKode/OpinionatedFramework/src/IOKode.OpinionatedFramework.ContractImplementations.CommandExecutor/CommandExecutor.cs:line 87 at IOKode.OpinionatedFramework.Commands.CommandExtensions.InvokeAsync(Command command, CancellationToken cancellationToken) in /Users/monty/RiderProjects/IOKode/OpinionatedFramework/src/IOKode.OpinionatedFramework.Foundation/Commands/CommandExtensions.cs:line 10 at IOKode.OpinionatedFramework.ContractImplementations.CommandExecutor.Tests.CommandExecutorTests.MiddlewareIsInvoked_Success() in /Users/monty/RiderProjects/IOKode/OpinionatedFramework/src/IOKode.OpinionatedFramework.ContractImplementations.CommandExecutor.Tests/CommandExecutorTests.Middleware.cs:line 24 at Xunit.Sdk.TestInvoker`1.&lt;&gt;c__DisplayClass48_0.&lt;b__1&gt;d.MoveNext() in /_/src/xunit.execution/Sdk/Frameworks/Runners/TestInvoker.cs:line 264 --- End of stack trace from previous location --- at Xunit.Sdk.ExecutionTimer.AggregateAsync(Func`1 asyncAction) in /_/src/xunit.execution/Sdk/Frameworks/ExecutionTimer.cs:line 48 at Xunit.Sdk.ExceptionAggregator.RunAsync(Func`1 code) in /_/src/xunit.core/Sdk/ExceptionAggregator.cs:line 90

The error message indicates that an IndexOutOfRangeException is being thrown from within the _invokeMiddlewarePipeline method of your CommandExecutor class. This suggests that there's an issue with how the middleware pipeline is being invoked.

Unfortunately, without seeing the implementation of the CommandExecutor class, I can't definitively diagnose the issue. However, I can suggest a common pattern that often leads to this error in a middleware pipeline.

In a middleware pipeline, each middleware is responsible for invoking the next one. The last middleware in the pipeline should invoke the actual command. If any middleware tries to invoke a "next" middleware when it is already the last one, it will cause an IndexOutOfRangeException, because there is no next middleware to invoke

Avatar of the person chatting

This is the CommandExecutor implementation: using System.Reflection; using IOKode.OpinionatedFramework.Commands; using IOKode.OpinionatedFramework.Contracts; using Microsoft.Extensions.DependencyInjection; namespace IOKode.OpinionatedFramework.ContractImplementations.CommandExecutor; public class CommandExecutor : ICommandExecutor { private readonly ICommandMiddleware[] _middlewares; public CommandExecutor(ICommandMiddleware[] middlewares) { _middlewares = middlewares; } private void _setScope(IServiceScope scope) { var scopedServiceProviderField = typeof(Locator).GetField("_scopedServiceProvider", BindingFlags.NonPublic | BindingFlags.Static)!; ((AsyncLocal)scopedServiceProviderField.GetValue(null)!).Value = scope.ServiceProvider; } private MethodInfo _getExecuteMethod() { var executeMethod = typeof(TCommand).GetMethod("ExecuteAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; return executeMethod; } private async Task _invokeMiddlewarePipeline(TCommand command, CommandContext context, int index, CancellationToken cancellationToken) { if (index &gt;= _middlewares.Length) { await (Task)_getExecuteMethod().Invoke(command, new object?[] { cancellationToken })!; context.IsExecuted = true; } var middleware = _middlewares[index]; await middleware.ExecuteAsync(context, ctx =&gt; _invokeMiddlewarePipeline(command, ctx, index + 1, cancellationToken)); } private async Task _invokeMiddlewarePipeline(TCommand command, CommandContext context, int index, CancellationToken cancellationToken) where TCommand : Command { if (index &gt;= _middlewares.Length) { var commandResult = await (Task)_getExecuteMethod().Invoke(command, new object?[] { cancellationToken })!; context.IsExecuted = true; context.HasResult = true; context.Result = commandResult; return commandResult; } var middleware = _middlewares[index]; await middleware.ExecuteAsync(context, ctx =&gt; _invokeMiddlewarePipeline(command, ctx, index + 1, cancellationToken)); return default!; } private CommandContext _createContext(CancellationToken cancellationToken) { var context = new CommandContext { CommandType = typeof(TCommand), CancellationToken = cancellationToken, HasResult = false, Result = null, IsExecuted = false }; return context; } public async Task InvokeAsync(TCommand command, CancellationToken cancellationToken) where TCommand : Command { using var scope = Locator.ServiceProvider.CreateScope(); _setScope(scope); await _invokeMiddlewarePipeline(command, _createContext(cancellationToken), 0, cancellationToken); } public async Task InvokeAsync(TCommand command, CancellationToken cancellationToken) where TCommand : Command { using var scope = Locator.ServiceProvider.CreateScope(); _setScope(scope); return await _invokeMiddlewarePipeline(command, _createContext(cancellationToken), 0, cancellationToken); } }

From the implementation of the CommandExecutor, it seems the IndexOutOfRangeException is due to the fact that the ExecuteAsync method of the middleware is called, even when the index is equal to the length of the middleware array. In other words, even if the end of the middleware pipeline is reached, the code still attempts to call the next middleware in the pipeline, leading to an IndexOutOfRangeException.

To fix this, you should return the function early if the end of the middleware pipeline is reached. The corrected _invokeMiddlewarePipeline methods should look like this:

csharpCopy code
private async Task _invokeMiddlewarePipeline<TCommand>(TCommand command, CommandContext context, int index, CancellationToken cancellationToken) { if (index >= _middlewares.Length) { await (Task)_getExecuteMethod<TCommand>().Invoke(command, new object?[] { cancellationToken })!; context.IsExecuted = true; return; } var middleware = _middlewares[index]; await middleware.ExecuteAsync(context, ctx => _invokeMiddlewarePipeline(command, ctx, index + 1, cancellationToken)); } private async Task<TResult> _invokeMiddlewarePipeline<TCommand, TResult>(TCommand command, CommandContext context, int index, CancellationToken cancellationToken) where TCommand : Command<TResult> { if (index >= _middlewares.Length) { var commandResult = await (Task<TResult>)_getExecuteMethod<TCommand>().Invoke(command, new object?[] { cancellationToken })!; context.IsExecuted = true; context.HasResult = true; context.Result = commandResult; return commandResult; } var middleware = _middlewares[index]; await middleware.ExecuteAsync(context, ctx => _invokeMiddlewarePipeline<TCommand, TResult>(command, ctx, index + 1, cancellationToken)); return default!; }

This should prevent the IndexOutOfRangeException from being thrown.

Avatar of the person chatting

Back to tests: [Fact] public async Task MiddlewareIsInvoked_Success() { // Arrange Container.Clear(); Container.Services.AddTransient(_ =&gt; new CommandExecutor(new ICommandMiddleware[] { new TestMiddleware() })); Container.Initialize(); // Act var command = new VoidCommand(); await command.InvokeAsync(); // Assert Assert.Equal(1, TestMiddleware.Counter); } [Fact] public async Task MiddlewarePipelineExecutesInOrder_Success() { // Arrange Container.Clear(); Container.Services.AddTransient(_ =&gt; new CommandExecutor(new ICommandMiddleware[] { new TestMiddleware(), new TestMiddleware2() })); Container.Initialize(); // Act var command = new VoidCommand(); await command.InvokeAsync(); // Assert Assert.Equal(1, TestMiddleware.Counter); Assert.Equal(1, TestMiddleware2.Counter); Assert.True(TestMiddleware.Counter &lt; TestMiddleware2.Counter); } Here the problem is that Counter is static in middleware, so when the first test run, the value is inherited by the second tests. Other problems with MiddlewarePipelineExecutesInOrder_Success: - This test only ensure that the both middleware are executed, but not the order - If both counters are 1, the third assert is false. If the third assert is true, the counters cannot be equals, so one of then (or both) cannot be 1. This test will fail in any case

For the problem of a static Counter in TestMiddleware, you might want to reset the counter to 0 in the Dispose method or at the start of each test. This will make sure that each test starts with a fresh state.

Regarding the order of execution, you might want to use a static List<string> in each middleware. Each time a middleware is invoked, it adds its name to the list. By checking the order of the names in the list, you can verify the order of execution of the middlewares.

For TestMiddleware, you could have:

csharpCopy code
public class TestMiddleware : ICommandMiddleware { public static List<string> ExecutionOrder = new List<string>(); public async Task ExecuteAsync(CommandContext context, InvokeNextMiddlewareDelegate next) { ExecutionOrder.Add("TestMiddleware"); await next(context); } }

And for TestMiddleware2:

csharpCopy code
public class TestMiddleware2 : ICommandMiddleware { public static List<string> ExecutionOrder = new List<string>(); public async Task ExecuteAsync(CommandContext context, InvokeNextMiddlewareDelegate next) { ExecutionOrder.Add("TestMiddleware2"); await next(context); } }

Then, in your MiddlewarePipelineExecutesInOrder_Success test, you can assert the order of execution as follows:

csharpCopy code
[Fact] public async Task MiddlewarePipelineExecutesInOrder_Success() { // Arrange TestMiddleware.ExecutionOrder.Clear(); TestMiddleware2.ExecutionOrder.Clear(); Container.Clear(); Container.Services.AddTransient<ICommandExecutor, CommandExecutor>(_ => new CommandExecutor(new ICommandMiddleware[] { new TestMiddleware(), new TestMiddleware2() })); Container.Initialize(); // Act var command = new VoidCommand(); await command.InvokeAsync(); // Assert Assert.Equal(new List<string> { "TestMiddleware", "TestMiddleware2" }, TestMiddleware.ExecutionOrder); }

This way, you ensure that the middlewares are executed in the correct order.

Avatar of the person chatting

Can we create a permission feature to run a command? The command executor should check permission before running a command and throws an exception when there are not enough permissions. The permission should be attached to a command using a attribute. So, in this point of the framework development, there are any entity that represents an user

In order to implement a permission feature, you would need a representation of a user and the permissions associated with that user. If you don't already have these concepts in your application, you will need to create them.

Here is a simplified example of how you might go about this:

  1. Define a User

Create a User class (or interface) with an identifier and a set of permissions:

csharpCopy code
public class User { public string Id { get; set; } public List<string> Permissions { get; set; } }
  1. Define a Permission Attribute

You can create a custom attribute that can be used to associate permissions with commands:

csharpCopy code
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class RequiresPermissionAttribute : Attribute { public string Permission { get; set; } public RequiresPermissionAttribute(string permission) { Permission = permission; } }
  1. Modify the CommandExecutor

You'll need to modify the CommandExecutor to check for permissions before executing a command:

csharpCopy code
public async Task InvokeAsync<TCommand>(TCommand command, CancellationToken cancellationToken) where TCommand : Command { using var scope = Locator.ServiceProvider.CreateScope(); _setScope(scope); // Get the current user var user = ... // Fetch user from context, session, etc. // Get the command's required permissions var commandType = command.GetType(); var permissionAttribute = commandType.GetCustomAttribute<RequiresPermissionAttribute>(); // If the command requires a permission, check if the user has it if (permissionAttribute != null && !user.Permissions.Contains(permissionAttribute.Permission)) { throw new PermissionException("User does not have required permission to execute command."); } await _invokeMiddlewarePipeline(command, _createContext<TCommand>(cancellationToken), 0, cancellationToken); }

This is a very basic implementation and would likely need to be expanded upon for a real-world application. For example, you might want to support multiple permissions for a single command, or have more advanced permission checks.

Remember to consider security implications and best practices when implementing any sort of user and permission system. It's also important to properly handle exceptions and provide meaningful error messages for the end user.

Avatar of the person chatting

Now, I'll send you a series of six posts in Spanish about a progress of developing a framework in C#. Due length of the post, simply store it in your context window and respond me with a single phrase "Ok, I read it and stored it in my context window". After you got all posts, I instruct you to write a post about a new feature: The commands.

Sure, I'm ready to receive the posts.

Avatar of the person chatting

Esta entrada forma parte de una serie: - **¿Este framework tiene opinión?** - [Arquitectura limpia ¿opinionada?](https://iokode.blog/post/2023/arquitectura-limpia-opinionada) - [OpinionatedFramework: Validaciones](https://iokode.blog/post/2023/opinionated-framework-validaciones) - [OpinionatedFramework: Registrando y resolviendo servicios](https://iokode.blog/post/2023/opinionated-framework-resolviendo-servicios) - [OpinionatedFramework: Contratos y fachadas](https://iokode.blog/post/2023/opinionated-framework-contratos) - [OpinionatedFramework: Contenedor de dependencias estándar](https://iokode.blog/post/2023/opinionated-framework-contenedor-de-dependencias-estandar) --- Hace unos días [pregunté en Twitter](https://twitter.com/montyclt/status/1610012234087817217) si el framework [Laravel](https://laravel.com/) era un framework opinionado. Quería saber primero, que se entiende por un framework opinionado y segundo, si esto es bueno o no. Aunque la pregunta la hice usando Laravel de ejemplo, ya que tiene fama de ser un framework opinionado a raíz de los servicios que ofrece y los patrones que promueve, todo el contenido de esta entrada se podría aplicar a cualquier otro framework. El hilo dio debate y sacaron a la luz algunos argumentos interesantes sobre lo que es un framework opinionado. ## ¿Qué es un framework opinionado? Entre todas estas respuestas, me gustaría destacar especialmente [la de David Fowler](https://twitter.com/davidfowl/status/1610066514018992128), en el que explica que algunos frameworks pueden estar más opinionados que otros, y que las "opiniones" pueden venir en diferentes formas, entre ellas documentación o implementaciones por defecto. Otra posible forma de dar "opinión" es a través del famoso paradigma [convención sobre configuración](https://en.wikipedia.org/wiki/Convention_over_configuration). Se podría decir que un framework es opinionado cuando este te guía en la forma de hacer cosas. Veamos algunos ejemplos. ## Validaciones Algunos frameworks proponen ciertas formas de validar datos de entrada, en Laravel por ejemplo, tenemos las llamadas [validation request](https://laravel.com/docs/9.x/validation#form-request-validation), mientras que en ASP.NET Core tenemos el famoso [ModelState y los validation attributes](https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation). ## Contratos en Laravel Laravel ofrece una serie de servicios en el core como [emails](https://laravel.com/docs/9.x/mail), [broadcasting](https://laravel.com/docs/9.x/broadcasting), [caché](https://laravel.com/docs/9.x/cache), [notificaciones](https://laravel.com/docs/9.x/notifications), [eventos](https://laravel.com/docs/9.x/events), etc. En el core de Laravel hay una serie de interfaces que definen estos servicios llamados [contratos](https://laravel.com/docs/9.x/contracts), así como una implementación por defecto para cada uno de los contratos. Laravel también ofrece una serie de distintos mecanismos para resolver estos servicios, como puede ser [inyección de dependencias](https://laravel.com/docs/9.x/container), fachadas o [la función helper `app(string $serviceName)`](https://laravel.com/docs/9.x/helpers#method-app). Se podría decir entonces que tanto los contratos como las distintas formas de resolver estos servicios, es una forma de "opinión" de Laravel. No es la única, hay muchas otras como la predominancia del [sistema de plantillas Blade](https://laravel.com/docs/9.x/blade) o la "integración" que provee por defecto de [Vue.js](https://vuejs.org/). ### Fachadas Entre las distintas formas de resolver servicios que provee Laravel se encuentran las fachadas, una serie de clases estáticas que permiten acceder a los servicios de estos contratos desde cualquier parte. Esto es útil sobretodo en [aquellas clases donde no es fácil hacer inyección de dependencias](https://blog.koalite.com/2019/02/clases-estaticas-como-alternativa-a-inyeccion-de-dependencias/) o en aquellos proyectos que [se ha optado por no utilizar inyección de dependencias](https://blog.koalite.com/2016/02/las-ventajas-de-no-usar-inyeccion-de-dependencias/). ## La ausencia de contratos en ASP.NET Core A diferencia de Laravel, ASP.NET Core no proporciona muchos de estos servicios. Se entiende que el envío de una notificación o el acceso a una base de datos es cosa de tu aplicación y será implementado por esta dependiendo de la aplicación. Este enfoque no es malo, ya que si tu aplicación no accede a una base de datos, pues es una dependencia menos que tienes que manejar. Se podría decir que el framework es "más pequeño". Si tu aplicación necesita acceder a una base de datos, hay varias soluciones disponibles en NuGet, incluyendo Entity Framework, la solución de Microsoft considerada "oficial" en ASP.NET Core. Este enfoque permite además poder utilizar estas soluciones sin necesidad de depender de todo el framework en un proyecto que no lo requiere. Es simplemente desacoplamiento puro.

Ok, I read it and stored it in my context window.

Avatar of the person chatting

Esta entrada forma parte de una serie: - [¿Este framework tiene opinión?](https://iokode.blog/post/2023/este-framework-tiene-opinion) - **Arquitectura limpia ¿opinionada?** - [OpinionatedFramework: Validaciones](https://iokode.blog/post/2023/opinionated-framework-validaciones) - [OpinionatedFramework: Registrando y resolviendo servicios](https://iokode.blog/post/2023/opinionated-framework-resolviendo-servicios) - [OpinionatedFramework: Contratos y fachadas](https://iokode.blog/post/2023/opinionated-framework-contratos) - [OpinionatedFramework: Contenedor de dependencias estándar](https://iokode.blog/post/2023/opinionated-framework-contenedor-de-dependencias-estandar) --- Hasta ahora hemos hablado de las opiniones de un framework, pero estas suelen ser bypasseadas cuando se aplica [arquitectura limpia](https://www.amazon.es/dp/8441539901). Poco importa que tu framework incluya un contrato para el envío de notificaciones si tu capa aplicación no depende de él y por lo tanto no lo puede utilizar directamente. Poco importa la forma en la que el framework valide los datos de entrada si tu capa de aplicación va a hacerlo de otra forma diferente. Cuando se aplica arquitectura limpia, en las capas de aplicación y dominio se proporcionan una serie de interfaces que luego desde la capa de infraestructura llamarán a los servicios del framework. Veamos esto con un ejemplo. Creamos un caso de uso que se encarga crear un cliente, guardarlo en un medio de persistencia y luego enviar un correo electrónico a este cliente. ```csharp public class CreateCustomerUseCase { public void Invoke(CustomerModel model) { var customer = model.Create(); PersistentStorage.Save(customer); EmailSender.Send(new WelcomeEmail(customer)); } } ``` Ahora véamos que es lo que hacen los métodos `Save` y `Send` : ```csharp public static void Send(Email email) { var sender = Locator.Resolve(); sender.Send(email); } public static void Save(object entity) { var storage = Locator.Resolve(); storage.Save(entity); } ``` Como se puede observar, estos métodos estáticos son simples fachadas hacía una interfaz que proveé nuestra aplicación, pero que tendrá que ser implementada en la capa de aplicación utilizando el mecanismo que el framework provea. Ahora pongamos como ejemplo la implementación de la interfaz IEmailSender en la capa de infraestructura utilizando un framework que proveé su contrato para enviar emails e inyección de dependencias como forma de resolverlos. ```csharp public class AppEmailSender : Application.IEmailSender { private Framework.IEmailSender _EmailSender; public AppEmailSender(Framework.IEmailSender emailSender) { _EmailSender = emailSender; } public void Send(Application.Email email) { var frameworkEmail = new Framework.Email() { From = "myapp@example.com", To = email.To, Content = email.Content }; _EmailSender.Send(frameworkEmail); } } ``` ## ¿Por qué no se usan unas interfaces comunes? Generalmente porque este enfoque permite adaptar estos servicios al dominio concreto de la aplicación. Es posible que un email no sea exactamente lo mismo en un tipo de aplicación que en otro ni tampoco las operaciones sobre el mismo. ## ¿Qué problema hay con esto? El principal problema de este enfoque es que requiere un aburrido y repetitivo trabajo de escribir una serie de interfaces y valueobjects/estructuras (como la clase Email en este caso) similares por cada proyecto que se inicie. ## Conclusión En mi opinión esa flexibilidad no componsa ya que un email en casi cualquier aplicación será un mensaje compuesto de texto plano y/o HTML, una dirección de origen, una de destino, direcciones en copia y copia oculta y archivos adjuntos. Esto supone repetir las clases e interfaces para cada uno de los proyectos que inicias, tarea que puede ser especialmente pesada, sobretodo para aquellas clases que tienen que [mantener sus invariantes](https://blog.koalite.com/2014/01/diseno-por-contrato/). En el supuesto caso de que tu aplicación muy especifica entienda otra cosa por email, siempre puedes crear tu clase de dominio y obviar la clase proporcionada. ## IOKode.OpinionatedFramework Yo tengo una opinión de como debería de ser un proyecto de dominio y aplicación y he querido hacer un framework que la reespalde. Este dispone de una librería de validación, un contenedor de dependencias para la capa de dominio aislado del contenedor de Microsoft, una serie de contratos para servicios comunes así como fachadas para acceder directamente a ellos. El framework está aún en fase muy temprano de desarrollo y por lo tanto aún no está disponible en NuGet, pero el repositorio de desarrollo es público en GitHub. ### OpenSoruce y licencia abierta [El código fuente está disponible en GitHub](https://github.com/iokode/OpinionatedFramework) y la licencia es BSD de 2 cláusulas, lo que permite su uso ilimitado en proyectos tanto cerrados como abiertos. ### ¿Quieres colaborar? OpinionatedFramework es un proyecto open source [alojado en GitHub](https://github.com/iokode/OpinionatedFramework). Si quieres colaborar en el desarrollo, puedes enviar una pull request.

Ok, I read it and stored it in my context window.

Avatar of the person chatting

Esta entrada forma parte de una serie: - [¿Este framework tiene opinión?](https://iokode.blog/post/2023/este-framework-tiene-opinion) - [Arquitectura limpia ¿opinionada?](https://iokode.blog/post/2023/arquitectura-limpia-opinionada) - **OpinionatedFramework: Validaciones** - [OpinionatedFramework: Registrando y resolviendo servicios](https://iokode.blog/post/2023/opinionated-framework-resolviendo-servicios) - [OpinionatedFramework: Contratos y fachadas](https://iokode.blog/post/2023/opinionated-framework-contratos) - [OpinionatedFramework: Contenedor de dependencias estándar](https://iokode.blog/post/2023/opinionated-framework-contenedor-de-dependencias-estandar) --- La [validación de precondiciones e invaraintes](https://blog.koalite.com/2014/01/diseno-por-contrato/) es un requisito básico en el diseño de cualquier clase o estructura. Existen varios paquetes en NuGet para realizar esta validación de forma sencilla, pero prácticamente todos pecan del mismo problema: **no permiten personalizar el tipo de excepción que se lanzará cuando la validación no se cumple**. OpinionatedFramework incluye una librería de validación que refleja lo que para mi es el API perfecta para validaciones. ## Uso El uso de esta librería es simple: ```csharp Ensure.Exception(new Exception()).Category.Validation(args); ``` La librería lanzará la excepción proporcionada si no se cumple la validación. Para hacer su uso más cómodo, se incluyen algunos métodos que seleccionan ciertas excepciones: ```csharp Ensure.Argument(nameof(param)).String.NotEmpty(value); // ArgumentException Ensure.Argument(nameof(param)).NotNull(value); // ArgumentNullException Ensure.Base("Exception message.").String.NotEmpty(value); // Exception Ensure.Exception(new EmailException("Invalid email address.")).String.EmailAddress(value); // EmailException Ensure.InvalidOperation("Cannot add an empty string to the error bag.").String.NotEmpty(value); // InvalidOperationException ``` Estos métodos son: - `Argument`: Lanza una `ArgumentException` o una `ArgumentNullException` - `Base`: Lanza una `Exception` con el mensaje especificado. - `Exception`: Lanza la instancia de la excepción proporcionada. - `InvalidOperation`: Lanza una `InvalidOperationException` con el mensaje especificado. ## Categorías de validaciones (ensurers) En cuanto a las validaciones, esta librería incluye validaciones de las siguientes categorías: - `Assertion`: Valida booleanos a true o false. - `Enumerable`: Contiene validaciones sobre IEnumerable, por ejemplo, si está vacío, al menos tantos elementos, si contiene un elemento concreto… - `Number`: Contiene validaciones sobre valores númericos, por ejemplo si es valor está entre dos números, si es decimal... - `Object`: Contiene validaciones sobre objetos, por ejemplo, si es nulo. - `Stream`: Contiene validaciones sobre streams, por ejemplo, si se puede leer, se puede escribir… - `String`: Contiene validaciones sobre cadenas de carácteres, por ejemplo si está vacía, si es de tal tamaño, si contiene tal subcadena, si es un email válido, si cumple una expresión regular... - `Type`: Contiene validaciones sobre tipos, por ejemplo, si es un tipo valor o referencia, si es asignable a otro tipo… ## Puntos de extensión A cada una de estas categorías se les llama *Ensurers.* Escribir un ensurer nuevo es tan sencillo como crear una clase estática con las validaciones y añadirle el atributo `[Ensurer]`: ```csharp [Ensurer] public static class CustomerEnsurer { public static bool IsIndividual(Customer customer) { return customer is IndividualCustomer; } public static bool IsBusiness(Customer customer) { return customer is BusinessCustomer; } } ``` Tras escribir esta simple clase, un [source generator](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview) se encarga de hacer toda la magia necesaria y a partir de este momento ya pueden ser usados (muchas gracias a @Angeling3 por escribir los source generators): ```csharp Ensure.Exception(new CustomerException()).Customer.IsIndividual(customer); ``` Actualmente no lo permite, pero me gustaría permitir poder añadir nuevos métodos que seleccionan la excepción, de forma que la validación de arriba pueda ser escrita se la siguiente forma: ```csharp Ensure.Customer().Customer.IsIndividual(customer); ``` ## Desactivación por motivo de rendimiento Otro aspecto que aún no está disponible pero que pretendo añadir es la opción de poder desactivar las validaciones sobre ciertas condiciones. Esto permitiría poder ignorarlas evitando así un impacto en el rendimiento cuando, por ejemplo, la aplicación se compila en modo release. ## Aislado del resto del framework Esta librería será lanzada en un paquete NuGet aislado del resto de OpinionatedFramework, de forma que pueda ser utilizada sin necesidad de tener una dependencia sobre el framework completo. Sin embargo, el resto de OpinionatedFramework depende de ella, ya que es usada para validar las invariantes de las propias clases que proporciona el framework. ## ¿Quieres colaborar? OpinionatedFramework es un proyecto open source [alojado en GitHub](https://github.com/iokode/OpinionatedFramework). Si quieres colaborar en el desarrollo, puedes enviar una pull request. [Ensuring](https://github.com/iokode/OpinionatedFramework/tree/main/src/IOKode.OpinionatedFramework.Ensuring) es el proyecto dentro de OpinionatedFramework encargado de las validaciones.

Ok, I read it and stored it in my context window.

Avatar of the person chatting

Esta entrada forma parte de una serie: - [¿Este framework tiene opinión?](https://iokode.blog/post/2023/este-framework-tiene-opinion) - [Arquitectura limpia ¿opinionada?](https://iokode.blog/post/2023/arquitectura-limpia-opinionada) - [OpinionatedFramework: Validaciones](https://iokode.blog/post/2023/opinionated-framework-validaciones) - **OpinionatedFramework: Registrando y resolviendo servicios** - [OpinionatedFramework: Contratos y fachadas](https://iokode.blog/post/2023/opinionated-framework-contratos) - [OpinionatedFramework: Contenedor de dependencias estándar](https://iokode.blog/post/2023/opinionated-framework-contenedor-de-dependencias-estandar) --- Toca hablar de como en OpinionatedFramework se registrar y resuelven servicios, algo importantisimo ya que todos los contratos que el propio framework trae consigo mismo son servicios registrados y las fachadas son clases que tienen que resolver estos servicios. Pero antes de ello, vamos a hablar de distintas formas de resolver servicios. ## La inyección de dependencias por constructor Este es, probablemente, el patrón de diseño más extendido cuando se trata de resolver servicios. Estos se inyectan directamente en el constructor de la clase asegurandote que cuando la clase sea construída, posea todos los servicios de los que esta depende (dependencias). La principal ventaja de usar este patrón es que tu clase queda totalmente desacoplada del mecanismo que utilices para instanciarla. Esta clase no es consciente ni tiene porque ser consciente de que existe (o no) un contenedor que se encargue de crear las instancias. Además permite crear la instancia directamente usando la palabra `new`, lo que puede ser útil para utilizarla en un proyecto sencillo que no requiera de un contenedor de dependencias. Si estás escribiendo un componente de una librería que requiera de un servicio, esta debería de ser la forma por defecto de hacerlo y, así es como lo hice cuando diseñé el componente `[WordGenerator](https://github.com/iokode/inake/blob/main/IOKode.Inake/WordGenerator.cs#L19)` de la librería [Inake](https://github.com/iokode/inake). ### ¿Qué problemas tiene la inyección de dependencias por constructor? La inyección de dependiencias por constructor no está exenta de problemas. Bajo mi punto de vista esta tiene cuatro problemas principales: - **Dificultad de instanciación de objetos**: Aunque se pueden crear las instancias manualmente con la plabara `new`, la dificultad de instanciar una clase con sus dependencias (y las dependencias de las dependencias) hace que de forma prácticamente obligatoria tengamos que hacer uso de un contenedor que se encargue de crear las instancias mediante técnicas de reflexión. - **Imposibilidad de uso en clases estáticas**: Las clases estáticas no tienes constructores, lo que prácticamente hace imposible que esta pueda depender de servicios. Ello te obliga a hacer dinámicas y necesitar una instancia de una clase que por su funcionalidad tendría sentido que sea una clase estática, como puede serlo una factoría. - **Dificultad de inyección en entidades**: Las entidades siempre son un dolor de cabeza cuando requieren de un servicio. Por lo general están pensadas para ser creadas directamente utilizando `new` y, cuando requieren de un servicio, toca buscar alternativas como construirlas a través de un factoría. Este problema se agrava cuando toca escribir toda una capa de mappers para que tu ORM pueda deserializar datos en una entidad haciendo uso de la factoría que le inyecta los servicios. - **Expone detalles de implementación**: A través del constructor se exponen los servicios que la clase requiere para funcionar, lo que puede dar una serie de pistas de como funciona. Si uso un servicio que me tiene que dar leche, no quiero tener que proporcionarle la nevera de donde sacará la leche. Sobre el último punto, [hay opiniones contrarias](https://twitter.com/javiercampos/status/1625023444734685184). Aunque entiendo el razonamiento, personalmente no termino de compartirlo del todo. Recomiendo encarecidamente leer el hilo completo y que cada uno saque sus propias conclusiones. ## Service Locator Otro de los patrones (o antipatrón, [según a quién le preguntes](https://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/)) más famosos para resolver servicios es el llamado Service Locator. Este patrón es bastante sencillo, simplemente consiste en llamar a una función que se encargue de resolver un servicio. Muchas implementaciones de este patrón no son estáticas, teniendo una forma similar a esta: ```csharp public class Controller : IController { private readonly IServiceLocator _serviceLocator; public Controller(IServiceLocator serviceLocator) { _serviceLocator = serviceLocator; } public void Execute(TContext context) { var command = _serviceLocator.GetInstance&gt;(); if (!command.IsNull()) { command.Execute(context); } var disposable = command as IDisposable; if (disposable != null) disposable.Dispose(); } } ``` Esta aproximación tiene los mismos problemas que he comentado sobre la inyección de dependencias (ya que en esencia, se está haciendo DI para inyectar el service locator). En su lugar, yo preferiría utilizar un locator estático para esto: ```csharp public class Controller : IController { public void Execute(TContext context) { var command = ServiceLocator.GetInstance&gt;(); if (!command.IsNull()) { command.Execute(context); } var disposable = command as IDisposable; if (disposable != null) disposable.Dispose(); } } ``` Esta aproximación soluciona los problemas de arriba además de hacer la clase más pequeña al eliminar el constructor. ### Los problemas del patrón Service Locator. Este patrón tampoco está exento de problemas. Si muchas personas lo consideran un antipatrón no es precisamente porque les haya dado una cruzada irracional contra él. El principal problema del patrón Locator es que obliga a tu aplicación a referenciar al contenedor (o al menos al Locator si se hace una buena [segregación de interfaces](https://devexperto.com/principio-de-segregacion-de-interfaces/)). Esto rompe la idea de que tu aplicación no debe de depender del framework, si no que este debe de ser parte de la infraestructura. Esto aquí no es un problema ya que todo OpinionatedFramework está diseñado para actuar sobre las capas de dominio y aplicación. Otro de los problemas es que [oculta las dependencias de una clase](https://twitter.com/montyclt/status/1624974712185974784). Esto yo es algo que a mi me sigue pareciendo casi más una ventaja que un inconveniente, pues pienso que viola el principio *[tell, don’t ask](https://www.danielprimo.io/blog/manual-de-buenas-practicas-tell-don-t-ask)*. Sin embargo, hay quién opina que hacer las dependencias explícitas es algo positivo. Recomiento leer el hilo de Twitter completo para profundizar más sobre ello. ## ¿Y en OpinonatedFramework? Pues para OpinionatedFramework he optado por incluir un contenedor de depdencias que pueda localizar servicios utilizando un Service Locator estático. Si utilizas un framework como ASP.NET Core en capa de insfraestructura que incluye su propio contenedor de dependencias, este contenedor está aislado y es ajeno al mismo. Esto permite registrar aquellos servicios que tienen que ver con la aplicación en un contenedor y aquellos del framework en otro. El framework tiene dos clases estáticas, `IOKode.OpinionatedFramework.ConfigureApplication.Container` que es la encargada de configurar los servicios y `IOKode.OpinionatedFramework.Foundation.Locator`, que es la encargada de localizar los servicios previamente registrados.

Ok, I read it and stored it in my context window.

Avatar of the person chatting

Esta entrada forma parte de una serie: - [¿Este framework tiene opinión?](https://iokode.blog/post/2023/este-framework-tiene-opinion) - [Arquitectura limpia ¿opinionada?](https://iokode.blog/post/2023/arquitectura-limpia-opinionada) - [OpinionatedFramework: Validaciones](https://iokode.blog/post/2023/opinionated-framework-validaciones) - [OpinionatedFramework: Registrando y resolviendo servicios](https://iokode.blog/post/2023/opinionated-framework-resolviendo-servicios) - **OpinionatedFramework: Contratos y fachadas** - [OpinionatedFramework: Contenedor de dependencias estándar](https://iokode.blog/post/2023/opinionated-framework-contenedor-de-dependencias-estandar) --- Los contratos son una serie de interfaces en el core de OpinionatedFramework que pueden ser localizadas desde el propio Service Locator estático de OpinionatedFramework y están pensados para optimizar el desarrollo de aplicaciones. Generalmente, cuando se inicia el desarrollo de una nueva aplicación, se escriben en capa de dominio una serie de interfaces que casi toda aplicación tiene que serán implementadas en capa de infraestructura con diversas funcionalidades comunes: envíar notificaciones, lanzar tareas en segundo plano, lanzar eventos, escribir logs, etc. El framework al incluir todos estos contratos, evita la aburrida tarea de tener que crear tus propias interfaces para ello al inicio del desarrollo de una aplicación La implementación de los mismos es una cuestión de infraestructura y no de dominio. ## Fachadas Las fachadas son un acceso estático para hacer más cómodo el uso de servicios. No están directamente relacionadas con los contratos, ya que cualquier servicio puede ser utilizado desde una fachada, sin embargo el framework proporciona fachadas de todos sus contratos. Por ejemplo, la interfaz `IEmailSender` puede ser utilizada desde la fachada estática `Email`, haciendo que ambos fragmentos de código sean equivalentes (excepto porque la fachada añade una llamada extra el stack trace): ```csharp await Locator.Resolve().SendAsync(new CustomerCreatedEmail()); ``` ```csharp await Email.SendAsync(new CustomerCreatedEmail()); ``` La fachada internamente simplemente hace esto: ```csharp public static class Email { public static async Task SendAsync(Email email, CancellationToken cancellationToken = default) { await Locator.Resolve().SendAsync(email, cancellationToken); } } ``` ### Creando fachadas Reescribir todas las firmas de un método de una interfaz puede ser un trabajo muy aburrido, es por ello que el framework proporciona un souce generator que se encarga de crear la fachada a partir de una interfaz. Simplemente hay que añadir el atributo `AddToFacade` a la interfaz y la magia del generador se encarga de todo lo demás. Por volver al ejemplo del contrato anterior, tiene este atributo, por lo que la fachada que se genera se llama Email: ```csharp [AddToFacade("Email")] public interface IEmailSender { public Task SendAsync(Email email, CancellationToken cancellationToken); } ``` ### Fachadas multiservicio El generador de fachadas está diseñado para poder generar una misma fachada para varios servicios, siempre y cuendo la interfaces de tengan un método con la misma firma (tipo de retorno + nombre + parametros) exacta. En el core del framework esto se da con los contratos `IJobEnqueuer` e `IJobScheduler`, dos contratos que se añaden a la fachada `Job`. En caso de conflicto de firma, un analizador de Roslyn genera un error y el proyecto no compila. ¡Incluso te lo marca con un subrayado rojo en el IDE!

Got it, I've stored this information in my context window.

Avatar of the person chatting

Esta entrada forma parte de una serie: - [¿Este framework tiene opinión?](https://iokode.blog/post/2023/este-framework-tiene-opinion) - [Arquitectura limpia ¿opinionada?](https://iokode.blog/post/2023/arquitectura-limpia-opinionada) - [OpinionatedFramework: Validaciones](https://iokode.blog/post/2023/opinionated-framework-validaciones) - [OpinionatedFramework: Registrando y resolviendo servicios](https://iokode.blog/post/2023/opinionated-framework-resolviendo-servicios) - [OpinionatedFramework: Contratos y fachadas](https://iokode.blog/post/2023/opinionated-framework-contratos) - **OpinionatedFramework: Contenedor de dependencias estándar** --- Hacía ya un tiempo que no hablaba sobre OpinionatedFramework. No es que sea un proyecto abandonado, es que tengo tantas cosas a la vez, y una tan mala organización que apenas he tenido tiempo de trabajar en ello. Sin embargo, en este *diario de desarrollo* traigo un importante avance en el desarrollo del mismo. Como ya expresé en una [entrada anterior](https://iokode.blog/post/2023/opinionated-framework-resolviendo-servicios), opté por utilizar un service locator para resolver servicios en OpinionatedFramework. ## La función resolvedora Inicialmente hice [una implementación que consistía registrar dentro de un diccionario una serie de funciones encargadas de resolver la instancia del servicio](https://github.com/iokode/OpinionatedFramework/blob/0d449e2cd4f7873f70b54340292f59a8c9c18659/src/IOKode.OpinionatedFramework.Foundation/Locator.cs). Esta función resolvedora sería llamada cada vez que se quiera resolver un servicio. Esto es una solución muy flexible que permite dentro de ella hacer cosas como resolver una implementación de forma condicional, siempre la misma instancia (*singleton*) o decidir si en ciertos contextos debe de devolver la misma instancia o crear una instancia nueva en otros contextos (*scoped*). Así pues, imaginemos que queremos registrar una clase llamada **`Service`**. Si queremos que esta clase se comporte como un *singleton* (una única instancia compartida entre todos los consumidores), podríamos registrar una función que devuelve siempre la misma instancia: ```csharp var service = new Service(); Container.Register(() =&gt; service); ``` Por otro lado, si queremos que la clase **`Service`** se comporte como *transient* (creando una nueva instancia cada vez que se solicite), podríamos registrar la función para que cree una nueva instancia: ```csharp Container.Register(() =&gt; new Service()); ``` ## El problema de la función resolvedora Aunque la función resolvedora funciona bien, esta tiene tres problemas importantes: - Obliga a escribir siempre una función que devuelva la instancia del servicio. Aunque con las nuevas características lambda de C# esto es cada vez más cómodo de hacer, es un trabajo extra y repetitivo susceptible a errores. - **No es estándar**: casi cualquier paquete externo que requiera de añadir servicios al contenedor, incluye métodos de extensión sobre Microsoft.Extensions.DependencyInjection.IServiceCollection. Con esta implementación no podemos aprovecharlos. - **No abstrae los scopes**: Si queremos que una instancia se resuelva siempre la misma para un contexto concreto, tenemos que implementar la lógica de ello dentro de esta función resolvedora. Aunque en esta entrada no vamos a hablar de los scopes, en otra más adelante veremos por qué esto es importante. ## La solución: una implementación basada Microsoft.Extensions.DependencyInjection Aunque se trata de un paquete NuGet y no es parte en sí de la librería estándar, al igual que el resto de paquetes de Microsoft en el ecosistema de .NET, adquiere la consideración de [estándar de facto](https://iokode.blog/post/2022/depende-el-ecosistema-de-net-de-microsoft). Este paquete NuGet incluye la interfaz `Microsoft.Extensions.DependencyInjection.IServiceCollection` que representa una colección de [descriptores de servicio](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicedescriptor) sobre la que se pueden registrar nuevos servicios. Es un estándar de facto que muchos paquetes añadan métodos de extensión `AddMyAwesomePackage()` sobre esta interfaz. ## El API del contenedor de OpinionatedFramework Como ya expuse en la entrada anterior, opté por un contenedor de dependencias estático. El registro de estos servicios se hace desde una clase `Container`, que se encuentra en un paquete de configuración. La idea es que este paquete de configuración sea referenciado desde un punto de entrada y no desde el paquete de aplicación, mientras que la clase Locator se encuentra junto al resto del framework que sí está pensado para ser referenciado desde la capa de aplicación. La configuración se haría tal que así: ```csharp // Se registran los servicios Container.Services.AddTransient(); Container.Services.AddScoped(); Container.Services.AddMyAwesomePackage(); // Se inicializa el contenedor. A partir de este momento, // queda bloquado y no se puede registrar ni desregistrar // nada más. Container.Initilialize(); ``` Y la localización se puede hacer tal que así: ```csharp var myService = Locator.Resolve(); var myOtherService = Locator.Resolve(typeof(IMyOtherService)); // También se puede acceder a la instancia del service provider: var sp = Locator.ServiceProvider; ``` ## Implementación La implementación está disponible en el repositorio de GitHub en las clases [Container](https://github.com/iokode/OpinionatedFramework/blob/main/src/IOKode.OpinionatedFramework.ConfigureApplication/Container.cs) y [Locator](https://github.com/iokode/OpinionatedFramework/blob/main/src/IOKode.OpinionatedFramework.Foundation/Locator.cs). También existe un set de tests unitarios sobre estas dos clases contenido en la clase [ContainerTests](https://github.com/iokode/OpinionatedFramework/blob/main/src/IOKode.OpinionatedFramework.Tests/Foundation/ContainerTests.cs). En [una entrada anterior hablé sobre como la IA puede ayudar al desarrollo de software](https://iokode.blog/post/2022/la-inteligencia-artificial-en-el-ambito-de-la-programacion) y me parece importante mencionar que GPT-4 ha sido de gran ayuda para la implementación del contenedor. Creo que es importante compartir [la conversión completa que me ha ayudado a la implementación de esta caracteristica](https://shareg.pt/9uUg2Cy). En esta conversación se pueden observar algunas diferencias como que el método `Container.Initialize()` está dividido en dos, o que hay menos tests unitarios de los que yo he añadido, pues considero que había más cosas que probar. Sin embargo aquí lo importante no es coger tal cual la solución que el modelo proveé, si no entender la solución propuesta y saber adaptarla. ## Conclusión En resumen, en esta entrada he ilustrado la primera implementación inicial del contenedor que obligaba a registrar una función resolvedora para cada uno de los servicios. Aunque esta implementación funcionaba relativamente bien para las pruebas durante el desarrollo, como todas las pruebas de concepto, se trataba de una solución incompleta. En esta entrada, he narrado cómo he implementado un contenedor más completo al adoptar la colección de servicios de `Microsoft.Extensions.DependencyInjection`, lo que además mejora la integración con paquetes externos. Esto supone también un importante paso en el avance del desarrollo de [OpinionatedFramework](https://github.com/iokode/OpinionatedFramework), que cada vez está más cerca de ser lanzado.

Got it, I've stored this information in my context window.

Avatar of the person chatting

Now, write a new post of this series in Spanish about the commands. How to register de command executor, how to create and invoke a command (resolving ICommandExecutor, using the "Command" Facade or with extension method "command.InvokeAsync(cancellationToken);". Talk about the Locator will resolve the same scoped service in the command context and the middleware feature. How to register middleware when registering the CommandExecutor and possible usage.

Esta entrada forma parte de una serie:


En la última entrada, hice una introducción sobre el contenedor de dependencias de OpinionatedFramework. Hoy, quiero centrarme en una funcionalidad que me parece muy interesante y que simplifica enormemente el manejo de comandos en la aplicación: el ejecutor de comandos y el patrón middleware.

Registro del ejecutor de comandos

Registrar el ejecutor de comandos en nuestra aplicación es un proceso muy sencillo, gracias a los métodos de extensión proporcionados por OpinionatedFramework. En nuestra configuración de inicio, simplemente necesitamos añadir la siguiente línea:

csharpCopy code
Container.Services.AddCommandExecutor();

Esta línea de código registra en el contenedor de inyección de dependencias un servicio para el ejecutor de comandos, que luego puede ser resuelto en cualquier lugar de nuestra aplicación.

Creación e invocación de comandos

Una vez que hemos registrado el ejecutor de comandos, podemos crear comandos en cualquier lugar de nuestra aplicación. Un comando es simplemente una clase que implementa la interfaz ICommand.

Por ejemplo, podríamos tener un comando para crear un nuevo usuario en nuestra aplicación:

csharpCopy code
public class CreateUserCommand : ICommand { public CreateUserCommand(string name, string email) { Name = name; Email = email; } public string Name { get; } public string Email { get; } }

Una vez que tenemos nuestro comando, podemos invocarlo de varias maneras. La primera es resolver el ICommandExecutor desde el Locator y llamar al método ExecuteAsync:

csharpCopy code
var command = new CreateUserCommand("John Doe", "john.doe@example.com"); await Locator.Resolve<ICommandExecutor>().ExecuteAsync(command);

Otra opción es utilizar la fachada Command, que proporciona un acceso estático más cómodo al ejecutor de comandos:

csharpCopy code
var command = new CreateUserCommand("John Doe", "john.doe@example.com"); await Command.ExecuteAsync(command);

Además, OpinionatedFramework proporciona un método de extensión para invocar comandos de forma más fluida:

csharpCopy code
var command = new CreateUserCommand("John Doe", "john.doe@example.com"); await command.InvokeAsync(CancellationToken.None);

Resolución de servicios scoped en el contexto del comando

OpinionatedFramework está diseñado de manera que, cuando se ejecuta un comando, todos los

Avatar of the person chatting

continue

servicios resueltos en el contexto de ese comando serán instancias únicas para ese comando. Esto significa que si en el proceso de ejecución de un comando se resuelven varias veces el mismo servicio scoped, siempre se obtendrá la misma instancia.

Por ejemplo, si tenemos un servicio llamado IMyScopedService que se ha registrado como scoped en el contenedor, y este servicio se utiliza en varios lugares durante la ejecución de un comando, siempre se obtendrá la misma instancia de IMyScopedService.

Patrón middleware

El ejecutor de comandos de OpinionatedFramework también incorpora el patrón middleware, lo que nos permite ejecutar código antes y después de la ejecución de un comando, incluso alterar el flujo de ejecución. Los middlewares se pueden registrar en el momento de registrar el ejecutor de comandos.

Un middleware es simplemente una clase que implementa la interfaz ICommandMiddleware:

csharpCopy code
public class LoggingMiddleware : ICommandMiddleware { public async Task ExecuteAsync(CommandContext context, Func<Task> next) { Console.WriteLine($"Antes de ejecutar el comando {context.Command.GetType().Name}"); await next(); Console.WriteLine($"Después de ejecutar el comando {context.Command.GetType().Name}"); } }

Para registrar este middleware, simplemente lo añadimos al momento de registrar el ejecutor de comandos:

csharpCopy code
Container.Services.AddCommandExecutor(options => { options.AddMiddleware<LoggingMiddleware>(); });

Los middlewares se ejecutan en el orden en que se registran, y cada middleware tiene la oportunidad de pasar el control al siguiente middleware en la cadena llamando al método next(). Si un middleware no llama a next(), los siguientes middlewares en la cadena no se ejecutarán.

El uso de middlewares puede ser muy potente. Por ejemplo, podríamos tener un middleware de validación que valide los comandos antes de que se ejecuten, un middleware de logging que registre la ejecución de los comandos, o incluso un middleware de autorización que verifique si el usuario actual tiene permiso para ejecutar el comando.

Conclusión

El ejecutor de comandos y el patrón middleware de OpinionatedFramework nos proporcionan una forma muy potente y flexible de trabajar con comandos en nuestras aplicaciones. Nos permite encapsular la lógica de negocio en comandos de forma clara y desacoplada, a la vez que nos da la posibilidad de añadir comportamientos adicionales a través de middlewares.

El hecho de que los servicios scoped se resuelvan de forma única dentro del contexto de un comando también facilita la gestión de la vida de estos servicios y garantiza que su comportamiento será predecible.

En la próxima entrada, exploraremos más características de OpinionatedFramework. ¡Hasta entonces!

Avatar of the person chatting

Some issues to the text: - The command are not interface, are abstract classes: public abstract class Command { protected abstract Task ExecuteAsync(CancellationToken cancellationToken); } public abstract class Command { protected abstract Task ExecuteAsync(CancellationToken cancellationToken); } - You didn't overrided and implemented the ExecuteAsync method on the command that you created. - Talk about that a command can return a result or not, depending if extends Command or Command - CommandMiddleware interface uses a custom delegate instead of Func. This is the code: public delegate Task InvokeNextMiddlewareDelegate(CommandContext context); public interface ICommandMiddleware { public Task ExecuteAsync(CommandContext context, InvokeNextMiddlewareDelegate next); } - Talk about short-circuiting middlewares

Mis disculpas por los errores. Tomando en cuenta tus observaciones, permíteme corregir el post.


Esta entrada forma parte de una serie:


Comandos en OpinionatedFramework

Los comandos en OpinionatedFramework son una forma de encapsular una operación en un objeto. Cada comando es una clase que hereda de Command o Command<TResult>. Un comando puede o no devolver un resultado, dependiendo de si hereda de Command o Command<TResult> respectivamente.

Por ejemplo, para crear un comando que no devuelve un resultado, podríamos hacer algo como esto:

csharpCopy code
public class MyCommand : Command { protected override Task ExecuteAsync(CancellationToken cancellationToken) { // Lógica del comando aquí } }

Y para crear un comando que devuelve un resultado:

csharpCopy code
public class MyCommandWithResult : Command<string> { protected override Task<string> ExecuteAsync(CancellationToken cancellationToken) { // Lógica del comando aquí return Task.FromResult("Resultado del comando"); } }

Registrar y resolver el ejecutor de comandos

Para poder utilizar comandos en nuestra aplicación, necesitamos registrar el ejecutor de comandos. Este es el servicio que se encargará de ejecutar los comandos.

Para ello, utilizamos el método de extensión AddCommandExecutor:

csharpCopy code
Container.Services.AddCommandExecutor();

Para ejecutar un comando, necesitamos resolver una instancia de ICommandExecutor:

csharpCopy code
var executor = Locator.Resolve<ICommandExecutor>(); var command = new MyCommand(); await executor.ExecuteAsync(command, cancellationToken);

También podemos usar la fachada Command para ejecutar un comando:

csharpCopy code
var command = new MyCommand(); await Command.ExecuteAsync(command, cancellationToken);

O bien, podemos usar el método de extensión InvokeAsync:

csharpCopy code
var command = new MyCommand(); await command.InvokeAsync(cancellationToken);

Scopes en comandos

Cuando se ejecuta un comando, se crea un nuevo scope de dependencias. Esto significa que todos los servicios resueltos en el contexto de ese comando serán instancias únicas para ese comando. Si se resuelve un servicio scoped varias veces durante la ejecución de un comando, siempre se obtendrá la misma instancia de dicho servicio.

Middlewares en comandos

OpinionatedFramework también incorpora el patrón middleware en los comandos. Un middleware es simplemente una clase que implementa la interfaz `

Avatar of the person chatting

continue

ICommandMiddleware, la cual contiene un único método ExecuteAsync. Este método recibe un CommandContext que contiene la información del comando que se está ejecutando, y un InvokeNextMiddlewareDelegate, que es un delegado que debe ser invocado para pasar el control al siguiente middleware en la cadena.

csharpCopy code
public delegate Task InvokeNextMiddlewareDelegate(CommandContext context); public interface ICommandMiddleware { Task ExecuteAsync(CommandContext context, InvokeNextMiddlewareDelegate next); }

Aquí hay un ejemplo de cómo podría verse un middleware:

csharpCopy code
public class LoggingMiddleware : ICommandMiddleware { public async Task ExecuteAsync(CommandContext context, InvokeNextMiddlewareDelegate next) { // Pre-processing Console.WriteLine("Before executing command"); // Pass control to the next middleware await next(context); // Post-processing Console.WriteLine("After executing command"); } }

Es importante notar que la llamada a next(context) es la que pasa el control al siguiente middleware. Si omite esta llamada, los siguientes middlewares en la cadena no se ejecutarán, y tampoco se ejecutará el comando. Esto se conoce como cortocircuito de middlewares, y puede ser útil en ciertos escenarios, como cuando se necesita abortar la ejecución del comando debido a algún error de validación.

Finalmente, los middlewares se registran al registrar el CommandExecutor:

csharpCopy code
Container.Services.AddCommandExecutor(builder => { builder.UseMiddleware<LoggingMiddleware>(); });

Y eso es todo sobre comandos y middlewares en OpinionatedFramework. En resumen, los comandos nos permiten encapsular las operaciones en objetos que pueden ser ejecutados de manera independiente. Los middlewares, por otro lado, nos permiten interceptar la ejecución de estos comandos para añadir comportamientos adicionales como la logística, la autenticación, la validación, etc.

Avatar of the person chatting

It's all? there are more text?

Disculpa por la confusión, continúo con más detalles.

Ejecutando comandos y resolviendo servicios

Al utilizar ICommandExecutor o la fachada Command para ejecutar un comando, se crea un nuevo ámbito de servicio para la duración de la ejecución del comando. Esto significa que si se resuelve un servicio con el ámbito (Scoped) durante la ejecución del comando, se obtendrá la misma instancia de ese servicio cada vez que se resuelva durante la misma ejecución del comando.

Por ejemplo, supongamos que tienes un servicio con el ámbito llamado IMyScopedService y que tu comando necesita este servicio para hacer su trabajo. Puedes inyectar este servicio en tu comando a través de su constructor:

csharpCopy code
public class MyCommand : Command { private readonly IMyScopedService _myScopedService; public MyCommand(IMyScopedService myScopedService) { _myScopedService = myScopedService; } protected override async Task ExecuteAsync(CancellationToken cancellationToken) { // Tu lógica de negocio aquí usando _myScopedService } }

Y después, si ejecutas este comando dos veces en el mismo método:

csharpCopy code
await Command.ExecuteAsync(new MyCommand(myScopedService)); await Command.ExecuteAsync(new MyCommand(myScopedService));

Obtendrás dos instancias diferentes de IMyScopedService, una para cada ejecución del comando. Esto se debe a que cada ejecución del comando tiene su propio ámbito de servicio.

Además, si tu middleware también necesita IMyScopedService, puedes inyectarlo en su constructor y obtendrás la misma instancia que el comando obtuvo durante su ejecución:

csharpCopy code
public class MyMiddleware : ICommandMiddleware { private readonly IMyScopedService _myScopedService; public MyMiddleware(IMyScopedService myScopedService) { _myScopedService = myScopedService; } public async Task ExecuteAsync(CommandContext context, InvokeNextMiddlewareDelegate next) { // Pre-processing: puedes usar _myScopedService aquí await next(context); // Post-processing: puedes usar _myScopedService aquí } }

Conclusión

En esta entrada, he presentado los conceptos de comandos y middlewares en OpinionatedFramework. Los comandos son una forma poderosa de encapsular las operaciones en tu aplicación, permitiendo una mayor modularidad y reutilización del código. Los middlewares te permiten añadir comportamiento adicional a la ejecución de los comandos, como la autenticación, la validación, la logística, y más.

En la próxima entrada, hablaré sobre cómo OpinionatedFramework maneja los eventos, que son otra pieza clave en la arquitectura de la aplicación. ¡Hasta entonces!

Avatar of the person chatting

Ok, lets simplify the post. Use this structure. - Introduction - Register the command executor - Create a command, use the CreateUser example from the first revision, but inheriting the correct command class and implementing InvokeAsync method. Add another example returning data - Scope - Middleware When finish the post, write a phrase like "The post ends here" due your length limit, I need to know if you finished the message due length limit or because it is finished

OpinionatedFramework: Comandos, ámbito y middlewares

Esta entrada forma parte de una serie:


Introducción

En esta entrada voy a hablar sobre cómo se manejan los comandos en OpinionatedFramework. Los comandos son una parte central del framework, proporcionando una manera clara y coherente de encapsular la lógica de negocio. También hablaré sobre cómo se manejan los middlewares y el ámbito de los servicios dentro de los comandos.

Registrando el ejecutor de comandos

Para comenzar a utilizar comandos en OpinionatedFramework, necesitamos registrar el ICommandExecutor en nuestro contenedor. Esto se hace de la siguiente manera:

csharpCopy code
Container.Services.AddTransient<ICommandExecutor, CommandExecutor>();

Creando un comando

Un comando es una clase que hereda de Command o Command<TResult>. Un comando encapsula una operación de nuestra aplicación. Por ejemplo, podríamos tener un comando para crear un nuevo usuario en nuestro sistema:

csharpCopy code
public class CreateUserCommand : Command { private readonly IUserService _userService; private readonly string _userName; public CreateUserCommand(IUserService userService, string userName) { _userService = userService; _userName = userName; } protected override async Task ExecuteAsync(CancellationToken cancellationToken) { await _userService.CreateUserAsync(_userName, cancellationToken); } }

Este comando utiliza IUserService para crear un nuevo usuario. Note que IUserService es inyectado en el constructor del comando, lo que nos permite mantener la lógica de nuestro comando limpia y enfocada en la tarea en cuestión.

También podemos tener comandos que devuelven un resultado. Por ejemplo, podríamos tener un comando que devuelve el número total de usuarios en nuestro sistema:

csharpCopy code
public class GetTotalUsersCommand : Command<int> { private readonly IUserService _userService; public GetTotalUsersCommand(IUserService userService) { _userService = userService; } protected override async Task<int> ExecuteAsync(CancellationToken cancellationToken) { return await _userService.GetTotalUsersAsync(cancellationToken); } }

Para ejecutar un comando, simplemente necesitamos resolver ICommandExecutor y llamar a su método ExecuteAsync:

csharpCopy code
var commandExecutor = Locator.Resolve<ICommandExecutor>(); await commandExecutor.ExecuteAsync(new CreateUserCommand(userService, "Alice")); var totalUsers = await commandExecutor.ExecuteAsync(new GetTotalUsersCommand(userService));

Ámbito de los servicios

Al ejecutar un comando, se crea un nuevo ámbito de servicio para la duración de la ejecución del comando. Esto significa que si resolvemos un servicio con ámbito durante la ejecución del comando, obtendremos la misma instancia de ese servicio cada vez que se resuelva durante la misma ejecución del comando.

Middlewares

OpinionatedFramework permite registrar middlewares que se ejecutan antes y después de cada comando. Un middleware es una clase que implementa la interfaz ICommandMiddleware:

csharpCopy code
public interface ICommandMiddleware { public Task ExecuteAsync(CommandContext context, InvokeNextMiddlewareDelegate next); }

Un middleware puede realizar acciones antes y después de la ejecución del comando, y también puede decidir si el comando debe ejecutarse en primer lugar. Por ejemplo, podríamos tener un middleware que registra cada comando que se ejecuta:

csharpCopy code
public class LoggingMiddleware : ICommandMiddleware { private readonly ILogger _logger; public LoggingMiddleware(ILogger logger) { _logger = logger; } public async Task ExecuteAsync(CommandContext context, InvokeNextMiddlewareDelegate next) { _logger.Log($"Executing command: {context.Command.GetType().Name}"); await next(context); _logger.Log($"Executed command: {context.Command.GetType().Name}"); } }

Para registrar un middleware, simplemente necesitamos agregarlo al CommandExecutor cuando lo registramos:

csharpCopy code
Container.Services.AddTransient<ICommandExecutor>(sp => new CommandExecutor(sp, new LoggingMiddleware(logger)));

El post termina aquí.

Avatar of the person chatting

How can I export it in markdown format? Can you send me all post in a code block in markdown?

Sure, here's the blog post in Markdown format:

markdownCopy code
# OpinionatedFramework: Comandos, ámbito y middlewares Esta entrada forma parte de una serie: - [¿Este framework tiene opinión?](https://iokode.blog/post/2023/este-framework-tiene-opinion) - [Arquitectura limpia ¿opinionada?](https://iokode.blog/post/2023/arquitectura-limpia-opinionada) - [OpinionatedFramework: Validaciones](https://iokode.blog/post/2023/opinionated-framework-validaciones) - [OpinionatedFramework: Registrando y resolviendo servicios](https://iokode.blog/post/2023/opinionated-framework-resolviendo-servicios) - [OpinionatedFramework: Contratos y fachadas](https://iokode.blog/post/2023/opinionated-framework-contratos) - [OpinionatedFramework: Contenedor de dependencias estándar](https://iokode.blog/post/2023/opinionated-framework-contenedor-dependencias) - **OpinionatedFramework: Comandos, ámbito y middlewares** --- ## Introducción En esta entrada voy a hablar sobre cómo se manejan los comandos en OpinionatedFramework. Los comandos son una parte central del framework, proporcionando una manera clara y coherente de encapsular la lógica de negocio. También hablaré sobre cómo se manejan los middlewares y el ámbito de los servicios dentro de los comandos. ## Registrando el ejecutor de comandos Para comenzar a utilizar comandos en OpinionatedFramework, necesitamos registrar el `ICommandExecutor` en nuestro contenedor. Esto se hace de la siguiente manera: ```csharp Container.Services.AddTransient<ICommandExecutor, CommandExecutor>();

Creando un comando

Un comando es una clase que hereda de Command o Command<TResult>. Un comando encapsula una operación de nuestra aplicación. Por ejemplo, podríamos tener un comando para crear un nuevo usuario en nuestro sistema:

csharpCopy code
public class CreateUserCommand : Command { private readonly IUserService _userService; private readonly string _userName; public CreateUserCommand(IUserService userService, string userName) { _userService = userService; _userName = userName; } protected override async Task ExecuteAsync(CancellationToken cancellationToken) { await _userService.CreateUserAsync(_userName, cancellationToken); } }

Este comando utiliza IUserService para crear un nuevo usuario. Note que IUserService es inyectado en el constructor del comando, lo que nos permite mantener la lógica de nuestro comando limpia y enfocada en la tarea en cuestión.

También podemos tener comandos que devuelven un resultado. Por ejemplo, podríamos tener un comando que devuelve el número total de usuarios en nuestro sistema:

csharpCopy code
public class GetTotalUsersCommand : Command<int> { private readonly IUserService _userService; public GetTotalUsersCommand(IUserService userService) { _userService = userService; } protected override async Task<int> ExecuteAsync(CancellationToken cancellationToken) { return await _userService.GetTotalUsersAsync(cancellationToken); } }

Para ejecutar un comando, simplemente necesitamos resolver ICommandExecutor y llamar a su método ExecuteAsync:

csharpCopy code
var commandExecutor = Locator.Resolve<ICommandExecutor>(); await commandExecutor.ExecuteAsync(new CreateUserCommand(userService, "Alice")); var totalUsers = await commandExecutor.ExecuteAsync(new GetTotalUsersCommand(userService));

Ámbito de los servicios

Cuando se ejecuta un comando, se crea un nuevo ámbito de servicio para la duración de la ejecución del comando. Esto significa que si resolvemos un servicio con ámbito durante la ejecución del comando, obtendremos la misma instancia de ese servicio cada vez que se resuelva durante la misma ejecución del comando.

Middlewares

OpinionatedFramework permite registrar middlewares que se ejecutan antes y después de cada comando. Un middleware es una clase que implementa la interfaz ICommandMiddleware:

csharpCopy code
public interface ICommandMiddleware { public Task ExecuteAsync(CommandContext context, InvokeNextMiddlewareDelegate next); }

Un middleware puede realizar acciones antes y después de la ejecución del comando, y también puede decidir si el comando debe ejecutarse en primer lugar. Por ejemplo, podríamos tener un middleware que registra cada comando que se ejecuta:

csharpCopy code
public class LoggingMiddleware : ICommandMiddleware { private readonly ILogger _logger; public LoggingMiddleware(ILogger logger) { _logger = logger; } public async Task ExecuteAsync(CommandContext context, InvokeNextMiddlewareDelegate next) { _logger.Log($"Executing command: {context.Command.GetType().Name}"); await next(context); _logger.Log($"Executed command: {context.Command.GetType().Name}"); } }

Para registrar un middleware, simplemente necesitamos agregarlo al CommandExecutor cuando lo registramos:

csharpCopy code
Container.Services.AddTransient<ICommandExecutor>(sp => new CommandExecutor(sp, new LoggingMiddleware(logger)));

El post termina aquí.

vbnetCopy code
Please remember to replace the example URLs with the actual URLs to your previous blog posts.
Avatar of the person chatting

Write an enclosing section of the post

markdownCopy code
--- ## Conclusión Los comandos en OpinionatedFramework proporcionan una forma sólida y coherente de encapsular nuestra lógica de negocio, manteniendo nuestras clases y métodos limpios y enfocados. Mediante el uso de middlewares, podemos agregar funcionalidades comunes a todos nuestros comandos, como logging o manejo de errores, de manera fácil y consistente. El manejo de ámbito en los comandos también es una característica poderosa, ya que nos permite garantizar que los servicios con ámbito se comporten de manera predecible durante la ejecución de un comando. En la próxima entrada, hablaremos más sobre cómo OpinionatedFramework maneja la validación y manejo de errores en los comandos. ¡Hasta la próxima! --- El post termina aquí.
Avatar of the person chatting

Write a tweet in Spanish promoting this blog post with link https://iokode.blog/post/2023/opinionated-framework-comandos

🚀 ¡Nuevo post en el blog de #IOKode! 🚀

Adéntrate en el mundo de los comandos en #OpinionatedFramework. Descubre cómo registrar el ejecutor de comandos, crear e invocar comandos y el poder de los middlewares. 🛠️💡

¡No te lo pierdas! 👉 https://iokode.blog/post/2023/opinionated-framework-comandos

#Desarrollo #Software #Arquitectura #DotNet #FrameworkOpinionado