Category: projects

openAPISwaggerUI–part 2 – add ui for all

This is how I made

https://nuget.org/packages/OpenAPISwaggerUI in order to see the UI for an ASP.NET 9 project.

And hey, if you have any feedback, don’t be shy! Drop by https://github.com/ignatandrei/openAPISwaggerUI/ and let me know.

Step one in this epic quest: add all the mystical references (a.k.a. NuGet packages) to the project file.

	<ItemGroup>
		<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
		<PackageReference Include="NetCore2Blockly" Version="9.2024.1206.813" />
		<PackageReference Include="NSwag.AspNetCore" Version="14.2.0" />
		<PackageReference Include="RSCG_NameGenerator" Version="2024.26.8.2002" PrivateAssets="all" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
		<PackageReference Include="Scalar.AspNetCore" Version="1.2.56" />
		<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="7.2.0" />
		<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="7.2.0" />
	</ItemGroup>

Next, I had to figure out how to register these mystical packages to reveal the UI

public static WebApplication UseOpenAPISwaggerUI(this WebApplication app)
{
     
//goto /swagger-Swashbuckle
app.UseSwaggerUI(options =>
{
    options.SwaggerEndpoint("/openapi/v1.json", "v1");
    options.RoutePrefix = "swagger-Swashbuckle";
});
//scalar
//goto swagger-scalar/v1
app.MapScalarApiReference(opt =>
{
    opt.EndpointPathPrefix = "/swagger-scalar/{documentName}";
});
//redoc
//goto /api-docs
app.UseReDoc(options =>
{
    options.SpecUrl("/openapi/v1.json");
    options.RoutePrefix = "swagger-redoc";
});

//goto /nswag-swagger
app.UseSwaggerUi(options =>
{
    options.DocumentPath = "/openapi/v1.json";
    options.Path = "/swagger-nswag";
});
//goto /blocklyautomation
app.UseBlocklyUI(app.Environment);
app.UseBlocklyAutomation();
}

After this it was simple to use the extension in program.cs

app.MapOpenApi();
app.UseOpenAPISwaggerUI();

openAPISwaggerUI–part 1- idea

In .NET 9, Microsoft decided to play a little game of “Hide and Seek” with OpenAPI / Swagger JSON generation, leaving the UI part for us to figure out.

So, how do we choose the best UI for our project without turning it into a guessing game?

Well, I thought, why not create a project that lets you try all the options at once? It’s like a buffet, but for UIs!

You can grab it from https://nuget.org/packages/OpenAPISwaggerUI or install it with `dotnet add package OpenAPISwaggerUI`.

The UI I have found are from

Swashbuckle

NSwag

Redoc

Scalar

Visual Automation ( disclosure  : a project of mine )

And hey, if you have any feedback, don’t be shy! Drop by https://github.com/ignatandrei/openAPISwaggerUI/ and let me know.

[Interface2NullObject]Debugger and Converter–part 5

 

It will be interesting to see the Null Object in the debugger with properties generated from interface -see  https://www.nuget.org/packages/rscg_Interface_to_null_object

 

This is not very difficult to generate once you have the interface with properties

The code for generating

public string DebuggerDisplay()  
{
    StringBuilder sb = new StringBuilder("[System.Diagnostics.DebuggerDisplay(");
    sb.Append("\"");
    foreach (var item in props)
    {
        sb.Append(item.Name);
        sb.Append(" = {");
        sb.Append(item.Name);
        sb.Append("} " );
    }
    sb.Append("\")]");
    return sb.ToString();
}

 

and the code generated is

 

[System.Diagnostics.DebuggerDisplay("FirstName = {FirstName} LastName = {LastName} Department = {Department} ")]
public partial class Employee_null : global::IntegrationConsole.IEmployee

 

More interesting is to have a converter in order to obtain from the JSON string the interface, not the object .

 

The code for generating is

 

public class @(Model.Name + "Converter") : System.Text.Json.Serialization.JsonConverter<@(Model.FullName)>
    {
    public override @Model.FullName Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options)
    {
    // Deserialize the JSON to the concrete type @Model.Name
    return System.Text.Json.JsonSerializer.Deserialize<@nameClass>
        (ref reader, options);
        }

        public override void Write(System.Text.Json.Utf8JsonWriter writer, @Model.FullName value, System.Text.Json.JsonSerializerOptions options)
        {
        // Serialize the concrete type @Model.Name
        System.Text.Json.JsonSerializer.Serialize(writer, (@nameClass)value, options);
        }
        }


and the code generated is

//serialize and deserialize
var empString = JsonSerializer.Serialize(employee);
Console.WriteLine(empString);
//deserialize

var options = new JsonSerializerOptions
{
    PropertyNameCaseInsensitive = true,
    DefaultBufferSize = 128
};
options.Converters.Add(new IDepartmentConverter());
options.Converters.Add(new IEmployeeConverter());

var emp2 = JsonSerializer.Deserialize<IEmployee>(empString,options);
ArgumentNullException.ThrowIfNull(emp2);
Console.WriteLine(emp2.FirstName);
Console.WriteLine(emp2.Department.Name);
Debug.Assert(emp2.FirstName == "Andrei");

[Interface2NullObject]Replace default-part 4

So for https://github.com/ignatandrei/rscg_Interface_to_null_object we put the default return value as the default for the return type. For properties, it is enough easy to modify the data.  But for functions ?  Let’s say we return an array

public IEmployee[] Employees();

The default value is null – so it will be throwing an error when do “foreach ” .

So – I should think about a way to register default values for different methods.
I have the idea to register into the .csproj – so here it is

<ItemGroup>
	<CompilerVisibleProperty Include="I2NO_String" />
	<CompilerVisibleProperty Include="I2NO_IntegrationConsole_IEmployee_Array" />
	<CompilerVisibleProperty Include="I2NO_System_Collections_Generic_IAsyncEnumerable_Of_IntegrationConsole_IEmployee_EndOf" />
</ItemGroup>
<ItemGroup>
  <PackageReference Include="System.Linq.Async" Version="6.0.1" />
</ItemGroup>
<PropertyGroup>
	<I2NO_String>return ""</I2NO_String>
	<I2NO_IntegrationConsole_IEmployee_Array>return []</I2NO_IntegrationConsole_IEmployee_Array>
	<I2NO_System_Collections_Generic_IAsyncEnumerable_Of_IntegrationConsole_IEmployee_EndOf>return AsyncEnumerable.Empty_Of_IntegrationConsole.IEmployee_EndOf();</I2NO_System_Collections_Generic_IAsyncEnumerable_Of_IntegrationConsole_IEmployee_EndOf>
</PropertyGroup>

So this is the result

        public virtual string Name { get; set; } = default(string);
    
        public virtual IntegrationConsole.IEmployee[] Employees() {  return [] ; }
    
        public virtual System.Collections.Generic.IAsyncEnumerable<IntegrationConsole.IEmployee> EmployeesAsync() {  return AsyncEnumerable.Empty<IntegrationConsole.IEmployee>() ; }

[Interface2NullObject]Examples–part 3

Examples for rscg_Interface_to_null_object: Simplifying the Null Object Pattern

Now I can show some examples for rscg_Interface_to_null_object. This project aims to simplify the implementation of the Null Object Pattern in C# by automatically generating null object classes from interfaces.

I will start with those 2 interfaces:


using InterfaceToNullObject;

namespace IntegrationConsole;
[ToNullObject]
public interface IDepartment
{
    public string Name { get; set; }
}

and

using InterfaceToNullObject;

namespace IntegrationConsole;
[ToNullObject]
public interface IEmployee
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public IDepartment Department { get; set; }
    public string GetFullName();

    public string GetFullNameAndDepartment(string separator);
    public bool MoveEmployeeToDepartment(IDepartment department);

}

The generated code is the following

// <auto-generated>
    //     This code was generated by a tool :rscg_Interface_to_null_object
    //     Runtime Version: José Saramago is feeling diplomatic in Bissau
    //     DateOfTool : 2025-01-20 16:28:25
    //     Changes to this file may cause incorrect behavior and will be lost if
    //     the code is regenerated.
    // </auto-generated>
//------------------------------------------------------------------------------
/// <summary>
    /// This static partial class contains extension methods for sorting collections of IDepartment objects.
    /// </summary>

 #nullable enable
 #pragma warning disable CS8603
 #pragma warning disable CS8625
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::System.CodeDom.Compiler.GeneratedCode("GeneratorName","2025.10120.11628.125")]
public partial class Department_null : global::IntegrationConsole.IDepartment
{

        public virtual string Name { get; set; } = default(string);
    
}

#nullable restore
#pragma warning restore CS8603
#pragma warning restore CS8625

And the employee

// <auto-generated>
    //     This code was generated by a tool :rscg_Interface_to_null_object
    //     Runtime Version: José Saramago is feeling diplomatic in Bissau
    //     DateOfTool : 2025-01-20 16:28:25
    //     Changes to this file may cause incorrect behavior and will be lost if
    //     the code is regenerated.
    // </auto-generated>
//------------------------------------------------------------------------------
/// <summary>
    /// This static partial class contains extension methods for sorting collections of IEmployee objects.
    /// </summary>

 #nullable enable
 #pragma warning disable CS8603
 #pragma warning disable CS8625
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::System.CodeDom.Compiler.GeneratedCode("GeneratorName","2025.10120.11628.125")]
public partial class Employee_null : global::IntegrationConsole.IEmployee
{

        public virtual string FirstName { get; set; } = default(string);
    
        public virtual string LastName { get; set; } = default(string);
    
        public virtual IntegrationConsole.IDepartment Department { get; set; } = default(IntegrationConsole.IDepartment);
    
        public virtual string GetFullName() { return default(string); }
    
        public virtual string GetFullNameAndDepartment(string separator) { return default(string); }
    
        public virtual bool MoveEmployeeToDepartment(global::IntegrationConsole.IDepartment department) { return default(bool); }
    
}

#nullable restore
#pragma warning restore CS8603
#pragma warning restore CS8625

So please checkout rscg_Interface_to_null_object.

[Interface2NullObject] Idea- part 1

Every time I kick off a new project, I dive straight into classes/methods that do stuff – because who doesn’t love instant gratification? But now, I’m turning over a new leaf. From now on, I’m starting with interfaces and then gradually building out the classes that make the magic happen.

But here’s the catch – for every interface, I need a class with the same properties and methods, even if it just sits there doing nothing. Think of it like the https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.abstractions.nulllogger?view=net-9.0-pp or the Null Object Pattern.

So, how do I automate this? Enter Roslyn Source Code Generators! Roslyn will hand me the interface, methods, and properties on a silver platter, and I’ll have the class in no time!

Activities 2024 -projects

Compare EFCore providers : http://msprogrammer.serviciipeweb.ro/category/ef/

Inventory of > 70 Roslyn Code Generator at https://github.com/ignatandrei/rscg_examples

Created a free book about how are the Design Patterns used inside the .NET Patterns | Design Patterns used in .NET (C#) 

Created a .NET Tool, https://github.com/ignatandrei/PackageAnalyzer/ , that analyze the Visual Studio Solution

Created a ASP.NET Core Nuget package that lists various settings of the system https://github.com/ignatandrei/NetCoreUsefullEndpoints

Created a .NET Tool to watch simultaneously test and console https://github.com/ignatandrei/watch2/

Presentation about .NET at https://www.meetup.com/Bucharest-A-D-C-E-S-Meetup/

Github Repos

See what .NET software is doing :

https://github.com/ignatandrei/RSCG_WhatIAmDoing

Export diagram from classes

https://github.com/ignatandrei/RSCG_ExportDiagram

Adding Linq by string with a Roslyn Code Generator: https://github.com/ignatandrei/rscg_queryables

Generator of unique names for .NET assemblies : https://github.com/ignatandrei/NameGenerator

Adding IFormattable for each class: https://github.com/ignatandrei/RSCG_IFormattable

query ContributionGraph {
   user(login: "ignatandrei") {
     contributionsCollection(
       from: "2024-01-01T00:00:00+00:00"
       to: "2024-12-31T00:00:00+00:00"
     ) {
       commitContributionsByRepository(maxRepositories:100){
         repository{
           nameWithOwner
           url
           updatedAt
         }
       }    
     }
   }

}

Package analyzer and Feature Matrix

I have started a .NET Tool, https://github.com/ignatandrei/PackageAnalyzer , that can analyze your solution and generate various statistics.

The program can show you dependency matrix of projects

diagram

The X on the diagonal shows that the project is not dependent on himself.

The 1 shows that the project is dependent DIRECTLY on the other project ( read from left to right, in rows )

The 2( or more ) shows that the project is dependent INDIRECTLY on the other project ( read from left to right, in rows )

Watch2–part7–full test

Watch2 NuGet Package

The Watch2 NuGet package extends the functionality of dotnet watch by adding features such as console clearing and delay handling. It outputs the same information as dotnet watch.

1. Wrapper Interfaces

To facilitate testing, it is necessary to wrap various process creation classes from .NET (e.g., Process => ProcessWrapper, ProcessStartInfo => ProcessStartInfoWrapper). This allows for the testing of process outputs. These wrappers are primarily interfaces that can be mocked and redirected to the main class.

Most of these wrappers can be generated automatically using a Roslyn Code Generator.

Rule of Thumb

If the main class you want to wrap contains properties, methods, or events that return or accept instances of another class, you should create a wrapper for that class as well.

Example 1

The Process class has a property StartInfo of type ProcessStartInfo. Therefore, a wrapper for ProcessStartInfo must be created.

Example 2

The Process class has an event OutputDataReceived of type DataReceivedEventArgs. Consequently, a wrapper for DataReceivedEventArgs is required.

2. Rocks and Waiting for an Async Call

When mocking an async method with Rocks, it is essential to wait for the async call to complete.

For example, you must wait for .WaitForExitAsync() to finish before checking the process output.

Watch2–part 6- options

The Watch2 package passes all the command line arguments to the dotnet watch. So how can it have its own settings? Simple: by reading a watch.json file that is in the same folder where it is executing.

1. Testing
I was considering how to perform the testing, and there were two options:

Microsoft File Providers: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/file-providers?view=aspnetcore-8.0

TestableIO: https://github.com/TestableIO/System.IO.Abstractions

I thought that the second option (TestableIO) would be easier to use and more flexible. However, I do not need to test the creation of the file, but rather the content of the file. Therefore, I will use the first option (Microsoft File Providers).

2. Maintain JSON in Sync with C#
I need a solution to transform JSON to C# on the fly (i.e., at compile time). For this, a Roslyn Code Generator can be used. The rscgutils NuGet package will be helpful for this purpose.

Here is the relevant configuration in the project file:

<ItemGroup>
    <PackageReference Include="Microsoft.Extensions.FileProviders.Abstractions" Version="8.0.0" />
    <PackageReference Include="rscgutils" Version="2024.2000.2000" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

<ItemGroup>
    <ProjectReference Include="..\Watch2_Interfaces\Watch2_Interfaces.csproj" />
</ItemGroup>
<ItemGroup>
	<AdditionalFiles Include="options.gen.json" />
</ItemGroup>

Code in Program.cs
The code in Program.cs will utilize the above configurations and packages. Here is an example of how it might look:

This example demonstrates how to read the watch.json file using the Microsoft File Providers package. The rscgutils package will handle the transformation of JSON to C# at compile time, ensuring that the JSON configuration is always in sync with the C# code.

So the code in program.cs will be:

//test
//args = ["run --no-hot-reload"];
//string folder = @"D:\gth\RSCG_Examples\v2\Generator";

//uncomment this line for production
string folder = Environment.CurrentDirectory;

var serviceCollection = new ServiceCollection();
ConfigureServices(serviceCollection, folder);

var serviceProvider = serviceCollection.BuildServiceProvider();

var console = serviceProvider.GetRequiredService<IConsoleWrapper>();
var processManager = serviceProvider.GetRequiredService<ProcessManager>();
var startInfo = serviceProvider.GetRequiredService<IProcessStartInfo>();

var fileOptions = serviceProvider.GetRequiredService<IOptionsReader>();
if(!fileOptions.ExistsFile())
{
    var file = Path.Combine(folder, "watch2.json");
    File.WriteAllText(file, MyAdditionalFiles.options_gen_json);    
}

await processManager.StartProcessAsync(args, console, startInfo);



void ConfigureServices(IServiceCollection services,string folder)
{
    services.AddSingleton<IFileProvider>(new PhysicalFileProvider(folder));
    services.AddSingleton<IOptionsReader, OptionsReader>();
    services.AddSingleton<Ioptions_gen_json>(it =>
    {
        var optionsReader = it.GetRequiredService<IOptionsReader>();
        return optionsReader.GetOptions() ?? options_gen_json.Empty;
    });
    services.AddSingleton<IConsoleWrapper, ConsoleWrapper>();
    services.AddSingleton<ProcessManager, ProcessManager>();
    services.AddSingleton<IProcessStartInfo>(provider => new ProcessStartInfoWrapper
    {
    FileName = "dotnet",
    Arguments = "watch " + string.Join(' ', args),
    WorkingDirectory = folder,
    RedirectStandardOutput = true,
    RedirectStandardInput = true,
    RedirectStandardError = true,
    UseShellExecute = false,
    CreateNoWindow = true
    });

    services.AddLogging(loggingBuilder =>
    {
        loggingBuilder.ClearProviders();
        loggingBuilder.SetMinimumLevel(LogLevel.Trace);
        loggingBuilder.AddNLog("nlog.config");
    });
    services.AddSingleton<ILogger<ProcessManager>, Logger<ProcessManager>>();
    services.AddSingleton<Func<IProcessStartInfo, IProcessWrapper>>(it => new ProcessWrapper(it));
}

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.