Custom Membership with Microsoft.AspNet.Identity - CreateLocalUser fails

asp.net-identity asp.net-mvc asp.net-mvc-5 entity-framework owin

Question

I've been trying to implement a custom version of the new Identity features in ASP.NET 4.5 (Microsoft.AspNet.Identity), using Visual Studio 2013. After many hours of playing around with this, I've simplified my code in an effort to get it running without errors. I've listed my code below. When doing a Local Registration, the database tables are created, but the CreateLocalUser method fails. I'm hoping that someone can help me identify the changes needed.

Models/MembershipModel.cs

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Linq;
using System.Web;

namespace thePulse.web.Models
{
    public class PulseUser : IUser
    {
        public PulseUser() { }
        public PulseUser(string userName) 
        {
            UserName = userName;
        }

        [Key]
        public string Id { get; set; }
        [Required]
        [StringLength(20)]
        public string UserName { get; set; }
        [StringLength(100)]
        public string Email { get; set; }
        [Column(TypeName = "Date")]
        public DateTime? BirthDate { get; set; }
        [StringLength(1)]
        public string Gender { get; set; }
    }

    public class PulseUserClaim : IUserClaim
    {
        public PulseUserClaim() { }

        [Key]
        public string Key { get; set; }
        public string UserId { get; set; }
        public string ClaimType { get; set; }
        public string ClaimValue { get; set; }

    }

    public class PulseUserSecret : IUserSecret
    {
        public PulseUserSecret() { }
        public PulseUserSecret(string userName, string secret)
        {
            UserName = userName;
            Secret = secret;
        }

        [Key]
        public string UserName { get; set; }
        public string Secret { get; set; }

    }

    public class PulseUserLogin : IUserLogin
    {
        public PulseUserLogin() { }
        public PulseUserLogin(string userId, string loginProvider, string providerKey) 
        {
            LoginProvider = LoginProvider;
            ProviderKey = providerKey;
            UserId = userId;
        }

        [Key, Column(Order = 0)]
        public string LoginProvider { get; set; }
        [Key, Column(Order = 1)]
        public string ProviderKey { get; set; }
        public string UserId { get; set; }
    }

    public class PulseRole : IRole
    {
        public PulseRole() { }
        public PulseRole(string roleId)
        {
            Id = roleId;
        }

        [Key]
        public string Id { get; set; }
    }

    public class PulseUserRole : IUserRole
    {
        public PulseUserRole() { }

        [Key, Column(Order = 0)]
        public string RoleId { get; set; }
        [Key, Column(Order = 1)]
        public string UserId { get; set; }
    }

    public class PulseUserContext : IdentityStoreContext
    {
        public PulseUserContext(DbContext db) : base(db)
        {
            Users = new UserStore<PulseUser>(db);
            Logins = new UserLoginStore<PulseUserLogin>(db);
            Roles = new RoleStore<PulseRole, PulseUserRole>(db);
            Secrets = new UserSecretStore<PulseUserSecret>(db);
            UserClaims = new UserClaimStore<PulseUserClaim>(db);
        }
    }

    public class PulseDbContext : IdentityDbContext<PulseUser, PulseUserClaim, PulseUserSecret, PulseUserLogin, PulseRole, PulseUserRole>
    {
    }
}

Changes to Controllers/AccountController.cs

public AccountController() 
{

    IdentityStore = new IdentityStoreManager(new PulseUserContext(new PulseDbContext()));
    AuthenticationManager = new IdentityAuthenticationManager(IdentityStore);
}

//
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        try
        {
            // Create a profile, password, and link the local login before signing in the user
            PulseUser user = new PulseUser(model.UserName);
            if (await IdentityStore.CreateLocalUser(user, model.Password))
            {
                await AuthenticationManager.SignIn(HttpContext, user.Id, isPersistent: false);
                return RedirectToAction("Index", "Home");
            }
            else
            {
                ModelState.AddModelError("", "Failed to register user name: " + model.UserName);
            }
        }
        catch (IdentityException e)
        {
            ModelState.AddModelError("", e.Message);
        }
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

As I said above, this implementation fails when the CreateLocalUser method fails (Microsoft.AspNet.Identity.EntityFramework). I cannot figure out why.

1
22
7/29/2013 9:45:39 AM

Accepted Answer

The issue here is that IdentityStoreManager has strong dependency on the default implementation of identity EF models. For example, the CreateLocalUser method will create UserSecret and UserLogin objects and save them to stores, which won't work if the store is not using the default model type. So if you customize the model type, it won't work smoothly with IdentityStoreManager.

Since you only customize the IUser model, I simplified the code to inherit custom user from default identity user and reuse other models from identity EF models.

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Linq;
using System.Web;

namespace WebApplication11.Models
{
    public class PulseUser : User
    {
        public PulseUser() { }
        public PulseUser(string userName) : base(userName)
        {
        }

        [StringLength(100)]
        public string Email { get; set; }
        [Column(TypeName = "Date")]
        public DateTime? BirthDate { get; set; }
        [StringLength(1)]
        public string Gender { get; set; }
    }

    public class PulseUserContext : IdentityStoreContext
    {
        public PulseUserContext(DbContext db) : base(db)
        {
            this.Users = new UserStore<PulseUser>(this.DbContext);
        }
    }

    public class PulseDbContext : IdentityDbContext<PulseUser, UserClaim, UserSecret, UserLogin, Role, UserRole>
    {
    }
}

The code above should work with preview version of Identity API.

The IdentityStoreManager API in upcoming release is already aware of this issue and changed all the non-EF dependency code into a base class so that you can customize it by inheriting from it. It should solve all the problems here. Thanks.

12
7/29/2013 6:47:51 PM

Popular Answer

PulseUser.Id is defined as a string but doesn't appear to be set to a value. Were you meant to be using a GUID for the Id? If so, initialise it in the constructor.

    public PulseUser() : this(String.Empty) { }
    public PulseUser(string userName) 
    {
        UserName = userName;
        Id = Guid.NewGuid().ToString();
    }

You will also want to perform a check that the user name doesn't already exist. Look at overriding DbEntityValidationResult in PulseDbContext. Do a new MVC project in VS2013 to see an example.



Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow