NebulaSpace.Outcome
0.4.0
dotnet add package NebulaSpace.Outcome --version 0.4.0
NuGet\Install-Package NebulaSpace.Outcome -Version 0.4.0
<PackageReference Include="NebulaSpace.Outcome" Version="0.4.0" />
<PackageVersion Include="NebulaSpace.Outcome" Version="0.4.0" />
<PackageReference Include="NebulaSpace.Outcome" />
paket add NebulaSpace.Outcome --version 0.4.0
#r "nuget: NebulaSpace.Outcome, 0.4.0"
#:package NebulaSpace.Outcome@0.4.0
#addin nuget:?package=NebulaSpace.Outcome&version=0.4.0
#tool nuget:?package=NebulaSpace.Outcome&version=0.4.0
<div align="center">
NebulaSpace Outcome
A dependency-free result pattern for .NET with an ASP.NET Core integration that turns outcomes into clean HTTP responses.
Outcome<T> · Failure · IHttpResultFactory · Success/Error envelopes · exception mapping
</div>
Table of contents
- Why Outcome?
- Quick start
- How the pieces fit
- Prerequisites
- Packages
- Core types
- Failures
- ASP.NET Core integration
- MVC controllers
- Response envelopes
- Exception handling
- Pagination
- JSON serialization
- Testing
- Claude Code plugin
- Contributing
- License
Why Outcome?
Exceptions are for exceptional things. "Not found", "duplicate", "out of quota" are expected outcomes, not bugs. Model them as exceptions and you get try/catch control flow, a contract that hides the failure, and no compiler help.
Outcome<T> puts failure in the method signature. The caller has to handle both branches.
// Before: failure is invisible
public async Task<Order> CreateOrder(CreateOrderRequest request)
{
if (await _repo.Exists(request.Product))
throw new ConflictException("Product already exists");
return await _repo.Save(new Order(request.Product));
}
// After: the contract is explicit
public async Task<Outcome<Order>> CreateOrder(CreateOrderRequest request)
{
if (await _repo.Exists(request.Product))
return Outcome.Fail(new ConflictFailure("Product already exists"));
return await _repo.Save(new Order(request.Product)); // implicit success
}
On the web this pays off immediately. The same outcome becomes a 200 with your data, or the correct 4xx/5xx with a consistent error body. No try/catch, no hand-rolled error JSON.
Quick start
1. Install
dotnet add package NebulaSpace.Outcome
dotnet add package NebulaSpace.Outcome.AspNetCore
2. Return an Outcome<T> from your service
using NebulaSpace.Outcome;
using NebulaSpace.Outcome.AspNetCore.Failures;
public sealed class GreetingService
{
public Outcome<string> Greet(string name) =>
string.IsNullOrWhiteSpace(name)
? Outcome.Fail(new BadRequestFailure("Name cannot be empty."))
: Outcome.Success($"Hello, {name}!");
}
3. Register and map to an HTTP response
using NebulaSpace.Outcome.AspNetCore.Extensions;
using NebulaSpace.Outcome.AspNetCore.Http;
// Register once in Program.cs:
builder.Services.AddOutcome(options =>
{
options.Success.SetResponse(typeof(SuccessResponse<>)); // non-paginated
options.Success.SetResponse(typeof(SuccessResponse<,>)); // paginated
options.Success.SetResponse(typeof(SuccessResponse)); // no-payload (http.Ok())
options.Error.SetResponse<ErrorResponse>();
});
// Then in your endpoint:
app.MapGroup("/greet")
.MapGet("/{name}", (string name, IGreetingService service, IHttpResultFactory http) =>
service.Greet(name).ToResult(http, greeting => http.Ok(new { greeting })));
That's it. Success runs your handler; failure becomes the right HTTP error. The endpoint never sees a try/catch.
Full runnable sample:
samples/NebulaSpace.Outcome.Samples.WebApi.
How the pieces fit
One outcome, one response:
service endpoint HTTP
┌──────────────┐ ┌──────────────────────┐ ┌─────────────────┐
│ Outcome<T> │ │ .ToResult(http, ok) │ │ 200 + success │
│ success→data │ ───▶ │ success → run ok() │ ───▶ │ 4xx + error │
│ failure→Fail │ │ failure → map+render│ │ 5xx + error │
└──────────────┘ └──────────────────────┘ └─────────────────┘
The moving parts:
Outcome<T>— what your service returns. Success carries a value; failure carries aFailure.IHttpResultFactory— how your endpoint turns that outcome into anIResult.http.Ok(...),http.Created(...),http.NotFound(...), and the rest.- Envelopes — the response body. Success envelopes wrap your data; error envelopes wrap the failure. Swap one out and your API's response shape changes in a single place.
- Failure mapping — how a
Failurebecomes HTTP intent (status code, code, message). Built-in failures already know their status; app-specific failures useMapFailure<T>orIHttpFailure.
None of this is mandatory. The defaults work as-is; you override the parts your API needs to be consistent about.
Prerequisites
Before wiring up the ASP.NET Core integration, know the extension points. You do not have to implement all of them, but pagination expects you to bring your own types.
| Concept | Interface / type | Default if you don't customize |
|---|---|---|
| Result pattern | Outcome<T>, Failure, Unit |
Required — built into the core package |
| HTTP result mapping | IHttpResultFactory |
One is registered by AddOutcome |
| App success envelope | ISuccessEnvelope<T> (or non-generic) |
DefaultSuccessResponse (status-only) |
| App error envelope | IErrorEnvelope |
RFC 9457 ProblemDetails |
| Failure → HTTP mapping | IHttpFailure (per-failure) or options.Error.MapFailure<T>(...) (startup) |
Built-in BadRequestFailure, NotFoundFailure, etc. |
| Pagination | IPaginatedQuery (query) + IPaginationDetails (info) |
None — define your own. The Default* types are reference implementations, not a recommendation. See Pagination. |
Packages
| Package | What's inside | Targets | Dependencies |
|---|---|---|---|
NebulaSpace.Outcome |
Outcome, Outcome<T>, Failure, Unit |
netstandard2.0 | none |
NebulaSpace.Outcome.AspNetCore |
IHttpResultFactory, options, failure types, pagination, exception handling |
net7.0 | core + Microsoft.AspNetCore.App |
The core stays netstandard2.0 so it runs on .NET Framework 4.6.1+. The ASP.NET Core package
takes the net7.0 floor to enable
static abstract interface members
(C# 11). Both install on every later runtime.
The Type-based overloads (SetResponse(typeof(...))) close open-generic envelopes with
MakeGenericType per request, so Swagger documents Data: Order without writing a factory
per payload type. The delegate and generic overloads (SetResponse(Func<...>),
SetResponse<TEnvelope>()) are the reflection-free path and the recommended default.
Core types
Outcome<T> is an immutable readonly struct — a discriminated union. It is either a success (with a Value) or a failure (with a Failure), and its state is fixed at creation.
Outcome<string> ok = Outcome.Success("hello"); // success with a value
Outcome<Unit> done = Outcome.Success(); // success, no payload
Outcome<string> failed = Outcome.Fail(new NotFoundFailure("Not found"));
Outcome<Order> order = new Order(...); // implicit conversion
Reading the state
| Member | Description |
|---|---|
IsSuccess / IsFailure |
Which branch you're in |
Value |
Payload on success (throws InvalidOperationException on failure) |
Failure |
The Failure on failure (throws on success) |
Match(onSuccess, onFailure) |
Collapse both branches into one value |
Deconstruct(...) |
C# switch / pattern matching support |
Matching
// Idiomatic — forces you to handle both branches
var display = result.Match(
onSuccess: value => $"OK: {value}",
onFailure: failure => $"Error: {failure.Message}");
// Pattern matching
string message = result switch
{
(true, var value, _) => $"Value is {value}",
(false, _, var failure) => $"Failed with {failure.Message}",
_ => "Unreachable",
};
Service patterns
public async Task<Outcome<Order>> GetOrder(int id)
{
var order = await _repo.FindById(id);
if (order is null)
return Outcome.Fail(new NotFoundFailure("Order not found"));
return order; // implicit conversion
}
public async Task<Outcome<Unit>> DeleteOrder(int id)
{
if (!await _repo.Exists(id))
return Outcome.Fail(new NotFoundFailure("Order not found"));
await _repo.Remove(id);
return Outcome.Success();
}
Failures
Failures are immutable values derived from Failure. The base type carries a Message and an optional Code:
public sealed class NotFoundFailure : Failure
{
public NotFoundFailure(string message) : base(message, "not_found") { }
}
Built-in failures
These ship in the AspNetCore package and know their own HTTP intent. No registration needed:
| Failure | HTTP | Code |
|---|---|---|
BadRequestFailure |
400 | bad_request |
UnauthorizedFailure |
401 | unauthorized |
ForbiddenFailure |
403 | forbidden |
NotFoundFailure |
404 | not_found |
ConflictFailure |
409 | conflict |
ValidationFailure |
400 | validation |
InternalServerErrorFailure |
500 | internal_server_error |
Custom failures — three approaches
1. MapFailure<T> at startup — when you can't modify the failure type:
options.Error.MapFailure<QuotaExceededFailure>(f =>
new FailureDetails(StatusCodes.Status422UnprocessableEntity, f.Message, f.Code));
2. Implement IHttpFailure — the failure carries its own HTTP intent (Open/Closed):
public sealed class QuotaExceededFailure : Failure, IHttpFailure
{
public QuotaExceededFailure(string message) : base(message, "quota_exceeded") { }
public FailureDetails ToDetails() =>
new(StatusCodes.Status422UnprocessableEntity, Message, Code);
}
3. Typed error envelope — changes the shape of every error response. See Response envelopes.
Resolution order
MapFailure<T>wins, even over built-in failures.IHttpFailuremaps itself.- Unknown failures degrade to 500 (never throws).
A subclass inherits its nearest ancestor's mapping.
ASP.NET Core integration
Register
builder.Services.AddOutcome(options =>
{
options.Success.SetResponse(typeof(SuccessResponse<>)); // non-paginated
options.Success.SetResponse(typeof(SuccessResponse<,>)); // paginated
options.Success.SetResponse(typeof(SuccessResponse)); // no-payload (http.Ok())
options.Error.SetResponse<ErrorResponse>();
options.Error.MapFailure<QuotaExceededFailure>(f =>
new FailureDetails(StatusCodes.Status422UnprocessableEntity, f.Message, f.Code));
});
AddOutcome registers OutcomeOptions and IHttpResultFactory as singletons.
Multiple apps in one process can configure the library independently.
From outcome to response
Inject IHttpResultFactory into your endpoint or service:
// Sync
result.ToResult(http, order => http.Created(order, o => o.Id));
// Async
await outcomeTask.ToResultAsync(http, order => http.Created(order, o => o.Id));
// Async success handler
await outcomeTask.ToResultAsync(http, async order => await store.Save(order, http));
// Failure-only
failure.ToErrorResponse(http);
IHttpResultFactory helpers
| Helper | Behavior |
|---|---|
http.Ok() |
200 with no payload (status-only envelope) |
http.Ok(data) |
200, raw or wrapped per Success.Enabled |
http.Ok(data, pagination) |
200 with pagination metadata |
http.Created(data, idSelector) |
201 with Location derived from the request path |
http.Created(data, locationUri) |
201 with a manually specified Location |
http.NoContent() |
204 |
http.BadRequest(...) / http.NotFound(...) / http.Conflict(...) |
4xx with the configured error envelope |
http.FromFailure(failure) |
Map any failure to its response |
http.Created builds the Location header from the request path automatically:
return result.ToResult(http, order => http.Created(order, o => o.Id));
// POST /orders -> Location: /orders/{id}
// POST /admin/orders -> Location: /admin/orders/{id}
For non-standard URIs, use the manual overload:
return result.ToResult(http, order => http.Created(order, $"/orders/{order.Id}/items/42"));
MVC controllers
The library supports MVC controllers through two patterns. Both reuse the same IHttpResultFactory, envelopes, and failure mapping as Minimal APIs.
Pattern 1: [OutcomeResult] filter
Return Outcome<T> directly from an action. The filter converts it to the correct HTTP response:
[ApiController, Route("products")]
[OutcomeResult] // per-controller or global
public class ProductsController : ControllerBase
{
private readonly IProductService _service;
public ProductsController(IProductService service) => _service = service;
[HttpGet("{id:guid}")]
public Outcome<Product> Get(Guid id) => _service.GetProduct(id); // 200 or 404
[HttpDelete("{id:guid}")]
public Outcome<Unit> Delete(Guid id) => _service.DeleteProduct(id); // 204 or 404
}
Pattern 2: explicit ToActionResult / ToActionResultAsync
For custom success responses (201 with Location, pagination metadata, etc.):
[HttpPost]
public Task<IActionResult> Create([FromBody] CreateProductRequest request) =>
_service.CreateProduct(request)
.ToActionResultAsync(_http, product => _http.Created(product, p => p.Id));
[HttpGet]
public IActionResult List([FromQuery] OffsetQuery query)
{
var result = _service.GetProducts(query);
return Outcome.Success(result.Items)
.ToActionResult(_http, items => _http.Ok(items, result.ToOffsetInfo(query)));
}
Register
builder.Services.AddControllers();
builder.Services.AddOutcomeMvcFilter(); // registers [OutcomeResult] as a global filter
var app = builder.Build();
app.MapControllers();
The full controller sample lives alongside the Minimal API sample in
samples/NebulaSpace.Outcome.Samples.WebApi/Features/Products/ProductsController.cs.
Response envelopes
Define your own envelopes. The defaults are minimal: RFC 7807
DefaultProblemDetailsfor errors, a thin{ statusCode, data }wrapper for successes. Most APIs want one consistent, documented response shape —traceId,code, field-level errors. Typed envelopes give you:
- Your type, serialized as-is. The body is
ErrorResponse/SuccessResponse<T>. OpenAPI documents the exact shape. Properties are real C#. - One consistent body everywhere. Failures,
MapFailureresults, mapped exceptions, and unhandled exceptions all render through the same envelope. - Early errors at startup. If a type doesn't implement the interface,
SetResponsethrows immediately.
Success envelope
Implement ISuccessEnvelope<T> on an open-generic type. The library closes T per request so Swagger sees the exact payload type:
public sealed class SuccessResponse<TData> : ISuccessEnvelope<TData>
{
public int StatusCode { get; init; }
public TData? Data { get; init; }
public static ISuccessEnvelope<TData> From(
TData data, IPaginationDetails? pagination, HttpContext httpContext, int statusCode)
=> new SuccessResponse<TData> { StatusCode = statusCode, Data = data };
}
For paginated responses, use a 2-param generic so the second parameter carries the concrete pagination type (DefaultOffsetInfo, DefaultCursorInfo, or your own):
public sealed class SuccessResponse<TData, TPage> : ISuccessEnvelope<TData>
{
public int StatusCode { get; init; }
public TData? Data { get; init; }
public TPage? Pagination { get; init; }
public static ISuccessEnvelope<TData> From(
TData data, IPaginationDetails? pagination, HttpContext httpContext, int statusCode)
=> new SuccessResponse<TData, TPage>
{
StatusCode = statusCode,
Data = data,
Pagination = pagination is TPage typed ? typed : default,
};
}
options.Success.SetResponse(typeof(SuccessResponse<>)); // non-paginated
options.Success.SetResponse(typeof(SuccessResponse<,>)); // paginated
.Produces<SuccessResponse<Order>>() // Swagger knows Data: Order
.Produces<SuccessResponse<Order[], DefaultOffsetInfo>>() // Swagger knows Data + Pagination
When Success.Enabled is false, successes are returned raw with no wrapper.
No-payload envelope
http.Ok() (no arguments) returns a status-only envelope with no data field. The non-generic envelope type needs only a From(HttpContext, int) method — no ISuccessEnvelope<T> contract required:
public sealed class SuccessResponse
{
public int StatusCode { get; init; }
public static object From(HttpContext httpContext, int statusCode) =>
new SuccessResponse { StatusCode = statusCode };
}
options.Success.SetResponse(typeof(SuccessResponse)); // non-generic, no payload
group.MapGet("/ping", (IHttpResultFactory http) => http.Ok())
.Produces<SuccessResponse>(); // Swagger: { statusCode: 200 }
Without registration, the default is DefaultSuccessResponse — { "statusCode": 200 }.
Error envelope
ErrorOptions.SetResponse<T>() makes the error body any class implementing IErrorEnvelope. Zero-reflection dispatch via static abstract:
public sealed class ErrorResponse : IErrorEnvelope
{
public int? StatusCode { get; init; }
public string? Code { get; init; }
public string? Message { get; init; }
public string? TraceId { get; init; }
public IReadOnlyDictionary<string, string[]>? Errors { get; init; }
public static IErrorEnvelope From(FailureDetails details, HttpContext context) => new ErrorResponse
{
StatusCode = details.StatusCode,
Code = details.Code,
Message = details.Message,
TraceId = context.TraceIdentifier,
Errors = details.Errors,
};
}
options.Error.SetResponse<ErrorResponse>();
.Produces<SuccessResponse<Order>>()
.Produces<ErrorResponse>(StatusCodes.Status404NotFound);
{
"statusCode": 404,
"code": "not_found",
"message": "Order not found",
"traceId": "0HLMS12B6V4H6:00000001"
}
Without SetResponse, the default is RFC 7807 with only title, status, detail.
Exception handling
Unhandled exceptions normally fall through to ASP.NET Core's default error page — a different shape. The exception-handling middleware converts them into the same envelope as expected failures:
builder.Services.AddOutcome(options =>
{
options.Error.MapException<DomainException>(ex =>
new FailureDetails(StatusCodes.Status422UnprocessableEntity, ex.Message, ex.Code));
// FluentValidation → 400 with field errors (RFC 9457)
options.Error.MapException<ValidationException>(ex =>
new FailureDetails(
StatusCodes.Status400BadRequest,
"Validation Error",
code: "validation_error",
errors: ex.Errors
.GroupBy(e => e.PropertyName)
.ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray())));
options.Error.IncludeExceptionDetails = builder.Environment.IsDevelopment();
});
var app = builder.Build();
app.UseOutcomeExceptionHandler(); // BEFORE routing
How mapping works
MapException<T>works likeMapFailure<T>— register any exception type, the mapper returns HTTP intent, the error envelope renders it. Mapped exceptions bypassIncludeExceptionDetails.- Unmapped exceptions are logged in full (with stack trace) and become a generic 500.
IncludeExceptionDetails = false(default) never leaksex.Messageto the client. If the response has already started, the exception is re-thrown.
Ordering with custom middleware
UseOutcomeExceptionHandler only renders the response. To log exceptions to your own store, register your middleware before the handler — it catches, records, re-throws:
// 1. Your logging middleware FIRST (outermost)
app.Use(async (context, next) =>
{
try { await next(); }
catch (Exception ex)
{
await context.RequestServices
.GetRequiredService<IExceptionLogService>()
.LogAsync(context, ex);
throw;
}
});
// 2. THEN the outcome exception handler (inner)
app.UseOutcomeExceptionHandler();
Middleware runs outer-to-inner: your middleware listed first wraps everything after it.
Pagination
Offset, cursor, and custom strategies all work through one IPaginationDetails marker interface and http.Ok(data, pagination).
Build your own pagination types. The library ships
DefaultOffsetQuery/DefaultCursorQueryas reference implementations, but real APIs benefit from owning the query, result, and metadata types — app-specific caps, naming, extra fields, or a different strategy altogether. The sample project demonstrates this with customOffsetQueryandCursorQuerytypes built from scratch. Seesamples/NebulaSpace.Outcome.Samples.WebApi/Shared/.
Configuring limits
options.Pagination.MaxPage = 25; // max page number (offset)
options.Pagination.MaxLimit = 50; // max items per page (both strategies)
Defaults are int.MaxValue (uncapped). Limits live on static fields (not properties) to avoid ASP.NET Core's RequestDelegateFactory binding them as query parameters.
Building your own pagination
Every pagination strategy needs three types:
| Type | Implements | Role |
|---|---|---|
| Query | IPaginatedQuery |
Incoming parameters, bound from query string via [AsParameters] |
| Result | — | Service-layer return: items + raw navigation data |
| Info | IPaginationDetails |
Response metadata that goes into the success envelope |
AddOutcome discovers all IPaginatedQuery implementors at startup and calls Configure(PaginationOptions) on each — so your custom query types pick up the app's caps automatically.
// 1. Query — AddOutcome calls Configure() automatically
public sealed class OffsetQuery : IPaginatedQuery
{
public static int MaxPage = int.MaxValue;
public static int MaxLimit = int.MaxValue;
private int _page = 1;
private int _limit = 10;
public int Page { get => _page; set => _page = Math.Clamp(value, 1, MaxPage); }
public int Limit { get => _limit; set => _limit = Math.Clamp(value, 1, MaxLimit); }
public int Offset => (Page - 1) * Limit;
public static void Configure(PaginationOptions options)
{
MaxPage = Math.Max(1, options.MaxPage);
MaxLimit = Math.Max(1, options.MaxLimit);
}
}
// 2. Result — returned by the service layer
public sealed class OffsetResult<T>
{
public IReadOnlyList<T> Items { get; set; } = Array.Empty<T>();
public long TotalCount { get; set; }
public OffsetInfo ToOffsetInfo(OffsetQuery query) =>
OffsetInfo.Create(query, (int)TotalCount);
}
// 3. Info — goes into the success envelope
public sealed class OffsetInfo : IPaginationDetails
{
public int Page { get; set; }
public int Limit { get; set; }
public int TotalPages { get; set; }
public long? TotalItems { get; set; }
public bool HasMore => Page < TotalPages;
public static OffsetInfo Create(OffsetQuery query, int totalItems) => new()
{
Page = query.Page,
Limit = query.Limit,
TotalPages = totalItems == 0 ? 0 : (int)Math.Ceiling((double)totalItems / query.Limit),
TotalItems = totalItems,
};
}
Using pagination in an endpoint
// Service
public OffsetResult<Order> GetOrders(OffsetQuery query)
{
var items = _orders.Skip(query.Offset).Take(query.Limit).ToArray();
return new OffsetResult<Order> { Items = items, TotalCount = _orders.Count };
}
// Endpoint
group.MapGet("/", (IOrderService service, [AsParameters] OffsetQuery query, IHttpResultFactory http) =>
Outcome.Success(service.GetOrders(query)).ToResult(http, page =>
http.Ok(page.Items, page.ToOffsetInfo(query))))
.Produces<SuccessResponse<Order[], OffsetInfo>>();
Built-in defaults
The library ships ready-made types if you don't need custom naming or extra fields:
| Type | Description |
|---|---|
DefaultOffsetQuery |
page / limit with MaxPage / MaxLimit caps |
DefaultOffsetResult<T> |
Items + TotalCount |
DefaultOffsetInfo |
Page, Limit, TotalPages, TotalItems, HasMore |
DefaultCursorQuery |
cursor / limit with MaxLimit cap |
DefaultCursorResult<T> |
Items, PreviousCursor, NextCursor, TotalCount |
DefaultCursorInfo |
PreviousCursor, NextCursor, HasMore, TotalItems |
// Using the built-in defaults directly
group.MapGet("/", (IOrderService service, [AsParameters] DefaultOffsetQuery query, IHttpResultFactory http) =>
Outcome.Success(service.GetOrders(query)).ToResult(http, page =>
http.Ok(page.Items, page.ToOffsetInfo(query))))
.Produces<SuccessResponse<Order[], DefaultOffsetInfo>>();
Custom strategies
Any strategy (keyset, time-based, composite) works — implement the same three-type pattern (query, result, info) and it plugs straight into http.Ok(data, pagination). The sample project demonstrates this with custom offset and cursor types built from scratch.
JSON serialization
The library writes envelopes with WriteAsJsonAsync, which honors your app's JSON serializer options. A recommended setup (nulls omitted, camelCase, enums as strings):
using System.Text.Json;
using System.Text.Json.Serialization;
// Minimal API
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
// MVC (controllers) — configured separately
builder.Services.Configure<Microsoft.AspNetCore.Mvc.JsonOptions>(options =>
{
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
Testing
tests/
├── NebulaSpace.Outcome.Tests.Unit/ # core: Outcome, Failure, Match, deconstruct
├── NebulaSpace.Outcome.Tests.Integration/ # core integration (placeholder)
├── NebulaSpace.Outcome.AspNetCore.Tests.Unit/ # in-memory: factories, envelopes, pagination
└── NebulaSpace.Outcome.AspNetCore.Tests.Integration/ # TestServer: exception middleware end-to-end
dotnet test
Claude Code plugin
This repo ships a Claude Code plugin with skills that give Claude accurate guidance for using the library.
Install
/plugin marketplace add nebulaspace-id/outcome-csharp
/plugin install outcome-tools@outcome-csharp
What's inside
| Skill | Covers |
|---|---|
outcome-core |
Outcome<T>, Failure, Unit, matching, service patterns |
outcome-aspnetcore |
AddOutcome, IHttpResultFactory, envelopes, error mapping, pagination, endpoints |
Claude automatically loads the relevant skill when you write code that uses the library.
Team auto-config
Pre-register the marketplace for your team so every contributor only needs the install step. Add to .claude/settings.json:
"extraKnownMarketplaces": {
"outcome-csharp": {
"source": { "source": "github", "repo": "nebulaspace-id/outcome-csharp" }
}
}
Contributing
Contributions are welcome! Open an issue or a pull request.
- Bug reports: include the .NET version, package version, and a minimal repro.
- Feature requests: explain the use case; API changes are discussed before coding.
- PRs: keep the diff focused, add tests for new behavior, and ensure
dotnet build -warnaserroranddotnet testpass.
License
MIT © NebulaSpace
| 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 | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. 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.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on NebulaSpace.Outcome:
| Package | Downloads |
|---|---|
|
NebulaSpace.Outcome.AspNetCore
NebulaSpace Outcome — a result pattern library for .NET with ASP.NET Core integration. |
GitHub repositories
This package is not used by any popular GitHub repositories.