Finbuckle.MultiTenant icon indicating copy to clipboard operation
Finbuckle.MultiTenant copied to clipboard

Tenant could not be resolved with HostStrategy and DistributedCacheStore

Open marcelbeutner opened this issue 2 years ago • 3 comments

Hi, I use redis as my distributed cache store and all tenant relevant information are stored in there in the following manner:

  1. "__tenant__identifier__MyTenantName"
  2. "__tenant__id__d59b44f1-1457-4be8-8f07-1aaf1226f500"

The HostStrategy gets my tenant name from the subdomain (https://MyTenantName.mydomain.com) and the identifier is resolved correctly.

But if the request is like (https://MYTENANTNAME.mydomain.com) the tenant is not found.

This is because redis is case sensitive. Did I miss something or would it be possible that the "DistributedCacheStore.cs" make all lowercase before reading and writing?

Thanks!

marcelbeutner avatar Apr 05 '23 12:04 marcelbeutner

@marcelbeutner sorry for the slow reply. Did you find a solution or workaround?

I plan to add a normalized tenant identifier to prevent these issues but for now I'd recommend implementing IMultiTenantStrategy and/or IMultiTenantStore yourself to explicitly handle case as you see fit.

AndrewTriesToCode avatar Jul 13 '23 00:07 AndrewTriesToCode

Thanks Andrew for your answer!

Yes, I have indeed found a workaround that solves my problem. I copied the DistributedCacheStore class and nomalized the keys with "ToLower()".

So far we haven't found any problems, not even in production, so I think it would be conceivable to include this in the master branch as well.

By the way: This library is really fantastic and I think it's a must for multi-tenant applications - great work!!

Best regards Marcel

Here are my little changes:

` ///

/// Basic store that uses an IDistributedCache instance as its backing. Note that GetAllAsync is not implemented. /// /// The ITenantInfo implementation type. public class DistributedCacheStore<TTenantInfo> : IMultiTenantStore<TTenantInfo> where TTenantInfo : class, ITenantInfo, new() { private readonly IDistributedCache cache; private readonly string keyPrefix; private readonly TimeSpan? slidingExpiration;

///

/// Constructor for DistributedCacheStore. /// /// IDistributedCache instance for use as the store backing. /// public DistributedCacheStore(IDistributedCache cache) { this.cache = cache ?? throw new ArgumentNullException(nameof(cache)); this.keyPrefix = "tenant"; this.slidingExpiration = null; }

/// public async Task TryAddAsync(TTenantInfo tenantInfo) { var options = new DistributedCacheEntryOptions { SlidingExpiration = slidingExpiration }; var bytes = JsonSerializer.Serialize(tenantInfo);

await cache.SetStringAsync($"{keyPrefix}id__{tenantInfo.Id}".ToLower(), bytes, options);
await cache.SetStringAsync($"{keyPrefix}identifier__{tenantInfo.Identifier}".ToLower(), bytes, options);

return true;

}

/// public async Task<TTenantInfo?> TryGetAsync(string id) { var bytes = await cache.GetStringAsync($"{keyPrefix}id__{id}".ToLower()); if (bytes == null) return null;

var result = JsonSerializer.Deserialize<TTenantInfo>(bytes);

// Refresh the identifier version to keep things synced
await cache.RefreshAsync($"{keyPrefix}identifier__{result?.Identifier}".ToLower());

return result;

}

///

/// Not implemented in this implementation. /// /// public Task<IEnumerable<TTenantInfo>> GetAllAsync() { throw new NotImplementedException(); }

/// public async Task<TTenantInfo?> TryGetByIdentifierAsync(string identifier) { var bytes = await cache.GetStringAsync($"{keyPrefix}identifier__{identifier}".ToLower()); if (bytes == null) return null;

var result = JsonSerializer.Deserialize<TTenantInfo>(bytes);

// Refresh the identifier version to keep things synced
await cache.RefreshAsync($"{keyPrefix}id__{result?.Id}".ToLower());

return result;

}

/// public async Task TryRemoveAsync(string identifier) { var result = await TryGetByIdentifierAsync(identifier); if (result == null) return false;

await cache.RemoveAsync($"{keyPrefix}id__{result.Id}".ToLower());
await cache.RemoveAsync($"{keyPrefix}identifier__{result.Identifier}".ToLower());

return true;

}

/// public Task TryUpdateAsync(TTenantInfo tenantInfo) { // Same as adding for distributed cache. return TryAddAsync(tenantInfo); } } `

marcelbeutner avatar Jul 13 '23 06:07 marcelbeutner