Introduction

NOTE: This is the CF Starter project based on .Net 7.0 Razor application. API and UI are separate, so this project does NOT have a UI. You will need to grab a separate starter UI to consume these services.

NOTE: EF Core 7 (with latest Microsoft.Data.SqlClient) has a breaking change in the connection string. Previously the default of the connection string setting Encrypt default value has changed from false to true. This may make existing connection strings fail with an exception The certificate chain was issued by an authority that is not trusted. In this scenario you can add Encrypt=False; to your existing connection string. See https://learn.microsoft.com/en-us/sql/connect/ado-net/introduction-microsoft-data-sqlclient-namespace?view=sql-server-ver15#breaking-changes-in-40 for more details.

This is a starter project that is designed to kick start new projects. It has the typical UI/Service/ORM/Business tier setup used by EPS Software.

All projects are .Net 7.0 with the exception of the DB project (used to sync SQL Schema). This project is not distributed and will have to remain a .Net Framework 4.8 project

The ORM project uses Entity Framework Core 7.x for the ORM.

The Contract project uses CODE Framework Contracts.

The Implementation project uses CODE Framework Fundamentals.

Project Features:

Getting Started

You may use your favorite clone/rename/git-init or you can use the following PowerShell commands (these can be saved to a .ps1 file on your local hard disk if desired)

#To run from command line you must run the PowerShell as Admin
#and set the execution plan:
#   Set-ExecutionPolicy Unrestricted
#Then you can run the script from the command line:
#   .\cfservices -ProjectFolder "NewFolder" -ProjectPrefix "Code.Sample"
#
#These two parameters are passed from the command line,
#or if you run this directly you can update the values as desired
param(
    [Parameter()]
    #Set the folder, either full path ('C:\TFS\NewProject') 
    #or subdirectory ('./NewProject') relative to script)
    [String]$ProjectFolder = 'C:\TFS\NewFolder',
    #Project Prefix sets the Project File names (i.e. CF.Services.Implementation) and Namespace
    [String]$ProjectPrefix = 'Code.Project'
)

#################################################################################
#Changes to anything below should not be necessary
#################################################################################
[String]$ProjectFolderPath = $ProjectFolder
[String]$GitFolderPath = $ProjectFolder + '/.git'
[String]$PrefixReplace = $ProjectPrefix + '.'
[RegEx]$CFStarterRegex = '(CF.)'
[RegEx]$StartupFixRegex = '(' + $ProjectPrefix + '.tartup)'
git clone https://codemag@dev.azure.com/codemag/CODE.Framework.SPA/_git/Latest.Services $ProjectFolder
Remove-Item -Path $GitFolderPath -Recurse -Force -Confirm:$false
#Update all solution, project, and c# files with new project prefix
"Getting all Solution, Project, and C# files to update. This may take a moment..."
$files = Get-ChildItem -Path $ProjectFolderPath -Recurse -File
$totalFiles = $files.Count
$fileCnt = 0
"Updating file contents..."
foreach ($file in $files) {
    if ($file.name.EndsWith(".sln") -or $file.name.EndsWith(".csproj") -or $file.name.EndsWith(".cs")) {
        $fileContents = (Get-Content $file.fullname)
        $fileContents = $fileContents -Replace $CFStarterRegex, $PrefixReplace
        $fileContents = $fileContents -Replace $StartupFixRegex, "CFStartup"
        $fileContents = $fileContents -Replace "<UserSecretsId>03c9ac8d-2633-46bb-879c-6e6a32913f50</UserSecretsId>", ("<UserSecretsId>" + [guid]::NewGuid() + "</UserSecretsId>")
        Set-Content $file.fullname -Value $fileContents
    }
    Write-Progress -Activity "Updating Project Prefix from 'CF.' to '$ProjectPrefix.'->" -Status "File:$fileCnt of $totalFiles" -PercentComplete ([int32](($fileCnt / $totalFiles) * 100))
    $fileCnt++
}
"Updating file contents complete..."
#Rename the folders
"Finding folders..."
$dirs = Get-ChildItem -Path $ProjectFolderPath -rec |  Where-Object { $_.PSIsContainer -eq 1 } |  sort fullname -descending
$Qty = $dirs.Count
$Cnt = 0
"Renaming folders..."
foreach ( $dir in $dirs ) { 
    if ($dir.name.Contains("CF.")) {
        rename-item -path $dir.fullname -newname $dir.name.Replace("CF.", $PrefixReplace)
    }
    Write-Progress -Activity "Renaming Folders from 'CF.' to '$ProjectPrefix.'->" -Status "File:$fileCnt of $Qty" -PercentComplete ([int32](($Cnt / $Qty) * 100))
    $Cnt++
} 
"Renaming folders completed..."
#Rename files
"Finding files..."
$files = Get-ChildItem -Recurse $ProjectFolderPath | Where-Object { ! $_.PSIsContainer } | Select-Object Name, FullName, Length
$Qty = $files.Count
$Cnt = 0
"Renaming files..."
foreach ( $file in $files ) {
    if ($file.name.Contains("CF.")) {
        rename-item -path $file.fullname -newname $file.name.Replace("CF.", $PrefixReplace) 
    }
    Write-Progress -Activity "Updating Filenames from 'CF.' to '$ProjectPrefix.'->" -Status "File:$fileCnt of $Qty" -PercentComplete ([int32](($Cnt / $Qty) * 100))
    $Cnt++
} 
"Renaming files completed..."
"."
"."
"You may now open the project located at $ProjectFolderPath"

SQL Database Setup

To get the sample features running you will need to have a SQL database running. Look at the CF.Data.DB project which is a SQL Database project.

  1. You can use it to create the necessary DB/Schema https://docs.microsoft.com/en-us/sql/ssdt/how-to-use-schema-compare-to-compare-different-database-definitions?view=sql-server-ver15 OR you can run the Create DB script in the \Scripts folder.
  2. You'll need to create at least one Role in the Role table OR run the Seed Records script in the \scripts folder.

appsettings.json

The appsettings.json file contains common settings that are not likely to be changed on a per developer basis. This file is checked into source control and should NOT contain any sensitive data. Note that values listed here are superseded by any secrets.json or Azure Keyvault settings of the same name.

  "App": {
    "Name": "CF Starter App",
    "BaseUrl": "https://localhost:7264/",
    "EmailFromDisplay": "CF Starter",
    "EmailFromAddress": "noreply@codemag.com",
    "Environment": "local",
    "ExceptionDistribution": "",
    "IsUpdating": false,
    "UseInternalMessage": true,
    "UseInternalMessageAnonymous": true,
    "UseSmtp": false
  },
  "FileStorage": {
    "LocalFolder": "Uploads"
  }

Secrets.json

The secrets.json file is maintained by Visual Studio and is NOT checked into source control. Each developer can access the projects secrets by right clicking on the CF.UI.Razor project and selecting the Manage User Secrets menu option. Note that Visual Studio maintains this file based on the projects <UserSecretsId> property group in the csproj file. If you used the PowerShell script to clone the repository it should have been updated. If you used a different cloning process you should open the project file and update the <UserSecretsId> (GUID) to ensure that projects don't share secrets

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

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UserSecretsId>--project GUID here--</UserSecretsId>
  </PropertyGroup>
  ...

Settings configured here supersede any settings of the same name in the appsettings.json file

{
  "App": {
    "EmailOverride": "--your address--@codemag.com",
    "ExceptionDistribution": "--your address--@codemag.com",
    "Environment": "--friendly name--",
    "UseKeyVault": false,
    "KeyVault": "https://--keyvault name--.vault.azure.net"
  },
  "AzureStorage": {
    "AccountKey": "--get from Azure Portal--",
    "AccountName": "--get from Azure Portal--",
    "Url": "--get from Azure Portal--",
    "Container": "--get from Azure Portal--",
    "ConnectionString": "DefaultEndpointsProtocol=https;AccountName=--get from Azure Portal--;AccountKey=--get from Azure Portal--;EndpointSuffix=core.windows.net"
  },
  "Database": {
    "ConnectionString": "Server=--server name--.database.windows.net;Database=--datbase name--;user id=--User Name--;password=--User Password--;",
  },
  "EmailServer": {
    "Name": "smtp.sendgrid.net OR SMTP domain name",
    "Password": "--send grid API Key OR SMTP password--",
    "Port": "587",
    "User": "apikey OR SMTP user name"
  }
}

Azure Application settings

It is necessary to establish all of the above settings when publishing to Azure. After creating the Web Application in the Azure Portal you can navigate to the Deployment/Configuration menu. Here you will find the Application Settings tab that allows you to define settings. You can enter each one at a time, or click the Advanced edit and use the template below.

It is strongly recommended that all settings with sensitive information should be store in Azure KeyVault!!

[
  {
    "name": "App:BaseUrl",
    "value": "https://--xx--.azurewebsites.net/",
    "slotSetting": false
  },
  {
    "name": "App:EmailFromAddress",
    "value": "noreply@--xx--",
    "slotSetting": false
  },
  {
    "name": "App:EmailFromDisplay",
    "value": "--xx--",
    "slotSetting": false
  },
  {
    "name": "App:EmailOverride",
    "value": "",
    "slotSetting": false
  },
  {
    "name": "App:Environment",
    "value": "--xx--",
    "slotSetting": false
  },
  {
    "name": "App:ExceptionDistribution",
    "value": "--xx--@codemag.com",
    "slotSetting": false
  },
  {
    "name": "App:IsUpdating",
    "value": "false",
    "slotSetting": false
  },
  {
    "name": "App:Name",
    "value": "--xx--",
    "slotSetting": false
  },
  {
    "name": "App:UseInternalMessage",
    "value": "true",
    "slotSetting": true
  },
  {
    "name": "App:UseInternalMessageAnonymous",
    "value": "false",
    "slotSetting": false
  },
  {
    "name": "App:UseKeyVault",
    "value": "false",
    "slotSetting": false
  },
  {
    "name": "App:UseSmtp",
    "value": "true",
    "slotSetting": false
  },
  {
    "name": "AzureStorage:AccountKey",
    "value": "--xx--",
    "slotSetting": false
  },
  {
    "name": "AzureStorage:AccountName",
    "value": "--xx--",
    "slotSetting": false
  },
  {
    "name": "AzureStorage:ConnectionString",
    "value": "DefaultEndpointsProtocol=https;AccountName=--xx--;AccountKey=--xx--;EndpointSuffix=core.windows.net",
    "slotSetting": false
  },
  {
    "name": "AzureStorage:Container",
    "value": "documents",
    "slotSetting": false
  },
  {
    "name": "AzureStorage:Url",
    "value": "https://--xx--.blob.core.windows.net/",
    "slotSetting": false
  },
  {
    "name": "Database:ConnectionString",
    "value": "Server=--xx--.database.windows.net;Database=--xx--;user id=--xx--;password=--xx--;",
    "slotSetting": false
  },
  {
    "name": "EmailServer:Name",
    "value": "smtp.sendgrid.net",
    "slotSetting": false
  },
  {
    "name": "EmailServer:Password",
    "value": "--xx--",
    "slotSetting": false
  },
  {
    "name": "EmailServer:Port",
    "value": "587",
    "slotSetting": false
  },
  {
    "name": "EmailServer:User",
    "value": "apikey",
    "slotSetting": false
  }
]

Azure KeyVault settings

Before using an Azure KeyVault it is necessary for you to set your Visual Studio Azure Service Authentication to an account that has privileges on your KeyVault.

To do so, in Visual Studio select Tools > Options menu, then expand Azure Service Authentication > Account Selection. In the Choose an account select list pick the account that has privileges on the KeyVault. Note that if you have an older Work account (@codemag.com) that also had a “Live” account with the same (@codemag.com) name it will likely NOT work. It will be necessary to create a Microsoft Account to get KeyVault to work locally.

You must configure an Azure KeyVault for sensitive settings. At minimum it should contain:

AzureStorage--AccountKey
AzureStorage--ConnectionString
Database--ConnectionString
EmailServer--User
EmailServer--Password

Configuration

There are several places you need to update with your project:

  1. Right click on Web.Api project and select “Manage Secrets” (these should also be in Azure configuration and or Azure Key Vault). You can use the template above to scaffold out the initial properties

    Please create your own SendGrid API key (https://sendgrid.com/ ) There is a free level that can be used for development, but the client will likely need to purchase a plan for production use (or they can provide their own SMTP settings). If using Sendgrid make sure you authenticate your “EmailFromAddress” domain name (https://app.sendgrid.com/settings/sender_auth)

  2. Adding or removing Service Endpoints
    1. Add/Remove from CF.Services.Contract> Interface
    2. Add/Remove from CF.Services.Implementation
    3. In the CF.UI.Razor > CFStartup.cs file add/remove service endpoints in the AddServiceEndpoints method.
  3. ORM Scaffolding:
    1. Open Package Manager Console (or other suitable VS Command window)
    2. Enter the following, the ‘ConnectionString’ will refer to your configuration for actual conneciton settings. (NOTE: the user credentials in the connection string MUST have database creation privileges!!)

      Scaffold-DbContext "Name=Database:ConnectionString" Microsoft.EntityFrameworkCore.SqlServer -Project "<<Your ORM Project, i.e. XYZ.Data.ORM>>" -OutputDir "Models" -Context "DataContext" -ContextDir "Context" -Force