How can I change Mvc Routing spaces like "%20" to "-" - asp.net-mvc

My current routing is like https://localhost:44312/Games/Sea%20of%20Thieves.
I want to change as https://localhost:44312/Games/Sea-of-Thieves.
I'm working on .Net Core Mvc Project. Any ideas ?
[Route("Games/{gameName}")]
public IActionResult GameDetail(string gameName)
{
Game requestedGame = _unitOfWork.Games.GetGameByName(gameName);
GameDetailModel gameDetailModel = _unitOfWork.Games.GetGameDetail(requestedGame.Id, User.Identity.Name);
return View(gameDetailModel);
}

You should replace spaces with - when you create a link on your web site.
For example you can create an interface that replace space with -. then inject the interface on your view or place you create link.
public interface IUrlHelper
{
string RemoveSpace(string url);
}
public class UrlHelper : IUrlHelper
{
public string RemoveSpace(string url)
{
return url.Replace(" ","-");
}
}
in your view or anywhere you create a link you should use this interface
#inject IUrlHelper urlHelper
TestUrl
Note : remember that register IUrlHelper in services.

You can use middleware in startup Configure:
app.Use(async (context, next) =>
{
var url = context.Request.Path.Value;
if (url.Contains("/Games/")&&url.Contains(" "))
{
context.Response.Redirect(url.Replace(" ", "-"));
return;
}
await next();
});
result:

Related

Blazor IStringLocalizer injection to services

I am using IStringLocalizer approach to localize my Blazor app as discussed here.
Injecting the IStringLocalizer on razor pages works great. I also need this to localize some services - whether scoped or even singleton services.
Using constructor injection to inject my IStringLocalizer service into the service works. However, when users change the language via UI, the service (whether singleton or scoped) keeps the initial IStringLocalizer - i.e. the one with the original language used when starting the app, not the updated language selected by the user.
What is the suggested approach to retrieve the updated IStringLocalizer from code?
EDIT
To prevent more details, here is some piece of code.
First, I add a Resources folder and create there a default LocaleResources.resx (with public modifiers) and a LocaleResources.fr.resx file, which contain the key-value pairs for each language.
Supported cultures are defined in the appsettings.json file as
"Cultures": {
"en-US": "English",
"fr": "Français (Suisse)",
...
}
In startup, I register the Resources folder and the supported cultures :
public void ConfigureServices(IServiceCollection services {
...
services.AddLocalization(options => options.ResourcesPath = "Resources");
...
services.AddSingleton<MySingletonService>();
services.AddScoped<MyScopedService>();
}
// --- helper method to retrieve the Cultures from appsettings.json
protected RequestLocalizationOptions GetLocalizationOptions() {
var cultures = Configuration.GetSection("Cultures")
.GetChildren().ToDictionary(x => x.Key, x => x.Value);
var supportedCultures = cultures.Keys.ToArray();
var localizationOptions = new RequestLocalizationOptions()
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);
return localizationOptions;
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
...
app.UseRequestLocalization(GetLocalizationOptions());
...
app.UseEndpoints(endpoints => {
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
I created an empty LocaleResources.razor control at the root of the project (this is a trick used to inject a single resource file to all components).
I included a routing controller to change language :
[Route("[controller]/[action]")]
public class CultureController : Controller {
public IActionResult SetCulture(string culture, string redirectUri) {
if (culture != null) {
HttpContext.Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(
new RequestCulture(culture)));
}
return LocalRedirect(redirectUri);
}
}
And the language UI switcher looks like this (I use SyncFusion control here, but it could be any lookup actually, that shouldn't really matter)
#inject NavigationManager NavigationManager
#inject IConfiguration Configuration
<SfComboBox TValue="string" TItem="Tuple<string, string>" Placeholder="Select language" DataSource="#Cultures"
#bind-Value="selectedCulture" CssClass="lan-switch" Width="80%">
<ComboBoxFieldSettings Text="Item2" Value="Item1"></ComboBoxFieldSettings>
</SfComboBox>
<style>
.lan-switch {
margin-left: 5%;
}
</style>
#code {
string _activeCulture = System.Threading.Thread.CurrentThread.CurrentCulture.Name;
private string selectedCulture {
get => _activeCulture;
set {
_activeCulture = value;
SelectionChanged(value);
}
}
List<Tuple<string, string>> Cultures;
protected override void OnInitialized() {
var cultures = Configuration.GetSection("Cultures")
.GetChildren().ToDictionary(x => x.Key, x => x.Value);
Cultures = cultures.Select(p => Tuple.Create<string, string>(p.Key, p.Value)).ToList();
}
protected override void OnAfterRender(bool firstRender) {
if (firstRender && selectedCulture != AgendaSettings.SelectedLanguage) {
selectedCulture = AgendaSettings.SelectedLanguage;
}
}
private void SelectionChanged(string culture) {
if (string.IsNullOrWhiteSpace(culture)) {
return;
}
AgendaSettings.SelectedLanguage = culture;
var uri = new Uri(NavigationManager.Uri)
.GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped);
var query = $"?culture={Uri.EscapeDataString(culture)}&" +
$"redirectUri={Uri.EscapeDataString(uri)}";
NavigationManager.NavigateTo("/Culture/SetCulture" + query, forceLoad: true);
}
}
Finally, to the injection. I inject the IStringLocalizer to pages as follows and it works perfectly fine on razor controls:
#inject IStringLocalizer<LocaleResources> _loc
<h2>#_loc["hello world"]</h2>
Above, when I change language, the page displays the value in the corresponding resource file.
Now, to services: the MySingletonService and MyScopedService are registered at startup. They both have a constructor like
protected IStringLocalizer<LocaleResources> _loc;
public MySingletonService(IStringLocalizer<LocaleResources> loc) {
_loc = loc;
}
public void someMethod() {
Console.WriteLine(_loc["hello world"])
}
I run someMethod on a timer. Strangely, when I break on the above line, the result seems to oscillate : once it returns the default language's value, once the localized one...!
The answer to my question was: your code is correct!
The reason, I found out, is that I use a Scoped service that is started on the default App's start page:
protected async override Task OnAfterRenderAsync(bool firstRender) {
if (firstRender) {
MyScopedService.StartTimer();
}
await base.OnAfterRenderAsync(firstRender);
}
When users change language, the whole page is refreshed and a new instance of the scoped service is created and timer started. As my service did not implement IDisposable, the timer was not actually stopped.
So 2 solutions here:
use singleton services
make servcie disposable and ensure tasks are cancelled when service is disposed of.

api coming twice in URL "api/AuthenticationAPI/logout"

As a convention in our project, we have named all our API controller classes as AuthenticationApiController as opposed to controller's in MVC AuthenticationController.
But now when API gets invoked, we have to call it like /api/authenticationapi/logout.
Thought not a problem, but I am not liking that "api" word is coming twice in the URL.
Is there a way, I can customize the route which is defined as [Route("api/[controller]")] to remove api from controller name when URL is getting added to route table.
**Note: looking for a generic way, rather than hardcoding the name on every api controller.
Try to use Url Rewriter and refer to my demo which uses asp.net core 3.0:
1.Create RewriterRules
public class RewriteRules
{
public static void ReWriteRequests(RewriteContext context)
{
var request = context.HttpContext.Request;
var path = request.Path.Value;
if (path != null)
{
var array = path.Split("/");
if (array[1] == "api" && !array[2].EndsWith("api", StringComparison.OrdinalIgnoreCase))
{
array[2] = array[2] + "api";
var newPath = String.Join("/", array);
context.HttpContext.Request.Path = newPath;
}
}
}
}
2.Register it in startup Configure method(before app.UseMvc() if you use core 2.2)
app.UseRewriter(new RewriteOptions()
.Add(RewriteRules.ReWriteRequests)
);
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
3.Test
[Route("api/[controller]")]
[ApiController]
public class AuthenticationApiController : ControllerBase
{
// GET: api/TestApi
[HttpGet("logout")]
public IEnumerable<string> logout()
{
return new string[] { "value1", "value2" };
}
}
Call /api/authentication/logout it will comes into the action successfully

.NETCore Middleware Redirect Removing Path IIS Sub Application

I have a middleware class that reads the user's cookies and validates them, then redirects to a login URL if they are invalid or missing. The URL that the user is redirected to contains a 'back' parameter. This middleware works perfectly fine on a static-file SPA and when the application is not a nested application within IIS.
My Question, this does not work when using MVC controllers and Views, before the user is redirected to the login page somehow the path (which points to the nested IIS site) is stripped and the back URL does not contain the URL path. Any ideas?
What happens
Go to -> http://test.com/mysite
Redirects to -> http://login.com?back=http://test.com
What should happen
Go to -> http://test.com/mysite
Redirects to -> http://login.com?back=http://test.com/mysite
public class MyMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
private readonly IOptions<MyMiddlewareOptions> _options;
public MyMiddleware(RequestDelegate next, IOptions<MyMiddlewareOptions> options, ILoggerFactory logger)
{
_next = next;
_options = options;
_logger = logger.CreateLogger("My Middleware");
}
public async Task Invoke(HttpContext context)
{
var middlewareCookieValidator = context.RequestServices.GetService<IMiddlewareCookieValidator>();
await new CookieRequestCultureProvider().DetermineProviderCultureResult(context);
if (!_options.Value.Bypass && !Path.HasExtension(context.Request.Path.Value))
{
try
{
if (wslCookieValidator.HasCreatedCookies(context) || await middlewareCookieValidator.ValidateCookiesAsync())
{
context.Response.OnStarting(async () => await middlewareCookieValidator.GenerateAndApplyCookiesToResponse(context));
await _next(context);
}
else
{
var location = new Uri($"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}{context.Request.QueryString}");
context.Response.Redirect($"{_options.Value.Url}?back={location}");
}
}
catch (Exception exception)
{
throw new Exception($"RequestDelegate '_next' = {_next}. {exception.Message}");
}
}
else
{
await _next(context);
}
}
}
}
This might be an URL encoding issue. You should encode the URL you pass into the back query-string parameter using System.Net.WebUtility.UrlEncode(). For example:
using System.Net;
// ...
var location = new Uri($"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}{context.Request.QueryString}");
context.Response.Redirect($"{_options.Value.Url}?back={WebUtility.UrlEncode(location)}");
#Lex Li
Thank you for your answer, turns out due to IIS running with sub-applications.
I needed the following for it to work.
var location = new Uri($"{context.Request.Scheme}://{context.Request.Host}{context.Request.PathBase}{context.Request.Path}{context.Request.QueryString}");

How do I match a URL starting with a literal tilde character (~) in System.Web.Routing / ASP.NET MVC?

I'm converting some old-school code to ASP.NET MVC, and have hit a snag caused by our URL formats. We specify thumbnail width, height, etc. in a URL by prefixing the special URL path with a tilde, as in this example:
http://www.mysite.com/photo/~200x400/crop/some_photo.jpg
At the moment, this is resolved by a custom 404 handler in IIS, but now I want to replace /photo/ with an ASP.NET and use System.Web.Routing to extract the width, height, etc. from the incoming URL.
Problem is - I can't do this:
routes.MapRoute(
"ThumbnailWithFullFilename",
"~{width}x{height}/{fileNameWithoutExtension}.{extension}",
new { controller = "Photo", action = "Thumbnail" }
);
because System.Web.Routing won't allow a route to start with a tilde (~) character.
Changing the URL format isn't an option... we've supported this URL format publicly since 2000 and the web is probably rife with references to it. Can I add some kind of constrained wildcard to the route?
You could write a custom crop route:
public class CropRoute : Route
{
private static readonly string RoutePattern = "{size}/crop/{fileNameWithoutExtension}.{extension}";
private static readonly string SizePattern = #"^\~(?<width>[0-9]+)x(?<height>[0-9]+)$";
private static readonly Regex SizeRegex = new Regex(SizePattern, RegexOptions.Compiled);
public CropRoute(RouteValueDictionary defaults)
: base(
RoutePattern,
defaults,
new RouteValueDictionary(new
{
size = SizePattern
}),
new MvcRouteHandler()
)
{
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var rd = base.GetRouteData(httpContext);
if (rd == null)
{
return null;
}
var size = rd.Values["size"] as string;
if (size != null)
{
var match = SizeRegex.Match(size);
rd.Values["width"] = match.Groups["width"].Value;
rd.Values["height"] = match.Groups["height"].Value;
}
return rd;
}
}
which you would register like this:
routes.Add(
new CropRoute(
new RouteValueDictionary(new
{
controller = "Photo",
action = "Thumbnail"
})
)
);
and inside the Thumbnail action of Photo controller you should get all you need when you request /~200x400/crop/some_photo.jpg:
public ActionResult Thumbnail(
string fileNameWithoutExtension,
string extension,
string width,
string height
)
{
...
}

Redirecting to same ActionResult from different controllers

I have a User entity, and in various views, I want to create links to a user home page basically. This functionality should be available in different controllers, so I can easily redirect to the user's home page. Each user in my site has a role ; for example reader, writer, editor, manager and admin. Ideally, I want to try to achieve something like this:
In a controller, for example
public ActionResult SomeThingHere() {
return View(User.GetHomePage());
//OR
return RedirectToROute(User.GetHomePage());
}
in a View, I also want to use the same functionality, for example:
<%= Html.ActionLink("Link to home", user.GetHomePage() %>
Is it possible to achieve such a design in MVC? If so , how should I go about it?
I currently use a method like this, but it is only in one controller at the moment. Now I need to use the same code somewhere else and I am trying to figure out how I could refractor this and avoid repeating myself?
....
private ActionResult GetHomePage(User user){
if (user.IsInRole(Role.Admin))
return RedirectToAction("Index", "Home", new { area = "Admin" });
if (user.IsInRole(Role.Editor))
// Managers also go to editor home page
return RedirectToAction("Index", "Home", new {area = "Editor"});
if (user.IsInRole(Role.Reader))
// Writer and reader share the same home page
return RedirectToAction("Index", "Home", new { area = "Reader" });
return RedirectToAction("Index", "Home");
}
...
How about something like this:
private string GetArea(User u)
{
string area = string.empty;
if (User.IsInRole(Admin)) area = "admin";
else if (...)
return area;
}
I would suggest a custom extension to the HtmlHelper class. Top of my head (liable to have syntax errors), something like this
public static class RoleLinksExtension
{
public static string RoleBasedHomePageLink(this HtmlHelper helper, string text)
{
if (user.IsInRole(Role.Admin))
return helper.ActionLink(text, "Index", "Home", new { area = "Admin" });
// other role options here
return string.Empty; // or throw exception
}
}
Then it's just
<%= Html.RoleBasedHomePageLink("Link to home") %>
in your markup.
You don't really want to have a link to somewhere that simply redirects somewhere else, if you can avoid it.
Edit: No idea why I didn't think of this earlier, but if you do need to redirect (perhaps if you need some functionality before going to the home page), you could extend IPrinciple instead
public static class AreaHomePageExtensions
{
public static string GetArea(this IPrinciple user)
{
if (user.IsInRole(Role.Admin))
return "Admin";
// Other options here
}
}
Then you can do
return RedirectToAction("Index", "Home", new { area = User.GetArea() });
whenever you like.
Well I finally came up with a design that seems to work. I have written an controller extension,
with a GetHomePage Method. This extension can also be used in your views. Here is how I did It:
public static class UserHelperExtension {
public static string GetHomePage(this ControllerBase controller, User user) {
return = "http://" + controller.ControllerContext
.HttpContext.Request
.ServerVariables["HTTP_HOST"] + "/"
+ GetHomePage(user);
}
//need this for views
public static string GetHomePage(string httphost, User user) {
return = "http://" + httphost + "/" + GetHomePage(user});
}
private static string GetHomePage(User user) {
if (user.IsInRole(Role.Admin))
return "/Admin/Home/Index";
if (user.IsInRole(Role.Editor))
return "/Editor/Home/Index";
if (user.IsInRole(Role.Reader))
return "/Reader/Home/Index";
return "/Home/Index";
}
}
The action method in the controller looks like this:
using Extensions;
...
public ActionResult SomethingHere() {
return Redirect(this.GetHomePage(user));
}
...
In the view I have this:
...
<%# Import Namespace="Extensions"%>
<%=UserHelperExtension.GetHomePage(Request.ServerVariables["HTTP_HOST"], user)%>
...
The advantage is that I can easily use this "GetHomePage" method in various controllers,
or views thoughout my application, and the logic is in one place. The disadvantage is that
I would have preferred to have it more type safe. For example, in my orignal tests, I had access to RouteValues collection:
public void User_should_redirect_to_role_home(Role role,
string area, string controller, string action) {
...
var result = (RedirectToRouteResult)userController.SomeThingHere();
Assert.That(result.RouteValues["area"],
Is.EqualTo(area).IgnoreCase);
Assert.That(result.RouteValues["controller"],
Is.EqualTo(controller).IgnoreCase);
Assert.That(result.RouteValues["action"],
Is.EqualTo(action).IgnoreCase);
...
}
But now that I am using a string so it is not type safe, and checking the RedirectResult.Url.
...
var result = (RedirectResult) userController.SomethingHere();
Assert.That(result.Url.EndsWith("/" + area + "/" + controller + "/" + action),
Is.True);
...

Resources