Jezda.Common.Integrations.AzureDevOps
2.0.0
See the version list below for details.
dotnet add package Jezda.Common.Integrations.AzureDevOps --version 2.0.0
NuGet\Install-Package Jezda.Common.Integrations.AzureDevOps -Version 2.0.0
<PackageReference Include="Jezda.Common.Integrations.AzureDevOps" Version="2.0.0" />
<PackageVersion Include="Jezda.Common.Integrations.AzureDevOps" Version="2.0.0" />
<PackageReference Include="Jezda.Common.Integrations.AzureDevOps" />
paket add Jezda.Common.Integrations.AzureDevOps --version 2.0.0
#r "nuget: Jezda.Common.Integrations.AzureDevOps, 2.0.0"
#:package Jezda.Common.Integrations.AzureDevOps@2.0.0
#addin nuget:?package=Jezda.Common.Integrations.AzureDevOps&version=2.0.0
#tool nuget:?package=Jezda.Common.Integrations.AzureDevOps&version=2.0.0
Jezda.Common.Integrations.AzureDevOps
A strongly-typed .NET client for Azure DevOps REST API, built on top of Jezda.Common.Integrations.
This library simplifies interaction with Azure DevOps by providing easy-to-use methods for Work Items and WIQL queries.
✨ Features
- Work Item Management: Create and retrieve work items.
- WIQL Support: Execute raw WIQL queries and get typed results.
- Project Management: List all projects in the organization.
- Dependency Injection: Ready-to-use
IServiceCollectionextension.
📦 Installation
Add the project reference or NuGet package:
dotnet add package Jezda.Common.Integrations.AzureDevOps
⚙️ Configuration
Add the following section to your appsettings.json:
{
"Integrations": {
"AzureDevOps": {
"BaseUrl": "https://dev.azure.com/your-organization/",
"PersonalAccessToken": "your-pat-token",
"ApiVersion": "7.1"
}
}
}
🚀 Usage
1. Register Services
In your Program.cs:
using Jezda.Common.Integrations.Extensions;
builder.Services.AddAzureDevOpsIntegration(builder.Configuration);
2. Inject and Use
Inject IAzureDevOpsClient into your service:
public class MyService
{
private readonly IAzureDevOpsClient _adoClient;
public MyService(IAzureDevOpsClient adoClient)
{
_adoClient = adoClient;
}
public async Task SyncDataAsync()
{
// 1. Get all projects
var projects = await _adoClient.GetProjectsAsync();
// 2. Get work items via WIQL
var workItems = await _adoClient.GetWorkItemsByQueryAsync(
"SELECT [System.Id] FROM WorkItems WHERE [System.State] = 'Active'");
}
}
GetWorkItemsByQueryAsync takes flat queries only. A link query (FROM WorkItemLinks) throws
NotSupportedException. The result can hold fewer items than the query matched: an item deleted or
made unreadable after the query ran is left out, with a warning in the log that names the
organisation and the missing ids. A batch answered without a list throws instead, because it says
nothing about the items in it.
🔐 Errors and authentication
Both IAzureDevOpsClient and the IExternalTaskProvider for Azure DevOps report failures as
HttpRequestException with StatusCode set, so a caller can tell a rejected token from anything else:
| What happened | Result |
|---|---|
| Token rejected (401, 403) | StatusCode 401 / 403 |
Sign-in page instead of JSON (a redirect to /_signin, or 203 without JSON) |
StatusCode 401; for a redirect, the message names the target's host and path. |
| A body that is not JSON on any other success status (the base URL points at a website, a proxy or a captive portal answered), 204 included: every call reads JSON, so an empty answer is not read as no results or not found. | StatusCode is the success status itself, e.g. 200 or 204. The token was not checked, so this is not 401. |
Work item batch answered JSON without a list (a null body, or no value) |
StatusCode is null: the answer was a success, so the status says nothing. It is thrown after the response is read, so the resilience pipeline does not retry it. The message names the organisation and the batch's first and last id. |
| Base URL redirects anywhere else (3xx, e.g. after an organisation rename) | StatusCode is the 3xx; the message names the target. Redirects are not followed, because .NET drops Authorization on the next hop and a valid PAT would read as expired. |
| Any other failure | Its own status, or the original exception for dropped connections and cancellation. A timeout on IAzureDevOpsClient is TaskCanceledException from HttpClient.Timeout. |
Resilience pipeline on the IExternalTaskProvider client gave up |
Polly's TimeoutRejectedException, BrokenCircuitException or RateLimiterRejectedException. None derives from HttpRequestException, so also catch their common base, ExecutionRejectedException. |
ValidateConnectionAsync returns a rejection only for a rejected token. Every other failure propagates, so an
outage is not shown to the user as a bad PAT.
| Answer | On _apis/projects |
On _apis/wit/fields |
|---|---|---|
| 401 | InvalidToken |
MissingScope |
| 403 | AccessDenied |
MissingScope |
| sign-in page or redirect to one | SignInPage |
MissingScope |
The work item check runs only once projects passed, with the same token from the same address, so any refusal
there is the missing "Work Items (Read)" scope, not a bad token, Conditional Access or an address block. For a sign-in page,
Detail names the redirect target (host and path), which tells Azure DevOps from a gateway with its own
/_signin.
A 403 on _apis/projects is not always about the token: Conditional Access, an IP allow-list or a WAF can answer it for a valid
PAT. Validation still returns a rejection, AccessDenied, and logs a warning with the start of the response body, which tells these
apart.
Validation sends two requests: _apis/projects, which needs "Project and Team (Read)", and _apis/wit/fields,
which needs "Work Items (Read)". A PAT missing either scope returns a rejection, instead of validating and failing
later on the first work item call.
Keeping redirects off
AddAzureDevOpsIntegration turns off redirects on the default primary handler. Without that, a redirect drops
Authorization and a valid PAT reads as expired. It also sets the X-TFS-FedAuthRedirect: Suppress header, which
asks for 401 instead of a redirect to sign-in. Four setups need care:
- A custom primary handler registered before
AddAzureDevOpsIntegration: one that is notSocketsHttpHandlerorHttpClientHandlerstays as it is, and a warning is logged. Make sure it does not follow redirects. ConfigurePrimaryHttpMessageHandlercalled afterAddAzureDevOpsIntegration: the handler replaces the configured one and nothing warns, so setAllowAutoRedirect = falseon it yourself.AzureDevOpsClientgiven anHttpClientmade outside DI: setAllowAutoRedirect = falseon its handler, and send the header yourself.AzureDevOpsTaskProviderbuilt on your ownIHttpClientFactory: the same two apply to the client it creates underAzureDevOpsTaskProvider.HttpClientName.
🛠️ Key Models
AdoWorkItem: Represents a simplified work item with genericFieldsdictionary.Urlis the REST resource, which answers JSON;HtmlUrlis the page a person opens (_links.html.href, asked for with$expand=links), null when the answer had no links.AdoProject: Contains project details (Id, Name, Description, Url).
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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. |
-
net10.0
- Jezda.Common.Integrations (>= 2.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.9)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.