Ibrahim Venkat
Ibrahim Venkat

Reputation: 27

How to make Route based on a sub-domain MVC Core

How to make this - user1.domain.com goes to user1/index (not inside area) - user2.domain.com goes to user2/index (not inside area)

I mean's the

user1.domain.com/index

user2.domain.com/index

Are same view but different data depending on user{0}

using MVC Core 2.2

Upvotes: 0

Views: 430

Answers (2)

Ibrahim Venkat
Ibrahim Venkat

Reputation: 27

The problem after login the Identity cookie not shared in sub-domain

enter image description here

Here my Code where's wrong !!!

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }
    public static Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder dataProtectionBuilder;
    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<CookiePolicyOptions>(options =>
        {
            // This lambda determines whether user consent for non-essential cookies is needed for a given request.
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = SameSiteMode.None;
        });

        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(
                Configuration.GetConnectionString("ConnectionDb")));

        services.AddIdentity<ExtendIdentityUser, IdentityRole>(options =>
        {
            options.Password.RequiredLength = 8;
            options.Password.RequireUppercase = false;
            options.Password.RequireNonAlphanumeric = false;
            options.Password.RequiredUniqueChars = 0;
            options.Password.RequireLowercase = false;

        }).AddEntityFrameworkStores<ApplicationDbContext>(); // .AddDefaultTokenProviders();

        services.ConfigureApplicationCookie(options => options.CookieManager = new CookieManager());

        services.AddHttpContextAccessor();

        services.AddScoped<IUnitOfWork, UnitOfWork>();
        services.AddScoped<IExtendIdentityUser, ExtendIdentityUserRepository>();
        services.AddScoped<IItems, ItemsRepository>();

        services.AddMvc(otps =>
        {
            otps.ModelBinderProviders.Insert(0, new FromHostBinderProvider());
        });
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }
        app.UseStaticFiles();

        app.UseAuthentication();
        //app.UseHttpsRedirection();
        app.UseCookiePolicy();

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

And this class to sub-domain like that https://user1.localhost:44390/Home/Index

internal class FromHostAttribute : Attribute, IBindingSourceMetadata
{
    public static readonly BindingSource Instance = new BindingSource("FromHostBindingSource", "From Host Binding Source", true, true);
    public BindingSource BindingSource { get { return FromHostAttribute.Instance; } }
}
public class MyFromHostModelBinder : IModelBinder
{
    private readonly string _domainWithPort;

    public MyFromHostModelBinder()
    {
        this._domainWithPort = "localhost:44390";  // in real project, use by Configuration/Options
    }

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var req = bindingContext.HttpContext.Request;
        var host = req.Host.Value;
        var name = bindingContext.FieldName;
        var userStr = req.Host.Value.Substring(0, host.Length - this._domainWithPort.Length);
        if (string.IsNullOrEmpty(userStr))
        {
            bindingContext.ModelState.AddModelError(name, $"cannot get {name} from Host Domain");
        }
        else
        {
            var result = Convert.ChangeType(userStr, bindingContext.ModelType);
            bindingContext.Result = ModelBindingResult.Success(result);
        }
        return Task.CompletedTask;
    }

}
public class FromHostBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null) { throw new ArgumentNullException(nameof(context)); }
        var has = context.BindingInfo?.BindingSource == FromHostAttribute.Instance;
        if (has)
        {
            return new BinderTypeModelBinder(typeof(MyFromHostModelBinder));
        }
        return null;
    }
}
Using ICookieManager 
public class CookieManager : ICookieManager
{
    #region Private Members

    private readonly ICookieManager ConcreteManager;

    #endregion

    #region Prvate Methods

    private string RemoveSubdomain(string host)
    {
        var splitHostname = host.Split('.');
        //if not localhost
        if (splitHostname.Length > 1)
        {
            return string.Join(".", splitHostname.Skip(1));
        }
        else
        {
            return host;
        }
    }

    #endregion

    #region Public Methods

    public CookieManager()
    {
        ConcreteManager = new ChunkingCookieManager();
    }

    public void AppendResponseCookie(HttpContext context, string key, string value, CookieOptions options)
    {

        options.Domain = RemoveSubdomain(context.Request.Host.Host);  //Set the Cookie Domain using the request from host
        ConcreteManager.AppendResponseCookie(context, key, value, options);
    }

    public void DeleteCookie(HttpContext context, string key, CookieOptions options)
    {
        ConcreteManager.DeleteCookie(context, key, options);
    }

    public string GetRequestCookie(HttpContext context, string key)
    {
        return ConcreteManager.GetRequestCookie(context, key);
    }

    #endregion
}

Upvotes: 0

itminus
itminus

Reputation: 25350

There're several approaches depending on your needs.

How to make this - user1.domain.com goes to user1/index (not inside area) - user2.domain.com goes to user2/index (not inside area)

Rewrite/Redirect

One approach is to rewrite/redirect the url. If you don't like do it with nginx/iis, you could create an Application Level Rewrite Rule. For example, I create a sample route rule for your reference:

internal enum RouteSubDomainBehavior{ Redirect, Rewrite, }
internal class RouteSubDomainRule : IRule
{
    private readonly string _domainWithPort;
    private readonly RouteSubDomainBehavior _behavior;

    public RouteSubDomainRule(string domain, RouteSubDomainBehavior behavior)
    {
        this._domainWithPort = domain;
        this._behavior = behavior;
    }

    // custom this method according to your needs
    protected bool ShouldRewrite(RewriteContext context)
    {
        var req = context.HttpContext.Request;
        // only rewrite the url when it ends with target doamin
        if (!req.Host.Value.EndsWith(this._domainWithPort, StringComparison.OrdinalIgnoreCase)) { return false; }
        // if already rewrite, skip
        if(req.Host.Value.Length == this._domainWithPort.Length) { return false; }
        // ... add other condition to make sure only rewrite for the routes you wish, for example, skip the Hub
        return true;
    }

    public void ApplyRule(RewriteContext context)
    {
        if(!this.ShouldRewrite(context)) { 
            context.Result = RuleResult.ContinueRules; 
            return;
        }
        var req = context.HttpContext.Request;
        if(this._behavior == RouteSubDomainBehavior.Redirect){
            var newUrl = UriHelper.BuildAbsolute( req.Scheme, new HostString(this._domainWithPort), req.PathBase, req.Path, req.QueryString);
            var resp = context.HttpContext.Response;
            context.Logger.LogInformation($"redirect {req.Scheme}://{req.Host}{req.Path}?{req.QueryString} to {newUrl}");
            resp.StatusCode = 301;
            resp.Headers[HeaderNames.Location] = newUrl;
            context.Result = RuleResult.EndResponse;
        }
        else if (this._behavior == RouteSubDomainBehavior.Rewrite)
        {
            var host = req.Host.Value;
            var userStr = req.Host.Value.Substring(0, host.Length - this._domainWithPort.Length - 1);
            req.Host= new HostString(this._domainWithPort);
            var oldPath = req.Path;
            req.Path = $"/{userStr}{oldPath}";
            context.Logger.LogInformation($"rewrite {oldPath} as {req.Path}");
            context.Result = RuleResult.SkipRemainingRules;
        }
        else{
            throw new Exception($"unknow SubDomainBehavoir={this._behavior}");
        }
    }
}

(Note I use Rewrite here. If you like, feel free to change it to RouteSubDomainBehavior.Redirect.)

And then invoke the rewriter middleware just after app.UseStaticFiles():

app.UseStaticFiles();
// note : the invocation order matters!
app.UseRewriter(new RewriteOptions().Add(new RouteSubDomainRule("domain.com:5001",RouteSubDomainBehavior.Rewrite)));

app.UseMvc(...)

By this way,

  • user1.domain.com:5001/ will be rewritten as (or redirected to) domain.com:5001/user1
  • user1.domain.com:5001/Index will be rewritten as(or redirected to) domain.com:5001/user1/Index
  • user1.domain.com:5001/Home/Index will be rewritten as (or redirected to) domain.com:5001/user1//HomeIndex
  • static files like user1.domain.com:5001/lib/jquery/dist/jquery.min.js won't be rewritten/redirected because they're served by UseStaticFiles.

Another Approach Using IModelBinder

Although you can route it by rewritting/redirecting as above, I suspect what your real needs are binding parameters from Request.Host. If that's the case, I would suggest you should use IModelBinder instead. For example, create a new [FromHost] BindingSource:

internal class FromHostAttribute : Attribute, IBindingSourceMetadata
{
    public static readonly BindingSource Instance = new BindingSource( "FromHostBindingSource", "From Host Binding Source", true, true);
    public BindingSource BindingSource {get{ return FromHostAttribute.Instance; }} 
}
public class MyFromHostModelBinder : IModelBinder
{
    private readonly string _domainWithPort;

    public MyFromHostModelBinder()
    {
        this._domainWithPort = "domain.com:5001";  // in real project, use by Configuration/Options
    }

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var req = bindingContext.HttpContext.Request;
        var host = req.Host.Value;
        var name = bindingContext.FieldName;
        var userStr = req.Host.Value.Substring(0, host.Length - this._domainWithPort.Length - 1);
        if (userStr == null) {
            bindingContext.ModelState.AddModelError(name, $"cannot get {name} from Host Domain");
        } else {
            var result = Convert.ChangeType(userStr, bindingContext.ModelType);
            bindingContext.Result = ModelBindingResult.Success(result);
        }
        return Task.CompletedTask;
    }

}
public class FromHostBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null) { throw new ArgumentNullException(nameof(context)); }
        var has = context.BindingInfo?.BindingSource == FromHostAttribute.Instance;
        if(has){
            return new BinderTypeModelBinder(typeof(MyFromHostModelBinder));
        }
        return null;
    }
}

Finally, insert this FromHostBinderProvider in your MVC binder providers.

services.AddMvc(otps =>{
    otps.ModelBinderProviders.Insert(0, new FromHostBinderProvider());
});

Now you can get the user1.domain.com automatically by:

public IActionResult Index([FromHost] string username)
{
    ...
    return View(view_model_by_username);
}

public IActionResult Edit([FromHost] string username, string id)
{
    ...
    return View(view_model_by_username);
}

Upvotes: 3

Related Questions