Hosting multiple web sites using a single ASP.NET application

by Michael F. Collins, III May 18, 2009 06:51

When I initially started hosting my own web site, I had a single domain name: www.imaginaryrealities.com. Since then, I’ve taken advantage of the subdomain support of my hosting provider, WebHost4Life, and I’ve added additional subdomains to my collection such as services.imaginaryrealities.com, downloads.imaginaryrealities.com, media.imaginaryrealities.com, and others. In addition, I’ve started other web sites for other purposes. For example, I host my wife’s classroom website on my account at http://www.mrscollinsclassroom.net. The end result, is that while I started with a single web site, over time I’ve added more.

The traditional model for hosting multiple web sites is that I register the domains or subdomains with the hosting provider, and then I point the domain to a directory on my hosting account where the files for the web site will be hosted from. In the majority of cases, each web site points to a different directory, and a different application. For example, both my blog and my wife’s web site are built using BlogEngine.NET. To accomplish this, I have two copies of BlogEngine.NET installed in two different directories of my hosting account. Twice the space for two web sites. It would be great if, with my new web site engine that I’m building, I could consolidate all of these web sites to a single application installation. This post will show you how I’m thinking of doing this.

To start off, the application needs to know what web sites (or virtual web sites) the application supports. As each request comes in, the application needs to map the request to a specific virtual web site and then allow the request to be processed in the context of the virtual web site. To support the creation of virtual web sites, I defined two tables in the database. The first table is the Website table and stores the metadata for a virtual web site:

--------------------------------------------------------------------------
--
-- The Website table stores the definitions of virtual web sites that are
-- mapped to one or more domain names.
--
--------------------------------------------------------------------------

PRINT 'Creating table dbo.Website.';
GO

CREATE TABLE dbo.Website (
    WebsiteId INT IDENTITY(1,1) NOT NULL CONSTRAINT PKC_Website PRIMARY KEY
        CLUSTERED WITH FILLFACTOR = 90 ON "default",
    ApplicationId UNIQUEIDENTIFIER NOT NULL CONSTRAINT
        [aspnet_Applications-Website] FOREIGN KEY REFERENCES
        dbo.aspnet_Applications (ApplicationId) ON DELETE NO ACTION ON UPDATE
        CASCADE,
    Title NVARCHAR(256) NOT NULL,
    StyleSheetTheme NVARCHAR(256) NOT NULL,
    Theme NVARCHAR(256) NOT NULL
) ON "default";
GO

The Title field stores the title of the web site. At some point, this title will have to be globalized so that the title can be pulled from a resource to support different languages, but for now this will suffice. The ApplicationId field is a foreign key field that points to the ASP.NET aspnet_Applications table that stores the definitions of applications for the purposes of the ASP.NET providers such as membership and roles. By tying the ApplicationId field to my virtual web site, I can get the defined application name to use with the providers when I provide operations such as role-based security or authentication. The StyleSheetTheme and Theme fields define the default style sheet theme and theme to be used by the web site. I will use these fields later when I create a base class for the pages in my web site in order to automatically load the themes for the page’s rendering. For more information about these fields, see my previous post on ASP.NET themes.

The next issue is that given my set of virtual web sites, how do I map domain names to the web sites? I created a second table to help me support this task named WebsiteDomain:

--------------------------------------------------------------------------
--
-- The WebsiteDomain table maps domain names to virtual web sites.
--
--------------------------------------------------------------------------

PRINT 'Creating table dbo.WebsiteDomain.';
GO

CREATE TABLE dbo.WebsiteDomain (
    WebsiteDomainId INT IDENTITY(1,1) NOT NULL CONSTRAINT PKC_WebsiteDomain
        PRIMARY KEY CLUSTERED WITH FILLFACTOR = 90 ON "default",
    WebsiteId INT NOT NULL CONSTRAINT [Website-WebsiteDomain] FOREIGN KEY
        REFERENCES dbo.Website (WebsiteId) ON DELETE NO ACTION ON UPDATE
        CASCADE,
    DomainExpression NVARCHAR(256) NOT NULL,
    Description NVARCHAR(256) NOT NULL
) ON "default";
GO

The WebsiteDomain table maps a domain name, specified as a regular expression, to a virtual web site defined in the Website table that I defined earlier. The WebsiteId field is the foreign key field that defines the relationship between the two tables. The DomainExpression field stores the regular expression for a domain name. The Description field is just an extra field that at this point is not used but will provide a human-readable description of the domain name, possibly with examples.

With the database defined, I now need to put in the business logic in the web site application to map incoming requests to the correct virtual web site, and then support page processing. To do this, I’m going to intercept the BeginRequest event of the request pipeline inside of my Global.asax handler. My event handler will examine the host name that was specified in the request and will match the host name against a virtual web site using the regular expressions defined in the WebsiteDomain table. Once the virtual web site has been mapped, the handler will create a WebsiteRequestContext object that will contain the virtual web site’s settings. The reference to the WebsiteRequestContext object will be stored in the HttpContext.Items collection for pages or other HTTP handlers to access and use. The WebsiteRequestContext object is defined as:

/// 
/// Stores information about the current request that can be used when
/// processing the request or rendering the page or response.
/// 
public class WebsiteRequestContext
{
    /// 
    /// Initializes a new instance of the WebsiteRequestContext class.
    /// 
    /// 
    /// The unique identifier of the virtual web site.
    /// 
    public WebsiteRequestContext(int websiteId)
    {
        this.WebsiteId = websiteId;
    }

    /// 
    /// Gets or sets the name of the ASP.NET application for the virtual
    /// web site.
    /// 
    /// 
    /// The value of this property is the name of the ASP.NET application
    /// that the virtual web site uses for membership or roles.
    /// 
    public string ApplicationName
    {
        get;
        set;
    }

    /// 
    /// Gets or sets the name of the style sheet theme to use for rendering
    /// the ASP.NET page.
    /// 
    /// 
    /// The value of this property is the name of the style sheet theme
    /// to use for rendering the ASP.NET page.
    /// 
    public string StyleSheetTheme
    {
        get;
        set;
    }

    /// 
    /// Gets or sets the name of the theme to use for rendering the ASP.NET
    /// page.
    /// 
    /// 
    /// The value of this property is the name of the theme to use for
    /// rendering the ASP.NET page.
    /// 
    public string Theme
    {
        get;
        set;
    }

    /// 
    /// Gets or sets the title of the web site or page.
    /// 
    /// 
    /// The value of this property is the title of the page or web site.
    /// 
    public string Title
    {
        get;
        set;
    }

    /// 
    /// Gets the unique identifier of the virtual web site.
    /// 
    /// 
    /// The value of this property is the unique identifier of the virtual
    /// web site.
    /// 
    public int WebsiteId
    {
        get;
        private set;
    }
}

The properties on the WebsiteRequestContext class mirror the fields on the Website table for now. This object is created and stored by the event handler for the BeginRequest event:

/// 
/// Called when an incoming request is received from a web browser or
/// client to map the request to a virtual web site.
/// 
/// 
/// The  object.
/// 
/// 
/// The event arguments.
/// 
[SuppressMessage(
    "Microsoft.Performance",
    "CA1811:AvoidUncalledPrivateCode",
    Justification = "ASP.NET automatically ties this method to the BeginRequest event handler.")]
private void Application_BeginRequest(object sender, EventArgs e)
{
    var httpContext = HttpContext.Current;
    var website = FindWebsite(httpContext);
    var websiteRequestContext =
        new WebsiteRequestContext(website.Id)
        {
            ApplicationName = website.ApplicationName,
            StyleSheetTheme = website.StyleSheetTheme,
            Theme = website.Theme,
            Title = website.Title
        };
    httpContext.Items.Add(
        "WebsiteRequestContext",
        websiteRequestContext);
}

The Application_BeginRequest event handler will use the request URI in the HttpContext.Current object to match the request to a virtual web site using the FindWebsite method. When the virtual web site has been found, a new WebsiteRequestContext object is created with the virtual web site’s settings and is then stored in the HttpContext.Items collection to be available to the other HTTP modules or handlers (page, web service, etc.) that might process the request.

The FindWebsite method is defined below:

/// 
/// Maps the host name specified in the request to a virtual web site.
/// 
/// 
/// The  object for the current request.
/// 
/// 
/// Returns the  object for the virtual web site.
/// 
[SuppressMessage(
    "Microsoft.Performance",
    "CA1811:AvoidUncalledPrivateCode",
    Justification = "This method is called by the Application_BeginRequest event handler.")]
private static Website FindWebsite(HttpContext httpContext)
{
    var domainList =
        (List)httpContext.Cache["WebsiteDomains"];
    if (null == domainList)
    {
        var database = DatabaseFactory.CreateDatabase();

        var websiteDictionary = LoadWebsites(database);
        domainList = LoadDomains(database, websiteDictionary);
        AddDomainsToCache(httpContext, domainList, database);
    }

    var domainName = httpContext.Request.Url.Host;
    var domain = domainList.FirstOrDefault(x => x.IsMatch(domainName));
    return null != domain ? domain.Website : null;
}

First, the FindWebsite method will look in the ASP.NET application cache to see if the virtual web site metadata is present in memory or cache. If not, then the metadata will need to be loaded from the database. When the metadata has been loaded, the FindWebsite method will walk through the collection of virtual domains until a matching domain is found. When the matching domain is found, then the virtual web site’s information is returned. You’ll notice that the return value can either be a Website object or a null reference, but my Application_BeginRequest event handler doesn’t process the null value right now. That’s being left as a future enhancement that I’ll work on at a later point for when a request comes in that doesn’t match any of the defined domain names.

The Website class is defined below:

/// 
/// Domain object type for a virtual web site instance.
/// 
[SuppressMessage("Microsoft.Naming", "CA1724:TypeNamesShouldNotMatchNamespaces")]
public class Website
{
    /// 
    /// Initializes a new instance of the Website class.
    /// 
    /// 
    /// The unique identifier of the virtual web site.
    /// 
    public Website(int id)
    {
        this.Id = id;
    }

    /// 
    /// Gets or sets the name of the ASP.NET application that the virtual
    /// web site uses for memberships and roles.
    /// 
    /// 
    /// The value of this property is the name of the ASP.NET application
    /// for the virtual web site.
    /// 
    public string ApplicationName
    {
        get;
        set;
    }

    /// 
    /// Gets or sets the unique identifier for the virtual web site.
    /// 
    /// 
    /// The value of this property is the unique identifier of the virtual
    /// web site.
    /// 
    public int Id
    {
        get;
        private set;
    }

    /// 
    /// Gets or sets the default name of the style sheet theme for the
    /// web site.
    /// 
    /// 
    /// The value of this property is the name of the default style sheet
    /// theme for the web site's pages.
    /// 
    public string StyleSheetTheme
    {
        get;
        set;
    }

    /// 
    /// Gets or sets the name of the default theme to use for the web site.
    /// 
    /// 
    /// The value of this property is the name of the default theme to
    /// use for the web site's pages.
    /// 
    public string Theme
    {
        get;
        set;
    }

    /// 
    /// Gets or sets the title of the web site.
    /// 
    /// 
    /// The value of this property is the title of the web site.
    /// 
    public string Title
    {
        get;
        set;
    }
}

The Website class mirrors the WebsiteRequestContext class right now. You may question why I have two identical classes, and why I can’t just store the Website object into the HttpContext.Items collection. The main reason is that as I develop the web site engine, I may add additional information to the WebsiteRequestContext class, so I have the two classes that right now appear identical, but over time will hopefully not.

The last class that I need to define is the WebsiteDomain class that maps domain names to Website objects for the event handler:

/// 
/// Domain type that uses a regular expression to map a domain name to
/// a  object.
/// 
public class WebsiteDomain
{
    /// 
    /// The  object that will be used to evaluate domain
    /// names.
    /// 
    private readonly Regex domainExpression;

    /// 
    /// Initializes a new instance of the WebsiteDomain class.
    /// 
    /// 
    /// The regular expression used to match the virtual web site to a
    /// domain name.
    /// 
    /// 
    /// The  object representing the virtual web site.
    /// 
    public WebsiteDomain(string domainExpression, Website website)
    {
        this.domainExpression = new Regex(
            domainExpression,
            RegexOptions.Compiled | RegexOptions.Singleline);
        this.Website = website;
    }

    /// 
    /// Gets the  object that the domain represents.
    /// 
    /// 
    /// The value of this property is the  object
    /// representing the virtual web site.
    /// 
    public Website Website
    {
        get;
        private set;
    }

    /// 
    /// Matches the specified domain name to the virtual web site.
    /// 
    /// 
    /// The host name that was specified in the request.
    /// 
    /// 
    /// Returns true if  refers to a domain for the
    /// virtual web site, or false if  is not a
    /// match for the virtual web site.
    /// 
    public bool IsMatch(string host)
    {
        return this.domainExpression.IsMatch(host);
    }
}

The WebsiteDomain class uses the Regex class to parse the regular expression that is stored in the database and to evaluate requested domain names against the regular expression. If the domain name matches the regular expression, then the event handler can get the virtual web site information from the Website property.

Now that I’ve defined the Website and WebsiteDomain classes, I need to create them using the records in the database. First, I’ll load the Website objects from the database:

/// 
/// Loads the virtual web site definitions from the database.
/// 
/// 
/// The  object representing the web site's
/// content database.
/// 
/// 
/// Returns a dictionary mapping  objects to
/// their unique identifier.
/// 
[SuppressMessage(
    "Microsoft.Performance",
    "CA1811:AvoidUncalledPrivateCode",
    Justification = "This method is called by the Application_BeginRequest event handler.")]
private static Dictionary LoadWebsites(Database database)
{
    var websiteDictionary = new Dictionary();
    var selectWebsitesCommand = database.GetSqlStringCommand(
        "SELECT w.WebsiteId, w.Title, a.ApplicationName, w.StyleSheetTheme, w.Theme FROM dbo.Website AS w INNER JOIN dbo.aspnet_Applications AS a ON w.ApplicationId = a.ApplicationId");
    using (selectWebsitesCommand)
    {
        var reader = database.ExecuteReader(selectWebsitesCommand);
        using (reader)
        {
            while (reader.Read())
            {
                var websiteId = reader.GetInt32(0);
                var title = reader.GetString(1);
                var applicationName = reader.GetString(2);
                var stylesheetTheme = reader.GetString(3);
                var theme = reader.GetString(4);

                var website = new Website(websiteId)
                {
                    ApplicationName = applicationName,
                    StyleSheetTheme = stylesheetTheme,
                    Theme = theme,
                    Title = title
                };
                websiteDictionary.Add(websiteId, website);
            }
        }
    }
    return websiteDictionary;
}

I’m using the Enterprise Library’s Data Access Application Block (DAAB) to access the database and load the data. The LoadWebsites method will select all of the virtual web sites from the dbo.Website table and will store each Website object in a dictionary, keyed by the unique identifier from the Website table. Once the web sites are loaded, they need to be matched to their domain expression in the WebsiteDomain objects:

/// 
/// Loads the list of virtual domains from the database.
/// 
/// 
/// The  object representing the web site's
/// database.
/// 
/// 
/// A dictionary mapping  objects based on their
/// unique identifier.
/// 
/// 
/// Returns a list of  objects that can
/// be used to map requests to virtual web sites.
/// 
[SuppressMessage(
    "Microsoft.Performance",
    "CA1811:AvoidUncalledPrivateCode",
    Justification = "This method is called by the Application_BeginRequest event handler.")]
private static List LoadDomains(
    Database database,
    IDictionary websiteDictionary)
{
    var domainList = new List();
    var selectWebsiteDomainsCommand = database.GetSqlStringCommand(
        "SELECT WebsiteId, DomainExpression FROM dbo.WebsiteDomain");
    using (selectWebsiteDomainsCommand)
    {
        var reader =
            database.ExecuteReader(selectWebsiteDomainsCommand);
        using (reader)
        {
            while (reader.Read())
            {
                var websiteId = reader.GetInt32(0);
                var domainExpression = reader.GetString(1);

                var website = websiteDictionary[websiteId];
                var websiteDomain = new WebsiteDomain(
                    domainExpression,
                    website);
                domainList.Add(websiteDomain);
            }
        }
    }

    return domainList;
}

Using the dictionary that was built in the LoadWebsites method, the LoadDomains method will load the domain expressions from the dbo.WebsiteDomain table and will create a list of WebsiteDomain objects. The LoadDomains method will select the WebsiteId and DomainExpression field from each dbo.WebsiteDomain record. Using the WebsiteId value, the LoadDomains method will obtain the Website object from the dictionary that was created by the LoadWebsites method. Then a new WebsiteDomain object will be created mapping the virtual web site to the domain name regular expression. Finally, the list of WebsiteDomain objects will be returned, cached, and then used to match future requests to virtual web sites.

At this point, we’ve mapped requests to virtual web sites and stored a WebsiteRequestContext object in the HttpContext.Items collection. So how do we use this information? To support using the information such as the title, theme, and style sheet theme, I’ve created a custom base class to use for pages on my web site:

/// 
/// Base class for a website ASP.NET web forms page.
/// 
public class WebsitePage : Page
{
    /// 
    /// Gets or sets the style sheet theme for the page.
    /// 
    /// 
    /// The value of this property is the name of the style sheet theme
    /// to use for the page.
    /// 
    public override string StyleSheetTheme
    {
        get
        {
            var hasStyleSheetTheme = !String.IsNullOrEmpty(
                this.WebsiteRequestContext.StyleSheetTheme);
            return hasStyleSheetTheme
                ? this.WebsiteRequestContext.StyleSheetTheme
                : base.StyleSheetTheme;
        }

        set
        {
            base.StyleSheetTheme = value;
        }
    }

    /// 
    /// Gets or sets the name of the theme to use for rendering the page.
    /// 
    /// 
    /// The value of this property is the name of the theme to use for
    /// rendering the page.
    /// 
    public override string Theme
    {
        get
        {
            return !String.IsNullOrEmpty(this.WebsiteRequestContext.Theme)
                ? this.WebsiteRequestContext.Theme
                : base.Theme;
        }

        set
        {
            base.Theme = value;
        }
    }
    
    /// 
    /// Gets the  object containing the
    /// web site information for the current request.
    /// 
    /// 
    /// The value of this property is a 
    /// object.
    /// 
    protected WebsiteRequestContext WebsiteRequestContext
    {
        get
        {
            return (WebsiteRequestContext)
                this.Context.Items["WebsiteRequestContext"];
        }
    }

    /// 
    /// Sets the default title of the page before raising the Load event.
    /// 
    /// 
    /// The event arguments.
    /// 
    protected override void OnLoad(EventArgs e)
    {
        this.Title = this.WebsiteRequestContext.Title ?? String.Empty;

        base.OnLoad(e);
    }
}

As you can see, the WebsitePage class overrides the Theme and StyleSheetTheme properties to return the theme that was specified in the dbo.Website table. The WebsitePage class also overrides the OnLoad method to set the default title of the page to the title of the virtual web site. The title is set before the Load event is raised on the page in order to allow pages to override the page title.

What I’ve ended up with is not yet a complete solution, but it’s good enough to start hosting multiple domains using a single web application. As I continue to develop the engine for the web site application, I’ll be adding more functionality to this, but it’s a rather good start.



Tags:

ASP.NET | ASP.NET Themes | ImaginaryRealities.com

Comments

8/28/2009 8:28:10 PM #

trackback

一个虚拟主机上放多个网站(asp.net)

Asp.net不像Asp一样,建个文件夹就能放一个程序,互不干扰,为了让一个虚拟主机能放多个Asp.net,查找了不少资料,没有一个答案是完美的,不过有些资料倒给了我一些启发,通过思考,加上实践,终于...

emanlee

Powered by BlogEngine.NET 1.5.0.7
Theme by Mads Kristensen | Modified by Mooglegiant

Calendar

<<  March 2010  >>
MoTuWeThFrSaSu
22232425262728
1234567
891011121314
15161718192021
22232425262728
2930311234

View posts in large calendar

What I'm reading now


Add to Technorati Favorites

Disclaimer

The views expressed on this website/blog are the opinions of Michael F. Collins, III, and do not necessarily reflect the views of my employer.