Base solution for your next web application
Open Closed

Azure Storage - default/tenant specific connection string #5220


User avatar
0
mikatmlvertti created

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.

  1. Add storage connection string to Tenant entity
  2. Get that connection string when needed, or if not exist, get default.

Mika


15 Answer(s)
  • User Avatar
    0
    ismcagdas created
    Support Team

    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.

  • User Avatar
    0
    mikatmlvertti created

    Is it possible to add this new item to tenant cache?

  • User Avatar
    0
    ismcagdas created
    Support Team

    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);
    
  • User Avatar
    0
    mikatmlvertti created

    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?

  • User Avatar
    0
    alper created
    Support Team

    I think you need to replace the service with the code like below.

    Configuration.ReplaceService<ITenantCache, TenantaCacheExtended>(DependencyLifeStyle.Transient);
    
  • User Avatar
    0
    mikatmlvertti created

    <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?

  • User Avatar
    0
    ismcagdas created
    Support Team

    @MikaTmlVertti could you show the code how did you try it in the Initialize Method ?

  • User Avatar
    0
    mikatmlvertti created

    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.

  • User Avatar
    0
    mikatmlvertti created

    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().

  • User Avatar
    0
    ismcagdas created
    Support Team

    Great :)

  • User Avatar
    0
    maharatha created

    <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.

    1. Add storage connection string to Tenant entity
    2. Get that connection string when needed, or if not exist, get default.

    Mika

    Do you mind sharing the entire implementation?

  • User Avatar
    1
    mikatmlvertti created

    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.

  • User Avatar
    0
    strix20 created

    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.

  • User Avatar
    0
    maharatha created

    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

  • User Avatar
    0
    alper created
    Support Team

    @MikaTmlVertti , @strix20 thank you guys...