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.
Feature by feature
| Feature | MasterRecord | Entity Framework Core |
|---|---|---|
| Language | JavaScript (ESM) | C# / .NET |
| Context | class AppContext extends context | class AppContext : DbContext |
| DbSet registration | this.dbset(User) | DbSet<User> Users |
| Context lifetime | One per request (master.addScoped) or per unit of work; context pooling + reset() | Scoped DbContext; AddDbContextPool |
| Change tracking / unit of work | saveChanges() commits only this context's change set; dirty index, O(changes) saves | SaveChanges(); ChangeTracker |
| No-tracking queries | asNoTracking(), asTracking(), context-level tracking behaviour | AsNoTracking(), QueryTrackingBehavior |
| Find / Entry | find(id) identity-map first; entry(e).state / getDatabaseValues / reference / collection | Find(); Entry(e) |
| Eager loading | include('p => p.tags').thenInclude('t => t.category'), asSplitQuery() | Include().ThenInclude(), AsSplitQuery() |
| Explicit / lazy loading | entry(e).collection('x').load(); await post.comments (lazy) | Entry(e).Collection().Load(); lazy-loading proxies |
| Relationships | belongsTo / hasOne / hasMany / hasManyThrough / manyToMany (skip navigations, synthesized join entity), FK constraints in DDL | HasOne/HasMany/WithMany, skip navigations, FKs |
| Inheritance | TPH — dbset(Cat, { extends: Animal }) with discriminator | TPH, TPT, TPC |
| Composite keys | Two or more .primary() columns; find(a, b) | HasKey(a, b) |
| Owned / complex types | db.owned(Address) stored as JSON, hydrated on read, nested change detection | OwnsOne().ToJson(), ComplexProperty |
| Concurrency | .rowVersion() / .concurrencyToken(); explicit transactions | IsRowVersion / IsConcurrencyToken; transactions |
| Global query filters | Named filters, apply inside include(), ignoreQueryFilters() | HasQueryFilter, IgnoreQueryFilters |
| Events / interceptors | savingChanges / savedChanges / saveChangesFailed, tracker events, command interceptors | SavingChanges events, IDbCommandInterceptor |
| Set-based operations | executeUpdate / executeDelete, bulk insert | ExecuteUpdate / ExecuteDelete |
| Aggregates & grouping | count / any / min / max / sum / avg; groupBy().aggregate({ … }, { having, orderBy }) | LINQ aggregates; GroupBy + Select |
| Query language | Lambda strings / arrow functions with $$ params, where/and/orderBy/thenBy/skip/take/distinct | LINQ over IQueryable |
| Raw SQL | ctx.query() / ctx.execute(), parameterized | FromSqlRaw / ExecuteSqlRaw |
| Logging & resilience | Pluggable logging with parameter redaction; retry on transient failures | LogTo / EnableSensitiveDataLogging; EnableRetryOnFailure |
| Migrations | add-migration / update-database / migrations-status / script / remove-migration — atomic per migration; master db wrapper | dotnet ef migrations add / database update / list / script / remove |
| DDL modeling | defaultSql, computed, check constraints, unique / partial indexes, FK constraints | HasDefaultValueSql, HasComputedColumnSql, HasCheckConstraint, indexes |
| Health | ctx.canConnect() / ctx.healthCheck() | Database.CanConnect() |
| Active Record style | entity.save() / entity.delete() (optional) | |
| Databases | SQLite, MySQL, PostgreSQL | SQL Server, SQLite, PostgreSQL, MySQL, Cosmos, … |
| LINQ / IQueryable composition | ||
| TPT / TPC inheritance | ||
| Compiled queries | ||
| join() / leftJoin() in the builder | By design: include()/thenInclude() or ctx.query() | LINQ Join |
The same unit of work, side by side
// 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// 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 updateWhat 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 wayIQueryableallows.groupBy().aggregate()covers grouping; anything else isctx.query(). - No
join()/leftJoin()in the builder — by design. Relationship loading isinclude()/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.CompileQueryequivalent. Value conversion is done with field transformers rather than aHasConversionmodel 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.
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.