11

We override SaveChangesAsync() to update automatically for DateCreated, CreatedBy, LastDateModified and LastModifiedBy. With CreatedBy and LastModifiedBt, we need to the User Id of Identity.

In our constructor for ApplicationDbContext, we've added something like this:

_userName = httpContextAccessor.HttpContext.User.Identity.Name; //_userID = userManager.GetUserId(httpContext.HttpContext.User);

.. and always get the null in this httpContextAccessor.HttpContext. Any ideas? We included the source below.

Environment:

.NET Core 2.1

SQL Server

ApplicationDBContext.cs:

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using AthlosifyWebArchery.Models;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Linq.Expressions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;

namespace AthlosifyWebArchery.Data
{
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string>
    {
        private readonly string _userID;
        private readonly string _userName;


        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options,
                IHttpContextAccessor httpContextAccessor
                )
        : base(options)
        {
            _userName = httpContextAccessor.HttpContext.User.Identity.Name;

            //_userID = userManager.GetUserId(httpContext.HttpContext.User);

        }

        public DbSet<AthlosifyWebArchery.Models.TournamentBatchItem> TournamentBatchItem { get; set; }
        public DbSet<AthlosifyWebArchery.Models.TournamentBatch> TournamentBatch { get; set; }

        public virtual DbSet<AthlosifyWebArchery.Models.Host> Host { get; set; }

        public DbSet<AthlosifyWebArchery.Models.HostApplicationUser> HostApplicationUser { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);

            foreach (var entityType in builder.Model.GetEntityTypes())
            {
                // 1. Add the IsDeleted property
                entityType.GetOrAddProperty("IsDeleted", typeof(bool));

                // 2. Create the query filter

                var parameter = Expression.Parameter(entityType.ClrType);

                // EF.Property<bool>(post, "IsDeleted")
                var propertyMethodInfo = typeof(EF).GetMethod("Property").MakeGenericMethod(typeof(bool));
                var isDeletedProperty = Expression.Call(propertyMethodInfo, parameter, Expression.Constant("IsDeleted"));

                // EF.Property<bool>(post, "IsDeleted") == false
                BinaryExpression compareExpression = Expression.MakeBinary(ExpressionType.Equal, isDeletedProperty, Expression.Constant(false));

                // post => EF.Property<bool>(post, "IsDeleted") == false
                var lambda = Expression.Lambda(compareExpression, parameter);

                builder.Entity(entityType.ClrType).HasQueryFilter(lambda);
            }


            // Many to Many relationship

            builder.Entity<HostApplicationUser>()
                .HasKey(bc => new { bc.HostID, bc.Id });


            builder.Entity<HostApplicationUser>()
                .HasOne(bc => bc.Host)
                .WithMany(b => b.HostApplicationUsers)
                .HasForeignKey(bc => bc.HostID);

            builder.Entity<HostApplicationUser>()
                .HasOne(bc => bc.ApplicationUser)
                .WithMany(c => c.HostApplicationUsers)
                .HasForeignKey(bc => bc.Id);

        }

        public override int SaveChanges(bool acceptAllChangesOnSuccess)
        {
            OnBeforeSaving();
            return base.SaveChanges(acceptAllChangesOnSuccess);
        }

        public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
        {
            OnBeforeSaving();
            return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
        }

        private void OnBeforeSaving()
        {
            // Added
            var added = ChangeTracker.Entries().Where(v => v.State == EntityState.Added && typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();

            added.ForEach(entry =>
            {
                ((IBaseEntity)entry.Entity).DateCreated = DateTime.UtcNow;
                ((IBaseEntity)entry.Entity).CreatedBy = _userID;
                ((IBaseEntity)entry.Entity).LastDateModified = DateTime.UtcNow;
                ((IBaseEntity)entry.Entity).LastModifiedBy = _userID;
            });

            // Modified
            var modified = ChangeTracker.Entries().Where(v => v.State == EntityState.Modified && 
            typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();

            modified.ForEach(entry =>
            {
                ((IBaseEntity)entry.Entity).LastDateModified = DateTime.UtcNow;
                ((IBaseEntity)entry.Entity).LastModifiedBy = _userID;
            });

            // Deleted
            //var deleted = ChangeTracker.Entries().Where(v => v.State == EntityState.Deleted &&
            //typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();

            var deleted = ChangeTracker.Entries().Where(v => v.State == EntityState.Deleted).ToList();

            deleted.ForEach(entry =>
            {
                ((IBaseEntity)entry.Entity).DateDeleted = DateTime.UtcNow;
                ((IBaseEntity)entry.Entity).DeletedBy = _userID;
            });

            foreach (var entry in ChangeTracker.Entries()
                                    .Where(e => e.State == EntityState.Deleted &&
                                    e.Metadata.GetProperties().Any(x => x.Name == "IsDeleted")))
            {
                switch (entry.State)
                {
                    case EntityState.Added:
                        entry.CurrentValues["IsDeleted"] = false;
                        break;

                    case EntityState.Deleted:
                        entry.State = EntityState.Modified;
                        entry.CurrentValues["IsDeleted"] = true;
                        break;
                }
            }
        }
    }



}

Startup.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using AthlosifyWebArchery.Data;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using AthlosifyWebArchery.Models;
using DinkToPdf.Contracts;
using DinkToPdf;

namespace AthlosifyWebArchery
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            //services.AddHttpContextAccessor();

            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
            services.AddSingleton(typeof(IConverter), new SynchronizedConverter(new PdfTools()));

            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });

            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection")));

            // Extended Application User from IdentityUser 
            // and ApplicationRole from IdentityRole

            services.AddIdentity<ApplicationUser, ApplicationRole>(
                options => options.Stores.MaxLengthForKeys = 128)
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultUI()
                .AddDefaultTokenProviders();

            services.AddMvc()
                .AddRazorPagesOptions(options =>
                {
                    options.Conventions.AuthorizeFolder("/Tournaments");
                    options.Conventions.AuthorizeFolder("/TournamentAtheletes");
                    options.Conventions.AuthorizeFolder("/TournamentBatches");
                    options.Conventions.AuthorizeFolder("/TournamentContingents");
                    options.Conventions.AuthorizeFolder("/Admin");
                    //options.Conventions.AuthorizeFolder("/Private");
                    //options.Conventions.AllowAnonymousToPage("/Private/PublicPage");
                    //options.Conventions.AllowAnonymousToFolder("/Private/PublicPages");
                })
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, 
                                ApplicationDbContext context,
                                RoleManager<ApplicationRole> roleManager,
                                UserManager<ApplicationUser> userManager)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseCookiePolicy();

            app.UseAuthentication();

            app.UseMvc();

            //UserManagerInitialData.Initialize(context, userManager, roleManager).Wait();

        }


    }
}
dcpartners
  • 5,176
  • 13
  • 50
  • 73
  • Duplicate: https://stackoverflow.com/questions/39131925/injecting-ihttpcontextaccessor-into-applicationdbcontext-asp-net-core-1-0 ? – wannadream Nov 23 '18 at 01:49
  • The httpcontext is not available as yet at the time the dbcontext is initialized, so trying to access it in the constructor will fail. HttpContext should be access in methods which are called later in the pipeline. – Nkosi Nov 23 '18 at 02:02

2 Answers2

15

HttpContext is only valid during a request. When .NET Core creates an ApplicationDbContext class for the call to Configure there is no valid context.

You need to store a reference to the IHttpContextAccessor in your DbContext constructor and then you can use that variable to access the HttpContext property in your OnBeforeSaving() method.

For example:

public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string>
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options,
            IHttpContextAccessor httpContextAccessor
            )
    : base(options)
    {
        _httpContextAccessor = httpContextAccessor;
    }

   ....
}

Then, in your OnBeforeSaving() method:

private void OnBeforeSaving()
{
    var userName = _httpContextAccessor.HttpContext.User.Identity.Name;

    ...
}

Think of HttpContext as a telephone call. If you pick the phone up when no-one has called then there is no context i.e. it is null. When someone does call then you have a valid context. This is the same principal for a web call. The Configure method in Startup is not a web call and, as such, does not have a HttpContext.

From another site:

HttpContext object will hold information about the current http request. In detail, HttpContext object will be constructed newly for every request given to an ASP.Net application and this object will hold current request specific informations like Request, Response, Server, Session, Cache, User and etc. For every request, a new HttpContext object will be created which the ASP.Net runtime will use during the request processing. A new HttpContext object will be created at the beginning of a request and destroyed when the request is completed.

Simply Ged
  • 8,250
  • 11
  • 32
  • 40
  • Can elaborate more on this, please? – dcpartners Nov 23 '18 at 01:56
  • Isn't that why we did?!?! `public ApplicationDbContext(DbContextOptions options, IHttpContextAccessor httpContextAccessor ) : base(options) { _userName = httpContextAccessor.HttpContext.User.Identity.Name; //_userID = userManager.GetUserId(httpContext.HttpContext.User); }` – dcpartners Nov 23 '18 at 02:00
  • 1
    Didn't think about that ... work well. Thanks. We slighly modifed to get the user id by: `var userId = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);` – dcpartners Nov 23 '18 at 20:22
3

Above answer explain it well but i would like to highlight another scenario where it could be null as well. For ex:

public class SomeClass
{
   SomeClass(IHttpContextAccessor accessor) {}
   IActionResult SomeMethod()
   {
      _ = Task.Run(() =>
          {
             // use accessorHere
          }
   return Ok();
  }
}

There is a chance that Api call is returned before Thread can access the IHttpContextAccessor and there is a chance that IHttpConextAccessor.HttpContext could be null.

So it is better if we can fetch the required values from the HttpContext for ex: userclaims and pass them as seperate object to the required function.

GPuri
  • 495
  • 4
  • 11
  • 1
    I am doing fire and forget, similar to what you have shared and sometime in debug session I get null `HttpContext` I think its only a problem in debug session and not in release version of code. – Jawand Singh Aug 14 '22 at 20:30
  • 1
    Solved my issue when I had missed an await. Thank you. – FLICKER May 29 '23 at 18:26