Contract System

June 10, 2026 · View on GitHub

The ResourceContract is the single source of truth for every CRUD resource in DataSurface. All runtime features — endpoints, validation, filtering, sorting, expansion, authorization, hooks, and overrides — consume this contract.


How Contracts Are Produced

From C# Attributes (Static Resources)

The ContractBuilder scans assemblies for classes annotated with [CrudResource] and builds a ResourceContract for each one:

[CrudResource("users", MaxPageSize = 100)]
public class User
{
    [CrudKey]
    public int Id { get; set; }

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

For the full list of attributes and how they map to contract properties, see Attributes Reference.

From Database Metadata (Dynamic Resources)

The DynamicContractBuilder reads EntityDef and PropertyDef rows from the database and produces the same ResourceContract structure. See Dynamic Entities.


Contract Schema

ResourceContract

The root object describing a CRUD resource.

public sealed record ResourceContract(
    string ResourceKey,                                    // Stable identifier (e.g., "User")
    string Route,                                          // URL segment (e.g., "users")
    StorageBackend Backend,                                // Storage backend type
    ResourceKeyContract Key,                               // Primary key definition
    QueryContract Query,                                   // Filtering, sorting, pagination limits
    ReadContract Read,                                     // Expansion rules
    IReadOnlyList<FieldContract> Fields,                   // All scalar fields
    IReadOnlyList<RelationContract> Relations,             // All navigation properties
    IReadOnlyDictionary<CrudOperation, OperationContract> Operations,  // Per-operation config
    SecurityContract Security,                             // Authorization policies
    TenantContract? Tenant = null                          // Optional tenant isolation config
);

ResourceKeyContract

public sealed record ResourceKeyContract(
    string Name,      // CLR property name (e.g., "Id")
    FieldType Type    // Key type: Int32, Int64, Guid, or String
);

QueryContract

Defines what queries are allowed against this resource.

public sealed record QueryContract(
    int MaxPageSize,                           // Maximum items per page (default: 200)
    IReadOnlyList<string> FilterableFields,   // Fields allowed in filter[field]=value
    IReadOnlyList<string> SortableFields,     // Fields allowed in sort=field
    IReadOnlyList<string> SearchableFields,   // Fields included in full-text search
    string? DefaultSort                        // Optional default sort (e.g., "-createdAt")
);

ReadContract

Controls expansion and projection at read time.

public sealed record ReadContract(
    IReadOnlyList<string> ExpandAllowed,   // Relations that may be expanded
    int MaxExpandDepth,                     // Maximum expansion depth (default: 1)
    IReadOnlyList<string> DefaultExpand    // Relations expanded by default
);

OperationContract

Per-operation configuration including input/output shapes.

public sealed record OperationContract(
    bool Enabled,                              // Whether this operation is available
    IReadOnlyList<string> InputShape,          // Fields accepted in request body (API names)
    IReadOnlyList<string> OutputShape,         // Fields returned in response (API names)
    IReadOnlyList<string> RequiredOnCreate,    // Fields required on POST
    IReadOnlyList<string> ImmutableFields,     // Fields that cannot be changed on PATCH
    ConcurrencyContract? Concurrency           // Optional concurrency settings
);

FieldContract

Describes a scalar field exposed by a resource.

public sealed record FieldContract(
    string Name,                       // CLR property name
    string ApiName,                    // External API name (typically camelCase)
    FieldType Type,                    // Data type
    bool Nullable,                     // Whether null is allowed
    bool InRead,                       // Included in GET responses
    bool InCreate,                     // Accepted in POST body
    bool InUpdate,                     // Accepted in PATCH body
    bool Filterable,                   // Can use filter[field]=value
    bool Sortable,                     // Can use sort=field
    bool Hidden,                       // Hard-hidden (never exposed)
    bool Immutable,                    // Cannot be changed after creation
    bool Searchable,                   // Included in full-text search
    bool Computed,                     // Server-calculated read-only field
    string? ComputedExpression,        // Expression for computed fields
    object? DefaultValue,              // Default value applied on create
    FieldValidationContract Validation // Validation rules
);

FieldValidationContract

public sealed record FieldValidationContract(
    bool RequiredOnCreate,                     // Must be present on POST
    int? MinLength,                            // Minimum string length
    int? MaxLength,                            // Maximum string length
    decimal? Min,                              // Minimum numeric value
    decimal? Max,                              // Maximum numeric value
    string? Regex,                             // Pattern constraint
    IReadOnlyList<string>? AllowedValues       // Enum-like value restriction
);

RelationContract

Describes a navigation property relationship.

public sealed record RelationContract(
    string Name,                    // CLR navigation property name
    string ApiName,                 // External API name
    RelationKind Kind,              // Cardinality (ManyToOne, OneToMany, etc.)
    string TargetResourceKey,       // Related resource key
    RelationReadContract Read,      // Expansion behavior
    RelationWriteContract Write     // Write behavior
);

RelationReadContract

public sealed record RelationReadContract(
    bool ExpandAllowed,     // Can use expand=relation
    bool DefaultExpanded    // Automatically expanded without asking
);

RelationWriteContract

public sealed record RelationWriteContract(
    RelationWriteMode Mode,         // How writes are performed
    string? WriteFieldName,         // API field name for writes (e.g., "userId")
    bool RequiredOnCreate,          // Required on POST
    string? ForeignKeyProperty      // CLR FK property name
);

SecurityContract

public sealed record SecurityContract(
    IReadOnlyDictionary<CrudOperation, string?> Policies  // Policy name per operation
);

TenantContract

public sealed record TenantContract(
    string FieldName,       // CLR property name of the tenant field
    string FieldApiName,    // API name of the tenant field
    string ClaimType,       // Claim used to resolve the caller's tenant
    bool Required           // Whether a tenant value is required
);

Tenant fields are always server-managed: both contract builders force them out of the create/update input shapes, so clients can never write them.

ConcurrencyContract

public sealed record ConcurrencyContract(
    ConcurrencyMode Mode,       // None, RowVersion, or ETag
    string FieldApiName,        // API name of concurrency field
    bool RequiredOnUpdate       // Whether token is required on PATCH
);

Concurrency token fields are auto-exposed as read-only fields in read responses, so clients can always obtain the current token.


JSON Representation

Contracts can be serialized as JSON — useful for debugging, dynamic definitions, and the schema endpoint:

{
  "resourceKey": "Post",
  "route": "posts",
  "backend": "EfCore",
  "key": { "name": "Id", "type": "Int32" },
  "query": {
    "maxPageSize": 200,
    "filterableFields": ["id", "title", "authorId"],
    "sortableFields": ["id", "title", "createdAt"],
    "searchableFields": ["title", "content"],
    "defaultSort": "-createdAt"
  },
  "read": {
    "expandAllowed": ["author", "tags"],
    "maxExpandDepth": 1,
    "defaultExpand": []
  },
  "fields": [
    {
      "name": "Id", "apiName": "id", "type": "Int32",
      "inRead": true, "filterable": true, "sortable": true, "immutable": true
    },
    {
      "name": "Title", "apiName": "title", "type": "String",
      "inRead": true, "inCreate": true, "inUpdate": true,
      "filterable": true, "sortable": true,
      "validation": { "requiredOnCreate": true, "maxLength": 200 }
    }
  ],
  "relations": [
    {
      "name": "Author", "apiName": "author",
      "kind": "ManyToOne", "targetResourceKey": "User",
      "read": { "expandAllowed": true, "defaultExpanded": false },
      "write": { "mode": "ById", "writeFieldName": "authorId", "requiredOnCreate": true }
    }
  ],
  "security": {
    "policies": {
      "List": null, "Get": null,
      "Create": "Authenticated", "Update": "Authenticated", "Delete": "Admin"
    }
  }
}

Safety Defaults

These defaults are enforced unless explicitly relaxed:

RuleDefault
Opt-in exposureOnly [CrudResource] classes become endpoints
Field allowlistOnly annotated fields are accepted/emitted
Unknown field rejectionUnknown fields in request bodies → 400
No nested writesRelations written by ID only; unknown or inaccessible ids → 400 (targets load through the same tenant/row-level-security/soft-delete scope as reads)
Controlled expansionAllowlist + depth limit (default: 1)
Required paginationAll lists are paged (default: 20, max: 200)
Deterministic orderingEvery paged query is ordered: requested sort → DefaultSort → key tie-breaker
Tenant fields server-managedTenant fields are never client-writable
Filter/sort allowlistsOnly explicitly allowed fields
Startup validationInvalid contracts fail fast with diagnostics (duplicate resource keys, invalid DefaultSort, unreachable concurrency tokens, relations targeting unregistered types, …)

Next