NTResult 1.6.0

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

<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, and IsCanceled for three-state branching.
  • TryGetValue, GetValueOrDefault, and ValueOr for non-throwing access.
  • ValueOrThrow and ThrowOnFailure when an exception boundary is appropriate.
  • OnCanceled and OnCanceledAsync for optional cancellation-result callbacks; transport cancellation still propagates.
  • OnSuccess, OnFailure, and Finally for small continuations.
  • Async equivalents for Task<INTResult> and Task<INTResult<T>> in NTResult.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:

  1. ProblemDetails.Detail
  2. ProblemDetails.Title
  3. Response body text
  4. HTTP reason phrase
  5. 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 client INTResult<T> containing the deserialized value.
  • A string error body becomes the client result's ErrorMessage.
  • RFC Problem Details responses preserve their specific detail, with title as 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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.

Version Downloads Last Updated
1.6.0 47 9/11/2026
1.5.0 60 9/10/2026
1.4.0 2,101 8/21/2026
1.3.0 134 8/14/2026
1.2.0 275 7/28/2026
1.1.3 928 7/13/2026
1.1.2 157 7/13/2026
1.1.1 168 7/8/2026
1.1.0 2,620 6/8/2026
1.0.0 152 5/29/2026