Creating Data Types

Custom data types extend the type system with domain-specific storage, validation, and formatting. This guide walks through creating new data types for both string and decimal values.

Base Classes

Choose the appropriate base class for your data type:

Base Class CLR Type Built-in Behavior
DataTypeBase Any Abstract base — no built-in behavior
StringDataTypeBase string Automatic trimming to max length
DecimalDataTypeBase decimal Automatic rounding to stored precision
Text string String with max 100 chars (inherits StringDataTypeBase)
Decimal decimal Decimal with 2/2 precision (inherits DecimalDataTypeBase)

Most custom types inherit from Text or Decimal rather than the abstract bases.

Creating a String Data Type

Step 1: Define the Type

using Benevia.Core.Annotations;
using Benevia.Core.Events.DataTypes;
using Benevia.Core.Events.DataTypes.Attributes;

namespace Benevia.ERP.Model.Products;

[DataType<string>]
[DataAnnotations("Unit of measure name", "MaxLength(10)")]
public partial class UomName : StringDataTypeBase
{
}

Required elements:

  • partial class
  • [DataType<string>] — declares the CLR type
  • [DataAnnotations("description", annotations)] — adds data annotation attributes
  • Inherit from StringDataTypeBase, Text, or another string type

The [DataAnnotations] attribute takes a description string and one or more annotation strings that map to System.ComponentModel.DataAnnotations attributes (e.g., "MaxLength(50)" generates [MaxLength(50)] on properties using this type).

Step 2: Use It

[Property<UomName>("Name")]
public partial string Name { get; set; }

With Inheritance

Inherit from an existing type to reuse its configuration:

// Inherits Text's behavior but changes max length
[DataType<string>]
[DataAnnotations("Email", "MaxLength(50)")]
public partial class Email : Text
{
    public override Type UnderlyingType { get; init; } = typeof(string);
}

Data annotations are cumulative when inheriting — the child type gets both parent and its own annotations.

Creating a Decimal Data Type

Step 1: Define the Type

using Benevia.Core.Annotations;
using Benevia.Core.Events.DataTypes;
using Benevia.Core.Events.DataTypes.Attributes;

namespace Benevia.ERP.Model.Products;

[DataType<decimal>]
[DecimalPrecision(10, 2)]
public partial class ProductQuantity : DataTypes.Decimal
{
}

The [DecimalPrecision(storedDecimals, visualDecimals)] attribute controls:

  • Stored decimals: Precision stored in the database (values are rounded to this)
  • Visual decimals: Decimal places shown in the UI
// Store up to 10 decimal places, show 2 in the UI
[DecimalPrecision(10, 2)]
public partial class ProductQuantity : DataTypes.Decimal { }

// Store and show 4 decimal places
[DecimalPrecision(4, 4)]
public partial class ExchangeRate : DataTypes.Decimal { }

Step 2: Use It

[Property<ProductQuantity>("Quantity (Each)")]
public partial decimal Quantity { get; set; }

Adding Auto-Correct

Auto-correct transforms values when a property is set. Create a [Logic] class:

using Benevia.Core.Events.BusinessLogicDefinitions;
using Benevia.Core.Events.LogicBuilders;

namespace Benevia.Core.Events.DataTypes;

[Logic]
public class IdTextBL(IdText.Logic dataType)
{
    [RegisterLogic]
    public void IdText_AutoCorrect()
    {
        dataType.AutoCorrect().Transform(NormalizeForBarcode);
    }

    string NormalizeForBarcode(string input)
    {
        if (string.IsNullOrEmpty(input))
            return input;

        var upperCase = input.ToUpperInvariant();

        var sb = new StringBuilder();
        foreach (char c in upperCase)
            if (IsValidBarcode39Char(c))
                sb.Append(c);

        return sb.ToString();
    }

    static readonly char[] allowedSymbols = [' ', '-', '.', '/', '+', '%'];
    bool IsValidBarcode39Char(char c) =>
        (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || allowedSymbols.Contains(c);
}

Key elements:

  • Class has [Logic] attribute
  • Uses primary constructor injection of DataType.Logic (e.g., IdText.Logic)
  • Methods with [RegisterLogic] run at startup to wire up event subscriptions
  • AutoCorrect().Transform(func) registers a transformation function

Title Case Example

[Logic]
public class ProperNounBL(ProperNoun.Logic dataType)
{
    [RegisterLogic]
    public void ProperNoun_TitleCase()
    {
        dataType.AutoCorrect().Transform(ToTitleCase);
    }

    string ToTitleCase(string input)
    {
        if (string.IsNullOrEmpty(input))
            return input;

        var chars = input.Trim().ToCharArray();
        bool capitalizeNext = true;

        for (int i = 0; i < chars.Length; i++)
        {
            if (!char.IsLetter(chars[i]))
            {
                capitalizeNext = true;
            }
            else if (capitalizeNext)
            {
                chars[i] = char.ToUpper(chars[i]);
                capitalizeNext = false;
            }
        }

        return new string(chars);
    }
}

Adding Validation

Validation runs before save and rejects invalid values with error messages:

using Benevia.Core.Events.BusinessLogicDefinitions;
using Benevia.Core.Events.LogicBuilders;

namespace Benevia.Core.Events.DataTypes;

[Logic]
public class EmailBL(Email.Logic dataType)
{
    [RegisterLogic]
    public void Email_Validate()
    {
        dataType.Validate()
            .RejectIf(x => !IsValid(x))
            .WithMessage("The email address is not valid.");
    }

    bool IsValid(string emailAddress)
    {
        if (string.IsNullOrWhiteSpace(emailAddress))
            return true;

        if (!MailAddress.TryCreate(emailAddress, out var email))
            return false;

        var hostParts = email.Host.Split('.');
        if (hostParts.Length == 1)
            return false;
        if (hostParts.Any(p => p == string.Empty))
            return false;
        if (hostParts[^1].Length < 2)
            return false;

        return email.Address == emailAddress;
    }
}

Key elements:

  • Validate().RejectIf(predicate).WithMessage(message) — rejects when predicate returns true
  • Return true for empty/null values if the field is optional (use [Required] separately for that)

Positive Value Validation

[Logic]
public class PositiveDecimalBL(PositiveDecimal.Logic dataType)
{
    [RegisterLogic]
    public void Validate()
    {
        dataType.Validate()
            .RejectIf(x => x < 0)
            .WithMessage("The value must be positive.");
    }
}

Where to Place Data Types

Location When
Benevia.Core.Events.DataTypes General-purpose types reusable across all projects
Your model project (e.g., Benevia.ERP.Model) Domain-specific types tied to your business logic

Domain types in the model project can reference core types:

namespace Benevia.ERP.Model.Products;

[DataType<decimal>]
[DecimalPrecision(10, 2)]
public partial class ProductUnitPrice : DataTypes.Decimal
{
}

Complete Walk-Through: Phone Number Type

Here is a complete example combining a type definition with auto-correct formatting:

Type definition:

[DataType<string>]
[DataAnnotations("Formats USA 10 digit phone numbers", "MaxLength(25)")]
public partial class Phone : Text
{
}

Auto-correct logic:

[Logic]
public class PhoneBL(Phone.Logic dataType)
{
    [RegisterLogic]
    public void Phone_AutoCorrect()
    {
        dataType.AutoCorrect().Transform(FormatPhone);
    }

    string FormatPhone(string input)
    {
        if (string.IsNullOrEmpty(input))
            return input;

        // Extract digits
        var digits = new string(input.Where(char.IsDigit).ToArray());

        // Format 10-digit USA numbers
        if (digits.Length == 10)
            return $"({digits[..3]}) {digits[3..6]}-{digits[6..]}";

        return input;
    }
}

Usage on an entity:

[Property<DataTypes.Phone>("Phone")]
public partial string PhoneNumber { get; set; }

When a user enters 5551234567, the auto-correct transforms it to (555) 123-4567.

Checklist

  1. Define partial class with [DataType<CLRType>]
  2. Add [DataAnnotations] for description and data annotations
  3. For decimals, add [DecimalPrecision(stored, visual)]
  4. Inherit from appropriate base (Text, Decimal, or a custom parent type)
  5. Optionally add a [Logic] class with AutoCorrect() and/or Validate() event handlers
  6. Use with [Property<YourType>("Label")] on entity properties

<< Back to Entity Model

<< Back to home page