Creating custom Tasks

How to create a custom Task using C#.

Frends fully supports creating your own Task packages. To do this you must create a .NET class library which is then wrapped in a NuGet package file and finally imported into Frends through the Tasks admin page.

Creating custom Tasks is as easy as cloning or installing the .NET 8 Task template, which gives you a ready to go Visual Studio solution. Once done with development, the project can be packed to a NuGet package and imported to Frends. You can use your prefered C# IDE, such as Visual Studio, VS Code or Rider.

This guide will take you through steps and specifics on how to make a functional Frends Task.

Requirements

You will need to have .NET SDK installed, at minimum .NET SDK 8.0 is required.

Frends Tasks are usually written in C#, and for the best experience you will want a compatible integrated development environment (IDE). Some common examples are Visual Studio, VS Code and JetBrains Rider. You can also use any text editor and the dotnet command-line interface.

Some IDEs allow you to install the template to the project wizard but can always use dotnet new command when that is not possible.

Obtaining & Setting up the Template

Check out the instructions on how to get the template set up at .NET 8 Task template GitHub page.

Task Development Instructions

When creating a class library for a Frends Task, the Task should be implemented as a public static method with a return value. Non-static methods or methods with no return value (void) cannot be used as Tasks. The methods also cannot be overloaded, e.g. you cannot have Frends.TaskLibrary.CreateFile(string filePath) and Frends.TaskLibrary.CreateFile(string filePath, bool overwrite).

Task parameters

All parameters specified for the method will be used as Task parameters. If the parameter is of class type, it will be initialized as a structure.

For example:

using System.ComponentModel;

namespace Frends.TaskLibrary 
{ 
    /// <summary>
    /// File action type (nothing/delete/rename/delete)
    /// </summary>
    public enum ActionType 
    {
        /// <summary>
        /// Nothing is done to the file
        /// </summary>
        Nothing,

        /// <summary>
        /// File will be deleted
        /// </summary>
        Delete,
        /// <summary>
        /// File will be renamed
        /// </summary>
        Rename,

        /// <summary>
        /// File will be moved
        /// </summary>
        Move
    }

    /// <summary>
    /// File class
    /// </summary>
    public class File
    {
        /// <summary>
        /// File path
        /// </summary>
        [DefaultValue("\"C:\\Temp\\myFile.json\"")]
        public string Path { get; set; }

        /// <summary>
        /// Maximum size of the file
        /// </summary>
        [DefaultValue("0")]
        public int MaxSize { get; set; }


        /// <summary>
        /// Password for unlocking the file
        /// </summary>
        [PasswordPropertyText]
        public string Password { get; set; }
    }

    /// <summary>
    /// FileAction class defines what will be done to the file
    /// </summary>
    public class FileAction
    {
        /// <summary>
        /// Action to be done with the file
        /// </summary>
        public ActionType Action { get; set; }

        /// <summary>
        /// If ActionType is Move or Rename then To is the path to be used
        /// </summary>
        [DefaultValue("\"\"")]
        public string To { get; set; }
    }

    public static class Files 
    {
        /// <summary>
        /// DoFileAction task does the desired action to file
        /// </summary>
        /// <param name="file">File to handle</param>
        /// <param name="action">Action to perform</param>
        /// <returns>Returns information if task was successful</returns>
        public static string DoFileAction(File file, FileAction action)
        {
            // TODO: change logic
            return $"Input values. Path: '{file.Path}', Max size: '{file.MaxSize}', Action: '{action.Action}', To: '{action.To}'";
        }
    }
}

In case of a complex or large parameter structure you can use custom attributes from ComponentModel library. Use assets in System.ComponentModel and System.ComponentModel.DataAnnotations namespaces to specify how the parameters are shown in the UI.

Default value: DefaultValueAttribute

Task parameters may use the DefaultValueAttribute to provide a default value which is shown in the editor, remember that the parameters are expressions in the editor and default values need to be provided as such, e.g. "true" for a boolean value, "\"C:\Temp\\"" for a string containing a filePath.

Sensitive information not to be logged: PasswordPropertyTextAttribute

Also, if a parameter should not be logged, the PasswordPropertyTextAttribute should be added. The value of the parameter will be replaced with << Secret >> in the log. Parameters may have a more complex hierarchical structure, we recommend using at most only two levels of hierarchy.

Optional inputs: UIHintAttribute

[UIHint(nameof(Property),"", conditions: object[]]

Show or hide editor inputs based on the value of other inputs.

Example:

public bool Rename { get; set; }
[UIHint(nameof(Rename),"", true)]
public string NewFileName { get; set; }

The NewFileName field will only be visible if the Rename property has the value true.

public FileEnum FileOptions { get; set; }
[UIHint(nameof(FileOptions),"", FileEnum.Rename, FileEnum.CreateNew)]
public string NewFileName { get; set; }

The NewFileName field will only be visible if the FileOptions choise is either Rename or CreateNew

Default editor type: DisplayFormatAttribute

[DisplayFormat(DataFormatString = "")]

Sets the default editor input type. The parameter input editor will try to use this format when e.g. filling out new task parameters.

Possible values for DataFormatString are:

  • Json

  • Text

  • Xml

  • Sql

  • Expression

Example:

[DisplayFormat(DataFormatString = "Sql")]
public string Query { get; set; }

Tabbed parameter panels: PropertyTabAttribute

[PropertyTab]

Group parameters as tabs

Example:

public static bool Delete([PropertyTab] string fileName, [PropertyTab] OptionsClass options)

Customize Task discovery

By adding a FrendsTaskMetadata.json file to the root of the NuGet package, unwanted static methods can be skipped by listing only methods which are wanted as Tasks. For example the following JSON structure would only cause the DoFileAction to be considered as a Task:

{
    "Tasks": [
        {
            "TaskMethod": "Frends.TaskLibrary.FileActions.DoFileAction"
        }
    ]
}

XML Documentation

Custom Tasks can also be commented/documented in the code by using XML Documentation comments. These comments will show up in the Process Task editor automatically if the documentation XML file is included inside the Task NuGet (if the nuget Id is Frends.TaskLibrary then a file Frends.TaskLibrary.xml will be searched).

Generation of this file can be done automatically by enabling Build/Ouput/ XML documentation from Visual Studio for example. When the comments are being queried, the Task parameter definition is checked first and if this is not found then the type definition will be checked.

Packaging Task as NuGet package

Task libraries are distributed as NuGet packages (.nupkg). The assembly name and package Id must be identical, e.g. Frends.TaskTemplate.dll and Frends.TaskTemplate.1.0.0.0.nupkg.

Usually a Task can be packed using command dotnet pack , but sometimes when working with legacy code, you need to use nuget.exe and .nuspec files to pack the Task.

Last updated

Was this helpful?