Skip to main content
Version: 5.1.0

How to create a plugin

In this tutorial, we'll take a step-by-step guide to creating plugins for the Workflow Server. In this example, we will develop a plugin to integrate with a Trello board. The principle of operation will be as follows: boards with schemes will be created in Trello, lists will display activities, and cards will show processes.

Step One - Creating a plugin

Create a class that implements the IWorkflowPlugin interface named TrelloPlugin.cs.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using OptimaJet.Workflow.Core.Runtime;
using OptimaJet.Workflow.Plugins;

public class TrelloPlugin : IWorkflowPlugin
{
public void OnPluginAdd(WorkflowRuntime runtime, List<string> schemes = null)
{
throw new System.NotImplementedException();
}

public async Task OnRuntimeStartAsync(WorkflowRuntime runtime)
{
}

public string Name { get; } = "TrelloPlugin";
public bool Disabled { get; set; }
public Dictionary<string, string> PluginSettings { get; }
}

The interface members are assigned as follows:

  • Name - the plugin name.
  • PluginSettings - the dictionary of the plugin settings.
  • OnPluginAdd - called when the plugin is added to the runtime. This method is convenient for subscribing to the WorkflowRuntime events.
  • OnPluginStartAsync - called at the start of the runtime.

Step Two - Integration with Trello

To communicate with Trello, we will use HTTP requests, for this we will need an HttpClient object. We will also describe classes for working with Trello entities.

Entities

Models can be divided into different files.

public class Organization
{
public Organization()
{
Boards = new List<Board>();
}

public string Id { get; set; }
public string Name { get; set; }
public string DisplayName { get; set; }
public List<Board> Boards { get; }
}

public class Board
{
public Board()
{
Lists = new List<BoardList>();
}

public string Id { get; set; }
public string Name { get; set; }
public string IdOrganization { get; set; }
public List<BoardList> Lists { get; }
public Guid SchemeId { get; set; }
}

public class BoardList
{
public BoardList()
{
Cards = new List<Card>();
}

public string Id { get; set; }
public string Name { get; set; }
public string IdBoard { get; set; }
public List<Card> Cards { get; }
}

public class Card
{
public string Id { get; set; }
public string Name { get; set; }
public string Desc { get; set; }
public string IdList { get; set; }
public Guid ProcessId { get; set; }
}

And declare the necessary variables.

Variables

Place this code in TrelloPlugin.cs:

public string Token { get; set; }
public string Key { get; set; }
private HttpClient _client;
private Organization _organization;
private WorkflowRuntime _runtime;
private List<string> _schemes;
private const string _url = "https://api.trello.com/1";

Step Three - Plugin logic

This plugin will create one workspace and all further boards will be created in it.

Initial workspace

Place this code in TrelloPlugin.cs:

private async Task<Organization> OrganizationInit(string name)
{
var response =
await _client.GetAsync(
$"{_url}/organizations/{name.ToLower()}?token={Token}&key={Key}");
if (response.StatusCode != HttpStatusCode.OK)
{
response = await _client.PostAsync(
$"{_url}/organizations?displayName={name}&token={Token}&key={Key}", null);
}

return JsonConvert.DeserializeObject<Organization>(await response.Content.ReadAsStringAsync());
}

Initial board

Boards will be used to display the schemes.

  • The BoardInit method initializes the board.
  • The DeleteOldBoards method deletes already inactive boards.

Place this code in TrelloPlugin.cs:

private async Task<Board> BoardInit(Organization organization, string name)
{
var organizationBoards = new List<Board>();
var response =
await _client.GetAsync(
$"{_url}/organization/{organization.Name}/boards?key={Key}&token={Token}");
if (response.StatusCode == HttpStatusCode.OK)
{
organizationBoards = JsonConvert.DeserializeObject<List<Board>>(await response.Content.ReadAsStringAsync());
}

var board = organizationBoards.FirstOrDefault(c =>
string.Equals(c.Name, name, StringComparison.CurrentCultureIgnoreCase));
if (board != null) return board;
response = await _client.PostAsync(
$"{_url}/boards?name={name}&key={Key}&token={Token}&defaultLists=false&defaultLabels=false&idOrganization={organization.Id}",
null);
board = JsonConvert.DeserializeObject<Board>(await response.Content.ReadAsStringAsync());

return board;
}

private async Task DeleteOldBoards(Organization org)
{
var response =
await _client.GetAsync(
$"{_url}/organizations/{org.Id}/boards?key={Key}&token={Token}");
if (response.StatusCode == HttpStatusCode.OK)
{
var boards = JsonConvert.DeserializeObject<List<Board>>(await response.Content.ReadAsStringAsync());
foreach (var trelloBoard in boards)
{
if (org.Boards.Find(c => c.Id == trelloBoard.Id) == null)
{
await _client.DeleteAsync(
$"{_url}/boards/{trelloBoard.Id}?key={Key}&token={Token}");
}
}
}
}

Initial list

The activity will present a list.

  • The ListInit method initializes the list.
  • The DeleteOldLists method deletes already inactive lists.
  • The Sortlists method sorts the lists in the order of activity in the diagram.

Place this code in TrelloPlugin.cs:

private async Task<BoardList> ListInit(Board board, string name)
{
List<BoardList> boardLists = new List<BoardList>();
var response =
await _client.GetAsync($"{_url}/boards/{board.Id}/lists?key={Key}&token={Token}");
if (response.StatusCode == HttpStatusCode.OK)
{
boardLists = JsonConvert.DeserializeObject<List<BoardList>>(await response.Content.ReadAsStringAsync());
}

var list = boardLists.FirstOrDefault(c =>
string.Equals(c.Name, name, StringComparison.CurrentCultureIgnoreCase));
if (list == null)
{
response = await _client.PostAsync(
$"{_url}/lists?name={name}&idBoard={board.Id}&key={Key}&token={Token}", null);
list = JsonConvert.DeserializeObject<BoardList>(await response.Content.ReadAsStringAsync());
}

return list;
}

private async Task DeleteOldLists(Board board)
{
var response =
await _client.GetAsync($"{_url}/boards/{board.Id}/lists?key={Key}&token={Token}");
if (response.StatusCode == HttpStatusCode.OK)
{
var allLists = JsonConvert.DeserializeObject<List<BoardList>>(await response.Content.ReadAsStringAsync());
foreach (var list in allLists)
{
if (board.Lists.Find(c => c.Id == list.Id) == null)
{
await _client.PutAsync(
$"{_url}/lists/{list.Id}/closed?value=true&key={Key}&token={Token}", null);
}
}
}
}

private async Task SortLists(Board board)
{
int startPosition = 5;
var response =
await _client.GetAsync($"{_url}/boards/{board.Id}/lists?key={Key}&token={Token}");
if (response.StatusCode == HttpStatusCode.OK)
{
var lists = JsonConvert.DeserializeObject<List<BoardList>>(await response.Content.ReadAsStringAsync());
var pd = await _runtime.Builder.GetProcessSchemeAsync(board.SchemeId);
foreach (var activityDefinition in pd.Activities)
{
var list = lists.FirstOrDefault(c => c.Name.ToLower() == activityDefinition.Name.ToLower());
if (list != null)
{
await _client.PutAsync(
$"{_url}/lists/{list.Id}?pos={Math.Pow(2, startPosition)}&key={Key}&token={Token}",
null);
startPosition++;
}
}
}
}

Initial card

The process instance will be a card.

  • The CardInit method initializes the card.
  • The DeleteOldCards method deletes all cards whose processes were deleted.
  • The MoveCard method Moves the card when the process moves through the activities.

Place this code in TrelloPlugin.cs:

private async Task<Card> CardInit(BoardList list, string name, string desc, Guid processId)
{
List<Card> cardList = new List<Card>();
var response =
await _client.GetAsync($"{_url}/lists/{list.Id}/cards?key={Key}&token={Token}");
if (response.StatusCode == HttpStatusCode.OK)
{
cardList = JsonConvert.DeserializeObject<List<Card>>(await response.Content.ReadAsStringAsync());
}

var card = cardList.FirstOrDefault(c => string.Equals(c.Name, name, StringComparison.CurrentCultureIgnoreCase));
if (card == null)
{
response = await _client.PostAsync(
$"{_url}/cards?name={name}&desc={desc}&pos=top&idList={list.Id}&key={Key}&token={Token}",
null);
card = JsonConvert.DeserializeObject<Card>(await response.Content.ReadAsStringAsync());
}

card.ProcessId = processId;
return card;
}

private async Task DeleteOldCards(Board board)
{
foreach (var boardList in board.Lists)
{
var response =
await _client.GetAsync($"{_url}/lists/{boardList.Id}/cards?key={Key}&token={Token}");
if (response.StatusCode == HttpStatusCode.OK)
{
var CardsOnList = JsonConvert.DeserializeObject<List<Card>>(await response.Content.ReadAsStringAsync());
foreach (var card in CardsOnList)
{
if (boardList.Cards.Find(c => c.Id == card.Id) == null)
{
await _client.DeleteAsync($"{_url}/cards/{card.Id}?key={Key}&token={Token}");
}
}
}
}
}

private async Task MoveCard(Card card, BoardList oldList, BoardList newList)
{
var response = await _client.PutAsync(
$"{_url}/cards/{card.Id}?idList={newList.Id}&key={Key}&token={Token}", null);
if (response.StatusCode == HttpStatusCode.OK)
{
if (oldList != null)
{
oldList.Cards.Remove(oldList.Cards.Find(c => c.Id == card.Id));
}

var newCard = JsonConvert.DeserializeObject<Card>(await response.Content.ReadAsStringAsync());
newCard.ProcessId = card.ProcessId;
newList.Cards.Add(newCard);
}
}

Initial Trello

And finally, initialization of the entire board, all the logic of initialization of the Trello board according to the selected schemes is collected here.

  • The TrelloInit method accomplishes this task.

Place this code in TrelloPlugin.cs:

private async Task<Organization> TrelloInit(WorkflowRuntime runtime, List<string> schemes = null)
{
_organization = await OrganizationInit("WorkflowServer");
foreach (var scheme in schemes)
{
var board = await BoardInit(_organization, scheme);
board.Lists.Clear();
_organization.Boards.Add(board);
var pd = await runtime.Builder.GetProcessSchemeAsync(scheme);
board.SchemeId = pd.Id;
foreach (var activity in pd.Activities)
{
var list = await ListInit(board, activity.Name);
board.Lists.Add(list);
}

await DeleteOldLists(board);
await SortLists(board);
}

await DeleteOldBoards(_organization);

var pi = await runtime.PersistenceProvider.GetProcessInstancesAsync(null, null);
var piGroup = pi.GroupBy(c => c.SchemeId).ToList();
foreach (var processInstanceItems in piGroup)
{
var board = _organization.Boards.FirstOrDefault(c => c.SchemeId == processInstanceItems.Key);
if (board != null)
{
foreach (var boardList in board.Lists)
{
boardList.Cards.Clear();
var piOnActivity = processInstanceItems.Where(c => c.ActivityName == boardList.Name).ToList();
foreach (var card in piOnActivity)
{
StringBuilder descriptionString = new StringBuilder();
descriptionString.AppendLine($"Process id: {card.Id}");
descriptionString.AppendLine(
$"Creation date: {card.CreationDate.ToLongDateString()} {card.CreationDate.ToLongTimeString()}");
var Card = await CardInit(boardList, card.Id.ToString(), descriptionString.ToString(), card.Id);
boardList.Cards.Add(Card);
}
}

await DeleteOldCards(board);
}
}

return _organization;
}

At the end, we implement the OnPluginAdd method, here all the actions of the plugin are performed when starting work.

OnPluginAdd

Place this code in TrelloPlugin.cs:

public async void OnPluginAdd(WorkflowRuntime runtime, List<string> schemes = null)
{
_runtime = runtime;
_schemes = schemes;
if (schemes.IsNullOrEmpty())
{
_schemes = new List<string>();
var allSchemes = await runtime.GetSchemeCodesAsync();
_schemes.AddRange(allSchemes);
}

_client = new HttpClient();
_organization = await TrelloInit(runtime, _schemes);
runtime.OnSchemaWasChangedAsync += async (sender, args, cancellationToken) =>
{
await TrelloInit(_runtime, _schemes);
};

runtime.OnProcessActivityChanged += async (sender, args) =>
{
if (!_organization.Boards.Any(c =>
string.Equals(c.Name, args.SchemeCode, StringComparison.CurrentCultureIgnoreCase)))
{
_schemes.Add(args.SchemeCode);
await TrelloInit(runtime, _schemes);
}

var board = _organization.Boards.FirstOrDefault(c => c.Name == args.SchemeCode);
var oldList = args.PreviousActivityName == null
? null
: board.Lists.FirstOrDefault(x => x.Name.ToLower() == args.PreviousActivityName.ToLower());
var newList = board.Lists.FirstOrDefault(x => x.Name.ToLower() == args.CurrentActivityName.ToLower());
Card card;
if (oldList == null)
{
var descriptionBuilder = new StringBuilder();
descriptionBuilder.AppendLine($"Process id: {args.ProcessInstance.ProcessId}");
descriptionBuilder.AppendLine(
$"Creation date: {args.ProcessInstance.CreationDate.ToLongDateString()} {args.ProcessInstance.CreationDate.ToLongTimeString()}");
card = await CardInit(newList, args.ProcessId.ToString(), descriptionBuilder.ToString(),
args.ProcessId);
}
else
{
card = oldList.Cards.FirstOrDefault(c => c.ProcessId == args.ProcessId);
}

await MoveCard(card, oldList, newList);
};
}

Step Four - Initializing the plugin

Add the plugin to the application in the Program.cs file:

var plugin = new TrelloPlugin
{
Token = "YOUR TRELLO TOKEN",
Key = "YOUR TRELLO KEY"
};
workflowServer.WorkflowRuntime.WithPlugin(plugin, new List<string>());

All plugin code

TrelloPlugin.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using IdentityServer4.Extensions;
using OptimaJet.Workflow.Core.Runtime;
using OptimaJet.Workflow.Plugins;
using Newtonsoft.Json;


namespace WorkflowServer.Plugins;

public class TrelloPlugin : IWorkflowPlugin
{
public string Name { get; } = "Trello Plugin";
public bool Disabled { get; set; }
public Dictionary<string, string> PluginSettings { get; }
public string Token { get; set; }
public string Key { get; set; }

private HttpClient _client;
private Organization _organization;
private WorkflowRuntime _runtime;
private List<string> _schemes;
private const string _url = "https://api.trello.com/1";

public async void OnPluginAdd(WorkflowRuntime runtime, List<string> schemes = null)
{
_runtime = runtime;
_schemes = schemes;
if (schemes.IsNullOrEmpty())
{
_schemes = new List<string>();
var allSchemes = await runtime.GetSchemeCodesAsync();
_schemes.AddRange(allSchemes);
}

_client = new HttpClient();
_organization = await TrelloInit(runtime, _schemes);
runtime.OnSchemaWasChangedAsync += async (sender, args, cancellationToken) =>
{
await TrelloInit(_runtime, _schemes);
};

runtime.OnProcessActivityChanged += async (sender, args) =>
{
if (!_organization.Boards.Any(c =>
string.Equals(c.Name, args.SchemeCode, StringComparison.CurrentCultureIgnoreCase)))
{
_schemes.Add(args.SchemeCode);
await TrelloInit(runtime, _schemes);
}

var board = _organization.Boards.FirstOrDefault(c => c.Name == args.SchemeCode);
var oldList = args.PreviousActivityName == null
? null
: board.Lists.FirstOrDefault(x => x.Name.ToLower() == args.PreviousActivityName.ToLower());
var newList = board.Lists.FirstOrDefault(x => x.Name.ToLower() == args.CurrentActivityName.ToLower());
Card card;
if (oldList == null)
{
var descriptionBuilder = new StringBuilder();
descriptionBuilder.AppendLine($"Process id: {args.ProcessInstance.ProcessId}");
descriptionBuilder.AppendLine(
$"Creation date: {args.ProcessInstance.CreationDate.ToLongDateString()} {args.ProcessInstance.CreationDate.ToLongTimeString()}");
card = await CardInit(newList, args.ProcessId.ToString(), descriptionBuilder.ToString(),
args.ProcessId);
}
else
{
card = oldList.Cards.FirstOrDefault(c => c.ProcessId == args.ProcessId);
}

await MoveCard(card, oldList, newList);
};
}

public async Task OnRuntimeStartAsync(WorkflowRuntime runtime)
{
}

private async Task<Organization> OrganizationInit(string name)
{
var response =
await _client.GetAsync(
$"{_url}/organizations/{name.ToLower()}?token={Token}&key={Key}");
if (response.StatusCode != HttpStatusCode.OK)
{
response = await _client.PostAsync(
$"{_url}/organizations?displayName={name}&token={Token}&key={Key}", null);
}

return JsonConvert.DeserializeObject<Organization>(await response.Content.ReadAsStringAsync());
}


private async Task<Board> BoardInit(Organization organization, string name)
{
var organizationBoards = new List<Board>();
var response =
await _client.GetAsync(
$"{_url}/organization/{organization.Name}/boards?key={Key}&token={Token}");
if (response.StatusCode == HttpStatusCode.OK)
{
organizationBoards = JsonConvert.DeserializeObject<List<Board>>(await response.Content.ReadAsStringAsync());
}

var board = organizationBoards.FirstOrDefault(c =>
string.Equals(c.Name, name, StringComparison.CurrentCultureIgnoreCase));
if (board != null) return board;
response = await _client.PostAsync(
$"{_url}/boards?name={name}&key={Key}&token={Token}&defaultLists=false&defaultLabels=false&idOrganization={organization.Id}",
null);
board = JsonConvert.DeserializeObject<Board>(await response.Content.ReadAsStringAsync());

return board;
}

private async Task DeleteOldBoards(Organization org)
{
var response =
await _client.GetAsync(
$"{_url}/organizations/{org.Id}/boards?key={Key}&token={Token}");
if (response.StatusCode == HttpStatusCode.OK)
{
var boards = JsonConvert.DeserializeObject<List<Board>>(await response.Content.ReadAsStringAsync());
foreach (var trelloBoard in boards)
{
if (org.Boards.Find(c => c.Id == trelloBoard.Id) == null)
{
await _client.DeleteAsync(
$"{_url}/boards/{trelloBoard.Id}?key={Key}&token={Token}");
}
}
}
}

private async Task<BoardList> ListInit(Board board, string name)
{
List<BoardList> boardLists = new List<BoardList>();
var response =
await _client.GetAsync($"{_url}/boards/{board.Id}/lists?key={Key}&token={Token}");
if (response.StatusCode == HttpStatusCode.OK)
{
boardLists = JsonConvert.DeserializeObject<List<BoardList>>(await response.Content.ReadAsStringAsync());
}

var list = boardLists.FirstOrDefault(c =>
string.Equals(c.Name, name, StringComparison.CurrentCultureIgnoreCase));
if (list == null)
{
response = await _client.PostAsync(
$"{_url}/lists?name={name}&idBoard={board.Id}&key={Key}&token={Token}", null);
list = JsonConvert.DeserializeObject<BoardList>(await response.Content.ReadAsStringAsync());
}

return list;
}

private async Task DeleteOldLists(Board board)
{
var response =
await _client.GetAsync($"{_url}/boards/{board.Id}/lists?key={Key}&token={Token}");
if (response.StatusCode == HttpStatusCode.OK)
{
var allLists = JsonConvert.DeserializeObject<List<BoardList>>(await response.Content.ReadAsStringAsync());
foreach (var list in allLists)
{
if (board.Lists.Find(c => c.Id == list.Id) == null)
{
await _client.PutAsync(
$"{_url}/lists/{list.Id}/closed?value=true&key={Key}&token={Token}", null);
}
}
}
}

private async Task SortLists(Board board)
{
int startPosition = 5;
var response =
await _client.GetAsync($"{_url}/boards/{board.Id}/lists?key={Key}&token={Token}");
if (response.StatusCode == HttpStatusCode.OK)
{
var lists = JsonConvert.DeserializeObject<List<BoardList>>(await response.Content.ReadAsStringAsync());
var pd = await _runtime.Builder.GetProcessSchemeAsync(board.SchemeId);
foreach (var activityDefinition in pd.Activities)
{
var list = lists.FirstOrDefault(c => c.Name.ToLower() == activityDefinition.Name.ToLower());
if (list != null)
{
await _client.PutAsync(
$"{_url}/lists/{list.Id}?pos={Math.Pow(2, startPosition)}&key={Key}&token={Token}",
null);
startPosition++;
}
}
}
}

private async Task<Card> CardInit(BoardList list, string name, string desc, Guid processId)
{
List<Card> cardList = new List<Card>();
var response =
await _client.GetAsync($"{_url}/lists/{list.Id}/cards?key={Key}&token={Token}");
if (response.StatusCode == HttpStatusCode.OK)
{
cardList = JsonConvert.DeserializeObject<List<Card>>(await response.Content.ReadAsStringAsync());
}

var card = cardList.FirstOrDefault(c => string.Equals(c.Name, name, StringComparison.CurrentCultureIgnoreCase));
if (card == null)
{
response = await _client.PostAsync(
$"{_url}/cards?name={name}&desc={desc}&pos=top&idList={list.Id}&key={Key}&token={Token}",
null);
card = JsonConvert.DeserializeObject<Card>(await response.Content.ReadAsStringAsync());
}

card.ProcessId = processId;
return card;
}

private async Task DeleteOldCards(Board board)
{
foreach (var boardList in board.Lists)
{
var response =
await _client.GetAsync($"{_url}/lists/{boardList.Id}/cards?key={Key}&token={Token}");
if (response.StatusCode == HttpStatusCode.OK)
{
var CardsOnList = JsonConvert.DeserializeObject<List<Card>>(await response.Content.ReadAsStringAsync());
foreach (var card in CardsOnList)
{
if (boardList.Cards.Find(c => c.Id == card.Id) == null)
{
await _client.DeleteAsync($"{_url}/cards/{card.Id}?key={Key}&token={Token}");
}
}
}
}
}

private async Task MoveCard(Card card, BoardList oldList, BoardList newList)
{
var response = await _client.PutAsync(
$"{_url}/cards/{card.Id}?idList={newList.Id}&key={Key}&token={Token}", null);
if (response.StatusCode == HttpStatusCode.OK)
{
if (oldList != null)
{
oldList.Cards.Remove(oldList.Cards.Find(c => c.Id == card.Id));
}

var newCard = JsonConvert.DeserializeObject<Card>(await response.Content.ReadAsStringAsync());
newCard.ProcessId = card.ProcessId;
newList.Cards.Add(newCard);
}
}

private async Task<Organization> TrelloInit(WorkflowRuntime runtime, List<string> schemes = null)
{
_organization = await OrganizationInit("WorkflowServer");
foreach (var scheme in schemes)
{
var board = await BoardInit(_organization, scheme);
board.Lists.Clear();
_organization.Boards.Add(board);
var pd = await runtime.Builder.GetProcessSchemeAsync(scheme);
board.SchemeId = pd.Id;
foreach (var activity in pd.Activities)
{
var list = await ListInit(board, activity.Name);
board.Lists.Add(list);
}

await DeleteOldLists(board);
await SortLists(board);
}

await DeleteOldBoards(_organization);

var pi = await runtime.PersistenceProvider.GetProcessInstancesAsync(null, null);
var piGroup = pi.GroupBy(c => c.SchemeId).ToList();
foreach (var processInstanceItems in piGroup)
{
var board = _organization.Boards.FirstOrDefault(c => c.SchemeId == processInstanceItems.Key);
if (board != null)
{
foreach (var boardList in board.Lists)
{
boardList.Cards.Clear();
var piOnActivity = processInstanceItems.Where(c => c.ActivityName == boardList.Name).ToList();
foreach (var card in piOnActivity)
{
StringBuilder descriptionString = new StringBuilder();
descriptionString.AppendLine($"Process id: {card.Id}");
descriptionString.AppendLine(
$"Creation date: {card.CreationDate.ToLongDateString()} {card.CreationDate.ToLongTimeString()}");
var Card = await CardInit(boardList, card.Id.ToString(), descriptionString.ToString(), card.Id);
boardList.Cards.Add(Card);
}
}

await DeleteOldCards(board);
}
}

return _organization;
}
}

#region Models

public class Organization
{
public Organization()
{
Boards = new List<Board>();
}

public string Id { get; set; }
public string Name { get; set; }
public string DisplayName { get; set; }
public List<Board> Boards { get; }
}

public class Board
{
public Board()
{
Lists = new List<BoardList>();
}

public string Id { get; set; }
public string Name { get; set; }
public string IdOrganization { get; set; }
public List<BoardList> Lists { get; }
public Guid SchemeId { get; set; }
}

public class BoardList
{
public BoardList()
{
Cards = new List<Card>();
}

public string Id { get; set; }
public string Name { get; set; }
public string IdBoard { get; set; }
public List<Card> Cards { get; }
}

public class Card
{
public string Id { get; set; }
public string Name { get; set; }
public string Desc { get; set; }
public string IdList { get; set; }
public Guid ProcessId { get; set; }
}

#endregion