DataKit 1.0.0
dotnet add package DataKit --version 1.0.0
NuGet\Install-Package DataKit -Version 1.0.0
<PackageReference Include="DataKit" Version="1.0.0" />
<PackageVersion Include="DataKit" Version="1.0.0" />
<PackageReference Include="DataKit" />
paket add DataKit --version 1.0.0
#r "nuget: DataKit, 1.0.0"
#:package DataKit@1.0.0
#addin nuget:?package=DataKit&version=1.0.0
#tool nuget:?package=DataKit&version=1.0.0
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
- Packages
- Installation
- Getting Started
- Mapping
- Dependency Injection
- ASP.NET Core Integration
- TypeScript Generation
- API Reference
- License
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:
- Create a new
TDatainstance - For each writable property on
TData, look for a matching property on the source by name (case-insensitive) - Skip properties whose
[InMode]does not match the currentmode - Skip
[Lazy]properties whose name is not inincludes - 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:
- Use
existingif provided, otherwise create a newTEntity - For each readable property on
TData, look for a matching writable property onTEntity - Skip properties whose
[InMode]isDataMode.Response(they are output-only) - Skip
Optional<T>properties whereHasValueis false - 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 | Versions 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 is compatible. 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. |
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.3)
- Microsoft.Extensions.Options (>= 10.0.3)
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.3)
- Microsoft.Extensions.Options (>= 9.0.3)
-
net9.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.3)
- Microsoft.Extensions.Options (>= 9.0.3)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on DataKit:
| Package | Downloads |
|---|---|
|
DataKit.AspNetCore
ASP.NET Core integration for DataKit — model binding, DI helpers, and TypeScript generation endpoint. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 133 | 3/26/2026 |