基于主机的ASP MVC EF6多租户

时间:2016-12-08 18:58:26

标签: entity-framework filter multi-tenant interceptor

对不起,另一个多租户帖子。我找不到一个很好的网站解决方案,我已经阅读了很多关于ASP MVC多租户的好帖子,但我还是需要一些好的建议。

我有一个ASP MVC实体框架6 Code First Web应用程序。这个应用程序必须为许多不同的客户端使用单个数据库。

我有一个适用于所有客户端的实体,每个客户端可以拥有不同的主机。

public class Client
{
    public int ClientId { get; set; }
    public string Name { get; set; }
    ...
    public ICollection<ClientHost> Hosts { get; set; }
}

public class ClientHost
{
    public int ClientId { get; set; }
    public Client Client { get; set; }
    public string Name { get; set; }
}

我已经向需要过滤的所有实体添加了一列“ClientId”,因此我可以将数据与不同的客户端分开。

public class SomeEntity
{
    public int Id { get; set; }
    ...
    public int ClientId { get; set; }
}

我需要的第一件事是,基于主机,检索要使用的ClientId。

private static int GetClientId()
{
    var currentClient = Convert.ToInt32(HttpRuntime.Cache[CacheClient]);
    if (currentClient != null) return currentClient;

    lock (Synclock)
    {
        using (var dataContext = new MyDataContext())
        {
            var urlHost = HttpContext.Current.Request.Url.Host;
            currentClient = dataContext.Clients
               .FirstOrDefault(p => p.Hosts.Any(h => h.Name == urlHost));

            if (currentClient == null) return null;

            HttpRuntime.Cache.Insert(CacheClient, currentClient, null, Cache.NoAbsoluteExpiration, TimeSpan.FromSeconds(0), CacheItemPriority.Default, null);

            return currentClient;
        }

    }
}

问题1 如您所见,我从DB获取clientId并将其存储在缓存中,因此我不必在每次需要时调用DB。 我不知道是否有更好的方法来获取客户端ID,或者更好地存储它。

EDIT
经过调查,我在DbCOntext中创建了一个变量,并在Startup.cs文件中初始化它。

    public class MyDataContext : IdentityDbContext<ApplicationUser, CustomRole, int, CustomUserLogin, CustomUserRole, CustomUserClaim>
{
    public static string ClientId { get; set; }

    public MyDataContext() : base("MyDataBase") { }

    public static MyDataContext Create()
    {
        return new myDataContext();
    }
    ....
}

在Startup.cs

    public partial class Startup
{
    public void Configuration(IAppBuilder app)
    {
        MyDataContext.ClientId = ClientConfiguration.GetCurrentClientId();

        ConfigureAuth(app);
    }
}

问题2
一旦我有ClientId,我需要为每个需要它的查询添加一个过滤器。手动执行此操作可能会导致许多错误或忘记在某些地方执行此操作。

我需要一种方法,应用程序可以自动将过滤器添加到所有查询(只有那些需要它的实体),所以我不必担心客户端获取其他客户端的数据。此外,我需要将ClientId添加到所有插入和更新命令。

我已经阅读了有关过滤和/或使用EF拦截器的内容,但在阅读了一些有关该内容的帖子后,我无法弄清楚如何去做。需要一些帮助。

提前致谢。

修改

为了解决问题2,我遵循了Xabikos的这篇伟大帖子: http://xabikos.com/2014/11/17/Create-a-multitenant-application-with-Entity-Framework-Code-First-Part-1/

我已经改变了一点,因为我不使用Users来获取当前租户而是使用主机。这是程序的一部分我还不知道我将如何解决但是,假设我已经拥有ClientId,我可以为所有查询添加过滤器,而不会意识到这一点:

我已经替换了所有用户逻辑:

    private static void SetTenantParameterValue(DbCommand command)
    {
        if (MyDataContext.ClientId == 0) return;

        foreach (DbParameter param in command.Parameters)
        {
            if (param.ParameterName != TenantAwareAttribute.TenantIdFilterParameterName)
                continue;
            param.Value = MyDataContext.ClientId;
        }
    }

在所有地方都一样......

我只需要标记必须使用TenantAware过滤的实体,指明属性。在这种情况下,我在我的基类中,然后将该基类应用于我需要的所有实体。

[TenantAware("ClientId")]
public abstract class ClientEntity : Entity, IClientEntity
{
    public int ClientId { get; set; }
    public Client Client { get; set; }
}

1 个答案:

答案 0 :(得分:0)

以下是我过去做过的一些可能会有所帮助的事情。

问题1: 我不是会议的忠实粉丝,因为网络应该是无国籍的。但是,它有时是必要的。你的方法是合理的。你也可以使用cookies。我使用的是Json Web Tokens(JWT)通过我的身份验证提供程序(Auth0.com)。对于经过身份验证的每个请求,我会查找此客户端ID。这是一个例子。这也是MVC 6。你可以用饼干做同样的事情。

public class Auth0ClaimsTransformer : IClaimsTransformer
    {

        private string _accountId = AdminClaimType.AccountId.DefaultValue;
        private string _clientId = AdminClaimType.ClientId.DefaultValue;
        private string _isActive = AdminClaimType.IsActive.DefaultValue;

        public Task<ClaimsPrincipal> TransformAsync(ClaimsTransformationContext context)
        {
            foreach (var claim in context.Principal.Claims)
            {
                switch (claim.Type)
                {
                    case "accountId":
                        _accountId = claim.Value ?? _accountId;
                        break;
                    case "clientId":
                        _clientId = claim.Value ?? _clientId;
                        break;
                    case "isActive":
                        _isActive = claim.Value ?? _isActive;
                        break;
                }
            }
            ((ClaimsIdentity)context.Principal.Identity)
                .AddClaims(new Claim[]
                {
                    new Claim(AdminClaimType.AccountId.DisplayName, _accountId),
                    new Claim(AdminClaimType.ClientId.DisplayName, _clientId), 
                    new Claim(AdminClaimType.IsActive.DisplayName, _isActive)
                });

            return Task.FromResult(context.Principal);
        }

然后在我的Startup.cs配置方法中插入我的声明变换器。

app.UseJwtBearerAuthentication(options);

    app.UseClaimsTransformation(new ClaimsTransformationOptions
    {
        Transformer = new Auth0ClaimsTransformer()
    });

接下来,我使用基本身份验证控制器将我的声明解析为我可以在控制器中使用的属性。

[Authorize]
    [Route("api/admin/[controller]")]
    public class BaseAdminController : Controller
    {
        private long _accountId;
        private long _clientId;
        private bool _isActive;

        protected long AccountId
        {
            get
            {
                var claim = GetClaim(AdminClaimType.AccountId);
                if (claim == null)
                    return 0;

                long.TryParse(claim.Value, out _accountId);
                return _accountId;
            }
        }

        public long ClientId
        {
            get
            {
                var claim = GetClaim(AdminClaimType.ClientId);
                if (claim == null)
                    return 0;

                long.TryParse(claim.Value, out _clientId);
                return _clientId;
            }
        }

        public bool IsActive
        {
            get
            {
                var claim = GetClaim(AdminClaimType.IsActive);
                if (claim == null)
                    return false;

                bool.TryParse(claim.Value, out _isActive);
                return _isActive;
            }
        }

        public string Auth0UserId
        {
            get
            {
                var claim = User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier);
                return claim == null ? string.Empty : claim.Value;
            }
        }

        private Claim GetClaim(AdminClaimType claim)
        {
            return User.Claims.FirstOrDefault(x => x.Type == claim.DisplayName);
        }

最后在我的控制器中,提取哪个租户正在进行呼叫是微不足道的。 e.g。

public FooController : BaseController
{
 public async Task<IActionResult> Get(int id)
 {
   var foo = await _fooService.GetMultiTenantFoo(ClientId, id);
   return Ok(foo);
 }
}

问题2: 我过去使用的一种方法是创建一个BaseMultiTenant类。

public class BaseMultiTenant
{ 
  public int ClientId {get;set;}

  public virtual Client Client {get;set;}//if you are using EF
}

public class ClientHost : BaseMultiTenant
{
  public string Name {get;set;}
  //etc
}

然后,只需为基于多租户的实体创建扩展方法。我知道这并没有自动完成#34;但这是确保每个多租户实体仅由其所有者调用的简单方法。

public static IQueryable<T> WhereMultiTenant<T>(this IQueryable<T> entity, int clientId, Expression<Func<T, bool>> predicate)
    where T : BaseMultiTenant
{
    return entity.Where(x => x.ClientId == clientId)
                 .Where(predicate);
}

然后当有人要求他们的资源时,您可以:

var clientHost = _myContext.ClientHosts
                           .WhereMultiTenant(ClientId, 
                              x => x.Name == "foo")
                           .FirstOrDefault();

希望这有用。

还使用界面找到similar example