"Definitions are the single source of truth" sounds like there is one file holding all of the truth. Open a project's definition directory and what you actually see is thirteen definition types. Some are a single file at the root, and some are one file per program, scattered through subdirectories.
This post is a map. Every mechanism discussed later uses a few of these, so meeting them here means not having to stop and explain each time. At the end of the fourth section there is a quick reference covering all thirteen, marked with which post expands each one. If a name is unfamiliar later, come back here.
This post covers:
- The three boot files:
SystemSettings,DatabaseSettings,DbCategorySettings - The three files for one form:
FormSchema,TableSchema,FormLayout - Binding and navigation:
ProgramSettings,MenuSettings,PluginSettings - The four shared by every form:
LanguageResource,PermissionModels,CurrencySettings,UnitSettings - Four things all thirteen have in common: storage, caching, immutability, and what happens when one is missing
Each name below links to its source.
1. The three boot files: what this deployment runs on
| Definition | Stored as | What it governs |
|---|---|---|
SystemSettings |
A single file | Where the master key comes from, how the payload is packed and encrypted, and debug mode |
DatabaseSettings |
A single file | Which physical databases this deployment has, and how to connect to each |
DbCategorySettings |
A single file | Which category a table belongs to, and which database carries that category |
These three are the precondition for the system starting at all, and they are read in a fixed order:
SystemSettings (names where the master key is)
-> the master key decrypts the key that protects settings
-> which decrypts the connection passwords in DatabaseSettings
-> only then do the database identifiers in DbCategorySettings resolve to anything
The order is not a convention. It is the only solution the dependencies allow. The reverse path does not exist: before a database can be reached, there is no opportunity to look a key up in a database.
The first link in the chain has to be readable when nothing else has started, so it cannot be encrypted itself. That is why the master key is not stored inside SystemSettings. That file records only where the key is:
<MasterKeySource>
<Type>Environment</Type>
<Value>BEE_MASTER_KEY</Value>
</MasterKeySource>
The framework separates that one startup read accordingly: no service, no cache, straight off the file, because at that moment nothing else exists yet.
Why the database concerns are two files
They answer different questions:
DatabaseSettings |
DbCategorySettings |
|
|---|---|---|
| What it answers | Which databases this deployment has and how to connect | Which category of data lands in which database |
| What it is | A fact about the deployment environment | A fact about the application's structure |
| When it changes | Moving data centers, swapping database servers, repointing a test environment | Adding a table or moving a body of data to a different database |
| How sensitive it is | Holds connection passwords and needs encryption and access control | Holds no secrets, and developers edit it daily |
Splitting them lets each side change on its own, and separates the sensitive from the everyday as a side effect.
How many databases the data actually lands in, and how that relates to the table name prefix, is the subject of Connections, Transactions and Query Conditions in the Data Access Layer.
The definition layer holds database settings without knowing databases exist
A file recording connection strings, filed under the definition layer, looks like a layering violation. It is not, because what it holds is a description, not a connection.
The definition layer package has an extremely short reference list. It does not know a database exists, or HTTP, or a UI. A connection string, as far as it is concerned, is a piece of text with an identifier attached. What actually opens a connection is the data access layer. The definition layer only declares an interface that can hand over DatabaseSettings, and lets the data access layer use it.
2. The three definitions of one form
| Definition | Stored as | What it governs |
|---|---|---|
FormSchema |
One per program | The hub: fields, types, cross table relationships, master and detail, computed fields and validation rules |
TableSchema |
One per table | The physical table's columns, types, lengths, nullability and indexes |
FormLayout |
One per layout | How those fields are arranged on screen |
The three edits for adding a required date to orders in Why ERP Needs a Definition-Driven Framework are these three files.
FormSchema is the hub. The other two each face one direction, one toward the database and one toward the screen.
-
FormSchemadeclares which fields exist, what type each is, which ones point at other forms, which details hang off the master, which values are calculated, and what to check before saving. The framework uses it to assemble CRUD SQL and to decide the shape of data crossing layers. -
TableSchemacares about physical storage rather than business semantics, so lengths, indexes and precision can be tuned separately by whoever knows the database. -
FormLayoutis the only one of the three that concerns the screen alone. How things are arranged is a design time decision, written into a layout definition and followed at run time.
The relationship between the three also fixes the order of edits. FormSchema describes which fields the form has, so a change starts there. TableSchema follows with the physical column. If it needs to be visible, it then goes into FormLayout.
3. Binding and navigation: who handles this program, and where it appears
| Definition | Stored as | What it governs |
|---|---|---|
ProgramSettings |
A single file | Which business object and which repository a program identifier binds to. Left blank, the framework default applies |
MenuSettings |
A single file | The navigation tree's grouping, ordering, captions and visibility, with each item pointing at a program |
PluginSettings |
A single file | Which plugins run at save and delete for each program, executed in declaration order |
-
ProgramSettingsis a registry, one row per program, deciding who handles that program's requests. Leave the two binding columns blank and the framework's default CRUD flow applies. Name a class and that class takes over. When The Northwind Case Study: Eight Forms and a Custom Business Object said that of the eight forms only the order needs code, this registry is what it was reading. -
MenuSettingsis separate from the registry because the two are read by different ends. The registry is read by the server and holds class names from the code. The menu is read by the front end and holds ordering, captions and visibility. On top of that, the same program can legitimately appear at several places in the menu, so a node carries its own identifier rather than using the program identifier as its key. -
PluginSettingsrecords which plugins run before and after save and delete for each program, and in what order. The difference from inheritance and overriding came up in an earlier post: inheritance replaces and there can be only one, while plugins layer and there can be several. The details get their own post later.
4. The ones every form reaches for
| Definition | Stored as | What it governs |
|---|---|---|
LanguageResource |
One set per language, split by form or category | Localized captions and option text |
PermissionModels |
A single file | Which permission models exist, which actions each governs, and how data scope is delimited |
CurrencySettings |
A single file | The decimal places and smallest circulating denomination of each currency |
UnitSettings |
A single file | The display decimal places of each unit of measure |
These four belong to no particular form, and every form reaches for them.
-
LanguageResourceis applied one entry at a time.FormSchemaalready carries field captions; the language resource supplies per language versions. Where one matches it replaces the caption, and where none matches the original stands. A project that does not need multiple languages never has to touch it, and adding it does not force you to translate every entry. -
PermissionModelsregisters what permissions there are to talk about. Which actions require authorization and how data scope is delimited are models. Who has been granted what is data rather than definition, and does not live in this file. A later post takes that apart. - Currency and unit of measure are system level: how many decimal places a currency has, how many a unit displays, consistently across the system. Which currencies a given company may use, and what its cash rounding is, are company level settings that come from the database with the company's data rather than from a definition file. Both levels have their place, and a later post on numeric semantics uses them.
The thirteen at a glance
The four sections above group by purpose. Here all thirteen are laid out together in the same order:
| Definition | What it governs | Where it is expanded |
|---|---|---|
SystemSettings |
Master key source, payload options, debug mode | This post |
DatabaseSettings |
Which databases this deployment has and how each connects | This post |
DbCategorySettings |
Which category of data lands in which database | This post |
FormSchema |
The hub, being fields, types, relationships, master and detail, rules and computed fields | The next post |
TableSchema |
The physical table, being columns, lengths, nullability and indexes | TableSchema and Database Structure Upgrades |
FormLayout |
How this form is arranged on screen | FormLayout and Screen Generation for Multiple Front Ends |
ProgramSettings |
Which business object and repository a program binds to. Server side only | API Contract Layering and the Type Registry |
MenuSettings |
The navigation menu's grouping, ordering, captions and visibility | API Contract Layering and the Type Registry |
PluginSettings |
Which plugins hang off each program, in declaration order | Business Logic Customization and the Four Plugin Stages |
LanguageResource |
Captions and option text, one set per language | Multilingual Customization and Resource Structure |
PermissionModels |
What permissions there are to talk about, being actions and data scope policy | The Three Axes of Permissions: Action, Row and Column |
CurrencySettings |
Decimal places and smallest circulating denomination per currency | Numeric Semantics, Multiple Currencies and Units of Measure |
UnitSettings |
Display decimal places per unit of measure | Numeric Semantics, Multiple Currencies and Units of Measure |
These names recur over the next twenty odd posts, always in their original form. A descriptive phrase cannot identify which one is meant. "The category file" could be DbCategorySettings or it could be PermissionModels, and "the settings file" fits eight of the thirteen.
5. Four things all thirteen have in common
They are all XML, and where they are stored is a separate question
The content is XML. Where it lives can be swapped: a file on disk, or a row in a database table with the XML in a column. The framework reads through one interface, so changing the storage means no layer above has to know.
What gets used at run time is not that XML. The framework reads it and restores it into a definition object, and everything after that is a question asked of the object. The XML is only what it looks like while lying on a disk or in a database.
Once read, they stay in memory
What sits in the cache is those restored objects, one slot per definition, evicted by usage. Reading one resets its timer, and only a definition nobody has touched for a while is dropped.
- The forms in daily use keep their definitions in memory almost permanently
- The one opened twice a year is evicted not long after
- The server does not carry the definitions of forms nobody opens
This is one of the returns on cutting definitions into separate pieces. The unit of caching is the unit of reading, so memory tracks actual usage rather than the total volume of definitions.
Keeping them does create one obligation: what happens when the source changes. Each cached definition watches its own source. A file is watched by modification time, and a database goes through a different notification mechanism. When the source changes, the next use reads a fresh copy. Cross-Node Invalidation for Definition and Data Caches takes this apart.
A definition you were handed must not be modified
Asking the framework for a definition gets you the shared one, not a copy. On the same server, every user gets the same object.
The price is that the object cannot be touched from then on:
- Want a view for one user only, with captions in their language? Copy first, then modify.
- Want to persist a change? Go through the framework's save method, which writes the file and invalidates the cache.
The only thing forbidden is modifying the shared copy in place, and that was always wrong anyway, since what it modifies is everyone's.
Missing comes in two kinds
The framework embeds a set of default definitions, and at run time they take no part at all. The framework reads only the definition directory you point it at. The embedded set is a starting point for a new project, copied in with a tool, once.
Not falling back is deliberate. If a missing FormSchema silently switched to the embedded one, the system would start up fine and then run on a definition you never deployed. "You forgot to deploy that file" would be rewritten into "the system does not behave the way you think," and the second one costs an order of magnitude more to track down.
But missing does not mean only one thing, so all thirteen are sorted into two kinds:
| Kind | Behavior | Which ones |
|---|---|---|
| Missing means misconfigured | Report it immediately with a message naming the file |
FormSchema, TableSchema, ProgramSettings, PermissionModels and the three boot files |
| Missing is a legitimate answer | Return nothing and let the caller take it from there |
FormLayout, LanguageResource where the caption in the structure is used, CurrencySettings and UnitSettings where default decimal places apply, MenuSettings and PluginSettings
|
That classification lives in the code, and it has to be decided by the framework rather than interpreted by each application. Otherwise the same oversight shows up differently in different modules, one reporting an error and one quietly using a default, which is much harder to diagnose than both reporting errors.
6. The case study: copy it in, then add on top
Northwind uses the department and employee forms the framework provides, and the way it does that is the one time action from the previous section: copy the default definitions into the application's own definition directory.
After copying, the application added two fields of its own to employee. Those two lines are in the case study's Employee.FormSchema.xml:
<FormField FieldName="title" Caption="Title" DbType="String" />
<FormField FieldName="hire_date" Caption="Hire Date" DbType="Date" />
Once added, that definition belongs entirely to the application and can be changed however you like. Conversely, fields the framework later adds to its default definition will not appear on their own, and there will be no notice. Whether to follow along is your edit to make.
The framework's system fields cannot be removed. They are what the permission and organization mechanisms recognize.
This is what "definitions are data" looks like from a maintenance point of view. Copying a piece of data out and maintaining it yourself and branching a program and maintaining it yourself cost different things. The first has no merge conflicts and no need to carry trunk fixes across version by version, and the price is that new things do not follow along automatically.
Thirteen files, one truth
The shape of the thirteen is roughly this. Three decide what the system runs on, three describe one form, three decide who executes and where the entrance is, and four are shared data every form reaches for.
These are not thirteen configuration files. They are thirteen aspects of one truth. They point at each other, with DbCategorySettings pointing at DatabaseSettings, a FormSchema pointing at another FormSchema, and ProgramSettings and MenuSettings each pointing at the same program, and they do not collide, because each one answers only the question that belongs to it.
The next post, How FormSchema Drives SQL, is about which tables a query joins and which statements a save issues.
📚 Part of the thirty-post series ERP Architect's Notes: Designing a Definition-Driven Framework
📘 Original article on HackMD, in Traditional Chinese:
👉 https://hackmd.io/@jeff377/day-04-definition-files
📢 You are welcome to republish this, please credit the source.
📬 Follow for more engineering notes:
Facebook | HackMD | GitHub | NuGet
Top comments (0)