Routing Razor Pages to /example.com/en/ format - asp.net-mvc

I have three languages on my website. I'm trying to get my razor pages to route to culture/localization like so:
https://localhost:44396/en/
https://localhost:44396/ru/
I have hundreds of lines of code commented out at this point using methods I've been googling for the past two days and nothing seems to do the job.
The website is mostly static so right now beyond the culture there is nothing else that needs routing.

Here's a way you can do it that doesn't require you to put a middleware attribute on all of your pages. This works globally.
In the ConfigureServices method of Startup.cs, add the following:
services.AddMvc().AddRazorPagesOptions(options => {
options.Conventions.AddFolderRouteModelConvention("/", model => {
foreach (var selector in model.Selectors) {
selector.AttributeRouteModel.Template = AttributeRouteModel.CombineTemplates("{lang=en}", selector.AttributeRouteModel.Template);
}
});
});
services.Configure<RequestLocalizationOptions>(options => {
var defaultCulture = new CultureInfo("en");
var supportedCultures = new CultureInfo[] {
defaultCulture,
new CultureInfo("fr")
};
options.DefaultRequestCulture = new RequestCulture(defaultCulture);
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
options.RequestCultureProviders.Insert(0, new RouteDataRequestCultureProvider() {
RouteDataStringKey = "lang",
UIRouteDataStringKey = "lang",
Options = options
});
});
This sets up the global route, your supported cultures, and sets the primary culture provider to come from the route. (This still leaves the other providers intact, so failing the Route values, it can still set the culture based on the Query String, Cookies, or Language Header.)
Now, in your Configure method (still in Startup.cs), add the following:
var routeBuilder = new RouteBuilder(app) {
DefaultHandler = app.ApplicationServices.GetRequiredService<MvcRouteHandler>(),
};
routeBuilder.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute(app.ApplicationServices));
var router = routeBuilder.Build();
app.Use(async (context, next) => {
var routeContext = new RouteContext(context);
await router.RouteAsync(routeContext);
context.Features[typeof(IRoutingFeature)] = new RoutingFeature() {
RouteData = routeContext.RouteData
};
await next();
});
var options = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
app.UseRequestLocalization(options.Value);
app.UseMvc();
There's some trickery here. Firstly, we have to call app.UseRequestLocalization before we call app.UseMvc, or else our program will run before we've changed the current culture. But the problem is, app.UseMvc() is the one that sets up RouteData. So, until you call it, the routing values are all blank. Ergo, when the RouteDataRequestCultureProvider goes to try and observe what {lang} is, it'll come back empty, and thus always default you to en. Catch 22.
So, we just go manually populate the RouteData ourselves in our own custom middleware. That way, the RouteDataRequestCultureProvider can see it, and all will work well.
(I admit this is not the most efficient, as you're just duplicating the routing work that app.UseMvc() will itself also do, but I'll take that unnoticeable delay to ensure all my pages are localized.)

I will tell you what I do which works. The only difference is that I use the 5 characters language code but I guess it is not something difficult to change.
Make sure that you have the following nuget library installed
Microsoft.AspNetCore.Localization.Routing
In the ConfigureServices method of the Startup.cs we type the following code under the servcies.AddMvc();
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizeFolder("/Account/Manage");
options.Conventions.AuthorizePage("/Account/Logout");
options.Conventions.AddFolderRouteModelConvention("/", model =>
{
foreach (var selector in model.Selectors)
{
var attributeRouteModel = selector.AttributeRouteModel;
attributeRouteModel.Template = AttributeRouteModel.CombineTemplates("{lang=el-GR}", attributeRouteModel.Template);
}
});
});
IList<CultureInfo> supportedCultures = new List<CultureInfo>
{
new CultureInfo("en-US"),
new CultureInfo("fr-FR"),
new CultureInfo("el-GR"),
};
var MyOptions = new RequestLocalizationOptions()
{
DefaultRequestCulture = new RequestCulture(culture: "en-US", uiCulture: "en-US"),
SupportedCultures = supportedCultures,
SupportedUICultures = supportedCultures
};
MyOptions.RequestCultureProviders = new[]
{
new RouteDataRequestCultureProvider() { RouteDataStringKey = "lang", Options = MyOptions } // requires nuget package Microsoft.AspNetCore.Localization.Routing
};
services.AddSingleton(MyOptions);
We add the following class
using Microsoft.AspNetCore.Builder;
public class LocalizationPipeline
{
public void Configure(IApplicationBuilder app, RequestLocalizationOptions options)
{
app.UseRequestLocalization(options);
}
}
Now you have to add the following line over your PageModel class:
[MiddlewareFilter(typeof(LocalizationPipeline))]
public class ContactModel : PageModel
{
public void OnGet()
{
}
}
I hope it helps.

Related

Localize the DisplayFormat in ASP.NET Core

I am trying to localize the DisplayFormat of several of my view models. I have been able to localize the Display:Name, Required and RegularExpression messages all from a shared resource file in a separate project.
In addition, I have been able to localize my razor views and any messages generated from my controllers. After some research, it appears I can't localize the DisplayFormat in the same manner as the other data annotations. Other posts on SO indicate I should create a custom attribute that inherits from Attribute or DisplayAtttribute.
DisplayFormat data annotation using resource string
Model Class DisplayFormat() How can I localization NullDisplayText?
Ideally I would like to retrieve the correct format string from my shared resource file within the custom attribute while passing in the ResourceKey name. I am not sure how to go about setting this up. Possibly using the IStringLocalizer<SharedResource>?
I have an extension method to setup localization services at startup
public static class LocalizationExtensions
{
public static IServiceCollection AddLocalizationServices(this IServiceCollection services)
{
services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("en-US"),
new CultureInfo("es-MX")
};
options.DefaultRequestCulture = new RequestCulture("en-US");
// Formatting numbers, dates, etc.
options.SupportedCultures = supportedCultures;
// UI strings that we have localized.
options.SupportedUICultures = supportedCultures;
});
services.AddLocalization(options => { options.ResourcesPath = "Resources"; } );
services.AddControllersWithViews()
.AddRazorRuntimeCompilation()
.AddViewLocalization(options => {
options.ResourcesPath = "Resources";
})
.AddDataAnnotationsLocalization(options => {
options.DataAnnotationLocalizerProvider = (type, factory) =>
{
return factory.Create(typeof(SharedResource));
};
});
//services.AddScoped<RequestLocalizationCookiesMiddleware>();
return services;
}
}
In my configure method in startup
var options = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
app.UseRequestLocalization(options.Value);
Any help would be appreciated.

ASP.NET Core 2 MVC manipulating return url

We have an ASP.NET Core 2 MVC application and an action method Search which normally returns an IActionResult in the following form:
return View("SearchResult", Model);
How can we manipulate the return url?
What we would like to do is to take the QueryString and Add/Remove various keys using the QueryHelpers and other built-in functionalities and then change the return Url to that with the new QueryString.
If we just leave return View("SearchResult", Model); the original Url is used and not the changed one.
Any assistance would be greatly appreciated.
You could not control the web browser url directly from server side.
For a workaround, you could try to URL Rewriting Middleware in ASP.NET Core like
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.AddSession();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseSession();
var options = new RewriteOptions()
.Add(context => {
var request = context.HttpContext.Request;
var query = request.Query;
StringValues color = "";
if (request.Path.StartsWithSegments("/Home/Test", StringComparison.OrdinalIgnoreCase)
&& query.TryGetValue("color", out color))
{
var response = context.HttpContext.Response;
response.StatusCode = StatusCodes.Status301MovedPermanently;
context.Result = RuleResult.EndResponse;
context.HttpContext.Session.SetString("query", request.QueryString.Value);
var items = query.SelectMany(x => x.Value, (col, value) => new KeyValuePair<string, string>(col.Key, value)).ToList();
// At this point you can remove items if you want
items.RemoveAll(x => x.Key == "color"); // Remove all values for key
var qb = new QueryBuilder(items);
response.Headers[HeaderNames.Location] =
request.Path + qb;
}
else if (request.Path.StartsWithSegments("/Home/Test", StringComparison.OrdinalIgnoreCase)
&& !query.TryGetValue("color", out color))
{
context.Result = RuleResult.SkipRemainingRules;
request.QueryString = new QueryString(context.HttpContext.Session.GetString("query"));
}
});
app.UseRewriter(options);
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}

Using View Localization in ASP.NET Core

I want to use localization in an ASP.NET Core applciation that uses Areas.
I have got a partial view Areas\Admin\Views\People\GetPeopleStatistics.cshtml
In this I want to use localiuation:
...
#inject IViewLocalizer Localizer
<h3>#Localizer["People Statistics"]:</h3>
...
I created a resource file for this: Resources\Admin\Views\People\GetPeopleStatistics.en.resx
I have the following configuration in Startup.cs:
services.AddLocalization(opts => { opts.ResourcesPath = "Resources"; });
services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix, opts => { opts.ResourcesPath = "Resources"; })
.AddDataAnnotationsLocalization();
services.AddAutoMapper();
services.Configure<RequestLocalizationOptions>(
opts =>
{
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("en"),
new CultureInfo("de")
};
opts.DefaultRequestCulture = new RequestCulture("en");
// Formatting numbers, dates, etc.
opts.SupportedCultures = supportedCultures;
// UI strings that we have localized.
opts.SupportedUICultures = supportedCultures;
});
Unfortunatelly this does not work. The application does not display the value from the resource file.
I am using Cookies to store the culture:
[HttpPost]
public async Task SetLanguage(string culture)
{
Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }
);
}
I also get the following error in Visual Studio:
Custom tool PublicResXFileCodeGenerator failed to produce an output for input file 'Resources\Admin\Views\People\GetPeopleStatistics.en.resx' but did not log a specific error. WebApplication D:\SVN Repositories\SRMS\trunk\PresentationLayer\WebApplication\Resources\Admin\Views\People\GetPeopleStatistics.en.resx 1
What am I doing wrong? How can I configure this?
You might want try the following: rightclick your solution and do 'clean solution'. That solved the issue for me when localization was not working and I was 100% sure everything was configured correctly.

Unit testing controllers using UserManager.FindById

I am new to unit testing .net applications and am having difficulty with what I would imagine to be a very simple case.
// GET: Entities
public ViewResult Index()
{
_User = UserManager.FindById(User.Identity.GetUserId());
return View(entityRepository.GetEntities(_User.entityId));
}
I want to test that the correct view is outputted but can't get past the user line. In other languages I would simply mock UserManager.FindById to always return some predefined object, but I can't get it to work.
Have been trying to follow the approach given here example mocking the IUserStore but can't get it to work with my example. To mock FindByNameAsync they have used store.As
Any advice would be gratefully received.
My attempt was to follow a similar method to that in link above. Obviously IUserPasswordStore is the wrong interface, but I am not sure how to find the correct one.
var store = new Mock<IUserStore<ApplicationUser>>(MockBehavior.Strict);
store.As<IUserPasswordStore<ApplicationUser>>()
.Setup(x => x.FindById(It.IsAny<string>()))
.Returns(ApplicationUser)null);
EntitiesController controller = new EntitiesController();
var result = controller.Index() as ViewResult;
Assert.AreEqual("Index", result.ViewName);
So, thanks to the guidance from #Nkosi and #haim770 I think I have an answer. It still seems overly complex though, so would be interested to hear if you know how to simplify.
It started with needing to write more testable code, I installed Unity and started to inject dependencies allowing me to mock them out.
The solution that passes is:
Mock<IPrincipal> mockPrincipal;
string username = "test#test.com";
[TestInitialize]
public void TestInitialize()
{
//Arrange
var identity = new GenericIdentity(username, "");
var nameIdentifierClaim = new Claim(ClaimTypes.NameIdentifier, username);
identity.AddClaim(nameIdentifierClaim);
mockPrincipal = new Mock<IPrincipal>();
mockPrincipal.Setup(x => x.Identity).Returns(identity);
mockPrincipal.Setup(x => x.IsInRole(It.IsAny<string>())).Returns(true);
}
[TestMethod]
public void EntitiesIndexDisplaysTheDefaultView()
{
var context = new Mock<HttpContextBase>();
var principal = mockPrincipal.Object;
context.Setup(x => x.User).Returns(principal);
var userManagerMock = new Mock<IUserStore<ApplicationUser>>(MockBehavior.Strict);
userManagerMock.As<IUserPasswordStore<ApplicationUser>>()
.Setup(x => x.FindByIdAsync(It.IsAny<string>()))
.ReturnsAsync(new ApplicationUser() { entityId = "id" });
var entitiesRepositoryMock = new Mock<IEntityRepository>();
entitiesRepositoryMock
.Setup(x => x.GetEntities(It.IsAny<string>()))
.Returns((IEnumerable<Entity>)new List<Entity>());
EntitiesController controller = new EntitiesController(new UserManager<ApplicationUser>(userManagerMock.Object),
entitiesRepositoryMock.Object);
controller.ControllerContext = new ControllerContext(context.Object, new RouteData(), controller);
var result = controller.Index() as ViewResult;
Assert.AreEqual("", result.ViewName);
}

How can I mimic two domains on a single asp.net website? Are RouteConstraints an option?

I'm running a single instance of Orchard CMS on my web server with two custom modules, ModuleFirst and ModuleSecond. For reasons, I want these two to act as separate websites with their own domain and homepage. I can not set up additional websites or use Orchard's built-in Tenants feature.
What I have
The way I went about achieving this is as follows:
Added two bindings to my website in IIS: first-domain.com and second-domain.com
Implemented a ThemeSelector (which I think acts like an ActionFilter) that switches the theme based on the host of the Url in the incoming Request
if (host.Contains("second-domain.com"))
{
useSecondTheme = true;
}
Make sure all routes are unique
This is working reasonably well for the most part. I can navigate to first-domain.com/foo and second-domain.com/bar and it looks like I'm on different websites.
The problem
For the two "homepages" I can't make a unique route because I don't want to add any suffixes. Both projects define a blank route that should lead to their respective Home/Index but I can't figure out how to make this work.
new RouteDescriptor {
Priority = 90,
Route = new Route(
"",
new RouteValueDictionary { // Defaults
{"area", "ModuleFirst"},
{"controller", "Home"},
{"action", "Index"},
},
new RouteValueDictionary(), // Constraints
new RouteValueDictionary { // Datatokens
{"area", "ModuleFirst"}
},
new MvcRouteHandler())
}
new RouteDescriptor {
Priority = 100,
Route = new Route(
"",
new RouteValueDictionary { // Defaults
{"area", "ModuleSecond"},
{"controller", "Home"},
{"action", "Index"},
},
new RouteValueDictionary(), // Constraints
new RouteValueDictionary { // Datatokens
{"area", "ModuleSecond"}
},
new MvcRouteHandler())
}
What I tried
I tried to implement an ActionFilter that Redirects to ModuleFirst/Home/Index when a request with host url first-domain.com reaches ModuleSecond/Home/Index but this obviously doesn't work since it just keeps hitting the highest priority route over and over and breaks the website.
I have also tried to implement a custom RouteConstraint on the route with the highest priority to block all incoming request that don't come from its intended domain, assuming that those would then fall back on the lower priority route.
public class SecondConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
return httpContext.Request.Url.DnsSafeHost.Contains("second-domain.com");
}
}
used as follows:
new RouteDescriptor {
Priority = 100,
Route = new Route(
"",
new RouteValueDictionary { // Defaults
{"area", "ModuleSecond"},
{"controller", "Home"},
{"action", "Index"},
},
new RouteValueDictionary { // Constraints
{"isSecond", new SecondConstraint()}
},
new RouteValueDictionary { // Datatokens
{"area", "ModuleSecond"}
},
new MvcRouteHandler())
}
I can now navigate to second-domain.com just fine and get the correct page but navigating to first-domain.com times out. I haven't managed to find any examples of RouteConstraints in Orchard though, so maybe I'm doing it wrong.
Though I would recommend anyone who is about to do something like this from scratch to use tenants and OAuth instead if possible, as Bertrand suggests in his comment, I thought of a fairly clean way to accomplish what I wanted.
Instead of using two separate modules, I just use one.
In my one module, I implement IRouteProvider to override the default route, which catches both first-domain.com/ and second-domain.com/
public class Routes : IRouteProvider
{
public void GetRoutes(ICollection<RouteDescriptor> routes)
{
foreach (var routeDescriptor in GetRoutes())
routes.Add(routeDescriptor);
}
public IEnumerable<RouteDescriptor> GetRoutes()
{
return new[] {
new RouteDescriptor {
Priority = 100,
Route = new Route(
"",
new RouteValueDictionary {
{"area", "MyModule"},
{"controller", "Home"},
{"action", "Index"},
},
new RouteValueDictionary(),
new RouteValueDictionary {
{"area", "MyModule"}
},
new MvcRouteHandler())
}
};
}
}
Then I created a custom Part with just a single property bool IsHomepage and attached it to the Page Type
public class SecondHomepagePart : ContentPart<SecondHomepagePartRecord>
{
public bool IsHomepage
{
get { return Retrieve(r => r.IsHomepage); }
set { Store(r => r.IsHomepage, value); }
}
}
In the driver I make sure that there is only ever one "second homepage"
protected override DriverResult Editor(SecondHomepagePart part, IUpdateModel updater, dynamic shapeHelper)
{
if (updater.TryUpdateModel(part, Prefix, null, null))
{
if (part.IsHomepage)
{
var otherHomepages = _orchardServices.ContentManager.Query<SecondHomepagePart>().Where<SecondHomepagePartRecord>(r => r.IsHomepage && r.Id != part.Id).List();
foreach (var homepagePart in otherHomepages)
{
homepagePart.IsHomepage = false;
}
}
}
return Editor(part, shapeHelper);
}
Then in my custom HomeController I just check the current domain, fetch the ContentItem with the blank AutoroutePart or the SecondHomepagePart where IsHomepage is set to true depending on the domain, build the Detail Shape of that item and then display that Shape in my custom Index.cshtml view.
Controller:
public ActionResult Index()
{
var host = HttpContext.Request.Url.DnsSafeHost;
var model = new IndexViewModel();
IContent homepageItem;
if (host.Contains("first-domain.com"))
{
homepageItem = _homeAliasService.GetHomePage();
}
else
{
homepageItem = _orchardServices.ContentManager.Query<SecondHomepagePart>()
.Where<SecondHomepagePartRecord>(r => r.IsHomepage)
.List()
.FirstOrDefault();
}
model.Homepage = _orchardServices.ContentManager.BuildDisplay(homepageItem, "Detail");
return View(model);
}
View:
#model MyModule.ViewModels.Home.IndexViewModel
#if (Model.Homepage != null)
{
#Display(Model.Homepage)
}
I use the same check in an IConditionProvider and IThemeSelector to have a layer rule for each domain and to set the appropriate theme automatically. Now I essentially have two completely different websites to the outside world but with shared content, widgets, parts, custom settings etc. This is a quick and easy solution for a client who sells the same products from the same stock but under different brandings and under different conditions, along with some exclusive content that's different for both.

Resources