Which Generation of Front-End Tech Should an ERP Bet On?
I bet the core on the backend and on definitions, and treat the front end as a swappable skin.
Which Generation of Front-End Tech Should an ERP Bet On?
I bet the core on the backend and on definitions, and treat the front end as a swappable skin.

After years of building ERP systems, one thing couldn’t be clearer: the real core asset is the backend logic. Business rules, workflows, and data structures have lifespans of one to two decades. Front-end technology, by contrast, turns over fast, with a new fashionable choice every few years. Tie your investment to one generation of front-end tech and the system becomes painful to evolve down the road.
Fortunately, ERP screens are fairly “formulaic.” Lists, forms, master-detail documents, queries, the layouts come and go but it’s always more or less the same handful. Since they’re this regular, I turned them into definitions and made a single definition the single source of truth for the whole system. This is the core approach of my framework, Bee.NET . It’s also an approach I’ve used in the industry for many years, even though mainstream frameworks rarely go this way, which is what I want to share here.
To validate the approach, I built a Northwind sample (a small inventory/sales app). If you just want the highlights first, these are the things that land hardest:
- One definition generates the front-end screen, the backend logic, and the database table structure together.
- The demo app has nine forms, and eight of them are zero-code; only the order form has business rules, so it got a single business object.
- You won’t find a single line of SQL in the whole project; the framework composes all data-access SQL from the definitions.
- The same definition runs against different databases; it supports SQL Server / PostgreSQL / MySQL / Oracle / SQLite.
- The front-end controls are native controls subclassed and rewritten, deeply integrated with the definitions, so developers barely touch UI details.
- To switch UI technology, all you swap is the “render-from-definition” skin; the backend and the definitions stay put.
- Definitions are structured, so AI reads them easily and can generate definition files to spec (the definitions in this Northwind sample were produced by AI).
Below I’ll walk through how each of these came about.
1️⃣ One definition, meant to grow on many front ends
The most labor-saving (and most maintainable) part of definition-driven design is that “the same FormSchema can feed different front ends.” In the samples folder I’ve gradually put up several minimal runnable examples, wiring every kind of front end to the same backend:
- Console: a pure API client, validating JSON-RPC calls.
- Blazor Server: components on the server, dispatched in-process (no HTTP hop).
- Blazor WebAssembly: the same components moved into the browser, over HTTP.
- MAUI: a native mobile app rendering the same schema.
- Avalonia: desktop (Windows / macOS / Linux).
- Plain JavaScript: a client with no .NET at all, calling JSON-RPC directly.
There are two connection strategies: Local (in-process, best performance) and Remote (HTTP). For the question of “can it connect at all,” these samples already give the answer: yes.
But all these samples prove the same one thing: it connects. What I actually care about is the part after it connects.
2️⃣ After it “connects,” the control still doesn’t understand the definition
Putting a TextBox on screen and binding it to a field’s value, anyone can do that. But a definition holds far more than just “the value”:
• What’s the maximum length of this field (MaxLength)?
• Is this a dropdown, and what are the options (ListItems)?
• Should this field be read-only right now (ReadOnly)?
• Is this field a foreign key that “relates to another table,” one that should open a picker to choose a row and bring back both the code and the name?
Binding the value alone is nowhere near enough. What I want is for the control to read this metadata itself and change its own behavior. That leaves just one question: where does this “understanding” belong?
3️⃣ For full integration with definitions, the control has to be subclassed and rewritten
My goal is clear: let front-end developers barely touch UI details, and keep all their energy for the business logic that truly matters. To do that, the control has to be “fully integrated” with the definition. And after trying everything, the only path that achieves full integration is to subclass and rewrite the native controls.
The common approach is to use the native control as-is and then bolt the definition on from outside with binding or behaviors. The control stays clean, but the definition is on one side and the control on the other, with a layer of glue forever in between. Every added field has to be re-wired on the outside, and the front-end developer still has a pile of UI plumbing to manage.
Subclassing and rewriting doesn’t have this problem. I take the native control and subclass it into a child that understands definitions by birth:
public class TextEdit : TextBox, IFieldEditor
{
// Bind to a field of a data object; automatically apply that field's
// definition metadata such as MaxLength.
}
The caller drops in a TextEdit, it reads the matching field’s definition on its own, and nothing has to be wired up outside. For the front-end developer, the UI layer is almost transparent: drop it in and it works, and all the attention goes to the business logic.
The cost is that you have to be able to modify the native control, which means first getting familiar with its internals; themes, styles, and editing behavior all have their quirks, so it takes extra effort at the framework layer. But in return, the application side barely has to deal with any of it.
Going one level deeper, I gathered the shared binding state (which data object, which field, how the value writes back, how to block the echo loop) into one component, plus a handy design: the container sets the data source once, and every field-named editor underneath wires itself up automatically, so it works the moment you place it, with not a single manual hookup.
4️⃣ From one field, to a grid, to a whole form
The same principle, “subclass native controls and make them understand definitions,” applied layer by layer upward, grows into a whole form.
At the bottom are the various field editors: text, multi-line, date, year-month, dropdown, checkbox, and the button editor for relational fields. Each one is a subclass of some native control, each reading the definition of its own field.
One level up is the grid control, assembling field editors into an editable detail table; which columns to show and which are editable are likewise decided by the definition.
At the top is the view layer. I split it into two containers, matching the two most common ERP screens: one for list browsing, one for single-record viewing and editing (a master area plus a detail grid). This split follows the user’s habits, not a technical constraint.
What best embodies this principle is the relational field. The definition states, in one line, “this field relates to which table, and which columns to bring back,” and the layout generator automatically turns it into a button that opens a picker; once you pick, the foreign key and the display text are written back together, and on reload the backend recomputes them via a JOIN. The caller writes not one line of UI. The labor-saving of definition-driven design is most obvious right here.
5️⃣ Why Avalonia as the pilot
For everything above (subclassing controls, splitting out a view layer), I didn’t roll it out to every front end at once; instead I picked one to pilot, and that was Avalonia. Why it?
Because Avalonia is itself cross-platform: the same UI codebase, each paired with a thin platform startup project, can run on the desktop (Windows / macOS / Linux), the Web (WebAssembly), and as a mobile app. Build the deeply integrated controls on it, and this same set of controls has a path to reach the Web and mobile later, not just serve the desktop. Polish once, benefit in many places, which is something a front end tied to a single platform can’t offer.
For now I’ve implemented these front-end controls with the desktop program first, and the Web and mobile versions will come later. As a result, this “subclassed controls + view layer” deep integration is currently complete only on Avalonia; the other front ends (such as MAUI and Blazor) are still at the simpler dynamic-form rendering stage, and porting waits until this side is finalized.
6️⃣ The Northwind sample: eight zero-code forms, not a single line of SQL
To keep this from being just a “control showcase,” I built a more complete demo app, bee-northwind-avalonia (https://github.com/jeff377/bee-northwind-avalonia), using the Northwind inventory/sales case everyone knows.

It’s exactly the framework’s core point: of these nine forms, eight are produced purely from definitions, with not one line of UI or CRUD code written. To add a master record, a document with multiple lookups, or a master-detail set, all a developer does is write a few definition files: describe the fields and relations with a FormSchema, pair it with the layout and table definitions, save and restart, and even the database tables are built automatically from the definitions, yielding a complete form that can create / query / update / delete and open pickers for related data.
There’s one more thing readers will probably feel even more: scour the whole project and you won’t find a single line of SQL. Creating tables, querying, inserts/updates/deletes, all the SQL underneath is composed by the framework from the definitions on the fly, and the developer never hand-writes any of it.
And since the SQL is composed by the framework in each database’s dialect, the same definition runs against different databases: it currently supports SQL Server, PostgreSQL, MySQL, Oracle, and SQLite, and switching is just a config change. For convenience, the demo uses the install-free SQLite.
The only place that got code is the order form: how the order number is generated, whether a status transition is allowed, how amounts are calculated, these are the real business logic, so I wrote a single business object, and left everything else to definitions. This is exactly the division of labor I want: a developer’s time should go to business logic, not to wiring one field after another onto the screen.
It references only published NuGet packages, so you can clone it and run it, which serves as a checkup on how far this approach currently goes.
7️⃣ Another against-the-grain choice: DataSet
This approach has one more even-less-mainstream aspect. Those editors bind not to strongly-typed DTOs but to a DataSet, so they all read values “by field name” rather than binding to a strongly-typed property.
Today the mainstream almost always uses strongly-typed DTOs, and an old thing like DataSet has long been rarely mentioned. But under definition-driven design, it actually fits better: the shape of the data is, by nature, grown at runtime from the definition, so a loose container accessed by field name is far less work than maintaining a heap of DTO classes that have to stay in sync with the definition. Change one field in the definition and you don’t have to go back and edit a string of corresponding classes.
Of course it has a cost, and a direct one: there’s no compile-time type checking, so a mistyped field name or a wrong type pulled out won’t get a peep from the compiler. That’s exactly why people left DataSet back in the day.
To plug that hole, I still rely on the definitions. Since everything grows from the definitions, I use them as the basis for unit tests, verifying behavior against them and pushing coverage up, letting the tests catch the errors the compiler can’t. Strong typing blocks errors with the compiler; I block them with definitions plus tests, just moving the safety net from compile time to test time. For this definition-driven scenario, DataSet is actually the better fit.
8️⃣ Definition files are AI-friendly
AI has a high capacity for understanding structured data, and definition files happen to be structured, which makes them especially easy for an LLM to read. Pair that with a skill describing how these definition files relate to one another (how the FormSchema, the layout, and the table definitions correspond, and which fields must stay consistent) and the AI’s understanding gets even sharper, making it easier to generate a whole set of correct definition files to spec. Adding a form can then shift from “a person writes the definition” to “describe the requirement and let the AI produce the definition.” That Northwind demo above is a ready example: its FormSchema, layout, and table definitions were all produced by AI from my descriptions of the requirements.
By comparison, for a large and complex system like an ERP, if the logic is scattered all over the code, the AI struggles to change it correctly; whereas definitions that are centralized and structured turn out to fit this need perfectly.
✅ Conclusion
In breadth, the same definition already connects to all kinds of front ends; in depth, I first got this “subclass controls, split out a view layer” approach up and running on Avalonia, enough to assemble into a working application.
Subclassing controls isn’t free: to modify a native control you have to understand its internals, which carries real difficulty. For a small project it may not pay off; if you only wire up a field or two now and then, the common approach of using a behavior is actually less work. But ERP is different. Its maintenance window easily runs over ten years, and this kind of upfront investment, amortized over such a long lifespan, becomes very worthwhile: spend the effort once at the framework layer, collapse the front end into a skin generated from definitions, and what you get back is years of an easier life on the application side. This is a choice that grew out of my own system. It won’t necessarily suit your project, so weigh it for yourself.
Further reading
- Bee.NET samples:minimal runnable examples for each front end(Console / Blazor / MAUI / Avalonia / JS)
- bee-northwind-avalonia: a more complete desktop demo built on the Northwind case
📘 Original HackMD Article 👉 https://hackmd.io/@jeff377/schema-driven-ui
📢 You’re welcome to share — just credit the source. 📬 Follow for more dev notes: Facebook | HackMD | GitHub | NuGet
메타데이터
- post_id
- ccdfc1c7b7cc
- slug
- which-generation-of-front-end-tech-should-an-erp-bet-on-ccdfc1c7b7cc
- url
- https://medium.com/@jeff377/which-generation-of-front-end-tech-should-an-erp-bet-on-ccdfc1c7b7cc
- canonical_url
- https://medium.com/@jeff377/which-generation-of-front-end-tech-should-an-erp-bet-on-ccdfc1c7b7cc
- author_url
- https://medium.com/@jeff377
- status
- ok
- fetched_at
- 2026-06-20 20:29:01