Base solution for your next web application
Open Closed

Implement Separete DB context for Reporting #2340


User avatar
0
maharatha created

Hi -

I use the Multi Tenant - Multi Database approach in my application. I would like all the reporting request to hit a replicated database for the Tenant.

I was wondering how do I go about it?

Thanks, Partha


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

    Hi,

    Sorry for the late response. You can check this example <a class="postlink" href="https://github.com/aspnetboilerplate/aspnetboilerplate-samples/tree/master/MultipleDbContextDemo">https://github.com/aspnetboilerplate/as ... ontextDemo</a>.

  • User Avatar
    0
    maharatha created

    I guess I asked the question in the wrong way.

    I am currently using multi-databse and multi -tenant architecture. So all my connection string are stored in Tenant table.

    In production each database will be replicated to either to the same server with a suffix as "DatabaseNaem_RPT" or might get replicated to a separate server with the same name.

    I am using Grids extensively and also providing excel export option which will export the entire grid just not the visible data.

    In such cases I would like to connect to the replicated or reporting database for the tenant and fetch the data.

    But both the Grid and excel export will use the same service and only the page size will change based on the request.

    So if I want to fetch all data I send the page size= number of records and for this scenario I would like to change the dbcontext connection string to the reporting database and for other request I will use the existing db context.

    I need some kind of smart solution where I don't have to make lot of changes .

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi,

    Sorry, I got it now. DbPerTenantConnectionStringResolver class is used to determine the connection string, see <a class="postlink" href="https://github.com/aspnetboilerplate/module-zero/blob/09cb578f09ee0318b479aa31dd0ceff56a5d218d/src/Abp.Zero.EntityFramework/Zero/EntityFramework/DbPerTenantConnectionStringResolver.cs">https://github.com/aspnetboilerplate/mo ... esolver.cs</a>.

    You can create your own implementation of IDbPerTenantConnectionStringResolver <a class="postlink" href="https://github.com/aspnetboilerplate/module-zero/blob/09cb578f09ee0318b479aa31dd0ceff56a5d218d/src/Abp.Zero/MultiTenancy/IDbPerTenantConnectionStringResolver.cs">https://github.com/aspnetboilerplate/mo ... esolver.cs</a>, and use it instead of DbPerTenantConnectionStringResolver.

    For example, you can add an http header while you request an excel file from server and decide the database if this header is set in your custom ConnectionStringResolver.

  • User Avatar
    0
    maharatha created

    Thanks a lot and I am going to try and implement this. Will let you know how it goes.

  • User Avatar
    0
    maharatha created

    I wrote the below class :

    public class DbPerTenantConnectionStringResolver : IDbPerTenantConnectionStringResolver
        {
            /// <summary>
            /// Reference to the session.
            /// </summary>
            public IAbpSession AbpSession { get; set; }
    
            private readonly ICurrentUnitOfWorkProvider _currentUnitOfWorkProvider;
            private readonly ITenantCache _tenantCache;
    
            /// <summary>
            /// Initializes a new instance of the <see cref="DbPerTenantConnectionStringResolver"/> class.
            /// </summary>
            public DbPerTenantConnectionStringResolver(
                IAbpStartupConfiguration configuration,
                ICurrentUnitOfWorkProvider currentUnitOfWorkProvider,
                ITenantCache tenantCache)
            {
                _currentUnitOfWorkProvider = currentUnitOfWorkProvider;
                _tenantCache = tenantCache;
    
                AbpSession = NullAbpSession.Instance;
            }
    
            public string GetNameOrConnectionString(ConnectionStringResolveArgs args)
            {
                if (args.MultiTenancySide == MultiTenancySides.Host)
                {
                    return GetNameOrConnectionString(new DbPerTenantConnectionStringResolveArgs(null, args));
                }
    
                return GetNameOrConnectionString(new DbPerTenantConnectionStringResolveArgs(GetCurrentTenantId(), args));
            }
    
            public virtual string GetNameOrConnectionString(DbPerTenantConnectionStringResolveArgs args)
            {
                if (args.TenantId == null)
                {
                    //Requested for host
                    return GetNameOrConnectionString(args);
                }
    
                var tenantCacheItem = _tenantCache.Get(args.TenantId.Value);
                if (tenantCacheItem.ConnectionString.IsNullOrEmpty())
                {
                    //Tenant has not dedicated database
                    return GetNameOrConnectionString(args);
                }
    
                return tenantCacheItem.ConnectionString;
            }
    
            protected virtual int? GetCurrentTenantId()
            {
                return _currentUnitOfWorkProvider.Current != null
                    ? _currentUnitOfWorkProvider.Current.GetTenantId()
                    : AbpSession.TenantId;
            }
        }
    

    I wrote this in my Entityframework project.

    Still the brek point is not hitting.

    Am I doing something wrong here ?

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi @maharatha,

    Sorry, I forgot to mention. You need to use ReplaceService in PreInitialize of your module.

    Configuration.ReplaceService<IDbPerTenantConnectionStringResolver, DbPerTenantConnectionStringResolver>
    

    Since you have the same name as original DbPerTenantConnectionStringResolver, be sure the second generic argument is your class or you can give DbPerTenantConnectionStringResolver another name.

  • User Avatar
    0
    maharatha created

    This is what I have done so far. Wrote a class on the Entityframeowrk project

    public class MyDbPerTenantConnectionStringResolver : IDbPerTenantConnectionStringResolver
        {
            /// <summary>
            /// Reference to the session.
            /// </summary>
            public IAbpSession AbpSession { get; set; }
    
            private readonly ICurrentUnitOfWorkProvider _currentUnitOfWorkProvider;
            private readonly ITenantCache _tenantCache;
    
            /// <summary>
            /// Initializes a new instance of the <see cref="SumitDbPerTenantConnectionStringResolver"/> class.
            /// </summary>
            public SumitDbPerTenantConnectionStringResolver(
                IAbpStartupConfiguration configuration,
                ICurrentUnitOfWorkProvider currentUnitOfWorkProvider,
                ITenantCache tenantCache)
            {
                _currentUnitOfWorkProvider = currentUnitOfWorkProvider;
                _tenantCache = tenantCache;
    
                AbpSession = NullAbpSession.Instance;
            }
    
            public string GetNameOrConnectionString(ConnectionStringResolveArgs args)
            {
                if (args.MultiTenancySide == MultiTenancySides.Host)
                {
                    return GetNameOrConnectionString(new DbPerTenantConnectionStringResolveArgs(null, args));
                }
    
                return GetNameOrConnectionString(new DbPerTenantConnectionStringResolveArgs(GetCurrentTenantId(), args));
            }
    
            public virtual string GetNameOrConnectionString(DbPerTenantConnectionStringResolveArgs args)
            {
                if (args.TenantId == null)
                {
                    //Requested for host
                    return GetNameOrConnectionString(args);
                }
    
                var tenantCacheItem = _tenantCache.Get(args.TenantId.Value);
                if (tenantCacheItem.ConnectionString.IsNullOrEmpty())
                {
                    //Tenant has not dedicated database
                    return GetNameOrConnectionString(args);
                }
    
                return tenantCacheItem.ConnectionString;
            }
    
            protected virtual int? GetCurrentTenantId()
            {
                return _currentUnitOfWorkProvider.Current != null
                    ? _currentUnitOfWorkProvider.Current.GetTenantId()
                    : AbpSession.TenantId;
            }
        }
    

    Then added the code the following code on the data module on the PreInitialize

    Configuration.ReplaceService<IDbPerTenantConnectionStringResolver, MyDbPerTenantConnectionStringResolver>();
    

    Still it's not overiding. Am I missing anything ?

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi,

    I didn't know this but there is a special case for IDbPerTenantConnectionStringResolver. You need to replace it like this.

    Configuration.ReplaceService(typeof(IConnectionStringResolver), () =>
    {
        IocManager.IocContainer.Register(
            Component.For<IConnectionStringResolver, IDbPerTenantConnectionStringResolver>()
                .ImplementedBy<MyDbPerTenantConnectionStringResolver>()
                .LifestyleTransient()
            );
    });