Showing posts with label ASP.NET Core. Show all posts
Showing posts with label ASP.NET Core. Show all posts

Tuesday, August 1, 2023

ASP.NET Core - Create, Read and Delete Cookie

Project
  • Open the Visual Studio. 
  • Create web project using ASP.NET Core Empty template.
  • Project name: CookieCreateDeleteEtc
  • Solution name: CookieCreateDeleteEtc
  • .NET Core version: 5.0
  • Add the AddRazorPages() into the IServiceCollection in Startup class.
  • Use MapRazorPages() as terminal middleware in Startup class.
  • Add a new folder in project root folder and rename it Pages. 
  • Add a Razor Page in Pages folder and name it Index. 
Look at the below updated code in Startup class.
Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace CascadingDropdownsRP
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
        }
    }
}
Update the Index.cshtml.
Index.cshtml

@page
@model CookieCreateDeleteEtc.Pages.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<style>
    .ck {
        font-size: 20px;
        color: blue;
        font-family: Impact;
    }
</style>
<div style="margin-left:30px;background-color:lightyellow;">
    <h1>Learn about cookies</h1>
    <label class="ck" style="color:red;">@ViewData["status"]</label><br /><br />
    <label class="ck" style="color:red;">@ViewData["info"]</label><br /><br />
    <a class="ck" asp-page-handler="Create">Create cookie</a><br />
    <a class="ck" asp-page-handler="Read">Read cookie</a><br />
    <a class="ck" asp-page-handler="Delete">Delete cookie</a><br />
</div>
Index.cshtml.cs
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System;

namespace CookieCreateDeleteEtc.Pages
{
    public class IndexModel : PageModel
    {
        private CookieOptions Cop
        {
            get
            {
                CookieOptions cop = new CookieOptions();
                cop.IsEssential = true;
                cop.MaxAge = TimeSpan.FromDays(2);
                cop.Expires = DateTime.Now.AddDays(1);
                cop.Path = "/";
                cop.Secure = true;
                cop.HttpOnly = true;
                cop.SameSite = SameSiteMode.None;
                return cop;
            }
        }
        public void OnGetCreate()
        {
            HttpContext.Response.Cookies.Append("mycookie", "cookie1", Cop);
            ViewData["status"] = "Cookie is created.";
            var cookieData = HttpContext.Response.Headers["Set-Cookie"].ToString();
            ViewData["info"] = cookieData;
        }
        public void OnGetRead()
        {
            var cuki = HttpContext.Request.Cookies["mycookie"];
            var status = string.IsNullOrEmpty(cuki) ? "missing." : cuki;
            //HttpContext.Request.Cookies.TryGetValue("mycookie", out string cuki2);
            ViewData["status"] = "Cookie is " + status;
            var cookieData = HttpContext.Response.Headers["Set-Cookie"].ToString();
            ViewData["info"] = cookieData;
        }
        public void OnGetDelete()
        {
            HttpContext.Response.Cookies.Delete("mycookie");
            ViewData["status"] = "Cookie is deleted.";
        }
    }
}
Result.

ASP.NET Core - Cookie Consent using ITrackingConsentFeature in Razor page

As we know that configuration done in Startup class is global in the app, we configure for cookie consent in the Startup class. 
Step1. To create cookie consent in .NET Core, we must configure the service for CookiePolicyOptions in ConfigureServices method of Startup class. This option is to check if consent policy should be evaluated on the request.
Step2. Add UseCookiePolicy middleware after UseRouting middleware in the Startup class.
Remaining steps are explained in the project.

Project
  • Open the Visual Studio. 
  • Create web project using ASP.NET Core Empty template.
  • Project name: CookieConsent
  • Solution name: CookieConsent
  • .NET Core version: 5.0
  • Add the AddRazorPages() into the ConfigureServices in Startup class.
  • Configure for CookiePolicyOptions in ConfigureServices as given in the code below. 
  • Use MapRazorPages() as terminal middleware in Startup class.
  • Use UseCookiePolicy() after UseRouting middleware in Startup class.
Look at the below updated code in Startup class.
Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace CookieConsent
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
            services.Configure<CookiePolicyOptions>(op =>
            {
                //Checks if consent policies should be evaluated on this request.
                op.CheckConsentNeeded = context => true;    
            });
        }
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseRouting();
            app.UseCookiePolicy();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
        }
    }
}
Add a new folder and rename it Pages. Add a Razor Page in Pages folder and name it Index. Two files are generated which are Index.cshtml and Index.cshtml.cs. Update the Index.cshtml. The consent cookie is created by client using JavaScript. We pass the consent string sent by server to document.cookie.This single line code of JavaScript is able to create cookie but the cookie string must be valid. If you want to see the consent string value then you can pass it to ViewData and can display on the razor page. By default, the expiry is 1 year after the cookie creation date. Note that model class is not needed in this project. We can delete content of that page.

Index.cshtml
@page
@using Microsoft.AspNetCore.Http.Features;

@{
    var TrackingConsentFeature = HttpContext.Features.Get<ITrackingConsentFeature>();
    var IsSuccess = TrackingConsentFeature?.CanTrack ?? false;
    string ConsentCookieString = TrackingConsentFeature?.CreateConsentCookie();
}

@if (!IsSuccess)
{
    <div id="banner1" style="background-color:lightyellow;margin:auto;border:solid green 3px;padding:10px;">
        This site uses cookies. Please click Accept button.
        <button id="accept">Accept</button>
    </div>
    <script type="text/javascript">
        (function () {
            var anchr = document.getElementById('accept');

            anchr.addEventListener("click", function () {
                // create cookie and store consent cookie data
                document.cookie = "@ConsentCookieString";
                banner1.style.display = "none";
            });

        })()
    </script>
}
<h1>Learn ITrackingConsentFeature in .NET Core</h1>
Run the application. We get the consent banner.
Click the Accept button. The cookie will be saved in the browser.

Monday, July 31, 2023

ASP.NET Core - Cookie authentication without ASP.NET Core Identity


In this tutorial we will learn about cookie authentication without ASP.NET Core Identity. First, we create an empty ASP.NET application with a razor page.

Project

  • Open the Visual Studio. 
  • Create web project using ASP.NET Core Empty template.
  • Project name: CookieAuthDemo
  • Solution name: CookieAuthDemo
  • .NET Core version: 5.0
  • Add the AddRazorPages() into the IServiceCollection in Startup class.
  • Use MapRazorPages() as terminal middleware in Startup class.
Look at the below updated code in Startup class.
Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace CascadingDropdownsRP
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
        }
    }
}

Add Pages folder in the root directory of the application and create a razor page named Index in it. 

Write some HTML tags for 'Hello Cookie' text in the Index page and run the application we get the Hello Cookie printed in the browser. Again add another folder named as Admin in the Pages folder. And inside this folder create another Index page, write some HTML tag in this page also and run the application. We reach to the Index page of Admin folder when we use the URL pointing to the Admin folder. Till now there is no restriction to use the Admin folder and its pages. Any user can access pages of this folder.

Restrict Access of folder

To restrict the access of this folder we use RazorPageOptions as given in the following code. The
RazorPageOptions provides options to authorize a razor page or folder or any folder in areas.


public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages(opt =>
            {
                opt.Conventions.AuthorizeFolder("/admin");
            });
        }
AuthorizeFolder, AuthorizeAreaFolder, AuthorizePage, AuthorizeAreaPage, AllowAnonymousToPage, AllowAnonymousToAreaPage, AllowAnonymousToFolder, AllowAnonymousToAreaFolder etc are extension methods of PageConventionCollection class. These methods are used to restrict access of a page or folder.
Run the application. We get the following exception

Since folder authorization is set in the Startup class in AddRazorPages but not middleware is added to support the authorization, we get error. To overcome this exception, we add the UseAuthorization middleware.
Run the application.We get the following exception.


Now add the following line of code  in ConfigureServices in Startup class which implies that authentication service is added in the app which is based on cookie authentication. But it is not enough because it just tells about authentication scheme which can be cookie or token etc. We must add cookie and its description. We will get error.

  services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);

We still get exception because we must add cookie and set its options, if needed.


With out setting cookie options, the default are applied. Cookie options are all about Login Path, Logout Path, Access denied path, cookie name, cookie domain, max age, http only, expire time span, sliding expiration, cookie manager etc. Cookie manager is used to request for a cookie or to delete a cookie etc.

In the following line of code, just cookie is added using AddCookie method.

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
Run the application. We get the following exception.
As we have not set the login path in the cookie, we are getting the default login path- Account/Login. It means that we must have Account/Login page. So, we create Account folder in the Pages folder and add a razor page called Login. Write <h1>Login here</h1> tag in this page and run the application. We get the following result.

Login form. Next task is to create a login form in the Login.cshtml razor page.


@page
@model CookieAuthDemo.Pages.Account.LoginModel


<h1>Login here</h1>

<form method="post">
    <table>
        <tr>
            <td><label>Name</label></td>
            <td><input type="text" /></td>
        </tr>
        <tr>
            <td><label>Password</label></td>
            <td><input type="password" /></td>
        </tr>
        <tr>
            <td colspan="2" style="text-align:right"> <input type="submit" /></td>
        </tr>
    </table>
</form>
<br />

@ViewData["Message"]

Login handler. When user clicks the submit button, the click event will be handled in the backing page of Login page. we write the following code which is without any login test.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;


namespace CookieAuthDemo.Pages.Account
{
    public class LoginModel : PageModel
    {
        public IActionResult OnPost(string returnUrl)
        {
            return LocalRedirect(returnUrl);
        }
    }
}
Run the application. We get the following result when we try to reach the admin folder.
Now fill the any name and password and click the submit button. We don’t reach the OnPost method. We get the following error. Why?
Add the following attribute in form tag.
action="/Pages/Account/Login.cshtml.cs"
Run the application. We get the following error.

Sign-in user class. We will use Tag helpers to overcome these limitations. Create a model class named UserSignin in the Models folder as given below.

namespace CookieAuthDemo.Models
{
    public class UserSignIn
    {
        public string Name { get; set; }
        public string Password { get; set; }
        public string Role { get; set; } 
    }
}

Update the Login page. Create controls by using tag helpers.



@page
@model CookieAuthDemo.Pages.Account.LoginModel
@using CookieAuthDemo.Models;
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h1>Login here</h1>
<form method="post">
    <table>
        <tr>
            <td><label asp-for="UserSignIn.Name"></label></td>
            <td><input type="text" asp-for="UserSignIn.Name" /></td>
        </tr>
        <tr>
            <td><label asp-for="UserSignIn.Password" ></label></td>
            <td><input type="password" asp-for="UserSignIn.Password" /></td>
        </tr>
        <tr>
            <td colspan="2" style="text-align:right"> <input type="submit"  /></td>
        </tr>
        <tr>
            <td colspan="2"><input type="hidden" asp-for="UserSignIn.Role" value="@ViewData["role"]" /></td>
        </tr>
    </table>
</form>

<br />

Click Handler. Update the backing Login Razor page. When user submits the form, OnPost event handler will execute. All the sign-in information is stored in claims identity and is tied with default cookie authentication scheme. Finally, this claims identity is encapsulated inside claims principal. The SignInAsync method takes two parameters, first is authentication scheme and second is principal. SignInAsync creates an encrypted cookie and adds it to the current response. If AuthenticationScheme isn't specified, the default scheme is used.
using CookieAuthDemo.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;

namespace CookieAuthDemo.Pages.Account
{
    public class LoginModel : PageModel
    {
        [BindProperty]

        public UserSignIn UserSignIn { get; set; }

        public async Task<IActionResult> OnPostAsync(string returnUrl)
        {
	    // let username and password matches
            // check it here 
            // then we create claims principal
            List<Claim> claims = new List<Claim>();
            claims.Add(new Claim(ClaimTypes.Name, UserSignIn.Name));
            claims.Add(new Claim("Password", UserSignIn.Password));
            claims.Add(new Claim(ClaimTypes.Role, UserSignIn.Role));

            var ci = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
            var principle = new ClaimsPrincipal(ci);
            await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principle);

            return LocalRedirect(returnUrl);
        }


        public void OnGet()
        {
            ViewData["role"] = "admin";
        }
    }
}

UseAuthentication. One of the important task that is performed by the built-in authentication middleware is to read the cookies and construct the ClaimsPrincipal and update the User object in the HttpContext.

Startup class

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;

using System.Linq;
using System.Threading.Tasks;

namespace CookieAuthDemo
{
    public class Startup
    {

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages(opt =>
            {
                opt.Conventions.AuthorizeFolder("/admin");
            });
            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme
               ).AddCookie();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())

            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();
            app.UseAuthentication();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
        }
    }
}
Admin/Index page. Note that when sign-in is successful, username can be displayed on the page. We use User.Identity.IsAuthenticated to check if user is authenticated or not on any page.

@page
@model CookieAuthDemo.Pages.Admin.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h1>You are on Index page of Admin</h1>
<h3>Is user authenticated: @User.Identity.IsAuthenticated</h3>
<h3>User name: @User.Identity.Name</h3> 

<h1><a asp-page-handler="Signout">Signout</a></h1>
Signout.

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Threading.Tasks;

namespace CookieAuthDemo.Pages.Admin
{
    public class IndexModel : PageModel
    {
        public async Task<IActionResult> OnGetSignoutAsync()
        {
            await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            return RedirectToPage("/Index");
        }
    }
}
Root Index page is also modified.

@page
@model CookieAuthDemo.Pages.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<div style="background-color:cyan;width:40%;margin-left:10px">
    <h3 style="text-align:right;margin-right:10px">Username: @User.Identity.Name</h3>
    <h1>Index of Root page</h1>
    <h3><a asp-page="Admin/Index">Go to Index page of Admin</a></h3>
    <br/>
    <br/>
</div>
Run the app. We get the following UI. On clicking the link, login form appears which is filled by user and button is clicked then Index page of admin opens and Username appears in the top right. We can sign-out on that page to delete the cookie also. Model validation is missing in this code which can be applied in the UserSingIn class.
REMARKS

  • Call UseAuthentication and UseAuthorization to set the HttpContext.User property.
  • AuthenticationScheme passed to AddAuthentication sets the default authentication scheme for the app. AuthenticationScheme is useful when there are multiple instances of cookie authentication and the app needs to authorize with a specific scheme. Setting the AuthenticationScheme to CookieAuthenticationDefaults.AuthenticationScheme provides a value of "Cookies" for the scheme. Any string value can be used that distinguishes the scheme.
  • The CookieAuthenticationOptions class is used to configure the authentication provider options. Configure CookieAuthenticationOptions in the AddCookie method:

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)

    .AddCookie(options =>

    {
        options.ExpireTimeSpan = TimeSpan.FromMinutes(20);
        options.SlidingExpiration = true;
        options.AccessDeniedPath = "/Forbidden/";

    });

  • Use CookiePolicyOptions provided to the Cookie Policy Middleware to control global characteristics of cookie processing and hook into cookie processing handlers when cookies are appended or deleted.
  • To create a cookie holding user information, construct a ClaimsPrincipal. The user information is serialized and stored in the cookie.
  • Create a ClaimsIdentity with any required Claims and call SignInAsync to sign in the user.
  • SignInAsync creates an encrypted cookie and adds it to the current response. If AuthenticationScheme isn't specified, the default scheme is used.


Sign out

To sign out the current user and delete their cookie, call SignOutAsync:

// Clear the existing external cookie

    await HttpContext.SignOutAsync(
        CookieAuthenticationDefaults.AuthenticationScheme);


NOTE: If CookieAuthenticationDefaults.AuthenticationScheme or "Cookies" isn't used as the scheme, supply the scheme used when configuring the authentication provider.


// using Microsoft.AspNetCore.Authentication;

await HttpContext.SignInAsync(

    CookieAuthenticationDefaults.AuthenticationScheme,
    new ClaimsPrincipal(claimsIdentity),
    new AuthenticationProperties
    {
        IsPersistent = true,
        ExpiresUtc = DateTime.UtcNow.AddMinutes(20)
    });

Sunday, July 30, 2023

ASP.NET Core Razor Pages - Authorization of Areas based on Roles

In previous article, we see the concept of Areas in ASP.NET Core Razor Pages. We learnt about creating areas and navigation to any page of any area. In this part, we will see how Authorization can be applied in Areas so that based on the roles of the users, access can be granted or denied for an area and its pages. When an user will try to navigate to any page of  unauthorized area, access will be denied. To achieve this goal, we have to make changes in the configuration of the application. As we know that configuration is done mostly in Startup class, we will update the Startup class as per that.

AddAuthorization method with overloaded options will be added as service in the IServiceCollection.

AuthorizationPolicy represents a collection of authorization requirements and the scheme or schemes they are evaluated against, all of which must succeed for authorization to succeed. AuthorizationOptions class helps to add or get authorization policy. A policy is name added to AuthorizationPolicy class. AuthenticationSchemes are names of schemes in string.

The AddRazorPages method will be overloaded with options. AddRazorPages supports razor pages in the app but using itsoverloaded version allows adding policy on the area folders. We can authorize an area folder with some role for authorization policy.

Authorization refers to the process that determines what a user is able to do. For example, an administrative user is allowed to create a document library, add documents, edit documents, and delete them. A non-administrative user working with the library is only authorized to read the documents.
Authorization is orthogonal and independent from authentication. However, authorization requires an authentication mechanism. Authentication is the process of ascertaining who a user is. Authentication may create one or more identities for the current user.
Authorization types
ASP.NET Core authorization provides a simple, declarative role and a rich policy-based model. When an identity is created it may belong to one or more roles. For example, Tracy may belong to the Administrator and User roles while Scott may only belong to the User role. How these roles are created and managed depends on the backing store of the authorization process. Roles are exposed to the developer through the IsInRole method on the ClaimsPrincipal class. AddRoles must be added to Role services.While roles are claims, not all claims are roles.
Startup class

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace RPWebApp
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            
            services.AddAuthorization(config =>
            {
                // First parameter is policy name
                // Second parameter is action delegate to build policy 
                // with the required role.
                // More than one role can be assigned using param array 
                // RequireUserName, RequireClaim, RequireRole etc. can be used
                // for authentication.
                // Policy string name will be used elsewhere in the app
                // wherever policy is required. e.g. in services.AddRazorPages();
                config.AddPolicy("teacher_policy",
                    authPolicy => authPolicy.RequireRole("Teach"));
                config.AddPolicy("student_policy",
                    authPolicy => authPolicy.RequireRole("Study"));
            });

            services.AddRazorPages(opt =>
            {
                // conventions collection useful in routes
                // param1: areaname, param2: folderpath, param3: policy name
                opt.Conventions.AuthorizeAreaFolder("Teacher","/", "teacher_policy");
                opt.Conventions.AuthorizeAreaFolder("Student","/", "student_policy");
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoint =>
            {
                endpoint.MapRazorPages();
            });
        }
    }
}
Role based Authorization
First of all, we added two policies names in the authorization configuration option and in authorization policy builder we used the roles to build the authorization. We could have used some other things like claim, assertion, username etc. as well instead of role. The AuthorizationPolicyBuilder class is used for this. Second, we used Conventions property of RazorPagesOptions class to get PageConventionCollection class. This class is used to configure folders in Razor pages. The AuthorizeAreaFolder is an extension method of PageConventionCollection.

The authorization policy named teacher_policy will look for a role named Teach.Similarly, the authorization policy named student_policy will look for a role named Study. Already mentioned that in stead of role, username, claim or any other assertion can be used. Note that these authorization policy names are used in AuthorizeAreaFolder method.

Authentication Missing
On running the app and clicking the link to go to teacher page, we get exception as given below. 

This is because of authentication missing in the code. Although role based authorization is set, it must be authenticated when link is clicked. There are different authentication schemes such as cookies or tokens. We can implement any of them in the application.

Add Cookies authentication 
We use the cookies authentication in this app. So, we add the following lines of code in ConfigureServices.
 services.AddAuthentication("Cookies").AddCookie(
                configCookie =>
                {
                    configCookie.LoginPath = "/Login";
                    configCookie.AccessDeniedPath = "/AccessDenied";
                }
                );
Note. We should use CookieAuthenticationDefaults.AuthenticationScheme which returns Cookies in AddAuthentication method.
Run the application, we still on clicking the links get the error as given below.



The reason is that the Login page is missing in the app. Add Login page in the root Pages folder. Design login form in this login page and run the application again. We navigate to the login page when link is clicked.
Note that we have not used app.UseAuthentication(); in the Startup class. It is required when login form is submitted for authentication.

When user fills the name and password in the login form and clicks the submit button. The data is verified from the database. We have not created database. Let name be Ajeet and password be Appliedk. If both matches then user is redirected to a page which is desired by the user. This redirected page is called ReturnUrl.

After successful login, authentication ticket is generated which contains details such as Email, Password etc.

 
We should add UseCookiePolicy middleware in the Configure method before UseAuthentication as given below.

In the OnGet of Teacher IndexModel page, we must implement the logic to check it.


From Stack overflow. The way the authentication stack works in ASP.NET Core is that you can configure a set of authentication schemes. Some of these schemes are meant to be used in combination, for example the cookie authentication scheme is rarely used on its own, but there are also schemes that can be used completely separate (for example JWT Bearer authentication).

Authentication actions
In the authentication world, there are certain actions that you can perform:

Authenticate: To authenticate basically means to use the given information and attempt to authenticate the user with that information. So this will attempt to create a user identity and make it available for the framework.

For example, the cookie authentication scheme uses cookie data to restore the user identity. Or the JWT Bearer authentication scheme will use the token that is provided as part of the Authorization header in the request to create the user identity.

Challenge: When an authentication scheme is challenged, the scheme should prompt the user to authenticate themselves. This could for example mean that the user gets redirected to a login form, or that there will be a redirect to an external authentication provider.

Forbid: When an authentication scheme is forbidden, the scheme basically just responds with something that tells the user that they may not do whatever they attempted to do. This is commonly a HTTP 403 error, and may be a redirect to some error page.

Sign-in: When an authentication scheme is being signed in, then the scheme is being told to take an existing user (a ClaimsPrincipal) and to persist that in some way. For example, signing a user in on the cookie authentication scheme will basically create a cookie containing that user’s identity.

Sign-out: This is the inverse of sign-in and will basically tell the authentication scheme to remove that persistance. Signing out on the cookie scheme will effectively expire the cookie.

What is the difference between DefaultScheme and DefaultSignInScheme?
DefaultSignInScheme : Sets the default scheme to sign in. 
DefaultSignOutScheme : Sets the default scheme to sign out. 
DefaultScheme : Sets the default fallback scheme.

Read the Stack overflow link for all details. Also MSDN
For the user, the normal interaction is through the cookie authentication scheme: When they access the web application, the cookie authentication scheme will attempt to authenticate them using their cookie. So using the cookie authentication scheme as the default scheme for all operations.

ASP.NET Core - Example of CRUD operations in a Single Razor Page

In previous post, we have seen that CRUD operations can be performed by using separate razor pages for each CRUD operation like Create, Delete etc. In this post, we will perform all CRUD operations on a single Razor page called Index in this project. The project folders structure after creating the project will be as shown below.


Project
  • Open the Visual Studio. 
  • Create web project using ASP.NET Core Empty template.
  • Project name: RPExercises
  • Solution name: RPExercises
  • .NET Core version: 5.0
  • Add the AddRazorPages() into the IServiceCollection in Startup class.
  • Use MapRazorPages() as terminal middleware in Startup class.
Look at the below updated code in Startup class.
Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace CascadingDropdownsRP
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
        }
    }
}
  • Add a new folder and rename it Pages.
  • Add a Razor Page in Pages folder and name it Index. Two files are generated which are Index.cshtml and Index.cshtml.cs
  • Add a folder and rename it Models.
  • In Models folder, create a model class called Student.
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RPExercises.Models
{
    public class Student
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }
        [Required]
        [MinLength(1)]
        [MaxLength(20)]
        public string Name { get; set; }
        public bool IsPassed { get; set; }
        [Required]
        public Qualification Degree { get; set; }
    }
    public enum Qualification
    {
        BA, MA, BCA, MCA
    }
} 
Install the EF Core packages by running the following commands.
  • Install-Package Microsoft.EntityFrameworkCore -version 5.0
  • Install-Package Microsoft.EntityFrameworkCore.Design -version 5.0
  • Install-Package Microsoft.EntityFrameworkCore.Tools -version 5.0
  • Install-Package Microsoft.EntityFrameworkCore.SqlServer -version 5.0

Add a folder and rename it Data. In this folder, add AppDbContext class as given below.

using Microsoft.EntityFrameworkCore;
using RPExercises.Models;

namespace RPExercises.Data
{
    public class AppDbContext: DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> options)
       : base(options)
        {
        }
        public DbSet<Student> tblStudents { get; set; }
    }
}
Add Connection String in appsettings.json file.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "ConnectionStrings": {
    "DBCS": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=Appliedk11;Integrated Security=True;Connect Timeout=30;Encrypt=False;Trust Server Certificate=False;Application Intent=ReadWrite;Multi Subnet Failover=False"
  },
  "AllowedHosts": "*"
}

Next step. Update the Startup class to add DbContext service with SQLServer database. .

public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
            services.AddDbContext<AppDbContext>(options =>
            {
                options.UseSqlServer(_cfg.GetConnectionString("DBCS"));
            });
        }
Perform Add-Migration and Update-Database commands in Package Manager Console.

Now update the Index.cshtml file.

@page "{handler?}"
@using RPExercises.Models;
@model RPExercises.Pages.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h2>Create students</h2>
<style type="text/css">
    body {
        margin-left: 8%;
    }

    .myclass {
        padding: 5px;
        border: 2px solid blue;
        background-color: beige;
        width: 30%;
        text-align: left;
    }
</style>
<form method="post" class="myclass">
    <h3> Input student details</h3>
    <label hidden asp-for="student.Id">Id</label>
    <input hidden asp-for="student.Id"  />
    <label asp-for="student.Name" >Name</label>
    <input asp-for="student.Name"  />
    <br />
    <label asp-for="student.IsPassed">IsPassed</label>
    <input asp-for="student.IsPassed"  />
    <br />
    <label asp-for="student.Degree">Highest Degree</label>
    BA<input type="radio" asp-for="student.Degree" value="@Qualification.BA" />
    MA<input type="radio" asp-for="student.Degree" value="@Qualification.MA" />
    BCA<input type="radio" asp-for="student.Degree" value="@Qualification.BCA" />
    MCA<input type="radio" asp-for="student.Degree" value="@Qualification.MCA" />
    <br />
    <br />
    <input type="submit" value="@ViewData["btnLabel"]" style="background-color:green;color:wheat;margin-left:80%" />
</form>

<br />
<hr />
<h2>List students</h2>
Students Count: @Model.students.Count 
<br/>
<table border="1" class="myclass">
    <tr>
        <th>Id</th>
        <th>Name</th>
        <th>Is Passed</th>
        <th>Degree</th>
        <th>Action</th>
    </tr>

    @foreach (Student s in Model.students)
    {
        <tr>
            <td>@s.Id</td>
            <td>@s.Name</td>
            <td>@s.IsPassed</td>
            <td>@s.Degree</td>
            <td>
                <a asp-page-handler="Edit" asp-route-id="@s.Id">Edit</a>|
                <a asp-page-handler="Delete" asp-route-id="@s.Id">Delete</a>
            </td>
        </tr>
    }
</table>
Update the Index.cshtml.cs file.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RPExercises.Data;
using RPExercises.Models;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace RPExercises.Pages
{
    public class IndexModel : PageModel
    {
        [BindProperty]
        public Student student { get; set; }
        public List<Student> students { get; set; } = new List<Student>();
        private readonly AppDbContext ctx;

        public IndexModel(AppDbContext ctx)
        {
            this.ctx = ctx;
        }
        public async Task<IActionResult> OnGet()
        {
            students = await ctx.tblStudents.ToListAsync();
            ViewData["btnLabel"] = "Create";
            return Page();
        }

        public async Task<IActionResult> OnPost()
        {
            if (student.Id==0)
            {
                await ctx.tblStudents.AddAsync(student);
                await ctx.SaveChangesAsync();
                
            }
            else
            {
                Student studentEdit = await ctx.tblStudents.FindAsync(student.Id);
               
                if (HttpContext.Request.Path.Value.ToLower().Contains("edit"))
                {
                    studentEdit.Id = student.Id;
                    studentEdit.Name = student.Name;
                    studentEdit.IsPassed = student.IsPassed;
                    studentEdit.Degree = student.Degree;
                    ctx.Entry(studentEdit).State = EntityState.Modified;
                }
                if (HttpContext.Request.Path.Value.ToLower().Contains("delete"))
                {
                    ctx.Entry(studentEdit).State = EntityState.Deleted;
                }
                    ctx.SaveChanges();

            }
            return RedirectToPage();
        }

        public async Task<IActionResult> OnGetEdit(int id)
        {
            // get data from table
            Student studentEdit = await ctx.tblStudents.FindAsync(id);
            // update form by updating BindProperty
            student = studentEdit;
            students = await ctx.tblStudents.ToListAsync();
            ViewData["btnLabel"] = "Edit";
            return Page();
        }

        public async Task<IActionResult> OnGetDelete(int id)
        {
            // get data from table
            Student studentDelete = await ctx.tblStudents.FindAsync(id);
            // update form by updating BindProperty
            student = studentDelete;
            students = await ctx.tblStudents.ToListAsync();
            ViewData["btnLabel"] = "Delete";
            return Page();
        }
    }
}
Run the application and do CRUD operations. We get the following output.

Wednesday, July 26, 2023

ASP.NET Core Razor Pages with all about Areas and navigation to their pages

Objectives

  • What is Areas in ASP.NET Core
  • What are benefits of creating Areas
  • How to create Areas in ASP.NET Core project
    • In MVC
    • In Razor Page
  • How to navigate pages in and between different areas

Areas

Areas are used to split a big ASP.NET Core project into manageable parts. Each area refers to almost independent entity in the application. Each area has its own Model, Views, Data and Controllers. For example, in a School project, Student, Teacher and Management are separate areas and they can be independently developed. Area can be used in MVC and Web API. Creating areas in project facilitates the division of development works among the developers and their testing etc.

Benefits of Areas

  • Modularity of the application
  • Division of tasks among developers
  • Efficiency gain in development speed

How to Create Areas

In the root of the application, folder named as Areas is created and different areas are created as subfolders inside the Areas folder. Let's see this by creating a project.
  1. Create web project using ASP.NET Core Web App (Model-View-Controller) template. 
  2. Create a new folder in solution explorer and rename it as Areas. Note that it should be named Areas not Area. 
  3. Right click Areas folder (Add > Area...) to add a new Area in it. Use scaffolding to create subfolders in an area automatically. You can create them manually also.

Create subfolders such as Admin, User, Teacher, Student, Management etc. inside inside Areas folder using scaffolding. Each area will have its own data, controllers, views and models folders created automatically, if you use scaffolding. You can do it manually as well.


Routing in case of Areas in MVC

The routing engine will look at the template to select an action from an area. In Startup class, we map a new route with following parameters as given below.
app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "areas",
                    pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });

Navigation In and Between Areas

To navigate inside an area, we follow the old technique as used without Areas. We use anchor tag with asp-page and asp-page-handler attributes if you are using Tag helpers. In case of navigation from one area to another, we must also include as-area attribute in the anchor tag.

Exception in case of Areas

In case of areas, each area has a HomeController and Index action. The application throws exception that it is not able to choose the default Index view. Resolve this issue.

Solution
Use [Area("Admin")] attribute before HomeController in Admin area and [Area("User")] attribute before HomeController in User area. Look at the following code.

[Area("Admin")]
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
    }
NOTE: Now the following URLs will work properly.
  • https://localhost:44337/user/home/
  • https://localhost:44337/user/home/Index
  • https://localhost:44337/admin/home/
  • https://localhost:44337/admin/home/Index
In view, we use asp-area attribute in the form control.

Exercise of Areas in Razor Pages. 

Create a login page with two anchor links in an ASP.NET Core Razor page project. In the root Index page,  add two anchor links. One anchor link navigates the user to Teacher area and another one to Student area.

Solution.

Important. Each area MUST have Pages folder and then any Razor Page in it. Otherwise area will not work in Razor pages. Create Pages folder manually in each area of the Razor page app. At the same time, remember that we can use configuration to change the root folder for Razor pages.
The following example changes the root folder from the default Pages to Content by using AddRazorPagesOptions:

builder.Services.AddRazorPages()
    .AddRazorPagesOptions(options => {
        options.RootDirectory = "/Content";
    });
Or we can use the WithRazorPagesRoot extension method:

builder.Services.AddRazorPages().WithRazorPagesRoot("/Content");

We have added the Index files in Teacher and Student areas including the root of application.

Index.cshtml in Teacher

  
@page
@model RPWebApp.Areas.Teacher.Pages.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h1>Inside Teacher/Pages/Index</h1>
<h5>Each Area MUST have Pages folder and then any Razor Page in it.</h5>

<a asp-page="Teacher">Go to Teacher of  Teacher arae</a>
<br/>
<a asp-page="Index">Go to Index of Teacher area</a>
<br/>
<a asp-area="" asp-page="Index">Go to App Root Index</a>

Index.cshtml in Student

@page
@model RPWebApp.Areas.Student.Pages.IndexModel
@{
}
<h1>Student/Pages/Index</h1>
<h5>Each Area MUST have Pages folder and then any Razor Page in it.</h5>
On clicking the links given in the root Index page we navigate to the desired page.

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

<div class="text-center">
    <h5> Root Index Page.</h5>
</div>

<div>
    <h5>Each Area MUST have Pages folder and then any Razor Page in it.</h5>
    <a asp-area="Teacher" asp-page="/Index">Teacher Area</a>
    <br />
    <a asp-area="Student" asp-page="/Index">Student Area</a>
</div>


In the next part we will see how Authorization can be applied in Areas so that based on the roles of the users, access can be granted or denied for an area.

ASP.NET Core Web API Ecommerce Project and API testing in Postman Part-2

In PART-I, we have used domain classes directly in the controller class without using DTO.
This is second part of the ASP.NET Core Web API Ecommerce Project. In this post we will see the requirement of DTO and will amend the previous code using DTOs.


What is DTO? 
  • DTO stands for Data Transfer Object. It is a class that sits between controller class and domain class. 
  • DTO transfers data either from controller class to domain class or from domain class to controller class. Data of DTO class and domain class are mapped before data transfer. Data mapping can be done manually or by using package like AutoMapper.
  • Relation between Domain model and DTO is one-to-one. By convention, the name of a DTO class should be suffixed with Dto. DTO is an intermediary and temporary object which is used to transfer data but domain class is stable object which properties are fixed. For different kinds of data transfer, we need to create different DTO classes. The same is not true with domain classes. 
  • Domain classes are used by EF Core DbContext to map database tables with the domain classes. Tables is database are stable structure, similarly domain classes are stable structure. Same is not true with DTO so DTO cannot be used in DbContext.
  • DTO is POCO i.e. Plain old CLR object. DTO class has no complex logic nor any method. It has just getter setter properties.


Why is DTO needed? 
  • To provide a subset of domain model class. 
  • To provide a superset of domain model class. 
  • To hide and secure important properties of a domain model class. 
  • To display data on a user form
  • To send data from user form to the controller and ultimately to the DAL(Data access layer).
Does DTO provide data validation? 
ASP.NET Core provides data validation out-of-the-box. As a developer we have to use DataAnnotation for this purpose. Annotating properties of a DTO provides data validation out-of-the-box. 

Project updates

Create Dtos folder in the EShoppingApi project and add a ProductDto class.cs file. Update the ProductsController in the light of ProductDto class.

ProductDto.cs

namespace EShoppingApi.Dtos
{
    public class ProductDto
    {
        public string Name { get; set; }
        public string Description { get; set; }
        public double Price { get; set; }
    }
}

ProductsController.cs

using EShopping.Domain.Models;
using EShoppingApi.Dtos;
using EShoppingApi.Interfaces;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Data;
using System.Linq;


namespace EShoppingApi.Controllers
{
    [Route("api/[controller]")]
    public class ProductsController : Controller
    {
        private readonly ISeeder seeder;

        public ProductsController(ISeeder seeder)
        {
            this.seeder = seeder;
        }

        [HttpGet]
        public IActionResult GetAllProducts()
        {
            List productDtos = new List();

            var products = seeder.GetAllProducts();
            // map dto to product
            foreach (var p in products)
            {
                var dto = new ProductDto();
                dto.Name = p.Name;
                dto.Price = p.Price;
                dto.Description = p.Description;
                productDtos.Add(dto);
            }
            // user gets productDtos not products
            return Ok(productDtos);
        }

        [HttpGet]
        [Route("{id}")]
        public IActionResult GetProductById(int id)
        {
            var products = seeder.GetAllProducts();
            var product = products.Where(p => p.Id == id).FirstOrDefault();
            if (product == null)
            {
                return NotFound();
            }
            // map product to productDto and return productDto
            ProductDto productDto = new ProductDto();
            productDto.Name = product.Name;
            productDto.Price = product.Price;
            productDto.Description = product.Description;
            return Ok(productDto);
        }

        [HttpPost]
        public IActionResult CreateProduct([FromBody] ProductDto productDto)
        {
            Product product = new Product();
            // map productDto to product
            product.Name = productDto.Name;
            product.Price = productDto.Price;
            product.Description = productDto.Description;
            product.Id = 3; // hard-coded. In real database Key is auto-incremented.
            //  and add product 
            var success = seeder.AddProduct(product);
            if (success)
            {
                return Created("https://localhost:5001/api/products/{product.Id}", product);
            }
            return BadRequest("Failed to add product.");
        }

        [HttpDelete]
        [Route("{id}")]
        public IActionResult DeleteProduct(int id)
        {
            var success = seeder.RemoveProduct(id);
            if (success)
            {
                return Ok("Deleted product");
            }
            return BadRequest("Failed to delete product.");
        }

        [HttpPut]
        [Route("{id}")]
        public IActionResult UpdateProduct([FromBody] ProductDto productDto, int id)
        {
            Product product = new Product()
            {
                Name = productDto.Name,
                Price = productDto.Price,
                Description = productDto.Description
            };
            var success = seeder.UpdateProduct(id, product);
            if (success)
            {
                return Ok("Product updated successfully.");
            }
            return BadRequest("Failed to update product.");
        }
    }
}
Note. We have just created DTO class and updated the ProductsController but NOT updated ISeeder.cs, Seeder.cs and Startup.cs files. The result will be same as before but using DTO provides all the benefits as mentioned above.

We have used a single DTO called ProductDto for all Http verbs. It is not good practice. For different Http verbs, we should create different Dto objects such as ProductDtoGet, ProductDtoPost, ProductDtoPut etc.

What is difference between Get DTO and Post DTO in web API?The Get DTO object is used to send data from server to the client (e.g. Postman or Browser). On the other hand, Post DTO object is used to send data from client (e.g. Postman or Browser) to server.

Run the application and test in Postman. We get the same result as before in Part I.
We modify the ProductDto as per Http Verbs then the code will be as follows.


ASP.NET Core Web API Ecommerce Project and API testing in Postman

Project
  • Open the Visual Studio. 
  • Create web project using ASP.NET Core Web API template.
  • Project name: EShoppingApi
  • Solution name: EShoppingApi
  • .NET Core version: 5.0
  • Delete WeatherForecastController.cs file from Controllers folder.
  • Also delete WeatherForecas.cs file from root folder.
  • Run the application. We get URL like https://localhost:44330/weatherforecast and some 404 error message because weather controller is deleted.
  • The port number will vary system to system. From where this URL is decided? Open the launchsettings file which is inside Properties folder. It contains launchUrl property in Profiles.
Look at the Startup class. It contains AddControllers service which is suitable for API. The MapControllers method helps to map an endpoint to a controller. Attribute Routing is usually followed.
Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace EShoppingApi
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {

            services.AddControllers();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}
  • In API application, we need domain classes. We create a separate class library project for this.
  • Create class library project named as EShopping.Domain. Delete the Class1.cs file.
  • Add a new folder in this project and rename it as Models. Add a class file called Product in it.
Product.cs

namespace EShopping.Domain.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public double Price { get; set; }
    }
}

Save all. We reference the class library project in Api project. Add ProductsController in Controllers folder. Note that it should be API controller not MVC controller. We get the following.


using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace EShoppingApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ProductsController : ControllerBase
    {
    }
}

Before going ahead, create a seeder so that product data is available when we use ProductsController. Seeder class provides dummy data of Product model class. For this, we create Services and Interfaces folder in EShoppingApi project. In the Interfaces folder, create ISeeder.cs interface. The purpose of creating ISeeder interface is get dependency inversion so that Seeder will be available in any controller class via dependency injection.

ISeeder.cs


using EShopping.Domain.Models;
using System.Collections.Generic;

namespace EShoppingApi.Interfaces
{
    public interface ISeeder
    {
        List GetAllProducts();
        bool AddProduct(Product product);
        bool RemoveProduct(int pid); // product id
        bool UpdateProduct(int id, Product product);
    }
}
 

Seeder.cs Implement these methods of ISeeder in Seeder class created in Services folder.


using EShopping.Domain.Models;
using EShoppingApi.Interfaces;
using System.Collections.Generic;
using System.Linq;
using System.Security;

namespace EShoppingApi.Services
{
    public class Seeder : ISeeder
    {
        List<Product> products = new List<Product>();
        public bool AddProduct(Product product)
        {
            products.Add(product);
            return true;
        }

        public List<Product> GetAllProducts()
        {

            Product product1 = new Product()
            {
                Id = 1,
                Name = "Sony Earbuds T5.3",
                Price = 1250.4,
                Description = "Good quality"
            };
            Product product2 = new Product()
            {
                Id = 2,
                Name = "Boat Earbuds T5.2",
                Price = 999.4,
                Description = "Average quality"
            };
            products.Add(product1);
            products.Add(product2);
            return products;
        }

        public bool RemoveProduct(int pid)
        {
            var product = products.Where(p => p.Id == pid).FirstOrDefault();
            products.Remove(product);
            return true;
        }

        public bool UpdateProduct(int id, Product updatedProduct)
        {
            var oldprod = products.FirstOrDefault(p => p.Id == id);
            if (oldprod != null)
            {
                products.Remove(oldprod);
                products.Add(updatedProduct);
                return true;
            }
            return false;
        }
    }
}

Register the Seeder service in Startup class as given below.


 public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddSingleton();
        }
Important: Without AddSingleton method, seeding will not work when we add a new product in the collection of products.

Save all and rebuild the solution. Now, update the ProductsController class as given below.


using EShopping.Domain.Models;
using EShoppingApi.Interfaces;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Linq;

namespace EShoppingApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ProductsController : ControllerBase
    {
        private readonly ISeeder seeder;

        public ProductsController(ISeeder seeder)
        {
            this.seeder = seeder;
        }

        [HttpGet]
        public IActionResult GetAllProducts()
        {
            var products = seeder.GetAllProducts();

            return Ok(products);
        }

        [HttpGet]
        [Route("{id}")]
        public IActionResult GetProductById(int id)
        {
            var products = seeder.GetAllProducts();
            var product = products.Where(p => p.Id == id).FirstOrDefault();
            if (product == null)
            {
                return NotFound();
            }
            return Ok(product);
        }

        [HttpPost]
        public IActionResult CreateProduct([FromBody] Product product)
        {
            var success = seeder.AddProduct(product);
            if (success)
            {
                return Created("https://localhost:5001/api/products/{product.Id}", product);
            }
            return BadRequest("Failed to add product.");
        }

        [HttpDelete]
        [Route("{id}")]
        public IActionResult DeleteProduct(int id)
        {
            var success = seeder.RemoveProduct(id);
            if (success)
            {
                return Ok("Deleted product");
            }
            return BadRequest("Failed to delete product.");
        }

        [HttpPut]
        [Route("{id}")]
        public IActionResult UpdateProduct([FromBody] Product product, int id)
        {
            var success = seeder.UpdateProduct(id, product);
            if (success)
            {
                return Ok("Product updated successfully.");
            }
            return BadRequest("Failed to update product.");
        }
    }
}

Now an important point. Before testing in Postman, make some changes in Visual Studio so that API can be tested without running the browser.

We should not run the browser to test APIs. It is slow process. To disable it, we make changes in properties of API app in Visual Studio. Open the application Properties window. Note that we open Properties windows for API project, EShoppingApi, not class library. Go to the Debug tab. Click the URL. Launch Profiles window opens as given below. Select EShoppingApi. Uncheck the Launch Browser and delete weatherforecast in Url. Close the window. Click Save All button. 
Another change required is to run the application in project name profile, not IISExpress, because we use Kestrel not IISExpress during API testing. Look at the image below.

After making changes in the Visual Studio as done above, we should now begin testing. Save all and rebuild the solution. When we run the application, console window appears as given below and Kestrel server handles the request.

You can make change in the launchsettings.json file available in Properties folder. Set the value of "launchBrowser": false in profiles section in the file. It will disable the launching the app in browser.

Testing in Postman

Now open the Postman to test the APIs. I have done testing offline in Scratch Pad of Postman. When we are offline, click the Settings link as given below.

Then click Scratch Pad menu item.
Postman will open itself in Scratch pad. Look at the image below. A collection named ESHOP is created for testing the APIs. We should create a collection before starting the API testing. It helps to store all the tests in the collection. We can even import old tests in the collection.

First step is to create a collection for testing APIs of the project. Next, to create an API in collection, click the New button. Create New window will open as given below.
Click the Request icon. Save Request window appears as given below.
Fill the details and click Save button.

After creating collection and its different requests, now we are ready for testing APIs in Postman. We must first run the API application before testing in Postman. Open the Postman. Click the 'Get All products' request saved in the collection. Select GET from dropdown and fill the request URL as given below in the image and click Send button. We get the response in the body as given below. Note that in the Postman, you can select any other response format so that data will appear in that format in the body. By default, it is JSON.

Tip.

If you get issue in getting response for the API request, click Settings and turn off SSL certificate verification. For each request you may have to disable it. This settings is for Request sent, not for Postman app. Look at the image below.


Look at the Postman right pane carefully. Concise note is given in the below image about the pane.
Although, a working API is demonstrated in the above example, it is not the right approach which is followed in production. We must use DTO in the Web API to handle the requests. The drawbacks of this demo project and importance of DTO will be explained in the next post in PART-II.

Hot Topics