DevOp.Jira 0.2.0

dotnet add package DevOp.Jira --version 0.2.0
                    
NuGet\Install-Package DevOp.Jira -Version 0.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DevOp.Jira" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DevOp.Jira" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="DevOp.Jira" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DevOp.Jira --version 0.2.0
                    
#r "nuget: DevOp.Jira, 0.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DevOp.Jira@0.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DevOp.Jira&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=DevOp.Jira&version=0.2.0
                    
Install as a Cake Tool

DevOp.Jira

A practical .NET client for Jira Cloud.

DevOp.Jira is built for application code that needs to talk to Jira without becoming a pile of hand-written URLs and anonymous JSON objects. It gives you a typed client, a fluent issue query API, LINQ-to-JQL translation, raw JQL support, and wrappers for the Jira operations teams usually automate: transitions, comments, attachments, worklogs, issue links, watchers, votes, users, spaces, metadata, and JQL helper endpoints.

Install

dotnet add package DevOp.Jira

First Request

using DevOp.Jira;

var options = new JiraClientOptions
{
    BaseUrl = new Uri("https://your-domain.atlassian.net"),
    Email = "you@example.com",
    ApiToken = "your-api-token"
};

using var client = new JiraClient(options);

var issue = await client.Issues.GetAsync("PROJ-123");
Console.WriteLine(issue?.Summary);

Dependency Injection

using DevOp.Jira.Extensions;

services.AddJiraClient(options =>
{
    options.BaseUrl = new Uri("https://your-domain.atlassian.net");
    options.Email = "you@example.com";
    options.ApiToken = "your-api-token";
});

Configuration binding is also supported:

services.AddJiraClient(configuration, sectionName: "Jira");

Example appsettings.json:

{
  "Jira": {
    "BaseUrl": "https://your-domain.atlassian.net",
    "Email": "you@example.com",
    "ApiToken": "your-api-token",
    "UserAgent": "DevOp.Jira/1.0",
    "Timeout": "00:00:30",
    "MaxPageSize": 300,
    "RetryCount": 3,
    "RetryDelay": "00:00:00.200",
    "RetryMaxDelay": "00:00:05",
    "DefaultIssueTypeName": "Task",
    "DefaultPriorityName": "Medium",
    "TeamFieldKey": "customfield_10001",
    "CustomFieldMappings": {
      "Customer": "customfield_12345",
      "Team": "customfield_67890"
    }
  }
}

Options are validated when the client is created or resolved from dependency injection.

Query Issues Naturally

The most comfortable path is the fluent query builder:

var issues = await client.Issues.Query()
    .ForSpace("PROJ")
    .OpenSprints()
    .AssignedToCurrentUser()
    .Take(50)
    .ToListAsync();

Common triage queries stay readable:

var staleUnassigned = await client.Issues.Query()
    .ForSpace("PROJ")
    .Unassigned()
    .UpdatedBefore(DateTime.UtcNow.AddDays(-14))
    .WithoutLabel("ignore")
    .ToListAsync();

var overdueHighPriority = await client.Issues.Query()
    .ForSpace("PROJ")
    .Overdue()
    .ForPriority("High", "Highest")
    .ToListAsync();

LINQ predicates are translated to JQL:

var recentDone = await client.Issues.Query()
    .Where(x => x.Space == "PROJ" && x.Status == "Done" && x.Created >= DateTime.UtcNow.AddDays(-30))
    .OrderByDescending(x => x.Created)
    .ToListAsync();

Projection is supported when you only need part of the model:

var rows = await client.Issues.Query()
    .ForSpace("PROJ")
    .ForStatus("In Progress", "Blocked")
    .Select(issue => new
    {
        issue.Key,
        issue.Summary,
        Assignee = issue.Assignee?.DisplayName
    })
    .ToListAsync();

Raw JQL is available when you want full control:

var page = await client.Issues.SearchAsync(
    "project = PROJ AND statusCategory != Done ORDER BY updated DESC",
    maxResults: 50);

var all = await client.Issues.SearchAllAsync(
    "project = PROJ AND labels in (customer-impact)",
    pageSize: 100);

Jira Cloud issue search uses nextPageToken; this client follows that model instead of reintroducing startAt paging for /rest/api/3/search/jql.

Common Issue Workflows

Move an issue through its workflow:

var transitions = await client.Issues.GetTransitionsAsync("PROJ-123");
var done = transitions.FirstOrDefault(x => x.Name == "Done");

if (done?.Id is not null)
{
    await client.Issues.TransitionAsync("PROJ-123", done.Id);
}

Work with comments:

var comment = await client.Issues.AddCommentAsync(
    "PROJ-123",
    "Checked the latest deployment logs.");

await client.Issues.UpdateCommentAsync(
    "PROJ-123",
    comment.Id!,
    "Updated with the final result.");

await client.Issues.DeleteCommentAsync("PROJ-123", comment.Id!);

Store small automation state with Jira issue entity properties:

await client.Issues.SetPropertyAsync(
    "PROJ-123",
    "devop.audit",
    new { source = "deployment", deploymentId = "deploy-42" });

var audit = await client.Issues.GetPropertyAsync<Dictionary<string, object>>(
    "PROJ-123",
    "devop.audit");

Attach files and include them in search results:

await client.Issues.AddAttachmentAsync("PROJ-123", "./diagnostics.txt");

var issue = await client.Issues.Query()
    .IncludeAttachments()
    .Where(x => x.Key == "PROJ-123")
    .ToListAsync();

Link issues, inspect watchers, and vote:

await client.Issues.LinkAsync("PROJ-123", "PROJ-456", "Relates");

var watchers = await client.Issues.GetWatchersAsync("PROJ-123");
await client.Issues.AddWatcherAsync("PROJ-123", "account-id");

var votes = await client.Issues.GetVotesAsync("PROJ-123");
await client.Issues.AddVoteAsync("PROJ-123");

Worklogs

var created = await client.Worklogs.AddAsync(
    "PROJ-123",
    "Investigated customer report",
    timeSpentSeconds: 3600);

created.Comment = "Updated investigation notes";
await client.Worklogs.UpdateAsync("PROJ-123", created.Id!, created);

var recent = await client.Worklogs.Query()
    .ForIssue("PROJ-123")
    .StartedAfter(DateTime.UtcNow.AddDays(-14))
    .ToListAsync();

Spaces And Users

Atlassian's UI calls projects "Spaces" in many places. The Jira REST API and JQL still use project terminology. DevOp.Jira exposes client.Spaces for the user-facing concept and handles the Jira project endpoints internally.

var spaces = await client.Spaces.GetAllAsync();
var space = await client.Spaces.GetAsync("PROJ");
var versions = await client.Spaces.GetVersionsAsync("PROJ");
var components = await client.Spaces.GetComponentsAsync("PROJ");

var me = await client.Users.GetCurrentAsync();
var assignable = await client.Users.GetAssignableAsync("PROJ", query: "alex");

Metadata

Use client.Metadata for Jira catalog data:

var priorities = await client.Metadata.GetPrioritiesAsync();
var resolutions = await client.Metadata.GetResolutionsAsync();
var statuses = await client.Metadata.SearchStatusesAsync(searchString: "done");
var role = await client.Metadata.GetSpaceRoleAsync("PROJ", roleId: 10002);

Custom Fields

Jira custom field IDs differ by site. Configure mappings once:

services.AddJiraClient(options =>
{
    options.BaseUrl = new Uri("https://your-domain.atlassian.net");
    options.Email = "you@example.com";
    options.ApiToken = "your-api-token";
    options.CustomFieldMappings["Customer"] = "customfield_12345";
});

Then query and read using friendly names:

var byCustomer = await client.Issues.Query()
    .ForCustomField("Customer", "ACME")
    .ToListAsync();

var issue = await client.Issues.GetAsync("PROJ-123");
var customer = issue?.GetPropertyString("Customer");

Mappings can also be checked against Jira:

var customerFieldKey = await client.Metadata.ResolveCustomFieldKeyAsync("Customer");

JQL Helper Endpoints

var autocomplete = await client.Jql.GetAutocompleteDataAsync();

var parsed = await client.Jql.ParseAsync(new[]
{
    "project = PROJ AND status = Done"
});

var matches = await client.Jql.MatchAsync(
    issueIds: new[] { 10001L, 10002L },
    jqls: new[] { "status = Done" });

The helper methods return typed models. Use the matching Json methods, such as ParseJsonAsync, when you need the raw Jira response.

Error Handling

Jira failures throw JiraRequestException with the status code, reason phrase, response body, request URL, HTTP method, and parsed Jira error details when available.

try
{
    await client.Issues.GetAsync("PROJ-404");
}
catch (JiraRequestException ex)
{
    Console.WriteLine($"{(int)ex.StatusCode} {ex.ReasonPhrase}");
    Console.WriteLine(string.Join(Environment.NewLine, ex.ErrorMessages));
}

Notes

  • Jira Cloud only.
  • Authentication uses Basic auth with email and API token.
  • Markdown issue descriptions, comments, and worklog comments are converted to Jira ADF for writes.
  • Issue descriptions and comments are normalized back to Markdown where supported.
  • The repository includes a runnable console sample and package validation script.
  • The repository tracks a public API baseline so intentional breaking changes are visible.
  • Target frameworks: netstandard2.1, net8.0, and net10.0.
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0 107 4/26/2026

Renames the Jira Cloud client package to DevOp.Jira.
     - Breaking renames the NuGet package from DevOp.Jira.Client to DevOp.Jira
     - Breaking moves public namespaces from DevOp.Jira.Client to DevOp.Jira
     - Updates generated DTO, test, documentation, and CI paths for the new package identity
     - Improves package validation for DevOp.Jira artifacts and XML documentation