European ASP.NET 4.5 Hosting BLOG

BLOG about ASP.NET 4, ASP.NET 4.5 Hosting and Its Technology - Dedicated to European Windows Hosting Customer

European ASP.NET Core Hosting :: Paging Using Repeater Control In ASP.NET With And Without Stored Procedure

clock October 16, 2019 11:06 by author Peter

By default, pagination is not enabled in a Repeater control. We have to write custom pager control to use paging in a Repeater control. Here, I will explain how to implement paging in Repeater control in ASP.NET with and without a stored procedure. I am using Visual Studio 2019 to create the application.

Step 1
First, we have to create a table “tblCustomers” to test the paging in the repeater control.
CREATE TABLE [dbo].[tblCustomers]( 
[Id] [int] NOT NULL, 
[Name] [nvarchar](50) NULL, 
[Company] [nvarchar](50) NULL, 
[Phone] [nvarchar](50) NULL, 
[Address] [nvarchar](50) NULL, 
[Country] [nvarchar](50) NULL, 
[Email] [nvarchar](50) NULL 


After creating the table, add some record to the table.

Step 2
Open Visual Studio and click on "Create a new project".
Select ASP.NET Web Application from templates and click on “Next”.
Then, give the project name as “AspRepeater” and then click “Create”.
Now, choose “Web Forms” from the template and click on “Create”.

ASP.NET Repeater Control without Stored Procedure

Step 3
Now, create a new weborm “RepeaterControl” and write the code following code in your “RepeaterControl.aspx” page.
Code for RepeaterControl.aspx page.
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="RepeaterControl.aspx.cs" Inherits=" AspRepeater.RepeaterControl" %> 

<!DOCTYPE html> 

<html xmlns="http://www.w3.org/1999/xhtml"> 
<head runat="server"> 
<title>Repeater Control without Stored Procedure</title> 
</head> 
<body> 
<form id="form1" runat="server"> 
    <div> 
        <asp:Repeater ID="Repeater2" runat="server"> 
            <HeaderTemplate> 
                <table id="tbDetails" style="width: 100%; border-collapse: collapse;" border="1" cellpadding="5" cellspacing="0"> 
                    <tr style="background-color: lightgray; height: 30px; color: black; text-align: center"> 
                        <th>Id</th> 
                        <th>Customer Name</th> 
                        <th>Company Name</th> 
                        <th>Phone</th> 
                        <th>Address</th> 
                        <th>E-Mail</th> 
                    </tr> 
            </HeaderTemplate> 
            <ItemTemplate> 
                <tr style="height: 25px;"> 
                    <td> 
                        <%#Eval("Id").ToString()%> 
                    </td> 
                    <td> 
                        <%#Eval("Name").ToString()%> 
                    </td> 
                    <td> 
                        <%#Eval("Company").ToString()%> 
                    </td> 
                    <td> 
                        <%#Eval("Phone").ToString()%> 
                    </td> 
                    <td> 
                        <%#Eval("Address").ToString()%>, <%#Eval("Country").ToString()%> 
                    </td> 
                    <td> 
                        <%#Eval("Email").ToString()%> 
                    </td>                         
                </tr> 
            </ItemTemplate> 
            <FooterTemplate> 
                </table> 
            </FooterTemplate> 
        </asp:Repeater> 
    </div> 
    <br /> 
    <div style="text-align:center"> 
        <asp:Repeater ID="Repeater1" runat="server" OnItemCommand="Repeater1_ItemCommand"> 
            <ItemTemplate> 
                <asp:LinkButton ID="lnkPage" 
                    Style="padding: 8px; margin: 2px; background: lightgray; border: solid 1px #666; color: black; font-weight: bold" 
                    CommandName="Page" CommandArgument="<%# Container.DataItem %>" runat="server" Font-Bold="True"><%# Container.DataItem %> 
                </asp:LinkButton> 
            </ItemTemplate> 
        </asp:Repeater> 
    </div> 
</form> 
</body> 
</html> 


Code for RepeaterControl.aspx.cs,
using System; 
using System.Collections; 
using System.Configuration; 
using System.Data; 
using System.Data.SqlClient; 
using System.Web.UI.WebControls; 

namespace AspRepeater 

public partial class RepeaterControl : System.Web.UI.Page 
{  
    private int iPageSize = 15; 

    protected void Page_Load(object sender, EventArgs e) 
    { 
        if (!IsPostBack) 
        { 
            GetCustomers(); 
        } 
    } 

    private void GetCustomers() 
    { 
        DataTable dtData = new DataTable(); 
        string conString = ConfigurationManager.ConnectionStrings["dbConnection"].ConnectionString; 
        SqlConnection sqlCon = new SqlConnection(conString); 
        sqlCon.Open(); 
        SqlCommand sqlCmd = new SqlCommand("Select * From tblCustomers", sqlCon); 
        SqlDataAdapter sqlDa = new SqlDataAdapter(sqlCmd); 
        sqlDa.Fill(dtData); 
        sqlCon.Close(); 

        PagedDataSource pdsData = new PagedDataSource(); 
        DataView dv = new DataView(dtData); 
        pdsData.DataSource = dv; 
        pdsData.AllowPaging = true; 
        pdsData.PageSize = iPageSize; 
        if (ViewState["PageNumber"] != null) 
            pdsData.CurrentPageIndex = Convert.ToInt32(ViewState["PageNumber"]); 
        else 
            pdsData.CurrentPageIndex = 0; 
        if (pdsData.PageCount > 1) 
        { 
            Repeater1.Visible = true; 
            ArrayList alPages = new ArrayList(); 
            for (int i = 1; i <= pdsData.PageCount; i++) 
                alPages.Add((i).ToString()); 
            Repeater1.DataSource = alPages; 
            Repeater1.DataBind(); 
        } 
        else 
        { 
            Repeater1.Visible = false; 
        } 
        Repeater2.DataSource = pdsData; 
        Repeater2.DataBind(); 
    } 
     
    protected void Repeater1_ItemCommand(object source, RepeaterCommandEventArgs e) 
    { 
        ViewState["PageNumber"] = Convert.ToInt32(e.CommandArgument); 
        GetCustomers(); 
    } 



ASP.NET Repeater Control with Stored Procedure

Step 4
By using a Stored Procedure, we can fetch only one-page records from the available records based on the page index. For example, if our table has 300 records and we need to display only 15 records per page, then we will fetch only 15 records based on the page index.

Script for the Stored Procedure,
CREATE PROCEDURE GetCustomer 
@PageIndex INT = 1, 
@PageSize INT = 15, 
@RecordCount INT OUTPUT 
AS 
BEGIN 
SET NOCOUNT ON; 
SELECT ROW_NUMBER() OVER(ORDER BY Id ASC)AS RowNumber,ID, 
Name,Company,Phone,Address,Country,Email 
INTO #Results FROM tblCustomers 
  
SELECT @RecordCount = COUNT(*) FROM #Results 
        
SELECT * FROM #Results WHERE RowNumber BETWEEN(@PageIndex -1) * @PageSize + 1  
AND (((@PageIndex -1) * @PageSize + 1) + @PageSize) - 1 
  
DROP TABLE #Results 
END 

Step 5
Now, create a new webform “RepeaterControl1” and write the following code in your “RepeaterControl1.aspx” page.

Code for RepeaterControl.aspx page.

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="RepeaterControl1.aspx.cs" Inherits="AspRepeater.RepeaterControl1" %> 

<!DOCTYPE html> 

<html xmlns="http://www.w3.org/1999/xhtml"> 
<head runat="server"> 
<title>Repeater Control with Stored Procedure</title> 
</head> 
<body> 
<form id="form1" runat="server"> 
    <div> 
        <asp:Repeater ID="Repeater1" runat="server"> 
            <HeaderTemplate> 
                <table id="tbDetails" style="width: 100%; border-collapse: collapse;" border="1" cellpadding="5" cellspacing="0"> 
                    <tr style="background-color: lightgray; height: 30px; color: black; text-align: center"> 
                        <th>Id</th> 
                        <th>Customer Name</th> 
                        <th>Company Name</th> 
                        <th>Phone</th> 
                        <th>Address</th> 
                        <th>E-Mail</th> 
                    </tr> 
            </HeaderTemplate> 
            <ItemTemplate> 
                <tr style="height: 25px;"> 
                    <td> 
                        <%#Eval("Id").ToString()%> 
                    </td> 
                    <td> 
                        <%#Eval("Name").ToString()%> 
                    </td> 
                    <td> 
                        <%#Eval("Company").ToString()%> 
                    </td> 
                    <td> 
                        <%#Eval("Phone").ToString()%> 
                    </td> 
                    <td> 
                        <%#Eval("Address").ToString()%>, <%#Eval("Country").ToString()%> 
                    </td> 
                    <td> 
                        <%#Eval("Email").ToString()%> 
                    </td> 
                </tr> 
            </ItemTemplate> 
            <FooterTemplate> 
                </table> 
            </FooterTemplate> 
        </asp:Repeater> 
    </div> 
    <br /> 
    <div style="text-align:center"> 
        <asp:Repeater ID="Repeater2" runat="server" OnItemCommand="Repeater2_ItemCommand"> 
            <ItemTemplate> 
                <asp:LinkButton ID="lnkPage" 
                    Style="padding: 8px; margin: 2px; background: lightgray; border: solid 1px #666; color: black; font-weight: bold" 
                    CommandName="Page" CommandArgument="<%# Container.DataItem %>" runat="server" Font-Bold="True"><%# Container.DataItem %> 
                </asp:LinkButton> 
            </ItemTemplate> 
        </asp:Repeater> 
    </div> 
</form> 
</body> 
</html> 

Code for RepeaterControl1.aspx.cs.
using System; 
using System.Collections.Generic; 
using System.Configuration; 
using System.Data; 
using System.Data.SqlClient; 
using System.Web.UI.WebControls; 

namespace AspRepeater 

public partial class RepeaterControl1 : System.Web.UI.Page 

    private int iPageSize = 15; 
    protected void Page_Load(object sender, EventArgs e) 
    { 
        if (!IsPostBack) 
        { 
            this.GetCustomers(1); 
        } 
    } 

    private void GetCustomers(int iPageIndex) 
    { 
        string conString = ConfigurationManager.ConnectionStrings["dbConnection"].ConnectionString; 
        SqlConnection sqlCon = new SqlConnection(conString); 
        sqlCon.Open(); 
        SqlCommand sqlCmd = new SqlCommand("GetCustomer", sqlCon); 
        sqlCmd.CommandType = CommandType.StoredProcedure; 
        sqlCmd.Parameters.AddWithValue("@PageIndex", iPageIndex); 
        sqlCmd.Parameters.AddWithValue("@PageSize", iPageSize); 
        sqlCmd.Parameters.Add("@RecordCount", SqlDbType.Int, 4); 
        sqlCmd.Parameters["@RecordCount"].Direction = ParameterDirection.Output; 
        IDataReader iDr = sqlCmd.ExecuteReader(); 
        Repeater1.DataSource = iDr; 
        Repeater1.DataBind(); 
        iDr.Close(); 
        sqlCon.Close(); 
        int iRecordCount = Convert.ToInt32(sqlCmd.Parameters["@RecordCount"].Value); 

        double dPageCount = (double)((decimal)iRecordCount / Convert.ToDecimal(iPageSize)); 
        int iPageCount = (int)Math.Ceiling(dPageCount); 
        List<ListItem> lPages = new List<ListItem>(); 
        if (iPageCount > 0) 
        { 
            for (int i = 1; i <= iPageCount; i++) 
                lPages.Add(new ListItem(i.ToString(), i.ToString(), i != iPageIndex)); 
        } 
        Repeater2.DataSource = lPages; 
        Repeater2.DataBind(); 
    } 

    protected void Repeater2_ItemCommand(object source, RepeaterCommandEventArgs e) 
    { 
        int iPageIndex = Convert.ToInt32(e.CommandArgument); 
        GetCustomers(iPageIndex); 
    } 

 



European ASP.NET Core Hosting :: TextBox Autocomplete with .NET Core 3.0

clock October 11, 2019 11:56 by author Peter

.NET Core 3.0 is the latest version of .NET Core and now supports WinForms and WPF. This sample shows how you can perform fill data to Autocomplete TextBox in another thread without slow down the main UI (User Interface). With this sample, you can do a better user experience for the end-user.

TextBox Autocomplete
Autocomplete helps the user to search/type from a know list. In my application, I build a list to Autocomplete with > 10.000 items and is very fast even on slow computers.

Autocomplete is an old feature, but this POST will demonstrate how to perform his use to load data fastly already in NET Core 3.0.

When you fill a Collection, if you have a huge suggestion list, this will make the UI (User Interface) freezes. But if you process in another thread the UI will not have any issues.

More technical information you can have here.

How it works

Like the other POST what uses a delegate, here, we use another thread to load data and delegate to bind the Autocomplete Collection in the current UI thread.
using System;  
using System.Data.SqlClient;  
using System.Drawing;  
using System.Windows.Forms;  
 
namespace TextBoxWithAutoComplete  
{  
 
    /// <summary>  
    /// 10-10-2019  
    /// </summary>  
    public partial class Form1 : Form  
    {  
 
        private TextBox textBox1;  
        public Form1()  
        {  
            InitializeComponent();  
 
            textBox1 = new TextBox  
            {  
                Location = new Point(0, 0),  
                Size = new Size(100, 32),  
                Visible = true,  
                TabIndex = 0  
            };  
            Controls.Add(textBox1);  
 
            Load += Form1_Load;  
        }  
 
        public void Form1_Load(object sender, EventArgs e)  
        {  
            Show(); // You need to show the form to avoid a thread error  
 
            LoadData(); // Start to load Data  
        }  
 
        private SqlConnection GetConnection()  
        {  
            // Init your connection  
            var oCnn = new SqlConnection  
            {  
                ConnectionString = "YOUR_CONNECTION_STRING"  
            };  
            oCnn.Open();  
            return oCnn;  
        }  
 
 
        #region TreadSafeLoading  
        private delegate void UpdateUIDelegate(AutoCompleteStringCollection myCollection);  
 
        /// <summary>  
        /// Load Data - You can make public to load as you need  
        /// </summary>  
        private void LoadData()  
        => // Start this process in a new thread  
            new System.Threading.Thread(() => OutOfThreadProcess()).Start();  
        // You can start others, every one in a new thread!  
 
        /// <summary>  
        /// Start fill in another Thread  
        /// </summary>  
        private void OutOfThreadProcess()  
        {  
            // inialize the collection  
            var myCollection = new AutoCompleteStringCollection();  
            using var oCnn = GetConnection();  
 
            // Start your SQL Query  
            var cmd = new SqlCommand($"Select [FirstName] + ' ' + [LastName] as [contactName] From [AdventureWorks].[Person].[Contact] Order By [FirstName], [LastName]", oCnn);  
 
            // Read and fill the collection  
            using var reader = cmd.ExecuteReader();  
            while (reader.Read())  
                myCollection.Add(reader.GetString(0));  
 
            // Invoke in a UI thread  
            _ = Invoke(new UpdateUIDelegate(UpdateUIInvoke), myCollection);  
        }  
 
        /// <summary>  
        /// Current UI Thread threatment  
        /// </summary>  
        /// <param name="myCollection"></param>  
        private void UpdateUIInvoke(AutoCompleteStringCollection myCollection)  
        {  
            // Setup up in corrent UI Thread  
            textBox1.AutoCompleteMode = AutoCompleteMode.SuggestAppend;  
            textBox1.AutoCompleteSource = AutoCompleteSource.CustomSource;  
            textBox1.AutoCompleteCustomSource = myCollection;  
#if (IS_TELERIK_RadTextBoxControl)  
            // If you use Telerik  
            // I found an issue in Telerik by Progress, without fix.  
            if (ParentForm != null)  
                ParentForm.FormClosing += new FormClosingEventHandler(UIFormClosing);  
#endif  
        }  
#if (IS_TELERIK_RadTextBoxControl)  
        private void UIFormClosing(object sender, FormClosingEventArgs e) => textBox1.AutoCompleteCustomSource = null;  
 
#endif  
        #endregion  
 
    }  
}  

Observation

When you use this technic may you receive errors if you run the new thread without fire the Show() method from the form, this occurs because the current window(form) hasn't the Handle initialized.

Conclusion
I design this technic when I upgrade to .NET Core 3 and develop a fix for the FileSystemWatcher event that fires in a new thread, so I took the same idea to make Autocomplete load collection works in a new thread.



European ASP.NET Core Hosting :: Knowing about WinForms and .NET Core 3.0

clock October 11, 2019 09:29 by author Scott

Desktop support for .NET Core 3.0 has been in preview for some time, it is slated to be released later this year. As per indicators by Microsoft, .NET Core will receive more updates compared to full .NET Framework, keeping this in mind, it makes sense to start thinking about migrating existing applications and creating new ones on .NET Core if the application still has some years to go.

Desktop support in .NET Core has not been completely migrated yet; however, it is possible to create and migrate WinForms applications using the preview bits. In this article, we'll consider the creation and migration of a WinForms application in .NET Core 3.0 that uses ComponentOne controls.

Creating a New .NET Core 3.0 Application

To create a new .NET Core 3.0 application, it's recommended to download VS 2019 and the nightly build of the .NET Core 3.0 SDK. Make sure that you’ve installed the .NET Framework 4.7.2 and .NET Core 2.1 development tools in the Visual Studio Installer. This software will be necessary whether you plan on creating a new project or migrating an existing one to .NET Core 3.0.

The simplest way to work with .NET Core 3.0 (for now) is to use dotnet.exe command line utility. It can be used to create a new project, add/restore dependencies, build, etc.

.NET Core 3 - Using x64 and x86 Versions

Visual Studio uses the appropriate version depending on your .NET Core 3 project’s target platform – x86 or x64. It uses last installed version for both platforms. So, if you intend to build your project for both platforms, you must download and install both platform SDK versions with the same version.

These are the typical paths for .NET Core 3 command line utility:

  • c:\Program Files\dotnet\dotnet.exe - for x64 version of .Net Core 3
  • c:\Program Files (x86)\dotnet\dotnet.exe- for x86 version of .Net Core 3

Let’s use x64 version of dotnet.exe to simplify it for further description.

Create a New Visual Studio Project

First, you’ll need to open the Visual Studio Developer Command Prompt to create a project through the command line. Currently, the tooling for Visual Studio is very limited in its support of .NET Core 3.0, so it’s easiest to create a project through the command line interface.

First, we’ll create the project:

“c:\Program Files\dotnet\dotnet.exe” **new** winforms -o TestAppWinFormsCore

Once this is finished, we’ll navigate into the project directory:

cd TestAppWinFormsCore

Finally, we can run the project to make sure that it works:

“c:\Program Files\dotnet\dotnet.exe” run

Now that the project has been created, we can use Visual Studio 2019 to open it and modify its contents. 

In the Visual Studio 2019, open the TestAppWinFormsCore.csproj file we just created.

You should see something like this in Visual Studio:

.NET Core 3.0 uses a new project type and has no design-time support currently, so the process of adding controls will be a little different than what you may be accustomed to. We need to add C1 libraries manually to the project to make use of them, also, we should add the controls manually to Form1.cs because of missing design-time support.

Because of this, let’s use a trick. We'll add the classic .NET Framework WinForms project in order to use its Form designer:

1. Right-click on Solution node and Add – New Project… - Windows Forms Desktop project (with .Net Framework, say version 4.0). The name of new project is by default WindowsFormsApp1.

2. Remove the existing Form1 from WindowsFormsApp1 project

3. Add Form1.cs from TestAppWinFoirmsCore project as link:

After that, the following view of Solution Explorer appears:

Now, we can edit Form1 form in the usual way:

Now, remove unneeded “Hello .NET Core!” label and add some C1 control, for example C1DockingTab:

Launch WindowsFormsApp1 application and observe C1DockingTab on Form1. But this will be a .Net Framework 4 application.

Adding C1DockingTab caused adding C1.Win.C1Command.4 reference to WindowsFormsApp1 project. Therefore, the same reference should be added to TestAppWinFormsCore project also. For this let’s take a look in the properties of the C1.Win.C1Command.4 reference (right-click – Properties…) and find the path to the assembly. It looks like C:\Program Files (x86)\ComponentOne\WinForms Edition\bin\v4.0\C1.Win.C1Command.4.dll. Add this reference to the TestAppWinFormsCore project (right-click on Dependencies – Add reference… - Browse… - choose C:\Program Files (x86)\ComponentOne\WinForms Edition\bin\v4.0\C1.Win.C1Command.4.dll).

You should be able to run the code at this point, though you’ll notice a ComponentOne nag screen since the TestAppWinFormsCore project doesn’t contain any licensing information.

We can correct this by adding a licenses.licx file to the project. To do so, right-click on TestAppWinFormsCore project and select Add -> Existing Items. And browse the licenses.licx file from WindowsFormsApp1/Properties folder.

Build and rerun the app and you should no longer see a nag screen.

And now this is a genuine .NET Core 3.0 WinForms app. You can see this in the Modules window. All system references are from C:\Program Files\dotnet... folder.

Migrating an Existing Project to .NET Core 3.0

It is possible to migrate an existing project to .NET Core 3.0, though the process may require a few more steps than you would expect. Also, before trying to migrate a project, it’s worth running Microsoft’s portability analyzer tool on the project you want to convert.

The tool can give you an idea (ahead of time) how compatible your project will be with .NET Core 3.0, and it points out potential problems you may run into (included unsupported APIs).

If you’ve made up your mind to try to migrate a project, it’s easiest to begin the process by creating a project through dotnet.exe as described above.Let’s look at the WeatherChart sample migration. This sample demonstrates the C1FlexChart control possibilities.

First, we’ll create a new DotNetCore3FlexChart project from the command line:

“c:\Program Files\dotnet\dotnet.exe” **new** winforms -o DotNetCore3FlexChart

Once this is finished, we’ll navigate into the project directory and open DotNetCore3FlexChart.csproj from Visual Studio 2019.

Next, add the WeatherChart project to the DotNetCore3FlexChart solution. WeatherChart project is placed in Documents\ComponentOne Samples\WinForms\C1FlexChart\CS\WeatherChart\WeatherChart\WeatherChart.csproj by default.

Here, we'll use the WeatherChart project in our .NET Core 3.0 project. Therefore, the WeatherChart project reference should be added to DotNetCore3FlexChart dependencies (right-click on Dependencies – Add Reference… - Projects - WeatherChart)

After that, open Project.cs from DotNetCore3FlexChart project and change the following line:

Application.Run(new Form1()); 

by 

Application.Run(new WeatherChart.Form1()); 

It means that the .NET Core 3.0 app (DotNetCore3FlexChart) will run WeatherChart.Form1 from WeatherChart assembly instead of own Form1\.

That’s all!

If you run the WeatherChart project it will be launched as .NET Framework 4.0 app.

But if you run the DotNetCore3FlexChart project then the WeatherChart assembly will be launched in .NET Core 3.0 environment.

.NET Core 3.0 Preview Caveats

Using the .NET Core 3.0 Preview for any kind of day-to-day work isn’t recommended at this point, as it’s obviously still work in progress. As the steps above illustrate, much of the project creation and migration processes require a lot of manual configuration from the user, and it’s possible to find some bugs and unfinished implementations presently.The lack of designer support also makes working with the preview somewhat difficult, though Microsoft has committed to adding this feature in the coming months.

Overall, .NET Core 3.0 will be an important change for developers, though this preview is merely the first step.



European ASP.NET Core 3 Hosting :: ASP.NET Core 3 Certificate Authentication

clock October 8, 2019 12:04 by author Scott

This article shows how Certificate Authentication can be implemented in ASP.NET Core 3.0. In this example, a shared self signed certificate is used to authenticate one application calling an API on a second ASP.NET Core application.

Setting up the Server

Add the Certificate Authentication using the Microsoft.AspNetCore.Authentication.Certificate NuGet package to the server ASP.NET Core application.

 

This can also be added directly in the csproj file.

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

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Authentication.Certificate"
      Version="3.0.0-preview6.19307.2" />
  </ItemGroup>

  <ItemGroup>
    <None Update="sts_dev_cert.pfx">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>

</Project>

The authentication can be added in the ConfigureServices method in the Startup class. This example was built using the ASP.NET Core documentation. The AddAuthentication extension method is used to define the default scheme as “Certificate” using the CertificateAuthenticationDefaults.AuthenticationScheme string. The AddCertificate method then adds the configuration for the certificate authentication. At present, all certificates are excepted which is not good and the MyCertificateValidationService class is used to do extra validation of the client certificate. If the validation fails, the request is failed and the request for the resource will be rejected.

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<MyCertificateValidationService>(); 

    services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
        .AddCertificate(options => // code from ASP.NET Core sample
        {
            options.AllowedCertificateTypes = CertificateTypes.All;
            options.Events = new CertificateAuthenticationEvents
            {
                OnCertificateValidated = context =>
                {
                    var validationService =
                        context.HttpContext.RequestServices.GetService<MyCertificateValidationService>(); 
                    if (validationService.ValidateCertificate(context.ClientCertificate))
                    {
                        var claims = new[]
                        {
                            new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer),
                            new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer)
                        }; 

                        context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
                        context.Success();
                    }
                    else
                    {
                        context.Fail("invalid cert");
                    } 

                    return Task.CompletedTask;
                }
            };
        }); 

    services.AddAuthorization(); 

    services.AddControllers();
}

The AddCertificateForwarding method is used so that the client header can be specified and how the certificate is to be loaded using the HeaderConverter option. When sending the certificate with the HttpClient using the default settings, the ClientCertificate was always be null. The X-ARR-ClientCert header is used to pass the client certificate, and the cert is passed as a string to work around this.

services.AddCertificateForwarding(options =>
{
    options.CertificateHeader = "X-ARR-ClientCert";
    options.HeaderConverter = (headerValue) =>
    {
        X509Certificate2 clientCertificate = null;
        if(!string.IsNullOrWhiteSpace(headerValue))
        {
            byte[] bytes = StringToByteArray(headerValue);
            clientCertificate = new X509Certificate2(bytes);
        }

        return clientCertificate;
    };
});

The Configure method then adds the middleware. UseCertificateForwarding is added before the UseAuthentication and the UseAuthorization.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...     
    app.UseRouting(); 

    app.UseCertificateForwarding();
    app.UseAuthentication();
    app.UseAuthorization(); 

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

The MyCertificateValidationService is used to implement validation logic. Because we are using self signed certificates, we need to ensure that only our certificate can be used. We validate that the thumbprints of the client certificate and also the server one match, otherwise any certificate can be used and will be be enough to authenticate.

using System.IO;
using System.Security.Cryptography.X509Certificates; 

namespace AspNetCoreCertificateAuthApi
{
    public class MyCertificateValidationService
    {
        public bool ValidateCertificate(X509Certificate2 clientCertificate)
        {
            var cert = new X509Certificate2(Path.Combine("sts_dev_cert.pfx"), "1234");
            if (clientCertificate.Thumbprint == cert.Thumbprint)
            {
                return true;
            } 

            return false;
        }
    }
}

The API ValuesController is then secured using the Authorize attribute.

[Route("api/[controller]")]
[ApiController]
[Authorize]
public class ValuesController : ControllerBase


...

The ASP.NET Core server project is deployed in this example as an out of process application using kestrel. To use the service, a certificate is required. This is defined using the ClientCertificateMode.RequireCertificate option.

public static IWebHost BuildWebHost(string[] args)
  => WebHost.CreateDefaultBuilder(args)
  .UseStartup<Startup>()
  .ConfigureKestrel(options =>
  {
    var cert = new X509Certificate2(Path.Combine("sts_dev_cert.pfx"), "1234");
    options.ConfigureHttpsDefaults(o =>
    {
        o.ServerCertificate = cert;
        o.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
    });
  })
  .Build();

Implementing the HttpClient

The client of the API uses a HttpClient which was create using an instance of the IHttpClientFactory. This does not provide a way to define a handler for the HttpClient and so we use a HttpRequestMessage to add the Certificate to the “X-ARR-ClientCert” request header. The cert is added as a string using the GetRawCertDataString method.

private async Task<JArray> GetApiDataAsync()
{
    try
    {
        var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234"); 

        var client = _clientFactory.CreateClient(); 

        var request = new HttpRequestMessage()
        {
            RequestUri = new Uri("
https://localhost:44379/api/values"),
            Method = HttpMethod.Get,
        }; 

        request.Headers.Add("X-ARR-ClientCert", cert.GetRawCertDataString());
        var response = await client.SendAsync(request); 

        if (response.IsSuccessStatusCode)
        {
            var responseContent = await response.Content.ReadAsStringAsync();
            var data = JArray.Parse(responseContent); 

            return data;
        } 

        throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
    }
    catch (Exception e)
    {
        throw new ApplicationException($"Exception {e}");
    }
}

If the correct certificate is sent to the server, the data will be returned. If no certificate is sent, or the wrong certificate, then a 403 will be returned. It would be nice if the IHttpClientFactory would have a way of defining a handler for the HttpClient. I also believe a non valid certificates should fail per default and not require extra validation for this. The AddCertificateForwarding should also not be required to use for a default HTTPClient client calling the service.

Certificate Authentication is great, and helps add another security layer which can be used together with other solutions. See the code and ASP.NET Core src code for further documentation and examples. Links underneath. 



About HostForLIFE.eu

HostForLIFE.eu is European Windows Hosting Provider which focuses on Windows Platform only. We deliver on-demand hosting solutions including Shared hosting, Reseller Hosting, Cloud Hosting, Dedicated Servers, and IT as a Service for companies of all sizes.

We have offered the latest Windows 2016 Hosting, ASP.NET Core 2.2.1 Hosting, ASP.NET MVC 6 Hosting and SQL 2017 Hosting.


Tag cloud

Sign in