ASP.Net MVC4 Mobile-Aware OutputCache - asp.net-mvc-4

I'm working on upgrading an application from MVC3 to MVC4 and noticed something that I assumed (hoped?) would "just work".
CODE:
[OutputCache(Duration = 600, VaryByParam = "none")]
public ActionResult Index()
{
return View();
}
This is a textbook caching example for ASP.Net. Whenever a browser hits the page, it checks the cache to see if something exists, generates the view if not, and then sends the cached results.
This works great; however, playing around with the Mobile view functionality of MVC4, I noticed that the above code does not check to see if the Request is from a Mobile Device. So if I hit that route on a desktop, the desktop view will be displayed on my phone until cache is invalidated. The reverse is true as well (if I first hit the page with a phone, the desktop will then see the mobile view instead).
Is there a parameter that I could use to make this work like I hoped or am I looking at building a customer OutputCacheProvider?

After a bit more digging, I found a solution to the issue.
Updated Controller Action
[OutputCache(Duration = 600, VaryByCustom = "IsMobile")]
public ActionResult Index()
{
return View();
}
Override GetVaryByCustomString in Global.asax
public override string GetVaryByCustomString(HttpContext context, string custom)
{
if (custom.ToLowerInvariant() == "ismobile" && context.Request.Browser.IsMobileDevice)
{
return "mobile";
}
return base.GetVaryByCustomString(context, custom);
}

This is Correct GetVaryByCustomString method
public override string GetVaryByCustomString(HttpContext context, string custom)
{
if (custom.ToLowerInvariant() == "ismobile")
{
return context.GetVaryByCustomStringForOverriddenBrowser();
}
return base.GetVaryByCustomString(context, custom);
}

Related

Blazor Wasm Identity Login/Logout event

In a .Net 5 Blazor WASM app using the standard identity from the Wasm template, I want to do something when a user logs in (or logs out). I've tried the RemoteAuthenticatorView in Authentication.razor:
#page "/authentication/{action}"
#using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="#Action"
OnLogInSucceeded="OnLoginSucceeded"
OnLogOutSucceeded="OnLogoutSucceeded1"
/>
#code{
[Parameter] public string Action { get; set; }
public async void OnLoginSucceeded()
{
// do something
}
public void OnLogoutSucceeded1()
{
// do something
}
}
and also, I tried in a component (where I really want to take the action to merge a shopping basket) :
AuthenticationStateProvider.AuthenticationStateChanged += OnAuthenticationStateChanged;
private async void OnAuthenticationStateChanged(Task<AuthenticationState> task)
{
var user = (await task).User;
Console.WriteLine(DateTime.Now.ToString("hh:mm:ss:fff") + " InitialDataLoader -> OnAuthenticationStateChanged -> IsUserAuthenticated: " + user.Identity.IsAuthenticated);
}
I have breakpoints in each method and after I finally hit the Login breakpoint in both the RemoteAuthenticatorView and my component (it didn't break at all for a while), it now breaks but once only. If I logout - no logout event. If I login again, even as another user, I don't hit the breakpoints.
Is there a consistent method to generate or detect a login?
Turns out it was working ok, just the breakpoints were not working - no idea why not. I added Console.WriteLine messages to each & all events are hit correctly. So both techniques work as expected.

ProducesAttribute causes "No output formatter was found for content types"

Consider this simple controller action:
[HttpGet("{imageId}")]
[ResponseCache(Duration = 604800)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Produces("image/jpeg")]
public async Task<IActionResult> GetImageAsync(int imageId)
{
if (imageId <= 0)
{
return NotFound();
}
byte[] imageBytes = await _imageProvider.GetImageAsync(
imageId,
Request.HttpContext.RequestAborted).ConfigureAwait(false);
if (imageBytes is null)
{
return NotFound();
}
return File(imageBytes, MediaTypeNames.Image.Jpeg);
}
This method works fine, however in telemetry I am getting this for every call:
Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor: Warning: No output formatter was found for content types 'image/jpeg, image/jpeg' to write the response.
Simply commenting out the ProducesAttribute prevents the telemetry from being logged.
I want to keep ProducesAttribute because it enables my Swagger UI page to work. In Swagger it shows this API has an expected output media type of image/jpeg. If I remove the attribute it changes to text/plain and it doesn't render correctly.
So the question is, how can I fix this controller action to not create a bunch of unnecessary telemetry, while also allowing Swagger UI to work correctly?

Appending hash/fragment to RedirectResult results in cumbersome code

The code works but is silly.
When the View is returned to the user the page scrolls to the companyId anchor.
Silly is that I have to expose another public action with another route (without 'terms')
I want to redirect to /terms/companyId but then I get an ambigiousAction exception that this action with same routes already exists...
How to solve that dilemma if possible not change the first route?
[HttpGet("~/terms/{companyId}")]
public IActionResult Home(string companyId})
{
string url = Url.Action(nameof(HomeForRedirect), new { companyId}) + "#conditions";
return new RedirectResult(url);
}
[HttpGet("{companyId}")]
public IActionResult HomeForRedirect(string companyId)
{
Viewbag.CompanyId = companyId;
return View(nameof(Home));
}
If I'm understanding your code, you essentially want the URL /terms/{companyId} to redirect to /{controller}/{companyId}#conditions? The easiest path would be to attach both routes to the same action and do the redirect in a conditional. Something like:
[HttpGet("{companyId}", Order = 1)]
[HttpGet("~/terms/{companyId}", Order = 2)]
public IActionResult Home(string companyId)
{
if (Context.Request.Path.StartsWith("/terms"))
{
var url = Url.Action(nameof(Home), new { companyId }) + "#conditions";
return Redirect(url);
}
ViewBag.CompanyId = companyId;
return View();
}
An even better method would be to simply do the redirect directly in IIS. There's a not insignificant amount of processing that needs to occur to handle a request in ASP.NET Core machinery, and it's totally wasted effort simply to redirect. Use the URL Rewrite module in IIS to set up your redirect for this URL, and then your application doesn't have to worry about it at all. You just have your normal run-of-the-mill Home action that returns a view, and everything will just work.
A few other notes since it seems like you're new to this:
It's better to use the Route attribute rather than the more specific HttpGet etc. The default is GET.
Return the controller methods like Redirect rather than instances of IActionResult (i.e. new RedirectResult(...)).
The default is to return a view the same name as the action. So, assuming your action is Home, you can just do return View(), rather than return View(nameof(Home)).

Redirecting to an Action Method from another Action Method in the same controller

I am a Newbie in asp.net and currently I am doing a web page application in MVC4 with Login functionality.
My Index action method looks like this-
public ActionResult Index()
{
var PageModelList1 = new DataAccessLayer.DataAccess().GetPageInfo();
ViewData["MenuList"] = PageModelList1.PageModelList;
return View();
}
and my LogIn action method looks like-
[HttpPost]
public ActionResult LogIn(LogInModel model, string returnUrl)
{
if (ModelState.IsValid)
{
var PageModelList1 = new DataAccessLayer.DataAccess().GetPageInfo(model.UserName,model.Password);
ViewData["MenuList"] = PageModelList1.PageModelList;
return RedirectToAction("Index", "MyController");
}
ModelState.AddModelError("", "login failed");
return PartialView("_LogIn", model);
}
what I need is, when I Login successfully, the RedirectToAction("Index", "Deimos") should take place but the 'MenuList' there should be the new 'MenuList' from LogIn action method. How could I do it?
RedirectToAction will send a 302 response to the browser with the new url as the location header value and browser will make a totally new request to go to that page. This new request has no idea what you did in the previous request. So ViewData will not work. You may consider using TempData.
But TempData's life is only until the next request. After that it is gone. So if you want something on all the subsequent requests(like a menu to be shown to user), I suggest you read it from a database table every time you load the page. You can store the items to a cache after the first read to avoid constant hit(s) to the database if you are worried about that.
Another option is to set the menu items to Session variables and read from there. I am not a big fan of setting stuff like that to session. I prefer to read it from a cache (in which data was loaded from a db call) or so.

TempData not working for second request in MVC4

I have never seen this before and am stumped. I have the following controller sequence:
/// <summary>
/// Helper method to store offerId to TempData
/// </summary>
/// <param name="offerId"></param>
private void StoreOfferInTempData(string offerId)
{
if (TempData.ContainsKey(SelectedOfferKey))
TempData.Remove(SelectedOfferKey);
TempData.Add(SelectedOfferKey, offerId);
}
[HttpPost]
[AllowAnonymous]
public virtual ActionResult Step1(MyViewModel model)
{
if (ModelState.IsValid)
{
StoreOfferInTempData(model.SelectedOfferId);
return RedirectToAction(MVC.Subscription.Register());
}
MySecondViewModel model2 = new MySecondViewModel { OfferId = model.SelectedOfferId };
return View(model2);
}
[HttpGet]
[AllowAnonymous]
public virtual ActionResult Register()
{
string offerId = TempData[SelectedOfferKey] as string; //we get a valid value here
... error handling content elided ...
RegisterViewModel model = new RegisterViewModel { OfferId = offerId };
return View(model);
}
[HttpPost]
[AllowAnonymous]
public virtual ActionResult Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
CreateCustomerResult result = CustomerService.CreateAccount(model.Email, model.NewPassword);
if (result.Success)
{
... content elided; storing customer to Session ...
MyMembershipProvider.PersistUserCookie(result.Principal, true);
//need to store our selected OfferId again for use by the next step
StoreOfferInTempData(model.OfferId);
return RedirectToAction(MVC.Subscription.Payment());
}
model.ErrorMessage = result.ErrorMessage;
}
return View(model);
}
[HttpGet]
public ActionResult Payment()
{
string offerId = TempData[SelectedOfferKey] as string; //this is null???
... content elided ...
return View(model);
}
The first round of storage to TempData behaves as expected. The value is present in the subsequent HttpGet method and is marked for deletion such that it is no longer there when I go to add it again. However, on the third HttpGet method, it returns null.
I have tried using different Keys for each round with no change. I can assure you that at no time other than those displayed am I checking TempData, so I see no way the value would be marked for deletion somehow. Also, it fails in the Payment method whether it has an [AllowAnonymous] attribute or not (so not due to any http to https switch or anything like that.
Seems like it must be something very simple, but my searches have turned up nothing. Any help greatly appreciated.
UPDATE: On further inspection, it seems my whole context is hosed on this step, for some reason. We're using IoC in the controllers, but none of the IoC-instantiated items are there. The mystery deepens.
The lifespan of TempData is only until it's read back out or the next request has processed (which ever comes first). You shouldn't be relying on TempData if you're going in to two (or three) requests. Instead, use the session or a database.
The purpose of TempData is to hand-off information between requests not to perpetuate until you clear it (that's what sessions are for).
Aha! Well, this is obscure enough that I hope it helps someone else. Turns out, I had forgotten to run my T4MVC.tt file after creating the Payment() actions, so the RedirectToAction taking an MVC.Subscription.Payment() action was not instantiating the controller properly. I'm not clear on all the underlying magic here, but if you run into this and are using T4MVC.tt, make sure you ran it!
Comments on why this would be are welcome.
Use TempData.Keep("key") to retain values between multiple post-backs