RestLib.InMemory
0.5.0
See the version list below for details.
dotnet add package RestLib.InMemory --version 0.5.0
NuGet\Install-Package RestLib.InMemory -Version 0.5.0
<PackageReference Include="RestLib.InMemory" Version="0.5.0" />
<PackageVersion Include="RestLib.InMemory" Version="0.5.0" />
<PackageReference Include="RestLib.InMemory" />
paket add RestLib.InMemory --version 0.5.0
#r "nuget: RestLib.InMemory, 0.5.0"
#:package RestLib.InMemory@0.5.0
#addin nuget:?package=RestLib.InMemory&version=0.5.0
#tool nuget:?package=RestLib.InMemory&version=0.5.0
RestLib
3 lines to a production-ready REST API
RestLib is a .NET 8 library for ASP.NET Core Minimal APIs that generates CRUD endpoints from your model and repository. It bakes in secure defaults, cursor pagination, filtering, sorting, OpenAPI metadata, and RFC 9457 Problem Details so you can ship consistent APIs faster.
Install
Install the core package:
dotnet add package RestLib
For demos, tests, and quick prototypes, add the optional in-memory adapter:
dotnet add package RestLib.InMemory
Quick Start
Create a new app:
dotnet new web -n MyApi
cd MyApi
Define a model:
public class Product
{
public Guid Id { get; set; }
public required string Name { get; set; }
public decimal Price { get; set; }
}
Configure RestLib:
using RestLib;
using RestLib.InMemory;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRestLib();
builder.Services.AddRestLibInMemory<Product, Guid>(p => p.Id, Guid.NewGuid);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.MapRestLib<Product, Guid>("/api/products", config =>
{
config.AllowAnonymous();
});
app.Run();
Run the app and open Swagger:
dotnet run
That gives you:
GET /api/products- list all with cursor paginationGET /api/products/{id}- fetch a single resourcePOST /api/products- createPUT /api/products/{id}- replacePATCH /api/products/{id}- partially updateDELETE /api/products/{id}- delete
Why RestLib
Every backend project starts the same way: define a model, write CRUD endpoints, add validation, handle errors, set up pagination, wire Swagger, and repeat for every entity.
RestLib removes that repetition while keeping the parts that matter explicit:
- Proper REST semantics inspired by the Zalando REST API Guidelines
- Secure-by-default endpoints with per-operation opt-out
- Machine-readable RFC 9457 Problem Details responses
- Hook-based extensibility instead of controller inheritance
- OpenAPI metadata and package-ready defaults out of the box
Features
Secure by Default
All endpoints require authorization unless you opt out per operation:
app.MapRestLib<Product, Guid>("/api/products", config =>
{
config.AllowAnonymous(RestLibOperation.GetAll, RestLibOperation.GetById);
config.RequirePolicy(RestLibOperation.Delete, "AdminOnly");
});
Standards-Compliant Responses
RestLib follows the Zalando REST API Guidelines and uses RFC 9457 Problem Details for error payloads.
snake_caseJSON properties- Cursor-based pagination
- Structured validation and error responses
- Consistent HTTP status codes
{
"type": "/problems/not-found",
"title": "Resource Not Found",
"status": 404,
"detail": "Product with ID '999' does not exist.",
"instance": "/api/products/999"
}
Advanced Filtering
Enable query-string filtering with no custom parser code:
app.MapRestLib<Product, Guid>("/api/products", config =>
{
config.AllowFiltering(p => p.CategoryId, p => p.IsActive);
});
Example request:
GET /api/products?category_id=5&is_active=true
Sorting
Control result ordering with an allow-list of sortable properties:
app.MapRestLib<Product, Guid>("/api/products", config =>
{
config.AllowSorting(p => p.Price, p => p.Name);
config.DefaultSort("name:asc");
});
GET /api/products?sort=price:asc,name:desc&limit=20
Sort fields use snake_case names and support asc/desc directions.
Disallowed fields return a 400 Problem Details response.
Rate Limiting
Integrate with ASP.NET Core's rate limiting middleware to throttle requests per operation:
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("read-policy", o => { o.PermitLimit = 100; o.Window = TimeSpan.FromMinutes(1); });
options.AddFixedWindowLimiter("write-policy", o => { o.PermitLimit = 20; o.Window = TimeSpan.FromMinutes(1); });
});
app.UseRateLimiter();
app.MapRestLib<Product, Guid>("/api/products", config =>
{
config.UseRateLimiting("read-policy", RestLibOperation.GetAll, RestLibOperation.GetById);
config.UseRateLimiting("write-policy", RestLibOperation.Create, RestLibOperation.Update);
});
Set a default policy and exempt specific operations:
config.UseRateLimiting("default-policy");
config.DisableRateLimiting(RestLibOperation.GetById);
Rate limiting is opt-in. RestLib applies the named policy to endpoints; the application defines and registers the actual policies.
Select Operations
Expose only the operations you want, and mix custom endpoints with generated ones:
app.MapRestLib<Category, Guid>("/api/categories", config =>
{
config.IncludeOperations(RestLibOperation.GetAll, RestLibOperation.GetById);
});
app.MapPost("/api/categories", async (Category category, IRepository<Category, Guid> repo) =>
{
return Results.Created($"/api/categories/{category.Id}", await repo.CreateAsync(category));
});
You can also move this declarative resource configuration out of Program.cs and into JSON while keeping your model, repository, and hooks strongly typed:
{
"RestLib": {
"Resources": {
"Products": {
"Name": "products",
"Route": "/api/products",
"AllowAnonymousAll": true,
"Operations": {
"Exclude": ["Delete"]
},
"Filtering": ["CategoryId", "IsActive"],
"Sorting": ["Price", "Name", "CreatedAt"],
"DefaultSort": "name:asc",
"OpenApi": {
"Tag": "Product",
"Summaries": {
"GetAll": "List products"
}
}
}
}
}
}
var productResource = builder.Configuration
.GetSection("RestLib:Resources:Products")
.Get<RestLibJsonResourceConfiguration>()!;
builder.Services.AddJsonResource<Product, Guid>(productResource);
var app = builder.Build();
app.MapJsonResources();
Extensible via Hooks
Inject custom logic into the pipeline without subclassing framework types:
app.MapRestLib<Product, Guid>("/api/products", config =>
{
config.UseHooks(hooks =>
{
hooks.BeforePersist = ctx =>
{
if (ctx.Entity is Product product && ctx.Operation == RestLibOperation.Create)
{
product.CreatedAt = DateTime.UtcNow;
}
return Task.CompletedTask;
};
});
});
If you want a cleaner startup file, JSON config can select named hooks per operation while the hook implementations stay in C#:
builder.Services.AddNamedHook<Product, Guid>(HookNames.SetUpdatedAt, ctx =>
{
if (ctx.Entity is Product product)
{
product.UpdatedAt = ctx.Operation == RestLibOperation.Create ? null : DateTime.UtcNow;
}
return Task.CompletedTask;
});
{
"Hooks": {
"BeforePersist": {
"ByOperation": {
"Create": ["SetUpdatedAt"],
"Update": ["SetUpdatedAt"],
"Patch": ["SetUpdatedAt"]
}
}
}
}
This keeps route, auth, filtering, operation selection, OpenAPI metadata, and hook selection in JSON while your actual behavior remains strongly typed and testable in C#. A simple pattern is to centralize hook names in a HookNames class and use those constants when registering handlers.
Persistence-Agnostic
Use the in-memory adapter or plug in your own repository implementation:
public class ProductRepository : IRepository<Product, Guid>
{
private readonly MyDbContext _db;
public ProductRepository(MyDbContext db)
{
_db = db;
}
public async Task<Product?> GetByIdAsync(Guid id, CancellationToken ct)
=> await _db.Products.FindAsync([id], ct);
// Implement the remaining IRepository members...
}
builder.Services.AddRepository<Product, Guid, ProductRepository>();
Performance
RestLib adds minimal overhead compared to hand-written Minimal APIs. In some cases, it is faster due to optimized code paths.
| Operation | Raw API | RestLib | Overhead | Memory |
|---|---|---|---|---|
| GET by ID | 67.5 us | 69.5 us | +3% | +2% |
| GET all | 173.3 us | 116.5 us | -33% | +7% |
| POST | 97.3 us | 99.4 us | +2% | +13% |
| PUT | 88.6 us | 114.2 us | +29% | +13% |
Benchmarks were run on .NET 8.0 with 100 seeded items.
<details> <summary>Full benchmark results</summary>
BenchmarkDotNet v0.14.0, Windows 11 (10.0.26200.7623)
Intel Core i3-8130U CPU 2.20GHz (Kaby Lake), 1 CPU, 4 logical and 2 physical cores
.NET SDK 9.0.308
[Host] : .NET 8.0.22 (8.0.2225.52707), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.22 (8.0.2225.52707), X64 RyuJIT AVX2
| Method | Categories | Mean | Error | StdDev | Ratio | Allocated | Alloc Ratio |
|------------------------------ |----------- |----------:|---------:|----------:|------:|----------:|------------:|
| 'Raw Minimal API - POST' | Create | 97.30 us | 5.561 us | 16.396 us | 1.00 | 11.65 KB | 1.00 |
| 'RestLib - POST' | Create | 99.35 us | 1.567 us | 1.308 us | 1.02 | 13.20 KB | 1.13 |
| | | | | | | | |
| 'Raw Minimal API - GET all' | GetAll | 173.26 us | 9.706 us | 28.619 us | 1.00 | 17.34 KB | 1.00 |
| 'RestLib - GET all' | GetAll | 116.54 us | 1.539 us | 3.037 us | 0.67 | 18.62 KB | 1.07 |
| | | | | | | | |
| 'Raw Minimal API - GET by ID' | GetById | 67.49 us | 4.209 us | 12.409 us | 1.00 | 10.15 KB | 1.00 |
| 'RestLib - GET by ID' | GetById | 69.48 us | 4.483 us | 13.218 us | 1.03 | 10.31 KB | 1.02 |
| | | | | | | | |
| 'Raw Minimal API - PUT' | Update | 88.64 us | 1.745 us | 3.010 us | 1.00 | 12.22 KB | 1.00 |
| 'RestLib - PUT' | Update | 114.16 us | 6.813 us | 20.088 us | 1.29 | 13.86 KB | 1.13 |
</details>
Learn More
Architecture Decisions
Key decisions are documented as Architecture Decision Records:
| ADR | Decision |
|---|---|
| ADR-001 | Cursor-based pagination over offset |
| ADR-002 | Authorization required by default |
| ADR-003 | Minimal APIs over controllers |
| ADR-004 | snake_case JSON naming |
| ADR-005 | RFC 9457 Problem Details for errors |
| ADR-006 | Operation allowlists and denylists |
Packages
| Package | Description | NuGet |
|---|---|---|
RestLib |
Core library | RestLib |
RestLib.InMemory |
In-memory repository for testing and prototyping | RestLib.InMemory |
Requirements
- .NET 8.0 or later
- ASP.NET Core Minimal APIs
Contributing
Contributions are welcome. Read the contributing guide.
License
This project is licensed under the MIT License. See the license.
Acknowledgments
| 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
- RestLib (>= 0.5.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.5.2 | 93 | 5/27/2026 |
| 2.5.1 | 94 | 5/18/2026 |
| 2.5.0 | 108 | 5/13/2026 |
| 2.4.0 | 91 | 5/13/2026 |
| 2.3.0 | 113 | 4/16/2026 |
| 2.2.1 | 105 | 4/12/2026 |
| 2.2.0 | 107 | 4/11/2026 |
| 2.1.0 | 104 | 4/11/2026 |
| 2.0.0 | 108 | 4/10/2026 |
| 1.3.1 | 101 | 4/6/2026 |
| 1.3.0 | 101 | 4/4/2026 |
| 1.2.0 | 103 | 4/4/2026 |
| 1.1.0 | 106 | 4/4/2026 |
| 1.0.0 | 102 | 4/4/2026 |
| 0.8.0 | 104 | 4/1/2026 |
| 0.7.0 | 106 | 3/31/2026 |
| 0.6.0 | 103 | 3/30/2026 |
| 0.5.0 | 102 | 3/29/2026 |
| 0.4.0 | 101 | 3/26/2026 |
| 0.3.0 | 112 | 3/11/2026 |