Contract-driven CRUD HTTP endpoints for ASP.NET Core
DataSurface eliminates CRUD boilerplate by generating fully-featured HTTP endpoints from a single source of truth: the ResourceContract. Define your resources once using C# attributes or database metadata, and get automatic validation, filtering, sorting, pagination, and more.
You define what a resource is — fields, validation, security, relations — and DataSurface handles:
- CRUD endpoints
- Validation
- Filtering, sorting, pagination
- Authorization & row-level security
- Concurrency, caching, auditing, and observability
All without writing DTOs, controllers, or repetitive glue code.
- Handwritten CRUD controllers
- Read/Create/Update/Delete DTOs
- Manual validation plumbing
- Query parsing logic
- Boilerplate authorization checks
- Repeated Swagger/OpenAPI definitions
- Full control over your domain model
- Strong typing
- Explicit security rules
- Override hooks when you do need custom logic
Most ASP.NET Core applications repeat the same pattern:
- Entity
- DTOs (Read / Create / Update)
- Controller
- Validation
- Query parsing
- Authorization checks
Multiply that by 20–50 entities and the cost becomes significant.
DataSurface collapses all of that into one contract.
You describe what is allowed, not how to wire it.
The result:
- Fewer files
- Less drift between layers
- Consistent behavior across all resources
- Faster iteration without sacrificing control
- Entity
- 3–5 DTOs
- Controller with ~200 lines
- Manual validation
- Manual filtering & paging
- Swagger configuration
- Repeated authorization logic
User.cs
UserReadDto.cs
UserCreateDto.cs
UserUpdateDto.cs
UsersController.cs
UserValidator.cs
- Entity
- Attributes describing the contract
[CrudResource("users")]
public class User
{
[CrudKey]
public int Id { get; set; }
[CrudField(CrudDto.Read | CrudDto.Create | CrudDto.Update, RequiredOnCreate = true)]
public string Email { get; set; } = default!;
}app.MapDataSurfaceCrud();That’s it!
DataSurface can be used in two ways:
- Generates REST endpoints via Minimal APIs
- Full OpenAPI / Swagger support
- Ideal for frontend, mobile, or external integrations
GET /api/users
POST /api/users
PATCH /api/users/{id}
DELETE /api/users/{id}- Call CRUD operations directly
- Same validation, security, hooks, and contracts
- Ideal for internal services, background jobs, or modular monoliths
await crudService.CreateAsync("User", body, context, ct);No controllers. No HTTP. Same guarantees.
✅ You build data-heavy APIs
✅ You want consistent CRUD behavior
✅ You want fewer DTOs and controllers
✅ You need strong validation & security
✅ You support dynamic or metadata-driven entities
❌ You want full handcrafted controllers for every endpoint
❌ Your API is mostly bespoke workflows, not CRUD
❌ You dislike declarative configuration
DataSurface is not a replacement for custom business logic — it handles the 80% so you can focus on the 20%.
- Features
- Packages
- Quick Start
- Guides
- Feature Details
- Auto-generated Endpoints
- Field-level Control
- Default Values
- Computed Fields
- Validation
- Field Projection
- Soft Delete
- Timestamps
- Filtering & Sorting
- Pagination
- Expansion
- HEAD Support
- Authorization
- Row-level Security
- Resource Authorization
- Field Authorization
- Tenant Isolation
- Concurrency
- Hooks
- Overrides
- Dynamic Entities
- Compiled Queries
- Query Caching
- Response Caching
- Bulk Operations
- Import/Export
- Async Streaming
- Webhooks
- Rate Limiting
- API Key Authentication
- Audit Logging
- Structured Logging
- Metrics
- Distributed Tracing
- Health Checks
- Schema Endpoint
- Attributes Reference
- Configuration Options
- Architecture
- Quick Checklist
- Planned Features
| Feature | Description |
|---|---|
| Auto-generated endpoints | GET, POST, PATCH, DELETE, PUT via Minimal APIs |
| Field-level control | Choose which fields appear in read/create/update DTOs |
| Default values | Automatically apply defaults when creating resources |
| Computed fields | Server-calculated read-only fields |
| Validation | Required, immutable, length, range, regex, allowed values |
| Field projection | Select specific fields via ?fields= query parameter |
| Soft delete | Built-in ISoftDelete convention support |
| Timestamps | Auto-populate CreatedAt/UpdatedAt via ITimestamped |
| Filtering & Sorting | Allowlisted fields with operators (eq, gt, contains, etc.) |
| Pagination | Built-in page + pageSize with configurable max |
| Expansion | expand=relation with depth limits |
| HEAD support | HEAD requests return count headers without body |
| Authorization | Per-operation policy names |
| Row-level security | IResourceFilter<T> for tenant/user-based query filtering |
| Resource authorization | IResourceAuthorizer<T> for instance-level access control |
| Field authorization | IFieldAuthorizer for field-level read/write control |
| Tenant isolation | Automatic multi-tenancy with [CrudTenant] attribute |
| Concurrency | Row version + ETag / If-Match headers |
| Hooks | Global and entity-specific lifecycle hooks |
| Overrides | Replace any CRUD operation with custom logic |
| Dynamic entities | Runtime-defined resources without recompilation |
| Compiled queries | Pre-compiled EF Core queries for common operations |
| Query caching | Optional IDistributedCache integration |
| Response caching | ETag-based 304 responses, configurable Cache-Control |
| Bulk operations | Batch create/update/delete via /bulk endpoint |
| Import/Export | Bulk data import/export in JSON or CSV format |
| Async streaming | IAsyncEnumerable support via /stream endpoint |
| Webhooks | Publish events when CRUD operations occur |
| Rate limiting | ASP.NET Core rate limiting integration |
| API key authentication | Machine-to-machine authentication |
| Audit logging | IAuditLogger for tracking all CRUD operations |
| Structured logging | Built-in ILogger integration with operation timing |
| Metrics | OpenTelemetry-compatible counters and histograms |
| Distributed tracing | Activity/span integration for request tracing |
| Health checks | IHealthCheck implementations for monitoring |
| Schema endpoint | GET /api/$schema/{resource} returns JSON Schema |
| Feature flags | Selectively enable/disable features with presets |
Typical combinations:
- Static only:
Core+EFCore+Http - Dynamic only:
Core+Dynamic+Http+Admin - Both: All of the above
using DataSurface.Core.Annotations;
using DataSurface.Core.Enums;
[CrudResource("users")]
public class User
{
[CrudKey]
public int Id { get; set; }
[CrudField(CrudDto.Read | CrudDto.Create | CrudDto.Update, RequiredOnCreate = true)]
public string Email { get; set; } = default!;
[CrudField(CrudDto.Read | CrudDto.Filter | CrudDto.Sort)]
public DateTime CreatedAt { get; set; }
[CrudConcurrency]
public byte[] RowVersion { get; set; } = default!;
}using DataSurface.EFCore.Services;
using System.Reflection;
// Register contracts and EF Core services
builder.Services.AddDataSurfaceEfCore(opt =>
{
opt.AssembliesToScan = [Assembly.GetExecutingAssembly()];
});
// Register CRUD runtime
builder.Services.AddScoped<CrudHookDispatcher>();
builder.Services.AddSingleton<CrudOverrideRegistry>();
builder.Services.AddScoped<EfDataSurfaceCrudService>();
builder.Services.AddScoped<IDataSurfaceCrudService>(sp =>
sp.GetRequiredService<EfDataSurfaceCrudService>());using DataSurface.Http;
app.MapDataSurfaceCrud();Result: Your API now has these endpoints:
GET /api/users— List with filtering, sorting, paginationHEAD /api/users— Get count only (inX-Total-Countheader)GET /api/users/{id}— Get single resourcePOST /api/users— CreatePATCH /api/users/{id}— UpdateDELETE /api/users/{id}— DeleteGET /api/$schema/users— Get JSON Schema for resource
For compile-time defined entities backed by Entity Framework Core.
[CrudResource("posts", MaxPageSize = 100)]
public class Post
{
[CrudKey]
public int Id { get; set; }
[CrudField(CrudDto.Read | CrudDto.Create | CrudDto.Update | CrudDto.Filter | CrudDto.Sort,
RequiredOnCreate = true, MaxLength = 200)]
public string Title { get; set; } = default!;
[CrudField(CrudDto.Read | CrudDto.Create | CrudDto.Update)]
public string? Content { get; set; }
[CrudField(CrudDto.Read | CrudDto.Filter)]
public int AuthorId { get; set; }
[CrudField(CrudDto.Read)]
public DateTime CreatedAt { get; set; }
[CrudRelation(ReadExpandAllowed = true, WriteMode = RelationWriteMode.ById)]
public User Author { get; set; } = default!;
[CrudConcurrency]
public byte[] RowVersion { get; set; } = default!;
}using DataSurface.EFCore.Context;
public class AppDbContext : DeclarativeDbContext<AppDbContext>
{
public AppDbContext(
DbContextOptions<AppDbContext> options,
DataSurfaceEfCoreOptions dsOptions,
IResourceContractProvider contracts)
: base(options, dsOptions, contracts) { }
}// Program.cs
using DataSurface.EFCore.Services;
using DataSurface.Http;
var builder = WebApplication.CreateBuilder(args);
// EF Core
builder.Services.AddDbContext<AppDbContext>(opt =>
opt.UseSqlServer(connectionString));
// DataSurface contracts
builder.Services.AddDataSurfaceEfCore(opt =>
{
opt.AssembliesToScan = [typeof(Program).Assembly];
});
// DataSurface runtime
builder.Services.AddScoped<CrudHookDispatcher>();
builder.Services.AddSingleton<CrudOverrideRegistry>();
builder.Services.AddScoped<EfDataSurfaceCrudService>();
builder.Services.AddScoped<IDataSurfaceCrudService>(sp =>
sp.GetRequiredService<EfDataSurfaceCrudService>());
var app = builder.Build();
// Map CRUD endpoints
app.MapDataSurfaceCrud();
app.Run();For entities defined at runtime without recompilation.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.AddDataSurfaceDynamic(schema: "dbo");
}using DataSurface.Dynamic.DI;
using DataSurface.Dynamic.Contracts;
// Static contracts (if any)
builder.Services.AddDataSurfaceEfCore(opt => { /* ... */ });
// Dynamic contracts
builder.Services.AddDataSurfaceDynamic(opt =>
{
opt.Schema = "dbo";
opt.WarmUpContractsOnStart = true;
});
// Use composite provider for both static + dynamic
builder.Services.AddScoped<IResourceContractProvider>(sp =>
sp.GetRequiredService<CompositeResourceContractProvider>());
// Use router to dispatch to correct backend
builder.Services.AddScoped<DataSurfaceCrudRouter>();
builder.Services.AddScoped<IDataSurfaceCrudService>(sp =>
sp.GetRequiredService<DataSurfaceCrudRouter>());app.MapDataSurfaceCrud(new DataSurfaceHttpOptions
{
MapStaticResources = true,
MapDynamicCatchAll = true // Enables /api/d/{route}
});Manage dynamic entity definitions via REST API.
using DataSurface.Admin.DI;
using DataSurface.Admin;
builder.Services.AddDataSurfaceAdmin();
app.MapDataSurfaceAdmin(new DataSurfaceAdminOptions
{
Prefix = "/admin/ds",
RequireAuthorization = true,
Policy = "DataSurfaceAdmin"
});Available endpoints:
| Method | Path | Description |
|---|---|---|
GET |
/admin/ds/entities |
List all entity definitions |
GET |
/admin/ds/entities/{key} |
Get single entity definition |
PUT |
/admin/ds/entities/{key} |
Create or update entity definition |
DELETE |
/admin/ds/entities/{key} |
Delete entity definition |
GET |
/admin/ds/export |
Export all definitions as JSON |
POST |
/admin/ds/import |
Import definitions from JSON |
POST |
/admin/ds/entities/{key}/reindex |
Rebuild search indexes |
Generate typed schemas for Swashbuckle.
using DataSurface.OpenApi;
builder.Services.AddSwaggerGen(swagger =>
{
builder.Services.AddDataSurfaceOpenApi(swagger);
});This adds:
- Typed request/response schemas per resource
- Query parameter documentation for filtering
- Proper
PagedResult<T>schema for list responses
DataSurface generates fully-featured REST endpoints via Minimal APIs:
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/{resource} |
List with filtering, sorting, pagination |
HEAD |
/api/{resource} |
Get count only (in X-Total-Count header) |
GET |
/api/{resource}/{id} |
Get single resource |
POST |
/api/{resource} |
Create new resource |
PATCH |
/api/{resource}/{id} |
Partial update (only provided fields) |
PUT |
/api/{resource}/{id} |
Full replacement (all fields required) |
DELETE |
/api/{resource}/{id} |
Delete resource |
GET |
/api/$schema/{resource} |
Get JSON Schema for resource |
PUT vs PATCH:
- PATCH — Partial update: only fields in the request body are modified
- PUT — Full replacement: all updatable fields must be provided (returns 400 if any are missing)
To enable PUT endpoints:
app.MapDataSurfaceCrud(new DataSurfaceHttpOptions
{
EnablePutForFullUpdate = true
});See Quick Start for setup instructions.
Control which fields appear in read/create/update DTOs using the [CrudField] attribute with CrudDto flags.
[CrudResource("products")]
public class Product
{
[CrudKey]
public int Id { get; set; }
[CrudField(CrudDto.Read | CrudDto.Create | CrudDto.Update, RequiredOnCreate = true)]
public string Name { get; set; } = default!;
[CrudField(CrudDto.Read | CrudDto.Create)] // Can set on create, but not update
public string SKU { get; set; } = default!;
[CrudField(CrudDto.Read)] // Read-only, never in request bodies
public DateTime CreatedAt { get; set; }
// No attribute = not exposed via API
internal string InternalNotes { get; set; } = default!;
}See [CrudField] in Attributes Reference for full details.
Automatically apply default values when creating resources. Defaults are applied server-side when a field is not provided in the request body:
[CrudResource("orders")]
public class Order
{
[CrudKey]
public int Id { get; set; }
[CrudField(CrudDto.Read | CrudDto.Create, DefaultValue = "pending")]
public string Status { get; set; } = default!;
[CrudField(CrudDto.Read | CrudDto.Create, DefaultValue = 0)]
public int Priority { get; set; }
[CrudField(CrudDto.Read | CrudDto.Create, DefaultValue = false)]
public bool IsUrgent { get; set; }
}Behavior:
- Defaults are only applied on create operations (POST)
- If a field is provided in the request, the provided value is used
- If a field is omitted, the
DefaultValueis applied - Works with strings, numbers, booleans, and other primitive types
Define server-calculated read-only fields that are evaluated at read time. Computed fields are never stored in the database—they're calculated dynamically based on other field values:
[CrudResource("employees")]
public class Employee
{
[CrudKey]
public int Id { get; set; }
[CrudField(CrudDto.Read | CrudDto.Create | CrudDto.Update)]
public string FirstName { get; set; } = default!;
[CrudField(CrudDto.Read | CrudDto.Create | CrudDto.Update)]
public string LastName { get; set; } = default!;
[CrudField(CrudDto.Read, ComputedExpression = "FirstName + ' ' + LastName")]
public string FullName { get; set; } = default!;
[CrudField(CrudDto.Read | CrudDto.Create)]
public decimal Salary { get; set; }
[CrudField(CrudDto.Read | CrudDto.Create)]
public decimal Bonus { get; set; }
[CrudField(CrudDto.Read, ComputedExpression = "Salary + Bonus")]
public decimal TotalCompensation { get; set; }
}Supported Expressions:
- String concatenation:
"FirstName + ' ' + LastName" - Numeric operations:
"Salary + Bonus","Price * Quantity" - Property references: Direct property names like
"PropertyName"
Notes:
- Computed fields are read-only—they cannot be set via POST or PATCH
- Values are calculated fresh on every read operation
- The expression references CLR property names (not API names)
DataSurface provides comprehensive built-in validation via [CrudField] attributes:
[CrudResource("users")]
public class User
{
[CrudKey]
public int Id { get; set; }
// Required on create
[CrudField(CrudDto.Read | CrudDto.Create | CrudDto.Update, RequiredOnCreate = true)]
public string Email { get; set; } = default!;
// Immutable after creation
[CrudField(CrudDto.Read | CrudDto.Create, Immutable = true)]
public string Username { get; set; } = default!;
// String length validation
[CrudField(CrudDto.Read | CrudDto.Create | CrudDto.Update, MinLength = 8, MaxLength = 100)]
public string Password { get; set; } = default!;
// Numeric range validation
[CrudField(CrudDto.Read | CrudDto.Create | CrudDto.Update, Min = 0, Max = 150)]
public int Age { get; set; }
// Regex pattern validation
[CrudField(CrudDto.Read | CrudDto.Create | CrudDto.Update, Regex = @"^\+?[1-9]\d{1,14}$")]
public string? PhoneNumber { get; set; }
// Allowed values (enum-like validation)
[CrudField(CrudDto.Read | CrudDto.Create | CrudDto.Update, AllowedValues = "Active|Inactive|Pending")]
public string Status { get; set; } = default!;
}Validation Rules:
| Rule | Description |
|---|---|
RequiredOnCreate |
Field must be present on POST requests |
Immutable |
Field rejected on PATCH requests (can only be set on create) |
MinLength |
Minimum string length |
MaxLength |
Maximum string length |
Min |
Minimum numeric value |
Max |
Maximum numeric value |
Regex |
Regular expression pattern the value must match |
AllowedValues |
Pipe-separated list of valid values |
Additional Behavior:
- Unknown fields in request bodies are automatically rejected
- Validation errors return HTTP 400 with detailed problem details
Select specific fields to return using the ?fields= query parameter. This reduces payload size and improves performance:
GET /api/users?fields=id,email,nameResponse:
{
"items": [
{ "id": 1, "email": "john@example.com", "name": "John" },
{ "id": 2, "email": "jane@example.com", "name": "Jane" }
]
}Usage:
- Comma-separated list of field API names
- Only requested fields are included in the response
- Invalid field names are ignored
- Works with list (
GET /api/resource) and single (GET /api/resource/{id}) endpoints
Entities implementing ISoftDelete are automatically filtered instead of permanently deleted:
using DataSurface.EFCore.Interfaces;
public class User : ISoftDelete
{
public int Id { get; set; }
public string Email { get; set; } = default!;
// Automatically set to true on DELETE, filtered from queries
public bool IsDeleted { get; set; }
}- On delete:
IsDeletedis set totrueinstead of removing the row - On queries: Soft-deleted records are automatically filtered out
- Control: Disable via
EnableSoftDeleteFilter = falsein options
Entities implementing ITimestamped get automatic timestamp population:
using DataSurface.EFCore.Interfaces;
public class User : ITimestamped
{
public int Id { get; set; }
public string Email { get; set; } = default!;
// Auto-populated by DeclarativeDbContext
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}- On insert: Both
CreatedAtandUpdatedAtare set toDateTime.UtcNow - On update: Only
UpdatedAtis refreshed - Control: Disable via
EnableTimestampConvention = falsein options
?filter[price]=100 # equals (default)
?filter[price]=eq:100 # equals
?filter[price]=neq:100 # not equals
?filter[price]=gt:100 # greater than
?filter[price]=gte:100 # greater than or equal
?filter[price]=lt:100 # less than
?filter[price]=lte:100 # less than or equal
?filter[name]=contains:john # string contains
?filter[name]=starts:john # string starts with
?filter[name]=ends:son # string ends with
?filter[status]=in:a|b|c # in list (pipe-separated)
?filter[email]=isnull:true # is null
?filter[email]=isnull:false # is not null
Search across all searchable fields using the q parameter:
?q=john # searches all fields marked with Searchable = true
Mark fields as searchable:
[CrudField(CrudDto.Read | CrudDto.Filter, Searchable = true)]
public string Title { get; set; }
[CrudField(CrudDto.Read | CrudDto.Filter, Searchable = true)]
public string Description { get; set; }Return only specific fields using the fields parameter:
?fields=id,title,createdAt # only return these fields
?sort=title,-createdAt # Comma-separated, `-` prefix for descending
Fields must have CrudDto.Filter or CrudDto.Sort flags to be filterable/sortable.
| Parameter | Example | Description |
|---|---|---|
page |
?page=2 |
Page number (1-based, default: 1) |
pageSize |
?pageSize=50 |
Items per page (default: 20) |
{
"items": [...],
"page": 1,
"pageSize": 20,
"total": 142
}Response headers (on list endpoints):
X-Total-Count: 142
X-Page: 1
X-Page-Size: 20Maximum page size is configurable via MaxPageSize on [CrudResource].
Include related resources using the expand parameter:
?expand=author,tags
Relations must have ReadExpandAllowed = true in [CrudRelation] to be expandable. Maximum expansion depth is configurable via MaxExpandDepth on [CrudResource].
Use HEAD to get only the count without fetching data:
HEAD /api/users?filter[status]=activeResponse:
HTTP/1.1 200 OK
X-Total-Count: 42
X-Page: 1
X-Page-Size: 200Set authorization policies per operation using [CrudAuthorize]:
[CrudAuthorize(Policy = "AdminOnly")] // All operations
[CrudAuthorize(Operation = CrudOperation.Delete, Policy = "SuperAdmin")]
public class User { }See [CrudAuthorize] in Attributes Reference for full details.
Filter queries based on user context using IResourceFilter<T>:
using DataSurface.EFCore.Interfaces;
public class TenantResourceFilter : IResourceFilter<Order>
{
private readonly ITenantContext _tenant;
public TenantResourceFilter(ITenantContext tenant) => _tenant = tenant;
public Expression<Func<Order, bool>>? GetFilter(ResourceContract contract)
=> o => o.TenantId == _tenant.TenantId;
}
// Register
builder.Services.AddScoped<IResourceFilter<Order>, TenantResourceFilter>();- Automatic application: Filters apply to List, Get, Update, and Delete operations
- Security guarantee: Users can only access records matching the filter
- Non-generic option: Implement
IResourceFilterfor dynamic type filtering
Authorize access to specific resource instances using IResourceAuthorizer<T>:
using DataSurface.EFCore.Interfaces;
public class OrderAuthorizer : IResourceAuthorizer<Order>
{
private readonly IHttpContextAccessor _http;
public OrderAuthorizer(IHttpContextAccessor http) => _http = http;
public Task<AuthorizationResult> AuthorizeAsync(
ResourceContract contract,
Order? entity,
CrudOperation operation,
CancellationToken ct)
{
var userId = _http.HttpContext?.User.FindFirst("sub")?.Value;
// Owner can do anything with their orders
if (entity?.OwnerId == userId)
return Task.FromResult(AuthorizationResult.Success());
// Admins can access all orders
if (_http.HttpContext?.User.IsInRole("Admin") == true)
return Task.FromResult(AuthorizationResult.Success());
return Task.FromResult(AuthorizationResult.Fail("You can only access your own orders."));
}
}
// Register
builder.Services.AddScoped<IResourceAuthorizer<Order>, OrderAuthorizer>();Integration with ASP.NET Core Authorization:
public class PolicyResourceAuthorizer : IResourceAuthorizer
{
private readonly IAuthorizationService _auth;
private readonly IHttpContextAccessor _http;
public PolicyResourceAuthorizer(IAuthorizationService auth, IHttpContextAccessor http)
{
_auth = auth;
_http = http;
}
public async Task<AuthorizationResult> AuthorizeAsync(
ResourceContract contract,
object? entity,
CrudOperation operation,
CancellationToken ct)
{
var user = _http.HttpContext?.User;
if (user is null)
return AuthorizationResult.Fail("No authenticated user.");
// Use ASP.NET Core policy-based authorization with resource
var policyName = $"{contract.ResourceKey}.{operation}";
var result = await _auth.AuthorizeAsync(user, entity, policyName);
return result.Succeeded
? AuthorizationResult.Success()
: AuthorizationResult.Fail("Access denied by policy.");
}
}
// Register
builder.Services.AddScoped<IResourceAuthorizer, PolicyResourceAuthorizer>();- Instance-level checks: "Can this user access Order #123?"
- Operation-specific: Different rules for Get vs Update vs Delete
- Typed and non-generic: Use
IResourceAuthorizer<T>for compile-time safety orIResourceAuthorizerfor global policies - Integrates with ASP.NET Core: Leverage existing
IAuthorizationServiceand policies
Control which fields users can read or write using IFieldAuthorizer:
using DataSurface.EFCore.Interfaces;
public class SensitiveFieldAuthorizer : IFieldAuthorizer
{
private readonly IHttpContextAccessor _http;
public SensitiveFieldAuthorizer(IHttpContextAccessor http) => _http = http;
public bool CanReadField(ResourceContract contract, string fieldName)
{
if (fieldName == "salary")
return _http.HttpContext?.User.IsInRole("HR") ?? false;
return true;
}
public bool CanWriteField(ResourceContract contract, string fieldName, CrudOperation op)
{
if (fieldName == "isAdmin")
return _http.HttpContext?.User.IsInRole("Admin") ?? false;
return true;
}
}
// Register
builder.Services.AddScoped<IFieldAuthorizer, SensitiveFieldAuthorizer>();- Read redaction: Unauthorized fields are removed from responses
- Write validation: Unauthorized field writes throw
UnauthorizedAccessException
Implement automatic multi-tenancy with the [CrudTenant] attribute. Tenant isolation ensures users can only access data belonging to their tenant:
[CrudResource("orders")]
public class Order
{
[CrudKey]
public int Id { get; set; }
[CrudTenant(ClaimType = "tenant_id", Required = true)]
public string TenantId { get; set; } = default!;
[CrudField(CrudDto.Read | CrudDto.Create | CrudDto.Update)]
public string ProductName { get; set; } = default!;
[CrudField(CrudDto.Read | CrudDto.Create)]
public decimal Amount { get; set; }
}Behavior:
- On queries: Automatically filters results to only include records matching the user's tenant claim
- On create: Automatically sets the tenant field to the user's tenant claim value
- On update/delete: Validates the resource belongs to the user's tenant
Configuration Options:
| Option | Description |
|---|---|
ClaimType |
The claim type to extract tenant ID from (e.g., "tenant_id", "org_id") |
Required |
If true, requests without the tenant claim are rejected with 401 |
Custom Tenant Resolution:
For advanced scenarios, implement ITenantResolver:
public class CustomTenantResolver : ITenantResolver
{
private readonly IHttpContextAccessor _http;
public CustomTenantResolver(IHttpContextAccessor http) => _http = http;
public string? ResolveTenantId(TenantContract tenant)
{
// Custom logic: header, subdomain, database lookup, etc.
return _http.HttpContext?.Request.Headers["X-Tenant-Id"].FirstOrDefault();
}
}
// Register
builder.Services.AddScoped<ITenantResolver, CustomTenantResolver>();Row version fields enable optimistic concurrency via ETag headers.
Response:
HTTP/1.1 200 OK
ETag: W/"AAAAAAB="Update with concurrency check:
PATCH /api/users/1
If-Match: W/"AAAAAAB="
Content-Type: application/json
{"email": "new@example.com"}See [CrudConcurrency] in Attributes Reference for configuration.
Run for all resources.
public class AuditHook : ICrudHook
{
public int Order => 0; // Lower runs first
public Task BeforeAsync(CrudHookContext ctx)
{
Console.WriteLine($"Before {ctx.Operation} on {ctx.Contract.ResourceKey}");
return Task.CompletedTask;
}
public Task AfterAsync(CrudHookContext ctx)
{
Console.WriteLine($"After {ctx.Operation}");
return Task.CompletedTask;
}
}
// Register
builder.Services.AddScoped<ICrudHook, AuditHook>();Run only for a specific entity type.
public class UserHook : ICrudHook<User>
{
public int Order => 0;
public Task BeforeCreateAsync(User entity, JsonObject body, CrudHookContext ctx)
{
entity.CreatedAt = DateTime.UtcNow;
return Task.CompletedTask;
}
public Task AfterCreateAsync(User entity, CrudHookContext ctx)
{
// Send welcome email
return Task.CompletedTask;
}
}
// Register
builder.Services.AddScoped<ICrudHook<User>, UserHook>();Completely replace CRUD logic for a resource.
var registry = app.Services.GetRequiredService<CrudOverrideRegistry>();
registry.Override("User", CrudOperation.Create,
async (CreateOverride)((contract, body, ctx, ct) =>
{
// Custom creation logic
var user = new User { Email = body["email"]!.GetValue<string>() };
ctx.Db.Add(user);
await ctx.Db.SaveChangesAsync(ct);
return new JsonObject { ["id"] = user.Id, ["email"] = user.Email };
}));Runtime-defined resources without recompilation. See Guide: Dynamic Resources for full setup instructions.
Pre-compiled EF Core queries for common operations:
builder.Services.AddSingleton<CompiledQueryCache>();
// Usage in custom code
var cache = sp.GetRequiredService<CompiledQueryCache>();
var findById = cache.GetOrCreateFindByIdQuery<User, int>("Id");
var user = findById(dbContext, 5);Cache query results using IDistributedCache:
// Add Redis cache
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
});
// Configure DataSurface caching
builder.Services.Configure<DataSurfaceCacheOptions>(options =>
{
options.EnableQueryCaching = true;
options.DefaultCacheDuration = TimeSpan.FromMinutes(5);
options.ResourceConfigs["Product"] = new ResourceCacheConfig
{
Duration = TimeSpan.FromMinutes(30),
CacheList = true,
CacheGet = true
};
});
builder.Services.AddSingleton<IQueryResultCache, DistributedQueryResultCache>();Enable ETag-based conditional GET (304 Not Modified) and Cache-Control headers:
app.MapDataSurfaceCrud(new DataSurfaceHttpOptions
{
EnableConditionalGet = true, // If-None-Match → 304 response
CacheControlMaxAgeSeconds = 300 // Cache-Control: max-age=300
});Clients can cache responses and send If-None-Match headers to receive 304 responses when data hasn't changed.
Batch create, update, and delete operations via POST /api/{resource}/bulk:
{
"create": [
{ "name": "User 1", "email": "user1@example.com" },
{ "name": "User 2", "email": "user2@example.com" }
],
"update": [
{ "id": 5, "patch": { "name": "Updated Name" } }
],
"delete": [10, 11, 12],
"stopOnError": true,
"useTransaction": true
}Register the bulk service:
builder.Services.AddScoped<IDataSurfaceBulkService, EfDataSurfaceBulkService>();Stream large datasets via GET /api/{resource}/stream (NDJSON format):
// Register streaming service
builder.Services.AddScoped<IDataSurfaceStreamingService, EfDataSurfaceStreamingService>();
// Client usage
await foreach (var item in streamingService.StreamAsync("User", spec))
{
// Process each item as it arrives
}Response format (newline-delimited JSON):
{"id":1,"name":"User 1"}
{"id":2,"name":"User 2"}
{"id":3,"name":"User 3"}
Bulk data import and export via dedicated endpoints:
app.MapDataSurfaceCrud(new DataSurfaceHttpOptions
{
EnableImportExport = true
});Export Endpoint:
GET /api/users/export?format=json
GET /api/users/export?format=csv- Exports all records (respecting query filters and security)
- Supports JSON and CSV formats
- CSV format includes headers matching API field names
Import Endpoint:
POST /api/users/import
Content-Type: application/json
[
{ "email": "user1@example.com", "name": "User 1" },
{ "email": "user2@example.com", "name": "User 2" }
]- Imports an array of records
- Each record is validated against the resource contract
- Returns summary of imported, failed, and skipped records
Publish events when CRUD operations occur. Useful for integrations, audit trails, and event-driven architectures:
app.MapDataSurfaceCrud(new DataSurfaceHttpOptions
{
EnableWebhooks = true
});Implement and register a webhook publisher:
using DataSurface.Core.Webhooks;
public class MyWebhookPublisher : IWebhookPublisher
{
private readonly HttpClient _http;
private readonly ILogger<MyWebhookPublisher> _logger;
public MyWebhookPublisher(HttpClient http, ILogger<MyWebhookPublisher> logger)
{
_http = http;
_logger = logger;
}
public async Task PublishAsync(WebhookEvent evt, CancellationToken ct)
{
_logger.LogInformation("Webhook: {Operation} on {Resource} id={Id}",
evt.Operation, evt.ResourceKey, evt.EntityId);
// Send to external endpoint
await _http.PostAsJsonAsync("https://hooks.example.com/datasurface", evt, ct);
}
}
// Register
builder.Services.AddSingleton<IWebhookPublisher, MyWebhookPublisher>();WebhookEvent properties:
Operation— Create, Update, or DeleteResourceKey— The resource that changedEntityId— ID of the affected entityTimestamp— UTC timestampPayload— JSON representation of the entity (for create/update)
Failure Handling:
- Webhook publishing is fire-and-forget by default
- Failures are logged but don't fail the CRUD operation
- Implement retry logic in your
IWebhookPublisherif needed
Integrate with ASP.NET Core rate limiting to protect your API:
// Configure rate limiting
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("DataSurfacePolicy", opt =>
{
opt.PermitLimit = 100;
opt.Window = TimeSpan.FromMinutes(1);
opt.QueueLimit = 10;
});
});
// Enable rate limiting on DataSurface endpoints
app.MapDataSurfaceCrud(new DataSurfaceHttpOptions
{
EnableRateLimiting = true,
RateLimitingPolicy = "DataSurfacePolicy"
});
// Don't forget to use the rate limiter middleware
app.UseRateLimiter();Per-Resource Policies:
Configure different policies per resource using [CrudAuthorize]:
[CrudResource("high-traffic")]
[CrudAuthorize(RateLimitingPolicy = "HighTrafficPolicy")]
public class HighTrafficResource { /* ... */ }Enable API key authentication for machine-to-machine access:
app.MapDataSurfaceCrud(new DataSurfaceHttpOptions
{
EnableApiKeyAuth = true,
ApiKeyHeaderName = "X-Api-Key" // Default header name
});Request:
GET /api/users
X-Api-Key: your-api-key-hereCustom Validation:
Implement IApiKeyValidator for custom validation logic:
using DataSurface.Http;
public class DatabaseApiKeyValidator : IApiKeyValidator
{
private readonly AppDbContext _db;
public DatabaseApiKeyValidator(AppDbContext db) => _db = db;
public async Task<bool> ValidateAsync(string apiKey, CancellationToken ct)
{
return await _db.ApiKeys
.AnyAsync(k => k.Key == apiKey && k.IsActive && k.ExpiresAt > DateTime.UtcNow, ct);
}
}
// Register
builder.Services.AddScoped<IApiKeyValidator, DatabaseApiKeyValidator>();Default Behavior:
- Without
IApiKeyValidator, any non-empty API key is accepted - With
IApiKeyValidator, the validator determines validity - Missing or invalid API keys return HTTP 401 Unauthorized
Track all CRUD operations using IAuditLogger:
using DataSurface.EFCore.Interfaces;
public class DatabaseAuditLogger : IAuditLogger
{
private readonly AppDbContext _db;
private readonly IHttpContextAccessor _http;
public DatabaseAuditLogger(AppDbContext db, IHttpContextAccessor http)
{
_db = db;
_http = http;
}
public async Task LogAsync(AuditLogEntry entry, CancellationToken ct)
{
_db.AuditLogs.Add(new AuditLog
{
UserId = _http.HttpContext?.User.FindFirst("sub")?.Value,
Operation = entry.Operation.ToString(),
ResourceKey = entry.ResourceKey,
EntityId = entry.EntityId,
Timestamp = entry.Timestamp,
Success = entry.Success,
Changes = entry.Changes?.ToJsonString(),
PreviousValues = entry.PreviousValues?.ToJsonString()
});
await _db.SaveChangesAsync(ct);
}
}
// Register
builder.Services.AddScoped<IAuditLogger, DatabaseAuditLogger>();AuditLogEntry properties:
Operation— The CRUD operation performedResourceKey— The resource being accessedEntityId— The entity ID (if applicable)Timestamp— UTC timestampSuccess— Whether the operation succeededChanges— JSON of fields written (create/update)PreviousValues— JSON of previous values (update)
Both EfDataSurfaceCrudService and DynamicDataSurfaceCrudService emit structured logs:
[DBG] List User page=1 pageSize=20
[DBG] List User completed in 45ms, returned 20/142 items
[INF] Created User in 12ms
[INF] Updated User id=5 in 8ms
[INF] Deleted User id=5 in 3ms
Log levels:
Debug— Operation start and read completionsInformation— Mutating operations (create, update, delete)
Structured properties:
{Resource}— Resource key{Id}— Entity ID (when applicable){ElapsedMs}— Operation duration{Count}/{Total}— List result counts
OpenTelemetry-compatible metrics via DataSurfaceMetrics:
// Register metrics
builder.Services.AddSingleton<DataSurfaceMetrics>();
// Configure OpenTelemetry
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics => metrics.AddMeter("DataSurface"));Available metrics:
| Metric | Type | Description |
|---|---|---|
datasurface.operations |
Counter | Total CRUD operations by resource and operation |
datasurface.errors |
Counter | Failed operations by resource, operation, and error type |
datasurface.operation.duration |
Histogram | Operation duration in milliseconds |
datasurface.rows_affected |
Counter | Rows affected by operations |
Activity/span integration via DataSurfaceTracing:
// Configure OpenTelemetry
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing.AddSource("DataSurface"));Trace attributes:
datasurface.resource— Resource keydatasurface.operation— CRUD operationdatasurface.entity_id— Entity ID (when applicable)datasurface.rows_affected— Rows returned/affecteddatasurface.query.*— Query parameters (page, page_size, filter_count, sort_count)
Built-in IHealthCheck implementations:
builder.Services.AddHealthChecks()
.AddCheck<DataSurfaceDbHealthCheck>("datasurface-db")
.AddCheck<DataSurfaceContractsHealthCheck>("datasurface-contracts")
.AddCheck<DynamicMetadataHealthCheck>("datasurface-dynamic-metadata")
.AddCheck<DynamicContractsHealthCheck>("datasurface-dynamic-contracts");Health checks:
DataSurfaceDbHealthCheck— Database connectivityDataSurfaceContractsHealthCheck— Static contracts loadedDynamicMetadataHealthCheck— Dynamic entity definitions table accessibleDynamicContractsHealthCheck— Dynamic contracts loaded
Get JSON Schema for any resource:
GET /api/$schema/usersResponse:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:datasurface:User",
"title": "User",
"type": "object",
"properties": {
"id": { "type": "integer", "format": "int32" },
"email": { "type": "string", "maxLength": 255 },
"createdAt": { "type": "string", "format": "date-time" }
},
"required": ["email"],
"x-operations": {
"list": { "enabled": true },
"get": { "enabled": true },
"create": { "enabled": true, "requiredOnCreate": ["email"] },
"update": { "enabled": true },
"delete": { "enabled": true }
},
"x-query": {
"maxPageSize": 200,
"filterableFields": ["email", "createdAt"],
"sortableFields": ["email", "createdAt"]
}
}Useful for:
- Client-side form generation
- API documentation
- Contract validation
Marks a class as a CRUD resource.
[CrudResource("users",
ResourceKey = "User", // Default: class name
MaxPageSize = 200, // Default: 200
MaxExpandDepth = 2, // Default: 1
EnableList = true, // Default: true
EnableGet = true, // Default: true
EnableCreate = true, // Default: true
EnableUpdate = true, // Default: true
EnableDelete = true)] // Default: true
public class User { }Marks the primary key property.
[CrudKey(ApiName = "id")] // Optional: customize API name
public int Id { get; set; }Controls field visibility and behavior.
[CrudField(
CrudDto.Read | CrudDto.Create | CrudDto.Update | CrudDto.Filter | CrudDto.Sort,
ApiName = "email", // Optional: customize API name
RequiredOnCreate = true, // Validation: required on POST
Immutable = false, // If true: rejected on PATCH
Hidden = false, // If true: never exposed
MinLength = 1, // String validation
MaxLength = 255, // String validation
Min = 0, // Numeric validation
Max = 100, // Numeric validation
Regex = @"^[\w@.]+$")] // Pattern validation
public string Email { get; set; }CrudDto flags:
| Flag | Effect |
|---|---|
Read |
Included in GET responses |
Create |
Accepted in POST body |
Update |
Accepted in PATCH body |
Filter |
Can be used in filter[field]=value |
Sort |
Can be used in sort=field |
Configures navigation property behavior.
[CrudRelation(
ReadExpandAllowed = true, // Can use expand=author
DefaultExpanded = false, // Auto-expand without asking
WriteMode = RelationWriteMode.ById, // How to write
WriteFieldName = "authorId", // Field name for writes
RequiredOnCreate = false)]
public User Author { get; set; }RelationWriteMode options:
None— Cannot write relationById— Write via FK field (e.g.,authorId)ByIdList— Write via ID array (e.g.,tagIds)NestedDisabled— Nested objects rejected
Marks a row version field for optimistic concurrency.
[CrudConcurrency(RequiredOnUpdate = true)]
public byte[] RowVersion { get; set; }Sets authorization policies per operation.
[CrudAuthorize(Policy = "AdminOnly")] // All operations
[CrudAuthorize(Operation = CrudOperation.Delete, Policy = "SuperAdmin")]
public class User { }[CrudHidden]
Completely hides a property from the contract.
[CrudHidden]
public string InternalSecret { get; set; }Excludes a property from contract generation (use for EF navigation properties you don't want exposed).
builder.Services.AddDataSurfaceEfCore(opt =>
{
opt.AssembliesToScan = [typeof(Program).Assembly];
// All conventions are opt-in (disabled by default)
opt.AutoRegisterCrudEntities = true; // Auto-register in DbContext
opt.EnableSoftDeleteFilter = true; // Apply IsDeleted filter
opt.EnableRowVersionConvention = true; // Configure RowVersion columns
opt.EnableTimestampConvention = true; // Auto-populate CreatedAt/UpdatedAt
opt.UseCamelCaseApiNames = true; // camelCase API names (default: true)
// Use a feature preset or customize individual features
opt.Features = DataSurfaceFeatures.Standard; // Default is Minimal
opt.ContractBuilderOptions.ExposeFieldsOnlyWhenAnnotated = true;
});app.MapDataSurfaceCrud(new DataSurfaceHttpOptions
{
ApiPrefix = "/api", // Route prefix (default)
MapStaticResources = true, // Map static entity routes (default: true)
// All advanced features are opt-in (disabled by default)
MapDynamicCatchAll = true, // Map /api/d/{route} (default: false)
DynamicPrefix = "/d", // Dynamic route prefix
MapResourceDiscoveryEndpoint = true, // GET /api/$resources (default: false)
EnableEtags = true, // ETag response headers (default: false)
EnableBulkOperations = true, // Bulk endpoints (default: false)
EnableStreaming = true, // Streaming endpoints (default: false)
EnableConditionalGet = true, // If-None-Match/304 (default: false)
EnablePutForFullUpdate = true, // PUT for full replacement (default: false)
EnableImportExport = true, // Import/export endpoints (default: false)
// Security & infrastructure (opt-in)
RequireAuthorizationByDefault = false, // Require auth on all endpoints
DefaultPolicy = null, // Default auth policy
EnableRateLimiting = false, // Enable rate limiting
RateLimitingPolicy = null, // Rate limiting policy name
EnableApiKeyAuth = false, // Enable API key authentication
ApiKeyHeaderName = "X-Api-Key", // API key header name
EnableWebhooks = false, // Enable webhook publishing
ThrowOnRouteCollision = false // Fail on duplicate routes
});builder.Services.AddDataSurfaceDynamic(opt =>
{
opt.Schema = "dbo"; // DB schema for dynamic tables
opt.WarmUpContractsOnStart = true; // Load contracts at startup
});app.MapDataSurfaceAdmin(new DataSurfaceAdminOptions
{
Prefix = "/admin/ds", // Route prefix
RequireAuthorization = true, // Require auth (default: true for security)
Policy = null // Auth policy (default: uses default policy)
});DataSurface follows an opt-in philosophy — advanced features are disabled by default for security and simplicity. Use DataSurfaceFeatures presets or configure individual features:
builder.Services.AddDataSurfaceEfCore(opt =>
{
// Use a preset (Minimal is the default)
opt.Features = DataSurfaceFeatures.Minimal; // Core CRUD only (default)
opt.Features = DataSurfaceFeatures.Standard; // Core + security + observability
opt.Features = DataSurfaceFeatures.Full; // All features including webhooks
// Or customize individual features (starting from Minimal defaults)
opt.Features = new DataSurfaceFeatures
{
// Core (enabled by default)
EnableFieldValidation = true,
EnableDefaultValues = true,
// Advanced features (opt-in)
EnableComputedFields = true,
EnableFieldProjection = true,
EnableTenantIsolation = true,
EnableRowLevelSecurity = true,
EnableResourceAuthorization = true,
EnableFieldAuthorization = true,
EnableAuditLogging = true,
EnableMetrics = true,
EnableTracing = true,
EnableQueryCaching = true,
EnableHooks = true,
EnableOverrides = true,
EnableWebhooks = true
};
});Available Feature Flags:
| Category | Feature | Default | Description |
|---|---|---|---|
| Core CRUD | EnableFieldValidation |
✅ | MinLength, MaxLength, Min, Max, Regex, AllowedValues |
EnableDefaultValues |
✅ | Apply default values on create | |
EnableComputedFields |
❌ | Evaluate computed expressions at read time | |
EnableFieldProjection |
❌ | Support ?fields= query parameter |
|
| Security | EnableTenantIsolation |
❌ | [CrudTenant] attribute support |
EnableRowLevelSecurity |
❌ | IResourceFilter<T> support |
|
EnableResourceAuthorization |
❌ | IResourceAuthorizer<T> support |
|
EnableFieldAuthorization |
❌ | IFieldAuthorizer support |
|
| Observability | EnableAuditLogging |
❌ | IAuditLogger integration |
EnableMetrics |
❌ | OpenTelemetry metrics | |
EnableTracing |
❌ | Distributed tracing | |
| Caching | EnableQueryCaching |
❌ | IQueryResultCache integration |
| Extensibility | EnableHooks |
❌ | Lifecycle hooks |
EnableOverrides |
❌ | CRUD operation overrides | |
| Integration | EnableWebhooks |
❌ | Webhook publishing |
Presets:
| Preset | Description | Use Case |
|---|---|---|
Minimal |
Core CRUD + validation only | Simple APIs, microservices, maximum performance (default) |
Standard |
Core + security + observability | Most production applications |
Full |
All features enabled | Feature-rich applications with webhooks |
┌─────────────────────────────────────────────────────────────────┐
│ HTTP Layer │
│ DataSurface.Http: Minimal API mapping, query parsing, ETags │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ IDataSurfaceCrudService │
│ ListAsync, GetAsync, CreateAsync, UpdateAsync, DeleteAsync │
└─────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ EfDataSurfaceCrudService │ │ DynamicDataSurfaceCrudService │
│ DataSurface.EFCore │ │ DataSurface.Dynamic │
│ (Static EF entities) │ │ (JSON records) │
└──────────────────────────────┘ └──────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ ResourceContract │
│ DataSurface.Core: Single source of truth │
│ - Fields, Relations, Operations, Query limits, Security │
└─────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ ContractBuilder │ │ DynamicContractBuilder │
│ (C# attributes → Contract) │ │ (DB metadata → Contract) │
└──────────────────────────────┘ └──────────────────────────────┘
| Interface | Purpose |
|---|---|
IDataSurfaceCrudService |
Executes CRUD operations |
IResourceContractProvider |
Provides contracts by resource key |
ICrudHook |
Global lifecycle hooks |
ICrudHook<T> |
Entity-specific lifecycle hooks |
ITimestamped |
Auto-timestamp convention interface |
ISoftDelete |
Soft-delete convention interface |
IResourceFilter<T> |
Row-level security filtering |
IResourceAuthorizer<T> |
Resource instance authorization |
IFieldAuthorizer |
Field-level read/write authorization |
IAuditLogger |
CRUD operation audit logging |
- Add package references (
DataSurface.Core,DataSurface.EFCore,DataSurface.Http) - Annotate entities with
[CrudResource],[CrudKey],[CrudField] - Call
AddDataSurfaceEfCore()with assemblies to scan - Register
CrudHookDispatcher,CrudOverrideRegistry,EfDataSurfaceCrudService - Register
IDataSurfaceCrudService - Call
app.MapDataSurfaceCrud()
- Add
DataSurface.OpenApifor Swagger schemas - Add
DataSurface.Adminfor runtime entity management - Configure
DataSurfaceFeaturesfor selective feature enablement - Register
IWebhookPublisherfor webhook integration - Register
IAuditLoggerfor audit logging - Register
IApiKeyValidatorfor custom API key validation - Register
ITenantResolverfor custom tenant resolution - Register
IResourceFilter<T>for row-level security - Register
IResourceAuthorizer<T>for resource authorization - Register
IFieldAuthorizerfor field-level authorization
The following features are planned for future releases. Contributions are welcome!
| Feature | Description | Status |
|---|---|---|
| GraphQL Endpoint | /api/graphql with auto-generated schema from contracts |
Planned |
| Change Data Capture | Track historical changes with entity versioning and temporal queries | Planned |
| Fluent Configuration | builder.Resource<T>().Field(x => x.Name).Validation(...) syntax |
Planned |
| Feature | Description | Status |
|---|---|---|
| Cross-backend Expansion | Expand dynamic entities referencing EF entities and vice versa | Planned |
| Async Job Queue | Background processing for long-running operations with status tracking | Planned |
| gRPC Support | gRPC endpoints alongside REST for high-performance scenarios | Planned |
| Real-time Updates | SignalR/WebSocket integration for live data subscriptions | Planned |
| Batch Validation | Validate multiple entities in a single request before commit | Planned |
| Feature | Description | Status |
|---|---|---|
| OData Support | OData query syntax compatibility ($filter, $select, $expand) |
Considering |
| JSON Patch | RFC 6902 JSON Patch support for partial updates | Considering |
| Conditional Creates | If-None-Match: * header support for idempotent creates |
Considering |
| Field Masking | Automatic PII/sensitive data masking in responses | Considering |
| Query Cost Analysis | Estimate and limit query complexity before execution | Considering |
| Multi-database Support | Route different resources to different databases | Considering |
| Optimistic Offline Sync | Conflict resolution for mobile/offline scenarios | Considering |
| Schema Migrations | Track and apply contract changes across environments | Considering |
Have a feature request? Open an issue on GitHub with the enhancement label!