Skip to content

Mapping Pocos

Stelio Kontos edited this page Jul 15, 2023 · 12 revisions

Mapping Pocos (aka Models/Entities), in our opinion, is super simple. However, as there's a lot of flexibility built-in, it might seem a little confusing to begin with. Please don't confuse complexity with flexibility. Because different methods of mapping suit different situations it's unlikely you'll use everything. Pick what works for you and the project. In many cases, simple turn-key convention mapping will work.

Note: The Standard mapper has been replaced with the newer Convention mapper, with regression tests in-place. The Convention mapper offers more flexibility in customizing the mapper's functionality, without having to "roll your own" using inheritance. While the Standard mapper will continue to be shipped and supported, we suggest that you try out the newer Convention mapper, as it may simplify your existing implementation.

A few common questions:

  • Do I have to use attributes? No
  • Can I use my own attributes? Yes (possible with either a custom mapper or an altered convention mapper)
  • Can I register mappers per type/assembly? Yes
  • Can I have different default mappers for each PetaPoco instance? Yes (however, note that mappers registered using the static Mapper methods are global)
  • Can I remove/unregister a mapper by type/assembly? Yes

Convention Mapper

The convention mapper carries over the conventions used by the standard mapper, but is also configurable. Having an easily configurable convention mapper fits nicely with fluent configuration. For example, the convention mapper makes altering the convention for PostgreSQL very simple, or customizing the naming convention for the table and column names, as shown below:

.UsingDefaultMapper<ConventionMapper>(m =>
{
    // OrderLine => order_lines
    m.InflectTableName = (inflector, tbl) => inflector.Pluralise(inflector.Underscore(tbl));
    // OrderLineId => order_line_id
    m.InflectColumnName = (inflector, col) => inflector.Underscore(col);
})

Default Convention

Table Mapping Convention

PetaPoco uses the following process when mapping tables:

  1. The convention mapper first checks if the class is decorated with TableNameAttribute to obtain the table name. If not found, the Type.Name for the class and is passed through the InflectTableName hook (which by convention does nothing), and used as the database table name.
  2. The mapper then checks if the class is decorated with PrimaryKeyAttribute. If found, the attribute's properties are used to determine the primary key column name, auto-increment flag, and (if Oracle) an optional sequence name. If found, table mapping ends here.
  3. If not found, it attempts to find the first property in that class that fits the naming format "Id", "{Type.Name}Id", or "{Type.Name}_Id", case-insensitive. If no property is found, the table is treated as one without a primary key, and table mapping ends here.
  4. If a property fitting this naming convention is found, the PropertyInfo.Name for that property is passed through the InflectColumnName hook (which by convention does nothing), and used as the primary key column name.
  5. The property is then passed through IsPrimaryKeyAutoIncrement to determine whether the column is auto-incrementing. By convention, it will be true if the PropertyInfo.PropertyType is of type long, ulong, int, uint, short, or ushort.
  6. Finally, the property is passed through the GetSequenceName hook (which by convention does nothing).

Pseudocode for MapTable:

Does class have a TableNameAttribute?
    Yes: table name = TableNameAttribute.Value
    No:  table name = call.InflectTableName(Type.Name)
Continue

Does class have a PrimaryKeyAttribute?
    Yes:
        primary key = PrimaryKeyAttribute.Value
        auto increment = PrimaryKeyAttribute.AutoIncrement
        sequence name = PrimaryKeyAttribute.SequenceName
    No:
        Does any property match "Id", "{Type.Name}Id" or "{Type.Name}_Id"?
            Yes:
                primary key = call.InflectColumnName(PropertyInfo.Name)
                auto increment = call.IsPrimaryKeyAutoIncrement(PropertyInfo.PropertyType)
                sequence name = call.GetSequenceName(Type, PropertyInfo)
            No:
                no primary key

Pseudocode for IsPrimaryKeyAutoIncrement:

Is property type `long`, `ulong`, `int`, `uint`, `short`, or `ushort`?
    Yes: primary key is auto-increment
    No:  primary key is not auto-increment

Column Mapping Convention

PetaPoco uses the following process when mapping columns:

  1. The convention mapper first checks if the class containing the property is decorated with ExplicitColumnsAttribute. If it is, the current property must be decorated with either the ColumnAttribute or ResultColumnAttribute, otherwise the property is skipped.
  2. The mapper then checks if the current property is decorated with the IgnoreAttribute. If it is, the property is skipped.
  3. Next, the mapper maps the column name by checking for the ColumnAttribute or ResultColumnAttribute value. If decorated and set, this value is used. Otherwise, the PropertyInfo.Name value is passed through the InflectColumnName hook (which by convention does nothing).
  4. If the property is decorated with the ResultColumnAttribute, the ColumnInfo.ResultColumn property is set to true.

Pseudocode for Map Column:

Does class have a ExplicitColumnsAttribute?
    Yes:
        Does property have a ColumnAttribute or ResultColumnAttribute?
            Yes: Continue
            No:  End - property not mapped
    No: Continue

Does property have a IgnoreAttribute?
    Yes: End - property not mapped
    No:  Continue

Does property have a ColumnAttribute or ResultColumnAttribute?
    Yes:
        Does the attribute have Name value?
            Yes: column name = Attribute.Name
            No:  column name = call.InflectColumnName(PropertyInfo.Name)
        Continue
        Is attribute a ResultColumnAttribute?
            Yes: result column = true
    No:
        column name = call.InflectColumnName(PropertyInfo.Name)

Mapping Attributes

TableName

Decorate your POCO class with this attribute to specify a specific DB table it should be mapped to.

PrimaryKey

Decorate your POCO class with this attribute to specify the primary key column. Additionally, specifies whether the column is auto incrementing and the optional sequence name for Oracle sequence columns.

ExplicitColumns

Decorate your POCO class with this attribute to ignore all properties, except ones explicitly marked with a Column or ResultColumn attribute. The result is equivalent to adding the Ignore attribute to all properties except the ones you want.

Column

Used to explitly identify a property as a column. which can decorate a Poco property to mark the property as a column. It may also optionally supply the DB column name.

Note: when no column name is supplied and the Convention mapper is being used, the property name is passed through the InflectColumnName hook.

ResultColumn

Used to explicitly identify a property as a result only column. A result only column is a column that is only populated in queries and is not used for updates or inserts operations.

Note: By default, ResultColumns are not included in auto-generated SQL. To read a ResultColumn from the database, you must either specify IncludeInAutoSelect.Yes when applying the attribute, or build the SELECT statement yourself and specify the columns you want.

Ignore

Any properties in your POCO class marked with this attribute will be ignored by PetaPoco. The result is equivalent to using the ExplicitColumns class attribute in conjunction with the Column/ResultColumn attributes.

Global Mapper Registrations

The global Mappers class is a static helper class which is used to register mappings in a global manner. Any global registered mapping overrides a PetaPoco's instance default mapper (see Default Mapper Per PetaPoco Instance below).

The mapper class API, as it stands:

public static class Mappers
{
    // Registers a mapper for all POCO types in a specific assembly.
    public static void Register(Assembly assembly, IMapper mapper);
    // Registers a mapper for a single POCO type.
    public static void Register(Type type, IMapper mapper);

    // Removes all mappers for all POCO types in a specific assembly.
    public static void Revoke(Assembly assembly);
    // Removes the mapper for a specific POCO type.
    public static void Revoke(Type type);
    // Removes an instance of a mapper.
    public static void Revoke(IMapper mapper);
    // Revokes all registered mappers.
    public static void RevokeAll();
}

Default Mapper Per PetaPoco Instance

When using the fluent configuration or the constructor which accepts an instance of IMapper, a PetaPoco consumer is able to control the default mapper. Put simply, the default mapper is the mapper which is used when no mapper has been registered for the Poco (See Global mapper registrations above).

If no default mapper is supplied, the default mapper PetaPoco will use is the Convention mapper (without modification).

Roll Your Own Mapper

To create your own mapper, you'll need to extend the IMapper interface.

The contract, as it stands:

TableInfo GetTableInfo(Type pocoType);
ColumnInfo GetColumnInfo(PropertyInfo pocoProperty);
Func<object, object> GetFromDbConverter(PropertyInfo targetProperty, Type sourceType);
Func<object, object> GetToDbConverter(PropertyInfo sourceProperty);

GetTableInfo

For this method, the implementer must return a TableInfo object populated using the details for the given Poco type, or null if the mapper cannot map the given Poco type.

GetColumnInfo

For this method, the implementer must return a ColumnInfo object populated using the details for the given Poco column, or null if the column is not mapped. A handy tip to get the parent poco's type is pocoProperty.DeclaringType.

GetFromDbConverter and GetToDbConverter

For these methods, the implementer may apply any conversions to<->from DB and Poco types. For instance, in SQLite, all numbers are stored as longs. In addition, there's no real support for a DateTime type, so it is common to store the value as a number using DateTime.Ticks. Given that all numbers are stored as longs, a conversion step to and from is required - both the GetFromDbConverter and GetToDbConverter participate in fulfilling this.

If you need to control conversion on a per-field basis, see ValueConverters

Checkout SqliteDBTestProvider as an example. Another good example is the ConventionMapper