0

I am trying to add a new user and some other associated entities including a claim as one transaction. My classes are basically defined as below. Note that I am using int primary keys on my User class and that it is an identity column (value is provided by the database on insertion).

public class User : IdentityUser<int>
{
    // custom props here
}

public class UserClaim : IdentityUserClaim<int>
{
    // i actually haven't added anything to this class, it's mainly here so 
    // i don't have to keep specifying the user's primary key type
}

public class OtherEntity 
{
    public int UserId { get; set; }

    [ForeignKey(nameof(UserId))]
    public User User { get; set; }

    // other stuff
}

I then want to add the user etc. to the database something like this:

User user = new User(){ /* set props */ };

OtherEntity other = new OtherEntity()
{
    User = user
};

UserClaim claim = new UserClaim()
{
    /* this is where the problem is */
    UserId = user.Id,
    ClaimType = "my-claim-type",
    ClaimValue = "my-claim-value"
};

context.AddRange(user, other, claim);
context.SaveChanges();

I can easily link the User to the OtherEntity because I have set up the navigation property so I can just add the User to it and entity framework takes care of the filling in the UserId column. I cannot do this with UserClaim because it doesn't have the navigation property. I could call context.SaveChanges() after adding the User and entity framework would get the User.Id created by the database for me which I could use to set UserId on the UserClaim, but that would mean two transactions.

I have tried adding the navigation property to my definition of UserClaim as follows:

public class UserClaim : IdentityUserClaim<int>
{
    [ForeignKey(nameof(UserId))]
    public User User { get; set; }
}

But I get following runtime error:

InvalidOperationException: The relationship from 'UserClaim.User' to 'User' with foreign key properties {'UserId' : int} cannot target the primary key {'Id' : int} because it is not compatible. Configure a principal key or a set of compatible foreign key properties for this relationship.

Is there a way of creating both the user, and the claim in the same transaction?

Ben
  • 5,525
  • 8
  • 42
  • 66

2 Answers2

0

Inserting related data is documented in Entity Framework: https://learn.microsoft.com/pl-pl/ef/core/saving/related-data And it is also well described in other topics, for example: Entity Framework Foreign Key Inserts with Auto-Id

Every of you entites need to be set correctly for relations (foreign keys) in your entites models (without them, EF don't know what to do) and when you are adding it, you need to start from beginning, so UserClaim must be set from your User entity, for example

    var user = new User(){
    //properites
    OtherEntity = new OtherEntity()
    {
        Id = 0, /*will be set automatically*/
        UserId = 0 /*will be set automatically*/
        /* other properites */
    };
    Claim = new UserClaim(){
        Id = 0, /*will be set automatically*/
        UserId = 0 /*will be set automatically*/
        /* other properites */
    }
}

ontext.Add(user);
context.SaveChanges();

You didn't provide all the information about your relations, I've just assumed this from your code.

PS. AddRange has only one parameter.

EDIT: To clarify my answer, for everything to work, AddRange/Add need to be called with your base entity and relations inside, in tree manner. But first you need to configure your models, something like that.

public class User : IdentityUser<int>
{
    [ForeignKey("UserClaimId")]
    public virtual UserClaim Claim {get;set;}
}

public class UserClaim : IdentityUserClaim<int>
{
    [ForeignKey("UserId")]
    public virtual User User {get;set;}
}

public class OtherEntity 
{
    public int UserId { get; set; }

    [ForeignKey("UserId")]
    public User User { get; set; }

    // other stuff
}

You can also use OnModelCreating to set up entites, as described in documentation: https://learn.microsoft.com/en-us/ef/core/modeling/relationships

Entity Framework right now don't know anything about relations in Database, data types etc.

Updated code example

Edit, don't have enough reputation to add comment to your answer

Well, EF still needs to track entities. I would need to write and run some examples to check if there is some way to improve trips to database. Maybe someone could tell you more about mechanism that is behind adding related-data and if there is a way to improve it. There are some libraries that helps with improving performance of saving changes to database, for example: https://entityframework-extensions.net You could try it.

Michał
  • 106
  • 1
  • 6
  • AddRange take a params array, abs can be used as above. – Ben Oct 14 '19 at 17:13
  • I tried to distil my examples down to make it clearer, obviously failed. I know how to handle related entities. My problem is specifically with the relationship between the user and claim classes derived from the asp.net identity classes. The classes provided by the identity framework do not explicitly define the relationship as far as I can tell, though it must do it somewhere as the foreign keys exist in my database. – Ben Oct 14 '19 at 17:16
  • FYI IdentityUser doesn't have a Claims property to add the claim to. I can add one to my derived User class but I dont know how to properly configure that with with entity Framework and get the error at the end of my question – Ben Oct 14 '19 at 17:18
  • I needed to check documentation, because I don't remember a structure of each object, IdentityUserClaim have UserId property https://learn.microsoft.com/en-us/previous-versions/aspnet/mt151785%28v%3dvs.108%29 but don't have reference to User object as I can see there, and it's need to be set if you want do use related data insert. You need to specify it. I can see what you are trying to do, but still a little bit confused. – Michał Oct 14 '19 at 17:48
  • Ok I shall try and clear things up when I get back to my PC. Thanks for your time thus far – Ben Oct 14 '19 at 17:57
  • It looks like you are trying to map a one-to-one relationship between `User` and `UserClaim` above which should be a `one-to-many`. After sleeping on it I realized I was asking the wrong question. I will update shortly with the correct question/answer. Thanks for your time Michal – Ben Oct 15 '19 at 07:51
  • My bad, I didn't check documentation and assumed that this is one-to-one relationship. – Michał Oct 15 '19 at 15:59
0

My question should have been: "How do I add navigation properties between ASP.NET Identity classes?"

If I had looked for the answer to that I would have found the microsoft docs explaining how to do it!

The docs linked above tell you how to add the User.UserClaims navigation property:

public class User : IdentityUser<int>
{
    public virtual ICollection<UserClaim> Claims { get; set; }
}

public class DataContext : IdentityDbContext</* Identity classes */>
{
    protected override void OnModelCreating(ModelBuilder builder)
    {
        builder.Entity<User>(e =>
        {
            e.HasMany(u => u.Claims)
             .WithOne()
             .HasForeignKey(c => c.UserId) 
             .IsRequired();
        });
    }
}

But it doesn't show how to make the reverse UserClaim.User navigation property. I worked out that this can be done like this:

public class UserClaim : IdentityUserClaim<int>
{
    public virtual User User { get; set; }
}

public class DataContext : IdentityDbContext</* Identity classes */>
{
    protected override void OnModelCreating(ModelBuilder builder)
    {
        builder.Entity<User>(e =>
        {
            e.HasMany(u => u.Claims)
             .WithOne(c => c.User) // <- this line is different
             .HasForeignKey(c => c.UserId) 
             .IsRequired();
        });
    }
}

You can then create a new user and claim at the same time as per my question:

User user = new User(){ /* set props */ };

UserClaim claim = new UserClaim()
{
    User = user, // <- this is the bit you can't do without the nav. property
    ClaimType = "my-claim-type",
    ClaimValue = "my-claim-value"
};

context.AddRange(user, claim);
context.SaveChanges();

I guess it's common sense, though I didn't realise until I inspected the actual SQL hitting the database, but this will still require two trips to the database even though we are only calling .SaveChanges() once! Entity Framework will first save the User and get the ID for the inserted row which it will then use when inserting the UserClaim.

Ben
  • 5,525
  • 8
  • 42
  • 66