Skip to content

Mapping Pocos

Aaron Sherber edited this page Dec 7, 2018 · 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, and because different methods of mapping suit different situations it's unlikely you'll use everything. Pick what works for you and the project. Most of the time, simple turn-key convention mapping will work for you!

Note: The Standard mapper has been replaced with newer Convention mapper (Regression tests in-place). The primary reason for the replacement is so that more scenarios can be supported without needing inheritance/"roll your own" to alter functionality. The Standard mapper will continue to be shipped and supported. However, we suggest a look at the newer convention mapper as it may simplify existing code.

A few common questions:

  • Do I have to use attributes? No
  • Can I register mappers per type/assembly? Yes
  • Can I remove a registration per type/assembly? Yes
  • Can I have different default mappers for PetaPoco instance? Yes (However, mappers registered through Mappers are global)
  • Can I use my own attributes? Yes (this possible with a custom mapper or altered convention mapper)

Convention mapper

The convention mapper carry overs the conventions used by standard mapper, but the key difference is that the convention is now configurable. Having a easily configurable convention mapper fits nicely with fluent configuration. For example, the convention mapper makes altering the convention for PostgreSQL very simple.

    .UsingDefaultMapper<ConventionMapper>(m =>
    {
        // OrderLine becomes order_lines
        m.InflectTableName = (inflector, tn) => inflector.Pluralise(inflector.Underscore(tn));

        // OrderLineId becomes order_line_id
        m.InflectColumnName = (inflector, cn) => inflector.Underscore(cn);
    })

Convention mapper default convention

Map table convention

When mapping the table info, the convention mapper will first try and find a TableNameAttribute for the DB table name, or when not found the table name will be the Type.Name, with the Type.Name value being passed passed through the InflectTableName hook (by convention does nothing). Next the mapper will then look for a PrimaryKeyAttribute to get the primary key column name, auto increment flag and optional sequence name (Oracle). When no PrimaryKeyAttribute can be found, it will try and find the first Poco property fitting the the naming format (casing does not matter) Id, Type.Name + "Id" or Type.Name + "_Id". Should a property fitting this naming convention be found, the PropertyInfo.Name is passed through the InflectColumnName (by convention does nothing) hook and used as the DB table column name. The property is then passed through the IsPrimaryKeyAutoIncrement for a decision of whether the column is an auto incrementing column or not, which by convention will be true if the PropertyInfo.PropertyType is any type of these types long, ulong, int, uint, short or ushort. Lastly the property is passed through the GetSequenceName hook, which by convention does nothing.

Pseudo code - map table

    Does type have a TableNameAttribute?
        Table name = TableNameAttribute.Value
    Else
        Table name = call.InflectTableName(Type.Name)
    Continue

    call.MapPrimaryKey

Pseduo code - map primary key

    Does type have a PrimaryKeyAttribute?
        Primary Key = PrimaryKeyAttribute.Value
        Auto Increment = PrimaryKeyAttribute.AutoIncrement
        Sequence Name = PrimaryKeyAttribute.SequenceName
    Else does any property match "Id", Type.Name + "Id" or Type.Name + "_Id"
        Primary Key = call.InflectTableName(PropertyInfo.Name)             
        Auto Increment = call.IsPrimaryKeyAutoIncrement(PropertyInfo.PropertyType)
        Sequence Name = call.GetSequenceName(Type, PropertyInfo)
    Else
        No primary key

Pseduo code - is primary key auto increment

    Is property type any of theses Long, ULong, Int, UInt, Short or UShort
        Yes - is auto increment
    Else
        No - no is not auto increment

Pseduo code - Get sequence name/Inflect table name/Inflect column name

    Does nothing

Map column convention

When mapping the column info, the convention mapper will first check if the parent class is decorated with the ExplicitColumnsAttribute. If the parent is decorated, it will then require the current property to be decorated with either the ColumnAttribute or ResultColumnAttribute, else no mapping is produced. Then the mapper will check if the current property is decorated with the IgnoreAttribute, which when found will not produce a mapping. The mapper will next map the column name by checking for the ColumnAttribute or ResultColumnAttribute value, if decorated and set, else the PropertyInfo.Name with the value being passed passed through the InflectColumnName hook (by convention does nothing) will be used. Where the property was decorated with the ResultColumnAttribute, the ColumnInfo.ResultColumn property will be flagged.

Pseudo code - map column

    Does parent have ExplicitColumnsAttribute?
        Does property not have either ColumnAttribute or ResultColumnAttribute?
            End = Do not map
        Continue
    Continue

    Dose property have IgnoreAttribute?
        End = Do not map
    Continue

    Does property have ColumnAttribute or ResultColumnAttribute
        Does attribute have Name value?
            Column name = Attribute.Name
        Else
            Column name = call.InflectColumnName(PropertyInfo.Name)
        Continue

        Is attrbute a ResultColumnAttribute?
             Result column = true
    Else
        Column name = call.InflectColumnName(PropertyInfo.Name)

Pseduo code - Inflect column name

    Does nothing

Mapping attributes

TableName

Is an attribute, which when applied to a Poco class, specifies the the DB table name which it maps to.

PrimaryKey

Is an attribute, which when applied to a Poco class, specifies primary key column. Additionally, specifies whether the column is auto incrementing and the optional sequence name for Oracle sequence columns.

ExplicitColumns

Is an attribute which, when applied to a Poco class, states all columns must be explicitly mapped using either the Column or ResultColumn attribute.

Column

Is an attribute which can decorate a Poco property to mark the property as a column. It may also optionally supply the DB column name.

ResultColumn

Is an attribute which can decorate a Poco 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: when no column name is supplier and the Convention mapper is being used, the property name is passed through the InflectColumnName hook.

Note: ResultColumns are not included in auto-generated SQL. To read a ResultColumn from the database, you must explicitly state all columns required in a complete SELECT statement.

Ignore

Is an attribute which can decorate a Poco property to ensure PetaPoco does not map column, and therefore ignores the column.

Global mapper registrations

The global Mapper 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 below Default mapper per PetaPoco instance .

The mapper class API, as it stands is:

    public static class Mappers
    {
        public static void Register(Assembly assembly, IMapper mapper)
        public static void Register(Type type, IMapper mapper)
    
        public static void Revoke(Assembly assembly)
        public static void Revoke(Type type)
        public static void Revoke(IMapper mapper)
        public static void RevokeAll()
    }

Register(Assembly assembly, IMapper mapper) Registers a mapper for all types in a specific assembly.

Register(Type type, IMapper mapper) Registers a mapper for a single POCO type.

Revoke(Assembly assembly) Remove all mappers for all types in a specific assembly.

Revoke(Type type) Remove the mapper for a specific type.

Revoke(IMapper mapper) Revoke an instance of a mapper.

RevokeAll() Revokes all registered mappers.

Default mapper per PetaPoco instance

When using the fluent configuration or the constructor which accepts and 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 above Global mapper registrations.

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 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. A handy tip to get the parent poco's type is pocoProperty.DeclaringType. The returning of null means the column is not mapped.

GetFromDbConverter and GetToDbConverter

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

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

Example Pocos/Mappings

    public class Note
    {
        public int Id { get; set; }

        public DateTime CreatedOn { get; set; }

        public string Text { get; set; }
    }
    [ExplicitColumns]
    [TableName("Orders")]
    [PrimaryKey("Id")]
    public class Order
    {
        [Column]
        public int Id { get; set; }

        [Column]
        public Guid PersonId { get; set; }

        [Column]
        public string PoNumber { get; set; }

        [Column]
        public DateTime CreatedOn { get; set; }

        [Column]
        public string CreatedBy { get; set; }

        [Column("OrderStatus")]
    }
    [TableName("OrderLines")]
    [PrimaryKey("Id")]
    public class OrderLine
    {
        [Column]
        public int Id { get; set; }

        [Column]
        public int OrderId { get; set; }

        [Column(Name = "Qty")]
        public short Quantity { get; set; }

        [Column]
        public decimal SellPrice { get; set; }

        [ResultColumn]
        public decimal Total { get; set; }
    }
    [TableName("People")]
    [PrimaryKey("Id", AutoIncrement = false)]
    public class Person
    {
        [Column]
        public Guid Id { get; set; }

        [Column(Name = "FullName")]
        public string Name { get; set; }

        [Column]
        public long Age { get; set; }

        [Column]
        public int Height { get; set; }

        [Column]
        public DateTime? Dob { get; set; }

        [Ignore]
        public string NameAndAge => $"{Name} is of {Age}";
    }
    [TableName("TransactionLogs")]
    public class TransactionLog
    {
        public string Description { get; set; }

        public DateTime CreatedOn { get; set; }
    }