Search code examples
c#entity-frameworkasp.net-web-apiasp.net-identity

Registering and update model in a single API call


I have an ASP.net API 2 application with a register method, which basically does the following:

var user = new ApplicationUser() { UserName = user.email, Email = user.email };
UserManager.AddToRole(user.Id, "SomeRole");

In the same controller, I would like to assign a model class to the application user, for example:

var parent = db.Parents.Find(parentToCreate.id);
db.SaveChanges();

This gives me an error that the user name is already taken. I know that the issue is relating to there being two model trackers, one for the UserManager.CreateAsync and one for updating the db. Will it be alright to create users without using CreateAsync or is there another way to avoid this error?

Note that I think that this could be achieved by putting a Parent property on the account property, but not all accounts are parents, so I do not want to do this solution. A parent has an account, so there is an account property on the parent.

As requested, the full register method is as follows:

[AllowAnonymous]
[Route("RegisterParent")]
public async Task<IHttpActionResult>RegisterParent(ParentRegisterBindingModel model)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }


        var user = new ApplicationUser() { UserName = model.email, Email = model.email };


        Parent parentToCreate = new Parent();
        db.Parents.Add(parentToCreate);

        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        db.SaveChanges();


        try
        {
            IdentityResult result = await UserManager.CreateAsync(user, model.password);

           // The following two lines give an error
           parentToCreate.account = user;
           // the above two lines give an error
           UserManager.AddToRole(user.Id, "ParentRole");
           db.SaveChanges();
        }
        catch (Exception e)
        {
            Console.Write(e);
            // e returns here with message 
        }

        return Ok(200);

    }

Solution

  • This is a simplified example based on minimal example provided in OP.

    Based on conversation to clarify current design the Parent model would need to be updated to have a proper code first Foreign Key relationship

    public class Parent { 
        //...other properties
    
        //Foreign key 
        public string accountid { get; set; }
        //navigation property
        [ForeignKey("accountid")] 
        public ApplicationUser account { get; set; } 
    }
    

    With that then you only need to assign the user id when creating the parent.

    Refactoring/abstracting out specific responsibilities.

    public interface IParentService {
        Task AddParentAsync(ApplicationUser user);
    }
    
    public class ParentService : IParentService {
        ApplicationDbContext db;
        public ParentService(ApplicationDbContext db) {
            this.db = db;
        }
    
        public async Task AddParentAsync(ApplicationUser user) {
            Parent parentToCreate = new Parent() {
                //...set other properties
                accountid = user.Id
            };
            db.Parents.Add(parentToCreate);
            await db.SaveChangesAsync();
        }
    }
    

    Next separating the action into distinct processes to avoid concurrency issues.

    public class AccountController : ApiController {
        ApplicationUserManager userManager;
        IParentService parentService;
        public AccountController(ApplicationUserManager userManager, IParentService parentService) {
            this.userManager = userManager;
            this.parentService = parentService;
        }
    
        [AllowAnonymous]
        [Route("RegisterParent")]
        public async Task<IHttpActionResult> RegisterParent(ParentRegisterBindingModel model) {
            if (!ModelState.IsValid) {
                return BadRequest(ModelState);
            }
            var user = new ApplicationUser() { UserName = model.email, Email = model.email };
            var result = await userManager.CreateAsync(user, model.password);
            if (result.Succeed) {
                try {
                    await userManager.AddToRole(user.Id, "ParentRole");
                    await parentService.AddParentAsync(user);
                    return Ok();
                } catch (Exception e) {
                    userManager.Delete(user.Id);
                    Console.Write(e);
                    // e returns here with message 
                    return BadRequest(); //OR InternalServerError();
                }
            } else {
                foreach (var error in result.Errors) {
                    ModelState.AddModelError("", error);
                }
                return BadRequest(ModelState);
            }            
        }
    }
    

    You would obviously register dependencies with the DI framework to allow for proper injection.