C#

Set Up & Compile

Install VSCode and https://dotnet.microsoft.com/en-us/download

To start coding:

dotnet --version

dotnet new console -o app

cd app

code . To open project in VSCode

dotnet run

To compile:

dotnet build -c Realease

Data types

When a value type is declared, the data is stored in the memory location of the variable. When a reference type is declared, the data is stored somewhere else in memory and the variable contains a pointer to it.

Value types include int, byte, bool, and char. Reference types include string, arrays and class.

/* 
An integer is whole number, -2, -1, 0, 1, 2, etc.
A floating point is a number that can have decimal places, 8.2, 3.14, etc.
When declaring a float or double, the letter F or D should be appended to the value. 
*/

/* Floating points */
float piFloat = 3.1415926535F;
double piDouble = 3.1415926535D;

Console.WriteLine("pi as float: {0}", piFloat);
Console.WriteLine("pi as double: {0}", piDouble);

/* Booleans and Characters
A bool is a true or false value.
A char is a single letter or number, represented by an integer.  Those "integers" are standardised in the ASCII and Unicode tables.  Different languages allow different byte sizes for characters, from 1 to 4 bytes.  C# uses 2 bytes, which allows it to use any character in UTF-16.
A character is defined with single quotes.
*/
// bool myBool = true;
// char myChar = 'A';

/* Arrays and Tuples
The array and tuple are both types of collections.  An array can hold multiple values of a single data type; whereas a tuple can hold multiple values but of various data types.  Both types are fast to use at runtime, but they are fixed size

You can also create an empty array by declaring the number of elements you want to have.  The values in the array are assigned the default value for the relevant data type.  For an integer, that would be 0.

An element in an array can be accessed by its index.  Arrays are "zero-indexed" which means the first element is index 0, the second is index 1 and so on.  The index of an array is accessed using square brackets, e.g. array[0].

A tuple is declared using parenthesise for both the data types and initial values; and instead of accessing a tuple index via square brackets, we use the period, ..  Each item is given a name like Item1, Item2, etc.
*/
int[] intArray = {1,2,3,4,5};
// Empty array --> int[] intArray = new int[5];

/* To print the 3rd element:*/
Console.WriteLine("{0}", intArray[2]);

/* Tuples*/
(string, string, int) Tuple = ("Charles", "Dickens", 1812);
// Console.WriteLine("{0} {1} was born in {2}.", Tuple.Item1, Tuple.Item2, Tuple.Item3);
/* In a more friendly way*/
(string firstName, string lastName, int dob) = Tuple;
Console.WriteLine("{0} {1} was born in {2}.", firstName, lastName, dob);

/* String
A string can be declared using double quotes.
There are multiple ways to concatenate strings.  One way is via interpolation, denoted by a prepended $.
Another is by using the + operator.
*/
string name = "Ghost";
string surname = "Spectre";
// To concatenate
/* Using $
string nameandsur = $"{name} {surname}";
//Using +
string nameandsur = name + " " + surname;
*/
//Using string concat
string nameandsur = string.Concat(name, " ", surname);
Console.WriteLine(nameandsur);

Variables

- Declaring

Declaring the data type tells the compiler specifically what type of data the variable will hold, which is called "explicit typing".

However, the green dotted line underneath the keywords suggests that we use the var keyword instead. This is "implicit typing" where the compiler infers the data type based on the value assigned to it. One of the main benefits of using implicit typing is that it can shorten your code and make it more readable.

var cannot be used when declaring a variable without assigning it a value or when the compiler cannot infer from context what the data type is meant to be.

Every variable declared in C# is mutable, which means it can be changed. To make a variable immutable, prepend it with the const keyword.

- Naming Conventions

C# uses different text casing depending on where it's being declared. Here is a summary of the conventions:

- Casting

Casting is the process of converting one data type to another, which comes in two flavours: "implicit" and "explicit".

Example of declaring an int and then casting it to a double (implicit cast):

var i = 20;
double d = di;

However, you cannot implicitly cast a double to an integer - it must be explicitly cast:

double d2 = 3.14D;
int i2  = (int)d2;

These two methods exist because explicit casting is typically more "dangerous", as it can result in a loss of data precision. An integer is a whole number and cannot hold decimal places. Converting 20 from an integer simply results in 20 as a double. But converting 3.14 from a double results in 3 as an integer - we lose the .14 precision on the conversion.

Explicit casting exists because the compiler is forcing you to think about (and acknowledge) the conversion, rather than having it handled automatically for you.

Other data types can be cast to each other where it makes sense. For example, because a char is a really just a Unicode value, you can cast between char's and int's:

var c = 'A';
int i = c;
Console.WriteLine($"{c} == {i}");

c = (char)i;
Console.WriteLine($"{i} == {c}");

However, you cannot convert nonsensically such as a string to a float.

Collections

- Lists

A List<T> is a collection of generic data types that can be accessed by index. It also contains methods for adding, inserting, removing, searching and sorting the values. A list can only hold one type of data at a time.

var integers = new List<int> { 1, 2, 3, 4, 5 };
var intem:int = integers.Find(mathc:v:int => v == 3);

Console.WriteLine(item);

- Hashtable

The Hashtable is like a stripped-down dictionary that is designed for pure performance. It does not maintain any order in the collection and allows values to be looked up very quickly. It's a good candidate when computing against large data sets but for general use, the dictionary is friendlier.

using System;
using System.Collections;

var table = new Hashtable
{
	{ 0, "Charles Dickens" },
	{1, "George Orwell" },
	{2, "Mark Twain" },
	{3, "Jane Austen" }
};

foreach(DictionaryEntry entry in table)
Console.WriteLine($"{entry.Key} : {entry.Value}");	

- Queue

There are two main types of queue: the Queue and the Stack. The queue is a first-in-first-out (FIFO) collection and the stack is last-in-first-out (LIFO). These are useful when the order of data is of the strictest importance and can be used in places such as message queues.

To push an object into a queue, use the Enqueue method. To remove and return the next object, use Dequeue.

var queue = new Queue<int>();

// add items
queue.Enqueue(item:1);
queue.Enqueue(item: 2);
queue.Enqueue(item: 3);

// dequeue them all
while (queue. TryDequeue (out var value:int))
Console.WriteLine(value);

In this example, the printed order is 1, 2, and 3.

The stack functions in the same way using the Push and Pop methods.

var stack = new Stack<int>();

// add items
stack.Push (item: 1);
stack.Push(item: 2);
stack.Push(item: 3);

// remove them all
while (stack.TryPop (out var value:int))
Console.WriteLine(value);

Operators

- Mathematical Operators

+ for add.

- for subtract.

* for multiply.

/ for divide.

% for modulus.

- Logical operator

Logical operators are used when we want to evaluate an expression in order to obtain a true or false outcome. This could be as simple as comparing the size of two numbers. The most common operators are:

== for equals.

> for greater than.

< for less than.

>= for greater than or equal to.

<= for less than or equal to.

The bang ! is used to flip the logic of an operator, most commonly paired with equals.

!= for not equal.

- Bitwise Operators

A bitwise operators changes a value at the binary level. The common bitwise operators are:

& for AND.

| for OR.

^ for XOR.

<< for left shift.

>> for right shift.

Control Flow

- If / Else

if (condition1)
{
	// do something
}
else if (condition2)
{
	// do something else
}
else
{
	// catch all
}

Each condition is evaluated from top to bottom. If one is true, the relevant code will execute, the flow will break out and no other conditions will be evaluated. This allows the final else case to be a kind of "catch all" in the event that every condition evaluates to false.

Multiple logical operators can be combined. For example:

using System;

var condition1 = true;
var condition2 = false;
var condition3 = false;

if (condition1 || condition2 && confition3)
	Console.WriteLine("Goof to go");

It seems that what we're trying to say is that condition1 OR condition2 needs to be true AND condition3 needs to be true as well. Since condition3 is set to false, the entire expression should come out as false, right? But it doesn't.

This is due to the order in which the conditions are evaluated - the AND statement is evaluated first, followed by the OR.

"condition2 && condition3" evaluates to "false" because condition2 and condition3 are both currently set to false. This would also be the case if one of them was true - because it's an AND operator, both condition2 and condition3 would have to be true for that portion to come out as true overall.

The next part of the expression is then evaluated, which is "condition1 || condition2". Since condition1 is true, this expression comes out as true and the code inside the braces is executed.

To address this, we need to put parenthesise around the OR expression to force it to be evaluated first:

if ((condition1 || condition2) && confition3)
	Console.WriteLine("Goof to go");

Now it will evaluate "condition1 || condition2", which is true because condition1 is true. But then true && condition3 comes out as false, because condition3 is false. So this time, we don't see the "Good to go" message. Rider has already correctly evaluated the condition to false. It has greyed out the line to show that this code is not currently reachable, since as there's no code that changes the state of the conditions.

- Switch

The switch keyword is a pattern-matching construct which allows you to add cases for certain conditions. This is generally nicer to use than multiple else if statements. Take the following example:

var animal = "Dog";

if (animal == "Dog")
{
	Console.WriteLine("Woof");
}
else if (animal == "Cat")
{
	Console.WriteLine("Meow");
}
else
{
	Console.WriteLine("Unknown");
}

This can be condensed down into:

var animal = "Dog";
var sound:string = animal switch
{
	"Dog" => "Woof",
	"Cat" => "Meow",
	_ => "Unknown"
};

Console.WriteLine(sound);

The _ => is used as a "catch all" in case none of the cases match.

- Enums

An enum (or enumeration) is a set of pre-defined constants (i.e. values that cannot be changed)

Enums are referenced by integer values by default rather than strings or anything else.

- Loops

There are two main types of general purpose loops called for and while. In almost all cases, we want to perform some loop until a pre-determined condition has been met.

For Loop

A for loop is made up of three statements:

Statement 1 is executed one time at the start of the loop.

Statement 2 defines the condition for executing the loop code.

Statement 3 is executed after every loop.

using System;

for (var i = 0; i < 10; i++)
Console.WriteLine($"i is {i}");

In this example:

Statement 1: Initialises the starting value of a counter, i to 0.

Statement 2: states that if i is less than 10, then execute the code.

Statement 3: increments i by 1 after each loop.

Once i is no longer less than 10, the loop will break. These loops are commonly used to iterate over elements in a collection, using the length of the collection.

var array = new[] {1,2,3,4,5};
for (var i = 0; i < array.Length; i++)
Console.WriteLine(array[i]);

- While Loop

A while loop can be more flexible than a for loop, because we do not need a predetermined range to iterate over (although they can be written like that). A while loop will loop forever whilst the condition is met. This condition is usually defined outside of the loop itself but can be manipulated from inside the loop.

var i = 0; while (i < 10)
{
	Console.WriteLine(i);
	i++;
}

Another way to write this could be.

var i = 0;
while (true)
{
	if (i > 10)
	break;
	
	Console.WriteLine(i);
	i++;
}

The break keyword can be used to break out of a loop at any time, regardless of whether its condition is met or not. The continue keyword can be used to skip the remaining code in a loop and move immediately onto the next loop iteration.

var i = 0;
while (i < 10)
{
	if (i == 5)
		continue;
	Console.WriteLine(i);
	i++;
}

You must be careful however, as this can introduce unexpected runtime bugs. The code above will actually loop infinitely, because we continue before incrementing the counter. Therefore never allowing it to go beyond 5.

- ForEach Loop

The foreach loop provides an easier way to loop over a collection without having to have a counter.

var list = new List<int> { 1, 2, 3, 4, 5 };
foreach (var i:int in list)
Console.WriteLine(i);

- Scopes

For example, the following code will throw the error "cannot find value `foo` in this scope" and will not compile.

{
	// "outer" scope
	
	{
		// "inner" scope
		var foo = "foo";
	}
	
	// "outer" scope
	Console.WriteLine(foo);
}

- Command Line Arguments

using System;

for (var i = 0; i < args.Length; i++)
Console.WriteLine($"Argument {i} is {args[i]}");

If your application has mandatory arguments, it's common to do a length check and exit from the program if not enough are provided.

using System;

if (args.Length < 2)
{
	Console.WriteLine("Not enough arguments");
	ShowUsage();
	return;
}

void ShowUsage()
{
	Console.WriteLine("Usage: app.exe <arg1> <arg2>");
}

- Prompting for Input

using System;

while (true)
{
	// print a pseudo prompt
	Console.Write("> ");
	
	// read from stdin
	var input:string = Console.ReadLine();

	// loop again if the string was empty
	if (string.IsNullOrWhiteSpace(input))
		continue;
	
	// print to stdout
	Console.WriteLine($"You said: {input}");
}

String comparisons can be used to take action on certain input. For example, typing "exit" to break out of the loop and close the program. The string type has an Equals() method which is perfect for this.

// break if "exit"
if (input.Equals("exit"))
    break;

By default, this method is case-sensitive which means "EXIT", "eXiT", etc would not match. You can provide a StringComparison enum to make is case-insensitive.

// break if "exit"
if (input.equals (value: "exit", StringComparison.OrdinalIgnoreCase))
    break;

Classes & Methods

- Classes

Classes in C# are the heart of how it handles object-oriented programming and can be thought of as templates to store and/or operate on data.

After the class has been defined, we can create an instance of a person.

var person = new Person
{
	FirstName = "Charles",
	LastName = "Dickens",
	DateOfBirth = new DateOnly (year: 1812, month: 2, day:7)
};

internal class Person
{
	public string FirstName { get; set; }
	public string LastName { get; set; }
	public DateOnly DateOfBirth { get; set; }

}

- Properties

FirstName, LastName and DateOfBirth are all "properties" on this class. The get and set methods define how the data can be read or modified. The accessibility on the properties is marked as public, which allows access from outside of the class. This makes sense in most cases, but not others. For example, the date of birth of a person does not change, so we may not want this data to be modified after it has been set.

To that end, we can replace set with init. This prevents us from changing the data after the initial value has been set.

var person = new Person
{
	FirstName = "Charles",
	LastName = "Dickens",
	DateOfBirth = new DateOnly (year: 1812, month: 2, day:7)
};

person.DateOfBirth = new DateOnly (year: 0000, month: 0, day: 0);

internal class Person
{
	public string FirstName { get; set; }
	public string LastName { get; set; }
	public DateOnly DateOfBirth { get; init; }
}

We can also define "computed" properties that can be made up of data from existing properties. For example, we have FirstName and LastName, but what if we wanted a FullName as well? No problem.

var person = new Person
{
	FirstName = "Charles",
	LastName = "Dickens",
	DateOfBirth = new DateOnly (year: 1812, month: 2, day:7)
};

Console.WriteLine(person.FullName);

internal class Person
{
	public string FirstName { get; set; }
	public string LastName { get; set; }
	public string FullName => $"{FirstName} {LastName}";
	public DateOnly DateOfBirth { get; init; }
}

- Constructors

One potential issue with our code is that it's possible to create a person without setting any of the property values. We have a "person" but without a name or date of birth.

var person = new Person();

Constructors can be used to force us to pass data to the object before it can be fully instantiated. Every class has an implied "empty constructor" which would look like this:

internal class Person
{
	public Person()
	{
	
	}
}

Mandatory parameters can be added to the parentheses, like so:

internal class Person
{
	public Person(string firstName, string lastName, DateOnly dateOfBirth)
	{
	
}

Those parameter values can then be set on the class properties:

public Person(string firstName, string lastName, DateOnly dateOfBirth)
{
	FirstName = firstName;
	LastName = lastName;
	DateOfBirth = dateOfBirth;
}

We'll now see that a new Person cannot be created without passing this data in on the constructor.

- Methods

A class can contain methods (sometimes also called functions), that are useful when needing to get or set data associated with a particular instance of a class. Let's go back to the DateOfBirth property again. Let's say we do want to allow it to be changed (in case there was a data input error the first time), but we want some data validation logic around it. For example, we may not want a date of birth to be set to a date in the future.

The first step is to change the accessibility of the DateOfBirth property. We previously set it to init, but now we want it to change. It needs to be private set to ensure the data can only be set from inside the class (i.e. the method we're going to write).

public DateOnly DateOfBirth { get; private set; }

public bool SetDateOfBirth (DateOnly dob)
{
	if (dob > DateOnly.FromDateTime(DateTime.UtcNow))
	return false;

	DateOfBirth = dob;
	return true;
}

This SetDateOfBirth method takes a DateOnly value and return a bool to indicate whether the property was set or not. It will simply convert the current UTC time to a DateOnly format and compare it with the dob parameter supplied. If dob is greater, then it must be in the future - in which case the method will return false without changing the property. Otherwise, it will set the property and return true.

// create person with "wrong" dob
var person = new Person(firstName: "Charles", lastName: "Dickens", DateOnly.MinValue);

// this should fail
var success:bool = person.SetDateOfBirth (dob: new DateOnly (year: 2030, month:1, day:1));
Console.WriteLine(success ? "Successfully set DOB" : "Setting DOB failed");

// this should succeed
success = person.SetDateOfBirth (dob: new DateOnly (year: 1812, month:2, day:7));
Console.WriteLine(success? "Successfully set DOB" : "Setting DOB failed");

// confirm
Console.WriteLine(person.DateOfBirth);

- Polymorphism

Polymorphism is a trait of object oriented programming which makes them very versatile by allowing classes to become related by inheritance. In this example, I have an Animal class which has a single property, Name; and two further classes, Dog and Cat which inherit from Animal (declared using :).

internal class Animal
{
	public string Name { get; set; }
}

	internal class Dog: Animal
{

}

internal class Cat: Animal
{

}

We can then instantiate an instance of Dog and Cat, and they will automatically have a Name property.

var dog = new Dog { Name = "Lassie" };
var cat = new Cat { Name = "Salem" };

- Abstraction

By default, every (non-static) class can be instantiated, which is not always desirable. We created some classes to represent different animals in the previous example, but it may not make sense to be able to create an instance of the Animal class itself.

In this case, we can mark the class as abstract.

- Overrides

We can also declare abstract methods on a class, which will force those inheriting it to provide their own implementation.

We don't want to define the body of the method on the abstract class, because each inheriting type (Dog, Cat, etc) will want to do this in a different way. Methods marked as abstract are mandatory, so Rider will alert us that Dog and Cat are not currently implementing them.

- Interfaces

An interface is another form of abstraction - it's like an abstract class, but can only contain methods and properties. You cannot define methods that also have implementations.

The naming convention for an interface is to have it begin with an "I".

internal interface IAnimal
{
	string Name { get; }
	void MakeNoise();
}

A class can inherit from an interface in the same way as an abstract class.

public class Dog: IAnimal
{
	public string Name { get; }
	public Dog(string name)
	{
		Name = name;
	}

public void MakeNoise()
	{
	Console.WriteLine($"Woof, my name is {Name}.");
		}
}

Error Handling

- Exceptions

When bad things happen in C#, they can cause exceptions to be thrown. If those exceptions are not caught and dealt with, the program will crash. For example - the following code will throw an IndexOutOfRangeException because we're trying to access the 6th element of an array that only has 5 elements.

It will then crash before it gets to the second Console.WriteLine().

var array = new[] { 1, 2, 3, 4, 5 };
Console.WriteLine(array[5]);
Console.WriteLine("I'm still alive...");

- Try/Catch

Exceptions can be handled using the try / catch keywords. If we structure the code this way, the exception will be handled safely and we will see the second line get printed, as the program will no longer crash.

var array = new[] { 1, 2, 3, 4, 5 };
try
{
	Console.WriteLine(array[5]);
}
catch (Exception e)
{
	Console.WriteLine($"Something went wrong: {e.Message}");
}

Console.WriteLine("I'm still alive...");

The try/catch blocks add quite a lot of bulk to the code, so instead of being over-reliant on them you should really write your code more safely in the first place.

In addition to try / catch, there's the finally keyword. Code inside the finally block is always executed, regardless of whether an exception was thrown or not. This is useful for scenarios such as database access because we would likely want to close the connection in either case. Failure to do so could lead to connection exhaustion or database locks, etc.

var conn = new SqlConnection("blah");
conn.Open();

var command = new SqlCommand(cmdText: "insert whatever into something...");

try
{
	// execute db query
	command.ExecuteNonQuery();
}
catch
{
	// catch any exceptions
	// raise error to user
}
finally
{
	conn.Close();
}

- Catching Specific Exceptions

There are multiple types of exception in C#, all of which inherit from the base Exception class. The IndexOutOfRangeException mentioned earlier inherits from SystemException, which in turn inherits from Exception. By putting Exception in the catch block, we're catching every single type of exception that C# can produce.

This is useful in terms of accounting for all possibilities, but there may be cases where we actually want to know what kind of exception is being thrown and act accordingly. For example, if we're working with threads and cancellation tokens, there are a few exceptions that we could potentially see.

Declaring multiple catch blocks allows us to handle each case.

try
{
	// do something
}
catch (TaskCanceledException)
{
	// handle task cancelled
}
catch (ThreadAbortException)
{
	// handle thread aborted
}
catch (OperationCanceledException)
{
	// handle operation cancelled
}
catch (Exception)
{
	// catch all
}

- Error Propagation

It's quite common to structure code in such a way that functions will call other functions. This is generally because modular code is more usable and leads to less code repetition. In this example, I have a program that opens a file and reads the content as a string.

using System;
using System.IO;

var content:string = ReadFile(path: "C:\\test.txt");
Console.WriteLine(content);

string ReadFile(string path)
{
	using var fs:FileStream = OpenFileStream(path);
	using var sr = new StreamReader(fs);
	
	return sr.ReadToEnd();
}

FileStream OpenFileStream(string path)
{
	return File.OpenRead(path);
}

When I run this code, OpenFileStream throws a FileNotFoundException because the path C:\test.txt does not exist on my computer. So how do we handle such a case? We could wrap the call to File.OpenRead() in a try/catch block, but the method still has to return a FileStream and we don't have one.

If we return null, ReadFile will then throw an ArgumentNullException because you can't pass null into a StreamReader. That would force us to implement another try/catch and then figure out how to return data back to the main caller.

All this gets very messy and is really not necessary as exceptions in C# are automatically propagated up the call stack. This means if any method along the chain throws an exception, it is automatically passed back to the original caller.

try
{
	var content:string = ReadFile(path:"C:\\test.txt");
	Console.WriteLine(content);
}
catch (IOException e)
{
	Console.WriteLine(e.Message);
}

You should only catch exceptions in a method if that method can reasonably recover from the error. In this example, maybe we would want OpenFileStream to catch the FileNotFoundException, create a new file and return that FileStream back.

FileStream OpenFileStream(string path)
{
	try
	{
		return File.OpenRead (path);
	}
catch (FileNotFoundException)
	{
		return File.Create(path);
	}
}

This is the only exception the method will catch, because it's the only error it can recover from. Any other exception will still be thrown up the call stack. Do not catch exceptions in a method that cannot recover it - allow the error to propagate up the stack until it reaches a point where it can be recovered, or the error presented to an end user.

Generics

- Generic Types

We've seen instances where letters such as T are used to represent a data type, such as List<T>. This allows you to create a list containing any data type, even custom classes that you create. For example, we could have a List<Person>.

using System.Collections.Generic;
var people = new List<Person>();

internal class Person
{
	
}

This is far more flexible than having specific concrete implementations only for C#'s default data types. You can also leverage generics in your own classes and methods. Let's use (de)serialization as a working example.

byte[] SerializePerson (Person person)
{
	using var ms = new MemoryStream();
	JsonSerializer.Serialize(ms, person);
	
	return ms.ToArray();
}

Person DeserializePerson(byte[] json)
{
	using var ms = new MemoryStream(json);
	return JsonSerializer.Deserialize<Person>(ms);
}

[Serializable]
internal class Person
{
	[JsonPropertyName("first_name")] public string FirstName { get; set; }
	[JsonPropertyName("last_name")] public string LastName { get; set; }
}

Here we have a Person class that has been decorated with the Serializable attribute and each property in that class has the JsonPropertyName attribute. The SerializePerson method takes in a Person and uses the .NET System.Text.Json.JsonSerializer to convert it to a JSON string (encoded as bytes). The DeserializePerson method then does the opposite - it takes in the encoded JSON string and returns a Person object.

var person = new Person
{
	FirstName = "VergaName",
	LastName = "SurnameVerga"
};

var json:byte[] = SerializePerson (person);
Console.WriteLine(Encoding.Default.GetString(json));

Now imagine that we have multiple classes that we want to serialize and deserialize - having separate methods for every class is clearly not very efficient and would be a maintenance nightmare. What we can do instead is refactor these methods to accept generics. We can do this by replacing the concrete type of Person, with T.

byte[] SerializePerson<T>(T obj)
{
	using var ms = new MemoryStream();
	JsonSerializer.Serialize(ms, obj);
	
	return ms.ToArray();
}

T DeserializePerson<T>(byte[] json)
{
	using var ms = new MemoryStream(json);
	return JsonSerializer.Deserialize<T>(ms);
}

- Contraints

One concern that generics introduce is passing a nonsensical data type into the method. These methods are designed for serializing/deserializing objects to/from JSON, but not everything is serializable. For example - the compiler allows us to pass an empty memory span which builds just fine, but will throw an InvalidOperationException at runtime.

We could (and perhaps should) implement a try/catch which will return an empty byte array, but it would also be useful if we could prevent a developer from passing in this data type in the first place. These are called constraints.

Constraints are declared using the where keyword and are usually found on methods or classes. The syntax is where T :, followed by a comma-separated list of constraints.

byte[] Serialize<T>(T obj) where T : class

In this example, the class constraint means that T must be a reference type (classes are reference types, and structs are value types). All the possible constraints are documented by Microsoft.

Now the compiler won't allow us to build the code.

Concurrency

- Threads

New threads are easy to create from the System.Threading namespace, with either a ThreadStart or ParameterizedThreadStart delegate. The former is a method that does not have input parameters, and the later is a method that has one input parameter. In both cases, the return type is always void.

A new thread will not run until Start is called.

using System;
using System.Threading;

var t1 = new Thread (RunLoop);
var t2 = new Thread(RunLoop);

t1.Start();
t2.Start();

void RunLoop()
{
	for (var i = 1; i <= 10; i++)
		Console.WriteLine(i);
}

A thread is run in the foreground by default, but can be made to run in the background by setting the IsBackground property to true.

var t1 = new Thread (RunLoop)
{
	IsBackground = true
};

The only difference being that a program will not automatically close if there are any foreground threads running. Backgrounds threads will not keep the program alive if all foregrounds threads finish.

A single parameter can be passed to a thread via it's Start method. The parameter on the method must be declared as an object, which means a type check should be performed on it before use.

t1.Start(parameter: 10);
t2.Start(parameter: 5);

void RunLoop(object obj)
{
	if (obj is not int counter)
		return;
	
	for (var i = 1; i <= counter; i++)
		Console.WriteLine(i);
}

- Task

The Task.Run methods looks the same as when dealing with the thread, but returns a Task<TResult>. These tasks must be awaited somewhere to prevent the program from closing before they're finished. A Task can be waited on with the await keyword and execution flow will be blocked until the task is complete.

using System;
using System.Threading.Tasks;

var t1:Task = Task.Run(PrintLoop);
var t2:Task = Task.Run(PrintLoop);


await Task. WhenAll(t1, t2);

void PrintLoop()
{
	for (var i = 0; i <= 10; i++)
		Console.WriteLine(i);
}

The return value of a Task can be accessed upon completion.

var result:int = await Task.Run(RunLoop);
Console.WriteLine(result);

int RunLoop()
{
	var total = 0;

	for (var i = 1; i <= 10; i++)
		total += i;
	
	return total;
}

- Parallel

The Parallel class has a few interesting methods that are designed to run concurrently over a range or collection - like the for and foreach loops. Parallel.For takes a starting and ending integer, and a method to execute. The method accepts the integer as a parameter.

using System;
using System.Threading.Tasks;

Parallel. For (fromInclusive:1, toExclusive: 10, Print);

void Print(int i)
{
	Console.WriteLine($"This is loop #{i}");
}

Parallel.ForEach takes a collection and a method - the method accepts the datatype, T, of the collection

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

var people = new List<Person>
{
	new(name: "Stephen King"),
	new (name: "George Orwell"),
	new(name: "Charles Dickens"),
	new(name: "Mark Twain")
};

Parallel.ForEach(people, PrintPeople);

void PrintPeople (Person person)
{
	Console.WriteLine(person.Name);
}

In each case, you'll see that the values are printed to the console in a seemingly random order. This is because every loop iteration is run at exactly the same time.

- Channels

Channels can be used as a means of communicating directly between threads and tasks. They are effectively a thread-safe queue that allows a "producer" to write message into and a "consumer" to read messages from. There are two main types of channel - unbounded and bounded. An unbound channel allows for infinite message capacity, whilst a bounded channel explicitly sets a maximum message capacity.

When creating a channel, you must provide a data type, T depending on what you want the channel to handle.

using System;
using System.Threading. Channels;
using System.Threading.Tasks;

var channel = Channel.CreateUnbounded<string>();

var task = Task.Run(async () =>
{
	for (var i = 0; i < 10; i++)
		await channel.Writer.WriteAsync(item: $"This is loop {i}");
	
	channel.Writer.Complete();
});

Here, we're creating a new unbounded channel of type string, then dropping into a Task.Run. Within that task, we're calling WriteAsync on the channel's Writer with a message. One we've finished writing we call Complete() on the writer, after which, no more messages can be sent.

while (!task.IsCompleted)
{
	try
	{
		var message:string = await channel.Reader.ReadAsync();
		Console.WriteLine(message);
	}
	catch (ChannelClosedException)
	{
	Console.WriteLine("Channel has been closed");
	break;
	}
}

Outside of the task, we drop into another loop for as long as our task is not complete. We continuously attempt to read messages from the channel's Reader, using ReadAsync() and write them to the console. We're catching the ChannelClosedException, which is triggered once Complete is called on the writer. We use this as a signal to break out of the loop and continue execution flow.

LINQ

LINQ is short for Language-Integrated Query and allows you to write queries against strongly typed collections using C# keywords and operators. It's especially useful when querying objects that have multiple properties. LINQ can be complicated to explain, so here's a quick example to get started.

We have our Person class again and a collection of random people.

internal class Person
{
	public string FirstName { get; init; }
	public string LastName { get; init; }
	public string FullName
		=> $"{FirstName} {LastName}";

	public DateOnly DateOfBirth { get; init; }
	public int Age
		=> DateTime.Today. Year
}


//random people
var people = new List<Person>
{

new() { FirstName = "Jose", LastName = "Gomez", DateOfBirth = new DateOnly (year: 1957, month: 2, day:3) },
new() { FirstName = "Carolyn", LastName = "Farias", DateOfBirth = new DateOnly (year:1972, month:11, day:22) },
new() { FirstName = "Rosemarie", LastName = "Pickens", DateOfBirth = new DateOnly (year: 1993, month: 5, day:17) },
new() { FirstName = "Hester", LastName = "Funk", DateOfBirth = new DateOnly (year: 1986, month:11, day:1) },
new() { FirstName = "Dianne", LastName = "Soria", DateOfBirth = new DateOnly (year: 1979, month:7, day:26)}
};

How would you go about finding every person that was born after 1975? LINQ makes this very easy.

// find everyone who was born after 1975
var persons:Person[] = people.Where(p:Person => p.DateOfBirth. Year > 1975).ToArray();
Console.WriteLine($"Found {persons. Length} people. They are: ");

// print them
foreach (var person in persons)
Console.WriteLine($"{person.FullName}, born on {person.DateOfBirth}, age {person.Age}.");

- Where

Where filters the collection based on the provided predicate and returns an IEnumerable<T>. In the example shown previously, we used this to find people where the Year property of their DateOfBirth was greater than 1975. The predicate can contain one or multiple conditions and be a combination of AND's, ORs, and so on.

Here, we're looking for people 50 or younger and born on a Monday.

// find everyone 50 or younger, born on a monday
var persons:Person[] = people. Where (p:Person => p.DateOfBirth.DayOfWeek== DayOfWeek. Monday && p.Age <= 50).ToArray(); people.Where(p:Person

Predicates that have multiple conditions can look a bit unwieldly when written in this way. You may also extract the code in to a separate method, which is also useful if you need to use it multiple times.

var persons:Person[] = people.Where(FiftyOrYoungerBornOnMonday).ToArray();

bool FiftyOrYoungerBornOn Monday (Person person)
{
	return person.Age <= 50 &&
		person.DateOfBirth.DayOfWeek == DayOfWeek. Monday;
}

- Any

Any returns a bool, indicating whether a collection has any elements matching the predicate. If no predicate is provided, it indicates whether or not the collection is empty.

if (people. Any())
	Console.WriteLine("People has elements");

if (!people.Any (p:Person => p.Age < 20))
Console.WriteLine("Nobody is under the age of 20");

- First(OrDefault)

First returns the first element of a collection that matches the predicate. If no predicate is provided, it will just return the first element of the collection.

var person = people.First(p:Person => p.LastName.StartsWith("F"));
Console.WriteLine(person.FullName);

If no element matches the predicate, an InvalidOperationException, "sequence contains no matching element" exception will be thrown. FirstOrDefault can be used to return null if no elements are found. This saves the program from throwing an exception, but a null reference check should be performed on the returned object.

var person = people.FirstOrDefault(p:Person => p.LastName.StartsWith("D"));
Console.WriteLine(person is null ? "No matching person found" : person.FullName);

- OrderBy(Descending)

OrderBy and OrderByDescending allow you to order a collection based on a key. It uses the equality comparer for the specific data type.

// youngest to oldest
var ascending:lOrdered Enumerable<Person> = people.OrderBy(p:Person => p.Age);
// oldest to youngest
var descending:lOrdered Enumerable<Person> = people.OrderByDescending (p:Person => p.Age);

You can implement your own comparer (inherit from IComparer) to compare custom data types.

Platform Invoke

Platform Invoke, often shortened to P/Invoke, allows you to access functions in unmanaged C libraries from your managed C# code. The most common use case (particularly in the offensive security world) is to access the native Windows APIs (although since .NET is cross-platform it can be used with any C library).

These functions are declared with the extern keywork and DllImport attribute. The following example shows how to import the OpenProcess API from kernel32.dll. We simply define the function signature (i.e. the function name, any input parameters, and the return type).

[DllImport(dllName:"kernel32", SetLastError = true)]
static extern nint OpenProcess (uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId);

In most cases, you will also want to set SetLastError to true, as this allows you to retrieve the error code should the API call fail, with Marhsal.GetLastWin32Error.

var hProcess:nint = OpenProcess (dwDesiredAccess: 0xF01FF, blnheritHandle: false, dwProcessld: 26768);

if (hProcess == nint.Zero)
	Console.WriteLine("OpenProcess failed: {0}", Marshal.GetLastWin32Error());
else
Console.WriteLine("hProcess: 0x{0}", hProcess);

- Marshalling

Some Windows APIs such as LoadLibraryW require an LPCWSTR, which is a pointer to a null-terminated 16-bit unicode string. P/Invoke can automatically marshal between managed and unmanaged datatypes using the MarshalAs attribute, so we don't have to do it manually.

[DllImport(dllName: "kernel32", SetLastError = true)]
static extern nint LoadLibraryW(
	[MarshalAs(UnmanagedType.LPWStr)] string lpLibFileName);

var hProcess:nint = LoadLibraryW(IpLibFileName: "amsi.dll");

- Enums

Enums can be used in conjunction with APIs that have pre-determined values for their parameters. For instance, OpenProcess requires a set of ProcessAccessRights which are defined here. Instead of remembering values, we can define then in an enum - as long as the underlying datatype matches (in this case, uint), it will work.

The Flags attribute on the enum tells C# that the values can be treated as a bit field (i.e. you can perform bitwise operations on them). In the particular case of this API, it allows you to build up a desired access value of the exact privileges that you want the final handle to have.

[DllImport(dllName: "kernel32", SetLastError = true)]
static extern nint OpenProcess (ProcessAccessRights dwDesiredAccess, bool bInheritHandle, uint dwProcessId);

var hProcess:nint = OpenProcess(
	dwDesiredAccess: ProcessAccessRights.PROCESS_VM_READ | ProcessAccessRights.PROCESS_VM_WRITE,
	blnheritHandle: false, dwProcessId: 26768);

[Flags]
internal enum ProcessAccessRights: uint
{
	// many missing for brevity
	PROCESS_VM_READ = 0x0010,
	PROCESS_VM_WRITE = 0x0020,
	PROCESS_VM_OPERATION = 0x0008
}

NuGet

NuGet is the .NET package manager, which allows you to bring external code into your project. The most popular NuGet repository is https://www.nuget.org/, where anybody can write and publish libraries.

Packages can also be installed directly from the command line.

dotnet add package RestSharp

As a test, we can call the CoinGecko API to get the current Bitcoin prices.

var client = new RestClient(baseUrl: new Uri("https://api.coingecko.com/api/v3/"));
var req = new RestRequest(resource: "simple/price?ids=bitcoin&vs_currencies=GBP%2CUSD");
var resp = await client.GetAsync<Root>(req);

Console.WriteLine("Current BTC Price:");
Console.WriteLine($"GBP : £{resp.Bitcoin.Gbp}");
Console.WriteLine($"USD : ${resp.Bitcoin.Usd}");

internal class Bitcoin
{
	[JsonPropertyName("gbp")] public double Gbp { get; set; }
	[JsonPropertyName("usd")] public double Usd { get; set; }
}

internal class Root
{
	[JsonPropertyName("bitcoin")] public Bitcoin Bitcoin { get; set; }
}

- dotnet pack

You can create your own libraries and publish them as NuGet packages. First, create a new .NET Class Library project and implement a simple calculator.

using System.Numerics;

namespace Calculator;

public class Calculator<T> where T: IBinaryInteger<T>
{
	public T Add (T il, T i2)
		=> il + 12;
	
	public T Subtract(T i1, T i2)
		=> i1 - 12;

	public T Multiply(T i1, T 12)
		=> i1 × 12;

	public T Divide(T il, T i2)
		=> il / i2;
}

The usual build process to output a DLL would be something like:

dotnet build -c Release

dotnet pack

The resulting nupkg package can be shared as-is, at nuget.org or your own NuGet package server.

Last updated