RestLib.InMemory
2.2.1
See the version list below for details.
dotnet add package RestLib.InMemory --version 2.2.1
NuGet\Install-Package RestLib.InMemory -Version 2.2.1
<PackageReference Include="RestLib.InMemory" Version="2.2.1" />
<PackageVersion Include="RestLib.InMemory" Version="2.2.1" />
<PackageReference Include="RestLib.InMemory" />
paket add RestLib.InMemory --version 2.2.1
#r "nuget: RestLib.InMemory, 2.2.1"
#:package RestLib.InMemory@2.2.1
#addin nuget:?package=RestLib.InMemory&version=2.2.1
#tool nuget:?package=RestLib.InMemory&version=2.2.1
RestLib
3 lines to a production-ready REST API
RestLib is a .NET 10 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, field selection, HATEOAS hypermedia links, 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;
using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRestLib();
builder.Services.AddRestLibInMemory<Product, Guid>(p => p.Id, Guid.NewGuid);
builder.Services.AddOpenApi();
var app = builder.Build();
app.MapOpenApi();
app.MapScalarApiReference();
app.MapRestLib<Product, Guid>("/api/products", config =>
{
config.AllowAnonymous();
});
app.Run();
Run the app and open the API reference at /scalar:
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
- Opt-in HATEOAS hypermedia links (Richardson Maturity Model Level 3)
- 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 (forward-only by design — see ADR-001)
- 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);
config.AllowFiltering(p => p.Price, FilterOperators.Comparison);
config.AllowFiltering(p => p.Name, FilterOperators.String);
});
Equality filters use direct query parameters:
GET /api/products?category_id=5&is_active=true
Operator filters use bracket syntax for ranges, partial matches, and set membership:
GET /api/products?price[gte]=20&price[lte]=100
GET /api/products?name[contains]=widget
GET /api/products?status[in]=active,pending
Ten operators are available: eq, neq, gt, lt, gte, lte, contains,
starts_with, ends_with, and in. Each property declares which operators it supports via
preset arrays (FilterOperators.Comparison, FilterOperators.String,
FilterOperators.All) or individual FilterOperator values. Eq is always
implicitly allowed. See ADR-013 for details.
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.
Field Selection
Return only the fields your client needs with sparse fieldsets:
app.MapRestLib<Product, Guid>("/api/products", config =>
{
config.AllowFieldSelection(p => p.Id, p => p.Name, p => p.Price, p => p.CategoryId);
});
GET /api/products?fields=id,name,price
Only the selected fields are included in the response. Unknown or disallowed
fields return a 400 Problem Details response. If no fields parameter is sent,
the full entity is returned.
Field selection works with both GetAll (collection) and GetById (single entity) endpoints, and combines with filtering, sorting, and pagination.
Batch Operations
Create, update, patch, or delete multiple resources in a single request:
app.MapRestLib<Product, Guid>("/api/products", config =>
{
config.EnableBatch(BatchAction.Create, BatchAction.Delete, BatchAction.Patch);
});
POST /api/products/batch
Content-Type: application/json
{
"action": "create",
"items": [
{ "name": "Keyboard", "price": 49.99 },
{ "name": "Mouse", "price": 29.99 }
]
}
The response reports per-item status. All succeeded returns 200; mixed results return 207 Multi-Status with individual status codes per item.
Batch size is limited to 100 items by default (configurable via
RestLibOptions.MaxBatchSize). Hooks fire once per item, and validation runs
per item with errors reported individually.
HATEOAS Hypermedia Links
Enable HAL-style _links on every entity response for discoverability:
builder.Services.AddRestLib(opts =>
{
opts.EnableHateoas = true;
});
Responses include contextual navigation links:
{
"id": "a1b2c3d4-...",
"name": "Keyboard",
"price": 49.99,
"_links": {
"self": { "href": "https://api.example.com/api/products/a1b2c3d4-..." },
"collection": { "href": "https://api.example.com/api/products" },
"update": { "href": "https://api.example.com/api/products/a1b2c3d4-..." },
"patch": { "href": "https://api.example.com/api/products/a1b2c3d4-..." }
}
}
Links are CRUD-aware: update, patch, and delete only appear when those
operations are enabled on the endpoint. Batch responses include per-item links.
For custom link relations (e.g., related resources), implement
IHateoasLinkProvider<TEntity, TKey>:
public class ProductLinkProvider : IHateoasLinkProvider<Product, Guid>
{
public IEnumerable<HateoasLink> GetLinks(Product entity, Guid key, string baseUrl, string collectionPath)
{
yield return new HateoasLink("category", $"{baseUrl}/api/categories/{entity.CategoryId}");
}
}
builder.Services.AddHateoasLinkProvider<Product, Guid, ProductLinkProvider>();
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>();
Versioning
RestLib integrates with any ASP.NET Core versioning strategy via route groups.
URL prefix versioning
var v1 = app.MapGroup("/api/v1");
var v2 = app.MapGroup("/api/v2");
v1.MapRestLib<Product, Guid>("/products", cfg =>
{
cfg.AllowAnonymous();
cfg.ExcludeOperations(RestLibOperation.Patch, RestLibOperation.Delete);
cfg.AllowFiltering(p => p.CategoryId);
});
v2.MapRestLib<Product, Guid>("/products", cfg =>
{
cfg.AllowAnonymous();
cfg.AllowFiltering(p => p.CategoryId, p => p.IsActive);
cfg.AllowSorting(p => p.Price, p => p.Name);
cfg.AllowFieldSelection(p => p.Id, p => p.Name, p => p.Price);
});
Prefix-less overload on a route group
When the route group already has the full path configured, use the prefix-less overload:
app.MapGroup("/api/v1/products").MapRestLib<Product, Guid>(cfg =>
{
cfg.AllowAnonymous();
});
With Asp.Versioning.Http
// Install: Asp.Versioning.Http
builder.Services.AddApiVersioning();
var versionedApi = app.NewVersionedApi("Products");
versionedApi
.MapGroup("/api/v{version:apiVersion}/products")
.HasApiVersion(1.0)
.MapRestLib<Product, Guid>(cfg => cfg.AllowAnonymous());
versionedApi
.MapGroup("/api/v{version:apiVersion}/products")
.HasApiVersion(2.0)
.MapRestLib<Product, Guid>(cfg =>
{
cfg.AllowAnonymous();
cfg.AllowFieldSelection(p => p.Id, p => p.Name, p => p.Price);
});
RestLib does not depend on Asp.Versioning.Http — install it only if you need
query-string, header, or media-type versioning strategies.
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 | 170.5 us | 217.4 us | +27% | +4% |
| GET all | 313.4 us | 271.7 us | -13% | +14% |
| POST | 265.5 us | 384.8 us | +45% | +13% |
| PUT | 232.1 us | 282.0 us | +22% | +13% |
Benchmarks were run on .NET 10.0.5 with 100 seeded items on Linux (Ubuntu 24.04).
Absolute times are higher than typical due to running without process priority
elevation — focus on the relative overhead (Ratio column) rather than raw
microseconds. Re-run with cd benchmarks/RestLib.Benchmarks && dotnet run -c Release
to get numbers for your hardware.
<details> <summary>Full benchmark results</summary>
BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Intel Core i3-8130U CPU 2.20GHz (Kaby Lake), 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.201
[Host] : .NET 10.0.5 (10.0.5, 10.0.526.15411), X64 RyuJIT x86-64-v3
DefaultJob : .NET 10.0.5 (10.0.5, 10.0.526.15411), X64 RyuJIT x86-64-v3
| Method | Categories | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio |
|-------------------------------------------- |--------------- |---------:|---------:|----------:|---------:|------:|--------:|----------:|------------:|
| 'Raw Minimal API - POST' | Create | 265.5 us | 30.76 us | 89.74 us | 260.1 us | 1.12 | 0.56 | 12.46 KB | 1.00 |
| 'RestLib - POST' | Create | 384.8 us | 71.82 us | 203.73 us | 326.7 us | 1.63 | 1.07 | 14.10 KB | 1.13 |
| | | | | | | | | | |
| 'Raw Minimal API - GET all' | GetAll | 313.4 us | 39.61 us | 111.73 us | 285.8 us | 1.12 | 0.54 | 16.74 KB | 1.00 |
| 'RestLib - GET all' | GetAll | 271.7 us | 31.49 us | 92.86 us | 246.1 us | 0.97 | 0.46 | 19.05 KB | 1.14 |
| | | | | | | | | | |
| 'RestLib - GET all (no fields)' | GetAll_Fields | 289.6 us | 41.97 us | 121.78 us | 261.4 us | 1.18 | 0.70 | 19.07 KB | 1.00 |
| 'RestLib - GET all (?fields=id,name)' | GetAll_Fields | 475.3 us | 51.86 us | 149.62 us | 448.8 us | 1.93 | 0.99 | 39.98 KB | 2.10 |
| 'RestLib - GET all (?fields=id,name,price)' | GetAll_Fields | 563.9 us | 88.23 us | 255.96 us | 498.5 us | 2.29 | 1.42 | 43.43 KB | 2.28 |
| | | | | | | | | | |
| 'Raw Minimal API - GET by ID' | GetById | 170.5 us | 26.94 us | 79.43 us | 149.6 us | 1.26 | 0.93 | 9.76 KB | 1.00 |
| 'RestLib - GET by ID' | GetById | 217.4 us | 29.76 us | 82.47 us | 203.9 us | 1.61 | 1.07 | 10.13 KB | 1.04 |
| | | | | | | | | | |
| 'RestLib - GET by ID (no fields)' | GetById_Fields | 124.3 us | 14.85 us | 42.84 us | 112.1 us | 1.12 | 0.55 | 10.21 KB | 1.00 |
| 'RestLib - GET by ID (?fields=id,name)' | GetById_Fields | 162.7 us | 16.38 us | 46.47 us | 149.7 us | 1.46 | 0.65 | 12.22 KB | 1.20 |
| | | | | | | | | | |
| 'Raw Minimal API - PUT' | Update | 232.1 us | 24.69 us | 72.81 us | 221.1 us | 1.10 | 0.51 | 12.78 KB | 1.00 |
| 'RestLib - PUT' | Update | 282.0 us | 37.88 us | 108.06 us | 258.5 us | 1.34 | 0.69 | 14.44 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 |
| ADR-007 | Hybrid field projection strategy |
| ADR-008 | Batch operations with partial success |
| ADR-009 | Allow-list sorting with default sort |
| ADR-010 | API versioning via route groups |
| ADR-011 | Query parameter filtering |
| ADR-012 | Hook pipeline for extensibility |
| ADR-013 | Filter operators beyond equality |
| ADR-014 | ETag support for caching and concurrency |
| ADR-015 | Data Annotation validation |
| ADR-016 | JSON resource configuration |
| ADR-017 | Rate limiting integration |
| ADR-018 | PATCH JsonElement coupling acknowledgement |
| ADR-019 | HATEOAS hypermedia links |
| ADR-020 | Structured logging |
Packages
| Package | Description | NuGet |
|---|---|---|
RestLib |
Core library | RestLib |
RestLib.InMemory |
In-memory repository for testing and prototyping | RestLib.InMemory |
Requirements
- .NET 10.0 or later
- ASP.NET Core Minimal APIs
Known Limitations
Forward-only cursor pagination — cursors support forward traversal only; there is no backward/previous-page navigation.
Post-fetch field selection — field projection is applied after the full entity is retrieved from the repository, not pushed down to the data source.
Flat properties only — filtering, sorting, and field selection operate on top-level entity properties; nested or related entity paths are not supported.
No built-in search — full-text or fuzzy search is not included; implement it in your repository if needed.
No CORS configuration — RestLib does not configure CORS. If your API is consumed by browsers, add ASP.NET Core's built-in CORS middleware:
builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => policy .AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader()); }); app.UseCors();See the ASP.NET Core CORS documentation for production-ready policies.
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 | net10.0 is compatible. 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. |
-
net10.0
- RestLib (>= 2.2.1)
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 |
v2.0.0 — HATEOAS hypermedia links, batch pipeline overhaul, and breaking API changes. New: opt-in HAL-style _links on all entity responses with custom link provider extensibility. Breaking: RestLibProblemDetails.Errors changed to IReadOnlyDictionary; IBatchRepository gained required GetByIdsAsync method. See https://github.com/Adrian01987/RestLib/blob/main/CHANGELOG.md for full release notes.