<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:a10="http://www.w3.org/2005/Atom" version="2.0">
  <channel xmlns:media="http://search.yahoo.com/mrss/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <title>ABP.IO Stories</title>
    <link>https://abp.io/community/articles</link>
    <description>A hub for ABP Framework, .NET, and software development. Access articles, tutorials, news, and contribute to the ABP community.</description>
    <lastBuildDate>Sun, 04 Oct 2026 06:57:18 Z</lastBuildDate>
    <generator>Community - ABP.IO</generator>
    <image>
      <url>https://abp.io/assets/favicon.ico/favicon-32x32.png</url>
      <title>ABP.IO Stories</title>
      <link>https://abp.io/community/articles</link>
    </image>
    <a10:link rel="self" type="application/rss+xml" title="self" href="https://abp.io/community/rss?member=GDUnit" />
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/abp-blazor-multitenant-subdomain-resolution-c1x4un8x</guid>
      <link>https://abp.io/community/posts/abp-blazor-multitenant-subdomain-resolution-c1x4un8x</link>
      <a10:author>
        <a10:name>GDUnit</a10:name>
        <a10:uri>https://abp.io/community/members/GDUnit</a10:uri>
      </a10:author>
      <category>blazor</category>
      <category>multi-tenancy</category>
      <category>saas</category>
      <title>Abp Blazor Multi-Tenant Subdomain Resolution</title>
      <description>Describes a way to implement per-tenant subdomain resolution for ABP using the Blazor UI.</description>
      <pubDate>Fri, 07 Oct 2022 16:10:14 Z</pubDate>
      <a10:updated>2026-10-04T04:44:40Z</a10:updated>
      <content:encoded><![CDATA[<h1>Abp Blazor Multi-Tenant Subdomain Resolution</h1>
<p>A common requirement for software-as-a-service applications is to be able to provide a specific URL to each tenant that will take them directly to their portion of the application, often together with customised branding or data for their company.</p>
<p>Here we will describe a method of implementing per-tenant subdomains for Blazor UI using the ABP framework.</p>
<p>For this case we will consider that we have a non-tiered solution (no separate auth server).
If you have a tiered solution, you will need to repeat steps 2 and 3 as appropriate for that module.</p>
<p>We will also consider that we are going to use mybookstore.com as a base domain.</p>
<p>See the completed solution for this example at <a href="https://github.com/gdunit/AbpBlazorMultiTenantSubdomain" title="Example Repository">this github repository</a>.</p>
<h3>Step 1</h3>
<p>DbMigrator - appsettings.json</p>
<ul>
<li>Amend the Clients inside the IdentityServer config settings.</li>
<li>For each client, amend the RootUrl, noting the {0} where the tenant domain will be inserted:</li>
</ul>
<pre><code>  &quot;RootUrl&quot;: &quot;https://{0}.mybookstore.com:{port number goes here}&quot;
</code></pre>
<p>Important: Run the DBMigrator to ensure that the clients are updated within the auth server.</p>
<h3>Step 2</h3>
<p>Now we will amend the AppSettings.json file for the HttpApi.Host project.
Note the addition of the SelfUrlWithoutTenant property:</p>
<pre><code>  &quot;App&quot;: {
    &quot;SelfUrl&quot;: &quot;https://{0}.api.mybookstore.com:44399&quot;,
    &quot;SelfUrlWithoutTenant&quot;: &quot;https://api.mybookstore.com:44399&quot;,
    &quot;CorsOrigins&quot;: &quot;https://*.mybookstore.com,http://*.mybookstore.com:4200,https://*.mybookstore.com:44307,http://mybookstore.com:4200,https://mybookstore.com:44307&quot;
  },
</code></pre>
<h3>Step 3</h3>
<p>Now, we will amend the HttpApiHost module class' ConfigureServices() method:</p>
<pre><code>  context.Services.AddAbpStrictRedirectUriValidator();
  context.Services.AddAbpClientConfigurationValidator();
  context.Services.AddAbpWildcardSubdomainCorsPolicyService();
  Configure&lt;AbpTenantResolveOptions&gt;(options =&gt;
  {
      options.AddDomainTenantResolver(configuration[&quot;App:SelfUrl&quot;]);
  });

  Configure&lt;IdentityServerOptions&gt;(options =&gt;
  {
      options.IssuerUri = configuration[&quot;App:SelfUrlWithoutTenant&quot;];
  });
</code></pre>
<p>And add the following two lines to the AddAuthentication() configuration as follows:</p>
<pre><code>  context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
  .AddJwtBearer(options =&gt;
  {
      // Add new config options as below
      // Other options already there should be left as-is 
      // See https://github.com/abpframework/abp/issues/3304
      options.TokenValidationParameters.ValidateIssuer = false;
      options.TokenValidationParameters.ValidateAudience = false;
  });
</code></pre>
<h3>Step 4</h3>
<p>Now, we are done with the back end! We will change the Blazor project's appsettings.json:</p>
<pre><code>  &quot;App&quot;: {
    &quot;SelfUrl&quot;: &quot;https://{0}.mybookstore.com:44307&quot;
  },
  &quot;AuthServer&quot;: {
    &quot;Authority&quot;: &quot;https://{0}.api.mybookstore.com:44399&quot;,
    &quot;ClientId&quot;: &quot;BookStore_Blazor&quot;,
    &quot;ResponseType&quot;: &quot;code&quot;
  },
  &quot;RemoteServices&quot;: {
    &quot;Default&quot;: {
      &quot;BaseUrl&quot;: &quot;https://{0}.api.mybookstore.com:44399&quot;
    }
  },
</code></pre>
<h3>Step 5</h3>
<p>Amend the Blazor project's module class, ConfigureServices() again:
Here, we cannot use a tenant resolver directly as it is not supported.
But because the app will initialise on the client, we can set the URL at startup.</p>
<p>Add the following fields &amp; methods:</p>
<pre><code>  private static readonly string[] ProtocolPrefixes = { &quot;http://&quot;, &quot;https://&quot; };

  private void ConfigureRemoteServices(WebAssemblyHostBuilder builder)
  {
      Configure&lt;AbpRemoteServiceOptions&gt;(options =&gt;
      {
          options.RemoteServices.Default =
              new RemoteServiceConfiguration(GetApiServerAuthorityWithTenantSubDomain(builder));
      });
  }

  private static string GetAuthServerAuthorityWithTenantSubDomain(WebAssemblyHostBuilder builder)
  {
      return ConvertToTenantSubDomain(builder, &quot;AuthServer:Authority&quot;);
  }

  private static string GetApiServerAuthorityWithTenantSubDomain(WebAssemblyHostBuilder builder)
  {
      return ConvertToTenantSubDomain(builder, &quot;RemoteServices:Default:BaseUrl&quot;);
  }

  private static string ConvertToTenantSubDomain(WebAssemblyHostBuilder builder, string configPath)
  {
      var baseUrl = builder.HostEnvironment.BaseAddress;
      var configUrl = builder.Configuration[configPath];
      return configUrl.Replace(&quot;{0}.&quot;, GetTenantName(baseUrl));
  }

  // This is a naive implementation which assumes [tenantdomain].[domain].[suffix]
  private static string GetTenantName(string baseUrl)
  {
      var hostName = baseUrl.RemovePreFix(ProtocolPrefixes);
      var urlSplit = hostName.Split('.');
      // If the url has two (or even) splits ([domain].[suffix]) we assume the host.
      // If three (or odd), then we assume a tenant subomain is added and add the period also.
      return urlSplit.Length % 2 == 0 ? null : $&quot;{urlSplit.FirstOrDefault()}.&quot;;
  }        
</code></pre>
<p>Amend the ConfigureAuthentication() options to add the new line at the bottom shown here:</p>
<pre><code>  private static void ConfigureAuthentication(WebAssemblyHostBuilder builder)
  {
      builder.Services.AddOidcAuthentication(options =&gt;
      {
          builder.Configuration.Bind(&quot;AuthServer&quot;, options.ProviderOptions);
          options.UserOptions.RoleClaim = JwtClaimTypes.Role;
          options.ProviderOptions.DefaultScopes.Add(&quot;BookStore&quot;);
          options.ProviderOptions.DefaultScopes.Add(&quot;role&quot;);
          options.ProviderOptions.DefaultScopes.Add(&quot;email&quot;);
          options.ProviderOptions.DefaultScopes.Add(&quot;phone&quot;);
          // Add this line here
          // Override the domain from the config with the actual tenant specific domain
          options.ProviderOptions.Authority = GetAuthServerAuthorityWithTenantSubDomain(builder);
      });
  }
</code></pre>
<p>Finally, add the call to ConfigureRemoteServices() underneath ConfigureAuthentication():</p>
<pre><code>  ConfigureRemoteServices(builder);
</code></pre>
<h3>Step 6</h3>
<p>Now, in order to make this run locally we need to:</p>
<p>Amend the hosts file on your computer to add the custom domains, here we imagine there is the host and one tenant, tenant1 (change as needed for your case):</p>
<pre><code>  127.0.0.1 mybookstore.com
  127.0.0.1 tenant1.mybookstore.com
  127.0.0.1 api.mybookstore.com
  127.0.0.1 tenant1.api.mybookstore.com
</code></pre>
<p>Finally, amend the applicationhost.config file.
In the <sites> node, amend the <binding> for each to make it a wildcard. This will let you access subdomains from the parent locally via IIS Express.
Note that there is no domain stated here, just the port:</p>
<pre><code>  &lt;binding protocol=&quot;https&quot; bindingInformation=&quot;*:44307:&quot; /&gt;
</code></pre>
<pre><code>  &lt;binding protocol=&quot;https&quot; bindingInformation=&quot;*:44399:&quot; /&gt;
</code></pre>
<p>Important Note: If you are running JetBrains Rider as an IDE, you may need to uncheck the &quot;Generate ApplicationHost.config&quot; checkbox in the &quot;Edit Configuration&quot; options for each runnable project. Otherwise, rider will overwrite the file on each run.</p>
<h3>Step 7</h3>
<p>Finally you can run the application:</p>
<ul>
<li>Run the host as admin</li>
<li>Add a tenant (tenant1)</li>
<li>Logout</li>
<li>Go to tenant1.mybookstore.com</li>
<li>You should be redirected to tenant1.api.mybookstore.com. Login as the tenant admin user</li>
<li>You should now be logged into the tenant and redirected to the Blazor homepage!</li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/58a0848a-9d64-0554-7575-3a06c5630118" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/58a0848a-9d64-0554-7575-3a06c5630118" medium="image" />
    </item>
  </channel>
</rss>