Comparison

MasterRecord vs Entity Framework

Entity Framework Core pioneered the change-tracked DbContext + code-first migrations pattern in .NET. MasterRecord brings the same context-and-saveChanges workflow to JavaScript — and releases 1.6 through 1.22 closed most of the remaining gap: concurrency tokens, FK constraints, set-based updates, query filters and interceptors, logging and retries, a dotnet ef-style migrations CLI, EF-style navigation loading, many-to-many skip navigations,thenInclude/split queries, TPH inheritance, composite keys, groupBy aggregates and owned JSON types.

At a glance

Feature by feature

FeatureMasterRecordEntity Framework Core
LanguageJavaScript (ESM)C# / .NET
Contextclass AppContext extends contextclass AppContext : DbContext
DbSet registrationthis.dbset(User)DbSet<User> Users
Context lifetimeOne per request (master.addScoped) or per unit of work; context pooling + reset()Scoped DbContext; AddDbContextPool
Change tracking / unit of worksaveChanges() commits only this context's change set; dirty index, O(changes) savesSaveChanges(); ChangeTracker
No-tracking queriesasNoTracking(), asTracking(), context-level tracking behaviourAsNoTracking(), QueryTrackingBehavior
Find / Entryfind(id) identity-map first; entry(e).state / getDatabaseValues / reference / collectionFind(); Entry(e)
Eager loadinginclude('p => p.tags').thenInclude('t => t.category'), asSplitQuery()Include().ThenInclude(), AsSplitQuery()
Explicit / lazy loadingentry(e).collection('x').load(); await post.comments (lazy)Entry(e).Collection().Load(); lazy-loading proxies
RelationshipsbelongsTo / hasOne / hasMany / hasManyThrough / manyToMany (skip navigations, synthesized join entity), FK constraints in DDLHasOne/HasMany/WithMany, skip navigations, FKs
InheritanceTPH — dbset(Cat, { extends: Animal }) with discriminatorTPH, TPT, TPC
Composite keysTwo or more .primary() columns; find(a, b)HasKey(a, b)
Owned / complex typesdb.owned(Address) stored as JSON, hydrated on read, nested change detectionOwnsOne().ToJson(), ComplexProperty
Concurrency.rowVersion() / .concurrencyToken(); explicit transactionsIsRowVersion / IsConcurrencyToken; transactions
Global query filtersNamed filters, apply inside include(), ignoreQueryFilters()HasQueryFilter, IgnoreQueryFilters
Events / interceptorssavingChanges / savedChanges / saveChangesFailed, tracker events, command interceptorsSavingChanges events, IDbCommandInterceptor
Set-based operationsexecuteUpdate / executeDelete, bulk insertExecuteUpdate / ExecuteDelete
Aggregates & groupingcount / any / min / max / sum / avg; groupBy().aggregate({ … }, { having, orderBy })LINQ aggregates; GroupBy + Select
Query languageLambda strings / arrow functions with $$ params, where/and/orderBy/thenBy/skip/take/distinctLINQ over IQueryable
Raw SQLctx.query() / ctx.execute(), parameterizedFromSqlRaw / ExecuteSqlRaw
Logging & resiliencePluggable logging with parameter redaction; retry on transient failuresLogTo / EnableSensitiveDataLogging; EnableRetryOnFailure
Migrationsadd-migration / update-database / migrations-status / script / remove-migration — atomic per migration; master db wrapperdotnet ef migrations add / database update / list / script / remove
DDL modelingdefaultSql, computed, check constraints, unique / partial indexes, FK constraintsHasDefaultValueSql, HasComputedColumnSql, HasCheckConstraint, indexes
Healthctx.canConnect() / ctx.healthCheck()Database.CanConnect()
Active Record styleentity.save() / entity.delete() (optional)
DatabasesSQLite, MySQL, PostgreSQLSQL Server, SQLite, PostgreSQL, MySQL, Cosmos, …
LINQ / IQueryable composition
TPT / TPC inheritance
Compiled queries
join() / leftJoin() in the builderBy design: include()/thenInclude() or ctx.query()LINQ Join
Show me the code

The same unit of work, side by side

MasterRecord
MasterRecord
// backend/app/models/AppContext.js
import context from 'masterrecord/context';
import Post from './Post.js';
import Tag from './Tag.js';

class AppContext extends context {
  constructor() {
    super();
    this.env('config/environments');
    this.dbset(Post);
    this.dbset(Tag);
  }
}
export default AppContext;

// in a controller (this.db = this request's AppContext)
const posts = await this.db.Post
  .where((p) => p.published == true)
  .include('p => p.tags').thenInclude('t => t.category')
  .orderByDescending((p) => p.id)
  .take(10)
  .toList();

const post = this.db.Post.new();
post.title = 'Hello';
post.tags = [tag, { label: 'new' }];   // link existing + insert new
await this.db.saveChanges();           // one unit of work

// migrations
//   master db new AddPosts && master db migrate
Entity Framework Core
Entity Framework Core
// Data/AppContext.cs
public class AppContext : DbContext
{
    public DbSet<Post> Posts { get; set; }
    public DbSet<Tag> Tags { get; set; }
}

// in a controller (_db = scoped AppContext)
var posts = await _db.Posts
    .Where(p => p.Published)
    .Include(p => p.Tags).ThenInclude(t => t.Category)
    .OrderByDescending(p => p.Id)
    .Take(10)
    .ToListAsync();

var post = new Post { Title = "Hello" };
post.Tags.Add(tag);
post.Tags.Add(new Tag { Label = "new" });
_db.Posts.Add(post);
await _db.SaveChangesAsync();          // one unit of work

// migrations
//   dotnet ef migrations add AddPosts && dotnet ef database update
Honest accounting

What still differs

  • No LINQ, no IQueryable. Queries are built from lambda strings or arrow functions (where((p) => p.id == $$, id)) and a fixed set of builder methods. You cannot compose arbitrary expression trees, project into anonymous types, or hand an unexecuted query to another layer the way IQueryable allows. groupBy().aggregate() covers grouping; anything else is ctx.query().
  • No join()/leftJoin() in the builder — by design. Relationship loading is include()/thenInclude(); hand-written joins use raw SQL.
  • Inheritance is TPH only. Table-per-type and table-per-concrete-type mappings are not implemented.
  • Composite-key limits. Foreign keys to a composite-key entity and manyToMany() owners with composite keys are not supported.
  • No compiled queries. There is no EF.CompileQuery equivalent. Value conversion is done with field transformers rather than a HasConversion model API.
  • Providers. SQLite, MySQL and PostgreSQL only — no SQL Server, Cosmos DB or third-party providers.
  • Typing. Entities are plain classes whose fields are builder methods; there is no compile-time shape checking of queries or results.
The verdict

Which should you choose?

Choose MasterRecordif you want EF’s change-tracking + migrations workflow — scoped contexts, saveChanges() as a unit of work, include/thenInclude, query filters, concurrency tokens, a dotnet ef-style CLI — in the Node ecosystem, with an optional Active-Record style on top.

Choose Entity Framework Core if you are building on .NET, need LINQ and IQueryable composition, TPT/TPC mappings, compiled queries, or providers beyond SQLite, MySQL and PostgreSQL.