Quick Start

June 10, 2026 · View on GitHub

Build a working DataSurface API in three steps.

Step 1: Define Your Entity

Annotate a C# class with DataSurface attributes to describe what should be exposed via the API:

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, MaxLength = 255)]
    public string Email { get; set; } = default!;

    [CrudField(CrudDto.Read | CrudDto.Create | CrudDto.Update | CrudDto.Filter,
        RequiredOnCreate = true)]
    public string Name { get; set; } = default!;

    [CrudField(CrudDto.Read | CrudDto.Filter | CrudDto.Sort)]
    public DateTime CreatedAt { get; set; }

    [CrudConcurrency]
    public byte[] RowVersion { get; set; } = default!;
}

Key points:

  • [CrudResource("users")] — registers the class as a CRUD resource at /api/users
  • [CrudKey] — marks the primary key
  • [CrudField(...)] — controls which DTOs include this field and its validation rules
  • [CrudConcurrency] — enables optimistic concurrency via ETags

Step 2: Register Services

using DataSurface.EFCore.Services;
using DataSurface.EFCore.Context;
using System.Reflection;

var builder = WebApplication.CreateBuilder(args);

// EF Core
builder.Services.AddDbContext<AppDbContext>(opt =>
    opt.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

// DataSurface contracts and EF Core services.
// The generic overload aliases AppDbContext to the base DbContext the CRUD
// services depend on, and registers the full CRUD runtime (hooks, overrides,
// security dispatcher, bulk and streaming services).
builder.Services.AddDataSurfaceEfCore<AppDbContext>(opt =>
{
    opt.AssembliesToScan = [typeof(Program).Assembly];
});

Your DbContext should extend DeclarativeDbContext to get automatic convention support:

public class AppDbContext : DeclarativeDbContext<AppDbContext>
{
    public AppDbContext(
        DbContextOptions<AppDbContext> options,
        DataSurfaceEfCoreOptions dsOptions,
        IResourceContractProvider contracts)
        : base(options, dsOptions, contracts) { }
}

Step 3: Map Endpoints

using DataSurface.Http;

var app = builder.Build();

app.MapDataSurfaceCrud();

app.Run();

Result

Your API now has these endpoints:

MethodEndpointDescription
GET/api/usersList with filtering, sorting, pagination
HEAD/api/usersCount only (X-Total-Count header)
GET/api/users/{id}Get single user
POST/api/usersCreate a new user
PATCH/api/users/{id}Partial update
DELETE/api/users/{id}Delete user
GET/api/$schema/usersJSON Schema for resource (disable via MapSchemaEndpoint = false)
GET/api/$resourcesList all available resources (opt-in via MapResourceDiscoveryEndpoint = true)

Try It

# Create a user
curl -X POST /api/users \
  -H "Content-Type: application/json" \
  -d '{"email": "alice@example.com", "name": "Alice"}'

# List users with filtering and sorting
curl "/api/users?filter[name]=contains:alice&sort=-createdAt&page=1&pageSize=10"

# Get a single user
curl /api/users/1

# Update a user — rowVersion comes from a prior GET; it is required on update
# because the entity has [CrudConcurrency] (RequiredOnUpdate defaults to true)
curl -X PATCH /api/users/1 \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice Johnson", "rowVersion": "AAAAAAAAB9E="}'

# Delete a user
curl -X DELETE /api/users/1

Adding More Entities

Add as many resources as you need — each one is just a class with attributes:

[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 | CrudDto.Filter | CrudDto.Sort)]
    public DateTime CreatedAt { get; set; }

    [CrudRelation(ReadExpandAllowed = true, WriteMode = RelationWriteMode.ById)]
    public User Author { get; set; } = default!;

    [CrudConcurrency]
    public byte[] RowVersion { get; set; } = default!;
}

All entities discovered by AssembliesToScan are automatically registered — no additional wiring needed.


Adding Dynamic Resources

To support runtime-defined entities alongside static ones, see Dynamic Entities.

Adding OpenAPI / Swagger

To generate typed schemas, see OpenAPI Integration.

Next Steps