Memory2 1.0.0
dotnet add package Memory2 --version 1.0.0
NuGet\Install-Package Memory2 -Version 1.0.0
<PackageReference Include="Memory2" Version="1.0.0" />
<PackageVersion Include="Memory2" Version="1.0.0" />
<PackageReference Include="Memory2" />
paket add Memory2 --version 1.0.0
#r "nuget: Memory2, 1.0.0"
#:package Memory2@1.0.0
#addin nuget:?package=Memory2&version=1.0.0
#tool nuget:?package=Memory2&version=1.0.0
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
- Open a store
- Define models
- Work with sets
- Create data
- Read data
- Hydrate query results
- Query with LINQ
- Build one result from several model types
- Query inner objects directly
- Find the parent or root
- Save an inner object directly
- Update and upsert
- Delete data
- Subscribe to live conditions
- Synchronize changes
- Save, verify, and reopen
- Read versus query
- LINQ behavior
- Common patterns
- Performance guidance
- Troubleshooting
- High-level API map
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:
- Build the query.
- Narrow the result.
- 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;
Selectwhen you need a few values;Any,Count,Sum,Min, orMaxwhen 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:
WhereSelectAnyAllCountandLongCountFirstandLastElementAtContainsDistinctUnionIntersectExcept- ordering and ranked windows
SkipandTakeMinandMaxAggregateGroupByJoinZipSequenceEqualConcat,Append, andPrependDefaultIfEmpty
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 | 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 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. |
-
.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 |