C#/.NET | Enterprise API Integration Toolkit
A NuGet-ready C# library providing typed, production-hardened HTTP clients for ServiceNow, Jira, and PagerDuty — the three APIs at the core of most enterprise AIOps and IT automation platforms.
Built as a reusable library with Polly retry pipelines, IMemoryCache response caching, and a clean DI registration extension.
| Client | Operations |
|---|---|
IServiceNowClient |
Create incident, get incident (cached), update incident (PATCH by sys_id) |
IJiraClient |
Search issues (JQL), get issue (cached), create issue (ADF description) |
IPagerDutyClient |
List incidents (cached), acknowledge incident, get on-call user |
ApiToolkit/
├── Clients/
│ ├── IServiceNowClient.cs Typed interface
│ ├── ServiceNowClient.cs Basic auth, REST v2, cache, Polly
│ ├── IJiraClient.cs
│ ├── JiraClient.cs Basic auth, REST v3, ADF builder, cache
│ ├── IPagerDutyClient.cs
│ └── PagerDutyClient.cs Token auth, From: header, cache
├── Configuration/
│ └── ApiToolkitOptions.cs Strongly-typed options (ServiceNow/Jira/PD/Resilience/Cache)
├── Exceptions/
│ └── EnterpriseApiException.cs Typed exception with Platform + StatusCode
├── Extensions/
│ └── ServiceCollectionExtensions.cs AddEnterpriseClients() DI extension
├── Models/
│ ├── ServiceNowModels.cs Records + internal DTOs
│ ├── JiraModels.cs
│ └── PagerDutyModels.cs
└── Resilience/
└── ResiliencePipelineFactory.cs Polly v8 retry (exponential + jitter, 5xx only)
// Program.cs / Startup
services.AddEnterpriseClients(options =>
{
options.ServiceNow.InstanceUrl = "https://your-instance.service-now.com";
options.ServiceNow.Username = "admin";
options.ServiceNow.Password = Environment.GetEnvironmentVariable("SNOW_PASS")!;
options.Jira.Url = "https://your-org.atlassian.net";
options.Jira.Email = "you@company.com";
options.Jira.Token = Environment.GetEnvironmentVariable("JIRA_TOKEN")!;
options.PagerDuty.ApiKey = Environment.GetEnvironmentVariable("PD_API_KEY")!;
options.Resilience.MaxRetries = 3; // 0 = disabled
options.Cache.TtlSeconds = 300; // 0 = disabled
});
// Inject IServiceNowClient, IJiraClient, or IPagerDutyClient anywhere
public class IncidentService(IServiceNowClient snow, IPagerDutyClient pd)
{
public async Task SyncAsync(string incidentId)
{
var pdIncident = await pd.ListIncidentsAsync("triggered");
await snow.CreateIncidentAsync(new CreateIncidentRequest(
pdIncident.Incidents[0].Title, urgency: 2, category: "software"));
}
}- Retry policy: exponential backoff with jitter, retries on HTTP 5xx and
HttpRequestException - No retry on 4xx: client errors (bad request, unauthorized, not found) are surfaced immediately
- Configurable: set
MaxRetries = 0to disable; setretryDelayin tests toTimeSpan.Zero - Thread-safe:
ResiliencePipeline<HttpResponseMessage>from Polly v8 is immutable
IMemoryCache(in-process) with configurable TTLGetIncidentAsync/GetIssueAsync/ListIncidentsAsynccache by keyUpdateIncidentAsyncinvalidates the cache key after a successful PATCH- Cache serves as optimization for
UpdateIncidentAsync(reads sys_id from cache, skipping a GET)
dotnet test41 tests across 5 test classes — all HTTP calls mocked via MockHttpMessageHandler (queue-based).
- C# .NET 8 with nullable reference types enabled
- Polly 8.4 —
ResiliencePipelineBuilder<HttpResponseMessage>withRetryStrategyOptions - Newtonsoft.Json 13 —
JObject/JTokenfor polymorphic field shapes (e.g., ServiceNowassigned_to) - Microsoft.Extensions.Caching.Memory —
IMemoryCachefor response caching - Microsoft.Extensions.Http —
IHttpClientFactory/ typedHttpClientDI - xUnit + FluentAssertions + NSubstitute — unit tests with expressive assertions
Building this toolkit surfaced a few non-obvious production concerns:
- Polly v8 breaks from v7 —
ResiliencePipelineBuilder<T>replacesAsyncPolicy, andHttpRequestMessageis single-use (each retry needs a fresh instance created inside the lambda). - ServiceNow
assigned_tois polymorphic — can be a plain string or a JSON object depending on the API version and field configuration.JToken+ type switch handles both shapes cleanly. - Jira Cloud REST v3 requires ADF — plain string descriptions are rejected with HTTP 400. The
BuildAdfDocument()helper wraps text in the mandatory doc/paragraph/text node tree. - Cache optimization in update flows —
UpdateIncidentAsyncfetchessys_idviaGetIncidentAsync, which hits the cache if already loaded. This collapses a GET+PATCH flow from 2 HTTP calls to 1 HTTP call after the first fetch. InternalsVisibleTovia csproj — cleaner thanAssemblyInfo.csfor exposing internal constructors to the test project without making them public.