Skip to content

Repository files navigation

csharp-api-integration-toolkit

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.


What It Does

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

Architecture

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)

Quick Start

// 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"));
    }
}

Resilience

  • 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 = 0 to disable; set retryDelay in tests to TimeSpan.Zero
  • Thread-safe: ResiliencePipeline<HttpResponseMessage> from Polly v8 is immutable

Caching

  • IMemoryCache (in-process) with configurable TTL
  • GetIncidentAsync / GetIssueAsync / ListIncidentsAsync cache by key
  • UpdateIncidentAsync invalidates the cache key after a successful PATCH
  • Cache serves as optimization for UpdateIncidentAsync (reads sys_id from cache, skipping a GET)

Running Tests

dotnet test

41 tests across 5 test classes — all HTTP calls mocked via MockHttpMessageHandler (queue-based).


Tech Stack

  • C# .NET 8 with nullable reference types enabled
  • Polly 8.4ResiliencePipelineBuilder<HttpResponseMessage> with RetryStrategyOptions
  • Newtonsoft.Json 13JObject/JToken for polymorphic field shapes (e.g., ServiceNow assigned_to)
  • Microsoft.Extensions.Caching.MemoryIMemoryCache for response caching
  • Microsoft.Extensions.HttpIHttpClientFactory / typed HttpClient DI
  • xUnit + FluentAssertions + NSubstitute — unit tests with expressive assertions

What I Learned

Building this toolkit surfaced a few non-obvious production concerns:

  1. Polly v8 breaks from v7ResiliencePipelineBuilder<T> replaces AsyncPolicy, and HttpRequestMessage is single-use (each retry needs a fresh instance created inside the lambda).
  2. ServiceNow assigned_to is 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.
  3. 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.
  4. Cache optimization in update flowsUpdateIncidentAsync fetches sys_id via GetIncidentAsync, 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.
  5. InternalsVisibleTo via csproj — cleaner than AssemblyInfo.cs for exposing internal constructors to the test project without making them public.

About

Typed C# clients for ServiceNow, Jira, and PagerDuty with Polly retry, memory caching, and .NET DI integration

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages