RestLib.InMemory 0.5.0

There is a newer version of this package available.
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
                    
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="RestLib.InMemory" Version="0.5.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RestLib.InMemory" Version="0.5.0" />
                    
Directory.Packages.props
<PackageReference Include="RestLib.InMemory" />
                    
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 RestLib.InMemory --version 0.5.0
                    
#r "nuget: RestLib.InMemory, 0.5.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 RestLib.InMemory@0.5.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=RestLib.InMemory&version=0.5.0
                    
Install as a Cake Addin
#tool nuget:?package=RestLib.InMemory&version=0.5.0
                    
Install as a Cake Tool

RestLib

3 lines to a production-ready REST API

Build Coverage NuGet License

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 pagination
  • GET /api/products/{id} - fetch a single resource
  • POST /api/products - create
  • PUT /api/products/{id} - replace
  • PATCH /api/products/{id} - partially update
  • DELETE /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_case JSON 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 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.

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
Loading failed