Search code examples
c#exceptionazure-sql-databaseazure-application-insightsconcurrency-exception

Why am I receiving a DbUpdateConcurrencyException when fetching 'User' data from Sql Server in Azure?


Problem - I can't figure out why I'm getting a DbUpdateConcurrencyException from Azure app-insights when I fetch data from my controller and then refresh the page? It also seems to be throwing this exception on other controller methods that get called at the same time as GetMemberProfile(), such as GetEventFilters() and this method doesn't even fetch data from the DB!

Examples below of both methods.

QUESTION - Why would I be receiving this exception on a fetch, with no save or update? I can't figure out how to diagnose this error because it's coming from Azure app-insights and mostly when I refresh the page after I've fetched the User data? I don't get any exception thrown in my code using localhost, and I've tried to wrap the calls around try catch statements and I also have exception middleware running. Can someone please help me figure out how to determine why this is happening? Is there some way I can diagnose it? Ex. determine if my db context connection is messed up. Data seems to fetching and saving ok, despite getting the exceptions.

FYI - I upgraded from Net6 to Net7 and EFCore6 to EFCore7, but not sure if there were any breaking changes that would do this as I looked on the MS page. I also recently hooked up Azure alerts and wired emails to be sent on exceptions.

What I tried - I tried adding RowVersion to the User table and two other child tables from User, but still received errors after adding a concurrency timestamp. Found link here - handling concurrency

The image below is what I see in Azure portal when I click on the email and dig into the alert details. You can see the exception is being thrown on GetMemberProfile, which only fetches data (code below).

enter image description here enter image description here

Controller code to fetch Member profile data.

[HttpGet("edit/profile", Name = "GetMemberProfile")]
public async Task<IActionResult> GetMemberProfile()
{
    var user = await _userManager.FindByEmailFromClaimsPrincipleWithPracticingStylesPoses(HttpContext.User);   

    return Ok(new {
        Id = user.Id,
        Introduction = user.Introduction,
        Gender = user.Gender,
        YearStarted = user.YearStarted,
        ExperienceLevel = user.ExperienceLevel,
        PracticedStyles = p.PracticedStyles.Select(p => (int)p.YogaStyle),
        PracticedPoses = p.PracticedPoses.Select(p => (int)p.YogaPose)
   });
}

public static async Task<User> FindByEmailFromClaimsPrincipleWithPracticingStylesPoses(this UserManager<User> input, ClaimsPrincipal user)
{
    var email = user?.Claims?.FirstOrDefault(x => x.Type == ClaimTypes.Email)?.Value;
    return await input.Users.Include(x => x.PracticedStyles).Include(p => p.PracticedPoses).SingleOrDefaultAsync(x => x.Email == email);
}

Here is where I see another DbUpdateConcurrencyException exception while just fetching this data from enums and returning it to my client. I get another Azure app-insite exception. I don't get it, I'm not doing anything with my DB here.

        [AllowAnonymous]
        [HttpGet("filters")]
        public IActionResult GetEventFilters()
        {
            var lengths = ((EventLength[])Enum.GetValues(typeof(EventLength))).Select(c => new EnumModel() { Value = (int)c, Name = ((int)c).ToString() + " min" }).ToList();
            var styles = ((YogaStyle[])Enum.GetValues(typeof(YogaStyle))).Select(c => new EnumModel() { Value = (int)c, Name = c.ToString() }).ToList();
            var access = ((YogabandAccess[])Enum.GetValues(typeof(YogabandAccess))).Select(c => new EnumModel() { Value = (int)c, Name = c.ToString() }).ToList();
            var type = ((RegistrantType[])Enum.GetValues(typeof(RegistrantType))).Select(c => new EnumModel() { Value = (int)c, Name = c.ToString() }).ToList();
            var poses = ((YogaPose[])Enum.GetValues(typeof(YogaPose))).Select(c => new EnumModel() { Value = (int)c, Name = c.GetAttribute<DisplayAttribute>().Name }).ToList();
            var levels = ((YogaLevel[])Enum.GetValues(typeof(YogaLevel))).Select(c => new EnumModel() { Value = (int)c, Name = c.ToString() }).ToList();
            var cancel = ((CancelReason[])Enum.GetValues(typeof(CancelReason))).Select(c => new EnumModel() { Value = (int)c, Name = c.GetAttribute<DisplayAttribute>().Name }).ToList();
            var decline = ((DeclineReason[])Enum.GetValues(typeof(DeclineReason))).Select(c => new EnumModel() { Value = (int)c, Name = c.GetAttribute<DisplayAttribute>().Name }).ToList();

            return Ok(new
            {
                Lengths = lengths,
                Styles = styles,
                Access = access,
                Type = type,
                Poses = poses,
                Levels = levels,
                Cancel = cancel,
                Decline = decline
            });
        }

Here is my datacontext file. I left out the DbSet values

 public class DataContext : IdentityDbContext<User, Role, int, IdentityUserClaim<int>, UserRole, IdentityUserLogin<int>, IdentityRoleClaim<int>, IdentityUserToken<int>> // DbContext
 {
    public DataContext(DbContextOptions<DataContext> options) 
        : base(options) 
    {
         // left out each DbSet value
         protected override void OnModelCreating(ModelBuilder modelBuilder)
         {
            base.OnModelCreating(modelBuilder);
            modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());

            public static void SetOnModelCreatingExtend(ModelBuilder modelBuilder) 
            { 
              // left out each value because of length
             }
        }

    }
}

In Startup.cs here is ConfigureServices.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<DataContext>(options => options.UseSqlServer(_config.GetConnectionString("SqlServerConnection"), y => y.UseNetTopologySuite()));
    services.AddApplicationInsightsTelemetry();
}

UPDATE - 6/19/2023

Here are 2 screenshots of an exception I just received coming from GetMemberProfile(), which doesn't save DB data. It just fetches User and sends its data to the client, but I receive an error from Azure telling me an exception was thrown while trying to save to the DB. Huh what!

Here is the stack trace from the Azure app-insights portal error.

enter image description here

Here is a screenshot of the Azure portal.

enter image description here


Solution

  • SOLVED

    I figured out why I was seeing DbUpdateConcurrencyException's coming from , what seemed like, fetching data from my controllers and not updating anything on the specific page I was on within the site. I have a service 'LogUserActivityService' that tracks all activity per user and updates a field in AspNetUser called 'LastActive'. I had forgotten about this...

    Here is how it works and what was happening.

    With each call to a controller, by the client, this code below gets executed.

    public class LogUserActivityService : IAsyncActionFilter
    {
        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            var resultContext = await next();
    
            if (!resultContext.HttpContext.User.Identity.IsAuthenticated) return;
    
            var userId = resultContext.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
            var repo = resultContext.HttpContext.RequestServices.GetService<UserManager<User>>();
            
            var user = await repo.FindByIdAsync(userId);
            user.LastActive = DateTime.UtcNow;
            var result = await repo.UpdateAsync(user);
        }
    }
    

    and it's registered in Startup.cs like this:

    services.AddScoped<LogUserActivityService>();
    

    QUESTION - Why was I getting so many DbUpdateConcurrencyExceptions on a single page where I didn't even save any data?

    Answer - On some pages, where I did a page refresh (ex. User Update page), I might send 4 GET requests to different controllers. Ex.

    1. Account Controller, Get User info, create JWT token and return data to client to save locally. Done with every page refresh.
    2. Member Controller - Get specific member data and pass back to page to edit and update
    3. Fetch filters the page needs (ex. items for a drop down the user needs to select to edit his/her data
    4. Fetch filters from another controller

    From my OP, a page refresh hits my server 4 times at almost the same instant calling 'OnActionExecutionAsync', then in this method proceeds to fetch the user from UserManager and updates 'LastActivity' then saves. And doing this in a async manner caused the concurrency exception as User was being tracked when UserManager fetched it.

    Solution - There are probably better and more robust solutions, but here is a simple one. I just removed the call to fetch the User from UserManager, injected my dbContext and executed a raw Sql statement from the context. I believe this removes any tracking as I'm not fetching any User, there by eliminating any concurrency issues. Please, if someone has a different or better solution add to my solution or add another solution that will help make it more robust.

    public class LogUserActivityService : IAsyncActionFilter
    {
        private readonly DataContext _dbContext;
    
        public LogUserActivityService(DataContext dbContext) {
            _dbContext = dbContext;
        }
        
        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            var resultContext = await next();
    
            if (!resultContext.HttpContext.User.Identity.IsAuthenticated) return;
    
            var userId = resultContext.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
    
            var sql = "UPDATE AspNetUsers SET LastActive = '" + DateTime.UtcNow.ToString() + "' WHERE Id = " + userId;
            var updatedRows = await _dbContext.Database.ExecuteSqlRawAsync(sql);
    
        }
    }