MortenRoemer.ThreadSafetyTagging.Analyzer 1.1.1

dotnet add package MortenRoemer.ThreadSafetyTagging.Analyzer --version 1.1.1
                    
NuGet\Install-Package MortenRoemer.ThreadSafetyTagging.Analyzer -Version 1.1.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="MortenRoemer.ThreadSafetyTagging.Analyzer" Version="1.1.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MortenRoemer.ThreadSafetyTagging.Analyzer" Version="1.1.1" />
                    
Directory.Packages.props
<PackageReference Include="MortenRoemer.ThreadSafetyTagging.Analyzer" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add MortenRoemer.ThreadSafetyTagging.Analyzer --version 1.1.1
                    
#r "nuget: MortenRoemer.ThreadSafetyTagging.Analyzer, 1.1.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package MortenRoemer.ThreadSafetyTagging.Analyzer@1.1.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=MortenRoemer.ThreadSafetyTagging.Analyzer&version=1.1.1
                    
Install as a Cake Addin
#tool nuget:?package=MortenRoemer.ThreadSafetyTagging.Analyzer&version=1.1.1
                    
Install as a Cake Tool

C# Thread Safety Analyzer

Setup

To use the thread safety tags and its analyzer install these Nuget packages:

dotnet add package MortenRoemer.ThreadSafetyTagging 
dotnet add package MortenRoemer.ThreadSafetyTagging.Analyzer

Note: In PowerPlatform plugin development only the Analyzer package is necessary

Abstract

Note: Using this library effectively requires some basic familiarity with multi-threading concepts and issues. You can get a general overview of the topic here: Thread-Safety (Wikipedia)

Detecting multi-threading-issues for any given code is notoriously hard and this library does not try to achieve this. In C# it is and always will be easy to write some lambda-magic and cobble some tasks and threads together, and you will likely introduce multi-threading issues if you are not careful.

A well-adjusted person is one who makes the same mistake twice without getting nervous - Alexander Hamilton

This analyzer will not change that fact, but in my personal experience multi-threading issues usually are not introduced by reckless multi-threading approaches or unlucky delegate variable captures.

Instead, most of multi-threading issues I had to deal with creep up later in the development cycle. Typically, in projects where multiple developers are involved and iterate upon earlier designs. This makes intuitive sense: Any experienced developer that builds a system from scratch should be able to build without any multi-threading issues. This developer will have "perfect" knowledge about how threads will interactive with any piece of code in this project during this initial phase. This initial developer will make assumptions on how all the types should be used so that the multi-threading-safety guarantee is still uphold. As the project gets more and more complex, more developers get onboarded and memories fade these assumptions are lost.

This could be fixed by having comments like these in the source code:

// This class is intended to be used by one thread exclusively
// PLEASE DO NOT USE CONCURRENTLY
public class SomeService
{
    // ...
}

But this generally does not happen because:

  • The information in the comment feels like common knowledge to the author
  • The comment is only effective if the next developer reads all the comments of all the classes they use
  • These comments do not guarantee any consistency over type boundaries, so can not be trusted

This gets even harder if we acknowledge that C# is an object-oriented language, so we tend to cluster smaller objects into bigger and bigger structures.

Other modern languages with focus on memory and threading safety are aware of this and generally do not allow mixing different structures with weaker safety guarantees. Examples are Rust (Official Website) and Go (Official Website).

The goal of this project therefore is to enable the developer to tag types with the intended safety guarantees and the analyzer can then make sure that any future alteration will not break those guarantees.

The different thread safety modes

Immutable Types

The safest types to use in any threading context are immutable types. That means types that have only read-only properties and fields and none of those have interior mutability.

You can annotate those with the ImmutableMemoryAccess attribute

Here is an example for an immutable type declaration:

[ImmutableMemoryAccess]
public sealed class SomeClass
{
    public SomeClass(Guid id, IEnumerable<string> labels)
    {
        Id = id;
        Labels = labels.ToImmutableArray();
    }

    public Guid Id { get; }
    
    public readonly ImmutableArray<string> Labels;
}

By definition immutable classes can only contain or inherit from other immutable classes

Synchronized Types

Chances are that your system is build out of subsystems. Those are typically injected as dependencies into bigger systems to handle tasks based on business logic.

As those are typically shared between different threads it is important to make sure that these handle concurrent access. These are called synchronized types.

Note: It makes sense to mark interfaces and classes with the SynchronizedMemoryAccess attribute. Then this analyzer makes sure that all interfaces do not break this thread-safety guarantee.

Here is an example of a synchronized interface and class definition:

[SynchronizedMemoryAccess]
public interface ISomeService
{
    string AddStar();
}

[SynchronizedMemoryAccess]
public sealed class SomeService : ISomeService
{
    public SomeClass()
    {
        // You can give an initializer delegate that is executed only once
        Value = new Mutex(() => new StringBuilder());
    }

    // The Mutex helper class encapsulates other classes and prevents
    // concurrent access
    private readonly Mutex<StringBuilder> Value;
    
    public string AddStar()
    {
        return Value.Do(builder => {
            builder.Append('*');
            return builder.ToString();
        });
    }
}

Exclusive Types

All other classes are only safe to use by only one thread at a time. This is not a bad thing as excessive synchronization brings its own problems like deadlocks and slow performance.

So most of your types will still be exclusive types, but you need to make sure that they never leave their scope without proper synchronization.

Here is an example of an exclusive class definition:

[ExclusiveMemoryAccess]
public sealed class SomeRecord
{
   public Guid Id { get; set; }
   
   public string? Name { get; set; }
   
   public SomeRecord[]? Children { get; set; }
}

Skipping Memory Checks

There are some cases where this analyzer can not determine the safety guarantees.

Consider this field in an immutable context:

private readonly Func<T> Action;

This delegate can be either Immutable, Synchronized or Exclusive depending on the variables that were captured during the delegate construction.

In this case you are responsible to check the safety of the type yourself. You can then deactivate the analyzer warning for this field with the SkipMemorySafetyCheck attribute:

[SkipMemorySafetyCheck(Because = "containing action is immutable")]
private readonly Func<T> Action;
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.1 393 4/21/2025
1.1.0 215 4/21/2025
1.0.7 1,876 7/22/2024
1.0.6 171 7/22/2024
1.0.5 180 7/21/2024
1.0.4 175 7/15/2024
1.0.3 174 7/14/2024
1.0.2 182 7/9/2024
1.0.1 172 7/8/2024
1.0.0 172 7/7/2024