diff --git a/src/YouTrackSharp/Agiles/Agile.cs b/src/YouTrackSharp/Agiles/Agile.cs
new file mode 100644
index 00000000..536af0ee
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/Agile.cs
@@ -0,0 +1,143 @@
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using YouTrackSharp.Json;
+using YouTrackSharp.Projects;
+using YouTrackSharp.SerializationAttributes;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Represents an agile board configuration.
+ ///
+ public class Agile
+ {
+ ///
+ /// Id of the Agile.
+ ///
+ [JsonProperty("id")]
+ public string Id { get; set; }
+
+ ///
+ /// The name of the agile board. Can be null.
+ ///
+ [JsonProperty("name")]
+ public string Name { get; set; }
+
+ ///
+ /// Owner of the agile board. Can be null.
+ ///
+ [JsonProperty("owner")]
+ public User Owner { get; set; }
+
+ ///
+ /// The user group that can view this board. Can be null.
+ ///
+ [Verbose]
+ [JsonProperty("visibleFor")]
+ public UserGroup VisibleFor { get; set; }
+
+ ///
+ /// When true, the board is visible to everyone who can view all projects that are associated with the board.
+ ///
+ [JsonProperty("visibleForProjectBased")]
+ public bool VisibleForProjectBased { get; set; }
+
+ ///
+ /// Group of users who can update board settings. Can be null.
+ ///
+ [Verbose]
+ [JsonProperty("updateableBy")]
+ public UserGroup UpdateableBy { get; set; }
+
+ ///
+ /// When true, anyone who can update the associated projects can update the board.
+ ///
+ [JsonProperty("updateableByProjectBased")]
+ public bool UpdateableByProjectBased { get; set; }
+
+ ///
+ /// When true, the orphan swimlane is placed at the top of the board. Otherwise, the orphans swimlane is located
+ /// below all other swimlanes.
+ ///
+ [JsonProperty("orphansAtTheTop")]
+ public bool OrphansAtTheTop { get; set; }
+
+ ///
+ /// When true, the orphans swimlane is not displayed on the board.
+ ///
+ [JsonProperty("hideOrphansSwimlane")]
+ public bool HideOrphansSwimlane { get; set; }
+
+ ///
+ /// A custom field that is used as the estimation field for the board. Can be null.
+ ///
+ [Verbose]
+ [JsonProperty("estimationField")]
+ public CustomField EstimationField { get; set; }
+
+ ///
+ /// A custom field that is used as the original estimation field for the board. Can be null.
+ ///
+ [Verbose]
+ [JsonProperty("originalEstimationField")]
+ public CustomField OriginalEstimationField { get; set; }
+
+ ///
+ /// A collection of projects associated with the board.
+ ///
+ [Verbose]
+ [JsonProperty("projects")]
+ public List Projects { get; set; }
+
+ ///
+ /// The set of sprints that are associated with the board.
+ ///
+ [Verbose]
+ [JsonProperty("sprints")]
+ public List Sprints { get; set; }
+
+ ///
+ /// A sprint that is actual for the current date. Read-only. Can be null.
+ ///
+ [Verbose]
+ [JsonProperty("currentSprint")]
+ public Sprint CurrentSprint { get; set; }
+
+ ///
+ /// Column settings of the board. Read-only.
+ ///
+ [Verbose]
+ [JsonProperty("columnSettings")]
+ public ColumnSettings ColumnSettings { get; set; }
+
+ ///
+ /// Settings of the board swimlanes. Can be null.
+ ///
+ [Verbose]
+ [JsonProperty("swimlaneSettings")]
+ [JsonConverter(typeof(KnownTypeConverter))]
+ public SwimlaneSettings SwimlaneSettings { get; set; }
+
+ ///
+ /// Settings of the board sprints. Read-only.
+ ///
+ [Verbose]
+ [JsonProperty("sprintsSettings")]
+ public SprintsSettings SprintsSettings { get; set; }
+
+ ///
+ /// Color coding settings for the board. Can be null.
+ ///
+ [Verbose]
+ [JsonProperty("colorCoding")]
+ [JsonConverter(typeof(KnownTypeConverter))]
+ public ColorCoding ColorCoding { get; set; }
+
+ ///
+ /// Status of the board. Read-only.
+ ///
+ [Verbose]
+ [JsonProperty("status")]
+ public AgileStatus Status { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/AgileColumn.cs b/src/YouTrackSharp/Agiles/AgileColumn.cs
new file mode 100644
index 00000000..8f2a90e6
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/AgileColumn.cs
@@ -0,0 +1,47 @@
+using System.Collections.Generic;
+using Newtonsoft.Json;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Represents settings for a single board column
+ ///
+ public class AgileColumn
+ {
+ ///
+ /// Id of the AgileColumn.
+ ///
+ [JsonProperty("id")]
+ public string Id { get; set; }
+
+ ///
+ /// Text presentation of values stored in a column. Read-only. Can be null.
+ ///
+ [JsonProperty("presentation")]
+ public string Presentation { get; set; }
+
+ ///
+ /// true if a column represents resolved state of an issue. Can be updated only for newly created value. Read-only.
+ ///
+ [JsonProperty("isResolved")]
+ public bool IsResolved { get; set; }
+
+ ///
+ /// Order of this column on board, counting from left to right.
+ ///
+ [JsonProperty("ordinal")]
+ public int Ordinal { get; set; }
+
+ ///
+ /// WIP limit for this column. Can be null.
+ ///
+ [JsonProperty("wipLimit")]
+ public WIPLimit WipLimit { get; set; }
+
+ ///
+ /// Field values represented by this column.
+ ///
+ [JsonProperty("fieldValues")]
+ public List FieldValues { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/AgileColumnFieldValue.cs b/src/YouTrackSharp/Agiles/AgileColumnFieldValue.cs
new file mode 100644
index 00000000..30b240e3
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/AgileColumnFieldValue.cs
@@ -0,0 +1,22 @@
+using Newtonsoft.Json;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Represents a field value or values, parameterizing agile column.
+ ///
+ public class AgileColumnFieldValue : DatabaseAttributeValue
+ {
+ ///
+ /// Presentation of a field value or values. Can be null.
+ ///
+ [JsonProperty("name")]
+ public string Name { get; set; }
+
+ ///
+ /// True, if field has type State and teh value is resolved or all values are resolved. Read-only.
+ ///
+ [JsonProperty("isResolved")]
+ public bool IsResolved { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/AgileService.cs b/src/YouTrackSharp/Agiles/AgileService.cs
new file mode 100644
index 00000000..89293de3
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/AgileService.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using YouTrackSharp.Internal;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Service offering Agile-related operations.
+ ///
+ public class AgileService : IAgileService
+ {
+ private readonly Connection _connection;
+
+ private readonly FieldSyntaxEncoder _fieldSyntaxEncoder;
+
+ ///
+ /// Creates an instance of the class.
+ ///
+ ///
+ /// A instance that provides a connection to the remote YouTrack server instance.
+ ///
+ ///
+ /// An instance that allows to encode types into Youtrack request URL format for fields
+ ///
+ public AgileService(Connection connection, FieldSyntaxEncoder fieldSyntaxEncoder)
+ {
+ _connection = connection;
+ _fieldSyntaxEncoder = fieldSyntaxEncoder;
+ }
+
+ ///
+ public async Task> GetAgileBoards(bool verbose = false)
+ {
+ HttpClient client = await _connection.GetAuthenticatedHttpClient();
+
+ const int batchSize = 10;
+ List agileBoards = new List();
+ List currentBatch;
+
+ do
+ {
+ string fields = _fieldSyntaxEncoder.Encode(typeof(Agile), verbose);
+
+ HttpResponseMessage message = await client.GetAsync($"api/agiles?fields={fields}&$top={batchSize}&$skip={agileBoards.Count}");
+
+ string response = await message.Content.ReadAsStringAsync();
+
+ currentBatch = JsonConvert.DeserializeObject>(response);
+
+ agileBoards.AddRange(currentBatch);
+ } while (currentBatch.Count == batchSize);
+
+ return agileBoards;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/AgileStatus.cs b/src/YouTrackSharp/Agiles/AgileStatus.cs
new file mode 100644
index 00000000..1958fad3
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/AgileStatus.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using Newtonsoft.Json;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Shows if the board has any configuration problems.
+ ///
+ public class AgileStatus
+ {
+ ///
+ /// Id of the AgileStatus.
+ ///
+ [JsonProperty("id")]
+ public string Id { get; set; }
+
+ ///
+ /// true if the board is in valid state and can be used. Read-only.
+ ///
+ [JsonProperty("valid")]
+ public bool Valid { get; set; }
+
+ ///
+ /// If `true`, then a background job is currently being executed for the board. In this case, while a background
+ /// job is running, the board cannot be updated. Read-only.
+ ///
+ [JsonProperty("hasJobs")]
+ public bool HasJobs { get; set; }
+
+ ///
+ /// List of configuration errors found for this board. Read-only.
+ ///
+ [JsonProperty("errors")]
+ public List Errors { get; set; }
+
+ ///
+ /// List of configuration-related warnings found for this board. Read-only.
+ ///
+ [JsonProperty("warnings")]
+ public List Warnings { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/AttributeBasedSwimlaneSettings.cs b/src/YouTrackSharp/Agiles/AttributeBasedSwimlaneSettings.cs
new file mode 100644
index 00000000..e14a3854
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/AttributeBasedSwimlaneSettings.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using YouTrackSharp.Json;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Settings of swimlanes that are identified by the set of values in the selected field. For example, you can set
+ /// swimlanes to represent issues for each Assignee.
+ ///
+ public class AttributeBasedSwimlaneSettings : SwimlaneSettings
+ {
+ ///
+ /// CustomField which values are used to identify swimlane.
+ ///
+ [JsonProperty("field")]
+ [JsonConverter(typeof(KnownTypeConverter))]
+ public FilterField Field { get; set; }
+
+ ///
+ /// Swimlanes that are visible on the Board.
+ ///
+ [JsonProperty("values")]
+ public List Values { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/ColorCoding.cs b/src/YouTrackSharp/Agiles/ColorCoding.cs
new file mode 100644
index 00000000..e158fba9
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/ColorCoding.cs
@@ -0,0 +1,19 @@
+using Newtonsoft.Json;
+using YouTrackSharp.SerializationAttributes;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Describe rules according to which different colors are used for cards on agile board.
+ ///
+ [KnownType(typeof(FieldBasedColorCoding))]
+ [KnownType(typeof(ProjectBasedColorCoding))]
+ public class ColorCoding
+ {
+ ///
+ /// Id of the ColorCoding.
+ ///
+ [JsonProperty("id")]
+ public string Id { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/ColumnSettings.cs b/src/YouTrackSharp/Agiles/ColumnSettings.cs
new file mode 100644
index 00000000..10e91946
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/ColumnSettings.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using YouTrackSharp.Projects;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Agile board columns settings.
+ ///
+ public class ColumnSettings
+ {
+ ///
+ /// Id of the ColumnSettings.
+ ///
+ [JsonProperty("id")]
+ public string Id { get; set; }
+
+ ///
+ /// Custom field, which values are used for columns. Can be null.
+ ///
+ [JsonProperty("field")]
+ public CustomField Field { get; set; }
+
+ ///
+ /// Columns that are shown on the board.
+ ///
+ [JsonProperty("columns")]
+ public List Columns { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/CustomFilterField.cs b/src/YouTrackSharp/Agiles/CustomFilterField.cs
new file mode 100644
index 00000000..22d66106
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/CustomFilterField.cs
@@ -0,0 +1,17 @@
+using Newtonsoft.Json;
+using YouTrackSharp.Projects;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Represents a custom field of the issue.
+ ///
+ public class CustomFilterField : FilterField
+ {
+ ///
+ /// Reference to settings of the custom field. Read-only.
+ ///
+ [JsonProperty("customField")]
+ public CustomField CustomField { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/DatabaseAttributeValue.cs b/src/YouTrackSharp/Agiles/DatabaseAttributeValue.cs
new file mode 100644
index 00000000..51e6ed51
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/DatabaseAttributeValue.cs
@@ -0,0 +1,19 @@
+using Newtonsoft.Json;
+using YouTrackSharp.SerializationAttributes;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Represents string reference to the value.
+ ///
+ [KnownType(typeof(SwimlaneEntityAttributeValue))]
+ [KnownType(typeof(AgileColumnFieldValue))]
+ public class DatabaseAttributeValue
+ {
+ ///
+ /// Id of the DatabaseAttributeValue.
+ ///
+ [JsonProperty("id")]
+ public string Id { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/FieldBasedColorCoding.cs b/src/YouTrackSharp/Agiles/FieldBasedColorCoding.cs
new file mode 100644
index 00000000..f8526fe0
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/FieldBasedColorCoding.cs
@@ -0,0 +1,17 @@
+using Newtonsoft.Json;
+using YouTrackSharp.Projects;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Allows to set card's color based on a value of some custom field.
+ ///
+ public class FieldBasedColorCoding : ColorCoding
+ {
+ ///
+ /// Sets card color based on this custom field. Can be null.
+ ///
+ [JsonProperty("prototype")]
+ public CustomField Prototype { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/FieldStyle.cs b/src/YouTrackSharp/Agiles/FieldStyle.cs
new file mode 100644
index 00000000..c7f4a6e0
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/FieldStyle.cs
@@ -0,0 +1,28 @@
+using Newtonsoft.Json;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Represents the style settings of the field in YouTrack.
+ ///
+ public class FieldStyle
+ {
+ ///
+ /// Id of the FieldStyle.
+ ///
+ [JsonProperty("id")]
+ public string Id { get; set; }
+
+ ///
+ /// Background color. Read-only. Can be null.
+ ///
+ [JsonProperty("background")]
+ public string Background { get; set; }
+
+ ///
+ /// Foreground color. Read-only. Can be null.
+ ///
+ [JsonProperty("foreground")]
+ public string Foreground { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/FilterField.cs b/src/YouTrackSharp/Agiles/FilterField.cs
new file mode 100644
index 00000000..1760ff4f
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/FilterField.cs
@@ -0,0 +1,31 @@
+using Newtonsoft.Json;
+using YouTrackSharp.SerializationAttributes;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Represents an issue property, which can be a predefined field, a custom field, a link, and so on.
+ ///
+ [KnownType(typeof(PredefinedFilterField))]
+ [KnownType(typeof(CustomFilterField))]
+ public class FilterField
+ {
+ ///
+ /// Id of the FilterField.
+ ///
+ [JsonProperty("id")]
+ public string Id { get; set; }
+
+ ///
+ /// Presentation of the field. Read-only.
+ ///
+ [JsonProperty("presentation")]
+ public string Presentation { get; set; }
+
+ ///
+ /// The name of the field. Read-only.
+ ///
+ [JsonProperty("name")]
+ public string Name { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/IAgileService.cs b/src/YouTrackSharp/Agiles/IAgileService.cs
new file mode 100644
index 00000000..9b4393a5
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/IAgileService.cs
@@ -0,0 +1,27 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace YouTrackSharp.Agiles
+{
+ public interface IAgileService
+ {
+ ///
+ /// Retrieves the available agile boards from the server.
+ ///
+ ///
+ /// If the full representation of agile boards should be returned.
+ /// If this parameter is false, all the fields (and sub-fields) marked with the
+ /// are omitted (for more information, see
+ /// and related classes).
+ ///
+ ///
+ /// Uses the REST API
+ ///
+ /// Read a list of Agiles
+ ///
+ ///
+ /// A of available boards
+ /// When the call to the remote YouTrack server instance failed.
+ public Task> GetAgileBoards(bool verbose = false);
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/IssueBasedSwimlaneSettings.cs b/src/YouTrackSharp/Agiles/IssueBasedSwimlaneSettings.cs
new file mode 100644
index 00000000..b818c2dd
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/IssueBasedSwimlaneSettings.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using YouTrackSharp.Json;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Base entity for different swimlane settings
+ ///
+ public class IssueBasedSwimlaneSettings : SwimlaneSettings
+ {
+ ///
+ /// CustomField which values are used to identify swimlane.
+ ///
+ [JsonProperty("field")]
+ [JsonConverter(typeof(KnownTypeConverter))]
+ public FilterField Field { get; set; }
+
+ ///
+ /// Value of a field that a card would have by default. Can be null.
+ ///
+ [JsonProperty("defaultCardType")]
+ public SwimlaneValue DefaultCardType { get; set; }
+
+ ///
+ /// When issue has one of this values, it becomes a swimlane on this board.
+ ///
+ [JsonProperty("values")]
+ public List Values { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/PredefinedFilterField.cs b/src/YouTrackSharp/Agiles/PredefinedFilterField.cs
new file mode 100644
index 00000000..19e4a1a1
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/PredefinedFilterField.cs
@@ -0,0 +1,9 @@
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Represents a predefined field of the issue. Predefined fields are always present in an issue and |cannot be
+ /// customized in a project. For example, project, created, |updated, tags, and so on.
+ ///
+ public class PredefinedFilterField : FilterField
+ { }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/Project.cs b/src/YouTrackSharp/Agiles/Project.cs
new file mode 100644
index 00000000..42576e88
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/Project.cs
@@ -0,0 +1,29 @@
+using Newtonsoft.Json;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Represents a project in the context of an agile board. The class only provides the id, short name and name of the
+ /// project see for more info on how to access a YouTrack project.
+ ///
+ public class Project
+ {
+ ///
+ /// Id of the Project.
+ ///
+ [JsonProperty("id")]
+ public string Id { get; set; }
+
+ ///
+ /// The ID of the project. This short name is also a prefix for an issue ID. Can be null.
+ ///
+ [JsonProperty("shortName")]
+ public string ShortName { get; set; }
+
+ ///
+ /// The name of the issue folder. Can be null.
+ ///
+ [JsonProperty("name")]
+ public string Name { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/ProjectBasedColorCoding.cs b/src/YouTrackSharp/Agiles/ProjectBasedColorCoding.cs
new file mode 100644
index 00000000..13ede654
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/ProjectBasedColorCoding.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+using Newtonsoft.Json;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Allows to set card's color based on it's project
+ ///
+ public class ProjectBasedColorCoding : ColorCoding
+ {
+ ///
+ /// Collection of per-project color settings
+ ///
+ [JsonProperty("projectColors")]
+ public List ProjectColors { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/ProjectColor.cs b/src/YouTrackSharp/Agiles/ProjectColor.cs
new file mode 100644
index 00000000..e4280f2e
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/ProjectColor.cs
@@ -0,0 +1,28 @@
+using Newtonsoft.Json;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Represent color setting for one project on the board.
+ ///
+ public class ProjectColor
+ {
+ ///
+ /// Id of the ProjectColor.
+ ///
+ [JsonProperty("id")]
+ public string Id { get; set; }
+
+ ///
+ /// A project, to which color this setting describes. Read-only. Can be null.
+ ///
+ [JsonProperty("project")]
+ public Project Project { get; set; }
+
+ ///
+ /// A color, that issues of this project will have on the board. Read-only. Can be null.
+ ///
+ [JsonProperty("color")]
+ public FieldStyle Color { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/Sprint.cs b/src/YouTrackSharp/Agiles/Sprint.cs
new file mode 100644
index 00000000..b7a9485d
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/Sprint.cs
@@ -0,0 +1,23 @@
+using Newtonsoft.Json;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Represents a sprint in the context of an agile board. The class only provides the sprint's id and name, see for more info on how to access a YouTrack sprint.
+ ///
+ public class Sprint
+ {
+ ///
+ /// Id of the Sprint.
+ ///
+ [JsonProperty("id")]
+ public string Id { get; set; }
+
+ ///
+ /// Name of the sprint. Can be null.
+ ///
+ [JsonProperty("name")]
+ public string Name { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/SprintsSettings.cs b/src/YouTrackSharp/Agiles/SprintsSettings.cs
new file mode 100644
index 00000000..ab32bdea
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/SprintsSettings.cs
@@ -0,0 +1,64 @@
+using Newtonsoft.Json;
+using YouTrackSharp.Projects;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Describes sprints configuration.
+ ///
+ public class SprintsSettings
+ {
+ ///
+ /// Id of the SprintsSettings.
+ ///
+ [JsonProperty("id")]
+ public string Id { get; set; }
+
+ ///
+ /// If true, issues should be added to the board manually. If false, issues are shown on the board based on the
+ /// query and/or value of a field.
+ ///
+ [JsonProperty("isExplicit")]
+ public bool IsExplicit { get; set; }
+
+ ///
+ /// If true, cards can be present on several sprints of this board.
+ ///
+ [JsonProperty("cardOnSeveralSprints")]
+ public bool CardOnSeveralSprints { get; set; }
+
+ ///
+ /// New cards are added to this sprint by default. This setting applies only if isExplicit == true. Can be null.
+ ///
+ [JsonProperty("defaultSprint")]
+ public Sprint DefaultSprint { get; set; }
+
+ ///
+ /// If true, agile board has no distinct sprints in UI. However, in API it will look like it has only one active
+ /// (not-archived) sprint.
+ ///
+ [JsonProperty("disableSprints")]
+ public bool DisableSprints { get; set; }
+
+ ///
+ /// Issues that match this query will appear on the board. This setting applies only if isExplicit == false. Can be
+ /// null.
+ ///
+ [JsonProperty("explicitQuery")]
+ public string ExplicitQuery { get; set; }
+
+ ///
+ /// Based on the value of this field, issues will be assigned to the sprints. This setting applies only if
+ /// isExplicit == false. Can be null.
+ ///
+ [JsonProperty("sprintSyncField")]
+ public CustomField SprintSyncField { get; set; }
+
+ ///
+ /// If true, subtasks of the cards, that are present on the board, will be hidden if they match board query. This
+ /// setting applies only if isExplicit == false.
+ ///
+ [JsonProperty("hideSubtasksOfCards")]
+ public bool HideSubtasksOfCards { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/SwimlaneEntityAttributeValue.cs b/src/YouTrackSharp/Agiles/SwimlaneEntityAttributeValue.cs
new file mode 100644
index 00000000..d7773967
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/SwimlaneEntityAttributeValue.cs
@@ -0,0 +1,23 @@
+using Newtonsoft.Json;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Represents a single swimlane in case of AttributeBasedSwimlaneSettings.
+ ///
+ public class SwimlaneEntityAttributeValue : DatabaseAttributeValue
+ {
+ ///
+ /// Name of the swimlane. Can be null.
+ ///
+ [JsonProperty("name")]
+ public string Name { get; set; }
+
+ ///
+ /// If true, issues in this swimlane are considered to be resolved. Can be updated only for newly created value.
+ /// Read-only.
+ ///
+ [JsonProperty("isResolved")]
+ public bool IsResolved { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/SwimlaneSettings.cs b/src/YouTrackSharp/Agiles/SwimlaneSettings.cs
new file mode 100644
index 00000000..479bc7f9
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/SwimlaneSettings.cs
@@ -0,0 +1,25 @@
+using Newtonsoft.Json;
+using YouTrackSharp.SerializationAttributes;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Base entity for different swimlane settings
+ ///
+ [KnownType(typeof(AttributeBasedSwimlaneSettings))]
+ [KnownType(typeof(IssueBasedSwimlaneSettings))]
+ public class SwimlaneSettings
+ {
+ ///
+ /// Id of the SwimlaneSettings.
+ ///
+ [JsonProperty("id")]
+ public string Id { get; set; }
+
+ ///
+ /// Name of a value. Read-only. Can be null.
+ ///
+ [JsonProperty("name")]
+ public string Name { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/SwimlaneValue.cs b/src/YouTrackSharp/Agiles/SwimlaneValue.cs
new file mode 100644
index 00000000..5f2433a3
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/SwimlaneValue.cs
@@ -0,0 +1,22 @@
+using Newtonsoft.Json;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Represents single swimlane in case of IssueBasedSwimlaneSettings.
+ ///
+ public class SwimlaneValue
+ {
+ ///
+ /// Id of the SwimlaneValue.
+ ///
+ [JsonProperty("id")]
+ public string Id { get; set; }
+
+ ///
+ /// Name of a value. Read-only. Can be null.
+ ///
+ [JsonProperty("name")]
+ public string Name { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/User.cs b/src/YouTrackSharp/Agiles/User.cs
new file mode 100644
index 00000000..25f99515
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/User.cs
@@ -0,0 +1,30 @@
+using Newtonsoft.Json;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Represents a user in the context of an agile board. The class only provides the id, ringId (hub ID) and full name
+ /// of the user, see for more info on how to access a YouTrack user.
+ ///
+ public class User
+ {
+ ///
+ /// Id of the User.
+ ///
+ [JsonProperty("id")]
+ public string Id { get; set; }
+
+ ///
+ /// ID of the user in Hub. You can use this ID for operations in Hub, and for matching users between YouTrack and
+ /// Hub. Read-only. Can be null.
+ ///
+ [JsonProperty("ringId")]
+ public string RingId { get; set; }
+
+ ///
+ /// Full name of the user.
+ ///
+ [JsonProperty("fullName")]
+ public string FullName { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/UserGroup.cs b/src/YouTrackSharp/Agiles/UserGroup.cs
new file mode 100644
index 00000000..0c68e62e
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/UserGroup.cs
@@ -0,0 +1,52 @@
+using Newtonsoft.Json;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Represents a group of users.
+ ///
+ public class UserGroup
+ {
+ ///
+ /// Id of the UserGroup.
+ ///
+ [JsonProperty("id")]
+ public string Id { get; set; }
+
+ ///
+ /// The name of the group. Read-only. Can be null.
+ ///
+ [JsonProperty("name")]
+ public string Name { get; set; }
+
+ ///
+ /// ID of the group in Hub. Use this ID for operations in Hub, and for matching groups between YouTrack and Hub. Read-only. Can be null.
+ ///
+ [JsonProperty("ringId")]
+ public string RingId { get; set; }
+
+ ///
+ /// The number of users in the group. Read-only.
+ ///
+ [JsonProperty("userCount")]
+ public long UserCount { get; set; }
+
+ ///
+ /// The URL of the group icon. Read-only. Can be null.
+ ///
+ [JsonProperty("icon")]
+ public string Icon { get; set; }
+
+ ///
+ /// True if this group contains all users, otherwise false. Read-only.
+ ///
+ [JsonProperty("allUsersGroup")]
+ public bool AllUsersGroup { get; set; }
+
+ ///
+ /// Project that has this group set as a team. Returns null, if there is no such project. Read-only. Can be null.
+ ///
+ [JsonProperty("teamForProject")]
+ public Project TeamForProject { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Agiles/WIPLimit.cs b/src/YouTrackSharp/Agiles/WIPLimit.cs
new file mode 100644
index 00000000..3d31b1b7
--- /dev/null
+++ b/src/YouTrackSharp/Agiles/WIPLimit.cs
@@ -0,0 +1,28 @@
+using Newtonsoft.Json;
+
+namespace YouTrackSharp.Agiles
+{
+ ///
+ /// Represents WIP limits for particular column. If they are not satisfied, the column will be highlighted in UI.
+ ///
+ public class WIPLimit
+ {
+ ///
+ /// Id of the WIPLimit.
+ ///
+ [JsonProperty("id")]
+ public string Id { get; set; }
+
+ ///
+ /// Maximum number of cards in column. Can be null.
+ ///
+ [JsonProperty("max")]
+ public int? Max { get; set; }
+
+ ///
+ /// Minimum number of cards in column. Can be null.
+ ///
+ [JsonProperty("min")]
+ public int? Min { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/ConnectionExtensions.cs b/src/YouTrackSharp/ConnectionExtensions.cs
index 9258eebb..117018be 100644
--- a/src/YouTrackSharp/ConnectionExtensions.cs
+++ b/src/YouTrackSharp/ConnectionExtensions.cs
@@ -1,5 +1,7 @@
using System;
using YouTrackSharp.AgileBoards;
+using YouTrackSharp.Agiles;
+using YouTrackSharp.Internal;
using YouTrackSharp.Issues;
using YouTrackSharp.Management;
using YouTrackSharp.Projects;
@@ -71,6 +73,16 @@ public static IProjectCustomFieldsService ProjectCustomFieldsService(this Connec
{
return new ProjectCustomFieldsService(connection);
}
+
+ ///
+ /// Creates a .
+ ///
+ /// The to create a service with.
+ /// for working with YouTrack agile boards.
+ public static IAgileService CreateAgileService(this Connection connection)
+ {
+ return new AgileService(connection, new FieldSyntaxEncoder());
+ }
///
/// Creates a .
diff --git a/src/YouTrackSharp/Internal/Field.cs b/src/YouTrackSharp/Internal/Field.cs
new file mode 100644
index 00000000..31557d05
--- /dev/null
+++ b/src/YouTrackSharp/Internal/Field.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace YouTrackSharp.Internal
+{
+ ///
+ /// Simplified representation of an object's field.
+ /// It describes the name of the field and its sub-fields.
+ /// The 'subfields' are the members of the this field's type
+ ///
+ ///
+ /// This class allows to encode the recursive structure of an object's fields.
+ /// An object's field has a type, which itself has other members, each being of a type, and so on, down to the
+ /// primitive types.
+ ///
+ public class Field
+ {
+ ///
+ /// Name of the object's field
+ ///
+ public string Name { get; }
+
+ ///
+ /// Sub-fields of this field's type
+ ///
+ public List Subfields { get; }
+
+ ///
+ /// Creates an instance of , with the given name, type and subfields.
+ ///
+ /// Name of the field
+ /// Sub-fields of the field's type
+ public Field(string name, IEnumerable subfields)
+ {
+ Name = name;
+ Subfields = subfields.ToList();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Internal/FieldSyntaxEncoder.cs b/src/YouTrackSharp/Internal/FieldSyntaxEncoder.cs
new file mode 100644
index 00000000..53d210f1
--- /dev/null
+++ b/src/YouTrackSharp/Internal/FieldSyntaxEncoder.cs
@@ -0,0 +1,256 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Newtonsoft.Json;
+using YouTrackSharp.SerializationAttributes;
+
+namespace YouTrackSharp.Internal
+{
+ ///
+ /// Used to convert a given to the Youtrack REST API's 'fields' syntax, described at the following
+ /// location:
+ /// https://www.jetbrains.com/help/youtrack/standalone/api-fields-syntax.html
+ /// .
+ ///
+ public class FieldSyntaxEncoder
+ {
+ ///
+ /// Cached used to store types that were already resolved.
+ ///
+ ///
+ /// The result of the field query can vary depending on the verbose option and the max nesting level.
+ /// Therefore, the key used is a combination of the , verbosity and max depth.
+ ///
+ private Dictionary, string> Cache { get; }
+
+ ///
+ /// Creates an instance of
+ ///
+ public FieldSyntaxEncoder()
+ {
+ Cache = new Dictionary, string>();
+ }
+
+ ///
+ /// Encodes the given 's members to the Youtrack REST API's 'fields'-syntax, described at
+ ///
+ /// https://www.jetbrains.com/help/youtrack/standalone/api-fields-syntax.html
+ /// .
+ /// This method uses reflexion to retrieve and encodes all public writable properties, decorated with a
+ /// .
+ /// It also concatenates the fields retrieved from known subclasses of the given type (defined by the
+ /// ).
+ /// Because the base class and the different subclasses may have fields with similar names (but different types and
+ /// sub-fields), all the fields discovered this way are "factored" together. Therefore, if two known subclasses both
+ /// have a field with the same name, but each with different types and so, different sub-fields, the two fields will
+ /// be factored into one containing the union of their respective sub-fields.
+ /// For example:
+ /// - The first subclass has a field named A with subfields B, C and D, ie. A(B,C,D)
+ /// - The second subclass also has a field A but with subfields D,E and F, ie. A(D,E,F)
+ /// In that case, these two fields will be combined into one, with name A and subfields B, C, D, E and F, ie.
+ /// A(B,C,D,E,F). Note that D appears only once.
+ ///
+ /// Type to encode
+ ///
+ /// Verbosity. If true, will include all public writable properties of the given , marked
+ /// with the .
+ /// But if false, it will exclude from these all the ones that were marked with the
+ /// .
+ ///
+ /// Max sub-field depth to go from the given type.
+ /// Fields of the given type, encoded to Youtrack
+ /// REST API's 'field' syntax
+ ///
+ ///
+ ///
+ /// This method does not work well with types that have (direct or indirect) reference loops.
+ /// For example, a class A that contains two fields of type B and C, where the type B itself contains a field of
+ /// type A. This would result in a stack-overflow.
+ ///
+ public string Encode(Type type, bool verbose = true, int maxDepth = Int32.MaxValue)
+ {
+ type = GetLeafType(type);
+
+ Tuple key = new Tuple(type, verbose, maxDepth);
+
+ if (Cache.ContainsKey(key))
+ {
+ return Cache[key];
+ }
+
+ IEnumerable fields = GetFields(type, verbose, maxDepth, 0);
+
+ Cache[key] = string.Join(",", fields.Select(ToString));
+
+ return Cache[key];
+ }
+
+ ///
+ /// Converts the recursive structure into a string representation, following Youtrack REST API's
+ /// 'field' syntax
+ /// Example: lines(fromPoint(x,y),toPoint(x,y)),color
+ ///
+ /// to convert
+ ///
+ private string ToString(Field field)
+ {
+ if (!field.Subfields.Any())
+ {
+ return field.Name;
+ }
+
+ string subfields = string.Join(",", field.Subfields.Select(ToString));
+
+ return $"{field.Name}({subfields})";
+ }
+
+ ///
+ /// Recursive method which builds the structure for the given 's properties.
+ /// This method uses reflexion to convert all the public writable properties of teh given ,
+ /// decorated with a to a .
+ /// It also retrieves the fields from known subclasses of the given type (defined by the
+ /// ).
+ /// Because the base class and the different subclasses may have fields with similar names (but different types and
+ /// sub-fields), all the fields discovered this way are "factored" together. Therefore, if two known subclasses both
+ /// have a field with the same name, but different type, each with different sub-fields, the two fields will
+ /// be factored into one containing the union of their respective sub-fields.
+ /// For example:
+ /// - The first subclass has a field named A with subfields B, C and D, ie. A(B,C,D)
+ /// - The second subclass also has a field A but with subfields D,E and F, ie. A(D,E,F)
+ /// In that case, these two fields will be combined into one, with name A and subfields B, C, D, E and F, ie.
+ /// A(B,C,D,E,F). Note that D appears only once.
+ ///
+ /// Type to convert
+ ///
+ /// Verbosity. If true, will include all public writable properties of the given , marked
+ /// with the .
+ /// But if false, it will exclude from these all the ones that were marked with the
+ /// .
+ ///
+ /// Max sub-field depth to go from the given type.
+ /// Current sub-field level (to compare with )
+ /// Fields of the given type, encoded to Youtrack
+ /// REST API's 'field' syntax
+ ///
+ ///
+ ///
+ /// This method does not work well with types that have (direct or indirect) reference loops.
+ /// For example, a class A that contains two fields of type B and C, where the type B itself contains a field of
+ /// type A. This would result in a stack-overflow.
+ ///
+ private IEnumerable GetFields(Type type, bool verbose, int maxDepth, int level)
+ {
+ if (level == maxDepth)
+ {
+ return Enumerable.Empty();
+ }
+
+ List fields = new List();
+
+ IEnumerable properties = GetProperties(type, verbose);
+ foreach (PropertyInfo propertyInfo in properties)
+ {
+ Type propertyType = GetLeafType(propertyInfo.PropertyType);
+ string propertyName = propertyInfo.GetCustomAttribute()?.PropertyName;
+
+ IEnumerable subfields = GetFields(propertyType, verbose, maxDepth, level + 1);
+
+ fields.Add(new Field(propertyName, subfields));
+ }
+
+ return Merge(fields);
+ }
+
+ ///
+ /// Merges the given fields into a factored representation, where fields with the same name are combined into one.
+ /// For example, if there are two fields named "A"
+ /// - The first with subfields B, C and D, ie. A(B,C,D)
+ /// - The second with subfields D,E and F, ie. A(D,E,F)
+ /// These two fields will be combined into one, with name A and subfields B, C, D, E and F, ie.
+ /// A(B,C,D,E,F). Note that D appears only once.
+ ///
+ /// s to factor
+ /// Factored fields
+ private IEnumerable Merge(IEnumerable fields)
+ {
+ IEnumerable> groups = fields.GroupBy(field => field.Name);
+
+ foreach (IGrouping group in groups)
+ {
+ IEnumerable subfields = group.SelectMany(f => f.Subfields);
+ subfields = Merge(subfields);
+
+ yield return new Field(group.Key, subfields);
+ }
+ }
+
+ ///
+ /// Retrieves the of all public, writable properties marked with a
+ /// of the given and
+ /// all of its known subclasses (defined by the .
+ /// If is set to false, it will exclude the ones marked with a
+ /// .
+ ///
+ /// from which to retrieve the properties
+ ///
+ /// If false the properties marked with will be excluded from the results.
+ ///
+ ///
+ /// The of all public, writable properties marked with a
+ /// of the given and
+ /// all of its known subclasses.
+ ///
+ private IEnumerable GetProperties(Type type, bool verbose)
+ {
+ PropertyInfo[] properties = type.GetProperties();
+
+ IEnumerable propertyInfos = properties.Where(p => p.CanWrite)
+ .Where(
+ p =>
+ p.GetCustomAttribute() !=
+ null);
+
+ if (!verbose)
+ {
+ propertyInfos = propertyInfos.Where(p => p.GetCustomAttribute() == null);
+ }
+
+ IEnumerable knownSubtypes =
+ type.GetCustomAttributes(false).Select(attr => attr.Type);
+ IEnumerable subtypesProperties =
+ knownSubtypes.SelectMany(subtype => GetProperties(subtype, verbose));
+
+ return propertyInfos.Concat(subtypesProperties);
+ }
+
+ ///
+ /// Returns the "leaf" type argument of any generic type.
+ /// Note: Generic types are nested, the "leaf" meaning the non-generic type at the bottom of the nesting chain
+ ///
+ /// For example, a of , will yield .
+ /// And a of of
+ /// will also yield .
+ ///
+ /// For generic types with multiple type parameters, it will always follow the last, which is usually the value type.
+ /// For example, a of key and value ,
+ /// will yield .
+ /// A of key and value of
+ /// , will also yield .
+ ///
+ /// Input
+ /// Leaf type if the input type is generic, otherwise itself
+ ///
+ /// The returned type is guaranteed to be non-generic.
+ ///
+ private Type GetLeafType(Type type)
+ {
+ if (!type.IsGenericType)
+ {
+ return type;
+ }
+
+ return GetLeafType(type.GetGenericArguments().Last());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Json/KnownTypeConverter.cs b/src/YouTrackSharp/Json/KnownTypeConverter.cs
new file mode 100644
index 00000000..5bbb793f
--- /dev/null
+++ b/src/YouTrackSharp/Json/KnownTypeConverter.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using YouTrackSharp.SerializationAttributes;
+
+namespace YouTrackSharp.Json
+{
+ ///
+ /// This allows to convert a JSON string into a concrete object that extends a given base
+ /// type.
+ /// This is used in conjunction with the , defined on
+ /// the base class, and which lists the possible sub-types of that class.
+ /// Among these, the concrete sub-type instantiated is inferred from the Json object's "$type" field, which is
+ /// compared to the defined known types (ignoring their namespace).
+ /// If the "$type" field is undefined, or no matching the "$type" parameter is
+ /// found, the base class is instantiated instead.
+ ///
+ /// Base type
+ public class KnownTypeConverter : JsonConverter where T : new()
+ {
+ private readonly TypedJObjectConverter _objectConverter;
+
+ ///
+ public override bool CanRead => true;
+
+ ///
+ public override bool CanWrite => false;
+
+ public KnownTypeConverter()
+ {
+ _objectConverter = new TypedJObjectConverter();
+ }
+
+ ///
+ public override void WriteJson(JsonWriter writer, T value, JsonSerializer serializer)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// Reads the JSON representation of the object.
+ /// This method will instanciate a subclass of the given , based on the "$type" parameter
+ /// of the json string, which is compared to defined for that base class.
+ /// If the "$type" parameter is not defined, or no matching is found, the json is
+ /// deserialized to an instance of the base class directly.
+ ///
+ /// The to read from.
+ /// Type of the object.
+ /// The existing value of object being read. If there is no existing value then null will be used.
+ /// The existing value has a value.
+ /// The calling serializer.
+ /// The object value.
+ public override T ReadJson(JsonReader reader, Type objectType, T existingValue, bool hasExistingValue,
+ JsonSerializer serializer)
+ {
+ if (reader.TokenType == JsonToken.Null)
+ {
+ return default;
+ }
+
+ List types = typeof(T).GetCustomAttributes().Select(attr => attr.Type).ToList();
+
+ JObject obj = JObject.Load(reader);
+
+ return _objectConverter.ReadObject(obj, types, serializer);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Json/KnownTypeListConverter.cs b/src/YouTrackSharp/Json/KnownTypeListConverter.cs
new file mode 100644
index 00000000..0d773245
--- /dev/null
+++ b/src/YouTrackSharp/Json/KnownTypeListConverter.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using YouTrackSharp.SerializationAttributes;
+
+namespace YouTrackSharp.Json
+{
+ ///
+ /// This class allows to convert a JSON array into a list of objects, each extending a given base type.
+ /// This is used in conjunction with the , defined on
+ /// the base class, and which lists the possible sub-types of that class.
+ /// Among these, the concrete sub-type instantiated is inferred from each Json object's "$type" field, which is
+ /// compared to the defined known types (ignoring their namespace).
+ /// If the "$type" field is undefined, or no matching the "$type" parameter is
+ /// found, the base class is instantiated instead.
+ ///
+ /// Base type
+ public class KnownTypeListConverter : JsonConverter> where T : new()
+ {
+ private readonly TypedJObjectConverter _objectConverter;
+
+ ///
+ public override bool CanRead => true;
+
+ ///
+ public override bool CanWrite => false;
+
+ public KnownTypeListConverter()
+ {
+ _objectConverter = new TypedJObjectConverter();
+ }
+
+ ///
+ public override void WriteJson(JsonWriter writer, List value, JsonSerializer serializer)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// Reads the JSON representation of the object.
+ /// This method will instanciate a subclass of the given , based on the "$type" parameter
+ /// of the json string, which is compared to defined for that base class.
+ /// If the "$type" parameter is not defined, or no matching is found, the json is
+ /// deserialized to an instance of the base class directly.
+ ///
+ /// The to read from.
+ /// Type of the object.
+ /// The existing value of object being read. If there is no existing value then null will be used.
+ /// The existing value has a value.
+ /// The calling serializer.
+ /// The object value.
+ public override List ReadJson(JsonReader reader, Type objectType, List existingValue,
+ bool hasExistingValue, JsonSerializer serializer)
+ {
+ if (reader.TokenType == JsonToken.Null)
+ {
+ return null;
+ }
+
+ List knownTypes =
+ typeof(T).GetCustomAttributes().Select(attr => attr.Type).ToList();
+
+ JArray array = JArray.Load(reader);
+
+ return array.Select(token => _objectConverter.ReadObject(token as JObject, knownTypes, serializer))
+ .ToList();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Json/TypedJObjectConverter.cs b/src/YouTrackSharp/Json/TypedJObjectConverter.cs
new file mode 100644
index 00000000..e66d75da
--- /dev/null
+++ b/src/YouTrackSharp/Json/TypedJObjectConverter.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace YouTrackSharp.Json
+{
+ public class TypedJObjectConverter
+ {
+ public T ReadObject(JObject token, List knownTypes, JsonSerializer serializer)
+ {
+ if (token == null)
+ {
+ throw new ArgumentException("Invalid token");
+ }
+
+ string jsonType = token["$type"]?.ToString();
+ if (jsonType == null)
+ {
+ return token.ToObject(serializer);
+ }
+
+ Type type = knownTypes.FirstOrDefault(t => t.Name.EndsWith(jsonType));
+
+ if (type == null)
+ {
+ return token.ToObject(serializer);
+ }
+
+ return (T)token.ToObject(type, serializer);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/SerializationAttributes/KnownTypeAttribute.cs b/src/YouTrackSharp/SerializationAttributes/KnownTypeAttribute.cs
new file mode 100644
index 00000000..317b51a8
--- /dev/null
+++ b/src/YouTrackSharp/SerializationAttributes/KnownTypeAttribute.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace YouTrackSharp.SerializationAttributes
+{
+ ///
+ /// Attribute used to decorate a parent class with its known subtypes.
+ /// This is used to identify candidate subclasses when generating REST requests (to include the subclasses'
+ /// properties).
+ ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
+ public class KnownTypeAttribute : Attribute
+ {
+ ///
+ /// Known subclass's type
+ ///
+ public Type Type { get; }
+
+ ///
+ /// Creates an instance of the , with the given subclass' type.
+ ///
+ ///
+ public KnownTypeAttribute(Type type)
+ {
+ Type = type;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/SerializationAttributes/VerboseAttribute.cs b/src/YouTrackSharp/SerializationAttributes/VerboseAttribute.cs
new file mode 100644
index 00000000..80b94321
--- /dev/null
+++ b/src/YouTrackSharp/SerializationAttributes/VerboseAttribute.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace YouTrackSharp.SerializationAttributes
+{
+ ///
+ /// This attribute is used to mark a property as being only retrieved when verbose information was requested.
+ ///
+ [AttributeUsage(AttributeTargets.Property)]
+ public class VerboseAttribute : Attribute
+ { }
+}
\ No newline at end of file
diff --git a/tests/YouTrackSharp.Tests/Infrastructure/ConnectionStub.cs b/tests/YouTrackSharp.Tests/Infrastructure/ConnectionStub.cs
new file mode 100644
index 00000000..22696a18
--- /dev/null
+++ b/tests/YouTrackSharp.Tests/Infrastructure/ConnectionStub.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+namespace YouTrackSharp.Tests.Infrastructure
+{
+ ///
+ /// Connection mock used to return predefined HTTP responses, for testing purposes.
+ ///
+ public class ConnectionStub : Connection
+ {
+ private readonly HttpClientHandler _handler;
+ private TimeSpan TimeOut => TimeSpan.FromSeconds(100);
+
+ ///
+ /// Creates an instance of with give response delegate
+ ///
+ ///
+ /// to associate to this connection.
+ /// This can be used to pass a stub handler for testing purposes.
+ ///
+ public ConnectionStub(HttpClientHandler handler) : base("http://fake.connection.com/")
+ {
+ _handler = handler;
+ }
+
+ ///
+ /// Creates an configured to return a predefined message and HTTP status
+ /// on request.
+ ///
+ /// configured to return a predefined message and HTTP status
+ public override Task GetAuthenticatedHttpClient()
+ {
+ HttpClient httpClient = new HttpClient(_handler);
+ httpClient.BaseAddress = ServerUri;
+ httpClient.Timeout = TimeOut;
+
+ return Task.FromResult(httpClient);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/YouTrackSharp.Tests/Infrastructure/Connections.cs b/tests/YouTrackSharp.Tests/Infrastructure/Connections.cs
index 2a45479b..94d5a0ab 100644
--- a/tests/YouTrackSharp.Tests/Infrastructure/Connections.cs
+++ b/tests/YouTrackSharp.Tests/Infrastructure/Connections.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using System.Net;
using System.Net.Http;
using System.Security.Authentication;
@@ -22,7 +23,7 @@ public static string ServerUrl
public static Connection Demo3Token =>
new BearerTokenConnection(ServerUrl, "perm:ZGVtbzM=.WW91VHJhY2tTaGFycA==.L04RdcCnjyW2UPCVg1qyb6dQflpzFy", ConfigureTestsHandler);
-
+
public static class TestData
{
public static readonly List
\ No newline at end of file