Lesson 0 HTTP Protocol
Request + Response + Simple Input Validation
Lesson 1 Razor Pages + Statemanagement
Lesson 2 Razor Pages Page Structure + Validation
Lesson 3 Database Interaction (Dapper)
Lesson 4 Ajax Requests + Other techniques
git clone https://github.com/NHLStenden/WebdevCourseRazorPages.git WebdevCourseRazorPages
cd WebdevCourseRazorPagesExecute the following command in the terminal:
Run the project (Examples), click on an example
/Lesson1/GetRequest
Run the project (Examples), click on an example
README.md files contain exercises
Program the solutions in Lesson1Exercises, Assignment1.cshtml, etc..
Exercises:
Tests:
Demo in IDE
HTTP Protocol:
client
server
Domain Name Server
See the image on the previous slide for a graphical explanation.
Domain Name Server
The IP4 Address of www.google.com. 216.58.214.4
HTTP GET Message is used to retrieve a website
Demo in Postman and Chrome Development Tools
Method / HTTP-verb (GET, POST)
Path
Verion of HTTP Protocol
Headers
HTTP Status Code
HTTP Protocol / Version
Headers
Body (Content)
Content-Type: specifies the type of content (MIMI-type, for example: text/html, text/css)
Client
Server
GET .../login
Response "Login Page"
POST .../login
Response "Login Success"
Retrieve a website, with a GET Request
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
public class SynchronousSocketClient {
public static void StartClient() {
// Data buffer for incoming data.
byte[] bytes = new byte[1024];
// Connect to a remote device.
try {
// Establish the remote endpoint for the socket.
// This example uses port 11000 on the local computer.
IPHostEntry ipHostInfo = Dns.GetHostEntry("www.google.com");
IPAddress ipAddress = ipHostInfo.AddressList[0];
IPEndPoint remoteEP = new IPEndPoint(ipAddress,80);
// Create a TCP/IP socket.
Socket sender = new Socket(ipAddress.AddressFamily,
SocketType.Stream, ProtocolType.Tcp );
// Connect the socket to the remote endpoint. Catch any errors.
try {
sender.Connect(remoteEP);
Console.WriteLine("Socket connected to {0}",
sender.RemoteEndPoint.ToString());
string getMessage = "GET / HTTP/1.1\r\nContent-Length: 0\r\n\r\n";
//string getMessage = "GET /lesson0/getRequest HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: 0\r\n\r\n";
// Encode the data string into a byte array.
byte[] msg = Encoding.UTF8.GetBytes(getMessage);
Console.WriteLine(getMessage);
// Send the data through the socket.
int bytesSent = sender.Send(msg);
// Receive the response from the remote device.
int bytesRec = sender.Receive(bytes);
Console.WriteLine("HTTP Response: ");
Console.WriteLine(Encoding.ASCII.GetString(bytes,0,bytesRec));
// Release the socket.
sender.Shutdown(SocketShutdown.Both);
sender.Close();
} catch (ArgumentNullException ane) {
Console.WriteLine("ArgumentNullException : {0}",ane.ToString());
} catch (SocketException se) {
Console.WriteLine("SocketException : {0}",se.ToString());
} catch (Exception e) {
Console.WriteLine("Unexpected exception : {0}", e.ToString());
}
} catch (Exception e) {
Console.WriteLine( e.ToString());
}
}
public static int Main(String[] args) {
StartClient();
return 0;
}
}
Try not to Query (READ/SELECT) in OnGet(..) or OnPost() methods!
The server should process the HTTP Request and return the HTTP Response!
In ASP.NET Core the Request is processed in the Request Pipeline
//middleware A
app.Use(async (context, next) =>
{
if (context.Request.Path.Value
.StartsWith("/lesson0/getRequestPipelineExample",
StringComparison.OrdinalIgnoreCase))
{
//Write to the context.Response.Body
await context.Response.WriteAsync("<h1>Hello World</h1>");
context.Response.Headers.Add("Content-Type", "text/html");
context.Response.StatusCode = (int)HttpStatusCode.OK; //HttpStatusCode.OK == 200
}
else
{
await next.Invoke();
}
});
//middleware B
// app.Use(async (context, next) =>
// {
// ...
// await next.Invoke();
// });
app.Run(async context =>
{
await context.Response.WriteAsync("<h1>Page Not Found</h1>");
});Demo - Inspect with debugger!
Razor
Razor Pages
Statemangement
Handler Methods
Demo of ASP.NET Core Razor WebPages
Todo list
Razor is a markup syntax for embedding server-based code into webpages. The Razor syntax consists of Razor markup, C#, and HTML. Files containing Razor generally have a .cshtml file extension.
Visual Studio:
PageCentricSimple.cshtml
PageCentricSimple.cshtml.cs
PageCentricSimple.cshtml
PageCentricSimple.cshtml.cs
Model gives access to Page Model public properties & methods & variables
Content Page must have @model directive to couple Page Model
/Lesson1/PageCentricSimple
Show in Chrome & Debugger & IDE
Three ways to get the Query String value(s):
[BindProperty(SupportsGet = true)]
<a href="/Lesson1/RouteParameters/decrement/0">decrement</a>
String after @page is called the Route Template
Examples:
Three ways to get the Route Data values:
<a href="/Lesson1/RouteParameters/decrement/0">decrement</a>
String after @page is called the Route Template
Client
Server
GET .../login
Response "Login Page"
POST .../login
Response "Login Success"
<form method="post">
<input type="text" name="username">
<button type="submit">Verzenden</button>
</form>method="post"
Three ways to get the form data:
Let's take a look at Coolblue in the Chrome Development Tools
//To write
TempData["Lievelingsgetal"] = lievelingsgetal.ToString();
//Redirect
return RedirectToPage("ShowTempData");//To read
@TempData["Lievelingsgetal"]
string strCount = HttpContext.Session.GetString("count");
if (strCount != null)
{
Count = Convert.ToInt32(strCount);HttpContext.Session.SetString("count", Count.ToString());Usage: store temporary data (20 minutes by default)
Doesn't work in live preview!
Three techniques:
Attributes are applied to properties on the inbound model - typically a PageModel or ViewModel
Attributes are applied to properties on the inbound model - typically a PageModel or ViewModel
public class Product
{
public int ProductId { get; set; }
[Required(ErrorMessage = "Naam mag niet leeg zijn"), MinLength(2), MaxLength(12),
Display(Name = "Naam", Prompt = "Geef een geldige product naam op")]
public string Name { get; set; }
[MaxLength(128)]
[DefaultValue("Geen beschrijving aanwezig")]
public string Description { get; set; }
[Required, Range(0, 10000)]
public decimal Price { get; set; }
public decimal? SalePrice { get; set; }
//[DefaultValue(typeof(DateTime), DateTime.Now)]
//https://www.learnrazorpages.com/razor-pages/forms/dates-and-times#:~:text=The%20default%20value%20for%20a,00%3A00%20in%20the%20control.
[DataType(DataType.Date)]
public DateTime InShopDate { get; set; } = DateTime.Today;
[Required]
public int CategoryId { get; set; }
public Category Category { get; set; }
}Take a look with chrome dev tools how they are rendered
@section Scripts
{
<partial name="_ValidationScriptsPartial" />
<script>
var settings = {
validClass: "is-valid",
errorClass: "is-invalid"
};
$.validator.setDefaults(settings);
$.validator.unobtrusive.options = settings;
</script>
}//Lesson2/Products/Index.cshtml
@page
@model Examples.Pages.Lesson2.Products.Index
@{
Layout = "Products/_ProductsLayout";
}//Lesson2/Products/_ProductsLayout.cshtml
@{
Layout = "Shared/_Layout";
}
<h1>Products Layout</h1>
<h1>Category Info for Category 1, see _ProductLayout.cshtml</h1>
@await Component.InvokeAsync("CategoryInfoComponent", "Category 1")
@RenderBody()//Shared/_Layout.cshtml
//check in Code Editor@* In the _Layout page *@
if(IsSectionDefined("Scripts")) {
RenderSection("Scripts");
}
@* In a Content Page */
@section Scripts {
<script src="popupWidget.js"></script>
}
---------------------------------------
if(IsSectionDefined("Footer")) {
RenderSection("Footer");
} else {
@* Default Footer *@
}
@* In a Content Page */
@section Footer {
@* optional footer *@
}public class CategoryInfoComponent : ViewComponent
{
private ProductsRepository _productsRepository;
public CategoryInfoComponent()
{
_productsRepository = new ProductsRepository();
}
public IViewComponentResult Invoke(string categoryName = null)
{
var products = _productsRepository.GetProductsInShop()
.OrderBy(x => x.Name)
.ToList();
if (!string.IsNullOrWhiteSpace(categoryName))
{
products = products.Where(x => x.Category.Name == categoryName).ToList();
}
List<CategorySummary> categorySummaries = new List<CategorySummary>();
foreach (var productByGroup in products
.GroupBy(x => x.Category))
{
categorySummaries.Add(new CategorySummary()
{
CategoryName = productByGroup.Key.Name,
NumberOfProducts = productByGroup.Count()
});
}
// ReSharper disable once Mvc.ViewComponentViewNotResolved
return View(categorySummaries);
}
}1. InvokeAsync("viewComponentname", <Param1>, <Param2>,...)
2. web components (kebab case naming), not working for me
Parameters
@page "{filterCategory?}"//startup.cs --> public void ConfigureServices(IServiceCollection services)
services
.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AddPageRoute("/Archive/Post", "Post/{year}/{month}/{day}/{title}");
});@page "{year:int}/{month:int}/{day:int?}"
@model TodoExample.Pages.Todo.TestUrls
<h1>Test</h1>
@Request.RouteValues["year"]
<br>
@Request.RouteValues["month"]
<br>
@Request.RouteValues["day"]
<br>public void OnGet(int year, int month, int? day)
{
}public class ProductIdExistsConstraint : IRouteConstraint
{
public bool Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values,
RouteDirection routeDirection)
{
var value = values["productid"];
if (value == null)
return false;
int productId;
if (int.TryParse(value.ToString(), out productId))
{
var productsRepository = new ProductsRepository();
var product = productsRepository.GetProductById(productId);
return product != null;
}
else
{
return false;
}
}
}//Startup.cs
public void ConfigureServices(IServiceCollection services)
{
...
services.Configure<RouteOptions>(options =>
{
options.ConstraintMap.Add("productIdExists", typeof(ProductIdExistsConstraint));
});
...
}@page "{productid:productIdExists}"
@model Examples.Pages.Lesson2.Products.Details
@{
Layout = "_Layout";
}
<div class="row">Register:
Use:
Get some feeling when to use which technique to reduce code duplication and improve code reuse.
Dry (Don't Repeat Yourself):
If you do a lot of times the same task (code) think about the techniques of this lesson and OOP in general to avoid this!
dotnet add package Dapper
dotnet add package MySql.Data
var connectionString = "Server=127.0.0.1;Port=3306;Database=Examples;Uid=root;Pwd=Test@1234!;"
using var db = new MySqlConnection(connectionString);
var todos =
db.Query<Todo>("SELECT * FROM Todo")
.ToList();Column names and Properties must match!
public static List<Todo> Get()
{
using var connection = new MySqlConnection(GetConnectionString());
return connection.Query<Todo>("SELECT Id, Name, Completed FROM Todo")
.ToList();
}public static Todo? Get(int id)
{
string sql = "SELECT Id, Name, Completed FROM Todo WHERE Id = @Id";
using var connection = new MySqlConnection(GetConnectionString());
return connection.QuerySingleOrDefault<Todo>(sql, new { Id = id });
} public static void Delete(int id)
{
using var connection = new MySqlConnection(GetConnectionString());
var sql = "DELETE FROM Todo WHERE Id = @Id";
connection.Execute(sql, new { Id = id });
}
public static Todo Create(Todo todo)
{
using var connection = new MySqlConnection(GetConnectionString());
var sql = "INSERT INTO Todo (Name, Completed) VALUES (@Name, @Completed); " +
"SELECT LAST_INSERT_ID();";
var id = connection.ExecuteScalar<int>(sql, todo);
todo.Id = id;
return todo;
}
| CRUD | Repository | SQL |
|---|---|---|
| Create | Add(...) | INSERT |
| Read | GetById(id), GetAll(), Get(), etc | SELECT |
| Update | Update(...) | UPDATE |
| Delete | Delete/Remove | DELETE |
Simple explanation:
Longer explanation:
public List<Todo> GetWithSQLInjection(string filter)
{
var todos = new List<Todo>();
try
{
//De code is anders, omdat SQL injectie anders alsnog wordt tegengehouden
using (var connection = Connect())
{
//Doe dit nooit zelf een querystring in elkaar zetten!
//!!!SQL INJECTIE!!! Alle gegevens kunnen gestolen worden, etc :-(
var sql = @"SELECT TodoId, Description, Done
FROM Todo WHERE Description LIKE '%" + filter + "%'";
//todos = connection.Query<Todo>(sql).ToList();
var reader = connection.ExecuteReader(sql);
while (reader.Read())
{
todos.Add(new Todo()
{
TodoId = reader.GetInt32(0),
Description = reader.GetString(1),
Done = reader.GetBoolean(2)
});
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
return todos;
}Always, Use Prepared Statement (Dapper will do this for us!) in combination with Parameter Placeholders!
Use (Query) Parameter Placeholders
for example: @Description, @Done
Use a prepare statements
Creates an SQL statement that can be executed on the server (Dapper will do this for us)