Memory2 1.0.0

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

Memory2

Memory2 gives C# and .NET a new kind of memory.

Store enormous connected systems. Query them with LINQ. Change live state. Keep queries active as subscriptions. Apply ordered synchronization changes. Save the complete store, verify it, and reopen it later.

This guide covers Memory2's high-level C# API.


Contents


Install

Install Memory2 from NuGet:

dotnet add package Memory2

Then import the namespace:

using Memory2;

Open a store

In memory

Use an in-memory store for tests, temporary workloads, or applications that do not need persistence.

using var db = MemoryStore.CreateInMemory();

Backed by a file

using var db = MemoryStore.Open("application.m2");

Backed by a stream

using var stream = new MemoryStream();
using var db = MemoryStore.Open(stream, leaveOpen: true);

Use leaveOpen: true when another part of the application owns the stream.


Define models

Memory2 works with ordinary C# classes.

public sealed class Account
{
    public long Id { get; set; }
    public string Name { get; set; } = "";
    public decimal Balance { get; set; }
    public bool Active { get; set; }
}

Connected models can contain owned objects and collections.

public sealed class Vehicle
{
    public long Id { get; set; }
    public string Vin { get; set; } = "";
    public List<Tire> Tires { get; set; } = new();
}

public sealed class Tire
{
    public long Id { get; set; }
    public string Position { get; set; } = "";
    public int Pressure { get; set; }
    public TireColor Color { get; set; }
    public List<TireUse> Uses { get; set; } = new();
}

public sealed class TireUse
{
    public long Id { get; set; }
    public int Distance { get; set; }
    public UseMetadata Metadata { get; set; } = new();
}

public sealed class UseMetadata
{
    public string Location { get; set; } = "";
    public bool Inspected { get; set; }
}

public enum TireColor
{
    Unknown,
    Black,
    Green,
    Blue
}

Model guidance

  • Use public properties.
  • Give collection properties a usable default such as new().
  • Keep identity stable after creation.
  • Prefer clear ownership: a nested object should belong to one surrounding graph.
  • Use enums and scalar properties for values that will be filtered or ordered frequently.

Work with sets

A set is the high-level entry point for one model type.

var accounts = db.Set<Account>();
var vehicles = db.Set<Vehicle>();

Use a set to create, read, update, delete, and subscribe to that type.


Create data

var accounts = db.Set<Account>();

Account account = accounts.Create(new Account
{
    Name = "Ada",
    Balance = 250m,
    Active = true
});

Memory2 assigns identity when required by the model and store.

Create a connected graph the same way:

var vehicles = db.Set<Vehicle>();

Vehicle vehicle = vehicles.Create(new Vehicle
{
    Vin = "VIN-0001",
    Tires =
    {
        new Tire
        {
            Position = "FrontLeft",
            Pressure = 32,
            Color = TireColor.Black,
            Uses =
            {
                new TireUse
                {
                    Distance = 12_000,
                    Metadata = new UseMetadata
                    {
                        Location = "road",
                        Inspected = true
                    }
                }
            }
        }
    }
});

Read data

Use GetRequired when the object must exist.

Account account = accounts.GetRequired(accountId);

A required read returns the complete C# object.

Use a query when the application only needs an answer, a small projection, a count, or a filtered set.


Hydrate query results

A query does not need to construct a complete C# object for every record it examines. Keep filtering, ordering, grouping, counting, and projection inside Memory2 whenever the final answer does not require full models.

Call Hydrate() when application code needs complete object graphs for the filtered matches.

Fleet[] eastFleets = db.From<Fleet>()
    .Where(x => x.Name.StartsWith("fleet-east"))
    .Take(10)
    .Hydrate();

Each returned Fleet is a complete configured graph, including its owned vehicles, tires, uses, and metadata.

Fleet first = eastFleets[0];

Vehicle vehicle = first.Vehicles[0];
Tire tire = vehicle.Tires[0];
UseMetadata metadata = tire.Uses[0].Metadata;

The order matters:

  1. Build the query.
  2. Narrow the result.
  3. Call Hydrate() only when complete C# models are required.

Query, hydrate, and read are different operations

Query answers a question without building complete models for every input examined.

int[] pressures = db.From<Tire>()
    .Where(x => x.Active)
    .Select(x => x.Pressure)
    .ToArray();

Hydrate builds complete models for the selected query results.

Tire[] tires = db.From<Tire>()
    .Where(x => x.Active && x.Pressure < 28)
    .Hydrate();

Read retrieves a known object by identity.

Account account = accounts.GetRequired(accountId);

Use the least expensive operation that returns what the application actually needs.

Hydration and direct inner-object access

Hydration is useful when code needs a complete selected graph. It is not required to change one object inside that graph.

Tire tire = db.Query<Tire>()
    .Where(x => x.Pressure < 28)
    .First();

tire.Pressure = 34;
db.Save(tire);

The tire can be queried and saved directly. Its vehicle or fleet only needs to be loaded or hydrated when application logic actually needs that surrounding object.


Query with LINQ

Memory2 uses familiar LINQ expressions.

var activeAccounts = db.Query<Account>()
    .Where(x => x.Active)
    .OrderByDescending(x => x.Balance)
    .ToList();

Filter

var lowBalance = db.Query<Account>()
    .Where(x => x.Active && x.Balance < 100m)
    .ToList();

Select only what is needed

var names = db.Query<Account>()
    .Where(x => x.Active)
    .Select(x => x.Name)
    .ToList();

Count

long activeCount = db.Query<Account>()
    .LongCount(x => x.Active);

Check whether a match exists

bool hasOverdrawnAccount = db.Query<Account>()
    .Any(x => x.Balance < 0m);

First match

Account richest = db.Query<Account>()
    .Where(x => x.Active)
    .OrderByDescending(x => x.Balance)
    .First();

Group and aggregate

var totalsByRegion = db.Query<CustomerAccount>()
    .Where(x => x.Active)
    .GroupBy(x => x.Region)
    .Select(group => new
    {
        Region = group.Key,
        Total = group.Sum(x => x.Balance),
        Count = group.Count()
    })
    .ToList();

Join

var accountOwners = db.Query<Account>()
    .Join(
        db.Query<Customer>(),
        account => account.CustomerId,
        customer => customer.Id,
        (account, customer) => new
        {
            customer.Name,
            account.Balance
        })
    .ToList();

Build one result from several model types

Query<TResult>() can combine different source types into one typed result.

public sealed class OperationsAction
{
    public string Kind { get; set; } = "";
    public string Reference { get; set; } = "";
    public decimal Priority { get; set; }
}
var actions = db.Query<OperationsAction>()
    .Include<Order>(orders => orders
        .Where(order => order.Status == OrderStatus.Paid)
        .WhereExists<Payment, long>(
            order => order.Id,
            payment => payment.OrderId)
        .WhereNotExists<Shipment, long>(
            order => order.Id,
            shipment => shipment.OrderId)
        .Select(order => new OperationsAction
        {
            Kind = "Release order",
            Reference = order.Reference,
            Priority = order.Total
        }))
    .Include<SupportTicket>(tickets => tickets
        .Where(ticket => ticket.Open && ticket.Severity >= 8)
        .Select(ticket => new OperationsAction
        {
            Kind = "Escalate support",
            Reference = ticket.Reference,
            Priority = ticket.Severity
        }))
    .OrderByDescending(action => action.Priority)
    .ToList();

This is useful for work queues, dashboards, release controls, alerts, and operational views that would otherwise require several queries and a manual merge.

Existence filters

Use WhereExists when a related record must exist.

.WhereExists<Payment, long>(
    order => order.Id,
    payment => payment.OrderId)

Use WhereNotExists when a related record must not exist.

.WhereNotExists<Shipment, long>(
    order => order.Id,
    shipment => shipment.OrderId)

Query inner objects directly

An object inside a larger connected model can be queried as its own type.

Tire tire = db.Query<Tire>()
    .Where(x => x.Color == TireColor.Green)
    .OrderByDescending(x => x.Pressure)
    .First();

The Vehicle does not need to be loaded first.

This works well when the application cares about the exact part that needs attention:

  • a tire inside a vehicle;
  • a sensor inside a robot;
  • a line item inside an order;
  • a status effect inside a player;
  • a component inside a machine;
  • metadata inside a stored history record.

Find the parent or root

After querying an inner object, locate the surrounding owner only when needed.

Vehicle vehicle = db.Root<Vehicle>(tire);

For a closer containing object, use Parent<T>() when the model shape has an intermediate owner.

TireUse use = db.Query<TireUse>()
    .First(x => x.Metadata.Location == "track");

Tire parentTire = db.Parent<Tire>(use);
Vehicle rootVehicle = db.Root<Vehicle>(use);

This avoids loading the entire graph merely to discover or change one inner object.


Save an inner object directly

An inner object can be changed and saved without loading or rewriting the root.

Tire tire = db.Query<Tire>()
    .First(x => x.Pressure < 30);

// The vehicle has not been loaded.
tire.Pressure = 34;

db.Save(tire);

Memory2 already knows where the queried object belongs.

Important

Save the tracked object returned by Memory2. A newly constructed object with similar values is not automatically the same stored object.


Update and upsert

Update a loaded root

Account account = accounts.GetRequired(accountId);
account.Balance += 100m;

db.Save(account);

Upsert through a set

account.Active = false;
accounts.Upsert(account);

Use Upsert when the object may already exist and the set should create or replace it as appropriate.

Apply several changes together

var changes = new MemoryChangeSet()
    .Upsert("billing", 101, account)
    .Upsert("billing", 102, anotherAccount);

SynchronizationBatchResult result = db.Apply(changes);

Use a change set when ordering, replay protection, or synchronization status matters.


Delete data

Delete a root through its set:

accounts.Delete(accountId);

When a root owns a connected graph, deleting the root removes the owned graph from the visible store.

Use deletion for the whole owned domain. Do not manually delete every inner object one by one unless those objects are independent roots.


Subscribe to live conditions

A subscription keeps a condition active and calls application logic when an object enters, changes inside, or leaves that condition.

var events = db.Set<OperationalEvent>();

using var subscription = events.Subscribe(
    item => item.Ready &&
            item.Severity >= 7 &&
            item.Topic == "orders",
    transition =>
    {
        switch (transition.Kind)
        {
            case SubscriptionChangeKind.Entered:
                StartWork(transition.Current);
                break;

            case SubscriptionChangeKind.Updated:
                RefreshWork(transition.Current);
                break;

            case SubscriptionChangeKind.Left:
                StopWork(transition.Previous);
                break;
        }
    });

Change state normally:

OperationalEvent item = events.GetRequired(eventId);
item.Topic = "orders";
item.Ready = true;
item.Severity = 9;
events.Upsert(item);

Memory2 evaluates the condition and reports the transition.

Transition meanings

  • Entered: the object did not match before and matches now.
  • Updated: the object matched before and still matches after the change.
  • Left: the object matched before and no longer matches.

Subscription uses

One stored change can drive:

  • work queues;
  • alerts;
  • user-interface updates;
  • achievements;
  • background processing;
  • cache refreshes;
  • indexing;
  • orchestration;
  • monitoring.

Dispose subscriptions when they are no longer needed.

using var subscription = ...;

Synchronize changes

Memory2 can apply ordered changes from another service, process, device, or replica.

var batch = new MemoryChangeSet()
    .Upsert(
        source: "warehouse-a",
        sequence: 42,
        model: account,
        expectedMutationVersion: 7);

SynchronizationBatchResult result = db.Apply(batch);

Each change has:

  • a stable source name;
  • a sequence number from that source;
  • the model being applied;
  • an optional expected mutation version.

Applied

The next expected change is accepted and written.

Already applied

A retry of the same source and sequence is recognized. The change is not written twice.

Gap

A later sequence arrives before an earlier one. The missing change must be supplied first.

Conflict

The expected mutation version no longer matches the stored version. A stale writer cannot silently overwrite newer state.

Producer guidance

  • Give each producer a stable source name.
  • Persist its sequence counter.
  • Retry the same sequence after a transport failure.
  • Do not issue a new sequence for the same logical retry.
  • Use expected mutation versions when overwriting stale state would be unsafe.
  • Request missing sequences when a gap is reported.

Save, verify, and reopen

Memory2 can persist the complete store to disk or a stream.

Save

using var db = MemoryStore.Open("application.m2");

var accounts = db.Set<Account>();
accounts.Upsert(account);

db.Save();

Verify

VerificationResult verification = db.Verify();

if (!verification.IsValid)
{
    foreach (VerificationIssue issue in verification.Issues)
        Console.Error.WriteLine(issue.Message);
}

Reopen

using var reopened = MemoryStore.Open("application.m2");
Account restored = reopened.Set<Account>().GetRequired(accountId);

Stream example

using var stream = new MemoryStream();

using (var db = MemoryStore.Open(stream, leaveOpen: true))
{
    db.Set<Account>().Create(new Account
    {
        Name = "Ada",
        Balance = 250m,
        Active = true
    });

    db.Save();
}

stream.Position = 0;

using var restoredDb = MemoryStore.Open(stream, leaveOpen: true);
Account restored = restoredDb.Set<Account>().GetRequired(1);

Durability guidance

  • Save at application-defined durability boundaries.
  • Verify important files after writing them.
  • Reopen a saved store during deployment and backup checks.
  • Keep backup rotation and atomic file replacement policies at the application level.

Query, hydrate, and read

A read and a query solve different problems.

Read

Account account = accounts.GetRequired(id);

A read builds the complete C# object because application code asked for the object.

Query

bool shouldAlert = db.Query<Account>()
    .Any(x => x.Active && x.Balance < 0m);

A query can inspect stored values and return only the answer. It does not need to build every object it checks.

Use:

  • a read when you need the complete model;
  • Select when you need a few values;
  • Any, Count, Sum, Min, or Max when you need one answer;
  • a filtered query when you need only matching results.

This is why query throughput can be much higher than full-object read throughput.


LINQ behavior

Memory2 plans a complete LINQ expression as one query. Combined operators do not necessarily add their standalone costs together.

Filters can become one test

query
    .Where(x => x.Active)
    .Where(x => x.Region == Region.North)
    .Where(x => x.Score >= 90);

Memory2 can evaluate the combined condition during one pass.

Unused work can disappear

query
    .Select(x => new ExpensiveView(x))
    .Count();

The projection is unnecessary when only the count is observed.

Ordering can disappear

query
    .OrderByDescending(x => x.Score)
    .Count();

The order does not change the count.

Small ranked windows need less work

query
    .OrderByDescending(x => x.Score)
    .Skip(3)
    .Take(16);

Memory2 can retain the small required frontier instead of treating the query as a request to sort every matching record.

Terminal operations can be fused

Operations such as Any, Count, Sum, Min, and Max can be calculated while the query applies its filters.

Supported LINQ families

The demonstrated high-level surface includes common operations such as:

  • Where
  • Select
  • Any
  • All
  • Count and LongCount
  • First and Last
  • ElementAt
  • Contains
  • Distinct
  • Union
  • Intersect
  • Except
  • ordering and ranked windows
  • Skip and Take
  • Min and Max
  • Aggregate
  • GroupBy
  • Join
  • Zip
  • SequenceEqual
  • Concat, Append, and Prepend
  • DefaultIfEmpty

Use the simplest query that describes the answer. Let Memory2 prepare the execution plan.


Common patterns

Operational work queue

Combine orders, fraud holds, and support tickets into one result type, order them by urgency, and send the result to workers.

Direct component maintenance

Query a failed component inside a machine, find the machine only when needed, update the component, and save it directly.

Condition-driven logic

Subscribe to a predicate such as:

x => x.Active && x.Temperature >= 90

Start work when an object enters the condition and stop work when it leaves.

Retry-safe synchronization

Use a stable source and sequence number so a retried transport message cannot apply the same change twice.

Durable in-memory application state

Keep the live application state in Memory2, save at durability boundaries, verify the file, and reopen it after restart.

Query first, hydrate when needed

Keep filtering and projection inside Memory2. Call Hydrate() only after the result is small enough and application code needs complete models.

Query instead of loading

Replace this pattern:

var all = accounts.ToList();
var total = all.Where(x => x.Active).Sum(x => x.Balance);

with:

var total = db.Query<Account>()
    .Where(x => x.Active)
    .Sum(x => x.Balance);

Ask Memory2 for the answer instead of loading every object first.


Performance guidance

Build in Release mode

Do not use debugger-attached timings as production numbers.

dotnet run -c Release

Measure the operation you actually need

Do not compare a full-object read with a query scan as though they perform the same work.

Measure separately:

  • complete object reads;
  • small projections;
  • filtered queries;
  • aggregates;
  • writes;
  • mixed read/query/write traffic;
  • persistence.

Filter before hydrating

Use LINQ to narrow the result before calling Hydrate(). Hydration is for complete selected models, not for scanning or filtering the entire store.

Prefer projections for small answers

var balances = db.Query<Account>()
    .Where(x => x.Active)
    .Select(x => x.Balance)
    .ToList();

Do not construct complete objects when only one or two fields are required.

Query inner objects directly

Do not load a complete root merely to locate one owned object.

Tire tire = db.Query<Tire>()
    .First(x => x.Pressure < 30);

Save the object that changed

tire.Pressure = 34;
db.Save(tire);

Avoid rebuilding and rewriting the surrounding graph when only one inner object changed.

Keep subscriptions focused

Use clear predicates that describe a meaningful application condition. Dispose subscriptions that are no longer needed.

Treat published figures as workload measurements

Performance depends on:

  • hardware;
  • runtime version;
  • build configuration;
  • model shape;
  • property types;
  • result size;
  • query selectivity;
  • allocation requested by the caller.

Use the included performance lab to test the release and model shape you plan to deploy.


Troubleshooting

Hydration returns more data than expected

Apply every filter, ordering rule, and limit before calling Hydrate().

Fleet[] fleets = db.From<Fleet>()
    .Where(x => x.Region == "east")
    .OrderBy(x => x.Name)
    .Take(20)
    .Hydrate();

Hydrate() constructs complete configured graphs for the current query result. It should normally be the final operation after the result has been narrowed.

A query is slower than expected

Check whether the application is:

  • returning complete objects when a projection would be enough;
  • requesting a large result list;
  • sorting more rows than needed;
  • calling custom methods that cannot be translated;
  • running under a debugger;
  • using a Debug build;
  • measuring result allocation rather than query execution.

A direct save cannot find the object

Use the tracked object returned by the same MemoryStore instance.

Tire tire = db.Query<Tire>().First(...);
tire.Pressure = 34;
db.Save(tire);

Do not construct a replacement object and expect Memory2 to infer its stored location.

A subscription does not fire

Confirm that:

  • the subscription was created before the change;
  • the returned subscription handle is still alive;
  • the changed object was saved or upserted;
  • the predicate actually changed truth state;
  • the subscription has not been disposed.

Synchronization reports a replay

The same source and sequence was already accepted. This is expected during retry-safe delivery.

Synchronization reports a gap

An earlier sequence is missing. Send the missing change before the later one.

Synchronization reports a conflict

The stored mutation version no longer matches the expected version. Reload or reconcile the newer state instead of overwriting it blindly.

Verification fails

Do not continue using the file as though it were valid. Preserve the file for diagnosis, restore a known-good backup, and inspect the verification issues returned by Memory2.


High-level API map

Store lifecycle

MemoryStore.CreateInMemory()
MemoryStore.Open(path)
MemoryStore.Open(stream, leaveOpen)
db.Save()
db.Verify()

Typed sets

db.Set<T>()
set.Create(model)
set.GetRequired(id)
set.Upsert(model)
set.Delete(id)
set.Subscribe(predicate, handler)

Queries

db.Query<T>()
db.Query<TResult>()
query.Where(...)
query.Select(...)
query.OrderBy(...)
query.OrderByDescending(...)
query.GroupBy(...)
query.Join(...)
query.Any(...)
query.Count(...)
query.LongCount(...)
query.First(...)
query.Last(...)
query.Sum(...)
query.Min(...)
query.Max(...)
query.ToList()

Multi-source queries

query.Include<TSource>(branch => ...)
branch.WhereExists<TRelated, TKey>(...)
branch.WhereNotExists<TRelated, TKey>(...)

Connected objects

db.Query<TInner>()
db.Parent<TParent>(inner)
db.Root<TRoot>(inner)
db.Save(inner)

Synchronization

new MemoryChangeSet()
    .Upsert(source, sequence, model, expectedMutationVersion)

db.Apply(changeSet)

Complete starter example

using Memory2;

using var db = MemoryStore.Open("app.m2");

var accounts = db.Set<Account>();

Account account = accounts.Create(new Account
{
    Name = "Ada",
    Balance = 250m,
    Active = true
});

using var subscription = accounts.Subscribe(
    x => x.Active && x.Balance < 0m,
    change => Console.WriteLine(
        $"Account warning: {change.Current?.Name ?? change.Previous?.Name}"));

account.Balance -= 300m;
accounts.Upsert(account);

bool hasWarnings = db.Query<Account>()
    .Any(x => x.Active && x.Balance < 0m);

Console.WriteLine($"Warnings: {hasWarnings}");

db.Save();

VerificationResult verification = db.Verify();
Console.WriteLine($"Verified: {verification.IsValid}");

Memory2 keeps the state live, queryable, reactive, synchronized, and durable through one high-level C# API.

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 was computed.  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 netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  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.
  • .NETStandard 2.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
1.0.0 0 7/26/2026