Day 19: The Scope of the Customization Layer and Overlay Granularity
DEV Community

Day 19: The Scope of the Customization Layer and Overlay Granularity

An ERP installed and used as it comes is close to nonexistent. The same set of definitions goes out to three companies, and one of them wants "customer" to read "dealer", the second wants three fewer fields on the order screen, and the third wants one more credit limit check before a save.

The three differ by an order of magnitude in weight, but as long as all of them are expressed as code they travel the same delivery path. In the comparison in Why ERP Needs a Definition-Driven Framework, what customization looks like under a definition-driven approach is a layer of data laid over the standard definitions. That layer hangs on the company: whichever company the user enters decides which customization gets laid on top.

This post covers:

  • Which definitions can be customized, and which two never can
  • The four overlay granularities, and why each one is what it is
  • Both layers read only, with combining happening only at the moment of lookup
  • Where the customization id comes from, and which end the overlay happens on

1. Which definitions can be customized

The customization id is a field on the company record, and it travels with whichever company the user entered. Multi-tenancy and a group with several companies are therefore both covered, which is a consequence of putting the layer on the company rather than the point of it.

Companies to ids is many to one: several companies in a group sharing one customization all point at the same id, and ones that want different business logic each point at their own.

Customization has two faces: making a program the package already has behave differently, and a customer wanting a program the package does not have. This layer answers the first, and the second does not travel through it.

The framework's approach is to put the differences in a separate root directory. One deployment has one set of package definitions under DefinePath, and every customization id has its own folder under CustomizePath, named after that id, holding only the parts that differ.

{DefinePath}/ <- package definitions (extract)
  FormSchema/TableSchema/FormLayout/Language/
  ProgramSettings.xml
  MenuSettings.xml
  PluginSettings.xml

{CustomizePath}/{customizeId}/ <- this id's differences, only these five
  FormLayout/Language/
  ProgramSettings.xml
  MenuSettings.xml
  PluginSettings.xml

The Purpose and Classification of Definition Files counted thirteen kinds of definition in the framework, and only five of them get into the customization layer.

What each of the five can change:

  • Language - Field captions, form names, message text, and the text of an option set. Overlay granularity: Text is per key, an option set is whole file replacement. Takes an assembly? No
  • FormLayout - Which fields appear on the screen and how they are arranged. Overlay granularity: Whole file replacement. Takes an assembly? No
  • MenuSettings - How the menu is grouped and which programs are in it. Overlay granularity: Whole file replacement. Takes an assembly? No
  • ProgramSettings - A program's behavior as a whole, and how it reads and writes its own data. Overlay granularity: Per ProgId, and per property below that. Takes an assembly? Yes
  • PluginSettings - One more step on an existing save or delete flow. Overlay granularity: Per ProgId, added together. Takes an assembly? Yes

That last column is where the real line runs. Language, FormLayout and MenuSettings change the definitions themselves, and delivering them means putting a few XML files into that id's folder. The content of ProgramSettings and PluginSettings is type names, and for a type to load, the assembly has to reach the host's bin. The first is swapping out a piece of data, the second is still a deployment.

Use the light one wherever a light one does. Changing a caption lands on language data, changing how a screen is arranged lands on a layout file, one more check before a save lands on a plugin, and replacing a program's behavior as a whole is where a custom business object's turn comes. That same check can be reached by overriding in a custom business object too, at the cost of one more class to maintain across every framework upgrade.

The two that cannot be customized, and the limit that follows

FormSchema and TableSchema are permanently excluded. FormSchema drives the database structure and the validation rules as well as the screen, and letting it diverge per customization would split the physical schema apart.

The mechanism for excluding them is not a blacklist, it is that three separate places have no route there.

  • The customization layer's read interface has no method for getting a FormSchema at all.
  • The read only storage throws NotSupportedException straight from those two getters.
  • The customized path class does not override those two paths either, so somebody who got past the first two still could not compute a file name pointing into a customization folder.

That ruling implies a limit which has to be planned for up front: a customization cannot add a field. What a customization can have is a different caption on an existing field, a screen that hides that field, a business object that treats it differently, or a plugin that fills it in. When one company genuinely needs data of its own, that is a schema change for everyone, and the places with no use for it leave the field empty.

The same line takes the rules with it. The declarative defaults, computed fields and validation rules from Declarative Business Rules and Computed Fields live in FormSchema, so those rules are the same for every company, and making a judgment differ has to go through a plugin or a custom business object.

2. The four overlay granularities, and why each one is what it is

Put two definitions of the same type side by side and combining them is not a well defined action. The same question gets completely different answers on a layout and on a language resource, so granularity is not a parameter left to the caller, it is fixed into the overlay method itself:

  • Whole file replacement - Used on FormLayout, MenuSettings, the option sets in a language resource. What a lookup means: If the customization has one, that one is used entire and the package's is not looked at at all
  • Per key - Used on Language text. What a lookup means: A key the customization declares takes the customization's value, and every other key comes from the package
  • Per ProgId, and per property below that - Used on Custom business objects, custom repositories. What a lookup means: The customization overrides only the bindings it names, and the ones it leaves blank stay with the package
  • Per ProgId, added together - Used on Business plugins. What a lookup means: The package's run first and the customization's follow

Whole file replacement is used on things with no intuitive answer for combining. A layout is one arrangement, and a partial merge cannot answer whether the fields under a block that moved away go with it. An option set only means anything looked at as a set, and merging it entry by entry makes both the order and the meaning of an entry nobody listed go vague at once. Changing one set of options means listing every entry that set should have.

Per key is used on things unrelated to one another. One field caption has no relationship at all to another, so a customization file holding only the few keys that change is enough: every other caption still comes from the package resource, and translations the package adds later come through on their own.

Per property exists because of a loss that would never get reported. One program entry in ProgramSettings carries two independent bindings, one naming a business object and one naming a repository. If the overlay replaced the whole entry, a customization that only wanted to swap the business object would clear the repository the package had bound along with it, and an empty string here is a legitimate instruction to use the framework default rather than an error, so the framework throws nothing and logs no warning. The symptom is that program quietly running on a different set of data access behavior.

<!-- Customize/acme/ProgramSettings.xml -->
<ProgramSettings>
  <Items>
    <ProgramItem ProgId="Order" BusinessObject="Acme.Erp.OrderBO, Acme.Erp" />
  </Items>
</ProgramSettings>

The repository and the display name from the package's entry are still in effect. Deliberately sending a binding back to the framework's own type is done by naming that type explicitly, not by leaving the attribute blank.

Adding them together is the only granularity that makes no choice, and it is used only on plugins. A binding and a step are not the same kind of thing. A binding says this program is this type, there can only be one, and a customization that wants a different one has to push the package's out. A plugin is one more step in a flow, and the two layers each adding their own does not conflict. The cost is that a customization cannot stop one of the package's plugins, and the framework deliberately gives no syntax for removing one. Taking that step out means inheriting the business object and overriding it. Which points a plugin can hook into, and what each of those points has to hand, is Business Logic Customization and the Four Plugin Stages.

Whole file replacement runs both ways

Customize a layout and that customization owns it completely. A field the package adds to FormSchema later will not be seen by any company pointing at that id, and the framework neither merges nor warns. This is the design's intent rather than a gap.

The FormLayout route in FormLayout and Screen Generation for Multiple Front Ends is the one saying which fields are not shown and which columns the detail displays, and that file has the last word on all of it. Arranging a layout in the customization layer is working on that file, and one more field in FormSchema does not mean those companies should now display it. Making it appear on those screens is a decision, and it is carried out by editing that customized layout file. Menus work the same way.

3. Both layers read only, with combining happening only at the moment of lookup

The most intuitive implementation is to combine the two layers into one complete definition at startup, cache that, and let the whole system read a single layer from then on. What blocks that route is the premise from Cross-Node Invalidation for Definition and Data Caches: what a cache hands out is a shared instance, not a copy.

There is one copy of the package layer for the whole process, and one copy of a customization layer shared by every session pointing at that id, so touching either one is touching every company currently reading it. Combining before caching can only be written two ways: writing the customization's content back into the package's copy, or storing a separately combined copy. The first drops one company's customization onto another company's head, and the second is one more cache to look after, needing to be invalidated whenever either of the two layers changes.

So the two layers are cached separately and are each read only, and the overlay happens at the moment that definition is read. Isolating the customization layer uses a mechanism that was already there: one customization id gets one cache container, and the storage behind that container only recognizes files under {CustomizePath}/{customizeId}/. With no such file it returns null, it does not fall back to the package layer, and it does not mix the package's content in. This id has no customization and this id customized something to empty therefore stay apart the whole way.

Three of the four granularities combine nothing. Whole file replacement and per key pick one of two values that already exist, and adding them together produces a list of type names with both layers' definition objects untouched. The only one that combines the two layers' content into a new object is per property, from CustomizeOverlay.cs:

if (customizeItem == null)
{
    return baseItem;
}
if (baseItem == null)
{
    return customizeItem;
}
// WARNING: every ProgramItem property must be merged here. One that is forgotten falls
// back to whole-entry replacement for that property alone.
return new ProgramItem(progId, Prefer(customizeItem.DisplayName, baseItem.DisplayName))
{
    BusinessObject = Prefer(customizeItem.BusinessObject, baseItem.BusinessObject),
    Repository = Prefer(customizeItem.Repository, baseItem.Repository),
};

With only one layer declaring that ProgId, that layer's own instance comes back directly and there is nothing to combine. With both declaring it, a new one is built, and neither layer's cached instance is touched.

4. Where the customization id comes from, and which end the overlay happens on

The run time half was established in Session Management and Lifecycle: the customization id is one of the six values written into the session by the step that enters a company, and it comes and goes with that company. Two things to add here.

The session is the only source. Every place on the server that needs a customization asks the session for itself, and the customization id is deliberately absent from the API's parameters (from BusinessObject.cs):

protected string GetCurrentCustomizeId()
{
    if (AccessToken == Guid.Empty)
        return string.Empty;
    return SessionInfoService.Get(AccessToken)?.CustomizeId ?? string.Empty;
}

The reason is in the value itself. It selects which id's customization files to read, so accepting it from the caller is the same as letting anybody read any company's customization.

An empty id short circuits the whole thing. Not logged in yet, not in a company yet, or a deployment with no customization at all, and the first step goes straight to the package layer without asking the customization layer or probing any file, which is the same route taken when the feature does not exist.

The same rules, run once at each end

The overlay rules are pulled out into a pure decision component with no storage, no cache, no session and no DI dependency, and the server and the client run the same piece of code.

package layer cache (one per process, read only) ────┐
                                                     ├─→ overlay, picking or adding by granularity ─→ effective value
customization layer cache (one per id,
Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.