Thursday, January 25, 2024

Never, ever make your Random static

Years ago it was popular to advocate for sharing the instance of the Random class. There are numerous sources, including blogs, the stack, etc. Ocasionally, someone dared to write that it could not be the best idea. But still, looking for information, people stumle across this decade-old recommendation.
That was the case of one of our apps. In one critical place, a shared instance of random was used

public static Random _rnd = new Random();

...
..._rnd.Next()...
This worked. For years. Until one day, it failed miserably.
You see, the random has two internal variables it overwrites each time Next is called so that it can advance to the next random value. If your app is just winning a lottery ticket in its concurrent execution, the two values can end up being overwritten with the same value.
And guess what, random starts to return 0 each time Next is called! For us, it was the case of an internal stress test, where a heavy traffic was directed onto the application but it can happen just anytime.
There are numerous solutions. First, starting from .NET6 there's the Random.Shared static property, marked as thread-safe. In older frameworks, one of alternative approaches should be used, e.g. this

Wednesday, January 24, 2024

FireFox slowly becomes a new Internet Explorer

For years in the past, developing web apps was a constant case of if (IE) do_something_else_than_for_other_browsers(). Sadly, we lately have some bad cases where things seem similar, but instead of IE we now have FF.
One of earlier cases concerned the location.reload API. In a specific case of an iframe, the app calls this to reload the content when users change their color theme. Worked everywhere instead of FF. As seen in the docs, FF has its own forceGet parameter, not supported in the spec but mentioned in the docs. Seems that location.reload works for us in FF only when this extra argument is provided.
Another case appeared lately, unfortunately. Take the WS-Federation protocol and consider a scenario where the identity provider and the service provider are on different domains.
Signing in works correctly. The service provider redirects with wa=wsignin1.0 and the identity provider responds with the SAML token POSTed to the service provider.
Signing out is implemented using nested iframes where the identity provider points to the service provider and adds wa=wsignoutcleanup1.0 to terminate the session (drop the service provider session cookie). As you know, there's been a change lately in the way cross domain cookies are handled. To prevent the CSRF, SameSite flag was added and in the default case, the cookie falls back to SameSite=lax which prevents it from being accessed cross different domains.
There's however still a way to make cookies available in cross domain requests, you are supposed to just mark them with SameSite=none;Secure. And guess what, this works in all other browsers, except FF. It turns out, the default security settings for FF prevent all crossdomain cookies, no matter if they are marked with SameSite=none or not.
Sure, users can opt out by lowering the security level or configure exceptions for your app but this doesn't change the fact that the specific scenario mentioned above just doesn't work in the default FF setup. Other browsers have their own security settings and, at least at the very moment, you can opt in for more strict settings (and cross domain cookies don't work anymore) but this requires a change in default settings. In case of FF, it's the default setting that it's against the samesite spec.

Monday, January 15, 2024

An interesting edge case in C# yield state machines

Among many useful LINQ methods, there's no functional ForEach. People often implement it on their own, an extension method is just a few lines of code.
Someone did that in one of our projects, too:
public static void ForEach1<T>( this IEnumerable<T> enumeration, Action<T> action )
{
	foreach ( T item in enumeration )
	{
		action( item );
	}
}
This works great. Someone else, however, noticed that while this works great, it doesn't allow further chaining, since the method returns void
This someone else modified the code slightly:
public static IEnumerable<T> ForEach2<T>( this IEnumerable<T> enumeration, Action<T> action )
{
	foreach ( T item in enumeration )
	{
		action( item );
		yield return item;
	}
}
This doesn't work great. Frankly, it doesn't work at all. Surprising but true. Take this:
internal class Program
{
	static void Main( string[] args )
	{
		Test1();
		Test2();

		Console.ReadLine();
	}

	static void Test1()
	{
		var array = new int[] { 1,2,3 };

		var list = new List<int>();

		array.ForEach1( e => list.Add( e ) );

		Console.WriteLine( list.Count() );
	}
	static void Test2()
	{
		var array = new int[] { 1,2,3 };

		var list = new List<int>();

		array.ForEach2( e => list.Add( e ) );

		Console.WriteLine( list.Count() );
	}
}

public static class EnumerableExtensions
{
	public static void ForEach1<T>( this IEnumerable<T> enumeration, Action<T> action )
	{
		foreach ( T item in enumeration )
		{
			action( item );
		}
	}

	public static IEnumerable<T> ForEach2<T>( this IEnumerable<T> enumeration, Action<T> action )
	{
		foreach ( T item in enumeration )
		{
			action( item );
			yield return item;
		}
	}
}
While this is expected to produce 3 for both tests, it produces 3 and 0. The upgraded version doesn't work as expected.
The reason for this is a kind-of-a-limitation of the state machine generated by the yield sugar. The machine doesn't execute any code until the very result is enumerated. This means that changing
array.ForEach2( e => list.Add( e ) );
to
array.ForEach2( e => list.Add( e ) ).ToList();
would "fix it". What a poor foreach, though, that requires an extra terminator (the ToList) and doesn't work otherwise.
Luckily, a simpler fix exists, just forget the state machine at all for this specific case:
public static IEnumerable<T> ForEach2<T>( this IEnumerable<T> enumeration, Action<T> action )
{
	foreach ( T item in enumeration )
	{
		action( item );
	}

	return enumeration;
}

Thursday, November 9, 2023

AmbiguousMatchException on a simple NET.Core Minimal API

One of my students pointed out to an unexpected issue in a simple .NET.Core app using the Minimal API
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapGet("/{param}", (string param) =>
{
    return $"Parametr: {param}";
});

app.MapGet("/{param:int}", (int param) =>
{
    return $"Parametr typu int: {param}";
});

app.MapGet("/{param:length(1,10)}", (string param) =>
{
    return $"Parametr z ograniczeniem długości: {param}";
});

app.MapGet("/{param:required}", (string param) =>
{
    return $"Parametr wymagany: {param}";
});

app.MapGet("/{param:regex(^\\d{{4}}$)}", (string param) =>
{
    return $"Parametr spełniający wyrażenie regularne: {param}";
});

app.Run();
The problem here is that routes overlap and it's not easily discoverable until a runtime exception is thrown when you try to navigate to a path that is matched by multiple endpoints. The AmbiguousMatchException is thrown.
My first attempt to workaround this was to wrap routes with UseRouting / UseEndpoints, hoping that this makes routing explicit. However, a closer look at the documentation reveals that
URL matching operates in a configurable set of phases. In each phase, the output is a set of matches. The set of matches can be narrowed down further by the next phase. The routing implementation does not guarantee a processing order for matching endpoints. All possible matches are processed at once. The URL matching phases occur in the following order.
and also
there are other matches with the same priority as the best match, an ambiguous match exception is thrown.
This is kind of complicated to people coming with .NET.Framework background where even if routes overlap, a first matching route is picked up and its handler is executed. The .NET.Core introduces the idea of priorities in routes, clearly covered by the documentation. In case there are multiple routes with the same priority, we have an exception.
Fortunately enough, the docs also give at least one hint on what to do - a custom EndpointSelector can be used. A custom selector could just replace the DefaultEndpointSelector that is normally registered in the service container, the default selector is useless as it's internal and its method is not virtual. But the custom selector is just a few lines of code.
using Microsoft.AspNetCore.Routing.Matching;
using System.Text.Json;
 
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<EndpointSelector, CustomEndpointSelector>();
var app = builder.Build();
 
app.UseRouting();
 
app.MapGet( "/", () => "Hello World!" );
 
app.MapGet( "/{param}", ( string param ) =>
{
    return $"Parametr: {param}";
} );
 
app.MapGet( "/{param:int}", ( int param ) =>
{
    return $"Parametr typu int: {param}";
} );
 
app.MapGet( "/{param:length(1,10)}", ( string param ) =>
{
    return $"Parametr z ograniczeniem długości: {param}";
} );
 
app.MapGet( "/{param:required}", ( string param ) =>
{
    return $"Parametr wymagany: {param}";
} );
 
app.MapGet( "/{param:regex(^\\d{{4}}$)}", ( string param ) =>
{
    return $"Parametr spełniający wyrażenie regularne: {param}";
} );
 
 
app.UseEndpoints( (endpoints) => { } );
 
app.Run();
 
class CustomEndpointSelector : EndpointSelector
{
    public override async Task SelectAsync( HttpContext httpContext, CandidateSet candidates )
    {
        CandidateState selectedCandidate = new CandidateState();
 
        for ( var i=0; i < candidates.Count; i++ )
        {
            if ( candidates.IsValidCandidate( i ) )
            {
                selectedCandidate = candidates[i];
                break;
            }
        }
 
        httpContext.SetEndpoint( selectedCandidate.Endpoint );
        httpContext.Request.RouteValues = selectedCandidate.Values;
    }
}
Please use this as a starting point to build a more sophisticated approach, fulfilling any specific requirements.

Thursday, October 19, 2023

Fido2.NetFramework

Just published a working code of a port of the fido2-net-lib library. The library is great, it works but there's a drawback - it targets .NET 6 (or higher) which means that creating Fido2/Webauthn compatible website in .NET 4.X using the library is impossible.
My ported library, called the Fido2.NetFramework is supposed to fill the gap. I downgraded the code so that it compiles using the old C# 4.8 compiler, I replaced all external references that targets APIs/libraries/frameworks that are not available in .NET 4.8. For example, the Nsec.Cryptography used in the source library, was replaced with BouncyCastle.
The code contains a working demo.
There's still some work to do, in particular, validate the port by porting unit tests and creating extra compatibility tests.

Monday, August 28, 2023

Rust/WebAssembly vs Javascript performance on Mandelbrot animation rendering

The pending question Is Rust/Wasm fast enough to even bother when comparing to Javascript? has now yet another biased benchmark. I've decided to take one of my Julia/Mandelbrot demos, benchmark it "as-is" and then, port to Rust and compare.
The HTML/Javascript source is short enough to post it here, the original demo code has some additional features (zooming) which are not present here
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Mandelbrot</title>
	<script type="text/javascript">
	        
        var cx = 0, cy = 0, kat1 = 0, kat2 = 1;           
        
        var WJulia = 1024;
        var HJulia = 1024;
        var frame  = 0;
        var startDate;
    
        var contextJulia;
        var contextSmall;
        var pixJulia;
        var imgdJulia;

        var iStart;
        var iEnd;
        var jStart;
        var jEnd;

        var iDelta;
        var jDelta;
        
        function setInitialScale()
        {
            iStart = -1.92;
            iEnd   = 1.92;
            jStart = -1.92;
            jEnd   = 1.92;

            iDelta = (iEnd-iStart)/1024;
            jDelta = (jEnd-jStart)/1024;

            startDate = new Date();
        }

        function SetupMandel()
        {	 
            var elemJulia = document.getElementById('JuliaCanvas');
            if (elemJulia && elemJulia.getContext)
            {
                contextJulia = elemJulia.getContext('2d');
                if (contextJulia)
                {
                    imgdJulia = contextJulia.createImageData(WJulia, HJulia);         
                    pixJulia = imgdJulia.data;
                }
            }

            setInitialScale();
          
            /* start */
            requestAnimationFrame( LoopMandel );
        }
                         
        function LoopMandel()
        {	 
            kat1 += 0.0021;
            kat2 += 0.0039;
            cx = .681 * Math.sin(kat1);
            cy = .626 * Math.cos(kat2);

            frame++;
            document.getElementById('fps').innerHTML = `FPS: ${1000*frame/(new Date()-startDate)}`;
			
            RysujMandel();	 
            
            requestAnimationFrame( LoopMandel );
        }
         
         
        /* tworzenie bitowego obrazu */
        function RysujMandel()
        {
            /* obliczenia */
            var wi, wj;
            var i, j;

            var iterations = 255;

            var px = 0;
            for (i = iStart, wi = 0; wi < 1024; i += iDelta, wi++)
            {
                var py = 0;
                for (j = jStart, wj = 0; wj < 1024; j += jDelta, wj++)
                {
                    var c = 0;

                    var x = cx;
                    var y = cy;
         
                    while (((x*x + y*y) < 4) && (c < iterations))
                    {
                        [x, y] = [x * x - y * y + i, 2 * x * y + j];
                        c++;
                    }

                    SetPixelColor( pixJulia, (py * WJulia + px) << 2, 255, 255-c, 255-c, 255 - (c/2) );
                                        
                    py++;
                }
         
                px++;
            }
            
            /* kopiowanie bitowego obrazu do context/canvas */
            contextJulia.putImageData(imgdJulia, 0, 0);		 
        }
         
        function SetPixelColor(pix,offs, a, r, g, b)
        {            
            pix[offs++] = r;
            pix[offs++] = g;
            pix[offs++] = b;
            pix[offs] = a;
        }
         
        window.addEventListener( 'load', SetupMandel );
        
        </script>    
</head>
<body>
    <span id="fps" style='display:block'></span>
    <canvas id="JuliaCanvas" width="1024" height="1024">
	</canvas>
</body>
</html>
When run on my machine and the Edge Browser, it makes ~10-11 frames per second. Not bad assuming 1024x1024 image and 255 iterations.
Same code ported to Rust and compiled with wasm-pack
// https://rustwasm.github.io/wasm-bindgen/examples/dom.html
use wasm_bindgen::prelude::*;
use wasm_bindgen::Clamped;
use web_sys::{CanvasRenderingContext2d, ImageData};

/* tworzenie bitowego obrazu */
#[wasm_bindgen]
pub fn mandel(
    ctx: &CanvasRenderingContext2d, 
    width: u32, height: u32,
    cx: f64, cy: f64) -> Result<(), JsValue> {

    let i_start = -1.92;
    let i_end   = 1.92;
    let j_start = -1.92;
    let j_end   = 1.92;

    let i_delta = (i_end-i_start)/width as f64;
    let j_delta = (j_end-j_start)/height as f64;

    /* obliczenia */    
    let iterations = 255;

    let mut pix_julia = Vec::with_capacity( usize::try_from(width * height).unwrap() );

    let mut j = j_start;
    for _wj in 0..height {

        let mut i = i_start;   
        for _wi in 0..width {
        
            let mut c: u8 = 0;

            let mut x = cx;
            let mut y = cy;
    
            while ( (x*x + y*y) < 4.0) && (c < iterations)
            {
                let _tx = x;
                x = x * x - y * y + i;
                y = 2.0 * _tx * y + j;
                c += 1;
            }

            pix_julia.push( 255-c );
            pix_julia.push( 255-c );
            pix_julia.push( 255-(c/2) );
            pix_julia.push( 255 );
            
            i  += i_delta;
        }
    
        j  += j_delta;
    }    

    let data = ImageData::new_with_u8_clamped_array_and_sh(Clamped(&pix_julia), width, height)?;
    ctx.put_image_data(&data, 0.0, 0.0)
}
The cargo.tomlwas
[package]
name = "hello-wasm"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2.87"

[dependencies.web-sys]
version = "0.3.4"
features = [
  'Document',
  'Element',
  'HtmlElement',
  'Node',
  'Window',
  'ImageData',
  'CanvasRenderingContext2d'  
]

[profile.release]
opt-level = 3
lto = true

[package.metadata.wasm-pack.profile.release]
wasm-opt = ["-O4", "--enable-mutable-globals"]
and the HTML
<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <title>Mandelbrot</title>
  </head>
  <body>
    <script type="module">
        import init, { mandel } from "./pkg/hello_wasm.js";
        
        (async function() {
		   await init();
           SetupMandel();    
        })();
	  
        var cx = 0, cy = 0, kat1 = 0, kat2 = 1;      
        var WJulia = 1024;
        var HJulia = 1024;        
        var frame  = 0;
		var startDate;
        
        var contextJulia;
        var contextSmall;
        var pixJulia;
        var imgdJulia;

        function SetupMandel()
        {	 
            var elemJulia = document.getElementById('JuliaCanvas');
            if (elemJulia && elemJulia.getContext)
            {
                contextJulia = elemJulia.getContext('2d');
                if (contextJulia)
                {
                    imgdJulia = contextJulia.createImageData(WJulia, HJulia);         
                    pixJulia = imgdJulia.data;
                }
            }
          
		    startDate = new Date();
			
            /* start */
            requestAnimationFrame( LoopMandel );
        }
                         
        function LoopMandel()
        {	 
            kat1 += 0.0021;
            kat2 += 0.0039;
            cx = .681 * Math.sin(kat1);
            cy = .626 * Math.cos(kat2);

            frame++;
            document.getElementById('fps').innerHTML = `FPS: ${1000*frame/(new Date()-startDate)}`;
			
            mandel(contextJulia, WJulia, HJulia, cx, cy);	 
            
            requestAnimationFrame( LoopMandel );
        }	  
    </script>
    <span id="fps" style='display: block'></span>	
    <canvas id="JuliaCanvas" width="1024" height="1024">
	</canvas>	
  </body>
</html>
And the result? Similar, it also makes 10-11 frames per second. Additional tests (different resolution and number of iterations) and possible optimizations can be applied here.

Thursday, July 20, 2023

Background music for coding - playlist

It's only an hour or so at the moment but nonetheless - this is a collection of music I listen while coding. I hope to expand the list in future.
Link to the Spotify playlist