How to contribute - neronotte/Greg.Xrm.Command GitHub Wiki

This article contains general guidelines for contributors.

  1. You cannot write directly into the master branch. Commit to the master branch requires a pull request and an approval by one of the project administrators.
  2. Dev branch names must follow the pattern feature\<feature-name-with-dashes-between-words>.
  3. When submitting a pull request, the pull request comments must be meaningful and should describe the content of the pull request in terms of intent. If any existing command is impacted by the pull request, it should be explicitly stated.

How to create a new command

Creating a new command is quite easy. Commands are stored in the Greg.Xrm.Command.Core project, under the Commands folder. The subfolders of the Command folder reflects the first level of the command verb tree. Below the subfolders of the Commands folder as per 2023-11-28:

  • Auth
  • Column
  • Help
  • Relationship
  • Solution
  • Table

If you are planning to create a whole new command namespace, add a new folder. Otherwise peek the folder of your choice.

The command class

A command is represented by a POCO class that describes your command arguments:

public class DoSomethingCommand
{
}

Command class name must end with Command, and must be decorated with the [Command] attribute to specify:

  • The verbs the command must respond to
  • The text to be provided when user asks for help on the command

In the following example, command can be invoked via pacx do something:

[Command("do", "something", HelpText="this command does something")]
public class DoSomethingCommand
{
}

Tipically, your command may have options. Options can be defined using c# properties, decorated with the [Option] attribute.

[Command("do", "something", HelpText="this command does something")]
public class DoSomethingCommand
{
    [Option("name", "n", HelpText = "The name of the thing to do")]
    public string Name { get; set; }
}

In the Option attribute you must specify:

Argument Req? Description
longName mandatory, implicit the long name of the command option. This is the name that will be used via -- prefix
shortName mandatory, implicit the short name of the command option. This is the name that will be used via - prefix. It should be no longer than 4 chars.
HelpText optional, explicit the text to be shown when the user asks for help on the command. Is the same text that is written in the command docs.
DefaultValue optional, explicit a static value to be used when the user doesn't provide any value for the current option.
SuppressValuesHelp optional useful only for enum type properties. Instructs the help generation infrastructure service to not to automatically expose all the supported values of the enum. Useful when not all the enum values are valid for your command.

Required arguments must be decorated with the Required attribute found in System.ComponentModel.DataAnnotations namespace. The actual presence of the required arguments in the command line provided by the user is automatically checked by the commanding infrastructure. There's no actual need to check for required arguments manually in the command code.

[Command("do", "something", HelpText="this command does something")]
public class DoSomethingCommand
{
    [Option("name", "n", HelpText = "The name of the thing to do")]
    [Required]
    public string Name { get; set; }
}

PLEASE NOTE: All validators from the System.ComponentModel.DataAnnotations namespace are supported and managed by the commanding infrastructure. For complex validations, the suggested approach is to use the Validation Attributes provided by the above namespace, and/or implement IValidatableObject in your command class. Avoid adding validation logic in you Command Executor class, unless you need to rely on external services for your validations (e.g. if you need to validate an info against Dataverse data).

The command executor class

Once you created the command definition, is time to think about command business logic. Command business logic must be wrapped in a class called command executor. A few guidelines:

  • The command executor must be placed in the same folder of the command.
  • The command executor class must match the name of the command class, + "executor" suffix (e.g. DoSomethingCommand --> DoSomethingCommandExecutor).
  • The command executor must implement ICommandExecutor<CommandType> interface.
public class DoSomethingCommandExecutor : ICommandExecutor<DoSomethingCommand>
{
    public async Task<CommandResult> ExecuteAsync(DoSomethingCommand command, CancellationToken cancellationToken)
    {
        ...
    }
}

Command executors are activated via Dependency Injection pipelines, so any dependency required by your command should be injected via constructor parameters. 2 commond dependencies that are used by all commands are:

  • IOutput: interface that must be used to write something in the output stream of the command.
  • IOrganizationServiceRepository: interface that must be used to access the connection to the CRM.

Important!: Don't write directly using Console.Write(), always use IOutput to write to the output stream.

This means that, most of the time, your command executor should look like this:

public class DoSomethingCommandExecutor : ICommandExecutor<DoSomethingCommand>
{
    private readonly IOutput output;
    private readonly IOrganizationServiceRepository organizationServiceRepository;

    public DeleteCommandExecutor(
        IOutput output, 
        IOrganizationServiceRepository organizationServiceRepository)
    {
        this.output = output;
        this.organizationServiceRepository = organizationServiceRepository;
    }

    public async Task<CommandResult> ExecuteAsync(DoSomethingCommand command, CancellationToken cancellationToken)
    {
        this.output.Write($"Connecting to the current dataverse environment...");
        var crm = await this.organizationServiceRepository.GetCurrentConnectionAsync();
        this.output.WriteLine("Done", ConsoleColor.Green);

        /* ... your logic here ... */

        return CommandResult.Success();
    }
}

Command result management

The ExecuteAsync method returns a CommandResult instance. That object can be used to return values from the command (e.g., table create command returns the table ID and the primary attribute ID), or to instruct the commanding infrastructure that something wrong happened.

If you want to return values for the command, you can use a dictionary-like approach:

var result = CommandResult.Success();
result["Table ID"] = response.EntityId;
result["Primary Column ID"] = response.AttributeId;
return result;

Otherwise, if you need to return an Exception, you can do the following:

try
{
   ...do something...
}
catch(Exceptio ex)
{
    return CommandResult.Fail(ex.Message, ex);
}

Please note: The commanding infrastructure will print out automatically the command result values and/or the exceptions, you don't need to do any specific logic to printout those values in your command logic.

Now you're ready to enjoy writing commands!