Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / productivity / SharePoint / SharePoint2013

SharePoint 2013 Low-Trust Provider-Hosted Add-In Setup Guide

5.00/5 (2 votes)
12 Dec 2017CPOL9 min read 13.4K  

1Introduction

The purpose of this article is to provide step by step instructions for setting up a low-trust provider-hosted SharePoint add-in where the external application is hosted in IIS. The external application will be setup to run CRUD (Create/Read/Update/Delete) operations against SharePoint without the need to start the application from within SharePoint.

2Setup Steps

2.1Configure SharePoint on-premises to use Azure Access Control Service (ACS) to authorize the provider-hosted add-in

2.1.1Install PowerShell modules

2.1.1.1Microsoft Online Services Sign-In Assistant

  1. Download Microsoft Online Services Sign-In Assistant PowerShell module from: https://www.microsoft.com/en-us/download/details.aspx?id=41950
  2. Install the module on your SharePoint 2013 environment

2.1.1.2Microsoft Azure Active Directory

  1. Download Microsoft Azure Active Directory PowerShell module from:

http://connect.microsoft.com/site1164/Downloads/DownloadDetails.aspx?DownloadID=59185

  1. Install the module on your SharePoint 2013 environment

2.1.2Create a certificate

  1. On your SharePoint 2013 server, open IIS Manager
  2. Click on your server name
  3. Double-click on Server Certificates

 

  1. Click on Create Self-Signed Certificate on the right hand side
  2. Enter a name for the certificate, then hit OK
  3. Right-click the certificate, then Export
  4. Select a location to save the certificate, and enter a password

  1. Click OK

2.1.3Make the certificate the Security Token Service (STS) certificate for your SharePoint 2013 environment

  1. On your SharePoint 2013 server, open SharePoint Management Shell as an administrator
  2. Run the following PowerShell script
PowerShell
$certPrKPath = "c:\location_of_your_.pfx_file"
$certPassword = "your_password"
$stsCertificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $certPrKPath, $certPassword, 20
Set-SPSecurityTokenServiceConfig -ImportSigningCertificate $stsCertificate -confirm:$false

2.1.4Prepare your SharePoint 2013 environment to use Azure Access Control Service (ACS)

  1. On your SharePoint 2013 server, go to the following path: C:\windows\system32\windowspowershell\V1.0\modules
  2. Create a new folder named: Connect-SP-Farm-To-AAD
  3. Copy the following script into a text file and save it with the name Connect-SP-Farm-To-AAD.psm1
PowerShell
function Connect-SPFarmToAAD {

param(

    [Parameter(Mandatory)][String]   $AADDomain,

    [Parameter(Mandatory)][String]   $SharePointOnlineUrl,

    #Specify this parameter if you don't want to use the default SPWeb returned

    [Parameter()][String]            $SharePointWeb,

    [Parameter()][System.Management.Automation.PSCredential] $O365Credentials,

    #Use these switches if you're replacing an existing connection to AAD.

    [Parameter()][Switch]            $RemoveExistingACS,

    [Parameter()][Switch]            $RemoveExistingSTS,

    [Parameter()][Switch]            $RemoveExistingSPOProxy,

    #Use this switch if you're replacing the Office 365 SharePoint site.

    [Parameter()][Switch]            $RemoveExistingAADCredentials,

    #Use this switch if you don't want to use SSL when you launch your app.

    [Parameter()][Switch]            $AllowOverHttp

)

    #Prompt for credentials right away.

    if (-not $O365Credentials) {

        $O365Credentials = Get-Credential -Message "Admin credentials for $AADDomain"

    }

    Add-PSSnapin Microsoft.SharePoint.PowerShell

    #Import the Microsoft Online Services Sign-In Assistant.

    Import-Module -Name MSOnline

    #Import the Microsoft Online Services Module for Windows Powershell.

    Import-Module MSOnlineExtended -force -verbose

    #Set values for Constants.

    New-Variable -Option Constant -Name SP_APPPRINCIPALID -Value '00000003-0000-0ff1-ce00-000000000000' | Out-Null

    New-Variable -Option Constant -Name ACS_APPPRINCIPALID -Value '00000001-0000-0000-c000-000000000000' | Out-Null

    New-Variable -Option Constant -Name ACS_APPPROXY_NAME -Value ACS

    New-Variable -Option Constant -Name SPO_MANAGEMENT_APPPROXY_NAME -Value 'SPO Add-in Management Proxy'

    New-Variable -Option Constant -Name ACS_STS_NAME -Value ACS-STS

    New-Variable -Option Constant -Name AAD_METADATAEP_FSTRING -Value 'https://accounts.accesscontrol.windows.net/{0}/metadata/json/1'

    New-Variable -Option Constant -Name SP_METADATAEP_FSTRING -Value '{0}/_layouts/15/metadata/json/1'

    #Get the default SPWeb from the on-premises farm if no $SharePointWeb parameter is specified.

    if ([String]::IsNullOrEmpty($SharePointWeb)) {

        $SharePointWeb = Get-SPSite | Select-Object -First 1 | Get-SPWeb | Select-Object -First 1 | % Url

    }


    #Configure the realm ID for local farm so that it matches the AAD realm.

    $ACSMetadataEndpoint = $AAD_METADATAEP_FSTRING -f $AADDomain

    $ACSMetadata = Invoke-RestMethod -Uri $ACSMetadataEndpoint

    $AADRealmId = $ACSMetadata.realm


    Set-SPAuthenticationRealm -ServiceContext $SharePointWeb -Realm $AADRealmId


    $LocalSTS = Get-SPSecurityTokenServiceConfig

    $LocalSTS.NameIdentifier = '{0}@{1}' -f $SP_APPPRINCIPALID,$AADRealmId

    $LocalSTS.Update()


    #Allow connections over HTTP if the switch is specified.

    if ($AllowOverHttp.IsPresent -and $AllowOverHttp -eq $True) {

        $serviceConfig = Get-SPSecurityTokenServiceConfig

        $serviceConfig.AllowOAuthOverHttp = $true

        $serviceConfig.AllowMetadataOverHttp = $true

        $serviceConfig.Update()

    }


    #Step 1: Set up the ACS proxy in the on-premises SharePoint farm. Remove the existing ACS proxy

    #if the switch is specified.

    if ($RemoveExistingACS.IsPresent -and $RemoveExistingACS -eq $True) {

        Get-SPServiceApplicationProxy | ? DisplayName -EQ $ACS_APPPROXY_NAME | Remove-SPServiceApplicationProxy -RemoveData -Confirm:$false

    }

    if (-not (Get-SPServiceApplicationProxy | ? DisplayName -EQ $ACS_APPPROXY_NAME)) {

        $AzureACSProxy = New-SPAzureAccessControlServiceApplicationProxy -Name $ACS_APPPROXY_NAME -MetadataServiceEndpointUri $ACSMetadataEndpoint -DefaultProxyGroup

    }


    #Remove the existing security token service if the switch is specified.

    if ($RemoveExistingSTS.IsPresent) {

        Get-SPTrustedSecurityTokenIssuer | ? Name -EQ $ACS_STS_NAME | Remove-SPTrustedSecurityTokenIssuer -Confirm:$false

    }

    if (-not (Get-SPTrustedSecurityTokenIssuer | ? DisplayName -EQ $ACS_STS_NAME)) {

        $AzureACSSTS = New-SPTrustedSecurityTokenIssuer -Name $ACS_STS_NAME -IsTrustBroker -MetadataEndPoint $ACSMetadataEndpoint

    }


    #Update the ACS Proxy for OAuth authentication.

    $ACSProxy = Get-SPServiceApplicationProxy | ? Name -EQ $ACS_APPPROXY_NAME

    $ACSProxy.DiscoveryConfiguration.SecurityTokenServiceName = $ACS_APPPRINCIPALID

    $ACSProxy.Update()


    #Retrieve the local STS signing key from JSON metadata.

    $SPMetadata = Invoke-RestMethod -Uri ($SP_METADATAEP_FSTRING -f $SharePointWeb)

    $SPSigningKey = $SPMetadata.keys | ? usage -EQ "Signing" | % keyValue

    $CertValue = $SPSigningKey.value


    #Connect to Office 365.

    Connect-MsolService -Credential $O365Credentials

    #Remove existing connection to an Office 365 SharePoint site if the switch is specified.

    if ($RemoveExistingAADCredentials.IsPresent -and $RemoveExistingAADCredentials -eq $true) {

        $msolserviceprincipal = Get-MsolServicePrincipal -AppPrincipalId $SP_APPPRINCIPALID

        [Guid[]] $ExistingKeyIds = Get-MsolServicePrincipalCredential -ObjectId $msolserviceprincipal.ObjectId -ReturnKeyValues $false | % {if ($_.Type -ne "Other") {$_.KeyId}}

        Remove-MsolServicePrincipalCredential -AppPrincipalId $SP_APPPRINCIPALID -KeyIds $ExistingKeyIds

    }

    #Step 2: Upload the local STS signing certificate

    New-MsolServicePrincipalCredential -AppPrincipalId $SP_APPPRINCIPALID -Type Asymmetric -Value $CertValue -Usage Verify


    #Step 3: Add the service principal name of the local web application, if necessary.

    $indexHostName = $SharePointWeb.IndexOf('://') + 3

    $HostName = $SharePointWeb.Substring($indexHostName)

    $NewSPN = '{0}/{1}' -f $SP_APPPRINCIPALID, $HostName

    $SPAppPrincipal = Get-MsolServicePrincipal -AppPrincipalId $SP_APPPRINCIPALID

    if ($SPAppPrincipal.ServicePrincipalNames -notcontains $NewSPN) {

        $SPAppPrincipal.ServicePrincipalNames.Add($NewSPN)

        Set-MsolServicePrincipal -AppPrincipalId $SPAppPrincipal.AppPrincipalId -ServicePrincipalNames $SPAppPrincipal.ServicePrincipalNames

    }


    #Remove the existing SharePoint Online proxy if the switch is specified.

    if ($RemoveExistingSPOProxy.IsPresent -and $RemoveExistingSPOProxy -eq $True) {

        Get-SPServiceApplicationProxy | ? DisplayName -EQ $SPO_MANAGEMENT_APPPROXY_NAME | Remove-SPServiceApplicationProxy -RemoveData -Confirm:$false

    }

    #Step 4: Add the SharePoint Online proxy

    if (-not (Get-SPServiceApplicationProxy | ? DisplayName -EQ $SPO_MANAGEMENT_APPPROXY_NAME)) {

        $spoproxy = New-SPOnlineApplicationPrincipalManagementServiceApplicationProxy -Name $SPO_MANAGEMENT_APPPROXY_NAME -OnlineTenantUri $SharePointOnlineUrl -DefaultProxyGroup

    } 

}
  1. Copy Connect-SP-Farm-To-AAD.psm1 to C:\Windows\System32\WindowsPowerShell\v1.0\Modules\Connect-SP-Farm-To-AAD
  2. Open the SharePoint Management Shell as an administrator and run the following cmdlet to verify that the Connect-SP-Farm-To-AAD module is listed:
PowerShell
Get-Module –listavailable

  1. Run the following cmdlet to import the module:
PowerShell
import-module Connect-SP-Farm-To-AAD
  1. Run the following cmdlet to verify that the Connect-SPFarmToAAD function is listed as part of the module:
PowerShell
Get-Command -module Connect-SP-Farm-To-AAD
  1. Run the following cmdlet to verify that the Connect-SPFarmToAAD function is loaded.
PowerShell
ls function:\ | where {$_.Name -eq "Connect-SPFarmToAAD"}

 

2.1.5Configure your SharePoint 2013 environment to use Azure Access Control Service (ACS)

  1. In the same SharePoint Management Shell windows that you have opened from last step (2.1.4), run the following script as a farm admin:

Note: you will be prompted for the tenant admin creds. So have them handy.

PowerShell
Connect-SPFarmToAAD -AADDomain 'your_company_name.onmicrosoft.com' -SharePointOnlineUrl https://your_company_name.sharepoint.com -SharePointWeb http://your_on-prem_web_application -AllowOverHttp 

Example:

PowerShell
Connect-SPFarmToAAD -AADDomain 'MyO365Domain.onmicrosoft.com' -SharePointOnlineUrl https://MyO365Domain.sharepoint.com -SharePointWeb http://ecmspotsbx –AllowOverHttp

 

Notes:

  1. SharePointWeb: is the on-prem web application which hosts the site collection which we want the external application to access
  2. If your SharePointWeb is on https and its certificate is not a self-signed certificate, then you do not need: -AllowOverHttp

2.2Create an IIS site to host the external application

2.2.1Create the IIS site

  1. On the server to host the external application, open IIS Manager
  2. Expand the server name
  3. Right click Sites, then click Add Website…
  4. Fill-in the fields as below, then hit OK:
    1. Site name: enter a name for your site
    2. Host name: enter the same site’s name
    3. Physical path: create a new folder under C:\inetpub\wwwroot, name the folder the same name you chose for the site

  1. Enable Directory Browsing
    1. Click on the IIS site that we just created
    2. Double click Directory Browsing

  1. Click Enable on the right hand side

2.2.2Add the IIS site to your Hosts file

  1. Open Notepad as administrator
  2. Navigate to: C:\Windows\System32\Drivers\etc
  3. Select All Files at the right-bottom corner
  4. Open the Hosts file
  5. Add an entry for the IIS site as follows:

127.0.0.1         your_site_name

2.2.3Test the IIS site setup

  1. In IIS Manager, under Sites, click on your site
  2. On the right hand side, click on Browse your_site_name on *:80 (http)
  3. Make sure that you do not get any errors
  4. You should get a page similar to the one below:

2.3Register the add-in in Azure Access Control Service (ACS)

2.3.1Register the add-in

  1. Go to the site collection which the external application will access
  2. Add the following at the end of the site collection’s URL, right after the site collection’s name: /_layouts/15/appregnew.aspx

Example: http://ecmspotsbx/sites/DevSC/_layouts/15/appregnew.aspx

  1. You will get the add-in registration page
  2. For the App Id and App Secret, click on Generate
  3. For the Title, enter a title for your add-in
  4. For the App Domain, enter the host name you entered when creating the IIS site in step: 2.2.1 -> 4 above.
  5. Leave the Redirect URL empty

  1. Click Create
  2. You will get an app identifier successfully created page
  3. Save the App Id, App Secret and App Domain

2.3.2Grant the add-in access to SharePoint

  1. Go to the site collection which the external application will access
  2. Add the following at the end of the site collection’s URL, right after the site collection’s name: /_layouts/15/appinv.aspx

Example:  http://ecmspotsbx/sites/DevSC/_layouts/15/appinv.aspx

  1. Enter your App Id, then hit Lookup.
  2. You will get your app’s registration information:
  3. Add the following permissions to Permission Request XML

<AppPermissionRequests AllowAppOnlyPolicy="true"> 

    <AppPermissionRequest Scope="http://sharepoint/content/sitecollection" Right="FullControl" /> 

  </AppPermissionRequests> 

This will grant the add-in full control access over the site collection, and will tell SharePoint that the add-in will use only the add-in’s permissions and that it will not use the permissions of the user running the add-in.

  1. Click Create
  2. SharePoint will ask you if you want to grant these permissions to the add-in
  3. Hit Trust It

2.3.3Get your SharePoint environment’s Realm

  1. Go to the site collection which the external application will access
  2. Add the following at the end of the site collection’s URL, right after the site collection’s name: /_layouts/15/appprincipals.aspx

Example:  http://ecmspotsbx/sites/DevSC/_layouts/15/appprincipals.aspx

  1. Find your add-in
  2. Save the ID after the “@” sign. This is your Realm.

2.4Create the add-in

2.4.1Create a SharePoint add-in project

  1. On the server where we created the IIS site in step 2.2 above, open Visual Studio 2015 or above as administrator.
  2. File -> New -> Project
  3. Select Office/SharePoint -> Apps -> App for SharePoint

Note: Depending on your version of Visual Studio, the path can be:

Office/SharePoint -> Office Add-ins -> SharePoint Add-in

  1. Enter a name for your project and hit OK
  2. Enter the URL of the site collection which the external application will access
  3. Select Provider-hosted

  1. Click Next
  2. Choose SharePoint 2013
  3. Click Next
  4. Choose ASP.Net Web Forms Application
  5. Click Next
  6. Choose Use Windows Azure Access Control Service (for SharePoint cloud apps)
  7. Click Finish

2.4.2Remove the app project

  1. In Solution Explorer, right-click the app project, then hit Remove

  1. Visual Studio will tell you that the app project will be removed
  2. Hit OK

2.4.3Publish the external app to IIS

  1. In Solution Explorer, right-click the web project, then hit properties
  2. Under Web -> Servers:
    1. Change IIS Express to local IIS
    2. In Project Url, replace localhost with the IIS site’s name we created in step 2.2 -> 2.2.1 above
    3. Click Create Virtual Directory

  1. You will get a message that the virtual directory has been created successfully
  2. Hit OK
  3. Hit Save

2.4.4Confirm that the Virtual Directory has been created successfully

  1. Go to IIS
  2. Right-click Sites, then hit Refresh
  3. Expand your IIS site
  4. Make sure that you can see your virtual directory

2.4.5Update the external application code

2.4.5.1Update the web.config

  1. In Solution Explorer, under the web project, open web.config
  2. Update the ClientId and ClientSecret entries with the values we saved in step 2.3 -> 2.3.1 -> 10 above. Make sure that there are no spaces in the value.
  3.  Add the following entries right after the ClientSecret entry:
C#
<add key="HostedAppHostName" value=""/>
<add key="Realm" value=""/>
<add key="SPHostUrl" value=""/>
<add key="SPAppWebUrl" value=""/>
  1. For HostedAppHostName value: enter the App Domain we saved in step 2.3 -> 2.3.1 -> 10 above

Example: LowTrustAddIn

  1. For Realm value: enter the realm we saved in step 2.3 -> 2.3.3 -> 4 above

Example: fa2e957d-d348-4cf8-94cf-dae799dfbbda

  1. For SPHostUrl value: enter the URL of the site collection which the external application will access

Example: http://ecmspotsbx/sites/DevSC

  1. For SPAppWebUrl value: enter the Project URL of step 2.4.3 above followed by: /Pages/Default.aspx

Example: http://LowTrustAddIn/LowTrustAddInWeb/Pages/Default.aspx

  1. Your appSettings should look similar to this:

2.4.5.2Update Default.aspx

  1. In Solution Explorer, under the web project -> pages, open Default.aspx
  2. Remove everything between <body></body>
  3. Add the following between <body></body>:
HTML
<a name="_Hlk499710851"><</a>form id="form1" runat="server">
        <div style="margin-bottom: 10px;">
            <b>List:</b>
            <asp:Label runat="server" ID="lblListUrl" />
        </div>
        <div style="margin-bottom: 10px;">
            <b>User:</b>
            <asp:Label runat="server" ID="lblUserName" />
        </div>
        <div style="margin-bottom: 10px;">
            <b>ID:</b>
            <asp:TextBox ID="txtItemId" runat="server"></asp:TextBox>
            <b>Status:</b>
            <asp:TextBox ID="txtItemStatus" runat="server"></asp:TextBox>
            <asp:Button ID="btnUpdateItem" runat="server" Text="Update Status" OnClick="btnUpdateItem_Click" />
        </div>
        <div>
            <span style="color: green"><asp:Label runat="server" ID="lblOperationSuccess" /></span>
            <span style="color: red"><asp:Label runat="server" ID="lblOperationFailure" /></span>
        </div>
    </form>
  1. Your final Default.aspx should look similar to this:

2.4.5.3Update Default.aspx.cs

  1. In Solution Explorer, under the web project -> pages, open Default.aspx.cs
  2. Remove the Page_PreInit() method
  3. Add a reference to the following namespaces:
    1. System.Web.Configuration
    2. Microsoft.IdentityModel.SecurityTokenService
    3. Microsoft.SharePoint.Client
C#
using System.Web.Configuration;
using Microsoft.IdentityModel.SecurityTokenService;
using Microsoft.SharePoint.Client;
  1. Add the following before the Page_Load() method:
C#
private string _siteUrl = "http://ecmspotsbx/sites/DevSC/TS1";
private string _listName = "Test List";
private string _fieldName = "Status";
private List _list;
private ClientContext _clientContext;
  1. _siteUrl: The URL of the site hosting the list we want to update

The site must be within the site collection where we registered the add-in in step 2.3 above.

  1. _listName: The Name of the list we want to update
  2. _fieldName: The list field’s name we want to update
  1. Replace everything in the Page_Load() method with the following:
C#
lblOperationSuccess.Text = "";
lblOperationFailure.Text = "";
lblListUrl.Text = _listName + " @ " + _siteUrl;

_clientContext = GetClientContextForUrl(_siteUrl);

// Get user name
var web = _clientContext.Web;
 _clientContext.Load(web, w => w.CurrentUser);
 _clientContext.ExecuteQuery();
lblUserName.Text = web.CurrentUser.Title;

 _list = _clientContext.Web.Lists.GetByTitle(_listName);

 

 

  1. Add the btnUpdateItem_Click() method after the Page_Load() method:
C#
protected void btnUpdateItem_Click(object sender, EventArgs e)
 {
     try
     {
         var itemId = txtItemId.Text;
         var itemStatus = txtItemStatus.Text;
         var listItem = _list.GetItemById(itemId);
         listItem[_fieldName] = itemStatus;
         listItem.Update();
         _clientContext.ExecuteQuery();

         lblOperationSuccess.Text = "Operation has been completed successfully!";

     }
     catch (Exception ex)
     {
         lblOperationFailure.Text = "Operation failed! " + ex.Message;
     }

 }

  1. Add the GetClientContextForUrl() method after the btnUpdateItem_Click() method:
C#
private ClientContext GetClientContextForUrl(string url)
{
    var targetWeb = new Uri(url);
    var targetRealm = WebConfigurationManager.AppSettings.Get("Realm");

    try

    {
        var responseToken = TokenHelper.GetAppOnlyAccessToken(TokenHelper.SharePointPrincipal, targetWeb.Authority, targetRealm).AccessToken;
        var clientContext = TokenHelper.GetClientContextWithAccessToken(targetWeb.ToString(), responseToken);
        return clientContext;
    }
    catch (RequestFailedException rex)
    {
        var clientId = WebConfigurationManager.AppSettings.Get("ClientId");
        var hostedAppHostName = WebConfigurationManager.AppSettings.Get("HostedAppHostName");
        var spHostUrl = WebConfigurationManager.AppSettings.Get("SPHostUrl");

        var message = string.Format(@"Token request failed:

                                1) Verify your App Identifier on {3}/_layouts/15/appPrincipals.aspx it should be '{0}@{1}'.

                                2) Verify your App Domain on {3}/_layouts/15/appInv.aspx is '{2}' for the App Id '{0}'.",

            clientId, targetRealm, hostedAppHostName, spHostUrl);

        throw new RequestFailedException(message, rex);
    }
}

2.4.6Publish the code changes

  1. In Solution Explorer, right-click the web project, then hit Publish
  2. For Select a Publish target, select Custom
  3. Hit Next
  4. Enter a profile name

Example: LowTrustAddIn

  1. For Publish method: select File System
  2. For Target location: this is your visual studio project location

Example: C:\Users\wfawzi\Documents\visual studio 2015\Projects\LowTrustAddIn\LowTrustAddInWeb

  1. Hit Publish

2.5Test the external application

Pre-requisites:

  1. The list which we entered its name in step 2.4.5.3 -> 4 must exist on the site we entered its URL in the same step
  2. The list must have a single-line-of-text column named “Status”
  3. The list must have at least one item

  1. Start the app:

In a browser navigate to the SPAppWebUrl URL of step 2.4.5.1 above.

Example: http://lowtrustaddin/LowTrustAddInWeb/Pages/Default.aspx

  1. You should get a page similar to the following:

 

  1. Enter the ID of the item you would like to update its status
  2. Enter the new status
  3. Hit Update Status

  1. You should get a message that the operation has been completed successfully

  1. Check your list
  2. You should find that the item’s status has been updated

3References

  • Use an Office 365 SharePoint site to authorize provider-hosted add-ins on an on-premises SharePoint site

https://docs.microsoft.com/en-us/sharepoint/dev/sp-add-ins/use-an-office-365-sharepoint-site-to-authorize-provider-hosted-add-ins-on-an-on

  • Context Token OAuth flow for SharePoint Add-ins

https://docs.microsoft.com/en-us/sharepoint/dev/sp-add-ins/context-token-oauth-flow-for-sharepoint-add-ins

  • Add-in authorization policy types in SharePoint

https://docs.microsoft.com/en-us/sharepoint/dev/sp-add-ins/add-in-authorization-policy-types-in-sharepoint

https://threewill.com/making-app-only-sharepoint-calls-without-launched-sharepoint/

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)