0

How do you pass variables to DbContext options if using it as a [service]?

For example if you wanted to create a global filter for UserId you would make DbContext require UserId variable like so...

FooDbContext.cs

public FooDbContext : DbContext {
    private static int _userId;

    public FooDbContext(Id userId, DbContextOptions options) 
       : base(options)
    {
       _userId = userId; 
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
       modelBuilder.Entity<Users>()
         .HasQueryFilter(filter => filter.UserId == _userId)
    }
}

Easy enough to use it like this...

[HttpGet]
public Task<List<Bar>> GetBarAsync(Id userId) {
   FooDbContext context = new FooDbContext(userId)
   return await context.Bar.ToListAsync();
}

But what about this?

[HttpGet]
public Task<List<Bar>> GetBarAsync(Id userId,
   [Service] FooDbContext context
) {
   return await context.Bar.ToListAsync();
}

How do you pass variables into a service?

1 Answers1

1

Where is this ServiceAttribute defined? This looks like [FromServices] from .Net Core MVC.

When using dependency injection in general, the initialization of the dependency is managed by the container. What it looks like you want is to ensure the DbContext is initialized with the currently logged in user provided a valid session is in-progress.

The general approach I use for that is rather than accepting a UserID in the DbContext is to declare a dependency like:

public interface ICurrentUserLocator 
{
    int CurrentUserId { get; }
}

then create an implementation that ties into your HttpContext security to return the current session UserId.

public sealed class HttpCurrentUserLocator : ICurrentUserLocator
{
     ICurrentUserLocator.CurrentUserId
     {
         get { return 0; } // TODO: Return the current session userId, I.e. from HttpContext.Current or Thread.CurrentPrincipal, etc.) 
     }
}

Then the DbContext's constructor becomes:

private readonly ICurrentUserLocator _currentUserLocator = null;

public FooDbContext(ICurrentUserLocator currentUserLocator, DbContextOptions options) 
   : base(options)
{
   _currentUserLocator = currentUserLocator ?? throw new ArgumentNullException("currentUserLocator"); 
}

From there you can apply filters etc. based on the _currentUserLocator.CurrentUserId sourced by the injected dependency.

You definitely should avoid static for something like a user ID reference as that would be shared across all instances of the DbContext which would be really problematic as different users log in.

Steve Py
  • 26,149
  • 3
  • 25
  • 43
  • This is great, love the explanation. With this example, how does ``FooDbContext(currentUserLocator, options)`` get registered in ``startup.cs`` because I get ``Unable to create object of type FooDbContext`` when trying to run migrations but not at runtime? –  Feb 01 '21 at 02:19
  • EF Core command line tools should look for a `static IHostBuilder CreateHostBuilder` to discover your context, but will fall back to another method. I would call `this.GetService()` instead of adding an argument. – Jeremy Lakeman Feb 01 '21 at 02:34
  • Hmm, I actively avoid using Migrations so not something I have direct experience with.. However I did come across this (https://stackoverflow.com/questions/42296196/add-migration-without-parameterless-dbcontext-and-dbcontextfactory-constructor) which enables a default constructor with some guards to ensure it's only used for Migrations purposes. Not a fan of GetService, smells of ServiceLocator which can be messy for testing substitution. In DbContext it's a bearable option as this code won't be running unit tests. :) – Steve Py Feb 01 '21 at 05:30