Category: NetCoreUsefullEndpoints

NetCoreUsefullEndpoints-part 10- adding LongRunningTasks

In the Nuget NetCoreUsefullEndpoints I have already an  endpoint to shutdown  –  see http://msprogrammer.serviciipeweb.ro/2023/02/20/netcoreusefullendpoints-part-9-adding-endpoints-for-shutdown/ .

However, this means that there are no ( or some … )  long running operations that are running … How to identify those ?

For the starting point , I was thinking at something simple, like an IDisposable that knows when the Long Running operation is finished

[HttpGet(Name = “GetWeatherForecast”)]
         public async Task<WeatherForecast[]> Get()
         {
             using var lr = UsefullExtensions.UsefullExtensions.AddLRTS(“weather”+DateTime.UtcNow.Ticks);
             await Task.Delay(5000);
             return Enumerable.Range(1, 5).Select(index => new WeatherForecast
             {
                 Date = DateTime.Now.AddDays(index),
                 TemperatureC = Random.Shared.Next(-20, 55),
                 Summary = Summaries[Random.Shared.Next(Summaries.Length)]
             })
             .ToArray();
         }

The implementation is very simple for adding:

internal static Dictionary<string, LongRunningTask> lrts = new();
public static LongRunningTask AddLRTS(string id, string? name = null)
{
     if(lrts.ContainsKey(id))
     {
         lrts[id].Dispose();
     }
     lrts.Add(id,new LongRunningTask(id, name ?? id));
     return lrts[id];
}

And for removing itself:

public record LongRunningTask(string id, string? name = “”) : IDisposable
{
     public void Dispose()
     {
         UsefullExtensions.lrts.Remove(id);
         GC.SuppressFinalize(this);
     }

}

Also, an endpoint will be registered for the user know what are the operations

var rh = route.MapGet(“api/usefull/LongRunningTasks/”,
             (HttpContext httpContext) =>
             {
                 var data=lrts.Select(it=>new { it.Key, it.Value.name }).ToArray();
                 return data;
             });

        var rhCount = route.MapGet(“api/usefull/LongRunningTasks/Count”,
             (HttpContext httpContext) =>
             {
                 return lrts.LongCount();
             });

And that will be to register and found what are the long running tasks in ASP.NET Core

NetCoreUsefullEndpoints-part 9- adding endpoints for shutdown

In my NuGet NetCoreUsefullEndpoints package  I want to can shutdown the application . Could be hard way, like calling Environment.Exit or the easy way , like calling a cancellation token. Also, what should happen with all the requests that are coming ? ( And I do not want to mention long running tasks – I do not found a useful registration / un-registration for those)

So I come with the following implementation for forced exit

//IEndpointRouteBuilder

var rhForced = route.MapPost(“api/usefull/shutdownForced/{id:int}”,
     (int id) =>
     {
         Environment.Exit(id);
     });

For the shutdown that is requested , I come with 3 modifications:

1.In Program.cs , I put a Middleware for not serving the HTTP

builder.Services.AddSingleton<MiddlewareShutdown>();

app.UseMiddleware<MiddlewareShutdown>();

await app.RunAsync(UsefullExtensions.UsefullExtensions.cts.Token);

2.  Register the routes in order to use the cancellation token

//IEndpointRouteBuilder

var rhSec = route.MapPost(“api/usefull/shutdownAfter/{seconds}”,
             (HttpContext httpContext, int seconds) =>
             {
                 RequestedShutdownAt = DateTime.UtcNow;
                 var h = cts.Token.GetHashCode();
                 cts?.CancelAfter(Math.Abs(seconds)*1000);
                 return h;

            });

3. Use a middleware to return 418 Status Code if the application is shutting down

public class MiddlewareShutdown : IMiddleware
{
     public async Task InvokeAsync(HttpContext context, RequestDelegate next)
     {
         if (UsefullExtensions.RequestedShutdownAt != null)
         {
             context.Response.StatusCode = 418;
             await context.Response.WriteAsync(“Service is stopping at ” + UsefullExtensions.RequestedShutdownAt!.Value.ToString(“s”));
             return;
         }
         await next(context);
         return;
     }
}

And with those you can shutdown gracefully any ASP.NET Core application ( without long running tasks!)

NetCoreUsefullEndpoints-part 8- adding start date

In my NuGet NetCoreUsefullEndpoints package  I have had already registered the actual date as

var rh = route.MapGet(“api/usefull/dateUTC”, (HttpContext httpContext) =>
            {
                return Results.Ok(DateTime.UtcNow);
            });

Now I want to register also the start date – the date where the application has been started.

1. How to do this  ?

2. What will be the route ?

For 1 can be a static constructor or, better , the singleton in C# :

private static DateTime startDateUTC = DateTime.UtcNow;

For 2 it is more complicated

My solution ( just break compatibility, since I have not a v1 and v2) was the following

var rh = route.MapGet(“api/usefull/date/startUTC”, (HttpContext httpContext) =>
{
     return Results.Ok(startDateUTC);
});

var rhUTC = route.MapGet(“api/usefull/date/nowUTC/”,
     (HttpContext httpContext) =>
     {
         return TypedResults.Ok(DateTime.UtcNow);
     });

rhUTC.AddDefault(corsPolicy, authorization);

So now I have same routing api/usefull/date

NetCoreUsefullEndpoints–part 7–restart application

In order to do the shutdown, I have added the following extension

public static CancellationTokenSource cts=new ();

public static void MapShutdown(this IEndpointRouteBuilder route, string? corsPolicy = null, string[]? authorization = null)
        {
            ArgumentNullException.ThrowIfNull(route);
            var rh = route.MapPost(“api/usefull/shutdown/”,
                (HttpContext httpContext) =>
                {
                    var h= cts.Token.GetHashCode();
                    cts?.Cancel();                   
                    return h;
                   
                });

           rh.AddDefault(corsPolicy, authorization);

        }

This code defines a static field called “cts” in the “UsefullExtensions” class. “cts” is an instance of the “CancellationTokenSource” class, which is used to create a CancellationToken that can be used to stop the application gracefully.

The “MapShutdown” method is an extension method for IEndpointRouteBuilder that creates a new endpoint for a POST request to the “api/usefull/shutdown/” URL. When the request is received, the method cancels the “cts” CancellationTokenSource if it is not null, and returns the hash code of the CancellationToken. The method also sets the “corsPolicy” and “authorization” parameters for the endpoint.

You can call with

app.MapUsefullAll();

or with

app.MapShutdown

Anyway , the runAsync should be modified with

await app.RunAsync(UsefullExtensions.UsefullExtensions.cts.Token);

The “RunAsync” method of the “app” object is used to start the application and listen for incoming requests. The method takes a CancellationToken as an argument, which allows the application to be stopped gracefully by cancelling the token. In this case, the token is provided by the “cts” field of the “UsefullExtensions” class. The code uses the “await” keyword to wait for the task returned by “RunAsync” to complete before continuing.

NetCoreUsefullEndpoints–part 6–passing to .NET 7

So  .NET 7 has appeared and I decided to pass NetCoreUsefullEndpoints to .NET 7 .

Also, for RateLimiter , I have considered that is good to know if the request is local or remote … so I decided to add connection ( remote IP, local IP, and more details) to the nuget package.

So I have created for .NET 6 this :

So , first things first : modify the version from  6.2022.1203.1551 to  7.2022.1203.1551 ( my versioning scheme is .NETCore version compliant of the package.year.MMdd.HHmm  – pretty stable and easy to decide what package you should add)

Then I want to profit to show in swagger the return type with TypedResults – so , as an example , I have modified from

route.MapGet(“api/usefull/httpContext/Connection”, (HttpContext httpContext) =>
{
var con = httpContext.Connection;
if (con == null)
{
     return Results.NoContent();
}
var conSerialize = new
{
     LocalIpAddress = con.LocalIpAddress?.ToString(),
     RemoteIpAddress = con.RemoteIpAddress?.ToString(),
     con.RemotePort,
     con.LocalPort,
     con.ClientCertificate,
     con.Id
};
return Results.Ok(conSerialize);

})

to

route.MapGet(“api/usefull/httpContext/Connection”,

Results<NoContent, Ok<object>>
(HttpContext httpContext) =>
{
var con = httpContext.Connection;
if (con == null)
{
     return TypedResults.NoContent();
}
var conSerialize = new
{
     LocalIpAddress = con.LocalIpAddress?.ToString(),
     RemoteIpAddress = con.RemoteIpAddress?.ToString(),
     con.RemotePort,
     con.LocalPort,
     con.ClientCertificate,
     con.Id
};
return TypedResults.Ok((object)conSerialize);
})

As you see , a pretty easy modification – indicating the INesteHttpResult Results<NoContent, Ok<object>>  ( so the swagger understand the 2 different return types )  and returning TypedResults instead of Results

Also the github ci must add the .NET Core and Azure App Service should be going to .NET 7 STS

[Nuget] dotnet-run-script

I found this awesome package – https://github.com/xt0rted/dotnet-run-script . It is good to make macros in global.json, then execute in a CICD scenario.

For example, NetCoreUsefullEndpoints used this in yaml ( pretty standard )

# – name: Restore dependencies

#   run: |

#     cd src

#     cd UsefullEndpoints

#     dotnet tool restore

#     dotnet pwsh readme.ps1

#     dotnet restore

# – name: Build

#   run: |

#     cd src

#     cd UsefullEndpoints

#     dotnet build –no-restore

# – name: Pack

#   run: |

#     cd src

#     cd UsefullEndpoints

#     cd UsefullExtensions

#     dotnet pack -o ../nugetPackages  –include-symbols –include-source

Now I have a global.json

{

“scripts”: {

“make_readme”:”dotnet pwsh readme.ps1″,

“prebuild”:”dotnet restore”,

“build”: “dotnet build –no-restore”,

“test”: “dotnet test –configuration Release”,

“prepack”:”dotnet r build”,

“pack”: “cd UsefullExtensions &&  dotnet pack -o ../nugetPackages  –include-symbols –include-source”

}

}

and the yaml is

– name: Restore dependencies

run: |

cd src

cd UsefullEndpoints

dotnet tool restore

dotnet r make_readme

dotnet r pack

Pretty easy  -and can be reproduced on local if you want, not just in CICD actions on source control…

NetCoreUsefullEndpoints-5 Endpoints summary

So for now the package https://www.nuget.org/packages/NetCoreUsefullEndpoints/ has the following endpoints:

GET=>/api/usefull/user/authorization

GET=>/api/usefull/user/noAuthorization

GET=>/api/usefull/environment

GET=>/api/usefull/errorWithILogger

GET=>/api/usefull/errorPure

GET=>/api/usefull/date

GET=>/api/usefull/endpoints/graph ( this calls https://github.com/dotnet/aspnetcore/blob/8bf447aa3f9719f6f162598708020dd4b420b49d/src/Http/Routing/src/Internal/DfaGraphWriter.cs#L22 )

GET=>/api/usefull/endpoints/text

GET=>/api/usefull/configuration ( this calls https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.configuration.configurationrootextensions.getdebugview?view=dotnet-plat-ext-6.0 )

You can see in practice at https://netcoreusefullendpoints.azurewebsites.net/swagger/

NetCoreUsefullEndpoints-4 Nuget package

or NugetPackages there is a simple .NET Pack CLI command  and many configurations

However, it is not so simple . See https://docs.microsoft.com/en-us/nuget/create-packages/package-authoring-best-practices .

In practice, I have a readme.md file ( for showing on nuget.org ) and a readme.txt file( for the programmer to show help after installing the package) THis is what I have in the csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

	<ItemGroup>
		<FrameworkReference Include="Microsoft.AspNetCore.App" />
		 <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />   
		<PackageReference Include="RSCG_Static" Version="2021.12.18.2037" PrivateAssets="all" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
		<None Include="../../../README.md" Pack="true" PackagePath="\" />
		<None Include="../../../docs/nuget.png" Pack="true" PackagePath="\" />    
		<None Include="../readme.txt" Pack="true" PackagePath="\" />
	</ItemGroup>
	<PropertyGroup>
		<Version>6.2022.722.712</Version>
		<Authors>Andrei Ignat</Authors>
		
		<Description>Some usefull extensions: error, environment, user</Description>
		<Title>NetCoreUsefullEndpoints</Title>
		<PackageId>NetCoreUsefullEndpoints</PackageId>
		<PackageTags>C#;.NET;ASP.NET Core;</PackageTags>
		<PackageReadmeFile>README.md</PackageReadmeFile>
		<RepositoryUrl>https://github.com/ignatandrei/NetCoreUsefullEndpoints</RepositoryUrl>
		<PackageProjectUrl>https://github.com/ignatandrei/NetCoreUsefullEndpoints</PackageProjectUrl>
		<RepositoryType>GIT</RepositoryType>
		<Copyright>MIT</Copyright>
		<PackageLicenseExpression>MIT</PackageLicenseExpression>
		<IncludeSymbols>true</IncludeSymbols>
		<PublishRepositoryUrl>true</PublishRepositoryUrl>
		<EmbedUntrackedSources>true</EmbedUntrackedSources>
		<Deterministic>true</Deterministic>
		<DebugType>embedded</DebugType>

	</PropertyGroup>
	<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
		<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
	</PropertyGroup>
	<PropertyGroup>
	  <TreatWarningsAsErrors>False</TreatWarningsAsErrors>
	  <PackageIcon>nuget.png</PackageIcon>
	</PropertyGroup>

</Project>

Also, the versioning will be a mixture of patron major version + calendar version FOr example there are 2 versions of https://www.nuget.org/packages/NetCoreUsefullEndpoints : 6.2022.722.712 – means for ASP.NET Core 6, on year 2022, month 7, day 22, hour 7, min 12 6.2022.721.1154 – means for ASP.NET Core 6, on year 2022, month 7, day 21, hour 11, min 54

You can see

  1. as Swagger at https://netcoreusefullendpoints.azurewebsites.net/swagger

  2. As BlocklyAutomation at https://netcoreusefullendpoints.azurewebsites.net/BlocklyAutomation

  3. As package at https://www.nuget.org/packages/NetCoreUsefullEndpoints

NetCoreUsefullEndpoints-3 CICD

Now I find usefull that, every time that I push some code, the code is build and the nuget package is attached . So GitHub Actions to the rescue.

There will be 2 GitHub actions – one for build and package , other for deploying to the Azure a test application This is the code for building. ( The code for azure is automatically generated by Azure … so no need to put here)

name: .NET

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

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    - name: Setup .NET
      uses: actions/setup-dotnet@v2
      with:
        dotnet-version: 6.0.x
    - name: Restore dependencies
      run: |
        cd src 
        cd UsefullEndpoints
        dotnet tool restore
        dotnet pwsh readme.ps1        
        dotnet restore
    - name: Build
      run: |
        cd src
        cd UsefullEndpoints
        
        dotnet build --no-restore
    - name: Pack
      run: |
        cd src
        cd UsefullEndpoints
        cd UsefullExtensions
        dotnet pack -o ../nugetPackages  --include-symbols --include-source

    - name: 'Upload nuget'
      #if: ${{ github.ref == 'refs/heads/main' }}
      uses: actions/upload-artifact@v2
      with:
        name: UsefullEndpoints_${{github.run_number}}
        path: src/UsefullEndpoints/nugetPackages
        retention-days: 1

You can see

  1. as Swagger at https://netcoreusefullendpoints.azurewebsites.net/swagger

  2. As BlocklyAutomation at https://netcoreusefullendpoints.azurewebsites.net/BlocklyAutomation

  3. As package at https://www.nuget.org/packages/NetCoreUsefullEndpoints

NetCoreUsefullEndpoints-2–MVP

So let’s start the implementation for user / error / environment.

The only difficulty resides in the fact that IEndpointRouteBuilder is not defined into a usual library, but the dll csproj must contain

	<ItemGroup>
		<FrameworkReference Include="Microsoft.AspNetCore.App" />
    </ItemGroup>

Otherwise , it is work as usual. For example, for seeing a user

 public static void MapUser(this IEndpointRouteBuilder route)
{
    ArgumentNullException.ThrowIfNull(route);
    route.MapGet("api/usefull/user/", (HttpContext httpContext) =>
    {
        return Results.Ok(httpContext.User?.Identity?.Name);
    }).RequiresAuthorization();
}

More difficult was for Environment – that is a Static class. Hopefully, there is a NUGET for this – https://www.nuget.org/packages/rscg_static – that allows to generate an interface from a static class. So I have added this

	<ItemGroup>
		<FrameworkReference Include="Microsoft.AspNetCore.App" />
    </ItemGroup>

And code for generating the interface

public partial class Helper
{
    public partial ISystem_Environment FromStaticEnvironment();

}

So, when I want to get the values from Environment, I use this

public static void MapUsefullEnvironment(this IEndpointRouteBuilder route, string? corsPolicy = null, string[]? authorization = null)
{
    ArgumentNullException.ThrowIfNull(route);

    var rh=route.MapGet("api/usefull/environment/", (HttpContext httpContext) =>
    {
        return Results.Ok(new Helper().FromStaticEnvironment());
    });
    rh.AddDefault(corsPolicy, authorization);

}

You can see

  1. as Swagger at https://netcoreusefullendpoints.azurewebsites.net/swagger

  2. As BlocklyAutomation at https://netcoreusefullendpoints.azurewebsites.net/BlocklyAutomation

  3. As package at https://www.nuget.org/packages/NetCoreUsefullEndpoints

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.