Author Archives: Shahed C

Validation in ASP .NET Core 3.1

By Shahed C on June 15, 2020

This is the twenty-second of a new series of posts on ASP .NET Core 3.1 for 2020. In this series, we’ll cover 26 topics over a span of 26 weeks from January through June 2020, titled ASP .NET Core A-Z! To differentiate from the 2019 series, the 2020 series will mostly focus on a growing single codebase (NetLearner!) instead of new unrelated code snippets week.

Previous post:

NetLearner on GitHub:

In this Article:

V is for Validation

To build upon a previous post on Forms and Fields in ASP .NET Core, this post covers Validation in ASP .NET Core. When a user submits form field values, proper validation can help build a more user-friendly and secure web application. Instead of coding each view/page individually, you can simply use server-side attributes in your models/viewmodels.

NOTE: As of ASP .NET Core 2.2, validation may be skipped automatically if ASP .NET Core decides that validation is not needed. According to the “What’s New” release notes, this includes primitive collections (e.g. a byte[] array or a Dictonary<string, string> key-value pair collection)

Validation in ASP .NET Core

Validation Attributes

To implement model validation with [Attributes], you will typically use Data Annotations from the System.ComponentModel.DataAnnotations namespace. The list of attribute does go beyond just validation functionality though. For example, the DataType attribute takes a datatype parameter, used for inferring the data type and used for displaying the field on a view/page (but does not provide validation for the field).

Common attributes include the following

  • Range: lets you specify min-max values, inclusive of min and max
  • RegularExpression: useful for pattern recognition, e.g. phone numbers, zip/postal codes
  • Required: indicates that a field is required
  • StringLength: sets the maximum length for the string entered
  • MinLength: sets the minimum length of an array or string data

From the sample code, here is an example from the LearningResource model class in NetLearner‘s shared library:

public class LearningResource
{
    public int Id { get; set; }

    [DisplayName("Resource")]
    [Required]
    [StringLength(100)]
    public string Name { get; set; }


    [DisplayName("URL")]
    [Required]
    [StringLength(255)]
    [DataType(DataType.Url)]
    public string Url { get; set; }

    public int ResourceListId { get; set; }
    [DisplayName("In List")]
    public ResourceList ResourceList { get; set; }

    [DisplayName("Feed Url")]
    public string ContentFeedUrl { get; set; }

    public List<LearningResourceTopicTag> LearningResourceTopicTags { get; set; }
}

From the above code, you can see that:

  • The value for Name is a required string, needs to be less than 100 characters
  • The value for Url is a required string, needs to be less than 255 characters
  • The value for ContentFeedUrl can be left blank, but has to be less than 255 characters.
  • When the DataType is provided (e.g. DataType.Url, Currency, Date, etc), the field is displayed appropriately in the browser, with the proper formatting
  • For numeric values, you can also use the [Range(x,y)] attribute, where x and y sets the minimum and maximum values allowed for the number

Here’s what it looks like in a browser when validation fails:

Validation errors in NetLearner.MVC
Validation errors in NetLearner.Pages
Validation errors in NetLearner.Blazor

The validation rules make it easier for the user to correct their entries before submitting the form.

  • In the above scenario, the “is required” messages are displayed directly in the browser through client-side validation.
  • For field-length restrictions, the client-side form will automatically prevent the entry of string values longer than the maximum threshold
  • If a user attempts to circumvent any validation requirements on the client-side, the server-side validation will automatically catch them.

In the MVC and Razor Pages web projects, the validation messages are displayed with the help of <div> and <span> elements, using asp-validation-summary and asp-validation-for.

NetLearner.Mvc: /Views/LearningResources/Create.cshtml

<div asp-validation-summary="ModelOnly" class="text-danger"></div>
 <div class="form-group">
     <label asp-for="Name" class="control-label"></label>
     <input asp-for="Name" class="form-control" />
     <span asp-validation-for="Name" class="text-danger"></span>
 </div>

NetLearner.Pages: /Pages/LearningResources/Create.cshtml

<div asp-validation-summary="ModelOnly" class="text-danger"></div>
 <div class="form-group">
     <label asp-for="LearningResource.Name" class="control-label"></label>
     <input asp-for="LearningResource.Name" class="form-control" />
     <span asp-validation-for="LearningResource.Name" class="text-danger"></span>
 </div>

In the Blazor project, the “The DataAnnotationsValidator component attaches validation support using data annotations” and “The ValidationSummary component summarizes validation messages”.

NetLearner.Blazor: /Pages/ResourceDetail.razor

<EditForm Model="@LearningResourceObject" OnValidSubmit="@HandleValidSubmit">
     <DataAnnotationsValidator />
     <ValidationSummary />

For more information on Blazor validation, check out the official documentation at:

Server-Side Validation

Validation occurs before an MVC controller action (or equivalent handler method for Razor Pages) takes over. As a result, you should check to see if the validation has passed before continuing next steps.

e.g. in an MVC controller

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(...)
{
   if (ModelState.IsValid)
   {
      // ... 
      return RedirectToAction(nameof(Index));
   }
   return View(...);
}

e.g. in a Razor Page’s handler code:

public async Task<IActionResult> OnPostAsync()
{
   if (!ModelState.IsValid)
   {
      return Page();
   }

   //... 
   return RedirectToPage(...);
}

Note that ModelState.IsValid is checked in both the Create() action method of an MVC Controller or the OnPostAsync() handler method of a Razor Page’s handler code. If IsValid is true, perform actions as desired. If false, reload the current view/page as is.

In the Blazor example, the OnValidSubmit event is triggered by <EditForm> when a form is submitted, e.g.

<EditForm Model="@SomeModel" OnValidSubmit="@HandleValidSubmit">

The method name specified refers to a C# method that handles the form submission when valid.

private async void HandleValidSubmit()
{
   ...
}

Client-Side Validation

It goes without saying that you should always have server-side validation. All the client-side validation in the world won’t prevent a malicious user from sending a GET/POST request to your form’s endpoint. Cross-site request forgery in the Form tag helper does provide a certain level of protection, but you still need server-side validation. That being said, client-side validation helps to catch the problem before your server receives the request, while providing a better user experience.

When you create a new ASP .NET Core project using one of the built-in templates for MVC or Razor Pages, you should see a shared partial view called _ValidationScriptsPartial.cshtml. This partial view should include references to jQuery unobtrusive validation, as shown below:

<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>

If you create a scaffolded controller with views/pages, you should see the following reference at the bottom of your page or view.

e.g. at the bottom of Create.cshtml view

@section Scripts {
   @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

e.g. at the bottom of the Create.cshtml page

@section Scripts {
   @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Note that the syntax is identical whether it’s an MVC view or a Razor page. If you ever need to disable client-side validation for some reason, that can be accomplished in different ways, whether it’s for an MVC view or a Razor page. (Blazor makes use of the aforementioned EditForm element in ASP .NET Core to include built-in validation, with the ability to track whether a submitted form is valid or invalid.)

From the official docs, the following code should be used within the ConfigureServices() method of your Startup.cs class, to set ClientValidationEnabled to false in your HTMLHelperOptions configuration.

services.AddMvc().AddViewOptions(options =>
{
   if (_env.IsDevelopment())
   {
      options.HtmlHelperOptions.ClientValidationEnabled = false;
   }
});

Also mentioned in the official docs, the following code can be used for your Razor Pages, within the ConfigureServices() method of your Startup.cs class.

services.Configure<HtmlHelperOptions>(o => o.ClientValidationEnabled = false);

Client to Server with Remote Validation

If you need to call a server-side method while performing client-side validation, you can use the [Remote] attribute on a model property. You would then pass it the name of a server-side action method which returns an IActionResult with a true boolean result for a valid field. This [Remote] attribute is available in the Microsoft.AspNetCore.Mvc namespace, from the Microsoft.AspNetCore.Mvc.ViewFeatures NuGet package.

The model property would look something like this:

[Remote(action: "MyActionMethod", controller: "MyControllerName")]
public string MyProperty { get; set; }

In the controller class, (e.g. MyControllerName), you would define an action method with the name specified in the [Remote] attribute parameters, e.g. MyActionMethod. 

[AcceptVerbs("Get", "Post")]
public IActionResult MyActionMethod(...)
{
   if (TestForFailureHere())
   {
      return Json("Invalid Error Message");
   }
   return Json(true);
}

You may notice that if the validation fails, the controller action method returns a JSON response with an appropriate error message in a string. Instead of a text string, you can also use a false, null, or undefined value to indicate an invalid result. If validation has passed, you would use Json(true) to indicate that the validation has passed.

So, when would you actually use something like this? Any scenario where a selection/entry needs to be validated by the server can provide a better user experience by providing a result as the user is typing, instead of waiting for a form submission. For example: imagine that a user is buying online tickets for an event, and selecting a seat number displayed on a seating chart. The selected seat could then be displayed in an input field and then sent back to the server to determine whether the seat is still available or not.

Custom Attributes

In addition to all of the above, you can simply build your own custom attributes. If you take a look at the classes for the built-in attributes, e.g. RequiredAttribute, you will notice that they also extend the same parent class:

  • System.ComponentModel.DataAnnotations.ValidationAttribute

You can do the same thing with your custom attribute’s class definition:

public class MyCustomAttribute: ValidationAttribute 
{
   // ...
}

The parent class ValidationAttribute, has a virtual IsValid() method that you can override to return whether validation has been calculated successfully (or not).

public class MyCustomAttribute: ValidationAttribute 
{
   // ...
   protected override ValidationResult IsValid(
      object value, ValidationContext validationContext)
   {
      if (TestForFailureHere())
      {
         return new ValidationResult("Invalid Error Message");
      }
      
      return ValidationResult.Success;
   }
}

You may notice that if the validation fails, the IsValid() method returns a ValidationResult() with an appropriate error message in a string. If validation has passed, you would return ValidationResult.Success to indicate that the validation has passed.

References

Unit Testing in ASP .NET Core 3.1

By Shahed C on May 25, 2020

This is the twenty-first of a new series of posts on ASP .NET Core 3.1 for 2020. In this series, we’ll cover 26 topics over a span of 26 weeks from January through June 2020, titled ASP .NET Core A-Z! To differentiate from the 2019 series, the 2020 series will mostly focus on a growing single codebase (NetLearner!) instead of new unrelated code snippets week.

Previous post:

NetLearner on GitHub:

In this Article:

U is for Unit testing

Whether you’re practicing TDD (Test-Driven Development) or writing your tests after your application code, there’s no doubt that unit testing is essential for web application development. When it’s time to pick a testing framework, there are multiple alternatives such as xUnit.netNUnit and MSTest. This article will focus on xUnit.net because of its popularity (and similarity to its alternatives) when compared to the other testing frameworks.

In a nutshell: a unit test is code you can write to test your application code. Your web application will not have any knowledge of your test project, but your test project will need to have a dependency of the app project that it’s testing.

Unit Testing Project Dependencies
Unit Testing Project Dependencies

Here are some poll results, from asking 500+ developers about which testing framework they prefer, showing xUnit.net in the lead (from May 2019).

similar poll on Facebook also showed xUnit.net leading ahead of other testing frameworks. If you need to see the equivalent attributes and assertions, check out the comparison table provided by xUnit.net:

To follow along, take a look at the test projects on Github:

Setting up Unit Testing

The quickest way to set up unit testing for an ASP .NET Core web app project is to create a new test project using a template. This creates a cross-platform .NET Core project that includes one blank test. In Visual Studio 2019, search for “.net core test project” when creating a new project to identify test projects for MSTest, XUnit and NUnit. Select the XUnit project to follow along with the NetLearner samples.

Test Project Templates in Visual Studio 2019
Test Project Templates in Visual Studio 2019

The placeholder unit test class includes a blank test. Typically, you could create a test class for each application class being tested. The simplest unit test usually includes three distinct steps: Arrange, Act and Assert.

  1. Arrange: Set up the any variables and objects necessary.
  2. Act: Call the method being tested, passing any parameters needed
  3. Assert: Verify expected results

The unit test project should have a dependency for the app project that it’s testing. In the test project file NetLearner.Mvc.Tests.csproj, you’ll find a reference to NetLearner.Mvc.csproj.

... 
<ItemGroup>
   <ProjectReference Include="..\NetLearner.Mvc\NetLearner.Mvc.csproj" />
</ItemGroup>
...

In the Solution Explorer panel, you should see a project dependency of the reference project.

Project Reference in Unit Testing Project
Project Reference in Unit Testing Project

If you need help adding reference projects using CLI commands, check out the official docs at:

Facts, Theories and Inline Data

When you add a new xUnit test project, you should get a simple test class (UnitTest1) with an empty test method (Test1). This test class should be a public class and the test method should be decorated with a [Fact] attribute. The attribute indicates that this is a test method without any parameters, e.g. Test1().

public class UnitTest1
{
   [Fact]
   public void Test1()
   {
      
   }
}

In the NetLearner Shared Library test project, you’ll see a test class (ResourceListServiceTests.cs) with a series of methods that take 1 or more parameters. Instead of a [Fact] attribute, each method has a [Theory] attribute. In addition to this primary attribute, each [Theory] attribute is followed by one of more [InlineData] attributes that have sample argument values for each method parameter.

[Theory(DisplayName = "Add New Resource List")]
[InlineData("RL1")]
public async void TestAdd(string expectedName)
{
   ...
}

In the code sample, each occurrence of [InlineData] should reflect the test method’s parameters, e.g.

  • [InlineData(“RL1”)] –> this implies that expectedName = “RL1”

NOTE: If you want to skip a method during your test runs, simply add a Skip parameter to your Fact or Theory with a text string for the “Reason”.

e.g.

  • [Fact(Skip=”this is broken”)]
  • [Theory(Skip=”we should skip this too”)]

Asserts and Exceptions

Back to the 3-step process, let’s explore the TestAdd() method and its method body.

public async void TestAdd(string expectedName)
{
    var options = new DbContextOptionsBuilder<LibDbContext>()
        .UseInMemoryDatabase(databaseName: "TestNewListDb").Options;

    // Set up a context (connection to the "DB") for writing
    using (var context = new LibDbContext(options))
    {
        // 1. Arrange
        var rl = new ResourceList
        {
            Name = "RL1"
        };

        // 2. Act 
        var rls = new ResourceListService(context);
        await rls.Add(rl);
    }

    using (var context = new LibDbContext(options))
    {
        var rls = new ResourceListService(context);
        var result = await rls.Get();

        // 3. Assert
        Assert.NotEmpty(result);
        Assert.Single(result);
        Assert.NotEmpty(result.First().Name);
        Assert.Equal(expectedName, result.First().Name);
    }
}

  1. During the Arrange step, we create a new instance of an object called ResourceList which will be used during the test.
  2. During the Act step, we create a ResourceListService object to be tested, and then call its Add() method to pass along a string value that was assigned via InlineData.
  3. During the Assert step, we compare the expectedName (passed by InlineData) with the returned result (obtained from a call to the Get method in the service being tested).

The Assert.Equal() method is a quick way to check whether an expected result is equal to a returned result. If they are equal, the test  method will pass. Otherwise, the test will fail. There is also an Assert.True() method that can take in a boolean value, and will pass the test if the boolean value is true.

For a complete list of Assertions in xUnit.net, refer to the Assertions section of the aforementioned comparison table:

If an exception is expected, you can assert a thrown exception. In this case, the test passes if the exception occurs. Keep in mind that unit tests are for testing expected scenarios. You can only test for an exception if you know that it will occur, e.g.

Exception ex = Assert
    .Throws<SpecificException>(() => someObject.MethodBeingTested(x, y));

The above code tests a method named MethodBeingTested() for someObject being tested. A SpecificException() is expected to occur when the parameter values x and y are passed in. In this case, the Act and Assert steps occur in the same statement.

NOTE: There are some differences in opinion whether or not to use InMemoryDatabase for unit testing. Here are some viewpoints from .NET experts Julie Lerman (popular Pluralsight author) and Nate Barbettini (author of the Little ASP .NET Core book):

Running Tests

To run your unit tests in Visual Studio, use the Test Explorer panel.

  1. From the top menu, click Test | Windows | Test Explorer
  2. In the Test Explorer panel, click Run All
  3. Review the test status and summary
  4. If any tests fail, inspect the code and fix as needed.
Test Explorer in VS2019
Test Explorer in VS2019

To run your unit tests with a CLI Command, run the following command in the test project folder:

> dotnet test

The results may look something like this:

As of xUnit version 2, tests can automatically run in parallel to save time. Test methods within a class are considered to be in the same implicit collection, and so will not be run in parallel. You can also define explicit collections using a [Collection] attribute to decorate each test class. Multiple test classes within the same collection will not be run in parallel.

For more information on collections, check out the official docs at:

NOTE: Visual Studio includes a Live Unit Testing feature that allows you to see the status of passing/failing tests as you’re typing your code. This feature is only available in the Enterprise Edition of Visual Studio.

Custom Names and Categories

You may have noticed a DisplayName parameter when defining the [Theory] attribute in the code samples. This parameter allows you to defined a friendly name for any test method (Fact or Theory)  that can be displayed in the Test Explorer. For example:

[Theory(DisplayName = "Add New Learning Resource")]

Using the above attribute above the TestAdd() method will show the friendly name “Add New Learning Resource” in the Test Explorer panel during test runs.

Unit Test with custom DisplayName
Unit Test with custom DisplayName

Finally, consider the [Trait] attribute. This attribute can be use to categorize related test methods by assigning an arbitrary name/value pair for each defined “Trait”. For example (from LearningResource and ResourceList tests, respectively):

[Trait("Learning Resource Tests", "Adding LR")]
public void TestAdd() { ... }

[Trait("Resource List Tests", "Adding RL")]
public void TestAdd { ... }

Using the above attribute for the two TestAdd() methods will categorize the methods into their own named “category”, e.g. Learning Resource Tests and Resource List Tests. This makes it possible to filter just the test methods you want to see, e.g. Trait: “Adding RL”

Filtering Unit Tests by Trait Values
Filtering Unit Tests by Trait Values

Next Steps: Mocking, Integration Tests and More!

There is so much more to learn with unit testing. You could read several chapters or even an entire book on unit testing and related topics. To continue your learning in this area, consider the following:

  • MemberData: use the MemberData attribute to go beyond isolated test methods. This allows you to reuse test values for multiples methods in the test class.
  • ClassData: use the ClassData attribute to use your test data in multiple test classes. This allows you to specify a class that will pass a set of collections to your test method.

For more information on the above, check out this Nov 2017 post from Andrew Lock:

To go beyond Unit Tests, consider the following:

  • Mocking: use a mocking framework (e.g. Moq) to mock external dependencies that you shouldn’t need to test from your own code.
  • Integration Tests: use integration tests to go beyond isolated unit tests, to ensure that multiple components of your application are working correctly. This includes databases and file systems.
  • UI Tests: test your UI components using a tool such as Selenium WebDriver or IDE in the language of your choice, e.g. C#. For browser support, you may use Chrome or Firefox extensions, so this includes the new Chromium-based Edge browser.

While this article only goes into the shared library, the same concepts carry over into the testing of each individual web app project (MVC, Razor Pages and Blazor). Refer to the following documentation and blog content for each:

Refer to the NetLearner sample code for unit tests for each web project:

In order to set up a shared service object to be used by the controller/page/component being tested, Moq is used to mock the service. For more information on Moq, check out their official documentation on GitHub:

For the Blazor testing project, the following references were consulted:

NOTE: Due to differences between bUnit beta 6 and 7, there are some differences between the Blazor guide and the NetLearner tests on Blazor. I started off with the Blazor guide, but made some notable changes.

  1. Instead of starting with a Razor Class Library template for the test project, I started with the xUnit Test Project template.
  2. There was no need to change the test project’s target framework from .NET Standard to .NET Core 3.1 manually, since the test project template was already Core 3.1 when created.
  3. As per the bUnit guidelines, the test class should no longer be derived from the ComponentTestFixture class, which is now obsolete: https://github.com/egil/bunit/blob/6c66cc2c77bc8c25e7a2871de9517c2fbe6869dd/src/bunit.web/ComponentTestFixture.cs
  4. Instead, the test class is now derived from the TestContext class, as seen in the bUnit source code: https://github.com/egil/bunit/blob/6c66cc2c77bc8c25e7a2871de9517c2fbe6869dd/src/bunit.web/TestContext.cs

References

Tag Helper Authoring in ASP .NET Core 3.1

By Shahed C on May 18, 2020

This is the twentieth of a new series of posts on ASP .NET Core 3.1 for 2020. In this series, we’ll cover 26 topics over a span of 26 weeks from January through June 2020, titled ASP .NET Core A-Z! To differentiate from the 2019 series, the 2020 series will mostly focus on a growing single codebase (NetLearner!) instead of new unrelated code snippets week.

Previous post:

NetLearner on GitHub:

NOTE: The NetLearner suite of apps doesn’t currently use custom tag helpers in the main branch, so you can check out the new sample code in the experimental subfolder, merged from a branch:

In this Article:

T is for Tag Helper Authoring

Tag Helpers are very useful for ASP .NET Core developers in creating HTML elements with server-side attributes. They work equally well in both Razor Pages and MVC views. Better yet, the syntax allows a front-end developer to easily customize the UI, with HTML/CSS knowledge.

If you need a refresher on built-in tag helpers in ASP .NET Core, you may revisit an earlier post in this series:

Tag Helpers in ASP .NET Core
Tag Helpers in ASP .NET Core

Authoring your own tag helpers is as easy as implementing the ITagHelper interface. To make things easier, the TagHelper class (which already implements the aforementioned interface) can be extended to build your custom tag helpers.

Custom Tag Helpers

As with most concepts introduced in ASP .NET Core, it helps to use named folders and conventions to ease the development process. In the case of Tag Helpers, you should start with a “TagHelpers” folder at the root-level of your project for your convenience. You can save your custom tag helper classes in this folder.

This blog post and its corresponding code sample builds upon the official tutorial for authoring tag helpers. While the official tutorial was originally written to cover instructions for MVC views, this blog post takes a look at a Razor Page example. The creation of Tag Helpers involves the same process in either case. Let’s start with the synchronous and asynchronous versions of a Tag Helper that formats email addresses.

The class EmailTagHelper.cs defines a tag helper that is a subclass of the TagHelper class, saved in the “TagHelpers” folder. It contains a Process() method that changes the output of the HTML tag it is generating.

public class EmailTagHelper : TagHelper
{
   ...
   // synchronous method, CANNOT call output.GetChildContentAsync();
   public override void Process(TagHelperContext context, TagHelperOutput output)
   {
      // ...
   } 
} 

The class AsyncEmailTagHelper.cs defines a tag helper that is also a subclass of the TagHelper class, saved in the aforementioned “TagHelpers” folder. It contains a ProcessAsync() method, which has a different signature (returns Task object instead of void) and grabs the child content from the output using output.GetChildContentAsync();

public class AsyncEmailTagHelper : TagHelper
{
   ...
   // ASYNC method, REQUIRED to call output.GetChildContentAsync();
   public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
   {
      // ... 
   }
}

In order to use the tag helper in a Razor Page, simply add a using statement for the Tag Helper’s namespace, and then include a custom HTML tag that has the same name as the Tag Helper’s class name (without the TagHelper suffix). For the Email and AsyncEmail Tag Helpers, the corresponding tags in your Razor Page would be <email> and <async-email> respectively.

In the EmailTester.cshtml page:

<email mail-to="Black.Widow"></email>

In the AsyncEmailTester.cshtml page:

<async-email>Black.Widow</async-email>

Note that the PascalCase capitalization in the class name corresponds to a lowercase tag in kebab-case. In a browser, the end result includes a clickable email link from the Razor Pages. Both the non-async and async version of the methods produce similar end results.

Email tag helper in a browser
Async Email tag helper in a browser
Async Email tag helper in a browser

Setting Attributes and Content

So how does the Process() method convert your custom tags into valid HTML tags? It does that in a series of steps.

  1. Set the HTML element as the tag name to replace it with, e.g. <a>
  2. Set each attribute within that HTML element, e.g. href
  3. Set HTML Content within the tags.

The process involved is slightly different between the synchronous and asynchronous versions of the Process method. In the synchronous EmailTagHelper.cs class, the Process() method does the following:

// 1. Set the HTML element
output.TagName = "a"; 

// 2. Set the href attribute
output.Attributes.SetAttribute("href", "mailto:" + address);

// 3. Set HTML Content
output.Content.SetContent(address);

In the asynchronous AsyncEmailTagHelper.cs class, the ProcessAsync() method does the following:

// 1. Set the HTML element
output.TagName = "a"; 

var content = await output.GetChildContentAsync();
var target = content.GetContent() + "@" + EmailDomain;

// 2. Set the href attribute within that HTML element, e.g. href
output.Attributes.SetAttribute("href", "mailto:" + target);

// 3. Set HTML Content
output.Content.SetContent(target);

The difference between the two is that the async method gets the output content asynchronously with some additional steps. Before setting the attribute in Step 2, it grabs the output content from GetChildContentAsync() and then uses content.GetContent() to extract the content before setting the attribute with output.Attributes.SetAttribute(). 

Updating Pre/Post Content

This section recaps the BoldTagHelper explained in the docs tutorial, by consolidating all the lessons learned. In the BoldTagHelper.cs class from the sample, you can see the following code:

[HtmlTargetElement("bold")]
[HtmlTargetElement(Attributes = "bold")]
public class BoldTagHelper : TagHelper
{
   public override void Process(TagHelperContext context, TagHelperOutput output)
   {
      output.Attributes.RemoveAll("bold");
      output.PreContent.SetHtmlContent("<strong>");
      output.PostContent.SetHtmlContent("</strong>");
   }
}

Let’s go over what the code does, line by line:

  • The [HtmlTargetElement] attribute forces a Tag Helper to target a specific element, e.g. [HtmlTargetElement(“bold”)], which will target a <bold> tag in a Razor Page or MVC View.
  • When one or more attributes are specified, e.g. [HtmlTargetElement(Attributes = “bold”)], the Tag Helper targets a bold attribute within an element, e.g. <p bold>
  • Combining the above one after the other gives you an OR condition, in which either scenario can be matched.
  • Combining them in a single [HtmlTargetElement] creates an AND condition, which would match a bold tag with a bold attribute, which is not very useful, e.g. [HtmlTargetElement(“bold”, Attributes = “bold”)]

Here is a snippet the corresponding Razor Page for testing the above scenario, BoldTester.cshtml:

<p bold>This paragraph uses a P tag with a bold attribute. 
Using a tag helper, the entire paragraph should be displayed with bold text.</p>

<bold>This statement uses a custom bold tag to be displayed in bold.</bold>

The tag helper affects both fragments, as seen in the screenshot below:

Bold tag helper in a browser
Bold tag helper in a browser

The statements in the Process() method accomplish the following:

  • The RemoveAll() method from output.Attributes removes the “bold” attribute within the tag, as it is essentially acting as a placeholder.
  • The SetHtmlContent() from output.PreContent adds an opening <strong> tag  inside the enclosing element, i.e. just after <p> or <bold>
  • The SetHtmlContent() from output.Postontent adds a closing </strong> tag  insidethe enclosing element, i.e. just before </p> or </bold>

Passing Complex Objects

What if you want to pass a more complex object, with properties and objects within it? This can be done by defining a C# model class, e.g. SuperheroModel.cs, that can be initialized inside in the Page Model class (SuperheroInfoTester.cshtml.cs) and then used in a Razor Page (SuperheroInfoTester.cshtml). The tag helper (SuperheroTagHelper.cs) then brings it all together by replacing <superhero> tags with whatever SuperHeroModel info is passed in.

Let’s take a look at all its parts, and how it all comes together.

Object ModelSuperheroModel.cs

public class SuperheroModel
{
   public string LastName { get; set; }
   public string FirstName { get; set; }
   public string SuperName { get; set; }
   public bool HasSurvived { get; set; }

   public bool ShowInfoWithSpoilers { get; set; }
}

Razor PageSuperheroInfoTester.cshtml

@page
@model SuperheroInfoTesterModel

... 

<h3>Black Widow Info:</h3>
<div condition="@Model.blackWidowInfo.ShowInfoWithSpoilers">
 <superhero hero-info="Model.blackWidowInfo" />
</div>
...

Page Model for Razor PageSuperheroInfoTester.cshtml.cs

public class SuperheroInfoTesterModel : PageModel
{
   public SuperheroModel blackWidowInfo { get; set; }
   // ...

   public void OnGet()
   {
      blackWidowInfo = new SuperheroModel
      {
         // ...
      }
      // ...
   }
}

Superhero Tag HelperSuperheroTagHelper.cs

public class SuperheroTagHelper : TagHelper
{
   public SuperheroModel HeroInfo { get; set; }

   public override void Process(TagHelperContext context, TagHelperOutput output)
   {
     // ...
   }
}

Going through the above code:

  1. The tag helper is named SuperheroTagHelper, implying that it can be used for <superhero> tags in a Razor Page, e.g. SuperHeroInfoTester.cshtml
  2. The tag helper also contains a SuperheroModel object called HeroInfo, which allows a hero-info attribute, i.e. <superhero hero-info=”Model.property”>
  3. The SuperheroModel class contains various public properties that provide information about a specific superhero.
  4. The SuperHeroInfoTesterModel page model class includes an OnGet() method that initializes multiple SuperheroModel objects, to be displayed in the Razor Page.

Inside the tag helper, the Process() method takes care of replacing the <superhero> tag with a <section> tag:

public override void Process(TagHelperContext context, TagHelperOutput output)
{
   string htmlContent = $@"<ul><li><strong>First Name:</strong> {HeroInfo.FirstName}</li>
<li><strong>Last Name:</strong> {HeroInfo.LastName}</li>
<li><strong>Superhero Name:</strong> {HeroInfo.SuperName}</li>
<li><strong>Survived Endgame? </strong> {HeroInfo.HasSurvived}</li></ul>";
 
   output.TagName = "section";
   output.Content.SetHtmlContent(htmlContent);
   output.TagMode = TagMode.StartTagAndEndTag; 
}

After initializing some HTML content to display a <ul> list, the above code in the Process() method accomplishes the following:

  1. Set the HTML element as the tag name to replace it with, e.g. <section>
  2. Set HTML Content within the tags.
  3. Set Tag Mode to include both start and end tags, e.g. <section> … </section>

End Result in Browser:

Superhero tag helper in a browser
Superhero tag helper in a browser

In a web browser, you can see that that the <superhero> tag has been converted into a <section> tag with <ul> content.

Handling Conditions

When you want to handle a UI element in different ways based on certain conditions, you may use a ConditionTagHelper. In this case, a condition is used to determine whether spoilers for the popular movie Avengers: Endgame should be displayed or not. If the spoiler flag is set to false, the character’s info is not displayed at all.

@page
@model SuperheroInfoTesterModel
...
<div condition="@Model.blackWidowInfo.ShowInfoWithSpoilers">
 <superhero hero-info="Model.blackWidowInfo" />
</div>
...

In the above code from the SuperheroInfoTester.cshtml page:

  • the <div> includes a condition that evaluates a boolean value, e.g. Model.blackWidowInfo.ShowInfoWithSpoilers
  • the Model object comes from the @model defined at the top of the page
  • the boolean value of ShowInfoWithSpoilers determines whether the <div> is displayed or not.

References

SignalR in ASP .NET Core 3.1

By Shahed C on May 11, 2020

This is the nineteenth of a new series of posts on ASP .NET Core 3.1 for 2020. In this series, we’ll cover 26 topics over a span of 26 weeks from January through June 2020, titled ASP .NET Core A-Z! To differentiate from the 2019 series, the 2020 series will mostly focus on a growing single codebase (NetLearner!) instead of new unrelated code snippets week.

Previous post:

NetLearner on GitHub:

In this Article:

S is for SignalR

SignalR has been around for 7+ years now, allowing ASP .NET developers to easily include real-time features in their web applications. SignalR Core has been available in ASP .NET Core since v2.1, as a cross-platform solution to add real-time features to web apps and more!

In this article, we’ll go over SignalR concepts, using a new sample I developed to allow web users to vote in a real-time online poll. Before you begin, take a look at the sample code project on GitHub:

Back in 2018, I ran a couple of polls on Facebook and Twitter to see what the dev community wanted to see. On Twitter, the #1 choice was “Polling/Voting app” followed by “Planning Poker App” and “Real-time game”. On Facebook, the #1 choice was “Real-time game” followed by “Polling/voting app”. As a result, I decided to complement this article with a polling sample app.

More importantly, Brady Gaster suggested that the sample app should definitely be “Not. Chat.” 🙂

In the sample project, take a look at the SignalRPoll project to see how the polling feature has been implemented. In order to create a project from scratch, you’ll be using both server-side and client-side dependencies.

SignalR poll in action
SignalR poll in action

If you need a starter tutorial, check out the official docs:

Dependencies

Visual Studio showing csproj + dependencies
Visual Studio showing csproj + dependencies

The Server-Side dependencies for SignalR Core are available via the Microsoft.AspNetCore.App package so this is a freebie when you create a new 3.1 web app project. In your server-side code, you can use the following namespace:

using Microsoft.AspNetCore.SignalR;

This will give you access to SignalR classes such as Hub and Hub<T> for your SignalR hub to derive from. In the sample project, the PollHub class inherits from the Hub class. Hub<T> can be used for strongly-typed SignalR hubs.

The Client Side dependencies for SignalR Core have to be added manually. Simply right-click on your web app project and select Add | Client-Side Library. In the popup that appears, select a provider (such as “unpkg”) and enter a partial search term for Library, so that you can ideally pick the latest stable version.

Steps to add client library via LibMan (aka Library Manager):

  • Right-click project in Solution Explorer
  • Select Add | Client-Side Library

In the popup that appears, select/enter the following:

  • Provider: choose from cdnjs, filesystem, unpkg
  • Library search term: @aspnet/signalr@1… pick latest stable if desired
  • Files: At a minimum, choose specific files signalr.js and/or its minified equivalent
Visual Studio, showing libman.json with client-side references
Visual Studio, showing libman.json with client-side references

If you need help with adding client-side references, check out this earlier blog post in this A-Z series:

Server-Side Hub

In the sample app, the PollHub class has a simple SendMessage() method with a few parameters. Derived from the sample Chat application, it starts with the user’s desired “user” value and a custom “message” that can be passed to the SignalR Hub. For the the Captain Marvel/America poll, the method also passes an Id and Value for the selected radio button.

public class PollHub : Hub
{
   public async Task SendMessage(string user, string message, string myProjectId, string myProjectVal)
   {
      await Clients.All.SendAsync("ReceiveMessage", user, message, myProjectId, myProjectVal);
   }
}

To ensure that the SendMessage method from the server has a trigger on the client-side, the client-side code must invoke the method via the SignalR connection created with HubConnectionBuilder() on the client side. Once called, the above code will send a call to ReceiveMessage on all the clients connected to the Hub.

Client-Side

On the client-side, the JavaScript file poll.js handles the call from the browser to the server, and receives a response back from the server as well. The following code snippets highlight some important areas:

var connection = new signalR.HubConnectionBuilder().withUrl("/pollHub").build(); 
... 
connection.on("ReceiveMessage", function (user, message, myProjectId, myProjectVal) { 
   ... 
   document.getElementById(myProjectId + 'Block').innerHTML += chartBlock;
}); 
...

document.getElementById("sendButton").addEventListener("click", function (event) {
   ...
   connection.invoke("SendMessage", user, message, myProjectId, myProjectVal)
   ... 
});

The above snippets takes care of the following:

  1. Creates a new connection objection using HubConnectionBuilder with a designated route
  2. Uses connection.on to ensure that calls to ReceiveMessage come back from the server
  3. Sets the innerHTML of a <span> block to simulate a growing bar chart built with small blocks
  4. Listens for a click event from the sendButton element on the browser
  5. When the sendButton is clicked, uses connection.invoke() to call SendMessage on the server

Configuration

The configuration for the SignalR application is set up in the Startup.cs methods ConfigureServices() and Configure(), as you may expect.

public void ConfigureServices(IServiceCollection services) 
{
   ...
   services.AddSignalR();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
   ...
   app.UseEndpoints(endpoints =>
   {
      ... 
      endpoints.MapHub<PollHub>("/pollHub");
   });
   ...
}

The above code takes care of the following:

  1. the ConfigureServices() method adds SignalR to the ASP.NET Core dependency injection system with a call to AddSignalR()
  2. the Configure() method adds SignalR to the middleware pipeline, while setting up the necessary route(s), using a call to UseSignalR().

At the time of this writing, I have more than one route set up for multiple hubs. For the polling app, we only need the call to MapHub<PollHub>() that sets up the route “/pollHub“. You may recall this route from the client-side JavaScript code where the initial connection is set up.

For streaming fragments of data over time, you should also take a look at Streaming in SignalR Core:

Running the App

To run the app, simply run the SignalRPoll app Visual Studio or from the command line. Then, click the Poll item in the top menu to go to the Poll page. This page is a simple Razor page that contains all the HTML elements necessary to display the poll. It also includes <script> references to jQuery, SignalR and poll.js client-side references.

NOTE: Even though I am using jQuery for this sample, please note that jQuery is not required to use SignalR Core. On a related note, you can also configure Webpack and TypeScript for a TypeScript client if you want.

This GIF animation below illustrates the poll in action. To record this GIF of 1 browser window, I also launched additional browser windows (not shown) pointing to the same URL, so that I could vote several times.

SignalR poll in action
SignalR poll in action

In a real world scenario, there are various ways to prevent a user from voting multiple times. Some suggestions include:

  • Disable the voting button as soon as the user has submitted a vote.
  • Use a cookie to prevent the user from voting after reloading the page.
  • Use authentication to prevent a user from voting after clearing cookies or using a different browser.

For more information on authenticating and authorizing users, check out the official docs:

Azure SignalR Service

Azure SignalR Service is a fully-managed service available in Microsoft’s cloud-hosted Azure services, that allows you to add real-time functionality and easily scale your apps as needed. Using Azure SignalR Service is as easy as 1-2-3:

  1. Add a reference to the Azure SignalR Service SDK
  2. Configure a connection string
  3. Call services.AddSignalR().AddAzureSignalR() and app.UseAzureSignalR in Startup.cs

For more information on Azure SignalR Service, check out the official docs and tutorials:

Packaging Changes in 3.x

You may have heard that ASP .NET Core 3.0 changed the way packages are made available to developers. So how does this affect SignalR for 3.x projects? Here is a recap from the official announcement:

  • Microsoft “will stop producing many of the NuGet packages that we have been shipping since ASP.NET Core 1.0. The API those packages provide are still available to apps by using a <FrameworkReference> to Microsoft.AspNetCore.App. This includes commonly referenced API, such as Kestrel, Mvc, Razor, and others.”
  • “This will not apply to all binaries that are pulled in via Microsoft.AspNetCore.App in 2.x.”
  • Notable exceptions include: The SignalR .NET client will continue to support .NET Standard and ship as NuGet package because it is intended for use on many .NET runtimes, like Xamarin and UWP.”

Sourcehttps://github.com/aspnet/Announcements/issues/325

References:

Razor Pages in ASP .NET Core 3.1

By Shahed C on May 4, 2020

This is the eighteenth of a new series of posts on ASP .NET Core 3.1 for 2020. In this series, we’ll cover 26 topics over a span of 26 weeks from January through June 2020, titled ASP .NET Core A-Z! To differentiate from the 2019 series, the 2020 series will mostly focus on a growing single codebase (NetLearner!) instead of new unrelated code snippets week.

Previous post:

NetLearner on GitHub:

In this Article:

R is for Razor Pages

Razor Pages were introduced in ASP .NET Core v2.0, and briefly covered in my 2018 series, and with more detail in my 2019 A-Z series. To complement this updated post in 2020, you may also refer to a previous posts in this series to learn more about Forms and Fields (specifically the Razor Pages section).

Built on top of MVC in ASP .NET Core, Razor Pages allows you to simplify the way you organize and code your web apps. Your Razor Pages may coexist along with a backend Web API and/or traditional MVC views backed by controllers. Razor Pages are typically backed by a corresponding .cs class file, which represents a Model for the Page with Model Properties and Action Methods that represent HTTP Verbs. You can even use your Razor knowledge to work on Blazor fullstack web development.

Razor Page with various elements, attributes and properties
Razor Page with various elements, attributes and properties

Core 3.x Packages

To follow along, take a look at the NetLearner Razor Pages project on Github:

Let’s start by taking a look at this 3.1 (LTS) project. The snippet below shows a .csproj for the sample app. This was created by starting with the Core 3.1 Razor Pages Template in VS2019 and then updating it to view/edit data from the shared NetLearner database.

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <UserSecretsId>aspnet-NetLearner.Pages-38D1655D-B03D-469A-9E4E-5DDDED554242</UserSecretsId>
  </PropertyGroup>

  <ItemGroup>
    <Compile Remove="Data\**" />
    <Content Remove="Data\**" />
    <EmbeddedResource Remove="Data\**" />
    <None Remove="Data\**" />
  </ItemGroup>


  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.0" />
    <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
  </ItemGroup>


  <ItemGroup>
    <ProjectReference Include="..\NetLearner.SharedLib\NetLearner.SharedLib.csproj" />
  </ItemGroup>

</Project>


For ASP .NET Core 3.x projects, both NewtonsoftJson and EF Core have been removed from the ASP .NET Core shared framework. Instead, they are available as NuGet packages that can be included via <PackageReference> tags in the .csproj project file.

This is reflected in the Solution Explorer, where the Dependencies tree may show NewtonsoftJson and/or EF Core packages nested under the NuGet node, if you use them in your project.

Dependencies in Solution Explorer

If you need a refresher on the new changes for ASP .NET Core 3.x, refer to the following:

Page Syntax

To develop Razor Pages, you can reuse syntax from MVC Razor Views, including Tag Helpers, etc. For more information on Tag Helpers, stay tuned for an upcoming post in this series. The code snippet below shows a typical Razor page, e.g. Index.cshtml:

@page
@model IndexModel
@{
 ViewData["Title"] = "Home page";
}

<!-- HTML content, with Tag Helpers, model attributes -->

Here is a quick recap of what a Razor Page is made of:

  1. Each Razor Page starts with an @page directive to indicate that it’s a Razor Page. This is different from Razor Views in MVC, which should not start with @page.
  2. The @page directive may be followed by an @model directive. This identifies the corresponding C# model class, typically located in the same folder as the .cshtml page itself.
  3. (Optional) You can include server-side code within an @{} block.
  4. The rest of the page should include any HTML content you would like to display. This includes any server-side Tag Helpers and Model attributes.

Running the Razor Pages web project from NetLearner shows a list of Learning Resources at the following URL:

e.g.  https://localhost:44343/ResourceLists

NetLearner's ResourceLists page
NetLearner’s ResourceLists page

Model Binding

The .cs model class associated  with the page includes both the model’s attributes, as well as action methods for HTTP Verbs. In a way, it consolidates the functionality of an MVC Controller and C# viewmodel class, within a single class file.

The simplest way to use model binding in a Razor Page use to use the [BindProperty] attribute on properties defined in the model class. This may include both simple and complex objects. In the sample, the Movie property in the CreateModel class is decorated with this attribute as shown below:

[BindProperty]
public ResourceList ResourceList { get; set; }

Note that [BindProperty] allows you to bind properties for HTTP POST requests by default. However, you will have to explicitly opt-in for HTTP GET requests. This can be accomplished by including an optional boolean parameter (SupportsGet) and setting it to True, e.g.

[BindProperty(SupportsGet = true)]
public string SearchString { get; set; }

This may come in handy when passing in QueryString parameters to be consumed by your Razor Page. Parameters are optional and are part of the route used to access your Razor Pages.

To use the Model’s properties, you can use the syntax Model.Property to refer to each property by name. Instead of using the name of the model, you have to use the actual word “Model” in your Razor Page code.

e.g. a page’s model could have a complex object…

public ResourceList ResourceList { get; set; }

Within the complex object, e.g. the shared ResourceList class has a public Id property:

public int Id { get; set; }

In the Razor Page that refers to the above model, you can refer to Model.Movie.ID by name:

@page
@model NetLearner.Pages.DetailsModel
...
<a asp-page="./Edit" asp-route-id="@Model.ResourceList.Id">Edit this List</a>

In this particular example, the <a> anchor tag is generated with a link to the Edit page with a route that uses a specific Movie ID value. The link points to the Edit page in the current subfolder (i.e. “ResourceLists”), indicated by the period and slash in the path. The generated HTML looks like the following:

<a href="/ResourceLists/Edit?id=1">Edit this List</a>
Details page in browser with HTML source
Details page in browser with HTML source

Page Parameters

Page parameters can be included with the @page directive at the top of the page. To indicate that a parameter is optional, you may include a trailing ? question mark character after the parameter name. You may also couple the parameter names with a data type, e.g. int for integers.

@page "{id}"

@page "{id?}"

@page "{id:int?}"

The above snippet shows 3 different options for an id parameter, an optional id parameter and an integer-enforced id parameter. In the C# model code, a property named id can be automatically bound to the page parameter by using the aforementioned [BindProperty] attribute.

In the sample, the  SearchString property in the IndexModel class for ResourceLists shows this in action.

[BindProperty(SupportsGet = true)]
public string SearchString { get; set; }

The corresponding page can then define an optional searchString parameter with the @page directive. In the HTML content that follows, Input Tag Helpers can be used to bind an HTML field (e.g. an input text field) to the field.

@page "{searchString?}"
...
Title: <input type="text" asp-for="SearchString" />

Page Routing

As you may have guessed, a page parameter is setting up route data, allowing you to access the page using a route that includes the page name and parameter:

e.g. https://servername/PageName/?ParameterName=ParameterValue

In the sample project, browsing to the ResourceLists page with the search string “videos” includes any search results that include the term “videos”, as shown in the following screenshot.

e.g.  https://localhost:44343/ResourceLists?SearchString=videos

Index page for Resource Lists with ?SearchString parameter
Index page for Resource Lists with ?SearchString parameter

Here, the value for SearchString is used by the OnGetAsync() method in the Index.cshtml.cs class for ResourceLists. In the code snippet below, you can see that a LINQ Query filters the movies by a subset of movies where the Title contains the SearchString value. Finally, the list of movies is assigned to the Movie list object.

...
public IList<ResourceList> ResourceList { get;set; }
...
public async Task OnGetAsync()
{
   ...
   if (!string.IsNullOrEmpty(SearchString))
   {
      resourceLists = resourceLists.Where(s => s.Name.Contains(SearchString));
   }
   ...
   ResourceList = await resourceLists.ToListAsync();
}

By convention, all Razor Pages should be in a root-level “Pages” folder. Think of this “Pages” folder as a virtual root of your web application. To create a link to a Razor Page, you may link to the name of a Razor Page at the root level (e.g. “/Index”) or a Razor Page within a subfolder (e.g. “/ResourceLists/Index” as seen in the shared _Layout.cshtml file).

<a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>

<a class="nav-link text-dark" asp-area="" asp-page="/ResourceLists/Index">Lists</a>
Navigation links in Razor Pages, from Layout page
Navigation links in Razor Pages, from Layout page

Handler Methods

The OnGetAsync() method seen in the previous method is triggered when the Razor Page is triggered by an HTTP GET request that matches its route data. In addition to OnGetAsync(), you can find a complete list of Handler Methods that correspond to all HTTP verbs. The most common ones are for GET and POST:

  • OnGet() or OnGetAsync for HTTP GET
  • OnPost() or OnPostAsync for HTTP POST

When using the Async alternatives for each handler methods, you should return a Task object (or void for the non-async version). To include a return value, you should return a Task<IActionResult> (or IActionResult for the non-async version).

public void OnGet() {}
public IActionResult OnGet() {}
public async Task OnGetAsync() {}

public void OnPost() {}
public IActionResult OnPost() {}
public async Task<IActionResult> OnPostAsync() {}

To implement custom handler methods, you can handle more than one action in the same HTML form. To accomplish this, use the asp-page-handler attribute on an HTML <button> to handle different scenarios.

<form method="post">
 <button asp-page-handler="Handler1">Button 1</button>
 <button asp-page-handler="Handler2">Button 2</button>
</form>

To respond to this custom handlers, the exact handler names (e.g. Handler1 and Handler2) need to be included after OnPost in the handler methods. The snippet below shows the corresponding examples for handling the two buttons.

public async Task<IActionResult> OnPostHandler1Async()
{
   //...
}
public async Task<IActionResult> OnPostHandler2Info()
{
   // ...
}

NOTE: if you need to create a public method that you don’t have to be recognized as a handler method, you should decorate such a method with the [NonHandler] attribute.

References