basquang™ on clouds

April 27, 2017

Calling Untrusted SSL HTTPS request using HttpClient give “Message=The remote certificate is invalid according to the validation procedure”

Filed under: JSON,Microsoft Technology,MVC,WebAPI — basquang @ 3:36 PM

Scenario:

+You want to deploy web application server (for example ASP.NET Web API) which require SSL certificate and HTTPS

+You want to make a call to that server using HttpClient

Problem:

+You may see the following errors message when make a call to https required url using HttpClient
HResult=-2146233079
Message=The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel.

HResult=-2146233087
Message=The remote certificate is invalid according to the validation procedure.

HResult=-2146233088
Message=An error occurred while sending the request.

Solution:

Using this lines of code to accept all server certificates from client. This line of code is using for testing purpose only with you are using self-signed or untrusted certificates

//Accept all server certificate
ServicePointManager.ServerCertificateValidationCallback =
    delegate (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
                {
                    return true;
                };

For example you want to make a https POST to Web API method in this post Hello ASP.NET Web API

static async Task PostSampleAsync()
        {
            //Accept all server certificate
            ServicePointManager.ServerCertificateValidationCallback =
                delegate (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
                {
                    return true;
                };
            client.BaseAddress = new Uri("https://10.1.16.193/WebAPISample/");
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            HttpResponseMessage response = await client.PostAsJsonAsync("api/values", "hello!");
            response.EnsureSuccessStatusCode();
            var result = response.Content.ReadAsAsync<string>().Result;
            Console.WriteLine(result);
        }

Happy coding!

Hello ASP.NET Web API

Filed under: JSON,Microsoft Technology,MVC,WebAPI — basquang @ 3:13 PM

1. Create a simple ASP.NET Web API project with HTTP GET and POST method

Create a simple Web API Controller like this:

    public class ValuesController : ApiController
    {
        string[] list = new string[] { "value1", "value2" };
        // GET api/values
        public IEnumerable<string> Get() { return list; }

        // GET api/values/1
        public string Get(int id) { return list[id]; }

        // POST api/values
        public string Post([FromBody]string value) { return value; }
    }

2. Test the Web API using web browser or POSTMAN

Navigate GET method by web browser

http://localhost:3649/api/values

webapi1

in POSTMAN http://localhost:3649/api/values/1

webapi2

in POSTMAN test HTTP POST method

webapi3

3. Call the Web API using HttpClient

+ Create a console application

+ Install Nuget package

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="Microsoft.AspNet.WebApi.Client" version="5.2.3" targetFramework="net452" />
  <package id="Newtonsoft.Json" version="6.0.4" targetFramework="net452" />
</packages>

+ Create a method to consume Web API method GET like this

static HttpClient client = new HttpClient();
        static void Main(string[] args)
        {
            RunAsync().Wait();
        }

        static async Task RunAsync()
        {
            //await PostSampleAsync();
            await GetSampleAsync();

            Console.ReadLine();
        }
static async Task GetSampleAsync()
        {
            // this is where we will send it
            string uri = "http://localhost:3649/api/values";

            // create a request
            HttpWebRequest request = (HttpWebRequest)
            WebRequest.Create(uri); request.KeepAlive = false;
            request.ProtocolVersion = HttpVersion.Version10;
            request.Method = "GET";

            /*====HTTP POST====*/
            //request.Method = "POST";
            //// turn our request string into a byte stream
            //byte[] postBytes = Encoding.ASCII.GetBytes("hello"); //param

            //// this is important - make sure you specify type this way
            //request.ContentType = "application/x-www-form-urlencoded";
            //request.ContentLength = postBytes.Length;
            //Stream requestStream = request.GetRequestStream();

            //// now send it
            //requestStream.Write(postBytes, 0, postBytes.Length);
            //requestStream.Close();

            // grab te response and print it out to the console along with the status code
            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            Console.WriteLine(new StreamReader(response.GetResponseStream()).ReadToEnd());
            Console.WriteLine(response.StatusCode);
        }

+ Run debug application you will see the result as below

webapi4

+ Create a method to consume Web API method POST like this

static async Task PostSampleAsync()
        {
            client.BaseAddress = new Uri("http://localhost:3649/");
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            HttpResponseMessage response = await client.PostAsJsonAsync("api/values", "hello!");
            response.EnsureSuccessStatusCode();
            var result = response.Content.ReadAsAsync<string>().Result;
            Console.WriteLine(result);
        }

+ Run debug application for this method you will see the result as below

webapi5.PNG

Happy coding!

July 11, 2014

[MVC]: Migrate an existing project using ASP.NET Identity 1.0 to 2.0 Complete

Filed under: Entity Framework (EF),MVC — basquang @ 4:13 PM
Tags: ,

Scenario:

You have an existing ASP.NET MVC project that using previous version of ASP.NET Identity (v1.0.0). You want to use new features which announced in ASP.NET Identity 2.0 RTM versions:

  • Two-Factor Authentication
  • Account Lockout
  • Account Confirmation
  • Password Reset
  • Security Stamp (Sign out everywhere)
  • Make the type of Primary Key be extensible for Users and Roles
  • Support IQueryable on Users and Roles
  • Delete User account
  • IdentityFactory Middleware/ CreatePerOwinContext
  • Indexing on Username
  • Enhanced Password Validator

For the full announcement please visit the official link here Announcing RTM of ASP.NET Identity 2.0.0

Solution:

As mentioned in the release, you need to migrate existing project using ASP.NET Identity 1.0 to 2.0 version. There are some guide here you can follow:

Updating ASP.NET applications from ASP.NET Identity 1.0 to 2.0.0-alpha1

Upgrading an Existing Project from ASP.NET Identity 1.0 to 2.0

But these guides is not completely, just steps to migrate ASP.NET Identity Code First database from v1.0 to v2.0.

So this article will describe complete steps to migrate your existing MVC project using ASP.NET Identity 1.0 to 2.0 RTM which include “Password Reset” function.

Existing project

My current MVC project using ASP.NET Identity 1.0 as noted in packages.config here:

<package id="Microsoft.AspNet.Identity.Core" version="1.0.0" targetFramework="net45" />
<package id="Microsoft.AspNet.Identity.EntityFramework" version="1.0.0" targetFramework="net45" />
<package id="Microsoft.AspNet.Identity.Owin" version="1.0.0" targetFramework="net45" />

The AspNetUsers table in ASP.NET Identity 1.0 has 5 columns: Id, UserName, PasswordHash, SecurityStamp and Discriminator:

Migrating ASP.NET Identity Database from 1.0 to 2.0

Step 1: Update ASP.NET Identity Packages

Open Package Manager Console then run following command in order

PM> Update-Package Microsoft.AspNet.Identity.Core

PM> Update-Package Microsoft.AspNet.Identity.EntityFramework

Restart Visual Studio then run this command in Package Manager Console

PM> Update-Package Microsoft.AspNet.Identity.Owin

Now run the application an try to logging on old user, you will see the error:

Server Error in ‘/’ Application.


The model backing the ‘ApplicationDbContext’ context has changed since the database was created. This could have happened because the model used by ASP.NET Identity Framework has changed or the model being used in your application has changed. To resolve this issue, you need to update your database. Consider using Code First Migrations to update the database (http://go.microsoft.com/fwlink/?LinkId=301867).  Before you update your database using Code First Migrations, please disable the schema consistency check for ASP.NET Identity by setting throwIfV1Schema = false in the constructor of your ApplicationDbContext in your application.
       public ApplicationDbContext() : base(“ApplicationServices”, throwIfV1Schema:false)

Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code. 

Exception Details: System.InvalidOperationException: The model backing the ‘ApplicationDbContext’ context has changed since the database was created. This could have happened because the model used by ASP.NET Identity Framework has changed or the model being used in your application has changed. To resolve this issue, you need to update your database. Consider using Code First Migrations to update the database (http://go.microsoft.com/fwlink/?LinkId=301867).  Before you update your database using Code First Migrations, please disable the schema consistency check for ASP.NET Identity by setting throwIfV1Schema = false in the constructor of your ApplicationDbContext in your application.
       public ApplicationDbContext() : base(“ApplicationServices”, throwIfV1Schema:false)

Source Error: 

 

Line 10:     public class ApplicationDbContext : IdentityDbContext<ApplicationUser>

Line 11:     {

Line 12:         public ApplicationDbContext()

Line 13:             : base("DefaultConnection")

Line 14:         {


Source File: c:\Users\Administrator\Documents\Visual Studio 2013\Projects\MvcIdentity2Migration\MvcIdentity2Migration\Models\IdentityModels.cs    Line: 12 

 

To resolve this error, we need to migrate existing code first database to 2.0 version

Step 2: Migrate Code First database.

Open IdentityModels.cs class file then change to the ApplicationDbContext constructor as below:

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext()
            : base("DefaultConnection", throwIfV1Schema: false)
        {
        }
        public static ApplicationDbContext Create()
        {
            return new ApplicationDbContext();
        }
    }

Open Package Manager Console then run these command

PM> Enable-Migrations

PM> Add-Migration Update1

Here Update1 is an identifier you can have for this migration. The migrations code generated should as below

public partial class Update1 : DbMigration
    {
        public override void Up()
        {
            RenameColumn(table: "dbo.AspNetUserClaims", name: "User_Id", newName: "UserId");
            RenameIndex(table: "dbo.AspNetUserClaims", name: "IX_User_Id", newName: "IX_UserId");
            DropPrimaryKey("dbo.AspNetUserLogins");
            AddColumn("dbo.AspNetUsers", "Email", c => c.String(maxLength: 256));
            AddColumn("dbo.AspNetUsers", "EmailConfirmed", c => c.Boolean(nullable: false));
            AddColumn("dbo.AspNetUsers", "PhoneNumber", c => c.String());
            AddColumn("dbo.AspNetUsers", "PhoneNumberConfirmed", c => c.Boolean(nullable: false));
            AddColumn("dbo.AspNetUsers", "TwoFactorEnabled", c => c.Boolean(nullable: false));
            AddColumn("dbo.AspNetUsers", "LockoutEndDateUtc", c => c.DateTime());
            AddColumn("dbo.AspNetUsers", "LockoutEnabled", c => c.Boolean(nullable: false));
            AddColumn("dbo.AspNetUsers", "AccessFailedCount", c => c.Int(nullable: false));
            AlterColumn("dbo.AspNetRoles", "Name", c => c.String(nullable: false, maxLength: 256));
            AlterColumn("dbo.AspNetUsers", "UserName", c => c.String(nullable: false, maxLength: 256));
            AddPrimaryKey("dbo.AspNetUserLogins", new[] { "LoginProvider", "ProviderKey", "UserId" });
            CreateIndex("dbo.AspNetRoles", "Name", unique: true, name: "RoleNameIndex");
            CreateIndex("dbo.AspNetUsers", "UserName", unique: true, name: "UserNameIndex");
            DropColumn("dbo.AspNetUsers", "Discriminator");
        }
        
        public override void Down()
        {
            AddColumn("dbo.AspNetUsers", "Discriminator", c => c.String(nullable: false, maxLength: 128));
            DropIndex("dbo.AspNetUsers", "UserNameIndex");
            DropIndex("dbo.AspNetRoles", "RoleNameIndex");
            DropPrimaryKey("dbo.AspNetUserLogins");
            AlterColumn("dbo.AspNetUsers", "UserName", c => c.String());
            AlterColumn("dbo.AspNetRoles", "Name", c => c.String(nullable: false));
            DropColumn("dbo.AspNetUsers", "AccessFailedCount");
            DropColumn("dbo.AspNetUsers", "LockoutEnabled");
            DropColumn("dbo.AspNetUsers", "LockoutEndDateUtc");
            DropColumn("dbo.AspNetUsers", "TwoFactorEnabled");
            DropColumn("dbo.AspNetUsers", "PhoneNumberConfirmed");
            DropColumn("dbo.AspNetUsers", "PhoneNumber");
            DropColumn("dbo.AspNetUsers", "EmailConfirmed");
            DropColumn("dbo.AspNetUsers", "Email");
            AddPrimaryKey("dbo.AspNetUserLogins", new[] { "UserId", "LoginProvider", "ProviderKey" });
            RenameIndex(table: "dbo.AspNetUserClaims", name: "IX_UserId", newName: "IX_User_Id");
            RenameColumn(table: "dbo.AspNetUserClaims", name: "UserId", newName: "User_Id");
        }
    }

Next we need to persist it to the database. Run the command ‘Update-Database –verbose’. The verbose flag lets you view the SQL queries generated. This should pass as expected.

The AspNetUsers in ASP.NET Identity 2.0 is as image below

Now run application and try to logon with old user. It will logged on as expected.

Adding “Password Reset” function to existing project.

Step 1: Right click on App_Start folder then add new IdentityConfig.cs with code bellow

public class ApplicationUserManager : UserManager<ApplicationUser>
    {
        public ApplicationUserManager(IUserStore<ApplicationUser> store)
            : base(store)
        {
        }

        public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
        {
            var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));
            // Configure validation logic for usernames
            manager.UserValidator = new UserValidator<ApplicationUser>(manager)
            {
                AllowOnlyAlphanumericUserNames = false,
                RequireUniqueEmail = true
            };
            // Configure validation logic for passwords
            manager.PasswordValidator = new PasswordValidator
            {
                RequiredLength = 6,
                RequireNonLetterOrDigit = true,
                RequireDigit = true,
                RequireLowercase = true,
                RequireUppercase = true,
            };
            // Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user
            // You can write your own provider and plug in here.
            manager.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider<ApplicationUser>
            {
                MessageFormat = "Your security code is: {0}"
            });
            manager.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider<ApplicationUser>
            {
                Subject = "Security Code",
                BodyFormat = "Your security code is: {0}"
            });
            manager.EmailService = new EmailService();
            manager.SmsService = new SmsService();
            var dataProtectionProvider = options.DataProtectionProvider;
            if (dataProtectionProvider != null)
            {
                manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
            }
            return manager;
        }
    }

    public class EmailService : IIdentityMessageService
    {
        public Task SendAsync(IdentityMessage message)
        {
            // Plug in your email service here to send an email.
            MailMessage email = new MailMessage("xxx@hotmail.com", message.Destination);
            email.Subject = message.Subject;
            email.Body = message.Body;
            email.IsBodyHtml = true;
            var mailClient = new SmtpClient("smtp.live.com", 587) { Credentials = new NetworkCredential("xxx@hotmail.com", "password"), EnableSsl = true };
            return mailClient.SendMailAsync(email);
        }
    }

    public class SmsService : IIdentityMessageService
    {
        public Task SendAsync(IdentityMessage message)
        {
            // Plug in your sms service here to send a text message.
            return Task.FromResult(0);
        }
    }

Step 2: Open the IdentityModels.cs class file then change code as below:

public class ApplicationUser : IdentityUser
    {
        public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
        {
            // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
            var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
            // Add custom user claims here
            return userIdentity;
        }
    }

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext()
            : base("DefaultConnection", throwIfV1Schema:false)
        {
        }

        public static ApplicationDbContext Create()
        {
            return new ApplicationDbContext();
        }
    }

Step 3: Open the Startup.Auth.cs class file then add these following code in the start of ConfigureAuth method

public void ConfigureAuth(IAppBuilder app)
	{
		// Configure the db context and user manager to use a single instance per request
		app.CreatePerOwinContext(ApplicationDbContext.Create);
		app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);

		// Enable the application to use a cookie to store information for the signed in user
		app.UseCookieAuthentication(new CookieAuthenticationOptions
		{
			AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
			LoginPath = new PathString("/Account/Login"),
			Provider = new CookieAuthenticationProvider
			{
				OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
					validateInterval: TimeSpan.FromMinutes(30),
					regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
			}
		});
		// Use a cookie to temporarily store information about a user logging in with a third party login provider
		app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

		// Uncomment the following lines to enable logging in with third party login providers
		//app.UseMicrosoftAccountAuthentication(
		//    clientId: "",
		//    clientSecret: "");

		//app.UseTwitterAuthentication(
		//   consumerKey: "",
		//   consumerSecret: "");

		//app.UseFacebookAuthentication(
		//   appId: "",
		//   appSecret: "");

		//app.UseGoogleAuthentication();
	} 

Step 4: Change AccountViewModels.cs class file. Open the file then add to view model for the Reset Password function

public class ResetPasswordViewModel
    {
        [Required]
        [EmailAddress]
        [Display(Name = "Email")]
        public string Email { get; set; }

        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "Confirm password")]
        [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; }

        public string Code { get; set; }
    }

    public class ForgotPasswordViewModel
    {
        [Required]
        [EmailAddress]
        [Display(Name = "Email")]
        public string Email { get; set; }
    }

Step 5: Change the AccountController.cs class file

Modify AccountController constructor method as bellow

private ApplicationUserManager _userManager;

public AccountController()
{
}

public AccountController(ApplicationUserManager userManager)
{
	UserManager = userManager;
}

public ApplicationUserManager UserManager
{
	get
	{
		return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
	}
	private set
	{
		_userManager = value;
	}
}

Remember to add this line of code in using code

using Microsoft.AspNet.Identity.Owin;

Step 6: Add Forgot Password Actions

// GET: /Account/ForgotPassword
[AllowAnonymous]
public ActionResult ForgotPassword()
{
	return View();
}

//
// POST: /Account/ForgotPassword
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
	if (ModelState.IsValid)
	{
		var user = await UserManager.FindByEmailAsync(model.Email);
		if (user == null || !(await UserManager.IsEmailConfirmedAsync(user.Id)))
		{
			ModelState.AddModelError("", "The user either does not exist or is not confirmed.");
			return View();
		}

		// For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=320771
		// Send an email with this link
		var code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
		var callbackUrl = Url.Action("ResetPassword", "Account",
		new { UserId = user.Id, code = code }, protocol: Request.Url.Scheme);
		await UserManager.SendEmailAsync(user.Id, "Reset Password",
		"Please reset your password by clicking here: <a href=\"" + callbackUrl + "\">link</a>");     

		return RedirectToAction("ForgotPasswordConfirmation", "Account");
	}

	// If we got this far, something failed, redisplay form
	return View(model);
}

//
// GET: /Account/ForgotPasswordConfirmation
[AllowAnonymous]
public ActionResult ForgotPasswordConfirmation()
{
	return View();
}

Step 7: Add Reset Password Actions

// GET: /Account/ResetPassword
[AllowAnonymous]
public ActionResult ResetPassword(string code)
{
	if (code == null)
	{
		return View("Error");
	}
	return View();
}

//
// POST: /Account/ResetPassword
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ResetPassword(ResetPasswordViewModel model)
{
	if (ModelState.IsValid)
	{
		var user = await UserManager.FindByEmailAsync(model.Email);
		if (user == null)
		{
			ModelState.AddModelError("", "No user found.");
			return View();
		}
		IdentityResult result = await UserManager.ResetPasswordAsync(user.Id, model.Code, model.Password);
		if (result.Succeeded)
		{
			return RedirectToAction("ResetPasswordConfirmation", "Account");
		}
		else
		{
			AddErrors(result);
			return View();
		}
	}

	// If we got this far, something failed, redisplay form
	return View(model);
}

//
// GET: /Account/ResetPasswordConfirmation
[AllowAnonymous]
public ActionResult ResetPasswordConfirmation()
{
	return View();
}

Step 8: Add view ForgotPassword.cshtml

@model MvcIdentity2Migration.Models.ForgotPasswordViewModel
@{
    ViewBag.Title = "Forgot your password?";
}

<h2>@ViewBag.Title.</h2>

@using (Html.BeginForm("ForgotPassword", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
    @Html.AntiForgeryToken()
    <h4>Enter your email.</h4>
    <hr />
    @Html.ValidationSummary("", new { @class = "text-danger" })
    <div class="form-group">
        @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" class="btn btn-default" value="Email Link" />
        </div>
    </div>
}

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Step 9: Add view ForgotPasswordConfirmation.cshtml

@{
    ViewBag.Title = "Forgot Password Confirmation";
}

<hgroup class="title">
    <h1>@ViewBag.Title.</h1>
</hgroup>
<div>
    <p>
        Please check your email to reset your password.
    </p>
</div>

Step 10: Add view ResetPassword.cshtml

@model MvcIdentity2Migration.Models.ResetPasswordViewModel
@{
    ViewBag.Title = "Reset password";
}

<h2>@ViewBag.Title.</h2>

@using (Html.BeginForm("ResetPassword", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
    @Html.AntiForgeryToken()
    <h4>Reset your password.</h4>
    <hr />
    @Html.ValidationSummary("", new { @class = "text-danger" })
    @Html.HiddenFor(model => model.Code)
    <div class="form-group">
        @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.Password, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" class="btn btn-default" value="Reset" />
        </div>
    </div>
}

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Step 11: Add view ResetPasswordConfirmation.cshtml

@{
    ViewBag.Title = "Reset password confirmation";
}

<hgroup class="title">
    <h1>@ViewBag.Title.</h1>
</hgroup>
<div>
    <p>
        Your password has been reset. Please @Html.ActionLink("click here to log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })
    </p>
</div>

 

Testing Password Reset function

Open AspNetUsers table then make sure old account has valid email and EmailConfirmed value is set to True

Run the application and test your Password Reset Function

Forgot Password page


Email Received

Reset Password page

Done!

Hope this help!

August 26, 2011

Logging: Using Log4Net in ASP.NET MVC 3

Filed under: Logging,MVC — basquang @ 9:08 AM

This article will describe how to configure logging with Log4Net in ASP.NET MVC 3 application.

Firstly, Create a new ASP.NET MVC 3 Internet Application.

From visual studio Tools –> Library Package Manager –> Manage NuGet Packages… In Manage NuGet Packages… windows, search Log4Net then install log4net package to your project.

image

1. Logging to SQL Server database.

1.1  you need create a table as script bellow to store your logging to SQL Server database.

CREATE TABLE [dbo].[Log](
	[ID] [int] IDENTITY(1,1) NOT NULL,
	[Date] [datetime] NOT NULL,
	[Thread] [varchar](255) NOT NULL,
	[Level] [varchar](20) NOT NULL,
	[Logger] [varchar](255) NOT NULL,
	[Message] [varchar](4000) NOT NULL
) ON [PRIMARY]

1.2 Open your web.config configuration file and add following log4net configuration sections

<configSections>
		<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
	</configSections>
	<log4net>
		<appender name="AdoNetAppender_SqlServer" type="log4net.Appender.AdoNetAppender">
			<connectionType value="System.Data.SqlClient.SqlConnection, System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
			<connectionString value="Data Source=BASQUANG;Initial Catalog=LoggingDB;Integrated Security=True" />
			<commandText value="INSERT INTO Log ([Date],[Thread],[Level],[Logger],[Message]) VALUES (@log_date, @thread, @log_level, @logger, @message)" />
			<parameter>
				<parameterName value="@log_date" />
				<dbType value="DateTime" />
				<layout type="log4net.Layout.PatternLayout" value="%date{yyyy'-'MM'-'dd HH':'mm':'ss'.'fff}" />
			</parameter>
			<parameter>
				<parameterName value="@thread" />
				<dbType value="String" />
				<size value="255" />
				<layout type="log4net.Layout.PatternLayout" value="%thread" />
			</parameter>
			<parameter>
				<parameterName value="@log_level" />
				<dbType value="String" />
				<size value="50" />
				<layout type="log4net.Layout.PatternLayout" value="%level" />
			</parameter>
			<parameter>
				<parameterName value="@logger" />
				<dbType value="String" />
				<size value="255" />
				<layout type="log4net.Layout.PatternLayout" value="%logger" />
			</parameter>
			<parameter>
				<parameterName value="@message" />
				<dbType value="String" />
				<size value="4000" />
				<layout type="log4net.Layout.PatternLayout" value="%message" />
			</parameter>
		</appender>		
		<root>
			<level value="All"/>
			<appender-ref ref="AdoNetAppender_SqlServer"/>
		</root>
	</log4net>

Note: Make sure you have already added the correctly version (4.0.0.0) of System.Data.dll to your project

2. Logging to Oracle database

2.1 you need create a table as script bellow to store your logging to Oracle database.

CREATE TABLE "LOG" (
  "DATETIME" TIMESTAMP(3) NULL,
  "THREAD" VARCHAR2(255 BYTE) NULL,
  "LOG_LEVEL" VARCHAR2(255 BYTE) NULL,
  "LOGGER" VARCHAR2(255 BYTE) NULL,
  "MESSAGE" VARCHAR2(4000 BYTE) NULL)
  STORAGE ( 
    NEXT 1048576 ) 
/

2.2 Open your web.config configuration file and add following log4net configuration sections

<configSections>
		<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
	</configSections>
	<log4net>		
		<appender name="AdoNetAppender_Oracle" type="log4net.Appender.AdoNetAppender">
			<connectionType value="System.Data.OracleClient.OracleConnection, System.Data.OracleClient, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
			<connectionString value="data source=XE;User ID=LoggingDB;Password=abcd1234-" />
			<commandText value="INSERT INTO Log (Datetime,Thread,Log_Level,Logger,Message) VALUES (:log_date, :thread, :log_level, :logger, :message)" />
			<bufferSize value="128" />
			<parameter>
				<parameterName value=":log_date" />
				<dbType value="DateTime" />
				<layout type="log4net.Layout.RawTimeStampLayout" />
			</parameter>
			<parameter>
				<parameterName value=":thread" />
				<dbType value="String" />
				<size value="255" />
				<layout type="log4net.Layout.PatternLayout">
					<conversionPattern value="%thread" />
				</layout>
			</parameter>
			<parameter>
				<parameterName value=":log_level" />
				<dbType value="String" />
				<size value="50" />
				<layout type="log4net.Layout.PatternLayout">
					<conversionPattern value="%level" />
				</layout>
			</parameter>
			<parameter>
				<parameterName value=":logger" />
				<dbType value="String" />
				<size value="255" />
				<layout type="log4net.Layout.PatternLayout">
					<conversionPattern value="%logger" />
				</layout>
			</parameter>
			<parameter>
				<parameterName value=":message" />
				<dbType value="String" />
				<size value="4000" />
				<layout type="log4net.Layout.PatternLayout">
					<conversionPattern value="%message" />
				</layout>
			</parameter>
		</appender>
		<root>
			<level value="All"/>
			<appender-ref ref="AdoNetAppender_Oracle"/>
		</root>
	</log4net>

Note: Make sure you have already added the correctly version (4.0.0.0) of System.Data.Oracle.dll to your project

3. Add this line of code to  Application_Start on Global.asax.cs file

log4net.Config.XmlConfigurator.Configure(); 

4. Add these lines of code on HomeController to testing your logging configuration

public ActionResult About()
        {
            log4net.ILog log = log4net.LogManager.GetLogger(this.GetType());
            log.Info("Test message for Log4Net"); 
            return View();
        }

5. Test your configuration. Run your website, from home page click on About then looking for a new record added on database table.

August 25, 2011

Logging: Using Elmah, Nuget in ASP.NET MVC 3 and Oracle database

Filed under: Logging,MVC,Oracle — basquang @ 3:00 PM

Logging is one of the most important thing you must to thinking about when intend to building a production website. There are many many way to implement logging in a website, and it was not able to easily.

So in this article, I will simplify the way to implement logging in ASP.NET MVC 3 using Elmah!

Configure Elmah with NuGet

1. Create a ASP.NET MVC 3 Internet Application

imageimage

2. From Visual Studio select Tools –> Library Package Manager –> Manage NuGet Packages…

In Manage NuGet Packages…Windows, seach Elmah then install this package for your project as bellow when finished.

imageimage

3. NuGet will automatically add reference elmah.dll to your project and the appropriate element of elmah on web.config file.

Now you can ready to test the Elmah logging by navigate your website to http://[yourwebsite]/elmah.axd as bellow

image

Logging your exception with Elmah.

But because we would like to use Elmah to handle our exceptions instead of MVC, we will replace the standard MVC [HandleError] attribute with our own custom attribute called [HandleErrorWithElmah].

So you must to create those class bellow

a. the HandleErrorWithELMAHAttribute.cs class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Elmah;

namespace MvcApplicationElmah.Logging
{
    public class HandleErrorWithELMAHAttribute : HandleErrorAttribute
    {
        public override void OnException(ExceptionContext context)
        {
            base.OnException(context);

            var e = context.Exception;
            if (!context.ExceptionHandled   // if unhandled, will be logged anyhow
                    || RaiseErrorSignal(e)      // prefer signaling, if possible
                    || IsFiltered(context))     // filtered?
                return;

            LogException(e);
        }

        private static bool RaiseErrorSignal(Exception e)
        {
            var context = HttpContext.Current;
            if (context == null)
                return false;
            var signal = ErrorSignal.FromContext(context);
            if (signal == null)
                return false;
            signal.Raise(e, context);
            return true;
        }

        private static bool IsFiltered(ExceptionContext context)
        {
            var config = context.HttpContext.GetSection("elmah/errorFilter")
                                     as ErrorFilterConfiguration;

            if (config == null)
                return false;

            var testContext = new ErrorFilterModule.AssertionHelperContext(
                                                                context.Exception, HttpContext.Current);

            return config.Assertion.Test(testContext);
        }

        private static void LogException(Exception e)
        {
            var context = HttpContext.Current;
            ErrorLog.GetDefault(context).Log(new Error(e, context));
        }
    }
}

b. The ErrorHandlingControllerFactory.cs class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace MvcApplicationElmah.Logging
{
    public class ErrorHandlingControllerFactory : DefaultControllerFactory
    {
        /// <summary>
        /// Injects a custom attribute 
        /// on every action that is invoked by the controller
        /// </summary>
        /// <param name="requestContext">The request context</param>
        /// <param name="controllerName">The name of the controller</param>
        /// <returns>An instance of a controller</returns>
        public override IController CreateController(
            RequestContext requestContext,
            string controllerName)
        {
            var controller =
                base.CreateController(requestContext,
                controllerName);

            var c = controller as Controller;

            if (c != null)
            {
                c.ActionInvoker =
                    new ErrorHandlingActionInvoker(
                        new HandleErrorWithELMAHAttribute());
            }

            return controller;
        }
    }
}

c. The ErrorHandlingActionInvoker.cs class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace MvcApplicationElmah.Logging
{
    public class ErrorHandlingActionInvoker : ControllerActionInvoker
    {
        private readonly IExceptionFilter filter;

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="filter">The exception filter to inject</param>
        public ErrorHandlingActionInvoker(IExceptionFilter filter)
        {
            if (filter == null)
            {
                throw new ArgumentNullException("filter");
            }

            this.filter = filter;
        }

        /// <summary>
        /// This methods returns all of the normal filters used
        /// PLUS it appends our custom filter to the end of the list 
        /// </summary>
        /// <param name="controllerContext">The context of the controller</param>
        /// <param name="actionDescriptor">The action descriptor</param>
        /// <returns>All of the action filters</returns>
        protected override FilterInfo GetFilters(
            ControllerContext controllerContext,
            ActionDescriptor actionDescriptor)
        {
            var filterInfo =
                base.GetFilters(controllerContext,
                actionDescriptor);

            filterInfo.ExceptionFilters.Add(this.filter);

            return filterInfo;
        }
    }
}

Now throw a new Exception in About action on your HomeController

public ActionResult About()
        {
            throw new Exception("A test exception for ELMAH");
            return View();
        }

Enable custom error in web.config

<customErrors mode="On" defaultRedirect="/Home/Error"></customErrors>

Register your custom error handler on Global.asax

protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            RegisterGlobalFilters(GlobalFilters.Filters);
            RegisterRoutes(RouteTable.Routes);

            ControllerBuilder.Current.SetControllerFactory(new MvcApplicationElmah.Logging.ErrorHandlingControllerFactory());
        }

Now, It’s time to test your custom exception that logged in Elmah.

imageimage

Logging to Oracle database

With a big website we need to log all exception to database. Elmah supports both of SQL Server and Oracle and more kind of databases. Is this section I will describe how to log the errors in Oracle database.

Firstly, we need to run the Oracle.sql script that provided from Elmah package that you has already downloaded.

image

/*
  
   ELMAH - Error Logging Modules and Handlers for ASP.NET
   Copyright (c) 2004-9 Atif Aziz. All rights reserved.
  
    Author(s):
  
      James Driscoll, mailto:jamesdriscoll@btinternet.com
  
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
  
      http://www.apache.org/licenses/LICENSE-2.0
  
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
  
*/

-- $Id: Oracle.sql 568 2009-05-11 14:18:34Z azizatif $

-- NB This script assumes you have logged on in the schema where you want to create the ELMAH objects

-- create a sequence for the errors (user to simulate an identity in SQL Server)
CREATE SEQUENCE elmah$error_seq START WITH 1 INCREMENT BY 1 NOMAXVALUE NOCYCLE NOCACHE NOORDER;

-- create the table to store the data
-- you can optionally specify tablespaces here too!
CREATE TABLE elmah$error
(
    -- if using Oracle 10g and above you can add DEFAULT SYS_GUID() 
    -- to the errorid definition.
    -- Oracle 8i doesn't like it with an NVARCHAR2
    -- haven't tested it against 9i
    errorid         NVARCHAR2(32) NOT NULL,
    application     NVARCHAR2(60) NOT NULL,
    host            NVARCHAR2(30) NOT NULL,
    type            NVARCHAR2(100) NOT NULL,
    source          NVARCHAR2(60),
    message         NVARCHAR2(500) NOT NULL,
    username        NVARCHAR2(50),
    statuscode      NUMBER NOT NULL,
    timeutc         DATE NOT NULL,
    sequencenumber  NUMBER NOT NULL,
    allxml          NCLOB NOT NULL,
    CONSTRAINT idx_elmah$error_pk 
        PRIMARY KEY (errorid) 
        USING INDEX -- TABLESPACE "TABLESPACE FOR INDEX"
) -- TABLESPACE "TABLESPACE FOR DATA"
/

-- trigger to make sure we get our sequence number in the table
CREATE TRIGGER trg_elmah$error_bi
BEFORE INSERT ON elmah$error
FOR EACH ROW
BEGIN
    SELECT elmah$error_seq.NEXTVAL INTO :new.sequencenumber FROM dual;
END trg_elmah$error_bi;
/

-- create the index on the table
CREATE INDEX idx_elmah$error_app_time_seq ON elmah$error(application, timeutc DESC, sequencenumber DESC)
/

-- package containing the procedure we need for ELMAH to log errors
CREATE OR REPLACE PACKAGE pkg_elmah$log_error
IS
    PROCEDURE LogError
    (
        v_ErrorId IN elmah$error.errorid%TYPE,
        v_Application IN elmah$error.application%TYPE,
        v_Host IN elmah$error.host%TYPE,
        v_Type IN elmah$error.type%TYPE,
        v_Source IN elmah$error.source%TYPE,
        v_Message IN elmah$error.message%TYPE,
        v_User IN elmah$error.username%TYPE,
        v_AllXml IN elmah$error.allxml%TYPE,
        v_StatusCode IN elmah$error.statuscode%TYPE,
        v_TimeUtc IN elmah$error.timeutc%TYPE
    );

END pkg_elmah$log_error;
/

CREATE OR REPLACE PACKAGE BODY pkg_elmah$log_error
IS
    PROCEDURE LogError
    (
        v_ErrorId IN elmah$error.errorid%TYPE,
        v_Application IN elmah$error.application%TYPE,
        v_Host IN elmah$error.host%TYPE,
        v_Type IN elmah$error.type%TYPE,
        v_Source IN elmah$error.source%TYPE,
        v_Message IN elmah$error.message%TYPE,
        v_User IN elmah$error.username%TYPE,
        v_AllXml IN elmah$error.allxml%TYPE,
        v_StatusCode IN elmah$error.statuscode%TYPE,
        v_TimeUtc IN elmah$error.timeutc%TYPE
    )
    IS
    BEGIN
        INSERT INTO elmah$error
            (
                errorid,
                application,
                host,
                type,
                source,
                message,
                username,
                allxml,
                statuscode,
                timeutc
            )
        VALUES
            (
                UPPER(v_ErrorId),
                v_Application,
                v_Host,
                v_Type,
                v_Source,
                v_Message,
                v_User,
                v_AllXml,
                v_StatusCode,
                v_TimeUtc
            );

    END LogError;   

END pkg_elmah$log_error;
/


-- package containing the procedure we need for ELMAH to retrieve errors
CREATE OR REPLACE PACKAGE pkg_elmah$get_error
IS
	-- NB this is for backwards compatibility with Oracle 8i
    TYPE t_cursor IS REF CURSOR;
    
    PROCEDURE GetErrorXml
    (
        v_Application IN elmah$error.application%TYPE,
        v_ErrorId IN elmah$error.errorid%TYPE,
        v_AllXml OUT elmah$error.allxml%TYPE
    );

    PROCEDURE GetErrorsXml
    (
        v_Application IN elmah$error.application%TYPE,
        v_PageIndex IN NUMBER DEFAULT 0,
        v_PageSize IN NUMBER DEFAULT 15,
        v_TotalCount OUT NUMBER,
        v_Results OUT t_cursor
    );
    
END pkg_elmah$get_error;
/

CREATE OR REPLACE PACKAGE BODY pkg_elmah$get_error
IS
    PROCEDURE GetErrorXml
    (
        v_Application IN elmah$error.application%TYPE,
        v_ErrorId IN elmah$error.errorid%TYPE,
        v_AllXml OUT elmah$error.allxml%TYPE
    )
    IS
    BEGIN
        SELECT  allxml
        INTO    v_AllXml
        FROM    elmah$error
        WHERE   errorid = UPPER(v_ErrorId)
        AND     application = v_Application;
    EXCEPTION
        WHEN NO_DATA_FOUND THEN
            v_AllXml := NULL;
    END GetErrorXml;

    PROCEDURE GetErrorsXml
    (
        v_Application IN elmah$error.application%TYPE,
        v_PageIndex IN NUMBER DEFAULT 0,
        v_PageSize IN NUMBER DEFAULT 15,
        v_TotalCount OUT NUMBER,
        v_Results OUT t_cursor
    )
    IS
        l_StartRowIndex NUMBER;
        l_EndRowIndex   NUMBER;
    BEGIN
        -- Get the ID of the first error for the requested page
        l_StartRowIndex := v_PageIndex * v_PageSize + 1;
        l_EndRowIndex := l_StartRowIndex + v_PageSize - 1;
        
        -- find out how many rows we've got in total
        SELECT  COUNT(*)
        INTO    v_TotalCount
        FROM    elmah$error
        WHERE   application = v_Application;

        OPEN v_Results FOR
            SELECT  *
            FROM
            (
                SELECT  e.*,
                        ROWNUM row_number
                FROM
                (
                    SELECT  /*+ INDEX(elmah$error, idx_elmah$error_app_time_seq) */
                            errorid,
                            application,
                            host,
                            type,
                            source,
                            message,
                            username,
                            statuscode,
                            timeutc
                    FROM    elmah$error
                    WHERE   application = v_Application
                    ORDER BY
                            timeutc DESC, 
                            sequencenumber DESC
                ) e
                WHERE ROWNUM <= l_EndRowIndex
            )
            WHERE   row_number >= l_StartRowIndex;
            
    END GetErrorsXml;

END pkg_elmah$get_error;
/

/* 
-- If you are securing the packages above, you will need to grant execute
-- privileges on them so that they can be called by the user connecting to the database.
-- NB As long as you use the schema owner for the connection string, this is not necessary,
-- although this is generally discouraged by Best Practices.

-- Option 1) Allow any user to execute the package (not recommended)
-- replace OWNER for the schema owner in the following statement
GRANT EXECUTE ON OWNER.pkg_elmah$log_error TO PUBLIC;
GRANT EXECUTE ON OWNER.pkg_elmah$get_error TO PUBLIC;

-- Option 2) Allow a single user to execute the package (better)
-- replace OWNER for the schema owner in the following statement
GRANT EXECUTE ON OWNER.pkg_elmah$log_error TO USER_NAME;
GRANT EXECUTE ON OWNER.pkg_elmah$get_error TO USER_NAME;

-- Option 3) Lock things down so that one user can only log errors, while another user can read and log errors (most secure)
-- replace OWNER for the schema owner in the following statement
-- LOGGING_USER_NAME will be used to connect to the database in all sites which log errors to the database
GRANT EXECUTE ON OWNER.pkg_elmah$log_error TO LOGGING_USER_NAME;
-- ADMIN_USER_NAME will be used to connect to the database in an admin portal which allows users to read errors
GRANT EXECUTE ON OWNER.pkg_elmah$log_error TO ADMIN_USER_NAME;
GRANT EXECUTE ON OWNER.pkg_elmah$get_error TO ADMIN_USER_NAME;

-- NB if you do take this approach, be sure to set the schemaOwner parameter in your web.config
*/

Then you need to install “Elmah on Oracle” package from NuGet. It will automatically add appropriate elements in web.config to support logging to Oracle database

image

Finally, you have to manual change the connection string to your oracle database that store the Elmah Logging table as bellow

<add name="elmah-oracle" connectionString="Data Source=XE;User ID=loggingdb;Password=*****;" />

You could be able to  test your configuration now. Starting the website, click on About then looking for a new record has already added to ELMAH$ERROR table in Oracle database.

image

Hope this help!

March 18, 2011

Stored Procedures Paging Solution in ASP.NET MVC 2

Filed under: MVC — basquang @ 2:39 PM

The article Server-Side Paging with the Entity Framework and ASP.NET MVC 3 describes how to paging using Entity Framework and ASP.NET MVC 3. Using Skip() and Take() extension methods is main idea of the article. But you can’t do it if your project using Oracle database. Because EF is not supported from Microsoft for Oracle database. So, I’ll describe how to implement paging from stored procedures in ASP.NET MVC 2.0. This approach bellow using Northwind database but it’s also applied for Oracle database.

1. Define the GetProductsByFilter stored procedures

This stored procedures will contain total rows of the table and selection of records by page size

CREATE PROCEDURE GetProductsByFilter 
	-- Add the parameters for the stored procedure here
	@i_Page_Index int = 0,
	@i_Page_Count int = 10,
	@o_total_rows int output
AS
BEGIN
	-- SET NOCOUNT ON added to prevent extra result sets from
	-- interfering with SELECT statements.
	SET NOCOUNT ON;
	--Select
	SELECT TOP (@i_Page_Count) a.* FROM 
     (
          SELECT p.*,
          ROW_NUMBER() OVER (ORDER BY p.ProductID) AS num
          FROM dbo.Products p
     ) AS a
     WHERE num > @i_Page_Index * @i_Page_Count;
    -- Get Total Rows
	SET @o_total_rows =  (SELECT  COUNT(1)		
        FROM Products);
END
GO

2. Define PagerParams class

This class used to store paging information. It’s include the current page index, the page size and the total records.

[Serializable]
    public class PagerParams
    {
        /// <summary>
        /// Page size
        /// </summary>
        private int _pageSize;
        /// <summary>
        /// Total record count
        /// </summary>
        private int _totalRecords;
        /// <summary>
        /// Page index
        /// </summary>
        private int _pageIndex;

        /// <summary>
        /// Gets the size of the page.
        /// </summary>
        /// <value>The size of the page.</value>
        public int PageSize
        {
            get { return _pageSize; }
        }

        /// <summary>
        /// Gets or sets the index of the page.
        /// </summary>
        /// <value>The index of the page.</value>
        public int PageIndex
        {
            get { return _pageIndex; }
            set
            {
                if (_pageSize == 0 && value > 0)
                {
                    throw new ArgumentOutOfRangeException("error");
                }
                if (_totalRecords > 0)
                {
                    if (value * _pageSize >= _totalRecords)
                    {
                        throw new ArgumentOutOfRangeException("error");
                    }
                }
                else if (_totalRecords == 0)
                {
                    if (value > 0)
                    {
                        throw new ArgumentOutOfRangeException("error");
                    }
                }
                _pageIndex = value;
            }
        }

        /// <summary>
        /// Gets or sets the total records.
        /// </summary>
        /// <value>The total records.</value>
        public int TotalRecords
        {
            get { return _totalRecords; }
            set
            {
                if (_totalRecords != value)
                {
                    _totalRecords = value;                  
                    if (_totalRecords > 0)
                    {
                        if (_pageIndex * _pageSize >= _totalRecords)
                        {                           
                            PageIndex = (_totalRecords - 1) / _pageSize;
                        }
                    }
                    else if (_totalRecords == 0)
                    {
                        PageIndex = 0;
                    }                    
                }
            }
        }      

        /// <summary>
        /// 
        /// </summary>
        /// <param name="pageSize"></param>
        /// <param name="pageIndex"></param>
        /// <param name="totalRecords"></param>
        public PagerParams(int pageSize, int pageIndex, int totalRecords)
        {
            _pageSize = pageSize;
            _totalRecords = totalRecords;
            PageIndex = pageIndex;
        }

        /// <summary>
        /// Gets the default pager parameters.
        /// </summary>
        /// <value>The default pager parameters.</value>
        public static PagerParams Default
        {
            get
            {
                return new PagerParams(20, 0, -1);
            }
        }

        /// <summary>
        /// Gets the no paging parameters.
        /// </summary>
        /// <value>The no paging parameters.</value>
        public static PagerParams NoPaging
        {
            get
            {
                return new PagerParams(int.MaxValue, 0, -1);
            }
        }
    }

3. Implement the IPagedList

After that We defines IPagedList from the idea of Rob Conery’s blog.

IPagedList interface

public interface IPagedList
    {
        int PageCount { get; }
        int TotalItemCount { get; }
        int PageIndex { get; }
        int PageNumber { get; }
        int PageSize { get; }
        bool HasPreviousPage { get; }
        bool HasNextPage { get; }
        bool IsFirstPage { get; }
        bool IsLastPage { get; }
    }   

PagedList class

public partial class PagedList : IPagedList
    {        
        public PagedList(PagerParams pagerParams)
        {
            Initialize(pagerParams);
        }

        #region IPagedList Members

        public int PageCount { get; private set; }
        public int TotalItemCount { get; private set; }
        public int PageIndex { get; private set; }
        public int PageNumber { get { return PageIndex + 1; } }
        public int PageSize { get; private set; }
        public bool HasPreviousPage { get; private set; }
        public bool HasNextPage { get; private set; }
        public bool IsFirstPage { get; private set; }
        public bool IsLastPage { get; private set; }

        #endregion

        protected void Initialize(PagerParams pagerParams)
        {
            //### argument checking
            if (pagerParams.PageIndex < 0)
            {
                throw new ArgumentOutOfRangeException("PageIndex cannot be below 0.");
            }
            if (pagerParams.PageSize < 1)
            {
                throw new ArgumentOutOfRangeException("PageSize cannot be less than 1.");
            }
           

            //### set properties           
            TotalItemCount = pagerParams.TotalRecords;
            PageSize = pagerParams.PageSize;
            PageIndex = pagerParams.PageIndex;
            if (TotalItemCount > 0)
            {
                PageCount = (int)Math.Ceiling(TotalItemCount / (double)PageSize);
            }
            else
            {
                PageCount = 0;
            }
            HasPreviousPage = (PageIndex > 0);
            HasNextPage = (PageIndex < (PageCount - 1));
            IsFirstPage = (PageIndex <= 0);
            IsLastPage = (PageIndex >= (PageCount - 1));           
        }
    }    

3. Create HtmlHelpers methods

Now We create a Pager class to render pagination.

 public class Pager
    {
        private ViewContext viewContext;
        private readonly int pageSize;
        private readonly int currentPage;
        private readonly int totalItemCount;
        private readonly RouteValueDictionary linkWithoutPageValuesDictionary;

        public Pager(ViewContext viewContext, int pageSize, int currentPage, int totalItemCount, RouteValueDictionary valuesDictionary)
        {
            this.viewContext = viewContext;
            this.pageSize = pageSize;
            this.currentPage = currentPage;
            this.totalItemCount = totalItemCount;
            this.linkWithoutPageValuesDictionary = valuesDictionary;
        }

        public string RenderHtml()
        {
            int pageCount = (int)Math.Ceiling(this.totalItemCount / (double)this.pageSize);
            int nrOfPagesToDisplay = 10;

            var sb = new StringBuilder();

            // Previous
            if (this.currentPage > 1)
            {
                sb.Append(GeneratePageLink("&lt;", this.currentPage - 1));
            }
            else
            {
                sb.Append("<span class=\"disabled\">&lt;</span>");
            }

            int start = 1;
            int end = pageCount;

            if (pageCount > nrOfPagesToDisplay)
            {
                int middle = (int)Math.Ceiling(nrOfPagesToDisplay / 2d) - 1;
                int below = (this.currentPage - middle);
                int above = (this.currentPage + middle);

                if (below < 4)
                {
                    above = nrOfPagesToDisplay;
                    below = 1;
                }
                else if (above > (pageCount - 4))
                {
                    above = pageCount;
                    below = (pageCount - nrOfPagesToDisplay);
                }

                start = below;
                end = above;
            }

            if (start > 3)
            {
                sb.Append(GeneratePageLink("1", 1));
                sb.Append(GeneratePageLink("2", 2));
                sb.Append("...");
            }
            for (int i = start; i <= end; i++)
            {
                if (i == this.currentPage)
                {
                    sb.AppendFormat("<span class=\"current\">{0}</span>", i);
                }
                else
                {
                    sb.Append(GeneratePageLink(i.ToString(), i));
                }
            }
            if (end < (pageCount - 3))
            {
                sb.Append("...");
                sb.Append(GeneratePageLink((pageCount - 1).ToString(), pageCount - 1));
                sb.Append(GeneratePageLink(pageCount.ToString(), pageCount));
            }

            // Next
            if (this.currentPage < pageCount)
            {
                sb.Append(GeneratePageLink("&gt;", (this.currentPage + 1)));
            }
            else
            {
                sb.Append("<span class=\"disabled\">&gt;</span>");
            }
            return sb.ToString();
        }

        private string GeneratePageLink(string linkText, int pageNumber)
        {
            var pageLinkValueDictionary = new RouteValueDictionary(this.linkWithoutPageValuesDictionary);
            pageLinkValueDictionary.Add("page", pageNumber);
            //var virtualPathData = this.viewContext.RouteData.Route.GetVirtualPath(this.viewContext, pageLinkValueDictionary);
            var virtualPathData = RouteTable.Routes.GetVirtualPath(this.viewContext.RequestContext, pageLinkValueDictionary);

            if (virtualPathData != null)
            {
                string linkFormat = "<a href="\&quot;{0}\&quot;">{1}</a>";
                return String.Format(linkFormat, virtualPathData.VirtualPath, linkText);
            }
            else
            {
                return null;
            }
        }
    }

and a PagingExtentions class

public static class PagingExtensions
    {
        #region HtmlHelper extensions

        public static string Pager(this HtmlHelper htmlHelper, int pageSize, int currentPage, int totalItemCount)
        {
            return Pager(htmlHelper, pageSize, currentPage, totalItemCount, null, null);
        }

        public static string Pager(this HtmlHelper htmlHelper, int pageSize, int currentPage, int totalItemCount, string actionName)
        {
            return Pager(htmlHelper, pageSize, currentPage, totalItemCount, actionName, null);
        }

        public static string Pager(this HtmlHelper htmlHelper, int pageSize, int currentPage, int totalItemCount, object values)
        {
            return Pager(htmlHelper, pageSize, currentPage, totalItemCount, null, new RouteValueDictionary(values));
        }

        public static string Pager(this HtmlHelper htmlHelper, int pageSize, int currentPage, int totalItemCount, string actionName, object values)
        {
            return Pager(htmlHelper, pageSize, currentPage, totalItemCount, actionName, new RouteValueDictionary(values));
        }

        public static string Pager(this HtmlHelper htmlHelper, int pageSize, int currentPage, int totalItemCount, RouteValueDictionary valuesDictionary)
        {
            return Pager(htmlHelper, pageSize, currentPage, totalItemCount, null, valuesDictionary);
        }

        public static string Pager(this HtmlHelper htmlHelper, int pageSize, int currentPage, int totalItemCount, string actionName, RouteValueDictionary valuesDictionary)
        {
            if (valuesDictionary == null)
            {
                valuesDictionary = new RouteValueDictionary();
            }
            if (actionName != null)
            {
                if (valuesDictionary.ContainsKey("action"))
                {
                    throw new ArgumentException("The valuesDictionary already contains an action.", "actionName");
                }
                valuesDictionary.Add("action", actionName);
            }
            var pager = new Pager(htmlHelper.ViewContext, pageSize, currentPage, totalItemCount, valuesDictionary);
            return pager.RenderHtml();
        }

        #endregion                

        #region    
        public static IPagedList ToPagedList(this PagerParams pagerParams)
        {
            return new PagedList(pagerParams);
        }
        #endregion
    }

4. Create ProductViewModel ViewModel

This model will have IPagedList properties and collection of Product Model

public class ProductViewModel
    {
        public IPagedList PagedList { get; set; }       
        public List<ProductModel> Products { get; set; }
    }

5. Create ProductServices to accessing to database using the GetProductsByFilter stored procedures.

In this procedure, we have to update TotalRecords for PagerParams

cmd.CommandText = "GetProductsByFilter"; //store procedure name                    
cmd.CommandType = CommandType.StoredProcedure;

SqlParameter i_Page_Index = cmd.Parameters.Add("@i_Page_Index", SqlDbType.Int);
i_Page_Index.Direction = ParameterDirection.Input;
i_Page_Index.Value = param.PageIndex;

SqlParameter i_Page_Count = cmd.Parameters.Add("@i_Page_Count", SqlDbType.Int);
i_Page_Count.Direction = ParameterDirection.Input;
i_Page_Count.Value = param.PageSize;

cmd.Parameters.Add("@o_total_rows", SqlDbType.Int).Direction = ParameterDirection.Output;                    
//cmd.ExecuteNonQuery();
//int total = Convert.ToInt32(cmd.Parameters["@o_total_rows"].Value);
using (SqlDataReader reader = cmd.ExecuteReader())
{
	//total = Convert.ToInt32(cmd.Parameters["@o_total_rows"].Value);
	while (reader.Read())
	{
		int id = Int32.Parse(reader["ProductID"].ToString());
		string name = reader["ProductName"].ToString();
		ProductModel model = new ProductModel
		{
			Id = id,
			ProductName = name
		};
		list.Add(model);
	}
	reader.NextResult();
	param.TotalRecords = Convert.ToInt32(cmd.Parameters["@o_total_rows"].Value);
}

6. The controller will look like this

The pageIndex is start from 0

public ActionResult Index(int? page)
        {
            int currentPageIndex = page.HasValue ? page.Value - 1 : 0;
            PagerParams param = new PagerParams(5, currentPageIndex, -1);
            ProductViewModel model = new ProductViewModel
            {
                Products = ProductServices.ListProducts(param)
            };
            model.PagedList = param.ToPagedList();
            return View(model);
        }

7. Create View and implement paging using HtmlHelpers. The view like this

<table class="gridview" width="100%">
	<tr>           
		<th>
			ID                 
		</th>
		<th>
			NAME
		</th>                                   
	</tr>
<% foreach (var item in Model.Products) { %>
	<tr>
		<td style="width:50px">               
		   <%= Html.Encode(item.Id) %>
		</td>            
		<td>
			<%= Html.Encode(item.ProductName) %>
		</td>                 
	</tr>
<% } %>
</table>
<div class="pager">
	<%= Html.Pager(ViewData.Model.PagedList.PageSize, ViewData.Model.PagedList.PageNumber, ViewData.Model.PagedList.TotalItemCount) %>        
</div>

8. Now is the results

image

Create a free website or blog at WordPress.com.