Multiple domain website in ASP.NET Core

This post describes how you can set up one website that handles multiple domain names in ASP.NET Core. A multi-domain website is cheaper to host, easier to maintain and easier to administer compared to multiple websites. A single multidomain website enables you to customize content, language and design for each domain.

A multidomain website enables you to translate your website to different languages and/or to have multiple shops on a website by connecting products to domains. You can connect your models to a domain, to a language and/or to a shop. You can combine connections, some models can be connected to a language and some models can be conntected to a domain.

A multiple domain website stores domain models in a database, gets the current domain on a request and use the properties in the domain model to fetch other resources like static texts and a page or a product.

Model

A domain model includes the properties that is needed to correctly connect resources to the domain name. The domain_name property is used to find the current domain.

public class WebDomain
{
    #region Variables

    public Int32 id { get; set; }
    public string website_name { get; set; }
    public string domain_name { get; set; }
    public string web_address { get; set; }
    public Int32 front_end_language { get; set; }
    public Int32 back_end_language { get; set; }
    public string analytics_tracking_id { get; set; }
    public string facebook_app_id { get; set; }
    public string facebook_app_secret { get; set; }
    public string google_app_id { get; set; }
    public string google_app_secret { get; set; }
    public bool noindex { get; set; }

    #endregion

    #region Constructors

    public WebDomain()
    {
        // Set values for instance variables
        this.id = 0;
        this.website_name = "";
        this.domain_name = "";
        this.web_address = "";
        this.front_end_language = 0;
        this.back_end_language = 0;
        this.analytics_tracking_id = "";
        this.facebook_app_id = "";
        this.facebook_app_secret = "";
        this.google_app_id = "";
        this.google_app_secret = "";
        this.noindex = false;

    } // End of the constructor

    public WebDomain(SqlDataReader reader)
    {
        // Set values for instance variables
        this.id = Convert.ToInt32(reader["id"]);
        this.website_name = reader["website_name"].ToString();
        this.domain_name = reader["domain_name"].ToString();
        this.web_address = reader["web_address"].ToString();
        this.front_end_language = Convert.ToInt32(reader["front_end_language"]);
        this.back_end_language = Convert.ToInt32(reader["back_end_language"]);
        this.analytics_tracking_id = reader["analytics_tracking_id"].ToString();
        this.facebook_app_id = reader["facebook_app_id"].ToString();
        this.facebook_app_secret = reader["facebook_app_secret"].ToString();
        this.google_app_id = reader["google_app_id"].ToString();
        this.google_app_secret = reader["google_app_secret"].ToString();
        this.noindex = Convert.ToBoolean(reader["noindex"]);

    } // End of the constructor

    #endregion

} // End of the class

Repository class

Our repository class for web domains includes all the methods that we need to manage web domains on our website. We are using MS SQL as database and distributed cache to speed up the time to get the current domain.

public class WebDomainRepository : IWebDomainRepository
{
    #region Variables

    private readonly IMsSqlDatabaseRepository database_repository;
    private readonly IDistributedCache distributed_cache;
    private readonly CacheOptions cache_options;

    #endregion

    #region Constructors

    public WebDomainRepository(IMsSqlDatabaseRepository database_repository, IDistributedCache distributed_cache, IOptions<CacheOptions> cache_options)
    {
        this.database_repository = database_repository;
        this.distributed_cache = distributed_cache;
        this.cache_options = cache_options.Value;

    } // End of the constructor

    #endregion

    #region Insert methods

    public Int32 Add(WebDomain post)
    {
        // Create the int to return
        Int32 idOfInsert = 0;

        // Create the sql statement
        string sql = "INSERT INTO dbo.web_domains (website_name, domain_name, web_address, front_end_language, back_end_language, "
            + "analytics_tracking_id, facebook_app_id, facebook_app_secret, google_app_id, google_app_secret, noindex) "
            + "VALUES (@website_name, @domain_name, @web_address, @front_end_language, @back_end_language, "
            + "@analytics_tracking_id, @facebook_app_id, @facebook_app_secret, @google_app_id, @google_app_secret, @noindex);SELECT CAST(SCOPE_IDENTITY() AS INT);";

        // Create parameters
        IDictionary<string, object> parameters = new Dictionary<string, object>();
        parameters.Add("@website_name", post.website_name);
        parameters.Add("@domain_name", post.domain_name);
        parameters.Add("@web_address", post.web_address);
        parameters.Add("@front_end_language", post.front_end_language);
        parameters.Add("@back_end_language", post.back_end_language);
        parameters.Add("@analytics_tracking_id", post.analytics_tracking_id);
        parameters.Add("@facebook_app_id", post.facebook_app_id);
        parameters.Add("@facebook_app_secret", post.facebook_app_secret);
        parameters.Add("@google_app_id", post.google_app_id);
        parameters.Add("@google_app_secret", post.google_app_secret);
        parameters.Add("@noindex", post.noindex);

        // Insert the post
        this.database_repository.Insert<Int32>(sql, parameters, out idOfInsert);

        // Return the id of the inserted item
        return idOfInsert;

    } // End of the Add method

    #endregion

    #region Update methods

    public void Update(WebDomain post)
    {
        // Create the sql statement
        string sql = "UPDATE dbo.web_domains SET website_name = @website_name, domain_name = @domain_name, web_address = @web_address, "
            + "front_end_language = @front_end_language, back_end_language = @back_end_language, "
            + "analytics_tracking_id = @analytics_tracking_id, facebook_app_id = @facebook_app_id, facebook_app_secret = @facebook_app_secret, "
            + "google_app_id = @google_app_id, google_app_secret = @google_app_secret, noindex = @noindex WHERE id = @id;";

        // Create parameters
        IDictionary<string, object> parameters = new Dictionary<string, object>();
        parameters.Add("@website_name", post.website_name);
        parameters.Add("@domain_name", post.domain_name);
        parameters.Add("@web_address", post.web_address);
        parameters.Add("@front_end_language", post.front_end_language);
        parameters.Add("@back_end_language", post.back_end_language);
        parameters.Add("@analytics_tracking_id", post.analytics_tracking_id);
        parameters.Add("@facebook_app_id", post.facebook_app_id);
        parameters.Add("@facebook_app_secret", post.facebook_app_secret);
        parameters.Add("@google_app_id", post.google_app_id);
        parameters.Add("@google_app_secret", post.google_app_secret);
        parameters.Add("@noindex", post.noindex);

        // Update the post
        this.database_repository.Update(sql, parameters);

    } // End of the Update method

    #endregion

    #region Count methods

    public Int32 GetCountBySearch(string[] keywords)
    {
        // Create the sql statement
        string sql = "SELECT COUNT(id) AS count FROM dbo.web_domains WHERE 1 = 1";
        for (int i = 0; i < keywords.Length; i++)
        {
            sql += " AND (website_name LIKE @keyword_" + i.ToString() + " OR domain_name LIKE @keyword_" + i.ToString() + ")";
        }
        sql += ";";

        // Create parameters
        IDictionary<string, object> parameters = new Dictionary<string, object>();
        for (int i = 0; i < keywords.Length; i++)
        {
            parameters.Add("@keyword_" + i.ToString(), "%" + keywords[i].ToString() + "%");
        }

        // Get the count
        Int32 count = this.database_repository.GetCount<Int32>(sql, parameters);

        // Return the count
        return count;

    } // End of the GetCountBySearch method

    #endregion

    #region Get methods

    public WebDomain GetCurrentDomain(HttpContext context)
    {
        // Get the domain name
        string domain_name = context.Request.Host.ToString();

        // Replace www.
        domain_name = domain_name.Replace("www.", "");

        // Get the web domain post
        WebDomain domain = GetFromCache(domain_name);

        // Make sure that the domain not is null
        if (domain == null)
        {
            domain = new WebDomain();
            domain.id = 0;
            domain.domain_name = "localhost";
            domain.web_address = "https://localhost:80";
            domain.front_end_language = 2;
            domain.back_end_language = 2;
            domain.analytics_tracking_id = "";
            domain.facebook_app_id = "";
            domain.facebook_app_secret = "";
            domain.google_app_id = "";
            domain.google_app_secret = "";
            domain.noindex = true;
        }

        // Return the domain
        return domain;

    } // End of the GetCurrentDomain method

    public KeyStringList GetDomainImageUrls(IHostingEnvironment environment, Int32 domain_id, bool showNoImageIcon)
    {
        // Create the list to return
        KeyStringList imageUrls = new KeyStringList(5);

        // Create paths
        string directoryPath = "/domains/" + domain_id.ToString() + "/images/";

        // Add images to the key string list
        imageUrls.Add("background_image", directoryPath + "background_image.jpg");
        imageUrls.Add("default_logotype", directoryPath + "default_logotype.png");
        imageUrls.Add("mobile_logotype", directoryPath + "mobile_logotype.png");
        imageUrls.Add("big_icon", directoryPath + "big_icon.png");
        imageUrls.Add("small_icon", directoryPath + "small_icon.png");

        if (showNoImageIcon == true)
        {
            // Create the no image path
            string noImagePath = "/images/no_image_wide.jpg";

            // Get all the keys in the dictionary
            List<string> keys = imageUrls.dictionary.Keys.ToList<string>();

            // Loop all the keys
            for (int i = 0; i < keys.Count; i++)
            {
                // Get the url
                string url = environment.WebRootPath + imageUrls.Get(keys[i]);

                // Check if the file exists
                if (System.IO.File.Exists(url) == false)
                {
                    imageUrls.Update(keys[i], noImagePath);
                }
            }
        }

        // Return the list
        return imageUrls;

    } // End of the GetDomainImageUrls method

    public WebDomain GetOneById(Int32 id)
    {
        // Create the sql statement
        string sql = "SELECT * FROM dbo.web_domains WHERE id = @id;";

        // Create parameters
        IDictionary<string, object> parameters = new Dictionary<string, object>();
        parameters.Add("@id", id);

        // Get a post
        WebDomain post = this.database_repository.GetModel<WebDomain>(sql, parameters);

        // Return the post
        return post;

    } // End of the GetOneById method

    public WebDomain GetFromCache(string domain_name)
    {
        // Create the post to return
        WebDomain post = null;

        // Get the cached settings
        string data = this.distributed_cache.GetString(domain_name);

        if (data == null)
        {
            // Get the post
            post = GetOneByDomainName(domain_name);

            // Make sure that something was found in the database
            if (post != null)
            {
                // Create cache options
                DistributedCacheEntryOptions cacheEntryOptions = new DistributedCacheEntryOptions();
                cacheEntryOptions.SetSlidingExpiration(TimeSpan.FromMinutes(this.cache_options.ExpirationInMinutes));
                cacheEntryOptions.SetAbsoluteExpiration(TimeSpan.FromMinutes(this.cache_options.ExpirationInMinutes));

                this.distributed_cache.SetString(domain_name, JsonConvert.SerializeObject(post), cacheEntryOptions);
            }
        }
        else
        {
            post = JsonConvert.DeserializeObject<WebDomain>(data);
        }

        // Return the post
        return post;

    } // End of the GetFromCache method

    public WebDomain GetOneByDomainName(string domain_name)
    {
        // Create the sql statement
        string sql = "SELECT * FROM dbo.web_domains WHERE domain_name = @domain_name;";

        // Create parameters
        IDictionary<string, object> parameters = new Dictionary<string, object>();
        parameters.Add("@domain_name", domain_name);

        // Get a post
        WebDomain post = this.database_repository.GetModel<WebDomain>(sql, parameters);

        // Return the post
        return post;

    } // End of the GetOneByDomainName method

    public IList<WebDomain> GetAll(string sort_field, string sort_order)
    {
        // Make sure that sort variables are valid
        sort_field = GetValidSortField(sort_field);
        sort_order = GetValidSortOrder(sort_order);

        // Create the sql statement
        string sql = "SELECT * FROM dbo.web_domains ORDER BY " + sort_field + " " + sort_order + ";";

        // Create parameters
        IDictionary<string, object> parameters = new Dictionary<string, object>();

        // Get the list of posts
        IList<WebDomain> posts = this.database_repository.GetModelList<WebDomain>(sql, parameters, 10);

        // Return the list of posts
        return posts;

    } // End of the GetAll method

    public IList<WebDomain> GetBySearch(string[] keywords, Int32 page_size, Int32 page_number, string sort_field, string sort_order)
    {
        // Make sure that sort variables are valid
        sort_field = GetValidSortField(sort_field);
        sort_order = GetValidSortOrder(sort_order);

        // Create the sql statement
        string sql = "SELECT * FROM dbo.web_domains WHERE 1 = 1";
        for (int i = 0; i < keywords.Length; i++)
        {
            sql += " AND (website_name LIKE @keyword_" + i.ToString() + " OR domain_name LIKE @keyword_" + i.ToString() + ")";
        }
        sql += " ORDER BY " + sort_field + " " + sort_order + " OFFSET @pageNumber ROWS FETCH NEXT @pageSize ROWS ONLY;";

        // Create parameters
        IDictionary<string, object> parameters = new Dictionary<string, object>();
        parameters.Add("@pageNumber", (page_number - 1) * page_size);
        parameters.Add("@pageSize", page_size);
        for (int i = 0; i < keywords.Length; i++)
        {
            parameters.Add("@keyword_" + i.ToString(), "%" + keywords[i].ToString() + "%");
        }

        // Get the list of posts
        IList<WebDomain> posts = this.database_repository.GetModelList<WebDomain>(sql, parameters, page_size);

        // Return the list of posts
        return posts;

    } // End of the GetBySearch method

    #endregion

    #region Delete methods

    public Int32 DeleteOnId(Int32 id)
    {
        // Create the sql statement
        string sql = "DELETE FROM dbo.web_domains WHERE id = @id;";

        // Create parameters
        IDictionary<string, object> parameters = new Dictionary<string, object>();
        parameters.Add("@id", id);

        // Delete the post
        Int32 errorNumber = this.database_repository.Delete(sql, parameters);

        // Return error code
        return errorNumber;

    } // End of the DeleteOnId method

    #endregion

    #region Helper methods

    public void RemoveFromCache()
    {
        // Get all domains
        IList<WebDomain> posts = GetAll("domain_name", "ASC");

        // Loop domains
        for (int i = 0; i < posts.Count; i++)
        {
            // Get the data
            string data = this.distributed_cache.GetString(posts[i].domain_name);

            // Only remove the cache if it exists
            if (data != null)
            {
                // Remove data from cache
                this.distributed_cache.Remove(posts[i].domain_name);
            }
        }

    } // End of the RemoveFromCache method

    #endregion

    #region Validation

    public string GetValidSortField(string sort_field)
    {
        // Make sure that the sort field is valid
        if (sort_field != "id" && sort_field != "website_name" && sort_field != "domain_name")
        {
            sort_field = "id";
        }

        // Return the string
        return sort_field;

    } // End of the GetValidSortField method

    public string GetValidSortOrder(string sort_order)
    {
        // Make sure that the sort order is valid
        if (sort_order != "ASC" && sort_order != "DESC")
        {
            sort_order = "ASC";
        }

        // Return the string
        return sort_order;

    } // End of the GetValidSortOrder method

    #endregion

} // End of the class

Requests

We get the current domain each time there is a request on our website. The current domain is used to fetch other resources, like static texts and images.

[HttpGet]
[Authorize(Roles = "Administrator,Editor,Translator")]
public IActionResult Index()
{
    // Get the current domain
    WebDomain current_domain = this.web_domain_repository.GetCurrentDomain(ControllerContext.HttpContext);

    // Get translated texts
    KeyStringList tt = this.static_text_repository.GetFromCache(current_domain.back_end_language, "id", "ASC");

    // Set form data
    ViewBag.CurrentDomain = current_domain;
    ViewBag.TranslatedTexts = tt;
    ViewBag.QueryParams = new QueryParams(ControllerContext.HttpContext.Request);

    // Return the view
    return View();

} // End of the index method

Leave a Reply

Your email address will not be published. Required fields are marked *