This PR introduces constructor mapping to the existing default mapper. Previously, mapping was achieved by creating a new instance of the class being mapped to using its default (parameterless) constructor, and then setting the values of its properties. This is not always possible, however, as some classes only have a constructor that requires parameters. Additionally, this meant that all the mapping code had a where T : new() constraint, which limited the usefulness of the mapper.
With constructor mapping, the default mapper will now decide which constructor to use to create the class according to the following priorities:
A constructor marked with the [MappingConstructor] attribute.
A parameterless constructor.
A constructor with the fewest parameters.
The mapper does not take into account whether it can satisfy the parameters of the constructor, since each time a record is mapped it may have a different set of fields. Therefore if you want the mapper to use a specific constructor it is recommended that you mark it with the [MappingConstructor] attribute.
Once it has decided which constructor to use, it will map the parameters in the same way as it maps properties. This means that the parameters can be marked with a [MappingSource] attribute to specify the source of the value. The [MappingIgnore] attributer is not permitted on constructor parameters since a constructor must have a value for each parameter.
After the object has been constructed, any properties that are not parameters of the constructor will be mapped in the same way as before.
Examples
For all these examples, assume the following record is being mapped.
forename
age
Bob
42
Mapping to a class with a parameterless constructor
public class Person
{
[MappingSource("forename")]
public string Name { get; set; }
public int Age { get; set; }
}
var person = record.AsObject<Person>();
This is the existing case. The object is created and then the propetries are set individually from values in the record. The [MappingSource] attribute is used to specify the source of the value; for the Name property; obviously, the Age property is set from the value in the record with the same name.
Mapping to a class with a constructor
public class Person
{
public Person(
[MappingSource("forename")] string name,
int age)
{
Name = name;
Age = age;
}
public string Name { get; }
public int Age { get; }
}
var person = record.AsObject<Person>();
In this case, the non-default constructor is called, and the parameters are mapped from the record. The Name parameter is mapped from the value in the record with the name forename, and the Age parameter is mapped from the value in the record with the name age.
Mapping to a class with a constructor marked with [MappingConstructor]
public class Person
{
public Person()
{
}
[MappingConstructor]
public Person(
[MappingSource("forename")] string name)
{
Name = name;
}
public string Name { get; }
public int Age { get; set; }
}
var person = record.AsObject<Person>();
In this case, although there is a default constructor, the constructor marked with [MappingConstructor] is used. The Name parameter is mapped from the value in the record with the name forename. After construction, the Age property is set from the value in the record with the name age.
Complex Types
Parameters to a constructor are treated exactly the same as properties, so they can have nested objects, lists, etc. Both constructor and property mapping will be used as appropriate, so if you are using constructor mapping for the top-level object, there is no need to use it for nested objects. Both constructor parameters and properties may be marked with the [MappingSource] attribute and this will be correctly observed.
Constructor Mapping
This PR introduces constructor mapping to the existing default mapper. Previously, mapping was achieved by creating a new instance of the class being mapped to using its default (parameterless) constructor, and then setting the values of its properties. This is not always possible, however, as some classes only have a constructor that requires parameters. Additionally, this meant that all the mapping code had a
where T : new()
constraint, which limited the usefulness of the mapper.With constructor mapping, the default mapper will now decide which constructor to use to create the class according to the following priorities:
[MappingConstructor]
attribute.The mapper does not take into account whether it can satisfy the parameters of the constructor, since each time a record is mapped it may have a different set of fields. Therefore if you want the mapper to use a specific constructor it is recommended that you mark it with the
[MappingConstructor]
attribute.Once it has decided which constructor to use, it will map the parameters in the same way as it maps properties. This means that the parameters can be marked with a
[MappingSource]
attribute to specify the source of the value. The[MappingIgnore]
attributer is not permitted on constructor parameters since a constructor must have a value for each parameter.After the object has been constructed, any properties that are not parameters of the constructor will be mapped in the same way as before.
Examples
For all these examples, assume the following record is being mapped.
Mapping to a class with a parameterless constructor
This is the existing case. The object is created and then the propetries are set individually from values in the record. The
[MappingSource]
attribute is used to specify the source of the value; for theName
property; obviously, theAge
property is set from the value in the record with the same name.Mapping to a class with a constructor
In this case, the non-default constructor is called, and the parameters are mapped from the record. The
Name
parameter is mapped from the value in the record with the nameforename
, and theAge
parameter is mapped from the value in the record with the nameage
.Mapping to a class with a constructor marked with
[MappingConstructor]
In this case, although there is a default constructor, the constructor marked with
[MappingConstructor]
is used. TheName
parameter is mapped from the value in the record with the nameforename
. After construction, theAge
property is set from the value in the record with the nameage
.Complex Types
Parameters to a constructor are treated exactly the same as properties, so they can have nested objects, lists, etc. Both constructor and property mapping will be used as appropriate, so if you are using constructor mapping for the top-level object, there is no need to use it for nested objects. Both constructor parameters and properties may be marked with the
[MappingSource]
attribute and this will be correctly observed.