8

I have the situation where a User can have several addresses. Accordingly, I have an ICollection on my user class. But I also want the user to be able to choose a default address. So I've done the following:

public class User 
{
    public int Id { get; set; }
    public int? DefaultAddressId { get; set; }
    [ForeignKey("DefaultAddressId")]
    public virtual Address DefaultAddress { get; set; }
    public virtual ICollection<Address> Addresses { get; set; }
    //properties were removed for purpose of this post
}

I would like to remove the public virtual Address DefaultAddress { get; set; } altogether, keep the DefaultAddressId and map it using the Fluent API instead because the current setup is causing a lot of trouble (in this and other classes where I have a similar setup). So can this be done using the fluent api?

UPDATE: The address class currently doesn't have any reference to the User class, it's a uni-directional relationship. But yes, an address belongs to only ONE user, it's not a many to many relationship. Here's the address class:

public class Address
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Details { get; set; }
    public virtual Area Area { get; set; }
}
Kassem
  • 8,116
  • 17
  • 75
  • 116
  • How does the `Address` class look like? Does it have a reference to a single `User` so that every Address belongs uniquely to one user? Or can users share an Address in which case I'd expect a many-to-many relationship? – Slauma Mar 30 '11 at 18:48
  • @Slauma: I've just updated the question. Check it out please. – Kassem Mar 30 '11 at 19:13
  • Why did you have problems with `public virtual Address DefaultAddress { get; set; }`? In my opinion this property is the best way to express the relationship you'd like to have - and I can't see a problem to map this to the DB. If you remove the property (and don't have a `User` property at the Address class) `DefaultAddressId` becomes basically a scalar property which doesn't participate in a relationship at all. So you would lose foreign key contraint checks in the database (every number can be in this property, no matter if an address with this ID exists or not). – Slauma Mar 30 '11 at 19:56
  • @Slauma: I'm not sure why there are problems with that. It was working fine before upgrading to EF 4.1 in addition to other problems that I have not yet solved yet. [refer to this question](http://stackoverflow.com/questions/5456063/dbentityvalidationexception-after-upgrading-to-ef-4-1) – Kassem Mar 30 '11 at 20:39
  • I've tested your model as it is above in your questions and it works fine for me. This model is actually exactly the solution I would choose to work with an Address collection and an DefaultAddress. I believe, your complications are somewhere else. – Slauma Mar 30 '11 at 21:01

4 Answers4

10

I would personally move the Foreign Key relation from User to Address, and add an IsDefaultAddress property on the address class.

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

    // This property marks the FK relation
    public virtual User User { get; set; }

    public string Name { get; set; }
    public string Details { get; set; }
    public virtual Area Area { get; set; }

    // This property signals whether this is the user's default address
    public bool IsDefaultAddress { get; set; }
}

EF will know that it needs a Foreign Key relation between Address and User.

This would simplify your model a great deal. That is, of course, if an address can only belong to one user (as asked by Slauma in the comments).

Sergi Papaseit
  • 15,999
  • 16
  • 67
  • 101
  • @Sergi Papaseit: +1 for the answer. I do not know why I like complicating things. I should've done that right from the start. I guess I was obsessing about "normalization" of the database. I guess sometimes there has to be some compromise for the sake of simplicity. Thanks again :) – Kassem Mar 30 '11 at 19:56
  • @Kassem - It's always good to keep [K.I.S.S](http://en.wikipedia.org/wiki/KISS_principle) and [YAGNI](http://en.wikipedia.org/wiki/YAGNI) in mind ;) Happy to help :) – Sergi Papaseit Mar 30 '11 at 19:57
  • 5
    Honestly I would go with the solution in the question. I don't know why it shouldn't work (I had just tested it and it's fine. It creates 2 relationships.) With your solution, one has to make sure in business logic that `IsDefaultAddress` isn't set on more than one Address of a user. And if you change the DefaultAddress one has to search for the old address and reset the old flag. The solution in the question expresses the "natural" relationships of the model better in my opinion and one doesn't need to worry about duplicate default flags. – Slauma Mar 30 '11 at 20:55
  • @Slauma - I must say, you do have a good point. My initial gut feeling would have been to do as I suggested, but it's true that having a `DefaultAddress` property in the `User` class expresses the relationship clearly. I guess it all depends on what you want to express/achieve. I would still add a reference to `User` in the `Address` class though. – Sergi Papaseit Mar 30 '11 at 21:53
  • @Slauma: This is actually why I went with the solution in the question. I did not want to worry about duplicate default addresses which might be caused by Sergi's solution. But, I donno, I want to keep things as simple as possible in order to track down what's causing problems in my model. But naturally, if I add a reference to the User in the Address class, it should work fine? – Kassem Mar 30 '11 at 21:59
  • @Kassem: I've added a test example in an answer here. You can try that out and should see that it works with your simple model in the question. Maybe it helps you to find the problems you are having in your "real" model by comparing the two models and their configuration. – Slauma Mar 31 '11 at 00:12
8

Your original model in the question should work. You can test it quite easily:

  • Create new console application (VS 2010)
  • Name it "EFTestApp"
  • Add reference to "EntityFramework.dll"
  • Delete content of Program.cs and copy the following code into the file

Program.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;

namespace EFTestApp
{
    public class User
    {
        public int Id { get; set; }
        public int? DefaultAddressId { get; set; }
        [ForeignKey("DefaultAddressId")]
        public virtual Address DefaultAddress { get; set; }
        public virtual ICollection<Address> Addresses { get; set; }
    }

    public class Address
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class Context : DbContext
    {
        public DbSet<User> Users { get; set; }
        public DbSet<Address> Addresses { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new Context())
            {
                try
                {
                    User user = new User() { Addresses = new List<Address>() };

                    Address address1 = new Address() { Name = "Address1" };
                    Address address2 = new Address() { Name = "Address2" };

                    user.Addresses.Add(address1);
                    user.Addresses.Add(address2);

                    context.Users.Add(user);

                    context.SaveChanges();
                    // user has now 2 addresses in the DB and no DefaultAddress

                    user.DefaultAddress = address1;
                    context.SaveChanges();
                    // user has now address1 as DefaultAddress

                    user.DefaultAddress = address2;
                    context.SaveChanges();
                    // user has now address2 as DefaultAddress

                    user.DefaultAddress = null;
                    context.SaveChanges();
                    // user has now no DefaultAddress again
                }
                catch (Exception e)
                {
                    throw;
                }
            }
        }
    }
}

In SQL Server Express it creates a new DB called "EFTestApp.Context". You can set breakpoints on every SaveChanges above, step over and watch the changes in the DB.

If you look at the relationships in the database then there are two, and in table Addresses in the DB is a foreign key column User_Id.

I think you could also remove public int? DefaultAddressId { get; set; } and [ForeignKey("DefaultAddressId")]. It creates the same database tables and relationships with an optional DefaultAddress.

Perhaps you want the relationship Address -> User as required (Addresses cannot live alone in the DB without a User). Then you can add this to the Context class:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<User>()
                .HasMany(u => u.Addresses)
                .WithRequired();
}

It makes User_Id in the Addresses table non nullable and sets up cascading delete by default. So, when a user gets deleted all its addresses get deleted as well.

Slauma
  • 175,098
  • 59
  • 401
  • 420
  • You can also use the `[Required]` attribute on `Addresses` in the `User` class to mark the relation as required (EF will add an On Delete cascade as well). I'm not sure how that'll work on a collection though. – Sergi Papaseit Mar 31 '11 at 06:52
  • 1
    @Sergi Papaseit: Yes, I tried that too to place the `[Required]` attribute on the `Addresses` collection but it doesn't work. There is no compiler error or EF exception, the foreign key simply stays nullable, so we still have an optional relationship. I guess we would need to add a `User` property on the `Address` class and then mark this property as `[Required]` if we wished to use DataAnnotations exclusively and no Fluent API. – Slauma Mar 31 '11 at 10:57
  • I am using this as well. But wondering if there is a way to avoid the intermediate savechanges before setting the default? – ravi Nov 26 '11 at 21:23
  • @ravi: Did you test it to remove the first `SaveChanges`? It should work, doesn't it? The intermediate `SaveChanges` were only for debugging and demonstration purposes to show the changes in the DB. – Slauma Nov 27 '11 at 00:08
  • yes. I tried it and it does not work. It seems if the primary key is an incremental identifier than it does not know what the id of the default is without saving first. I think it would work if we use guid as the pk. – ravi Nov 27 '11 at 23:36
2

DefaultAddressId doesn't need any specific mapping because it will be just column in User table without any relation (FK) to Address table. There will be no relation created because navigation property doesn't exist on either side. Also it should be one-to-one relation which will not work because EF doesn't support unique keys.

I like solution provided by @Sergi Papaseit

Ladislav Mrnka
  • 360,892
  • 59
  • 660
  • 670
  • Yeah I guess I'll just go with Sergi's answer. But do you suggest that I should add a navigational property back to User from the Address class? – Kassem Mar 30 '11 at 19:57
  • Follow @Sergi's approach you will need just `Addresses` collection in `User`. – Ladislav Mrnka Mar 30 '11 at 20:04
0

You don't need to map it if you are removing the DefaultAddress property. You can just have the property there and EF should know how to map it provided DefaultAddressId is in the User table

Gats
  • 3,452
  • 19
  • 20
  • How is EF supposed to know how to map it? Would it still be nullable? Because if a user hasn't yet added an address, this should be null and it shouldn't cause an error when adding the user to the database... – Kassem Mar 30 '11 at 17:56
  • Yes it would still be nullable. I have found some problems mapping a 1 to 1 relationship using the fluent API as it doesn't seem to have a way to map a key to a specific column. In my case I've used 1 to many in the mapping even though the object only contains 1 instance of the child. That's done using HasOptional(u => u.DefaultAddress).WithMany().HasKey(u => u.DefaultAddressId); – Gats Apr 01 '11 at 01:18