Category: projects

Watch2–part 4–CI and CD

Now I want to do build and auto-deployment as a Nuget Package( at https://www.nuget.org/packages/watch2 )

The most simple build action on GitHub Actions is simple example of how to do it.

name: .NET

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
      with:
        fetch-depth: 0

    - name: Setup .NET 8
      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: 8.0.x
    
    - name: build
      run: |
        cd src 
        cd Watch2
        dotnet restore
        dotnet build 
        
        

However, I want to do more than that. I want to build , test and deploy on demand the Nuget package to Nuget.org.

However , the building should be done first on local – and then with GitHub Actions – the deployment should be done.

For this I do like https://github.com/xt0rted/dotnet-run-script

This is how I use to do test and pack, by using a global.json file

{
  "scripts": {
    "build": "dotnet build --configuration Release",
    "test": "dotnet test --configuration Release",
    "ci": "dotnet r build && dotnet r test",
    "pack":"dotnet r ci && cd Watch2 && dotnet pack -o ../../nugetPackages"
  }
}

So the github action now looks like this

name: compile and deploy with tag v*

on:
  push:
    branches: [ "main" ]
    tags: [ 'v*' ] # Listen for tag pushes that match version tags
  pull_request:
    branches: [ "main" ]

jobs:
  build:
    # runs-on: windows-latest
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    
    - name: Setup .NET
      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: 8.0.x

    - name: Restore dependencies
      run: |
        dotnet tool install --global PowerShell
        cd src 
        cd Watch2
        dotnet restore
        
    - name: Build
      run: |
        cd src
        cd Watch2
        dotnet tool restore
        dotnet r pack

    - name: 'Upload nuget'
      uses: actions/upload-artifact@v4
      with:
        name: Nuget_${{github.run_number}}
        path: src/nugetPackages
        retention-days: 1
  
    - name: push to nuget
      if: startsWith(github.ref, 'refs/tags/v') # This line ensures the step runs only if a tag version is present
      run: |
        dir src/nugetPackages/*.*
        echo '1'
        dir src/nugetPackages/*.symbols.nupkg  
        echo '2'
        cd src
        cd nugetPackages
        dotnet nuget push "*.symbols.nupkg" --api-key  ${{ secrets.NUGET_KEY  }} --source https://api.nuget.org/v3/index.json
    

More, we should add to github Settings => Secrets and variables => Actions secrets a new secret with name NUGET_KEY

The build and test are made automatically . More, if I want to deploy also, then
1. I modify the version in the csproj file

	<PropertyGroup>
		<Version>8.2024.1102.950</Version>
	</PropertyGroup>

2. I push the changes to the repository with a tag ( e.g. v8.2024.1102.950)

So this achieves the CI/CD basic .

Watch2- part 3 -refactoring to interfaces and test

The Watch2 just intercepts the dotnet watch, adds a clear console, and a delay to give a breather between running the app and the tests.

So I need to have tests – if I want to be sure what the software does and to modify it without summoning the coding gremlins. More importantly, the output from dotnet watch could be different – so I need more tests .

The solution evolved from

to

The main points learned from the process of refactoring:

1. GitHub Copilot is like having a coding sidekick. Some modifications I made manually, but most of the code was generated by my new best friend.

2. For each class that doesn’t have an interface (like ProcessStartInfo, Process, Console), a Wrapper interface must be created. Because who doesn’t love wrapping things up?

3. It is very hard to make interfaces dependent just on interfaces – not on implementations.

4. Static variables could impact the test !

5. The Rocks nuget package really rocks for generating mocks

[assembly: Rock(typeof(IProcessWrapper), BuildType.Create)]
[assembly: Rock(typeof(IConsoleWrapper), BuildType.Create)]
[assembly: Rock(typeof(IDataReceivedEventArgs), BuildType.Make)]
[TestMethod]
public void TestRestart()
{
    // Arrange
    var mockProcess = new IProcessWrapperCreateExpectations();
    var mockConsole = new IConsoleWrapperCreateExpectations();
    var mockDataReceivedEventArgs = new IDataReceivedEventArgsMakeExpectations();
    mockProcess.Methods.Kill().ExpectedCallCount ( 1);
    mockConsole.Methods.WriteLine(Rocks.Arg.Any<string>()).Callback(it=> { });
    

    // Act
    (new ProcessManager()).HandleOutput(("Waiting for a file to change before"), mockConsole.Instance());

    // Assert
    mockProcess.Verify();
}

Watch2- part 2- first fast implementation

The first attempt to writing the code was very fast – and, surely, not testable. (Speedy Gonzales would be proud!)

Some observations:

1. If you start any process inside a console application, you should intercept the cancel signal and stop the process.
This is done by using the CancelKeyPress event of the Console class.
This is important because the user can cancel the process by pressing Ctrl+C.
(Because who doesn’t love a good Ctrl+C in the morning?)

Console.CancelKeyPress += delegate {
    // call methods to clean up
    Kill(proc);
};

2. If you want to stop the dotnet watch after it detects a change in the file system, you cannot. You need to kill the process and start it again. So, you need to kill the process and start it again.
(It’s like a phoenix, it must die to be reborn!)

3. Spectre.Console is a really good library to create console applications with a lot of features. I used it to create messages with different colors and styles.
(Because plain text is so last century.)

4. The code is very simple and easy to understand. I used the Process class to start the dotnet watch process and the Console class to write messages to the console.
(Simple and easy, just like making a cup of coffee… if you have a coffee machine.)

This is the final code:

using Spectre.Console;
using System.Diagnostics;
Process? proc = null;
Console.CancelKeyPress += delegate {
    // call methods to clean up
    Kill(proc);
};
bool shouldWait = false;
while (true)
{
    ProcessStartInfo startInfo = new ProcessStartInfo();
    startInfo.FileName = "dotnet";
    startInfo.Arguments = "watch " + string.Join(' ', args);
    //startInfo.LoadUserProfile = true;
    startInfo.WorkingDirectory = Environment.CurrentDirectory;

    startInfo.RedirectStandardOutput = true;
    startInfo.RedirectStandardInput = true;
    startInfo.RedirectStandardError = true;
    startInfo.UseShellExecute = false;
    startInfo.CreateNoWindow = true;

    proc = new Process();
    proc.StartInfo = startInfo;
    proc.OutputDataReceived += (sender, e) =>
    {
        if (shouldWait)
        {
            shouldWait = false;
            Kill(proc);
            Thread.Sleep(15_000);
            return;

        }
        var line = e.Data;
        if (line == null) return;
        if (line.Contains("dotnet watch"))
        {
            if (line.Contains("Started"))
            {
                Console.Clear();
            }
            if (line.Contains("Building"))
            {

            }
        }

        if (line.Contains(": error "))
        {
            AnsiConsole.MarkupLineInterpolated($"->[bold red]:cross_mark: {e.Data}[/]");
            return;
        }
        if (line.Contains("Waiting for a file to change before"))
        {
            Console.WriteLine("wain");
            shouldWait = true;

        }

        Console.WriteLine("->" + e.Data);


    };
    proc.ErrorDataReceived += (sender, e) =>
    {
        if (e.Data != null)
        {
            AnsiConsole.MarkupLineInterpolated($"->[bold red]:cross_mark: {e.Data}[/]");
            return;

        }
    };

    AnsiConsole.MarkupLineInterpolated($"[bold green]Starting...[/]");
    proc.Start();
    Console.Clear();
    proc.BeginOutputReadLine();
    proc.BeginErrorReadLine();
    await proc.WaitForExitAsync();
}

static void Kill(Process? proc)
{
    if (proc == null) return;
    if (proc.HasExited) return;
    proc.Kill(true);
    proc.Close();
    proc = null;
}

Watch2–part 1–idea

I frequently use .NET Watch for two main reasons:

  1. Running .NET applications during development.

  2. Executing tests.

Seriously, if you haven’t tried .NET Watch, you’re missing out. It’s like having a personal assistant who never sleeps compiling code . For more info, check out Microsoft’s documentation  at https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-watch

But, like all good things, it comes with a couple of quirks:

  1. The console output scrolls faster than a cat on a hot tin roof, making it hard to keep up. A Console.Clear (or cls command) would be a lifesaver to reset the console after each file change.

  2. Running dotnet watch for both the application and tests at the same time is like trying to juggle flaming torches—compilation conflicts galore! You’ll often find yourself stopping one of the watches to keep things from going up in flames.

To tackle these issues, I whipped up a simple .NET tool, named Watch2.  This little gem can clear the console and run the watch command with an optional delay. It’s built with .NET Core and can be installed as a global tool.

For more laughs and details, visit the NuGet package page  at https://www.nuget.org/packages/watch2 .

NetCoreUsefullEndpoints-part 13–adding runtime information

In the Nuget NetCoreUsefullEndpoints I have added information about the runtime information :

You can access by going to localhost:5027/api/usefull/runtimeinformationAll and the end result is

{
“frameworkDescription”: “.NET 8.0.8”,
“osDescription”: “Microsoft Windows 10.0.22631”,
“processArchitecture”: “X64”,
“osArchitecture”: “X64”
}

The code that returns this is

route.MapGet("api/usefull/runtimeinformation/", (HttpContext httpContext) =>
        {
            return TypedResults.Ok(new Helper().FromStaticRuntimeInformation());
        })

( I have used a Roslyn Code Generator , https://ignatandrei.github.io/RSCG_Examples/v2/docs/RSCG_Static#example–source-csproj-source-files- , that generates a class from a static class )

NetCoreUsefullEndpoints-part 12–adding url adresses

In the Nuget NetCoreUsefullEndpoints I have added information about the current process :

You can access by going to localhost:5027/api/usefull/adresses and the end result is

[
     “http://localhost:5027″
]

The code that returns this is

route.MapGet("api/usefull/adresses", (HttpContext httpContext, [FromServices] IServer server) =>
        {
            var adresses = server.Features.Get<IServerAddressesFeature>();
            var ret= adresses?.Addresses?.ToArray()??[] ;
            return TypedResults.Ok(ret);
        });

NetPackageAnalyzer–part 13–executable lines

The .NET Tool , https://www.nuget.org/packages/netpackageanalyzerconsole , can now analyze a solution and see the number of executable lines

The program is showing the number of executable lines per method , class , assembly .

diagram

https://learn.microsoft.com/en-us/visualstudio/code-quality/code-metrics-values?view=vs-2022

Install from https://nuget.org/packages/netpackageanalyzerconsole

NetPackageAnalyzer–part 12-CyclomaticComplexity

The .NET Tool , https://www.nuget.org/packages/netpackageanalyzerconsole , can now analyze a solution and see the cyclomatic complexity.

Cyclomatic Complexity for assembly, class, method

The cyclomatic complexity of a section of code is the number of linearly independent paths through the code. It is a quantitative measure of the number of linearly independent paths through a program’s source code.

The program is showing the cyclomatic complexity of the assembly, class, and method. You should start with the method – it is the easy to analyze .

diagram

Also , you can see on which methods you should focus to refactor – the ones with the highest cyclomatic complexity.

diagram

https://learn.microsoft.com/en-us/visualstudio/code-quality/code-metrics-cyclomatic-complexity?view=vs-2022

Install from https://nuget.org/packages/netpackageanalyzerconsole

NetPackageAnalyzer–part 11–Building Blocks , Test Projects and Root Projects

The .NET Tool , https://www.nuget.org/packages/netpackageanalyzerconsole , can now analyze a solution and see the Building Blocks , Test Projects and Root Projects

Building blocks Projects

I define as Building blocks projects the projects that have no reference to other projects.

If you are new to the solution , then you can start to those base projects – should be self contained and self explanatory

diagram

Root Projects

I define as root projects the projects that are not referenced by any other project and that are not test projects

Can be console, winforms, windows api, mobile , services … Important is that they contain the functionality for the business

diagram

Test Projects

The application shows test projects and the projects that are tested.

diagram

Install from https://nuget.org/packages/netpackageanalyzerconsole

Andrei Ignat weekly software news(mostly .NET)

* indicates required

Please select all the ways you would like to hear from me:

You can unsubscribe at any time by clicking the link in the footer of our emails. For information about our privacy practices, please visit our website.

We use Mailchimp as our marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp's privacy practices here.