EntitiesDb 3.7.0

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

Banner

NuGet Version NuGet Downloads Build & Publish License: MIT .NET 8 | netstandard2.1

A High-Performance, Lightweight C# Entity Component System

A modern, cache-efficient Entity Component System (ECS) for games, simulations, and other data-oriented workloads. EntitiesDb focuses on raw performance, simple APIs, and zero external dependencies — all in pure C#.

using var db = new EntityDatabase();

var player = db.Create(new Position(0, 0), new Velocity(1, 1));

var query = db.QueryBuilder.WithAll<Position, Velocity>().Build();

query.ForEach((ref Position pos, in Velocity vel) =>
{
    pos.X += vel.Dx;
    pos.Y += vel.Dy;
});

Highlights

  • 🚀 Fast — archetype-organized, fixed-size chunk storage; components of a type are contiguous in memory
  • 🧩 Simple — any struct/class/record is a component; expressive WithAll / WithAny / WithNone / WithOnly queries
  • ⚙️ Source-generated ForEach — lambdas with ref/in parameters are rewritten at compile time into strongly-typed loops; the generator ships inside the package, no setup
  • 🧵 Multithreaded — ForEachParallel / ForEachChunkParallel with a single fork/join per query, plus per-thread aggregates for reductions (opt-in via options)
  • 📦 Tags & inline buffers — zero-size tag components and [Buffer(n)] inline lists stored directly in the chunk
  • 🔍 Change filters — only visit chunks whose components were written since the last pass
  • 🧮 SIMD-friendly — reinterpret component handles as Vector128/256/512<T> and process several entities per instruction
  • 📝 Command buffers — thread-safe deferred Create / Add / Set / Remove / Destroy, applied with one Commit()
  • 🔧 Manual enumeration — walk archetypes, chunks and raw read/write handles when you need full control
  • 0️⃣ GC-friendly — unmanaged chunk memory, allocation-free iteration, no per-entity objects
  • 🎯 Targets net8.0 and netstandard2.1 — no dependencies on .NET 8 (System.Memory + System.Runtime.CompilerServices.Unsafe on netstandard)

Installation

dotnet add package EntitiesDb
<PackageReference Include="EntitiesDb" Version="*" />
Install-Package EntitiesDb

Requirements

  • Runtime: .NET 8+ or any runtime that supports netstandard2.1 (Unity, Mono, .NET Core 3.x, …)
  • SDK: the ForEach source generator needs a Roslyn 4.3+ compiler — .NET 6 SDK or newer, or a recent Visual Studio / Rider
  • Language: lambdas with ref / in parameters (C# 7.2+); the samples below use record struct (C# 10)

Consuming the project instead of the package? Reference the generator too, so ForEach gets rewritten:

<ProjectReference Include="..\EntitiesDb\EntitiesDb.csproj" />
<ProjectReference Include="..\EntitiesDb.SourceGenerators\EntitiesDb.SourceGenerators.csproj"
                  OutputItemType="Analyzer" ReferenceOutputAssembly="false" />

Without the generator, ForEach throws CodeGenerationException at runtime.


Quick Start

using EntitiesDb;

// 1. Define components — plain types, no base class or interface
public record struct Position(float X, float Y);
public record struct Velocity(float Dx, float Dy);
public record struct Health(int Points, int Max);

// 2. Create a database (defaults: 16 KB chunks, unlimited entities, parallel disabled)
using var db = new EntityDatabase();

// 3. Create entities (up to 16 components per call)
var player = db.Create(new Position(10, 10), new Velocity(5, 5), new Health(100, 100));
var rock   = db.Create(new Position(0, 0));

// 4. Work with a single entity
db.Add(rock, new Velocity(1, 0));
db.Has<Velocity>(rock);                             // true
ref var hp = ref db.Write<Health>(player);          // by-ref write
hp.Points -= 10;
ref readonly var pos = ref db.Read<Position>(rock); // by-ref read
db.Remove<Velocity>(rock);
db.Destroy(rock);

// 5. Build a query once, reuse it every frame
var movers = db.QueryBuilder
    .WithAll<Position, Velocity>()
    .Build();

// 6. Iterate — `ref` = write, `in` = read
movers.ForEach((ref Position pos, in Velocity vel) =>
{
    pos.X += vel.Dx;
    pos.Y += vel.Dy;
});

Feature Tour

Each snippet is a taste — follow the links for the full story in DOCS.md.

Queries

var query = db.QueryBuilder
    .WithAll<Damage, EnemyTag>()   // must have all
    .WithAny<PlayerTag, NpcTag>()  // at least one of
    .WithNone<BossTag>()           // none of
    .Build();

var exact = db.QueryBuilder.WithOnly<Position, Velocity>().Build(); // exactly these

→ QueryBuilder

Tags & Buffers

[Tag] public struct EnemyTag { }                       // zero-size marker
[Buffer(8)] public record struct Item(int Id, int Count); // inline list, 8 elements in-chunk

var e = db.Create(new Position(1, 1), new EnemyTag(), new[] { new Item(1, 5) });

var items = db.WriteBuffer<Item>(e);
items.Add(new Item(2, 1));

→ Buffers · Tags

Chunk iteration & SIMD

query.ForEachChunk((int len, WriteHandle<Position> pos, ReadHandle<Velocity> vel) =>
{
    var p = pos.Reinterpret<Position, Vector256<float>>(); // 4 entities per lane (2 floats each)
    var v = vel.Reinterpret<Velocity, Vector256<float>>();
    int simdLen = (len - (len & 3)) / 4;
    for (int i = 0; i < simdLen; i++) p[i] += v[i];
    for (int i = simdLen * 4; i < len; i++) { pos[i].X += vel[i].Dx; pos[i].Y += vel[i].Dy; }
});

→ ForEachChunk · SIMD

Multithreading

// parallel execution is opt-in — physical cores are the sweet spot (see Benchmarks)
using var db = new EntityDatabase(new EntityDatabaseOptions(parallelThreads: Environment.ProcessorCount / 2));

query.ForEachParallel((ref Position pos, in Velocity vel) => { pos.X += vel.Dx; pos.Y += vel.Dy; });

// per-thread state + join for reductions (SumAggregate : IParallelAggregate<int>)
var total = new SumAggregate();
query.ForEachParallel((in Wallet w, ref int local) => local += w.Gold, ref total);

No structural changes (Create/Add/Remove/Destroy) inside parallel loops — use a command buffer.

→ Multithreading · Parallel Aggregate

Change filters

[TrackChanges] public record struct Position(float X, float Y);

var moved = db.QueryBuilder
    .WithAll<Position, Renderable>()
    .WithChangeFilter<Position>()   // only chunks where Position was written since last pass
    .Build();

→ Change Filter

Command buffers

var commands = db.CreateCommandBuffer(initialCapacity: 256);

query.ForEachParallel((Entity e, in Health hp) =>
{
    if (hp.Points <= 0) commands.Destroy(e);   // thread-safe
});

commands.Commit();                              // apply on the main thread

→ Command Buffers

Manual enumeration

foreach (var (length, positions, velocities) in query.WriteHandles<Position, Velocity>())
    for (int i = 0; i < length; i++) { positions[i].X += velocities[i].Dx; }

→ Manual Enumeration


How It Works

  • An entity is an int id plus a version; ids are recycled and the version rejects stale handles.
  • Entities with the same set of components share an archetype. Each archetype owns a list of fixed-size chunks (16 KB by default, EntityDatabaseOptions.chunkByteSize) laid out structure-of-arrays: all Positions in a chunk are contiguous, then all Velocitys, and so on.
  • Unmanaged components live in unmanaged chunk memory (no GC tracking); class components are supported through parallel managed arrays but iterate slower.
  • A query is a signature filter that lazily matches archetypes; iterating it means walking matched chunks and handing out spans (ReadHandle<T> / WriteHandle<T>).
  • ForEach lambdas are captured by a Roslyn incremental source generator that emits a strongly-typed extension method per call site — no delegate invocation or boxing per entity. The generator is packaged as an analyzer inside the NuGet package.
  • Parallel methods run one fork/join per call over a fixed thread pool; threads claim batches of chunks from a shared cursor (dynamic scheduling, so a preempted thread can't stall the join) and per-thread state is created and joined through IParallelAggregate<T>. Steady-state parallel calls don't allocate.
  • Structural changes (create/destroy/add/remove) move entities between archetypes and are single-threaded; a thread-safe CommandBuffer defers them.
  • Limits: 256 component types per process, 16 components per Create/Add call, component size ≤ 32 KB.

Benchmarks

Numbers from the in-repo BenchmarkDotNet suite, which compares EntitiesDb against plain List<struct> / List<class> loops doing the same work. Snapshot: 2026-08-17, EntitiesDb 3.6.1, Intel Core i7-8700K (6C/12T), Windows 11, .NET 8.0.27 x64, library-default options (16 KB chunks), parallel rows on 12 threads. Full reports with mean/error/std-dev columns: src/EntitiesDb.Benchmark/results/2026-08-17-v3.6.1.

c1.Value += c2.Value over every entity — SystemWithTwoComponents (single archetype), median time per pass:

Method 100 000 entities 1 000 000 entities
Structs — List<struct> baseline (AoS) 59.8 μs 1,234.0 μs
Classes — List<class> baseline 143.4 μs 4,189.0 μs
EntitiesDb_ForEach — source-generated lambda 84.9 μs 968.8 μs
EntitiesDb_ForEachChunk — chunk handles 72.1 μs 738.7 μs
EntitiesDb_ForEachChunk_Simd — chunk handles + Vector256<int> 18.4 μs 421.1 μs
EntitiesDb_Enumeration_Simd — manual WriteHandles + SIMD 17.7 μs 408.7 μs

Same work, entities spread over 4 archetypes — SystemWithTwoComponentsVariedComposition (median):

Method 100 000 entities 1 000 000 entities
Structs baseline (4 lists) 66.2 μs 1,510.0 μs
Classes baseline (4 lists) 217.1 μs 8,896.6 μs
EntitiesDb_ForEach 76.6 μs 1,224.5 μs
EntitiesDb_ForEachChunk 56.0 μs 803.3 μs
EntitiesDb_Enumeration_Simd 18.1 μs 604.6 μs

Parallel — ParallelScaling, same c1 += c2 work, parallelThreads = 6 (physical cores), median per pass:

Method 100 000 entities 1 000 000 entities
ForEachChunk — single thread, for reference 55.2 μs 740.6 μs
ForEachParallel 22.5 μs 213.2 μs
ForEachChunkParallel 15.9 μs 176.0 μs
ForEachChunkParallel_Simd 7.4 μs 74.6 μs
Dispatch_OneEntity — fork/join floor (query matching 1 entity) 0.95 μs 0.94 μs

Parallel calls allocate nothing in steady state. Threads claim chunks from a shared cursor (guided self-scheduling), so a preempted thread only delays its current batch. Thread count matters: with 11–12 threads on this 6-core/12-thread CPU the same calls were 2–5× slower and highly variable (SMT siblings add nothing to memory-bound loops, and using every logical processor oversubscribes the box) — use physical cores.

Create 100 000 entities with one component — CreateEntityWithOneComponent (mean; managed allocations only ‡):

Method Mean Allocated (managed) ‡
Structs — List<struct>.Add 271.3 μs 781 KB
Classes — List<class>.Add 967.7 μs 3,125 KB
EntitiesDb — Reserve + Create 3,067.2 μs 84 KB
EntitiesDb_CommandBuffer — queue + Commit 16,142.5 μs 14,351 KB

Notes:

  • EntitiesDb_ForEach is the like-for-like row (a scalar per-entity loop, same as the baselines). *_Simd rows are hand-vectorized; a SIMD baseline over SoA arrays is on the follow-up list.
  • Medians are shown throughout; the machine was not isolated.
  • ‡ BenchmarkDotNet's Allocated column counts managed heap only. EntitiesDb stores entities and unmanaged components in native chunk memory, which is not included — the 84 KB is entity-map growth, not total memory.
  • Entity creation is a structural change and is not EntitiesDb's design center; the numbers are here for completeness. The command-buffer path trades throughput for thread-safety and deferral.
  • All timings are one machine on one day. Reproduce with dotnet run -c Release --project src/EntitiesDb.Benchmark.

Documentation

📚 Full guide: DOCS.md

Core Concepts · EntityDatabase · Entities · Components · Buffers · Tags · Queries · ForEach · ForEachChunk · Change Filter · Manual Enumeration · Multithreading · Parallel Aggregate · Command Buffers · SIMD · Attributes


Building From Source

git clone https://github.com/Juiix/EntitiesDb.git
cd EntitiesDb

dotnet build src/EntitiesDb.sln -c Release
dotnet test  src/EntitiesDb.Tests
dotnet run   -c Release --project src/EntitiesDb.Benchmark      # optional, slow

Repository layout:

Path What it is
src/EntitiesDb The library (net8.0, netstandard2.1). Much of the generic API surface (Create<T0..T15>, WithAll<…>, handles, …) is produced from the T4 templates in Templates/.
src/EntitiesDb.SourceGenerators Roslyn incremental generator for ForEach* calls; packed into the NuGet package under analyzers/dotnet/cs.
src/EntitiesDb.Tests xUnit test suite.
src/EntitiesDb.Benchmark BenchmarkDotNet suite (see its README).

Editing a .tt template requires re-running the T4 generator (Visual Studio does this on save; the generated .cs files are checked in).


Contributing

Issues and pull requests are welcome. Please run dotnet test src/EntitiesDb.Tests before opening a PR, and include a benchmark run for performance-sensitive changes.

License

MIT — free for commercial and open-source use.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  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 was computed.  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. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos 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
3.7.0 70 10/1/2026
3.6.0 751 8/6/2026
3.5.10 123 8/6/2026
3.5.9 106 8/6/2026
3.5.8 114 8/6/2026
3.5.7 389 4/1/2026
3.5.6 104 3/30/2026
3.5.5 104 3/26/2026
3.5.4 107 3/22/2026
3.5.3 103 3/21/2026
3.5.2 107 3/20/2026
3.5.1 103 3/18/2026
3.5.0 118 3/9/2026
3.4.17 106 3/9/2026
3.4.16 101 3/7/2026
3.4.15 107 3/4/2026
3.4.14 96 3/4/2026
3.4.13 105 3/4/2026
3.4.12 109 2/22/2026
3.4.11 106 2/22/2026
Loading failed

3.7.0: ParallelJobRunner signals only the workers a call needs (a one-job call never leaves the calling thread), runs jobs a worker hasn't started on the caller, and parks idle workers instead of spinning - more threads than cores no longer costs milliseconds per parallel call; parallel queries claim chunks from one shared list (a preempted thread no longer stalls the join) and are allocation-free in steady state; UntrackedWriteHandle and Chunk.MarkChanged<T>() to write tracked components without marking every chunk; generated ForEach/ForEachChunk honour change filters; a destroy carries the moved entity's pending changes; chunks past 32 KB keep their columns apart (int offsets); fresh entity ids start at version 0; a stale EntityData throws instead of reaching another archetype (chunk arrays are no longer pooled); EntityDatabase() and EntityDatabaseOptions.Default; TrimExcess on an empty database. 3.6.0: parallel queries execute one fork/join across all matching archetypes (was one per archetype); Archetype.Reserve zero-initializes unmanaged chunk memory.