Hi I need to add azure storage connection string for every tenant, if default is not used. So basically same functions as db connection string. Could you provide me some directions how to handle this.
- Add storage connection string to Tenant entity
- Get that connection string when needed, or if not exist, get default.
Mika
15 Answer(s)
-
0
Hi @MikaTmlVertti,
Here are the related classes for getting a connection string for the current tenant (or host).
<a class="postlink" href="https://github.com/aspnetboilerplate/aspnetboilerplate/blob/dev/src/Abp.Zero.Common/MultiTenancy/IDbPerTenantConnectionStringResolver.cs">https://github.com/aspnetboilerplate/as ... esolver.cs</a> <a class="postlink" href="https://github.com/aspnetboilerplate/aspnetboilerplate/blob/dev/src/Abp.Zero.EntityFramework/Zero/EntityFramework/DbPerTenantConnectionStringResolver.cs">https://github.com/aspnetboilerplate/as ... esolver.cs</a>
So, you can create a similar interface and it's implementation and use it in your project.
-
0
Is it possible to add this new item to tenant cache?
-
0
You can use CustomData attribute of TenantCacheItem. In order to do that, create a class deriving from TenantCache;
public class MyTenantaCache : TenantCache<Tenant, User> { public MyTenantaCache( ICacheManager cacheManager, IRepository<Tenant> tenantRepository, IUnitOfWorkManager unitOfWorkManager) : base(cacheManager, tenantRepository, unitOfWorkManager) { } protected override TenantCacheItem CreateTenantCacheItem(Tenant tenant) { return new TenantCacheItem { Id = tenant.Id, Name = tenant.Name, TenancyName = tenant.TenancyName, EditionId = tenant.EditionId, ConnectionString = SimpleStringCipher.Instance.Decrypt(tenant.ConnectionString), IsActive = tenant.IsActive, CustomData = tenant.azureConnStr }; } }
Then, you can register this class in the Initialize method of your module;
IocManager.Register<ITenantCache, MyTenantCache>(DependencyLifeStyle.Transient);
-
0
I made TenantaCacheExtended:
public class TenantaCacheExtended : TenantCache<Tenant, User> { public TenantaCacheExtended( ICacheManager cacheManager, IRepository<Tenant> tenantRepository, IUnitOfWorkManager unitOfWorkManager) : base(cacheManager, tenantRepository, unitOfWorkManager) { } protected override TenantCacheItem CreateTenantCacheItem(Tenant tenant) { return new TenantCacheItem { Id = tenant.Id, Name = tenant.Name, TenancyName = tenant.TenancyName, EditionId = tenant.EditionId, ConnectionString = SimpleStringCipher.Instance.Decrypt(tenant.ConnectionString), IsActive = tenant.IsActive, CustomData = SimpleStringCipher.Instance.Decrypt(tenant.AzureStorageConnectionString), }; } }
and it is located in Core project, same place where Tenant class is.
I tried to add IocManager.Register<ITenantCache, TenantaCacheExtended>(DependencyLifeStyle.Transient); to Core modules Initialize, PostInitialize and finally Application modules Initialize methods, but class is not being used. I had breakpoint at line "return new TenantCacheItem" but it newer got fired.
At the method where this custom data is supposed to be read, it is null.
Any advise?
-
0
I think you need to replace the service with the code like below.
Configuration.ReplaceService<ITenantCache, TenantaCacheExtended>(DependencyLifeStyle.Transient);
-
0
<cite>alper: </cite> I think you need to replace the service with the code like below.
I tried again in Core module Initialize and PostInitialize and Application module Initialize but it didn't work. Where do you think I need to add that Replace method?
-
0
@MikaTmlVertti could you show the code how did you try it in the Initialize Method ?
-
0
First without ReplaceService, then both Register and Replace and lastly only Replace.
public override void Initialize() { IocManager.RegisterAssemblyByConvention(typeof('my'ApplicationModule).GetAssembly()); IocManager.Register<ITenantCache, TenantaCacheExtended>(DependencyLifeStyle.Transient); Configuration.ReplaceService<ITenantCache, TenantaCacheExtended>(DependencyLifeStyle.Transient); }
And this is in public class 'my'ApplicationModule : AbpModule.
-
0
Started playing with settings and now I inserted those two lines to PreInitialize() and got exeption that there is allready class defined as ITenantCache. Removed Register and left only Replace -> my TenantCacheExtended CreateTenantCacheItem is being used.
So Not Initialize() but PreInitialize().
-
0
Great :)
-
0
<cite>MikaTmlVertti: </cite> Hi I need to add azure storage connection string for every tenant, if default is not used. So basically same functions as db connection string. Could you provide me some directions how to handle this.
- Add storage connection string to Tenant entity
- Get that connection string when needed, or if not exist, get default.
Mika
Do you mind sharing the entire implementation?
-
1
This forum would be nicer, if it notified when you are quoted or your topic is being updated.. I only get few days after last reply/other update and then notifications end.
So I added new property for Tenant class:
[StringLength(1024)] public virtual string AzureStorageConnectionString { get; set; }
and created new tenant cache class so the string is cached as normal db connection string:
public class TenantaCacheExtended : TenantCache<Tenant, User> { public TenantaCacheExtended( ICacheManager cacheManager, IRepository<Tenant> tenantRepository, IUnitOfWorkManager unitOfWorkManager) : base(cacheManager, tenantRepository, unitOfWorkManager) { } protected override TenantCacheItem CreateTenantCacheItem(Tenant tenant) { return new TenantCacheItem { Id = tenant.Id, Name = tenant.Name, TenancyName = tenant.TenancyName, EditionId = tenant.EditionId, ConnectionString = SimpleStringCipher.Instance.Decrypt(tenant.ConnectionString), IsActive = tenant.IsActive, CustomData = SimpleStringCipher.Instance.Decrypt(tenant.AzureStorageConnectionString) }; } }
which is being registered for DI at Modules PreInitialize() method
Configuration.ReplaceService<ITenantCache, TenantaCacheExtended>(DependencyLifeStyle.Transient);
To get the connection string at runtime, I use this: Interface:
public interface IAzureStoragePerTenantConnectionStringResolver { /// <summary> /// Gets the connection string for given args. /// </summary> string AzureStorageConnectionString(AzureStorageConnectionStringResolveArgs args); }
Parameters:
public class AzureStorageConnectionStringResolveArgs { public int? TenantId { get; set; } }
Runtime class:
public class AzureStoragePerTenantConnectionStringResolver : IAzureStoragePerTenantConnectionStringResolver, ITransientDependency { /// <summary> /// Reference to the session. /// </summary> private readonly IDefaultAzureStorageConnectionStringResolver _defaultResolver; private readonly ITenantCache _tenantCache; public AzureStoragePerTenantConnectionStringResolver( IDefaultAzureStorageConnectionStringResolver defaultResolver, ITenantCache tenantCache) { _tenantCache = tenantCache; _defaultResolver = defaultResolver; } public string AzureStorageConnectionString(AzureStorageConnectionStringResolveArgs args) { if (args.TenantId == null) { //Requested for host return _defaultResolver.GetDefaultAzureStorageConnectionString(); } var tenantCacheItem = _tenantCache.Get(args.TenantId.Value); var azureString = tenantCacheItem.CustomData as string; if (azureString.IsNullOrEmpty()) { //Tenant has not dedicated storage return _defaultResolver.GetDefaultAzureStorageConnectionString(); } return azureString; } }
If there is no Tenant specific Azure string, I use host: Interface:
public interface IDefaultAzureStorageConnectionStringResolver { string GetDefaultAzureStorageConnectionString(); }
Runtime class:
public class DefaultAzureStorageConnectionStringResolver : IDefaultAzureStorageConnectionStringResolver, ISingletonDependency { private readonly IConfigurationRoot _appConfiguration; private string DefaultConnectionString; public DefaultAzureStorageConnectionStringResolver(IHostingEnvironment env) { _appConfiguration = env.GetAppConfiguration(); DefaultConnectionString = null; } private void ResolveConnectionString() { DefaultConnectionString = _appConfiguration["AzureStorage:ConnectionString"]; if (string.IsNullOrWhiteSpace(DefaultConnectionString)) { throw new Exception("Unable to resolve Default Storage Connection string!"); } } public string GetDefaultAzureStorageConnectionString() { if (DefaultConnectionString == null) ResolveConnectionString(); return DefaultConnectionString; } }
So host azure con-string is being saved same way as the normal db connection string. In appsettings when developing and as enviromental variable at server.
That AzureStoragePerTenantConnectionStringResolver class is being used at my Azure handler.
private readonly ICurrentUnitOfWorkProvider _currentUnitOfWorkProvider; private readonly IAzureStoragePerTenantConnectionStringResolver _connectionStringResolver; //constructor DI private CloudBlobClient GetStorageClient() { var tenant = GetCurrentTenantId(); string connectionString = _connectionStringResolver.AzureStorageConnectionString (new AzureStorageConnectionStringResolveArgs() { TenantId= tenant}); var account = CloudStorageAccount.Parse(connectionString); return account.CreateCloudBlobClient(); } protected virtual int? GetCurrentTenantId() { return _currentUnitOfWorkProvider.Current != null ? _currentUnitOfWorkProvider.Current.GetTenantId() : AbpSession.TenantId; }
Also I copied StorageString functions and views at Create and Edit Tenant UI modals (Angular) Create:
<div class="form-group" *ngIf="!useHostDb"> <label>{{l("AzureStorageConnectionString")}} *</label> <input #azureStorageConnectionStringInput="ngModel" type="text" name="AzureStorageConnectionString" class="form-control" [(ngModel)]="tenant.azureStorageConnectionString" [ngClass]="{'edited':tenant.azureStorageConnectionString}" required maxlength="1024"> <validation-messages [formCtrl]="azureStorageConnectionStringInput"></validation-messages> </div>
Edit html:
<div class="form-group" *ngIf="currentAzureStorageConnectionString"> <label>{{l("AzureStorageConnectionString")}} *</label> <input #azureStorageConnectionStringInput="ngModel" type="text" name="AzureStorageConnectionString" class="form-control" [(ngModel)]="tenant.azureStorageConnectionString" required maxlength="1024"> <validation-messages [formCtrl]="azureStorageConnectionStringInput"></validation-messages> </div>
Edit ts:
currentConnectionString: string; currentAzureStorageConnectionString: string; -- this._tenantService.getTenantForEdit(tenantId).subscribe((tenantResult) => { this.tenant = tenantResult; this.currentConnectionString = tenantResult.connectionString; this.currentAzureStorageConnectionString = tenantResult.azureStorageConnectionString; --
and added AzureStorageConnectionString property to the necessary dto:s.
And few changes to TenantAppService for saving and editing:
[AbpAuthorize(AppPermissions.Pages_Tenants_Create)] [UnitOfWork(IsDisabled = true)] public async Task CreateTenant(CreateTenantInput input) { await TenantManager.CreateWithAdminUserAsync(input.TenancyName, input.Name, input.AdminPassword, input.AdminEmailAddress, input.ConnectionString, input.IsActive, input.EditionId, input.ShouldChangePasswordOnNextLogin, input.SendActivationEmail, input.SubscriptionEndDateUtc?.ToUniversalTime(), input.IsInTrialPeriod, AppUrlService.CreateEmailActivationUrlFormat(input.TenancyName), input.AzureStorageConnectionString ); } [AbpAuthorize(AppPermissions.Pages_Tenants_Edit)] public async Task<TenantEditDto> GetTenantForEdit(EntityDto input) { var tenantEditDto = ObjectMapper.Map<TenantEditDto>(await TenantManager.GetByIdAsync(input.Id)); tenantEditDto.ConnectionString = SimpleStringCipher.Instance.Decrypt(tenantEditDto.ConnectionString); tenantEditDto.AzureStorageConnectionString = SimpleStringCipher.Instance.Decrypt(tenantEditDto.AzureStorageConnectionString); return tenantEditDto; } [AbpAuthorize(AppPermissions.Pages_Tenants_Edit)] public async Task UpdateTenant(TenantEditDto input) { await TenantManager.CheckEditionAsync(input.EditionId, input.IsInTrialPeriod); input.ConnectionString = SimpleStringCipher.Instance.Encrypt(input.ConnectionString); input.AzureStorageConnectionString = SimpleStringCipher.Instance.Encrypt(input.AzureStorageConnectionString); var tenant = await TenantManager.GetByIdAsync(input.Id); ObjectMapper.Map(input, tenant); tenant.SubscriptionEndDateUtc = tenant.SubscriptionEndDateUtc?.ToUniversalTime(); await TenantManager.UpdateAsync(tenant); }
And TenantManager CreateWithAdminUserAsync:
public async Task<int> CreateWithAdminUserAsync( string tenancyName, string name, string adminPassword, string adminEmailAddress, string connectionString, bool isActive, int? editionId, bool shouldChangePasswordOnNextLogin, bool sendActivationEmail, DateTime? subscriptionEndDate, bool isInTrialPeriod, string emailActivationLink, string azureConnectionString) { --- //Create tenant var tenant = new Tenant(tenancyName, name) { IsActive = isActive, EditionId = editionId, SubscriptionEndDateUtc = subscriptionEndDate?.ToUniversalTime(), IsInTrialPeriod = isInTrialPeriod, ConnectionString = connectionString.IsNullOrWhiteSpace() ? null : SimpleStringCipher.Instance.Encrypt(connectionString), AzureStorageConnectionString = azureConnectionString.IsNullOrWhiteSpace() ? null : SimpleStringCipher.Instance.Encrypt(azureConnectionString) };
There you have.
-
0
I'm late to this party, unfortunately, as I could have provided some guidance since we implemented a similar solution several weeks ago.
We went one step further, however, and replace the TenantCacheItem so that we could have a strongly typed azure storage connection string, as well as any other tenant-specific items we would need in the future.
We also added an extension method to ITenantCache to GetExtendedCacheItem that will cast to our more specific type.
It's not the cleanest, but IMO it's better than using CustomData when you need more than 1 extra property (otherwise you have to simply know what's being stored on the custom data object and cast it everywhere accordingly, making extending and refactoring a nightmare.)
It's a shame that ITenantCache wasn't made as generic, that would have been a FAR cleaner solution.
-
0
Thank You @MikaTmlVertti . Really appreciate it.
@strix20 : No it's not late. I haven't started implementing. Will start in a day or two. Please share your idea as well. If not me might benefit others in the community. Thank you in advance
-
0
@MikaTmlVertti , @strix20 thank you guys...