This is a starter template for your projects' README.md file, which you should rename to
README.md
(replacing the one from this template)
You will need the following development tools to build, run, and test this project:
-
Windows or macOS.
-
JetBrains Rider (recommended) or Visual Studio
- Note: Using JetBrains Rider is recommended since the codebase includes more comprehensive customized tooling (coding standards, live templates, etc.)
- Note: If using Visual Studio, you will need to install the additional component
.NET Compiler Platform SDK
in order to run the built-in Roslyn source generators. - Note: if using Visual Studio, the built-in Roslyn analyzers will not work (due to .netstandard2.0 restrictions between Visual Studio and Roslyn)
-
Install the .NET8.0 SDK (specifically version 8.0.6). Available for Windows Download
-
Install NodeJs (18.20.4 LTS or later), available for Download
We have ensured that you won't need any other infrastructure running on your local machine (i.e., a Microsoft SQLServer database) unless you want to run infrastructure-specific integration tests.
- Build the solution in Rider
- OR
dotnet build src\SaaStack.sln
in the terminal
In Rider, 'Search Everywhere' for the action change memory settings
, and set 8000M
of memory, then restart Rider.
Open a terminal and run: dotnet dev-certs https --trust
In Rider, right-click on the solution node in the Explorer, and choose Add -> New Project.
In the bottom left corner, click the link "Manage Templates"
Click "Install Template..." and select the directory: C:\...\SaaStack\src\Tools.Templates\AnyProject
Repeat for these directories:
C:\...\SaaStack\src\Tools.Templates\HostProject
C:\...\SaaStack\src\Tools.Templates\InfrastructureProject
C:\...\SaaStack\src\Tools.Templates\IntegrationTestProject
C:\...\SaaStack\src\Tools.Templates\UnitTestProject
You only need the tools below installed if you are going to run specific Integration.Persistence
tests, for the persistence technology adapters you need to use in your codebase.
- If using the
AzureSqlServerStore
, installSQL Server 2019 Developer
. Available for download here - If using the
RedisDataStore
, installRedis Server
locally to run the tests. Available for download here - If using the
EventStoreEventStore
, installEventStore
locally to run the tests. Install using the Chocolatey command:choco install eventstore-oss
We would normally run these storage integration tests in CI periodically.
Only if you are deploying your product to Azure. (Delete this section otherwise)
For security, and to ensure the Azure Functions can run when running locally, you need to create your own version of local.settings.json
.
In the AzureFunctions.Api.WorkersHost
project:
- Create a new file called
local.settings.json
- Copy the following JSON:
{ "IsEncrypted": false, "Values": { "DebugMode": true, "AzureWebJobsStorage": "UseDevelopmentStorage=true", "AzureWebJobsServiceBus": "", "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", "APPINSIGHTS_INSTRUMENTATIONKEY": "" } }
- Create a new file called
appsettings.local.json
- Leave the content blank for now
DO NOT add these two files to source control!
You only need to perform this step once, prior to running any of the
Integration.Persistence
tests against Azure infrastructure (e.g., Azure Queue Storage, Azure Blob Storage, and Azure Table Storage)
In a Terminal window:
-
Navigate up to the
tools
directorycd ..
cd /tools/azurite
-
Run:
npm install
Only if you are deploying your product to AWS (Delete this section otherwise)
You only need to perform this step once, prior to running any of the
Integration.Persistence
tests against AWS infrastructure (e.g., CloudWatch, SQS queues, S3 Buckets, RDS/Dynamo databases, etc)
In a Terminal window:
- Install python
- Install Python package manager
- Install docker
- Install localstack:
python -m pip install localstack
Now, test that LocalStack works by running: localstack start
When testing, Docker will need to be running for LocalStack to be used
You only need to perform this step once, prior to running any of the
Integration.External
tests against 3rd party adapters (e.g., Flagsmith, Twillio, etc)
In the Infrastructure.Shared.IntegrationTests
project, create a new file called appsettings.Testing.local.json
and fill out the empty placeholders you see in appsettings.TestingOnly.json
with values from service accounts that you have created for testing those 3rd party services.
DO NOT add this file to source control!
cd src\WebsiteHost\ClientApp
npm install
npm run build
Note: As a result of this build step you should see new bundle file (e.g.
0123456789abcdef.bundle.js
) appear in thewwwroot
folder. This file should never be added to source control.
You need to create your own version of the .env
file on your computer (not source controlled).
- Copy the
src/WebsiteHost/ClientApp/.env.example
tosrc/WebsiteHost/ClientApp/.env
.
DO NOT add this file
.env
to source control! This files exists locally for security purposes, and in order to have the right environment variables in place when running and testing the JS App.
When pushed, all branches will be built and tested with GitHub actions
-
Ensure that the solution contains
0
warnings and0
compile errors.Note: Warnings are generated by the IDE, plugins and by Roslyn code analysis rules that run against the solution.
-
Run these tests:
In Rider, run all C# tests with Category=
Unit
,Unit.Tooling
,Unit.Architecture
,Integration.API
andIntegration.Website
Note: Use
Group By > Category
in Rider's unit test explorer to view these three categories easily.OR, in a terminal:
-
dotnet test --filter:"Category=Unit|Category=Unit.Tooling|Category=Unit.Architecture" src\SaaStack.sln
-
dotnet test --filter:"Category=Integration.API|Category=Integration.Website" src\SaaStack.sln
-
-
Configure your "Commit " window to select the "Cleanup with 'SaaStack Full' profile".
This solution contains comprehensive code formatting, and error-checking settings in the team-shared settings file src\SaaStack.sln.dotSettings
that contains numerous code style rules, 'Live Templates', 'File templates', and other important settings that we need to share across the team for consistency.
When changing any settings in Rider, make sure you save them for the whole team (team-shared), then add the SaaStack.sln.dotSettings
file to your next commit to save those rules for the rest of your team.
We use various stubs/fakes (whichever definition you like) in concert with configuration settings (appsettings.json
) and with #if TESTINGONLY
sections to create different environments for testing and for production.
There are 3 environments you need to be aware of and how they differ in their dependencies and configuration:
- Local (manual) testing (aka F5 debugging) -
Debug
orRelease
- Automated integration testing -
Debug
orRelease
- Production (and/or Staging etc) -
ReleaseForDeploy
Note: In all cases, in all environments, there should NEVER be any production settings nor secrets in any configuration file (i.e.
appsettings.json
) anywhere in this codebase! These production settings and secrets should only be defined in the CD pipeline, and replaced when a production build is packaged and deployed.
In production builds, we build and deploy the code in the ReleaseForDeploy
build configuration.
Note: In this build configuration, certain testing stubs, certain testing endpoints, and certain hardcoded testing values and functions are compiled out of the code (e.g. behind the
#if TESTINGONLY
conditional compilation variable).We absolutely need to do this because these specific testing code pieces should never exist in the production codebase and may expose security vulnerabilities and exposures we simply don't want in production environments.
The various 3rd party adapters we need in production (e.g., SendGridHttpServiceClient
and the AzureSqlServerStore
)
will be configured in the DI containers (of Program.cs
of the ApiHost1
project, and in the modules of each subdomain) to use code to talk to real 3rd party APIs and will be configured with specific production settings in the appsettings.json
file (overwritten by your CD server).
These are the real 3rd party public API adapters, which, if used with production settings, in local CI environments, or in automated testing environments, may incur financial service charges, trigger rate-limiting quotas, and/or pollute or corrupt real customer data!
Note: This should never happen by accident, but read and follow the next 2 sections to avoid the possibility of this happening. We have designed several safeguards in place that should make this impossible (albeit without working around it intentionally).
You will notice that in the production build (ReleaseForDeploy
), we have configured the code:
- By injecting the
AzureSqlServerStore
as the primaryIDataStore
. - By injecting various other dependencies according to the current value of the
$(HostingPlatform)
MS build property (e.g.,HOSTEDONAZURE
).
Note: that many of the other technology adapters (e.g.,
SendGridHttpServiceClient
) will not need to be explicitly configured in the DI container (for specific build flavors), that is because these adapters can be configured to point to local stubs instead of pointing to production environments.
On the CI server, integration testing is run in the
Release
build configuration, which permits the inclusion ofTESTINGONLY
code in the compilation necessary for integration testing. Integration testing on your local machine should be done in theDebug
build configuration. The only difference betweenDebug
andRelease
in practice is that there are some compiler optimizations configured inRelease
, which are closer to code in Production.
In automated integration testing (executed on both your local machine and on the CI build server), we run the APIs you are testing in their original production DI configuration defined in the Program.cs
file (in the respective ApiHost
project of the API code you are testing).
That DI configuration is modified slightly to swap out the 3rd party adapters for that ApiHost
so that we can program the 3rd party adapters to behave the way the tests need them to (and to query them for certain interactions).
This is what we modify and how:
-
We run this API production DI code in-process in a Kestrel web server in the same process as your Integration Tests (e.g., in the process of
xunit.console.exe
, not as a separate process). -
We replace the
appsettings.json
file with the one in your integration testing projectWhich should never ever contain ANY production settings or secrets!
-
We then manipulate the DI container (seen in the constructor of your integration testing project) and replace certain dependencies (e.g., the 3rd party adapters like:
IUserNotificationsService,
etc) with their respective hardcoded Stubbed equivalents (found in theStubs
directory of your integration testing project).
Note: These integration testing stubs will likely use no configuration settings from
appsettings.json
as their responses and behavior are hardcoded/canned in the hardcoded classes of the integration test project.
You may wish to modify these stubs to add the ability to query them to ensure they are called in the right ways in testing.
Note: When you are manual testing (like using F5 debugging), make sure that you compile the code and run it in
Debug
(orRelease
) build configuration that will include all code marked up by#if TESTINGONLY
compilation variable.
When you run any of the ApiHost
projects in this solution in your local environment, you are starting that ApiHost
project at a specific IP address, which starts a separate Kestrel server in external processes of its own (e.g., ApiHost.exe
for the API).
In local testing, all external services (i.e., SendGrid, Unleash, etc.) should be directed (via config) to point to the local Stubs TestingStubApiHost,
which will respond with specific fixed responses to any calls to these external services.
The goal of this testing strategy is to make it possible to run locally without connecting to any real live services over the internet.
Note: When you are doing manual testing on your local machine, either through the browser with PostMan, or with any other tools, you are actually running the code in production configuration as far as the adapters that DI injects into your code. However, most of these adapters will be using the configuration found in
appsettings.json
of theApiHost
project.
There will be numerous statements in the code using #if TESTINGONLY
to determine which concrete dependencies are actually used in the Debug
(or Release
) configuration.
You will notice that in local debugging, we have switched out the currently configured IDataStore
for the LocalMachineFileDataStore
, so that you can do all your local debugging without a SQL database being available on your local machine.
The LocalMachineFileDataStore
is configured to place your files in Environment.SpecialFolder.LocalApplicationData
, which resolves to these folders:
- On Windows:
%appdata%
- On macOS:
/home/<you>/.local/share
-
Build the code:
- Rebuild the solution
- OR
dotnet build src\SaaStack.sln
in the terminal
-
Run the backend:
- In Rider, run the
AllHosts
compound configuration (runs theApiHost1
server and theTestingStubApiHost
)
- In Rider, run the
-
Access the API on
https://localhost:5001
Sometimes (especially on MacOS), after manually testing, the processes do not shut down properly, leaving ports: 5001
and 5656
occupied. This then throws an exception when you try to run again later.
The message looks something like this:
System.IO.IOException: Failed to bind to address https://127.0.0.1:5656: address already in use.
To kill these processes:
- On Windows,
taskkill /f /im dotnet.exe
- On MacOS:
-
Find the processes:
lsof -Pni | grep "5001\|5101\|5656"
-
Kill the processes:
kill -9 <processid>
where<processid>
is the ID of the process in the list -
Alternatively, in MacOS:
- Use
lsof -ti :[PORT]
and locate the PID of the process, e.g.,lsof -ti :5656
. - Open "Activity Monitor", locate the process with that PID, and stop that process.
- Use
-
Run all C# tests with Category= Unit
, Unit.Architecture
, Integration.API
and Integration.Website
OR, in a terminal:
-
dotnet test --filter:"Category=Unit|Category=Unit.Tooling|Category=Unit.Architecture" src\SaaStack.sln
-
dotnet test --filter:"Category=Integration.API|Category=Integration.Website" src\SaaStack.sln
Note: All tests will be run in parallel in
Rider
or indotnet test
.
These tests ensure that 3rd party persistence technology adapters that are used in production environments work correctly.
Only run these kinds of tests when the code in the persistence technology adapters changes.
These tests should NOT be run frequently and can be scheduled to run as part of a nightly/weekly build.
Warning: These tests connect to and test real 3rd party systems in the cloud (usually across HTTP or some other protocol). Some of these tests require that you have the respective technology installed on your local machine (e.g., SQL Server Database).
Warning: They may incur charges, or they may trigger rate-limiting policies on the accounts they are run against.
dotnet test --filter:"Category=Integration.Persistence" src\SaaStack.sln
(requires installing the server persistence components listed at the top of this page)
Note: If any of the
Integration.Persistence
category of tests fail, it is likely due to the fact that you don't have that technology installed on your local machine, or that you are not running your IDE as Administrator, and therefore cannot start/stop those local services without elevated permissions.
Note: AWS infrastructure adapters require LocalStack to be running (in Docker) on your computer in order to work. (Run
localstack start
).
These tests ensure that 3rd party technology adapters that are used in production environments work correctly.
Only run these kinds of tests when the code in the technology adapters changes. These tests should not be run frequently and can be scheduled to run as part of a nightly/weekly build.
Warning: These tests connect to and test real 3rd party systems in the cloud (usually across HTTP).
Warning: They may incur charges, or they may trigger rate-limiting policies on the accounts they are run against.
dotnet test --filter:"Category=Integration.External" src\SaaStack.sln
(requires internet access to external services)
Note: We use the 2 dot Semantic Versioning scheme.
The latest changes for this new version are recorded in CHANGELOG.md and they follow a keep a changelog convention.
All assemblies and all hosts will share the same version number.
We will be using a tool called release-it to update the version and changelog when creating new releases.
First, make sure that all changes are documented in the various sections of the [Unreleased]
section of the CHANGELOG.md
- Copy the new version number to
src/GlobalAssemblyInfo.cs
For example:
[assembly: AssemblyVersion("2.0.0.0")]
[assembly: AssemblyFileVersion("2.0.0.0")]
[assembly: AssemblyInformationalVersion("2.0.0")]
- Commit, tag, and push the new version changes.
Note: Each build in CI will automatically append the last build number to the SemVer and update the version in
GlobalAssemblyInfo.cs