DataKit.AspNetCore 1.0.0

dotnet add package DataKit.AspNetCore --version 1.0.0
                    
NuGet\Install-Package DataKit.AspNetCore -Version 1.0.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="DataKit.AspNetCore" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DataKit.AspNetCore" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="DataKit.AspNetCore" />
                    
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 DataKit.AspNetCore --version 1.0.0
                    
#r "nuget: DataKit.AspNetCore, 1.0.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 DataKit.AspNetCore@1.0.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=DataKit.AspNetCore&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=DataKit.AspNetCore&version=1.0.0
                    
Install as a Cake Tool

DataKit

Unified data objects for .NET — request DTOs, response resources, and validation targets in a single class. Inspired by spatie/laravel-data. Complements JSON:API envelope libraries (like JsonApiKit) by handling the inner resource objects and request binding.

Table of Contents


Overview

DataKit lets you define a single C# class — a DataObject — that acts as a request DTO when receiving data from the client, a response resource when sending data back, and a validation target thanks to standard System.ComponentModel.DataAnnotations attributes.

public class TicketData : DataObject
{
    [InMode(DataMode.Response)]
    public string? Id { get; set; }

    [Required, MaxLength(300)]
    public string Title { get; set; } = string.Empty;

    public string? Priority { get; set; }

    [InMode(DataMode.Response)]
    public string? Status { get; set; }

    [Lazy]
    public UserData? AssignedTo { get; set; }

    [InMode(DataMode.Response)]
    public DateTime? CreatedAt { get; set; }
}

Key concepts:

Concept What it does
[InMode(DataMode.Response)] Property is populated only when building a response; excluded from request binding
[InMode(DataMode.Request)] Property is populated only when reading a request; excluded from response output
[Lazy] Property is excluded unless explicitly requested via an includes set (mirrors ?include= query params)
Optional<T> Distinguishes "field not sent" from "field sent as null" — useful for PATCH endpoints

Packages

Package Description Target Frameworks
DataKit Core types: DataObject, attributes, Optional<T>, transformers, mapper net8.0, net9.0, net10.0
DataKit.AspNetCore Model binding for DataObject in ASP.NET Core MVC/API net8.0, net10.0

Installation

dotnet add package DataKit

For ASP.NET Core projects:

dotnet add package DataKit.AspNetCore

Getting Started

Defining a Data Object

Subclass DataObject and add properties as normal. Standard System.ComponentModel.DataAnnotations attributes work out of the box because ASP.NET Core's validation pipeline sees the data object directly.

using DataKit;
using DataKit.Attributes;
using System.ComponentModel.DataAnnotations;

public class CreateProjectData : DataObject
{
    [Required, MaxLength(200)]
    public string Name { get; set; } = string.Empty;

    [MaxLength(2000)]
    public string? Description { get; set; }

    public string? OwnerId { get; set; }
}

Controlling Visibility with InMode

Decorate properties with [InMode(DataMode.Response)] to include them only when producing outbound responses, or [InMode(DataMode.Request)] to include them only when reading inbound requests. Properties without the attribute are always included.

public class ProjectData : DataObject
{
    // Only returned in API responses — never accepted from the client
    [InMode(DataMode.Response)]
    public string? Id { get; set; }

    [Required, MaxLength(200)]
    public string Name { get; set; } = string.Empty;

    // Only accepted from the client on create/update
    [InMode(DataMode.Request)]
    public string? RequestedByUserId { get; set; }

    [InMode(DataMode.Response)]
    public DateTime? CreatedAt { get; set; }
}

When the mapper runs in DataMode.Response, it skips RequestedByUserId. When it runs in DataMode.Request (i.e. MapFrom), it skips Id and CreatedAt.

Lazy Properties

Decorated with [Lazy], a property is excluded from mapping unless its camelCase name appears in the includes set. This is ideal for related resources that should only be loaded on demand, mirroring the ?include=assignedTo pattern in JSON:API.

public class TicketData : DataObject
{
    public string Title { get; set; } = string.Empty;

    [Lazy]
    public UserData? AssignedTo { get; set; }

    [Lazy]
    public IList<CommentData>? Comments { get; set; }
}
// Include nothing lazy
var data = mapper.MapTo<TicketData>(ticket, DataMode.Response);

// Include only the assigned user
var data = mapper.MapTo<TicketData>(ticket, DataMode.Response,
    includes: new HashSet<string> { "assignedTo" });

Optional<T>

Use Optional<T> on PATCH request data objects to distinguish between "the client did not send this field" and "the client explicitly sent null".

public class PatchTicketData : DataObject
{
    public Optional<string?> Title { get; set; }
    public Optional<string?> Priority { get; set; }
}
// Client sends: { "title": "New title" }
// Priority is Optional.Undefined — we must NOT overwrite the existing value

var patchData = ...; // bound from request
var existing = await repository.GetAsync(id);

var updated = mapper.MapFrom<PatchTicketData, Ticket>(patchData, existing);
// Only Title is updated; Priority is left untouched

Optional<T> has three key members:

Member Description
HasValue true if the field was explicitly set (even to null)
Value The wrapped value (only meaningful when HasValue is true)
Optional<T>.Undefined The default "not sent" state
Optional<T>.From(value) Create a defined optional
GetValueOrDefault(fallback) Returns value if defined, else fallback

Mapping

MapTo — Entity to Data Object

var ticket = await repository.GetAsync(id);

// Response mapping — include lazy AssignedTo
var data = mapper.MapTo<TicketData>(ticket, DataMode.Response,
    includes: new HashSet<string> { "assignedTo" });

MapTo will:

  1. Create a new TData instance
  2. For each writable property on TData, look for a matching property on the source by name (case-insensitive)
  3. Skip properties whose [InMode] does not match the current mode
  4. Skip [Lazy] properties whose name is not in includes
  5. Apply any registered transformer if the source and destination types differ

MapFrom — Data Object to Entity

// Create
var entity = mapper.MapFrom<CreateTicketData, Ticket>(requestData);

// Update (patch)
var existing = await repository.GetAsync(id);
var updated = mapper.MapFrom<PatchTicketData, Ticket>(patchData, existing);

MapFrom will:

  1. Use existing if provided, otherwise create a new TEntity
  2. For each readable property on TData, look for a matching writable property on TEntity
  3. Skip properties whose [InMode] is DataMode.Response (they are output-only)
  4. Skip Optional<T> properties where HasValue is false
  5. Apply a reverse transformer if needed

Transformers

Transformers handle type conversion during mapping — for example, storing a DateTime on the entity but serializing it as an ISO 8601 string on the data object.

services.AddDataKit(options =>
{
    options.AddTransformer<DateTime, string>(
        dt => dt.ToString("O"),
        str => DateTime.Parse(str));

    options.AddTransformer<Guid, string>(
        g => g.ToString(),
        str => Guid.Parse(str));
});

You can also implement IDataTransformer<TFrom, TTo> and register an instance:

public class UlidStringTransformer : IDataTransformer<Ulid, string>
{
    public string Transform(Ulid value) => value.ToString();
    public Ulid ReverseTransform(string value) => Ulid.Parse(value);
}

// Registration
options.AddTransformer<Ulid, string>(new UlidStringTransformer());

Dependency Injection

Register DataKit in your Program.cs or Startup.cs:

using DataKit.DependencyInjection;

builder.Services.AddDataKit(options =>
{
    options.AddTransformer<DateTime, string>(
        dt => dt.ToString("O"),
        str => DateTime.Parse(str));
});

This registers IDataObjectMapper as a singleton. Inject it anywhere:

public class TicketsController(IDataObjectMapper mapper) : ControllerBase
{
    [HttpGet("{id}")]
    public async Task<IActionResult> Get(string id)
    {
        var ticket = await repository.GetAsync(id);
        var data = mapper.MapTo<TicketData>(ticket, DataMode.Response);
        return Ok(data);
    }
}

ASP.NET Core Integration

Model Binding

Install DataKit.AspNetCore and call AddDataKitWithAspNetCore instead of AddDataKit:

using DataKit.AspNetCore.DependencyInjection;

builder.Services.AddDataKitWithAspNetCore(options =>
{
    options.AddTransformer<DateTime, string>(
        dt => dt.ToString("O"),
        str => DateTime.Parse(str));
});

This registers DataObjectModelBinderProvider, which automatically binds JSON request bodies into any DataObject subclass used as a controller action parameter:

[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateTicketData data)
{
    if (!ModelState.IsValid)
        return ValidationProblem();

    var ticket = mapper.MapFrom<CreateTicketData, Ticket>(data);
    await repository.AddAsync(ticket);
    return CreatedAtAction(nameof(Get), new { id = ticket.Id },
        mapper.MapTo<TicketData>(ticket, DataMode.Response));
}

TypeScript Generation

DataKit can generate TypeScript interface definitions from your data objects. This keeps frontend types in sync with the backend without manual duplication.

using DataKit.TypeScript;

var generator = new TypeScriptGenerator();

// Generate to string
string ts = generator.Generate([
    typeof(TicketData),
    typeof(UserData),
    typeof(CreateTicketData),
]);

// Generate to file
generator.GenerateToFile([
    typeof(TicketData),
    typeof(UserData),
], "resources/js/types/data.ts");

Example output:

// <auto-generated />
// Generated by DataKit TypeScriptGenerator

export interface TicketData {
  id?: string; // inMode: Response
  title: string;
  priority?: string;
  status?: string; // inMode: Response
  assignedTo?: UserData; // lazy
  createdAt?: string; // inMode: Response
}

export interface UserData {
  id?: string; // inMode: Response
  name: string;
  email: string;
}

Type mappings:

C# type TypeScript type
string string
bool boolean
int, long, float, double, decimal number
DateTime, DateTimeOffset, DateOnly string
Guid string
T? / Nullable<T> T? (optional)
Optional<T> T? (optional)
List<T>, IEnumerable<T> T[]
Dictionary<K, V> Record<K, V>
DataObject subclass interface reference
anything else unknown

API Reference

DataObject

Abstract base class. Subclass to create a data object.

DataMode

public enum DataMode { Request, Response }

InModeAttribute

[AttributeUsage(AttributeTargets.Property)]
public sealed class InModeAttribute(DataMode mode) : Attribute

LazyAttribute

[AttributeUsage(AttributeTargets.Property)]
public sealed class LazyAttribute : Attribute

Optional<T>

public readonly struct Optional<T>
{
    public bool HasValue { get; }
    public T? Value { get; }
    public static Optional<T> Undefined { get; }
    public static Optional<T> From(T? value);
    public static implicit operator Optional<T>(T? value);
    public T? GetValueOrDefault(T? fallback = default);
}

IDataTransformer<TFrom, TTo>

public interface IDataTransformer<TFrom, TTo>
{
    TTo Transform(TFrom value);
    TFrom ReverseTransform(TTo value);
}

IDataObjectMapper

public interface IDataObjectMapper
{
    TData MapTo<TData>(object source, DataMode mode = DataMode.Response,
        IReadOnlySet<string>? includes = null) where TData : DataObject, new();

    TEntity MapFrom<TData, TEntity>(TData data, TEntity? existing = default)
        where TData : DataObject where TEntity : new();
}

DataKitOptions

public sealed class DataKitOptions
{
    DataKitOptions AddTransformer<TFrom, TTo>(Func<TFrom, TTo> transform, Func<TTo, TFrom> reverseTransform);
    DataKitOptions AddTransformer<TFrom, TTo>(IDataTransformer<TFrom, TTo> transformer);
}

TypeScriptGenerator

public sealed class TypeScriptGenerator
{
    string Generate(IEnumerable<Type> dataTypes);
    void GenerateToFile(IEnumerable<Type> dataTypes, string outputPath);
}

License

MIT License — see LICENSE for details.

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 is compatible.  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
1.0.0 110 3/26/2026