Dresel / RouteLocalization

RouteLocalization is an MVC and Web API package that allows localization of your attribute routes.
MIT License
67 stars 13 forks source link

ArgumentException: An element with the key 'culture' already exists in the RouteValueDictionary. #67

Closed mdmoura closed 6 years ago

mdmoura commented 6 years ago

I am using RouteLocalization with ASP.NET Core and to change culture I have the following on a view:

    @{
        RouteValueDictionary routeValueDictionary = new RouteValueDictionary(ViewContext.RouteData.Values);
        routeValueDictionary["culture"] = null;

        <div>Original version of this route: <a href="@Url.RouteUrl(routeValueDictionary)">@(Url.RouteUrl(routeValueDictionary) ?? "[null]")</a>
        </div>
    }
    @{
        RouteValueDictionary routeValueDictionaryEN = new RouteValueDictionary(ViewContext.RouteData.Values);
        routeValueDictionaryEN["culture"] = "pt";

        <div>pt version of this route: <a href="@Url.RouteUrl(routeValueDictionaryEN)">@(Url.RouteUrl(routeValueDictionaryEN) ?? "[null]")</a>
        </div>
  }  

This is basically what is in a View on the samples.

When I click on the portuguese link I get the error:

ArgumentException: An element with the key 'culture' already exists in the RouteValueDictionary.
Parameter name: key
Microsoft.AspNetCore.Mvc.Internal.MiddlewareFilterBuilder+<>c+<<BuildPipeline>b__8_0>d.MoveNext()

I am using: new RouteValueDictionary(ViewContext.RouteData.Values)

What am I missing? Is there a better way to generate links to change from one culture to another?

Dresel commented 6 years ago

I can't reproduce this.

I have used your setup from your previous issue:

// Configure localization
services.AddSingleton(provider =>
{
    RequestLocalizationOptions requestLocalizationOptions = new RequestLocalizationOptions()
    {
        DefaultRequestCulture = new RequestCulture("en"),

        // Formatting numbers, dates, etc.
        SupportedCultures = new[] { new CultureInfo("en"), new CultureInfo("pt") },

        // UI strings that we have localized.
        SupportedUICultures = new[] { new CultureInfo("en"), new CultureInfo("pt") },
    };

    // Replaces CultureSensitiveActionFilterAttribute
    requestLocalizationOptions.RequestCultureProviders.Insert(0, new RouteDataRequestCultureProvider());

    return requestLocalizationOptions;
});

// Add framework services.
services
    .AddMvc(x => { x.Filters.Add(new MiddlewareFilterAttribute(typeof(LocalizationPipeline))); })
    .AddRouteLocalization(x =>
    {
        x.UseCulture("en")
            .WhereUntranslated()
            .AddDefaultTranslation();

        x.UseCulture("pt")
            .WhereController(nameof(HomeController))
            .WhereAction(nameof(HomeController.Index))
            .TranslateAction("");

        x.UseCulture("pt")
            .WhereController(nameof(AboutController))
            .WhereAction(nameof(AboutController.Index))
            .TranslateAction("quem-somos");

    })
    .AddTypedRouting()
    .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
    .AddDataAnnotationsLocalization();

services.AddSingleton(x => {
    RequestLocalizationOptions requestLocalizationOptions = new RequestLocalizationOptions()
    {
        DefaultRequestCulture = new RequestCulture("en"),
        SupportedCultures = new[] { new CultureInfo("en"), new CultureInfo("pt") },
        SupportedUICultures = new[] { new CultureInfo("en"), new CultureInfo("pt") },
    };
    requestLocalizationOptions.RequestCultureProviders.Insert(0, new RouteDataRequestCultureProvider());
    return requestLocalizationOptions;
});

services.Configure<MvcOptions>(options => options.Conventions.Add(new CollectRoutesApplicationConvention()));

and

@using Microsoft.AspNetCore.Localization
@using Microsoft.AspNetCore.Routing
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width"/>
    <title>RouteLocalization.MVC.Sample</title>
</head>
<body>
<div>
    @RenderBody()
</div>
<div>
    <div>Current Culture: @(Context.Features.Get<IRequestCultureFeature>().RequestCulture.UICulture.Name)</div>
    <div>Current localized Route: @Url.RouteUrl(ViewContext.RouteData.Values)</div>
    @{
        RouteValueDictionary routeValueDictionary = new RouteValueDictionary(ViewContext.RouteData.Values);
        routeValueDictionary["culture"] = null;

        <div>Original version of this route: <a href="@Url.RouteUrl(routeValueDictionary)">@(Url.RouteUrl(routeValueDictionary) ?? "[null]")</a>
        </div>
    }
    @{
        RouteValueDictionary routeValueDictionaryEN = new RouteValueDictionary(ViewContext.RouteData.Values);
        routeValueDictionaryEN["culture"] = "en";

        <div>English (en) version of this route: <a href="@Url.RouteUrl(routeValueDictionaryEN)">@(Url.RouteUrl(routeValueDictionaryEN) ?? "[null]")</a>
        </div>
    }
    @{
        RouteValueDictionary routeValueDictionaryPT = new RouteValueDictionary(ViewContext.RouteData.Values);
        routeValueDictionaryPT["culture"] = "pt";

        <div>German (de) version of this route: <a href="@Url.RouteUrl(routeValueDictionaryPT)">@(Url.RouteUrl(routeValueDictionaryPT) ?? "[null]")</a>
        </div>
    }
    <p>
        <strong>All discovered Routes:</strong>
    </p>
    <p>
        @foreach (string route in CollectRoutesApplicationConvention.Routes)
        {
            <div>@route</div>
        }
    </p>
</div>
</body>
</html>

Calling translated Urls works fine. Which Url causes this error? How does the rest of your configuration look like?

mdmoura commented 6 years ago

@Dresel I tried to change culture in all pages and got the same error in all of them. Here is my full configuration:

  public class Startup {

    public IConfiguration Configuration { get; }

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

    public void Configure(IApplicationBuilder applicationBuilder, IHostingEnvironment hostingEnvironment) {

      StaticFileOptions staticFileOptions = new StaticFileOptions();
      RewriteOptions rewriteOptions = new RewriteOptions();        

      if (hostingEnvironment.IsDevelopment()) {
        applicationBuilder.UseDeveloperExceptionPage();
        staticFileOptions.ContentTypeProvider = new FileExtensionContentTypeProvider()
          .With(x => {
            x.Mappings.With(y => {
              y.Add(".less", "text/css");
            });
            return x;
          });        
      } else { 
        applicationBuilder.UseStatusCodePagesWithReExecute("/errors/{0}");
      }

      applicationBuilder.UseRequestLocalization();

      applicationBuilder
        .UseRewriter(rewriteOptions)
        .UseStaticFiles(staticFileOptions);      

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

    } // Configure  

    public void ConfigureServices(IServiceCollection services) {          

      services
        .AddMvc(x => { x.Filters.Add(new MiddlewareFilterAttribute(typeof(LocalizationPipeline))); })
                .AddTypedRouting()
                .AddRouteLocalization(x => {
         x.UseCulture("pt").WhereController(nameof(HomeController)).WhereAction(nameof(HomeController.Index)).TranslateAction("");
                    x.UseCulture("pt").WhereController(nameof(AboutController)).WhereAction(nameof(AboutController.Index)).TranslateAction("quem-somos");
          x.UseCulture("pt").WhereController(nameof(ContactController)).WhereAction(nameof(ContactController.Index)).TranslateAction("contactos");
          x.UseCulture("pt").WhereController(nameof(NewsController)).WhereAction(nameof(NewsController.Index)).TranslateAction("noticias");
          x.UseCulture("pt").WhereController(nameof(WhyPortugalController)).WhereAction(nameof(WhyPortugalController.Index)).TranslateAction("porque-portugal");
          x.UseCulture("pt").WhereController(nameof(ServiceController))
            .WhereAction(nameof(ServiceController.FamilyLifeAndLeisure)).TranslateAction("servicos/lazer-e-familia")
            .WhereAction(nameof(ServiceController.FinancialWealthPlanningAndManagement)).TranslateAction("servicos/planeamento-financeiro")
            .WhereAction(nameof(ServiceController.RealEstateConsultingAndAdvisory)).TranslateAction("servicos/assessoria-imobiliaria")
            .WhereAction(nameof(ServiceController.ResidenceAndCitizenshipPlanning)).TranslateAction("servicos/residencia-e-nacionalidade")
            .WhereAction(nameof(ServiceController.TaxConsulting)).TranslateAction("servicos/assessoria-fiscal");         

        })          
        .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
        .AddDataAnnotationsLocalization();    

      services.AddPortableObjectLocalization(x => x.ResourcesPath = "wwwroot/assets");                 

      services.AddRouting(x => { x.AppendTrailingSlash = false; x.LowercaseUrls = true; });

      services.AddAntiforgery(x => { x.Cookie.Name = "_antiforg"; x.FormFieldName = "_antiforg"; });

      services.AddOptions();

      services.Configure<Settings>(Configuration);         

     services.AddSingleton(x => {

    RequestLocalizationOptions requestLocalizationOptions = new RequestLocalizationOptions() {        
       DefaultRequestCulture = new RequestCulture("en"),
           SupportedCultures = new[] { new CultureInfo("en"), new CultureInfo("pt") },
           SupportedUICultures = new[] { new CultureInfo("en"), new CultureInfo("pt") },
       };

    requestLocalizationOptions.RequestCultureProviders.Insert(0, new RouteDataRequestCultureProvider());

    return requestLocalizationOptions;

      });

      services.AddSingleton<IConfiguration>(Configuration);      

      services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
      services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();      

      services.AddTransient<IUrlHelperFactory, UrlHelperFactory>();
      services.AddTransient<IUrlHelper, UrlHelper>(x => (UrlHelper)x.GetService<IUrlHelperFactory>().GetUrlHelper(x.GetService<IActionContextAccessor>().ActionContext));      

      services.AddTransient<IMailService, MailService>();    

      services.AddScoped<IValidationService, ValidationService>();

      services.AddScoped<SingleInstanceFactory>(x => y => x.GetService(y));
      services.AddScoped<MultiInstanceFactory>(x => y => x.GetServices(y));

      services
        .Scan(x =>
          x.FromAssembliesOf(typeof(IMediator), typeof(Startup))
            .AddClasses(y => y.AssignableTo(typeof(IMediator))).AsImplementedInterfaces().WithScopedLifetime()            
            .AddClasses(y => y.AssignableTo(typeof(IRequestHandler<,>))).AsImplementedInterfaces().WithScopedLifetime()            
            .AddClasses(y => y.AssignableTo(typeof(IAsyncRequestHandler<,>))).AsImplementedInterfaces().WithScopedLifetime()
            .AddClasses(y => y.AssignableTo(typeof(INotificationHandler<>))).AsImplementedInterfaces().WithScopedLifetime()
            .AddClasses(y => y.AssignableTo(typeof(IAsyncNotificationHandler<>))).AsImplementedInterfaces().WithScopedLifetime());      

      services.Scan(x => x.FromAssembliesOf(typeof(Startup)).AddClasses(y => y.AssignableTo(typeof(IValidator))).AsImplementedInterfaces().WithScopedLifetime());

    } // ConfigureServices  

  } // Startup

Does this help?

Dresel commented 6 years ago

Can you access translated routes directly by entering in browser bar (like /pt/quem-somos)?

Do you use the RouteValueDictionary anywhere else? It sounds like you add the culture key twice...

Either upload a repo or tell me what to add to the sample project to reproduce your error or uncomment parts of your config until it works and you can isolate the problem.

mdmoura commented 6 years ago

I found the problem but not sure what I am doing wrong. I was trying to create links to change the culture but using only one line of code so I used:

<a href="@(Url.RouteUrl(new RouteValueDictionary(ViewContext.RouteData.Values).With(x => { x.Add("culture", null); return x; })))">EN</a>
<a href="@(Url.RouteUrl(new RouteValueDictionary(ViewContext.RouteData.Values).With(x => { x.Add("culture", "pt"); return x; })))">PT</a>

With is a simple extension which code is the following:

public static void With(this T value, Action action) { action(value); }

public static R With<T, R>(this T value, Func<T, R> function) {
  return function(value);
}

I was able to solve it by using

x["culture"] = null

And

x["culture"] = "pt"

Do you have a shorter way than what you use in your sample to generate culture change links?

Dresel commented 6 years ago

Well we are generating routes by passing route values (controller, action, culture, ...). To create a change culture link for the current link we need the current route values and change the culture to the culture we want.

One option to get the current route values is ViewContext.RouteData.Values. If you are on a translated route then there is already a culture (en for example) in this dictionary, so calling Add throws the exception above. That's why it's better to use the indexer dictionary[key] for adding / setting the culture value.

I don't know any "shorter" way, but if you want to keep your views clean, you could create an extension method for UrlHelper:

public static class UrlHelperExtension
{
    public static string RouteUrlWithCulture(this IUrlHelper urlHelper, string culture)
    {
        var routeValueDictionary = new RouteValueDictionary(urlHelper.ActionContext.RouteData.Values);
        routeValueDictionary["culture"] = culture;

        return urlHelper.RouteUrl(routeValueDictionary);
    }
}

and use it within your view:

<div><a href="@Url.RouteUrlWithCulture("pt")">Change Culture</a></div>

Does this help you?

mdmoura commented 6 years ago

Yes it does. Thank you for the help.