2

Writing a model for situation where I have two tables which are customers and users. Each user record might have an optional related customer record and vice versa, but none of them is a must. I figured out that FK Associations are not what I need, but Independent Associations are. But I just can find a way to make it work, I keep getting the 'Unable to determine the principal end...The principal end of this association must be explicitly configured using either the relationship fluent API or data annotations.' exception.

My models are very simple:

public class User
{
    [Key]
    public int          Id              { get; set; }
    [StringLength(20)]
    public string       CustomerId      { get; set; }
    public string       Password        { get; set; }
    public bool         Locked          { get; set; }

    //[ForeignKey("CustomerId")]
    public virtual Customer Customer    { get; set; }
}

public class Customer
{
    [Key]
    [Column("Id", TypeName = "nvarchar")]
    [StringLength(20)]
    public string       Id              { get; set; }   //  nvarchar    20
    [Required]
    public string       GivenName       { get; set; }   //  nvarchar    100
    [Required]
    public string       Surname         { get; set; }   //  nvarchar    100

    //[InverseProperty("Customer")]
    public virtual User User            { get; set; }
}

I've tried to add the ForeignKeyAttribute and InversePropertyAttribute, which are currently commented out, but they didn't help either. I would prefer to use data annotations and not fluent API, if it's possible in my case.

Jyrkka
  • 526
  • 1
  • 8
  • 26
  • You want to rely on Lazy Loading, correct? Have you had a look at this article: http://stackoverflow.com/questions/12606948/what-are-independent-associations-and-foreign-key-associations – DDiVita Jan 25 '15 at 15:50
  • @DDiVita Yes, Lazy Loading and yes, I've seen that question, but, the problem is that Principal has a `RequiredAttribute` and in my situation a user record can exist without a customer record. – Jyrkka Jan 25 '15 at 19:28
  • @DDiVita I've tried to use the `RequiredAttribute` as that answer suggests, but EF cannot find `Customer_ID`, then I added `ForeignKeyAttribute` and received this exception: \tSystem.Data.Entity.Edm.EdmAssociationEnd: : Multiplicity is not valid in Role 'User_Customer_Source' in relationship 'User_Customer'. Because the Dependent Role properties are not the key properties, the upper bound of the multiplicity of the Dependent Role must be '*'. – Jyrkka Jan 25 '15 at 19:45

1 Answers1

2

In one-to-one relation one end must be principal and second end must be dependent. Principal end is the one which will be inserted first and which can exist without the dependent one. Dependent end is the one which must be inserted after the principal because it has foreign key to the principal. When configuring one-to-one relationships, Entity Framework requires that the primary key of the dependent also be the foreign key.This problem is most easily solved by using a ForeignKey annotation on the dependent class to identify that it contains the foreign key. In your case, Customer could be the dependent and its key, Customer.UserId, should also be the foreign key. But both Keys must be declared using the same type:

public class User
{
   [Key]
   public int  Id  { get; set; }

   public virtual Customer Customer { get; set; }
}

public class Customer
{
   [Key, ForeignKey("User")]
   public int  UserId { get; set; }

   public virtual User User{ get; set; } 
}

I don't know how to resolve your problem using Data Annotations, but if you want to use Fluent Api, I think the configuration of the relationship would be like this:

 modelBuilder.Entity<User>().HasOptional(u => u.Customer).WithOptionalPrincipal(c => c.User);

Update

I understand your escenario, but if you have the same columns that you show in your model, I think you should have a one-to-many relationship mapped in DB instead one-to-one. Try to map your relationship this way:

public class User
{
    [Key]
    public int Id { get; set; }

    public string Password { get; set; }
    public bool Locked { get; set; }

    public string CustomerId { get; set; }

    [ForeignKey("CustomerId")]
    public virtual Customer Customer { get; set; }
}

public class Customer
{
    [Key]
    [Column("Id", TypeName = "nvarchar")]
    [StringLength(20)]
    public string Id { get; set; }   //  nvarchar    20
    [Required]
    public string GivenName { get; set; }   //  nvarchar    100
    [Required]
    public string Surname { get; set; }   //  nvarchar    100

    public virtual  ICollection<User> Users { get; set; }
}

Remember map your properties with the same column'names that you have in DB.

ocuenca
  • 38,548
  • 11
  • 89
  • 102
  • Thanks, I hope that it's possible to resolve this problem using `Data Annotations` though. Besides, I don't want these two tables to share same PK. – Jyrkka Jan 25 '15 at 19:37
  • I've tried to add the configuration as you advised, but I got an `SqlException` 0x80131904 Invalid column name 'User_Id'. Invalid column name 'User_Id'. Which is strange since I marked the `Id` with `KeyAttribute`. The row that throws this exception is `var user = users.Where(s => s.CustomerId.Equals(CustomerId)).FirstOrDefault();` and `users` is defined as `var users = from u in db.Users select u;` – Jyrkka Jan 25 '15 at 20:10
  • Which of them are you trying?. If you tried the second one, the property CustomerId in User is not mapped as FK, so, it's value always is null. I tried with this query var user = users.FirstOrDefault(s => s.Customer != null && s.Customer.Id.Equals(CustomerId)); and it worked. Did you drop the DB before test this model configuration? In the constructor of your Context add this: Database.SetInitializer(new DropCreateDatabaseIfModelChanges() ); – ocuenca Jan 25 '15 at 20:52
  • I've also tried to use `modelBuilder.Entity().HasOptional(c => c.User).WithOptionalPrincipal(u => u.Customer).Map(x => x.MapKey("CustomerId"));` but it gives me `MetadataException` Schema specified is not valid. Errors: (12,6) : error 0019: Each property name in a type must be unique. Property name 'CustomerId' was already defined. – Jyrkka Jan 25 '15 at 21:08
  • Yes, I'm talking about the second one, the first one shares PK, which is not an option in my case. If I map CustomerId as FK I get this ModelValidationException: One or more validation errors were detected during model generation: \tSystem.Data.Entity.Edm.EdmAssociationEnd: : Multiplicity is not valid in Role 'Customer_User_Target' in relationship 'Customer_User'. Because the Dependent Role properties are not the key properties, the upper bound of the multiplicity of the Dependent Role must be '*'. Why should I drop the database? I have a copy of the real data in my db. – Jyrkka Jan 25 '15 at 21:21
  • I suggested drop the DB because the model that you have apparently don't match with your DB schema. As I said before In one to one relationships, Entity Framework requires that the primary key of the dependent also be the foreign key. If you map CustomerId as FK is because you want to create a one to many relationship – ocuenca Jan 25 '15 at 21:54
  • Could you explain me what is the logic between Users and Customers? Maybe this way I could understand how are related your tables? – ocuenca Jan 25 '15 at 22:00
  • I've tried to use your query when commented out the `CustomerId` field, but got 4 times Invalid column name 'Customer_Id'. So I believe I have no other choice but to make Customer's `User` navigation property of type `ICollection` and each time to check manually that there is no duplicates? Since I can't make PK of the dependent to be FK. – Jyrkka Jan 25 '15 at 22:03
  • Sure, Customers are from Customer Management Module, whereas Users are from Self Service Module. Not every customer has access to Self Service, but there are some users who are not our customers yet. So there is a possibility for a user to become a customer, and he wont get more than one record in table of customers, and some customers may require self service access, and there is no reason for them to get two users. – Jyrkka Jan 25 '15 at 22:06
  • I think that I'll go with your advice, just one more question, I want in my Edit view for customer to add a check-box, that will be checked if current customer has user and this user's `Locked` property is set to false; upon submit I'll check if there is a user then I'll set the property to the inverted status of the check-box, and if there is no user but the check-box is checked I'll create one. So my question is how do I add this kind of check-box to my view, and how should I receive the status of that check box? Currently my view is strong typed, can this be done in a strong typed view? – Jyrkka Jan 27 '15 at 14:57
  • I'm not an specialist in Asp net mvc but I think you could resolve your problem creating a CustomerViewModel class with the properties that you want to edit, including the Locked property. Then, in the POST of your Action, you can apply the logic that you want. I suggest you read this post: http://weblogs.asp.net/shijuvarghese/view-model-pattern-and-automapper-in-asp-net-mvc-applications. – ocuenca Jan 27 '15 at 15:23