├── .gitignore ├── 24-25 ├── Contents │ ├── Ch1Unit1.md │ ├── Ch1Unit2.md │ ├── Ch1Unit3.md │ ├── Ch2Unit0.md │ ├── Ch2Unit1.md │ ├── Ch2Unit2.md │ ├── Ch2Unit3.md │ ├── Ch2Unit4.md │ ├── Ch2Unit5.md │ ├── Ch3Unit0.md │ ├── Ch3Unit1A.md │ ├── Ch3Unit1B.md │ ├── Ch3Unit2.md │ ├── Ch3Unit3.md │ ├── Ch3Unit4.md │ ├── Ch3Unit5.md │ ├── Ch3Unit6.md │ ├── Ch3Unit7.md │ ├── Code │ │ ├── HelloWorld │ │ │ ├── .rest │ │ │ ├── HelloWorld.csproj │ │ │ ├── Program.cs │ │ │ ├── appsettings.Development.json │ │ │ └── obj │ │ │ │ ├── Debug │ │ │ │ └── net6.0 │ │ │ │ │ ├── .NETCoreApp,Version=v6.0.AssemblyAttributes.cs │ │ │ │ │ ├── .NETCoreApp,Version=v6.0.AssemblyAttributes.cs.gz │ │ │ │ │ ├── HelloWorld.AssemblyInfo.cs │ │ │ │ │ ├── HelloWorld.AssemblyInfo.cs.gz │ │ │ │ │ ├── HelloWorld.AssemblyInfoInputs.cache │ │ │ │ │ ├── HelloWorld.AssemblyInfoInputs.cache.gz │ │ │ │ │ ├── HelloWorld.GeneratedMSBuildEditorConfig.editorconfig │ │ │ │ │ ├── HelloWorld.GeneratedMSBuildEditorConfig.editorconfig.gz │ │ │ │ │ ├── HelloWorld.GlobalUsings.g.cs │ │ │ │ │ ├── HelloWorld.GlobalUsings.g.cs.gz │ │ │ │ │ ├── HelloWorld.MvcApplicationPartsAssemblyInfo.cache │ │ │ │ │ ├── HelloWorld.MvcApplicationPartsAssemblyInfo.cs │ │ │ │ │ ├── HelloWorld.assets.cache │ │ │ │ │ ├── HelloWorld.csproj.AssemblyReference.cache │ │ │ │ │ ├── HelloWorld.csproj.CopyComplete │ │ │ │ │ ├── HelloWorld.csproj.CoreCompileInputs.cache │ │ │ │ │ ├── HelloWorld.csproj.FileListAbsolute.txt │ │ │ │ │ ├── HelloWorld.dll │ │ │ │ │ ├── HelloWorld.genruntimeconfig.cache │ │ │ │ │ ├── HelloWorld.pdb │ │ │ │ │ ├── apphost.exe │ │ │ │ │ ├── project.razor.json │ │ │ │ │ ├── project.razor.json.gz │ │ │ │ │ ├── ref │ │ │ │ │ └── HelloWorld.dll │ │ │ │ │ └── staticwebassets.build.json │ │ │ │ ├── HelloWorld.csproj.nuget.dgspec.json │ │ │ │ ├── HelloWorld.csproj.nuget.g.props │ │ │ │ ├── HelloWorld.csproj.nuget.g.targets │ │ │ │ ├── project.assets.json │ │ │ │ ├── project.nuget.cache │ │ │ │ └── staticwebassets.pack.sentinel │ │ └── Practicum2 │ │ │ ├── .rest │ │ │ ├── .vscode │ │ │ ├── launch.json │ │ │ └── tasks.json │ │ │ ├── Controllers │ │ │ ├── AddressController.cs │ │ │ └── PersonController.cs │ │ │ ├── Filters │ │ │ └── Filters.cs │ │ │ ├── Practicum2.csproj │ │ │ ├── Program.cs │ │ │ ├── Properties │ │ │ └── launchSettings.json │ │ │ ├── Services │ │ │ ├── AddressStorage.cs │ │ │ ├── IAddressStorage.cs │ │ │ ├── IPersonStorage.cs │ │ │ └── PersonStorage.cs │ │ │ ├── addresses │ │ │ ├── 5e6c33e3-81b7-4bad-993d-c7c5b33bd4b3.json │ │ │ └── d2803fe9-100e-4419-b6f0-82bc74e94918.json │ │ │ ├── appsettings.Development.json │ │ │ ├── obj │ │ │ ├── Debug │ │ │ │ └── net6.0 │ │ │ │ │ ├── .NETCoreApp,Version=v6.0.AssemblyAttributes.cs │ │ │ │ │ ├── Controllers and services.AssemblyInfo.cs │ │ │ │ │ ├── Controllers and services.AssemblyInfoInputs.cache │ │ │ │ │ ├── Controllers and services.GeneratedMSBuildEditorConfig.editorconfig │ │ │ │ │ ├── Controllers and services.GlobalUsings.g.cs │ │ │ │ │ ├── Controllers and services.csproj.AssemblyReference.cache │ │ │ │ │ ├── Practicum2.AssemblyInfo.cs │ │ │ │ │ ├── Practicum2.AssemblyInfoInputs.cache │ │ │ │ │ ├── Practicum2.GeneratedMSBuildEditorConfig.editorconfig │ │ │ │ │ ├── Practicum2.GlobalUsings.g.cs │ │ │ │ │ ├── Practicum2.MvcApplicationPartsAssemblyInfo.cache │ │ │ │ │ ├── Practicum2.MvcApplicationPartsAssemblyInfo.cs │ │ │ │ │ ├── Practicum2.assets.cache │ │ │ │ │ ├── Practicum2.csproj.AssemblyReference.cache │ │ │ │ │ ├── Practicum2.csproj.CopyComplete │ │ │ │ │ ├── Practicum2.csproj.CoreCompileInputs.cache │ │ │ │ │ ├── Practicum2.csproj.FileListAbsolute.txt │ │ │ │ │ ├── Practicum2.dll │ │ │ │ │ ├── Practicum2.genruntimeconfig.cache │ │ │ │ │ ├── Practicum2.pdb │ │ │ │ │ ├── apphost.exe │ │ │ │ │ ├── project.razor.json │ │ │ │ │ ├── ref │ │ │ │ │ └── Practicum2.dll │ │ │ │ │ ├── refint │ │ │ │ │ └── Practicum2.dll │ │ │ │ │ └── staticwebassets.build.json │ │ │ ├── Practicum2.csproj.nuget.dgspec.json │ │ │ ├── Practicum2.csproj.nuget.g.props │ │ │ ├── Practicum2.csproj.nuget.g.targets │ │ │ ├── project.assets.json │ │ │ ├── project.nuget.cache │ │ │ └── staticwebassets.pack.sentinel │ │ │ └── people │ │ │ ├── 7f0375b2-b93e-4c2e-8937-ad7d70ba18fa.json │ │ │ └── fd5b568c-6c3b-48df-af78-0a923732245b.json │ ├── Contents.md │ ├── img │ │ ├── pic1.png │ │ ├── pic2.png │ │ ├── pic3.png │ │ └── pic4.png │ └── react.md ├── StarterKit │ ├── .gitignore │ ├── Controllers │ │ ├── HomeController.cs │ │ └── LoginController.cs │ ├── Frontend │ │ ├── .gitignore │ │ ├── package.json │ │ ├── src │ │ │ ├── index.tsx │ │ │ ├── pages │ │ │ │ └── Home.tsx │ │ │ └── shared │ │ │ │ └── ErrorPage.tsx │ │ ├── tsconfig.json │ │ ├── webpack.config.js │ │ └── yarn.lock │ ├── Migrations │ │ ├── 20240703113217_InitialCreate.Designer.cs │ │ ├── 20240703113217_InitialCreate.cs │ │ ├── 20240911133945_theatreContext.Designer.cs │ │ ├── 20240911133945_theatreContext.cs │ │ └── DatabaseContextModelSnapshot.cs │ ├── Models │ │ ├── DatabaseContext.cs │ │ ├── ErrorViewModel.cs │ │ ├── Models.Admin.cs │ │ ├── Models.Calendar.cs │ │ └── Models.Theatre.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Services │ │ ├── ILoginService.cs │ │ └── LoginService.cs │ ├── StarterKit.csproj │ ├── StarterKit.sln │ ├── Utils │ │ └── EncryptionHelper.cs │ ├── Views │ │ ├── Home │ │ │ └── Index.cshtml │ │ ├── Shared │ │ │ ├── Error.cshtml │ │ │ ├── _Layout.cshtml │ │ │ ├── _Layout.cshtml.css │ │ │ └── _ValidationScriptsPartial.cshtml │ │ ├── _ViewImports.cshtml │ │ └── _ViewStart.cshtml │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── readme.md │ └── wwwroot │ │ ├── css │ │ └── site.css │ │ ├── favicon.ico │ │ ├── images │ │ └── portal.jpg │ │ └── lib │ │ └── bootstrap.min.css ├── idea.md ├── live-coding │ ├── .gitignore │ ├── .rest │ ├── Controllers │ │ └── WeatherForecastController.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── WeatherForecast.cs │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── data │ │ └── people │ │ │ ├── 847fa535-9850-4c40-8cca-e7374a994744.json │ │ │ ├── a7eb3760-b6bf-4908-839e-201348a8489a.json │ │ │ ├── b5078a76-35b9-4246-a107-08680c46128b.json │ │ │ └── cf068e0b-5091-4aad-a703-b196bdec8135.json │ ├── live-coding.csproj │ ├── live-coding.sln │ └── spa │ │ ├── client │ │ ├── index.tsx │ │ ├── package.json │ │ ├── page │ │ │ └── footer │ │ │ │ └── template.tsx │ │ ├── tsconfig.json │ │ ├── webpack.config.js │ │ └── yarn.lock │ │ ├── main.ts │ │ ├── package.json │ │ ├── server │ │ ├── index.html │ │ ├── package.json │ │ ├── spa.bundle.js │ │ ├── spa.bundle.js.map │ │ └── yarn.lock │ │ ├── tsconfig.json │ │ └── yarn.lock ├── modulewijzer.md ├── practicum │ └── StarterKit │ │ ├── bin │ │ └── Debug │ │ │ └── net8.0 │ │ │ └── project.razor.vscode.bin │ │ └── obj │ │ ├── Debug │ │ └── net8.0 │ │ │ ├── .NETCoreApp,Version=v8.0.AssemblyAttributes.cs │ │ │ ├── StarterKit.AssemblyInfo.cs │ │ │ ├── StarterKit.AssemblyInfoInputs.cache │ │ │ ├── StarterKit.GeneratedMSBuildEditorConfig.editorconfig │ │ │ ├── StarterKit.GlobalUsings.g.cs │ │ │ ├── StarterKit.RazorAssemblyInfo.cache │ │ │ ├── StarterKit.RazorAssemblyInfo.cs │ │ │ ├── StarterKit.assets.cache │ │ │ ├── StarterKit.csproj.AssemblyReference.cache │ │ │ └── project.razor.vscode.bin │ │ ├── StarterKit.csproj.nuget.dgspec.json │ │ ├── StarterKit.csproj.nuget.g.props │ │ ├── StarterKit.csproj.nuget.g.targets │ │ ├── project.assets.json │ │ └── project.nuget.cache ├── rubric.md └── todo.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | .DS_Store 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # Snowpack dependency directory (https://snowpack.dev/) 47 | web_modules/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Optional stylelint cache 59 | .stylelintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variable files 77 | .env 78 | .env.development.local 79 | .env.test.local 80 | .env.production.local 81 | .env.local 82 | 83 | # parcel-bundler cache (https://parceljs.org/) 84 | .cache 85 | .parcel-cache 86 | 87 | # Next.js build output 88 | .next 89 | out 90 | 91 | # Nuxt.js build / generate output 92 | .nuxt 93 | dist 94 | 95 | # Gatsby files 96 | .cache/ 97 | # Comment in the public line in if your project uses Gatsby and not Next.js 98 | # https://nextjs.org/blog/next-9-1#public-directory-support 99 | # public 100 | 101 | # vuepress build output 102 | .vuepress/dist 103 | 104 | # vuepress v2.x temp and cache directory 105 | .temp 106 | .cache 107 | 108 | # Docusaurus cache and generated files 109 | .docusaurus 110 | 111 | # Serverless directories 112 | .serverless/ 113 | 114 | # FuseBox cache 115 | .fusebox/ 116 | 117 | # DynamoDB Local files 118 | .dynamodb/ 119 | 120 | # TernJS port file 121 | .tern-port 122 | 123 | # Stores VSCode versions used for testing VSCode extensions 124 | .vscode-test 125 | 126 | # yarn v2 127 | .yarn/cache 128 | .yarn/unplugged 129 | .yarn/build-state.yml 130 | .yarn/install-state.gz 131 | .pnp.* 132 | 133 | /Contents/Code/HelloWorld/bin/ 134 | Project Ideas/ 135 | StarterKit/ 136 | 137 | 138 | .fake 139 | .DS_Store 140 | -------------------------------------------------------------------------------- /24-25/Contents/Ch1Unit3.md: -------------------------------------------------------------------------------- 1 | ### [Back to List of Topics](Contents.md) 2 | 3 | # Unit 2 - Middlewares, filters, and CQRS 4 | 5 | ## Pre- and post- processing: middleware 6 | The core underlying mechanism of ASP.Net is centered around middlewares. Middlewares are a chain of (anonymous) functions which can inspect the request, write to the response, and if all is well invoke the next step in the chain. Middlewares may also decide *not* to invoke the next step in the chain, and given that the last step is the execution of one of our controllers, this can effectively act as a filter (useful when implementing security restrictions for example). Let's start with a middleware that does nothing whatsoever. We pass to `app.Use` an anonymous function which takes as inputs the `context`, which handles reading and writing to the request and response respectively, and `next`, which represents the rest of the middleware chain. Our first middleware simply ensures that the chain is not interrupted by invoking `next.Invoke`: 7 | 8 | ```c# 9 | app.Use(async (context, next) => 10 | { 11 | await next.Invoke(); 12 | }); 13 | ``` 14 | 15 | We could add some logic in order to log requests (ideally not just to the console, maybe to some fast permanent storage): 16 | 17 | ```c# 18 | app.Use(async (context, next) => 19 | { 20 | await next.Invoke(); 21 | Console.WriteLine($"{context.Request.Path} was handled"); 22 | }); 23 | ``` 24 | 25 | A common security measure is to check for a specific header containing for example an API token. Let's lock down the `/api/person/sayHello` (finally, that thing was a security nightmare!) so that whenever our middleware detects a request to this path, we check whether or not the headers provided contain the key-value pair `(HelloApiToken, MyHelloApiToken)`: 26 | 27 | ```c# 28 | app.Use(async (context, next) => 29 | { 30 | if (context.Request.Path == "/api/person/sayHello") { 31 | if (!context.Request.Headers.ContainsKey("HelloApiToken")) { 32 | Console.WriteLine($"{context.Request.Path} was requested but there is no HelloApiToken header"); 33 | context.Response.StatusCode = 401; 34 | return; 35 | } 36 | if (context.Request.Headers["HelloApiToken"] != "MyHelloApiToken") { 37 | Console.WriteLine($"{context.Request.Path} was requested but the HelloApiToken is {context.Request.Headers["HelloApiToken"]} instead of 'MyHelloApiToken'"); 38 | context.Response.StatusCode = 401; 39 | return; 40 | } 41 | } 42 | await next.Invoke(); 43 | }); 44 | ``` 45 | 46 | Whenever the security check fails, we then set the status code of the response to `401` and skip the `next.Invoke`, thereby refusing to further process the unauthorized request. 47 | 48 | The api token we are using, `MyHelloApiToken`, is unsafe. Moreover, an api token should never be part of the source code, but should rather come from the app settings so that it's easier to switch to a different api token for each environment (development, testing, or acceptance). Let's add a safer api token to the `appsettings.Development.json`: 49 | 50 | ```json 51 | { 52 | ... 53 | "HelloApiToken": "e5e6c507-f46c-459f-a40c-15aac76f9638" 54 | } 55 | ``` 56 | 57 | We can now turn the previous, unsafe version `if (context.Request.Headers["HelloApiToken"] != "MyHelloApiToken") {`, into a safer version that uses the token from the appsettings: 58 | 59 | ```c# 60 | if (context.Request.Headers["HelloApiToken"] != builder.Configuration["HelloApiToken"] as string) { 61 | ``` 62 | 63 | We can test the endpoint with an api token as follows: 64 | 65 | ```rest 66 | GET https://localhost:5000/api/person/sayHello 67 | HelloApiToken: e5e6c507-f46c-459f-a40c-15aac76f9638 68 | ##### 69 | ``` 70 | 71 | ## Filters 72 | Middlewares are global mechanisms, meaning that they are applied to every request indiscriminately. This can be appropriate for some logic such as logging of all requests, but the latest example which manually checks for a given URL against the requested path is less than ideal because the logic actually belongs with a specific method of a specific controller, and those two pieces of code (the middleware and the controller method) are too far from each other. We can make the link between controller/method and filter more explicit by introducing an action filter. An action filter is a middleware which is not applied everywhere indiscriminately, but rather it is encapsulated in a class inheriting both `Attribute` and `IAsyncActionFilter`: 73 | 74 | ```c# 75 | public class HelloHeaderActionFilter : Attribute, IAsyncActionFilter 76 | { 77 | public async Task OnActionExecutionAsync( 78 | ActionExecutingContext actionContext, ActionExecutionDelegate next) 79 | { 80 | var context = actionContext.HttpContext; 81 | if (!context.Request.Headers.ContainsKey("HelloApiToken")) { 82 | Console.WriteLine($"{context.Request.Path} was requested but there is no HelloApiToken header"); 83 | context.Response.StatusCode = 401; 84 | return; 85 | } 86 | if (context.Request.Headers["HelloApiToken"] != "MyHelloApiToken") { 87 | Console.WriteLine($"{context.Request.Path} was requested but the HelloApiToken is {context.Request.Headers["HelloApiToken"]} instead of 'MyHelloApiToken'"); 88 | context.Response.StatusCode = 401; 89 | return; 90 | } 91 | await next(); 92 | // Do something after the action executes. 93 | } 94 | } 95 | ``` 96 | 97 | Setting the status code of the response and then returning before calling `next()` prevents the execution of the endpoint. Inheriting from `Attribute` makes it possible to use `HelloHeaderActionFilter` as an attribute, that is an annotation between square brackets on top of a method: 98 | 99 | ```c# 100 | [HelloHeaderActionFilter] 101 | [HttpGet("SayHello")] 102 | public async Task SayHello() => Ok("Hello!"); 103 | ``` 104 | 105 | Doing so, we are telling ASP.Net to execute the `OnActionExecutionAsync` right before the endpoint' method. Unlike the previous middleware implementation though we are only applying this filter to the method it is being attributed to, and not globally. Also, if we change the path via `HttpGet("SayHello")`, then the filter will be still applied correctly, thereby reducing the extra risk of making silly mistakes such as changing the url in the endpoint but not in the middleware. 106 | 107 | ### Options 108 | If we look carefully at the filter implementation, we might notice that something went wrong: we went back to the implementation where the api token is hardcoded, and this is not good. Let's fix this. 109 | 110 | First, we define the `HelloOptions` class: 111 | 112 | ```c# 113 | public class HelloOptions{ 114 | public string ApiToken {get; set; } 115 | } 116 | ``` 117 | 118 | Then we add an instance of these options to the appsettings (it can have any name we want, but in this case we go with `Hello`): 119 | 120 | ```json 121 | "Hello": { 122 | "ApiToken": "e5e6c507-f46c-459f-a40c-15aac76f9638" 123 | } 124 | ``` 125 | 126 | We register in our dependency injection that we want to make these options available. In such a case we speak of a configuration rather than a service: 127 | 128 | ```c# 129 | builder.Services.Configure(builder.Configuration.GetSection("Hello")); 130 | ``` 131 | 132 | Now we can extract an `IOptions` from the services in the attribute as follows: 133 | 134 | ```c# 135 | var helloOptions = 136 | context.RequestServices.GetService>() switch { 137 | { Value: var __ } => __, 138 | _ => new HelloOptions() { ApiToken = Guid.NewGuid().ToString() } 139 | }; 140 | ``` 141 | 142 | > Note that the `{ Value: var __ } => __,` is extracting the `Value` from the `IOption`. If no `HelloOptions` have been correctly registered, we generate a random `Guid` that will certainly not match with any api token provided as a fallback. A random `Guid` is much safer than an empty string, which would match with empty api tokens! 143 | 144 | We top it off by checking the provided header agains `helloOptions.ApiToken`: 145 | 146 | ```c# 147 | if (context.Request.Headers["HelloApiToken"] != helloOptions.ApiToken) { 148 | ``` 149 | 150 | ### CQRS: 151 | - TODO: still to come ..... 152 | 153 | **Exercises** 154 | - Define a middleware that logs all requests to a file; the path of the log file should come from the appsettings; 155 | - Define a middleware which stops all requests to a given endpoint unless the request comes from a given IP; 156 | - Define a middleware which stops all requests to a given endpoint unless the request uses one of a series of username/password combinations provided via basic authentication (the valid username/password combinations should come from the appsettings). 157 | 158 | 159 | **Extra reading materials** 160 | - https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-6.0 161 | - https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-6.0 162 | -------------------------------------------------------------------------------- /24-25/Contents/Ch2Unit5.md: -------------------------------------------------------------------------------- 1 | # Unit 5 - Advanced (hierarchical) data structures 2 | In this unit, we discuss the definition and management of operations around hierarchical data structures. In preivous courses, these data structures were discussed in their mutable version. In this course we explore their immutable implementation, in order to prevent side effects that could happen when updating the references or trying to use `null` values in the mutable implementation. We will start by explaining the implementation of generic trees, and then we will implement boolean and value expression trees, which are a popular data structure used in filters and similar dynamic UI-construction. 3 | 4 | ## Trees 5 | 6 | A tree can be recursively defined as either an empty tree, or a node containing data and a sequence of subtrees (children). This means that its type definition will be both polymorphic and recursive\: 7 | 8 | ```ts 9 | type Tree<'a> = 10 | | { kind:"Empty" } 11 | | { kind:"Node", value:'a, children:List> } 12 | ``` 13 | 14 | Like for lists, we can define `map` and `fold` higher\-order functions, respectively to mutate the content of each node in a tree, and to accumulate the result of a tree operation into an accumulator. 15 | 16 | The `map` function will traverse the tree in some order and apply a function to each node of the tree. We choose to first apply the function to the content of the current node, and then recursively traverse the list of sub\-trees and apply `map` to them. The `map` applied to an empty tree of course results in an empty tree. In the case of a non\-empty tree, we apply `f` to the current element, and then we call the `map` function **for lists** on **the list of subtrees**, by passing a function that calls the **tree map** on each element of the list. We generate a new node by taking the result of `f` applied to the current node and the result of mapping the list of trees with the tree map. 17 | 18 | ```ts 19 | const treeMap = (f : 'a => 'b) => (self:Tree<'a>) => 20 | self.kind == "Empty" ? Tree.Empty() 21 | : Tree.Node(f(self.value), tree.children.map(child => child.map(f))) 22 | ``` 23 | 24 | The `fold` works similarly to its counterpart for lists. It takes as input a function that takes as input a state and an element of the tree, and updates the state. Moreover, we pass to `fold` also the initial value of the state. The function updates the state by calling `f` with the current state and element, thus generating a new state that we call `state1`. It then call `fold` **for lists** passing a function that uses the accumulator and each tree. This function will use `fold` **for trees** to update the accumulator with each subtree. As initial value of the accumulator we pass `state1`, the newly generated state at the current level. 25 | 26 | ```ts 27 | const treeFold = (f : 'state => 'a => 'state) => (state : 'state) => (self:Tree<'a>): 'state => 28 | self.kind == "Empty" ? state 29 | : List.fold( ...treeFold ...) 30 | ``` 31 | 32 | ## Expressions 33 | We conclude the course with a practical example. Consider a frontend application that allows users to edit custom filters on tables. The table itself has a series of columns, which we call "variables" because, well, they vary with every table. Each variable has a type, which can either be primitive (number, bool, date, string, enum) or composite (array, tuple, sum): 34 | 35 | ```ts 36 | type Type = Primitive | Composite 37 | type Primitive = "number" | "boolean" | "date" | "string" 38 | type Composite = { kind:"[]", element:Type } | { kind:"*", first:Type, second:Type } | { kind:"+", first:Type, second:Type } 39 | ``` 40 | 41 | > Tuples and unions are binary operations, for simplicity. We can always represent more elements by nesting. 42 | 43 | We can compose expressions on these types. The simplest expressions are primitives, but of course we can compose multiple expressions together: 44 | 45 | ```ts 46 | type Expression = Primitive | Composite 47 | type Primitive = { kind:"const-number", value:number } 48 | | { kind:"const-bool", value:boolean } 49 | // string, date 50 | | { kind:"[a,b,c]", values:Array } 51 | | { kind:"(a,b)", first:Primitive, second:Primitive } 52 | | { kind:"inl", element:Primitive } | { kind:"inr", element:Primitive } 53 | type Composite = 54 | | { kind:"lookup", variable:string } 55 | | { kind:"->[a,b,c]", values:Array } | { kind:"a[i]", array:Expression, index:Expression } 56 | | { kind:"->(a,b)", first:Expression, second:Expression } | { kind:"fst", tuple:Expression } | { kind:"snd", tuple:Expression } 57 | | { kind:"match", conditional:Expression, onFirst:Expression, onSecond:Expression } | { kind:"->inl", tuple:Expression } | { kind:"->inr", tuple:Expression } 58 | | { kind:"if", conditional:Expression, onThen:Expression, onElse:Expression } 59 | | Algebraic 60 | type Algebraic = 61 | | { kind:"+", first:Expression, second:Expression } 62 | | { kind:"*", first:Expression, second:Expression } 63 | | { kind:"~-", expr:Expression } 64 | | { kind:"-", first:Expression, second:Expression } 65 | | { kind:"/", first:Expression, second:Expression } 66 | | { kind:"&&", first:Expression, second:Expression } 67 | | { kind:"||", first:Expression, second:Expression } 68 | | { kind:"!", expr:Expression } 69 | | { kind:"==", first:Expression, second:Expression } 70 | | { kind:">", first:Expression, second:Expression } 71 | | { kind:">=", first:Expression, second:Expression } 72 | ``` 73 | 74 | There you go! Now we have types and expressions. It is time to produce an evaluator. The evaluator takes as input the current value of the variables (all primitive values, meaning any possible value including arrays, tuples, and unions, but not requiring any further calculation or reduction), an expression to evaluate, and returns either a `Primitive` value or an error: 75 | 76 | 77 | 78 | ```ts 79 | type Scope = { variables:OrderedMap } 80 | const eval = (scope:Scope) => (expr:Expression) : Primtiive | "error" => { 81 | ... 82 | } 83 | ``` 84 | 85 | `eval` will inspect the `expr`, and based on the content determine what to do. Let us show a few cases. 86 | 87 | When the expression is a variable lookup, we try to find it in the scope, and if it is not available we just give an error: 88 | 89 | ```t 90 | if (expr.kind == "lookup") return scope.has(expr.variable) ? scope.get(expr.variable)![1] : "error" 91 | : ... 92 | ``` 93 | 94 | when we encounter a sum, we evaluate both arguments, and if they can be added we add them, otherwise we give an error: 95 | 96 | ```ts 97 | if (expr.kind == "+") { 98 | const first = eval(expr.first) 99 | const second = eval(expr.second) 100 | if (first == "error" || second == "error" || first.kind != second.kind) return "error" 101 | if (first.kind == "const-string") return { kind:"const-string", value:first.value+second.value } 102 | if (first.kind == "const-number") return { kind:"const-number", value:first.value+second.value } 103 | return "error" 104 | } 105 | : ... 106 | ``` 107 | 108 | when we encounter an array lookup, we evaluate the arguments and then try to perform the lookup: 109 | 110 | ```ts 111 | if (expr.kind == "a[i]") { 112 | const array = eval(expr.array) 113 | const index = eval(expr.index) 114 | if (array == "error" || index == "error" || array.kind != "[a,b,c]" || index.kind != "const-number") return "error" 115 | if (array.values.length <= index.value) return "error" 116 | return array.values[index.value] 117 | } 118 | : ... 119 | ``` 120 | 121 | and so on, according to the semantics we are used to. Omitted for brevity, but shown in class. 122 | 123 | 124 | # Exercises 125 | 126 | ## Exercise 1 - very hard, high risk, high reward 127 | Extend our little language with lambda declaration and invocation. You might need to carry the closure inside the lambda expression as a temporary `Scope`, and also deal with shadowing of arguments. 128 | 129 | ## Exercise 2 130 | Extend our little language with records and record lookup. 131 | 132 | ## Exercise 3 133 | Extend our little language with statements, in particular: conditional statement, variable assignment statement, and console printing. 134 | 135 | -------------------------------------------------------------------------------- /24-25/Contents/Ch3Unit6.md: -------------------------------------------------------------------------------- 1 | # Routing 2 | React SPAs are still web applications. Just like web applications have different pages each with its own URL, so does a React SPA. 3 | 4 | SPAs pages are "virtual". Thanks to packages such as React Router we can select which component to render based on the pattern of the URL. 5 | 6 | We start by adding the React Router packages specialized for routing in the browser: 7 | 8 | ``` 9 | "@types/react-router-dom": "^5.3.3", 10 | "react-router-dom": "^6.16.0", 11 | ``` 12 | 13 | We then need to add a `--proxy` command to our backend so that all accesses to `localhost:5000` pages, such as for example `localhost:5000/about-us` or `localhost:5000/products` or `localhost:5000/products/3` are all sent to our React application, which will take care of doing the actual routing: 14 | 15 | ``` 16 | http-server -p 5000 --proxy http://127.0.0.1:5000/? 17 | ``` 18 | 19 | ## Basic routing 20 | Let's start by getting very basic routing going. We will take care of a proper integration with the logic and flow of our application later. Of course first of all we need to import the necessary functions and elements from the React Router Dom library: 21 | 22 | ```tsx 23 | import { createBrowserRouter, createRoutesFromElements, Route, RouterProvider } from "react-router-dom"; 24 | ``` 25 | 26 | We create a browser router (that is a router that works with browser integration by manipulating the url in the url bar) with `createBrowserRouter`. We need to pass it the routes as an argument, which is a series of placeholder `Route` elements that each combine a url pattern (the `path` property) with an element to render when the url pattern is matched. The routes are tested in order, so make sure to resolve possible path overlaps by putting the most specific matches first: 27 | 28 | ```tsx 29 | const router = createBrowserRouter( 30 | createRoutesFromElements( 31 | <> 32 | } /> 33 | } /> 34 | } errorElement={}> 35 | 36 | 37 | ) 38 | ); 39 | ``` 40 | 41 | The last `Route` element also has an `errorElement` specified, which is used in order to provide a fallback renderer for when no path can be matched. It is a very good practice to provide such an `ErrorBoundary` fallback in order to greet your users with a decent page, even if the url they were looking for could not be found. 42 | 43 | Finally, we just render a `RouterProvider` element with the `router` object we just created as parameter: 44 | 45 | ```tsx 46 | root.render( 47 | 48 | ) 49 | ``` 50 | 51 | React Router (Dom) takes care of a few things for us. First of all, it matches the current url from the navigation bar in the browser with the right pattern we provided. It also extracts any dynamic elements, for example the `productId` in url patterns such as `/products/:productId` which can match `/products/1`, `/products/2`, and so on. 52 | 53 | The second thing React Router does for us, is rendering the appropriate element (or the error element when needed). 54 | 55 | ## Dispatching from the `main content` 56 | Great! Now it's time to do this properly for our application. First of all, we are going to get rid of the `page.tsx` file, and we will dispatch from the router to the `MainContent` widget directly. 57 | 58 | We also add two new files with a few new pages. We add the homepage: 59 | 60 | ```tsx 61 | // homepage/homepage.widget.tsx 62 | 63 | import React from "react"; 64 | 65 | export const Homepage = () => <> 66 |

Home

67 |

Welcome to the homepage

68 | ; 69 | ``` 70 | 71 | We also add the no-found error page: 72 | 73 | ```tsx 74 | // not-found-error/not-found-error.widget.tsx 75 | 76 | import React from "react"; 77 | 78 | export const NotFoundError = () => <> 79 |

Error - page not found

80 | ; 81 | ``` 82 | 83 | Most of the routing will be performed by the `main-content` widget, which will also take care of dispatching control to the appropriate sub-components. We add more props and state information next to the main-content state: 84 | 85 | ```ts 86 | // main-content.state.ts 87 | // this will be the parameter received by MainContent from the route 88 | export type PageType = "homepage" | "products" | "product" 89 | // this will be the parameter created by MainContent based on the PageType plus route parsing 90 | export type Route = { kind:"homepage" } | { kind:"products" } | { kind:"product", productId?:string } 91 | ``` 92 | 93 | The structure of the routes we will support is simple: either the homepage, the products overview, or a page with a single product (carrying the `id` of that product as parsed from the url). 94 | 95 | The `main-content` widget takes as input the page type: 96 | 97 | ```tsx 98 | export const MainContent = (props:{ pageType:PageType }) => 99 | ``` 100 | 101 | Based on the page type, a `route:Route` object can be built, reading the parameters from the url thanks to the `useParams` utility: 102 | 103 | ```ts 104 | export const MainContent = (props:{ pageType:PageType }) => 105 | const route:Route = 106 | props.pageType == "product" ? 107 | { kind:props.pageType, ...useParams<"productId">() } 108 | : { kind:props.pageType } 109 | ``` 110 | 111 | > `useParams<"productId">` returns `{ productId?:string }`, which we can embed directly with the spread operator in the `route` object we are constructing. If you want to find out more about this and other Typescript patterns, you can head over to the _Introduction to Typescript_ course on GrandeOmega. 112 | 113 | The renderer of `main-content` is now almost trivially a dispatcher that simply decides which elements vary based on the route: 114 | 115 | ```tsx 116 | <> 117 |
118 | setState(Updaters.currentUser(u))} /> 119 | { 120 | route.kind == "homepage" ? 121 | 122 | : products == "unloaded" ? ProductsUnloadedWidget() 123 | : products == "loading" ? ProductsLoadingWidget() 124 | : products == "API-error" ? 125 | ProductsAPIErrorRetryWidget(state, setState) 126 | : route.kind == "products" ? 127 | ProductsWidget({...state, products}, setState) 128 | : route.productId != undefined && products.has(route.productId) ? 129 | {}} deleteProduct={() => {}}/> 130 | : 131 | } 132 |