I use ActionFilters to pass query parm to ViewData, and Controller method parameters to ViewData for the returnUrl. Which was based on
https://andrewlock.net/using-an-iactionfilter-to-read-action-method-parameter-values-in-asp-net-core-mvc/
// Here are the filters
namespace MyNamespace.Utilities.Filters {
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using System.Linq;
public class ReturnUrlQueryToViewDataActionFilter : IActionFilter {
private readonly string parmName = "ReturnUrl"; // I actually keep this in a global static variable, but put here for example
public void OnActionExecuting(ActionExecutingContext context) {
if (context.Controller is Controller controller && controller.Url is IUrlHelper urlHelper) {
if (context.HttpContext.Request.Query[parmName] is StringValues stringValues/*Possible to have more than one matching query values(like a list) which would be comma seperated*/
&& stringValues.Count == 1 /*returnurl itself should never be a list form, even though query parms object make it possible, so if more than 1, just ignore it completly*/
&& stringValues.Single() is string returnUrl
&& !string.IsNullOrWhiteSpace(returnUrl)) {
if (urlHelper.IsLocalUrl(returnUrl)) {
controller.ViewData[parmName] = returnUrl;
} else if (context.HttpContext.RequestServices.GetRequiredService<ILogger<ReturnUrlQueryToViewDataActionFilter>>() is ILogger logger) {// WAS NOT A LOCAL RETURN URL
logger.LogWarning("RedirectUrl was NOT LOCAL URL: '{badReturnUrl}'; ViewData value WAS NOT SET.", returnUrl);
}
}
}
}
public void OnActionExecuted(ActionExecutedContext context) { }
}
// Used to take a "Named" parameter on the Action method
public class ReturnUrlParameterToViewDataActionFilter : IActionFilter {
private readonly string parmName = "ReturnUrl"; // I actually keep this in a global static variable, but put here for example
public void OnActionExecuting(ActionExecutingContext context) {
if (context.Controller is Controller controller && controller.Url is IUrlHelper urlHelper) {
if (context.ActionArguments.TryGetValue(parmName, out object value)
&& value is object obj
&& obj != null
&& obj.ToString() is string returnUrl
&& !string.IsNullOrWhiteSpace(returnUrl)) {
if (urlHelper.IsLocalUrl(returnUrl)) {
controller.ViewData[parmName] = returnUrl;
} else if (context.HttpContext.RequestServices.GetRequiredService<ILogger<ReturnUrlQueryToViewDataActionFilter>>() is ILogger logger) {// WAS NOT A LOCAL RETURN URL
logger.LogWarning("RedirectUrl was NOT LOCAL URL: '{badReturnUrl}'; ViewData value WAS NOT SET.", returnUrl);
}
}
}
}
public void OnActionExecuted(ActionExecutedContext context) { }
}
}
// add to the DI Container
services.AddScoped<ReturnUrlQueryToViewDataActionFilter>();
services.AddScoped<ReturnUrlParameterToViewDataActionFilter>();
// Example Usage of ReturnUrlQueryToViewDataActionFilter over a get method that may or maynot include a query parameter for "ReturnUrl",
[HttpGet]
[ServiceFilter(typeof(ReturnUrlQueryToViewDataActionFilter))]
public async Task<IActionResult> Edit(Guid? id, CancellationToken cancellationToken) {
// do a get to db to get the model and return to view that will ultimatly do a "Post Action"
// that uses a form with a hidden input for "ReturnUrl" that will then get picked up by the ReturnUrlParameterToViewDataActionFilter
}
// Example Usage of ReturnUrlParameterToViewDataActionFilter over a Post method that has a parameter on the Controller Action for "ReturnUrl"
[HttpPost]
[ValidateAntiForgeryToken]
[ServiceFilter(typeof(ReturnUrlParameterToViewDataActionFilter))]
public async Task<IActionResult> Edit(Guid id, string Name, string Description, string ReturnUrl){
// value gets sent back to Edit View if reshown to user IE. ModelState was not valid
// If Edit is successfull redirect the user (for safety check if Url is local, even though that is checked in the filters, can never be to safe
if(this.Url.IsLocal(ReturnUrl)){
// could be a few other things to check here for making sure url is correct depending
// on if you use virtual apps/paths, like checking for returnUrl.StartsWith('/') then prepend a ~ to get the virtual path to map correctly, but since this part can be very different depending on your setup
retun Redirect(this.Url.Content(ReturnUrl));
}
}
// I also use this TagHelper to take the ViewData value and transform it into a hidden field
namespace MyNamespace.Utilities.TagHelpers {
using Microsoft.AspNetCore.Razor.TagHelpers;
public class ReturnUrlHiddenInputTagHelper : TagHelper {
private readonly string parmName = "ReturnUrl"; // I actually keep this in a global static variable, but put here for example
[ViewContext, HtmlAttributeNotBound]
public ViewContext ViewContext { get; set; } // will be null until Process()
public override void Process(TagHelperContext context, TagHelperOutput output) {
string returnUrl = this.ViewContext?.ViewData?[parmName]?.ToString();
if (string.IsNullOrWhiteSpace(returnUrl)) {// If no return url, dont display
output.SuppressOutput();
return;
}
output.TagName = "input";
output.TagMode = TagMode.SelfClosing;
output.Attributes.Add("type", "hidden");
output.Attributes.Add("name", parmName);
output.Attributes.Add("value", returnUrl);
}
}
}
// Then inside your _ViewImports.cshtml include whatever your base namespace is that holds the TagHelper
@addTagHelper *, MyNamespace
// Then inside the Edit.cshtml form you put, which will put a hidden field from the ViewData value if located, if not it does nothing
<return-url-hidden-input />