Udemy course Dating App built with ASPNET Core and Angular infrastructure.
Full course solution designed by the instructor available here:
https://github.com/TryCatchLearn/DatingApp
Download and install .NET:
https://dotnet.microsoft.com/en-us/download
If you are using Mac Apple Silicon Chip, install .NET 6.0 for Arm64.
Download and install Node JS:
https://nodejs.org/en/
Edit: You can use version 18.15.0 LTS For this Udemy course you need to use Node
version 16, which is available for download here:
https://nodejs.org/download/release/v16.10.0/
https://www.nuget.org/packages/dotnet-ef/
Run:
dotnet tool install --global dotnet-ef --version 7.0.4
Configure your Git Access. I recommend using Git Credential Manager Core: https://github.com/git-ecosystem/git-credential-manager/blob/release/docs/install.md
Run:
git credential-manager configure
git config --show-origin --get credential.helper
You can erase existing OSX Keychain credentials with:
git config --unset credential.helper
git credential-osxkeychain erase
[Press Return]
If you're running into issues, make sure you aren't using a repo that was originally configured for SSH, and you are now attempting to use the same repo with Git Credential Manager - this will be significantly more complex to configure.
Matches |
---|
Edit Profile |
---|
Online Notifications |
---|
User Message Thread |
---|
In one VS Code Terminal, cd API/
then run:
dotnet watch --no-hot-reload
And in another Terminal instance, cd client
then run:
kill -9 $(lsof -t -i:4200);ng serve
ng serve
Now both services (API server and client app) will be running. Open this URL in a browser:
https://localhost:4200/
If you encounter this error:
System.IO.IOException: Failed to bind to address https://127.0.0.1:5001: address already in use.
You can try running:
lsof -i:5001
Example output:
Google 36572 john 70u IPv6 0x1223c1a0ae0e2ced 0t0 TCP localhost:60565->localhost:commplex-link (ESTABLISHED)
API 96825 john 244u IPv4 0x1223c1aa43f9eee5 0t0 TCP localhost:commplex-link (LISTEN)
API 96825 john 245u IPv6 0x1223c1a0ae0e3bed 0t0 TCP localhost:commplex-link (LISTEN)
API 96825 john 256u IPv6 0x1223c1a0ae0c436d 0t0 TCP localhost:commplex-link->localhost:60565 (ESTABLISHED)
Then run kill -9 <pid>
to manually kill the leftover "API" processes. In the above example, it
should be: kill -9 96825
.
Open Settings, search for "exclude", under "Files: Exclude", click on Add Pattern. Type
**/bin
and click OK. And do the same for **/obj
. This will hide these folders from the
Solution Explorer, since we won't interact with them very often.
Within Settings, search for "bracket" and make sure these two settings are Enabled:
- Auto Closing Brackets - Always
- Bracket Pair Colorization: Enabled - Checked
- Bracket Pair Colorization: Independent Color Pool Per Bracket Type - Unchecked
- Guides: Bracket Pairs - True
Open the Command Palette with: SHIFT + CMD + P
(MacOS).
Open the editor's More Actions... contextual menu with CMD + .
(MacOS); this will provide
you with helpful quick actions like "Remove unnecessary usings", or "Generate constructor".
Open the Keyboard Shortcuts window under Settings, then click on the small icon in the
top-right corner with tooltip "Open Keyboard Shortcuts (JSON)" (the icon looks like a piece
of paper with a folded corner, and a circular arrow on the left). In the keybindings.json
file, add this entry:
{
"key": "shift shift",
"command": "workbench.action.quickOpen"
}
Save the keybindings.json
file and then close it. Now, when you double-tap SHIFT, it will open
up a sort of "Global Object Search" form field, and you can type the name of an entity, like
our AppUser.cs
, and then press RETURN to open the file. Super-handy to have!
More details: https://stackoverflow.com/questions/29613191/intellij-shift-shift-shortcut-in-visual-studio-global-search
Under Settings > CodeLens, turn off "Show Main Code Lens". It adds extraneous noise to every method signature in the editor UI, with a bunch of "N references" indicators everywhere.
List currently installed tools:
dotnet tool list -g
A Walking Skeleton is a tiny implementation of the system that performs a small end-to-end function. It need not use the final architecture, but it should link together the main architectural components.
The architecture and the functionality can then evolve in parallel.
- Alistair Cockburn
LEARNING GOALS Implement the basic API functionality and have an introductory understanding of:
- Using the dotnet CLI
- API Controllers and Endpoints
- Entity Framework
- The API Project structure
- Configuration and Environment variables
- Source control
Run:
dotnet --info
dotnet -h
Create new solution file and project file:
dotnet new sln
dotnet new webapi -n API
ls
dotnet sln -h
dotnet sln add API/
dotnet sln list
Open Visual Studio Code, do "Show All Commands" (SHIFT + CMD + P), type "PATH", select option: Shell command: Install 'code' command in PATH.
If you run into a "Permission Denied" error, try uninstalling it first.
Open project by running this in the Terminal:
code .
Go to the Extensions tab (looks like 4 Tetris blocks), and search for "C#".
Install the C# Extension verified by Microsoft (powered by OmniSharp); you don't need to install any "Recommended Extensions" right now.
It's recommended to turn on Auto Save functionality, under File menu (click on "Auto Save").
With the same C# Extension still visible, Right click on the Extension and click on "Extension Settings". It might say "No Settings Found" -- no worries. Open the Command Palette (SHIFT + CMD + P), and search for "Reload" and click on the "Reload Window" option.
Under the Extension Settings, scroll down (while on the "User" tab) and find these settings:
- Omnisharp: Enable Async Completion (EXPERIMENTAL)
- Omnisharp: Enable Import Completion
- Omnisharp: Organize Imports On Format
Click the checkboxes to enable all of these settings.
Look for "Private Member Prefix" and input _
(underscore) and turn off the
"Use This For Ctor Assignments" checkbox.
You should get a Visual Studio Code notification prompting you to Restart OmniSharp, go ahead and accept that to restart the Extension.
You can "Toggle Terminal" with CTRL + Backtick (aka, Grave Accent; the key to the left of the "1" on your keyboard numbers row).
Back in the Extensions area, search for C# Extensions, and install the Extension maintained by JosKreativ. And, install Material Icon Theme by Philipp Kief (after) this is installed, you need to click on "Material Icon Theme" in the Command Palette area (it should automagically show up). You might need to click on Set File Icon Theme, depending on your operating environment.
Optionally: Under Preferences > Settings, search for "compact", and turn off "Explorer: Compact Folders".
Also install GitLens - Git supercharged by GitKraken if you want Git line annotations.
Open the Command Palette (SHIFT + CMD + P) and type ".net", and click on:
".NET: Generate Assets for Build and Debug". This will create the .vscode
directory
and the configuration files: launch.json
and tasks.json
.
Toggle the Terminal (CTRL + Backtick), change directory to API/
and then run:
dotnet run
On MacOS, you will probably be prompted with a Keychain request and will require your password. Here's the output from the Terminal for reference:
Building...
warn: Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServer[5]
The application is trying to access the ASP.NET Core developer certificate key.
A prompt might appear to ask for permission to access the key. When that happens,
select 'Always Allow' to grant 'dotnet' access to the certificate key in the future.
If you try to open the URL specified in your Properties/launchSettings.json
file, like
https://localhost:7183/ -- you're not gonna see anything interesting right now, since our
"API" controller doesn't have any associated UI and it's just listening for API endpoints;
you'll see something like this in your Browser:
This localhost page can’t be found
No webpage was found for the web address: https://localhost:7183/
HTTP ERROR 404
Examine the WeatherForecastController.cs
(after modifications to the codebase, this file
will no longer exist), and look at the [Route]
property -- the String, "Controller"
gets
removed from the class name, to build the URL for the endpoint.
So the active URL for the project at this point in time is actually:
https://localhost:5001/WeatherForecast
When you view this URL, the browser should show the JSON contents of an
IEnumerable<WeatherForecast>
like this:
[{
"date": "2023-03-16T17:54:14.96377-04:00",
"temperatureC": 12,
"temperatureF": 53,
"summary": "Cool"
}, {
"date": "2023-03-20T17:54:14.96394-04:00",
"temperatureC": 39,
"temperatureF": 102,
"summary": "Chilly"
}]
Let's shut it down (CTRL + C) and then inspect the Help for dotnet: dotnet run -h
.
Based on the help docs, we can specify which launch profile we want to use, with a command
like: dotnet run -lp "API"
. Note: You may run into a developer certificate issue, which
will be described in the Terminal; it should provide some suggested remediation steps,
such as running dotnet dev-certs https --clean
, which may require elevated privileges in
the Windows operating environment.
In the launchSettings.json
file, let's change the localhost ports within the "applicationUrl"
element to 5001 and 5000, something like this:
"applicationUrl": "https://localhost:5001;http://localhost:5000"
Swagger won't be used for this project, so you can remove the "launchUrl": "swagger"
entry.
If you want to see a demonstration of it, you can add this property back, for reference:
"profiles": {
"API": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
And then navigate to: https://localhost:5001/swagger/index.html to see the automagically generated API documentation for the project. We're gonna be using a different tool to test our application.
Inside our API.csproj
file, it lists packages that we have installed. Let's get rid of
this <ItemGroup>
element and its child entries, to simplify things:
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
</ItemGroup>
After installing or uninstalling packages, you need to then run: dotnet restore
. This
will result in some Project Errors we will fix now. Open appsettings.Development.json
and
let's bump the AspNetCore level to "Information"
:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Information"
}
}
}
Open the Program.cs
file, and clean up the services container to look like this:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
var app = builder.Build();
// Configure the HTTP request pipeline.
app.MapControllers();
app.Run();
Now let's run dotnet watch run
. You should see a message like: Hot reload enabled
. This feature
to "Hot Reload" and re-deploy changes as you make them in the Editor can be finicky (sometimes it
causes more problems than it solves).
After successfully running dotnet watch run
or dotnet run
, navigate to:
https://localhost:5001/api/users
You should see this output in the Browser UI:
[{
"id": 1,
"userName": "Bob"
}, {
"id": 2,
"userName": "Tom"
}, {
"id": 3,
"userName": "Jane"
}]
For API testing, it's more efficient to use Postman. Go ahead and create a new Postman Workspace named "UdemyDatingApp", and add a Collection called "Users", and add a Request called "Get Users".
Set the request URL to: https://localhost:5001/api/users
If you get an error like "Could not get response -- SSL Error: Unable to verify the first certificate", go to Settings (Preferences) and turn off "SSL Certificate Verification".
What is it? An Object Relational Mapper (ORM), which translates our code into SQL commands that update our tables in the database. Prior to .NET 3.5, we often used to write ADO.NET code to save or retrieve data from underlying database. It was a cumbersome and error-prone process.
Entity Framework automates a lot of these database-related activities for our application.
When we introduce our Entity Framework, we need to create an important class that derives from
the DbContext
class. This acts as a bridge between our domain and the database. This will
enable us to use LINQ (Link) queries.
Entity Framework works with database providers. The one we're going to use purely for development is SQLite, which does require a database server -- it's not production-worthy, but it is lightweight and portable, suitable for development (and something like SQL Server is not cross-platform).
Entity framework enables querying the database with LINQ, and it supports Change Tracking of entities, and allows us to Save our database. It also gives us optimistic Concurrency to protect overwriting changes from another user, and database Transactions. And it supports Caching and built-in Conventions (which govern how the model will be mapped to a database schema), and Configurations for entities. It also offers us Migrations, to make our database management more robust.
Under Extensions, search for and install "NuGet Gallery" by pcislo.
Then, open the Command Palette (SHIFT + CMD + P), and search for "NuGet" and click on the option that reads "NuGet: Open NuGet Gallery".
Look for "Microsoft.EntityFrameworkCore" by Microsoft, and scroll down to the .Sqlite.Core
package. Select the version of .NET you are using, you can run dotnet --version
in the
Terminal, and install the SQLite Core entity framework in the API.csproj
file.
If you look at the API.csproj
file, we should see a new entry like this:
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="7.0.4" />
</ItemGroup>
Back in the NuGet Gallery, look for "Microsoft.EntityFrameworkCore.Design" by Microsoft, and install that also (using the same dotnet version). Then, the CS Project file should look like this:
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="7.0.4" />
</ItemGroup>
We're also going to need "Microsoft.AspNetCore.Identity.EntityFrameworkCore" by Microsoft.
Install version 7.0.0
.
We installed the wrong SQLite framework -- we wanted simply Sqlite
instead of Sqlite.Core
... 😩
Delete the <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="7.0.4" />
package reference, then go back to NuGet Gallery and install:
Microsoft.EntityFrameworkCore.Sqlite by Microsoft
Double-check the API.csproj
file to confirm we are ready to rock-and-roll. 🤟
Open the Command Palette (SHIFT + CMD + P), and search for "NuGet" and click on the option that reads "NuGet: Open NuGet Gallery".
Look for "system.identity", and install "System.IdentityModel.Tokens.Jwt" by Microsoft.
I selected version 6.24.0
.
Look for "microsoft.aspnetcore.authentication", and install
"Microsoft.AspNetCore.Authentication.JwtBearer" by Microsoft.
I selected version 6.0.10
.
Open the Command Palette (SHIFT + CMD + P), and search for "NuGet" and click on the option that reads "NuGet: Open NuGet Gallery".
Look for "automapper", and install "AutoMapper.Extensions.Microsoft.DependencyInjection"
by Jimmy Bogard. I selected version 12.0.0
.
Under Extensions, search for and install "Angular Language Service" by Angular.
This extension provides a rich editing experience for Angular templates, both inline and external templates including:
- Completions lists
- AOT Diagnostic messages
- Quick info
- Go to definition
From the docs:
A DbContext instance represents a session with the database and can be used to query and save instances of your entities. DbContext is a combination of the Unit Of Work and Repository patterns.
Run:
dotnet ef migrations add InitialCreate -o Data/Migrations
Output:
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
Then run:
dotnet ef database update
Output snippets:
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE "__EFMigrationsHistory" (
"MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY,
"ProductVersion" TEXT NOT NULL
);
SELECT COUNT(*) FROM "sqlite_master" WHERE "name" = '__EFMigrationsHistory' AND "type" = 'table';
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE "Users" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Users" PRIMARY KEY AUTOINCREMENT,
"UserName" TEXT NULL
);
Open the Extensions tool window, search for SQLite, and install the Extension maintained by alexcvzz.
Then, open the Command Palette (SHIFT + CMD + P), and search for "sqlite" and click on the
option that reads "SQLite: Open Database". Choose our database file: API/datingapp.db
.
The UX design for this Extension has changed in subsequent versions of Visual Studio. As of this writing, there should now be a "SQLITE EXPLORER" expandable menu at the bottom of the Solution Explorer tool window (on the left sidebar).
Under the SQLITE EXPLORER menu, right-click on the "Users" table, and click on the "New Query [Insert]" option. You should see this SQL statement in the editor window:
-- SQLite
INSERT INTO Users (Id, UserName)
VALUES ();
Copy the complete statement a few times, and then populate VALUES()
with values:
1, "Bob"
2, "Tom"
3, "Jane"
Highlight the three queries in the editor, right-click, then click on the "Run Selected Query" option. If you click the "Run" arrow next to the Users table, you should see output like:
+----+----------+
| Id | UserName |
+----+----------+
| 1 | Bob |
| 2 | Tom |
| 3 | Jane |
+----+----------+
We wrap the ActionResult
return types with Task<>
, with documentation that reads:
Represents an asynchronous operation that can return a value.
And we pair this with the await
keyword:
This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
If you get an error like this when testing the API:
Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException:
The request matched multiple endpoints. Matches:
API.Controllers.UsersController.GetUser (API)
API.Controllers.UsersController.GetUser (API)
at Microsoft.AspNetCore.Routing.Matching.DefaultEndpointSelector.ReportAmbiguity(CandidateState[] candidateState)
at Microsoft.AspNetCore.Routing.Matching.DefaultEndpointSelector.ProcessFinalCandidates(HttpContext httpContext, CandidateState[] candidateState)
at Microsoft.AspNetCore.Routing.Matching.DfaMatcher.MatchAsync(HttpContext httpContext)
at Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.Invoke(HttpContext httpContext)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
HEADERS
=======
Accept: */*
Connection: keep-alive
Host: localhost:5001
User-Agent: PostmanRuntime/7.31.1
Accept-Encoding: gzip, deflate, br
It is likely a failure relating to the "Hot Reload" feature used by dotnet watch run
. You'll
need to shut down the dotnet runtime and restart it.
Complete the walking skeleton and have an introductory understanding of:
- Using the Angular CLI
- How to create a new Angular app
- The Angular project files
- The Angular bootstrap process
- Using the Angular HTTP Client Service
- Running an Angular app over HTTPS
- How to add packages using NPM
We will be using Angular to create a SPA (Single Page Application).
Check versions with:
node --version
npm --version
Then let's install Angular version 14:
npm install -g @angular/cli@14
If you get this sort of error:
npm ERR! code EACCES
npm ERR! syscall mkdir
npm ERR! path /usr/local/lib/node_modules/@angular
npm ERR! errno -13
npm ERR! Error: EACCES: permission denied, mkdir '/usr/local/lib/node_modules/@angular'
npm ERR! [Error: EACCES: permission denied, mkdir '/usr/local/lib/node_modules/@angular'] {
npm ERR! errno: -13,
npm ERR! code: 'EACCES',
npm ERR! syscall: 'mkdir',
npm ERR! path: '/usr/local/lib/node_modules/@angular'
npm ERR! }
npm ERR!
npm ERR! The operation was rejected by your operating system.
npm ERR! It is likely you do not have the permissions to access this file as the current user
npm ERR!
npm ERR! If you believe this might be a permissions issue, please double-check the
npm ERR! permissions of the file and its containing directories, or try running
npm ERR! the command again as root/Administrator.
You may need to run the command again, prefixed with sudo
After successful installation, run this to see the Angular details:
ng version
Which will have output like:
_ _ ____ _ ___
/ \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _|
/ △ \ | '_ \ / _` | | | | |/ _` | '__| | | | | | |
/ ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | |
/_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___|
|___/
Angular CLI: 14.2.11
Node: 16.10.0
Package Manager: npm 7.24.0
OS: darwin arm64
Angular:
...
Package Version
------------------------------------------------------
@angular-devkit/architect 0.1402.11 (cli-only)
@angular-devkit/core 14.2.11 (cli-only)
@angular-devkit/schematics 14.2.11 (cli-only)
@schematics/angular 14.2.11 (cli-only)
Next run:
ng new client
Which will create a client/
directory alongside our existing API/
folder.
Choose Yes for "Would you like to add Angular routing?", and choose CSS for our stylesheet format.
If you run into errors, try uninstalling the CLI and re-creating the client
with:
rm -r client/
npm cache clean --force
npm uninstall -g @angular/CLI
sudo chown -R 505:20 "/usr/local/lib/node_modules"
sudo npm install -g @angular/cli@14
npm -v
ng version
sudo ng new client
You should eventually see:
CREATE client/src/app/app.component.css (0 bytes)
CREATE client/src/app/app.component.html (23115 bytes)
CREATE client/src/app/app.component.spec.ts (1073 bytes)
CREATE client/src/app/app.component.ts (210 bytes)
✔ Packages installed successfully.
Directory is already under version control. Skipping initialization of git.
This might take a couple of minutes to download all the dependencies -- be patient 😉
To run the Angular client app, navigate to the client
dir with cd client
,
and then run ng serve
.
If you run into Error: EACCES: permission denied, mkdir
errors, then try manually
creating the .angular
directory and elevating the permissions for the client
dir:
sudo chmod -R 755 client/
sudo chmod -R 775 /usr/local/lib/node_modules/
cd client
mkdir .angular
When you successfully run ng serve
, you should see terminal output like this:
✔ Browser application bundle generation complete.
Initial Chunk Files | Names | Raw Size
vendor.js | vendor | 2.12 MB |
polyfills.js | polyfills | 318.00 kB |
styles.css, styles.js | styles | 210.08 kB |
main.js | main | 49.83 kB |
runtime.js | runtime | 6.51 kB |
| Initial Total | 2.69 MB
Build at: 2023-03-24T23:10:11.568Z - Hash: da54282da5c1aa00 - Time: 5577ms
** Angular Live Development Server is listening on localhost:4200, open your
browser on http://localhost:4200/ **
If we inspect the app root component, called app.component.ts
, we will see a decorator
called @Component
that specifies the app-root
that is declared in the index.html
file.
Files with the ts
prefix or suffix utilize TypeScript:
https://www.typescriptlang.org/
The OnInit
interface is described as:
A lifecycle hook that is called after Angular has initialized all data-bound properties of a directive. Define an ngOnInit() method to handle any additional initialization tasks.
Our AppComponent
within the app.module.ts
TypeScript file implements this interface.
We'll need two Terminal instances; one to run our client
app, and one to run our API
.
Under the Terminal tab, you can click the "Plus (+)" button to create another instance,
and then they'll be shown in the right sidebar.
In one Terminal, cd API
then dotnet watch run
. In the other Terminal, cd client
then
ng serve
. Now both services will be running.
Early on, if we run our application with:
ngOnInit(): void {
this.http.get('https://localhost:5001/api/users').subscribe({
next: response => this.users = response,
error: error => console.log(error),
complete: () => console.log('Request has completed')
})
}
Open up the DevTools Window by right-clicking in the Browser and clicking on "Inspect",
and we will get a CORS policy error:
https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
Access to XMLHttpRequest at 'https://localhost:5001/api/users' from origin
'http://localhost:4200' has been blocked by CORS policy: No 'Access-Control-Allow-Origin'
header is present on the requested resource.
Cross-Origin Resource Sharing (CORS) is an HTTP-header based mechanism that allows a server to indicate any origins (domain, scheme, or port) other than its own from which a browser should permit loading resources. CORS also relies on a mechanism by which browsers make a "preflight" request to the server hosting the cross-origin resource, in order to check that the server will permit the actual request. In that preflight, the browser sends headers that indicate the HTTP method and headers that will be used in the actual request.
For development purposes, we can fix this by adding this CORS builder to the Program.cs
:
app.UseCors(builder => builder
.AllowAnyHeader()
.AllowAnyMethod()
.WithOrigins("http://localhost:4200"));
Once this change goes live, we should now see this Header present in our API response:
Access-Control-Allow-Origin: http://localhost:4200
Which will allow all responses from the 4200 server access port number.
The documentation for *ngFor
reads:
A structural directive that renders a template for each item in a collection. The directive is placed on an element, which becomes the parent of the cloned templates.
The
ngForOf
directive is generally used in the shorthand form*ngFor
. In this form, the template to be rendered for each iteration is the content of an anchor element containing the directive.
A schematic is a template-based code generator that supports complex logic. It is a set of instructions for transforming a software project by generating or modifying code. Schematics are packaged into collections and installed with npm.
Schematics are part of the Angular ecosystem. The Angular CLI uses schematics to apply transforms to a web-app project. You can modify these schematics, and define new ones to do things like update your code to fix breaking changes in a dependency, for example, or to add a new configuration option or framework to an existing project.
Schematics that are included in the @schematics/angular
collection are run by default
by the commands ng generate
and ng add
.
https://angular.io/guide/schematics
Installation instructions are here: https://valor-software.com/ngx-bootstrap/#/documentation#installation
But at the time of this writing, if we try to run:
ng add ngx-bootstrap
It will fail with an error (since ngx-bootstrap version 9.0.0 is not compatible with
Angular CLI version 14). So instead, let's run these commands (make sure you are in the
client/
directory) with versions explicitly declared:
npm install ngx-bootstrap@9
npm install bootstrap@5
And let's install a font package to make our UI classy:
npm install font-awesome
Here's a complete list of the Icons available in version 4.7.0
of Font Awesome:
https://fontawesome.com/v4/icons/
And let's also install NGX Spinner:
npm install ngx-spinner@14.0.0 --save
See also:
https://github.com/Napster2210/ngx-spinner
https://napster2210.github.io/ngx-spinner/
June 22, 2023 Update: I ran npm audit fix
which updated a few of the dependencies
that were reported with vulnerabilities and changed the contents of package-lock.json
.
Inside our client/ssl/
folder, there are two files:
server.crt
server.key
Double-click on the server.crt
file to install it as a localhost
certificate in the
MacOS Keychain Access, under the "login" option in the left sidebar (it won't show up
if you have "System" or "System Roots" selected).
These are the detailed installation instructions:
- Double click on the certificate (
server.crt
) - Select your desired keychain (login should suffice)
- Add the certificate
- Open Keychain Access if it isn't already open
- Select the keychain you chose earlier
- You should see the certificate localhost
- Double click on the certificate
- Expand Trust
- Select the option Always Trust in When using this certificate
- Close the certificate window
The certificate should now be installed. This certificate was generated using this
openssl-custom.cnf
configuration:
[req]
default_bits = 2048
prompt = no
default_md = sha256
x509_extensions = v3_req
distinguished_name = dn
[dn]
C = US
ST = KS
L = Olathe
O = IT
OU = IT Department
emailAddress = webmaster@example.com
CN = localhost
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = *.localhost
DNS.2 = localhost
With Shell command:
#!bin/bash
openssl req \
-newkey rsa:2048 \
-x509 \
-nodes \
-keyout server.key \
-new \
-out server.crt \
-config ./openssl-custom.cnf \
-sha256 \
-days 7300
Implement basic authentication in our app and have an understanding of:
- How to store passwords in the Database
- Using inheritance in C# - DRY (Don't Repeat Yourself)
- Using the C# debugger
- Using Data Transfer Objects (DTOs)
- Validation
- JSON Web Tokens (JWTs)
- Using services in C#
- Middleware
- Extension methods - DRY
Requirements:
- Users should be able to log in
- Users should be able to register
- Users should be able to view other users
- Users should be able to privately message other users
We're going to Hash and Salt user passwords to demonstrate the basic concepts of authentication. This isn't a battle-hardened, bullet-proof solution; this is just one step above adding completely fake authentication. It just gives us the concept of how authentication works in a very visible way that we can code ourselves.
Read more about security vulnerabilities, attack vectors, brute force attacks, access controls,
rainbow tables and other software security concepts here:
https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
https://owasp.org/Top10/A07_2021-Identification_and_Authentication_Failures/
https://attack.mitre.org/techniques/enterprise/
https://security.stackexchange.com/questions/35523/is-salting-a-hash-really-as-secure-as-common-knowledge-implies
https://www.authgear.com/post/password-hashing-salting
Safe storage of passwords:
- Why don't we use ASP.NET Identity?
- Why are we storing the Password Salt in the Database? Isn't this less secure?
Don't worry! Later on we will refactor to the widely used and battle-hardened ASP.NET Core Identity: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity
By default, an API function written like this:
public async Task<ActionResult<AppUser>> Register(string username, string password)
Derives the parameters from the URL Query Params, like ?username=foo&password=bar
.
If we wanted to use query params, we would need to specify the attribute [FromBody]
.
However, we're going to use the power of the [ApiController]
attribute, and use a DTO
called RegisterDto
instead.
Industry standard for tokens (RFC 7519). They are self-contained and can contain:
- Credentials
- Claims
- Other information
Benefits of JWT:
- No session to manage; JWTs are self-contained tokens
- Portable; A single token can be used with multiple backends
- No Cookies are required; Mobile friendly
- Performance; Once a token is issued, there is no need to make a database request to verify a user's authentication
In its compact form, JSON Web Tokens consist of three parts separated by dots (.
),
which are:
- Header
- Payload
- Verify Signature
Therefore, a JWT typically looks like the following:
xxxxx.yyyyy.zzzzz
Header:
{
"alg": "HS256",
"typ": "JWT"
}
Payload:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
Signature:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
Read more here: https://jwt.io/introduction
To add a debugger breakpoint, just click to the left of the line number you want execution to break (stop). This will create a small Red Dot in the IDE Editor window.
To run and debug our API, click on the Run and Debug tool window (SHIFT + CMD + D), and then click on the Play button next to .NET Core Launch (web) located in the top-left corner of the IDE.
You can also Attach the debugger to an already-running API instance (note: this will not work when using Hot Reload). The list of "debuggable processes" is often quite large, so you can search the list for "API" and then click on the appropriate Process ID.
In Windows environments the debuggable process might be named API.exe
.
In the appsettings.Development.json
local configuration file, add this property:
"TokenKey": "super secret unguessable key"
When we call our new and improved Login API, a successful login response will have a
"token"
property that looks like this:
{
"username": "jim",
"token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiJqaW0iLCJuYmYiOjE2ODAyOTgwOTksImV4cCI6MTY4MDkwMjg5OSwiaWF0IjoxNjgwMjk4MDk5fQ.1qccJUbwCaZUpX2LoB_e2QuikREfpmJc0bWFmfQ61PGBSfYQVegc_o-RVVV4ig3-7QV9AbHX9l3skvbDmO_ckg"
}
If we go to https://jwt.ms/ and paste the token in there, we will see a Decoded Token like:
{
"alg": "HS512",
"typ": "JWT"
}.{
"nameid": "jim",
"nbf": 1680298099,
"exp": 1680902899,
"iat": 1680298099
}.[Signature]
Implement the login and register functionality into the apps as well as understanding:
- Creating components using the Angular CLI
- Using Angular Template forms
- Using Angular services
- Understanding Observables
- Using Angular structural directives to conditionally display elements on a page
- Component communication from parent to child
- Component communication from child to parent
Bootstrap 5.2 Examples: https://getbootstrap.com/docs/5.2/examples/
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">Carousel</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav me-auto mb-2 mb-md-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item">
<a class="nav-link disabled">Disabled</a>
</li>
</ul>
<form class="d-flex" role="search">
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-success" type="submit">Search</button>
</form>
</div>
</div>
</nav>
~/GitProjects/UdemyDatingApp/client % ng g --help
~/GitProjects/UdemyDatingApp/client % ng g component nav --dry-run
CREATE src/app/nav/nav.component.css (0 bytes)
CREATE src/app/nav/nav.component.html (18 bytes)
CREATE src/app/nav/nav.component.spec.ts (578 bytes)
CREATE src/app/nav/nav.component.ts (263 bytes)
UPDATE src/app/app.module.ts (541 bytes)
Run this command to create the Nav Bar component:
ng g component nav --skip-tests
NGX Bootstrap Dropdown component:
https://valor-software.com/ngx-bootstrap/#/components/dropdowns?tab=overview
New standard for managing async data included in ES7 (ECMAScript 2016):
https://www.w3schools.io/javascript/es7-features-introduction/
https://github.com/tc39/proposals
Observables were introduced in Angular v2, and everything that is asynchronous in Angular
uses Observables. For browser backwards compatibility, there is a compiler called Babel
that can assist.
https://babeljs.io/
Observables
are lazy collections of multiple values over time. We can use them to stream
data. We typically use them for HTTP requests and when we want our Components
to observe
values set up inside a Service
.
You can think of observables like a newsletter:
- Only subscribers of the newsletter receive the newsletter
- If no-one subscribes to the newsletter, it probably will not be printed
Promises...
- Provide a single future value
- Are not lazy
- Can not cancel
Observables...
- Emit multiple values over time
- Are lazy (they won't do anything until somebody subscribes to them)
- Are able to be canceled
- Can be used with map, filter, reduce and other operators
We can also use RxJS (Reactive Extensions JavaScript) with Observables!
https://rxjs.dev/
Implement routing in our Angular app and have an understanding of:
- Angular routing (Single Page Application)
- Adding a bootstrap theme
- Using Angular route guards (they don't provide good security, but prevent users from navigating to areas they're not supposed to).
- Using a Shared Module
<a class="nav-link" routerLink="/members" routerLinkActive="active">Matches</a>
<a class="nav-link" routerLink="/lists" routerLinkActive="active">Lists</a>
<a class="nav-link" routerLink="/messages" routerLinkActive="active">Messages</a>
https://angular.io/guide/routing-overview
https://github.com/scttcper/ngx-toastr
https://ngx-toastr.vercel.app/
cd client/
npm install --cache /tmp/empty-cache
npm install
npm install ngx-toastr@14.3.0 --save
npm install bootswatch
Implement global error handling in both the API and the Angular application. Also to have an understanding of:
- API Middleware
- Angular Interceptors
- Troubleshooting Exceptions - "Error Handling Utopia"
Implement further functionality into our API and gain an understanding of:
- Entity Framework Relationships
- Entity Framework Conventions
- Seeding Data into the Database
- The Repository Pattern
- Using AutoMapper
A Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection.
- Martin Fowler (Patterns of Enterprise Architecture)
We've got a web server, the Kestrel server, in our API https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel?view=aspnetcore-7.0 and requests come in to our controller endpoint. In our controllers we inject the DB context, and our DB context represents a session with our database.
In our case, the repository is an abstraction from DbContext
. The pattern
will encapsulate the logic (ex: GetUser(), GetUsers(), UpdateUser()
), instead
of exposing the hundreds of methods native to DbContext
. It also reduces
duplicate query logic, across multiple controllers. The repository pattern
also promotes testability - the repository is easier to test against than it
is to test the DbContext
(ex: IRepository, MockRepository
). The pattern
also decouples our application from the persistence framework (which in a way,
Entity Framework already accomplishes that to some extent).
Implement the components that make up the user interface in our client application and gain an understanding of:
- Using Typescript types
- Using the async pipe
- Using bootstrap for styling
- Basic CSS tricks to enhance the look
- Using a 3rd party photo gallery
https://github.com/kolkov/ngx-gallery
cd client/
npm install @kolkov/ngx-gallery --legacy-peer-deps
npm audit fix
Implement persistence when updating resources in the API and gaining an understanding of:
- Angular Template forms
- The
CanDeactivate
Route Guard - The
@ViewChild
decorator - Persisting changes to the API
- Adding loading indicators to the client app
- Caching data in Angular services
Implement photo upload functionality in the application and gain an understanding of the following:
- Photo storage options
- Adding related entities
- Using a 3rd party API
- Using the Debugger (again!)
- Updating and deleting resources
- What to return when creating resources in a REST based API
Photo Storage with Cloudinary:
https://cloudinary.com/
- Client uploads photo to API with JTW token
- Server uploads the photo to Cloudinary
- Cloudinary stores photo and sends response
- API saves photo URL and public ID to DB
- Saved in DB and given auto-generated ID
- 201 Created Response sent to client with location header
Open the Command Palette (SHIFT + CMD + P), and search for "NuGet" and click on the option that reads "NuGet: Open NuGet Gallery".
Look for "cloudinary", and install "CloudinaryDotNet" by Cloudinary.
I selected version 1.20.0
.
Additionally, I added API/appsettings.json
to the project .gitignore
file.
This is where we will store the Cloudinary secret API Keys.
This is the original contents of the file for reference:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
https://github.com/valor-software/ng2-file-upload
cd client/
npm install ng2-file-upload@2.0.0-3 --legacy-peer-deps
NOTE: Using npm install ng2-file-upload@next --legacy-peer-deps
will result
in these compiler errors:
Error: node_modules/ng2-file-upload/file-upload/file-drop.directive.d.ts:19:18 - error TS2707: Generic type 'DirectiveDeclaration' requires between 6 and 8 type arguments.
19 static ɵdir: i0.DirectiveDeclaration<FileDropDirective, "[ng2FileDrop]", never, { "uploader": "uploader"; }, { "fileOver": "fileOver"; "onFileDrop": "onFileDrop"; }, never, never, false, never>;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Error: node_modules/ng2-file-upload/file-upload/file-select.directive.d.ts:14:18 - error TS2707: Generic type 'DirectiveDeclaration' requires between 6 and 8 type arguments.
14 static ɵdir: i0.DirectiveDeclaration<FileSelectDirective, "[ng2FileSelect]", never, { "uploader": "uploader"; }, { "onFileSelected": "onFileSelected"; }, never, never, false, never>;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
** Angular Live Development Server is listening on localhost:4200, open your browser on https://localhost:4200/ **
✖ Failed to compile.
Implement more advanced forms using Reactive Forms in Angular and understand how to:
- Use Reactive Forms
- Use Angular Validation for inputs
- Implement custom validators
- Implement reusable form controls
- Working with Date inputs
Implement paging, sorting, and filtering, and gain an understanding of the following:
- How to implement pagination on the API & client
- Deferred Execution using IQueryable
- How to implement filtering on the API & client
- How to implement sorting on the API & client
- Using Action Filters
- Adding a TimeAgo pipe
- Implement caching in the client for paginated resources
- Helps avoid performance problems.
- Parameters are passed by query string:
- Page size should be limited.
- We should always paginate the results.
We build up an expression tree and we store this as an IQueryable
of type
whatever:
IQueryable<User> --> var query = context.Users
.Where(x => x.Gender == gender)
.OrderBy(x => x.UserName)
.Take(5)
.Skip(5)
.AsQueryable();
The query only gets executed when we use a ToListAsync()
:
query.ToListAsync()
query.ToArrayAsync()
query.ToDictionary()
query.Count()
https://ihym.github.io/ngx-timeago/
https://www.npmjs.com/package/ngx-timeago
cd client/
npm show ngx-timeago versions
npm install ngx-timeago@2.0.0 --save
Implement the 'Like User' functionality and an understanding of the following:
- Many to many relationship
- Configuring entities in the DbContext:
An
AppUser
can be liked by manyAppUsers
(and vice versa) - Implement a solution using the Fluent API
Implement the Messaging functionality and gain an understanding of the following:
- More many-to-many relationships
- Using query params in Angular
- Using Route resolvers in Angular
Refactor our code to use ASP.NET Identity and gain an understanding of the following:
- Using .Net Identity
- Role management
- Policy based authorization
- UserManager
- SignInManager
- RoleManager
- Identity and role management is battle-hardened, written and tested by Microsoft.
- Comes with a password hasher with 10,000 salt iterations by default.
- Full framework for managing members and roles.
- Provides an Entity Framework schema to create the needed tables.
- Highly customizable.
Note that ASP.NET Identity is different from Identity Server.
Implement SignalR into our application and understand how to:
- Use and set up SignalR on both the API and the client
- Implement online presence
- Implement live chat between users
- Open source library that provides real-time web functionality to apps
- Good for Dashboards and Monitoring apps
- Good for Collaborative apps (like whiteboards)
- Good for apps that require notifications and chat apps
- Handles connection (and re-connects) management automatically
- Sends messages to all connected clients simultaneously
- Sends messages to specific client groups of clients
- Supports WebSockets, Server-Sent events, and Long polling
- Offers a client side npm package
https://www.npmjs.com/package/@microsoft/signalr
cd client/
npm install @microsoft/signalr
npm audit fix
Implement the Unit of work pattern and gain an understanding of the following:
- The Unit of Work pattern
- Optimizing queries to the DB
- Adding a confirm dialog service
- Finishing touches
Described as: Maintains a list of objects affected by a business transaction and coordinates the writing of changes.
Right now, we have Controllers and Repositories, and we use SaveChanges()
.
The repositories are injected into the Controller, and each one needs its
own instance of the DataContext
as well. This could result in data
inconsistency, if one SaveChanges()
works, and the other does not.
The Unit of Work injects the DataContext
and passes that down as a parameter
to the different Repositories.
- Any photos a user uploads should be un-approved.
- Only admins or moderators can approve photos.
- No other user should be able to see unapproved photos.
- The user that uploaded the photo should be able to see the photo, but it should be clearly identified as "awaiting approval".
- When a user uploads their first photo, this should not be set as their "Main Photo" (because it must first be approved).
- When an Admin or a Moderator approves a photo for a user that does not have a Main Photo, then this action should set the photo to their Main.
- Add
isApproved
to the Photo entity. - Add a
DbSet
for the Photos so we can query directly. - Update the
PhotoDto
. - Update the
Seed.cs
Users so that the initial photo is approved for seeded users. - Drop the database and add a new migration.
- Add a Query filter to only return approved photos.
- Ignore the Query filter for the current user (
GetMemberAsync
) so the current user still sees their unapproved photos. - Add a
PhotoForApprovalDto
with the Photo ID, the URL, the Username and theisApproved
status.
... Further details are included in the course PDF.
Actually publish the app and gain an understanding of how to:
- Prepare the app for publication
- What to consider before publishing
- Switching DBs
- Serving static content from the API server.
- Publishing to Heroku (Free!) using PostgreSQL
- Integrating Heroku to GitHub
- Using git branches
- Environment variables - Cloudinary settings, Token key, etc.
- Localhost
- CORS (Cross-Origin Resource Sharing) - if hosting the client app in different domain
- Database - goodbye SQLite!
- Cost - what is the budget for this?
- Capacity / scalability
- Seed data - admin and moderator users?
- Fake API delays! (these need to be removed)
Docker Dashboard |
---|
Docker GitHub CI Action |
---|
Fly.io Web Dashboard |
---|
Update client/angular.json
"outputPath" to: ../API/wwwroot
.
Run:
ng build
Update "budgets" to:
"maximumWarning": "1mb"
"maximumError": "2mb"
Link to Docker for Desktop:
https://www.docker.com/products/docker-desktop/
Powerful, open source object-relational database system:
https://www.postgresql.org/
Run:
docker run --name postgres -e POSTGRES_PASSWORD=postgrespw -p 5432:5432 -d postgres:latest
In VS Code, go to the Extensions tab (looks like 4 Tetris blocks), and search for "Postgres".
Install the PostgreSQL by Chris Kolkman extension.
Then, open the Command Palette (SHIFT + CMD + P), and search for "NuGet" and click on the option that reads "NuGet: Open NuGet Gallery".
Look for "Npgsql.EntityFrameworkCore.PostgreSQL" by Shay Rojansky, Austin Drenski,
Yoh Deadfall, and check the API.csproj
checkbox, and install the 7.0.0
version.
The API.csproj
file should now have a new entry like this:
<ItemGroup>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.0" />
</ItemGroup>
Then drop the SQLite database by running:
cd API/
dotnet ef database drop
Then update our appsettings.Development.json
file from:
"DefaultConnection": "Data source=datingapp.db"
to:
"DefaultConnection": "Server=localhost; Port=5432; User Id=postgres; Password=postgrespw; Database=datingapp"
Then delete the Migrations/
folder (inside of API/Data/
), then run:
dotnet ef migrations add PostgresInitial -o Data/Migrations
If we run dotnet run
, we'll get an error like this:
at Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions.ExecuteSqlRawAsync(DatabaseFacade databaseFacade, String sql, IEnumerable`1 parameters, CancellationToken cancellationToken)
at Program.<Main>$(String[] args) in /Users/schanzke/GitProjects/UdemyDatingApp/API/Program.cs:line 56
Exception data:
Severity: ERROR
SqlState: 42601
MessageText: syntax error at or near "["
Position: 13
File: scan.l
Line: 1241
Routine: scanner_yyerror
Because PostgreSQL does not support the [Connections]
SQLite syntax (this is
fixed as part of this commit).
We'll also get this error (this is addressed by doing UTC conversions in
the Seed.cs
class):
fail: Program[0]
An error occurred during migration
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while
saving the entity changes. See the inner exception for details.
---> System.InvalidCastException: Cannot write DateTime with
Kind=Unspecified to PostgreSQL type 'timestamp with time zone',
only UTC is supported. Note that it's not possible to mix DateTimes
with different Kinds in an array/range. See the
Npgsql.EnableLegacyTimestampBehavior AppContext switch to enable legacy
behavior.
Go to the Extensions tab (looks like 4 Tetris blocks), and search for "Docker". Install the Docker by Microsoft extension.
These entries need to be added to the git-ignored appsettings.json
file:
"ConnectionStrings": {
"DefaultConnection": "Server=host.docker.internal; Port=5432; User Id=postgres; Password=postgrespw; Database=datingapp"
},
"TokenKey": "super secret unguessable key"
Build the Docker image by running this from our API/
folder:
cd API/
docker build -t <docker_username>/datingapp .
To run the Docker image:
docker run --rm -it -p 8080:80 docker.io/<docker_username>/datingapp:latest
And then in a browser, go to: http://localhost:8080/
The Docker image can be pushed to docker.io with:
docker push <docker_username>/datingapp:latest
If you have trouble authenticating, you can use docker login
.
Fly is a platform for running full stack apps and databases close to your users. We’ve been hammering on this thing since 2017, and we think it’s pretty great.
See website: https://fly.io/
Install the fly.io command line tools:
brew install flyctl
Then authenticate:
flyctl auth signup
Launch our Docker image to fly.io using:
fly launch --image docker-username/datingapp:latest
The configuration should look something like:
Using image docker-username/datingapp:latest
Creating app in /Users/username/GitProjects/UdemyDatingApp
We're about to launch your app on Fly.io. Here's what you're getting:
Organization: Full Name (fly launch defaults to the personal org)
Name: udemydatingapp (derived from your directory name)
Region: Ashburn, Virginia (US) (this is the fastest region for you)
App Machines: shared-cpu-1x, 256MB RAM (most apps need about 1GB of RAM)
Postgres: Fly Postgres
Redis: <none> (not requested)
Hostname: https://udemydatingapp-db.fly.dev/
Note: For this Udemy course, I was blocked from deployment with a 403: Forbidden error (probably ZScaler) like:
Failed attaching udemydatingapp to the Postgres cluster udemydatingapp-db:
can't build tunnel for personal: websocket: failed to WebSocket dial:
expected handshake response status code 101 but got 403.
Try attaching manually with 'fly postgres attach --app udemydatingapp udemydatingapp-db'
Error: can't build tunnel for personal: websocket: failed to WebSocket dial:
expected handshake response status code 101 but got 403
Error: failed to fetch an image or build from source: image must be amd64
architecture for linux os, found arm64 linux
I had to run this to fix the issue:
fly wireguard websockets disable
fly wireguard reset
fly postgres attach --app udemydatingapp udemydatingapp-db
Set the Cloudinary API Secret Key:
fly secrets set CloudinarySettings__ApiSecret=secret_apikey06PgY
Generate a strong password using a tool like: https://delinea.com/resources/password-generator-it-tool
And set the Token Key:
fly secrets set TokenKey=somestrongpasswordycPTHXIwZfpbJI
List the fly secrets by running fly secrets list
, and see:
NAME DIGEST CREATED AT
CloudinarySettings__ApiSecret 708c5f0c74b2f804 10m25s ago
DATABASE_URL b82821dc05bcc987 9s ago
TokenKey fc461f13437f184b 6m56s ago
Test the connection like:
fly ping udemydatingapp-db.internal
After updating the ApplicationServiceExtensions.cs
and Program.cs
we need
to rebuild and push an update Docker image:
cd API/
docker build -t <docker_username>/datingapp .
docker push <docker_username>/datingapp:latest
And deploy our app to fly.io:
fly deploy
Unfortunately we'll run into this error with the Apple Silicon ARM Chip:
Error: failed to fetch an image or build from source: image must be amd64
architecture for linux os, found arm64 linux
- Copy the template from https://github.com/docker/build-push-action
- Paste that template into a new
.github/workflows/docker-push.yml
file. - Go to the GitHub repository Settings > Secrets and variables > Actions.
- Create a 'New Repository Secret' with name:
DOCKERHUB_USERNAME
. - Create a 'New Repository Secret' with name:
DOCKERHUB_TOKEN
. This will require an Access Token to be generated in Docker Hub, under My Account > Security. See: https://docs.docker.com/security/for-developers/access-tokens/
Log into GitHub, go to Actions, select the docker-push
action, and Run the
Workflow. Hopefully it succeeds!
Finally, we can run fly deploy
. Since I haven't given my credit card, I get
the error:
Error: input:3: createRelease We need your payment information to continue!
Add a credit card or buy credit: https://fly.io/dashboard/full-name/billing
https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/
Some fly.io tips and tricks:
# Proxies connections to a Fly Machine through a WireGuard tunnel. By default,
# connects to the first Machine address returned by an internal DNS query on
# the app.
fly proxy 6543:5432 -a datingapp-db
# View application logs as generated by the application running on the Fly
# platform. Logs can be filtered to a specific instance using the
# --instance/-i flag or to all instances running in a specific region using
# the --region/-r flag.
fly logs -a datingapp
First of all we want to make sure our app runs without issue on Sql Server 2019. For windows you can just install this directly, but for Mac/Linux then you can get a docker image of SQL as Microsoft now has a Linux version of SQL. If you are on Windows but do not have SQL installed then so long as you have Docker then you can go ahead and do the same as me.
Since SQL is a bit of a big install I'm going to download the files to my computer by running the following command:
docker pull microsoft/mssql-server-linux:latest
SQL Server requires a bit more memory than other DBs so I am also going to increase the memory for Docker in the preferences to 4GB.
Then we can run the following command to run the SQL Server:
docker run -d --name sqldemo -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=Password1!' -p 1433:1433 microsoft/mssql-server-linux
Sql SA account needs a strong password, and I am not saying the above is (!) but it does meet the complexity requirements.
I'm going to create a new branch so that I do not interfere with the master
branch. Run the following:
git checkout -b AzurePublish
Add the following Sql Server provider via Nuget: Microsoft.EntityFrameworkCore.SqlServer
You can remove the package for Sqlite and Postgres if you still have them installed - we only need SqlServer for this Ensure you pick the same version as your runtime.
Open the appsettings.development.json
and change the default connection
string to the following:
"ConnectionStrings" : {
"DefaultConnection": "Server=localhost; User Id=sa; Password=Password1!; Database=datingappdb"
}
Update the ApplicationServiceExtensions
to use this:
services.AddDbContext<DataContext>(options =>
{
options.UseSqlServer(config.GetConnectionString("DefaultConnection"));
});
Delete the migrations folder from Data/Migrations and create a new migration for the Sql Server provider:
dotnet ef migrations add SqlInitial -o Data/Migrations
Check the migration and ensure you can see Sql server specific annotations in there:
migrationBuilder.CreateTable(
name: "AspNetRoles",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1", "1"),
Name = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
ConcurrencyStamp = table.Column<string>(type: "nvarchar(256)", nullable: true)
},
Restart the app and make sure everything works! ... Well, it doesn’t because
Sql server is special. So we need to add an extra bit of configuration here to
the DataContext.cs
class and make sure one of the UserLike
specifies no
action for the delete behaviour:
builder.Entity<UserLike>()
.HasOne(s => s.SourceUser)
.WithMany(l => l.LikedUsers)
.HasForeignKey(s => s.SourceUserId)
.OnDelete(DeleteBehavior.NoAction);
builder.Entity<UserLike>()
.HasOne(s => s.LikedUser)
.WithMany(l => l.LikedByUsers)
.HasForeignKey(s => s.LikedUserId)
.OnDelete(DeleteBehavior.Cascade);
Delete the migrations folder and recreate the migration:
dotnet ef migrations add SqlInitial -o Data/Migrations
Restart the app again and make sure we have success!
This time everything goes smoothly. Restart the angular app and ensure we
can operate the application without any errors on localhost 4200
. It should
work fine.