Register your EF Core entities once and get full GET / POST / PUT / PATCH / DELETE endpoints, filtering, sorting, pagination, authorization, and Swagger docs — with zero controllers and zero repositories.
What you get
One registration call replaces an entire data-access layer.
Full GET (list + by id), POST, PUT, PATCH, and DELETE routes are generated automatically for every registered entity. No controller code needed.
10 filter operators — eq, neq, like, lt, gt,
in, and more. Multi-field sorting with sort[field]=asc|desc.
Compound AND / OR groups supported.
Simple ?page=&pagesize= for flat arrays, or /pagedresult for
a rich envelope with total, pageCount, and items.
Integrates with ASP.NET Core's standard policy system. Require auth or specific policies
globally or per HTTP method — .WithDeletePolicy("IsAdmin").
Attach sync or async logic before or after every save operation. Use lambdas for quick
inline code, or implement IOtterApiBeforeSaveHandler<T> for DI-friendly classes.
OtterApiSwaggerDocumentFilter auto-generates full Swagger schemas for every
route, including query parameters, response shapes, and enum descriptions.
Static or per-request predicates that silently restrict which rows a client can ever see. Perfect for soft-deletes, multi-tenant isolation, and access control.
Define pre-filtered, pre-sorted GET endpoints like /products/featured or
/orders/latest — no extra controllers, just one fluent call.
Eager-load related entities on any GET request with ?include=Category.
Equivalent to EF Core's Include(), zero extra code.
Two lines of setup in Program.cs and your entire entity is exposed
as a REST API — complete with filtering, sorting, and pagination.
After startup, the following routes are live for your entity:
// 1. Register services
builder.Services.AddOtterApi<AppDbContext>(options =>
{
options.Path = "/api";
options.Entity<Product>("products")
.Authorize()
.WithDeletePolicy("IsAdmin")
.ExposePagedResult();
});
// 2. Register middleware
app.UseOtterApi();
GET /api/products?filter[isActive]=true&sort[price]=asc&page=1&pagesize=20
// Response 200 OK
[
{ "id": 3, "name": "Laptop", "price": 999.99 },
{ "id": 2, "name": "Mouse", "price": 29.99 }
]
Restrict which rows a client can ever access at the query level — not in application code. Use static predicates compiled once at startup, or scoped predicates that read from the JWT token on every request.
Hidden rows behave as if they don't exist: GET /products/10 returns
404, not 403. The same filter applies to list, count,
pagedresult, and write operations.
options.Entity<Order>("orders")
.WithQueryFilter(o => o.IsActive) // static
.WithScopedQueryFilter(sp =>
{
var http = sp.GetRequiredService<IHttpContextAccessor>();
var uid = http.HttpContext?.User.FindFirst("sub")?.Value;
return o => o.UserId == uid; // per-request
});
Execute arbitrary logic around every write operation. Set timestamps, validate business rules, publish events, or send notifications — without touching a controller.
Hooks can be synchronous or async, inline lambdas or full DI-enabled handler classes. Multiple hooks chain in registration order, and each hook sees the state left by the previous one.
Hooks Documentation →options.Entity<Product>("products")
.BeforeSave(async (ctx, product, _, op) =>
{
if (op == OtterApiCrudOperation.Post)
product.CreatedAt = DateTime.UtcNow;
var exists = await ctx.Set<Product>()
.AnyAsync(p => p.Name == product.Name);
if (exists)
throw new OtterApiException("DUPLICATE", "Name taken.", 409);
});
Register pre-configured GET endpoints with a single fluent call. Define a filter predicate, a sort expression, a row limit, and a URL slug — and OtterApi handles the rest.
Custom routes stack with client-supplied query parameters, respect entity-level query filters,
and fully support ?include= and ?filter[...] stacking.
options.Entity<Product>("products")
.WithCustomRoute("featured",
filter: p => p.Stock > 0,
sort: "Price desc",
take: 5)
.WithCustomRoute("latest",
sort: "CreatedAt desc",
take: 10);
// These are now live:
// GET /api/products/featured
// GET /api/products/latest