"My thinking was: if I first use raw SQL, I might build a better mental model of what's actually happening, instead of relying on abstractions I don't understand."
This Reddit comment sparked another round of the eternal ORM debate: raw SQL vs Entity Framework vs Dapper vs stored procedures.
For internal tools, Ivy sidesteps this debate entirely. You get direct EF Core access in your UI layer. No repository patterns. No unit of work abstractions. Just queries.
public class EmployeeDashboard : ViewBase
{
public override object? Build()
{
var db = UseService<AppDbContext>();
return db.Employees
.Where(e => e.Department == "Engineering")
.OrderBy(e => e.HireDate)
.ToDataTable(e => e.Id)
.Header(e => e.Name, "Name")
.Header(e => e.Email, "Email");
}
}
This is the entire data layer for this view. EF Core query, rendered as a table.
Why the ORM Debate Assumes Layers You Don't Need
The traditional ORM debate assumes layers:
- UI layer
- Service layer
- Repository layer
- Data layer
Each layer adds abstraction. Each abstraction has opinions. The debate is about which abstraction to use and where.
Ivy's Position: Skip the Layers for Internal Tools
For internal tools, those layers solve problems you don't have:
| Problem | Solution | Do You Need It? |
|---|---|---|
| Multiple UI clients | API layer | No — one internal app |
| Domain logic reuse | Service layer | Maybe — often overkill |
| Database swapping | Repository pattern | No — you have one database |
| Unit testing isolation | Interfaces everywhere | Debatable for internal tools |
Ivy assumes you're building one internal tool that talks to one database. The simplest architecture is: UI queries database directly.
What About Business Logic?
Put it in services, not repositories:
public class ExpenseApproval : ViewBase
{
public override object? Build()
{
var db = UseService<AppDbContext>();
var approver = UseService<ExpenseApprovalService>();
return db.PendingExpenses
.ToDataTable(e => e.Id)
.RowActions(new MenuItem("Approve"))
.HandleRowAction(async (evt) => {
var expense = await db.PendingExpenses.FindAsync(evt.Args.Id);
await approver.ApproveAsync(expense);
});
}
}
The ExpenseApprovalService can have whatever logic you need. But it's a service that does things, not a repository that just fetches data.
Building a Better Mental Model with Direct EF Core Access
The Reddit poster wanted a "better mental model" by starting with raw SQL. Valid point — and Ivy's model is similarly transparent:
- You have a
DbContextwith your tables - You write LINQ queries
- LINQ translates to SQL
- Results render directly in your UI
No N+1 queries to debug (server-side rendering handles pagination). No lazy loading surprises (you control what loads). No repository interfaces to navigate.
When Direct Database Access Isn't the Right Fit
Ivy's direct database access works best when:
- You're building internal tools, not public APIs
- You have one primary database
- Your team knows EF Core basics
- Sub-millisecond query performance isn't the priority
If those conditions don't hold, the ORM debate matters. For internal dashboards, reporting tools, and admin panels? Just query your data.
