简体   繁体   中英

How to handle exception thrown from Controller's Initialize method?

Question:

How to handle exception thrown from Controller's Initialize method?

Background story:

We have .NET MVC application that used to work with single database. We have many controllers that create database context in constructor as member and than use it in actions. Connection string was stored in Web.config. New requirement is that we want to support multiple clients each with separate database in same application instance ( multi-tenant ). We do not want controllers to be aware of existence of multiple databases. We have catalog database from which given clients connection string may be obtained. Initial approach was to create common base for controllers that overrides Controller.Initialize since it is the first place we could get user identity and query catalog database for clients connection string and initialize database context. It worked nice until we found out a need to have users not connected to any specific database. Then the idea was to throw exception in Initialize and catch it in exception filter to redirect user to page informing that this page features require being assign to database. Unfortunately Initialize is not action and exceptions thrown from it are not available to filters.

From your question, I understand that you are in the process of enabling tenant database model for the application that you are building. in that case, you should have a factory that does the following

  • Tenant resolution (either from the URL or based on any other input parameters)
  • Have a shard manager that looks up the connection string by a tenant identifier and then uses that to contact the right database for the tenant.

In short, you should be having something like the one below

public interface ITenantShardResolver
    {
        /// <summary>
        /// Gets the tenant specific shard based connection from the metadata
        /// </summary>
        /// <param name="tenantId">The tenant identifier</param>
        /// <param name="connectionStringName">The Connection string name</param>
        /// <returns>
        /// The DbConnection for the specific shard per tenant
        /// <see cref="DbConnection"/> 
        /// </returns>
        DbConnection GetConnection(Guid tenantId, string connectionStringName);
    }

The above is a generic interface that can be used to get the connectionstrings based on the established tenant context.

The base database context would look something like the one below,

public abstract class EntitiesContext : DbContext, IEntitiesContext
    {
        /// <summary>
        /// Constructs a new context instance using conventions to create the name of
        /// the database to which a connection will be made. The by-convention name is
        /// the full name (namespace + class name) of the derived context class.  See
        /// the class remarks for how this is used to create a connection. 
        /// </summary>
        protected EntitiesContext() : base()
        {
        }

        /// <summary>
        /// Initializes the entity context based on the established user context and the tenant shard map resolver
        /// </summary>
        /// <param name="userContext">The user context</param>
        /// <param name="shardResolver">The Tenant Shard map resolver</param>
        /// <param name="nameorConnectionString">The name or the connection string for the entity</param>
        protected EntitiesContext(MultiTenancy.Core.ProviderContracts.IUserContextDataProvider userContext, ITenantShardResolver shardResolver, string nameorConnectionString)
            : base(shardResolver.GetConnection(userContext.TenantId, nameorConnectionString), true)
        {

        }
}

A sample context would look like the one below,

public class AccommodationEntities : EntitiesContext
    {
        public AccommodationEntities(IUserContextDataProvider userContextProvider, ITenantShardResolver shardResolver)
            : base(userContextProvider, shardResolver, "AccommodationEntities")
        { }

        // NOTE: You have the same constructors as the DbContext here. E.g:
        public AccommodationEntities() : base("AccommodationEntities") { }

        public IDbSet<Country> Countries { get; set; }
        public IDbSet<Resort> Resorts { get; set; }
        public IDbSet<Hotel> Hotels { get; set; }
    }

The base service which can talk to the above context's would look like the one below

public abstract class MultiTenantServices<TEntity, TId>
    where TEntity : class, IMultiTenantEntity<TId>
    where TId : IComparable
{
    private readonly IMultiTenantRepository<TEntity, TId> _repository;

    /// <summary>
    /// Initializes a new instance of the <see cref="MultiTenantServices{TEntity, TId}"/> class.
    /// </summary>
    /// <param name="repository">The repository.</param>
    protected MultiTenantServices(IMultiTenantRepository<TEntity, TId> repository)
    {
        _repository = repository;
    }

With the sample entity service looking like the one below,

public class CountryService : MultiTenantServices<Country, int>
    {
        IMultiTenantRepository<Country, int> _repository = null;

        public CountryService(IMultiTenantRepository<Country, int> repository) : base(repository)
        {
            _repository = repository;
        }

The above code snippets illustrate a well tested and a good way to organize / architect your app for multi-tenancy.

HTH

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM