From 35da3b7166c983f90a0fe1e0b2b86a0b995d2232 Mon Sep 17 00:00:00 2001 From: Mateusz Gienieczko Date: Thu, 19 Oct 2023 15:18:30 +0100 Subject: [PATCH] Replace a bunch of Paragraph and Header usages. --- src/Sorcery/Pages/AboutMe.razor | 12 +- .../SimdCheatCodesForFreePerformance.razor | 7 +- .../CSharp/00-Basics/00-DotnetTaxonomy.razor | 190 +++--- .../01-ConfiguringYourEnvironment.razor | 58 +- .../CSharp/00-Basics/02-HelloWorld.razor | 83 ++- .../CSharp/00-Basics/03-BasicTypes.razor | 6 +- .../CSharp/00-Basics/04-ControlFlow.razor | 6 +- .../Teaching/CSharp/00-Basics/05-Arrays.razor | 6 +- .../CSharp/00-Basics/Introduction.razor | 4 +- .../01-ObjectOrientation/00-Classes.razor | 116 ++-- .../01-ObjectOrientation/01-inheritance.razor | 6 +- .../02-AbstractTypes.razor | 44 +- .../01-ObjectOrientation/03-Strings.razor | 6 +- .../01-ObjectOrientation/04-Attributes.razor | 6 +- .../05-TestingWithXUnit.razor | 280 +++++---- .../06-UsingGitHubClassroom.razor | 58 +- .../Assignment-DungeonWalker.razor | 584 +++++++++--------- .../01-ObjectOrientation/Introduction.razor | 8 +- .../02-ReferencesAndValues/00-Memory.razor | 98 ++- .../01-ReferenceTypesAndValueTypes.razor | 6 +- .../02-OperatorOverloading.razor | 6 +- .../03-Exceptions.razor | 2 +- .../04-Nullability.razor | 6 +- .../02-ReferencesAndValues/05-Casting.razor | 6 +- .../06-PassByReference.razor | 6 +- .../Assignment-LustrousLoot.razor | 195 +++--- .../02-ReferencesAndValues/Introduction.razor | 4 +- .../00-Indexers.razor | 2 +- .../01-Generics.razor | 2 +- .../02-Equality.razor | 2 +- .../03-Ordering.razor | 2 +- .../04-ExplicitInterfaceImplementations.razor | 2 +- .../05-Collections.razor | 2 +- .../06-Comparers.razor | 2 +- .../03-GenericsAndCollections/07-Tuples.razor | 2 +- .../08-Deconstruction.razor | 2 +- .../09-NestedTypes.razor | 2 +- .../10-HashCodeAsAMutableStruct.razor | 2 +- .../11-Records.razor | 2 +- .../12-PatternMatching.razor | 2 +- .../Assignment-DivergingDungeons.razor | 60 +- .../CSharp/04-LINQ/00-GenericVariance.razor | 2 +- .../CSharp/04-LINQ/01-Iterators.razor | 2 +- .../CSharp/04-LINQ/02-ExtensionMethods.razor | 2 +- .../CSharp/04-LINQ/03-AnonymousTypes.razor | 2 +- .../CSharp/04-LINQ/04-Delegates.razor | 2 +- .../CSharp/04-LINQ/05-LambdaExpressions.razor | 2 +- .../CSharp/04-LINQ/06-LinqQueries.razor | 2 +- .../CSharp/04-LINQ/07-LocalMethods.razor | 2 +- .../04-LINQ/Assignment-LayeredLayouts.razor | 54 +- .../05-Asynchrony/00-SimpleThreading.razor | 2 +- .../CSharp/05-Asynchrony/01-Events.razor | 2 +- .../05-Asynchrony/02-ExceptionHandling.razor | 2 +- .../03-DisposableResources.razor | 2 +- .../05-Asynchrony/04-AsyncAndAwait.razor | 2 +- .../CSharp/05-Asynchrony/05-ThreadPool.razor | 2 +- .../05-Asynchrony/06-Cancellation.razor | 2 +- .../07-AsyncInterfacesAndValueTask.razor | 2 +- .../Assignment-PersistedPathways.razor | 124 ++-- .../00-ExpressionTrees.razor | 2 +- .../06-EntityFramework/01-LinqToSql.razor | 2 +- .../02-NavigationProperties.razor | 2 +- .../06-EntityFramework/03-Inheritance.razor | 2 +- .../06-EntityFramework/04-Tracking.razor | 2 +- .../06-EntityFramework/05-Transactions.razor | 2 +- .../Assignment-EldritchEntities.razor | 180 +++--- .../07-ASP.NETCore/00-MinimalHttpServer.razor | 163 +++-- .../01-DependencyInjection.razor | 220 ++++--- .../CSharp/07-ASP.NETCore/02-Swagger.razor | 32 +- .../07-ASP.NETCore/03-ModelBinding.razor | 60 +- .../CSharp/07-ASP.NETCore/04-Logging.razor | 161 +++-- ...gnment-AdventureProgrammingInterface.razor | 36 +- .../CSharp/07-ASP.NETCore/Introduction.razor | 12 +- .../08-Reflection/00-TypesAndInstances.razor | 2 +- .../CSharp/08-Reflection/01-Members.razor | 2 +- .../CSharp/08-Reflection/02-Attributes.razor | 2 +- .../08-Reflection/03-SystemAttributes.razor | 2 +- .../CSharp/08-Reflection/04-Dynamic.razor | 2 +- .../05-AdvancedUnitTesting.razor | 14 +- .../Assignment-AdventurersAssemble.razor | 164 +++-- .../00-GarbageCollectionDetails.razor | 6 +- .../09-Performance/01-Benchmarking.razor | 12 +- .../CSharp/09-Performance/03-RefStructs.razor | 2 +- .../09-Performance/04-TheInModifier.razor | 2 +- .../09-Performance/05-ArrayPooling.razor | 2 +- .../CSharp/09-Performance/06-Unsafe.razor | 2 +- .../CSharp/09-Performance/07-Finalizers.razor | 2 +- .../Assignment-RapidReconnaissance.razor | 115 ++-- .../10-Concurrency/00-Synchronisation.razor | 2 +- .../01-ConcurrenctCollections.razor | 2 +- .../CSharp/10-Concurrency/02-Plinq.razor | 2 +- .../CSharp/10-Concurrency/03-Channels.razor | 2 +- .../Assignment-SimultaneousSimulations.razor | 34 +- .../CSharp/11-Blazor/00-Summary.razor | 12 +- .../CSharp/11-Blazor/01-HostedServices.razor | 12 +- .../CSharp/11-Blazor/02-SignalR.razor | 18 +- .../CSharp/11-Blazor/Assignment-Elector.razor | 216 +++---- .../Pages/Teaching/CSharp/Introduction.razor | 64 +- src/Sorcery/Pages/Teaching/Teaching.razor | 13 +- src/Sorcery/Pages/Todo.razor | 20 +- .../Components/Blogging/BlogPostCard.razor | 2 +- .../ModularCourse/CourseAssignment.razor | 3 +- .../ModularCourse/CourseSection.razor | 3 +- 103 files changed, 1842 insertions(+), 1875 deletions(-) diff --git a/src/Sorcery/Pages/AboutMe.razor b/src/Sorcery/Pages/AboutMe.razor index 964784b..fae5107 100644 --- a/src/Sorcery/Pages/AboutMe.razor +++ b/src/Sorcery/Pages/AboutMe.razor @@ -33,7 +33,7 @@ that the comments on this site are not mine, but I do moderate them. - + I'm currently working in Microsoft Ireland in the Identity team. Graduated with a Master's in Computer Science at the University of Warsaw, and taught a course on C#.NET, available on this page. @@ -41,13 +41,13 @@ and formal logic. You can find my CV here. My Master's was extended and published at ASPLOS'24 as Supporting Descendants in SIMD-Accelerated JSONPath. - + - + You can contact me at String.Format("{0}@@{1}.com", "mat", "gienieczko"). - + - Here's a gist in the form of a little timeline: + Here's a gist in the form of a little timeline: @@ -90,7 +90,7 @@ Master's in Computer Science Thesis: rapid execution of queries on JSON documents with SIMD. - Available here. + Available here. diff --git a/src/Sorcery/Pages/Sourcery/Posts/SimdCheatCodesForFreePerformance.razor b/src/Sorcery/Pages/Sourcery/Posts/SimdCheatCodesForFreePerformance.razor index 0fcd099..1a2d5b5 100644 --- a/src/Sorcery/Pages/Sourcery/Posts/SimdCheatCodesForFreePerformance.razor +++ b/src/Sorcery/Pages/Sourcery/Posts/SimdCheatCodesForFreePerformance.razor @@ -10,8 +10,8 @@ Modern CPUs have special instructions for a more complex mode of execution, together with separate registers for those instructions only. These are cheat codes - that compilers and library developers use to get massive performance gains without - fundamentally changing the algorithm being executed. + that compilers and library developers use to get massive performance gains + without fundamentally changing the algorithm being executed. }; } @@ -689,7 +689,8 @@ private int? Simd128Portable(ReadOnlySpan sensor1, ReadOnlySpan sens - We will discuss juicy details in the next part. To not miss it, subscribe to the RSS feed of Sourcery! + We will discuss juicy details in the next part. To not miss it, subscribe to the RSS feed of Sourcery, + or watch the Discussions in the GitHub repo! diff --git a/src/Sorcery/Pages/Teaching/CSharp/00-Basics/00-DotnetTaxonomy.razor b/src/Sorcery/Pages/Teaching/CSharp/00-Basics/00-DotnetTaxonomy.razor index e8fec3b..abc56c0 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/00-Basics/00-DotnetTaxonomy.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/00-Basics/00-DotnetTaxonomy.razor @@ -2,63 +2,62 @@ @inject CourseBook CourseBook; - + Here we go! This introduction is going to be a little verbose. I will limit the prose in the following sections, but I feel like at the beginning a more wordy sales pitch is needed. - - C# - - - - - - - - - - - - - - + + C# + + + + + + + + + + + + + C# (current version 10, as of this writing) – language designed in 2000 by a team led by Anders Hejlsberg for Microsoft. He's the madlad that also came up with TypeScript, Delphi, and authored Turbo Pascal. - - + + Wikipedia lists C#'s programming paradigms as: - + structured, imperative, object-oriented, event-driven, task-driven, functional, generic, reflective, concurrent - + ... which kinda makes you wonder about the usefulness of such a classification. I'm gonna sell you my take on C#. C#'s main paradigm is pragmatism. It is oriented towards safety and developer productivity and characterised by: - + memory safety; type safety and static type analysis; flexibility – you get a box of ergonomic tools that allow you to solve your problem efficiently... ... but designed so that you won't shoot yourself in the foot in the process. - + That is, until we decide we want to get rid of that with dynamic or unsafe, but that will be tackled - at the very end of the course - - + at the very end of the course + + C# is most often compared to Java, which is due mostly to historical reasons, but also a similar approach to object orientation. However, ever since C# 2.0 was released in 2005, this comparison stopped being accurate, as the languages diverged in their evolution. We will be highlighting those similarities and differences throughout the course, but it is important to remember that C# generously borrows from a lot of different languages. It uses the cumulative knowledge we have in programming language design to improve developer's experience. The design principle of allowing the user to achieve anything and everything in a safe, productive, and easy manner will become apparent as we introduce more and more features. - - + + This is C#. Say hello! - + - .NET + .NET @@ -66,7 +65,7 @@ - This is dotnet-bot, the .NET mascot. + This is dotnet-bot, the .NET mascot. @@ -80,12 +79,12 @@ - - + + .NET is a free, cross-platform and open source ecosystem for developers to build modern applications. It's an umbrella term for a whole host of technologies, allowing you to build websites, servers, console apps, mobile apps targeting Android or iOS. The current version as of writing is .NET 6. - - + + Most important part (for us at this moment) is the .NET runtime, or Common Language Runtime (CLR). What's that? @@ -100,58 +99,57 @@ Hey, would you look at that, that's also C#'s goal! - - + + C# is just one of the languages running on the CLR. The other notable one is Visual Basic, but the interesting one is F#. It is an ML-like functional language for .NET. You might find it familiar if you ever programmed in ML or OCaml. - - + + To host multiple languages, the CLR runs bytecode that's called, extremely creatively, Common Intermediate Code (CIL/IL). This code is what a compiler for C# or F# generates. You will never have to look at IL unless you're digging in the compiler, trying to super-optimise something, or are just curious. And we're all curious, right? It looks like this: - + $' -extends [System.Private.CoreLib]System.Object -{ -.method private hidebysig static - void '
$' ( - string[] args - ) cil managed - { - .maxstack 8 - .entrypoint - - IL_0000: ldstr ""Hello, World!"" - IL_0005: call void [System.Console]System.Console::WriteLine(string) - IL_000a: ret - } -} - ")/> - + .class private auto ansi abstract sealed beforefieldinit '$' + extends [System.Private.CoreLib]System.Object + { + .method private hidebysig static + void '
$' ( + string[] args + ) cil managed + { + .maxstack 8 + .entrypoint + IL_0000: ldstr ""Hello, World!"" + IL_0005: call void [System.Console]System.Console::WriteLine(string) + IL_000a: ret + } + } + ") /> + That is what the compiler generates for the C# Hello World program from above. Well, more or less, I had to lie cause there's a lot of additional metadata in the actual output. By the way – I will often lie during the course to simplify things, but don't worry, I'll always tell you when it happens. I'm a terrible liar. - - + + Code running under control of the CLR is also called managed code. In general, the word managed in .NET world means "under control of the CLR", and unmanaged means everything else, so for example system code somewhere in the kernel that runs when we try to print to console is unmanaged. - - - There are other implementations of CLR than .NET. The notable ones are .NET Framework and the Mono runtime. + + + There are other implementations of CLR than .NET. The notable ones are .NET Framework and the Mono runtime. .NET Framework is the old .NET that was specific to Windows and closed-source. It is no longer in development, only maintained. Before version 5, .NET was called .NET Core to distinguish it from .NET Framework. Ever since then, all of those projects are unified under the one .NET umbrella. - - + + For cross-compatibility it is important to note that the CLR has an accompanying standard, called .NET Standard. It defines the minimum runtime capabilities that all .NET implementations need to have, so if you use only .NET Standard your app is guaranteed to run on all .NET Framework, Mono and future .NET releases. - + - Base Class Library (BCL) + Base Class Library (BCL) @@ -172,51 +170,51 @@ extends [System.Private.CoreLib]System.Object - + The Base Class Library is similar to C++'s or Rust's std library. It contains the most foundational types and utilities for all other .NET libraries. Things like arrays, base collections, DateTime, TCP and HTTP clients, and many more. - - + + The BCL is open source and imposes very high quality standards. As with all good standard libraries, it is highly unlikely that you can write custom code that outperforms an equivalent method in the BCL. The BCL is also a very good standard for designing good APIs, so we will be often using examples from the BCL when talking about idiomatic C# class design. - - + + BCL is specific to the particular .NET implementation, but most of it is part of .NET Standard. That means that every existing .NET implementation shares them, so if you ever need to work on .NET Framework or Mono the library will be the same when it comes to this core components. When talking about BCL we will be implicitly talking about the BCL, so its implementation for .NET 6. - - Roslyn + + Roslyn - + - + - + - + Roslyn is the main compiler platform for C# (and Visual Basic). It's open-source and ships with .NET by default. One of its core features is that it's not only a compiler, but also a platform that allows other code to interface with the compilation process, look at the syntax tree of the program, and receive and add diagnostics. This allows creating static analysers, called Roslyn analysers, which allows you to add your own errors, warnings, and quick-fixes for them. This allows integrating your libraries into the language more, as you can detect common mistakes and give users first-class diagnostic messages. - - + + Moreover, it also allows you to write an analyser that enforces rules that are specific for your project. In larger projects with many developers there are often implicit usage rules that everyone is expected to follow. - Maybe some method is not supposed to be used in some context, or some pattern makes no sense. + Maybe some method is not supposed to be used in some context, or some pattern makes no sense. Normally there's a note about in the documentation, or the project wiki, or in a comment somewhere. With Roslyn, you can write an analyser that will automatically mark such cases as warnings. - - NuGet + + NuGet @@ -229,33 +227,33 @@ extends [System.Private.CoreLib]System.Object - + NuGet is the package manager for .NET. It provides the central repository for all the packages in the .NET world. Anyone can push their own package there and share it with the community. There are also ways of creating private NuGet repositories, which is useful when you want to reuse code across your organisation without making it open-source. - - + + .NET packages follow Semantic Versioning (SemVer), which makes updates rather painless. Of course, it's not really a statically enforcible standard, so some packages might violate that, but that's semver for you. Turning your library into a package is actually pretty easy and simply requires the developer to associate some metadata with the binaries that they create. - - Summary - + + Summary + .NET is an environment for developing modern applications in a number of languages targeting .NET. It provides memory safety, a unified type system and a robust standard library – the BCL. C# is compiled with Roslyn into bytecode called IL and then executed on the CLR. Package management and distribution is done with NuGet. - + + ("https://docs.microsoft.com/en-us/dotnet/core/introduction", "Introduction to .NET on docs.microsoft"), + ("https://github.com/dotnet/csharplang/", "dotnet/csharplang, the official repository for C# language design"), + ("https://github.com/dotnet/runtime", "dotnet/runtime, the official repository for the .NET runtime"), + ("https://github.com/dotnet/roslyn", "dotnet/roslyn, the official repository of the Roslyn compiler"), + ("https://www.nuget.org/", "NuGet Gallery"), + }) /> @code { diff --git a/src/Sorcery/Pages/Teaching/CSharp/00-Basics/01-ConfiguringYourEnvironment.razor b/src/Sorcery/Pages/Teaching/CSharp/00-Basics/01-ConfiguringYourEnvironment.razor index cb3e6be..ff3f829 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/00-Basics/01-ConfiguringYourEnvironment.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/00-Basics/01-ConfiguringYourEnvironment.razor @@ -2,11 +2,11 @@ @inject CourseBook CourseBook; - + Before we start playing with C# we need to first get all the required components. The guide will assume you're using either Windows or Linux. As for MacOS, I don't own one, so I wouldn't be able to test it. Use the resources at the bottom if you're struggling. - + Note: this course uses .NET 6. While .NET is backwards-compatible for the most part, keep that in mind if a newer version is already out. It is guaranteed to be incompatible with .NET 5 and .NET Core. @@ -16,25 +16,25 @@ You still need .NET 6 for the code, but the interactive runtime itself needs 5. This amounts to downloading the other binary or running apt install dotnet-sdk-5.0. - .NET, i.e. dotnet - + .NET, i.e. dotnet + The .NET installation instructions are detailed and guaranteed to be up-to-date, so I won't copy most of it. - - Windows - + + Windows + Go to https://dotnet.microsoft.com/en-us/download and download the .NET SDK for your platform. If you don't have your shell environment configured on Windows, I recommend grabbing Windows Terminal. It will make your life much easier. If I ever show commandline in the tutorial, it will be from Linux via WSL2, but only because Asciinema only works there. However, all dotnet commands work the same on both platforms. - - Linux - + + Linux + Navigate to https://docs.microsoft.com/en-gb/dotnet/core/install/linux for installation instructions on Linux. If you're using Ubuntu, Debian or something else that manages their dependencies through apt-get then follow me below. If not you're on your own, but you're using Linux, so you should be used to that. - - + + We just need to cast a simple spell and we're golden: path, without a package manager. The best way to do that is the dotnet-install.sh script. - - Verifying - + + Verifying + To verify that dotnet is correctly installed, run dotnet --info. It should look something like this: - - Visual Studio Code - + + Visual Studio Code + We will be using VS Code for interactive notebooks and GitHub Classroom integration. If you already have Code configured, check the extension list to make sure you have them all. - - + + To install Code, you go to https://code.visualstudio.com/Download and select your platform. Once it's installed you should be able to run it from your shell using code ⟨dir⟩ to open it in the specified directory. - - + + Here are the extensions that we need to install in Code: @@ -90,8 +90,8 @@ sudo apt-get install -y dotnet-sdk-6.0")/> – for browsing and installing C# libraries. - - + + There are also a few very helpful extensions that you might want to install if you haven't already: @@ -109,16 +109,14 @@ sudo apt-get install -y dotnet-sdk-6.0")/> – so that your final project doesn't contain those emberesing speling miskates. - - There's one more step left for configuring C# – go to the C#'s extension settings, search for Semantic Highlighting, and turn it on. Without it, syntax highlighting can get borked with some of the new language features. - - Summary - + + Summary + Everything's set up, let's write and run some code! - + - + We'll learn how to build and run a "Hello, World!" program before jumping to the main portion of the course in interactive notebooks. - - Structure of a program - + + Structure of a program + C# projects consist of two core parts: the sources in .cs files, and the project configuration in a .csproj file. If you know Rust, this is very similar to having your sources in .rs and a configuration in Cargo.toml. You usually won't define .csproj yourself, but we will - be adding some stuff there on occasion. - - + be adding some stuff there on occasion. + + The .csproj defines what SDK to use for building your app, which .NET version you are targeting, compilation flags that enable language features, dependencies on other projects, and all the external packages you depend on. Applications usually consist of a few separate projects that depend on each other. - - + + Each project is a single, contained compilation unit, called an assembly. - The usual output is a .dll file that contains all the compiled IL + The usual output is a .dll file that contains all the compiled IL and additional package metadata for the project. The .dll is fully portable – you can move it to a different machine running a different operating system with a completely different CPU architecture and it will still work. If your project contained an entry point, you can execute the .dll with dotnet run. It is also possible to compile into an executable, but that requires you to specify the platform and is not portable. If the project doesn't have an entry point, it's just a library that can be used from other code. - - The classic - + + The classic + .NET comes preinstalled with a handful of templates for different kinds of applications. The most basic one is console. You use dotnet new to create a fresh project from a template, passing the name of the project with the --name parameter. - + This creates a new directory called HelloWorld and a project with this familiar code inside: +Console.WriteLine(""Hello, World!"");")" /> You can run it with dotnet build and then dotnet run. @@ -46,29 +46,26 @@ Console.WriteLine(""Hello, World!"");")"/> - - Smoke, mirrors, and sorcery - + + Smoke, mirrors, and sorcery + Modern programming languages are complex enough to be indistinguishable from magic (I said modern, C doesn't count). In the one line of code above a lot of magic happens. Let's lift the curtain a bit: the code above is basically equivalent to the following, also valid, C# code: + using System; + namespace HelloWorld; + internal class Program + { + static void Main(string[] args) + { + Console.WriteLine(""Hello, World!""); + } + } + ")> Let's go through that line by line. - - + + The first line is what we call a using directive. It tells the compiler to bring into scope @@ -111,29 +108,29 @@ internal class Program - + So there, normally you would have to write all that code to get something printed on screen, but C# allows top-level statements, which means that instead of writing a Main method you can write your code script-like, and the entry point will be synthesised for you. Only one file in a project can have top-level statements, as every assembly can have at most one entry point. - - + + One notable lie here is that the actual names generated are not Program or Main, but rather magical names that only the compiler knows – such names are called unspeakable, because you can't refer to them in regular user code. Get used to that, the C# compiler is a certified level 20 Wizard. But worry not! We will make its magic clear, in time. Well, most of it. - - Summary - + + Summary + A single compilation unit in .NET is called an assembly. Instead of a traditional Main method, you can write the entry code using top-level statements. In C#, methods are contained within types, and types are grouped into namespaces. You can bring types into scope from different namespaces with a using directive. To write things to standard output we use System.Console.Write or System.Console.WriteLine. - + + ("https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/top-level-statements", "Top-level statements on docs.microsoft"), + ("https://sharplab.io/", "SharpLab, .NET playground with a desugariser and a decompiler"), + }) /> @code { diff --git a/src/Sorcery/Pages/Teaching/CSharp/00-Basics/03-BasicTypes.razor b/src/Sorcery/Pages/Teaching/CSharp/00-Basics/03-BasicTypes.razor index 3ad9ebc..c7354d8 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/00-Basics/03-BasicTypes.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/00-Basics/03-BasicTypes.razor @@ -6,11 +6,11 @@ } - + Download the notebooks repository and go through the first notebook, @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"), in VS Code. - - In this section we'll cover + + In this section we'll cover diff --git a/src/Sorcery/Pages/Teaching/CSharp/00-Basics/04-ControlFlow.razor b/src/Sorcery/Pages/Teaching/CSharp/00-Basics/04-ControlFlow.razor index 1a7b6b5..e3d450a 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/00-Basics/04-ControlFlow.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/00-Basics/04-ControlFlow.razor @@ -6,11 +6,11 @@ } - + Continuing with the notebooks repository go through the next notebook, @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"), in VS Code. - - In this section we'll cover + + In this section we'll cover diff --git a/src/Sorcery/Pages/Teaching/CSharp/00-Basics/05-Arrays.razor b/src/Sorcery/Pages/Teaching/CSharp/00-Basics/05-Arrays.razor index 8cf74ad..7ec284b 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/00-Basics/05-Arrays.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/00-Basics/05-Arrays.razor @@ -6,11 +6,11 @@ } - + Continuing with the notebooks repository go through the next notebook, @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"), in VS Code. - - In this section we'll cover + + In this section we'll cover diff --git a/src/Sorcery/Pages/Teaching/CSharp/00-Basics/Introduction.razor b/src/Sorcery/Pages/Teaching/CSharp/00-Basics/Introduction.razor index c368587..b5eddb7 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/00-Basics/Introduction.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/00-Basics/Introduction.razor @@ -2,9 +2,9 @@ @inject CourseBook CourseBook; - + We introduce the basics of programming in C# using interactive notebooks. In this module we will learn: - + what is C#, .NET, and all those others acronyms; diff --git a/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/00-Classes.razor b/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/00-Classes.razor index d1b5534..ed91add 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/00-Classes.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/00-Classes.razor @@ -6,66 +6,66 @@ } - + Continuing with the notebooks repository go through the next notebook, @($"{section.Module.Id}-{section.Module.RouteName}/{section.Id}-{section.RouteName}.dib"), in VS Code. - - In this section we'll cover - - - - - Access modifiers. - - - - - Classes. - - - - - Fields and readonly. - - - - - Methods. - - - - - Properties. - - - - - Constructors. - - - - - Initialisers. - - - - - Target typed new. - - - - - Overloading methods. - - - - - Static members. - - - - + + In this section we'll cover -@code { + + + + Access modifiers. + + + + + Classes. + + + + + Fields and readonly. + + + + + Methods. + + + + + Properties. + + + + + Constructors. + + + + + Initialisers. + + + + + Target typed new. + + + + + Overloading methods. + + + + + Static members. + + + + + + @code { } diff --git a/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/01-inheritance.razor b/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/01-inheritance.razor index e2f944d..b2fff30 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/01-inheritance.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/01-inheritance.razor @@ -6,11 +6,11 @@ } - + Continuing with the notebooks repository go through the next notebook, @($"{section.Module.Id}-{section.Module.RouteName}/{section.Id}-{section.RouteName}.dib"), in VS Code. - - In this section we'll cover + + In this section we'll cover diff --git a/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/02-AbstractTypes.razor b/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/02-AbstractTypes.razor index be0b3e0..08c2418 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/02-AbstractTypes.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/02-AbstractTypes.razor @@ -6,30 +6,30 @@ } - + Continuing with the notebooks repository go through the next notebook, @($"{section.Module.Id}-{section.Module.RouteName}/{section.Id}-{section.RouteName}.dib"), in VS Code. - - In this section we'll cover - - - - Abstract classes. - - - - - Interfaces. - - - - - Static classes. - - - - + + In this section we'll cover + + + + Abstract classes. + + + + + Interfaces. + + + + + Static classes. + + + + -@code { + @code { } diff --git a/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/03-Strings.razor b/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/03-Strings.razor index fc07583..623375e 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/03-Strings.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/03-Strings.razor @@ -6,11 +6,11 @@ } - + Continuing with the notebooks repository go through the next notebook, @($"{section.Module.Id}-{section.Module.RouteName}/{section.Id}-{section.RouteName}.dib"), in VS Code. - - In this section we'll cover + + In this section we'll cover diff --git a/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/04-Attributes.razor b/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/04-Attributes.razor index 14b4027..42f6637 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/04-Attributes.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/04-Attributes.razor @@ -6,11 +6,11 @@ } - + Continuing with the notebooks repository go through the next notebook, @($"{section.Module.Id}-{section.Module.RouteName}/{section.Id}-{section.RouteName}.dib"), in VS Code. - - In this section we'll cover + + In this section we'll cover diff --git a/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/05-TestingWithXUnit.razor b/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/05-TestingWithXUnit.razor index 42e0381..57d3610 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/05-TestingWithXUnit.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/05-TestingWithXUnit.razor @@ -6,75 +6,71 @@ } - + We won't be using a notebook for this one, as we need an actual project to run the tests. In .NET there are three major test frameworks: - - - MSUnit - - - - - NUnit - - - - - xUnit - - - We're going to use xUnit, because it's pretty much the simplest one of the bunch - and improves on some issues of NUnit, like test isolation and throw assertions. - NUnit is also a very popular choice, though. - - - Unit testing + + + MSUnit + + + + + NUnit + + + + + xUnit + + + We're going to use xUnit, because it's pretty much the simplest one of the bunch + and improves on some issues of NUnit, like test isolation and throw assertions. + NUnit is also a very popular choice, though. + + + Unit testing The idea of unit testing is fairly simple – we take single units of our production code, isolate them out, and comprehensively test. In C# we have two obvious units to test, classes and methods. It's best to isolate individual methods, but sometimes it's impossible to really test a scenario without calling more than one in sequence. - + C# also lends itself pretty well to Test Driven Development, which is a workflow where you repeatedly alternate between two modes: - - - - - write new unit tests until they stop passing; - - - - - write production code until the existing unit tests all pass. - - - - + + + + write new unit tests until they stop passing; + + + + + write production code until the existing unit tests all pass. + + + + + Implementing well-defined features with this workflow is extremely efficient. IDEs even have special switches that allow you to continuously run unit tests when you change your code, so the feedback loops is very tight. - - In assignments we will already have automated tests running for us, but it might be beneficial to write your own, additional tests to make sure your code works as you think it does. - - - Project setup - + + + Project setup + We're going to create a very simple project and then test it with xUnit. To start, create a new library project. I'll call it Testing. A class library is a project without an entry point, so it can define classes for other project to use, but is not runnable. - - - + We're going to create a simple Modulo(int x, int m) method that works as follows: - + @@ -92,34 +88,34 @@ - - + + Let's play around with TDD. In our library we put only a stub Calculator class (you can rename and replace the existing Class1.cs file): - - + 0; -}")"/> - +}")" /> + Now we create a test project. In C#, all tests are written in a special project separate from the code being tested. The convention is for every actual project to have an accompanying test project. My convention is to name it the same as the tested project, but with .UnitTests suffixed. We will be using the xUnit test framework, so create a new xUnit project: - + - + As is, our projects don't know about themselves. To be able to reference the code under test from the test project we need to add a reference to the Testing project. This can also be done from the CLI: - + - Anatomy of a .csproj - + Anatomy of a .csproj + It might be a good time to take a look at the configuration in our .csproj. - + @@ -130,16 +126,16 @@ public sealed class Calculator -")"/> - +")" /> + Not much happening here, it's the simplest possible csproj for a library. We specify that we're using .NET 6.0 and a feature called implicit usings, which brings some core namespaces into scope automatically for us, e.g. System. We also enable Nullable Reference Types, which we will cover in-depth in the next module. - - + + What about the test project? - + @@ -168,53 +164,53 @@ public sealed class Calculator -")"/> - +")" /> + Much more fun here. It starts similarly, although implicit usings are disabled here by default for reasons that elude me. The IsPackable bit means that if we were to publish a NuGet package from our library we don't want it to include this particular project, which makes sense – we don't need users to get our, perhaps large, test projects. - - + + Then on lines 10-21 we have a group of PackageReferences. These are external references to packages on NuGet, the package manager mentioned in @CourseBook.CSharpCourse["basics"]["dotnet-taxonomy"].DisplayName. We include the base .NET Test SDK that all testing libraries use, the xUnit package itself, its runner for the Visual Studio IDE integration and Coverlet, which is the default test coverage library for .NET. As you can see, in .NET we reference external packages by name and its exact version. - - + + Finally, at line 24 we have a local reference to the Testing project, which is located next to us. This is the line added by the dotnet add command we executed. - - Solution files - + + Solution files + When working with multiple projects it's useful to organise them together using a solution file. A solution file is a special metadata file with the .sln extension that tells our IDE and the dotnet CLI that the given assemblies are all part of a single conceptual project. We can create a solution file with: - + - + This creates an empty solution file, so we need to add our projects there: - + - + There's no real reason to manually inspect a solution file, it mostly just associates unique identifiers with all projects. The benefit of a solution file is that if we run a dotnet command like test the CLI will just run all test projects in the solution; without it, we'd have to manually list them in the command. - + - xUnit Facts - + xUnit Facts + Okay, let's write some tests. We'll start off with a simple Fact, which is xUnit's lingo for a parameterless test. It's good for unit tests to resemble the project under test as close as possible in terms of namespace structure, to make navigation easier. We create a CalculatorUnitTests.cs file and write our first Fact (you can rename and replace the existing UnitTest1.cs file): - + +")" /> - + We first bring our core project and the test framework stuff into scope. Then we declare a class that will hold the unit tests. The xUnit runner initialises this class independently before each test, so you can put common setup into its constructor, or define some fields on it that will be reinitialised before every test. - - + + Our test method is annotated with the FactAttribute that tells xUnit that it's actually a test that it should run, and that it has no parameters. We then write the test, following the Arrange-Act-Assert pattern, which is a useful template for writing tests that makes their logic clear to follow. - + @@ -265,23 +261,23 @@ public class CalculatorUnitTests - + In our case, we instantiate the class, perform the operation, and then use the static Assert class provided by xUnit to make sure the two are equal. The semantics of Equal are that we ought to provide the expected value as the first argument, and the actual value as the second. - - + + And... that's it! Our stub implementation should actually pass this test, as it always returns zero. Fire up the test and check: - + - - + + Okay, we should probably make a test that actually fails with our code. - + +")" /> - + Okay, now it's time to make our code work with the new test. - - + x % m; -")"/> +")" /> - + Oh, it broke the other test. We cannot modulo by zero and I told you that our method must return zero in such a case so that I didn't have to explain throwing exceptions in this module. Right. - - + m == 0 ? 0 : x % m; -")"/> +")" /> - xUnit Theories - + xUnit Theories + We're testing two values, but it'd be nice to have more. We don't really want to duplicate the entire method and just change the three numbers, instead we should make a parametrised test, which in xUnit is called a Theory. - - + - +")" /> + We change the attribute to TheoryAttribute, give the method parameters that we need, and then we can feed it values with the InlineDataAttribute. Now we can add more tests by just adding more data: - - + - +")" /> + If we run the tests now we'll see that xUnit creates a separate test for each set of parameters: - + - + Now we can add tests for the both-negative case: - + +")" /> - + Would you look at that, they all pass! So these are actually the semantics of C#'s % operator. We have some small code duplication here, so let's alleviate that and also add the other cases to our tests. - + - + + + Complex Theory data - Complex Theory data - - + I have to admit, that code isn't the cleanest. We have an obvious duplication where we test the code with positive and negative modulo for the same absolute values and expect the same result. Also, what if we wanted to generate tests? Say I wanted to test that for all numbers from $1$ to $1000$ it holds that $x - 1 \equiv x - 1 \mod x$. We know we cannot really do that from @CourseBook.CSharpCourse["object-orientation"]["attributes"].DisplayName, as attributes only accept compile-time constants as arguments. Well, we can, but copy-pasting a thousand lines of code is not really considered best practice. - - + + If we want test data that is not compile-time constant we can use the MemberDataAttribute, which takes a name of a property of the test class which returns an object of a special TheoryData type. Now, there's a bit of magic here that we don't quite understand yet – that type is generic. We will talk about generics in the next module, but we simply cannot escape from them here. In short, it means that we can have data for tests for various types of parameters. Let's write logic that will generate the appropriate data for us: - - + + ModuloTestData { @@ -493,11 +489,11 @@ public static TheoryData ModuloTestData return data; } }")"> - - + + Now we can also write that additional test I mentioned, as well as generate cases for the Modulo_GivenZeroAsModulus_ReturnsZero method. - + SmallNumbersData { @@ -514,10 +510,10 @@ public static TheoryData SmallNumbersData } }")"> - + Bringing it all together: - - + - - + + We generate over two thousand test cases, and all in under a hundred lines of code! - - Summary - + + Summary + Throughout the course we will be using xUnit as our test framework. To write tests we need a special type of a project that references other projects. We run tests with dotnet test. xUnit allows you to test other projects with Facts and Theories. You can provide constant data to Theories with the InlineDataAttribute, and complex, non-constant data with the MemberDataAttribute. - + - + ("https://xunit.net/", "xUnit.net"), + ("https://docs.microsoft.com/en-us/dotnet/core/testing/unit-testing-with-dotnet-test", "Unit testing C# in .NET Core using dotnet test and xUnit") + }) /> + @code { diff --git a/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/06-UsingGitHubClassroom.razor b/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/06-UsingGitHubClassroom.razor index b3b71b4..3c159e8 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/06-UsingGitHubClassroom.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/06-UsingGitHubClassroom.razor @@ -2,73 +2,73 @@ @inject CourseBook CourseBook; - + Buckle up, it's time for the first assignment! We first need to setup the GitHub Classroom machinery so that you can focus on the code and have tests running automatically for you. - - GitHub - + + GitHub + You should be familiar with git. I assume you know its basics, and if you don't please go read any of the available tutorials and come back. - - + + You need a GitHub account, so go ahead and set it up if you haven't got one. When you set it up, go to the ClassroomIntro assignment with this invite. You need to accept the assignment, and then you'll be transported to a repo created just for you. You can also go straight to VS Code. - - What's in an assignment? - + + What's in an assignment? + The assignment's description and goals will be described in the top-level README file. It will also contain any instructions on how to run the project. In this case, there's nothing to say, so it's rather short. - - + + Every assignment will be described in more detail on this website, in the assignment section at the end of each module. Your goal is to read it, understand it, and implement what it asks you to implement. The tests in the repository will guide you, but they are not the sole indicator of success. - - + + GitHub automatically creates a PR for you, called Feedback. It will automatically update whenever you push a commit, letting me know about your changes and running tests for you. That means that if you start doing the exercise early, I will be able to jump in to your PR and give you feedback. That's basically points for free – you can fix the mistakes before the deadline and avoid losing points for them. However, I make no promises about the frequency of my feedback on those, after all, there's quite a few of you and I'm not always available. - - + + As a reminder: you can get up to 2 points for the tests and 3 for code style. The tests score is visible in the Feedback PR, and will be between 0 and 200 – divide them by 100 to get your actual points for grading. - - VS Code integration - + + VS Code integration + Arguably the coolest feature is VS Code integration. Click the magical "Open in Visual Studio Code" button and you'll get put into VS Code. The first time it will ask you to download some extensions – let it. On the left hand side there will be the GitHub logo . Click it and sign in. You will need to authorise VS Code to login to GitHub. - - + + Once you're there, you'll see the list of all your classroom assignments. You can expand them to see the test status, and you can interact with the PR from there. Click around and explore! - - + + Under the GitHub icon there's also the Live Share tab. You can invite other students, or me, to do pair-programming and share the code space. I'm not sure how well-refined this feature is, but feel free to try it out! - - Privacy - + + Privacy + If you're worried – only I will be able to see your code and grade. The repository is private and cannot be accessed by anyone else. You can make it public in the Settings, if you want. - - Summary - + + Summary + We know how to use GitHub Classroom and its VS Code integration. Complete the test assignment by filling the Adder and HelloWorld classes to make sure everything is configured correctly. - + @code { diff --git a/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/Assignment-DungeonWalker.razor b/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/Assignment-DungeonWalker.razor index 5a0ce20..65f0e9b 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/Assignment-DungeonWalker.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/Assignment-DungeonWalker.razor @@ -2,13 +2,13 @@ @inject CourseBook CourseBook; - + A hero has entered the dungeon! They must traverse the dark rooms, looking for loot and defeating enemies, all the while avoiding vicious traps. Will they prevail, or will the forces of darkness win this time...? - - Overview - + + Overview + We are writing an engine for a game, where a hero fights through a dungeon filled with various obstacles. A Dungeon is a list of Rooms, with one Room being the start point, and each @@ -16,7 +16,7 @@ Room by Room, advancing to the Room's successor when they're done interacting with it. A Dungeon takes a IRoomLayout as an argument to its constructor, which tells it how to construct the Rooms: - + - + ")" /> + An IRoom is where the core of the logic happens. - + - + ")" /> + It takes in the Hero instance and a PlayerStatistics object used for tracking the Hero's achievements. It then processes the encounter, returning an ITurnOutcome, that can display its information as a string. - - + + Your task will be to fill out the implementation with missing pieces. Existing methods have a default "implementation" of: - - - + + + We don't know what the magical throw is, but don't worry. Treat this as a placeholder that says "if you ever call this method, please crash, it's not ready yet". - - Task 0. – Combat - + + Task 0. – Combat + Encounters happen in a CombatRoom. Each Character in the game has three basic statistics: - - - - - Health – when it falls to $0$ the character dies; - - - - - Attack damage – the base damage of every attack performed by the character; - - - - - Armour – absorbs damage, every attack against this character has its - damage reduced by the armour value. - - - - - + + + + Health – when it falls to $0$ the character dies; + + + + + Attack damage – the base damage of every attack performed by the character; + + + + + Armour – absorbs damage, every attack against this character has its + damage reduced by the armour value. + + + + Combat proceeds in rounds. Each round both characters prepare an attack and then receive the attacks simultaneously. Damage dealt to the character is the attack damage - of the attacker minus the armour of the defender. + of the attacker minus the armour of the defender. The character's health is reduced by the damage received. Combat continues until either one or both characters die. - - - + + + When called it must resolve the combat, which results in the Hero and/or Enemy involved in combat to fall to $0$ health. - - + + Note that this will require changes to the existing classes in DungeonWalker.Characters, to allow them to be affected by Combat. You can freely extend them with any methods you want, but you cannot change the existing signatures, since the tests are using them. - + We want combat to be flashy! The ITurnOutcome returned by the Combat.Resolve method should include some details about how combat proceeded. Here's an example output: - + - + ")" /> + The possible final outcomes are: - - - - "Enemy vanquished!" – when the Enemy dies and the Hero survives; - - - "The hero falls!" – when the Hero dies and the Enemy survives; - - - "The hero falls, but takes the fiend with them!" – when both die; - - - - Finally, don't forget about statistics! + + + "Enemy vanquished!" – when the Enemy dies and the Hero survives; + + + "The hero falls!" – when the Hero dies and the Enemy survives; + + + "The hero falls, but takes the fiend with them!" – when both die; + + + Finally, don't forget about statistics! The DungeonWalker.Logic.Statistics.PlayerStatistics class exists to keep track of Hero's progress. Its properties are rather self-explanatory, note only that if the Hero dies during combat, then the Room is not counted as cleared, however if both the Hero and the Enemy fall it should count as defeating - an Enemy. + an Enemy. You need to modify it during combat, which most likely requires adding methods to the PlayerStatistics class. - + This is tested by the "Combat" test group, worth 0.25 points. - Task 1. – Loot - + Task 1. – Loot + Existing Rooms are EmptyRoom, which does nothing, and the CombatRoom. Implement the new LootRoom, that contains one of the following possible Loot items for the Hero: - - - - HealthPotion that replenishes a percentage of the Hero's max health. - - - Chainmail that increases the Hero's armour by a fixed number. - - - DamageCrystal that increases the Hero's damage by a fixed number. - - - - LootPile that implements the composite design pattern. - It's a Loot that contains other Loot and applies all of it to the Hero. - - - - + + + HealthPotion that replenishes a percentage of the Hero's max health. + + + Chainmail that increases the Hero's armour by a fixed number. + + + DamageCrystal that increases the Hero's damage by a fixed number. + + + + LootPile that implements the composite design pattern. + It's a Loot that contains other Loot and applies all of it to the Hero. + + + To implement them use the ILoot interface. You can add any methods you want to it. - + - + ")" /> + To complete the implementation, you need to hook up the appropriate creation logic in LootFactory: - + throw new NotImplementedException(); } -")"/> - - This might, again, require changes to the existing classes in DungeonWalker.Characters, - to allow them to be affected by ILoot. - - - As the outcome, list every item in the Room, which can be either - a single item or a pile of other items. Here's the expected - output for a single Damage Crystal (strength $15$). - - - + + This might, again, require changes to the existing classes in DungeonWalker.Characters, + to allow them to be affected by ILoot. + + + As the outcome, list every item in the Room, which can be either + a single item or a pile of other items. Here's the expected + output for a single Damage Crystal (strength $15$). + + + - - ... for a pile with a potion and a chainmail: - - + ... for a pile with a potion and a chainmail: + + - - And also output for Loot that looks like this, which shows - that piles should be flattened: - + + And also output for Loot that looks like this, which shows + that piles should be flattened: + - - - Pile of: - - - Pile of: - - - Damage Crystal - - - Damage Crystal - - - - - Health Potion - - - - - + + Pile of: + + + Pile of: + + + Damage Crystal + + + Damage Crystal + + + + + Health Potion + + + + + - - One more corner case, for an empty pile display: - - - - Note that the value displayed for a Health Potion is the actual value healed, not - the percentage strength of the potion. The buffs are applied in the order in which they - appear in the array, in particular if one of the potions restores full health to the character - then all subsequent ones give no health. A character's health can never exceed their max health. - - - This is tested by the "Loot" test group, worth 0.25 points. - - Task 2. – Dungeons - - There are two Room layouts implemented, Basic and Adventure. - Adventure is used for input-output tests and touches all elements of the solution. - But they are all rather simple, in that they are just constant Rooms you could generate - by hand. Create two more interesting ones, both parametrised by a number $n$: - - - - RipAndTear – - In the first Room there's a Skeleton Warrior. In the second there's - a $100\%$ health Health Potion. In the third one there's a Skeleton Warrior. - And so on, and so forth, repeat for $n$ Rooms. - - - FistAndBucklers – - In every third Room there's a Loot Room with a Health Potion for $25\%$ health. - In every fifth Room there's a Loot Room with a Pile containing a Chainmail and a Damage Crystal, in that order, - for $5$ armour and $5$ attack damage, respectively. - If a Room falls into both of these categories then it has a Pile with all three items, - Health Potion first, then Chainmail and Damage Crystal. - Every Room that does not fall into this category is a combat with an Orc, - until the $31$-st Room – then the combat switches to be against a Giant. Repeat until $n$ Rooms are created. - - - - - - This is tested by the "Dungeons" test group, worth 0.25 points. - - Task 3. – Heroic hardened Heroes - - In the end we need to define more Heroes for the roster. - Now, our Heroes aren't just ordinary blokes. To represent their heroic tenacity, - they have a special ability that triggers when they're in dire straits. When at or below - $25\%$ max health (rounded down), they gain a special - Tenacity statistics buff, different for each Hero. - Fill out the DungeonWalker.Logic.Factories.HeroFactory static class to return correct - instances for Heroes: - - - - - Rogue – - $40$ - $40$ - $10$, - Tenacity: determined to bring the fight to a close, the Rogue strikes - where it hurts and gains 20 attack damage. - - - - - Warrior – - $50$ - $30$ - $20$, - Tenacity: hardened in battle, the Warrior refuses to yield and gains - 20 armour. - - - - - Wizard – - $45$ - $35$ - $15$ - Tenacity: the Wizard channels powerful defensive barriers, - and viciously precise eldritch blasts, gaining 10 armour and - 10 attack damage. - - - - - - - This is tested by the "Characters" test group, worth 0.25 points. - - Project structure and execution - - The solution is divided into three projects, the entry DungeonWalker, - the logic in DungeonWalker.Logic and tests in - DungeonWalker.Logic.Tests. There's no need for you to look at - DungeonWalker, in particular it uses many C# features that we haven't covered yet. - You shouldn't modify it, as it can cause tests to break. - - - DungeonWalker.Logic is the project you should edit. - You can add any code there and make changes to the existing code that you deem - necessary to complete the assignment. - - - DungeonWalker.Logic.Tests contains the automated tests. - They will run automatically when you commit your changes, and you can run them - manually with dotnet test. - - - DO NOT edit the tests! You can freely add your own, but editing the existing - graded tests in any way is prohibited. The tests exist so that everyone - can have automated feedback and normalised grades for the tests. If you - edit the existing test methods you will receive 0 points for tests. - - - Running the code is described in the README. Remember that the project - will crash if you don't complete the implementation. Here's a little demo - of a working solution: - - - Style - - More than half of the points are for style. - The IDE will handle trivialities like formatting for you, - that's not what this is about. We want to make sure that we know - how to write clean, idiomatic C# code that would pass serious code review - in a professional setting. - - - - Use the correct naming convention for a given member. - - - DO NOT use abbreviations. If you look at the BCL you won't find monstrosities - like strcmp or inet_pton. - Only universally recognisable abbreviations of computer terms are allowed, like - HttpClient, TcpSocket or XmlSerializer. - - - Standard code guidelines that apply everywhere else also apply in C#: - avoid code duplication, don't create overly long methods, use methods - from the standard library where applicable. - - - Remember that C# is supposed to be simple and elegant. - Use the features we know to reduce the amount of code and number - of lines. Use expression bodied members, if possible. - Use string interpolation instead of manual string addition, if possible. - - - Follow best OOP practices. Don't introduce inheritance where it's not needed. - Seal your classes by default. - Prefer abstract types to concrete ones as parameters and return types. - In general, follow SOLID. - - - - Remember, submitting code earlier will get you style feedback earlier, - which can only improve your grade. - - -@code { + + One more corner case, for an empty pile display: + + + + Note that the value displayed for a Health Potion is the actual value healed, not + the percentage strength of the potion. The buffs are applied in the order in which they + appear in the array, in particular if one of the potions restores full health to the character + then all subsequent ones give no health. A character's health can never exceed their max health. + + + This is tested by the "Loot" test group, worth 0.25 points. + + Task 2. – Dungeons + + There are two Room layouts implemented, Basic and Adventure. + Adventure is used for input-output tests and touches all elements of the solution. + But they are all rather simple, in that they are just constant Rooms you could generate + by hand. Create two more interesting ones, both parametrised by a number $n$: + + + RipAndTear – + In the first Room there's a Skeleton Warrior. In the second there's + a $100\%$ health Health Potion. In the third one there's a Skeleton Warrior. + And so on, and so forth, repeat for $n$ Rooms. + + + FistAndBucklers – + In every third Room there's a Loot Room with a Health Potion for $25\%$ health. + In every fifth Room there's a Loot Room with a Pile containing a Chainmail and a Damage Crystal, in that order, + for $5$ armour and $5$ attack damage, respectively. + If a Room falls into both of these categories then it has a Pile with all three items, + Health Potion first, then Chainmail and Damage Crystal. + Every Room that does not fall into this category is a combat with an Orc, + until the $31$-st Room – then the combat switches to be against a Giant. Repeat until $n$ Rooms are created. + + + + + + + This is tested by the "Dungeons" test group, worth 0.25 points. + + Task 3. – Heroic hardened Heroes + + In the end we need to define more Heroes for the roster. + Now, our Heroes aren't just ordinary blokes. To represent their heroic tenacity, + they have a special ability that triggers when they're in dire straits. When at or below + $25\%$ max health (rounded down), they gain a special + Tenacity statistics buff, different for each Hero. + Fill out the DungeonWalker.Logic.Factories.HeroFactory static class to return correct + instances for Heroes: + + + + Rogue – + $40$ + $40$ + $10$, + Tenacity: determined to bring the fight to a close, the Rogue strikes + where it hurts and gains 20 attack damage. + + + + + Warrior – + $50$ + $30$ + $20$, + Tenacity: hardened in battle, the Warrior refuses to yield and gains + 20 armour. + + + + + Wizard – + $45$ + $35$ + $15$ + Tenacity: the Wizard channels powerful defensive barriers, + and viciously precise eldritch blasts, gaining 10 armour and + 10 attack damage. + + + + + + + + This is tested by the "Characters" test group, worth 0.25 points. + + Project structure and execution + + The solution is divided into three projects, the entry DungeonWalker, + the logic in DungeonWalker.Logic and tests in + DungeonWalker.Logic.Tests. There's no need for you to look at + DungeonWalker, in particular it uses many C# features that we haven't covered yet. + You shouldn't modify it, as it can cause tests to break. + + + DungeonWalker.Logic is the project you should edit. + You can add any code there and make changes to the existing code that you deem + necessary to complete the assignment. + + + DungeonWalker.Logic.Tests contains the automated tests. + They will run automatically when you commit your changes, and you can run them + manually with dotnet test. + + + DO NOT edit the tests! You can freely add your own, but editing the existing + graded tests in any way is prohibited. The tests exist so that everyone + can have automated feedback and normalised grades for the tests. If you + edit the existing test methods you will receive 0 points for tests. + + + Running the code is described in the README. Remember that the project + will crash if you don't complete the implementation. Here's a little demo + of a working solution: + + + Style + + More than half of the points are for style. + The IDE will handle trivialities like formatting for you, + that's not what this is about. We want to make sure that we know + how to write clean, idiomatic C# code that would pass serious code review + in a professional setting. + + + Use the correct naming convention for a given member. + + + DO NOT use abbreviations. If you look at the BCL you won't find monstrosities + like strcmp or inet_pton. + Only universally recognisable abbreviations of computer terms are allowed, like + HttpClient, TcpSocket or XmlSerializer. + + + Standard code guidelines that apply everywhere else also apply in C#: + avoid code duplication, don't create overly long methods, use methods + from the standard library where applicable. + + + Remember that C# is supposed to be simple and elegant. + Use the features we know to reduce the amount of code and number + of lines. Use expression bodied members, if possible. + Use string interpolation instead of manual string addition, if possible. + + + Follow best OOP practices. Don't introduce inheritance where it's not needed. + Seal your classes by default. + Prefer abstract types to concrete ones as parameters and return types. + In general, follow SOLID. + + + Remember, submitting code earlier will get you style feedback earlier, + which can only improve your grade. + + + @code { } diff --git a/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/Introduction.razor b/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/Introduction.razor index 525755d..7bf5851 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/Introduction.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/01-ObjectOrientation/Introduction.razor @@ -2,11 +2,11 @@ @inject CourseBook CourseBook; - - We continue C# basics by introducing object orientation in the form of classes and interfaces. + + We continue C# basics by introducing object orientation in the form of classes and interfaces. In this module we will learn: - - + + classes, methods, fields, properties; abstract classes, interfaces, inheritance; diff --git a/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/00-Memory.razor b/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/00-Memory.razor index f10006e..12f2aa4 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/00-Memory.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/00-Memory.razor @@ -2,58 +2,56 @@ @inject CourseBook CourseBook; - + Automatic memory management is one of the most important features of CLR that makes programming easier, since you never really have to care about allocation, deallocation, double-frees, memory leaks, and all the other great experiences of coding in C. - - Allocator and collector - + + Allocator and collector + At the core of managed memory we have a managed heap, which is allocated by the runtime for the application (or per core, depending on configuration). All class instances we create land on this heap. The management of the heap falls onto two components: the allocator and the collector. - - - - - Allocator is responsible for allocating and initialising memory requested - by a program running on the CLR. In theory, its job is simple – when it gets a request - to create a new instance it needs to check if memory is available, and initialise it if - it is. The allocator, however, also manages when collections happen. When there's not enough - memory for the request, it tells the collector to run a GC cycle. - - - That's not the only time, though. There are two parameters here that need to be balanced – - memory consumption and CPU time. GC collections aren't free, but calling them too rarely - will cause the program to use up much more system memory than it actually needs. The allocator - keeps track of various parameters and makes the GC self-tuning. It records the - memory usage patterns of the application that's running and learns to optimise the two conflicting - goals more accurately as time goes on. It will make an informed decision on whether to call for a GC - based on number of allocations performed, average lifetimes of allocated objects, etc. - - - - - Collector performs the actual garbage collection. The .NET GC is a - mark-and-sweep collector. When triggered, it scans the program - for so called roots, which are objects that must be alive. These are all references - in static fields, local variables and arguments passed to methods currently executing - (including all the methods up in their call-stack). It then traverses the graph of objects - starting from these roots, treating references contained within object instances as edges. - This is the mark phase. After all reachable objects have been marked, all other objects - are classified as dead and collected in the sweep phase, where the collector goes through - all objects in the managed heap and reclaims memory from the dead ones. - - - - + + + + Allocator is responsible for allocating and initialising memory requested + by a program running on the CLR. In theory, its job is simple – when it gets a request + to create a new instance it needs to check if memory is available, and initialise it if + it is. The allocator, however, also manages when collections happen. When there's not enough + memory for the request, it tells the collector to run a GC cycle. + + + That's not the only time, though. There are two parameters here that need to be balanced – + memory consumption and CPU time. GC collections aren't free, but calling them too rarely + will cause the program to use up much more system memory than it actually needs. The allocator + keeps track of various parameters and makes the GC self-tuning. It records the + memory usage patterns of the application that's running and learns to optimise the two conflicting + goals more accurately as time goes on. It will make an informed decision on whether to call for a GC + based on number of allocations performed, average lifetimes of allocated objects, etc. + + + + + Collector performs the actual garbage collection. The .NET GC is a + mark-and-sweep collector. When triggered, it scans the program + for so called roots, which are objects that must be alive. These are all references + in static fields, local variables and arguments passed to methods currently executing + (including all the methods up in their call-stack). It then traverses the graph of objects + starting from these roots, treating references contained within object instances as edges. + This is the mark phase. After all reachable objects have been marked, all other objects + are classified as dead and collected in the sweep phase, where the collector goes through + all objects in the managed heap and reclaims memory from the dead ones. + + + This is a very simplified picture. The GC is a very complicated piece of engineering, as its performance characteristics are crucial for the performance of the entire .NET platform. We will discuss this in our module on performance, but for now there's one takeaway that we need: - + Memory pressure, i.e. the amount of memory allocated by the application, number and frequency of these allocations, has a direct impact on its performance. @@ -61,14 +59,14 @@ and high turnover (a lot of objects get collected), then such GCs will be very expensive. Therefore, heap allocations are expensive in terms of performance. - + Again, I am oversimplifying, but that's the gist. That's also what people mean when they say that "allocations are expensive" – it's not that the actual work performed during new is expensive, memory allocation in .NET is actually surprisingly fast. But more allocations means more memory pressure, means more GCs, means performance drops. - - Stack - + + Stack + Not all data needs to land on the heap. Every thread of a .NET program comes with a stack, which is a local memory area which can be managed without the involvement of the GC allocator. The stack is represented with a single pointer @@ -80,18 +78,18 @@ the stack pointer, so it's blazingly fast and requires no GC – things allocated on the stack frame of the current method effectively disappear when the method returns. - - Summary - + + Summary + In .NET we have the managed heap and the execution stack of a given thread. The memory on the heap is managed by the garbage collection system. Allocating on the stack is much faster, effectively being the thread's scratch space. - + + ("https://docs.microsoft.com/en-us/dotnet/standard/automatic-memory-management", "Automatic Memory Management on docs.microsoft") + }) /> @code { diff --git a/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/01-ReferenceTypesAndValueTypes.razor b/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/01-ReferenceTypesAndValueTypes.razor index 07d177c..0404f87 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/01-ReferenceTypesAndValueTypes.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/01-ReferenceTypesAndValueTypes.razor @@ -6,11 +6,11 @@ } - + We're going back to the notebooks repository. Go through @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - - In this section we'll cover + + In this section we'll cover diff --git a/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/02-OperatorOverloading.razor b/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/02-OperatorOverloading.razor index e38351a..b7a6970 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/02-OperatorOverloading.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/02-OperatorOverloading.razor @@ -6,11 +6,11 @@ } - + We're going back to the notebooks repository. Go through @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - - In this section we'll cover + + In this section we'll cover diff --git a/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/03-Exceptions.razor b/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/03-Exceptions.razor index bbddd02..7bdb44d 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/03-Exceptions.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/03-Exceptions.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover diff --git a/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/04-Nullability.razor b/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/04-Nullability.razor index 4aaabc2..297747f 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/04-Nullability.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/04-Nullability.razor @@ -6,11 +6,11 @@ } - + Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - - In this section we'll cover + + In this section we'll cover diff --git a/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/05-Casting.razor b/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/05-Casting.razor index e5cf2ac..5394790 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/05-Casting.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/05-Casting.razor @@ -6,11 +6,11 @@ } - + Next one in the notebooks repository: @($"{section.Module.Id}-{section.Module.RouteName}/{section.Id}-{section.RouteName}.dib"). - - In this section we'll cover + + In this section we'll cover diff --git a/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/06-PassByReference.razor b/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/06-PassByReference.razor index bcf5e58..24ddb0a 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/06-PassByReference.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/06-PassByReference.razor @@ -6,11 +6,11 @@ } - + Next one in the notebooks repository: @($"{section.Module.Id}-{section.Module.RouteName}/{section.Id}-{section.RouteName}.dib"). - - In this section we'll cover + + In this section we'll cover diff --git a/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/Assignment-LustrousLoot.razor b/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/Assignment-LustrousLoot.razor index f7df929..30b1606 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/Assignment-LustrousLoot.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/Assignment-LustrousLoot.razor @@ -2,31 +2,31 @@ @inject CourseBook CourseBook; - + Our tenacious heroes are strong, but not strong enough! To defeat their enemies they need reliable equipment and powerful trinkets that will enhance their abilities. The Dark Lord's machinery in the dungeon can be used to make the weapons even deadlier and armour even sturdier... - - Overview - + + Overview + Throughout the course we will be working on our DungeonWalker and making it more into a real game! This time we will be revisiting Loot and making it more fledged out by splitting it into Equipment and Consumable items. - - Task 0. – Modifiers and Health - + + Task 0. – Modifiers and Health + If you compare Health Potions to Damage Crystals and Chainmails, a key difference emerges – the first one gets consumed and applies its effects immediately, while the other two give permanent modifiers to the Hero – these we call Equipment. We want to expand on this by creating a Modifier type that will model that. - - + + First, we define an EquipmentBase abstract class that can be implemented by items to indicate they are not one-timers, but rather permanent additions to the Hero's arsenal. It has a single property, which returns a Modifier, and a TryUpgrade method that we will use later: - + throw new NotImplementedException(); - + public bool TryUpgrade() => throw new NotImplementedException(); } - ")"/> - + ")" /> + As usual, you can freely add stuff to this class, just don't change the two existing signatures. A Modifier is currently a rather dumb type: - + - +")" /> + We'd like it to be more useful. Your first task is to implement relevant operators and methods: - - - addition of two Modifiers that simply adds all individual modifier values; - dual to addition, subtraction of two Modifiers - multiplication by a float scalar that multiplies all individual modifier values, rounding down; - unary negation, equivalent to multiplying by negative one. - - + + addition of two Modifiers that simply adds all individual modifier values; + dual to addition, subtraction of two Modifiers + multiplication by a float scalar that multiplies all individual modifier values, rounding down; + unary negation, equivalent to multiplying by negative one. + + + We also want a readable ToString representation, e.g. our friend Chainmail would give this modifier: - + A magical sword that grants extraordinary strength could give: - + And a legendary artefact could grant: - + In short, just write out the non-zero modifiers in order max health, damage, armour. If the Modifier is zero across the board, print "nothing". - - - + + + To finish working with Modifiers, we need to change how Characters interact with it. The Character class has been reworked to have immutable base statistics and a Modifier, but it is bugged! It can cause the Character to have less health than zero – you need to fix that. - - + + You also need to fix the Hero's DisableTenacity and EnableTenacity methods to apply the modifier correctly. - + - +")" /> The tests do not compile until you finish this Task. If you add the required operators to the Modifier type they will compile. - + This is tested by the "Modifiers" test group, worth 0.33 points. - Task 1. – Equipment - - + Task 1. – Equipment + + Now we rework the existing Loot system to split items between Consumables and Equipment. Not all Equipment is equal. There are five distinct types of items the Heroes may equip: - - - - Melee Weapon - Ranged Weapon - Armour - Shield - Trinket - - - + + + Melee Weapon + Ranged Weapon + Armour + Shield + Trinket + These types constrain who can equip them. - + Rogue – - can equip a Melee Weapon, a Ranged Weapon, and a Trinket. + can equip a Melee Weapon, a Ranged Weapon, and a Trinket. - Warrior – - can equip a Melee Weapon, an Armour, and a Shield. + Warrior – + can equip a Melee Weapon, an Armour, and a Shield. - Wizard – - can equip a Ranged Weapon, and two Trinkets. + Wizard – + can equip a Ranged Weapon, and two Trinkets. - - + + You need to implement the missing TryEquip method on the Hero class and potentially all inheriting classes. - - + + You may need to modify the TestHero class located in the tests. This is allowed and expected. The TestHero can accept any equipment types. The tests only ever put at most five items into their equipment. - + The EquipmentFactory class has more unimplemented members for you. Implement the new possible Equipment drops. - - + + Chainmail – - good old Loot from the previous assignment, only now it's an Armour. + good old Loot from the previous assignment, only now it's an Armour. - DamageCrystal – - same as above, but now it's a Trinket. + DamageCrystal – + same as above, but now it's a Trinket. - TwoHandedSword – - a Melee Weapon that increases Hero's damage by $20$, but decreases armour by $10$ - (handling such a heavy weapon slows you down and makes you more susceptible to attacks). + TwoHandedSword – + a Melee Weapon that increases Hero's damage by $20$, but decreases armour by $10$ + (handling such a heavy weapon slows you down and makes you more susceptible to attacks). - StaffOfLife – - a Ranged Weapon that increases Hero's damage by $10$ and their max health by $10$. + StaffOfLife – + a Ranged Weapon that increases Hero's damage by $10$ and their max health by $10$. - SpikedShield – - a Shield that increases Hero's damage by $10$ and their armour by $15$. + SpikedShield – + a Shield that increases Hero's damage by $10$ and their armour by $15$. - TitanicBulwark – - an Armour that decreases Hero's damage by $10$, but increases armour by $20$ and max health by $50$. + TitanicBulwark – + an Armour that decreases Hero's damage by $10$, but increases armour by $20$ and max health by $50$. - EssenceOfMagic – - a Trinket that increases Hero's damage by $10$, armour by $5$, and max health by $20$. + EssenceOfMagic – + a Trinket that increases Hero's damage by $10$, armour by $5$, and max health by $20$. - + - + There's also a missing method in LootFactory. - + - + A Hero only picks up the Equipment if they have a slot to fit it into. If they do not then the item is ignored. This logic needs to be placed in Hero.TryEquip. - + - + Since picking up Loot can now result in the Hero ignoring it, we need to tweak the Loot messages a bit. Here's an example: - + - + This is tested by the "Loot" test group, worth 0.33 points. - Task 2. – Upgrades + Task 2. – Upgrades - + Every piece of Equipment has a quality level. The quality level provides a multiplier to the Equipment's' Modifier. - + @@ -282,38 +279,38 @@ The room is empty except for a large chest. And inside... - + All Equipment starts as Common. It can be upgraded using the ancient arcane workstations scattered across the dungeon. Once entered, these UpgradeRoom locations upgrade all Equipment on the Hero to the next level, unless it's already Heroic. - - + + Implement this system and the UpgradeRoom. It should return a nice description of the upgrade operation, omitting all equipment that could not be upgraded. A few examples: - + + ")" /> +However, they don't have any that could be upgraded. Time to move on.")" /> This is tested by the "Upgrades" test group, worth 0.34 points. - + - Project structure and execution - + Project structure and execution + The structure and execution stay the same. The only caveat is that you need to implement the Modifier operators for tests to compile. - + @code { diff --git a/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/Introduction.razor b/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/Introduction.razor index aed0c5d..fc7eaa8 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/Introduction.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/02-ReferencesAndValues/Introduction.razor @@ -2,9 +2,9 @@ @inject CourseBook CourseBook; - + We carry on to more advanced features specific to C# and the .NET type system. In this module we will learn: - + how CLR manages memory, basics of Garbage Collection; diff --git a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/00-Indexers.razor b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/00-Indexers.razor index d3f7f4e..d8eea2d 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/00-Indexers.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/00-Indexers.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Defining custom indexers. diff --git a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/01-Generics.razor b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/01-Generics.razor index ebc6e35..8e720a1 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/01-Generics.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/01-Generics.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover diff --git a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/02-Equality.razor b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/02-Equality.razor index b6765e8..5e59293 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/02-Equality.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/02-Equality.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Default equality semantics. diff --git a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/03-Ordering.razor b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/03-Ordering.razor index 5ddba12..c5c4c4d 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/03-Ordering.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/03-Ordering.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover IComparable<T> diff --git a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/04-ExplicitInterfaceImplementations.razor b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/04-ExplicitInterfaceImplementations.razor index d5f3359..077c9a8 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/04-ExplicitInterfaceImplementations.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/04-ExplicitInterfaceImplementations.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Problems with implementing multiple interfaces. diff --git a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/05-Collections.razor b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/05-Collections.razor index bd30001..183e3b4 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/05-Collections.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/05-Collections.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Generic collections in the BCL. diff --git a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/06-Comparers.razor b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/06-Comparers.razor index 89d6e75..a543e4c 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/06-Comparers.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/06-Comparers.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Plugging in equality with IEqualityComparer<T>. diff --git a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/07-Tuples.razor b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/07-Tuples.razor index 12d7b30..193c7ea 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/07-Tuples.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/07-Tuples.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Creating tuples. diff --git a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/08-Deconstruction.razor b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/08-Deconstruction.razor index cc82c39..f7e7174 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/08-Deconstruction.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/08-Deconstruction.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Deconstructing tuples. diff --git a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/09-NestedTypes.razor b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/09-NestedTypes.razor index 3585217..1158d46 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/09-NestedTypes.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/09-NestedTypes.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover How to declare nested types. diff --git a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/10-HashCodeAsAMutableStruct.razor b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/10-HashCodeAsAMutableStruct.razor index 1f062fa..fe2950d 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/10-HashCodeAsAMutableStruct.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/10-HashCodeAsAMutableStruct.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover How to create more complex hash codes. diff --git a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/11-Records.razor b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/11-Records.razor index b94a7f7..d058f28 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/11-Records.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/11-Records.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Declaring record classes. diff --git a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/12-PatternMatching.razor b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/12-PatternMatching.razor index ac9942c..d9ef4ef 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/12-PatternMatching.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/12-PatternMatching.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover is expressions and switch expressions. diff --git a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/Assignment-DivergingDungeons.razor b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/Assignment-DivergingDungeons.razor index e5a052f..da5ff6a 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/Assignment-DivergingDungeons.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/03-GenericsAndCollections/Assignment-DivergingDungeons.razor @@ -2,32 +2,32 @@ @inject CourseBook CourseBook; - + When the path is straight, pressing onwards is the only course of action. But when dark paths diverge and twist, choices need to be made, and resolve of our Heroes tested... - - Overview - + + Overview + We want to introduce non-determinism to our game. For that we need Dungeons that are more complicated than a path of Rooms. If we implement a graph type we'll be able to create complex Dungeons and test different paths through it for efficiency. - + - Task 0 – Run Leaderboard - + Task 0 – Run Leaderboard + We now want to be able to compare different runs of our Heroes. Since the dungeons now have multiple paths, multiple outcomes are possible. To facilitate this, PlayerStatistics have been changed to produce an immutable result at the end of a run, represented by the type DungeonWalker.Logic.Statistics.RunStatistics. It contains all the statistics and an identifier that is unique in a single execution of our game. - - + + Your task is to complete its implementation with equality and ordering. The rules for equality are simple: instances are equal if they have the same Id. For ordering we compare statistics in order to determine the best run: - + A Run with more cleared rooms is better (greater); if equal, then @@ -43,13 +43,13 @@ - + Then, we want to create a Leaderboard. The BestRunsCollection type is a mutable collection of Runs that implements the ICollection<RunStatistics> interface, and provides a special method: IEnumerable<(int rank, RunStatistics run)> EnumerateRanking(). It's supposed to enumerate the Runs from the best to the worst and assign them a rank. The ranks are as in natural leaderboards, an example looks like this: - + 0.5 point. - Task 1. – Graphs - + Task 1. – Graphs + This task is cut out for you. We have a new project, DungeonWalker.Graphs, defining two interfaces, IGraph<TLabel> and IGraphBuilder<TLabel>, as well as two basic types, Vertex<TLabel> and Edge<TLabel>. You need to fill out their implementations. Bulk of the work is in Graph.cs, where we need to implement both the graph and the builder. - - Novelty – XML Comments - + + Novelty – XML Comments + The code differs in one major aspect from what you've seen thus far – it has documentation! In C# we document public members in a structured manner with XML comments. These give your IDE the power to help you during autocomplete, showing descriptions of types, methods, and their parameters while you're filling out the arguments. Here's a sample: - + /// Gets the vertex with a given label, if present in the graph. @@ -94,23 +94,23 @@ /// If the graph does not contain a vertex with label . Vertex this[TLabel label] { get; } ")"/> - + The documentation should be pretty self-documenting (no apologies for the pun). Your implementation should fulfil the documentation, as this is what the tests test against. - - + + In many cases the documentation is exactly the same as in the interface (or base class) we're implementing. Since the compiler complains about undocumented public members, there's a shortcut to inherit the entire documentation for a given member: - + ")"> - Novelty – Analysers - + Novelty – Analysers + You might notice that the compiler is complaining about quite a few more things than usual. That's because I've finally enabled the default Roslyn analysers across the solution. Before this module its suggestions might've been confusing, as we didn't know generics for example. They're triggered by the following spells in the csproj files: - + true @@ -126,11 +126,11 @@ Vertex this[TLabel label] { get; } ")"/> - + These analysers include warnings and suggestions for common antipatterns in C#, as well as automatic fixes for them. Heed the compiler, if it complains about something there's a big chance I will too during review :) - + To get full benefits of the feature, you need to toggle a setting in VS Code located in @@ -141,13 +141,13 @@ Vertex this[TLabel label] { get; } This is tested by the "Graphs" test group, worth 1.0 point. - Project structure and execution - + Project structure and execution + A few new parameters were added to allow for randomising paths through the Dungeon. There is a single graph dungeon defined, MagicalMaze. You can provide the number of independent runs to execute with the --number flag, and fix a seed for the RNG with --seed. - + diff --git a/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/00-GenericVariance.razor b/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/00-GenericVariance.razor index 6936622..7166d70 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/00-GenericVariance.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/00-GenericVariance.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover The motivation behind variance. diff --git a/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/01-Iterators.razor b/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/01-Iterators.razor index d0680c5..ce27aee 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/01-Iterators.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/01-Iterators.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Defining iterators. diff --git a/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/02-ExtensionMethods.razor b/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/02-ExtensionMethods.razor index cbe970a..6e5f56b 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/02-ExtensionMethods.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/02-ExtensionMethods.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Defining extension methods. diff --git a/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/03-AnonymousTypes.razor b/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/03-AnonymousTypes.razor index e8038f8..ac87502 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/03-AnonymousTypes.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/03-AnonymousTypes.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Creating anonymous types. diff --git a/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/04-Delegates.razor b/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/04-Delegates.razor index 7a51e4a..433f07d 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/04-Delegates.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/04-Delegates.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Delegates as strongly-typed function pointers. diff --git a/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/05-LambdaExpressions.razor b/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/05-LambdaExpressions.razor index adb199d..76dce15 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/05-LambdaExpressions.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/05-LambdaExpressions.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Using lambda expressions. diff --git a/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/06-LinqQueries.razor b/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/06-LinqQueries.razor index b76ddac..824fd46 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/06-LinqQueries.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/06-LinqQueries.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Basic LINQ operators – Select, Where, OrderBy, GroupBy. diff --git a/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/07-LocalMethods.razor b/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/07-LocalMethods.razor index a6d9192..6086630 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/07-LocalMethods.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/07-LocalMethods.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Declaring local methods. diff --git a/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/Assignment-LayeredLayouts.razor b/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/Assignment-LayeredLayouts.razor index 788cd36..a1149cf 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/Assignment-LayeredLayouts.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/04-LINQ/Assignment-LayeredLayouts.razor @@ -2,23 +2,23 @@ @inject CourseBook CourseBook; - + What if the Dark Lord devises a new trick – Dungeons that loop all over themselves, over and over, trapping Heroes in perpetual madness! No, this is not possible, even the most evil entity would not create a Dungeon that is not topologically sortable. - - Overview - + + Overview + While having a generic graph structure sure is cool, not all graphs are suitable Dungeons for our game. Dungeons should be easily traversable via a single path from an entry room to the end, without the ability to enter the same room more than once. In other words, it should be an acyclic graph. - - + + A natural representation of such a Dungeon is a series of layers. Think the map from Slay the Spire, if you're familiar with the game. At each layer there's a number of reachable rooms, and each edge leads into a room from a higher layer. At the bottom are the entrances, at the top is the exit. - + @@ -27,19 +27,19 @@ Example layered graph map from Slay the Spire - + In this assignment your task is to implement an algorithm that decides if a graph is acyclic, and if yes partitions the vertices into layers, as described below. - + - Algorithm - + Algorithm + In graph theory parlance the problem we want to solve is called topological sorting – ordering vertices in such a manner that every edge goes from left to right, or deciding that the graph has a cycle and it cannot be done. Once we do that, it's easy to divide the graph into layers by simply traversing vertices in order and calculating shortest paths. The recursive definition of layers is as follows: - + the layer $0$ is exactly the vertices without any incoming edges; @@ -50,7 +50,7 @@ - + There's a number of different algorithms for this, and you are free to use your favourite, if you have one. If not, here's the arguably simplest one, attributed to Tarjan. We will construct a list of vertices in reverse topological order. First, initialize it to an empty list. @@ -69,45 +69,43 @@ Once we consider all the edges, mark the vertex as visited and add it to the result list. Once all vertices in the graph are visited, we return the reversed list as our result. - - + + That the algorithm indeed constructs a valid topological order follows from properties of a DFS, I leave the proof as an exercise. Finding the layers is now straightforward – every vertex with no edges is in layer zero. Every neighbour of a vertex in layer zero is in layer one. Continue until all edges are considered. Because neighbours are always to the right in the topological order, this calculation can be easily done in a single traversal of the sorted vertex list. - - Task 0. – Topological Sort - + + Task 0. – Topological Sort + Your job is pretty simple – complete the implementation in DungeonWalker.Graphs.Algorithms.TopologicalSort<TLabel>. The missing method is: - ? result) ")"/> - The method return false if the graph has a cycle, otherwise it returns true and fills in the layered result. - + This is tested by the "TopologicalSort" test group, worth 1 point. - Task 1. – Performance Considerations - + Task 1. – Performance Considerations + There are additional tests for much larger graphs. You can consider the first part as worth half points, and this part as worth the other half. - - + + When a graph has a lot of vertices, the size of the stack of a recursive DFS can also grow large. The stack of a single .NET thread is usually limited by one megabyte, which means that using as little as $4$ bytes per vertex in a DFS of depth $10^5$ is enough to overflow it (because a return pointer must always remain on the stack and it takes $8$ bytes). - - + + Therefore, the main issue for these tests is not speed, but the stack limit. The solution is conceptually simple – we need to create an explicit Stack<T> and use it instead of recursion. That should be enough to pass the tests, no need to fiddle with microoptimisations. We will tackle performance fun in a later module . - + This is tested by the "TopologicalSortPerformance" test group, worth 1 point. diff --git a/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/00-SimpleThreading.razor b/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/00-SimpleThreading.razor index 32ad14a..c10670d 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/00-SimpleThreading.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/00-SimpleThreading.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover The low-level Thread API basics. diff --git a/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/01-Events.razor b/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/01-Events.razor index e9713fc..49ac154 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/01-Events.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/01-Events.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Manually implementing the observer pattern. diff --git a/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/02-ExceptionHandling.razor b/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/02-ExceptionHandling.razor index 7e7a2ff..2963cae 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/02-ExceptionHandling.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/02-ExceptionHandling.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover The try, catch blocks. diff --git a/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/03-DisposableResources.razor b/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/03-DisposableResources.razor index 17c0362..e3b27ed 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/03-DisposableResources.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/03-DisposableResources.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Why we need explicit cleanup. diff --git a/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/04-AsyncAndAwait.razor b/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/04-AsyncAndAwait.razor index 89e1f6d..546177e 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/04-AsyncAndAwait.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/04-AsyncAndAwait.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover What is an asynchronous operation. diff --git a/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/05-ThreadPool.razor b/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/05-ThreadPool.razor index 5bed5e3..842c382 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/05-ThreadPool.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/05-ThreadPool.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover What is the ThreadPool? diff --git a/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/06-Cancellation.razor b/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/06-Cancellation.razor index 5c68e09..daca83c 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/06-Cancellation.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/06-Cancellation.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Cooperative cancellation. diff --git a/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/07-AsyncInterfacesAndValueTask.razor b/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/07-AsyncInterfacesAndValueTask.razor index 4ac3244..eb62f05 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/07-AsyncInterfacesAndValueTask.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/07-AsyncInterfacesAndValueTask.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover IAsyncDisposable, await using. diff --git a/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/Assignment-PersistedPathways.razor b/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/Assignment-PersistedPathways.razor index 2135b83..3a9390e 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/Assignment-PersistedPathways.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/05-Asynchrony/Assignment-PersistedPathways.razor @@ -2,33 +2,33 @@ @inject CourseBook CourseBook; - + Does a Dungeon exist in objective reality, or is it merely a trick, a figment of a Hero's imagination? We must make the dark pathways concrete and visible to all! - - Overview - + + Overview + Main variety in our game comes from different possible Dungeons. Keeping them all just as classes hardwired in the application code is limiting. We'd like to have a lot of Dungeons, be able to share new ones with our users, maybe even allow them to construct their own. In this assignment we take a first step towards that goal, by persisting our maps on disk with the local filesystem. - - - Task 0. – JSON Serialisation - + + + Task 0. – JSON Serialisation + First we need to make our graph serializable. There are two major libraries in .NET - when it comes to JSON processing, the long established + when it comes to JSON processing, the long established Json.NET, and the newer System.Text.Json, which is part of the BCL. We will use the latter for this assignment. - - + + To persist our graphs we need a more suited representation than the Graph<IRoom> class. In a new project, DungeonWalker.DataLayer, we define the type GraphTemplate<TLabel>, with only two simple properties: - + /// Gets the labels of vertices of the graph. @@ -42,11 +42,10 @@ public IReadOnlyList Vertices { get; init; } /// in the graph. /// public IReadOnlyList> AdjacencyList { get; init; } - ")"/> - + ")" /> + This is a simple encoding via adjacency lists. For example, a $4$-cycle could be encoded like this: - - > AdjacencyList { get; init; } [ 0 ] ] } - ")"/> - + ")" /> The heavy lifting of getting the IRoot labels to serialize is already done. Your job is to implement the conversion from IGraph<TLabel> to GraphTemplate<TLabel> and vice-versa. All of these are located in src/DungeonWalker.DataLayer/Serialization/GraphTemplate.cs. - + This is tested by the "Serialization" test group, worth 1 point. - - - + + + To serialize and deserialize with System.Text.Json we use the JsonSerializer class. It has a lot of overloads for Serialize and Deserialize methods, including async overloads, cancellation support, writing directly to streams, etc. - - + + Serialization is governed by a set of base converters that serialize basic types, and also any .NET object by just taking its public properties and mapping them into a JSON object. There can also be custom converters that manually take control of reading and writing separate JSON tokens. There are also some basic options, like whether to include whitespace (prettify the output), and whether to use PascalCase or camelCase for property names. These are all put into a JsonSerializerOptions object that can be passed to the serialization methods. - - Task 1. – File System Repository - + + Task 1. – File System Repository + The interface that we need to implement is: - - Task> ListNamesAsync(CancellationToken cancellationToken); } - ")"/> - + ")" /> Save, load, and list all. We first want to implement DungeonRepositoryBase, which will take care of translating IGraphRoomLayout to and from a GraphTemplate<RoomTemplate>. Use the RoomTemplate.FromRoom static method and your implementation of GraphTemplate. To create a IGraphRoomLayout from a graph you can use the GraphTemplate.Select method and the DungeonWalker.Logic.Dungeons.PredefinedGraphRoomLayout class. - - + + Finally, you need to implement FileSystemDungeonRepository, located in yet another project, DungeonWalker.DataLayer.FileSystem. It extends DungeonRepositoryBase interface using the local filesystem as database storage. The constructor takes a directory path, which is the location of all saved Dungeons. - - + + The _options private field contains the aforementioned JsonSerializerOptions object that you should use for all serialisation operations. Best not to modify it, it is already configured to be compatible with the tests and to have converters required for serialising our game data, like IRoom and ILoot objects. - - + + The solution code is pretty short when written correctly, but requires care to get right. Remember all the things we talked about in this module, as well as defensive programming patterns. Remember that: - - - - All I/O operations on the filesystem may fail. - The exceptions called by this must be wrapped in a FileSystemRepositoryException - that inherits from DungeonRepositoryException. - - - Precondition checks for async operations should happen synchronously. - There are no tests for this, but I will point that out during code review. Use the local method pattern - to deal with that. - - - All system file handles are expensive resources and must be properly disposed of. - - - The CancellationToken instances passed to the methods should be used for - all async operations that support cancellation. - - - Always choose the async overload for operations on the filesystem. - - + + + All I/O operations on the filesystem may fail. + The exceptions called by this must be wrapped in a FileSystemRepositoryException + that inherits from DungeonRepositoryException. + + + Precondition checks for async operations should happen synchronously. + There are no tests for this, but I will point that out during code review. Use the local method pattern + to deal with that. + + + All system file handles are expensive resources and must be properly disposed of. + + + The CancellationToken instances passed to the methods should be used for + all async operations that support cancellation. + + + Always choose the async overload for operations on the filesystem. + + + This is tested by the "FileSystemRepository" test group, worth 2 points. - Demo - + Demo + Finally, there is a demo that runs the code on some actual Dungeons on your local filesystem and test the entire repository. It's located in a new console project, DungeonWalker.FileSystemRepositoryDemo. Here's the output on a model solution: - - + + This demo is worth 1 point. diff --git a/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/00-ExpressionTrees.razor b/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/00-ExpressionTrees.razor index e21576e..8d459b6 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/00-ExpressionTrees.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/00-ExpressionTrees.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Lambdas into expression trees. diff --git a/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/01-LinqToSql.razor b/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/01-LinqToSql.razor index 99e0648..622043a 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/01-LinqToSql.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/01-LinqToSql.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Defining a model and a DbContext. diff --git a/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/02-NavigationProperties.razor b/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/02-NavigationProperties.razor index 239bb44..5e62dfd 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/02-NavigationProperties.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/02-NavigationProperties.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Defining one-to-many/many-to-many relationships. diff --git a/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/03-Inheritance.razor b/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/03-Inheritance.razor index 31a4256..2bebfdf 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/03-Inheritance.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/03-Inheritance.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Table-per-Hierarchy model. diff --git a/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/04-Tracking.razor b/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/04-Tracking.razor index aedc430..cb9cccf 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/04-Tracking.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/04-Tracking.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Entity changes tracking. diff --git a/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/05-Transactions.razor b/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/05-Transactions.razor index 6be6718..6b88e12 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/05-Transactions.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/05-Transactions.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Default transaction behavior. diff --git a/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/Assignment-EldritchEntities.razor b/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/Assignment-EldritchEntities.razor index 716d1bb..661b217 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/Assignment-EldritchEntities.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/06-EntityFramework/Assignment-EldritchEntities.razor @@ -2,72 +2,68 @@ @inject CourseBook CourseBook; - + Feeble text documents are not enough to convince the Heroes that those Dungeons can be beaten. We need something more tangible, a pattern that can be seen and studied. A schema. - - Overview - + + Overview + The goal of this assignment is to implement the IDungeonRepository interface known from the previous assignment, but using Entity Framework with an SQLite database. - - + + First we will need to define a slightly different model of a graph, more suited for a relational database. This is a common pattern in designing well-decoupled software, where there are multiple models of data suited for different tasks with mappings defined between them. We will also have to deal with LootPile being a pain for a relational model. In the end we will have to save and load our graphs with a well-designed LINQ query. - - Task 0. – Graph Model - + + Task 0. – Graph Model + Our model in DungeonWalker.DataLayer.Serialization requires the vertices to be persisted in an order, since that order is used in the adjacency list encoding. However, to persist an ordered collection in a relational database we need to explicitly define a model that will keep the ordinal number with the elements – there is no built-in solution. - + - + The .NET classes are already defined for you. We will use the same RoomTemplate hierarchy for rooms and loot, but the graph is redefined in DungeonWalker.DataLayer.Sql.Model. All data and navigation properties are already defined, there is no need to modify the types in any way. - - + + You need to define the actual model in DungeonWalkerDbContext.OnModelCreating. It should use TPH inheritance for both rooms and loot. The core entity is Graph<RoomTemplate> and will be the one queried in the repository implementation. Included is an ERD diagram of the expected database schema. - - - After you create a model, generate the migration and call it DungeonGraphs. Then setup a local SQLite database. + + + After you create a model, generate the migration and call it DungeonGraphs. Then setup a local SQLite database. This is done with the following magic spells, ran from the root solution directory: - - - + ")" /> This assumes you have the dotnet-ef tool installed. As a reminder, we install it with: - - - + ")" /> The database is created as DungeonWalker.db in your local user folder. - - - On Windows, it's in your user's AppData\Local directory. - On Linux, it's in the /home/{user}/.local/share directory. - On OSX, it's in the /Users/{user}/.local/share directory. - - + + On Windows, it's in your user's AppData\Local directory. + On Linux, it's in the /home/{user}/.local/share directory. + On OSX, it's in the /Users/{user}/.local/share directory. + + + You can connect to the database from the outside, which should be useful for debugging. I recommend DBeaver, - (Community Edition), an open-source database tool. Its usage is straightforward, the only hard thing is + (Community Edition), an open-source database tool. Its usage is straightforward, the only hard thing is installing the dark mode. - - + + And to drive the point home even more, here's the migration script generated from the expected model. You can get yours with dotnet ef migrations script --project ./src/DungeonWalker.DataLayer.Sql. - + + ")" /> This is NOT tested by any test groups. You can verify soundness manually by consulting the script and ERD diagram. - Task 1. – Loot Pile Issue - + Task 1. – Loot Pile Issue + There's an issue we need to tackle before we save the graph to our database. Currently the loot is a very complex object that can potentially be an arbitrary tree of LootPile entities. Retrieving such a structure from the database is complex. By default we would have to write a recursive query that traverses the levels of the tree. Another way would be to denormalise the database and include enough data that we could extract the entire tree at once. To keep things simple and less tedious, we ditch the idea of nested LootPile objects for the purposes of this assignment. - - + + To do that, you need to ensure two things. First, implement the LootPileLootTemplate.Flatten() method that returns an equivalent, flattened LootPileLootTemplate. Flattened here means that we only leave a single root LootPileLootTemplate and all the leaves of the original tree become immediate children of the pile. @@ -159,8 +155,8 @@ COMMIT; - TwoHandedSword - EssenceOfMagic - HealthPotion 50% - ")"/> - becomes: + ")" /> + becomes: - The order of the objects is arbitrary. Secondly, before saving a graph - you will have to make sure to flatten all the rooms first using LootRoomTemplate.FlattenLootPiles. - + ")" /> + The order of the objects is arbitrary. Secondly, before saving a graph + you will have to make sure to flatten all the rooms first using LootRoomTemplate.FlattenLootPiles. + This is tested by the "LootPileSerialization" test group, worth 0.5 points. - Task 2. – SQL Repository - + Task 2. – SQL Repository + Finish the implementation of DungeonWalker.FileSystem.Sql.SqlDungeonRepository, inheriting from DungeonRepositoryBase defined in the previous assignment. Any exceptions thrown during accessing the database should be rethrown wrapped in SqlRepositoryException. - - + + Hints and warnings: - - - - Remember that the save semantics are "overwrite if exists". For a database that means that you need to - first remove a graph with the given name, if it exists. - - - @{ - var navigation = CourseBook.CSharpCourse["entity-framework"]["navigation-properties"]; - } - The relation model is quite deep. You will need to use stuff described in @(navigation.DisplayName), - including ThenInclude, which you can find in the linked resource: Eager Loading of Related Data. - - - During loading, try to perform as much simplification as possible inside the query. If you need only a list of numbers - of vertices to reconstruct the graph, SELECT only the numbers, not all the edges. If you need - to sort some data, try to sort it on the database instead of locally. - - - To display queries and other diagnostic information from the database, use the DungeonWalkerDbContext's - constructor overload with LogLevel, specifying Information or even Debug. - - - - Here's a small query that can help you dump a saved graph using SQL, so you can verify the database structure from outside C#. - - + + Remember that the save semantics are "overwrite if exists". For a database that means that you need to + first remove a graph with the given name, if it exists. + + + @{ + var navigation = CourseBook.CSharpCourse["entity-framework"]["navigation-properties"]; + } + The relation model is quite deep. You will need to use stuff described in @(navigation.DisplayName), + including ThenInclude, which you can find in the linked resource: Eager Loading of Related Data. + + + During loading, try to perform as much simplification as possible inside the query. If you need only a list of numbers + of vertices to reconstruct the graph, SELECT only the numbers, not all the edges. If you need + to sort some data, try to sort it on the database instead of locally. + + + To display queries and other diagnostic information from the database, use the DungeonWalkerDbContext's + constructor overload with LogLevel, specifying Information or even Debug. + + + + + Here's a small query that can help you dump a saved graph using SQL, so you can verify the database structure from outside C#. + - - This is tested by the "SqlDungeonRepository" test group, worth 1 point. - - Demo - - As before, there is a demo. It's located in a new console project, DungeonWalker.SqlRepositoryDemo. - The expected output is as before, only that there might be some logging information from the database in between. - - - This demo is worth 0.5 points. - - -@code { +ORDER BY rr.OrdinalNumber")" /> + + + This is tested by the "SqlDungeonRepository" test group, worth 1 point. + + Demo + + As before, there is a demo. It's located in a new console project, DungeonWalker.SqlRepositoryDemo. + The expected output is as before, only that there might be some logging information from the database in between. + + + This demo is worth 0.5 points. + + + @code { } diff --git a/src/Sorcery/Pages/Teaching/CSharp/07-ASP.NETCore/00-MinimalHttpServer.razor b/src/Sorcery/Pages/Teaching/CSharp/07-ASP.NETCore/00-MinimalHttpServer.razor index e4ad59d..ad344cc 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/07-ASP.NETCore/00-MinimalHttpServer.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/07-ASP.NETCore/00-MinimalHttpServer.razor @@ -6,16 +6,16 @@ } - + Creating a project for ASP.NET core is a bit different from what we've been doing thus far. We need to use a different SDK and a completely different project type for the entry project. - Navigate to the 07-ASP.NETCore/DungeonWalker.Api directory in + Navigate to the 07-ASP.NETCore/DungeonWalker.Api directory in the notebooks repository and run: - + +dotnet sln add ./DungeonWalker.Api")" /> This will create a bunch of stuff for an example web API. Let's take it step-by-step, first the csproj: @@ -33,16 +33,16 @@ dotnet sln add ./DungeonWalker.Api")"/> - ")"/> - + ")" /> + The switch to ASP.NET Core is signified with the top line, where we use the Web SDK to build the project. This hooks up relevant AspNetCore libraries as implicit dependencies, some analysers, as well as better first-class support from CLI and IDEs. - - + + The rest is basically the same, the referenced Swashbuckle package gives us Swagger support for free – more on that later. Now let's take a look at the core of the project, Program.cs. - + - + ")" /> + Server configuration happens inside a builder. We first add services to the Dependency Injection mechanism, which allows us to resolve implementation of services in a clean manner – more on that also later. AddControllers is a core method that makes our server run. Then a lot of Swagger things follow. In the end, we configure automatic redirection from HTTP to HTTPS (a standard practice for all HTTP accessible servers), configure authorization services (which we won't use), configure routing with MapControllers and run the server. - - + + You can now run the server with dotnet run --project ./DungeonWalker.Api. The output will look something like this, modulo paths and port numbers: - + - + ")" /> + Let's tackle the ports first. They are set in Properties/launchSettings.json. Change the profiles['DungeonWalker.Api'].applicationUrl values to point to ports $10443$ and $10080$ for HTTPS and HTTP, respectively. We can now use curl to get some response from our server. - - - + - + ")" /> This is as expected – HTTP redirects us to HTTPS, and HTTPS endpoint gives us 404, since our server doesn't serve anything on the / route. - - Controllers and Routing - + + Controllers and Routing + Routing is what our server does when it gets a request to determine what code to run to serve the request. The route is the part of the URL after the root server URL, so when making a request to - https://gienieczko.com/teaching/csharp/7-aspnet-core/0-minimal-http-server + https://gienieczko.com/teaching/csharp/7-aspnet-core/0-minimal-http-server the route is '/teaching/csharp/7-aspnet-core/0-minimal-http-server'. The server has to parse that route and, em, route it to the correct handler, which in case of an ASP.NET Core is an action @@ -155,13 +153,12 @@ curl https://localhost:10443 --verbose https://duckduckgo.com/?q=gienieczko&t=h_&ia=web we have the path '/', but also three query parameters: 'q' with value 'gienieczko', 't' with value 'h_', and 'ia' with value 'web'. - - + + In ASP.NET Core the router parses the URL and determines which controller action to call. It can parse the query parameters from the URL and the body of the request (if there is a body), provide it as parameters to the action, and then interpret the return value as the response. Here is the example controller that exists in the generated template: - - - + ")" /> Let's break it down: - - - - The controller is defined as a class inheriting from ControllerBase - with the special ApiControllerAttribute. - - - The RouteAttribute defines how to access the controller. - The special '[controller]' value evaluates to the controller class name - with 'Controller' stripped, so in this case it's 'weatherForecast' - (URL-s are case insensitive). - - - The controller has a ILogger<WeatherForecastController> dependency - that is magically given to it in the constructor. This is Dependency Injection. - - - The controller's action is called Get and returns a sequence of - WeatherForecast objects. The Name is a friendly name - given to an action, e.g. allowing us to create a link to this particular action - just by using the name. It defines no explicit route, thus it is triggered - simply when a GET request is given to the root controller route. - - - + + + The controller is defined as a class inheriting from ControllerBase + with the special ApiControllerAttribute. + + + The RouteAttribute defines how to access the controller. + The special '[controller]' value evaluates to the controller class name + with 'Controller' stripped, so in this case it's 'weatherForecast' + (URL-s are case insensitive). + + + The controller has a ILogger<WeatherForecastController> dependency + that is magically given to it in the constructor. This is Dependency Injection. + + + The controller's action is called Get and returns a sequence of + WeatherForecast objects. The Name is a friendly name + given to an action, e.g. allowing us to create a link to this particular action + just by using the name. It defines no explicit route, thus it is triggered + simply when a GET request is given to the root controller route. + + We can see how this works in action right now, by issuing a GET to this action: - - - + ")" /> Okay, so it works. We have no idea how the controller gets instantiated, though. Does it even initialise the _logger field? Let's try to log something, change the Get body to: - - Get() { @@ -265,51 +256,51 @@ public IEnumerable Get() }) .ToArray(); } - ")"/> - + ")" /> + + Restart the server and run the curl again. You should see the following appear in the server's terminal (modulo the number of days): - - - Middleware - + Randomised 7 days to return...")" /> + + Middleware + The ASP.NET Core server is based around middleware. A request to the server goes through a pipeline of methods that take the request, operate on the response, and call the next one in the sequence, creating a stack of execution. A middleware component can also short-circuit the pipeline and return early. - + - + - + We can write and provide our own middleware to plug-in to this pipeline. This is a very powerful tool, allowing us to customise our server. You could write your own server from first principles using it. Usually we install some preimplemented middleware to get commonly used functionality. Every UseX call in Program.cs is a middleware. There are preimplemented middlewares for common things like cookies, CORS, cache, compression, sessions, etc. There's a ton more available as NuGet packages. - - Cancellation - + + Cancellation + ASP.NET Core injects CancellationToken values corresponding to the outside request into controller actions, as long as they have an appropriate parameter. Supporting cancellation in ASP.NET Core requires simply adding that last CancellationToken cancellationToken parameter to your method. - - + + You should always use it. There's no sense in wasting server resources when the outgoing request was cancelled by the user. - - Summary - + + Summary + We know how to setup the skeleton for an HTTP server, complete with HTTPS support. We know what routing is and how they are resolved into controllers and actions - + + ("https://docs.microsoft.com/en-us/aspnet/core/fundamentals/?view=aspnetcore-6.0", "ASP.NET Core fundamentals overview"), + ("https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-6.0", "ASP.NET Core Middleware"), + }) /> @code { diff --git a/src/Sorcery/Pages/Teaching/CSharp/07-ASP.NETCore/01-DependencyInjection.razor b/src/Sorcery/Pages/Teaching/CSharp/07-ASP.NETCore/01-DependencyInjection.razor index a6c56f5..7e942b9 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/07-ASP.NETCore/01-DependencyInjection.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/07-ASP.NETCore/01-DependencyInjection.razor @@ -6,17 +6,17 @@ } - + This section is going to be a bit more software-engineering focused than before, since Dependency Injection (DI) is a fundamental concept for complex software with many interdependent modules. - - Motivation - + + Motivation + Understanding the principles of DI is done most easily with a motivating example. Consider the incredibly complex problem of adding two numbers together. We want an API endpoint that will return the sum of two numbers that are loaded from a database. Well, let's get cracking. - + - + ")" /> + This is all we need. This is scaffolded in the 07-ASP.NETCore/Adder.Api directory. There's some things already configured to make the code runnable, you only need to run database setup: - - - + ")" /> Now we can curl seeding the database and the sum: - + - + ")" /> + Great, it works! Okay, so now, like good engineers, let's write unit tests. We need to test what happens when both numbers are there, when one of them is null, when repository throws an exception. Oh, and we need a test that will assert that the repository was disposed of. - - + + And obviously we want this to be independent from the database. We're running simple SQLite, but in a normal setting we'd be using Postgres or something, and unit tests need to be fast, easily runnable, repeatable, etc. - - + + How on earth would you do that? - - You can't. - + + You can't. + To manage complexity of programming we use Big Words like Separation of Concerns, or Decoupling. This is probably the most hands-on example of this. We have glued the Sum method so tight to the NumberRepository and AdderDbContext so hard it won't ever go off. It's terrible for, like, infinitely many reasons. - - - - If NumberRepository ever needs a different way to be constructed, - for example we decide to add logging and make the constructor take a logger, - we would have to change every place with new NumberRepository. - Imagine we had hundreds of actions, each instantiating its own NumberRepository... - - - If AdderDbContext ever needs a different way to be constructed we are also screwed. - You can probably see how this extrapolates – the issue becomes more and more pronounced with - every layer underneath us. At some point instantiating a high-level service in a controller - might require new-ing a dozen objects into existence. - - - Why does the AdderController even care about whether NumberRepository - uses a DbContext underneath? What if we decide that we need to hold our numbers in - a cloud storage? Do we go around and change every instantiation of the repository to use the - cloud provider instead of AdderDbContext? - - - For that matter, why does the AdderController even care that AdderDbContext exists. - Again, does our controller need to know about every single type in the project to perform its job? If it - needed a high-level service it'd necessarily know all about all the types below. - - - Why is the controller managing the lifetime of a DbContext? Just asking questions. - - - Finally, and most importantly – there is no way to test this method. None. Any unit test would necessarily have - to construct a database itself and then seed it with data to observe the results. - Changing our code to use a different database immediately invalidates all these tests. - Moreover, it's a nightmare to maintain – unit tests need to be simple and reliable. - - - + + + If NumberRepository ever needs a different way to be constructed, + for example we decide to add logging and make the constructor take a logger, + we would have to change every place with new NumberRepository. + Imagine we had hundreds of actions, each instantiating its own NumberRepository... + + + If AdderDbContext ever needs a different way to be constructed we are also screwed. + You can probably see how this extrapolates – the issue becomes more and more pronounced with + every layer underneath us. At some point instantiating a high-level service in a controller + might require new-ing a dozen objects into existence. + + + Why does the AdderController even care about whether NumberRepository + uses a DbContext underneath? What if we decide that we need to hold our numbers in + a cloud storage? Do we go around and change every instantiation of the repository to use the + cloud provider instead of AdderDbContext? + + + For that matter, why does the AdderController even care that AdderDbContext exists. + Again, does our controller need to know about every single type in the project to perform its job? If it + needed a high-level service it'd necessarily know all about all the types below. + + + Why is the controller managing the lifetime of a DbContext? Just asking questions. + + + Finally, and most importantly – there is no way to test this method. None. Any unit test would necessarily have + to construct a database itself and then seed it with data to observe the results. + Changing our code to use a different database immediately invalidates all these tests. + Moreover, it's a nightmare to maintain – unit tests need to be simple and reliable. + + + + I could go on for days about how terrible this is. The core issue of new and constructors is that they are completely static, just like static methods would be. All static code that our method depends on are hard baked in there. From the perspective of a unit test it's virtually the same as if we copy-pasted the definition of the static method into our method body. - - + + No, really, just look at the wall of text above. Think how many responsibilities the GetSumAsync method has. It's a good thing it doesn't manage the TCP socket that the request came in as well... That code is utter garbage, and I can say that because I wrote it myself. - - + + This is not the Jedi way. We figured that out waaay before – everything here is violating the fifth SOLID principle, Dependency Inversion, which states simply depend on abstractions, not on concretions. Abstractions we can reason about. Abstractions we can plug in and out. Abstractions clearly define their contracts. @@ -131,13 +129,13 @@ curl https://localhost:20443/adder/sum repository – this effectively means that we can pass the repo as an argument to the GetSumAsync method. This is not enough still, as NumberRepository is a concrete class – we still can't really test this method without coupling tightly to whatever it does under the hood, in this case to an SQLite database. - - Dependency Inversion - + + Dependency Inversion + Use abstraction. Extract the contract of a repository and Invert the Dependency. Think about it. Right now the controller tells you what to do. It says "give me a NumberRepository or I won't do my job." That's unacceptable. You're the boss. You tell it what repository it will work on and it better be happy with that choice! - + - + ")" /> + Now the controller gets some implementation of INumberRepository, which has an appropriate GetNumberAsync method, and it has to work with that. It doesn't, nor should it, care about what class exactly that is. - - + + If you're a functional programmer you might appreciate an analogy between the AdderController class and a function. The beautiful effect of pure FP is that everything is easily testable, as the only things that a function has access to are its parameters. It's perfectly isolated from everything else. @@ -177,11 +175,10 @@ public class AdderController : ControllerBase to its constructor. And, as mentioned above, the thing that breaks this beauty on a fundamental level is anything static, as these are dependencies that we don't pass as arguments, they are arbitrarily hardwired in by the class itself. - - - And now testing is a simple excercise: - - + + And now testing is a simple exercise: + Task.CompletedTask; } } - ")"/> - + ")" /> I don't know how you feel about it, but for me that's a little bit too much code. We'll talk about how to make our tests slicker later on. - - Dependency Injection with a Inversion of Control Container - + + Dependency Injection with a Inversion of Control Container + We have redefined the controller to take an abstract dependency as a parameter, but we still need to Inject the Dependency somehow. This is done with an Inversion of Control container, where IoC refers to the pattern we described above – it's the outside that control what implementation the controller uses, not the controller. - - + + The IoC container takes responsibility of resolving dependencies required by constructors and managing lifetimes of the instantiated services. When a service is requested, the IoC resolves the dependency graph by going through the constructors and finding registered implementations of required parameters. It then registers the instances internally to monitor their lifetime. In the standard ASP.NET Core IoC, also called by its namespace, Microsoft.Extensions.DependencyInjection, there are three lifetime types: - - - - Transient – a new instance gets created every time the service is requested. - - - Scoped – an intance is created per scope. By and large it means one per - HTTP request, but one can create more granular scopes manually. - - - Singleton – one instance gets created and reused throughout the application's - lifetime. - - - + + + Transient – a new instance gets created every time the service is requested. + + + Scoped – an intance is created per scope. By and large it means one per + HTTP request, but one can create more granular scopes manually. + + + Singleton – one instance gets created and reused throughout the application's + lifetime. + + + + Singletons should be obviously avoided. Turning stateless objects into singletons can be a memory optimisation, but only after measuring that it actually makes a difference. Most services are best as transient. Some require keeping state throughout a single request – for example DbContexts. We want the entity tracking feature to work for the entire request across services to maintain consistency. - - + + We register the dependencies in Program.cs. - - (); builder.Services.AddTransient(); - ")"/> - + ")" /> We add the DbContext as a concrete class with scoped lifetime. This is expected, since we really do need the concrete database to work on. The NumberRepository is registered as a transient implementation of INumberRepository. Now we can actually run the server and get our two-plus-three result from refactore code. - - + + And... that's it. Everything automagically works for us when we register the dependencies. This is the flow of work for designing new classes – take abstract dependencies in the constructor, register newly created services into the IoC, shabang, clean code. - - Summary - + + Summary + We've braced together through my software engineering diatribe to find out how to solve tight coupling problems with Dependency Inversion and IoC containers. - + + ("https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-6.0", "Dependency injection in ASP.NET Core"), + ("https://ardalis.com/new-is-glue/", "New is Glue"), + ("https://web.archive.org/web/20110714224327/http://www.objectmentor.com/resources/articles/dip.pdf", "The Dependency Inversion Principle (Uncle Bob)"), + ("https://martinfowler.com/bliki/InversionOfControl.html", "Inversion of Control (Martin Fowler)"), + }) /> @code { diff --git a/src/Sorcery/Pages/Teaching/CSharp/07-ASP.NETCore/02-Swagger.razor b/src/Sorcery/Pages/Teaching/CSharp/07-ASP.NETCore/02-Swagger.razor index 9857ccb..019aa6d 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/07-ASP.NETCore/02-Swagger.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/07-ASP.NETCore/02-Swagger.razor @@ -6,33 +6,33 @@ } - + Swagger is a generic specification for describing REST APIs, allowing both humans and computers to easily grasp the capabilities of an API. The newest versions of Swagger are called OpenAPI, but Swagger is catchier so it stuck. - - Swagger UI - + + Swagger UI + Swagger was already setup for us in Program.cs. Navigating to localhost:20443/swagger will show us the extremely exciting Adder API. - + - + The best part of the UI is the Try it out button, which allows us to send a request to the API from a much nicer interface than curl. - + - Swagger JSON - + Swagger JSON + The UI is nice for humans and testing the API, but for communicating with other systems the important part is the underlying swagger.json, available at localhost:20443/swagger/v1/swagger.json. - + - Additional metadata - + Additional metadata + We can configure the metadata in the builder.Services.AddSwaggerGen call. The most important thing we can do is include XML comments from our C# code. - + { @@ -123,10 +123,10 @@ builder.Services.AddSwaggerGen(options => - Summary - + Summary + We've met Swagger, which is the standard UI we will use to test and debug our APIs. - + - + Interesting API endpoints usually take some data. That data comes from multiple sources in an HTTP request – the route, query parameters, request body, etc. Converting between the incoming data and .NET types to use in actions is called model binding. - - Simple GET by ID - + + Simple GET by ID + We have the DungeonWalker.Api solution setup with the example Dungeon database from @entityFrameworkModule.DisplayName. Let's setup a standard query-by-id endpoint. - + /// Get a Dungeon by ID. @@ -43,9 +43,8 @@ public async Task> GetDungeonAsync(int id) return dungeon.ToView(); } ")" /> - + A few things here: - The RouteAttribute specifies a parameter named id @@ -63,19 +62,18 @@ public async Task> GetDungeonAsync(int id) In this case we return a 404 Not Found if a Dungeon with given ID does not exist. - You can test out the endpoint in Swagger, remember to run the seed endpoint first. - + - Swagger – return values - + Swagger – return values + If we look at the Swagger docs of this new endpoint we will only see status code 200 documented. To inform Swagger that there is a different return code we just need to add one doc line to the XML comment: - + @@ -93,10 +91,10 @@ public async Task> GetDungeonAsync(int id) [ProducesResponseType(404)] // <-- public async Task> GetDungeonAsync(int id) ")" /> - Parameters from query - + Parameters from query + Let's create a more complex query method that will fetch some parameters from the query string. - + > QueryRunsAsync(DungeonRunQueryParameters parameters) { @@ -144,7 +142,7 @@ public async Task>> QueryRunsAsync( return dungeonRuns.Select(d => d.ToView()).ToList(); } ")" /> - + The parameters in the method get mapped from the query. So if we query https://localhost:10443/runs?dungeonName=Adventure&heroClass=Warrior @@ -157,18 +155,18 @@ public async Task>> QueryRunsAsync( it will map to dungeonName = null, heroClass = "Warrior". If we omit both, https://localhost:10443/runs, both will map to null. - - Mapping complex types - + + Mapping complex types + We can also map to complex types by putting the attributes on its properties and then using that complex type as the argument. So by putting FromQueryAttribute on the DungeonRunQueryParameters.DungeonName and DungeonRunQueryParameters.HeroClass properties, then we could just use DungeonRunQueryParameters as the parameter. - - + + A more interesting case would be a POST request with a body. Let's allow users to post new runs into the system. - + /// Record a DungeonRun in the system. @@ -193,12 +191,12 @@ public async Task PostAsync([FromBody] DungeonRunPost dungeonRun) } } ")"/> - + The rest is boring implementation details. First, note CreatedAtAction – it allows us to return a 201 Created response with a link to the resource just created based on an action. Two, note the standardised response based on Problem Details. Now we can play with the requests: - + curl -X POST https://localhost:10443/Dungeon/seed -i HTTP/1.1 200 OK @@ -243,9 +241,9 @@ Transfer-Encoding: chunked {""type"":""https://tools.ietf.org/html/rfc7231#section-6.5.1"",""title"":""Bad Request"",""status"":400,""detail"":""Hero class Bicycle Repairman does not exist."",""traceId"":""00-fc21637a1c5f26a341c08814f0a38521-5c2aa73f624971c6-00""} ")" /> - + Okay, at the end, let's look at the implementation of the repository method. - + > CreateDungeonRunAsync(DungeonRunPost dungeonRun) { @@ -277,16 +275,16 @@ public async Task> CreateDungeonRunAsync(DungeonRunPost du static Result Fail(ApiError error) => Result.Failure(error); } ")"/> - + I pulled a sneaky on ya here. I installed the CSharpFunctionalExtensions package to get access to the Result types. You should definitely check out that package, as it makes handling errors much easier. - - Summary - + + Summary + We've learnt how to map values from the outside to our .NET models be it from route, query or body. We also know how to document return values for API endpoints. - + - + Let's talk about logging. Collecting structured logs is very important to be able to make sense of a deployed application. When we have the software locally debugging is easy, we can plug in the debugger and see everything. On production you have nothing aside from the logs you configure. - - Injecting Loggers - + + Injecting Loggers + By default, Microsoft.Extensions.Logging is used as the logging provider. To get a logger we depend on ILogger<T>, where T is the type to which we are injecting the logger. - + _logger; public DungeonController(IDungeonRepository repository, ILogger logger) => (_repository, _logger) = (repository, logger); ")" /> - + This might seem unintuitive, but the idea is that we want a logger that specifically knows what type is using it so that it can include that metadata in the logs. That's why all the logs you see in the output start with - + - + This is called a log category. We can create more granular categories by injecting ILoggerFactory instead and calling CreateLogger. To log something quick-and-dirty we call Log with an appropriate LogLevel, or, more conveniently, call LogX where X is the level. - + - + There are 6 logging levels. - - - - Trace – most detailed information used for tracking down hard-to-find bugs. - - - Debug – detailed development logs; should not be enabled on production. - - - Information – general flow of the application. - - - Warning – unexpected conditions that should be brought to attention, but - don't cause direct failure. - - - Error – a failure condition for the current operation. - - - Critical – a failure condition for the entire application that should cause - the server administrator to be woken up in the middle of the night. - - - + + + Trace – most detailed information used for tracking down hard-to-find bugs. + + + Debug – detailed development logs; should not be enabled on production. + + + Information – general flow of the application. + + + Warning – unexpected conditions that should be brought to attention, but + don't cause direct failure. + + + Error – a failure condition for the current operation. + + + Critical – a failure condition for the entire application that should cause + the server administrator to be woken up in the middle of the night. + + We set a filter on the log level for various categories. A filter causes all messages on levels before the filter to not be included. So setting it to Information filters out all Debug and Trace messages. - - Structured Logging - + + Structured Logging + Did I mention logging is extremely important? This is the primary way of getting information about your app's execution when it's deployed on production. Logging is the debugging idea we are all familiar with – stick a bunch of print statements into the code and then try to make sense of the output – only made more sustainable. - - + + Printing plain string values is pretty much useless – it just creates noise that makes debugging maybe slightly more easy, but not really. The revolutionary idea here is to not be logging plain strings. Instead, we give them structure. A structured log is an object that contains a number of named properties and a string template. Every log is given an identity, so that we can group different outputs from the same log statement together. For example, instead of doing this: - - + ")" /> We do this: - - - + ")" /> And then in our log collection system instead of seing pure strings we might see values like these: - - - +} + ")" /> Additionally, messages can be grouped into scopes. We start a scope by calling _logger.BeginScope and passing data to it. It returns an IDisposable - representing the scope. The scope ends when that sentinel is disposed of. - ASP.NET Core gives us a lot of additional scope information for free out of the box. - + representing the scope. The scope ends when that sentinel is disposed of. + ASP.NET Core gives us a lot of additional scope information for free out of the box. + - + ")" /> + But, as a programmer, we will see this in our output: - - SpanId:e72af56e1c49e3ac, TraceId:b5aa403711efc425c5bf3db232a66581, ParentId:0000000000000000 => ConnectionId:0HMHSLC0RMD99 => RequestPath:/Dungeons/Example RequestId:0HMHSLC0RMD99:0000000F => DungeonWalker.Api.Controllers.DungeonsController.GetAsync (DungeonWalker.Api) => DungeonController.GetAsync(Example) Not found: Example - ")"/> - - However, going to our log system we can now look at a specific Error log and say "okay, show me all the - logs for this TraceId that happened before the error". Maybe it turns out that when we look at all error messages + ")" /> + + + However, going to our log system we can now look at a specific Error log and say " + okay, show me all the + logs for this TraceId that happened before the error + ". Maybe it turns out that when we look at all error messages there are many coming from the same request path. And then when we do some simple SQL queries asking for the previous actions with the same TraceId it turns out that most of those errors have the same flow of execution leading to the error. Basically, we enable data science on our logs. One does not fully appreciate this power until something breaks and they need to debug it. - - Typesafe and Performant Logs with LoggerMessage - + + Typesafe and Performant Logs with LoggerMessage + Doing logs with the LogX methods has issues. One, it doesn't validate whether we use structured logging the correct way. When your logs are structured, there is an obvious schema of named properties that every message with given structure should follow. Moreover, every time we put a message into LogX it needs to be parsed to determine the structure. - - + + To help with both of these issues, ASP.NET Core introduces the static LoggerMessage class. It allows us to create a precompiled delegate that takes strongly-typed values for the named parameters of a structured log message. The typing solves the type safety issue, while the fact that they are compiled and then reused solves the performance issue. Usually, the way to utilise this is to create an extension class for ILogger and put the message configuration there statically. Then every structured message becomes a special extension method. The same is true for scopes. For example: - + _returningDungeon(logger, dungeon, null); } - ")"/> - + ")" /> + You should prefer defining your logs this way. It's a nice all-in-one solution, it makes sure your logs are structured, that the values are sensible and have correct types, and decouples the logging logic from the actual program flow. - - Summary - + + Summary + We've learnt how to instrument our applications with structured logs in ASP.NET Core. - + + ( + "https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-6.0", + "Logging in .NET Core and ASP.NET Core" + ), + ( + "https://andrewlock.net/defining-custom-logging-messages-with-loggermessage-define-in-asp-net-core/", + "LoggerMessage.Define (Andrew Lock)" + ) + }) /> @code { diff --git a/src/Sorcery/Pages/Teaching/CSharp/07-ASP.NETCore/Assignment-AdventureProgrammingInterface.razor b/src/Sorcery/Pages/Teaching/CSharp/07-ASP.NETCore/Assignment-AdventureProgrammingInterface.razor index 2ece1a6..b435abb 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/07-ASP.NETCore/Assignment-AdventureProgrammingInterface.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/07-ASP.NETCore/Assignment-AdventureProgrammingInterface.razor @@ -2,62 +2,62 @@ @inject CourseBook CourseBook; - + Sending Heroes on Adventures manually is tedious. Dozens of people have to work to equip them, prepare provisions, cheer them on their way... We need a more automated solution, or we'll never reach get rid of all the enemies! - - Overview - + + Overview + We will be implementing an ASP.NET Core API for our project. Simple enough. We need to define the controllers, integrate the, at this point legacy, game engine with the API, and add some logging to allow easy debugging. - - Task 0. – Tour de Code - + + Task 0. – Tour de Code + First, let's examine what is already there. Take a look at the models defined in DungeonWalker.Api.Model. They are simple objects meant to be returned by the API. We have two controllers, the DungeonsController, responsible for listing Dungeon data, and RunsController, that allows us to play the game and examine the highscores. - - + + Underneath, the well known IDungeonRepository provides the Dungeons, while IRunsRepository is a new addition that works on Runs. It is partially implemented in DungeonWalker.DataLayer.Sql.SqlRunsRepository. Partially, because I couldn't take away the pure joy of writing another LINQ query from you in GetBestRunsAsync. This part is untested, but you should start there. To get the database running you will need to apply the new migration that builds the DungeonRun tables. - + - + The IHeroRepository has one trivial implementation that just returns the classes we defined way back in the first assignment. Finally, the IGameEngine interface allows us to run the game. Its implementation is non-trivial, and uses IRandomNumberGenerator to abstract randomness away. Feel free to examine the code, although you don't need to change anything here. - - + + All of this is glued together in DungeonWalker.Dependencies. This is a pretty common architecture, where we separate different parts of the system into many projects, and then make the single Dependencies project depend on all of them to register them for Dependency Injection. That way our API never has to reference any of the low-level implementation projects, like DungeonWalker.DataLayer.Sql. - + Task 1. – DungeonsController - + Implement the actions ListNamesAsync and GetAsync. Remember to use structured logging for easier debugging. All logging messages I deemed necessary are defined in DungeonWalker.Api.Extensions.ILoggerExtensions, although you are free to define your own. Just remember to follow structured logging principles. - + This is tested by the "DungeonsController" test group, worth 1 point. Task 2. – RunsController - + Implement the actions GetBestRunsAsync and PlayAsync. Remember to use structured logging for easier debugging. PlayAsync is the tricky part, as it requires coordinating all the things we've built thus far. You need to use the IGameEngine to run the game, but that requires you to fetch the layout and Hero first. You need to then save the run, which could fail by itself. But once this is done, you can play DungeonWalker from the Swagger interface! Just remember to hit the seed endpoint first. - + This is tested by the "RunsController" test group, worth 1 point. diff --git a/src/Sorcery/Pages/Teaching/CSharp/07-ASP.NETCore/Introduction.razor b/src/Sorcery/Pages/Teaching/CSharp/07-ASP.NETCore/Introduction.razor index 8cd2dcf..dd6c7aa 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/07-ASP.NETCore/Introduction.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/07-ASP.NETCore/Introduction.razor @@ -2,21 +2,21 @@ @inject CourseBook CourseBook; - + In this module we will meet ASP.NET Core, the leading framework for creating web applications in .NET. This module will focus on creating web APIs, but there are many ways to create interactive web pages as well, with the cutting-edge being Blazor, allowing us to create SPA applications in C#, without writing any JavaScript. - - + + Unfortunately, we cannot use notebooks for this module. Running ASP.NET Core requires a different SDK than regular .NET programs and cannot be easily emulated from a notebook. However, the notebooks repository will contain some library code for you to use with the ASP.NET Core project, so it's best to work from there. - - + + In this module we will learn: - + setting up a minimal HTTP server; diff --git a/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/00-TypesAndInstances.razor b/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/00-TypesAndInstances.razor index 4e0c86b..9156fbd 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/00-TypesAndInstances.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/00-TypesAndInstances.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover typeof and GetType. diff --git a/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/01-Members.razor b/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/01-Members.razor index 4bea3c2..8181ecd 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/01-Members.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/01-Members.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover BindingFlags. diff --git a/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/02-Attributes.razor b/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/02-Attributes.razor index 22faa15..f4c8744 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/02-Attributes.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/02-Attributes.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Obtaining attribute metadata. diff --git a/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/03-SystemAttributes.razor b/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/03-SystemAttributes.razor index 64d1e32..0243e39 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/03-SystemAttributes.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/03-SystemAttributes.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Attributes interpreted by the compiler at callsites. diff --git a/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/04-Dynamic.razor b/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/04-Dynamic.razor index 0b202cf..b3ebbcd 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/04-Dynamic.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/04-Dynamic.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover The dynamic type. diff --git a/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/05-AdvancedUnitTesting.razor b/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/05-AdvancedUnitTesting.razor index e36382a..a3017ee 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/05-AdvancedUnitTesting.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/05-AdvancedUnitTesting.razor @@ -13,7 +13,7 @@ that project. Familiarise yourself with the ClassLib.Service class, it has one short method that we will be testing. - Testing for Exceptions + Testing for Exceptions We would first like to test that passing null to the method results in an ArgumentNullException. All test frameworks @@ -43,7 +43,7 @@ To test our service we need an instance of IRepository. We could create an empty implementation ourselves, but it's about time to learn about mocking. - Mocking + Mocking Mocking is the general solution to the problem of dependencies. When unit testing a service we want to mock all the dependencies to get them out of the picture, and focus the @@ -129,7 +129,7 @@ public void QueryAsync_GivenNull_ThrowsArgumentNullExceptionSynchronously() This one fails. Convert the method in Service to correctly throw the precondition sychronously before carrying on. - Happy path + Happy path We can now test the happy path of our code by using the Returns mocking method. @@ -151,7 +151,7 @@ public async Task QueryAsync_GivenIdOfExistingItem_ReturnsSuccessWithItem() You should be able to write a test for when the repository returns null by yourself now. - Synchronous Failure + Synchronous Failure The last bit is to write a test for when the repository fails with an exception. The case for a synchronous exception is very easy. @@ -171,7 +171,7 @@ public async Task QueryAsync_GivenIdOfExistingItem_ReturnsSuccessWithItem() Assert.Equal(exception, result.Error); } ")"/> - Asynchronous completion + Asynchronous completion Now, if we want to test an asynchronous case there is a small helper method, Task.Yield, to help us. It returns a task that does nothing except forcing us to @@ -218,7 +218,7 @@ public async Task QueryAsync_WhenRepositoryThrowsAsynchronousException_ReturnsFa You should now write a test for asynchronous null return by yourself. - How It's Done + How It's Done Under the hood, NSubstitute uses reflection magic to achieve its effects. Not only does it examine the contents of the abstract type that we are trying to mock @@ -237,7 +237,7 @@ public async Task QueryAsync_WhenRepositoryThrowsAsynchronousException_ReturnsFa There are facilities for creating types, members of those types, and creating methods on-the-fly by directly emitting IL. This is also the core namespace used by Source Generators. - Advanced Scenarios + Advanced Scenarios Sometimes more advanced scenarios need to be supported by mocking frameworks. We can configure what happens when a method is called with given arguments, diff --git a/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/Assignment-AdventurersAssemble.razor b/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/Assignment-AdventurersAssemble.razor index 92be58a..7011e9b 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/Assignment-AdventurersAssemble.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/08-Reflection/Assignment-AdventurersAssemble.razor @@ -2,111 +2,111 @@ @inject CourseBook CourseBook; - + With plenty of Dungeons to clear, our Heroes are spread thin! Reinforcements are needed from outside lands. As new candidates come while others are adventuring, will you help them assemble into a formidable party? - - Overview - + + Overview + We will be implementing a simple mod system for our game that will allow users to provide their own Hero classes in separate assemblies. - - Task 0. – Goal - + + Task 0. – Goal + In DungeonWalker.Extensibility we defined a HeroClassAttribute. It can be applied to any Hero class to mark it as one of Dungeon Walker Heroes. It has a single required property, Name, which will uniquely define the Hero (it can be different from the display name). - - + + All of the work needs to happen in DungeonWalker.DataLayer.Dynamic.HeroRepository. It should allow us to load a given type into the repository, or load all Hero types in a given assembly. - To be approprietly loaded, a type needs to: - - - - be tagged with the HeroClassAttribute; - - - be a subclass of DungeonWalker.Logic.Characters.Hero; - - - have a parameterless constructor; - - - have a name specified in the attribute not conflicting with any previously loaded Hero. - - - + To be appropriately loaded, a type needs to: + + + be tagged with the HeroClassAttribute; + + + be a subclass of DungeonWalker.Logic.Characters.Hero; + + + have a parameterless constructor; + + + have a name specified in the attribute not conflicting with any previously loaded Hero. + + + + To test this, we have an assembly completely outside of our main code. It is located in the dynamic directory. Two correct Hero subclasses are defined there, Valid.Paladin and Valid.BountyHunter. In the Invalid namespace there are types that do not satisfy one or more of the above requirements. As a first setup thing, compile this project and put the artifacts in a directory that will be used by the rest of the code: - - - + ")" /> + + It will compile the assembly and put it in dynamic/bin/Outside.Heroes.dll. Note that there is no dependency from any of the projects in src to this one. The Outside.Heroes does depend on DungeonWalker.Extensibility and DungeonWalker.Logic to define the Heroes, but Dungeon Walker has no idea that Outside exists. - - + + The HeroRepository, when constructed, loads all Hero classes defined in the base game, and then uses configuration options to dynamically load outside assemblies from a path. Details of how assembly loading works are not really riveting, but if you're interested in them then you can read this article. In the end, we get an Assembly object that we can then query for types. - - Task 1. – HeroRepository - + + Task 1. – HeroRepository + Implement the missing functions: - - - - GetHeroAsync - - - LoadAllHeroesFromAssembly - - - LoadHeroClass - - - + + + GetHeroAsync + + + LoadAllHeroesFromAssembly + + + LoadHeroClass + + + + The get method will probably not be truly asynchronous, as a reminder, Task.FromResult is used to produce Task instances from synchronous results. The name used to query for Heroes is the one provided in their HeroClassAttribute. - - + + Your solution should handle errors in two ways. First of all, the associated _logger instance has extension methods defined for all of the failure cases we defined: - - - - FailedToLoadHeroClassDueToNoAttribute - - - FailedToLoadHeroClassDueToLackOfAParameterlessConstructor - - - FailedToLoadHeroClassDueToNotSubclassingHero - - - FailedToLoadHeroClassDueToConflictingName - - - + + + FailedToLoadHeroClassDueToNoAttribute + + + FailedToLoadHeroClassDueToLackOfAParameterlessConstructor + + + FailedToLoadHeroClassDueToNotSubclassingHero + + + FailedToLoadHeroClassDueToConflictingName + + + + You need to log these failures when they occur (there are tests for that this time around). If they happen when loading all types from an assembly, these should be soft errors – we don't want our application to crash because of mistakes from an outside assembly. On the other hand, when a type is being loaded through LoadHeroClass(Type), you need to throw a DynamicLoaderException. - - + + Note that your implementation should be relatively efficient. We assume that we will load the assemblies once, but create the Heroes potentially many times. A solution that just stores the ConstructorInfo and calls it every time a Hero is requested @@ -114,38 +114,34 @@ dotnet publish ./dynamic/Outside.Heroes -o ./dynamic/bin (see Expression.New), or use a helper function that will call the constructor of a given T: new(), take its MethodInfo and compile it with CreateDelegate. - + This is tested by the "HeroRepository" test group, worth 1.5 points. - Task 2. – API tests - + Task 2. – API tests + To test these features, use the /heroes endpoint that returns a list of all Hero names from the system. Because dependencies are lazily resolved, you need to call this endpoint for any loading of the Outside.Heroes assembly to happen. Use the logs to make sure the code does what you want it to do. - - + + Finally, we can play the game from the console by running the main project: - - - + ")" /> This requires the API to be running in the background (easiest way is to run it in a separate terminal session). You can also list the Heroes from there by calling - - - + ")" /> This will be tested with the test_dynamic_heroes script located in the main directory. It clears out the database, seeds it again, and then calls the /heroes endpoint. The expected output is: - - + ")" /> + This test is worth 0.5 points. diff --git a/src/Sorcery/Pages/Teaching/CSharp/09-Performance/00-GarbageCollectionDetails.razor b/src/Sorcery/Pages/Teaching/CSharp/09-Performance/00-GarbageCollectionDetails.razor index 552d384..7dc2081 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/09-Performance/00-GarbageCollectionDetails.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/09-Performance/00-GarbageCollectionDetails.razor @@ -14,7 +14,7 @@ @previousGcSection.DisplayName. Before we talk about performance, however, we need to dive a little bit deeper into GC internals. - Talkin' 'bout My Generation + Talkin' 'bout My Generation The .NET GC is a generational GC. It divides the managed heap into three chunks called generations (and the LoC, explained below), that are collected separately. @@ -93,7 +93,7 @@ We cover that in @arrayPoolingSection.DisplayName. - Finalizer + Finalizer The dreaded finalizer is a piece of code that is supposed to run at the end of an instance's lifetime. Most objects don't have finalizers, and thus when they go out of scope the GC simply murders them and @@ -102,7 +102,7 @@ Finalizers are hairy and scary, and you should almost never use them. We talk about their intended usage in @finalizersSection.DisplayName. - Summary + Summary More allocations – more collections. Decreasing allocations can improve performance by diff --git a/src/Sorcery/Pages/Teaching/CSharp/09-Performance/01-Benchmarking.razor b/src/Sorcery/Pages/Teaching/CSharp/09-Performance/01-Benchmarking.razor index d6cb7be..61cafe4 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/09-Performance/01-Benchmarking.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/09-Performance/01-Benchmarking.razor @@ -28,7 +28,7 @@ and say "see, I improved performance by $67\%$". - BenchmarkDotNet + BenchmarkDotNet C# is a managed language, which makes things both easier and harder. It's easier to profile our code because the runtime can support the tooling better than as if we were running compiled machine code. @@ -99,7 +99,7 @@ The .NET team itself uses it to measure performance gains in the BCL, in ASP.NET Core, etc. When you want to prove to someone that something is faster, the BenchmarkDotNet results table is the way. - Case Study + Case Study In the notebook repository you can find the CaseStudy directory with an example algorithm that we'll be playing around with and measure. The problem statement is simple: given a pattern $p$ and an input string $w$, @@ -166,7 +166,7 @@ GlobalSetupAttribute. Any code that is not germane to the benchmarked logic but is required to setup the experiment should go here. In this case we read the input file and apply the RemoveMode. - Running the benchmark + Running the benchmark To run the benchmark we need to compile our code in the Release configuration, which enables optimisations and disables debugging support – in Debug configuration the compiler inserts a lot of production-irrelevant @@ -240,7 +240,7 @@ Intel Core i5-8600K CPU 3.60GHz (Coffee Lake), 1 CPU, 6 logical and 6 physical c - Testing Optimisations + Testing Optimisations Let's make an optimisation to our FromShortestSearch algorithm. We can make it skip over things more – if we match a short substring it makes @@ -251,7 +251,7 @@ Intel Core i5-8600K CPU 3.60GHz (Coffee Lake), 1 CPU, 6 logical and 6 physical c it is slightly faster for the $0.0$ case and it allocates much less memory for $0.75$. - Benchmarking Golden Rules + Benchmarking Golden Rules Use vetted tools that deal with the benchmark infrastructure for you. @@ -289,7 +289,7 @@ Intel Core i5-8600K CPU 3.60GHz (Coffee Lake), 1 CPU, 6 logical and 6 physical c when optimising stuff for the fun of it or for science you're free to do whatever you want. - Summary + Summary Use BenchmarkDotNet for benchmarking. Enough said. diff --git a/src/Sorcery/Pages/Teaching/CSharp/09-Performance/03-RefStructs.razor b/src/Sorcery/Pages/Teaching/CSharp/09-Performance/03-RefStructs.razor index 1a385de..f4d5476 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/09-Performance/03-RefStructs.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/09-Performance/03-RefStructs.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover slices – Span<T> and Memory<T>; diff --git a/src/Sorcery/Pages/Teaching/CSharp/09-Performance/04-TheInModifier.razor b/src/Sorcery/Pages/Teaching/CSharp/09-Performance/04-TheInModifier.razor index 051209b..caf97b2 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/09-Performance/04-TheInModifier.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/09-Performance/04-TheInModifier.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover passing by immutable reference with in; diff --git a/src/Sorcery/Pages/Teaching/CSharp/09-Performance/05-ArrayPooling.razor b/src/Sorcery/Pages/Teaching/CSharp/09-Performance/05-ArrayPooling.razor index 83f50e2..b32fbc7 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/09-Performance/05-ArrayPooling.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/09-Performance/05-ArrayPooling.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover the core ArrayPool<T>; diff --git a/src/Sorcery/Pages/Teaching/CSharp/09-Performance/06-Unsafe.razor b/src/Sorcery/Pages/Teaching/CSharp/09-Performance/06-Unsafe.razor index f91dfcd..8c5c06f 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/09-Performance/06-Unsafe.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/09-Performance/06-Unsafe.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover unsafe contexts; diff --git a/src/Sorcery/Pages/Teaching/CSharp/09-Performance/07-Finalizers.razor b/src/Sorcery/Pages/Teaching/CSharp/09-Performance/07-Finalizers.razor index 1d09898..75c39e1 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/09-Performance/07-Finalizers.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/09-Performance/07-Finalizers.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover why we should not use finalizers; diff --git a/src/Sorcery/Pages/Teaching/CSharp/09-Performance/Assignment-RapidReconnaissance.razor b/src/Sorcery/Pages/Teaching/CSharp/09-Performance/Assignment-RapidReconnaissance.razor index 03889cd..d7ea5bb 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/09-Performance/Assignment-RapidReconnaissance.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/09-Performance/Assignment-RapidReconnaissance.razor @@ -2,64 +2,93 @@ @inject CourseBook CourseBook; - + To prepare for excursions our Heroes send small recon squads. They report on the Dungeon's size, enemy numbers, and how shiny the loot inside is. Recon is accurate, but not too swift. The Heroes are tired of waiting – we need to work on that! - - Overview - + + Overview + We will be optimising a single algorithm evaluating a Dungeon. It is defined recursively, taking evaluations of rooms that have incoming edges to the current one, and looking at the room's type and contents. Your goal will be to optimise it, which we will measure with DungeonWalker.Evaluation.Benches. - - Task 0. – Optimisation - + + Task 0. – Optimisation + The project defining our algorithm is DungeonWalker.Evaluation. The types to look at are GraphDungeonEvaluator and Value. This is a very open-ended task – your job is to decrease the ratio of the benchmark for these types with respect to the baseline (GraphDungeonEvaluatorBaseline and ValueBaseline). Here's what you may and may not do to achieve that. - - - - You may not change the benchmark, GraphDungeonEvaluatorBaseline, ValueBaseline, - or SprawlingStronghold code. - - - You may not change the signature of the static Evaluate method. - - - You may not change the equality contract or the primary constructor of Value. - - - You may not break any tests in the solution. - - - You may change GraphDungeonEvaluator and Value implementations - in any way except for the exclusions above. - - - You may change the implementations of other graph algorithms in the solution, like - the TopologicalSort. - - - + + + You may not change the benchmark, GraphDungeonEvaluatorBaseline, ValueBaseline, + or SprawlingStronghold code. + + + You may not change the signature of the static Evaluate method. + + + You may not change the equality contract or the primary constructor of Value. + + + You may not break any tests in the solution. + + + You may change GraphDungeonEvaluator and Value implementations + in any way except for the exclusions above. + + + You may change the implementations of other graph algorithms in the solution, like + the TopologicalSort. + + + + Anything else is allowed, including unsafe code. To run the benchmark use: - + - + ")" /> + The results of the model solution on this bench are as follows: - + - MethodMeanErrorStdDevRatioGen 0Gen 1Allocated - -BaselineEvaluator2.562 s0.0208 s0.0194 s1.007000.00002000.0000172 MB -NewEvaluator1.793 s0.0028 s0.0026 s0.701000.0000-41 MB - + + + Method + Mean + Error + StdDev + Ratio + Gen 0 + Gen 1 + Allocated + + + + + BaselineEvaluator + 2.562 s + 0.0208 s + 0.0194 s + 1.00 + 7000.0000 + 2000.0000 + 172 MB + + + NewEvaluator + 1.793 s + 0.0028 s + 0.0026 s + 0.70 + 1000.0000 + - + 41 MB + + Your solution will be graded for style, as always, but the $3.0$ points usually given for tests will be given based on the optimisations ratio @@ -69,8 +98,8 @@ dotnet run --project .\src\DungeonWalker.Evaluation.Benches --configuration Rele If your solution vastly exceeds the model $30\%$ speedup there will be bonus points awarded, decided on a case-by-case basis once all solutions are examined. - - + + @code { diff --git a/src/Sorcery/Pages/Teaching/CSharp/10-Concurrency/00-Synchronisation.razor b/src/Sorcery/Pages/Teaching/CSharp/10-Concurrency/00-Synchronisation.razor index f482d39..e5bc3fc 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/10-Concurrency/00-Synchronisation.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/10-Concurrency/00-Synchronisation.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover lock statement and the Monitor static class; diff --git a/src/Sorcery/Pages/Teaching/CSharp/10-Concurrency/01-ConcurrenctCollections.razor b/src/Sorcery/Pages/Teaching/CSharp/10-Concurrency/01-ConcurrenctCollections.razor index 187bd97..d34b39b 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/10-Concurrency/01-ConcurrenctCollections.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/10-Concurrency/01-ConcurrenctCollections.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover System.Collections.Concurrent; diff --git a/src/Sorcery/Pages/Teaching/CSharp/10-Concurrency/02-Plinq.razor b/src/Sorcery/Pages/Teaching/CSharp/10-Concurrency/02-Plinq.razor index 685764e..08329cf 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/10-Concurrency/02-Plinq.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/10-Concurrency/02-Plinq.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover Parallel LINQ; diff --git a/src/Sorcery/Pages/Teaching/CSharp/10-Concurrency/03-Channels.razor b/src/Sorcery/Pages/Teaching/CSharp/10-Concurrency/03-Channels.razor index 5695f3e..61e9235 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/10-Concurrency/03-Channels.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/10-Concurrency/03-Channels.razor @@ -10,7 +10,7 @@ Next one in the notebooks repository: @($"{course.Module.Id}-{course.Module.RouteName}/{course.Id}-{course.RouteName}.dib"). - In this section we'll cover + In this section we'll cover asynchronous communication. diff --git a/src/Sorcery/Pages/Teaching/CSharp/10-Concurrency/Assignment-SimultaneousSimulations.razor b/src/Sorcery/Pages/Teaching/CSharp/10-Concurrency/Assignment-SimultaneousSimulations.razor index 1d23ae1..068371d 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/10-Concurrency/Assignment-SimultaneousSimulations.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/10-Concurrency/Assignment-SimultaneousSimulations.razor @@ -2,53 +2,53 @@ @inject CourseBook CourseBook; - + Running through Dungeon simulations also takes too much time. An ingenious idea is being thrown around – maybe independent simulations could be done simultaneously? This would require training for the person responsible for the whiteboard with run results to multitask. - - Overview - + + Overview + We will be parallelising dungeon runs, so that many simulations can be performed quickly to find best possible results for our Heroes. This will require creating a ranking that can be safely accessed from multiple threads. - - Task 0. – Concurrent Ranking - + + Task 0. – Concurrent Ranking + Implement the ConcurrentBestRunsCollection in DungeonWalker.Logic.Statistics. The collection must be thread-safe for all methods. You can achieve that however you like, but the recommendation is System.Collections.Immutable.ImmutableSet. - - + + In the spirit of "immutable is thread-safe", using an immutable collections will make many things easier. As a reminder, immutable collections return a reference to a new collection instance on modification instead of actually mutating the original. You will still need to lock the reference on writes. Pay attention to the ranking enumerator – makes sure that it actually enumerates over a stable snapshot of the collection! - - + + The tricky part is Remove. You need to return a boolean specifying whether the element was removed. The immutable collection returns only a reference to a modified collection. Note, however, that if an element is not removed, then the reference returned is the same as the original. In other words, if x is not in collection, then . - + This is tested by the "ConcurrentStatistics" test group, worth 1.5 points. - Task 1. – Game Engine - + Task 1. – Game Engine + We should now utilise this thread-safe collection in the GameEngine located in DungeonWalker.Logic.Execution. The RunMultiple method heuristically checks whether multithreading would be beneficial – at least ten times as many items as CPU cores. The fallback version is a simple sequential loop. - - + + Your entire task is to implement RunMany using the ConcurrentBestRunsCollection for collecting results and PLINQ for processing. Note that there are no correctness tests for this – you will need to manually run the Play endpoind through the Swagger API, or by using the test_multiple_runs script. - + The script successfully executing is worth 0.5 points, the output is not checked. diff --git a/src/Sorcery/Pages/Teaching/CSharp/11-Blazor/00-Summary.razor b/src/Sorcery/Pages/Teaching/CSharp/11-Blazor/00-Summary.razor index c385b99..704487f 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/11-Blazor/00-Summary.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/11-Blazor/00-Summary.razor @@ -6,21 +6,21 @@ } - + Blazor is a powerful framework that allows us to write web UI components in .NET and compile them to WebAssembly, the native browser language. This means you can write your application entirely in C# and never write a single line of JS ever again! - - + + You can create standalone Blazor applications and host them without a fully-fledged ASP.NET Core server. This is in fact how this website is hosted! In the case of our assignment, we host it with an ASP.NET Core server. The client then talks to the server just like a normal client would, invoking HTTP requests to the exposed API. - - + + This is an extremely powerful setup. Instead of a separated development experience, where there is a traditional backend server and then a frontend webpage written in React, Angular, or your other favourite JS framework, you have a unified solution written in .NET. - + diff --git a/src/Sorcery/Pages/Teaching/CSharp/11-Blazor/01-HostedServices.razor b/src/Sorcery/Pages/Teaching/CSharp/11-Blazor/01-HostedServices.razor index 624e968..f0500c6 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/11-Blazor/01-HostedServices.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/11-Blazor/01-HostedServices.razor @@ -7,19 +7,19 @@ } - + Normally, an ASP.NET Core server simply handles requests. An HTTP request comes, all of the machinery processes it, gives a response, and then ends. However, sometimes we want servers to be able to perform long-running tasks not tied to a particular request. - - + + A Hosted Service is a piece of code that runs on the server independent of requests. It will continue running even if users stop issuing requests. Usual use case is to have a worker service that performs tasks taken from some asynchronous queue. - - + + All you need to do to implement a Hosted Service is to inherit from BackgroundService and then implement its ExecuteAsync method. Channels that we covered in @channels.DisplayName are an ideal asynchronous queue to use with such services. - + diff --git a/src/Sorcery/Pages/Teaching/CSharp/11-Blazor/02-SignalR.razor b/src/Sorcery/Pages/Teaching/CSharp/11-Blazor/02-SignalR.razor index b33f615..cceb609 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/11-Blazor/02-SignalR.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/11-Blazor/02-SignalR.razor @@ -7,33 +7,33 @@ } - + A common problem for frontend applications is that they want to display some dynamic state that might be updated independently of the user browsing the webpage. Think webchats, notifications, very dynamic content like stock prices, etc. A very primitive approach to solving that is to continuously send requests to the backend, say every second, asking whether there were any updates. The correct approach is using WebSockets. - - + + WebSocket is a communication protocol that allows the server to send information to the client without previous requests coming its way. Underneath it uses standard TCP and is compatible with HTTP. SignalR is the .NET implementation of the WebSocket protocol. - - SignalR in ASP.NET Core - + + SignalR in ASP.NET Core + In ASP.NET Core, SignalR functions by the way of Hubs, enabling a simple version of RPC (Remote Procedure Call, where you call methods of a process from another process). A Hub defines a set of .NET methods and expects its clients to implement a certain interface. Then the clients can call methods of the Hub, and the Hub can in response trigger method calls of connected clients. - - + + For example, the client sends a request to call Subscribe(int conversationId) with some parameter. The server processes the method call by adding the connected client to a list of subscribers on a given id. Then another client sends a call to SendMessage(int conversationId, string message). The server processes the call and in return calls a method ReceiveMessage(string message) on all clients that were subscribed to the given conversation. All of this is handled via WebSockets, which allows the frontend clients to implement callback methods reacting to a message being received. - + - + Change of pace – we are leaving the DungeonWalker project alone. In this assignment you will implement the backend of Elector, an election system/simulator, and as an aside learn why First-Past-the-Post is an awful voting system. - - Overview - + + Overview + The application is a Blazor website with an ASP.NET Core backend server. The server is connected to an SQLite database, with Entity Framework serving as data layer infrastructure. @@ -44,113 +44,113 @@ - - Task 0.0. – Understand Single Transferable Vote (STV) - + + Task 0.0. – Understand Single Transferable Vote (STV) + STV is the most popular ranked-choice voting system. Ranked choice means that ballots (the forms that a single voter gets to fill in) are filled with a ranked list of candidates instead of a single checkmark next to the most favoured candidate. This makes elections more representative and avoids power consolidation. - - + + Imagine a system where there are three parties, Red, Blue, and Green. Red gets around $45\%$ of the popular vote, Blue gets $40\%$ and Green also gets $15\%$, and we have single-winner elections, e.g. presidential elections. In First-Past-the-Post (FPtP), Red wins in a landslide. But it turns out that Green voters would much rather see a Blue candidate than a Red one. In FPtP Green effectively jeopardises Blue's chances, by taking away its electorate. This encourages big parties, limiting representation. Any similarities to the electorial shitshow of any well-known, big country is entirely not coincidental. - - + + In a ranked-choice voting system, the Green voters would be able to specify Blue as their second choice. In SVT, their votes would be transfered to the Blue party and help them win. There are many different ranked-choice system, and SVT is not necessarily the best one, but it is rather simple to understand. - - + + Every ballot contains a list of votes, where some candidates are given a preference. Those preferences create an ordered list. So, a ballot by a Green voter could give preference $1$ to Green, $2$ to Blue, and no preference to $Red$. - + For the purposes of this excercise, any ballot that contains no preferences or assigns the same ordinal to multiple candidates is an invalid vote. - + After the votes are gathered, the outcome is determined in rounds. First, a quota is established – the required number of votes for a candidate to be elected. There are many choices for the quota, but we will be using the Droop quota, formulated as: $$\lfloor \frac{\text{valid votes}}{\text{available seats} + 1} \rfloor + 1$$ To understand its justification, see that for a single seat (single-winner elections) the quota is simply the majority of votes. - + - + The election now proceeds in rounds. - - - - Take all remaining ballots and assign them to the first candidate in order of preference that has not yet been elected or eliminated. - If a ballot contains no such candidates, it is exhausted and discarded. - - - If there are as many candidates as seats remaining, elect all remaining candidates and end the election. - - - Otherwise, if no candidate has at least as many votes as the quota, eliminate the candidate with the lowest number of votes. - All of their ballots return to the pool of remaining ballots. Go to 1. - - - Otherwise, elect all candidates that have enough votes. For each such candidate, calculate the surplus of votes, - i.e. how many votes above quota they have. Then look at all of their votes and gather the transferable votes, - i.e. votes that have a next-preference candidate that is not yet elected or eliminated (in other words, will not be exhausted - if returned to the pool). All other votes are non-transferrable and stay with the candidate. - - - If there are at most as many transferable votes as the surplus, then the operation is simple – transfer all ballots to their - next-preference candidate. - - - Otherwise, for each next-preference candidate only a fraction of votes is transfered. The exact number is calculated as: + + + Take all remaining ballots and assign them to the first candidate in order of preference that has not yet been elected or eliminated. + If a ballot contains no such candidates, it is exhausted and discarded. + + + If there are as many candidates as seats remaining, elect all remaining candidates and end the election. + + + Otherwise, if no candidate has at least as many votes as the quota, eliminate the candidate with the lowest number of votes. + All of their ballots return to the pool of remaining ballots. Go to 1. + + + Otherwise, elect all candidates that have enough votes. For each such candidate, calculate the surplus of votes, + i.e. how many votes above quota they have. Then look at all of their votes and gather the transferable votes, + i.e. votes that have a next-preference candidate that is not yet elected or eliminated (in other words, will not be exhausted + if returned to the pool). All other votes are non-transferrable and stay with the candidate. + + + If there are at most as many transferable votes as the surplus, then the operation is simple – transfer all ballots to their + next-preference candidate. + + + Otherwise, for each next-preference candidate only a fraction of votes is transfered. The exact number is calculated as: - $$\lfloor \frac{\text{transferable votes for next-preference} \cdot \text{surplus}}{\text{total transferable votes}} \rfloor$$ + $$\lfloor \frac{\text{transferable votes for next-preference} \cdot \text{surplus}}{\text{total transferable votes}} \rfloor$$ - The votes to be transfered are selected at random. Remaining votes are exhausted. - See the warning in Task 2. for details. - - - If any seats remain, start the next round (go to 1). Otherwise, end. - - + The votes to be transfered are selected at random. Remaining votes are exhausted. + See the warning in Task 2. for details. + + + If any seats remain, start the next round (go to 1). Otherwise, end. + + + - + Note that there are many ways of transfering votes under STV, including ones that consider fractional votes. Again, this one is the simplest, so we will go with it for the purposes of the exercise. It's also the one that most implementations of STV use, for example for the Irish lower-house elections or for the Australian senate. - - Task 0.1. – Understand the architecture and the user flow - + + Task 0.1. – Understand the architecture and the user flow + A user creates an election through the Elections screen by supplying a name, number of seats, and the number of voters. The election can be in one of five states: - - - - NotStarted – just after creation. - - - GatheringVotes – ballots are being added to the election. - - - GatheredVotes – all ballots are added, waiting. - - - CalculatingOutcome – running the algorithm to determine winners of the election. - - - CalculatedOutcome – election ended, winners decided. - - - + + + NotStarted – just after creation. + + + GatheringVotes – ballots are being added to the election. + + + GatheredVotes – all ballots are added, waiting. + + + CalculatingOutcome – running the algorithm to determine winners of the election. + + + CalculatedOutcome – election ended, winners decided. + + + + After an election is created, the user can add candidates. After that, they click the Start button to begin gathering votes. - - + + Underneath, a few things happen. The controller action simply posts a message to an asynchronous queue. Then a BackgroundService, VotesGatheringService, picks up the task to gather ballots. A BackgroundService @@ -158,52 +158,52 @@ are two such workers – one coordinates generating votes, the other calculating outcomes. A background service can run tasks independent of the front-end, so even if a user gets bored waiting for results and closes their browser, the worker will continue churning away on the server. - - + + The service coordinates all the changes between the IBallotSource generating ballots, the repositories from Elector.Data, and the SignalR Hub. The ballot source is already implemented and creates a stream of pseudorandomly generated ballots, with a small chance of generating an invalid one. All ballots need to be persisted in the database. The ballots are flushed in batches of $100$. Every flush, a message is posted to the SignalR Hub. The message is sent to the front-end, so that it can update the vote screen. - - - + + + After votes are gathered, the user can click a button to calculate the outcome. This posts another task to a queue that is consumed by a OutcomeCalculationService. Then an STV algorithm is ran that reports its progress every round to tell the user which candidates are elected and which are eliminated in a very similar manner – status is persisted in the database, updates posted to the SignalR Hub. - - Task 1. – Controller Actions - + + Task 1. – Controller Actions + We start with controller actions. The BallotsController and CandidatesController are thin and easy. ElectionsController is a bit more involved – the StartAsync and CalculateAsync actions must publish a task to the IElectionTaskQueue if the state change succeeds. - - + + Take notice that some of the repository operations may fail – they return a Result object. If the result is a failure, the operation should respond with a BadRequest with the error message as body(with the exception of GetAsync in ElectionsController, which should return a NotFound). - - + + As usual, you can use the Swagger UI to see all actions and make test runs. Find them at https://localhost:20443/swagger. - + This is tested by the Controllers test group. - Task 2. – STV algorithm - + Task 2. – STV algorithm + Next, we implement the core STV algorithm. The task is to implement NewElectionAsync from Elector.System.SingleTransferableVoteSystem. The method is supposed to fetch candidates and ballots from the repositories and produce a IRoundBasedElection implementation that can run the election. Every round it produces a report of which candidates were elected or eliminated in that round. - - + + If you're unsure about corner cases, check the SingleTransferableVoteSystemUnitTests. They contain a plain-English explanation of all the test cases to guide your debugging. - + The transfer votes step in the algorithm is inherently nondeterministic – a number of votes are transfered, but it is not clear which to choose and which to leave exhausted. @@ -214,16 +214,16 @@ the $N$ votes with smallest Ids are chosen to be transferred, while the $K$ votes with the highest Ids are exhausted. - + This is tested by the System test group. - Task 3. – Repositories - + Task 3. – Repositories + The last step between us and a fully working solution is the database. You need to configure the model, which is pretty straight-forward, and then implement the missing repository, the BallotsRepository. - - + + AddBallotsAsync must add ballots and votes to a given election. If an election with given Id does not exist, a failure should be returned. If saving changes fails for any reason, a failure should be returned. GetAllValidBallotsForElectionAsync should return a list of valid ballots of a given election. @@ -231,30 +231,30 @@ with the same preference. GetElectionOutcomeReportAsync should return a report about elected candidates, consisting of a dictionary mapping candidate ids to nullable boolean values – if the candidate was elected, it should be true, if eliminated, then false, otherwise null. - - + + The most involved one is GetElectionVoteReportAsync. You need to calculate the number of valid and invalid ballots, as well as the number of first-preference votes for each candidate, so the number of ballots with that candidate marked as first preference. As a challenge, try to do it in two database queries only (this is not checked)! - + - + After this is implemented you should be able to run the app end-to-end. As a reminder, to setup the database properly you need to generate a migration and then apply it. - + - + dotnet ef migrations add Initial --project ./src/Elector.Data + dotnet ef database update --project ./src/Elector.Data + ") /> + There a number of tests that rely on the API to run in the background. First, run the server in a terminal window, then run the Elector.ApiTests test project. You can also use the api_tests script in the tests directory to automatically run the suite. There is also a PowerShell script create_test_elections that creates the two complex test cases from the Elector.ApiTests suite that you can then manually debug, skipping the tedious manual candidate creation. - + The api_tests script must successfully execute for this part. diff --git a/src/Sorcery/Pages/Teaching/CSharp/Introduction.razor b/src/Sorcery/Pages/Teaching/CSharp/Introduction.razor index ffc2c00..8760d34 100644 --- a/src/Sorcery/Pages/Teaching/CSharp/Introduction.razor +++ b/src/Sorcery/Pages/Teaching/CSharp/Introduction.razor @@ -2,12 +2,12 @@ @inject CourseBook CourseBook; - + C# is a modern, multiparadigm programming language based on the multiplatform .NET environment. It focuses on developer ergonomics and productivity without sacrificing safety thanks to its automated memory management and robust static analysis. - - + + The course aims at introducing C# and .NET from the very basics. Prior knowledge of some other general-purpose language is assumed, as well as basic knowledge of programming concepts such as threading, SQL databases and object-orientation. The desired effect at the end is for the student to: @@ -21,14 +21,14 @@ have gained more throughout knowledge of the language and ecosystem than is required for a junior position as a C#.NET developer. - + - + The course is split into 10 modules and is accompanied by live workshops; however, the course itself is standalone and can be followed individually. All code used in the course is published as interactive notebooks or standalone applications. - + - Teaser + Teaser During the course we will cover: @@ -48,45 +48,45 @@ performance-oriented programming in C# - Grading + Grading - + The grade is based on two components: microassignments, which are given each week at the end of a module, and the final project. - + - + Each microassignment is worth 5 points. 2 points are for automated testing, 3 are for code quality. Assignments are given via GitHub Classroom and you will automatically see the results of automated tests when working on your code. There will be at least 10 microassignments, but the maximum number of points for this module is 50. - - + + I will try to read your code as soon as you push any changes, so doing the assignment early is encouraged. Basically, if you go through a few rounds of code review with me before the deadline you will most likely get full score. If you submit working but ugly code at the last moment, you'll probably get at most 2 points. The details of working with GitHub Classroom will be introduced at the end of the first module. - - + + Microassignments are meant to be a continuous feedback loop, both for you, to know how well you have grasped the material, and for me, to know how bad I am at explaining it. It is therefore very important for the overall health of our course that you're doing them consistently. The time for them will be up until Tuesday, 18:00, the day before next class. - + To pass in the first term you need to get at least 25 points for these microassignments! - + Note that this threshold is chosen so that passing automated tests for all microassignments guarantees meeting it. The intention is that you shouldn't have to sweat to pass, but to get a good grade you need to put in work. - + - + Over the course of the semester you're expected to come up with an idea for a project and implement it in C#.NET using the tools you've learnt. This can be any application you feel like doing, be it a web app in Blazor, a desktop app in WPF, a CLI application doing something interesting, or a video game. The app will be graded in four categories – UI, logic, data layer, and tests. Each is worth 10 points, and there's additional 10 bonus points if you do something especially interesting. You can expect roughly 5 of those points be for the actual functionality, and 5 as points for code quality. In total, you can get at most 50 points for the project. - + @@ -113,16 +113,16 @@ Bonus (???): surprise me. - + It will be clear from the modules which parts of .NET correspond to which categories of the project. Usage of Entity Framework will obviously be graded in data access, while your Blazor components will be UI. - - + + You can do the project solo or in a pair. The requirements are intentionally informal and not precise – the point is to do something cool with C# and .NET and showcase it. In particular, the above are just guidelines. If you want to do something crazy with .NET as your project, and it doesn't work well with the grading model then hit me up! The project is supposed to be your opportunity to be creative, so there's no artificial limitations. - + @@ -137,25 +137,25 @@ - + If you fail the first term, you will be given a special assignment to complete until September. It will be a single, big assignment on GitHub Classroom spanning the entire course. The grade is binary: either you pass all tests or not. You also need to complete your project – what "complete" means will be discussed on an individual basis. Either way, the grade in this case is either pass or fail, meaning either a 3 or a 2. - - + + If you get a passing grade in the first term, you can get a better grade in the second term by improving on your project. You will have until September to work on it and can ask me for advice and review throughout. The points will be given according to the same system as in the first term. - - + + There is no way to regain points for microassignments in the second term. - + - + You can use the below calculator to get your grade based on the points you currently have. - + diff --git a/src/Sorcery/Pages/Teaching/Teaching.razor b/src/Sorcery/Pages/Teaching/Teaching.razor index b8eeec0..4f4e748 100644 --- a/src/Sorcery/Pages/Teaching/Teaching.razor +++ b/src/Sorcery/Pages/Teaching/Teaching.razor @@ -4,17 +4,16 @@ - Teaching - + Teaching - Past - + Past + Databases labs. Archival resources can be currently found at the old page, in Polish. I will gradually port things that I deem worthy over here. - - + + C#.NET course. See Teaching/C# for the course resources. - + diff --git a/src/Sorcery/Pages/Todo.razor b/src/Sorcery/Pages/Todo.razor index ed349f7..40268f1 100644 --- a/src/Sorcery/Pages/Todo.razor +++ b/src/Sorcery/Pages/Todo.razor @@ -4,23 +4,23 @@ - + Hey! You shouldn't be here! - + - + You're here either because you typed this URL directly, or you just found a dead link that should've been fixed before I deployed the new version of the page. - - - If it's the latter, I'd appreciate if you could file an issue on + + + If it's the latter, I'd appreciate if you could file an issue on GitHub telling me how you got here. - - + + If it's the former, well, I can't really do anything for you. Enjoy this cute picture of a rabbit, I guess. - - + + diff --git a/src/Sorcery/Shared/Components/Blogging/BlogPostCard.razor b/src/Sorcery/Shared/Components/Blogging/BlogPostCard.razor index df6a64c..eb8ce3a 100644 --- a/src/Sorcery/Shared/Components/Blogging/BlogPostCard.razor +++ b/src/Sorcery/Shared/Components/Blogging/BlogPostCard.razor @@ -12,7 +12,7 @@
Published: @Post.DateOfPublication?.ToString("yyyy-MM-dd")
-
+
Tags:
diff --git a/src/Sorcery/Shared/Components/ModularCourse/CourseAssignment.razor b/src/Sorcery/Shared/Components/ModularCourse/CourseAssignment.razor index 17dee75..6cd38a4 100644 --- a/src/Sorcery/Shared/Components/ModularCourse/CourseAssignment.razor +++ b/src/Sorcery/Shared/Components/ModularCourse/CourseAssignment.razor @@ -11,8 +11,7 @@ Back to @previousSection.DisplayName. - @Assignment.DisplayName - + @Assignment.DisplayName GitHub Classroom invitation to the assignment. diff --git a/src/Sorcery/Shared/Components/ModularCourse/CourseSection.razor b/src/Sorcery/Shared/Components/ModularCourse/CourseSection.razor index f7f9cfe..afbfbf8 100644 --- a/src/Sorcery/Shared/Components/ModularCourse/CourseSection.razor +++ b/src/Sorcery/Shared/Components/ModularCourse/CourseSection.razor @@ -16,8 +16,7 @@ Back to @Section.Previous.DisplayName. } - @Section.DisplayName - + @Section.DisplayName @ChildContent