C# (2024)
As ever, the terse c# notes are revision and cribsheet notes taken from the other sources. None of it is mine, but its a quick reminder.
They are slanted towards an experienced dev getting into C#.
Last updated Nov 2024.
Why C#
Well, I’m a coder for pay, and they are paying me. Some of the older C# is very dull, some of the newer c# is really good. So here are the notes.
General notes
C# is ancient, and yet has very strong releases to add all the bells and whistles it can copy from everyone else. Back in the day it did some amazing stuff with Linq and so on, but its 2024 and thats nothing special any more.
The CLR is like the JVM, and has been optimised forever. They have wasm and they have frameworks with more frameworks piled on top. However, the core use case for me is:
Server side restful services in docker images in K8s. So thats Kestral web server, dotnet core on Linux, with entity framework or ado.net to a SqlServer database.
How to ship
You compile into assemblies which are named *.dll. Its not compiled code, it is interpretted with Just In Time compiling. These assemblies are held in Nugget repositories. So your project in the Rider IDE has a nugget manager, which lets you pull nuggets of certain versions, targeted at different DotNet runtime versions.
Oh yes, DotNet runtime is the thing that runs the code, you install it. Then you code in a c# version, and c# versions are compatible with a DotNet runtime version - each DotNet version supports a range of C# (and other) languages.
Naming convention
PascalCase for classes, methods, properties and so on.
camelCase for method parameters
_camelCase for class fields.
I as the first letter of an Interface.
string
Use string and not String, and string can use == for equality
Verbatim string
string fn = @"c:\docs\stuff.txt";
string fn1 = "c:\\docs\\stuff.txt";
string interpolation
i.e. embedding variables inline in a string. Use @ for verbatim, and $ for interpolation. So you could combine with @$ or $@.
tname is a variable in this example, and @ means it is fine to use the multi-line string. Note that raw strings are better for multiline (see below).
var mySql = $@"create table {tname}
(
DataFooId bigint,
)";
Raw string
Starts and ends with a minimum of 3 quotes.
var sl = """This is "raw" and has \, ` and ".""";
Raw strings can also span multiple lines. Additionally, all white spaces preceding the closing “”” are removed from all lines and white spaces after the starting “”” are ignored. They can also be combined with interpolation.
var mySql = $"""
create table {tname}
(
DataFooId bigint,
)""";
Config
appsettings.json, with appsettings.Local.json (add to .gitignore) or appsettings.Development.json or appsettings.Production.json.
For dev and prod store the secrets in some secrets manager, and let k8s override them at runtime.
A rest server
So, c# runs on linux using dotnet core. Basically use a recent version of c# and you will be fine.
Kestrel is the webserver for c# on linux - and the most recent versions mean you can start up a webserver with a database connection, with some CRUD end points in very few lines of code.
namespaces
Stick a namespace at the top of each file, and ideally tie the namespace to the directory … but lots of teams just do strange stuff.
Aliasing types
using MyLocalType = Some.Other.ClassThingy
Stupid variable names matching keywords
If you prefix the name with @ then you can use @for, or @switch etc as variable names.
Constants
public const string Hello = "Hello"; // Evaluated at compile time.
public static readonly string Hi = "Hello";
public static readonly DateTime StartTime = DateTime.Now; // At runtime.
A const when referenced in another assembly will be baked into their compiled assembly - just pulling in a new assembly version will not update unless they recompile.
var for implicit typing
var x = "hello";
Target typed
System.Text.StringBuilder sb1 = new();
Equality
Classes do not compare using == unless they implement the Equals method. To ignore the Equals and see if classes have the same reference you can use object.ReferenceEquals.
Records implement Equals automatically.
Conditional
int x = (s=="true")?100:-1;
Destructure
If its implemented then you can extract values of fields into local variables, in all sorts of expressions.
Array initialisation
char [] v = {'a','e','i','o','u'};
int[,] r =
{
{1,2},
{3,4}
}
Array Indexing
You can use ranges to get slices of arrays, and using the hat (^) you can also offset from the end of an array.
var array = new int[] {1,2,3,4,5};
var slice1 = array[2..^3]; // from index 2 to 3rds from end
var slice2 = array[..^3]; // from start to 3rd from end
var slice3 = array[2..]; // from index 2 to end
var slice4 = array[..]; // all of it
Ranges
As mentioned you can use ^1 to get the last item of an array. This is all done using the types Index and Range, which you can read about on your own.
Access modifiers
public, internal, private, protected, protected internal, private protected.
Parameters
Out parameter
When calling you can declare the variable as you call it. Out params are often used to return multiple values.
Surname("John Doe", out var sname);
Console.WriteLine(sname);
void Surname(string nm, out string surname) {...}
Try Parse
This pattern will try to do something, return false if it fails, true if it works, and return the correct output in an out var. It means you can do this:
if (TryParse(myStr, out var sname)) {
// It worked and sname can now be used.
}
Multiple parameters of the same type
The last parameter in a method can be declared ‘params’, and must be an array.
int Count(params int[] theVals) {...}
Optional parameter
You can default method params, which means the caller doesn’t need to pass them in.
void Stuff(int x = -1) {...}
If you decide to pass them in you can do it using the parameter name, as in p:4, ie param p has value 4.
Stuff(x: 2);
Structs
Are value types, so you cannot have many references to it, ie its like an int.
Value types cannot be null.
Records
Immutable. Use ‘with’ keyword to copy it and replace some values in some fields. It defines Equals, ToString, GetHashCode and Deconstructor.
To clone, replacing a field value:
rec1 with { Fld2="j"};
Primary constructors in C# can go on the same line as the declaration, and all the params become properties, and this is also true for records, so they are terse as heck. But shouldn’t be used for Entity Framework which relies heavily on object reference equality.
public record FooThing(int Current, int Total, TimeSpan? DurationSinceLastChange);
Interfaces
A class can implement many interfaces. Interfaces can contain default method implementations, or just method signatures with no implementations.
Naming convention is to stick I on the front. eg IReportWriter.
Classes
Pretty much as you expect, apart from Properties which are just sugar for member variable with getters, setters and possibly init. Also primary constructors now, like the record example above.
The this keyword refers to the class instance we are in.
The base keyword refers to the class we inherit from. The object type is the base of everything.
Classes have constructors, overloaded constructors, primary constructors, private constructors. They have inheritance and partial classes. They can have extension methods…. and so on.
A virtual function can be overridden in a class which inherits from it.
An abstract class can never be instantiated. For instance it could only implement some functions from an interface. It could also declare abstract methods with no implementation.
A sealed class cannot be overridden.
A nested class (or enum etc) can access the enclosing types private members.
Properties
class Cl
{
public int Count {get; set;}
public int Other {get; init;} = -20; // init only property
}
Deconstructor
Multiple out parameter, which also means pattern matching in switch expressions all work - Records do them anyway. This is really good modern magic, and you should read the switch expressions below which all work due to deconstructors.
Generics
As in every other language…you can constrain the generic type with ‘where T:base-class’.
public IReport<T> CreateDestProvider<T>(string nm)
where T: IFileContext
{
}
Tuples
A very simple way to store values without any boiler plate.
var foo = {"foo", "bar"};
Enums
public enum WeekendDays {Saturday = 0, Sunday = 6}
int dayNum = (int)WeekendDays.Sunday;
WeekendDays wd = (WeekendDays)0;
Enum.IsDefined can be used to check an enum has a value.
Switch
There are two types, the switch statement which is typical old school imperative, and the switch expression which is pattern matching modern glory.
Switch statement
switch (f)
{
case int i when i==1:
Console.WriteLine("One");
break;
default:
break;
}
Switch expression
static string Classify(double measurement) => measurement switch
{
<-40.0 => "Too Low",
>=-40.0 and <20.0 => "Good",
>= 20.0 => "Too high",
double.NAN => "Unknonw",
};
Underscore
If you don’t want a value, you just use _. eg in a switch expression, or when calling an out parameter.
Optional, or rather all the null stuff
Sadly there is no Option and so all the projects write their own - which is a mistake. Code in the language you have not one you used to code in IMHO.
Null conditional, Nullable
A special type which means you can use the ? operators, and the returned value will also be Nullable.
string ? s = null;
str?.Length has type Nullable<int>
Null coalescing operator ??
If the value to the left is null use the value to the right.
string s1 = f.foo?.ToString()??"";
string s2 = f.fld1?.foo?.ToString()??"";
if (f.fld1?.foo==null)
{
}
Null forgiving operator
class Person
{
public string? MiddleName; // Optional middle name, so no middle name would be null.
}
...
Console.WriteLine(person.MiddleName.Length); // compiler warns that MiddleName can be null
Console.WriteLine(person.MiddleName!.Length); // compiler doesnt warn.
Not null object destructure pattern
Using { } calls destructure which confirms it is not null.
public static void Test(object? obj)
{
if (obj is {}) // could do if (obj is not null)
{
Console.WriteLine("not null");
}
}
also:
if (a is int b) // if a is of type int? then this will be true if its not null.
and
if (a is {} b)
are compiled into exactly the same CIL. (intermediate language - kinda java byte code).
Logical Patterns
OK, all that null stuff is wierd, and silly compared to Option. But hang on, the language is pretty good at other stuff, such as the modern pattern matching:
In particular the logical patterns stuff:
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/patterns
is operator
Is matches a pattern.
static bool IsLetter(char c) => c is (>='a' and <='z') or (>='A' and <='Z');
You can also use var to create a named variable as part of an is
bool IsJohnJowBloggs(string name)=>
name.ToUpper() is var upper && (upper=='JOE BLOGGS" || upper="JOHN BLOGGS");
as operator
It casts an object, resulting in null if its not the right type, or a variable of that type.
GetType and typeof
GetType is runtime, which typeof is compile time.
Caller info attributes
You can annotate method parameters to have them populated with information about the call stack
[CallerMemberName]
[CallerFilePath]
[CallerLineNumber]
nameof
string name = nameof (someVariable); // name is someVariable
Lambdas, Delegates, Func
C# does it all. A delegate type defines a type which can be used for parameters and return types. Any function with the same signature can be used when the delegate type is used.
delegate int MathThing(int n);
int Cubed(int n) => n*n*n;
MathThing f1 = Cubed;
int c = f1(3);
Streaming
If you ever thought Scala FS2 pipes were cool you will love IEnumerable and yield. yield means return the next value now, and on the next call resume the function on the line after it. Wierd eh? Very like the way C# async functions work- building a state machine for your compiled code to hide the complexity from the source code.
public class Foo
{
private int _acc = 2;
public IEnumerable<int> Generator()
{
foreach (var n in Enumerable.Range(1, 10))
{
_acc = n*10 + _acc;
yield return _acc;
// execution resumes here when the call asks for the next item in the enumeration
}
}
public static void EgTest()
{
var p = new Foo();
foreach (var n in p.Generator())
{
Console.WriteLine(n);
}
}
}
Other stuff
The indexer allows you to access like an array.
Finalizers execute before garbage collection.
Partial classes and methods - you could have a code generator and create many of the functions in a partial class. You then have another file holding the same partial class which you manually code. So you can have two files, each holding the same partial class definition, but one could be code generated (for instance).