DCTekSolutions.ExternalCallGuard 1.0.0

dotnet add package DCTekSolutions.ExternalCallGuard --version 1.0.0
                    
NuGet\Install-Package DCTekSolutions.ExternalCallGuard -Version 1.0.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="DCTekSolutions.ExternalCallGuard" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DCTekSolutions.ExternalCallGuard" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="DCTekSolutions.ExternalCallGuard" />
                    
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 DCTekSolutions.ExternalCallGuard --version 1.0.0
                    
#r "nuget: DCTekSolutions.ExternalCallGuard, 1.0.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 DCTekSolutions.ExternalCallGuard@1.0.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=DCTekSolutions.ExternalCallGuard&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=DCTekSolutions.ExternalCallGuard&version=1.0.0
                    
Install as a Cake Tool

ExternalCallGuard

NuGet NuGet Downloads Target

ExternalCallGuard by DC Tek Solutions is a dependency-injected resilience and observability boundary for calls that leave your application. It gives HTTP, database, file, cloud, microservice, and third-party SDK operations one consistent place for retries, timeouts, circuit breaking, cancellation, and semantic telemetry.

Free to use in personal and commercial projects. This package is closed-source proprietary software; see LICENSE.md.

Why use it?

External calls fail differently from ordinary in-process code. A network can reset, a service can return 429, a database can become briefly unavailable, or an SDK call can hang. Handling those conditions independently at every call site produces duplicated policy code and inconsistent behavior.

ExternalCallGuard centralizes that behavior while leaving the actual external operation in your code:

Your operation
    -> named policy selection
    -> retry when the failure is transient
    -> timeout each attempt
    -> optionally stop traffic with a circuit breaker
    -> publish semantic lifecycle telemetry
    -> return the result or preserve the original failure

The guard does not own HTTP clients, database connections, credentials, payloads, or business fallbacks.

Install

dotnet add package DCTekSolutions.ExternalCallGuard --version 1.0.0

The core package automatically brings in DCTekSolutions.ExternalCallGuard.Abstractions. Install DCTekSolutions.ExternalCallGuard.NLogTelemetrySink separately when you want structured NLog telemetry.

Package family

Package Use it when
DCTekSolutions.ExternalCallGuard.Abstractions A project needs only the stable contracts, descriptors, policy keys, or telemetry events.
DCTekSolutions.ExternalCallGuard An application needs retry, timeout, circuit-breaker, cancellation, and telemetry execution.
DCTekSolutions.ExternalCallGuard.NLogTelemetrySink An application wants semantic guard events written to NLog.

Quick start

using DCTekSolutions.ExternalCallGuard.Abstractions.Interfaces;
using DCTekSolutions.ExternalCallGuard.Abstractions.Models;
using DCTekSolutions.ExternalCallGuard.DependencyInjectionSetups;
using DCTekSolutions.ExternalCallGuard.Interfaces;

services.AddExternalCallGuard();

AddExternalCallGuard returns the same IServiceCollection, so registration can be chained. Its optional lambda receives ExternalCallGuardOptions; it configures options only and is not the external operation to execute. Most applications should use this extension method. Composition roots that require an explicit registry object can use the equivalent API:

var registry = new ServiceRegistry(options =>
{
    options.DefaultPolicy.RetryCount = 2;
    options.DefaultPolicy.Timeout = TimeSpan.FromSeconds(10);
});

registry.RegisterServices(services);

Inject IExternalCallGuard, describe the call, and place the real SDK or I/O operation inside the delegate:

var call = new ExternalCallDescriptor(
    dependency: "InventoryApi",
    operation: "GetItem");

var item = await externalCallGuard.ExecuteAsync(
    call,
    ct => inventoryClient.GetItemAsync(itemId, ct),
    cancellationToken);

Dependency and Operation should be stable, low-cardinality, and non-sensitive. They identify the interaction in logs and telemetry; they do not select the resilience behavior. The optional policy key selects behavior independently.

Default policy

The default policy is intentionally useful out of the box:

Setting Default Meaning
RetryCount 4 Up to four retries after the first attempt, for five total attempts.
RetryDelay 500 ms Starting delay between retries.
UseExponentialBackoff true Later retries wait progressively longer.
UseJitter true Retry timing is randomized to avoid synchronized retry storms.
RetryTimeouts true A guard-imposed timeout may start a fresh attempt if a retry remains.
Timeout 30 seconds Maximum duration of each attempt, not the entire call.
CircuitBreakerEnabled false Circuit breaking is opt-in.
TelemetryTimeout 2 seconds Maximum time allowed for each telemetry-sink write.

Configure the default policy during registration:

services.AddExternalCallGuard(options =>
{
    options.DefaultPolicy.RetryCount = 3;
    options.DefaultPolicy.RetryDelay = TimeSpan.FromMilliseconds(500);
    options.DefaultPolicy.UseExponentialBackoff = true;
    options.DefaultPolicy.UseJitter = true;
    options.DefaultPolicy.RetryTimeouts = true;
    options.DefaultPolicy.Timeout = TimeSpan.FromSeconds(20);
    options.TelemetryTimeout = TimeSpan.FromSeconds(2);
});

Timeout is per attempt. With a 20-second timeout and three retries, the operation can make as many as four independently timed attempts, plus retry delays.

Exactly what RetryTimeouts means

RetryTimeouts answers one narrow question: after this guard stops an attempt because that attempt exceeded Timeout, may the guard start a completely new attempt?

Suppose Timeout = 30 seconds and RetryCount = 2:

  • With RetryTimeouts = true, attempt 1 can time out at 30 seconds, then attempts 2 and 3 may each start from the beginning with their own 30-second limit. The worst-case operation time is therefore roughly 90 seconds plus retry delays.
  • With RetryTimeouts = false, a guard-imposed timeout on attempt 1 ends the call immediately. The two configured retries remain unused for that timeout.

This setting does not make one attempt run longer, resume the timed-out attempt, retry caller cancellation, or automatically classify every TimeoutException as retryable. It applies only when ExternalCallGuard itself enforces the policy's per-attempt timeout. A TimeoutException thrown by the external operation is an ordinary operation exception and goes through RetryPredicate or the registered default classifier. With RetryCount = 0, RetryTimeouts = true cannot do anything because no retry is available.

Named policies

Use named policies when different operations need different safety rules:

using DCTekSolutions.ExternalCallGuard.Abstractions.Models;

public static class ExternalCallPolicies
{
    public static readonly ExternalCallPolicyKey ReadOnlyApi = new("ReadOnlyApi");
    public static readonly ExternalCallPolicyKey Mutation = new("Mutation");
    public static readonly ExternalCallPolicyKey LongRunning = new("LongRunning");
}

services.AddExternalCallGuard();

services.AddExternalCallGuardPolicy(
    ExternalCallPolicies.ReadOnlyApi,
    policy =>
    {
        policy.RetryCount = 3;
        policy.Timeout = TimeSpan.FromSeconds(15);
    });

services.AddExternalCallGuardPolicy(
    ExternalCallPolicies.Mutation,
    policy =>
    {
        policy.RetryCount = 0;
        policy.Timeout = TimeSpan.FromSeconds(30);
    });

services.AddExternalCallGuardPolicy(
    ExternalCallPolicies.LongRunning,
    policy =>
    {
        policy.RetryCount = 1;
        policy.Timeout = TimeSpan.FromMinutes(5);
    });

Select a policy in the descriptor:

var call = new ExternalCallDescriptor(
    "OrdersApi",
    "CreateOrder",
    ExternalCallPolicies.Mutation);

An omitted policy uses ExternalCallPolicyKeys.Default. Selecting an unregistered key throws ExternalCallGuardConfigurationException; it is never silently replaced with another policy.

Common use cases

HTTP or REST read

Read operations are often safe to retry when the failure is transient:

var response = await externalCallGuard.ExecuteAsync(
    new ExternalCallDescriptor(
        "CatalogApi",
        "GetProduct",
        ExternalCallPolicies.ReadOnlyApi),
    async ct =>
    {
        using var response = await httpClient.GetAsync($"products/{productId}", ct);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    },
    cancellationToken);

The built-in classifier treats HttpRequestException, socket failures, network-stream failures, and timeouts as transient. Because .NET Standard 2.1 does not expose HttpRequestException.StatusCode, the built-in classifier cannot distinguish HTTP status codes. Applications targeting a newer runtime can use RetryPredicate to retry only selected statuses, such as HTTP 408, 429, or 5xx.

SQL or Entity Framework query

The guard can surround database work without owning the context or connection:

var customer = await externalCallGuard.ExecuteAsync(
    new ExternalCallDescriptor("CustomerDatabase", "GetCustomer"),
    ct => dbContext.Customers
        .AsNoTracking()
        .SingleOrDefaultAsync(x => x.Id == customerId, ct),
    cancellationToken);

Database failures are retried only when your registered classifier identifies them as transient. Do not assume every SQL exception is safe to repeat.

File or network-share read

var contents = await externalCallGuard.ExecuteAsync(
    new ExternalCallDescriptor("SharedFiles", "ReadPriceList"),
    ct => File.ReadAllTextAsync(filePath, ct),
    cancellationToken);

Mutating operation

Retries can duplicate a charge, message, order, upload, or delete. Disable them unless the destination supplies a reliable idempotency guarantee:

await externalCallGuard.ExecuteAsync(
    new ExternalCallDescriptor(
        "PaymentsApi",
        "CapturePayment",
        ExternalCallPolicies.Mutation),
    ct => paymentsClient.CaptureAsync(request, ct),
    cancellationToken);

Custom retry classification

RetryPredicate replaces the default classifier for ordinary operation exceptions. Return true only for failures that are safe and useful to retry:

services.AddExternalCallGuardPolicy(
    ExternalCallPolicies.ReadOnlyApi,
    policy =>
    {
        policy.RetryCount = 2;
        policy.RetryPredicate = exception =>
            exception is HttpRequestException
            {
                StatusCode: HttpStatusCode.TooManyRequests
            }
            || exception is SocketException;
    });

Caller cancellation is always terminal. Guard-imposed timeouts are controlled by RetryTimeouts, not by RetryPredicate.

Application-wide failure classification

Use RetryPredicate when one named policy needs a special rule. To replace failure classification for every policy that does not define a predicate, implement IExternalCallFailureClassifier and register it before AddExternalCallGuard:

public sealed class RateLimitedException : Exception
{
    public RateLimitedException(TimeSpan retryAfter) => RetryAfter = retryAfter;

    public TimeSpan RetryAfter { get; }
}

public sealed class ApplicationFailureClassifier : IExternalCallFailureClassifier
{
    public bool IsTransient(Exception exception) =>
        exception is RateLimitedException
        or HttpRequestException
        or SocketException
        or TimeoutException;
}

services.AddSingleton<IExternalCallFailureClassifier, ApplicationFailureClassifier>();
services.AddExternalCallGuard();

Returning true makes the exception eligible for a retry when the selected policy has one left; it does not force a retry. A custom classifier replaces the built-in classifier, so include every exception type the application considers transient. The guard handles caller cancellation and its own per-attempt timeout separately. Classifiers are singleton services and must be fast, thread-safe, deterministic, and free of side effects.

The built-in classifier treats HttpRequestException, SocketException, an IOException with an inner SocketException, and a regular operation-thrown TimeoutException as transient. It treats OperationCanceledException and unknown exception types as terminal.

Dependency-requested retry delays

When an SDK exception contains an authoritative wait time such as Retry-After, implement IExternalCallRetryDelayProvider and register it before AddExternalCallGuard:

public sealed class RetryAfterDelayProvider : IExternalCallRetryDelayProvider
{
    public TimeSpan? GetRetryDelay(Exception exception) =>
        exception is RateLimitedException rateLimited
            ? rateLimited.RetryAfter
            : null;
}

services.AddSingleton<IExternalCallRetryDelayProvider, RetryAfterDelayProvider>();
services.AddExternalCallGuard();

Return a non-negative TimeSpan to use that delay for the next retry, or null to use the named policy's configured backoff and jitter. The provider runs only after the policy decides that a retryable failure will be retried. It is also a singleton service and must be safe for concurrent calls. The built-in provider always returns null.

Circuit breaker

A circuit breaker prevents an unhealthy dependency from being hammered repeatedly:

services.AddExternalCallGuardPolicy(
    ExternalCallPolicies.ReadOnlyApi,
    policy =>
    {
        policy.RetryCount = 2;
        policy.Timeout = TimeSpan.FromSeconds(15);
        policy.CircuitBreakerEnabled = true;
        policy.CircuitBreakerFailureThreshold = 0.5;
        policy.CircuitBreakerSamplingDuration = TimeSpan.FromSeconds(30);
        policy.CircuitBreakerMinimumThroughput = 10;
        policy.CircuitBreakerBreakDuration = TimeSpan.FromSeconds(30);
    });

With these values, the circuit can open when at least 10 calls were sampled in 30 seconds and at least half had transient failures. Calls fail immediately while the circuit is open. After 30 seconds, a recovery probe determines whether normal traffic can resume. Circuit state is shared by calls using the same policy and dependency name.

Cancellation versus timeout

The two conditions are deliberately different:

  • Caller cancellation means the caller no longer wants the work. It is never retried and remains cancellation.
  • Guard timeout means one attempt ran longer than its policy allows. It becomes a framework-neutral TimeoutException after timeout retries are exhausted.

The external delegate must observe the cancellation token supplied by the guard. Ignoring it prevents prompt cooperative cancellation even though the caller has stopped waiting.

Telemetry

The guard emits provider-neutral events for:

  • Call started
  • Retry scheduled
  • Call succeeded
  • Call failed
  • Caller cancellation
  • Guard timeout

Zero telemetry sinks are valid. One or many sinks may be registered, and sinks run in registration order. A sink failure or sink timeout never changes the business result or replaces the original business exception.

For structured NLog output:

dotnet add package DCTekSolutions.ExternalCallGuard.NLogTelemetrySink --version 1.0.0
using DCTekSolutions.ExternalCallGuard.NLogTelemetrySink.DependencyInjectionSetups;

services.AddExternalCallGuard();
services.AddNLogTelemetrySink();

Safe metadata

Caller-defined metadata is copied into the descriptor and can be consumed by telemetry sinks:

var call = new ExternalCallDescriptor(
    "InventoryApi",
    "GetAvailability",
    metadata: new Dictionary<string, object?>
    {
        ["region"] = "east",
        ["requestKind"] = "summary"
    });

Never place passwords, API keys, authorization headers, bearer tokens, connection strings, request/response bodies, secrets, or personal information in dependency names, operation names, or metadata.

Framework notes

  • Single-targets .NET Standard 2.1.
  • Can be consumed by runtimes that implement .NET Standard 2.1, including .NET Core 3.0+ and .NET 5+. .NET Framework does not implement .NET Standard 2.1.
  • .NET Standard 2.1 does not expose HttpRequestException.StatusCode, so the built-in classifier treats every HttpRequestException as transient. Use RetryPredicate for status-aware behavior in applications targeting a newer runtime.
  • Microsoft.Extensions.Resilience and Polly are implementation details; their types do not appear in IExternalCallGuard.
  • The service is safe for concurrent use. Per-call execution state is not stored on the singleton guard.

License

Free for personal and commercial use under the included DC Tek Solutions license. Package license acceptance is required during installation.

Support / Donate

ExternalCallGuard is developed by DC Tek Solutions. For documentation and support, visit the ExternalCallGuard support wiki.

If this package saves you time, please consider supporting continued development:

Buy Me A Coffee

Thank you for your support!

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 was computed.  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 was computed.  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
1.0.0 50 9/21/2026

Initial standalone release of the ExternalCallGuard resilience and telemetry execution library.