EntitiesDb 3.7.0
dotnet add package EntitiesDb --version 3.7.0
NuGet\Install-Package EntitiesDb -Version 3.7.0
<PackageReference Include="EntitiesDb" Version="3.7.0" />
<PackageVersion Include="EntitiesDb" Version="3.7.0" />
<PackageReference Include="EntitiesDb" />
paket add EntitiesDb --version 3.7.0
#r "nuget: EntitiesDb, 3.7.0"
#:package EntitiesDb@3.7.0
#addin nuget:?package=EntitiesDb&version=3.7.0
#tool nuget:?package=EntitiesDb&version=3.7.0

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/recordis a component; expressiveWithAll / WithAny / WithNone / WithOnlyqueries - ⚙️ Source-generated
ForEach— lambdas withref/inparameters are rewritten at compile time into strongly-typed loops; the generator ships inside the package, no setup - 🧵 Multithreaded —
ForEachParallel/ForEachChunkParallelwith 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 oneCommit() - 🔧 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.0andnetstandard2.1— no dependencies on .NET 8 (System.Memory+System.Runtime.CompilerServices.Unsafeon 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
ForEachsource generator needs a Roslyn 4.3+ compiler — .NET 6 SDK or newer, or a recent Visual Studio / Rider - Language: lambdas with
ref/inparameters (C# 7.2+); the samples below userecord struct(C# 10)
Consuming the project instead of the package? Reference the generator too, so
ForEachgets rewritten:<ProjectReference Include="..\EntitiesDb\EntitiesDb.csproj" /> <ProjectReference Include="..\EntitiesDb.SourceGenerators\EntitiesDb.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />Without the generator,
ForEachthrowsCodeGenerationExceptionat 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
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));
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();
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
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; }
How It Works
- An entity is an
intid 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: allPositions in a chunk are contiguous, then allVelocitys, and so on. - Unmanaged components live in unmanaged chunk memory (no GC tracking);
classcomponents 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>). ForEachlambdas 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
CommandBufferdefers them. - Limits: 256 component types per process, 16 components per
Create/Addcall, 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_ForEachis the like-for-like row (a scalar per-entity loop, same as the baselines).*_Simdrows 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
Allocatedcolumn 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 | Versions 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. |
-
.NETStandard 2.1
- System.Memory (>= 4.6.3)
- System.Runtime.CompilerServices.Unsafe (>= 6.1.2)
-
net8.0
- No dependencies.
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 |
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.