My setup:
- ASP.NET 4.5 web api (on Azure) saving data to SQL db (also on Azure)
- AngularJS web front end (another Azure web site)
When a user first signs up, I show them a "getting started intro". The intro is only supposed to run once - I log the timestamp of the intro launch date as a custom field in the ASP.NET user table.
Imagine my surprise when I log in (as a user would) and see the intro TWICE.
The AngularJS front end is properly sending the "intro viewed" message to the ASP.NET api, and the api responds with a success message. However, when I look at the raw data in the db, the timestamp is most definitely NOT updated. Consequently, the user will see the intro a second time (at which point the timestamp gets recorded in the db properly).
I have a crappy workaround. After the client requests an OAuth Bearer token from my server, the client then requests user information (to decide whether or not to show the tour). Waiting 100ms and then sending the "tour viewed" message back to the server masks the issue.
I've not seen ANY other issues storing data at any point. Because our db is on Azure, I can't hook up Profiler and the built in auditing doesn't give me any clues.
Is there something about requesting the token that leaves ASP.NET identity in a funny state? And it takes a brief wait before you can write to the table? Are custom fields that extend the base Identity setup prone to problems like this? Is the UserManager possibly doing something weird in its black box?
Does anyone have suggestions for how to continue debugging this problem? Or ever hear of anything like it?
Here's the relevant code that should be updating the "tour viewed" timestamp in the db:
[HttpPost, Route("UserInfo")] public async Task<IHttpActionResult> UpdateUserInfo(UpdateBindingModel model) { var currentUser = UserManager.FindById(User.Identity.GetUserId()); if (model.FirstName != null) { currentUser.FirstName = model.FirstName; } if (model.LastName != null) { currentUser.LastName = model.LastName; } if (model.SetIntroViewCompleteDate) { currentUser.IntroViewCompleteDate = DateTime.UtcNow; } if (model.SetIntroViewLaunchDate) { currentUser.IntroViewLaunchDate = DateTime.UtcNow; } if (model.SetTipTourCompleteDate) { currentUser.TipTourCompleteDate = DateTime.UtcNow; } if (model.SetTipTourLaunchDate) { currentUser.TipTourLaunchDate = DateTime.UtcNow; } IdentityResult result = await UserManager.UpdateAsync(currentUser); if (result.Succeeded) { var data = new UserInfoViewModel { FirstName = currentUser.FirstName, LastName = currentUser.LastName, IntroViewLaunchDate = currentUser.IntroViewLaunchDate }; return Ok(data); } return InternalServerError(); }
UPDATE ********* 4/18
I've also tried to move completely away from UserManager stuff. I've tried the following modifications (pulling the user data from a table like I would access any other data), but it still behaves the same. I'm starting to think that putting custom fields on the ApplicationUser object is a bad idea...
New db retrieve and save looks like this:
ApplicationDbContext newContext = new ApplicationDbContext(); var currentUser = await (from c in newContext.Users where c.Email == User.Identity.Name select c).SingleOrDefaultAsync(); //update some values await newContext.SaveChangesAsync();
3 Answers
Answers 1
Basically the problem might be with initialization of the `UserManager' and the fact that this class works on the db context so you need to persist changes to that context. Here is an example:
var userStore = new UserStore<ApplicationUser>(new MyDbContext()); var userManager = new UserManager(userStore);
That way you remember both manager and context. Then in your method you would normally call:
IdentityResult result = await userManager.UpdateAsync(currentUser);
followed by persisting this change to db context:
var dbContext = userStore.context; dbContext.saveChanges();
Answers 2
Based on your comment that waiting 100ms masks the issue, I think you may have a problem with the multiple async await calls. Try running the calls synchronously and see if you still have the same issue. My guess is that the problem might go away. My experience has been that using async await can be tricky when you have calls to asynchronous methods that call other asynchronous methods. You may have code that is executing without the proper results returned.
Answers 3
Well, here's what I did to solve the problem. I totally de-coupled my custom user data from the built in ASP.NET identity stuff. I've now got a separate object (and therefore separate SQL table) that stores things like FirstName, LastName, LastActiveDate, etc, etc.
This has solved my problem entirely, though it has introduced another call to the database in certain situations. I've deemed it to be not a big enough performance issue to worry about. I'm left thinking that this was some sort of weird race condition involving the generation of a token for an ASP.NET identity user then quickly writing to an Azure SQL database - lord knows what it was exactly in my code that caused the problem.
If you've got a problem that's hard to solve, often the best plan is to change the problem.
Now I need to find a meta thread discussing what to do with bounty points when you've blown up the problem...
0 comments:
Post a Comment