7

I have problem in which i would like to create N, two in the example, user objects (e.g. Customer & Supplier) which all inherent from the asp.net IdentityUser object. These object have very different additional data besides the the data from the IdentityUser. I would like to use the IdentityUser user as this gives me a flexible way of taking care of authentication and authorization.

This example has been very stripped down but should supply sufficient information concerning the not being able to create a concrete user (e.g. Customer of Supplier). It seems i need to use the UserManager object as this also takes care of creating for example the password hash and additional security information.

I get presented the following error:

{"Attaching an entity of type 'Supplier' failed because another entity of the same type already has the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate."}

Classes which inherent from IdentityUser

 public class Customer : IdentityUser
 {
    public string CustomerProperty { get; set; }
 }

 public class Supplier : IdentityUser
 {
    public string SupplierProperty { get; set; }
 }

Database context class

 public class ApplicationDbContext : IdentityDbContext {

      public ApplicationDbContext() : base("ApplicationDbContext")
      {
         Database.SetInitializer(new ApplicationDbInitializer());
      }

      public DbSet<Customer> CustomerCollection { get; set; }
      public DbSet<Supplier> SupplierCollection { get; set; }
 }

Seeding class which throws the exception

 public class ApplicationDbInitializer : DropCreateDatabaseAlways<ApplicationDbContext>
 {
    protected override void Seed(ApplicationDbContext context)
    {
        var userStore = new UserStore(context);
        var userManager = new UserManager(userStore);


        // Seed customer user which inherents from asp.net IdentityUser 
        var user = userManager.FindByEmail("customer@customer.com");
        if (user == null)
        {
            user = new User()
            {
                UserName = "customer@customer.com",
                Email = "customer@customer.com"
            };

            userManager.Create(user, userPassword);

            var customerUser = new Customer()
            {
                Id = user.Id,
                CustomerProperty = "Additional Info"
            };

            context.Entry(customerUser).State = EntityState.Modified;
            context.SaveChanges();
        }

        // Seed supplier user which inherents from asp.net IdentityUser 
        var user = userManager.FindByEmail("supplier@supplier.com");
        if (user == null)
        {
            user = new User()
            {
                UserName = "supplier@supplier.com",
                Email = "supplier@supplier.com"
            };

            userManager.Create(user, userPassword);

            var supplierUser = new Supplier()
            {
                Id = user.Id,
                IBAN = "212323424342234",
                Relationship = "OK"
            };

            context.Entry(supplierUser).State = EntityState.Modified;
            context.SaveChanges();
        }
    }
}

**** UPDATE ****

The solution below works but i am still struggling with two issues:

  1. I would always like to have one user type (e.g. Customer of Supplier) associated with the IdentityUser. I though about using an interface but this doesn't work.
  2. If i also add the virtual reference towards the IdentityUser on the user types i get an 'Unable to determine the principal end of an association between the types 'ApplicaitonUser' and 'Supplier'. The principal end of this association must be explicitly configured using either the relationship fluent API or data annotations.' exception.

Classes

 public class Customer 
 {
    [Key]
    public int CustomerId { get;set; }
    public string CustomerProperty { get; set; }

    *public virtual User User { get; set; }*

 }

 public class Supplier 
 {
    [Key]
    public int SupplierId { get;set; }
    public string SupplierProperty { get; set; }

    *public virtual User User { get; set; }*
 }

**Class IdentityUser (which works) **

public class User : IdentityUser
{
    public virtual Supplier Supplier { get; set; }
    public virtual Customer Customer { get; set; }
}

**Class IdentityUser (what i would like) **

public class User : IdentityUser
{
    public virtual IConcreteUser ConcreteUser{ get; set; }
}

Database context class

 public class ApplicationDbContext : IdentityDbContext {

      public ApplicationDbContext() : base("ApplicationDbContext")
      {
         Database.SetInitializer(new ApplicationDbInitializer());
      }

      public DbSet<Customer> CustomerCollection { get; set; }
      public DbSet<Supplier> SupplierCollection { get; set; }
 }

**Seeding class **

 public class ApplicationDbInitializer : DropCreateDatabaseAlways<ApplicationDbContext>
 {
protected override void Seed(ApplicationDbContext context)
{
    var userStore = new UserStore(context);
    var userManager = new UserManager(userStore);
    var roleManager = new RoleManager(roleStore);

    var user = userManager.FindByEmail("customer@customer.com");
    if (user == null)
    {
        user = new ApplicationUser()
        {
            UserName = "customer@customer.com",
            Email = "customer@customer.com"
            Customer = new Customer()
            {
                CustomerProperty = "Additional Info"
            }
        };

        userManager.Create(user, userPassword);
        roleManager.AddUserToRole("Customer");
    }

    user = userManager.FindByEmail("supplier@supplier.com");
    if (user == null)
    {
        user = new ApplicationUser()
        {
            UserName = "supplier@supplier.com",
            Email = "supplier@supplier.com",
            Supplier = new Supplier()
            {
                IBAN = "212323424342234",
                Relationship = "OK"
            }
        };

        userManager.Create(user, userPassword);
        roleManager.AddUserToRole("Supplier");
    }
}

}

Frank
  • 3,959
  • 4
  • 19
  • 24
  • 6
    I strongly would suggest to fix the broken design. There is zero need to use inheritance on the ASP.NET level. A login user is funddamentally different from the underlying entity in whatever database management or functioanl group you use. I.e. make a simple asp.net user and do not tie ot to your complex model. Keep them separate. – TomTom Nov 21 '14 at 10:24
  • Have you tried following the instructions in the error and setting EntityState.Addedd instead of EntityState.Modified? – Ben Robinson Nov 21 '14 at 10:27
  • I think the problem lies in the "mixed state": It is neither `EntityState.Modified` nor `EntityState.Added` as `User` is already added, but e.g. `Supplier` not... – Christoph Fink Nov 21 '14 at 10:30
  • @TomTom: if i keep the user (e.g. Customer & Supplier) how would i know which user logs in? They will all login with credentials from the IdentityUser. – Frank Nov 21 '14 at 10:55
  • You tell the user the ID of the internal user. But you do not put the database level inherited user up to the authentication service. – TomTom Nov 21 '14 at 10:57
  • @Ben Robinson Setting the EntityState.Addedd does not work and throws an System.Data.Entity.Validation.DbEntityValidationException error. – Frank Nov 21 '14 at 10:59
  • @ChrFin Do you have any suggestion for fixing this? – Frank Nov 21 '14 at 11:00
  • @TomTom what you are saying is that you extend the Customer and Supplier with an additional User Id which references the IdentityUser Id - correct? This does not seem very graceful as this will split certain updates (e.g. username, email and additional information per user type like IBAN, relationship and customerproperty) for a user resides in two tables. – Frank Nov 21 '14 at 11:05
  • No. If they do not have an ID field you have a problem already. What I say is KEEP ASP.NET AND YOUR SYSTEM SEPARATE. Noone needs IBAN in ASP.NET identity - so do not dump it there. – TomTom Nov 21 '14 at 11:07
  • @TomTom that is strange as MSDN (and actually many others) supply examples for extending the IdentityUser (http://blogs.msdn.com/b/webdev/archive/2013/10/16/customizing-profile-information-in-asp-net-identity-in-vs-2013-templates.aspx). They seem to add additional data to the IdentityUser. I understand you don't like the way the model has been build, but can it work? Thank you for your comments BTW. – Frank Nov 21 '14 at 11:10
  • It breaks SOLID principles. People making examples normally do not think about maintainability and other irrelevant factors. Especially MSDN is famous for making examples out of antipatterns and worst practices. Adding data is ok - but not adding hugh amounts. Keep the ASP.NET level idntity an object only used for this (entity id, roles etc.) and then load the real object when you need it. Do not tie your application level business objects to Identity. THis is turning best practices upside down. – TomTom Nov 21 '14 at 11:14
  • @TomTom with the way you propose the model would the user objects, besides their own id en additional custome fields ofcourse, contain an id which links to the IdentityUser correct? Otherwise i don't understand how i know, and which type of user, logged in. I need to make a correlation somewhere right? – Frank Nov 21 '14 at 11:14
  • If setting the state to added causes an `DbEntityValidationException` then that you have moved pas the posted problem but you are now seeing a second problem that was not apparent because of the problem you have posted. The exception is caused invalid data, if you inspect the exception you should get appropriate message that tell you what is wrong with the data you are trying to save. – Ben Robinson Nov 21 '14 at 11:22
  • Why shouldn't a supplier be allowed to also be a customer? – Christoph Fink Nov 21 '14 at 13:33
  • @ChrFin - A supplier would be allowed to be a customer but with a different account as there are different (asp.net identity) security rules per type of user (e.g. two legged authentication vs. normal login). The bigger problem is that i can't add a virtual application user reference to the user types. – Frank Nov 21 '14 at 13:37
  • 1
    Thats again bad design IMO. Requiring TFA for suppliers is fine, but THAT can then very well be a customer too, can't it? So simply enable/enforce TFA if the user is a supplier. Other security rules should be role-based anyhow IMO... – Christoph Fink Nov 21 '14 at 13:40

2 Answers2

12

As others do too I think this is a design problem. There are some alternative approaches like:

  1. use roles to define the "user-type" (a user can be supplier AND customer)
  2. make the Supplier and Customer entities a relation not extension of the user

e.g.:

public class ApplicationUser : IdentityUser
{
    public virtual Customer Customer { get; set; }
    public virtual Supplier Supplier { get; set; }
}

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

    public virtual ApplicationUser User { get; set; }
    public string CustomerProperty { get; set; }
}

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

    public virtual ApplicationUser User { get; set; }
    public string SupplierProperty { get; set; }
}

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Supplier> Suppliers { get; set; }
}

public class ApplicationDbInitializer
             : DropCreateDatabaseAlways<ApplicationDbContext>
{
    protected override void Seed(ApplicationDbContext context)
    {
        var userStore = new UserStore(context);
        var userManager = new UserManager(userStore);
        var roleManager = new RoleManager(roleStore);

        var user = userManager.FindByEmail("customer@customer.com");
        if (user == null)
        {
            user = new ApplicationUser()
            {
                UserName = "customer@customer.com",
                Email = "customer@customer.com"
                Customer = new Customer()
                {
                    CustomerProperty = "Additional Info"
                }
            };

            userManager.Create(user, userPassword);
            roleManager.AddUserToRole("Customer");
        }

        user = userManager.FindByEmail("supplier@supplier.com");
        if (user == null)
        {
            user = new ApplicationUser()
            {
                UserName = "supplier@supplier.com",
                Email = "supplier@supplier.com",
                Supplier = new Supplier()
                {
                    IBAN = "212323424342234",
                    Relationship = "OK"
                }
            };

            userManager.Create(user, userPassword);
            roleManager.AddUserToRole("Supplier");
        }
    }
}

and in your logic you can do something like:

if (User.IsInRole("Customer"))
{
    // do something
}

DISCLAIMER: This is not a "copy&paste" example and should just give you an idea of a different approach.

Christoph Fink
  • 22,727
  • 9
  • 68
  • 113
  • an excellent example but don't the Supplier and Customer objects need their own key (e.g. SupplierId & CustomerId)? And I don't yet understand how the relation between the SupplierId, CustomerId and the ApplicationUser is known on a database level. Does the ApplicationUser has foreign keys towards the Supplier and Customer or the other way around? How is this enforced int he code besides linking the objects using virtual properties? – Frank Nov 21 '14 at 12:35
  • `need their own key` - yes, as disclaimed not a c&p example, but I added them now. `how the relation between the SupplierId, CustomerId and the ApplicationUser is known on a database level` - the `public virtual ApplicationUser User { get; set; }` navigation property will be converted to a FK by EF. – Christoph Fink Nov 21 '14 at 12:40
  • This works well if i have i only have the virtual relationship inthe ApplicationUser side. If i also add this virtual relationshop towards the ApplicationUser in the Supplier object i get the: 'Unable to determine the principal end of an association between the types 'ApplicationUser' and 'Supplier'. The principal end of this association must be explicitly configured using either the relationship fluent API or data annotations.' exception. As i also only want 1 type of user (supplier/customer) associated with the ApplicationUser i thought of using an interface. this doesn't work? – Frank Nov 21 '14 at 12:57
  • Can you update the question with your current code? If you defined it correctly this shouldn't be a problem. I have multiple of such relations "on my users"... – Christoph Fink Nov 21 '14 at 13:03
  • appending: '[Key, ForeignKey("ApplicationUser")]' to the user types resolved the issue of the navigating between the user to to the applicatiouser :) – Frank Nov 21 '14 at 13:52
6

I just resolved a similar problem. I created a navigation property of abstract type DomainUser in my AppUser (that inherits from Identity User)

public class AppUser : IdentityUser
{
    public DomainUser DomainUser { get; set; }
}

DomainUser looks like this:

public abstract class DomainUser : IAggregateRoot
{
    public Guid Id { get; set; }
    public AppUser IdentityUser { get; set; }
}

I inherit from DomainUser in all concrete domain user types:

public class AdministrationUser : DomainUser
{
    public string SomeAdministrationProperty { get; set; }
}

public class SupplierUser : DomainUser
{
    public string SomeSupplierProperty { get; set; }
}

public class Customer : DomainUser
{
    public string SomeCustomerProperty { get; set; }
}

And in DbContext in OnModelCreating method I configured Entity Framework to store all entities inherited from DomainUser in separate tables (it's called Table per Concrete Type). And configured one to one relationship between IdentityUser and DomainUser:

modelBuilder.Entity<DomainUser>()
            .Map<AdministrationUser>(m =>
            {
                m.MapInheritedProperties();
                m.ToTable("AdministrationUsers");
            })
            .Map<SupplierUser>(m =>
            {
                m.MapInheritedProperties();
                m.ToTable("SupplierUsers");
            })
            .Map<Customer>(m =>
            {
                m.MapInheritedProperties();
                m.ToTable("Customers");
            });

modelBuilder.Entity<DomainUser>()
            .HasRequired(domainUser => domainUser.IdentityUser)
            .WithRequiredPrincipal(groomUser => groomUser.DomainUser);

This code added column "DomainUser_Id" to table AspNetUsers and now I'm able to access IdentityUser navigation property in each domain user and DomainUser navigation property in AppUser.

tseshevsky
  • 761
  • 8
  • 10