Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Constructor mapping #751

Conversation

RichardIrons-neo4j
Copy link
Contributor

@RichardIrons-neo4j RichardIrons-neo4j commented Nov 7, 2023

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:

  1. A constructor marked with the [MappingConstructor] attribute.
  2. A parameterless constructor.
  3. 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.

@RichardIrons-neo4j RichardIrons-neo4j marked this pull request as ready for review November 10, 2023 09:29
@RichardIrons-neo4j RichardIrons-neo4j self-assigned this Nov 10, 2023
@RichardIrons-neo4j
Copy link
Contributor Author

Copy link
Contributor

@thelonelyvulpes thelonelyvulpes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome. If we could add test cases init setter properties and record classes that'd be great, though we can do that in a separate change if you would prefer.

Copy link
Contributor

@AndyHeap-NeoTech AndyHeap-NeoTech left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work!

@RichardIrons-neo4j RichardIrons-neo4j merged commit 37fb458 into neo4j:5.0 Nov 22, 2023
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants