NTResult 1.6.0
dotnet add package NTResult --version 1.6.0
NuGet\Install-Package NTResult -Version 1.6.0
<PackageReference Include="NTResult" Version="1.6.0" />
<PackageVersion Include="NTResult" Version="1.6.0" />
<PackageReference Include="NTResult" />
paket add NTResult --version 1.6.0
#r "nuget: NTResult, 1.6.0"
#:package NTResult@1.6.0
#addin nuget:?package=NTResult&version=1.6.0
#tool nuget:?package=NTResult&version=1.6.0
<p align="center"> <img src="Logo.png" alt="NTResult logo" width="128" height="128" /> </p>
NTResult
NTResult is a small set of .NET libraries for representing an operation as either a successful value or an error. It keeps expected failures explicit without requiring exceptions for normal control flow, then carries that result cleanly through ASP.NET Core endpoints and Refit clients.
The typical flow is:
application/service -> INTResult<T> -> HTTP response -> Refit IApiResponse<T> -> INTResult<T>
Packages
| Package | Use it for | Targets |
|---|---|---|
NTResult |
Core INTResult, INTResult<T>, Expected, Optional, and NTFileDownload types |
.NET 8 |
NTResult.AspNetCore.Http |
Minimal API and controller integration through HttpNTResult, ToIResult, and NTResultControllerBase |
.NET 8 and later, including .NET 11 Preview |
NTResult.Refit |
Converting Refit IApiResponse values into client-side INTResult values |
.NET 8 and later, including .NET 11 Preview |
Install only the packages needed by each project. The integration packages reference the core package transitively.
# Domain, application, or shared project
dotnet add package NTResult
# ASP.NET Core API project
dotnet add package NTResult.AspNetCore.Http
# Refit client project
dotnet add package NTResult.Refit
Core package: NTResult
Create successful or failed results with the NTResult factory. A typed result exposes its value only on success and retains the original exception on failure.
using NTResult;
using NTResult.Exceptions;
public sealed record Order(int Id, string Description);
public static INTResult<Order> FindOrder(int id)
{
Order? order = id == 42 ? new Order(42, "Replacement keyboard") : null;
return order is null
? NTResult.Failure<Order>(new NotFoundException(typeof(Order), id))
: NTResult.Success(order);
}
INTResult<Order> result = FindOrder(42);
if (result.TryGetValue(out var order))
{
Console.WriteLine(order.Description);
}
else if (result.HasFailed)
{
Console.WriteLine(result.ErrorMessage);
}
Useful result members include:
IsSuccessful,HasFailed, andIsCanceledfor three-state branching.TryGetValue,GetValueOrDefault, andValueOrfor non-throwing access.ValueOrThrowandThrowOnFailurewhen an exception boundary is appropriate.OnCanceledandOnCanceledAsyncfor optional cancellation-result callbacks; transport cancellation still propagates.OnSuccess,OnFailure, andFinallyfor small continuations.- Async equivalents for
Task<INTResult>andTask<INTResult<T>>inNTResult.Ext.
The core package also includes Expected<TValue, TError>, Optional<T>, and NTFileDownload for code that needs those more specialized representations.
Server package: NTResult.AspNetCore.Http
HttpNTResult combines an INTResult with an ASP.NET Core IResult. It can be returned directly from a minimal API and supports common HTTP outcomes such as OK, Created, Accepted, Bad Request, Not Found, Unauthorized, Forbidden, redirects, file downloads, and Internal Server Error.
using Microsoft.AspNetCore.Mvc;
using NTResult.AspNetCore.Http;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/orders/{id:int}", (int id) =>
{
var order = id == 42 ? new Order(42, "Replacement keyboard") : null;
return order is null
? HttpNTResult<Order>.NotFound($"Order {id} was not found")
: HttpNTResult<Order>.Ok(order);
});
app.MapPost("/orders", (Order order) =>
HttpNTResult<Order>.Created($"/orders/{order.Id}", order));
app.MapGet("/health/dependency", () =>
HttpNTResult.InternalServerError(new ProblemDetails
{
Title = "Dependency unavailable",
Detail = "The inventory service could not be reached."
}));
app.Run();
If application services already return core INTResult values, convert them at the HTTP boundary:
using NTResult.AspNetCore.Http.Ext;
app.MapGet("/orders/{id:int}", async (int id, OrderService service) =>
(await service.FindOrderAsync(id)).ToIResult());
ToIResult maps NotFoundException to 404, UnauthorizedAccessException to 401, ForbiddenException to 403, and other failures to 400. Cancellation exceptions propagate to the ASP.NET Core host instead of being assigned an HTTP status without request context. Use HttpNTResult.RequestTimeout() for an incomplete request (408), HttpNTResult.GatewayTimeout() for an upstream timeout (504), or CustomError with an explicit IResult when the endpoint needs another specific response.
NTResultControllerBase provides the same result factories as protected helpers for controller-based APIs.
Client package: NTResult.Refit
Keep Refit methods typed as IApiResponse<T>, then convert the response into the same result abstraction used by the rest of the application.
using NTResult;
using NTResult.Refit.Ext;
using Refit;
public interface IOrdersApi
{
[Get("/orders/{id}")]
Task<IApiResponse<Order>> GetOrderAsync(int id);
}
IOrdersApi api = RestService.For<IOrdersApi>("https://api.example.com");
INTResult<Order> result = await api.GetOrderAsync(42).ToNTResultAsync();
if (result.IsSuccessful)
{
Console.WriteLine(result.Value.Description);
}
else if (result.HasFailed)
{
Console.WriteLine(result.ErrorMessage);
}
After applying the special 408, 499, and 504 mappings described below, the converter selects the most useful available error in this order:
ProblemDetails.DetailProblemDetails.Title- Response body text
- HTTP reason phrase
- A status-code fallback message
HTTP 408 and 504 responses become failures containing a TimeoutException. HTTP 499 becomes a canceled result regardless of response headers or body, including responses from servers that do not use NTResult. Transport cancellation propagates as an exception from ToNTResultAsync. Created and redirect responses can return their Location header when the requested result type is string or Uri.
See cancellation and migration guidance for the three-state contract, serialization, Intersect examples, and breaking changes.
File downloads
A Refit stream response converts to INTResult<NTFileDownload>. Dispose the returned download after consuming it; that releases both the stream and its owning HTTP response.
public interface IReportsApi
{
[Get("/reports/{id}")]
Task<IApiResponse<Stream>> DownloadAsync(int id);
}
INTResult<NTFileDownload> result = await reportsApi.DownloadAsync(7).ToNTResultAsync();
using NTFileDownload download = result.ValueOrThrow();
await download.Contents.Stream!.CopyToAsync(destinationStream);
For other disposable Refit content types, keep and dispose the original IApiResponse<T> after the returned content is no longer needed. Non-disposable response content is disposed automatically during conversion.
Server-sent events (.NET 10+)
NTResult.AspNetCore.Http supports native ASP.NET Core SSE responses for results containing
IAsyncEnumerable<T>. Create a full result with HttpNTResult.ServerSentEvents; ordinary ToIResult
conversion keeps its existing behavior.
Choose the event payload type
T is the data in one event. It can be a string, number, DTO, record, collection, or any
type supported by your JSON serialization configuration. SSE still requires a stream:
INTResult<MyDto> alone is not an SSE response. Return INTResult<IAsyncEnumerable<MyDto>>
to send DTOs as they become available, or yield a single item if you only need one event.
| Data per event | Server result | Refit conversion |
|---|---|---|
| Plain text | INTResult<IAsyncEnumerable<string>> |
ToNTResultAsync(textParser) |
| Number or DTO | INTResult<IAsyncEnumerable<T>> |
ToNTResultAsync(jsonTypeInfo) |
| A batch of DTOs | INTResult<IAsyncEnumerable<List<T>>> |
Pass metadata for List<T> |
| Data with event type, ID, or retry interval | INTResult<IAsyncEnumerable<SseItem<T>>> |
Pass metadata for T, not SseItem<T> |
| Different payload types in one stream | Use a shared envelope, configured polymorphic base type, or SseItem<JsonElement> |
Use matching metadata or a custom SseItemParser<T> |
Strings are written as raw text, so pass a UTF-8 parser to the Refit conversion for string events.
That parser also lets you inspect JSON events as text without deserializing them.
Non-string data is JSON; types such as streams and HTTP result objects should be converted to
serializable event payloads first. An INTResult without a value needs an explicit payload
(for example, a status DTO) if you want to send an event.
Set up the shared JSON contract
Target .NET 10 or later. Reference NTResult.AspNetCore.Http in the server and NTResult.Refit
in the client. Place your payload types and JSON context in a shared project, or provide equivalent
definitions on both sides. Put each declaration below in its own source file.
public sealed record ProgressUpdate(int Percent, string Message);
using System.Text.Json;
using System.Text.Json.Serialization;
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
[JsonSerializable(typeof(ProgressUpdate))]
[JsonSerializable(typeof(List<ProgressUpdate>))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(JsonElement))]
public partial class AppJsonContext : JsonSerializerContext;
Replace or extend the [JsonSerializable] entries for your payload types. Register the actual
collection or envelope type when that is what each event contains. You do not need metadata for
IAsyncEnumerable<T> or SseItem<T>: the SSE writer serializes each event's data separately.
Configure converters and polymorphic derived types when your payload requires them.
Produce typed events on the server
Register the context before building the app, then return a typed async stream:
using NTResult;
using NTResult.AspNetCore.Http;
using System.Runtime.CompilerServices;
var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default));
var app = builder.Build();
app.MapGet("/progress", (CancellationToken cancellationToken) =>
{
return HttpNTResult.ServerSentEvents(ReadUpdates(cancellationToken), eventType: "progress");
});
app.Run();
static async IAsyncEnumerable<ProgressUpdate> ReadUpdates([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
yield return new ProgressUpdate(0, "Starting");
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); // Replace with your asynchronous work.
yield return new ProgressUpdate(100, "Complete");
}
The stream-only factory returns HttpNTResult<IAsyncEnumerable<T>>, implementing both
INTResult<IAsyncEnumerable<T>> and IResult. Its Value is the original stream.
For an existing service, use the request-aware factory to preserve success, failure, and cancellation:
app.MapGet("/progress", (HttpRequest request, IProgressService service) =>
HttpNTResult.ServerSentEvents(request, service.GetUpdatesAsync, eventType: "progress"));
Use either endpoint example, not both for the same route. Replace ProgressUpdate with your
own T throughout the stream and JSON context. For collections, yield a collection for one
batch event, or yield its elements individually for one event per element.
For per-event metadata, return INTResult<IAsyncEnumerable<SseItem<T>>> using
System.Net.ServerSentEvents.SseItem<T> and call HttpNTResult.ServerSentEvents(stream) without an event type.
The native writer preserves each item's EventType, EventId, and ReconnectionInterval.
using System.Net.ServerSentEvents;
static async IAsyncEnumerable<SseItem<ProgressUpdate>> ReadEvents([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await foreach (var update in ReadUpdates(cancellationToken))
{
yield return new SseItem<ProgressUpdate>(update, "progress")
{
EventId = update.Percent.ToString(),
ReconnectionInterval = TimeSpan.FromSeconds(2)
};
}
}
// In an endpoint:
// return HttpNTResult.ServerSentEvents(ReadEvents(cancellationToken));
The request-aware overload awaits the selected service factory and returns Task<HttpNTResult>.
Strings are sent as text; other payloads use the application's HTTP JSON serializer options.
Native AOT applications must register JSON serialization metadata for their event payload types.
In the request-aware overload, failures (including custom HTTP errors) and canceled results use the existing HTTP mappings
before enumeration begins. Successful results use the negotiated format even if they carry another HTTP success
response. A null stream is invalid; an empty stream produces an empty SSE response or JSON [].
SSE enumeration starts when ASP.NET Core executes the response; JSON enumeration finishes before
the helper returns. Both receive RequestAborted;
async iterators should use [EnumeratorCancellation] to observe client disconnects.
Exceptions after streaming starts propagate and cannot become a new HTTP error response.
The stream-only HttpNTResult.ServerSentEvents(stream) overload does not negotiate Accept.
Use HttpNTResult.ServerSentEvents(request, events) below when one endpoint should support both SSE and JSON.
The package retains its .NET 8 target; these SSE APIs are available on .NET 10 and later.
Consuming SSE with Refit
On .NET 10+, SSE parsing is part of the existing IApiResponseExt.ToNTResult() /
ToNTResultAsync() API. Pass event metadata or a parser to receive an INTResult<NTSseStream<T>>.
The parser uses native SseParser and preserves event types, IDs, and retry intervals.
Calling the stream overload without metadata or a parser still returns INTResult<NTFileDownload>.
using NTResult.Refit.Ext;
using Refit;
public interface IProgressApi
{
[Get("/progress")]
[Headers("Accept: text/event-stream")]
Task<IApiResponse<Stream>> GetProgressAsync(CancellationToken cancellationToken);
}
The Refit return type remains Task<IApiResponse<Stream>> for every event payload type.
Refit provides the open response body; the SSE adapter parses individual events. Create a client
with your server's base address (or use your existing Refit registration):
using var httpClient = new HttpClient { BaseAddress = new Uri("https://localhost:5001") };
var api = RestService.For<IProgressApi>(httpClient);
// Produces INTResult<NTSseStream<ProgressUpdate>>.
var result = await api.GetProgressAsync(cancellationToken)
.ToNTResultAsync(AppJsonContext.Default.ProgressUpdate);
if (result.IsCanceled)
return;
if (result.HasFailed)
{
Console.WriteLine(result.ErrorMessage);
return;
}
using var events = result.Value;
await foreach (var item in events.WithCancellation(cancellationToken))
{
ProgressUpdate update = item.Data;
Console.WriteLine($"{item.EventId}: {item.EventType}: {update.Percent}% - {update.Message}");
}
Here cancellationToken is supplied by the calling operation. Pass it both to the Refit request
and to enumeration. The using scope owns the HTTP response, including when you break out early.
For JSON payloads, pass generated JsonTypeInfo<T> metadata:
ToNTResultAsync(AppJsonContext.Default.ProgressUpdate). For mixed event types or other data
formats, pass a SseItemParser<T> delegate that receives the event type and UTF-8 data bytes.
ToNTResult(jsonTypeInfo) and ToNTResult(itemParser) handle an already received IApiResponse<Stream>.
JSON parsing uses the supplied metadata, rather than automatically using Refit's serializer settings.
Match that metadata's naming and converter settings to the server. For ASP.NET Core's default
camel-case JSON, generate the context with [JsonSourceGenerationOptions(JsonSerializerDefaults.Web)].
For raw text events, pass a UTF-8 parser instead of JSON metadata:
using System.Text;
var result = await api.GetProgressAsync(cancellationToken)
.ToNTResultAsync(static (_, bytes) => Encoding.UTF8.GetString(bytes));
// result is INTResult<NTSseStream<string>>; handle its state and dispose/enumerate its Value as above.
SSE results use the same IsSuccessful, HasFailed, IsCanceled, OnSuccess, and OnFailure
contracts as other INTResult values. There is no separate server-side SSE conversion extension:
create the response with HttpNTResult.ServerSentEvents(...), then use ToIResult() when your
endpoint receives an INTResult-typed value.
For reusable client code, accept JsonTypeInfo<T> alongside the response task:
using NTResult;
using NTResult.Refit;
using NTResult.Refit.Ext;
using Refit;
using System.Text.Json.Serialization.Metadata;
static Task<INTResult<NTSseStream<T>>> OpenEventsAsync<T>(Task<IApiResponse<Stream>> response, JsonTypeInfo<T> jsonTypeInfo)
=> response.ToNTResultAsync(jsonTypeInfo);
// DTO events:
// await OpenEventsAsync(api.GetProgressAsync(cancellationToken), AppJsonContext.Default.ProgressUpdate);
// Batch events from an endpoint returning List<ProgressUpdate> per event:
// await OpenEventsAsync(api.GetProgressAsync(cancellationToken), AppJsonContext.Default.ListProgressUpdate);
The metadata must describe the actual data sent by the endpoint. Changing the client type does not change the server's payload. JSON null is allowed when the chosen type supports it; include null handling in your consumer when your contract permits null events.
Different payload types in one stream
For named events with unrelated payloads, one option is to send SseItem<JsonElement> and
select the client parser by event type. Add each DTO to AppJsonContext and convert each server
payload with its metadata:
var progressEvent = new SseItem<JsonElement>(
JsonSerializer.SerializeToElement(new ProgressUpdate(50, "Working"), AppJsonContext.Default.ProgressUpdate),
"progress");
var countEvent = new SseItem<JsonElement>(
JsonSerializer.SerializeToElement(12, AppJsonContext.Default.Int32),
"count");
// Yield these from an IAsyncEnumerable<SseItem<JsonElement>>, then call HttpNTResult.ServerSentEvents(stream).
On the client, supply a parser returning a common type. This example returns object; a shared
domain interface or envelope is also suitable:
var result = await api.GetProgressAsync(cancellationToken).ToNTResultAsync<object>(
(eventType, bytes) => eventType switch
{
"progress" => JsonSerializer.Deserialize(bytes, AppJsonContext.Default.ProgressUpdate)
?? throw new JsonException("Expected a progress payload."),
"count" => JsonSerializer.Deserialize(bytes, AppJsonContext.Default.Int32),
_ => throw new JsonException($"Unsupported event type: {eventType}")
});
// Handle the result, dispose its stream, and enumerate as in the typed example above.
// item.Data is ProgressUpdate or int; item.EventType and item.EventId remain available.
These snippets use System.Net.ServerSentEvents and System.Text.Json. If using a polymorphic
base type instead, configure matching JSON discriminators and derived types on both sides.
Avoid sending arbitrary runtime objects without an explicit serialization contract.
Negotiate SSE with a JSON fallback
Use HttpNTResult.ServerSentEvents(request, events) with one factory that opens your stream.
If the client accepts SSE, items are sent as they arrive. Otherwise, the helper enumerates the
same stream to completion and returns all values in order as one JSON array. An empty stream
returns []; null elements remain null. The factory runs once, with HttpContext.RequestAborted,
and is never invoked when negotiation returns 406.
The JSON path waits for completion and holds the collected values in memory. Use finite streams for endpoints that offer this fallback; use the stream-only SSE helper for ongoing subscriptions. Request cancellation is passed to both the factory and enumeration.
Request Accept |
Selected response |
|---|---|
Missing, empty, or */* |
JSON |
application/json |
JSON |
text/event-stream |
SSE |
text/event-stream, application/json;q=0.9 |
SSE |
text/event-stream;q=0.5, application/json |
JSON |
text/event-stream;q=0, */* |
JSON |
text/plain, or both supported formats excluded with q=0 |
406 Not Acceptable |
SSE requires an explicit, matching text/event-stream media range with a positive quality.
Wildcard ranges alone do not opt into streaming. The higher effective quality wins; ties favor
explicit SSE. More specific JSON ranges override broader wildcards, including exclusions.
Malformed headers or quality values return 406. The helper adds Vary: Accept while preserving
existing Vary fields. These are application selection rules built on
HTTP content negotiation.
For example, given a service with this method:
public interface IProgressService
{
Task<INTResult<IAsyncEnumerable<ProgressUpdate>>> GetUpdatesAsync(CancellationToken cancellationToken);
}
Register the service and HTTP JSON context as above, then map the negotiated endpoint:
using NTResult.AspNetCore.Http;
app.MapGet("/progress", (HttpRequest request, IProgressService service) =>
HttpNTResult.ServerSentEvents(request,
service.GetUpdatesAsync,
eventType: "progress"));
The method returns Task<HttpNTResult>, preserving the full INTResult state as well as the HTTP
response. Either successful representation is a successful result; rejected negotiation is a failed
result with HTTP 406. Return the factory result directly from an endpoint, or use ToIResult() when
your service exposes it as INTResult.
For events of type T, JSON contains a List<T>. Register JSON metadata for both T and
List<T> (the shared context above includes both for ProgressUpdate). If each event is itself
a collection, the JSON response is an array of those collections.
For streams of SseItem<T>, use the overload without eventType: SSE preserves the metadata,
while JSON collects only each item's Data. Strings become JSON strings inside the array.
Both formats use HTTP JSON options and retain the existing factory failure and cancellation mappings.
Errors or cancellation during collection propagate before the JSON response is written.
Authenticate and authorize the endpoint normally.
Use SSE from an NTResult controller
NTResultControllerBase exposes the same ServerSentEvents helpers on .NET 10+.
For negotiation, omit the request argument: the helper uses the controller's Request and
passes RequestAborted to the factory and enumeration. Return the resulting INTResult through
the usual ToIResult() conversion at the action boundary.
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NTResult.AspNetCore.Http;
using NTResult.AspNetCore.Http.Ext;
[Route("api/progress")]
public sealed class ProgressController(IProgressService _service) : NTResultControllerBase
{
[HttpGet]
public async Task<IResult> Get()
{
var result = await ServerSentEvents(_service.GetUpdatesAsync, eventType: "progress");
return result.ToIResult();
}
}
Register controllers with builder.Services.AddControllers() and app.MapControllers().
Keep the HTTP JSON configuration shown above; these helpers execute HTTP results using those
options. Direct streaming is also available as ServerSentEvents(values, eventType) or
ServerSentEvents(items) for an IAsyncEnumerable<SseItem<T>>. The direct helpers return
INTResult<IAsyncEnumerable<T>> / INTResult<IAsyncEnumerable<SseItem<T>>>; the negotiated
helpers return Task<INTResult> and retain success, failure, and cancellation states.
Consume either negotiated format with Refit
Keep the Refit return type as Task<IApiResponse<Stream>>, but advertise both formats:
public interface IProgressApi
{
[Get("/progress")]
[Headers("Accept: text/event-stream, application/json;q=0.9")]
Task<IApiResponse<Stream>> GetProgressAsync(CancellationToken cancellationToken);
}
Use this interface instead of the SSE-only example. To request JSON, use Accept: application/json
or omit Accept. A dynamic Refit [Header("Accept")] parameter can select the preference per call.
using NTResult.Refit.Ext;
var result = await api.GetProgressAsync(cancellationToken).ToNTResultAsync(
AppJsonContext.Default.ProgressUpdate, // TEvent: data inside each SSE event.
AppJsonContext.Default.ListProgressUpdate, // TJson: all event values as one JSON array.
cancellationToken);
if (result.IsCanceled)
return;
if (result.HasFailed)
{
Console.WriteLine(result.ErrorMessage);
return;
}
using var response = result.Value; // NTSseOrJson<ProgressUpdate, List<ProgressUpdate>>
if (response.IsStreaming)
{
await foreach (var item in response.Events.WithCancellation(cancellationToken))
Console.WriteLine($"{item.EventType}: {item.Data.Percent}%");
}
else
{
foreach (ProgressUpdate update in response.Value)
Console.WriteLine($"{update.Percent}%: {update.Message}");
}
Pass metadata for List<T> as the second argument, where T is the event data type.
For raw text or mixed SSE payloads, replace the first metadata argument with a
SseItemParser<TEvent> delegate; for example, (_, bytes) => Encoding.UTF8.GetString(bytes)
for text. The same async conversion is available on an already received IApiResponse<Stream>.
The client selects its branch from the response Content-Type, not its request's Accept.
SSE remains open until consumed or disposed. JSON is deserialized and its transport closed before
the result returns. Invalid JSON becomes a failed result; transport I/O and cancellation propagate.
Missing or unsupported success content types fail rather than guessing a format. HTTP errors,
including 406 and canceled 499, use the existing mappings before content-type validation.
The overload with one event metadata/parser argument remains SSE-only. Supply both event and fallback metadata to accept a negotiated JSON response.
Errors, cancellation, and response ownership
The adapter takes ownership of the response. Keep the returned stream in a using scope even
if you never enumerate it. Enumeration is single-use and closes the response on completion,
early exit, cancellation, or a parsing/I/O error. The outer INTResult describes opening the
stream: HTTP failures retain the existing mappings (including canceled HTTP 499), while errors
during enumeration propagate to the caller. The SSE-only conversion rejects successful responses
with a missing body or a content type other than text/event-stream; the negotiated conversion
also accepts application/json. Reconnection and sending
Last-Event-ID on a new request remain the caller's responsibility.
End-to-end example
Using NTResult.AspNetCore.Http on the server and NTResult.Refit on the client preserves both sides of the operation:
- A successful
HttpNTResult<T>becomes a successful clientINTResult<T>containing the deserialized value. - A string error body becomes the client result's
ErrorMessage. - RFC Problem Details responses preserve their specific
detail, withtitleas a fallback. - The Refit response is disposed after conversion unless its disposable content must remain alive.
This lets callers work with one INTResult<T> contract instead of separately handling status codes, response bodies, deserialization errors, and thrown HTTP exceptions.
Legacy TnTResult names
Obsolete TnTResult, HttpTnTResult, and related extension aliases remain available for source compatibility. New code should use the NTResult names shown above.
Build and test
Run these commands from the repository root:
dotnet restore NTResult.slnx
dotnet build NTResult.slnx --configuration Release
dotnet test --project .\NTResult.Tests\NTResult.Tests.csproj --configuration Release
License
NTResult is licensed under the MIT License.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. 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. |
-
net8.0
- No dependencies.
NuGet packages (2)
Showing the top 2 NuGet packages that depend on NTResult:
| Package | Downloads |
|---|---|
|
NTResult.Refit
Refit integration for NTResult, enabling functional result types in Refit-based HTTP clients for .NET. |
|
|
NTResult.AspNetCore.Http
ASP.NET Core integration for NTResult, providing seamless result handling and controller extensions for web APIs. |
GitHub repositories
This package is not used by any popular GitHub repositories.