RFC: Custom fields v2¶
Problem statement¶
Our strong suite is (mostly about) customized solutions that are based on some common denominator across our multiple clients in the cruise line industry. However, sometimes said customizations cannot be refactored/fitted into a common domain concept since it's just too custom. This could lead to potentially diverging code bases and deliverables based on apps, clients or even departments within the same company.
In order to increase the degree of customization we offer our clients, our Suite 3.0 based services should support user definable custom fields (CF from here onward). Admin level users should be able to pick an entity owned by our applications (i.e. Inspections or Service Orders) and define an arbitrary number of CF, which should then be mostly indistinguishable from "native fields".
Duration¶
Deadline for this RFC is set to the second Scrum of Scrum meeting scheduled after officially opening the RFC.
Current state¶
- AMOS offers some degree of customization with something akin to CF, its UserDef fields (e.g. userDefText1). The amount and type of fields per entity is fixed (i.e. you only get 4 userDefText fields for the Work Order entity).
- Some applications like AIMS provide customizable labels both for application owned custom fields as well as for AMOS owned user def fields (pending validate with RM)
-
MPM has a more generic approach for managing CFs, which was built from the ground-up. Users can pick any entity and create new CFs for it. A Custom Field Definition allows the user to set:
- name
- type: one of string, CLOB, number, date, money, option
- default value
- attributes: one of required, hidden
MPM custom fields are supported server side by adding an interface and performing some ORM level configuration for the entity that should "support" that feature.
Client side, Custom Field Values are sent in a
customFields
property along with the entity payload, the data type being a dictionary keyed by Custom Field ID. They are then displayed based on a set of generic form control fields.Architecturally, they resemble an EAV schema, however there's one table for the definition, and one for the values. Do note no FK are possible for the value tables, due to its custom nature (we would need to dynamically target the FK constraint target table based on the value of another column)
Proposer¶
RAR - Ramiro Adolfo Rivera - Suite 3.0 Team
Detail¶
Suite 3.0 considerations¶
Some terms first:
- Consumer Service: A service which owns/manages an entity that requires CF, and consumes the CF library to implement it. These are usually Backend Services
- App service or BFF: a service which works as an API gateway or coordinates otherwise application specific business use cases e.g. AIMS, AIMS BFF.
- Satellite service: a service in charge of performing some client or environment specific operation, which enriches or generates data which would be later ingested into the different Backend or App services e.g. some custom satellite service for a Crystal specific feature, a sync service between AMOS and Identity for AMOS users, etc.
- Admin center: combination of frontend and backend service whose responsibility is to provide an administration point for a given environment to some administrative type of user.
By the very nature of the Suite 3.0 based apps, custom fields make a lot of sense. On a first discussion with LC and JMR we arrived at the conclusion that there shouldn't exists a separate service for handling all suite custom fields, but that it would rather act as a sort of add on or plugin, augmenting already existing or new consumer services.
Why? Because it makes sense :P. In all seriousness, consumer services should allow enriching their entities with Custom Fields, which the consumer service will barely acknowledge exists.
In a sense, the Assets service will just receive and hand off processing of Custom Field values addition/edition/deletion to the Custom Fields library. Why? Because a consumer service should contain only domain specific and client/app agnostic logic, and if something is a custom field, then how specific to the domain actually is that field? However, it's the perfect place for managing and storing the values, since it's in the heart of all events and processing sagas in the microservices network.
Both App or Satellite service are better suited for using and consuming Custom Fields (until maybe a later point in time where upgrading the Custom Field to Client Field or similar, would make sense, if a client specific satellite service is created). The reason for this is twofold: on one hand, frontend apps like the AIMS frontend, would ideally be aware of Custom Field Values existence, thus allowing to CRUD them alongside native values (so a satellite service can read/update them while doing some process, without requiring modifications to its frontend app). On the other hand, not all Custom Fields need be visible to the end user, they could be used as inter service communication only metadata, which would be readily available for all other services consuming the Domain Entity data.
The Admin center users will be able to browse all Custom Fields defined for any entity type, as well as create/update Custom Field definitions from it.
Server-side¶
Implementation would follow MPM's database architecture. Each implementing service would get two new tables on its store: Custom Fields and Custom Field Values. Each service would own said data for its entities. Adding a new Custom Field type should ideally be simple (not necessarily easy) and said process should adhere to the O in S.O.L.I.D..
Warning
Until the use case presents itself, no consumer service will be able to define and register their own Custom Field type.
This library could provide utilities to be used in the Admin center, so that it can automagically listen to Custom Field definitions changes
Client-side¶
Out of RFC scope
While out of scope for this RFC, ideally Custom Fields would be displayed and updated on the different frontend applications the same way a Native Field is. This is, for the end user, working with a Custom Field should feel no different than for example updating an Inspection's description.
Pitfalls to avoid¶
Not everything is a Custom Field, nor should it be. We don't want Custom Entities. Custom Entities are the worst version of the Anemic Domain anti-pattern I can conjure in my mind. Initially, Custom Fields should serve as a key value store for entity metadata specific to a client or environment.
Only if it makes sense in the context of a client, or if it gives us time for a proper implementation (i.e. our development speed cannot match the required velocity of feature delivery), should Custom Fields be suggested/introduced to the end user.
Proposal¶
Database Schema¶
In the future, Custom Fields library should provide database mapping utilities at 3 different level/points:
- Admin Center
- Backend Service
- Search Service (or any service with materialized views)
This section only addresses item (2).
3 tables will be added to any Consumer Service of Custom Fields:
Custom Fields Definition¶
Contains Custom Field definitions or metadata. Each Custom Field Definition
belongs to a single entity type and can be identified either by its ID or (more
likely to be used) its string key. e.g. itsynch.suite.users.replicated-to-amos
Custom Field Values¶
Custom Field Values will contain a row for every Entity-CustomFieldDefinition combination. It has FK to its respective Definition and Collection (see next section).
RawValue
will contain some basic information about serialization version and
the actual serialized value. e.g.
Additionally, a (de-normalized) CustomFieldType
column will be available to be
used as an Entity Framework Core discriminator column on TPH strategy.
Custom Field Values Collection¶
Custom Field Values Collection is basically a workaround for an issue MPM's CF
implementation had. Basically, we could not have relational integrity enforced
by Foreign Keys, since each Custom Field Value would "know" its target entity by
a TargetEntityId
property, and the target entity schema's had no changes.
On this implementation, each IEntityWithCustomFields
implementor's backing
database table will get a new column CustomFieldValueCollectionId
, a FK
pointing to its corresponding Custom Field Values Collection table row.
This way, queries will be more efficient and we get integrity checks where we need them the most.
HTTP presentation¶
Consumer services (and any other service TBH) implementing the Custom Fields library feature, will expose the following new HTTP endpoints (names provided for readability purposes, endpoints are REST like named, but Suite 3.0 auto wired HTTP App Services will most likely generate different names)
Definitions¶
GET /custom-fields
¶
HTTP GET endpoint for retrieving all custom fields definition defined in this service.
Entities with Custom Fields¶
When an entity fulfils the rules for "implementing Custom Fields", then all
.GET
HTTP calls to that entity endpoint (e.g. api/v1/inspections/getAll
)
should include in their payload the Custom Field Values for that entity.
SEE Out of scope
Messaging¶
Custom messages related to Custom Fields will not be raised, but rather Custom Fields related payload will be by default (could be configurable) including in all/some consumer service messages payload.
List of messages:
CustomFieldDefinitionUpdate
: handled by consumer services, updates an existing Custom Field definition. If everything is OK, reply to the void withCustomFieldDefinitionsUpdated
CustomFieldDefinitionsUpdated
: handled (initially) by Admin Center, to keep its state up to date regarding existing Custom Fields definitions through the SuiteCustomFieldLibraryInitialized
: handled by Admin Center, sent by consumer services on initialization
Constraints/Considerations¶
Here follows a list of unordered assumptions for this RFC, please share any comments about them. They are not an exhaustive list by no means:
- For the server-side implementation, only an EF Core (latest version) implementation will be written.
- NH is out of scope.
- When in need of JSON de/serialization, the same serialization tool as the rest of the suite will be used (be it System.Json or Newtonsoft.Json).
- C# extension methods would be avoided as much as possible, but when required, attention will be payed in order to make them unit test friendly.
- Unit test utilities might be provided if within time budget
- Default implementation methods for interfaces might be leveraged if it makes sense (a.k.a. if its fun for me to do so)
- Hooks/messages for intercepting/interacting with the processing of Custom Fields from the satellite/application services will be properly documented and shared when/if appropriate
- Consumer service level configuration of Custom Fields should be kept to a
single place (possible startup or
ConfigureServices
method) - Entity level configuration in order to support custom fields will (be tried to) be kept at a minimum.
Examples¶
Usage¶
All versions consume the following common symbols:
The idea of the ICustomFieldsManager
is to limit boilerplate code for handling
common operations across entities with Custom Fields, as well as simplifying
unit testing.
Approach 01 - IGNORE¶
Approach 01 leverages default interface implementations for providing the
ICustomFieldsManager
Approach 02 - IGNORE¶
Approach 02 leverages extension methods instead. Right now I cannot think of a
case where this could limit our unit tests, but that is usually the case with
logic-heavy extension methods. Thus, the ICustomFieldsManager
stays.
Approach 03 - IGNORE¶
Approach 03 leverages is basically Approach 01 sans the ICustomFieldsManager
Approach 04¶
Approach 04 is just some extension method syntactic sugar on top of Approach 03.
Approach 05 (CHOSEN)¶
Entities will implement the IEntityWithCustomFields
interface. A special
repository/service will be injected in order to manipulate said entities, so
that we can have DI capable symbols from the start (otherwise, we'd lose for
example logging capabilities).
Additionally, said interface will only as a marker for Suite Decorators to take action on.
C# | |
---|---|
By using Suite Decorators, tables for entities implementing
IEntityWithCustomFields
will get a new FK column to Custom Field Values
Collection table (see database schema section).
Configuration¶
Module¶
Extension methods will be provided to add Sagas to the Admin Center and/or Consumer Services, in order to install the Library.
If further configuration is required, the IOptions<T>
pattern will be used.
EF Core¶
Entities implementing the required interfaces will be automagically configured using Suite Decorators
Testing¶
This section covers how Consumer Services can test their Custom Fields relying flows, it doesn't contain info on how the Custom Fields Library will be tested.
With all the approaches presented before, testing arrangement boils down to
setting whatever our test scenario demands on CustomFieldValues
property,
which is just an ICollection<ICustomFieldValue>
.
Assertions are a bit more complicated. One untested idea could be doing something like:
This way, ITestEntityWithCustomFields
could intercept calls that rely on
building an ICustomFieldsManager
and replace it with a more assertion-friendly
version under the hood.
Saga utilities¶
Saga utilities are methods to be used within the normal saga of the Consumer Service entities. Sagas for managing Custom Field Definitions and Values are not described here.
Note that message interfaces inheritance is not something we want to do. So all saga utilities will either take a lambda function to determine from where to read/write the custom fields related information, or need to be specified where required
Out of scope¶
Here are some of the capabilities/features that will be out of scope for this RFC implementation:
- Simplified querying capabilities for AppServices: if you want to get an
IEntityWithCustomFields
that actually contains its Custom Fields back from a repository query, you'd better manually add the requiredSpecification
or do a separate query (for testing purposes, or similar) - Utilities to listen/emit/etc messages and integrate with Custom Fields at the "Search Service Layer" (read: materialized views)
- Providing message inheritance for messages that have Custom Fields related fields, for now no inheritance is provided. Lambdas are used as a manual workaround for developers. Pending investigation on MT side is required to determine if some message inheritance is possible.