Search code examples
asp.net-identityasp.net-mvc-routingasp.net-mvc-scaffoldingasp.net-core-localizationlazziya.expresslocalization

Asp.Net Core 3.0 MVC - Scaffolded Identity Page Routing for Culture Cookie


I have implemented this nuget package for localization in my Asp.Net Core MVC 3.0 app and have found it great.

https://github.com/LazZiya/ExpressLocalization

For all pages except my scaffolded identity pages it is working fine.

When I navigate to my scaffolded Register.cshtml page I am having a problem where my localization cookie is not being placed into the URL. But if I type the culture (Such as de or en etc.) into the URL manually the localization works fine.

For example, when I click the Register link the URL should be enter image description here

but instead it is always

enter image description here

Startup.cs

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.EntityFrameworkCore;
using MyApp.Data;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using LazZiya.ExpressLocalization;
using Microsoft.AspNetCore.Localization;
using MyApp.LocalizationResources;
using MyApp.Models;

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

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( Configuration.GetConnectionString("DefaultConnection")));
            services.AddDefaultIdentity<ApplicationUser>( options => { options.SignIn.RequireConfirmedAccount = true; }).AddEntityFrameworkStores<ApplicationDbContext>();
            services.AddControllersWithViews();
            services.AddRazorPages();

            var cultures = new []
            {
                new CultureInfo("de"),
                new CultureInfo("en"),
            };

            services.AddControllersWithViews()
                .AddExpressLocalization<LocSource>(ops =>
                    {
                        ops.UseAllCultureProviders = false;
                        ops.ResourcesPath = "LocalizationResources";
                        ops.RequestLocalizationOptions = o =>
                        {
                            o.SupportedCultures = cultures;
                            o.SupportedUICultures = cultures;
                            o.DefaultRequestCulture = new RequestCulture("en");
                        };
                    });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");

                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }
            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();
            app.UseRequestLocalization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{culture=en}/{controller=Home}/{action=Index}/{id?}");
                endpoints.MapControllerRoute(
                    name: "Features",
                    pattern: "{culture=en}/{controller=Features}/{action=Features}/{id?}");
                endpoints.MapControllerRoute(
                    name: "About",
                    pattern: "{culture=en}/{controller=About}/{action=About}/{id?}");
                endpoints.MapControllerRoute(
                    name: "Help",
                    pattern: "{culture=en}/{controller=Help}/{action=Help}/{id?}");
                endpoints.MapRazorPages();
            });
        }
    }
}

Register.cshtml.cs (I think something is wrong here because my MVC Pages do not have a null returnUrl )

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using MyApp.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using LocSourceNameReferenceLibrary;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Http;

namespace MyApp.Areas.Identity.Pages.Account
{
    [AllowAnonymous]
    public class RegisterModel : PageModel
    {
        private readonly SignInManager<ApplicationUser> _signInManager;
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly ILogger<RegisterModel> _logger;
        private readonly IEmailSender _emailSender;
        RegisterPageLocSourceNames _locSourceRegisterPageNameReferenceLibrary = new RegisterPageLocSourceNames();
        SharedCrossPageLocSourceNames _locSourceSharedCrossPageNameReferenceLibrary = new SharedCrossPageLocSourceNames();

        public RegisterModel(
            UserManager<ApplicationUser> userManager,
            SignInManager<ApplicationUser> signInManager,
            ILogger<RegisterModel> logger,
            IEmailSender emailSender)
        {
            _userManager = userManager;
            _signInManager = signInManager;
            _logger = logger;
            _emailSender = emailSender;
        }

        [BindProperty]
        public InputModel Input { get; set; }
        public string ReturnUrl { get; set; }
        public string PageTabTitle { get; set; }
        public string Title { get; set; }
        public string SubTitle { get; set; }
        public string Heading { get; set; }
        public string ServiceHeading { get; set; }
        public string EmailHeading { get; set; }
        public string PasswordHeading { get; set; }
        public string ConfirmPassword { get; set; }
        public string RegisterButtonName { get; set; }
        public string NavBarHome { get; set; }
        public string NavBarFeatures { get; set; }
        public string NavBarAbout { get; set; }
        public string NavBarHelpCenter { get; set; }

        public IList<AuthenticationScheme> ExternalLogins { get; set; }

        public class InputModel
        {
            [Required]
            [EmailAddress]
            [Display(Name = "Email")]
            public string Email { get; set; }

            [Required]
            [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
            [DataType(DataType.Password)]
            [Display(Name = "Password")]
            public string Password { get; set; }

            [DataType(DataType.Password)]
            [Display(Name = "Confirm Password")]
            [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
            public string ConfirmPassword { get; set; }
        }

        public async Task OnGetAsync(string returnUrl = null)
        {
            PageTabTitle = _locSourceRegisterPageNameReferenceLibrary.GetLocSourcePageTabTitleNameReferenceForRegisterPage();
            Title = _locSourceRegisterPageNameReferenceLibrary.GetLocSourceTitleNameReferenceForRegisterPage();
            SubTitle = _locSourceRegisterPageNameReferenceLibrary.GetLocSourceSubtitleNameReferenceForRegisterPage();
            Heading = _locSourceRegisterPageNameReferenceLibrary.GetLocSourceHeadingNameReferenceForRegisterPage();
            ServiceHeading = _locSourceRegisterPageNameReferenceLibrary.GetLocSourceServiceHeadingNameReferenceForRegisterPage();
            EmailHeading = _locSourceRegisterPageNameReferenceLibrary.GetLocSourceEmailHeadingNameReferenceForRegisterPage();
            PasswordHeading = _locSourceRegisterPageNameReferenceLibrary.GetLocSourcePasswordHeadingNameReferenceForRegisterPage();
            ConfirmPassword = _locSourceRegisterPageNameReferenceLibrary.GetLocSourceConfirmPasswordHeadingNameReferenceForRegisterPage();
            RegisterButtonName = _locSourceRegisterPageNameReferenceLibrary.GetLocSourceRegisterButtonNameReferenceForRegisterPage();
            NavBarHome = _locSourceRegisterPageNameReferenceLibrary.GetLocSourceNavBarHomeReferenceForRegisterPage();
            NavBarFeatures = _locSourceRegisterPageNameReferenceLibrary.GetLocSourceNavBarFeaturesReferenceForRegisterPage();
            NavBarAbout = _locSourceRegisterPageNameReferenceLibrary.GetLocSourceNavBarAboutReferenceForRegisterPage();
            NavBarHelpCenter = _locSourceRegisterPageNameReferenceLibrary.GetLocSourceNavBarHelpCenterReferenceForRegisterPage();

            ReturnUrl = returnUrl;
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
        }

        public async Task<IActionResult> OnPostAsync(string returnUrl = null)
        {
            returnUrl ??= Url.Content("~/");
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
            if (ModelState.IsValid)
            {
                var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email };
                var result = await _userManager.CreateAsync(user, Input.Password);
                if (result.Succeeded)
                {
                    _logger.LogInformation("User created a new account with password.");

                    var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                    code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                    var callbackUrl = Url.Page(
                        "/Account/ConfirmEmail",
                        pageHandler: null,
                        values: new { area = "Identity", userId = user.Id, code },
                        protocol: Request.Scheme);

                    await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                        $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

                    if (_userManager.Options.SignIn.RequireConfirmedAccount)
                    {
                        return RedirectToPage("RegisterConfirmation", new { email = Input.Email });
                    }
                    else
                    {
                        await _signInManager.SignInAsync(user, isPersistent: false);
                        return LocalRedirect(returnUrl);
                    }
                }
                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(string.Empty, error.Description);
                }
            }

            // If we got this far, something failed, redisplay form
            return Page();
        }
    }
}

Areas > Identity > Pages > Account > _ViewImports.cshtml

@using MyApp
@using MyApp.Areas.Identity.Pages.Account
@using LazZiya.ExpressLocalization
@inject ISharedCultureLocalizer _loc
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, LazZiya.TagHelpers
@addTagHelper *, LazZiya.ExpressLocalization

I have also added a RouteTemplateModelConvention.cs class as per these instructions, but I don't think it is doing anything. It says in the comments it should be automatically invoked. I have placed it in the root ~/RouteTemplateModelConvention.cs

This is my folder structure:

Folder Structure

I'm guessing that because the scaffolding of the identity pages automatically used Razor pages for Register and Login, my MVC Localization implementation is not working for these.

But...it is working if I manually add the culture ("en" or "de" ) into the browser URL, its just not picking it up automatically.

I have tried any number of variations to get this working, but am sure it is something simple I'm missing in the routing for the cookie...Any ideas please?

Update:

I have tried adding the culture parameter to the <a></a> link using asp-route-culture=“@CultureInfo.CurrentCulture.Name” in my shared _Layout.cshtml file, but am getting an error saying that @CultureInfo does not exist in the current context.

Update:

Ok progress... I added the @using System.Globalization namespace to my layout file. Now the URL is https://localhost:44305/"de"/Identity/Account/Register I'm trying to figure out why the inverted commas are in the URL. Any ideas?


Solution

  • Using asp-route-culture="@CultureInfo.CurrentCulture.Name"should fix the issue.

    In your code it seems that you are using inverted commas“...” they should be quotation marks "..." :)