How to develop Link 3 artifacts
This article describes how to develop artifacts to Link 3.
Prerequisites
Visual Studio 2022
.NET 6
Visual Studio Templates
It is possible to get project-templates in Visual Studio 2022 supporting all Link3 artifact-types.
To install the templates use the following command:
dotnet new install Bizbrains.Link.Templates --nuget-source https://bizbrains-productteam.pkgs.visualstudio.com/Link3Public/_packaging/Link3Public/nuget/v3/index.json
It’s recommended to close your running Visual Studio instances before installing the templates.
After installation you should be able to choose the “Link Step“ project type (search for Bizbrains):
When creating the new project, you will be prompted to choose the type of artifact and to give your artifact a name (which will be the pre-fix for the auto-generated classes). Avoid spaces in the name.
The templates contains a nuget.config file. If the nuget-dependencies can’t be resolved - move the nuget.config file to the same directory as your solution (.sln) file. Nuget.config is not supported on project-folder level.
Now you can compile the project and fill in the missing methods. Look for “TODO”'s in the code.
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.
HttpHandler Steps
Usage: Use this to expose any kind of functionality for an HTTP endpoint. This could be a custom API, a webhook, or even a website.
Base class: LinkHttpHandlerBase2<T>
Note: The original LinkHttpHandlerBase<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:
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:
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:
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
LinkStringLength
LinkRequired
LinkRegularExpression
LinkDependsOn
LinkArray
LinkDefaultValue
LinkPassword
LinkArtifactAssembly
LinkArtifactXml
LinkEnum
LinkChoice
Code example
The following code shows different way of using the attributes:
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:
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:
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:
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:
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:
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.
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.
throw new LinkFlowException("Validation error", *******, retryAllowed: false);
It’s also possible to give the document a specific status - eg. Ignored if this is needed:
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
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
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
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
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
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
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
Compare Plugin
The following example shows how to implement a Compare Plugin.
Used base class: TestToolCompareBase<T>
Required packages: Bizbrains.Link.Base
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