Repository navigation
Client Usage
The IArgusClient interface and its ArgusClient implementation send requests over a named pipe and return responses using the ARGUS/1.0 protocol.
Register the client with dependency injection:
services.AddArgusClient("mypipe");With a custom default timeout:
services.AddArgusClient("mypipe", TimeSpan.FromSeconds(60));With automatic retries (see Automatic Retries):
services.AddArgusClient("mypipe", TimeSpan.FromSeconds(60), retry => retry.MaxRetries = 3);Configuring each created client directly (timeout, retry policy, compression, …):
services.AddArgusClient("mypipe", client =>
{
client.DefaultTimeout = TimeSpan.FromSeconds(60);
client.RetryPolicy = new ArgusRetryPolicy();
client.Compression.Enabled = true;
});The client is registered as transient. Each request creates a new named pipe connection. If an IArgusBodySerializerRegistry is registered (e.g., via AddArgusPipeHost or AddArgusPlainTextProtocol), the client uses it automatically.
The client provides typed methods for each verb:
| Method | Description |
|---|---|
GetAsync(route) |
Send a GET request |
PostAsync(route, body) |
Send a POST request with an optional string body |
PutAsync(route, body) |
Send a PUT request with an optional string body |
PatchAsync(route, body) |
Send a PATCH request with an optional string body |
DeleteAsync(route) |
Send a DELETE request |
HeadAsync(route) |
Send a HEAD request |
All methods accept optional timeout and cancellationToken parameters.
public class MyService : BackgroundService
{
private readonly IArgusClient client;
public MyService(IArgusClient client)
{
this.client = client;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// GET
var response = await this.client.GetAsync("/items", cancellationToken: stoppingToken);
Console.WriteLine($"Status: {response.StatusCode}");
Console.WriteLine($"Body: {response.Body}");
// POST
var json = JsonSerializer.Serialize(new { name = "Widget" });
response = await this.client.PostAsync("/items", json, cancellationToken: stoppingToken);
// PUT
var updateJson = JsonSerializer.Serialize(new { name = "Updated Widget" });
response = await this.client.PutAsync("/items/some-id", updateJson, cancellationToken: stoppingToken);
// DELETE
response = await this.client.DeleteAsync("/items/some-id", cancellationToken: stoppingToken);
}
}Overloads accepting IReadOnlyDictionary<string, string> append query parameters to the route:
var queryParams = new Dictionary<string, string>
{
{ "page", "1" },
{ "size", "25" }
};
var response = await client.GetAsync("/items", queryParams, cancellationToken: stoppingToken);
// Sends: GET /items?page=1&size=25 ARGUS/1.0Query parameter overloads are available for GetAsync, PostAsync, PutAsync, PatchAsync, DeleteAsync, and HeadAsync.
Keys and values are percent-encoded. A null value is sent as an empty value: ["filter"] = null becomes ?filter=, and the server sees context.QueryValues["filter"] == "".
For large payloads, send a Stream body using chunked transfer encoding:
using var fileStream = File.OpenRead("largefile.bin");
var response = await client.PostAsync(
"/upload",
fileStream,
contentType: "application/octet-stream",
cancellationToken: stoppingToken);Stream body overloads are available for PostAsync, PutAsync, and PatchAsync.
The client has a DefaultTimeout property (30 seconds by default). You can override it per request:
// Set a default timeout for all requests
client.DefaultTimeout = TimeSpan.FromSeconds(60);
// Override for a specific request
var response = await client.GetAsync(
"/slow-operation",
timeout: TimeSpan.FromMinutes(5),
cancellationToken: stoppingToken);The timeout covers the whole call: connecting, sending, receiving and -- when retries are enabled -- every retry and the delays between them.
By default the client reads response bodies of any size. When the server is not fully trusted, set MaxResponseBodySize (in bytes) so a misbehaving server cannot exhaust the client's memory:
using var client = new ArgusClient("myservice")
{
MaxResponseBodySize = 10 * 1024 * 1024 // 10 MB
};The limit is applied like the host's MaxRequestBodySize:
- A
Content-Lengthabove the limit is rejected before the body is read, so a header such asContent-Length: 2147483647does not allocate anything. - A chunked body is rejected as soon as it exceeds the limit, so an endless stream ends.
- A compressed body is rejected when its decompressed size exceeds the limit, so a small gzip bomb cannot expand into gigabytes.
A response above the limit throws an ArgusProtocolException (an InvalidOperationException) from SendAsync, GetAsync and the other methods. The default retry policy does not retry it. 0 (the default) means no limit, and negative values throw ArgumentOutOfRangeException. With dependency injection, set it through the configure overload: services.AddArgusClient("myservice", client => client.MaxResponseBodySize = 10 * 1024 * 1024).
Named pipes can fail transiently, most commonly when the pipe breaks because the server restarts mid-request. ArgusClient can retry such failures automatically. Retries are disabled by default (client.RetryPolicy is null).
Waiting for a server that hasn't started yet, or for a free pipe instance when all are busy, does not need retries:
ArgusClientalready keeps waiting to connect until the timeout expires.
Enable retries through dependency injection:
services.AddArgusClient("mypipe", TimeSpan.FromSeconds(30), retry =>
{
retry.MaxRetries = 3;
retry.InitialDelay = TimeSpan.FromMilliseconds(100);
retry.Strategy = RetryBackoffStrategy.Exponential;
});or directly on a client instance:
using var client = new ArgusClient("mypipe")
{
RetryPolicy = new ArgusRetryPolicy { MaxRetries = 5 },
Logger = loggerFactory.CreateLogger<ArgusClient>()
};| Property | Type | Default | Description |
|---|---|---|---|
MaxRetries |
int |
3 |
Maximum number of retries after the first attempt. 0 disables retries. |
InitialDelay |
TimeSpan |
100 ms | Delay before the first retry; later delays derive from it according to Strategy. |
MaxDelay |
TimeSpan |
10 s | Upper bound for any single delay. |
Strategy |
RetryBackoffStrategy |
Exponential |
How the delay grows (see below). |
RetryNonIdempotentRequests |
bool |
false |
Also retry POST and PATCH after the request was sent. |
ShouldRetry |
Func<Exception, bool> |
ArgusRetryPolicy.IsTransient |
Decides whether an exception is transient. The default accepts any IOException, including a broken pipe and an EndOfStreamException for a connection closed before the response arrived. |
Negative values and undefined strategies are rejected with ArgumentOutOfRangeException.
RetryBackoffStrategy |
Delay before retry n | With the defaults (retries 1, 2, 3) |
|---|---|---|
Fixed |
InitialDelay |
100, 100, 100 ms |
Linear |
n × InitialDelay
|
100, 200, 300 ms |
Exponential |
2^(n-1) × InitialDelay
|
100, 200, 400 ms |
policy.GetDelay(n) returns the delay for retry n, capped at MaxDelay.
| Situation | Retried? |
|---|---|
| Failure while connecting, before anything was sent | Yes |
Failure after sending a GET, HEAD, PUT or DELETE
|
Yes |
Failure after sending a POST or PATCH
|
Only with RetryNonIdempotentRequests = true -- the server may already have processed the request |
Failure after sending a streamed body (BodyStream) |
Only if the stream is seekable; it is rewound to its original position before the retry |
TimeoutException or OperationCanceledException
|
Never, regardless of ShouldRetry
|
Non-2xx responses (404, 500, ...) |
Never -- they are valid responses, not transport failures |
Cancelling the CancellationToken also stops the client from waiting between retries. When the timeout expires during a delay, the call throws TimeoutException. When all retries are used up, the last exception is rethrown.
Each retry is logged as a warning, including the exception, verb, route, pipe name, retry number and delay. AddArgusClient sets ILogger<ArgusClient> automatically when logging is registered. Without logging, the client uses NullLogger.
services.AddArgusClient("mypipe", null, retry =>
{
// Only retry when the server closed the connection before sending a response
retry.ShouldRetry = ex => ex is EndOfStreamException;
});ArgusTransfer implements retries itself rather than depending on Polly, so the package adds no third-party dependency. If you need circuit breakers, hedging or rate limiting, wrap your
IArgusClientcalls in your own PollyResiliencePipeline.
Send credentials with the standard Authorization header:
var request = new ArgusRequest { Verb = ArgusVerb.GET, Route = "/items" };
request.SetAuthorization("Bearer", token); // Authorization: Bearer <token>
var response = await client.SendAsync(request);
if (response.StatusCode == ArgusStatusCode.Unauthorized)
{
// missing or invalid credentials
}Values containing line breaks are rejected. See Authentication for the server side.
With client.Compression.Enabled = true, the client accepts gzip-compressed responses and compresses request bodies of at least 1 KB. Compressed responses are decoded transparently. Only enable it against a host that supports compression; see Compression.
For full control, build an ArgusRequest manually and send it with SendAsync:
var request = new ArgusRequest
{
Verb = ArgusVerb.POST,
Route = "/items",
Body = jsonBody
};
request.Headers["X-Custom-Header"] = "custom-value";
var response = await client.SendAsync(request, cancellationToken: stoppingToken);Each verb method has an EnsureSuccessAsync-suffixed counterpart that throws ArgusRequestException on a non-2xx response, eliminating an explicit status check at every call site:
// Throws on 4xx / 5xx; returns the ArgusResponse on 2xx.
var response = await this.client.GetEnsureSuccessAsync("/items/123");
var item = JsonSerializer.Deserialize<SampleItem>(response.Body);The 16 overloads mirror the existing methods exactly (including query-parameter and Stream-body variants):
SendEnsureSuccessAsync-
GetEnsureSuccessAsync/PostEnsureSuccessAsync/PutEnsureSuccessAsync/PatchEnsureSuccessAsync/DeleteEnsureSuccessAsync/HeadEnsureSuccessAsync
Equivalently, for a single request, you can call EnsureSuccessStatusCode() on the response yourself:
var response = (await this.client.GetAsync("/items/123"))
.EnsureSuccessStatusCode();The non-EnsureSuccess methods never throw on non-2xx -- they always return the response and leave the decision to the caller. See Error Handling for the full overload list, the ArgusRequestException shape, and structured error bodies via ArgusProblemDetails.
When a handler returns an RFC 7807 problem-details body (Content-Type: application/problem+json), read it with ArgusProblemDetails.TryRead:
var response = await client.GetAsync("/items/123");
if (ArgusProblemDetails.TryRead(response, out var problem))
{
Console.WriteLine($"{problem.Status} {problem.Title}: {problem.Detail}");
}See Error Handling for producing problem details on the server and reading extension members.
The ArgusResponse provides:
| Property | Type | Description |
|---|---|---|
StatusCode |
ArgusStatusCode |
The response status code |
Body |
string |
The response body (if any) |
Headers |
Dictionary<string, string> |
Response headers |
CorrelationToken |
Guid |
Matches the request's correlation token |
Timestamp |
DateTime |
When the response was created |
var response = await client.GetAsync("/items/123");
if (response.StatusCode == ArgusStatusCode.Ok)
{
var item = JsonSerializer.Deserialize<SampleItem>(response.Body);
}
else if (response.StatusCode == ArgusStatusCode.NotFound)
{
Console.WriteLine("Item not found");
}IArgusClient derives from IDisposable, but ArgusClient holds no resources between requests: every request creates and disposes its own pipe connection. Dispose therefore releases nothing. It only marks the client as disposed, after which SendAsync and the other methods throw ObjectDisposedException; a request that is already in progress is not interrupted.
IDisposable stays on the interface so that an implementation that does hold resources, such as pooled pipe connections, can be added without a breaking change. Dispose a client you create yourself, so that your code remains correct for such an implementation:
using var client = new ArgusClient("mypipe");
var response = await client.GetAsync("/items");Because nothing is held, a client can also be kept and reused for the lifetime of the application; there is no connection that goes stale.
With dependency injection the container disposes the clients it creates. A container keeps a reference to every transient IDisposable it resolves until the scope (or, for the root provider, the application) ends. Resolve IArgusClient from a scope or inject it into a service once, rather than resolving it repeatedly from the root provider in a loop, where the instances accumulate until shutdown.
copyright @ Sam Gerené