Logical Types
Logical types are defined under the types
element of the module
and constitute one of the core elements of the Xomega modeling, allowing you to freely model your application on the logical level without being restricted by any physical types of any specific technology or platform. This, in turn, helps you make your application consistent across all layers and, therefore, easy to learn and maintain.
The logical types do eventually map to specific physical types in various layers to allow Xomega to generate an actual application for you, but you should always try to define your logical types on a conceptual level for your domain model rather than just mirroring a set of physical types from one platform or another.
For example, you may have a logical type user
that represents a user of your application and is mapped to a physical type that is used to store the user ID, such as integer
. If, later on, you decide to switch the physical type for the user ID to a string
, then it will be an easy change, as opposed to refactoring it across all layers of your entire application.
Separately, you can also define two logical sub-types internal user
and external user
that would map to the same physical type and may even provide no additional configuration initially. Now, whenever you need to define a field that stores a user ID, such as create_user
, you can decide whether to set its type to the internal user
, external user
, or just a generic user
type based on your specific domain.
Even though either choice will result in the same application being generated, this information will be captured in your Xomega model on the logical level, and you will always be able to easily modify it later to add certain validations, specify special formatting rules, or restrict a list of allowed values in the UI, as needed.
Type configuration
Logical type definitions for string-based types may include a size
attribute, which would be used as a maximum length for the corresponding database columns, or also for validation on the service or client side, or for restricting user input on the UI.
Any additional configuration of the logical type definition comes from the nested config
element, which may contain various technology, platform, or layer-specific configurations defined in other namespaces, as illustrated below.
<types>
<type name="code" base="string" size="15">
<config>
<sql:type name="nchar" db="sqlsrv"/>
</config>
<usage generic="true"/>
<doc>
<summary>A short fixed-size string that is typically used for codes.</summary>
</doc>
</type>
</types>
These configurations include mapping the logical type to a physical database type, EDM type, or a C# type, specifying the Xomega Framework property or UI controls to use for displaying and editing fields of this type in various UI frameworks, as well as any other additional configurations.
Logical types allow you to maintain consistency within your application both horizontally, where different fields of the same type will have the same lengths, physical types, and edit controls, as well as vertically, where you ensure that physical types, validations and UI controls for the same field are consistent across different layers.
Standard type configs
The type configurations can have any custom configs, but the standard ones included with Xomega are listed in the following example with a brief description for each config.
<type name="money" base="decimal">
<config>
<!-- SQL type of the specified DB for the logical type -->
<sql:type name="money" db="sqlsrv"/>
<!-- CLR type for the logical type -->
<clr:type name="decimal" valuetype="true"/>
<!-- EDM type for the logical type to help build and Entity Data Model -->
<edm:type Type="Decimal"/>
<!-- Xomega Framework property for the logical type, with both the class and namespace.
May also include tsModule and tsClass (if different from class)
for the corresponding XomegaJS property to build TypeScript SPA apps. -->
<xfk:property class="MoneyProperty" namespace="Xomega.Framework.Properties"
tsModule="xomega"/>
<!-- additional UI display config for the type, such as a typical length of formatted values,
which is used to set up default column widths on UI grids. -->
<ui:display-config typical-length="12"/>
<!-- Xomega Blazor controls to use for the logical type -->
<ui:blazor-control>
<XNumericBox />
</ui:blazor-control>
<!-- Xomega Syncfusion Blazor controls to use for the logical type -->
<ui:sf-blazor-control>
<XSfNumericTextBox/>
</ui:sf-blazor-control>
<!-- WPF controls to use for the logical type -->
<ui:control>
<TextBox Style="{StaticResource ControlStyle}"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"/>
</ui:control>
<!-- WebForms controls to use for the logical type -->
<ui:web-control xmlns:asp="clr-namespace:System.Web.UI.WebControls;assembly=System.Web">
<asp:TextBox runat="server" CssClass="decimal"/>
</ui:web-control>
<!-- HTML controls to use for the logical type in SPA apps -->
<ui:html-control>
<input class="decimal" xmlns="http://www.w3.org/1999/xhtml"/>
</ui:html-control>
</config>
</type>
The UI control configuration elements, i.e., the ones that end with control
, also allow you to specify multi-value
and use-in-grid
boolean attributes.
This way, you can configure different controls for editing multi-value properties, e.g., a ListBox
vs. a ComboBox
, or configure a certain control to be used in grids instead of just text, e.g., to show a CheckBox
for boolean columns.
Split configurations
Xomega model allows you to specify additional type configurations in separate type-config
elements under the types
element, similar to how you define partial classes in C#.
This allows you to keep configurations for specific technologies or frameworks in separate files, which you can easily exclude from the model if your solution doesn't use those technologies or frameworks, as shown below.
<module xmlns="http://www.xomega.net/omodel"
xmlns:uc="UserControl"
xmlns:asp="clr-namespace:System.Web.UI.WebControls;assembly=System.Web"
xmlns:ui="http://www.xomega.net/ui"
name="framework">
<types>
<type-config type="date time">
<ui:web-control>
<uc:DateTimeControl TextCssClass="datetime" runat="server"/>
</ui:web-control>
</type-config>
<type-config type="selection">
<ui:web-control>
<asp:DropDownList runat="server"/>
</ui:web-control>
<ui:web-control multi-value="true">
<asp:ListBox runat="server"/>
</ui:web-control>
</type-config>
</types>
</module>
Any configs defined in the type-config
elements are combined with the type's own configs. The type-config
elements also appear in the list of references for the given type when you search for type references, e.g., with Shft+F12
.
After you create a Xomega solution, you will find the split-out type configurations under the Framework > TypeConfigs folder in the model project. So, for example, if your solution doesn't use WebForms, you'll be able to exclude the web_forms.xom
file from your project, or simply delete it.
Type inheritance
Similar to class inheritance in C#, you can specify a base type for any logical type in the base
attribute, and your type will inherit all configurations from the base type or any of its own base types. You can override any of the base type's configurations in your type in addition to adding new configurations.
In the following example, the employee
type is a sub-type of person
, which eventually has the integer
as the root base type.
<types>
<type name="integer">
<config>
<sql:type name="int" db="sqlsrv"/>
<clr:type name="int" valuetype="true"/>
<xfk:property class="IntegerProperty" namespace="Xomega.Framework.Properties" tsModule="xomega"/>
<ui:display-config typical-length="6"/>
</config>
</type>
<type name="integer key" base="integer">
<config>
<xfk:property class="IntegerKeyProperty" namespace="Xomega.Framework.Properties"/>
</config>
</type>
<type name="business entity" base="integer key"/>
<type name="person" base="business entity"/>
<type name="employee" base="person"/>
</types>
So the employee
type will inherit all configurations from the integer
type except for the xfk:property
, which it will inherit from the integer key
since it's overridden there.
You cannot inherit a type from multiple base types that are not part of the same hierarchy. If you need to combine configurations from multiple types, you may need to set one of them as the base type and just add the missing configurations from the other base type to your own type.
Type documentation
You can (and should) document the purpose for your logical type in its doc
element. At a minimum, we recommend providing the type description in the summary
element, as illustrated below, but you can also add more detailed documentation in the remaining part of the doc
element.
<types>
<type name="code" base="string" size="15">
<doc>
<summary>A short fixed-size string that is typically used for codes.</summary>
</doc>
</type>
</types>
The type's documentation will help you work with your models and understand your model elements, but it can also be used as default documentation for fields or parameters of that type. When used on specialized types, this could save you from separately supplying documentation on all those fields and parameters, where appropriate.
Type enumeration
If the values of your logical type should be restricted to a certain enumeration, or if you want to configure a list of possible values when selecting the value, then you can associate that logical type with a static or dynamic enumeration that is also defined in the model, as described below.
Static enumeration
If you have a static enumeration defined in your model, then you can associate a logical type with that enumeration using a nested enum
element, where you set the ref
attribute to the enumeration name, as follows.
<module xmlns="http://www.xomega.net/omodel" name="framework">
<types>
<type name="operator" base="enumeration">
<enum ref="operators"/>
</type>
</types>
<enums>
<enum name="operators">[...]
</enums>
</module>
You want to use this configuration when the values for your logical type come from a static list, which does not change from one release of your app to another, and, therefore, can be specified in your static data model.
You can also control the type of validation of the type values against the specified enumeration by setting the validation
attribute on the type's enum
element to one of the following values.
active-item
- type value should be an active item of the enum.any-item
- type value should be any item of the enum (default).none
- no validation of the type value against this enum.
For example, the following configuration will turn off validation against the specified enumeration.
<type name="operator" base="enumeration">
<enum ref="operators" validation="none"/>
</type>
This association will not only help you configure the selection and validation of the values of that type in the generated app, but it will also enable using the allowed values from the associated enumeration when generating documentation for the fields and parameters of that type, and therefore make that documentation much more detailed and useful.
Dynamic enumeration
If the possible values for the type are not static but are sourced from a service operation that is configured to cache its results as a dynamic enumeration using xfk:enum-cache
config, then you can also associate such a dynamic enumeration with your logical type, much the same way you do it with static enumerations. You just need to use the dynamic enumeration name that is configured in the enum-name
attribute when you set the enum's ref
attribute on your type.
In the following example, the dynamic enumeration product
is sourced by the read enum
operation of the product
object and is associated with the product
type, as shown below.
<types>
<type name="product" base="integer enumeration">
<enum ref="product"/>
</type>
</types>
<objects>
<object name="product">
<fields>[...]
<operations>
<operation name="read enum">
<output list="true">
<param name="product id"/>
<param name="name"/>
<param name="is active" type="boolean" required="true"/>
<param name="list price"/>
</output>
<config>
<xfk:enum-cache enum-name="product" id-param="product id" desc-param="name"
is-active-param="is active"/>
</config>
</operation>
</operations>
</object>
</objects>
Custom enumeration
If you store multiple enumerations in a generic set of tables, such as dictionary tables, and return them all in the same operation using a dictionary service, then your xfk:enum-cache
specification will not have an enum-name
attribute, and the name of your enumerations will not be available in the Xomega model.
However, if you define a logical type that you want to associate with such an enumeration, you would normally get a model validation error if the ref
attribute doesn't reference a known enumeration. In order to suppress this error, you will need to set the custom="true"
attribute on that enum
element, as follows.
<type name="error severity" base="integer enumeration">
<enum ref="error severity" custom="true"/>
</type>
Alternatively, you can create a custom Xomega Framework EnumProperty associated with your enumeration as follows.
public class ScheduleTypeProperty : EnumIntProperty
{
public ScheduleTypeProperty(DataObject parent, string name) : base(parent, name)
{
EnumType = "schedule type";
}
}
Then you can just use that custom enum property on your logical type, as shown below.
<type name="schedule type" base="integer enumeration">
<config>
<xfk:property class="ScheduleTypeProperty" namespace="MyProject.Client.Common.DataProperties"/>
</config>
</type>
Base enumeration types
In addition to associating an enumeration with a logical type, you would also typically use one of the predefined enumeration types as a base type in order to inherit the appropriate configurations, which include selection controls for various technologies, Xomega Framework enum properties, etc.
The following snippet shows base types available in Xomega for enumerations by default, along with their short descriptions.
<types>
<!-- string-based enumeration with value selection from a fixed list -->
<type name="enumeration" base="selection" size="25">[...]
<!-- integer-based enumerations with value selection from a fixed list -->
<type name="integer enumeration" base="selection">[...]
<type name="tiny int enumeration" base="integer enumeration">[...]
<type name="small int enumeration" base="integer enumeration">[...]
<type name="big int enumeration" base="integer enumeration">[...]
<!-- boolean-based enumeration with value selection from a fixed list -->
<type name="boolean enumeration" base="selection">[...]
<!-- guid-based enumeration with value selection from a fixed list -->
<type name="guid enumeration" base="selection">[...]
<!-- string value that is typed in, with a suggestion list available -->
<type name="suggest string" base="enumeration">[...]
</types>
You don't have to use any of these types as a base type for your enumeration. If your logical type needs to have another base type, then you can just add the appropriate configurations from one of these default types directly into your type.
Type usage
In order to help other developers better use logical types in large projects, you can configure certain usage attributes in the type definition using the usage
element, as described below.
Generic types
If a logical type that you defined is not referenced anywhere in your model, Xomega will show you a warning alerting you of that fact to help you fix your model, as illustrated below.
However, when you first create a Xomega solution, your initial model will have a set of predefined generic types, and obviously, not all of them will be used in your model.
Similarly, you may want to define your own generic types for future use that would not be referenced in the model. To avoid showing the above warning on such types, you can set the generic="true"
attribute on its usage
element as follows.
<type name="code" base="string" size="15">
<usage generic="true"/>
<doc>
<summary>A short fixed-size string that is typically used for codes.</summary>
</doc>
</type>
The predefined framework types that were added as part of the initial model will be already marked as generic, so you just need to set this attribute on any generic types that you define manually.
Deprecated types
In large projects refactoring core logical types that are used extensively in many places in the model may not be a straightforward task and may require gradual updates by different developers.
To facilitate this process, you can declare a logical type as deprecated and indicate another logical type that should be used instead of the current one by setting the replaced-by
attribute on the usage
element.
For example, the type image
has been deprecated in SQL Server in favor of the varbinary(max)
type. However, to allow importing models from the databases that still use it, the model has a generic image
type defined that is marked with the replaced-by="large binary"
attribute, as follows.
<type name="image" base="binary">
<config>
<sql:type name="image" db="sqlsrv"/>
</config>
<usage generic="true" replaced-by="large binary"/>
<doc>
<summary>Large binary data that can exceed 8000 bytes.</summary>
</doc>
</type>
If you try to use the image
logical type in your model now, Xomega will show you a warning, advising you to use the large binary
type instead, as follows.
This is similar to the Obsolete
attribute that you can put on C# members, which would then display a warning wherever those members are used.
List types
If a field in your object stores an array of values, then you can define a special list type in the model (or use one of the predefined types from the list_types.xom
file included under the Framework folder of the Model project). You can inherit your type from the base list
type and set the clr:type
to a collection, such as List<string>
, as shown below.
<type name="list" base="string">[...]
<type name="string list" base="list">
<config>
<clr:type name="List<string>" namespace="System.Collections.Generic"/>
<xfk:property class="TextProperty" namespace="Xomega.Framework.Properties" tsModule="xomega"
multi-value="true" />
</config>
<usage generic="true"/>
<doc>
<summary>Unlimited list of strings.</summary>
</doc>
</type>
You should also configure your xfk:property
with the multi-value="true"
attribute, as illustrated above. This will automatically configure the generated properties as multi-valued.
When using list types, don't to set the list="true"
attribute on the corresponding parameters of your operations. Otherwise, you'll get a list of lists for those parameters.
SQL types for arrays
You should also configure the sql:type
for your list type to allow storing the values as an array. For databases that support array types natively, such as PostgreSQL, you can specify the corresponding native type as follows.
<type-config type="string list">
<sql:type name="text[]" db="pgsql"/>
</type-config>
For databases that don't have native support for arrays, such as SQL Server, the base list
type uses nvachar(max)
to allow storing a JSON array, which is what EF Core 8 uses for array types.