Category: .NET

RSCG – CopyCat

RSCG – CopyCat
 
 

name CopyCat
nuget https://www.nuget.org/packages/Copycat/
link https://github.com/Otaman/Copycat/
author Serhii Buta

Implementation of the Decorator pattern in C# – only for not implemented methods

 

This is how you can use CopyCat .

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="Copycat" Version="0.2.0-beta.1" OutputItemType="Analyzer"   />
  </ItemGroup>
	<PropertyGroup>
		<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
		<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
	</PropertyGroup>
</Project>


The code that you will use is


using CCDemo;

ICoffee c =new Coffee();
c= new CoffeeWithLogging(c);
await c.Prepare();



namespace CCDemo;

internal interface ICoffee
{
    //for the moment does not work for properties in interface
    //string? Name { get; set; }
    Task<bool> Prepare();

    string[] GetIngredients();
}


namespace CCDemo;
internal class Coffee : ICoffee
{
    public string? Name { get; set; }
    public async Task<bool> Prepare()
    {
        Console.WriteLine("start prepare coffee");
        await Task.Delay(1000);
        Console.WriteLine("finish prepare coffee");
        return true;
    }
    public string[] GetIngredients() => new[] { "water", "coffee" };

}



using Copycat;

namespace CCDemo;
[Decorate]
internal partial class CoffeeWithLogging: ICoffee
{
    [Template]
    private string[] AddLogging(Func<string[]> action)
    {
        try
        {
            Console.WriteLine($"start logging {nameof(action)}  ");
            return action();
        }
        catch (Exception e)
        {
            Console.WriteLine($"exception  {nameof(action)} ");
            throw;
        }
        finally
        {
               Console.WriteLine($"end logging {nameof(action)} ");
        }
    }


    [Template]
    public async Task<bool> AddLogging(Func<Task<bool>> action)       
    {
        try
        {
            Console.WriteLine($"start logging {nameof(action)} ");
            return await action();
        }
        catch (Exception e)
        {
            Console.WriteLine($"exception  {nameof(action)} ");
            throw;
        }
        finally
        {
            Console.WriteLine($"end logging {nameof(action)} ");
        }
    }
}


 

The code that is generated is

// <auto-generated/>
using Copycat;

namespace CCDemo;
internal partial class CoffeeWithLogging
{
    private CCDemo.ICoffee _decorated;
    public CoffeeWithLogging(CCDemo.ICoffee decorated)
    {
        _decorated = decorated;
    }

    /// <see cref = "CoffeeWithLogging.AddLogging(Func{Task{bool}})"/>
    public async //for the moment does not work for properties in interface
    //string? Name { get; set; }
    Task<bool> Prepare()
    {
        try
        {
            Console.WriteLine($"start logging {nameof(Prepare)} ");
            return await _decorated.Prepare();
        }
        catch (Exception e)
        {
            Console.WriteLine($"exception  {nameof(Prepare)} ");
            throw;
        }
        finally
        {
            Console.WriteLine($"end logging {nameof(Prepare)} ");
        }
    }

    /// <see cref = "CoffeeWithLogging.AddLogging(Func{string[]})"/>
    public string[] GetIngredients()
    {
        try
        {
            Console.WriteLine($"start logging {nameof(GetIngredients)}  ");
            return _decorated.GetIngredients();
        }
        catch (Exception e)
        {
            Console.WriteLine($"exception  {nameof(GetIngredients)} ");
            throw;
        }
        finally
        {
            Console.WriteLine($"end logging {nameof(GetIngredients)} ");
        }
    }
}

Code and pdf at

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

RSCG – AspectGenerator

RSCG – AspectGenerator
 
 

name AspectGenerator
nuget https://www.nuget.org/packages/AspectGenerator/
link https://github.com/igor-tkachev/AspectGenerator
author Igor Tkachev

AOP for methods in the same project. Uses interceptors

 

This is how you can use AspectGenerator .

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="AspectGenerator" Version="0.0.9-preview" OutputItemType="Analyzer"  />
  </ItemGroup>
<PropertyGroup>
    
    <InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);AspectGenerator</InterceptorsPreviewNamespaces>

    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
    <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
</Project>


The code that you will use is


using AG;

var p=new Person { FirstName="Ignat", LastName="Andrei" };
var x= p.FullName();
Console.WriteLine(x);   


namespace AG;

internal class Person
{
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
    [Metrics]
    public string FullName()
    {
        return $"{FirstName} {LastName}";
    }
}


using System.Diagnostics;
using AspectGenerator;
namespace AG;

[Aspect(
       // Specify the name of the method used in the 'using' statement
       // that returns an IDisposable object.
       OnUsing = nameof(OnUsing)
       )]
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
sealed class MetricsAttribute : Attribute
{
    //static readonly ActivitySource _activitySource = new("Sample.Aspect");

    public static Activity? OnUsing(InterceptInfo info)
    {
        Console.WriteLine($"Entering {info.MemberInfo.Name}");
        return null;
        //var data=_activitySource.StartActivity(info.MemberInfo.Name);
        //return data;
    }
}

 

The code that is generated is

// <auto-generated/>
#pragma warning disable
#nullable enable

using System;

#if AG_GENERATE_API || !AG_NOT_GENERATE_API

namespace AspectGenerator
{
	/// <summary>
	/// <para>Defines an aspect.</para>
	/// <para>Create a new attribute decorated with this attribute to define an aspect.</para>
	/// </summary>
	[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
#if AG_PUBLIC_API
	public
#endif
	sealed class AspectAttribute : Attribute
	{
		public string?   OnInit            { get; set; }
		public string?   OnUsing           { get; set; }
		public string?   OnUsingAsync      { get; set; }
		public string?   OnBeforeCall      { get; set; }
		public string?   OnBeforeCallAsync { get; set; }
		public string?   OnCall            { get; set; }
		public string?   OnAfterCall       { get; set; }
		public string?   OnAfterCallAsync  { get; set; }
		public string?   OnCatch           { get; set; }
		public string?   OnCatchAsync      { get; set; }
		public string?   OnFinally         { get; set; }
		public string?   OnFinallyAsync    { get; set; }
		public string[]? InterceptMethods  { get; set; }
		public bool      UseInterceptType  { get; set; }
		public bool      PassArguments     { get; set; }
		public bool      UseInterceptData  { get; set; }
	}

#if AG_PUBLIC_API
	public
#endif
	enum InterceptType
	{
		OnInit,
		OnUsing,
		OnBeforeCall,
		OnAfterCall,
		OnCatch,
		OnFinally
	}

#if AG_PUBLIC_API
	public
#endif
	enum InterceptResult
	{
		Continue,
		Return,
		ReThrow     = Continue,
		IgnoreThrow = Return
	}

#if AG_PUBLIC_API
	public
#endif
	struct Void
	{
	}

#if AG_PUBLIC_API
	public
#endif
	partial class InterceptInfo
	{
		public object?         Tag;
		public InterceptType   InterceptType;
		public InterceptResult InterceptResult;
		public Exception?      Exception;

		public InterceptInfo?                                        PreviousInfo;
		public System.Reflection.MemberInfo                          MemberInfo;
		public object?[]?                                            MethodArguments;
		public Type                                                  AspectType;
		public System.Collections.Generic.Dictionary<string,object?> AspectArguments;
	}

#if AG_PUBLIC_API
	public
#endif
	partial class InterceptInfo<T> : InterceptInfo
	{
		public T ReturnValue;
	}

#if AG_PUBLIC_API
	public
#endif
	partial struct InterceptData<T>
	{
		public object?         Tag;
		public InterceptType   InterceptType;
		public InterceptResult InterceptResult;
		public Exception?      Exception;

		public InterceptInfo<T>?                                     PreviousInfo;
		public System.Reflection.MemberInfo                          MemberInfo;
		public object?[]?                                            MethodArguments;
		public Type                                                  AspectType;
		public System.Collections.Generic.Dictionary<string,object?> AspectArguments;

		public T ReturnValue;
	}
}

#endif

#if AG_GENERATE_InterceptsLocationAttribute || !AG_NOT_GENERATE_InterceptsLocationAttribute

namespace System.Runtime.CompilerServices
{
	[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
	sealed class InterceptsLocationAttribute(string filePath, int line, int character) : Attribute
	{
	}
}

#endif

// <auto-generated/>
#pragma warning disable
#nullable enable

using System;

using SR  = System.Reflection;
using SLE = System.Linq.Expressions;
using SCG = System.Collections.Generic;

namespace AspectGenerator
{
	using AspectGenerator = AspectGenerator;

	static partial class Interceptors
	{
		static SR.MethodInfo GetMethodInfo(SLE.Expression expr)
		{
			return expr switch
			{
				SLE.MethodCallExpression mc => mc.Method,
				_                           => throw new InvalidOperationException()
			};
		}

		static SR.MethodInfo MethodOf<T>(SLE.Expression<Func<T>> func) => GetMethodInfo(func.Body);
		static SR.MethodInfo MethodOf   (SLE.Expression<Action>  func) => GetMethodInfo(func.Body);

		static SR. MemberInfo                 FullName_Interceptor_MemberInfo        = MethodOf(() => default(AG.Person).FullName());
		static SCG.Dictionary<string,object?> FullName_Interceptor_AspectArguments_0 = new()
		{
		};
		//
		/// <summary>
		/// Intercepts AG.Person.FullName().
		/// </summary>
		//
		// Intercepts p.FullName().
		[System.Runtime.CompilerServices.InterceptsLocation(@"D:\gth\RSCG_Examples\v2\rscg_examples\AspectGenerator\src\AG\Program.cs", line: 4, character: 10)]
		//
		[System.Runtime.CompilerServices.CompilerGenerated]
		//[System.Diagnostics.DebuggerStepThrough]
		public static string FullName_Interceptor(this AG.Person __this__)
		{
			// AG.MetricsAttribute
			//
			var __info__0 = new AspectGenerator.InterceptInfo<string>
			{
				MemberInfo      = FullName_Interceptor_MemberInfo,
				AspectType      = typeof(AG.MetricsAttribute),
				AspectArguments = FullName_Interceptor_AspectArguments_0,
			};

			using (AG.MetricsAttribute.OnUsing(__info__0))
			{
				__info__0.ReturnValue = __this__.FullName();
			}

			return __info__0.ReturnValue;
		}
	}
}

Code and pdf at

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

RSCG – mocklis

RSCG – mocklis
 
 

name mocklis
nuget https://www.nuget.org/packages/mocklis/
link https://mocklis.readthedocs.io/en/latest/getting-started/index.html
author Esbjörn Redmo

Generating mocks from classes for unit tests

 

This is how you can use mocklis .

The code that you start with is


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

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

    <IsPackable>false</IsPackable>
    <IsTestProject>true</IsTestProject>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0-preview-23577-04" />
    <PackageReference Include="Mocklis" Version="1.4.0-alpha.2" />
    <PackageReference Include="MSTest.TestAdapter" Version="3.2.0-preview.23623.1" />
    <PackageReference Include="MSTest.TestFramework" Version="3.2.0-preview.23623.1" />
    <PackageReference Include="coverlet.collector" Version="6.0.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\MockLisClock\MockLisClock.csproj" />
  </ItemGroup>
	<PropertyGroup>
		<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
		<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
	</PropertyGroup>

</Project>


The code that you will use is


namespace TestClock;

[MocklisClass]
public partial class TestMock : IMyClock
{

}



using Mocklis;

namespace TestClock;

[TestClass]
public class TestClock
{
    [TestMethod]
    public void TestMyClock()
    {
        var mockSetup = new TestMock();
        mockSetup.GetNow.Return(DateTime.Now.AddYears(-1));

        // When testing the mock like this you need to cast to the interface.
        // This is different from e.g. Moq where the mocked instance and the 'programming interface' are different things.
        // With Mocklis they are the same. The 99% case is where the mock is passed to another constructor as a dependency,
        // in which case there's an implicit cast to the interface.
        var mock = (IMyClock)mockSetup;
        var data = mock.GetNow();
        Assert.AreEqual(DateTime.Now.Year - 1, data.Year);

    }
}


global using Microsoft.VisualStudio.TestTools.UnitTesting;
global using MockTest;
global using Mocklis.Core;

 

The code that is generated is

// <auto-generated />

#nullable enable

namespace TestClock
{
    partial class TestMock
    {
        public global::Mocklis.Core.FuncMethodMock<global::System.DateTime> GetNow { get; }

        global::System.DateTime global::MockTest.IMyClock.GetNow() => GetNow.Call();

        public global::Mocklis.Core.FuncMethodMock<global::System.DateTime> GetUtcNow { get; }

        global::System.DateTime global::MockTest.IMyClock.GetUtcNow() => GetUtcNow.Call();

        public TestMock() : base()
        {
            this.GetNow = new global::Mocklis.Core.FuncMethodMock<global::System.DateTime>(this, "TestMock", "IMyClock", "GetNow", "GetNow", global::Mocklis.Core.Strictness.Lenient);
            this.GetUtcNow = new global::Mocklis.Core.FuncMethodMock<global::System.DateTime>(this, "TestMock", "IMyClock", "GetUtcNow", "GetUtcNow", global::Mocklis.Core.Strictness.Lenient);
        }
    }
}

Code and pdf at

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

RSCG – RSCG_UtilityTypes

RSCG – RSCG_UtilityTypes
 
 

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

Add omit and pick to selectively generate types from existing types

 

This is how you can use RSCG_UtilityTypes .

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_UtilityTypes" Version="2023.1223.1230" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
	  <PackageReference Include="RSCG_UtilityTypesCommon" Version="2023.1223.1230" />
  </ItemGroup>
	<PropertyGroup>
		<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
		<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
	</PropertyGroup>
</Project>


The code that you will use is


using UtilDemo;

var p=new PersonFull();
p.FirstName="Andrei";
p.LastName="Ignat";
Person1 p1=(Person1)p ;
Person2 p2=(Person2)p ;
Console.WriteLine(p1.FirstName);
Console.WriteLine(p2.LastName);


using RSCG_UtilityTypesCommon;

namespace UtilDemo;
[Pick("Person1",nameof(FirstName),nameof(LastName))]
[Omit("Person2", nameof(Salary))]
public class PersonFull
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Salary { get; set; }

}


 

The code that is generated is

namespace UtilDemo
{
partial class Person1
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }

public static explicit operator Person1(PersonFull data )
    {
        var ret= new Person1 ();
        ret.FirstName = data.FirstName;
ret.LastName = data.LastName;
        return ret;
    }



public static explicit operator PersonFull(Person1 data )
    {
        var ret= new PersonFull ();
        ret.FirstName = data.FirstName;
ret.LastName = data.LastName;
        return ret;
    }


}
}

namespace UtilDemo
{
partial class Person2
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }

public static explicit operator Person2(PersonFull data )
    {
        var ret= new Person2 ();
        ret.FirstName = data.FirstName;
ret.LastName = data.LastName;
        return ret;
    }



public static explicit operator PersonFull(Person2 data )
    {
        var ret= new PersonFull ();
        ret.FirstName = data.FirstName;
ret.LastName = data.LastName;
        return ret;
    }


}
}

Code and pdf at

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

RSCG – Ling.Audit

RSCG – Ling.Audit
 
 

name Ling.Audit
nuget https://www.nuget.org/packages/Ling.Audit/
link https://github.com/ling921/dotnet-lib/
author Jing Ling

Generating audit data from class implementation of interfaces

 

This is how you can use Ling.Audit .

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="Ling.Audit" Version="1.1.0" />
  </ItemGroup>
	<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 LingDemo;

Console.WriteLine("Hello, World!");
var p = new Person();
await Task.Delay(2000);
p.FirstName = "Andrei";
p.LastName = "Ignat";
Console.WriteLine(p.CreationTime);
Console.WriteLine(p.LastModificationTime);


using Ling.Audit;

namespace LingDemo;
partial class Person :IFullAudited<Guid>
{
    public int ID { get; set; }
    public string FirstName { get; set; }= string.Empty;
    public string LastName { get; set; } = string.Empty;
}


 

The code that is generated is

// <auto-generated/>

#nullable enable annotations
#nullable disable warnings

namespace LingDemo
{
    partial class Person
    {
        /// <summary>
        /// Gets or sets the creation time of this entity.
        /// </summary>
        public virtual global::System.DateTimeOffset CreationTime { get; set; }
    
        /// <summary>
        /// Gets or sets the creator Id of this entity.
        /// </summary>
        public virtual global::System.Nullable<global::System.Guid> CreatorId { get; set; }
    
        /// <summary>
        /// Gets or sets the last modification time of this entity.
        /// </summary>
        public virtual global::System.Nullable<global::System.DateTimeOffset> LastModificationTime { get; set; }
    
        /// <summary>
        /// Gets or sets the last modifier Id of this entity.
        /// </summary>
        public virtual global::System.Nullable<global::System.Guid> LastModifierId { get; set; }
    
        /// <summary>
        /// Gets or sets whether this entity is soft deleted.
        /// </summary>
        public virtual global::System.Boolean IsDeleted { get; set; }
    
        /// <summary>
        /// Gets or sets the deletion time of this entity.
        /// </summary>
        public virtual global::System.Nullable<global::System.DateTimeOffset> DeletionTime { get; set; }
    
        /// <summary>
        /// Get or set the deleter Id of this entity.
        /// </summary>
        public virtual global::System.Nullable<global::System.Guid> DeleterId { get; set; }
    }
}

Code and pdf at

https://ignatandrei.github.io/RSCG_Examples/v2/docs/Ling.Audit

Aspire Blazor WebAssembly and WebAPI

 

Aspire is the new visualizer – see https://github.com/dotnet/aspire

I am very fond of WebAPI  –  it allows for all people to see the functionality of a site , in a programmatic way ( side note: , my nuget package, https://www.nuget.org/packages/NetCore2Blockly , allows to make workflows from your WebAPI)

And Blazor WebAssembly is a nice addition that the WebAPI . I am talking about Interactive WebAssembly (https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?preserve-view=true&view=aspnetcore-8.0  )  . I do want ( for the moment ) to use Interactive Server because

  1. it is easy to forget to add functionality to the WebAPI
  2. it is not separating UI from BL

So I decided to add an Blazor WebAssembly and WebAPI into Aspire to see how they work together.

The first problem that  I have is how to transmit the WebAPI URL to the Blazor WebAssembly . Think that is not Interactive Server or Auto – in order to have the environment or configuration . Blazor Interactive WebAssembly  are just static files that are downloaded to the client. And they are executed in the browser.

But I have tried with adding to the Environment in usual way

builder.AddProject<projects.exampleblazorapp>(nameof(Projects.ExampleBlazorApp))
.WithEnvironment(ctx =&gt;
{
if (api.Resource.TryGetAllocatedEndPoints(out var end))
{
if (end.Any())
	ctx.EnvironmentVariables["HOSTAPI"] = end.First().UriString;
}

 

And no use!

After reading ASP.NET Core Blazor configuration | Microsoft Learn  and aspire/src/Microsoft.Extensions.ServiceDiscovery at main · dotnet/aspire (github.com) and API review for Service Discovery · Issue #789 · dotnet/aspire (github.com) I realized that the ONLY way is to put in wwwroot/appsettings.json

So I came with the following code that tries to write DIRECTLY to wwwroot/appsettings.json file


namespace Aspire.Hosting;
public static class BlazorWebAssemblyProjectExtensions
{
    public static IResourceBuilder<ProjectResource> AddWebAssemblyProject<TProject>(
        this IDistributedApplicationBuilder builder, string name,
        IResourceBuilder<ProjectResource> api) 
        where TProject : IServiceMetadata, new()
    {
        var projectbuilder = builder.AddProject<TProject>(name);
        var p=new TProject();
        string hostApi= p.ProjectPath;
        var dir = Path.GetDirectoryName(hostApi);
        ArgumentNullException.ThrowIfNull(dir);
        var wwwroot = Path.Combine(dir, "wwwroot");
        if (!Directory.Exists(wwwroot)) {
            Directory.CreateDirectory(wwwroot);
        }
        var file = Path.Combine(wwwroot, "appsettings.json");
        if (!File.Exists(file))
            File.WriteAllText(file, "{}");
        projectbuilder =projectbuilder.WithEnvironment(ctx =>
        {
            if (api.Resource.TryGetAllocatedEndPoints(out var end))
            {
                if (end.Any())
                {
                    
                    var fileContent = File.ReadAllText(file);

                    Dictionary<string, object>? dict;
                    if (!string.IsNullOrWhiteSpace(fileContent))
                        dict = new Dictionary<string, object>();
                    else
                        dict = JsonSerializer.Deserialize<Dictionary<string,object>>(fileContent!);

                    ArgumentNullException.ThrowIfNull(dict);
                    dict["HOSTAPI"] = end.First().UriString;                    
                    JsonSerializerOptions opt = new JsonSerializerOptions(JsonSerializerOptions.Default)
                            { WriteIndented=true};
                    File.WriteAllText(file,JsonSerializer.Serialize(dict,opt));
                    ctx.EnvironmentVariables["HOSTAPI"]=end.First().UriString;
                    
                }
                    
            }

        });
        return projectbuilder;

    }
}

And in Aspire

var api = builder.AddProject<Projects.ExampleWebAPI>(nameof(Projects.ExampleWebAPI));
builder.AddWebAssemblyProject<Projects.ExampleBlazorApp>(nameof(Projects.ExampleBlazorApp), api);

And in Blazor Interactive WebAssembly


var hostApi = builder.Configuration["HOSTAPI"];
if (string.IsNullOrEmpty(hostApi))
{
    hostApi = builder.HostEnvironment.BaseAddress;
    var dict = new Dictionary<string, string?> { { "HOSTAPI", hostApi } };
    builder.Configuration.AddInMemoryCollection(dict.ToArray());
}

builder.Services.AddKeyedScoped("db",(sp,_) => new HttpClient { BaseAddress = new Uri(hostApi) });

What about deploying the code to production ? Well, I think that is better to wrote yourself to wwwroot/appsettings.json and remove the data . But I will try to deploy and let you know….

Aspire , containers and dotnet watch

Aspire is the new visualizer – see https://github.com/dotnet/aspire

If you use dotnet run ( or Visual Studio)  with an Aspire host that instantiate some containers  , then , when you stop the project, the container is released.

But, if you use

dotnet watch run –nohotreload

then the containers are not  deleted. The nice solution is to investigate dotnet watch and Aspire . And maybe fill a bug.

The easy ? Delete the containers first !


void DeleteDockerContainers()
{
    var process = new Process
    {
        StartInfo = new ProcessStartInfo
        {
            FileName = "powershell.exe",
            Arguments = $$"""
-Command "docker rm -f $(docker ps -a -q)"
""",
            RedirectStandardOutput = true,
            UseShellExecute = false,
            CreateNoWindow = true,
        }
    };
    process.Start();
    while (!process.StandardOutput.EndOfStream)
    {
        var line = process.StandardOutput.ReadLine();
        Console.WriteLine(line);
    }
}

Aspire, Sql Server Docker Container and multiple Connections strings

Aspire is the new visualizer – see https://github.com/dotnet/aspire

When creating a Docker container with Sql Server , the connection that is returned is without database – means that , usually, is connecting to the master.

That’s not that we usually want, so the following code is means that WebAPI will be connected to master

var builder = DistributedApplication.CreateBuilder(args);
var rb= builder.AddSqlServerContainer("Db2Gui", "<YourStrong@Passw0rd>");
builder.AddProject<Projects.ExampleWebAPI>(nameof(Projects.ExampleWebAPI))
   .WithReference(rb)
    ;

Instead , do what they do – but add the database

var builder = DistributedApplication.CreateBuilder(args);

var rb= builder.AddSqlServerContainer("Db2Gui", "<YourStrong@Passw0rd>");

builder.AddProject<Projects.ExampleWebAPI>(nameof(Projects.ExampleWebAPI))
    .WithEnvironment(ctx=>
    {
        var connectionStringName = $"ConnectionStrings__";
        var res=rb.Resource;
        var cn = res.GetConnectionString();
        ctx.EnvironmentVariables[connectionStringName+ "ApplicationDBContext"] = cn+ $";database=tests;";
        ctx.EnvironmentVariables[connectionStringName+ "NorthwindDBContext"] = cn + $";database=northwind;";
        ctx.EnvironmentVariables[connectionStringName+ "PubsDBContext"] = cn + $";database=pubs;";
    })
    //.WithReference(rb, "")
    ;

It is true that you can make the following :

builder.AddProject<Projects.ExampleWebAPI>(nameof(Projects.ExampleWebAPI))
    .WithReference(rb.AddDatabase("tests"), "ApplicationDBContext")
    .WithReference(rb.AddDatabase("northwind"), "NorthwindDBContext")
    .WithReference(rb.AddDatabase("pubs"), "PubsDBContext")
    .WithReference(rb.AddDatabase("NotCreated"), "NotCreated")

    ;

But it does not CREATE the database ( and it is a good thing …. EF EnsureCreated verifies JUST the existence of the database not of the tables within)
So thats why I prefer WithEnvironment rather than .WithReference(rb.AddDatabase

Deploy Blazor WASM to Github Pages in 7 steps

Assumptions:

You have an Blazor Interactive WebAssembly ( CSR ) , not a Server ( static or interactive)

I will make as the repo is  https://github.com/ignatandrei/tilt  . Change my name with yours and TILT  with your repo

So let’s start

Step 1   You must configure GitHub Pages – create a docs folder and put an index.html  . Then  goto github Settings => Pages (https://github.com/ignatandrei/tilt/settings/pages )  and put there main / docs  . 

Step 2  Verify it is working. If your repo is https://github.com/ignatandrei/tilt , then browse to https://ignatandrei.github.io/TILT/ and ensure that you can see the index. If not, goto Step 1

Step 3  Add 2 files .nojekyll (content : null , empty …. just create it ) and .gitattributes ( content : *.js binary )

Step 4 dotnet publish your Blazor WASM csproj . Find the folder wwwroot where was published.

Step  5  Find index.html  in the folder . Edit base href , put the repo name  (<base href=”/TILT/” />  ). Also you can modify the css/js  by adding the date ( e.g.  <link rel=”stylesheet” href=”css/app.css?202312162300″ /> ) .

Step 6  Copy the index.html and the other files inside the docs folder . Also, copy the index.html as 404.html file

Step 7 Commit and push. Now you can enjoy your Blazor site hosted for free in github  : https://github.com/ignatandrei/tilt 

Note 1 : If some url’s do not work , then try to add the following

@inject IWebAssemblyHostEnvironment HostEnvironment
@{
     var baseAddress = HostEnvironment.BaseAddress;
     if (!baseAddress.EndsWith(“/”)) baseAddress += “/”;

}

to the url

Note 2:  For more deployments please read https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/webassembly?view=aspnetcore-8.0

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.