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.
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:
Object | Casing |
Classes | PascalCase |
Public Members | PascalCase |
Private Members | _camelCase |
Methods | PascalCase |
Variables | camelCase |
Enums | PascalCase |
- 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):
However, you cannot implicitly cast a double to an integer - it must be explicitly cast:
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:
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.
- 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.
- 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.
In this example, the printed order is 1, 2, and 3.
The stack functions in the same way using the Push and Pop methods.
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
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:
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:
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:
This can be condensed down into:
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.
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.
- 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.
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.
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.
- Scopes
For example, the following code will throw the error "cannot find value `foo` in this scope" and will not compile.
- Command Line Arguments
If your application has mandatory arguments, it's common to do a length check and exit from the program if not enough are provided.
- Prompting for 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.
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.
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.
- 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.
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.
- 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.
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:
Mandatory parameters can be added to the parentheses, like so:
Those parameter values can then be set on the class properties:
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).
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.
- 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 :).
We can then instantiate an instance of Dog and Cat, and they will automatically have a Name property.
- 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".
A class can inherit from an interface in the same way as an abstract class.
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().
- 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.
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.
- 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.
- 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.
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.
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.
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>.
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.
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.
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.
- 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.
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.
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.
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.
- 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.
The return value of a Task can be accessed upon completion.
- 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.
Parallel.ForEach takes a collection and a method - the method accepts the datatype, T, of the collection
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.
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.
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.
How would you go about finding every person that was born after 1975? LINQ makes this very easy.
- 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.
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.
- 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.
- 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.
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.
- 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.
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).
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.
- 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.
- 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.
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.
- 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.
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