Category: .NET

RSCG – polytype

RSCG – polytype
 
 

name polytype
nuget https://www.nuget.org/packages/polytype/
link https://github.com/eiriktsarpalis/PolyType
author Eirik Tsarpalis

Generating shape like reflection from classes. See PolyType.Examples for more details

 

This is how you can use polytype .

The code that you start with is


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

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="PolyType" Version="0.16.1" />
    <PackageReference Include="PolyType.Examples" Version="0.16.1" />
  </ItemGroup>
	<PropertyGroup>
		<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
		<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
	</PropertyGroup>
</Project>


The code that you will use is


using PolyType.Examples.JsonSerializer;
using PolyType.Examples.CborSerializer;
using PolyType.Examples.XmlSerializer;
using ConsoleApp1;
using PolyType.Examples.Cloner;

Person person = new("Pete", 70);
Console.WriteLine(JsonSerializerTS.Serialize(person)); // {"Name":"Pete","Age":70}
Console.WriteLine(XmlSerializer.Serialize(person));    // <value><Name>Pete</Name><Age>70</Age></value>
Console.WriteLine(CborSerializer.EncodeToHex(person)); // A2644E616D656450657465634167651846
person.Childs = [new Person("Andrei", 55)];

person.Childs[0].ID = 1;
var q = Cloner.Clone(person);
person.Childs[0].ID = 10;
Console.WriteLine(q);
Console.WriteLine(person);
Console.WriteLine(q.Childs[0]);
Console.WriteLine(person.Childs[0]);



using PolyType;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp1;
[GenerateShape]
public partial record Person(string name, int age)
{
    public Person[] Childs { get; set; } = [];

    public int ID;
}

 

The code that is generated is

// <auto-generated/>

#nullable enable annotations
#nullable disable warnings

namespace ConsoleApp1
{
    public partial record Person : global::PolyType.IShapeable<global::ConsoleApp1.Person>
    {
        static global::PolyType.Abstractions.ITypeShape<global::ConsoleApp1.Person> global::PolyType.IShapeable<global::ConsoleApp1.Person>.GetShape() 
            => global::PolyType.SourceGenerator.GenerateShapeProvider.Default.Person;
    }
}

// <auto-generated/>

#nullable enable annotations
#nullable disable warnings

namespace PolyType.SourceGenerator
{
    internal partial class GenerateShapeProvider
    {
        private const global::System.Reflection.BindingFlags InstanceBindingFlags = 
            global::System.Reflection.BindingFlags.Public | 
            global::System.Reflection.BindingFlags.NonPublic | 
            global::System.Reflection.BindingFlags.Instance;
        
        /// <summary>Gets the default instance of the <see cref="GenerateShapeProvider"/> class.</summary>
        public static GenerateShapeProvider Default { get; } = new();
        
        /// <summary>Initializes a new instance of the <see cref="GenerateShapeProvider"/> class.</summary>
        public GenerateShapeProvider() { }
    }
}

// <auto-generated/>

#nullable enable annotations
#nullable disable warnings

namespace PolyType.SourceGenerator
{
    internal partial class GenerateShapeProvider
    {
        /// <summary>Gets the generated shape for specified type.</summary>
#nullable disable annotations // Use nullable-oblivious property type
        public global::PolyType.Abstractions.ITypeShape<int> Int32 => _Int32 ??= Create_Int32();
#nullable enable annotations // Use nullable-oblivious property type
        private global::PolyType.Abstractions.ITypeShape<int>? _Int32;

        private global::PolyType.Abstractions.ITypeShape<int> Create_Int32()
        {
            return new global::PolyType.SourceGenModel.SourceGenObjectTypeShape<int>
            {
                Provider = this,
                IsRecordType = false,
                IsTupleType = false,
                CreatePropertiesFunc = null,
                CreateConstructorFunc = null,
            };
        }
    }
}

// <auto-generated/>

#nullable enable annotations
#nullable disable warnings

namespace PolyType.SourceGenerator
{
    internal partial class GenerateShapeProvider : global::PolyType.ITypeShapeProvider
    {
        /// <summary>
        /// Gets the generated <see cref="global::PolyType.Abstractions.ITypeShape{T}" /> for the specified type.
        /// </summary>
        /// <typeparam name="T">The type for which a shape is requested.</typeparam>
        /// <returns>
        /// The generated <see cref="global::PolyType.Abstractions.ITypeShape{T}" /> for the specified type.
        /// </returns>
        public global::PolyType.Abstractions.ITypeShape<T>? GetShape<T>()
            => (global::PolyType.Abstractions.ITypeShape<T>?)GetShape(typeof(T));
        
        /// <summary>
        /// Gets the generated <see cref="global::PolyType.Abstractions.ITypeShape" /> for the specified type.
        /// </summary>
        /// <param name="type">The type for which a shape is requested.</param>
        /// <returns>
        /// The generated <see cref="global::PolyType.Abstractions.ITypeShape" /> for the specified type.
        /// </returns>
        public global::PolyType.Abstractions.ITypeShape? GetShape(global::System.Type type)
        {
            if (type == typeof(global::ConsoleApp1.Person[]))
                return Person_Array;
            
            if (type == typeof(string))
                return String;
            
            if (type == typeof(global::ConsoleApp1.Person))
                return Person;
            
            if (type == typeof(int))
                return Int32;
            
            return null;
        }
    }
}

// <auto-generated/>

#nullable enable annotations
#nullable disable warnings

namespace PolyType.SourceGenerator
{
    internal partial class GenerateShapeProvider
    {
        /// <summary>Gets the generated shape for specified type.</summary>
#nullable disable annotations // Use nullable-oblivious property type
        public global::PolyType.Abstractions.ITypeShape<global::ConsoleApp1.Person> Person => _Person ??= Create_Person();
#nullable enable annotations // Use nullable-oblivious property type
        private global::PolyType.Abstractions.ITypeShape<global::ConsoleApp1.Person>? _Person;

        private global::PolyType.Abstractions.ITypeShape<global::ConsoleApp1.Person> Create_Person()
        {
            return new global::PolyType.SourceGenModel.SourceGenObjectTypeShape<global::ConsoleApp1.Person>
            {
                Provider = this,
                IsRecordType = true,
                IsTupleType = false,
                CreatePropertiesFunc = CreateProperties_Person,
                CreateConstructorFunc = CreateConstructor_Person,
            };
        }

        private global::PolyType.Abstractions.IPropertyShape[] CreateProperties_Person() => new global::PolyType.Abstractions.IPropertyShape[]
        {
            new global::PolyType.SourceGenModel.SourceGenPropertyShape<global::ConsoleApp1.Person, string>
            {
                Name = "name",
                DeclaringType = (global::PolyType.Abstractions.IObjectTypeShape<global::ConsoleApp1.Person>)Person,
                PropertyType = String,
                Getter = static (ref global::ConsoleApp1.Person obj) => obj.name,
                Setter = null,
                AttributeProviderFunc = static () => typeof(global::ConsoleApp1.Person).GetProperty("name", InstanceBindingFlags, null, typeof(string), [], null),
                IsField = false,
                IsGetterPublic = true,
                IsSetterPublic = false,
                IsGetterNonNullable = true,
                IsSetterNonNullable = false,
            },

            new global::PolyType.SourceGenModel.SourceGenPropertyShape<global::ConsoleApp1.Person, int>
            {
                Name = "age",
                DeclaringType = (global::PolyType.Abstractions.IObjectTypeShape<global::ConsoleApp1.Person>)Person,
                PropertyType = Int32,
                Getter = static (ref global::ConsoleApp1.Person obj) => obj.age,
                Setter = null,
                AttributeProviderFunc = static () => typeof(global::ConsoleApp1.Person).GetProperty("age", InstanceBindingFlags, null, typeof(int), [], null),
                IsField = false,
                IsGetterPublic = true,
                IsSetterPublic = false,
                IsGetterNonNullable = true,
                IsSetterNonNullable = false,
            },

            new global::PolyType.SourceGenModel.SourceGenPropertyShape<global::ConsoleApp1.Person, global::ConsoleApp1.Person[]>
            {
                Name = "Childs",
                DeclaringType = (global::PolyType.Abstractions.IObjectTypeShape<global::ConsoleApp1.Person>)Person,
                PropertyType = Person_Array,
                Getter = static (ref global::ConsoleApp1.Person obj) => obj.Childs,
                Setter = static (ref global::ConsoleApp1.Person obj, global::ConsoleApp1.Person[] value) => obj.Childs = value,
                AttributeProviderFunc = static () => typeof(global::ConsoleApp1.Person).GetProperty("Childs", InstanceBindingFlags, null, typeof(global::ConsoleApp1.Person[]), [], null),
                IsField = false,
                IsGetterPublic = true,
                IsSetterPublic = true,
                IsGetterNonNullable = true,
                IsSetterNonNullable = true,
            },

            new global::PolyType.SourceGenModel.SourceGenPropertyShape<global::ConsoleApp1.Person, int>
            {
                Name = "ID",
                DeclaringType = (global::PolyType.Abstractions.IObjectTypeShape<global::ConsoleApp1.Person>)Person,
                PropertyType = Int32,
                Getter = static (ref global::ConsoleApp1.Person obj) => obj.ID,
                Setter = static (ref global::ConsoleApp1.Person obj, int value) => obj.ID = value,
                AttributeProviderFunc = static () => typeof(global::ConsoleApp1.Person).GetField("ID", InstanceBindingFlags),
                IsField = true,
                IsGetterPublic = true,
                IsSetterPublic = true,
                IsGetterNonNullable = true,
                IsSetterNonNullable = true,
            },
        };

        private global::PolyType.Abstractions.IConstructorShape CreateConstructor_Person()
        {
            return new global::PolyType.SourceGenModel.SourceGenConstructorShape<global::ConsoleApp1.Person, (string, int, global::ConsoleApp1.Person[], int, byte Flags)>
            {
                DeclaringType = (global::PolyType.Abstractions.IObjectTypeShape<global::ConsoleApp1.Person>)Person,
                ParameterCount = 4,
                GetParametersFunc = CreateConstructorParameters_Person,
                DefaultConstructorFunc = null,
                ArgumentStateConstructorFunc = static () => default((string, int, global::ConsoleApp1.Person[], int, byte Flags)),
                ParameterizedConstructorFunc = static (ref (string, int, global::ConsoleApp1.Person[], int, byte Flags) state) => { var obj = new global::ConsoleApp1.Person(state.Item1, state.Item2);  if ((state.Flags & 1) != 0) obj.Childs = state.Item3; if ((state.Flags & 2) != 0) obj.ID = state.Item4; return obj; },
                AttributeProviderFunc = static () => typeof(global::ConsoleApp1.Person).GetConstructor(InstanceBindingFlags, new[] { typeof(string), typeof(int) }),
                IsPublic = true,
            };
        }

        private global::PolyType.Abstractions.IConstructorParameterShape[] CreateConstructorParameters_Person() => new global::PolyType.Abstractions.IConstructorParameterShape[]
        {
            new global::PolyType.SourceGenModel.SourceGenConstructorParameterShape<(string, int, global::ConsoleApp1.Person[], int, byte Flags), string>
            {
                Position = 0,
                Name = "name",
                ParameterType = String,
                Kind = global::PolyType.Abstractions.ConstructorParameterKind.ConstructorParameter,
                IsRequired = true,
                IsNonNullable = true,
                IsPublic = true,
                HasDefaultValue = false,
                DefaultValue = default!,
                Setter = static (ref (string, int, global::ConsoleApp1.Person[], int, byte Flags) state, string value) => state.Item1 = value,
                AttributeProviderFunc = static () => typeof(global::ConsoleApp1.Person).GetConstructor(InstanceBindingFlags, new[] { typeof(string), typeof(int) })?.GetParameters()[0],
            },

            new global::PolyType.SourceGenModel.SourceGenConstructorParameterShape<(string, int, global::ConsoleApp1.Person[], int, byte Flags), int>
            {
                Position = 1,
                Name = "age",
                ParameterType = Int32,
                Kind = global::PolyType.Abstractions.ConstructorParameterKind.ConstructorParameter,
                IsRequired = true,
                IsNonNullable = true,
                IsPublic = true,
                HasDefaultValue = false,
                DefaultValue = default,
                Setter = static (ref (string, int, global::ConsoleApp1.Person[], int, byte Flags) state, int value) => state.Item2 = value,
                AttributeProviderFunc = static () => typeof(global::ConsoleApp1.Person).GetConstructor(InstanceBindingFlags, new[] { typeof(string), typeof(int) })?.GetParameters()[1],
            },

            new global::PolyType.SourceGenModel.SourceGenConstructorParameterShape<(string, int, global::ConsoleApp1.Person[], int, byte Flags), global::ConsoleApp1.Person[]>
            {
                Position = 2,
                Name = "Childs",
                ParameterType = Person_Array,
                Kind = global::PolyType.Abstractions.ConstructorParameterKind.PropertyInitializer,
                IsRequired = false,
                IsNonNullable = true,
                IsPublic = true,
                HasDefaultValue = false,
                DefaultValue = default!,
                Setter = static (ref (string, int, global::ConsoleApp1.Person[], int, byte Flags) state, global::ConsoleApp1.Person[] value) => { state.Item3 = value; state.Flags |= 1; },
                AttributeProviderFunc = static () => typeof(global::ConsoleApp1.Person).GetProperty("Childs", InstanceBindingFlags, null, typeof(global::ConsoleApp1.Person[]), [], null),
            },

            new global::PolyType.SourceGenModel.SourceGenConstructorParameterShape<(string, int, global::ConsoleApp1.Person[], int, byte Flags), int>
            {
                Position = 3,
                Name = "ID",
                ParameterType = Int32,
                Kind = global::PolyType.Abstractions.ConstructorParameterKind.FieldInitializer,
                IsRequired = false,
                IsNonNullable = true,
                IsPublic = true,
                HasDefaultValue = false,
                DefaultValue = default,
                Setter = static (ref (string, int, global::ConsoleApp1.Person[], int, byte Flags) state, int value) => { state.Item4 = value; state.Flags |= 2; },
                AttributeProviderFunc = static () => typeof(global::ConsoleApp1.Person).GetField("ID", InstanceBindingFlags),
            },
        };

        [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_name")]
        private static extern void Person_name_SetAccessor(global::ConsoleApp1.Person obj, string value);

        [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_age")]
        private static extern void Person_age_SetAccessor(global::ConsoleApp1.Person obj, int value);

        [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Field, Name = "ID")]
        private static extern ref int Person_ID_Accessor(global::ConsoleApp1.Person obj);
    }
}

// <auto-generated/>

#nullable enable annotations
#nullable disable warnings

namespace PolyType.SourceGenerator
{
    internal partial class GenerateShapeProvider
    {
        /// <summary>Gets the generated shape for specified type.</summary>
#nullable disable annotations // Use nullable-oblivious property type
        public global::PolyType.Abstractions.ITypeShape<global::ConsoleApp1.Person[]> Person_Array => _Person_Array ??= Create_Person_Array();
#nullable enable annotations // Use nullable-oblivious property type
        private global::PolyType.Abstractions.ITypeShape<global::ConsoleApp1.Person[]>? _Person_Array;

        private global::PolyType.Abstractions.ITypeShape<global::ConsoleApp1.Person[]> Create_Person_Array()
        {
            return new global::PolyType.SourceGenModel.SourceGenEnumerableTypeShape<global::ConsoleApp1.Person[], global::ConsoleApp1.Person>
            {
                ElementType = Person,
                ConstructionStrategy = global::PolyType.Abstractions.CollectionConstructionStrategy.Span,
                DefaultConstructorFunc = null,
                EnumerableConstructorFunc = null,
                SpanConstructorFunc = static values => values.ToArray(),
                GetEnumerableFunc = static obj => obj,
                AddElementFunc = null,
                Rank = 1,
                Provider = this,
           };
        }
    }
}

// <auto-generated/>

#nullable enable annotations
#nullable disable warnings

namespace PolyType.SourceGenerator
{
    internal partial class GenerateShapeProvider
    {
        /// <summary>Gets the generated shape for specified type.</summary>
#nullable disable annotations // Use nullable-oblivious property type
        public global::PolyType.Abstractions.ITypeShape<string> String => _String ??= Create_String();
#nullable enable annotations // Use nullable-oblivious property type
        private global::PolyType.Abstractions.ITypeShape<string>? _String;

        private global::PolyType.Abstractions.ITypeShape<string> Create_String()
        {
            return new global::PolyType.SourceGenModel.SourceGenObjectTypeShape<string>
            {
                Provider = this,
                IsRecordType = false,
                IsTupleType = false,
                CreatePropertiesFunc = null,
                CreateConstructorFunc = null,
            };
        }
    }
}

Code and pdf at

https://ignatandrei.github.io/RSCG_Examples/v2/docs/polytype

RSCG – Datacute.EmbeddedResourcePropertyGenerator

RSCG – Datacute.EmbeddedResourcePropertyGenerator
 
 

name Datacute.EmbeddedResourcePropertyGenerator
nuget https://www.nuget.org/packages/Datacute.EmbeddedResourcePropertyGenerator/
link https://github.com/datacute/EmbeddedResourcePropertyGenerator/
author Stephen Denne

Generating class to access easy the embedded resource

 

This is how you can use Datacute.EmbeddedResourcePropertyGenerator .

The code that you start with is


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

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  
  <ItemGroup>
    <EmbeddedResource Include="TestData\Countries.txt" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Datacute.EmbeddedResourcePropertyGenerator" Version="1.0.0" >
    </PackageReference>
  </ItemGroup>
	<PropertyGroup>
		<AdditionalFileItemNames>$(AdditionalFileItemNames);EmbeddedResource</AdditionalFileItemNames>
	</PropertyGroup>
	<PropertyGroup>
		<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
		<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
	</PropertyGroup>
</Project>


The code that you will use is


// See https://aka.ms/new-console-template for more information
using EmbedDemo;

Console.WriteLine("Hello, World!");

var data= TestData.Countries;

Console.WriteLine(data);


using Datacute.EmbeddedResourcePropertyGenerator;
namespace EmbedDemo;
[EmbeddedResourceProperties(".txt", "TestData")]
public static partial class TestData;



USA
Germany
France
Romania
Italy


 

The code that is generated is

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by the Datacute.EmbeddedResourcePropertyGenerator.
// </auto-generated>
//------------------------------------------------------------------------------

#nullable enable

namespace EmbedDemo;
/// <summary>
/// This class's properties are generated from project files meeting the criteria:
/// <list type="bullet">
/// <item>
/// <description>they are both an <c>EmbeddedResource</c> and an <c>AdditionalFile</c></description>
/// </item>
/// <item>
/// <description>they are in the project folder <c>TestData</c></description>
/// </item>
/// <item>
/// <description>they have the extension <c>.txt</c></description>
/// </item>
/// </list>
/// </summary>
public static partial class TestData
{
    private static class EmbeddedResource
    {
        public static string Read(string resourceName)
        {
            var assembly = typeof(TestData).Assembly;
            using var stream = assembly.GetManifestResourceStream(resourceName)!;
            using var streamReader = new global::System.IO.StreamReader(stream, global::System.Text.Encoding.UTF8);
            var resourceText = streamReader.ReadToEnd();
            return resourceText;
        }
        public static class BackingField
        {
            public static string? Countries;
        }
        public static class ResourceName
        {
            public const string Countries = "EmbedDemo.TestData.Countries.txt";
        }
    }
    static partial void ReadEmbeddedResourceValue(ref string? backingField, string resourceName, string propertyName);
    static partial void AlterEmbeddedResourceReturnValue(ref string value, string resourceName, string propertyName);

    /// <summary>Text value of the Embedded Resource: Countries.txt</summary>
    /// <value>
    /// <code>
    /// USA
    /// Germany
    /// France
    /// Romania
    /// Italy
    /// 
    /// </code>
    /// </value>
    /// <remarks>
    /// The value is read from the embedded resource on first access.
    /// </remarks>
    public static string Countries
    {
        get
        {
            ReadEmbeddedResourceValue(ref EmbeddedResource.BackingField.Countries, EmbeddedResource.ResourceName.Countries, "Countries");
            var value = EmbeddedResource.BackingField.Countries ??= EmbeddedResource.Read(EmbeddedResource.ResourceName.Countries);
            AlterEmbeddedResourceReturnValue(ref value, EmbeddedResource.ResourceName.Countries, "Countries");
            return value;
        }
    }
}

Code and pdf at

https://ignatandrei.github.io/RSCG_Examples/v2/docs/Datacute.EmbeddedResourcePropertyGenerator

RSCG – rscg_queryables

RSCG – rscg_queryables
 
 

name rscg_queryables
nuget https://www.nuget.org/packages/rscg_queryables/
https://www.nuget.org/packages/rscg_queryablesCommon/
link https://github.com/ignatandrei/rscg_queryables
author Andrei Ignat

Generating code for .Where and .OrderBy by string, not by lambda

 

This is how you can use rscg_queryables .

The code that you start with is


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

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
	<ItemGroup>
		<PackageReference  Include="rscg_queryablesCommon" Version="2024.1110.1815" />
		<PackageReference Include="rscg_queryables" Version="2024.1110.1815"  OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
	</ItemGroup>
	<PropertyGroup>
		<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
		<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
	</PropertyGroup>
</Project>


The code that you will use is


using SortAndWhere;


Console.WriteLine("Hello, World!");

var students = new Student[]
{
    new Student { FirstName = "John", LastName = "Doe", StartYear = 1935},
    new Student { FirstName = "Ignat", LastName = "Andrei", StartYear = 1989 },
};

var orderedExplicitly = students.OrderBy(p => p.FirstName).ToArray();
var orderedImplicitly = students.OrderBy("firStnaMe").ToArray();
var orderedImplicitly2 = students.AsQueryable().OrderBy("fIrsTnAme").ToArray();


//Search by property name

var search = students.AsQueryable().Where("firstName", rscg_queryablesCommon.WhereOperator.Equal, "John").ToArray();

Console.WriteLine("found : " + search.Length);

search = students.AsQueryable().Where(Student_.Where_Expr("firstName", rscg_queryablesCommon.WhereOperator.Equal, "John")).ToArray();
Console.WriteLine("found : " + search.Length);





using rscg_queryablesCommon;

namespace SortAndWhere;
[MakeSortable]
[MakeWhere]
public class Student
{
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;

    public int StartYear { get; set; }

}


 

The code that is generated is


public static partial class ExtensionsSortable_SortAndWhere_Student {
    
    #region Enumerable
    public static System.Linq.IOrderedEnumerable<global::SortAndWhere.Student> OrderBy
    (
    this IEnumerable<global::SortAndWhere.Student>
        source, string propertyName
    )
    {

        return OrderByAscDesc(source,propertyName,true);
    }
        public static System.Linq.IOrderedEnumerable<global::SortAndWhere.Student>
            OrderByDescending
            (
            this IEnumerable<global::SortAndWhere.Student>
                source, string propertyName
                )
                {

                return OrderByAscDesc(source,propertyName,false);
                }
    public static System.Linq.IOrderedEnumerable<global::SortAndWhere.Student> OrderByAscDesc
    (
     this IEnumerable<global::SortAndWhere.Student> source, string propertyName, bool Ascending
    )
   {

                        if(string.Equals(propertyName, "FirstName", StringComparison.OrdinalIgnoreCase)){
                            if(Ascending)
                                return source.OrderBy(x => x.FirstName);
                            else
                                return source.OrderByDescending(x => x.FirstName);                            
                        }
                    
                        if(string.Equals(propertyName, "LastName", StringComparison.OrdinalIgnoreCase)){
                            if(Ascending)
                                return source.OrderBy(x => x.LastName);
                            else
                                return source.OrderByDescending(x => x.LastName);                            
                        }
                    
                        if(string.Equals(propertyName, "StartYear", StringComparison.OrdinalIgnoreCase)){
                            if(Ascending)
                                return source.OrderBy(x => x.StartYear);
                            else
                                return source.OrderByDescending(x => x.StartYear);                            
                        }
                                    throw new ArgumentException($"Property {propertyName} not found", propertyName);
    }

    public static System.Linq.IOrderedEnumerable<global::SortAndWhere.Student> ThenByAscDesc
    (
     this IOrderedEnumerable<global::SortAndWhere.Student> source, string propertyName, bool Ascending
    )
   {

                        if(string.Equals(propertyName, "FirstName", StringComparison.OrdinalIgnoreCase)){
                            if(Ascending)
                                return source.ThenBy(x => x.FirstName);
                            else
                                return source.ThenByDescending(x => x.FirstName);                            
                        }
                    
                        if(string.Equals(propertyName, "LastName", StringComparison.OrdinalIgnoreCase)){
                            if(Ascending)
                                return source.ThenBy(x => x.LastName);
                            else
                                return source.ThenByDescending(x => x.LastName);                            
                        }
                    
                        if(string.Equals(propertyName, "StartYear", StringComparison.OrdinalIgnoreCase)){
                            if(Ascending)
                                return source.ThenBy(x => x.StartYear);
                            else
                                return source.ThenByDescending(x => x.StartYear);                            
                        }
                                    throw new ArgumentException($"Property {propertyName} not found", propertyName);
    }
    public static System.Linq.IOrderedEnumerable<global::SortAndWhere.Student> ThenBy
    (
    this IOrderedEnumerable<global::SortAndWhere.Student>
        source, string propertyName
    )
    {

        return ThenByAscDesc(source,propertyName,true);
    }
    public static System.Linq.IOrderedEnumerable<global::SortAndWhere.Student> ThenByDescending
    (
    this IOrderedEnumerable<global::SortAndWhere.Student>
        source, string propertyName
    )
    {

        return ThenByAscDesc(source,propertyName,false);
    }

    #endregion 

#region Queryable
    public static System.Linq.IOrderedQueryable<global::SortAndWhere.Student> OrderBy
    (
    this IQueryable<global::SortAndWhere.Student>
        source, string propertyName
    )
    {

        return OrderByAscDesc(source,propertyName,true);
    }
        public static System.Linq.IOrderedQueryable<global::SortAndWhere.Student>
            OrderByDescending
            (
            this IQueryable<global::SortAndWhere.Student>
                source, string propertyName
                )
                {

                return OrderByAscDesc(source,propertyName,false);
                }
    public static System.Linq.IOrderedQueryable<global::SortAndWhere.Student> OrderByAscDesc
    (
     this IQueryable<global::SortAndWhere.Student> source, string propertyName, bool Ascending
    )
   {

                        if(string.Equals(propertyName, "FirstName", StringComparison.OrdinalIgnoreCase)){
                            if(Ascending)
                                return source.OrderBy(x => x.FirstName);
                            else
                                return source.OrderByDescending(x => x.FirstName);                            
                        }
                    
                        if(string.Equals(propertyName, "LastName", StringComparison.OrdinalIgnoreCase)){
                            if(Ascending)
                                return source.OrderBy(x => x.LastName);
                            else
                                return source.OrderByDescending(x => x.LastName);                            
                        }
                    
                        if(string.Equals(propertyName, "StartYear", StringComparison.OrdinalIgnoreCase)){
                            if(Ascending)
                                return source.OrderBy(x => x.StartYear);
                            else
                                return source.OrderByDescending(x => x.StartYear);                            
                        }
                                    throw new ArgumentException($"Property {propertyName} not found", propertyName);
    }
    public static System.Linq.IOrderedQueryable<global::SortAndWhere.Student> ThenByAscDesc
    (
     this IOrderedQueryable<global::SortAndWhere.Student> source, string propertyName, bool Ascending
    )
   {

                        if(string.Equals(propertyName, "FirstName", StringComparison.OrdinalIgnoreCase)){
                            if(Ascending)
                                return source.ThenBy(x => x.FirstName);
                            else
                                return source.ThenByDescending(x => x.FirstName);                            
                        }
                    
                        if(string.Equals(propertyName, "LastName", StringComparison.OrdinalIgnoreCase)){
                            if(Ascending)
                                return source.ThenBy(x => x.LastName);
                            else
                                return source.ThenByDescending(x => x.LastName);                            
                        }
                    
                        if(string.Equals(propertyName, "StartYear", StringComparison.OrdinalIgnoreCase)){
                            if(Ascending)
                                return source.ThenBy(x => x.StartYear);
                            else
                                return source.ThenByDescending(x => x.StartYear);                            
                        }
                                    throw new ArgumentException($"Property {propertyName} not found", propertyName);
    }
    public static System.Linq.IOrderedQueryable<global::SortAndWhere.Student> ThenBy
    (
    this IOrderedQueryable<global::SortAndWhere.Student>
        source, string propertyName
    )
    {

        return ThenByAscDesc(source,propertyName,true);
    }
    public static System.Linq.IOrderedQueryable<global::SortAndWhere.Student> ThenByDescending
    (
    this IOrderedQueryable<global::SortAndWhere.Student>
        source, string propertyName
    )
    {

        return ThenByAscDesc(source,propertyName,false);
    }

    #endregion 

}

public static class ExtensionsWhere_SortAndWhere_Student{
    

        internal static System.Linq.Expressions.Expression < Func < global::SortAndWhere.Student,bool>>
        FirstName_Expr_Equal(string argument) => (a => a.FirstName== argument);

        internal static Func<global::SortAndWhere.Student,bool>
            FirstName_Equal(string argument) => (a => a.FirstName== argument);

        internal static System.Linq.Expressions.Expression < Func < global::SortAndWhere.Student,bool>>
        FirstName_Expr_NotEqual(string argument) => (a => a.FirstName != argument);

        internal static Func<global::SortAndWhere.Student,bool>
                FirstName_NotEqual(string argument) => (a => a.FirstName != argument);

    
        internal static System.Linq.Expressions.Expression < Func < global::SortAndWhere.Student,bool>>
        LastName_Expr_Equal(string argument) => (a => a.LastName== argument);

        internal static Func<global::SortAndWhere.Student,bool>
            LastName_Equal(string argument) => (a => a.LastName== argument);

        internal static System.Linq.Expressions.Expression < Func < global::SortAndWhere.Student,bool>>
        LastName_Expr_NotEqual(string argument) => (a => a.LastName != argument);

        internal static Func<global::SortAndWhere.Student,bool>
                LastName_NotEqual(string argument) => (a => a.LastName != argument);

    
        internal static System.Linq.Expressions.Expression < Func < global::SortAndWhere.Student,bool>>
        StartYear_Expr_Equal(int argument) => (a => a.StartYear== argument);

        internal static Func<global::SortAndWhere.Student,bool>
            StartYear_Equal(int argument) => (a => a.StartYear== argument);

        internal static System.Linq.Expressions.Expression < Func < global::SortAndWhere.Student,bool>>
        StartYear_Expr_NotEqual(int argument) => (a => a.StartYear != argument);

        internal static Func<global::SortAndWhere.Student,bool>
                StartYear_NotEqual(int argument) => (a => a.StartYear != argument);

    
   

}
public static class Student_{


        public static System.Linq.IQueryable<global::SortAndWhere.Student>
            Where(this System.Linq.IQueryable<global::SortAndWhere.Student> original,
                string propertyName,rscg_queryablesCommon.WhereOperator operatorWhere, string argument)
        {
            return original.Where(Where_Expr(propertyName, operatorWhere, argument));
        }
        public static System.Linq.Expressions.Expression < Func < global::SortAndWhere.Student,bool>>
        Where_Expr(string propertyName,rscg_queryablesCommon.WhereOperator operatorWhere, string argument)
            {

                    if(string.Equals(propertyName,  "FirstName", StringComparison.OrdinalIgnoreCase)){
                    switch(operatorWhere){
                    case rscg_queryablesCommon.WhereOperator.Equal:
                    return ExtensionsWhere_SortAndWhere_Student.FirstName_Expr_Equal( argument);
                    case rscg_queryablesCommon.WhereOperator.NotEqual:
                    return ExtensionsWhere_SortAndWhere_Student.FirstName_Expr_NotEqual( argument);
                    default:
                    throw new ArgumentException($"Operator {operatorWhere} not found");
                    }//end switch
                    }//end if FirstName

                
                    if(string.Equals(propertyName,  "LastName", StringComparison.OrdinalIgnoreCase)){
                    switch(operatorWhere){
                    case rscg_queryablesCommon.WhereOperator.Equal:
                    return ExtensionsWhere_SortAndWhere_Student.LastName_Expr_Equal( argument);
                    case rscg_queryablesCommon.WhereOperator.NotEqual:
                    return ExtensionsWhere_SortAndWhere_Student.LastName_Expr_NotEqual( argument);
                    default:
                    throw new ArgumentException($"Operator {operatorWhere} not found");
                    }//end switch
                    }//end if LastName

                
            throw new ArgumentException("Property "+ propertyName +" not found for string type");
            }





        public static Func<global::SortAndWhere.Student,bool>
            Where(string propertyName,rscg_queryablesCommon.WhereOperator operatorWhere, string argument)
            {

                    if(string.Equals(propertyName,  "FirstName", StringComparison.OrdinalIgnoreCase)){
                    switch(operatorWhere){
                    case rscg_queryablesCommon.WhereOperator.Equal:
                    return ExtensionsWhere_SortAndWhere_Student.FirstName_Equal( argument);
                    case rscg_queryablesCommon.WhereOperator.NotEqual:
                    return ExtensionsWhere_SortAndWhere_Student.FirstName_NotEqual( argument);
                    default:
                    throw new ArgumentException($"Operator {operatorWhere} not found");
                    }//end switch
                    }//end if FirstName

                
                    if(string.Equals(propertyName,  "LastName", StringComparison.OrdinalIgnoreCase)){
                    switch(operatorWhere){
                    case rscg_queryablesCommon.WhereOperator.Equal:
                    return ExtensionsWhere_SortAndWhere_Student.LastName_Equal( argument);
                    case rscg_queryablesCommon.WhereOperator.NotEqual:
                    return ExtensionsWhere_SortAndWhere_Student.LastName_NotEqual( argument);
                    default:
                    throw new ArgumentException($"Operator {operatorWhere} not found");
                    }//end switch
                    }//end if LastName

                
            throw new ArgumentException("Property "+ propertyName +" not found for string type");
            }

    
        public static System.Linq.IQueryable<global::SortAndWhere.Student>
            Where(this System.Linq.IQueryable<global::SortAndWhere.Student> original,
                string propertyName,rscg_queryablesCommon.WhereOperator operatorWhere, int argument)
        {
            return original.Where(Where_Expr(propertyName, operatorWhere, argument));
        }
        public static System.Linq.Expressions.Expression < Func < global::SortAndWhere.Student,bool>>
        Where_Expr(string propertyName,rscg_queryablesCommon.WhereOperator operatorWhere, int argument)
            {

                    if(string.Equals(propertyName,  "StartYear", StringComparison.OrdinalIgnoreCase)){
                    switch(operatorWhere){
                    case rscg_queryablesCommon.WhereOperator.Equal:
                    return ExtensionsWhere_SortAndWhere_Student.StartYear_Expr_Equal( argument);
                    case rscg_queryablesCommon.WhereOperator.NotEqual:
                    return ExtensionsWhere_SortAndWhere_Student.StartYear_Expr_NotEqual( argument);
                    default:
                    throw new ArgumentException($"Operator {operatorWhere} not found");
                    }//end switch
                    }//end if StartYear

                
            throw new ArgumentException("Property "+ propertyName +" not found for int type");
            }





        public static Func<global::SortAndWhere.Student,bool>
            Where(string propertyName,rscg_queryablesCommon.WhereOperator operatorWhere, int argument)
            {

                    if(string.Equals(propertyName,  "StartYear", StringComparison.OrdinalIgnoreCase)){
                    switch(operatorWhere){
                    case rscg_queryablesCommon.WhereOperator.Equal:
                    return ExtensionsWhere_SortAndWhere_Student.StartYear_Equal( argument);
                    case rscg_queryablesCommon.WhereOperator.NotEqual:
                    return ExtensionsWhere_SortAndWhere_Student.StartYear_NotEqual( argument);
                    default:
                    throw new ArgumentException($"Operator {operatorWhere} not found");
                    }//end switch
                    }//end if StartYear

                
            throw new ArgumentException("Property "+ propertyName +" not found for int type");
            }

    

}

Code and pdf at

https://ignatandrei.github.io/RSCG_Examples/v2/docs/rscg_queryables

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));
}

Watch2- part 5–DI

The Watch2 NuGet package project now has interfaces, making it test-friendly and ready for action.

To make life easier, we’ve sprinkled some Dependency Injection (DI) magic throughout the project.

Classes have been refactored to embrace constructor injection, ensuring all necessary interfaces are on board.

The DI container has been set up in `Program.cs`, making dependency resolution a breeze. The tests have also been given a makeover to fit the new DI style.

This is a never-ending journey, but hey, it’s a pretty good start!

The program.cs now looks like this:



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

var serviceProvider = serviceCollection.BuildServiceProvider();

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

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

void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IConsoleWrapper, ConsoleWrapper>();
services.AddSingleton<ProcessManager, ProcessManager>();
services.AddSingleton<IProcessStartInfo>(provider => new ProcessStartInfoWrapper
{
FileName = "dotnet",
Arguments = "watch " + string.Join(' ', args),
WorkingDirectory = Environment.CurrentDirectory,
RedirectStandardOutput = true,
RedirectStandardInput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
});

    services.AddLogging(loggingBuilder =>
    {
        loggingBuilder.ClearProviders();
        loggingBuilder.SetMinimumLevel(LogLevel.Trace);
        loggingBuilder.AddNLog("nlog.config");
    });
}

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 .

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.