Louis
Louis

Reputation: 89

User details within the DbContext model builder

I'm new to razor pages / efcore / aspnet identity and have been trying to figure this out but its beating me.

Basically, I use AspNet Identity for user authentication & authorisation. I've extended AspNetUsers with an additional OrganisationId, which is an FK to Organisation entity; and added the ID as a claim in the identity claim store. This works fine.

Now I need to set an efcore global filter based on the authenticated user's organisationId so they can only view data that is assigned to their organisation.

However, I can't access the authenticated user details within the ModelBuilder.

public class SDMOxContext : IdentityDbContext<
        ApplicationUser, ApplicationRole, string,
        ApplicationUserClaim, ApplicationUserRole, ApplicationUserLogin,
        ApplicationRoleClaim, ApplicationUserToken>
    {

        public SDMOxContext(DbContextOptions<SDMOxContext> options)
            : base(options)
        { }

        protected override void OnModelCreating(ModelBuilder builder)
        {

            base.OnModelCreating(builder);
        // Set global filter so users can only see projects within their organisation.
        builder.Entity<Project>().HasQueryFilter(project => project.OrganisationId == 1);

    }

Instead of 1 in the global filter, I need to enter the user organisationid, which is stored as a user claim. Usually I get it with this:

User.FindFirstValue("OrganisationId")

However, User doesn't exist in the current context.

Upvotes: 0

Views: 604

Answers (2)

Erik Philips
Erik Philips

Reputation: 54638

So I would need to apply the query filter at a later stage, ie. after user authentication? Any pointers where to start with a mid-tier/logic-tier approach?

Granted this is an opinion on architecture, but I break it down like this:

Data-Tier - This tier's responsibility to to access resources (normally) outside the executing application. This includes; Databases, File IO, Web Api's etc.

Business/Logic-Tier - This tier's responsibility (which could be broken down further) should Authenticate, Authorize, Validate and build objects that represent the businesses needs. To build these objects, it may consume one or more data access objects (for example, it may use an IO DA to retrieve the Image from a local file system or Azure storage and a Database DA to retrieve metadata about that image).

Presentation/Exposure-Tier - This tier's responsibility is to wrap and transform the object into the consumers need (winforms, wpf, html, json, xml, binary serialization etc).

By leaving logic out of the data-tier (even in multi-tenant systems) you gain the ability to access data across all systems (and trust me there is a lot of money to be made here).

This is probably way more than I can explain in such a short place and very my opinion. I'm going to be leaving out quite a bit but here goes.

Data-Tier

namespace ProjectsData
{
  public interface IProjectDA 
  { 
    IProjectDO GetProject(Guid projectId, Guid organizationId);
  }
  private class ProjectDA : DbContext, IProjectDA
  {
    public ProjectDA (...)
    public IEnumerable<ProjectDO> Projects { get; set; }
    protected override void OnModelCreating(ModelBuilder builder) {... }
    public IProjectDO GetProject(Guid projectId, Guid organizationId)
    {
      var result = Projects
        .FirstOrDefault(p => p.Id == projectId && OrganizationId = organizationId);
      return result;
    }
  }

  public interface IProjectDO{ ... }
  private class ProjectDO: IProjectDO
  {
    public Guid Id { get; set; }
    public Guid OrganizationId { get; set; }
    public Guid CategoryId { get; set; }
  }
}

Logic

namespace ProjectBusiness
{
  public interface IProjectBO { .. }
  public interface IOrganization 
  { 
    Guid OrganizationId { get; }
  }
  private class ProjectBA : IProjectBO
  {
    private readonly IProjectDA _projectDA;
    private readonly IIdentity _identity;
    private readonly IOrganization _organization;
    public  ProjectLogic(IProjectDA projectDA, 
      IIdentity identity,
      IOrganizationContext organizationContext)
    {
      _projectDA = projectDA;
      _identity = identity;
    }
    public IProjectBO GetProject(Guid id)
    {
      var do = _projectDA
        .GetProject(id, _organization);

      var result = map.To<ProjectBO>(do);

      return result;
    }
  }
  public interface IProjectBO { .. }
  private class ProjectBO 
  { 
    public Guid Id { get; set; }
    public Guid OrganizationId { get; set; }
    public Guid CategoryId { get; set; }
  }
}

So under these circumstances the data layer is aware of type of request, but isn't multi-tenant aware. It isn't limiting all request based on anything. This architecture is advantageous in a number of ways.

First, in the above example, your product takes off and your supervisor wants to know what Categories are the most popular.

namespace StatisticsBusiness
{
  public interface IStatisticsBO 
  {
    IEnumerable<ICategoryStatisticBO> CategoryStatistics { get; set; }
  }
  public interface ICategoryStaticBO
  {
    Guid CategoryId { get; }
    int ProjectCount { get; }
  }
  private class StatisticsBA : IStatisticsBO
  {
    private readonly IProjectDA _projectDA;
    private readonly IIdentity _identity;
    public  ProjectLogic(IProjectDA projectDA, 
      IIdentity identity)
    {
      _projectDA = projectDA;
      _identity = identity;
    }

    public IEnumerable<IProjectBO GetOrderedCategoryPopularity()
    {
      var dos = _projectDA
        .GetProjectCategoryCounts()

      var result = map.To<IEnumerable<IStatisticsBO>>(dos);

      return result;
    }
  }
  public interface IStatisticsBO{ .. }
  private class StatisticsBO 
  { 
    public Guid CategoryId { get; }
    public int ProjectCount { get; }
  }
}

Note: Some people prefer to pass an expression as a predicate. Both have their advantages and disadvantages. If you decide to go the predicate route, then you'll have to decide if all your Data Access types use predicates or not. Just realize that using predicates against IO or Web Api might be more effort that it's worth.

Secondly, some requirement causes you not to be able to use Entity Framework. You replace it with Dapper or some other new better technology/framework. All you have to create new I<whataver>DA classes because the consuming logic is unaware of anything other than those interfaces (programming against an interface, the L in SOLID programming principles and the I in SOLID programming principles).

I don't use this pattern all the time because for some smaller websites, it's too much work for the payoff.

Upvotes: 1

Naved Deshmukh
Naved Deshmukh

Reputation: 538

I will suggest to decompose the solution in tow parts

  1. Add an organization id in your dbcontext, much like a tenant id in multi-tenant env. See this link for example.

  2. Next challenge will be to pass the organization id as a parameter to DbContext constructor. For this you can create a factory for DbContext. Since you store the OrganizationId in claims. The factory can access the same claim HttpContext and pass the organization id as a parameter while instanting the dbContext.

It's not perfect but can give you a starting point.

Upvotes: 0

Related Questions