Asc.Utils.Commander 1.0.0

dotnet add package Asc.Utils.Commander --version 1.0.0
                    
NuGet\Install-Package Asc.Utils.Commander -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Asc.Utils.Commander" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Asc.Utils.Commander" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Asc.Utils.Commander" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Asc.Utils.Commander --version 1.0.0
                    
#r "nuget: Asc.Utils.Commander, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Asc.Utils.Commander@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Asc.Utils.Commander&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Asc.Utils.Commander&version=1.0.0
                    
Install as a Cake Tool

Asc.Utils.Commander

Utility for simplifying the use of the command pattern. It includes concepts from the unit of work pattern but without data persistence. It allows commands to be managed as reusable objects in memory. It simplifies the management of all commands executed by a given command processor, such as what to execute in case of success or failure.

The commands generated by this utility can be processed sequentially or simultaneously. Works with both synchronous and asynchronous delegates.

For each command you can define:

  • Unique identifier (mandatory).
  • Parameters that are accessible by all delegates associated with the command.
  • The job you want the command to execute (mandatory).
  • What should be executed if the job runs successfully.
  • What should be executed if the job execution fails and generates an exception of a certain type.
  • What should be executed if the job execution fails and generates an exception of any type
  • What should be executed when the job completes, whether successfully or not.

For each command processor you can define:

  • What should be executed immediately before the commands job.
  • What should be executed when a command job is executed successfully.
  • What should be executed when a command job fails and throws an exception of a certain type.
  • What should be executed when a command job fails and throws an exception of any type (mandatory).
  • What should be executed when a command's job completes, whether successfully or not.
  • Set if commands are processed sequentially or simultaneously (mandatory).
  • Set the maximum number of commands that can be processed simultaneously.

With these options, this utility is especially useful for implementing metrics, logs, or statistics. It forces the developer to think of a command as an atomic unit of code. It improves exception handling by unifying it in the command processor definition.

Installation

Install-Package Asc.Utils.Commander

Usage

Metrics, logs and exception management example

  public void AppStart(IAppBuilder appBuilder)
  {
    //Some other code

    ICommandProcessor commandProcessor = Commander.Instance.GetConcurrentCommandProcessorBuilder()
      .SetMaxThreads(10)
      .OnBeforeAnyJob((ICommand command) =>
      {
        IEventTracker tracker = GetTrackerFromCommand(command);
        logger.LogInfo($"Running {command.Id}");

        if (tracker is null)
          return

        TrackCommandParameters(tracker, GetOptionalParametersFromCommand(command));
        tracker.Start();
      })
      .OnAnyJobFailure((TimeoutException ex, IExecutedCommand command) =>
      {
        logger.LogWarning($"Operation {command.Id} failed due to timeout");
        GetTrackerFromCommand(command)?.TrackException(ex);
      })
      //This is mandatory
      .OnAnyJobFailure((Exception ex, IExecutedCommand command) =>
      {
        logger.LogException(ex, $"{command.Id} failed. Check exception");
        GetTrackerFromCommand(command)?.TrackException(ex);
      })
      .OnAfterAnyJob(IExecutedCommand command) =>
      {
        if (command.CommandResult == ExecutedCommandResult.Succeeded)
          logger.LogInfo($"Command {command.Id} executed. Elapsed time: {command.JobElapsedTime}");
        else
          logger.LogWarning($"Command {command.Id} execution failed");

        GetTrackerFromCommand(command)?.End();
      }
      .Build();

    appBuilder.Services.RegisterInstance<ICommandProcessor>(commandProcessor);

    //Some other code
  }

  private IEventTracker GetTrackerFromCommand(ICommand command)
  {
    if (command is null || command.Parameters is null
      || !command.Parameters.ContainsKey(Constants.TRACKER_PARAM_KEY))
    {
    return null;
    }

    return command.Parameters[Constants.TRACKER_PARAM_KEY].OfType<IEventTracker>();
  }

  private IEnumerable<KeyValuePair<string, string>> GetOptionalParametersFromCommand(ICommand command)
  {
    if (command is null || command.Parameters is null || !command.Parameters.Any())
      return;

    return command.Parameters
      .Where(it => it.Key != Constants.TRACKER_PARAM_KEY)
      .Select(it => new KeyValuePair<string, string>(it.Key, it.Value.OfType<string>()));
  }

  private void TrackCommandParameters(IEventTracker tracker, Dictionary<string, string> parameters)
  {
    if (tracker is null || parameters is null || !parameters.Any())
      return;

    foreach (KeyValuePair<string, string> parameter in parameters)
      tracker.AddParameter(parameter.Key, parameter.Value);
  }

  // ...
  //Later, somewhere in you app
  // ...

  public void PurchaseProducts()
  {
    ICommandProcessor commandProcessor = App.Services.Resolve<ICommandProcessor>();

    //Products will be purchased in parallel
    //because this command processor was created as a concurrent one
    foreach (Product product in currentShoppingCart.Products)
        commandProcessor.ProcessCommand(GetCommandToPurchaseProduct(product));
  }

  private ICommand GetCommandToPurchaseProduct(Product product)
  {
    string commandId = "Product.Purchase";
    IProductProcessor productProcessor = App.Services.Resolve<IProductProcessor>();
    IToastService toastService = App.Services.Resolve<IToastService>();
    ITrackingService trackingService = App.Services.Resolve<ITrackingService>();

    return Commander.Instance.GetCommandBuilder<PurchasedProduct>()
      .Job(async () => return await productProcessor.PurchaseProduct(product)) //This is mandatory
      .OnSuccess((PurchasedProduct purchasedProduct) =>
        toastService.ShowToast(
          $"Purchased {purchasedProduct.Amount} items of {purchasedProduct.Name} product"
        )
      )
      .OnFailure((InsufficientStockException ex) =>
      {
        string message = $"Cannot purchase {product.Amount} items of {product.Name} since there is only {ex.ProductStockSize} in stock";
        toastService.ShowWarningToast(message);
      })
      .OnFailure((Exception _) =>
        toastService.ShowWarningToast($"Product {product.Name} purchase failed. Try it later"))
      .AddOrReplaceParameter(Constants.TRACKER_PARAM_KEY, trackingService.BuildTrackerForEvent(commandId))
      .AddOrReplaceParameter("productId", product.Id)
      .AddOrReplaceParameter("amount", product.Amount)
      .SetId(commandId) //This is mandatory
      .Build();
  }

UI Commands

  public void AppStart(IAppBuilder appBuilder)
  {
    //Some other code

    ICommandProcessor uiCommandProcessor = Commander.Instance.GetSequentialCommandProcessorBuilder()
      .OnBeforeAnyJob(() =>
      {
        navigationService.CurrentPage.IsBusy = true;
        navigationService.CurrentPage.ShowSpinner();
      })
      //This is mandatory
      .OnAnyJobFailure((Exception ex, IExecutedCommand command) =>
        logger.LogException(ex, $"{command.Id} failed. Check exception"))
      .OnAfterAnyJob(() =>
      {
        navigationService.CurrentPage.HideSpinner();
        navigationService.CurrentPage.IsBusy = false;
      })
      .Build();

    appBuilder.Services.RegisterInstance<ICommandProcessor>(commandProcessor);

    //Some other code
  }

  // ...
  //Later, somewhere in you app
  // ...

  private ICommand? purchaseProductsCommand;

  private void ShoppingCartPurchaseButton_Clicked()
  {
    if (IsBusy)
      return;

    ICommandProcessor commandProcessor = App.Services.Resolve<ICommandProcessor>();
    ICommand command = GetPurchaseProductsCommand();

    //Commands will be processed one by one
    //since the used command processor has been created as a sequential one.
    commandProcessor.ProcessCommand(command);
  }

  //Purchase cart products have always same logic so you can reuse an ICommand instance
  private ICommand GetPurchaseProductsCommand()
  {
    if (purchaseProductsCommand is not null)
      return purchaseProductsCommand;

    purchaseProductsCommand = Commander.Instance.GetCommandBuilder()
      .Job(() => shoppingCartService.PurchaseProducts())
      .OnFailure((Exception _) => toastService.ShowWarningToast("Something failed. Try it later"))
      .SetId("Cart.PurchaseItems")
      .Build();

    return purchaseProductsCommand;
  }

Bad practices

  • Calling ICommandProcessor.ProcessCommand recursively from within a command's job that will be processed by the same command processor.
  • Do not propagate an exception that occurs within a command's job. This way, no delegates registered for error cases will be invoked.

More info

See public interfaces summaries:

  • ICommand
  • IExecutedCommand
  • ICommandBuilder
  • ICommandBuilder<>
  • ICommander
  • ICommandParameter
  • ICommandProcessor
  • ISequentialCommandProcessorBuilder
  • IConcurrentCommandProcessorBuilder
  • ICommandProcessorBuilder

to get more info about how to use this utility.

Icon from Flaticon:

<a href="https://www.flaticon.com/free-icons/soldier" title="soldier icons">Soldier icons created by imaginationlol - Flaticon</a>

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net9.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 254 3/17/2025

net9.0 version