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.
- String.Format("{0}@@{1}.com", "mat", "gienieczko")
.
- structured, imperative, object-oriented, event-driven, task-driven, functional, generic, reflective, concurrent-
dynamic
or unsafe
, but that will be tackled
- at the very end of the course 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.
- apt install dotnet-sdk-5.0
.
dotnet
dotnet
dotnet
commands work the same on both platforms.
- 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.
- dotnet
is correctly installed, run dotnet --info
. It should look
something like this:
code 〈dir〉
to open it in the specified
directory.
- .cs
.csproj
.rs
and a configuration in Cargo.toml
. You usually won't define .csproj
yourself, but we will
- be adding some stuff there on occasion.
- .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.
- .dll
.dll
.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.
- console
.
You use dotnet new
to create a fresh project from a template, passing the name of the project with the --name
parameter.
- HelloWorld
and a project with this familiar code inside:
dotnet build
and then dotnet run
.
using
directive. It tells the compiler to bring into scope
@@ -111,29 +108,29 @@ internal class Program
Main
method you can write your code script-like, and the entry point will be Program
or Main
, but rather magical names that only the compiler
knows – such names are called 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
.
- readonly
.
- new
.
- readonly
.
+ new
.
+ 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.
- Modulo(int x, int m)
method that works as follows:
- Calculator
class (you can rename and replace the existing Class1.cs
file):
- .UnitTests
suffixed. We will be using the xUnit test framework,
so create a new xUnit project:
- Testing
project.
This can also be done from the CLI:
- .csproj
.csproj
.csproj
.
- csproj
for a library.
We specify that we're using .NET 6.0 and a feature called System
. We also
enable 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.
- PackageReference
s. 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.
- Testing
project, which is located next to us.
This is the line added by the dotnet add
command we executed.
- .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:
- 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.
- CalculatorUnitTests.cs
file and write our first
Fact (you can rename and replace the existing UnitTest1.cs
file):
- 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 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.
- 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:
- %
operator.
We have some small code duplication here, so let's alleviate that and also add the other cases to our tests.
- 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:
- Modulo_GivenZeroAsModulus_ReturnsZero
method.
- 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
.
- Adder
and HelloWorld
classes to make sure everything
is configured correctly.
- IRoomLayout
as an argument to its constructor,
which tells it how to construct the Rooms:
- IRoom
is where the core of the logic happens.
- 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.
- 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".
- CombatRoom
. Each Character
in the game has three basic statistics:
- 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.
ITurnOutcome
returned by the Combat.Resolve
method
should include some details about how combat proceeded. Here's an example output:
- 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.
- 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.
+ ILoot
interface. You can add any methods you want to it.
- LootFactory:
- DungeonWalker.Characters
,
- to allow them to be affected by ILoot
.
- DungeonWalker.Characters
,
+ to allow them to be affected by ILoot
.
+ 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.
- DungeonWalker.Logic.Factories.HeroFactory
static class to return correct
- instances for Heroes:
- Rogue
–
- Warrior
–
- Wizard
–
- 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
.
- strcmp
or inet_pton
.
- Only universally recognisable abbreviations of computer terms are allowed, like
- HttpClient
, TcpSocket
or XmlSerializer
.
- 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.
+ DungeonWalker.Logic.Factories.HeroFactory
static class to return correct
+ instances for Heroes:
+ Rogue
–
+ Warrior
–
+ Wizard
–
+ 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
.
+ strcmp
or inet_pton
.
+ Only universally recognisable abbreviations of computer terms are allowed, like
+ HttpClient
, TcpSocket
or XmlSerializer
.
+ new
is expensive, memory allocation in .NET is actually surprisingly fast.
But more allocations means more memory pressure, means more GCs, means performance drops.
- Modifier
type that
will model that.
- 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:
- float
scalar that multiplies all individual modifier values, rounding down;float
scalar that multiplies all individual modifier values, rounding down;ToString
representation, e.g. our friend Chainmail would give this modifier:
"nothing"
.
- DisableTenacity
and EnableTenacity
methods
to apply the modifier correctly.
- Modifier
type they will compile.
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.
TryEquip
method on
the Hero
class and potentially all inheriting classes.
- 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.
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$.
LootFactory
.
- Hero.TryEquip
.
- UpgradeRoom
locations
upgrade all Equipment on the Hero to the next level, unless it's already Heroic.
- UpgradeRoom
. It should return a nice
description of the upgrade operation, omitting all equipment that could not be upgraded.
A few examples:
- 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:
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:
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;
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.
- Id
.
For ordering we compare statistics in order to determine the best run:
- 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:
- 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.
- csproj
files:
- MagicalMaze
. You can provide
the number of independent runs to execute with the --number
flag,
and fix a seed for the RNG with --seed
.
- 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:
DungeonWalker.Graphs.Algorithms.TopologicalSort<TLabel>
. The missing method is:
- false
if the graph has a cycle, otherwise
it returns true
and fills in the layered result.
- 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 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:
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:
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:
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;
Json.NET
, and the
newer System.Text.Json
, which is part of the BCL. We will use the latter
for this assignment.
- Graph<IRoom>
class. In a new project, DungeonWalker.DataLayer
,
we define the type GraphTemplate<TLabel>
, with only two simple properties:
- 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
.
- 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.
- JsonSerializerOptions
object that can be passed to the serialization methods.
- 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.
- 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.
- _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.
- FileSystemRepositoryException
- that inherits from DungeonRepositoryException
.
- 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.
- CancellationToken
instances passed to the methods should be used for
- all async operations that support cancellation.
- FileSystemRepositoryException
+ that inherits from DungeonRepositoryException
.
+ 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.
+ CancellationToken
instances passed to the methods should be used for
+ all async operations that support cancellation.
+ DungeonWalker.FileSystemRepositoryDemo
.
Here's the output on a model solution:
- 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:
IDungeonRepository
interface known from
the previous assignment,
but using Entity Framework with an SQLite database.
- 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.
- 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.
- 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.
- 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.
- DungeonGraphs
. Then setup a local SQLite database.
+
+ DungeonGraphs
. Then setup a local SQLite database.
This is done with the following magic spells, ran from the root solution directory:
- dotnet-ef
tool installed. As a reminder, we install it with:
- DungeonWalker.db
in your local user folder.
- AppData\Local
directory./home/{user}/.local/share
directory./Users/{user}/.local/share
directory.AppData\Local
directory./home/{user}/.local/share
directory./Users/{user}/.local/share
directory.dotnet ef migrations script --project ./src/DungeonWalker.DataLayer.Sql
.
- 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.
- 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:
LootRoomTemplate.FlattenLootPiles
.
- LootRoomTemplate.FlattenLootPiles
.
+
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
.
- ThenInclude
, which you can find in the linked resource: Eager Loading of Related Data.
- 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.
- DungeonWalkerDbContext
's
- constructor overload with LogLevel
, specifying Information
or even Debug
.
- ThenInclude
, which you can find in the linked resource: Eager Loading of Related Data.
+ 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.
+ DungeonWalkerDbContext
's
+ constructor overload with LogLevel
, specifying Information
or even Debug
.
+ DungeonWalker.SqlRepositoryDemo
.
- The expected output is as before, only that there might be some logging information from the database in between.
- DungeonWalker.SqlRepositoryDemo
.
+ The expected output is as before, only that there might be some logging information from the database in between.
+ 07-ASP.NETCore/DungeonWalker.Api
directory in
+ Navigate to the 07-ASP.NETCore/DungeonWalker.Api
directory in
the notebooks repository
and run:
- csproj
:
AspNetCore
libraries as implicit dependencies, some analysers, as well as
better first-class support from CLI and IDEs.
- 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
.
- 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.
- dotnet run --project ./DungeonWalker.Api
. The output
will look something like this, modulo paths and port numbers:
- 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.
- /
route.
- /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
'.
- ControllerBase
- with the special ApiControllerAttribute
.
- 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).
- ILogger<WeatherForecastController>
dependency
- that is magically given to it in the constructor. This is Dependency Injection.
- 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.
- ControllerBase
+ with the special ApiControllerAttribute
.
+ 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).
+ ILogger<WeatherForecastController>
dependency
+ that is magically given to it in the constructor. This is Dependency Injection.
+ 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.
+ _logger
field? Let's try to log something, change the Get
body to:
- curl
again. You should see the following appear in the server's terminal
(modulo the number of days):
- 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.
- 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.
- 07-ASP.NETCore/Adder.Api
directory.
There's some things already configured to make the code runnable, you only need to run database setup:
- curl
seeding the database and the sum:
- null
, when
repository
throws an exception. Oh, and we need a test that will assert that the repository was
disposed of.
- 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.
- 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
...
- 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.
- 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
?
- 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.
- DbContext
? Just asking questions.
- 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
...
+ 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.
+ 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
?
+ 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.
+ DbContext
? Just asking questions.
+ 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.
- 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.
- 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.
- 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!
- 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.
- 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.
- Microsoft.Extensions.DependencyInjection
, there are three
lifetime types:
-
- DbContext
s. We want the entity tracking
feature to work for the entire request across services to maintain consistency.
-
- Program.cs
.
-
- 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.
- Program.cs
. Navigating to localhost:20443/swagger
will
show us the extremely exciting Adder API.
- curl
.
- swagger.json
, available at localhost:20443/swagger/v1/swagger.json
.
- builder.Services.AddSwaggerGen
call.
The most important thing we can do is include XML comments from our C# code.
- DungeonWalker.Api
solution setup with the example Dungeon database from
@entityFrameworkModule.DisplayName. Let's setup
a standard query-by-id endpoint.
- RouteAttribute
specifies a parameter named id
@@ -63,19 +62,18 @@ public async Taskseed
endpoint first.
- dungeonName = null, heroClass = "Warrior"
.
If we omit both, https://localhost:10443/runs,
both will map to null
.
- FromQueryAttribute
on the DungeonRunQueryParameters.DungeonName
and DungeonRunQueryParameters.HeroClass
properties, then we could just use DungeonRunQueryParameters
as the parameter.
- 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:
- Result
types. You should definitely
check out that package, as it makes handling errors much easier.
- 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.
- 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.
- Information
filters out all Debug
and Trace
messages.
- print
statements into the code and then try
to make sense of the output – only made more sustainable.
- _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.
- LoggerMessage
LoggerMessage
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.
- 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:
- 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.
- 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.
- 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.
- 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
.
- DungeonsController
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.
- RunsController
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.
- 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:
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:
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.
- 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.
Service
to correctly
throw the precondition sychronously before carrying on.
Returns
mocking method.
@@ -151,7 +151,7 @@ public async Task QueryAsync_GivenIdOfExistingItem_ReturnsSuccessWithItem()
null
by yourself now.
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
null
return by yourself.
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.
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).
- 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:
- HeroClassAttribute
;
- DungeonWalker.Logic.Characters.Hero
;
- HeroClassAttribute
;
+ DungeonWalker.Logic.Characters.Hero
;
+ 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:
- 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.
- 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.
- HeroRepository
HeroRepository
GetHeroAsync
- LoadAllHeroesFromAssembly
- LoadHeroClass
- GetHeroAsync
+ LoadAllHeroesFromAssembly
+ LoadHeroClass
+ 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
.
- _logger
instance has extension methods defined for all of the failure cases we defined:
- FailedToLoadHeroClassDueToNoAttribute
- FailedToLoadHeroClassDueToLackOfAParameterlessConstructor
- FailedToLoadHeroClassDueToNotSubclassingHero
- FailedToLoadHeroClassDueToConflictingName
- FailedToLoadHeroClassDueToNoAttribute
+ FailedToLoadHeroClassDueToLackOfAParameterlessConstructor
+ FailedToLoadHeroClassDueToNotSubclassingHero
+ FailedToLoadHeroClassDueToConflictingName
+ LoadHeroClass(Type)
, you need to throw
a DynamicLoaderException
.
- 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
.
- /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.
- 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:
- 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
.
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$.
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:
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:
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:
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:
DungeonWalker.Evaluation.Benches
.
- 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.
- GraphDungeonEvaluatorBaseline
, ValueBaseline
,
- or SprawlingStronghold
code.
- Evaluate
method.
- Value
.
- GraphDungeonEvaluator
and Value
implementations
- in any way except for the exclusions above.
- TopologicalSort
.
- GraphDungeonEvaluatorBaseline
, ValueBaseline
,
+ or SprawlingStronghold
code.
+ Evaluate
method.
+ Value
.
+ GraphDungeonEvaluator
and Value
implementations
+ in any way except for the exclusions above.
+ TopologicalSort
.
+ 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:
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:
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
.
- lock
the reference on writes.
Pay attention to the ranking enumerator – makes sure that it actually enumerates over a stable snapshot
of the collection!
- 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 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.
- 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.
- BackgroundService
and then implement its ExecuteAsync
method. Channels that we covered in @channels.DisplayName are an ideal asynchronous queue
to use with such services.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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).
- Controllers
test group.
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.
- SingleTransferableVoteSystemUnitTests
.
They contain a plain-English explanation of all the test cases to guide your debugging.
- System
test group.
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
.
- 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)!
- 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.
- api_tests
script must successfully execute for this part.