Loding pages by posting parameters - asp.net-core

The subject might not be clear since I couldn't find a better way to express it.
I am developing a web application using ASP.NET Core 6.0 with Razor Pages. Our previous application was an SPA using Ext JS where any call to server was returning only data and where I was also able to make any kind of call (GET/POST) to get the data.
For example, in the above picture from my old application, I make an ajax call with POST to get the list of periods when I open this page. I make a POST because I am sending the period type in my request payload. Sure I can pass these parameters in a GET request, however my other views have many criteria, so passing these criteria in the query string is not what I want. So, I decided to make it a standard to make my calls with POST method if there are any criteria payload, make GET request only when fething an entity with a simple key parameter (like Id) or GET any list that doesn't have any criteria.
Now, I am quite confused how to do same thing in my new ASP.NET Core Razor Pages web application. Normally, the menu items navigate to the page using link as below, which makes a GET request:
<a asp-area="System" asp-page="/ProfessionList">#AppLocalizer["Profession List"]</a>
<a asp-area="System" asp-page="/PeriodList">#AppLocalizer["Profession List"]</a>
In order to make a POST request, I replaced the menu item for period list as following which makes a POST request with a default periodType payload:
<a asp-area="System" asp-page="/ProfessionList">#AppLocalizer["Profession List"]</a>
<form asp-area="System" asp-page="/PeriodList" method="post">
<input type="hidden" name="periodType" value="1" hidden />
<button type="submit" >#AppLocalizer["Period List"]</button>
</form>
And the corresponding PeriodType.cshtml.cs file is as following:
[Authorize]
public class PeriodListModel: BaseEntityListPageModel<List<JsonPeriodEx>> {
public PeriodListModel(ILogger<BaseEntityListPageModel<List<JsonPeriodEx>>> logger, WebApi webApi) : base(logger, webApi) {
}
public IActionResult OnGet() {
PageData = JsonConvert.DeserializeObject<List<JsonPeriodEx>>(TempData["PageData"].ToString());
return Page();
}
public async Task<IActionResult> OnPostAsync(int periodType) {
var jsonResult = await _WebApi.DoPostAsync<List<JsonPeriodEx>>("/PeriodEx/GetList", new[] { new { Property = "periodType", Value = periodType } });
if (jsonResult.IsLoggedOut)
return RedirectToPage("/Login", new { area = "Account" });
if (jsonResult.Success) {
PageData = jsonResult.Data;
TempData["PageData"] = JsonConvert.SerializeObject(PageData);
return RedirectToPage("/PeriodList");
} else {
return RedirectToPage("/Error");
}
}
}
OnPostAsync successfully binds to the posted periodType parameter and gets the list of periods. Now, at the end of a successful call I want to follow the Post/Redirect/Get pattern and redirect to OnGet with the data from OnPostAsync, which is stored in TempData.
Now, according to the above scenario, is my approach, explained above, correct or should I implement it differently?
Thanks in advance

For these cases I would prefer TempData. Much easier and less code.
public async Task OnGet()
{
TempData["myParamToPass"] = 999;
...
}
public async Task OnPostReadData()
{
if (TempData.ContainsKey("myParamToPass"))
{
var myParamToPassValue = TempData.Peek("myParamToPass") as int?;
...
}
...
}

Related

How to get URI in Blazor AuthorizationHandler

I'm using a custom policy to secure a page in a server-side Blazor app. All is working well except one of my policies requires knowing the query parameters of the request. For example, the URI path is something like https://mywebsite/profile/1234, which is used to view/edit the profile with id=1234. Obviously we only want the user with profileId = 1234 editing this page. How can I check for this in my IAuthorizationHandler?
I tried injecting the HttpContext and reading the request.Query items, but it's just always "/" or "/_blazor", because it's a SPA course. I tried injecting NavigationManager (formerly UriHelper) to get the URI from there, but got an error:
'RemoteNavigationManager' has not been initialized.
I also tried using the Resource parameter to pass the information into my handler. I couldn't find any examples of how to do this, so this is my attempt:
Here is my profile.razor code, where I am limiting access with Policy="CanEditProfile"
#inject NavigationManager NavigationManager
<AuthorizeView Policy="CanEditProfile">
<NotAuthorized>
<h2 class="mt-5">You are not authorized to view this page</h2>
</NotAuthorized>
<Authorized>
<div class="container my-profile">
<h2>My Profile</h2>
And my IAuthorizationHandler code:
public Task HandleAsync(AuthorizationHandlerContext context)
{
if (context == null || httpContextAccessor.HttpContext == null) return Task.CompletedTask;
// try getting path from httpContext
var path = httpContextAccessor.HttpContext.Request.Path.Value;
Console.WriteLine($"Path = {path}"); // this is always "/_blazor"
// try getting path from resource, passed in from blazor page component
var resource = context.Resource?.ToString();
Console.WriteLine($"Resource = {resource}"); // this is always null
var pendingRequirements = context.PendingRequirements.ToList();
foreach (var requirement in pendingRequirements)
{
if (requirement is EditMemberPermission)
{
// if this user is admin, then grant permission
var isAdmin = context.User.IsInRole("Admin");
if (isAdmin)
{
context.Succeed(requirement);
continue;
}
// get requested memberId from uri parameter, e.g. /profile/1234
var requestedMemberId = // How do I get this?
if (IsOwner(context.User, requestedMemberId))
{
context.Succeed(requirement);
}
}
}
return Task.CompletedTask;
}
Any ideas on how to achieve this? It seems like it would be a common scenario, to secure a page based on which page data (query param "id") the user is trying to access. Many of the examples mention securing a Resource, and show it as an optional parameter, but no examples I could find show actually passing a value and using it. How can you secure a resource if you don't know what the resource is?
I thought there might be a way to pass the Resource parameter from the .razor page to the Auth handler, like this, but I haven't gotten that to work either.
<AuthorizeView Policy="CanEditProfile" Resource="<pass url somehow?>" />
Thanks in advance.
I got this working by using this code in my profile.razor:
#page "/profile/{MemberId}"
<AuthorizeView Policy="CanEditProfile" Resource="#MemberId">
... page content
</AuthorizeView>
#code {
[Parameter]
public string MemberId { get; set; }
}
This gets the MemberId parameter from the route, and passes it as a Resource to my IAuthorizationHandler. In that handler method, I can fetch it like this:
public Task HandleAsync(AuthorizationHandlerContext context)
{
if (context == null) return Task.CompletedTask;
// get member id from resource, passed in from blazor page component
var resource = context.Resource?.ToString();
var hasParsed = int.TryParse(resource, out int requestedMemberId);
if (hasParsed)
{
// compare the requested memberId to the user's actual claim of memberId
var isAuthorized = requestedMemberId == context.User.GetMemberIdClaim();
// now we know if the user is authorized or not, and can act accordingly
}

What is difference between Page() and Rediirect() to self in ASP.NET Core Razor pages?

I have a very simple page that has 2 forms. When I submit one form it resets the other. There is some kind of hidden optimization is going on because when I refresh the page it presents the correct result.
Here is the page:
<div asp-validation-summary="All"></div>
<div class="col-md-3">
<form method="POST">
<fieldset>
<div>Host Name: <input asp-for="ClientConfig.HostName" /></div>
<div>Responses in HTML? <input type="checkbox" asp-for="ClientConfig.Html" /></div>
<input type="submit" asp-page-handler="ClientConfiguration" />
</fieldset>
</form>
<p>Base URL = #Model.ClientConfig.Summary</p>
</div>
<form method="POST">
<fieldset>
<div>Name: <input asp-for="Customer.Name" /></div>
<div>New? <input type="checkbox" asp-for="Customer.New" /></div>
<input type="submit" asp-page-handler="Customer" />
</fieldset>
</form>
<ul>
<li>Customer = #Model.Customer.Summary</li>
</ul>
Here is the model...
public class ClientConfig
{
public static ClientConfig Instance { get; set; } = new ClientConfig();
[Required, StringLength(100)] public string HostName { get; set; } = "LocalHost";
public bool Html { get; set; }
public string Summary => HostName + (Html ? " (Html)" : "");
}
public class Customer
{
public static Customer Instance { get; set; } = new Customer();
[Required, StringLength(100)] public string Name { get; set; } = "Default";
public bool New { get; set; }
public string Summary => Name + (New ? " (New)" : "");
}
public class IndexModel : PageModel
{
public IndexModel()
{
ClientConfig = ClientConfig.Instance;
Customer = Customer.Instance;
}
[BindProperty] public ClientConfig ClientConfig { get; set; }
[BindProperty] public Customer Customer { get; set; }
public async Task<IActionResult> OnPostCustomerAsync()
{
Customer.Instance = Customer;
return Page();
}
public async Task<IActionResult> OnPostClientConfigurationAsync()
{
ClientConfig.Instance = ClientConfig;
return Page();
}
}
So what is "return Page();" doing? According to the documentation it is simply rendering the current page. Not true. To verify this, simply refresh the page. It will be different, accurate with both forms filled in. Also if you replace "return Page()" with "return Redirect("/Index");" the result will also be accurate. So again, what is "return Page()" doing? There is some kind of undocumented optimization that resets all the forms except the one recently submitted.
You have multiple separate forms on your page with separate form values: In one form you are submitting the client configuration object, in the other you are submitting the customer object.
So when you are actually submitting a form, only that form's data is being submitted. For example, if you are submitting the customer form, the client configuration data is not being transferred in the POST request (and the other way around).
As such, when you render the page by returning Page(), only the data that is currently in the page model is being rendered. If you are submitting the customer form, then only the customer data is available (same for the client configuration form).
This happens simply because you only have partial data on a page where you would need more to fill in all forms. If you want to prevent that, you will have to combine the data into a single model and form.
Now, if you refresh the page in the browser, then your browser is typically smart enough not to clear form values immediately. If you do a hard refresh using Ctrl + F5, then the browser should also reset the values.
It's also possible that your browser is performing an auto-fill for the forms here. This will typically only apply for GET requests. So that could be the reason why you are getting this result when you return a Redirect() because that completes the form POST with a GET request.
When I submit one form it resets the other.
That's the expected behavior for the way you coded your page. When the form POSTs to the server, the server does three things:
creates a new IndexModel object using its constructor,
binds the object's properties to the POSTed form values, and
binds the object to its view.
In your code, step (1) resets properties to their default values. Step (2) overwrites those defaults with POSTed form values. Since you're submitting only one form, the other form's values retain their defaults. That's why submitting one resets the other.
So what is "return Page();" doing? According to the documentation it is simply rendering the current page. Not true. To verify this, simply refresh the page. It will be different, accurate with both forms filled in. Also if you replace "return Page()" with "return Redirect("/Index");" the result will also be accurate.
When you submit a form, return Page() renders the page in the context of a POST. On the other hand, when you refresh or redirect, the context is a GET. The difference you see happens because the context is different: the response to a POST is different from the response to a GET.
Right. After quite a long time of pondering this problem, I've finally figured it out. The problem: Razor pages moves in mysterious ways its wonders to perform.
My initial assumption was wrong. The page model constructor is not being bypassed. The page model is being properly constructed from static values. However after construction all bound objects on the page are reset. So this is not an "undocumented optimization"...it is an undocumented impairment.
The fix for this is to reset the page model from static values before returning Page().
public async Task<IActionResult> OnPostCustomerAsync()
{
Customer.Instance = Customer;
ClientConfig = ClientConfig.Instance;
return Page();
}
public async Task<IActionResult> OnPostClientConfigurationAsync()
{
ClientConfig.Instance = ClientConfig;
Customer = Customer.Instance;
return Page();
}
This is obviously a massive kluge, but no elegant solution exists. Anyone?

Adding a WEB API method ruins my SWAGGER UI

This first method is fine. But when I add the second method the body of the SWAGGER UI is a bunch of html gibberish. And I creating the route the wrong way?
// GET api/checklist/1288
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
var model = _checkListService.Get(id);
return Ok(model);
}
// http://localhost:64783/api/checklist/GetDelinquentItems?id=1288
[Route("GetDelinquentItems")]
public async Task<IActionResult> GetDelinquentItems(int id)
{
var model = _checkListService.GetDelinquentItems(id);
return Ok(model);
}
That 'html gibberish' (indeed not the most elegant way to show an error) still contains some useful information. The first line says:
500 internal server error
and in the last three lines you can read:
Ambiguos HTTP method for action...CheckListController.GetDelinquentItems... Actions require explicit HttpMethod binding for Swagger
therefore another
[HttpGet("{id}")]
before the GetDelinquentItems() method should solve the problem.

Validation messages from custom model validation attributes are locked to first loaded language

I am working on a multi lingual website using Umbraco 7.2.4 (.NET MVC 4.5). I have pages for each language nested under home nodes with their own culture:
Home (language selection)
nl-BE
some page
some other page
my form page
fr-BE
some page
some other page
my form page
The form model is decorated with validation attributes that I needed to translate for each language. I found a Github project, Umbraco Validation Attributes that extends decoration attributes to retrieve validation messages from Umbraco dictionary items. It works fine for page content but not validation messages.
The issue
land on nl-BE/form
field labels are shown in dutch (nl-BE)
submit invalid form
validation messages are shown in dutch (nl-BE culture)
browse to fr-BE/form
field labels are shown in french (fr-BE)
submit invalid form
Expected behavior is: validation messages are shown in french (fr-BE culture)
Actual behavior is: messages are still shown in dutch (data-val-required attribute is in dutch in the source of the page)
Investigation to date
This is not a browser cache issue, it is reproducible across separate browsers, even separate computers: whoever is generating the form for the first time will lock the validation message culture. The only way to change the language of the validation messages is to recycle the Application Pool.
I doubt that the Umbraco Validation helper class is the issue here but I'm out of ideas, so any insight is appreciated.
Source code
Model
public class MyFormViewModel : RenderModel
{
public class PersonalDetails
{
[UmbracoDisplayName("FORMS_FIRST_NAME")]
[UmbracoRequired("FORMS_FIELD_REQUIRED_ERROR")]
public String FirstName { get; set; }
}
}
View
#inherits Umbraco.Web.Mvc.UmbracoTemplatePage
var model = new MyFormViewModel();
using (Html.BeginUmbracoForm<MyFormController>("SubmitMyForm", null, new {id = "my-form"}))
{
<h3>#LanguageHelper.GetDictionaryItem("FORMS_HEADER_PERSONAL_DETAILS")</h3>
<div class="field-wrapper">
#Html.LabelFor(m => model.PersonalDetails.FirstName)
<div class="input-wrapper">
#Html.TextBoxFor(m => model.PersonalDetails.FirstName)
#Html.ValidationMessageFor(m => model.PersonalDetails.FirstName)
</div>
</div>
note: I have used the native MVC Html.BeginForm method as well, same results.
Controller
public ActionResult SubmitFranchiseApplication(FranchiseFormViewModel viewModel)
{
if (!ModelState.IsValid)
{
TempData["Message"] = LanguageHelper.GetDictionaryItem("FORMS_VALIDATION_FAILED_MESSAGE");
foreach (ModelState modelState in ViewData.ModelState.Values)
{
foreach (ModelError error in modelState.Errors)
{
TempData["Message"] += "<br/>" + error.ErrorMessage;
}
}
return RedirectToCurrentUmbracoPage();
}
}
LanguageHelper
public class LanguageHelper
{
public static string CurrentCulture
{
get
{
return UmbracoContext.Current.PublishedContentRequest.Culture.ToString();
// I also tried using the thread culture
return System.Threading.Thread.CurrentThread.CurrentCulture.ToString();
}
}
public static string GetDictionaryItem(string key)
{
var value = library.GetDictionaryItem(key);
return string.IsNullOrEmpty(value) ? key : value;
}
}
So I finally found a workaround. In attempt to reduce my app to its simplest form and debug it, I ended up recreating the "UmbracoRequired" decoration attribute. The issue appeared when ErrorMessage was set in the Constructor rather than in the GetValidationRules method. It seems that MVC is caching the result of the constructor rather than invoking it again every time the form is loaded. Adding a dynamic property to the UmbracoRequired class for ErrorMessage also works.
Here's how my custom class looks like in the end.
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter,
AllowMultiple = false)]
internal class LocalisedRequiredAttribute : RequiredAttribute, IClientValidatable
{
private string _dictionaryKey;
public LocalisedRequiredAttribute(string dictionaryKey)
{
_dictionaryKey = dictionaryKey;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
ModelMetadata metadata, ControllerContext context)
{
ErrorMessage = LanguageHelper.GetDictionaryItem(_dictionaryKey); // this needs to be set here in order to refresh the translation every time
yield return new ModelClientValidationRule
{
ErrorMessage = this.ErrorMessage, // if you invoke the LanguageHelper here, the result gets cached and you're locked to the current language
ValidationType = "required"
};
}
}

Persisting MVC4 controller data thru multiple post backs

I have a MVC4 controller that calls its view multiple times, each time with a different set of ViewBags. The view renders the contents of a Form based on the absence or presence of those ViewBags via something like this:
#using (Html.BeginForm())
{
#if (ViewBag.Foo1 != null)
{
#Html.DropDownList("Bar1",....
}
#if (ViewBag.Foo2 != null)
{
#Html.DropDownList("Bar2",....
}
<input name="ActionButton" type="submit" value="Ok"" />
}
Each time the user clicks the submit button, the controller checks to see what is in the collection and makes a new set of ViewBags before calling the view again, sort of like this:
public ActionResult Create()
{
ViewBag.Foo1 = "blawblaw";
return View();
}
[HttpPost]
public ActionResult Create(FormCollection collection)
{
if (collection["Bar1"] != null)
{
string FirstPass = collection["Bar1"];
ViewBag.Foo2 = "blawblaw";
}
if (collection["Bar2"] != null)
{
string SecondPass = collection["Bar2"];
ViewBag.Foo3 = "blawblaw";
}
return View();
}
What I need to do now is somehow have each pass thu the controller 'remember' something about its previous passes. That is, in my example, the second pass thru the controller (the one where collection["Bar2"] is true), the value of FirstPass is null.
How can I do that?
In that case have a look at best practices for implementing a wizard in MVC. Some good suggestions here. Personally I would still consider using separate and distinct urls. Also, If you have db access in your solution you can still store temporary data before updating the main model. Think about what you want to happen if the user doesn't complete the whole journey the first time round...