Skip to main content
Skip table of contents

How to develop Link 3 artifacts

This article describes how to develop artifacts to Link 3.

Prerequisites

  • Visual Studio 2022

  • .NET 6

Nuget

Nuget-packages for Link 3 are placed in a public devops project here:

https://bizbrains-productteam.visualstudio.com/Link3Public/_artifacts/feed/Link3Public

Follow the link above to see the repository and click the “Connect to Feed” button to see setup instruction for numerous different tools.

Create a new nuget-source in visual studio using the this link: https://bizbrains-productteam.pkgs.visualstudio.com/Link3Public/_packaging/Link3Public/nuget/v3/index.json

It should look like this:

Notice! At the time of writing only pre-release packages are available, so it’s necessary to check the “Include prerelease” checkbox:

Otherwise no packages will be shown.

Bizbrains.Link.Base

This package contains all interfaces and base-classes needed to develop Link Artifacts.

Bizbrains.Link.Base.Implementations

This package contains some implementations from the base-package. Eg. an implementation of ILinkMessage, Exceptions, etc. It also contains the LinkPropertyAccessor implementation, which is used to access context properties in a strongly-types way.

Bizbrains.Link.Repositories

Contains all the repositories in Link. These repositories can be used to communicate with the database. A repository in Link is characterized by only handling a single entity. And an entity typically represents a table in the database.

Bizbrains.Link.Services

Services that contains business-logic are placed in this package.

Interfaces / base classes

The Bizbrains.Link.Base nuget package contains all needed interfaces used to develop artifacts for Link.
Below image is a class diagram for all base interfaces and abstract base classes.

Typically artifacts should be implemented using the abstract base classes and not the interfaces directly. This is because the base-classes provides some “convenience functionality” which makes it easier to implement an artifact for Link. The base classes also use generics to tell the type of the configuration-class for the artifact.

Flow base-classes

The following list is the base-classes needed when implementing what we call “flow-steps”. Flow-steps is implementations that can be hooked in different places in the flow (see Link 3 architecture - TODO: Make architecture drawing).

Retrieve Steps

Usage: (S)Ftp polling, Http polling, etc.
Base class: TransportRetrieveBase<T>

Initialize Steps

Usage: Pre-disassembler, disassembler, post-disassembler
Base class: InitializeStepBase<T>

Itinerary Steps

Usage: Transformation, validation, all kind of logic (previously implemented in Biztalk Pipelines)
Base class: ItineraryStepBase2<T>
Note: The original ItineraryStepBase<T> has been flagged as Obsolete and will be removed in the future, so it is important to use the new base class.

Send Steps

Usage: All kind of transport
Base class: TransportSendBase<T>

Outbox Steps

Usage: Used to place documents in outbox
Base class: TransportOutboxBase<T>

Duplicate Check Steps

Usage: Selects a specific value from the payload that the duplicate check framework should run against as part of the Initialize flow.
Base class: DuplicateCheckBase2<T>

Note: The original DuplicateCheckBase<T> has been flagged as Obsolete and will be removed in the future, so it is important to use the new base class.

Format Type Probe Steps

Usage: Validates that the payload format is of a specific type. E.g. XML
Base class: LinkFormatTypeProbeBase2<T>

Note: The original LinkFormatTypeProbeBase<T> has been flagged as Obsolete and will be removed in the future, so it is important to use the new base class.

Test Tool base-classes

The following list is the base-classes used for the test tool.

Compare Plugin

Usage: Have two responsibilities.
First is to prepare the stream for comparison. This could be based on the config of the plugin. Example is to handle ID’s or timestamps that are generated on run time.
Second is to compare the two streams.

Base class: TestToolCompareBase<T>

Tracking Action base-classes

The following is the base-class used for tracking actions.

Tracking UI Action

Usage: Have two responsibilities.
First is to validate the selected document or documents. Documents that are not in a final state will automatically be rejected, but the plugin can also reject documents based on its own requirements.
Second is to perform the actual action, which is also entirely up to the plugin.

The base class take two configuration classes. The first is for static configuration, that is configured when the button is configured; the second is runtime configuration, which the user will supply when activating the button.

Base class: TrackingActionBase<TConfig, TRunTimeConfig>

Dependency Injection

In Link 3 we use dependency injection. This means that we will always work on interfaces and not on the actual implementations. Therefore you will find that only interfaces, entities and models are public in our code.

If you are not familiar with dependency injection, it’s highly recommended that you get some basic knowledge before developing artifacts for Link 3.

You can find some information here: https://docs.microsoft.com/en-us/dotnet/core/extensions/dependency-injection

ILinkStep registration

All classes that implement (or derive from classes that implement) ILinkStep must be registered with DI as transient services. To ensure backwards compatibility, any such class that is registered as a scoped service is automatically (and silently) changed to a transient service. Any attempt to register an ILinkStep as a singleton results in an exception being thrown.

Singletons

While it is possible to register any service class, except ILinkStep’s, as a singleton, it is not supported and should be avoided because it will cause memory leaks and eventually containers will crash and restart.

Context properties

Note that only simple types in the value-field are supported (string, int, double, bool, etc.).

If complex types are added to the property-bag it will most likely throw an exception, when the message is serialized to the queue.

LinkPropertyAccessor

The LinkPropertyAccessor is used to access context-properties in a strongly-typed way. The constructor of the LinkPropeLinkPropertyAccessor takes an ILinkMessage or an ILinkPropertyBag, and then you can access all known properties in Link.This is a replacement for the xml-schemas used in Biztalk and Link 2.

The accessor provides methods for getting and setting values in context-properties without knowing the namespace and key for the property. It also provides methods for getting a value, which must exist - and throws a well-defined exception if the property doesn’t exist.

Example code:

C#
var accessor = new LinkPropertyAccessor(linkMessage);
// Optional use of property
if (accessor.PartnerIn.IdentificationValue.HasValue)
{
  var partnerInIdentification = accessor.PartnerIn.IdentificationValue.Value;
  // Do what's needed...
}

// Set context-property:
accessor.PartnerIn.IdentificationValue.Value = "123456789";
accessor.PartnerIn.IdentificationQualifier.Value = "GLN";

// Mandatory use of property
var partnerInIdentification = accessor.PartnerIn.IdentificationValue.ValueMandatory; // This will throw an exception if the distribution-id has not been set

See code examples to see more code.

Custom context properties

Via the ILinkPropertyBag it’s possible to add and maintain custom context-properties. There is no accessor for maintaining these properties, so you must cast the value yourself or use the generic-typed method.

Example code:

C#
var propertyBag = linkMessage.Properties;

// Set custom property
propertyBag.Add("MyProperty", "http://mycompany/link/myproperty", "MyPropertyValue");
// or
propertyBag["MyProperty", "http://mycompany/link/myproperty", false] = "MyPropertyValue";

// Get value
var myValue = (string)propertyBag["MyProperty", "http://mycompany/link/myproperty"];
// or
var myvalue = propertyBag.GetValue<string>("MyProperty", "http://mycompany/link/myproperty");

Readonly properties

The ILinkPropertyBag offers the possibility of setting readonly properties. This means that the property can only be set once - and after that it’s immutable across the entire flow.

Example code:

C#
var propertyBag = linkMessage.Properties;

// Set readonly property
propertyBag["MyProperty", "http://mycompany/link/myproperty", true] = "MyPropertyValue";

// Try setting it again - will result in an exception of type LinkPropertyBagReadOnlyFieldException:
propertyBag["MyProperty", "http://mycompany/link/myproperty", false] = "MyPropertyValue";

Artifact Configuration Classes

Artifacts can via configuration-classes expose properties that will be reflected in the Link UI.

So if your artifacts for example needs an URL which is required that the users types in, when configuring your artifact, you can simply create a property-class with an URL property inside.

Should I use LinkConfigBase or ILinkConfig?

The artifact configuration-framework contains two types of configurations:

  • Artifacts that support retries

    • Use the LinkConfigBase as your base class
      The areas that support retries are: Initialize, Itinerary and Send

  • Artifacts that doesn’t support retries

    • Use the ILinkConfig as your base class/interface
      The areas for this is Retrieve steps

Configuration attributes

There is a set of attributes, which can be used to control the apperance of the properties. See list below.

LinkDisplay

Show properties
  • Name (The label of the presentation-field)

  • Description (The tooltip of the presentation-field)

  • Order (The order inside the group)

  • GroupName (The name of the presentation-group which this fields belongs to. If blank it will be set to “Settings”)

LinkStringLength

Show properties
  • MaximumLength (The maximum length of the input-field)

LinkRequired

Show properties

No properties

Mark the presentation-field as required

LinkRegularExpression

Show properties
  • Pattern (The regular expression used on the input-field)

  • ErrorMessage (The message to show if the pattern doesn’t match the inputted value)

LinkDependsOn

Show properties

This attribute can be used, if some properties depends on the value chosen in other properties. If the Value of the given PropertyName doesn’t match, the presentation-field will be hidden for the user - and all other validation for the field will be disabled.

Properties:

  • PropertyName (The name of the property this field is dependant of. Use the nameof keyword to get the name of the property instead of providing a string)

  • PropertyValues (The values that will make the field show in the UI)

LinkArray

Show properties

This attribute can be used to show an dynamic list of elements. These elements can consist of multiple fields and are defined in code as a class with a number of properties decorated with Link* attributes. E.g. LinkDisplay, LinkRequired

Properties:

  • MinElements (The minimum number of items this array should hold)

  • MaxElements (The maximum number of items this array can hold)

LinkDefaultValue

Show properties
  • Value (The default value of the presentation-field)

LinkPassword

Show properties

No properties

Shows the presentation-field as a password-field

LinkArtifactAssembly

Show properties
  • AssemblyType (The type of the assembly to choose: TransportRetrieve, TransportSend, ItineraryStep, InitStep, Dependency, IntegratedFtpProvider)

LinkArtifactXml

Show properties
  • XmlType (The type of the xml to choose: Schema, Xslt)

LinkEnum

Show properties

This attribute can only be set on enum-fields.

  • DisplayName (The name to show in the dropdown. The value will remain the enum-field-name)

LinkChoice

Show properties

This attribute can be used to show a dropdown with dynamic values. Instead of using the enum-type, it’s here possible runtime to create a list of choices that can populated in a dropdown.

Properties:

  • IConfigAttributeChoiceServiceType (The type of the implementation that provides the key/value pairs to show in the dropdown. The given type must implement the IConfigAttributeChoiceService interface)

Code example

The following code shows different way of using the attributes:

Show code
C#
public class HTTPStepConfig : LinkConfigBase
{
      [LinkRequired]
      [LinkDefaultValue(nameof(RestCommands.POST))]
      [LinkDisplay(Name = "REST Command", Description = "", GroupName = "HTTP Settings", Order = 10)]
      public RestCommands RestCommand { get; set; }

      [LinkRequired]
      [LinkDisplay(Name = "Endpoint URL", Description = "", GroupName = "HTTP Settings", Order = 11)]
      public string EndpointUrl { get; set; }

      [LinkDisplay(Name = "Header: Content-Type", Description = "", GroupName = "HTTP Settings", Order = 12)]
      public string ContentType { get; set; }

      [LinkDisplay(Name = "Header: Accept", Description = "", GroupName = "HTTP Settings", Order = 13)]
      public string Accept { get; set; }

      [LinkRequired]
      [LinkDisplay(Name = "Connection Timeout", Description = "", GroupName = "HTTP Settings", Order = 14)]
      [LinkDefaultValue("30")]
      public int? Timeout { get; set; }

      [LinkRequired]
      [LinkDefaultValue(nameof(AuthenticationType.Basic))]
      [LinkDisplay(Name = "Authentication Type", Description = "", GroupName = "HTTP Settings - Authentication", Order = 20)]
      public AuthenticationType AuthenticationTypes { get; set; }

      [LinkRequired]
      [LinkDisplay(Name = "Username", Description = "", GroupName = "HTTP Settings - Authentication", Order = 21)]
      [LinkDependsOn(nameof(AuthenticationTypes), nameof(AuthenticationType.Basic))]
      public string BasicUsername { get; set; }

      [LinkRequired]
      [LinkPassword]
      [LinkDisplay(Name = "Password", Description = "", GroupName = "HTTP Settings - Authentication", Order = 22)]
      [LinkDependsOn(nameof(AuthenticationTypes), nameof(AuthenticationType.Basic))]
      public string BasicPassword { get; set; }

      [LinkRequired]
      [LinkDisplay(Name = "Api Key", Description = "", GroupName = "HTTP Settings - Authentication", Order = 25)]
      [LinkDependsOn(nameof(AuthenticationTypes), nameof(AuthenticationType.ApiKey))]
      public string ApiKeyName { get; set; }

      [LinkRequired]
      [LinkDisplay(Name = "Api Key Value", Description = "", GroupName = "HTTP Settings - Authentication", Order = 26)]
      [LinkDependsOn(nameof(AuthenticationTypes), nameof(AuthenticationType.ApiKey))]
      public string ApiKeyValue { get; set; }

      [LinkDisplay(Name = "Api Key in URL", Description = "If checked the Api Key will be in the URI instead of the headers", GroupName = "HTTP Settings - Authentication", Order = 27)]
      [LinkDependsOn(nameof(AuthenticationTypes), nameof(AuthenticationType.ApiKey))]
      public bool ApiSetKeyInUri { get; set; }
}

The above will result in the following UI when used in a TransportSend<T> implementation:

The group “General” is generated because the configuration class inherits from LinkConfigBase, which have these properties.

LinkChoice attribute - populate dynamic values runtime

This attribute can be used to show a dropdown with dynamic values. Instead of using the enum-type, it’s here possible runtime to create a list of choices that can populated in a dropdown.

Examples of usage:

Simple static list of choices:

C#
public class MyItineraryStepConfig : LinkConfigBase 
{
    [LinkChoice(typeof(MyConfigService))] // Set the type that implements the IConfigAttributeChoiceService interface
    public string MyChoice { get; set; }
}

// Implementation of the IConfigAttributeChoiceService interface that just returnes a list of static values
public class MyConfigService : IConfigAttributeChoiceService
{
    public Task<IEnumerable<ConfigAttributeChoiceItem>> GetChoices(LinkChoiceAttribute attribute)
    {
        var choices = new List<ConfigAttributeChoiceItem>();
        choices.Add(new ConfigAttributeChoiceItem { Key = "1", DisplayName = "Item 1" });
        choices.Add(new ConfigAttributeChoiceItem { Key = "2", DisplayName = "Item 2" });
        choices.Add(new ConfigAttributeChoiceItem { Key = "3", DisplayName = "Item 3" });
        choices.Add(new ConfigAttributeChoiceItem { Key = "4", DisplayName = "Item 4" });
        choices.Add(new ConfigAttributeChoiceItem { Key = "5", DisplayName = "Item 5" });
        choices.Add(new ConfigAttributeChoiceItem { Key = "6", DisplayName = "Item 6" });

        return Task.FromResult(choices.AsEnumerable());
    }
}

Use a service to provide choices:

C#
public class MyItineraryStepConfig : LinkConfigBase 
{
    [LinkDisplay(Name = "Partner")]
    [LinkChoice(typeof(MyConfigPartnerService))]
    public int PartnerId { get; set; }
}

// Implementation of the IConfigAttributeChoiceService interface that loads all partners and convert them to choices
public class MyConfigPartnerService : IConfigAttributeChoiceService
{
    private readonly IPartnerSearchRepository partnerRepository;

    public MyConfigPartnerService(IPartnerSearchRepository partnerRepository) // Inject the partner-search-repository
    {
        this.partnerRepository = partnerRepository;
    }

    public async Task<IEnumerable<ConfigAttributChoiceItem>> GetChoices()
    {
        // Load all partners
        var partners = await partnerRepository.Search(DataAccessRestrictionMode.None, "", false);
        // Convert partners to choice-items with the partner-id as key and a concatenation of the partner-name and main-identification as display name.
        var choices = partners.PartnerSearchEntities.Select(p => new ConfigAttributChoiceItem { Key = p.Id.ToString(), DisplayName = $"{p.Name} ({p.MainIdentificationName}: {p.MainIdentificationValue})" });
        return choices;
    }
}

Call an external service to provide choices:

C#
public class MyItineraryStepConfig : LinkConfigBase 
{
    [LinkDisplay(Name = "Products")]
    [LinkChoice(typeof(MyConfigExternalService))]
    public int ProductId { get; set; }
}

// Implementation of the IConfigAttributeChoiceService interface that loads a list of products from an external service
public class MyConfigExternalService : IConfigAttributeChoiceService
{
    public MyConfigExternalService() { }

    public async Task<IEnumerable<ConfigAttributChoiceItem>> GetChoices()
    {
        var client = new HttpClient();
        var result = await client.GetAsync("https://dummyjson.com/products");
        var jsonStream = await result.Content.ReadAsStreamAsync();
        var productResult = await JsonSerializer.DeserializeAsync<ProductResult>(jsonStream);
        var choices = productResult.Products.Select(p => new ConfigAttributChoiceItem { Key = p.Id.ToString(), DisplayName = $"{p.Title} ({p.Price})" });
        return choices;
    }

    private class ProductResult
    {
        [JsonPropertyName("products")]
        public Product[] Products { get; set; }
    }

    private class Product
    {
        [JsonPropertyName("id")]
        public int Id { get; set; }
        [JsonPropertyName("title")]
        public string Title { get; set; }
        [JsonPropertyName("price")]
        public double Price { get; set; }
    }
}

LinkArray attribute

This attribute can be used to show a list of elements consisting of one or more fields. The user is allowed to add or remove elements. The number of elements can be restricted in code using the MinElements and MaxElements properties.

Examples of usage:
C#
public class MyItineraryStepConfig : LinkConfigBase 
{
    [LinkArray(MinElements = 1, MaxElements = 4)]
    [LinkDisplay(Name = "Custom data", Description = "Some custom data", GroupName = "My Settings", Order = 1)]
    public MyCustomData[] CustomData { get; set; }
}

public class MyCustomData
{
    [LinkRequired]
    [LinkDisplay(Name = "Name", Description = "Custom prop 1", Order = 1)]
    public string Name { get; set; }

    [LinkDisplay(Name = "Value", Description = "Custom prop 2", Order = 2)]
    public int Value { get; set; }
    
    [LinkArtifactXml(ArtifactXmlType.Schema, XmlType = ArtifactXmlType.Schema)]
    [LinkDisplay(Name = "Schema", Description = "Custom prop 3", Order = 3)]
    public Guid Schema { get; set; }
}

StepKeyName

All steps in Link inherits from the ILinkStep interface. And this interface has two properties:

  • ConfigType

  • StepKeyName

The ConfigType is the type of the configuration-object. This is typically implemented as a generic type in a derived base-class. Like: TransportRetrieveBase<T> where T will be set as the ConfigType via the base-class.

The StepKeyName is a string and must be a unique representation of the step. There are no validation of the name, but we highly recommend to use a standard way of naming your step.

For steps that are going via the MessageBroker (ItinerarySteps and SendTransportSteps) this name is used as the queue-name on the message-broker. Therefore it must be unique, so scaling can be done based on the steps.

Link comes with some system-artifacts, and the StepKeyName for these artifacts are prefixed with: “system-”. See System artifacts for more information about this.

For custom steps we recommend the following convention (all in lower-case):

“customer-area-subarea-logic”

Customer:
The name of the company that has developed the step

Area:
This could be: “initialize”, “itinerary”, “transport”

Subarea:
This might not always be necessary, but for transport-types it could be: “retrieve”, “send”, “outbox”

Logic
This is a value that represents the logic for the step. Be so specific as possible to prevent collision with other steps. Examples: “xml-validation”, “xslt-transform”, “split-payload”, etc. (TODO: Find better examples).

Complete examples:

  • “bizbrains-initialize-xml-disassembler”

  • “microsoft-itinerary-edifact”

  • “apple-transport-send-to-icloud”

  • “amazon-transport-retrieve-from-webservices”

ILinkFactory<T> and LinkFactoryBase<T>

All artifacts in Link 3 have the responsibility for creating the instance of the ILinkStep they implement.

This is done via the LinkFactoryBase<T> base class which implement the ILinkFactory<T> interface.

Example of an LinkFactoryBase usage:

C#
public class MyItineraryStepFactory : LinkStepFactoryBase<ILinkItineraryStep>
{
    public override void ConfigureServices(IServiceCollection serviceCollection)
    {
        // We add our step to the service-collection, because dependency injection will then resolve any services, repositories, logging and other stuff we expect in our constructor
        serviceCollection.AddTransient<MyItineraryStep>();
        // Add additional services, if you have any...
    }

    public override ILinkItineraryStep Create(IServiceProvider serviceProvider)
    {
        return serviceProvider.GetRequiredService<MyItineraryStep>();
    }
}

Logging

All logging in artifacts are done via the ILogger<T> interface, which is part of Microsoft.Extensions.Logging.

To get an ILogger<T> just put it in your constructor, and the dependency-injection will make sure you get an instance. Remember to use the type-specific ILogger<T> and not the generic ILogger interface.

Required packages: Microsoft.Extensions.Logging

Add serviceCollection.AddLogging(); in ConfigureServices(IServiceCollection serviceCollection) Method.

CODE
public class MyItineraryStep : ItineraryStepBase2<MyItineraryStepConfig>
{
    private readonly ILogger<MyItineraryStep> logger;

    public MyItineraryStep(ILogger<MyItineraryStep> logger)
    {
        this.logger = logger;
    }
}

Cancellation token

Cancellation tokens provide a graceful way to signal to the underlying framework that it needs to stop what it is doing. This could, for example, be because the system is shutting down. This gives you control over how you want you code to stop, even if it hasn’t finished it’s task, which can be particularly useful in situations where your code could end up being long running.

Many of our interfaces have methods that take a cancellation token. By default cancellation tokens are checked immediately before the actual implementation of an artifact is executed.

Error handling

LinkExceptionBase

In Link 3 there is a base exception class, that should be used: LinkExceptionBase
By using this exception it’s possible to give an error-code, so the error will be routed to the right stakeholder in the error handling engine.

LinkFlowException

For steps running in an engine that supports retry (Itinerary and Send) - it’s possible to control the behavior of the retry-mechanism by throwing this exception.

LinkFlowException supports stopping the retry, if it doesn’t make sense to retry. Eg. validation - if the document fails in validation, it will also fail when retrying.

CODE
throw new LinkFlowException("Validation error", *******, retryAllowed: false);

It’s also possible to give the document a specific status - eg. Ignored if this is needed:

CODE
throw new LinkFlowException("Validation error", *******, documentStatus: "Ignored", retryAllowed: false);

Link Services and Repositories

In Link 3 there are different layers of services.

Repositories

The lowest layer is what we call repositories. A repository communicates with the database and takes or returns entities (like Partners, Distributions, etc.).

In Link 2 and below we provide nested entities via the “Common” layer. Eg. a Distribution contains full Partner objects - and the Partner objects contains full Identification objects - and so on.

This is changed in Link 3. An entity has only one level of properties - and no complex types are allowed on these entities.

This means that if you have a distribution-id and want to lookup details on one of the partners, you need to first load the DistributionEntity via the IDistributionRepository, then you need to load the PartnerEntity via the IPartnerRepository based on the partner-id on the distribution.

Services

Services are a higher level than repositories, and services typically provides some kind of business-logic.
Eg. we consider all itinerary-, send-, polling-, etc.- steps as services, because they each represents some business logic.

Link 3 also comes with some centralized helper-services. These services can be found in the Bizbrains.Link.Services package. (TODO: Create a list of typically used services).

As everything in Link 3 each repository and service is available through an interface and not the direct implementation.

Code examples

Retrieve Steps

The following example is a RetrieveStep that downloads the content of the configured Url and save it to Inbox.

Used base class: TransportRetrieveBase<T>
Required packages: Bizbrains.Link.Base, Bizbrains.Link.Base.Implementations, Bizbrains.Link.Services

Show code
CODE
using Bizbrains.Link.Base;
using Bizbrains.Link.Base.ConfigAttributes;
using Bizbrains.Link.Services.Transaction.Tracking;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Link.Examples.Retrieve
{
    public class MyRetrieveStepFactory : LinkStepFactoryBase<ILinkTransportRetrieve>
    {
        public override string StepDisplayName => "My http retrieve";

        public override string StepDescription => "Retrieves a payload from the configured Url.";

        public override void ConfigureServices(IServiceCollection serviceCollection)
        {
            serviceCollection.AddTransient<MyRetrieveStep>();
        }

        public override ILinkTransportRetrieve Create(IServiceProvider serviceProvider)
        {
            return serviceProvider.GetRequiredService<MyRetrieveStep>();
        }
    }

    public class MyRetrieveStep : TransportRetrieveBase<MyRetrieveStepConfig>
    {
        private readonly ILogger<MyRetrieveStep> logger;
        private readonly ILinkMessageFactory linkMessageFactory;

        public MyRetrieveStep(ILogger<MyRetrieveStep> logger, ILinkMessageFactory linkMessageFactory)
        {
            this.logger = logger;
            this.linkMessageFactory = linkMessageFactory;
        }

        public override string StepKeyName => "bizbrains-transport-retrieve-myretrieve";

        protected override string GenerateDisplayAddress(MyRetrieveStepConfig config)
        {
            return "MyRetriever";
        }

        protected override async Task Retrieve(ILinkRetrieveProvider retrieveProvider, MyRetrieveStepConfig config, CancellationToken ct)
        {
            var httpClient = new HttpClient();
            var httpRequest = new HttpRequestMessage(HttpMethod.Get, config.Url);

            logger.LogInformation($"Calling {config.Url}");
            var response = await httpClient.SendAsync(httpRequest, ct);
            logger.LogInformation("Http call done - reading response...");

            var body = await response.Content.ReadAsStreamAsync();

            logger.LogInformation($"Response read. Length: {body.Length}. Creating LinkMessage...");
            var linkMessage = await linkMessageFactory.CreateMessage(body);

            logger.LogInformation("LinkMessage created. Save the message in Inbox");
            await retrieveProvider.SaveToInbox(linkMessage, new LinkRetrieveAdditionalInfo { FileName = "MyHttpResponse.txt" });
        }
    }

    public class MyRetrieveStepConfig : ILinkConfig
    {
        [LinkRequired]
        [LinkDisplay(Name = "Url", GroupName = "Http", Order = 1)]
        public string Url { get; set; }
    }
}

Initialize Steps

The following code is an example of an artifact implementing the InitializeStepBase<T> base class. it is just a simple passthrough:

Used base class: InitializeStepBase<T>

Required packages: Bizbrains.Link.Base, Bizbrains.Link.Base.Implementations

Click here to expand...
CODE
using Bizbrains.Link.Base;
using Bizbrains.Link.Services.Transaction.Tracking;
using Microsoft.Extensions.DependencyInjection;

namespace Bizbrains.Link.Initialize.InitializeSteps.Xml
{
    public class XmlDisassemblerFactory : LinkStepFactoryBase<ILinkInitializeStep>
    {
        public override string StepDisplayName => "Custom InitStep Xml Disassembler";

        public override string StepDescription => "Custom Xml disassembler";

        public override void ConfigureServices(IServiceCollection serviceCollection)
        {
            serviceCollection.AddTransient<XmlDisassembler>();
            base.ConfigureServices(serviceCollection);
        }

        public override ILinkInitializeStep Create(IServiceProvider serviceProvider)
        {
            return serviceProvider.GetRequiredService<XmlDisassembler>();
        }
    }

    public class XmlDisassembler : InitializeStepBase<XmlDisassemblerConfig>
    {
        private readonly ILinkMessageFactory linkMessageFactory;

        public override string StepKeyName => "Custom-initialize-xml";

        public XmlDisassembler(ILinkMessageFactory linkMessageFactory)
        {
            this.linkMessageFactory = linkMessageFactory;
        }

        public override async Task<IEnumerable<ILinkMessage>> Process(ILinkMessage message, XmlDisassemblerConfig config, CancellationToken cancellationToken)
        {
            return new List<ILinkMessage>() { await linkMessageFactory.CreateXmlMessage(message.Body.Data, message.Properties) };
        }
    }
}

Itinerary Steps

The following code is an example of an artifact implementing the ItineraryStepBase2<T> base class. The step can take an XPath and evaluate it against the payload, and promote the value to the configured property-namespace and property-name.

Used base class: ItineraryStepBase2<T>
Required packages: Bizbrains.Link.Base

Show code
C#
using Bizbrains.Link.Base;
using Bizbrains.Link.Base.ConfigAttributes;
using Bizbrains.Link.Base.Exceptions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Xml.Linq;
using System.Xml.XPath;

namespace Link.Examples.ItineraryStep
{
    public class MyItineraryStepFactory : LinkStepFactoryBase<ILinkItineraryStep2>
    {
        public override string StepDisplayName => "My itinerary step";
        public override string StepDescription => "This step extracts a value from an XPath and promotes the value to context-properties.";
        public override void ConfigureServices(IServiceCollection serviceCollection)
        {
            // We add our step to the service-collection, because dependency injection will then resolve any services, repositories, logging and other stuff we expect in our constructor
            serviceCollection.AddTransient<MyItineraryStep>();
            // Add additional services, if you have any...
        }

        public const string ErrorCodeXPath = "AHREU"; // Generate the error-code in the Link UI (Developer -> Error codes -> Generate code)
        public const string ErrorCodeUnexpectedError = "01W7K";
        public override IEnumerable<LinkErrorCode> RegisterErrorCodes()
        {
            var errorCodes = new List<LinkErrorCode>();
            errorCodes.Add(new LinkErrorCode(ErrorCodeXPath, LinkExceptionTypeArea.Itinerary, LinkExceptionCategory.MissingConfigurationError, "MyItinerary", "XPath doesnt' return any element.", "Contact developer"));
            errorCodes.Add(new LinkErrorCode(ErrorCodeUnexpectedError, LinkExceptionTypeArea.Itinerary, LinkExceptionCategory.UnexpectedError, "MyItinerary", "Something went wrong during the XPath promotion. This payload might not be valid XML.", "Verify that the payload is valid XML"));
            return base.RegisterErrorCodes();
        }

        public override ILinkItineraryStep2 Create(IServiceProvider serviceProvider)
        {
            return serviceProvider.GetRequiredService<MyItineraryStep>();
        }
    }

    public class MyItineraryStep : ItineraryStepBase2<MyItineraryStepConfig>
    {
        private readonly ILogger<MyItineraryStep> logger;

        public MyItineraryStep(ILogger<MyItineraryStep> logger)
        {
            this.logger = logger;
        }

        public override string StepKeyName => "bizbrains-itinerary-mystep";

        protected override Task<ILinkMessage> Execute(ILinkMessage linkMessage, MyItineraryStepConfig config, CancellationToken cancellationToken)
        {
            logger.LogInformation($"Calling execute on {GetType()}");
            try
            {
                var xDocument = XDocument.Load(linkMessage.Body.Data);
                var xElement = xDocument.XPathSelectElement(config.PromotionXpath) ?? throw new LinkException($"The XPath {config.PromotionXpath} doesn't return any elements.", MyItineraryStepFactory.ErrorCodeXPath);

                linkMessage.Properties.Add(config.PromotionPropertyName, config.PromotionPropertyNamespace, xElement.Value); // Promote the value of the xpath to the given namespace and name
                linkMessage.Body.Data.Seek(0, SeekOrigin.Begin); // Remember to rewind the body-stream

                logger.LogInformation("All done!");
            }
            catch(Exception ex)
            {
                throw new LinkException("Something went wrong during the XPath promotion. This payload might not be valid XML.", MyItineraryStepFactory.ErrorCodeUnexpectedError, innerException: ex);
            }

            return Task.FromResult(linkMessage);
        }
    }

    public class MyItineraryStepConfig : LinkConfigBase
    {
        [LinkRequired]
        [LinkDisplay(Name = "XPath", GroupName = "XPath promotion", Description = "The XPath used to retrieve the value that should be promoted", Order = 1)]
        public string PromotionXpath { get; set; }
        [LinkRequired]
        [LinkDisplay(Name = "Context Property Namespace", GroupName = "XPath promotion", Order = 2)]
        public string PromotionPropertyNamespace { get; set; }
        [LinkRequired]
        [LinkDisplay(Name = "Context Property Key", GroupName = "XPath promotion", Order = 3)]
        public string PromotionPropertyName { get; set; }
    }
}

The following example does not do anything with the payload. It’s just to show how to use repositories to lookup entities.

Used base class: ItineraryStepBase2<T>
Required packages: Bizbrains.Link.Base, Bizbrains.Link.Base.Implementations, Bizbrains.Link.Repositories

Show code
C#
using Bizbrains.Link.Base;
using Bizbrains.Link.Base.Implementations.MessageProperties;
using Bizbrains.Link.Repositories.Configuration.DistributionModel;
using Bizbrains.Link.Repositories.Configuration.Partner;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Bizbrains.Examples.ItineraryStep2
{
    public class MyItineraryStepFactory : LinkStepFactoryBase<ILinkItineraryStep2>
    {
        public override string StepDisplayName => "My itinerary step";

        public override string StepDescription => "This step does nothing but logging some info...";
        public override void ConfigureServices(IServiceCollection serviceCollection)
        {
            // We add our step to the service-collection, because dependency injection will then resolve any services, repositories, logging and other stuff we expect in our constructor
            serviceCollection.AddTransient<MyItineraryStep>();
            // Add additional services, if you have any...
        }

        public override ILinkItineraryStep2 Create(IServiceProvider serviceProvider)
        {
            return serviceProvider.GetRequiredService<MyItineraryStep>();
        }
    }

    public class MyItineraryStep : ItineraryStepBase2<MyItineraryStepConfig>
    {
        private readonly ILogger<MyItineraryStep> logger;
        private readonly IDistributionRepository distributionRepository;
        private readonly IPartnerRepository partnerRepository;

        public MyItineraryStep(ILogger<MyItineraryStep> logger, IDistributionRepository distributionRepository, IPartnerRepository partnerRepository)
        {
            this.logger = logger;
            this.distributionRepository = distributionRepository;
            this.partnerRepository = partnerRepository;
        }

        public override string StepKeyName => "bizbrains-itinerary-mystep";

        protected override async Task<ILinkMessage> Execute(ILinkMessage linkMessage, MyItineraryStepConfig config, CancellationToken cancellationToken)
        {
            // Access context-properties
            var accessor = new LinkPropertyAccessor(linkMessage);
            var distributionId = accessor.Distribution.Id.ValueMandatory; // ValueMandatory will throw an exception with a telling text about the property and the missing value.

            // Load distribution-entity
            var distributionEntity = await distributionRepository.GetById(distributionId, false);
            logger.LogInformation($"Distribution details - PartnerInId: {distributionEntity.PartnerInId}, PartnerOutId: {distributionEntity.PartnerOutId}");

            // Load sender-partner-entity
            var senderPartner = await partnerRepository.GetById(accessor.PartnerIn.Id.ValueMandatory);
            logger.LogInformation($"Sender partner-name: {senderPartner.Name}");

            return linkMessage;
        }
    }

    public class MyItineraryStepConfig : LinkConfigBase { }
}

Send Steps

The following example shows how to implement a send-step. This step takes the body of the ILinkMessage and transmits it over Http. The configuration-class provides the Url and the Http Verb.

Used base class: TransportSendBase<T>
Required packages: Bizbrains.Link.Base

Show code
C#
using Bizbrains.Link.Base;
using Bizbrains.Link.Base.ConfigAttributes;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Link.Examples.SendStep
{
    public class MySendStepFactory : LinkStepFactoryBase<ILinkTransportSend>
    {
        public override string StepDisplayName => "My Http Sender";

        public override string StepDescription => "This step will post the body of the LinkMessage to the configured http-address";
        public override void ConfigureServices(IServiceCollection serviceCollection)
        {
            // We add our step to the service-collection, because dependency injection will then resolve any services, repositories, logging and other stuff we expect in our constructor
            serviceCollection.AddTransient<MySendStep>();
            // Add additional services, if you have any...
        }

        public override ILinkTransportSend Create(IServiceProvider serviceProvider)
        {
            return serviceProvider.GetRequiredService<MySendStep>();
        }
    }

    public class MySendStep : TransportSendBase<MySendStepConfig>
    {
        private readonly ILogger<MySendStep> logger;

        public MySendStep(ILogger<MySendStep> logger)
        {
            this.logger = logger;
        }

        public override string StepKeyName => "bizbrains-transport-send-httpsender";

        protected override string GenerateDisplayAddress(MySendStepConfig config)
        {
            // The display-address is used when creating the transport-type in Link.
            // Then this method will be invoked, and you can put together whatever makes sense for your transport-type.
            return $"{config.Url} ({config.HttpVerb})";
        }

        protected override async Task Send(ILinkMessage linkMessage, ILinkSendProvider sendProvider, MySendStepConfig config, CancellationToken ct)
        {
            var httpClient = new HttpClient();
            var httpMethod = config.HttpVerb == HttpVerb.Put ? HttpMethod.Put : HttpMethod.Post;
            var httpRequest = new HttpRequestMessage(httpMethod, config.Url);
            httpRequest.Content = new StreamContent(linkMessage.Body.Data);

            logger.LogInformation($"Calling {config.Url} ({config.HttpVerb})");
            var response = await httpClient.SendAsync(httpRequest, ct);
            logger.LogInformation("Http call done - reading response...");

            var body = await response.Content.ReadAsStreamAsync();

            logger.LogInformation($"Response read. Length: {body.Length}. Setting body on LinkMessage...");
            linkMessage.SetBody(body);
        }
    }

    public class MySendStepConfig : LinkConfigBase
    {
        [LinkRequired]
        [LinkDisplay(Name = "Url", GroupName = "Http", Order = 1)]
        public string Url { get; set; }

        [LinkRequired]
        [LinkDisplay(Name = "Http method", GroupName = "Http", Order = 2)]
        [LinkDefaultValue(nameof(HttpVerb.Post))]
        public HttpVerb HttpVerb { get; set; } // Enums will be presented as dropdowns
    }

    public enum HttpVerb
    {
        // Use the LinkEnum attribute to put a display-name on an enum.
        [LinkEnum("Post method")]
        Post,
        [LinkEnum("Put method")]
        Put
    }
}

Duplicate Check Steps

The following example shows how to implement a duplicate check step. This step finds a specific value in the body of the ILinkMessage and returns it. The configuration-class provides the from and to position of the desired value in the paylod.

Used base class: DuplicateCheckBase2<T>
Required packages: Bizbrains.Link.Base

Show code
C#
using Bizbrains.Link.Base;
using Bizbrains.Link.Base.ConfigAttributes;
using Bizbrains.Link.Base.DuplicateCheck;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Link.Examples.DuplicateCheck
{
    public class MyDuplicateCheckFactory : LinkStepFactoryBase<ILinkDuplicateCheck2>
    {
        public override string StepDisplayName => "My Duplicate Check";

        public override string StepDescription => "Finds a value in the payload at a specific position to use in the duplicate check framework";

        public override void ConfigureServices(IServiceCollection serviceCollection)
        {
            // We add our step to the service-collection, because dependency injection will then resolve any services, repositories, logging and other stuff we expect in our constructor
            serviceCollection.AddTransient<MyDuplicateCheck>();
            // Add additional services, if you have any...
        }

        public override ILinkDuplicateCheck2 Create(IServiceProvider serviceProvider)
        {
            return serviceProvider.GetRequiredService<MyDuplicateCheck>();
        }
    }

    internal class MyDuplicateCheck : DuplicateCheckBase2<MyDuplicateCheckConfig>
    {
        private readonly ILogger<MyDuplicateCheck> logger;

        public MyDuplicateCheck(ILogger<MyDuplicateCheck> logger)
        {
            this.logger = logger;
        }

        public override DuplicateCheckType DuplicateCheckType => DuplicateCheckType.Interchange;

        public override string StepKeyName => "bizbrains-duplicate-check-myduplicatecheck";

        protected override Task<DuplicateCheckResult> GetDuplicateCheckValue(ILinkMessage message, MyDuplicateCheckConfig config, CancellationToken cancellationToken)
        {
            logger.LogInformation("Starting MyDuplicateCheck");

            var reader = new StreamReader(message.Body.Data);
            var payload = reader.ReadToEnd();

            // Find the value based on the position specified in the config.
            var payloadValue = payload[config.PositionFrom..config.PositionTo];

            // Remember to rewind the body-stream
            message.Body.Data.Seek(0, SeekOrigin.Begin);

            var result = new DuplicateCheckResult()
            {
                CheckValue = payloadValue
            };

            logger.LogInformation("Finished MyDuplicateCheck");
            return Task.FromResult(result);
        }
    }

    public class MyDuplicateCheckConfig : ILinkConfig
    {
        [LinkRequired]
        [LinkDisplay(Name = "Position from", GroupName = "MyDuplicateCheck Config", Description = "Starting position of value in payload", Order = 1)]
        public int PositionFrom { get; set; }

        [LinkRequired]
        [LinkDisplay(Name = "Position to", GroupName = "MyDuplicateCheck Config", Description = "Ending position of value in payload", Order = 2)]
        public int PositionTo { get; set; }
    }
}

Format Type Probe

The following example shows how to implement a Format Type prober.

Used base class: LinkFormatTypeProbeBase2<T>
Required packages: Bizbrains.Link.Base, Bizbrains.Link.Base.Implementations

Show code
C#
using Bizbrains.Link.Base;
using Bizbrains.Link.Base.ConfigAttributes;
using Bizbrains.Link.Base.Exceptions;
using Bizbrains.Link.Base.Implementations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Text;

namespace Link.Examples.FormatTypeProbe
{
    public class MyFormatProbeFactory : LinkStepFactoryBase<ILinkFormatTypeProbe2>
    {
        public override string StepDisplayName => "MyFormat Probe";

        public override string StepDescription => "Checks if the paylod is of type MyFormat";

        public override void ConfigureServices(IServiceCollection serviceCollection)
        {
            // We add our step to the service-collection, because dependency injection will then resolve any services, repositories, logging and other stuff we expect in our constructor
            serviceCollection.AddTransient<MyFormatProbe>();
            // Add additional services, if you have any...
        }

        public override ILinkFormatTypeProbe2 Create(IServiceProvider serviceProvider)
        {
            return serviceProvider.GetRequiredService<MyFormatProbe>();
        }

        public const string MySettingInvalidError = "A1B2C"; // Generate the error-code in the Link UI (Developer -> Error codes -> Generate code)
        public override IEnumerable<LinkErrorCode> RegisterErrorCodes()
        {
            var errorCodes = new List<LinkErrorCode>();
            errorCodes.Add(new LinkErrorCode(MySettingInvalidError, LinkExceptionTypeArea.Initialize, LinkExceptionCategory.MissingConfigurationError, "MyFormatProbe", "Configuration field MySetting contains an invalid value", "Only use valid values"));
            return base.RegisterErrorCodes();
        }
    }

    internal class MyFormatProbe : LinkFormatTypeProbeBase2<MyFormatProbeConfig>
    {
        private readonly ILogger<MyFormatProbe> logger;

        public MyFormatProbe(ILogger<MyFormatProbe> logger)
        {
            this.logger = logger;
        }

        public override Task<bool> Probe(Stream payload, Encoding encoding, MyFormatProbeConfig config, CancellationToken cancellationToken)
        {
            try
            {
                logger.LogInformation("Probing for MyFormat");

                //Implement logic for checking if payload is of type MyFormat

                if (config.MySetting != "ValidValue")
                {
                    throw new LinkException($"The value \"{config.MySetting}\" is not a valid value for field MySetting", MyFormatProbeFactory.MySettingInvalidError);
                }

                return Task.FromResult(true);
            }
            catch (Exception)
            {
                return Task.FromResult(false);
            }
        }
    }

    public class MyFormatProbeConfig : LinkConfigBase
    {
        [LinkRequired]
        [LinkDisplay(Name = "MySetting", GroupName = "MyFormat Config", Description = "Used for probing MyFormat", Order = 1)]
        public string MySetting { get; set; }
    }
}

Compare Plugin

The following example shows how to implement a Compare Plugin.

Used base class: TestToolCompareBase<T>
Required packages: Bizbrains.Link.Base

Show code
C#
using Bizbrains.Link.Base;
using Bizbrains.Link.Base.ConfigAttributes;
using Bizbrains.Link.Base.TestTool;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Text;
using System.Xml;
using System.Xml.Linq;

namespace Link.Examples.Compare
{
    public class MyXmlCompareFactory : LinkFactoryBase<ITestToolCompare>
    {
        public override void ConfigureServices(IServiceCollection serviceCollection)
        {
            serviceCollection.AddTransient<MyXmlCompare>();
        }

        public override ITestToolCompare Create(IServiceProvider serviceProvider)
        {
            return serviceProvider.GetRequiredService<MyXmlCompare>();
        }
    }

    public class MyXmlCompare : TestToolCompareBase<MyXmlCompareConfig>
    {
        private readonly ILogger logger;
        public override string StepKeyName => "bizbrains-testtool-myxmlcompare";

        public MyXmlCompare(ILogger<MyXmlCompare> logger)
        {
            this.logger = logger;
        }

        public override Task<TestToolResult> Compare(TestToolDocumentContainer documentContainer, CancellationToken cancellationToken)
        {
            TestToolResult result = new TestToolResult();
            List<TestToolErrorDescription> errors = new List<TestToolErrorDescription>();
            //
            //Convert incomming streams to more easily handled objects.
            //

            cancellationToken.ThrowIfCancellationRequested();

            //
            //Iterate through xml documents and compare element by element
            //

            result.Errors = errors;

            if (errors.Count > 0)
            {
                result.Success = false;
            }
            else
            {
                result.Success = true;
            }
            return Task.FromResult(result);
        }

        public override Task<TestToolDocumentContainer> Prepare(TestToolDocumentContainer documentContainer, MyXmlCompareConfig config, CancellationToken cancellationToken)
        {
            //
            //Convert incomming streams to more easily handled objects.
            //

            cancellationToken.ThrowIfCancellationRequested();

            //
            //Prepare both streams based on config. could be removing nodes or changing data so they will compare(like date time objects that will always be different.
            //

            //
            // Convert manipulated objects back to streams and return them
            //

            TestToolDocumentContainer resultContainer = new TestToolDocumentContainer();
            resultContainer.OriginalDocument = StringToStream(xmlDoc1.OuterXml, documentContainer.OriginalDocumentEncoding);
            resultContainer.OriginalDocumentEncoding = documentContainer.OriginalDocumentEncoding;
            resultContainer.TestDocument = StringToStream(xmlDoc2.OuterXml, documentContainer.TestDocumentEncoding);
            resultContainer.TestDocumentEncoding = documentContainer.TestDocumentEncoding;

            return Task.FromResult(resultContainer);
        }


        private static Stream StringToStream(string src, Encoding encoding)
        {
            byte[] byteArray = encoding.GetBytes(src);
            return new MemoryStream(byteArray);
        }
    }



    public class MyXmlCompareConfig : ILinkConfig
    {
        [LinkDisplay(Name = "Xpath", GroupName = "Xpath 1", Order = 10)]
        public string Xpath1 { get; set; } = "";

        [LinkDefaultValue(nameof(PrepareConfig.Ignore))]
        [LinkDisplay(Name = "Xpath Action", Description = "What to do with this xpath. Either remove the node or set the content to fixed value that will compare", GroupName = "Xpath 1", Order = 11)]
        public PrepareConfig PrepareConfig1 { get; set; }

        [LinkDisplay(Name = "Xpath", GroupName = "Xpath 2", Order = 20)]
        public string Xpath2 { get; set; } = "";

        [LinkDefaultValue(nameof(PrepareConfig.Ignore))]
        [LinkDisplay(Name = "Xpath Action", Description = "What to do with this xpath. Either remove the node or set the content to fixed value that will compare", GroupName = "Xpath 2", Order = 21)]
        public PrepareConfig PrepareConfig2 { get; set; }


    }


    public enum PrepareConfig
    {
        Ignore,
        Remove
    }
}

Document Type Classifier Resolver Plugin

The following example shows how to implement a Document Type Classifier Resolver Plugin.

Used base class: DocumentTypeClassifierResolverBase2<T>
Required packages: Bizbrains.Link.Base

Show code
C#
using Bizbrains.Link.Base;
using Bizbrains.Link.Base.DocumentTypeClassifierResolver;
using Microsoft.Extensions.DependencyInjection;

namespace Bizbrains.Link.SystemArtifacts.DocumentTypeClassifierResolvers.My;

public class MyDocumentTypeClassifierResolverFactory : LinkStepFactoryBase<ILinkDocumentTypeClassifierResolver2>
{
    public override string StepDisplayName => "Bizbrains Document Type Classifier Resolver My";

    public override string StepDescription => "Document Type Classifier Resolver plugin for MY documents";

    public override ILinkDocumentTypeClassifierResolver2 Create(IServiceProvider serviceProvider) => serviceProvider.GetRequiredService<MyDocumentTypeClassifierResolver>();

    public override void ConfigureServices(IServiceCollection serviceCollection)
    {
        base.ConfigureServices(serviceCollection);
        serviceCollection.AddTransient<MyDocumentTypeClassifierResolver>();
    }
}

public class MyDocumentTypeClassifierResolver : DocumentTypeClassifierResolverBase2<MyDocumentTypeClassifierResolverConfig>
{
    public override string StepKeyName => "system-document-type-classifier-resolver-my";

    protected override async Task<DocumentTypeClassifierResolverResult> Resolve(ILinkMessage message, IEnumerable<MyDocumentTypeClassifierResolverConfig> configurations, CancellationToken cancellationToken)
    {
        await Task.CompletedTask;

        var messageType = "MyMessageType";
        return new DocumentTypeClassifierResolverResult() { Result = configurations.FirstOrDefault(c => c.MessageType == messageType) };
    }
}

public class MyDocumentTypeClassifierResolverConfig : ILinkConfig
{
    public string MessageType { get; set; }
}

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.