DCTekSolutions.ExternalCallGuard
1.0.0
dotnet add package DCTekSolutions.ExternalCallGuard --version 1.0.0
NuGet\Install-Package DCTekSolutions.ExternalCallGuard -Version 1.0.0
<PackageReference Include="DCTekSolutions.ExternalCallGuard" Version="1.0.0" />
<PackageVersion Include="DCTekSolutions.ExternalCallGuard" Version="1.0.0" />
<PackageReference Include="DCTekSolutions.ExternalCallGuard" />
paket add DCTekSolutions.ExternalCallGuard --version 1.0.0
#r "nuget: DCTekSolutions.ExternalCallGuard, 1.0.0"
#:package DCTekSolutions.ExternalCallGuard@1.0.0
#addin nuget:?package=DCTekSolutions.ExternalCallGuard&version=1.0.0
#tool nuget:?package=DCTekSolutions.ExternalCallGuard&version=1.0.0
ExternalCallGuard
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
TimeoutExceptionafter 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 everyHttpRequestExceptionas transient. UseRetryPredicatefor 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:
Thank you for your support!
| Product | Versions 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. |
-
.NETStandard 2.1
- DCTekSolutions.ExternalCallGuard.Abstractions (>= 1.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Options (>= 10.0.0)
- Microsoft.Extensions.Resilience (>= 10.0.0)
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.