ntsue
ntsue

Reputation: 2395

How to modify this Entity framework code first code?

I am brand new to Entity Framework code first, so any help or direction would be much appreciated.

I currently have the following classes:

public partial class Customer
{
    public int Id { get; set; }
    private ICollection<Address> _addresses;
}

public partial class Address
{
    public int Id { get; set; }
    public string Street { get; set; };
    public string City { get; set; };
    public string Zip { get; set; };
}

and the following

public partial class CustomerMap : EntityTypeConfiguration<Customer>
{
    public CustomerMap()
    {
        this.ToTable("Customer");
        this.HasKey(c => c.Id);

        this.HasMany<Address>(c => c.Addresses)
            .WithMany()
            .Map(m => m.ToTable("CustomerAddresses"));
    }
}

This works as I would expect, and creates a Customer, Address and CustomerAddresses table for the mapping. Now for my question.. what would I do if I needed to modify the code to produce the following...

I want to add a CompanyCode attribute to the "CustomerAddresses" table... and then instead of constructing a collection of addresses.. i want to be able to construct a hashtable, where the key is the CompanyCode, and the value is the collection of addresses.

So if I had the following:

Customer
ID     C1

Address
ID     A1
ID     A2

CustomerAddresses
CustomerID      C1
AddressID     A1
CompanyCode    ABC

CustomerID      C1
AddressID     A2
CompanyCode    ABC

CustomerID      C1
AddressID     A2
CompanyCode    XYZ

so then, Customer.Addresses["ABC"] would return a collection of addresses with ID, A1 and A2. Whereas Customer.Addresses["XYZ"] would return a collection of addresses with ID A2.

Any direction/help would be much appreciated... thanks.

Upvotes: 1

Views: 138

Answers (1)

Slauma
Slauma

Reputation: 177163

As far as I can tell it isn't possible to introduce such a navigation property with an indexer. Your indexer is actually a query and you must express this as a query. The only way I see is that you leave the navigation collection as is and introduce a second (not mapped) property that uses the navigation collection for the filter. The big drawback is that such a filter would happen in memory with LINQ-to-Objects and requires that you always load the full collection first from the database (by eager or lazy loading for example) before you filter the collection.

I would probably leave such a filter out of the entity itself and implement it in a repository or service class or generally the place/module where you load the entities from the database.

The first thing you need to do is exposing the CustomerAddresses table as an entity in your model because with your additional custom property CompanyCode you can't use a many-to-many relationship anymore, instead you need two one-to-many relationships. The new entity would look like this:

public partial class CustomerAddress
{
    public int CustomerId { get; set; }
    // public Customer Customer { get; set; } // optional

    public int AddressId { get; set; }
    public Address Address { get; set; }

    public string CompanyCode { get; set; }
}

And the Customer needs to be changed to:

public partial class Customer
{
    public int Id { get; set; }
    public ICollection<CustomerAddress> CustomerAddresses { get; set; }
}

You need to change the mapping to:

public CustomerMap()
{
    this.ToTable("Customer");
    this.HasKey(c => c.Id);

    this.HasMany(c => c.CustomerAddresses)
        .WithRequired() // or .WithRequired(ca => ca.Customer)
        .HasForeignKey(ca => ca.CustomerId);
}

And create a new mapping for the new entity:

public CustomerAddressMap()
{
    this.ToTable("CustomerAddresses");
    this.HasKey(ca => new { ca.CustomerId, ca.AddressId, ca.CompanyCode });
    // or what is the PK on that table?
    // Maybe you need an Id property if this key isn't unique

    this.HasRequired(ca => ca.Address)
        .WithMany()
        .HasForeignKey(ca => ca.AddressId);
}

Now, in some service class you could load the filtered addresses:

public List<Address> GetAddresses(int customerId, string companyCode)
{
    return context.CustomerAddresses.Where(ca =>
        ca.CustomerId == customerId && ca.CompanyCode == companyCode)
        .ToList();
}

Or, if you want to load the customer together with the filtered addresses:

public Customer GetCustomer(int customerId, string companyCode)
{
    var customer = context.Customer.SingleOrDefault(c => c.Id == customerId);
    if (customer != null)
        context.Entry(customer).Collection(c => c.CustomerAddresses).Query()
            .Where(ca => ca.CompanyCode == companyCode)
            .Load();
    return customer;
}

The last example are two database queries.

In the Customer entity you could use a helper property that projects the addresses out of the CustomerAddresses collection:

public partial class Customer
{
    public int Id { get; set; }
    public ICollection<CustomerAddress> CustomerAddresses { get; set; }

    public IEnumerable<Address> Addresses
    {
        get
        {
            if (CustomerAddresses != null)
                return CustomerAddresses.Select(ca => ca.Address);
            return null;
        }
    }
}

Keep in mind that this property does not query the database and the result relies on what is already loaded into CustomerAddresses.

Upvotes: 1

Related Questions