Starting up with C# and .Net Core

Starting up with C# and .Net Core

Get the tools

.Net core

Ensure you have the .net core sdk installed.

Download and install it from here

The run the following command to ensure you got it all right:

dotnet

(then)

dotnet --list-sdks

The first should just show you itself. The second lists the sdks you have installed with their version numbers.

IDEs

You should at least have Visual Studio Code

Start it up and from the Extensions icon (on the left side, 5th down probably) add the following extensions:

  • C# (from Microsoft)
  • GitLens

Optionally you can also install Visual Studio (large IDE) which is superb for debugging. Can come in handy. Select Community Edition, which is free.

Basic c# .net core command line application

Create a basic application, Add a folder Test and then there:

dotnet new nunit

This is a scaffolding operation for a unit test project, the code generated is the csproj project files, and then the UnitTest1.cs file which contains the unit test itself in C# code.

You can build and run test tests simply by:

dotnet test

If you just wanted to build, without running the test, it is just

dotnet build

The start code by writing:

code .

(Notice the dot!)

And it will start up with the project in the editor.

The dotnet new command has multiple templates to choose from. Get a list by using the dotnet new --list

Create a new sibling folder to test (name it cons), and then run dotnet new console which gives you the basic Hello World example program.

You can also add yet another sibling folder MyLib, and then run dotnet new classlib.

Notice, some of the classnames and namespaces are in lower case only. In C# the convention is Pascal convention, first letter Uppercase, the rest lower case.

More info on dotnet new

Now change the name from Class1 to Math

Add a method like:

public double Add (double a, double b)
{
    return a + b;
}

The project is called mylib (as the file mylib.csproj). You rename this to e.g. MyMath.csproj.

Now, go back to the root folder for these three projects and run:

dotnet new sln

It will create a file named after the folder you're in, in my case examples.sln

Assuming you have installed VS Community edition, start it up doing:

devenv examples.sln

It will be slower on startup than VS code, as expected - it is a bigger IDE, but after a while you'll see the Solution explorer to the right side, showing a single node named Solution (which is the last file you created)

Now from the context menu on that node, select Add/Existing projects, and located and add the three csproj files you have recently created.

When finished, it should look like:

Select the top level Build menu, and do Build solution

If you get any problems, open up the 3 csproj files (double click in sln). Ensure the Cons and the Test have TargetFramework set to netcoreapp3.1 and the MyMath have the same set to netstandard2.1

Do another build, and you should be fine..... perhaps a context menu Restore Nuget packages, if it is hard to get going.

Now, click the Dependency node on Cons and select Add Project Reference. and then select the MyMath project. Do the same for the Test project.

Now, take a look inside the csproj files and see that it has now added a ProjectReference node. Now you know how it looks, and you can add these in the editor later, if you prefer that.

<ItemGroup>
    <ProjectReference Include="..\mylib\MyMath.csproj" />
  </ItemGroup>

Now change your program code in Program.cs to look like:

using System;
using System.Linq;

namespace cons
{
    public class Program
    {
        static void Main(string[] args)
        {
            if (!args.Any())
            {
                Console.WriteLine("Math arg1 arg2");
                return;
            }

            if (args.Length != 2)
            {
                Console.WriteLine($"Math needs two arguments, just got {args.Length}");
                return;
            }

            var ok = double.TryParse(args[0], out double a);
            if (!ok)
            {
                Console.WriteLine($"First argument must be a float number, but was {args[0]}");
                return;
            }

            ok = double.TryParse(args[1], out double b);
            if (!ok)
            {
                Console.WriteLine($"Second argument must be a float number, but was {args[1]}");
                return;
            }

            var math = new MyLib.Math();
            var result = math.Add(a, b);
            Console.WriteLine($"Result is {result:F2}");
        }
    }
}

And you can try to run it doing :

dotnet run 45.5  45
Result is 90.50

or without arguments or whatever, to check the error handling.

Notice in the code the use of string interpolation, using the $" {somevar}" syntax.

Notice also the inclusion of the System.Linq, which gave you access to the Any method.

And notice the use of the var keyword, which says that the variable should be anything inferred by what is on the right hand side. So in this case the result will be a double, because the math.Add statement returns a double.

Now, the code is a bit duplicated, and the static Main is a bit big, so let us rearrange it a bit.

using System;
using System.Collections.Generic;
using System.Linq;

namespace cons
{
    public class Program
    {
        static void Main(string[] args)
        {
            var program = new Program(args);
            program.Run();
        }

        private readonly IReadOnlyCollection<string> arguments;

        public Program(IReadOnlyCollection<string> args)
        {
            arguments = args;
        }

        public void Run()
        {
            if (!arguments.Any())
            {
                Console.WriteLine("Math arg1 arg2");
                return;
            }

            if (arguments.Count != 2)
            {
                Console.WriteLine($"Math needs two arguments, just got {arguments.Count}");
                return;
            }

            var result1 = Parse(arguments.First(), "First");
            if (!result1.Ok)
                return;
            var result2 = Parse(arguments.Skip(1).First(), "Second");
            if (!result2.Ok)
                return;
            
            var math = new MyLib.Math();
            var result = math.Add(result1.Result, result2.Result);
            Console.WriteLine($"Result is {result:F2}");
        }

        private (bool Ok, double Result) Parse(string arg, string position)
        {
            var ok = double.TryParse(arguments.First(), out double parsedValue);
            if (!ok)
            {
                Console.WriteLine($"{position} argument must be a float number, but was {arg}");
            }

            return (ok, parsedValue);
        }
    }
}

We have now introduced a few more concepts:

  • The program is split into a simple static Main, and the rest as Instance methods.
  • Constructors with parameters
  • Local member variable
  • Generic list with string
  • Tuples as results from a method
  • Private function
dotnet run 45.5  45
Result is 91.00

Oops - something went wrong somewhere..... That result is not correct.

Unit testing

Now go into the test project.

Return to Visual Studio,a dn look at the left side. You should see a Test Explorer window there. (If not, get it from the top menu, Test/Test Explorer)

Press the Run All button to the left in the top button bar of that hub.

It should go green. Double click the Test1 node, to see the code.

We now want to unit test the program we wrote.

Let us start with checking the Math library we had

        [Test]
        public void TestMath()
        {
            var sut = new MyLib.Math();
            var result = sut.Add(45.5, 45);
            Assert.That(result, Is.EqualTo(90.5).Within(0.001));
        }

Notice the Within constraint added. Doubles are never exact, so we put a range to it.

Now do a Test All again, and it should go green.

Testing the program

The program itself seems to have the error, but it is not that easy to test. So let us make the program itself more testable.

First, add a project reference in the Test project to the cons project.

Then make the Run method return a double, which should be the result from the Add operation. There is one snag here, and that is in case of errors, we can't really see that explicitly. So we will introduce a bool Property.

Add the following to the class:

        public bool Ok { get; private set; } = false;

Now, in the Run method, after the parsing, just before the call the Math.Add, do:

    Ok = true;

This property is now read only from the outside. The private set'er ensures we can set it from inside the class.

Then, make the Parse method public too. We do this so that we can test this by itself. Make a /// comment above the Parse method in Visual Studio, it should expand to a comment block: Add the comment Made public for testing

Go back to the test project, and create a new test class for testing the Program class. The other is now for Math Test. And yes, you should rename it!

Add a new Test method, but now we shall use another type of test:

using System.Collections.Generic;
using cons;
using NUnit.Framework;

namespace examples
{
    public class ProgramTest
    {
        [TestCase("45.5", "45", 90.5)]
        public void TestRunMethod(string a, string b, double expected)
        {
            var sut = new Program(new List<string> {a,b});

            var result = sut.Run();

            Assert.That(result, Is.EqualTo(expected).Within(0.001));
        }
    }
}

This is a parameterized tests, and we can add more cases if we like. Add a few more to check.

Running this test, will show it as red. We sort of knew that already, so we have to dig deeper. We know that the Math test is green, this is sort of integrating the two. Let us see if we can remove the Math class from the equation first.

We start off by adding an interface to the Math class:

    public interface IMath
    {
        double Add (double a, double b);
    }

    public class Math : IMath
    {
        public double Add (double a, double b)
        {
            return a + b;
        }
    }

And then we inject that into the Program instead of new'ing it up internally:

 public class Program
    {
        static void Main(string[] args)
        {
            var program = new Program(args, new MyLib.Math());
            program.Run();
        }

        private readonly IReadOnlyCollection<string> arguments;
        private IMath Math { get; }

        public Program(IReadOnlyCollection<string> args, IMath math)
        {
            arguments = args;
            Math = math;
        }

and using it, we can remove the new'ing further down, and just use the private Math Property. Notice we don't have any setter on it, since it is initialized in the constructor.

        var result = Math.Add(result1.Result, result2.Result);
        Console.WriteLine($"Result is {result:F2}");
        return result;

Now, we can turn to the test again, and let us start with adding in mocking.

In the test project csproj file, add in the NSubstitute package:

 <ItemGroup>
    <PackageReference Include="NUnit" Version="3.12.0" />
    <PackageReference Include="NUnit3TestAdapter" Version="3.16.1" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
    <PackageReference Include="NSubstitute" Version="4.2.2" />
  </ItemGroup>

First, you need to add in the new MyLib.Math() to the ctor for the Program call.

Then create a new copy of that test, call it TestRunMethodOnly, but now add in a mock for the Math instance.

        [Test]
        public void TestRunMethodOnly()
        {
            var math = Substitute.For<IMath>();
            math.Add(45.5, 45).Returns(90.5);
            var sut = new Program(new List<string> { "45.5", "45" }, math);

            var result = sut.Run();

            Assert.That(result, Is.EqualTo(90.5).Within(0.001));
        }

Try to run these. You will see they both are red, which means the error must be in the Program class, since the last one doesn't have the real Math class, but are using a mock.

Now, let us test the Parse method. Notice we still need to construct the Program class, but we will not be using the arguments, so they can be dummies both of them. We will still use the same arguments though, just since we have it.

        [TestCase("10", 10.0)]
        [TestCase("20.4", 20.4)]
        [TestCase("45.5", 45.5)]
        public void TestParse(string arg, double expected)
        {
            var math = Substitute.For<IMath>();
            var sut = new Program(new List<string> { "45.5", "45" }, math);
            var result = sut.Parse(arg, "whatever");
            Assert.Multiple(() =>
            {
                Assert.That(result.Ok);
                Assert.That(result.Result, Is.EqualTo(expected).Within(0.001));
            });
        }

Running this, we get the following results:

And, we see that regardless of what we give in, the result is equal to 45.5. Which incidentially is the same number as what we have inserted as parameter in the constructor, so the Parse method seems to pick that one up instead of the real parameter.

Looking at the Parse method we see the line:

public (bool Ok, double Result) Parse(string arg, string position)
        {
            var ok = double.TryParse(arguments.First(), out double parsedValue);

and indeed, the argument is arguments.First(), and not the arg parameter to the method. We fix this, and then rerun the tests:

Debugging the NUnit3TestAdapter

A test adapter sits between a TestHost and the test framework. If you use Visual Studio or dotnet, both starts up a TestHost as a seperate process. The testhost is responsible for locating the adapters, and then invoke them to run the test frameworks on the test code. Debugging the adapters is hard, because it sits between these processes, of which you have no control. Continue Reading →

Beware of the end-of-life for .Net Framework and .Net Core versions

There are multiple versions of .Net Framework, and also of .Net Core. Many of the versions have now reached their end of life point. That means the versions are no longer supported, not with bug fixes and not with security fixes either.

Some version are in the LTS (Long Term Support) mode. This means they will receive critical fixes, and compatible fixes for a period of 3 years. Microsoft also states that every second release will be set up for LTS.

Looking at how this is for .Net Core, the following screen shot is from the .Net Core LifeCycle per Nov 6th 2019.

Note that the only fully supported .Net Core version will be version 3.0 by the end of this year! The .Net Core 2.1 will be on LTS however.

The .Net Core Support Policy describes in more details how this works, also what release cadence they have right now.

And a similar list for the .Net Framework lifecycle

As can be seen here the .net 4.0 to .net 4.5.1 is at end of life. If you got anything here, it is really time to upgrade.

Don't panic

Now all of this doesn't mean that you can't use the older versions, at least when they are fairly new, like .Net Core 2.2. It is more about the risk increasing as other parts of your system evolves.

What you will see is that there will be no further minor upgrades, and no bugs and security fixes on your chosen framework version. This may not be that much of an issue for the near future. Depending on what operating systems you're supporting, you may get into unwanted situation for newer ones. It may mean you might need to hold back on updating certain other components in your system.

And, as time goes on, you could expect to see other tools dropping support.

E.g. For NUnit - support for .Net Core 1.* is being dropped now.

Supported OS lifecycles

Your chosen .net framework or .net core version will run on an operating system, and the different versions support different version of the available operating system. Microsoft will handle only OS support for their own operating system. When you try to figure out where it all ends, you need to work through this dependency chain of lifecycle support. It is pretty obvious that the earlier versions you try to support, the harder it will be.

The .Net Core Supported OS Policy covers what operating systems are supported by the different versions of .Net Core. It has links to the underlying operating sysyems supports. E.g. the .Net Core 3.0 are supported by these operating systems.

Links

Download start page for all .net core and .net frameworks

Microsoft Lifecycle Policy FAQ

.NET Core Support Policy