Category Archives: Learn

JavaScript, CSS, HTML & Other Static Files in ASP .NET Core

By Shahed C on March 11, 2019

This is the tenth of a series of posts on ASP .NET Core in 2019. In this series, we’ll cover 26 topics over a span of 26 weeks from January through June 2019, titled A-Z of ASP .NET Core!

ASPNETCoreLogo-300x267 A – Z of ASP .NET Core!

In this Article:

J is for JavaScript, CSS, HTML & Other Static Files

NOTE: this article will teach you how to include and customize the use of static files in ASP .NET Core web applications. It is not a tutorial on front-end web development.

If your ASP .NET Core web app has a front end – whether it’s a collection of MVC Views or a Single-Page Application (SPA) – you will need to include static files in your application. This includes (but is not limited to): JavaScript, CSS, HTML and various image files.

When you create a new web app using one of the built-in templates (MVC or Razor Pages), you should see a “wwwroot” folder in the Solution Explorer. This points to a physical folder in your file system that contains the same files seen from Visual Studio. However, this location can be configured, you can have multiple locations with static files, and you can enable/disable static files in your application if desired. In fact, you have to “opt in” to static files in your middleware pipeline.

Blog-Diagram-AspNetCore-mw

You can browse the (template-generated) sample app (with static files) on GitHub at the following location:

Web sample app with static files: https://github.com/shahedc/AspNetCoreStaticSample

Configuring Static Files via Middleware

Let’s start by observing the Startup.cs configuration file. We’ve seen this file several times throughout this blog series. In the Configure() method, you’ll find the familiar method call to enable the use of static files.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
   ...
   app.UseStaticFiles();
   ...
   app.UseMvc(...);
}

This call to app.UseStaticFiles() ensures that static files can be served from the designated location, e.g. wwwroot. In fact, this line of code looks identical whether you look at the Startup.cs file in an MVC or Razor Pages project.

It’s useful to note the placement of this line of code. It appears before app.UseMvc(), which is very important. This ensures that static file requests can be processed and sent back to the web browser without having to touch the MVC middleware. This becomes even more important when authentication is used.

Take a look at either the MVC or Razor Pages projects that have authentication added to them. In the code below, you can see the familiar call to app.UseStaticFiles() once again. However, there is also a call to app.UseAuthentication(). It’s important for the authentication call to appear after the call to use static files. This ensure that the authentication process isn’t triggered when it isn’t needed.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
   ...
   app.UseStaticFiles();
   ...
   app.UseAuthentication();
   ...
   app.UseMvc(...);
}

By using the middleware pipeline in this way, you can “short-circuit” the pipeline when a request has been fulfilled by a specific middleware layer. If a static file has been successfully served using the Static Files middleware, it prevents the next layers of middleware (i.e. authentication, MVC) from processing the request.

NOTE: if you need to secure any static files, e.g. private images, you can consider a cloud solution such as Azure Blob Storage to store the files. The files can then be served from within the application, instead of actual static files. You could also serve secure files (from outside the wwwroot location) as a FileResult() object returned from an action method that has an [Authorize] attribute.

Customizing Locations for Static Files

It may be convenient to have the default web templates create a location for your static files and also enable the use of those static files. As you’ve already seen, enabling static files isn’t magic. Removing the call to app.useStaticFiles() will disable static files from being served. In fact, the location for static files isn’t magic either.

In a previous post, we had discussed how the Program.cs file includes a call to CreateDefaultBuilder() to set up your application:

public class Program
{
   ...
   public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
      WebHost.CreateDefaultBuilder(args)
         .UseStartup<Startup>();
}

Behind the scenes, this method call sets the “content root” to the current directory, which contains the “wwwroot” folder, your project’s “web root”. These can both be customized.

WebHost.CreateDefaultBuilder(args).UseContentRoot("c:\\<content-root>")
WebHost.CreateDefaultBuilder(args).UseWebRoot("public")

You may also use the call to app.UseStaticFiles() to customize an alternate location to serve static files. This allows you to serve additional static files from a location outside of the designated web root.

...
using Microsoft.Extensions.FileProviders;
using System.IO;
...
public void Configure(IApplicationBuilder app)
{
   ...
   app.UseStaticFiles(new StaticFileOptions
   {
      FileProvider = new PhysicalFileProvider(
         Path.Combine(env.ContentRootPath, "AltStaticRoot")),
         RequestPath = "/AltStaticFiles"
   });
}

Wait a minute… why does it look like there are two alternate locations for static files? There is a simple explanation:

  • In the call to Path.Combine(), the “AltStaticRoot” is an actual folder in your current directory. This Path class and its Combine() method are available in the System.IO namespace.
  • The “AltStaticFiles” value for RequestPath is used as a root-level “virtual folder” from which images can be served. The PhysicalFileProvider class is available in the Microsoft.Extensions.FileProviders namespace.

The following markup may be used in a .cshtml file to refer to an image, e.g. MyImage01.png:

<img src="~/AltStaticFiles/MyImages/MyImage01.png" />

The screenshot below shows an example of an image loaded from an alternate location.

code sample for alt images

The screenshot below shows a web browser displaying such an image.

VS2017-AltStaticFile-Browser

You can find the above code in the RazorWebAltStatic sample web app on GitHub:

Preserving CDN Integrity

When you use a CDN (Content Delivery Network) to serve common CSS and JS files, you need to ensure that the integrity of the source code is reliable. You can rest assured that ASP .NET Core has already solved this problem for you in its built-in templates. Let’s take a look at the shared Layout page, e.g. _Layout.cshtml in a Razor Pages web application.

<environment include="Development">
 <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
</environment>

<environment exclude="Development">
 <link 
   rel="stylesheet" 
   href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/css/bootstrap.min.css"
   asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
   asp-fallback-test-class="sr-only" 
   asp-fallback-test-property="position" 
   asp-fallback-test-value="absolute"
   crossorigin="anonymous"
   integrity="sha256-eSi1q2PG6J7g7ib17yAaWMcrr5GrtohYChqibrV7PBE="/>
</environment>

Right away, you’ll notice that there are two conditional <environment> blocks in the above markup. The first block is used only during development, in which the bootstrap CSS file is obtained from your local copy. When not in development (e.g. staging, production, etc), the bootstrap CSS file is obtained from a CDN, e.g. CloudFlare.

You could use an automated hash-generation tool to generate the SRI (Subresource Integrity) hash values, but you would have to manually copy the value into your code. You can try out the relatively-new LibMan (aka Library Manager) for easily adding and updating your client-side libraries.

LibMan (aka Library Manager)

The easiest way to use LibMan is to use the built-in features available in Visual Studio. Using LibMan using the IDE is as easy as launching it from Solution Explorer. Specify the provider from the library you want, and any specific files you want from that library.

If you’ve already read my SignalR article from my 2018 blog series, you may recall the steps to add a 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, e.g. @aspnet/signalr@1… pick latest stable if desired
  • Files: At a minimum, choose specific files, e.g. signalr.js and/or its minified equivalent

VS2017-SignalR-ClientLib

For more on LibMan (using VS or CLI), check out the official docs:

In any case, using LibMan will auto-populate a “libman.json” manifest file, which you can also inspect and edit manually. The aforementioned SignalR article also includes a real-time polling web app sample. You can view its libman.json file to observe its syntax for using a SignalR client library.

{
  "version": "1.0",
  "defaultProvider": "unpkg",
  "libraries": [
    {
      "library": "@aspnet/signalr@1.1.0",
      "destination": "wwwroot/lib/signalr/",
      "files": [
        "dist/browser/signalr.js",
        "dist/browser/signalr.min.js"
      ]
    }
  ]
}

Every time you save the libman.json file, it will trigger LibMan’s restore process. This pulls down the necessary libraries from their specified source, and adds them to your local file system. If you want to trigger this restore process manually, you can always choose the “Restore Client-Side Libraries” option by right-clicking the libman.json file in Solution Explorer.

What About NPM or WebPack?

If you’ve gotten this far, you may be wondering: “hey, what about NPM or WebPack?”

It’s good to be aware that LibMan is a not a replacement for your existing package management systems. In fact, the Single-Page Application templates in Visual Studio (for Angular and React) currently use npm and WebPack. LibMan simply provides a lightweight mechanism to include client-side libraries from external locations.

For more information on WebPack in ASP .NET Core, I would recommend these 3rd-party articles:

References

Additional reference for pre-compressing static files, found on Twitter via:

@DotNetAppDevhttps://twitter.com/DotNetAppDev/status/1106299093922533376

 

 

IIS Hosting for ASP .NET Core Web Apps

By Shahed C on March 4, 2019

This is the ninth of a series of posts on ASP .NET Core in 2019. In this series, we’ll cover 26 topics over a span of 26 weeks from January through June 2019, titled A-Z of ASP .NET Core!

ASPNETCoreLogo-300x267 A – Z of ASP .NET Core!

In this Article:

I is for IIS Hosting

If you’ve been reading my weekly blog posts in this series (or you’ve worked with ASP .NET Core), you probably know that ASP .NET Core web apps can run on multiple platforms. Since Microsoft’s IIS (Internet Information Services) web server only runs on Windows, you may be wondering why we would need to know about IIS-specific hosting at all.

Well, we don’t need IIS to run ASP .NET Core, but there are some useful IIS features you can take advantage of. In fact, ASP .NET Core v2.2 introduced in-process hosting in IIS with the ASP .NET Core module. You can run your app either in-process or out of process.

From the 2.2 release notes of what’s new:

“In earlier versions of ASP.NET Core, IIS serves as a reverse proxy. In 2.2, the ASP.NET Core Module can boot the CoreCLR and host an app inside the IIS worker process (w3wp.exe). In-process hosting provides performance and diagnostic gains when running with IIS.” 

You can browse an IIS-configured sample app on GitHub at the following location:

Web IIS-configured sample: https://github.com/shahedc/IISHostedWebApp

NOTE: The actual web.config file has been intentionally left out from the above repo, and replaced with a reference file, web.config.txt. When you follow the configuration steps outlined below, the actual web.config file will be generated with the proper settings.

Blog-Diagram-IIS-in-out-proxy

Developing for IIS

In order to develop for IIS on your local development machine while working with Visual Studio, you should complete the following steps:

  1. Install/Enable IIS locally: add IIS via Windows Features setup
  2. Configure IIS: add website with desired port(s)
  3. Enable IIS support in VS: include dev-time IIS support during setup
  4. Configure your web app: enable HTTPS, add launch profile for IIS

NOTE: If you need help with any of the above steps, you may follow the detailed guide in the official docs:

After Step 1 (IIS installation), the list of Windows Features should show that IIS and many of its components have been installed.

Windows-Features-IIS

During and after Step 2 (IIS configuration), my newly added website looks like the screenshots shown below:

Add New Website

Add New Website

Explore Added Website

Explore Added Website

For Step 3 (IIS support in VS) mentioned above, make sure you select the “Development time IIS support” option under the “ASP .NET and web development” workload. These options are shown in the screenshot below:

VS2017-IIS-Setup

After Step 4 (web app config), I can run the web application locally on IIS. Check out the next section to learn more about your web application configuration. In addition to the .csproj settings, you’ll also need a web.config file in addition to your appsettings.json file.

Optionally, you could set up a VM on Azure with IIS enabled and deploy to that web server. If you need some help with the setup, you can select a pre-configured Windows VM with IIS pre-installed from the Azure Portal.

If you need help with creating an Azure VM with IIS, check out the following resources:

IIS Configuration in Visual Studio

After completing Step 4 using the instructions outlined above, let’s observe the following:

  • Profile: Debug tab in project properties
  • Project: .csproj settings
  • Settings: launchSettings.json
  • Config: web.config file

Profile: With your web project open in Visual Studio, right-click the project in Solution Explorer, and then click Properties. Click the Debug tab to see the newly-added IIS-specific profile. Note that the Hosting Model offers 3 options: Default, In-Process and Out-of-process.

VS2017-Profile-IIS

Project: Again in the Solution Explorer, right-click the project, then click Edit <projname>.csproj to view the .csproj file as a text file. Here, the <AspNetCoreHostingModel> setting is shown set to InProcess, and can be changed to OutOfProcess if desired.

VS2017-IIS-csproj

Settings: Under the Properties node in the Solution Explorer, open the launchSettings.json file. Here, you can see all the settings for your newly-created IIS-specific Profile. You can manually add/edit settings directly in this file without having to use the Visual Studio UI.

VS2017-IIS-launch

Config: You may recognize the XML-based web.config file from previous versions of ASP .NET, but the project’s settings and dependencies are now spread across multiple files, such as the .csproj file and appsettings.json config file. For ASP .NET Core projects, the web.config file is only used specifically for an IIS-hosted environment. In this file, you may configure the ASP .NET Core Module that will be used for your IIS-hosted web application.

VS2017-IIS-webconfig

Once your web app is configured, you can run it from Visual Studio using the profile you created previously. In order to run it using a specific profile, click the tiny dropdown next to the green Run/Debug button to select the desired profile. Then click the button itself. This should launch the application in your web browser, pointing to localhost running on IIS, e.g. https://localhost/<appname>

IIS Profile

IIS Profile

Running in a browser

Running in a browser 

ASP .NET Core Module

Now that your IIS server environment has been set up (either locally or in the cloud), let’s learn about the ASP .NET Core Module. This is a native IIS module that plugs into the actual IIS pipeline (not to be confused with your web app’s request pipeline). It allows you to host your web app in one of two ways:

  • In-process: the web app is hosted inside the IIS worker process, i.e. w3wp.exe (previously known as the World Wide Web Publishing Service)
  • Out of process: IIS forwards web server requests to the web app, which uses Kestrel (the cross-platform web server included with ASP .NET Core)

As you’ve seen in an earlier section, this setting is configured in the web.config file:

<PropertyGroup>
   <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>

The lists below shows shows some notable differences between hosting in-process vs out of process.

Blog-Diagram-IIS-in-out

How you configure your app is up to you. For more details on the module and its configuration settings, browse through the information available at:

BONUS: Publishing to a VM with IIS

You can publish your ASP .NET Core web app to a Virtual Machine (either on your network or in Azure) or just any Windows Server you have access to. There are several different options:

Here are some prerequisites to be aware of:

  • IIS must be pre-installed on the server with relevant ports enabled
  • WebDeploy must be installed (which you would normally have on your local dev machine)
  • The VM must have a DNS name configured (Azure VMs can have fully qualified domain names, e.g. <machinename>.eastus.cloudapp.azure.com)

References

 

 

Handling Errors in ASP .NET Core

By Shahed C on February 25, 2019

This is the eighth of a series of posts on ASP .NET Core in 2019. In this series, we’ll cover 26 topics over a span of 26 weeks from January through June 2019, titled A-Z of ASP .NET Core!

ASPNETCoreLogo-300x267 A – Z of ASP .NET Core!

In this Article:

This article will refer to the following sample code on GitHub:

Web Error Handling Samples: https://github.com/shahedc/ErrorHandlingWeb

H is for Handling Errors

Unless you’re perfect 100% of the time (who is?), you’ll most likely have errors in your code. If your code doesn’t build due to compilation errors, you can probably correct that by fixing the offending code. But if your application encounters runtime errors while it’s being used, you may not be able to anticipate every possible scenario.

Runtime errors may cause Exceptions, which can be caught and handled in many programming languages. Unit tests will help you write better code, minimize errors and create new features with confidence. In the meantime, there’s the good ol’ try-catch-finally block, which should be familiar to most developers.

NOTE: You may skip to the next section below if you don’t need this refresher.

Blog-Diagram-errors

Exceptions with Try-Catch-Finally

The simplest form of a try-catch block looks something like this:

try
{
   // try something here

} catch (Exception ex)
{
  // catch an exception here
}

You can chain multiple catch blocks, starting with more specific exceptions. This allows you to catch more generic exceptions toward the end of your try-catch code.  In a string of catch() blocks, only the caught exception (if any) will cause that block of code to run.

try
{
   // try something here

} catch (IOException ioex)
{
  // catch specific exception, e.g. IOException

} catch (Exception ex)
{
  // catch generic exception here

}

Finally, you can add the optional finally block. Whether or not an exception has occurred, the finally block will always be executed.

try
{
   // try something here

} catch (IOException ioex)
{
  // catch specific exception, e.g. IOException

} catch (Exception ex)
{
  // catch generic exception here

} finally
{
   // always run this code

}

Try-Catch-Finally in Sample App

In the MVC sample app, the Reader Controller uses a Data Service from a shared .NET Standard Library to read from a data file that may exist in the Web App’s static files. It displays a view with some hard-coded data and tries to replace some data with additional information obtained from the data file.

// hard-code some data
var data1 = new DataItem
{
 Id = 1,
 SomeData = "data 1 initialized"
}; 
var data2 = new DataItem
{
 Id = 2,
 SomeData = "data 2 initialized"
}; 
var data3 = new DataItem
{
 Id = 3,
 SomeData = "data 3 initialized"
};

Then, try to read some data from a data file, to replace information in the data model.

// get data from file if possible
try
{ // Open the text file using a stream reader.
 var webRoot = _env.WebRootPath;
 var file = System.IO.Path.Combine(webRoot, @"data\datafile.txt");

 using (StreamReader sr = new StreamReader(file))
 {
 // Read the stream to a string, overwrite some data
 data2.SomeData = sr.ReadToEnd();
 }
}
catch (IOException ioException)
{
 data2.SomeData = $"IO Error: {ioException.Message}";
}
catch (Exception exception)
{
 data2.SomeData = $"Generic Error: {exception.Message}";
}
finally
{
 data3.SomeData = "All done!";
}

In the above code, you can see a series of try-catch blocks, ending with a finally block:

  1. try to read the file and overwrite some data in the 2nd data object.
  2. catch an IOException, capture the error message
  3. catch a generic Exception if a different exception has occurred, capture the generic error message
  4. finally, overwrite some data in the 3rd data object whether or not any errors have occurred.

Run the MVC app and navigate to the Reader Controller. If there are no errors, you should see just the hard-coded data, with some data replaced from the data file.

ErrorApp-Success

If you rename/delete the data file, then run the program again, you should see an error message as well. This reflects the IOException that occurred while attempting to read a missing file.

ErrorApp-IOError

Error Handling for MVC

In ASP .NET Core MVC web apps, unhandled exceptions are typically handled in different ways, depending on whatever environment the app is running in. The default template uses the DeveloperExceptionPage middleware in a development environment but redirects to a shared Error view in non-development scenarios. This logic is implemented in the Configure() method of the Startup.cs class.

if (env.IsDevelopment())
{
   app.UseDeveloperExceptionPage();
}
else
{
   app.UseExceptionHandler("/Home/Error");
   ...
}

The DeveloperExceptionPage middleware can be further customized with DeveloperExceptionPageOptions properties, such as FileProvider and SourceCodeLineCount.

var options = new DeveloperExceptionPageOptions
{
   SourceCodeLineCount = 2
};  
app.UseDeveloperExceptionPage(options);

Using the snippet shown above, the error page will show the offending line in red, with a variable number of lines of code above it. The number of lines is determined by the value of SourceCodeLineCount, which is set to 2 in this case. In this contrived example, I’m forcing the exception by throwing a new Exception in my code.

ErrorApp-MvcError-Dev-SourceLine

For non-dev scenarios, the shared Error view can be further customized by updating the Error.cshtml view in the Shared subfolder. The ErrorViewModel has a ShowRequestId boolean value that can be set to true to see the RequestId value.

@model ErrorViewModel
@{
   ViewData["Title"] = "Error";
}

<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>

@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}

<h3>header content</h3>
<p>text content</p> 

In the MVC template’s Home Controller, the Error() action method sets the RequestId to the current Activity.Current.Id if available or else it uses HttpContext.TraceIdentifier. These values can be useful during debugging.

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
   return View(new ErrorViewModel { 
      RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier 
   });
}

UPDATE:

Wait… what about Web API in ASP .NET Core? After posting this article in a popular ASP .NET Core group on Facebook, I got some valuable feedback from the admin:

Dmitry Pavlov: “For APIs there is a nice option to handle errors globally with the custom middleware https://code-maze.com/global-error-handling-aspnetcore – helps to get rid of try/catch-es in your code. Could be used together with FluentValidation and MediatR – you can configure mapping specific exception types to appropriate status codes (400 bad response, 404 not found, and so on to make it more user friendly and avoid using 500 for everything). “

For more information on the aforementioned items, check out the following resources:

Later on in this series, we’ll cover ASP .NET Core Web API in more detail, when we get to “W is for Web API”. Stay tuned!

Error Handling for Razor Pages

Since Razor Pages still use the MVC middleware pipeline, the exception handling is similar to the scenarios described above. For starters, here’s what the Configure() method looks like in the Startup.cs file for the Razor Pages web app sample.

if (env.IsDevelopment())
{
   app.UseDeveloperExceptionPage();
}
else
{
   app.UseExceptionHandler("/Error");
   ...
}

In the above code, you can see the that development environment uses the same DeveloperExceptionPage middleware. This can be customized using the same techniques outlined in the previous section for MVC pages, so we won’t go over this again.

As for the non-dev scenario, the exception handler is slightly different for Razor Pages. Instead of pointing to the Home controller’s Error() action method (as the MVC version does), it points to the to the /Error page route. This Error.cshtml Razor Page found in the root level of the Pages folder.

@page
@model ErrorModel
@{
   ViewData["Title"] = "Error";
}

<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>

@if (Model.ShowRequestId)
{
    <p>
        <strong>Request ID:</strong> <code>@Model.RequestId</code>
    </p>
}

<h3>custom header text</h3>
<p>custom body text</p>

The above Error page for looks almost identical to the Error view we saw in the previous section, with some notable differences:

  • @page directive (required for Razor Pages, no equivalent for MVC view)
  • uses ErrorModel (associated with Error page) instead of ErrorViewModel (served by Home controller’s action method)

Logging Errors

To log errors in ASP .NET Core, you can use the built-in logging features or 3rd-party logging providers. In ASP .NET Core 2.x, the use of CreateDefaultBuilder() in Program.cs takes of care default Logging setup and configuration (behind the scenes).

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
   WebHost.CreateDefaultBuilder(args)
      .UseStartup<Startup>();

NOTE: The Web Host Builder is being replaced by the Generic Host Builder in ASP .NET Core 3.0, but you can expect similar initial behavior. For more information on Generic Host Builder, take a look at the previous blog post in this series: Generic Host Builder in ASP .NET Core.

The host sets up the logging configuration for you, e.g.:

.ConfigureLogging((hostingContext, logging) =>
{
   logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
   logging.AddConsole();
   logging.AddDebug();
   logging.AddEventSourceLogger();
})

Since ASP .NET Core is open-source, you can find the above snippet (or something similar) on Github. Here is a link to the 2.2 release of WebHost:

To make use of error logging (in addition to other types of logging) in your MVC web app, you may call the necessary methods in your controller’s action methods. Here, you can log various levels of information, warnings and errors at various severity levels.

As seen in the snippet below, you have to do the following in your MVC Controller that you want to add Logging to:

  1. Add using statement for Logging namespace
  2. Add a private readonly variable for an ILogger object
  3. Inject an ILogger<model> object into the constructor
  4. Assign the private variable to the injected variable
  5. Call various log logger methods as needed.
...
using Microsoft.Extensions.Logging;

public class MyController: Controller
{
   ...
   private readonly ILogger _logger; 

   public MyController(..., ILogger<MyController> logger)
   {
      ...
      _logger = logger;
   }
   
   public IActionResult MyAction(...)
   {
      _logger.LogTrace("log trace");
      _logger.LogDebug("log debug");
      _logger.LogInformation("log info");
      _logger.LogWarning("log warning");
      _logger.LogError("log error");
      _logger.LogCritical("log critical");
   }
}

In Razor Pages, the logging code will go into the Page’s corresponding Model class. As seen in the snippet below, you have to do the following to the Model class that corresponds to a Razor Page:

  1. Add using statement for Logging namespace
  2. Add a private readonly variable for an ILogger object
  3. Inject an ILogger<model> object into the constructor
  4. Assign the private variable to the injected variable
  5. Call various log logger methods as needed.
...
using Microsoft.Extensions.Logging;

public class MyPageModel: PageModel
{
   ...
   private readonly ILogger _logger;

   public MyPageModel(..., ILogger<MyPageModel> logger)
   {
      ...
      _logger = logger; 
   }
   ...
   public void MyPageMethod() 
   {
      ... 
      _logger.LogInformation("log info"); 
      _logger.LogError("log error");
      ...
   }
} 

You may have noticed that Steps 1 through 5 are pretty much identical for MVC and Razor Pages. This makes it very easy to quickly add all sorts of logging into your application, including error logging.

Transient fault handling

Although it’s beyond the scope of this article, it’s worth mentioning that you can avoid transient faults (e.g. temporary database connection losses) by using some proven patterns, practices and existing libraries. To get some history on transient faults, check out the following article from the classic “patterns & practices”. It describes the so-called “Transient Fault Handling Application Block”.

More recently, check out the docs on Transient Fault Handling:

And now in .NET Core, you can add resilience and transient fault handling to your .NET Core HttpClient with Polly!

You can get more information on the Polly project on the official Github page:

References

 

Generic Host Builder in ASP .NET Core

By Shahed C on February 18, 2019

This is the seventh of a series of posts on ASP .NET Core in 2019. In this series, we’ll cover 26 topics over a span of 26 weeks from January through June 2019, titled A-Z of ASP .NET Core!

ASPNETCoreLogo-300x267 A – Z of ASP .NET Core!

In this Article:

G is for Generic Host Builder

The Generic Host Builder in ASP .NET Core was introduced in v2.1, but only meant for non-HTTP workloads. However, it is intended to replace the Web Host Builder when v3.0 is released in 2019.

Generic Host Builder in ASP .NET Core 3.0

Generic Host Builder in ASP .NET Core 3.0

Generic Host Builder in 2.x

So, if the Generic Host Builder isn’t currently used for web hosting in v2.x, what can it be used for? The aforementioned non-HTTP workloads include a number of capabilities according to the documentation, including:

  • app config, e.g. set base path, add hostsettings.json, env variables, etc
  • dependency injection, e.g. various hosted services
  • logging capabilities, e.g. console logging

The HostBuilder class is available from the following namespace, implementing the IHostBuilder interface:

using Microsoft.Extensions.Hosting;

At a minimum, the Main() method of your .NET Core app would look like the following:

public static async Task Main(string[] args)
{
   var host = new HostBuilder()
      .Build(); 

   await host.RunAsync();
}

Here, the Build() method initializes the host, so (as you may expect) it can only be called once for initialization. Additional options can be configured by calling the ConfigureServices() method before initializing the host with Build().

var host = new HostBuilder()
   .ConfigureServices((hostContext, services) =>
   {
      services.Configure<HostOptions>(option =>
      {
         // option.SomeProperty = ...
      });
   })
   .Build();

Here, the ConfigureServices() method takes in a HostBuilderContext and an injected collection of IServiceCollection services. The options set in the Configure() can be used to set additional HostOptions. Currently, HostOptions just has one property, i.e. ShutdownTimeout.

You can see more configuration capabilities in the official sample, broken down into the snippets below:

Host Config Snippet:

.ConfigureHostConfiguration(configHost =>
{
   configHost.SetBasePath(Directory.GetCurrentDirectory());
   configHost.AddJsonFile("hostsettings.json", optional: true);
   configHost.AddEnvironmentVariables(prefix: "PREFIX_");
   configHost.AddCommandLine(args);
})

App Config Snippet: 

.ConfigureAppConfiguration((hostContext, configApp) =>
{
   configApp.AddJsonFile("appsettings.json", optional: true);
   configApp.AddJsonFile(
      $"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", 
      optional: true);
   configApp.AddEnvironmentVariables(prefix: "PREFIX_");
   configApp.AddCommandLine(args);
})

Dependency Injection Snippet: 

.ConfigureServices((hostContext, services) =>
{
   services.AddHostedService<LifetimeEventsHostedService>();
   services.AddHostedService<TimedHostedService>();
})

Logging Snippet: 

.ConfigureLogging((hostContext, configLogging) =>
{
   configLogging.AddConsole();
   configLogging.AddDebug();
})

Web Host Builder in 2.x

The WebHostBuilder class is available from the following namespace (specific to ASP .NET Core), implementing the IWebHostBuilder interface:

using Microsoft.AspNetCore.Hosting;

The Web Host Builder in ASP .NET Core is currently used for hosting web apps as of v2.x. As mentioned in the previous section, it will be replaced by the Generic Host Builder in v3.0. At a minimum, the Main() method of your ASP .NET Core 2.x web app would look like the following:

public class Program
{
   public static void Main(string[] args)
   {
      CreateWebHostBuilder(args).Build().Run();
   } 

   public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
      WebHost.CreateDefaultBuilder(args)
         .UseStartup<Startup>();
}

If you’re not familiar with the shorthand syntax of the helper method CreateWebHostBuilder() shown above, here’s what it would normally look like, expanded:

public static IWebHostBuilder CreateWebHostBuilder(string[] args)
{
   return WebHost.CreateDefaultBuilder(args).UseStartup<Startup>();
}

NOTE: This type of C# syntax is known as an Expression Body Definition, introduced for methods in C# 6.0, and additional features in C# 7.0.

The CreateDefaultBuilder() method performs a lot of “magic” behind the scenes, by making use of pre-configured defaults. From the official documentation, here is a summary of the default configuration from the Default Builder:

For more information on some of the above, here are some other blog posts in this series, you may find useful:

Generic Host Builder for Web Apps in 3.x

Going forward, ASP .NET Core 3.0 will allow you to use the updated Generic Host Builder instead of the Web Host Builder in your web apps. As of Preview 2, the templates available in ASP .NET Core 3.0 have already been updated to include the Generic Host Builder.

At a minimum, the Main() method of your .NET Core 3.0 web app would now look like the following:

public static void Main(string[] args)
{
   CreateHostBuilder(args)
      .Build()
      .Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
   Host.CreateDefaultBuilder(args)
      ConfigureWebHostDefaults(webBuilder =>
      {
         webBuilder.UseStartup<Startup>();
      });

Here’s an expanded representation of the CreateHostBuilder() method:

public static IHostBuilder CreateHostBuilder(string[] args)
{
   return Host.CreateDefaultBuilder(args)
      ConfigureWebHostDefaults(webBuilder =>
      {
         webBuilder.UseStartup<Startup>();
      });
}

This CreateHostBuilder() method in the 3.0 template looks very similar to the 2.x call to CreateWebHostBuilder() mentioned in the previous section. In fact, the main difference is that the call to WebHost.CreateDefaultBuilder() is replaced by Host.CreateDefaultBuilder(). Using the CreateDefaultBuilder() helper method makes it very easy to switch from v2.x to v3.0.

Another difference is the call to ConfigureWebHostDefaults(). Since the new host builder is a Generic Host Builder, it makes sense that we have to let it know that we intend to configure the default settings for a Web Host. The ConfigureWebHostDefaults() method does just that.

Going forward, it’s important to know the following:

  • WebHostBuilder will be deprecated and then removed in the near future
  • However, the IWebHostBuilder interface will remain
  • You won’t be able to inject just any service into the Startup class…
  • … instead, you have IHostingEnvironment and IConfiguration

If you’re wondering about the reason for the limitation for injecting services, this change prevents you from injecting services into the Startup class  before ConfigureServices() gets called.

References

 

 

Forms and Fields in ASP .NET Core

By Shahed C on February 13, 2019

This is the sixth of a series of posts on ASP .NET Core for 2019. In this series, we’ll cover 26 topics over a span of 26 weeks from January through June 2019, titled A-Z of ASP .NET Core!

ASPNETCoreLogo-300x267 A – Z of ASP .NET Core!

In this Article:

This article will refer to the following sample code on GitHub:

Web Forms And Bindings: https://github.com/shahedc/FormsAndBindings

F is for Forms (and Fields)

Before Tag Helpers were available, you would have to use HTML Helper methods to create forms and their elements in a ASP .NET Core views. This meant that your form could look something like this:

@using (Html.BeginForm())
{
   <input />
}

With the introduction of Tag Helpers, you can now make your web pages much more cleaner. In fact, Tag Helpers work with both MVC Views and Razor Pages. The syntax is much simpler:

<form method="post">

This looks like HTML because it is HTML. You can add additional server-side attributes within the <form> tag for additional features.

<form asp-controller="ControllerName" asp-action="ActionMethod" method="post">

In this above example, you can see how the attributes asp-controller and asp-action can be used to specify a specific controller name and action method. When these optional attributes are omitted, the current controller and default action method will be used.

Optionally, you can also use a named route, e.g.

<form asp-route="NamedRoute" method="post">

The asp-route attribute will look for a specific route with the name specified. When the form is submitted via HTTP POST, the action method will then attempt to read the form values via a passed values or bound properties.

In a Controller’s class file within an MVC app, you can set an optional Name for your action method’s Route attribute, as shown below:

[Route("/ControllerName/ActionMethod", Name = "NamedRoute")]
public IActionResult ActionMethod()
{
}

While you won’t find new Tag Helper equivalents for each and every HTML Helper you may have used in the past, you should consider using a Tag Helper wherever possible. You can even create your own custom Tag Helpers as well. For more information on custom Tag Helpers, check out the official documentation:

Tag Helpers for HTML form elements

Below is a list of Tag Helpers with their corresponding HTML form elements:

Input Tag Helper

Let’s say you have a model with a couple of fields:

public class MyModel
{
   public string MyProperty1 { get; set; }
   public string MyProperty2 { get; set; }
}

You can use the following syntax to use an Input Tag Helper with an expression name assigned to the asp-for attribute. This allows you to refer to the properties without requiring the “Model.” prefix in your Views and Pages.

@model MyModel 
...
<!-- Syntax -->
<input asp-for="<Expression Name>" />
...
<!-- Examples -->
<input asp-for="MyProperty1" />
<input asp-for="MyProperty2" />

Corresponding to the Input Tag Helper, there are existing HTML Helpers, with some differences:

  • Html.TextBox: doesn’t automatically set the type attribute
  • Html.TextBoxFor: also doesn’t automatically set the type attribute; strongly typed
  • Html.Editor: suitable for collections, complex objects and templates (while Input Tag Helper is not).
  • Html.EditorFor: also suitable for collections, complex objects and templates; strongly typed

Since Input Tag Helpers use an inline variable or expression in your .cshtml files, you can assign the value using the @ syntax as shown below:

@{
   var myValue = "Some Value";
 }
 <input asp-for="@myValue" />

This will generate the following textbox input field:

<input type="text" id="myValue" name="myValue" value="Some Value" />

To create more specific fields for email addresses, passwords, etc, you may use data-type attributes on your models to auto-generate the necessary fields. These may include one of the following enum values:

  • CreditCard
  • Currency
  • Custom
  • Date
  • DateTime
  • Duration
  • EmailAddress
  • Html
  • ImageUrl
  • MultilineText
  • Password
  • PhoneNumber
  • PostalCode
  • Text
  • Time
  • Upload
  • Url
// For example: 
[DataType(DataType.Date)]
public DateTime DateOfBirth { get; set; }

Note that each attribute can be applied on a field for the view/page generator to infer the data type, but is not used for data validation. For validation, you should use the appropriate validation techniques in your code. We will cover validation in a future blog post, but you can refer to the official docs for now:

Checkboxes

Any boolean field in your model will automatically be turned into a checkbox in the HTML form. There is no extra work necessary to specify that the input type is a “checkbox”. In fact, the generated HTML includes the “checkbox” type automatically, sets the “checked” property if checked and wraps it in a label with the appropriate caption. For example, imagine a boolean field named “IsActive”:

// boolean field in a model class
public bool IsActive { get; set; }
<!-- input field in page/view wrapped in label -->
<label class="form-check-label">
   <input class="form-check-input" asp-for="IsActive" /> 
   @Html.DisplayNameFor(model => model.IsActive)
</label>
<!-- HTML generated for boolean field -->
<label class="form-check-label">
<input 
   class="form-check-input" 
   type="checkbox" 
   checked="checked" 
   data-val="true" 
   data-val-required="The IsActive field is required." 
   id="IsActive" 
   name="IsActive" 
   value="true"> IsActive
</label>

Hidden Fields

In case you’re wondering how you can generate a hidden <input> field, you can simply use the [HiddenInput] attribute on your hidden field’s property, as shown below. If you wish, you can explicitly set “type=hidden” in your Page/View, but I prefer to set the attribute in the model itself.

// hidden property in model class
[HiddenInput] 
public string SomeHiddenField { get; set; } = "Some Value";
<!-- hidden field in page/view -->
<input asp-for="SomeHiddenField" /> 
<!-- HTML generated for hidden field --> 
<input type="hidden" id="SomeHiddenField" name="SomeHiddenField" value="Some Value">

Radio Buttons

For radio buttons, you can create one <input> tag for each radio button option, with a reference to a common field, and a unique value for each radio button. Each input element can be wrapped in a label to include a proper (clickable) text caption . You can generate these in a loop or from a collection from dynamically generated radio buttons.

// string property in model class
public string ExperienceLevel { get; set; }
<!-- input fields for radio buttons in page/view -->
<label><input asp-for="ExperienceLevel" value="N" type="radio" />Novice</label> 
<label><input asp-for="ExperienceLevel" value="B" type="radio"/>Beginner</label> 
<label><input asp-for="ExperienceLevel" value="I" type="radio" />Intermediate</label> 
<label><input asp-for="ExperienceLevel" value="A" type="radio"/>Advanced</label>
<!-- HTML generated for radio buttons --> 
<label><input value="N" type="radio" id="ExperienceLevel" name="ExperienceLevel">Novice</label>
<label><input value="B" type="radio" id="ExperienceLevel" name="ExperienceLevel">Beginner</label>
<label><input value="I" type="radio" id="ExperienceLevel" name="ExperienceLevel">Intermediate</label>
<label><input value="A" type="radio" id="ExperienceLevel" name="ExperienceLevel">Advanced</label>

Textarea Tag Helper

The multiline <textarea> field can be easily represented by a Textarea Tag Helper. This is useful for longer strings of text that need to be seen and edited across multiple lines.

public class MyModel
{
   [MinLength(5)]
   [MaxLength(1024)]
   public string MyLongTextProperty { get; set; }
}

As you would expect, you can use the following syntax to use a Textarea Tag Helper with an expression name assigned to the asp-for attribute.

@model MyModel 
...
<textarea asp-for="MyLongTextProperty"></textarea>

This will generate the following textarea input field:

<textarea 
   data-val="true" 
   data-val-maxlength="The field ... maximum length of '1024'." 
   data-val-maxlength-max="1024" 
   data-val-minlength="The field ... minimum length of '5'." 
   data-val-minlength-min="5" 
   id="MyLongTextProperty" 
   maxlength="1024" 
   name="MyLongTextProperty"
></textarea>

Note that the property name and its attributes are used to create that textarea with the necessary id, name, maxlength and data validation settings.

Corresponding to the Textarea Tag Helper, the existing HTML Helper is shown below:

  • Html.TextAreaFor

Label Tag Helper

The <label> field can be represented by a Label Tag Helper. A label usually goes hand-in-hand with a specific <input> field, and is essential in creating text captions for more accessible web applications. The Display attribute from your model’s fields are used for the label’s displayed text values. (You could use the DisplayName attribute instead and omit the Name parameter, but it limits your ability to use localized resources.)

public class MyModel
{
   [Display(Name = "Long Text")]
   public string MyLongTextProperty { get; set; }
}

You can use the following syntax to use a Label Tag Helper along with an Input Tag Helper.

@model MyModel 
...
<label asp-for="MyLongTextProperty"></label>
<input asp-for="MyLongTextProperty" />

This will generate the following HTML elements:

<label for="MyLongTextProperty">Long Text</label>
<input type="text" id="MyLongTextProperty" name="MyLongTextProperty" value="">

Note that the property name and its attributes are used to create both the label with its descriptive caption and also the input textbox with the necessary id and name.

Corresponding to the Label Tag Helper, the existing HTML Helper is shown below:

  • Html.LabelFor

Select Tag Helper

The <select> field (with its nested <option> fields) can be represented by a Select Tag Helper. This visually represents a dropdown or listbox, from which the user may select one or more options. In your model, you can represent this with a List<SelectListItem> of items, made possible by the namespace Microsoft.AspNetCore.Mvc.Rendering.

...
using Microsoft.AspNetCore.Mvc.Rendering;

public class MyModel
{
   public string MyItem { get; set; }
   
   public List<SelectListItem> MyItems { get; } = new List<SelectListItem>
   {
      new SelectListItem { Value = "Item1", Text = "Item One" },
      new SelectListItem { Value = "Item2", Text = "Item Two" },
      new SelectListItem { Value = "Item3", Text = "Item Three" },
   };
}

You can use the following syntax to use a Select Tag Helper.

@model MyModel 
...
<select asp-for="MyItem" asp-items="Model.MyItems"></select>

Note that the asp-items attribute does require a “Model.” prefix, unlike the asp-for attribute that we have been using so far. This will generate the following HTML:

<select id="MyItem" name="MyItem">
   <option value="Item1">Item One</option>
   <option value="Item2">Item Two</option>
   <option value="Item3">Item Three</option>
</select>

Note that the property name and its attributes are used to create both the dropdown list and also the nested options available for selection. For more customization, optgroups and multiple selections, check out the “Select Tag Helper” section in the Tag Helpers documentation at:

Corresponding to the Select Tag Helper, the existing HTML Helpers are shown below:

  • Html.DropDownListFor
  • Html.ListBoxFor

MVC Sample

In the sample repo, you’ll find an MVC web project with various models, views and controllers.

  • Models: In the “Models” folder, you’ll find a Human.cs class (shown below) with some fields we will use to display HTML form elements.
  • Views: Within the “Views” subfolder, the “Human” subfolder contains auto-generated views for the HumanController’s methods, while the “Attr” subfolder contains simple Index and Detail views to be used by the AttrController class.
  • Controllers: The HumanController class was auto-generated for the Human model, while the AttrController class was manually written to illustrate the use of various attributes (e.g. FromQuery, FromRoute, FromForm) within the action methods.
public class Human
{
   public int Id { get; set; }
   public string FirstName { get; set; }
   public string LastName { get; set; }
   public string Address { get; set; }
   public DateTime DateOfBirth { get; set; }
   public int FavoriteNumber { get; set; }
   public bool IsActive { get; set; }
}

Take a look at the Create and Edit views for the Human class, and you’ll recognize familiar sets of <label> and <input> fields that we discussed earlier.

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

To bind these fields, the Create and Edit methods that respond to HTTP POST both use a [Bind] attribute for the Human parameter with specific fields to bind from the model:

[HttpPost]
public async Task<IActionResult> Create(
   [Bind("Id,FirstName,LastName,Address,DateOfBirth,FavoriteNumber,IsActive")] Human human)

... 
[HttpPost]
public async Task<IActionResult> Edit(
   int id, 
   [Bind("Id,FirstName,LastName,Address,DateOfBirth,FavoriteNumber,IsActive")] Human human)

In the AttrController class, you’ll find a different approach to gathering information submitted in an HTML form.

  • FromQuery: The Index() method uses a [FromQuery] attribute which can obtain information from the URL’s QueryString parameters
  • FromRoute: The first Details() method uses a [FromRoute] attribute which can obtain information from route values.
  • FromForm: The second Details() method uses a [FromForm] attribute which links the submitted form fields to the fields of the corresponding model passed to the method.
[HttpGet]
public IActionResult Index([FromQuery] string humanInfo) { }
...
[HttpGet]
public IActionResult Details([FromRoute] int id) { }
...
[HttpPost]
public IActionResult Details([FromForm] Human human) { }
...

The first two methods above can be reached via HTTP GET requests by accessing a URL, while the third one can be reached when submitting the Details form with a submit button. Run the sample application to click around and view its behavior.

Forms-Bindings

 

<!-- From shared _Layout.cshtml, link to Index page -->
<a 
   class="nav-link text-dark" 
   asp-area="" 
   asp-controller="Attr" 
   asp-action="Index" 
   asp-route-humanInfo="John"
>Attributes</a>

<!-- From Index.cshtml, link to Details page -->
<a 
   class="nav-link text-dark" 
   asp-area="" 
   asp-controller="Attr" 
   asp-action="Details" 
   asp-route-id="1"
>View Details</a>
<!-- From Details.cshtml, self-submitting form -->
<form asp-action="Details">
...
<input type="submit" value="Submit" class="btn btn-primary" />
...
</form>

Note that <a> tags have an asp-route-xx attribute that can have any text value appended to the end of it. These route parameters, e.g. id and humanInfo, correspond directly to action method parameters seen in Index() and Details() methods of the AttrController class.

Razor Pages with BindProperty

Compared to MVC views, the newer Razor Pages make it a lot easier to bind your model properties to your HTML forms. The [BindProperty] attribute can be applied to MVC Controllers as well, but is much more effective within Razor Pages.

In the sample repo, you’ll find a Razor web project with multiple subfolders, including Models and Pages.

  • Models: In the “Models” folder of the Razor web project, you’ll find a Human.cs class (shown below) with some fields we will use to display HTML form elements.
  • Pages: Within the “Pages” subfolder, the “Human” subfolder contains auto-generated Razor Pages along with corresponding .cs classes that contain the necessary Get/Post methods.

Here, the HumanModel class is named slightly differently from the aforementioned (MVC) example’s Human class, since the subfolder within Pages is also called Human. (We would have to rename one or the other to avoid a naming conflict.)

 public class HumanModel
 {
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Address { get; set; }
    public DateTime DateOfBirth { get; set; }
    public int FavoriteNumber { get; set; }
    public bool IsActive { get; set; }
 }

This time, take a look at the Create and Edit pages for the Human class, and you’ll once again recognize familiar sets of <label> and <input> fields that we discussed earlier.

 <div class="form-group">
    <label asp-for="HumanModel.FirstName" class="control-label"></label>
    <input asp-for="HumanModel.FirstName" class="form-control" />
    <span asp-validation-for="HumanModel.FirstName" class="text-danger"></span>
 </div>

Since there are no controller classes in the Razor web project, let’s take a look at the corresponding C# classes for the Create and Edit pages, i.e. Create.cshtml.cs and Edit.cshtml.cs. In both of these classes, we’ll find the [BindProperty] attribute in use, right after the constructor and before the Get/Post methods.

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

This [BindProperty] attribute allows you to declaratively bind the HumanModel class and its properties for use by the HTML form in the corresponding Razor Page. This is an opt-in feature that allows to choose which properties to bind. If you wish, you could alternatively bind all public properties in the class by using the [BindProperties] attribute above the class, instead of above each individual member.

NOTE: By default, a Razor Page’s default methods for HTTP GET and HTTP POST are OnGet() and OnPost() respectively. If you wish to use custom page handlers in your HTML forms, you must create custom methods with the prefix OnPost followed by the name of the handler (and optionally followed by the word Async for async methods)

<!-- buttons with custom page handlers --> 
<input type="submit" asp-page-handler="Custom1" value="Submit 1" />
<input type="submit" asp-page-handler="Custom2" value="Submit 2" />
// action methods in .cs file associated with a Razor Page 
public async Task<IActionResult> OnPostCustom1Async() { } 
public async Task<IActionResult> OnPostCustom2sync() { }

The standard set of Get/Post methods are shown below, from Create.cshtml.cs:

public IActionResult OnGet()
{
   return Page();
}
public async Task<IActionResult> OnPostAsync()
{
   if (!ModelState.IsValid)
   {
      return Page();
   } 
   _context.HumanModel.Add(HumanModel);
   await _context.SaveChangesAsync(); 
   return RedirectToPage("./Index");
}

Note that the HumanModel is passed to the DB Context to add it to the database. If you were to remove the aforementioned [BindProperty] attribute, HumanModel would be null and the save operation would fail. The above approach only opts in to accepting HTTP POST requests. To enable use of BindProperty for HTTP GET requests as well, simply set the optional parameter SupportsGet to true, as shown below.

[BindProperty(SupportsGet = true)]

References