EFCF 6 Complex Type mapping failing after refactoring toward Value Object pattern

105 views Asked by At

I was refactoring a working implementation of a Complex Type class to apply the Value Object pattern which would make it an immutable type.

I got that working, I had thought, until I tried to set up the migrations for these changes (the main change that I think would matter to EF is that I changed some properties over to being get-only where they previously had setters). I am currently getting the following errors when I run Update-Database:

PM> update-database
Specify the '-Verbose' flag to view the SQL statements being applied to the target database.
Applying explicit migrations: [201701010745393_initial].
Applying explicit migration: 201701010745393_initial.
Running Seed method.
System.Data.Entity.Core.EntityCommandCompilationException: An error occurred while preparing the command definition. See the inner exception for details. ---> System.Data.Entity.Core.MappingException: 
(29,10) : error 3004: Problem in mapping fragments starting at line 29:No mapping specified for properties Contact.Address in Set Contacts.

(46,10) : error 3004: Problem in mapping fragments starting at line 46:No mapping specified for properties Company.Address in Set Companies.

(56,10) : error 3004: Problem in mapping fragments starting at line 56:No mapping specified for properties CompanyLocation.Address in Set Locations.

This would appear to be defying what I have been reading about how EF handles Complex Types, in that it should map things automatically whenever it encounters entities which make use of a property that is a Complex Type. This was previously the case, however it is no longer working with the change to an immutable class. I am not sure why that is.

Here are the relevant classes pertaining to these error messages:

public class Company : PocoBase
{
    [Required]
    public Address Address { get; set; }

    public virtual ICollection<Client> Clients { get; set; }

    public virtual ICollection<CompanyLocation> Locations { get; set; }

    [Required]
    public string Name { get; set; }
}

public class CompanyLocation : PocoBase
{
    [Required]
    public Address Address { get; set; }

    public virtual Company Company { get; set; }

    [ForeignKey("Company")]
    public Guid CompanyId { get; set; }

    public string Description { get; set; }

    public string Label { get; set; }
}

public class Contact : PocoBase
{
    public Address Address { get; set; }

    [Required]
    public string CellNumber { get; set; }

    public virtual Client Client { get; set; }

    [ForeignKey("Client")]
    public Guid ClientId { get; set; }

    public virtual Company Company { get; set; }

    [ForeignKey("Company")]
    public Guid CompanyId { get; set; }

    [Required]
    public string Email { get; set; }

    [Required]
    public string Name { get; set; }

    [Required]
    public string OfficeNumber { get; set; }
}

And, of course, the all-important Address class, which is now causing problems!

[ComplexType]
public class Address
{
    [Required]
    public string City { get; }

    [Required]
    public string Country { get; }

    [Required, StringLength(10, MinimumLength = 5)]
    public string PostalCode { get; }

    [Required, StringLength(2, MinimumLength = 2)]
    public string State { get; }

    [Required]
    public string StreetAddress { get; }

    public string UnitNumber { get; }

    public Address(string street, string city, string state, string zip, string unit = null) : this("United States", street, city, state, zip, unit) { }

    public Address(string country, string street, string city, string state, string zip, string unit = null)
    {
        VerifyZipCodeFormat(zip);

        Country = country;
        StreetAddress = street;
        City = city;
        State = state;
        PostalCode = zip;
        UnitNumber = unit;
    }

    private static void VerifyZipCodeFormat(string zip)
    {
        if (zip.Length > 5)
            if (zip[5] != '-')
                throw new ArgumentOutOfRangeException(nameof(zip), zip[5], "Postal Code must be in the format of \"XXXXX\" or \"XXXXX-XXXX\"");
            else if (zip.Length != 10)
                throw new ArgumentOutOfRangeException(nameof(zip), zip.Length, "Postal Code must be either 5 or 10 characters, in either the format of \"XXXXX\" or \"XXXXX-XXXX\"");
    }

    public Address WithCity(string city)
    {
        return new Address(Country, StreetAddress, city, State, PostalCode, UnitNumber);
    }
    public Address WithCountry(string country)
    {
        return new Address(country, StreetAddress, City, State, PostalCode, UnitNumber);
    }
    public Address WithStateOrProvince(string state)
    {
        return new Address(Country, StreetAddress, City, state, PostalCode, UnitNumber);
    }
    public Address WithStreetAddress(string street)
    {
        return new Address(Country, street, City, State, PostalCode, UnitNumber);
    }
    public Address WithUnitNumber(string unit)
    {
        return new Address(Country, StreetAddress, City, State, PostalCode, unit);
    }
    public Address WithZipOrPostalCode(string zip)
    {
        VerifyZipCodeFormat(zip);

        return new Address(Country, StreetAddress, City, State, zip, UnitNumber);
    }
}

I have tried to do this mapping manually, in the same vein that I believe the EFCF Convention is - Address_PropertyName in the migrations file that EF does try to create when I run Add-Migration, but this did not work either.

Neither having the [ComplexTypeAttribute] nor using the modelBuilder.ComplexType() lines have solved these error messages, so I have to assume it is related to this being an immutable class now.

I had hoped that adding the [ColumnAttribute] with column names would solve the issue somehow, as that seemed to be indicated in this post, but that did not solve the issue either.

1

There are 1 answers

2
Ivan Stoev On BEST ANSWER

Neither having the [ComplexTypeAttribute] nor using the modelBuilder.ComplexType() lines have solved these error messages, so I have to assume it is related to this being an immutable class now.

That's correct. EF6 does not map get only properties and there is no way tp change that behavior with data annotations or fluent configuration (note that the behavior in EF Core is different, but still causes issues), so providing private setters will fix the issue:

[ComplexType] // optional
public class Address
{
    [Required]
    public string City { get; private set; }

    [Required]
    public string Country { get; private set; }

    [Required, StringLength(10, MinimumLength = 5)]
    public string PostalCode { get; private set; }

    [Required, StringLength(2, MinimumLength = 2)]
    public string State { get; private set; }

    [Required]
    public string StreetAddress { get; private set; }

    public string UnitNumber { get; private set; }

    // ...
}

Now, I know this is not equivalent to the original implementation which ensures the fields are set only during the construction. But in general I would suggest you to forget the OO principles, patterns and practices when modeling EF entities. Entity classes are basically DTOs representing database tables, records and relationships with no associated business logic. Also note that EF tracks entities using reference identity, so applying immutable principle on entity types will only cause you problems (fortunately that doesn't apply for complex types).