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 theWorkflowRuntime
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