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 fromfalse
totrue
. 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 addEncrypt=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:
- Large file upload support
- File storage Interface to either Azure blob storage or local disk
- SMTP support with multiple recipients and attachment support
- Server/UI token based authentication per user device (support ability to invalidate tokens in DB to force user log in)
- Random salted password encryption (one way encryption) - Updated to SHA512
- Application/Connection String support via appsettings.json > secrets.json > Azure Keyvault
- EF context helpers for read/write optimization
- EF connection string from settings
- EF raw SQL query example
- CODE Framework Email logging
- Basic user log in, authentication, and security
- Token tracking by user\browser in UserToken table
- User email verification
- Password recovery
- User management
- Message feature that can serve as application messaging, developer emails, and or email logging
- CF Logger to Message system if configuration UseInternalMessage is true
- Tacking of long running tasks with UI status display
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.
- 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.
- 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"
}
App.Name : Used when sending communications to users. By default the
SendForgotPasswordEmail
andSendConfirmEmailEmail
use this value to indicate where the email can from.App.BaseUrl : Used whenever the application needs to provide a URL for hrefs. By default the
SendForgotPasswordEmail
andSendConfirmEmailEmail
use this value for links back to the application.App.EmailFromDisplay : Used in all email communications as the display name of the sender.
App.EmailFromAddress : Uses in all email communications as the address of the sender.
App.Environment : String used when sending Logger exceptions. Helpful differentiate exception emails that come from development/test/production envrionments.
App.ExceptionDistribution : This is a comma delimited list of emails that will receive Logger exeptions emails. You can set a value in the
appsettings.json
but if the developer does not override in theirsecrets.json
these persons will get emails from all developer exceptions.App.IsUpdating : If this flag is set to
true
the UI will navigate to an “updating” page, then poll the service every 5 seconds until this flag is cleared. Useful in production if you want to prevent users from writing records during an update.App.UseInternalMessage : If this flag is set to
true
emails generated by the application will be sent to the SQLMessage
tables. The UI has a view that pulls in messages. This can be an alternative during development to using SMTP, or can be leveraged as an application messaging system. Note that the Internal Message systems does NOT look at the EmailOverride setting (next section)App.UseInternalMessageAnonymous : If this flag is set to
true
then users can see all emails, not just their own.App.UseSmtp : If set to
true
will send communications via SMTP (must also configure settings in next section). It is possible to use both Internal Messaging and SMTP (both flags settrue
)FileStorage.LocalFolder : This setting is only required if you use the
DocumentStorageFileProvider
. It can be ignored if only using Azure Blob Storage for documents. Path can be relative to application (documents
) or fully qualified (C:\\Temp\\Documents
). Do NOT use FileStore when publishing application to Azure
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"
}
}
App.EmailOverride : If this setting is anything other than an empty string all emails will be sent to this address instead of intended recipient. This is primarily used during development to prevent real users from receiving testing emails. If you are using SMTP it is important to enter a developer email address on all test environments here
App.ExceptionDistribution : Refer to
appsettings.json
descriptionApp.Environment : Allows the individual developer to establish a friendly name for exception emails.
App.UseKeyVault : If this flag is set to
true
it will look at the next setting to load additional settings. At present KeyVault setting have the highest priority and will supersede bothappsettings.json
andsecrets.json
values.App.KeyVault : URL to your KeyVault
AzureStorage.AccountKey : Used by
DocumentStorageAzureProvider
. Value can be found in the projects Azure Portal.AzureStorage.AccountName : Used by
DocumentStorageAzureProvider
. Value can be found in the projects Azure Portal.AzureStorage.Url : Used by
DocumentStorageAzureProvider
. Value can be found in the projects Azure Portal.AzureStorage.Container : Used by
DocumentStorageAzureProvider
. Value can be found in the projects Azure Portal.AzureStorage.ConnectionString : Used by
DocumentStorageAzureProvider
. Value can be found in the projects Azure Portal.Database.ConnectionString : Used by EntityFramework for database connection. You can either use an Azure DB connection string or point to local DB.
EmailServer.Name : If
App.UseSmtp
set true this is used for the email server address (IP Address or Domain Name)EmailServer.Password : If
App.UseSmtp
set true this is used for credentials when logging into the SMTP server.EmailServer.Port : If
App.UseSmtp
set true this is used for the SMTP port (typically 587 for SSL connections)EmailServer.User : If
App.UseSmtp
set true this is used for credentials when logging into the SMTP server.
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:
- 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)
- Adding or removing Service Endpoints
- Add/Remove from CF.Services.Contract> Interface
- Add/Remove from CF.Services.Implementation
- In the CF.UI.Razor > CFStartup.cs file add/remove service endpoints in the AddServiceEndpoints method.
- ORM Scaffolding:
- Open Package Manager Console (or other suitable VS Command window)
- 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